Skip to content

Commit f35625f

Browse files
Override get_isolation_level_values to advertise SERIALIZABLE only
The previous ``supported_isolation_levels`` class attribute was a bespoke name that SQLAlchemy never consults. The real introspection surface is ``get_isolation_level_values(dbapi_connection)``, which parent ``SQLiteDialect`` overrides to return both READ UNCOMMITTED and SERIALIZABLE. dqlite runs every statement through Raft consensus and cannot honour READ UNCOMMITTED, so a caller passing ``isolation_level="READ UNCOMMITTED"`` to ``create_engine`` was silently accepted and routed into pysqlite's PRAGMA read_uncommitted code path with no effect. Override the method to return ``["SERIALIZABLE"]`` only and drop the bespoke attribute. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 950ba51 commit f35625f

2 files changed

Lines changed: 37 additions & 10 deletions

File tree

src/sqlalchemydqlite/base.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import math
66
import types
77
import warnings
8-
from collections.abc import Callable
8+
from collections.abc import Callable, Sequence
99
from typing import Any
1010

1111
from sqlalchemy import types as sqltypes
@@ -119,11 +119,6 @@ class DqliteDialect(SQLiteDialect):
119119
# SQLAlchemy release decouples the two.
120120
non_native_boolean_check_constraint = False
121121

122-
# dqlite runs every statement through Raft consensus; there is no
123-
# exposed way to weaken isolation. Declaring this explicitly lets
124-
# applications introspect via ``engine.dialect.supported_isolation_levels``.
125-
supported_isolation_levels: tuple[str, ...] = ("SERIALIZABLE",)
126-
127122
# Since isolation is always SERIALIZABLE and cannot be weakened, the
128123
# reported isolation level is trustworthy across transactions. SQLAlchemy
129124
# skips defensive isolation-level resets when this is True.
@@ -237,6 +232,21 @@ def create_connect_args(self, url: URL) -> tuple[list[Any], dict[str, Any]]:
237232

238233
return [], kwargs
239234

235+
def get_isolation_level_values(
236+
self, dbapi_connection: DBAPIConnection
237+
) -> Sequence[IsolationLevel]:
238+
"""Return the isolation levels dqlite accepts.
239+
240+
The parent ``SQLiteDialect`` advertises ``["READ UNCOMMITTED",
241+
"SERIALIZABLE"]`` because stdlib sqlite3 implements
242+
``READ UNCOMMITTED`` via ``PRAGMA read_uncommitted`` in
243+
shared-cache mode. dqlite runs every statement through Raft
244+
consensus and has no mechanism to weaken isolation, so advertise
245+
only what we can honour — ``set_isolation_level`` below rejects
246+
anything else explicitly.
247+
"""
248+
return ["SERIALIZABLE"]
249+
240250
def get_isolation_level(self, dbapi_connection: DBAPIConnection) -> IsolationLevel:
241251
"""Return the isolation level.
242252

tests/test_dialect_dialect_config.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- ``create_connect_args`` plumbs the ``timeout`` URL query
77
through and rejects typos.
88
- ``set_isolation_level`` explicitly rejects AUTOCOMMIT.
9-
- ``supported_isolation_levels`` is declared.
9+
- ``get_isolation_level_values`` advertises only SERIALIZABLE.
1010
"""
1111

1212
from unittest.mock import MagicMock
@@ -20,9 +20,26 @@
2020
from sqlalchemydqlite.base import DqliteDialect
2121

2222

23-
class TestSupportedIsolationLevels:
24-
def test_declared(self) -> None:
25-
assert DqliteDialect.supported_isolation_levels == ("SERIALIZABLE",)
23+
class TestGetIsolationLevelValues:
24+
def test_only_serializable(self) -> None:
25+
"""dqlite accepts SERIALIZABLE only; inheriting SQLiteDialect's
26+
``["READ UNCOMMITTED", "SERIALIZABLE"]`` would let callers set a
27+
level we cannot honour (PRAGMA read_uncommitted has no effect
28+
server-side).
29+
"""
30+
dialect = DqliteDialect()
31+
assert dialect.get_isolation_level_values(MagicMock()) == ["SERIALIZABLE"]
32+
33+
def test_read_uncommitted_not_advertised(self) -> None:
34+
dialect = DqliteDialect()
35+
assert "READ UNCOMMITTED" not in dialect.get_isolation_level_values(MagicMock())
36+
37+
def test_defined_locally(self) -> None:
38+
"""Must be overridden on DqliteDialect itself; inheriting
39+
SQLiteDialect's version would silently re-introduce
40+
READ UNCOMMITTED.
41+
"""
42+
assert "get_isolation_level_values" in DqliteDialect.__dict__
2643

2744

2845
class TestSetIsolationLevel:

0 commit comments

Comments
 (0)