Repository navigation
Calvin commit refuses a predicate UPDATE or DELETE with XX000, and GRAPH RAG FUSION names a hit in no edge by its surrogate — transaction blocks and trigger bodies fail, fusion rows do not join to documents #424
Description
Version / build tested against
origin/main @ d1e957217 (debug build, nodedb 0.7.0-dev, in-process pgwire test server).
Defect B also reproduces on v0.6.0 and v0.6.1. Defect A reproduces on v0.6.0 and 1e9a393cb, where the client saw SQLSTATE 40000 because the stage refusal did not yet reach it.
Deployment mode
Origin — single node (local)
Engine(s) involved
Document, Graph, Vector
Summary
Found while loading a code graph of 79,037 symbols into NodeDB and querying it with GRAPH RAG FUSION. The two defects have separate causes. The linked PR holds one fix commit for each.
A. A predicate write cannot commit through Calvin. COMMIT of a transaction block whose commit takes the Calvin path fails when the block holds a predicate UPDATE or DELETE. The Calvin stage runs a bulk plan over the surrogates the plan names and refuses a plan that names none. The autocommit path reads that set at recon and injects it. A transaction block buffers the plan with none. A write to a collection that holds edges always plans as a predicate write, because it derives the edge tasks of each row. A DELETE of any row there reaches the refusal alone. An UPDATE reaches it beside an edge write. A plain autocommit INSERT fails the same way when its BEFORE or SYNC AFTER trigger body holds a predicate write, because the body joins the statement's transaction. A CALL of a procedure fails the same way when its body holds a predicate DELETE, or a predicate UPDATE and a DELETE, on an edge-bearing collection. Nothing is applied. Since #422 the client sees the participant's own error.
B. GRAPH RAG FUSION returns a surrogate as node_id. For a hit that is in no edge the Data Plane has no node name. It falls back to the surrogate's decimal. SEARCH ... USING VECTOR, full-text and hybrid search translate the surrogate to the primary key. Fusion is the one search response that skips that step. A client cannot join such a row to its document, and SELECT ... WHERE id = '1' finds nothing.
Evidence (probes, SQL scripts, test output): https://github.com/EnRaiha/nodedb-fusion-evidence
Steps to reproduce
Defect A, transaction block:
CREATE COLLECTION g;
UPSERT INTO g { id: 'd0', title: 't0' };
UPSERT INTO g { id: 'd1', title: 't1' };
UPSERT INTO g { id: 'd2', title: 't2' };
UPSERT INTO g { id: 'd3', title: 't3' };
GRAPH INSERT EDGE IN 'g' FROM 'd0' TO 'd1' TYPE 'l';
BEGIN;
DELETE FROM g WHERE id = 'd3';
COMMIT;ERROR: internal error: calvin bulk predicate write reached overlay staging without ollp_predicted_surrogates for collection 'g'; a Calvin bulk plan must be recon-injected before dispatch (SQLSTATE XX000)
Defect A, trigger body (same collection g):
CREATE COLLECTION c (id TEXT PRIMARY KEY, v INT) WITH (engine='document_strict');
CREATE SYNC TRIGGER bump AFTER INSERT ON c FOR EACH ROW
BEGIN UPDATE g SET title = 'trig' WHERE title = 't2'; END;
INSERT INTO c (id, v) VALUES ('c1', 1);The INSERT fails with the same error. A BEFORE INSERT trigger with the same body fails the same way.
Defect B:
CREATE COLLECTION ragf_names (id TEXT PRIMARY KEY, name TEXT);
CREATE VECTOR INDEX idx_ragf_names ON ragf_names (embedding) METRIC cosine DIM 2;
INSERT INTO ragf_names (id, embedding) VALUES ('lonely', ARRAY[1.0, 0.0]); -- nearest the query, in no edge
INSERT INTO ragf_names (id, embedding) VALUES ('a', ARRAY[0.0, 1.0]);
INSERT INTO ragf_names (id, embedding) VALUES ('b', ARRAY[-1.0, 0.0]);
GRAPH INSERT EDGE IN 'ragf_names' FROM 'a' TO 'b' TYPE 'hop';
GRAPH RAG FUSION ON ragf_names QUERY ARRAY[1.0, 0.0] VECTOR_FIELD 'embedding'
VECTOR_TOP_K 2 EXPANSION_DEPTH 1 EDGE_LABEL 'hop' FINAL_TOP_K 10;{"results":[{"node_id":"a","rrf_score":0.0325,"vector_rank":1,"vector_distance":1.0,"hop_distance":0},
{"node_id":"1","rrf_score":0.0164,"vector_rank":0,"vector_distance":0.0,"hop_distance":null},
{"node_id":"b","rrf_score":0.0161,"vector_rank":null,"vector_distance":null,"hop_distance":1}], ...}These statements run through the in-process pgwire test server. Output is from the tests' own messages, with scores shortened.
Sibling shapes, checked on d1e957217:
| Shape | Result |
|---|---|
Predicate UPDATE, predicate DELETE, UPDATE ... RETURNING, and non-key UPDATE on two collections, in a block with an edge write |
XX000 |
Autocommit INSERT with a SYNC AFTER body (UPDATE or DELETE) or a BEFORE body (UPDATE) |
XX000 |
CALL of a procedure whose body is a predicate DELETE, or a predicate UPDATE and a DELETE, on an edge-bearing collection |
XX000, shown as data plane error: Internal { detail: ... } |
UPDATE ... FROM, MERGE matched UPDATE and DELETE, in a block |
Commit |
Autocommit MERGE ... DELETE into an edge-bearing collection |
Commits |
Predicate UPDATE and DELETE on a KV, columnar or vector-primary collection, in a block with an edge write |
Commit |
An ASYNC AFTER trigger body with a predicate UPDATE |
Applies |
Expected behavior
A. COMMIT succeeds. The block's predicate writes apply together with its edge writes. The INSERT with a trigger body commits with the body's write.
B. Every search response names a row by its primary key. The hit in no edge returns "lonely".
Actual behavior
A. COMMIT fails and nothing is applied. The participant refuses the plan, and the autocommit path is the only caller that injects the matched set.
B. The hit returns its surrogate decimal as node_id.
What actually happened? (check all that are true)
- Acknowledged/committed data was lost, corrupted, or silently wrong
- The server crashed, hung, or failed to start
- A security or isolation boundary was crossed
- Core functionality is broken with no acceptable workaround
- A workaround exists (rewrite the query, avoid one path, etc.)
The fourth box is defect A: the block cannot commit, and running its statements one by one gives up atomicity. The fifth is defect B: run SEARCH ... USING VECTOR and GRAPH NEIGHBORS, and join in the client.
Proposed severity
A: SEV-2 — High: major functionality broken; stored data intact. B: SEV-3 — Medium: feature wrong, but operational and a workaround exists.
Reproducibility
Always — every attempt
Last known-good version / commit (if a regression)
Unknown. Neither defect is a regression as far as checked.
Environment & logs
Linux x86_64, debug build through cargo test (in-process pgwire server, trust mode, fresh data directory). Probe file, SQL scripts and test output are in the evidence repository above.
The SQL above also ran against a real server binary over pgwire (psycopg2) on d1e957217, with the same results. Not run: hand-typed psql sessions.
Before submitting
- I searched existing issues and this is not a duplicate. Related: Calvin infra aborts are reported to clients as serialization conflicts #266 (closed, Calvin infra aborts reported as serialization conflicts) and feat(array): SQL relations, bulk load, region aggregates, narrow types and SRIDs; fix SQL output, redaction, audit and Calvin refusals #422 (carries the stage refusal to the client). Defect B was reported as graph: GRAPH RAG FUSION returns a surrogate number as node_id for any hit in no edge — every other search path returns the user PK #420 and closed by the reporter. This issue replaces it.
- I reproduced this on a current
mainbuild (d1e957217), not a stale local branch. - This is not a security vulnerability (those go to a private advisory).
Update 2026-10-19: more defects of the same family, fixed in the linked PR
A sweep of every path that submits to Calvin found these. Each has a test in PR #426.
| Id | Defect | Where |
|---|---|---|
| A2 | A predicate UPDATE or DELETE of the source of a cross-shard materialized sum is refused with XX000 over HTTP /v1/query, NDJSON, WebSocket RPC, native SQL and native direct ops. pgwire works |
atomic.rs and direct_ops.rs send the statement down the static Calvin path with no matched rows |
| A3 | Two predicate writes in one request: the coordinator scans the first and injects its rows into both. The request answers success and the second update changes nothing | dependent_recon.rs |
| A4 | The coordinator's transaction class drops the row images a sum balance was settled from, so the stage cannot abort a stale balance on the autocommit routes | dependent_recon.rs |
| A5 | A request with a predicate update and a second update of one sum balance answers 200 and leaves the balance off the sum (42 against 51) | settlement reads committed rows only |
| A6 | A block that writes a predicate commits on one shard from the post-images staged at statement time. A row another session deleted comes back, and SET n = n + 1 loses the other session's update |
commit/run.rs. Staging a bulk update also skipped state-transition constraints, which the single-shard commit caught and the Calvin stage did not (the apply failed and halted the scheduler) |
| B2 | Hybrid search, a subquery over it, and a vector-primary SELECT id answer the surrogate number, or NULL, on a node without the surrogate binding |
text_hybrid.rs, raw.rs, vector_search_exec.rs |
Defect B also named the surrogate on a cluster node without the binding. That is fixed in the PR as well.
Activity