From 18e6139714df483d042d791727a77fc604b6f222 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Sun, 30 Aug 2026 16:15:01 -0700 Subject: [PATCH 1/2] Add regression test for issue ladybugdb/ladybug#866 (SIGSEGV re-executing parameterized write) Replicates the scenario from issue #866: the same parameterized write query string (CREATE (:Contribution {id: $id, embedding: $embedding})) executed repeatedly through Connection.execute() and the implicit prepared-statement cache, followed by a vector similarity read. On the 0.20.0 engine this segfaulted on the second execution: the cached-physical-plan fast path calls prepareForReuse(), which reaches FactorizedTable::clear() on the write statement root ResultCollector. That FactorizedTable has an empty result schema, and the constructor skips allocating flatTupleBlockCollection / inMemOverflowBuffer for empty schemas, so clear() dereferenced a null pointer. The vector extension and ARRAY_COSINE_SIMILARITY in the original report were not involved in the crash. Verified: this test SIGSEGVs against a v0.20.0 build and passes on current main (which contains the clear() empty-schema guard from ladybugdb/ladybug#862). --- test/test_issue.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/test_issue.py b/test/test_issue.py index 5cea6b0..0faf62a 100644 --- a/test/test_issue.py +++ b/test/test_issue.py @@ -152,6 +152,49 @@ def test_issue_483_numpy_ndarray_parameter(conn_db_readwrite: ConnDB) -> None: result.close() +def test_issue_866_repeated_parameterized_write_then_vector_read( + conn_db_readwrite: ConnDB, +) -> None: + # https://github.com/LadybugDB/ladybug/issues/866 + # Re-executing the same parameterized write query string through the implicit + # prepared-statement cache takes the cached-physical-plan fast path, whose + # prepareForReuse() calls FactorizedTable::clear() on the root ResultCollector of + # the write. Write statements return no columns, so that FactorizedTable has an + # empty schema and never allocates its block collections; clear() dereferenced + # the null collection (SIGSEGV on 0.20.0). The vector similarity query in the + # original report was unrelated to the crash. + conn, _ = conn_db_readwrite + conn.execute( + "CREATE NODE TABLE Contribution(id STRING, embedding FLOAT[4], PRIMARY KEY(id))" + ) + embeddings = { + "c1": [1.0, 0.0, 0.2, 0.0], + "c2": [0.0, 1.0, 0.0, 0.2], + "c3": [0.8, 0.0, 0.1, 0.0], + } + # Same parameterized query string executed repeatedly (fast path on every run + # but the first). + for node_id, embedding in embeddings.items(): + result = conn.execute( + "CREATE (:Contribution {id: $id, embedding: $embedding})", + {"id": node_id, "embedding": embedding}, + ) + result.close() + + result = conn.execute( + "MATCH (c:Contribution) RETURN c.id, ARRAY_COSINE_SIMILARITY(c.embedding, [1.0, 0.0, 0.2, 0.0])" + " AS sim ORDER BY c.id" + ) + assert result.has_next() + assert result.get_next() == ["c1", pytest.approx(1.0)] + assert result.has_next() + assert result.get_next() == ["c2", pytest.approx(0.0)] + assert result.has_next() + assert 0.0 < result.get_next()[1] < 1.0 + assert not result.has_next() + result.close() + + # TODO(Maxwell): check if we should change getCastCost() for the following test # def test_issue_3248(conn_db_readwrite: ConnDB) -> None: # conn, _ = conn_db_readwrite From 9fcd84780f2454f76307b4f15ab55d61e63414bc Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Sun, 30 Aug 2026 19:41:09 -0700 Subject: [PATCH 2/2] Fix test_fsm for COPY that no longer force-checkpoints under auto_checkpoint=false Since ladybug commit 4ca1d6f5d ("Fix #755: COPY respects auto_checkpoint setting"), COPY with auto_checkpoint=false leaves the data in the WAL instead of checkpointing. The FSM tests captured used page ranges right after COPY, at which point storage_info() reported in-memory chunks with start_page_idx = INVALID (nothing was on disk yet), so after drop + checkpoint there was nothing in the FSM to compare against and all reclaim assertions failed. Fix: checkpoint explicitly after COPY in the fixtures (and after the re-copy in test_fsm_reclaim_node_table_recopy) so the tests measure real on-disk page indices, preserving the tests' intent of verifying FSM reclaim of persisted pages. --- test/test_fsm.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/test_fsm.py b/test/test_fsm.py index 7b9855f..4026002 100644 --- a/test/test_fsm.py +++ b/test/test_fsm.py @@ -100,6 +100,10 @@ def fsm_node_table_setup(tmp_path: Path): conn.execute( f"COPY person FROM ['{LBUG_ROOT}/dataset/tinysnb/vPerson.csv', '{LBUG_ROOT}/dataset/tinysnb/vPerson2.csv'](ignore_errors=true, header=false)" ) + # COPY no longer force-checkpoints when auto_checkpoint=false (fix for #755). + # Checkpoint explicitly so the data pages are on disk and storage_info() + # reports real page indices for the reclaim assertions below. + conn.execute("checkpoint") return db, conn @@ -112,6 +116,7 @@ def fsm_rel_table_setup(fsm_node_table_setup): conn.execute( f"COPY knows FROM ['{LBUG_ROOT}/dataset/tinysnb/eKnows.csv', '{LBUG_ROOT}/dataset/tinysnb/eKnows_2.csv']" ) + conn.execute("checkpoint") return fsm_node_table_setup @@ -138,6 +143,7 @@ def fsm_rel_group_setup(tmp_path: Path): conn.execute( f'COPY likes FROM "{LBUG_ROOT}/dataset/rel-group/edge.csv" (FROM="personB", TO="personA");' ) + conn.execute("checkpoint") return db, conn @@ -193,6 +199,7 @@ def test_fsm_reclaim_node_table_recopy(fsm_node_table_setup) -> None: conn.execute( f"COPY person FROM ['{LBUG_ROOT}/dataset/tinysnb/vPerson.csv', '{LBUG_ROOT}/dataset/tinysnb/vPerson2.csv'](ignore_errors=true, header=false)" ) + conn.execute("checkpoint") new_num_pages = get_total_used_pages(conn) assert prev_num_pages == new_num_pages