Skip to content

fix(query): route named-graph SPARQL DELETE/INSERT WHERE to the target graph (and make MODIFY durable) - #368

Open
teipsum wants to merge 2 commits into
GrafeoDB:mainfrom
teipsum:fix-named-graph-modify
Open

fix(query): route named-graph SPARQL DELETE/INSERT WHERE to the target graph (and make MODIFY durable)#368
teipsum wants to merge 2 commits into
GrafeoDB:mainfrom
teipsum:fix-named-graph-modify

Conversation

@teipsum

@teipsum teipsum commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #367.

What was wrong

Named-graph SPARQL Update worked for the concrete-term forms (INSERT DATA,
DELETE DATA) and DROP GRAPH, but the pattern / MODIFY path ignored the
target graph entirely. The result was a set of silent failures (no error, no
warning):

  • a GRAPH-wrapped INSERT ... WHERE mis-wrote into the default graph
    (data corruption);
  • a GRAPH-wrapped DELETE WHERE was a silent no-op;
  • a WITH <g> MODIFY with an unqualified WHERE bound zero rows and no-oped;
  • a DELETE/INSERT ... WHERE MODIFY emitted no WAL record, so it was lost
    on wal-directory replay (even on the default graph).

See the linked issue for runnable repros.

Root cause (paths/lines at main, 4ebae02)

The concrete-term operators already resolve the per-graph sub-store via the
existing RdfStore::graph / graph_or_create API; the pattern/MODIFY family
did not. Specifically:

  • Translator dropped the GRAPH block.
    SparqlTranslator::extract_triples_from_pattern
    (crates/grafeo-engine/src/query/translators/sparql.rs:487) matched only
    Basic / Group and returned Vec::new() for NamedGraph, so a
    GRAPH-wrapped DELETE WHERE produced an empty template; DeleteTripleOp.graph
    was hard-coded None (sparql.rs:469).
  • Pattern executors used the bare default store.
    RdfInsertPatternOperator (rdf/mod.rs:1850) and RdfDeletePatternOperator
    (rdf/mod.rs:2134) called store.insert / store.remove on the default
    store and logged graph: None to WAL/CDC
    (rdf/mod.rs:1861/1873, 2145/2157).
  • MODIFY executor ignored template.graph.
    RdfModifyOperator::next (rdf/mod.rs:2688) deleted via store.remove
    (:2720) and inserted via store.insert (:2793) on the default store, and
    the struct had no WAL field at all.
  • Read side never redirected for WITH.
    SparqlTranslator::translate_modify (sparql.rs:498) built the WHERE plan
    with self.dataset = None, so an unqualified WHERE under WITH <g> scanned
    the default graph (contrast translate_select, which sets self.dataset).
  • USING was dropped at dispatch (sparql.rs:334, using_clauses: _).

The fix

Two logical commits in one PR.

Commit 1 — routing correctness

  • extract_triples_from_pattern descends into GRAPH blocks and tags each
    extracted triple with its enclosing graph; translate_delete_where sets
    DeleteTripleOp.graph accordingly.
  • RdfInsertPatternOperator, RdfDeletePatternOperator and RdfModifyOperator
    resolve the per-template named sub-store and insert/remove there, with the
    resolved graph carried into the CDC events (and the pattern operators' WAL
    records). The delete path uses the non-creating graph lookup so a
    zero-match named delete never materialises an empty sub-store.
  • translate_modify redirects the WHERE clause's default graph to the WITH
    graph before planning it, mirroring translate_select, so an unqualified
    WITH-scoped WHERE binds in the named graph. A GRAPH-wrapped WHERE still binds
    through the existing graph-context stack and is unaffected.
  • Fail closed on variants that cannot be routed: USING / USING NAMED and a
    variable graph target (GRAPH ?g) now return an explicit error instead of
    silently dropping the clause or mis-targeting.
  • Documents the transaction-handle atomicity contract on
    Session::execute_sparql.

Commit 2 — MODIFY durability

RdfModifyOperator now emits InsertRdfTriple / DeleteRdfTriple WAL records
for the triples it inserts and removes, carrying the resolved per-template
graph. WAL replay already routes by the record's graph field, so this is a
writer-side-only change. The operator receives the session WAL handle through
plan_modify, the same way the pattern operators already do.

Tests

  • tests/spec/rdf/sparql/update_advanced.gtest: named-graph MODIFY oracles —
    the mis-write (named gets the triple, default stays empty), the fingerprint
    trio (named DELETE WHERE deletes, default still works, DROP GRAPH still
    works), the GRAPH-wrapped MODIFY rename, and the WITH read-side binding.
  • crates/grafeo-engine/tests/named_graph_modify.rs: the store-level
    mis-write check, the non-creating-on-zero-match-delete invariant, and the
    fail-closed USING / variable-graph guards (including the zero-binding case).
  • crates/grafeo-engine/tests/named_graph_modify_durability.rs: close/reopen
    WAL-replay durability for a named-graph and a default-graph MODIFY.

All four touched crates pass (grafeo-engine, grafeo-core,
grafeo-adapters, grafeo-spec-tests); cargo fmt and
cargo clippy --all-targets --all-features -- -D warnings are clean.

Notes / scope

  • No store-API change: the per-graph sub-store API already exists; this reuses
    it on the pattern/MODIFY path.
  • LPG/Cypher/Gremlin mutation is a separate store and is untouched.
  • The pattern/MODIFY operators remain non-transactional (unchanged,
    pre-existing). Honest caveat: happy to split this into more commits/PRs if
    preferred.

Summary by cubic

Routes named-graph SPARQL DELETE/INSERT WHERE (MODIFY) to the correct graph and makes MODIFY durable via WAL. Fixes silent no-ops, wrong-graph writes, and data loss on WAL replay.

  • Bug Fixes

    • GRAPH-wrapped deletes/inserts now target the named graph; the default graph is untouched.
    • WITH now scopes the WHERE plan to that graph; unqualified patterns bind in .
    • Pattern and MODIFY executors resolve the per-template graph; deletes use non-creating lookup; CDC/WAL include the graph.
    • Unsupported variants fail closed with clear errors: USING/USING NAMED and GRAPH ?g.
    • MODIFY emits WAL records for inserts/deletes (with graph), so changes persist across close/reopen.
  • Migration

    • Replace USING/USING NAMED in MODIFY with WITH <g> or explicit GRAPH <g>.
    • Avoid GRAPH ?g in DELETE/INSERT templates; use a concrete graph IRI.

Written for commit 417d629. Summary will update on new commits.

Review in cubic

teipsum and others added 2 commits June 11, 2026 15:45
…t graph

Named-graph SPARQL Update was silently broken: a GRAPH-wrapped or
WITH-scoped DELETE WHERE / MODIFY parsed, executed, and reported success
while changing nothing in the designated named graph, and an
INSERT-via-MODIFY silently wrote into the default graph instead of the
named one (wrong-graph data, no error).

Root cause is in the pattern/MODIFY mutation path of the RDF planner:

- the translator dropped the GRAPH block when extracting DELETE WHERE
  templates, so a GRAPH-wrapped DELETE WHERE produced an empty template
  and a no-op plan;
- RdfInsertPatternOperator, RdfDeletePatternOperator and
  RdfModifyOperator ignored the template graph and called the bare
  default-store insert/remove;
- a WITH-scoped WHERE was planned against the default graph, so an
  unqualified WHERE bound zero rows and the whole MODIFY no-oped.

The concrete-term operators already route through the per-graph
sub-store API; this change applies the same idiom to the
pattern/template family:

- extract_triples_from_pattern descends into GRAPH blocks and carries
  each triple's graph onto DeleteTripleOp;
- the pattern and MODIFY executors resolve the per-template named
  sub-store and insert/remove there, with matching CDC and WAL graph
  attribution. The delete path uses the non-creating graph lookup so a
  zero-match named delete never materialises an empty sub-store;
- translate_modify redirects the WHERE clause's default graph to the
  WITH graph before planning it, so an unqualified WITH-scoped WHERE
  binds in the named graph;
- update variants that cannot be routed fail closed: USING / USING
  NAMED and variable graph targets (GRAPH ?g) now raise an explicit
  error instead of silently dropping the clause or mis-targeting.

Adds named-graph MODIFY oracles to update_advanced.gtest and a
named_graph_modify integration test covering the mis-write, the
non-creating delete, and the fail-closed guards, and documents the
transaction-handle atomicity contract on Session::execute_sparql.

Co-Authored-By: Claude <noreply@anthropic.com>
RdfModifyOperator emitted no WAL record at all, so a DELETE/INSERT-WHERE
MODIFY was not durable even on the default graph: under wal-directory
storage the mutation was applied in memory but silently lost on the next
replay.

Emit InsertRdfTriple / DeleteRdfTriple WAL records for the triples a
MODIFY inserts and removes, carrying the resolved per-template graph so
replay re-materialises named-graph mutations into the correct sub-store
(WAL replay already routes by the record's graph field). The MODIFY
operator now receives the session WAL handle, threaded through
plan_modify the same way the pattern operators already receive it.

Adds named_graph_modify_durability, a close/reopen WAL-replay test
covering both a named-graph MODIFY and a default-graph MODIFY.

Co-Authored-By: Claude <noreply@anthropic.com>
@teipsum
teipsum requested a review from StevenBtw as a code owner June 11, 2026 20:43

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 6 files

Re-trigger cubic

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.27536% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ates/grafeo-engine/src/query/translators/sparql.rs 97.61% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 70 untouched benchmarks


Comparing teipsum:fix-named-graph-modify (417d629) with main (4ebae02)

Open in CodSpeed

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.

SPARQL named-graph DELETE/INSERT WHERE is silently broken (wrong-graph writes, no-op deletes, lost on replay)

1 participant