Skip to content

Commit 901fb11

Browse files
Import _DescriptionTuple from the dbapi layer in the async adapter
The async adapter re-declared ``_DescriptionTuple`` alongside the dbapi layer with a comment explaining the defensive duplication. But dbapi is a hard dependency of sqlalchemy-dqlite, so there is no import boundary to preserve — and the two aliases can only drift by silent modification of one side. Import the tuple shape from the dbapi layer instead and keep the outer ``Sequence[...]`` wrapper so the adapter's permissive passthrough contract (accepting list or tuple outputs) is unchanged. A new pinning test asserts the two aliases are identity-equal so a future refactor reintroducing a local copy would fail loudly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0423e4c commit 901fb11

2 files changed

Lines changed: 22 additions & 10 deletions

File tree

src/sqlalchemydqlite/aio.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
OperationalError,
2020
ProgrammingError,
2121
)
22+
from dqlitedbapi.types import _DescriptionTuple
2223
from sqlalchemydqlite.base import DqliteDialect
2324

2425
logger = logging.getLogger(__name__)
@@ -28,19 +29,14 @@
2829

2930
__all__ = ["AsyncAdaptedConnection", "AsyncAdaptedCursor", "DqliteDialect_aio"]
3031

31-
# Description tuple shape per PEP 249 (name, type_code, display_size,
32-
# internal_size, precision, scale, null_ok). dqlite populates only
33-
# ``name`` and ``type_code`` — the other five are always ``None``.
34-
# Re-declared here (not imported) to keep the sqlalchemy-dqlite
35-
# runtime contract explicit to type-checkers even if the dbapi layer
36-
# later exposes a named alias.
3732
# PEP 249 specifies ``cursor.description`` as a sequence of sequences —
3833
# a ``list[tuple]`` is the canonical shape but a strict type alias of
3934
# ``list`` would reject a dbapi cursor that returns a tuple-of-tuples
40-
# (which sqlalchemy's own aiosqlite adapter accepts). Keep the alias
41-
# permissive so the adapter passes through whatever the underlying
42-
# cursor returns without copying.
43-
_DescriptionTuple = tuple[str, int | None, None, None, None, None, None]
35+
# (which sqlalchemy's own aiosqlite adapter accepts). Widen the outer
36+
# alias to ``Sequence`` so the adapter passes through whatever the
37+
# underlying cursor returns without copying. The inner 7-tuple shape is
38+
# imported from the dbapi layer (single source of truth) so a future
39+
# column (real display_size, etc.) propagates here automatically.
4440
_Description = Sequence[_DescriptionTuple] | None
4541

4642

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Pin the description-tuple alias to the dbapi-layer single source
2+
of truth. If a future refactor re-declares ``_DescriptionTuple``
3+
locally it must not drift from the dbapi shape; importing ensures
4+
drift can only happen by deliberate coordinated change to both
5+
modules.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from dqlitedbapi.types import _DescriptionTuple as _DbapiDescriptionTuple
11+
from sqlalchemydqlite.aio import _DescriptionTuple as _AdapterDescriptionTuple
12+
13+
14+
def test_description_tuple_alias_is_shared() -> None:
15+
"""The adapter imports the dbapi-layer alias — ``is`` must hold."""
16+
assert _AdapterDescriptionTuple is _DbapiDescriptionTuple

0 commit comments

Comments
 (0)