Skip to content

Commit ee7e44b

Browse files
Raise DecodeError when zero-column RowsResponse has no end marker
Previously, a truncated zero-column result with no DONE/PART marker was silently accepted as has_more=False. Now raises DecodeError, consistent with the non-zero-column path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 47ae8d2 commit ee7e44b

2 files changed

Lines changed: 56 additions & 28 deletions

File tree

src/dqlitewire/messages/responses.py

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -290,20 +290,21 @@ def decode_body(
290290
# Zero-column results cannot have row data (each row would be zero
291291
# bytes), so skip the row loop and consume the end marker directly.
292292
if column_count == 0:
293-
from dqlitewire.constants import ROW_DONE_BYTE, ROW_PART_BYTE
294-
295-
has_more = False
296-
if offset < len(data):
297-
marker_byte = data[offset]
298-
if marker_byte == ROW_DONE_BYTE:
299-
has_more = False
300-
elif marker_byte == ROW_PART_BYTE:
301-
has_more = True
302-
else:
303-
raise DecodeError(
304-
f"Expected DONE or PART marker for zero-column result, "
305-
f"got 0x{marker_byte:02x}"
306-
)
293+
from dqlitewire.constants import ROW_DONE_BYTE, ROW_PART_BYTE, WORD_SIZE
294+
295+
if offset + WORD_SIZE > len(data):
296+
raise DecodeError(
297+
"RowsResponse body exhausted without end marker (zero-column result)"
298+
)
299+
marker_byte = data[offset]
300+
if marker_byte == ROW_DONE_BYTE:
301+
has_more = False
302+
elif marker_byte == ROW_PART_BYTE:
303+
has_more = True
304+
else:
305+
raise DecodeError(
306+
f"Expected DONE or PART marker for zero-column result, got 0x{marker_byte:02x}"
307+
)
307308
return cls(
308309
column_names=[],
309310
column_types=[],
@@ -383,20 +384,21 @@ def decode_rows_continuation(
383384
column_types: list[ValueType] = []
384385

385386
if column_count == 0:
386-
from dqlitewire.constants import ROW_DONE_BYTE, ROW_PART_BYTE
387-
388-
has_more = False
389-
if offset < len(data):
390-
marker_byte = data[offset]
391-
if marker_byte == ROW_DONE_BYTE:
392-
has_more = False
393-
elif marker_byte == ROW_PART_BYTE:
394-
has_more = True
395-
else:
396-
raise DecodeError(
397-
f"Expected DONE or PART marker for zero-column result, "
398-
f"got 0x{marker_byte:02x}"
399-
)
387+
from dqlitewire.constants import ROW_DONE_BYTE, ROW_PART_BYTE, WORD_SIZE
388+
389+
if offset + WORD_SIZE > len(data):
390+
raise DecodeError(
391+
"RowsResponse continuation exhausted without end marker (zero-column result)"
392+
)
393+
marker_byte = data[offset]
394+
if marker_byte == ROW_DONE_BYTE:
395+
has_more = False
396+
elif marker_byte == ROW_PART_BYTE:
397+
has_more = True
398+
else:
399+
raise DecodeError(
400+
f"Expected DONE or PART marker for zero-column result, got 0x{marker_byte:02x}"
401+
)
400402
return cls(
401403
column_names=column_names,
402404
column_types=[],

tests/test_messages_responses.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,32 @@ def test_zero_columns_malformed_no_infinite_loop(self) -> None:
302302
with pytest.raises(DecodeError, match="Expected DONE or PART marker"):
303303
RowsResponse.decode_body(data)
304304

305+
def test_zero_columns_missing_marker_raises(self) -> None:
306+
"""Zero-column result with no end marker should raise DecodeError."""
307+
import pytest
308+
309+
from dqlitewire.exceptions import DecodeError
310+
from dqlitewire.types import encode_uint64
311+
312+
# column_count=0, but no marker follows
313+
data = encode_uint64(0)
314+
with pytest.raises(DecodeError, match="end marker"):
315+
RowsResponse.decode_body(data)
316+
317+
def test_zero_columns_continuation_missing_marker_raises(self) -> None:
318+
"""Zero-column continuation with no end marker should raise DecodeError."""
319+
import pytest
320+
321+
from dqlitewire.exceptions import DecodeError
322+
323+
# Empty data for a zero-column continuation
324+
with pytest.raises(DecodeError, match="end marker"):
325+
RowsResponse.decode_rows_continuation(
326+
data=b"",
327+
column_names=[],
328+
column_count=0,
329+
)
330+
305331
def test_decode_rows_continuation(self) -> None:
306332
"""Continuation messages (after PART marker) have rows but no column header."""
307333
from dqlitewire.tuples import encode_row_header, encode_row_values

0 commit comments

Comments
 (0)