Skip to content

Commit 07002ec

Browse files
docs(wire): clarify protocol-ambiguous fields and pin header reserved
- ISSUE-62: add regression test pinning Header.reserved/extra to 0 on encode. Upstream C (message.h) names this field ``extra`` and reserves it for future protocol extensions — if upstream ever repurposes it (e.g. compression flags), this test fails and forces a conscious update rather than silent mis-decode. - ISSUE-64: OpenRequest.flags and OpenRequest.vfs are ignored by the upstream dqlite server (gateway.c/handle_open never reads them). Document that they exist for protocol compatibility only — users should keep the defaults. - ISSUE-68: document the ValueType.UNIXTIME asymmetry explicitly. Upstream C server emits UNIXTIME from query.c for DATETIME columns, upstream C client (tuple.c) rejects UNIXTIME with DQLITE_PARSE, and our Python decoder accepts it — strictly more permissive than the C client. Mock servers must not send UNIXTIME to real C clients. - ISSUE-69: document the ``tail_offset``-drives-schema derivation on StmtResponse. ``tail_offset=None`` → schema=0 (V0 body); ``tail_offset`` set to any int → schema=1 (V1 body). Mock-server authors must match to the request schema byte. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 44da05e commit 07002ec

4 files changed

Lines changed: 46 additions & 3 deletions

File tree

src/dqlitewire/constants.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,16 @@ class ValueType(IntEnum):
7272
BLOB = 4
7373
NULL = 5
7474
# Unix time (deprecated, maps to INTEGER).
75-
# Server-to-client ONLY: the C server's tuple_decoder has no inbound
76-
# case for DQLITE_UNIXTIME, so this tag must never appear on outgoing
77-
# parameters. encode_params_tuple enforces this defensively.
75+
# Server-to-client ONLY: the upstream C server emits UNIXTIME from
76+
# ``query.c`` for DATETIME columns, but the upstream C tuple_decoder
77+
# (``tuple.c``) has no inbound case for DQLITE_UNIXTIME and rejects
78+
# it with DQLITE_PARSE. This Python decoder is strictly more
79+
# permissive than the C client — it accepts UNIXTIME on incoming row
80+
# values and returns int64 seconds-since-epoch. Mock servers built
81+
# on this encoder must NOT send UNIXTIME to real C clients.
82+
# ``encode_params_tuple`` additionally enforces that this tag never
83+
# appears on outgoing parameters (the server's parameter parser
84+
# rejects it too).
7885
UNIXTIME = 9
7986
ISO8601 = 10 # ISO8601 string (maps to TEXT)
8087
BOOLEAN = 11 # Boolean (maps to INTEGER)

src/dqlitewire/messages/requests.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ class OpenRequest(Message):
108108
"""Open a database.
109109
110110
Body: text name, uint64 flags, text vfs
111+
112+
Note: the upstream dqlite server (``gateway.c``/``handle_open``)
113+
currently IGNORES both ``flags`` and ``vfs`` fields. They are encoded
114+
on the wire for protocol compatibility but have no server-side effect.
115+
Keep the defaults (flags=0, vfs="") unless you're intentionally
116+
exercising a future server version or building a mock server.
111117
"""
112118

113119
MSG_TYPE: ClassVar[int] = RequestType.OPEN

src/dqlitewire/messages/responses.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ class StmtResponse(Message):
153153
V0 body: uint32 db_id, uint32 stmt_id, uint64 num_params
154154
V1 body: uint32 db_id, uint32 stmt_id, uint64 num_params, uint64 tail_offset
155155
156+
Schema selection: ``_get_schema()`` derives the header schema byte from
157+
``tail_offset``. ``tail_offset=None`` (default) → schema=0 (V0 body);
158+
``tail_offset`` set to any int (including ``0``) → schema=1 (V1 body).
159+
Mock-server authors must match this to the schema byte of the inbound
160+
:class:`PrepareRequest`, since upstream dqlite servers dispatch on the
161+
request's schema byte, not on any reply-side field.
162+
156163
Note: V1 tail_offset is not present in the canonical Go client
157164
(go-dqlite). The Go EncodePrepare always uses schema=0 and DecodeStmt
158165
does not read tail_offset. This feature may be supported by the C

tests/test_messages_requests.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,29 @@
2828
)
2929

3030

31+
class TestHeaderReservedField:
32+
"""Pin the header's reserved/extra uint16 to zero (ISSUE-62).
33+
34+
Upstream C (``message.h``) names this field ``extra`` and reserves it
35+
for future protocol extensions. All current upstream servers send 0.
36+
If upstream ever repurposes the field (compression flag, extended
37+
schema, etc.), this test fails — forcing a conscious decision rather
38+
than silent mis-decode.
39+
"""
40+
41+
def test_encoded_reserved_is_zero(self) -> None:
42+
cases = [LeaderRequest(), HeartbeatRequest(timestamp=0), OpenRequest(name="db")]
43+
for msg in cases:
44+
encoded = msg.encode()
45+
header = Header.decode(encoded[:HEADER_SIZE])
46+
assert header.reserved == 0, f"{type(msg).__name__} emitted non-zero reserved"
47+
48+
def test_decoded_reserved_is_zero_after_encode_decode(self) -> None:
49+
msg = LeaderRequest()
50+
header = Header.decode(msg.encode()[:HEADER_SIZE])
51+
assert header.reserved == 0
52+
53+
3154
class TestLeaderRequest:
3255
def test_encode_has_body(self) -> None:
3356
"""LeaderRequest body must contain a reserved uint64 per Go spec."""

0 commit comments

Comments
 (0)