Skip to content

Commit d2d2f66

Browse files
feat: wrap wire EncodeError as client-level DataError
Out-of-range ints, unsupported types, and other wire-layer parameter encoding failures surfaced as dqlitewire.exceptions.EncodeError — leaking wire-library implementation detail across the abstraction boundary. Application code catching DqliteError to log/alert missed them entirely. Introduce DataError in dqliteclient.exceptions and wrap EncodeError at _run_protocol's entry. Connection stays usable (nothing was sent on the wire). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 97ce996 commit d2d2f66

4 files changed

Lines changed: 30 additions & 0 deletions

File tree

src/dqliteclient/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from dqliteclient.connection import DqliteConnection
1111
from dqliteclient.exceptions import (
1212
ClusterError,
13+
DataError,
1314
DqliteConnectionError,
1415
DqliteError,
1516
InterfaceError,
@@ -34,6 +35,7 @@
3435
"ProtocolError",
3536
"ClusterError",
3637
"OperationalError",
38+
"DataError",
3739
]
3840

3941
__version__ = "0.1.0"

src/dqliteclient/connection.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
from typing import Any
88

99
from dqliteclient.exceptions import (
10+
DataError,
1011
DqliteConnectionError,
1112
InterfaceError,
1213
OperationalError,
1314
ProtocolError,
1415
)
1516
from dqliteclient.protocol import DqliteProtocol
17+
from dqlitewire.exceptions import EncodeError as _WireEncodeError
1618

1719
# dqlite error codes that indicate a leader change (SQLite extended error codes)
1820
# SQLITE_IOERR_NOT_LEADER = SQLITE_IOERR | (40 << 8) = 10250
@@ -259,6 +261,11 @@ async def _run_protocol[T](self, fn: Callable[[DqliteProtocol, int], Awaitable[T
259261
self._in_use = True
260262
try:
261263
return await fn(protocol, db_id)
264+
except _WireEncodeError as e:
265+
# Client-side parameter-encoding error. The wire bytes were
266+
# never written, so the connection is still usable — convert
267+
# into the client-level DataError and let it propagate.
268+
raise DataError(str(e)) from e
262269
except (DqliteConnectionError, ProtocolError) as e:
263270
self._invalidate(e)
264271
raise

src/dqliteclient/exceptions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ class ClusterError(DqliteError):
2121
"""Cluster-related error (leader not found, etc)."""
2222

2323

24+
class DataError(DqliteError):
25+
"""Client-side parameter-encoding error.
26+
27+
Raised when a parameter value cannot be serialized onto the wire —
28+
e.g. an int outside ``[-2^63, 2^63)``, an embedded null byte in a
29+
TEXT string, or an unsupported Python type.
30+
"""
31+
32+
2433
class OperationalError(DqliteError):
2534
"""Database operation error."""
2635

tests/test_connection.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,18 @@ async def test_string_params_rejected_with_clear_error(self, connected_connectio
850850
with pytest.raises(TypeError, match="list or tuple"):
851851
await conn.execute("SELECT ?", "alice") # type: ignore[arg-type]
852852

853+
async def test_int64_overflow_raises_dataerror(self, connected_connection) -> None:
854+
"""Out-of-range int (|v| >= 2^63) must surface as DataError, not the
855+
internal wire-package EncodeError. The connection must remain alive.
856+
"""
857+
from dqliteclient.exceptions import DataError
858+
859+
conn, _, _ = connected_connection
860+
huge = 2**70
861+
with pytest.raises(DataError):
862+
await conn.execute("INSERT INTO t VALUES (?)", [huge])
863+
assert conn.is_connected
864+
853865
async def test_cross_event_loop_raises_interface_error(self) -> None:
854866
"""Using a connection from a different event loop must raise InterfaceError."""
855867
import asyncio

0 commit comments

Comments
 (0)