Skip to content

Commit bdd24bf

Browse files
Pin DBAPI bind-time DataError boundary (UUID/Path/array; int64 + 16 MiB BLOB caps)
Three boundary classes that were not pinned end-to-end at the dbapi surface: * Unsupported types (UUID, Path, array.array) — common in callers porting from psycopg/asyncpg. Must surface as DataError, not as EncodeError leaking past the dbapi boundary. * IntEnum (an int subclass) — pin observed behavior: round-trips as int, since the wire encoder accepts int subclasses. Future refactors that tightened the type check would be a deliberate decision. * int64 overflow (binding 2**63, etc.) — wire encoder rejects with EncodeError; dbapi must wrap as DataError. * 16 MiB BLOB cap — wire encoder enforces; dbapi must wrap. Plus the at-cap successful boundary: 2**63 - 1 and -(2**63) bind and round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dc783d4 commit bdd24bf

1 file changed

Lines changed: 97 additions & 0 deletions

File tree

tests/integration/test_misc_coverage.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,103 @@ def test_complex_rejected_as_data_error(self, cluster_address: str) -> None:
209209
with pytest.raises(DataError):
210210
c.execute("SELECT ?", [complex(1, 2)])
211211

212+
def test_uuid_rejected_as_data_error(self, cluster_address: str) -> None:
213+
"""UUID is not a wire-recognized type — must surface as DataError.
214+
Common in callers porting from psycopg/asyncpg which both
215+
accept UUID."""
216+
from uuid import UUID
217+
218+
from dqlitedbapi.exceptions import DataError
219+
220+
with connect(cluster_address, database="test_bind_types") as conn:
221+
c = conn.cursor()
222+
with pytest.raises(DataError):
223+
c.execute(
224+
"SELECT ?",
225+
[UUID("12345678-1234-5678-1234-567812345678")],
226+
)
227+
228+
def test_path_rejected_as_data_error(self, cluster_address: str) -> None:
229+
"""``pathlib.Path`` (sometimes used for filename columns) must
230+
surface as DataError so a caller is steered to ``str(path)``."""
231+
from pathlib import Path
232+
233+
from dqlitedbapi.exceptions import DataError
234+
235+
with connect(cluster_address, database="test_bind_types") as conn:
236+
c = conn.cursor()
237+
with pytest.raises(DataError):
238+
c.execute("SELECT ?", [Path("/tmp/foo")])
239+
240+
def test_array_array_rejected_as_data_error(self, cluster_address: str) -> None:
241+
"""``array.array`` is bytes-like but not in the accepted
242+
BLOB-input set. Must surface as DataError."""
243+
from array import array
244+
245+
from dqlitedbapi.exceptions import DataError
246+
247+
with connect(cluster_address, database="test_bind_types") as conn:
248+
c = conn.cursor()
249+
with pytest.raises(DataError):
250+
c.execute("SELECT ?", [array("b", b"hello")])
251+
252+
def test_intenum_round_trips_as_int(self, cluster_address: str) -> None:
253+
"""``enum.IntEnum`` is an ``int`` subclass; the wire encoder
254+
accepts it as INTEGER. Pin observed behavior so a future
255+
refactor that tightened the type check (rejecting subclasses)
256+
is a deliberate decision, not an accident."""
257+
from enum import IntEnum
258+
259+
class _Color(IntEnum):
260+
RED = 1
261+
262+
with connect(cluster_address, database="test_bind_types") as conn:
263+
c = conn.cursor()
264+
c.execute("SELECT ?", [_Color.RED])
265+
row = c.fetchone()
266+
assert row == (1,) or row == (int(_Color.RED),)
267+
268+
269+
@pytest.mark.integration
270+
class TestBindBoundaryDataErrors:
271+
"""Boundary inputs that must surface at the DBAPI as ``DataError``
272+
(not as raw ValueError / EncodeError leaking from the wire layer).
273+
The wire encoder enforces caps; the dbapi's ``_call_client``
274+
wraps the wire's ValueError into PEP 249 ``DataError``. Pin the
275+
end-to-end contract so a future narrowing of the wrap cannot
276+
silently let a wire exception leak past the dbapi boundary."""
277+
278+
@pytest.mark.parametrize(
279+
"value",
280+
[2**63, -(2**63) - 1, 2**63 + 1, 2**100, -(2**100)],
281+
)
282+
def test_bind_int_over_int64_raises_data_error(self, cluster_address: str, value: int) -> None:
283+
from dqlitedbapi.exceptions import DataError
284+
285+
with connect(cluster_address, database="test_bind_overflow") as conn:
286+
c = conn.cursor()
287+
with pytest.raises(DataError):
288+
c.execute("SELECT ?", [value])
289+
290+
@pytest.mark.parametrize("value", [2**63 - 1, -(2**63), 0, 1, -1])
291+
def test_bind_int_at_int64_boundary_succeeds(self, cluster_address: str, value: int) -> None:
292+
with connect(cluster_address, database="test_bind_overflow") as conn:
293+
c = conn.cursor()
294+
c.execute("SELECT ?", [value])
295+
assert c.fetchone() == (value,)
296+
297+
def test_bind_blob_over_cap_raises_data_error(self, cluster_address: str) -> None:
298+
"""A bind value larger than the wire-layer 16 MiB BLOB cap
299+
must surface as ``DataError`` — never as a raw EncodeError or
300+
a silent truncation."""
301+
from dqlitedbapi.exceptions import DataError
302+
303+
big = b"x" * (16 * 1024 * 1024 + 1)
304+
with connect(cluster_address, database="test_bind_overflow") as conn:
305+
c = conn.cursor()
306+
with pytest.raises(DataError):
307+
c.execute("SELECT ?", [big])
308+
212309

213310
@pytest.mark.integration
214311
class TestCursorDescriptionEdgeCases:

0 commit comments

Comments
 (0)