Skip to content

Commit f4b0d70

Browse files
feat(sqlalchemy): enable supports_native_boolean
dqlite's wire protocol has a first-class BOOLEAN tag (ValueType.BOOLEAN = 11) and the server returns native Python bool for columns typed BOOLEAN. Previously we inherited ``supports_native_boolean = False`` from the pysqlite dialect, so SQLAlchemy emitted ``CHECK (col IN (0, 1))`` on every Boolean column — redundant given the wire contract. Set ``supports_native_boolean = True`` and add an integration test that verifies: - DDL for Column(Boolean) does not emit a CHECK clause - Round-trip preserves Python bool - Reflection recovers the Boolean type Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1584d48 commit f4b0d70

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

src/sqlalchemydqlite/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ class DqliteDialect(SQLiteDialect):
6969
# Enable SQLAlchemy statement caching
7070
supports_statement_cache = True
7171

72+
# dqlite's wire protocol has a first-class BOOLEAN tag
73+
# (``ValueType.BOOLEAN = 11``); the server returns native Python
74+
# booleans for columns tagged BOOLEAN and dqlitedbapi passes them
75+
# through unchanged. Unlike the inherited pysqlite dialect
76+
# (``supports_native_boolean = False``), we don't need SQLAlchemy
77+
# to emit a ``CHECK (col IN (0, 1))`` constraint — the wire
78+
# contract enforces the 0/1 invariant.
79+
supports_native_boolean = True
80+
7281
# dqlite runs every statement through Raft consensus; there is no
7382
# exposed way to weaken isolation. Declaring this explicitly lets
7483
# applications introspect via ``engine.dialect.supported_isolation_levels``.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Integration tests for ISSUE-44: supports_native_boolean = True.
2+
3+
DDL for ``Column(Boolean)`` must not emit ``CHECK (col IN (0, 1))``
4+
because dqlite's wire protocol enforces the invariant natively.
5+
"""
6+
7+
from collections.abc import Generator
8+
9+
import pytest
10+
from sqlalchemy import Boolean, Column, Integer, create_engine, inspect, text
11+
from sqlalchemy.engine import Engine
12+
from sqlalchemy.orm import Session, declarative_base
13+
14+
Base = declarative_base()
15+
16+
17+
class BoolModel(Base): # type: ignore[valid-type,misc]
18+
__tablename__ = "native_bool_test"
19+
id = Column(Integer, primary_key=True)
20+
flag = Column(Boolean, nullable=False)
21+
22+
23+
@pytest.mark.integration
24+
class TestNativeBoolean:
25+
@pytest.fixture
26+
def engine(self, engine_url: str) -> Generator[Engine]:
27+
engine = create_engine(engine_url)
28+
Base.metadata.create_all(engine)
29+
yield engine
30+
Base.metadata.drop_all(engine)
31+
engine.dispose()
32+
33+
def test_ddl_does_not_emit_check_constraint(self, engine: Engine) -> None:
34+
"""With supports_native_boolean=True, no CHECK clause should appear."""
35+
with engine.connect() as conn:
36+
result = conn.execute(
37+
text("SELECT sql FROM sqlite_master WHERE type='table' AND name='native_bool_test'")
38+
).first()
39+
assert result is not None
40+
ddl = result[0].upper()
41+
assert "CHECK" not in ddl, f"Unexpected CHECK in DDL: {result[0]}"
42+
43+
def test_roundtrip_preserves_native_bool(self, engine: Engine) -> None:
44+
with Session(engine) as s:
45+
s.add_all([BoolModel(flag=True), BoolModel(flag=False)])
46+
s.commit()
47+
rows = s.query(BoolModel).order_by(BoolModel.id).all()
48+
assert [r.flag for r in rows] == [True, False]
49+
assert all(isinstance(r.flag, bool) for r in rows)
50+
51+
def test_reflect_column_is_boolean(self, engine: Engine) -> None:
52+
insp = inspect(engine)
53+
cols = {c["name"]: c for c in insp.get_columns("native_bool_test")}
54+
assert "flag" in cols
55+
# SQLAlchemy should be able to reflect the BOOLEAN type back.
56+
# We accept either Boolean or the inherited SQLite numeric
57+
# fallback, but assert type_affinity is Boolean.
58+
from sqlalchemy import types
59+
60+
assert cols["flag"]["type"].python_type is bool or isinstance(
61+
cols["flag"]["type"], types.Boolean
62+
)
63+
# Non-null reflection works.
64+
assert cols["flag"]["nullable"] is False
65+
66+
67+
@pytest.mark.integration
68+
class TestDialectFlag:
69+
def test_supports_native_boolean_is_true(self, engine_url: str) -> None:
70+
engine = create_engine(engine_url)
71+
try:
72+
assert engine.dialect.supports_native_boolean is True
73+
finally:
74+
engine.dispose()

0 commit comments

Comments
 (0)