Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 51 additions & 5 deletions src_py/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,58 @@ def _pybind_value_signature(value: Any) -> tuple:
tuple((str(k), _pybind_value_signature(v)) for k, v in items),
)
if isinstance(value, (list, tuple)):
homogeneous = _pybind_homogeneous_list_signature(value)
if homogeneous is not None:
return homogeneous
if not value:
int_width_order = {
"int8": 0,
"uint8": 1,
"int16": 2,
"uint16": 3,
"int32": 4,
"uint32": 5,
"int64": 6,
}

def _merge(a: tuple, b: tuple) -> tuple:
# Any/unknown poisoning: fall back to first-seen (keeps old key).
if a == b:
return a
if a == ("any",) or b == ("any",):
return ("any",)
if (
len(a) == 1
and len(b) == 1
and a[0] in int_width_order
and b[0] in int_width_order
):
# Mirror C++ tryGetMaxLogicalType over list children: the
# prepared type is the max over ALL elements, so the cache key
# must reflect the widest int, not the first element's width.
return a if int_width_order[a[0]] >= int_width_order[b[0]] else b
if a[0] == "list" and b[0] == "list" and len(a) == 2 and len(b) == 2:
return ("list", _merge(a[1], b[1]))
if a[0] == "struct" and b[0] == "struct":
fa, fb = dict(a[1]), dict(b[1])
if set(fa) != set(fb):
return ("struct", tuple(sorted(set(fa) | set(fb))))
return ("struct", tuple((k, _merge(fa[k], fb[k])) for k in fa))
if a[0] == "map" and b[0] == "map":
return ("map", _merge(a[1], b[1]), _merge(a[2], b[2]))
# Heterogeneous / unmergeable: keep first-seen (old behaviour).
return a

merged: tuple | None = None
for item in value:
if item is None:
continue
sig = _pybind_value_signature(item)
merged = sig if merged is None else _merge(merged, sig)
if merged is None:
return ("list", ("any",))
return ("list", _pybind_value_signature(value[0]))
# Mirror C++ pyHomogeneousListType: a homogeneous int/bool/float list
# prepares as LIST(INT64)/LIST(BOOL)/LIST(DOUBLE) regardless of value
# widths, so normalise the key the same way to avoid duplicate prepares.
if len(merged) == 1 and merged[0] in int_width_order:
return ("list", ("int64",))
return ("list", merged)
return ("unknown", type(value).__name__)


Expand Down
21 changes: 21 additions & 0 deletions test/test_pybind_implicit_prepare_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,24 @@ def test_pybind_implicit_prepare_reuses_same_struct_shape(
{"st": {"key1": [1, 2, 3], "value": [3, 7, 98]}},
{"st": {"key1": [4, 5, 6], "value": [7, 8, 9]}},
]


def test_pybind_implicit_prepare_segregates_struct_list_by_int_width(
fake_pybind_connection: _FakePybindConnection,
) -> None:
"""
Regression test for issue #57.

A list-of-struct parameter whose later rows need a wider int type than
the first call's must not reuse the narrower cached statement (values
were silently dropped/truncated through the stale plan).
"""
conn = lb.Connection(_FakeDatabase())

conn.execute("UNWIND $rows AS r RETURN r.d", {"rows": [{"d": 0}]})
conn.execute("UNWIND $rows AS r RETURN r.d", {"rows": [{"d": 1}, {"d": 128}]})
conn.execute("UNWIND $rows AS r RETURN r.d", {"rows": [{"d": 2}, {"d": 128}]})
conn.execute("UNWIND $rows AS r RETURN r.d", {"rows": [{"d": 4}, {"d": 300}]})

# int8 -> uint8 -> uint8 (reuse) -> int16 : 3 prepared statements.
assert len(fake_pybind_connection.prepare_calls) == 3
Loading