Ladybug version
0.20.0. Clean on 0.18.3, 0.19.0 and 0.19.1.
What operating system are you using?
Linux x86-64, Python 3.10–3.14.
What happened?
Calling Connection.execute(<query string>, <params>) twice with the same
string for a write query segfaults the process on the second call. No exception,
no message — the interpreter dies with SIGSEGV (exit 139).
conn.execute("CREATE (:Log {id: $id, value: $val})", {"id": 1, "val": "a"}) # ok
conn.execute("CREATE (:Log {id: $id, value: $val})", {"id": 2, "val": "b"}) # SIGSEGV
Expected: two Log rows. Actual: the process dies on the second call.
Variants, all on 0.20.0:
| variant |
|
same string twice, parameterized CREATE |
SIGSEGV |
| one extra space in the second string |
ok |
prepare() once, execute the statement twice |
ok |
same string twice inside BEGIN TRANSACTION |
SIGSEGV |
same string twice, parameterized MATCH ... SET |
SIGSEGV |
same string twice, parameterized read-only MATCH ... RETURN |
ok |
| two writes with literal values, no parameters |
ok |
Keyed by query text (a single extra space avoids it), the explicit
PreparedStatement path is unaffected, writes only, and transactions change
nothing — which points at the cached physical plan reused on the parameterized
write path.
The reproducer needs no third-party packages, and crashes identically on every
Python ladybug supports (3.10–3.14).
Suspected origin, not bisected. Two 0.20.0 changes interact here: #841 made
repeated executions of a cached physical plan correct while the thread-local
ResultSet cache was still inert, and #850 then activated that cache shortly
before the release. The shipping combination — a cached plan re-executed with a
live TLS ResultSet cache on a write pipeline — postdates #841's verification,
and resetForReuse() may not cover a write pipeline's per-execution state.
Workaround. Prepare once and execute the statement:
stmt = conn.prepare("CREATE (:Log {id: $id, value: $val})")
conn.execute(stmt, {"id": 1, "val": "a"})
conn.execute(stmt, {"id": 2, "val": "b"})
Are there known steps to reproduce?
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10,<3.15"
# dependencies = ["ladybug==0.20.0"]
# ///
"""ladybug 0.20.0: SIGSEGV on the second execute() of the same parameterized
write query string. Clean: 0.18.3, 0.19.0, 0.19.1.
On 0.20.0 this script is expected to die with SIGSEGV (exit 139) at the second
execute() -- that is the bug, not a script defect.
uv run cached_plan_param_write_segv.py
"""
import os
import sys
import tempfile
import ladybug as kuzu
tmp = tempfile.mkdtemp(prefix="lbug_planseg_")
conn = kuzu.Connection(kuzu.Database(os.path.join(tmp, "db")))
conn.execute("CREATE NODE TABLE Log(id INT64, value STRING, PRIMARY KEY(id));")
print(f"ladybug {kuzu.__version__}", flush=True)
query = "CREATE (:Log {id: $id, value: $val})"
conn.execute(query, {"id": 1, "val": "a"})
print("first execute: ok", flush=True)
conn.execute(query, {"id": 2, "val": "b"}) # 0.20.0: SIGSEGV here
print("second execute: ok", flush=True)
n = conn.execute("MATCH (l:Log) RETURN count(l)").get_next()[0]
print(f"rows: {n} (expected 2)")
sys.exit(0 if n == 2 else 1)
Ladybug version
0.20.0. Clean on 0.18.3, 0.19.0 and 0.19.1.
What operating system are you using?
Linux x86-64, Python 3.10–3.14.
What happened?
Calling
Connection.execute(<query string>, <params>)twice with the samestring for a write query segfaults the process on the second call. No exception,
no message — the interpreter dies with SIGSEGV (exit 139).
Expected: two
Logrows. Actual: the process dies on the second call.Variants, all on 0.20.0:
CREATEprepare()once, execute the statement twiceBEGIN TRANSACTIONMATCH ... SETMATCH ... RETURNKeyed by query text (a single extra space avoids it), the explicit
PreparedStatementpath is unaffected, writes only, and transactions changenothing — which points at the cached physical plan reused on the parameterized
write path.
The reproducer needs no third-party packages, and crashes identically on every
Python
ladybugsupports (3.10–3.14).Suspected origin, not bisected. Two 0.20.0 changes interact here: #841 made
repeated executions of a cached physical plan correct while the thread-local
ResultSet cache was still inert, and #850 then activated that cache shortly
before the release. The shipping combination — a cached plan re-executed with a
live TLS ResultSet cache on a write pipeline — postdates #841's verification,
and
resetForReuse()may not cover a write pipeline's per-execution state.Workaround. Prepare once and execute the statement:
Are there known steps to reproduce?