Skip to content

Commit 27150c8

Browse files
Import no-transaction substrings from dqlitewire instead of inlining
Replace the inline "no transaction is active" / "cannot rollback" literals in _is_no_tx_rollback_error with the wire-layer constant NO_TRANSACTION_MESSAGE_SUBSTRINGS. The client and dbapi now share one source of truth, eliminating the silent-divergence risk when a server-side wording change requires updating one but not the other. Pin via a unit test that exercises the recogniser with each substring drawn from the wire constant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 12ee7aa commit 27150c8

2 files changed

Lines changed: 53 additions & 5 deletions

File tree

src/dqliteclient/connection.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
)
2626
from dqlitewire import DEFAULT_MAX_CONTINUATION_FRAMES as _DEFAULT_MAX_CONTINUATION_FRAMES
2727
from dqlitewire import DEFAULT_MAX_TOTAL_ROWS as _DEFAULT_MAX_TOTAL_ROWS
28-
from dqlitewire import LEADER_ERROR_CODES
28+
from dqlitewire import LEADER_ERROR_CODES, NO_TRANSACTION_MESSAGE_SUBSTRINGS
2929
from dqlitewire import SQLITE_BUSY as _SQLITE_BUSY
3030
from dqlitewire import TX_AUTO_ROLLBACK_PRIMARY_CODES as _TX_AUTO_ROLLBACK_PRIMARY_CODES
3131
from dqlitewire import primary_sqlite_code as _primary_sqlite_code
@@ -341,24 +341,26 @@ def _is_no_tx_rollback_error(exc: BaseException) -> bool:
341341
reply from the server during a ROLLBACK.
342342
343343
Recognised by SQLite primary code 1 (``SQLITE_ERROR``) plus the
344-
"no transaction is active" or "cannot rollback" wording. Both
344+
wording fragments listed in
345+
:data:`dqlitewire.NO_TRANSACTION_MESSAGE_SUBSTRINGS`. Both
345346
conditions must hold so a disk-full / constraint / IO error whose
346347
message happens to include the magic substring is not silently
347348
treated as benign.
348349
349350
Used by the ``transaction()`` context manager and by the pool's
350351
``_reset_connection`` to distinguish "server already auto-rolled
351352
back; preserve the slot" from a real ROLLBACK failure that
352-
requires invalidation. Centralising the check avoids drift if the
353-
SQLite-error wording ever changes.
353+
requires invalidation. The substring list lives in the wire layer
354+
so this recogniser and the dbapi's ``_is_no_transaction_error``
355+
cannot drift apart on a server-side wording change.
354356
"""
355357
if not isinstance(exc, OperationalError):
356358
return False
357359
code = getattr(exc, "code", None)
358360
if code is None or _primary_sqlite_code(code) != 1: # SQLITE_ERROR
359361
return False
360362
msg = str(exc).lower()
361-
return "no transaction is active" in msg or "cannot rollback" in msg
363+
return any(s in msg for s in NO_TRANSACTION_MESSAGE_SUBSTRINGS)
362364

363365

364366
# RFC 1035 hostname labels are ASCII letters, digits, and hyphen. We
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Pin: the client-layer ``_is_no_tx_rollback_error`` recogniser
2+
shares its substring list with ``dqlitewire.NO_TRANSACTION_MESSAGE_SUBSTRINGS``.
3+
4+
A previous shape inlined the literal substrings in two places
5+
(client and dbapi). Centralising the source in the wire layer
6+
makes drift impossible at the literal level — both layers
7+
import the same tuple object.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from dqliteclient.connection import _is_no_tx_rollback_error
13+
from dqlitewire import NO_TRANSACTION_MESSAGE_SUBSTRINGS
14+
15+
16+
def test_recogniser_uses_wire_layer_substrings() -> None:
17+
"""Drive the recogniser with each substring (alongside the right
18+
SQLite code) and verify it returns True. If a future maintainer
19+
drops a substring on either side, this test detects it via the
20+
wire constant."""
21+
from dqliteclient.exceptions import OperationalError
22+
23+
for substr in NO_TRANSACTION_MESSAGE_SUBSTRINGS:
24+
exc = OperationalError(1, f"prefix {substr} suffix")
25+
assert _is_no_tx_rollback_error(exc), (
26+
f"recogniser must accept the substring {substr!r} "
27+
"from dqlitewire.NO_TRANSACTION_MESSAGE_SUBSTRINGS"
28+
)
29+
30+
31+
def test_recogniser_rejects_unrelated_message() -> None:
32+
from dqliteclient.exceptions import OperationalError
33+
34+
exc = OperationalError(1, "some unrelated SQLite error")
35+
assert not _is_no_tx_rollback_error(exc)
36+
37+
38+
def test_substring_constant_has_expected_values() -> None:
39+
"""Pin the canonical substring list — both clauses must be
40+
present so a server-version drift that drops one but not the
41+
other still triggers the recogniser. If a server change requires
42+
an update, the integration pin
43+
``test_no_transaction_error_wording.py`` (in dbapi) catches it
44+
against a live cluster."""
45+
assert "no transaction is active" in NO_TRANSACTION_MESSAGE_SUBSTRINGS
46+
assert "cannot rollback" in NO_TRANSACTION_MESSAGE_SUBSTRINGS

0 commit comments

Comments
 (0)