Skip to content

Commit 7f18910

Browse files
Fix off-by-one in RowsResponse max_rows limit check
Changed > to >= so the limit fires when len(rows) reaches max_rows instead of max_rows+1. Previously max_rows+1 rows would be decoded into memory before the error was raised. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9c85405 commit 7f18910

2 files changed

Lines changed: 33 additions & 2 deletions

File tree

src/dqlitewire/messages/responses.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ def decode_body(
349349
rows.append(values)
350350
offset += consumed
351351

352-
if len(rows) > max_rows:
352+
if len(rows) >= max_rows:
353353
raise DecodeError(f"Row count {len(rows)} exceeds maximum {max_rows}")
354354

355355
if offset == prev_offset:
@@ -441,7 +441,7 @@ def decode_rows_continuation(
441441
rows.append(values)
442442
offset += consumed
443443

444-
if len(rows) > max_rows:
444+
if len(rows) >= max_rows:
445445
raise DecodeError(f"Row count {len(rows)} exceeds maximum {max_rows}")
446446

447447
if offset == prev_offset:

tests/test_messages_responses.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,37 @@ def test_max_rows_limit_decode_rows_continuation(self) -> None:
481481
with pytest.raises(DecodeError, match="Row count.*exceeds maximum"):
482482
RowsResponse.decode_rows_continuation(body, ["x"], 1, max_rows=2)
483483

484+
def test_max_rows_exact_boundary_rejects_at_limit(self) -> None:
485+
"""max_rows=3 with exactly 3 rows should raise DecodeError.
486+
487+
The max_rows parameter is a strict upper bound: at most max_rows - 1
488+
rows should be decoded without error. When the number of rows
489+
reaches max_rows, the limit has been exceeded and DecodeError
490+
should fire immediately — without decoding another row first.
491+
"""
492+
import pytest
493+
494+
from dqlitewire.exceptions import DecodeError
495+
from dqlitewire.tuples import encode_row_header, encode_row_values
496+
from dqlitewire.types import encode_text, encode_uint64
497+
498+
def build_body(n_rows: int) -> bytes:
499+
body = encode_uint64(1) # column_count=1
500+
body += encode_text("x")
501+
for i in range(n_rows):
502+
body += encode_row_header([ValueType.INTEGER])
503+
body += encode_row_values([i], [ValueType.INTEGER])
504+
body += encode_uint64(0xFFFFFFFFFFFFFFFF) # DONE
505+
return body
506+
507+
# Exactly max_rows rows should raise
508+
with pytest.raises(DecodeError, match="exceeds maximum"):
509+
RowsResponse.decode_body(build_body(3), max_rows=3)
510+
511+
# One fewer than max_rows should succeed
512+
decoded = RowsResponse.decode_body(build_body(2), max_rows=3)
513+
assert len(decoded.rows) == 2
514+
484515

485516
class TestEmptyResponse:
486517
def test_encode(self) -> None:

0 commit comments

Comments
 (0)