Skip to content

Commit 8454a4b

Browse files
Require row_types length to match rows length at encode time
RowsResponse.encode_body previously accepted a ``row_types`` list shorter than ``rows`` and silently fell through to per-row inference for the trailing entries. That contradicts the documented invariant that ``row_types`` is either empty (infer all) or exactly one-to-one with ``rows``. Raise ``EncodeError`` instead so a partial list surfaces loudly at encode time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0a25e82 commit 8454a4b

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

src/dqlitewire/messages/responses.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,16 @@ def encode_body(self) -> bytes:
300300
f"column_types length ({len(self.column_types)}) != "
301301
f"column_names length ({col_count})"
302302
)
303+
# ``row_types`` must either be empty (infer per-row from values /
304+
# column_types) or exactly match ``rows`` one-to-one. A shorter
305+
# list previously fell through to inference silently for the
306+
# trailing rows, contradicting the documented invariant.
307+
if self.row_types and len(self.row_types) != len(self.rows):
308+
raise EncodeError(
309+
f"row_types length ({len(self.row_types)}) != "
310+
f"rows length ({len(self.rows)}); pass an empty row_types "
311+
f"to infer per-row types from the values"
312+
)
303313
# Zero-column rows produce zero bytes per row, so the encoded
304314
# output is indistinguishable from a zero-row result set — the
305315
# decoder's zero-column fast path returns no rows. Reject at

tests/test_messages_responses.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,33 @@ def test_mismatched_row_types_length(self) -> None:
676676
with pytest.raises(EncodeError, match="row_types\\[0\\] has 1 types, expected 2"):
677677
resp.encode_body()
678678

679+
def test_partial_row_types_rejected(self) -> None:
680+
"""row_types must be empty or exactly match rows one-to-one."""
681+
resp = RowsResponse(
682+
column_names=["a"],
683+
column_types=[ValueType.INTEGER],
684+
row_types=[[ValueType.INTEGER]], # only one entry, 3 rows
685+
rows=[[1], [2], [3]],
686+
)
687+
with pytest.raises(EncodeError, match="row_types length .* != rows length"):
688+
resp.encode_body()
689+
690+
def test_empty_row_types_with_rows_infers_ok(self) -> None:
691+
"""Empty row_types is valid — types are inferred per-row."""
692+
resp = RowsResponse(
693+
column_names=["a"],
694+
rows=[[1], [2], [3]],
695+
)
696+
resp.encode_body()
697+
698+
def test_full_row_types_matches_rows_ok(self) -> None:
699+
resp = RowsResponse(
700+
column_names=["a"],
701+
row_types=[[ValueType.INTEGER], [ValueType.INTEGER], [ValueType.INTEGER]],
702+
rows=[[1], [2], [3]],
703+
)
704+
resp.encode_body()
705+
679706
def test_empty_column_types_with_rows_ok(self) -> None:
680707
"""Empty column_types is valid — types are inferred from values."""
681708
resp = RowsResponse(

0 commit comments

Comments
 (0)