Fix earlier clauses' writes being invisible to later clauses (#2493) - #2495
Open
gregfelice wants to merge 1 commit into
Open
Fix earlier clauses' writes being invisible to later clauses (#2493)#2495gregfelice wants to merge 1 commit into
gregfelice wants to merge 1 commit into
Conversation
…2493) In a multi-part query, a clause that reads saw only the rows written by the first input row of a preceding CREATE or SET. Everything written by the remaining input rows stayed invisible for the rest of the statement, so UNWIND [1, 2, 3] AS i CREATE (:v {id: i}) WITH count(*) AS ignored MATCH (n:v) RETURN count(n) returned 1 instead of 3, while all three vertices were persisted. Root cause: entities are written with the global command id (insert_entity_tuple for CREATE, the cid in update_entity_tuple for SET), and CommandCounterIncrement() advances that id once per input row. The executor's snapshot does not follow it -- CommandCounterIncrement() updates the current and secondary snapshots, not the pushed one that es_snapshot points at, and Increment_Estate_CommandId bumps curcid only once, when the clause begins. So curcid stayed one step past the command id used by the first input row, and only that row's tuples satisfied cmin < curcid. The visible unit is the command id, which covers one input row: three vertices created from a single input row were all visible, while three created from three input rows yielded one. entity_exists() already worked around this locally; nothing applied the same correction to ordinary MATCH scans. Fix: when a CREATE or SET clause reaches the end of its input, raise es_snapshot->curcid to the global command id, making everything the clause wrote visible to the clauses that read after it. Doing this at end of input rather than after each row is what preserves the existing protection against a clause seeing its own writes: by that point the subtree is exhausted, so raising curcid cannot feed a written row back into the pattern that wrote it. Max() is used because Increment_Estate_CommandId can push curcid above the global command id, and lowering it would hide tuples that are already visible. REMOVE is covered by the SET path, which it shares. DELETE already synchronizes curcid explicitly and was unaffected. Adds regression coverage to cypher_create and cypher_set for visibility of every written row, for writes from an earlier clause driving a later one, and for a write clause still not seeing its own writes. All are verified to fail without this change. Note that apache#2491, which reports the same visibility problem, additionally hits a separate defect tracked in apache#2494, where the planner can place the DML CustomScan on a side of a join the executor never pulls, so the writes are skipped entirely. That is not addressed here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2493. Also fixes the visibility half of #2491, and the underlying defect behind #2490.
Problem
In a multi-part query, a clause that reads sees only the rows written by the first input row of a preceding
CREATEorSET. Everything written by the remaining input rows is invisible for the rest of the statement.Returns
1. All three vertices are persisted.The visible unit is the command id, which covers one input row — so the symptom depends on how the write is driven, not how much it writes:
MATCHUNWIND range(1,2) AS i CREATE (:N {id:i})UNWIND range(1,8) AS i CREATE (:N {id:i})CREATE (:N),(:N),(:N)— one input rowSETbehaves the same way:MATCH (n:x) SET n.marked = true WITH count(*) AS ig MATCH (m:x) WHERE m.marked = true RETURN count(m)sees 1 of 3, though all 3 updates persist.Root cause
Entities are written with the global command id —
insert_entity_tupleforCREATE, thecidinupdate_entity_tupleforSET— andCommandCounterIncrement()advances that id once per input row.The executor's snapshot does not follow it.
CommandCounterIncrement()updates the current and secondary snapshots, not the pushed onees_snapshotpoints at, andIncrement_Estate_CommandIdbumpscurcidonly once, when the clause begins. Socurcidsits one step past the command id used by the first input row, and only that row's tuples satisfycmin < curcid.This is already documented in-tree at
src/backend/executor/cypher_utils.c:248-260, whereentity_exists()works around it locally withMax(saved_curcid, GetCurrentCommandId(false)). Nothing applied the same correction to ordinaryMATCHscans.Fix
When a
CREATEorSETclause reaches the end of its input, raisees_snapshot->curcidto the global command id:Doing this at end of input rather than after each row is what preserves the existing protection against a clause seeing its own writes: by that point the subtree is exhausted, so raising
curcidcannot feed a written row back into the pattern that wrote it.Max()becauseIncrement_Estate_CommandIdcan pushcurcidabove the global command id, and lowering it would hide tuples that are already visible.REMOVEis covered by theSETpath it shares.DELETEalready synchronizescurcidexplicitly (cypher_delete.c:356,:482) and was unaffected;MERGEbehaved correctly in the same probes.Testing
Full suite green — 42/42, PostgreSQL 18.4.
Behaviour changes, all verified:
rows=1, bound=13, 3SET3 vertices, laterMATCHon the new propertyREMOVEfrom 3, laterMATCHcounts still-markedMATCH (n:N) CREATE (:N)— must not self-feedNew regression coverage in
cypher_createandcypher_setfor visibility of every written row, for writes from an earlier clause driving a later one, and for a write clause still not seeing its own writes. Each new assertion was confirmed to fail without this change —visiblereports 1 instead of 3,edges0 instead of 6,still_marked2 instead of 0.I also ran the suite on unmodified
masterand with this change through an identical harness: the outputs are byte-identical apart from the new assertions, so nothing else moved.Out of scope
#2491 additionally hits a separate defect, filed as #2494: the planner can place the DML
CustomScanon a side of a join the executor never pulls, so the writes are skipped entirely and nothing persists. That is untouched here, and #2491 will still fail until it is fixed.