Skip to content

Commit e085af7

Browse files
fix: override type to NULL for None values in RowsResponse encode
_get_row_types now sets the type nibble to ValueType.NULL for None values, matching Go's per-row type header behavior where the nibble reflects the actual value, not the column schema. Previously, encoding a RowsResponse with explicit column_types and None values in rows crashed with EncodeError after the issue 094 fix. Closes #137 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 235f69e commit e085af7

2 files changed

Lines changed: 62 additions & 6 deletions

File tree

src/dqlitewire/messages/responses.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,24 @@ def _get_row_types(self, row_idx: int, row: list[Any]) -> list[ValueType]:
254254
return value cannot silently rewrite the message's private
255255
copy. This preserves the aliasing invariant that
256256
``__post_init__`` establishes (issue 042, issue 052).
257+
258+
None values override the declared type to NULL, matching Go's
259+
per-row type header behavior where the nibble reflects the actual
260+
value, not the column schema (issue 137).
257261
"""
258262
if self.row_types and row_idx < len(self.row_types):
259-
return list(self.row_types[row_idx])
260-
if self.column_types:
261-
return list(self.column_types)
262-
# Infer from values
263-
264-
return [encode_value(v)[1] for v in row]
263+
types = list(self.row_types[row_idx])
264+
elif self.column_types:
265+
types = list(self.column_types)
266+
else:
267+
# Infer from values
268+
return [encode_value(v)[1] for v in row]
269+
270+
# Override type to NULL for None values, matching Go's behavior
271+
for i, v in enumerate(row):
272+
if v is None and i < len(types):
273+
types[i] = ValueType.NULL
274+
return types
265275

266276
def encode_body(self) -> bytes:
267277
col_count = len(self.column_names)

tests/test_messages_responses.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,52 @@ def test_empty_column_types_with_rows_ok(self) -> None:
621621
assert decoded.rows == [[42]]
622622

623623

624+
class TestRowsResponseNullInTypedColumn:
625+
"""137: None values in rows with explicit column types must encode correctly."""
626+
627+
def test_none_with_explicit_column_types(self) -> None:
628+
"""None in a TEXT column must encode as NULL, not crash."""
629+
resp = RowsResponse(
630+
column_names=["id", "name"],
631+
column_types=[ValueType.INTEGER, ValueType.TEXT],
632+
row_types=[[ValueType.INTEGER, ValueType.TEXT]],
633+
rows=[[1, None]],
634+
)
635+
encoded = resp.encode_body()
636+
decoded = RowsResponse.decode_body(encoded)
637+
assert decoded.rows == [[1, None]]
638+
# The type nibble for the None column should be NULL (5), not TEXT (3)
639+
assert decoded.row_types[0][1] == ValueType.NULL
640+
641+
def test_none_with_explicit_row_types_integer(self) -> None:
642+
"""None in an INTEGER column with explicit row_types."""
643+
resp = RowsResponse(
644+
column_names=["val"],
645+
column_types=[ValueType.INTEGER],
646+
row_types=[[ValueType.INTEGER]],
647+
rows=[[None]],
648+
)
649+
encoded = resp.encode_body()
650+
decoded = RowsResponse.decode_body(encoded)
651+
assert decoded.rows == [[None]]
652+
assert decoded.row_types[0][0] == ValueType.NULL
653+
654+
def test_mixed_none_and_values(self) -> None:
655+
"""Row with mix of None and real values."""
656+
resp = RowsResponse(
657+
column_names=["a", "b", "c"],
658+
column_types=[ValueType.INTEGER, ValueType.TEXT, ValueType.FLOAT],
659+
row_types=[[ValueType.INTEGER, ValueType.TEXT, ValueType.FLOAT]],
660+
rows=[[None, "hello", None]],
661+
)
662+
encoded = resp.encode_body()
663+
decoded = RowsResponse.decode_body(encoded)
664+
assert decoded.rows == [[None, "hello", None]]
665+
assert decoded.row_types[0][0] == ValueType.NULL
666+
assert decoded.row_types[0][1] == ValueType.TEXT
667+
assert decoded.row_types[0][2] == ValueType.NULL
668+
669+
624670
class TestRowsResponseWordBoundary:
625671
"""115: row header padding crosses word boundary at 17+ columns."""
626672

0 commit comments

Comments
 (0)