Skip to content

Commit b9d64df

Browse files
docs(wire): pin Python-only divergences and clarify ClusterRequest V0
Post-audit doc pass. A cross-package audit against upstream dqlite C (dqlite-upstream/src/) and go-dqlite confirmed no wire-level drift and no unfounded claims about upstream, but flagged two documentation gaps: - README had no consolidated list of the Python-specific caps and validations that diverge from C/Go (max_rows/max_message_size caps, full-8-byte row-marker check, BOOLEAN strict encode, FilesResponse alignment, UNIXTIME outbound rejection, StmtResponse short-body rejection, ClusterRequest V0 skip). Add a "Deliberate divergences from upstream" section listing each item and what it protects against. - ClusterRequest.format=0 rejection said "not supported" which reads like upstream removed it. The V0 format is still a valid upstream wire format — we just chose not to implement the ServersResponse decoder for it. Sharpen the docstring and the error messages to make the self-imposed limitation explicit, and update the two tests that matched the old wording. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9e16329 commit b9d64df

3 files changed

Lines changed: 52 additions & 8 deletions

File tree

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,43 @@ socket and decoder.
4444

4545
Based on the [dqlite wire protocol specification](https://canonical.com/dqlite/docs/reference/wire-protocol).
4646

47+
## Deliberate divergences from upstream
48+
49+
This library implements the dqlite wire protocol faithfully but adds a
50+
handful of defensive guards that the upstream C server and the
51+
canonical [go-dqlite](https://github.com/canonical/go-dqlite) client do
52+
not. They protect a Python client running in potentially adversarial
53+
network contexts and are all opt-out-able.
54+
55+
**Python-specific caps** (not present in C or Go; `None` disables):
56+
57+
- `DEFAULT_MAX_ROWS` (`MessageDecoder(max_rows=...)`, default 1,000,000)
58+
— per-query row cap.
59+
- `DEFAULT_MAX_MESSAGE_SIZE` (`ReadBuffer(max_message_size=...)`, default
60+
64 MiB) — envelope cap on a single frame.
61+
- `_MAX_PARAM_COUNT` (100,000), `_MAX_COLUMN_COUNT` (10,000),
62+
`_MAX_FILE_COUNT` (100), `_MAX_NODE_COUNT` (10,000) — internal
63+
sanity bounds on decoded tuple / response sizes.
64+
65+
**Stricter-than-Go validations** (match the C server's intent):
66+
67+
- `decode_row_header` requires the full 8-byte marker (C defines
68+
`DQLITE_RESPONSE_ROWS_DONE = 0xff..ff` / `_PART = 0xee..ee`;
69+
go-dqlite checks only the first byte).
70+
- `encode_value(value, ValueType.BOOLEAN)` rejects arbitrary ints
71+
(accepts only `bool` or exactly `0`/`1`).
72+
- `FilesResponse.encode_body` rejects non-8-aligned file content (C's
73+
`dumpFile` asserts `len % 8 == 0`).
74+
- `encode_params_tuple` rejects `ValueType.UNIXTIME` outbound (C's
75+
`tuple_decoder__next` cannot decode it on the server side).
76+
- `StmtResponse` rejects a 16-byte body when `schema=1` (C's V1
77+
response is 24 bytes).
78+
79+
**Not implemented** (valid upstream formats this library chose to skip):
80+
81+
- `ClusterRequest` `format=0` — V0 response with id+address only. We
82+
only decode the V1 variant that includes node role.
83+
4784
## Development
4885

4986
See [DEVELOPMENT.md](DEVELOPMENT.md) for setup and contribution guidelines.

src/dqlitewire/messages/requests.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,12 @@ class ClusterRequest(Message):
514514
"""Request cluster information.
515515
516516
Body: uint64 format
517+
518+
Note: format=0 (V0: id+address only, no role) IS a valid upstream
519+
dqlite wire format. This Python library chooses not to implement it
520+
because :class:`ServersResponse` only decodes V1 (id+address+role).
521+
Callers that need V0 compatibility should decode :class:`ServersResponse`
522+
themselves. Use ``format=1`` for the default path.
517523
"""
518524

519525
MSG_TYPE: ClassVar[int] = RequestType.CLUSTER
@@ -524,9 +530,9 @@ def __post_init__(self) -> None:
524530
_check_uint64("format", self.format)
525531
if self.format == 0:
526532
raise ValueError(
527-
"ClusterRequest format=0 (V0) is not supported. "
528-
"ServersResponse only decodes V1 format (with node role fields). "
529-
"Use format=1."
533+
"ClusterRequest format=0 (V0) is valid in upstream dqlite but "
534+
"not implemented in this Python library: ServersResponse only "
535+
"decodes V1 (with node role fields). Use format=1."
530536
)
531537

532538
def encode_body(self) -> bytes:
@@ -537,8 +543,9 @@ def decode_body(cls, data: bytes, schema: int = 0) -> "ClusterRequest":
537543
format_val = decode_uint64(data)
538544
if format_val == 0:
539545
raise DecodeError(
540-
"ClusterRequest format=0 (V0) is not supported. "
541-
"ServersResponse only decodes V1 format (with node role fields)."
546+
"ClusterRequest format=0 (V0) is valid in upstream dqlite but "
547+
"not implemented in this Python library: ServersResponse only "
548+
"decodes V1 (with node role fields)."
542549
)
543550
return cls(format_val)
544551

tests/test_messages_requests.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,8 @@ def test_default_format_is_v1(self) -> None:
442442
assert msg.format == 1
443443

444444
def test_format_v0_rejected(self) -> None:
445-
"""120: V0 cluster format not supported by ServersResponse decoder."""
446-
with pytest.raises(ValueError, match="format=0.*not supported"):
445+
"""120: V0 cluster format not implemented by ServersResponse decoder."""
446+
with pytest.raises(ValueError, match="format=0.*not implemented"):
447447
ClusterRequest(format=0)
448448

449449
def test_decode_format_v0_raises_decode_error(self) -> None:
@@ -452,7 +452,7 @@ def test_decode_format_v0_raises_decode_error(self) -> None:
452452
from dqlitewire.types import encode_uint64
453453

454454
body = encode_uint64(0) # format=0
455-
with pytest.raises(DecodeError, match="format=0.*not supported"):
455+
with pytest.raises(DecodeError, match="format=0.*not implemented"):
456456
ClusterRequest.decode_body(body)
457457

458458

0 commit comments

Comments
 (0)