Skip to content

Commit 8aea406

Browse files
Reject zero-column RowsResponse rows at encode time
encode_body() silently accepted a RowsResponse with empty column_names and a non-empty rows list. Each row contributed zero bytes to the encoded payload, so the resulting message was indistinguishable from a zero-row result set — round-tripping lost the rows. decode_body() already has a zero-column fast path that returns no rows. Raise EncodeError when col_count == 0 and rows is non-empty. Place the check above the per-row length validation so the higher-level diagnostic wins over the "row N has 0 values, expected 0" message that would otherwise fire (and confuse). Zero-column-zero-row remains valid (symmetric with the decoder). SQLite cannot produce zero-column results from sqlite3_step, so the C server never hits this case; the fix is purely symmetry hardening for mock-server authors and fuzzers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5647b89 commit 8aea406

2 files changed

Lines changed: 24 additions & 0 deletions

File tree

src/dqlitewire/messages/responses.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,15 @@ def encode_body(self) -> bytes:
300300
f"column_types length ({len(self.column_types)}) != "
301301
f"column_names length ({col_count})"
302302
)
303+
# Zero-column rows produce zero bytes per row, so the encoded
304+
# output is indistinguishable from a zero-row result set — the
305+
# decoder's zero-column fast path returns no rows. Reject at
306+
# encode time rather than silently lose row count.
307+
if col_count == 0 and self.rows:
308+
raise EncodeError(
309+
f"RowsResponse with zero columns cannot carry rows "
310+
f"(got {len(self.rows)} empty row(s))"
311+
)
303312
for i, row in enumerate(self.rows):
304313
if len(row) != col_count:
305314
raise EncodeError(f"Row {i} has {len(row)} values, expected {col_count}")

tests/test_messages_responses.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,21 @@ def test_empty_column_types_with_rows_ok(self) -> None:
686686
decoded = RowsResponse.decode_body(encoded)
687687
assert decoded.rows == [[42]]
688688

689+
def test_zero_columns_with_rows_raises(self) -> None:
690+
"""Zero-column rows are nonsensical: each row emits 0 bytes, so
691+
the encoded message is indistinguishable from a zero-row result
692+
set. Reject at encode time so the encoder is symmetric with the
693+
decoder's zero-column fast path (it returns no rows).
694+
"""
695+
resp = RowsResponse(
696+
column_names=[],
697+
column_types=[],
698+
row_types=[],
699+
rows=[[]],
700+
)
701+
with pytest.raises(EncodeError, match=r"zero columns.*1 empty row"):
702+
resp.encode_body()
703+
689704

690705
class TestRowsResponseNullInTypedColumn:
691706
"""137: None values in rows with explicit column types must encode correctly."""

0 commit comments

Comments
 (0)