Skip to content

Fix SIGSEGV re-executing a parameterized write query string (#862) - #863

Merged
adsharma merged 1 commit into
mainfrom
fix_write_sigsegv
Aug 30, 2026
Merged

Fix SIGSEGV re-executing a parameterized write query string (#862)#863
adsharma merged 1 commit into
mainfrom
fix_write_sigsegv

Conversation

@adsharma

Copy link
Copy Markdown
Contributor

Summary

Calling Connection.execute(<query string>, <params>) twice with the same string for a parameterized write query segfaulted on the second call (0.20.0 regression, #862). Reads and the explicit prepare()-without-params path were unaffected.

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

Root cause

The Python string+params path implicitly prepares with its parameters bound, so the second execution takes the cached-physical-plan fast path in ClientContext::executeNoLock(), which calls prepareForReuse() on the cloned plan. That reaches ResultCollector::prepareForReuse()FactorizedTable::clear().

For a write statement, the plan's root ResultCollector has an empty result schema (writes return no columns). FactorizedTable's constructor skips allocating flatTupleBlockCollection / unFlatTupleBlockCollection / inMemOverflowBuffer entirely when the schema is empty, but clear() unconditionally dereferenced the null flatTupleBlockCollection — null-pointer SIGSEGV.

This explains every variant in the issue:

  • reads are unaffected (non-empty result schema → allocations exist)
  • prepare() once + execute() twice is unaffected (unknownParameters non-empty → canReuseCachedPlanWith() returns false → rebind path, no prepareForReuse())
  • transactions change nothing (the fast path is used inside them too)

Fix

FactorizedTable::clear() now early-returns for empty-schema tables, mirroring the constructor's guard. No behavior change for tables that do allocate.

Testing

  • New regression tests in test/api/prepare_test.cpp:
    • RepeatedExecuteCachedPlanParameterizedWrite: parameterized CREATE ×3, parameterized MATCH ... SET ×2, writes inside BEGIN TRANSACTION/COMMIT, final row-count verification
    • RepeatedExecuteCachedPlanParameterizedRead: guards the non-empty-schema path through the fast path
  • Verified the issue's reproducer (ASAN) crashes before the fix and passes after, along with all variants from the issue (extra-space, DDL via string path, 3rd+ executions)
  • Full ASAN api_test suite: 283/283 passed; clang-format clean

Fixes #862

Re-executing the same parameterized write query (e.g. CREATE (:Log {id:
$id, value: $val})) through the implicit prepared-statement cache
crashed with SIGSEGV on the second execution. Reads and explicit
prepare()-without-params (which rebinds on every execute) were
unaffected.

On the second execution the cached-physical-plan fast path calls
prepareForReuse(), which reaches ResultCollector::prepareForReuse() and
FactorizedTable::clear(). For a write statement the root
ResultCollector's FactorizedTable has an empty result schema (writes
return no columns), and the FactorizedTable constructor skips allocating
flatTupleBlockCollection / unFlatTupleBlockCollection /
inMemOverflowBuffer entirely for an empty schema. clear() unconditionally
dereferenced the null block collection.

Fix: make FactorizedTable::clear() early-return for empty-schema tables,
mirroring the constructor's guard.

Add regression tests covering repeated executions of parameterized
CREATE, MATCH...SET, writes inside an explicit transaction, and
parameterized reads through the cached-plan fast path.

Fixes #862
@adsharma
adsharma merged commit d09008e into main Aug 30, 2026
4 checks passed
@adsharma
adsharma deleted the fix_write_sigsegv branch August 30, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: SIGSEGV re-executing a parameterized write query string (0.20.0 regression)

1 participant