Skip to content

Commit 88ae813

Browse files
Pin is_disconnect OS-level branches
DqliteDialect.is_disconnect classifies OSError, ConnectionError, BrokenPipeError, and TimeoutError alongside the dbapi-level OperationalError / InterfaceError taxonomy so SA's pool can invalidate slot on transport failures. Tests only pinned the dbapi-level paths and the wrapped DqliteConnectionError __cause__ chain; the three OS-level types had no direct assertion, leaving a refactor that narrows the tuple (e.g. "OSError covers Linux, drop ConnectionError") to silently break pool recycling. Parametrize across OSError, BrokenPipeError, ConnectionError, ConnectionResetError, and TimeoutError. Add an inverse pin that ProgrammingError is not classified as a disconnect — guarding against the symmetric widening regression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5c1755e commit 88ae813

1 file changed

Lines changed: 33 additions & 0 deletions

File tree

tests/test_dialect.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,39 @@ 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+
@pytest.mark.parametrize(
385+
"exc",
386+
[
387+
OSError(32, "broken pipe"),
388+
BrokenPipeError(32, "broken pipe"),
389+
ConnectionError("peer went away"),
390+
ConnectionResetError(104, "connection reset by peer"),
391+
TimeoutError("read timed out"),
392+
],
393+
)
394+
def test_recognizes_os_level_disconnect_branches(self, exc: BaseException) -> None:
395+
"""The ``is_disconnect`` tuple includes OSError, ConnectionError,
396+
BrokenPipeError, and TimeoutError. ``ConnectionError`` and
397+
``TimeoutError`` are semantically distinct from OSError on
398+
Python 3.11+ — pin each branch so a refactor narrowing the
399+
tuple (e.g. dropping ConnectionError because "OSError covers
400+
it on Linux") would fail loudly and not silently break
401+
pool invalidation on transport failures.
402+
"""
403+
dialect = DqliteDialect()
404+
assert dialect.is_disconnect(exc, None, None) is True
405+
406+
def test_does_not_flag_programming_error(self) -> None:
407+
"""Inverse pin: ProgrammingError is not a transport failure
408+
and must not route through the disconnect path — otherwise SA
409+
would invalidate a healthy connection on e.g. a syntax error.
410+
"""
411+
import dqlitedbapi.exceptions
412+
413+
dialect = DqliteDialect()
414+
e = dqlitedbapi.exceptions.ProgrammingError("no such function: foo")
415+
assert dialect.is_disconnect(e, None, None) is False
416+
384417
def test_recognizes_wrapped_dqlite_connection_error_via_cause(self) -> None:
385418
"""The dbapi ``_call_client`` handler wraps a client-level
386419
``DqliteConnectionError`` into a bare ``OperationalError`` (no

0 commit comments

Comments
 (0)