Skip to content

Commit 077f92b

Browse files
Classify wrapped DqliteConnectionError via __cause__ in is_disconnect
The dbapi's _call_client wraps a client-level DqliteConnectionError into a bare OperationalError, so the direct isinstance check in is_disconnect against DqliteConnectionError only fires for connect- path errors, not cursor-path ones. Walk the standard __cause__ chain — set automatically by `raise ... from e` — so the chained form is classified identically without inventing a new attribute on the dbapi exception. The existing substring fallback still catches any wrapper without a __cause__. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 770b778 commit 077f92b

2 files changed

Lines changed: 27 additions & 0 deletions

File tree

src/sqlalchemydqlite/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,15 @@ def is_disconnect(self, e: Any, connection: Any, cursor: Any) -> bool:
350350
# Explicit connection-level error types from the client layer.
351351
if isinstance(e, _client_exc.DqliteConnectionError):
352352
return True
353+
# The dbapi ``_call_client`` handler wraps
354+
# ``_client_exc.DqliteConnectionError`` into a bare
355+
# ``dbapi.OperationalError`` (no code). The wrapped class is
356+
# unreachable by the direct isinstance above, but Python sets
357+
# ``__cause__`` from ``raise ... from e``, so walking the chain
358+
# keeps the disconnect classification working without inventing
359+
# a new attribute.
360+
if isinstance(getattr(e, "__cause__", None), _client_exc.DqliteConnectionError):
361+
return True
353362
# Underlying OS-level transport failures (socket RST, broken pipe,
354363
# DNS, connect refused) surface as these stdlib types.
355364
if isinstance(e, (ConnectionError, BrokenPipeError, TimeoutError, OSError)):

tests/test_dialect.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,24 @@ def test_does_not_flag_other_interface_errors(self) -> None:
381381
e = dqlitedbapi.exceptions.InterfaceError("arraysize must be positive")
382382
assert dialect.is_disconnect(e, None, None) is False
383383

384+
def test_recognizes_wrapped_dqlite_connection_error_via_cause(self) -> None:
385+
"""The dbapi ``_call_client`` handler wraps a client-level
386+
``DqliteConnectionError`` into a bare ``OperationalError`` (no
387+
code). Without walking ``__cause__`` the direct
388+
isinstance branch would miss the wrapped form entirely. Pin
389+
the chain inspection so the dead-code isinstance branch above
390+
remains load-bearing for chained errors too.
391+
"""
392+
import dqliteclient.exceptions as _client_exc
393+
import dqlitedbapi.exceptions
394+
395+
dialect = DqliteDialect()
396+
original = _client_exc.DqliteConnectionError("peer RST")
397+
try:
398+
raise dqlitedbapi.exceptions.OperationalError("wrapped") from original
399+
except dqlitedbapi.exceptions.OperationalError as wrapped:
400+
assert dialect.is_disconnect(wrapped, None, None) is True
401+
384402

385403
class TestIsolationLevel:
386404
def test_set_isolation_level_warns_on_unsupported(self) -> None:

0 commit comments

Comments
 (0)