Skip to content

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

Closed

Description

@EnRaiha

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions