gregfelice commented on issue #2493:
URL: https://github.com/apache/age/issues/2493#issuecomment-5169431669
Reproduced, but the numbers on current `master` differ from what you saw on
1.7.0, and the two features you identified as the trigger turn out not to be
involved.
**Environment:** `master` @ 80141740 (1.8.0), PostgreSQL 18.4, built from
source.
## Result on master
```
rows | bound | downstream
------+-------+------------
1 | 1 | 0
```
You reported `bound = 0` on 1.7.0; on master it is `1`. Expected is `3`. The
three nodes persist, as you noted.
## The `COUNT { }` predicate and `OPTIONAL MATCH` are not involved
| Variant | rows, bound |
|---|---|
| As filed | 1, 1 |
| `WHERE COUNT { ... }` removed | 1, 1 |
| `MATCH` instead of `OPTIONAL MATCH` | 1, 1 |
| Same `COUNT { }` predicate against pre-existing data (no `CREATE` in the
query) | **3, 3** ✅ |
The last row is the control: the predicate itself is fine. The only variable
that matters is whether the nodes were created earlier in the same statement.
## Root cause
A clause that reads sees only the rows written by the **first input row** of
a preceding writing clause. Everything written by input rows 2..n is invisible
for the rest of the statement.
| Setup | Rows created | Visible to the later `MATCH` | Persisted |
|---|---|---|---|
| `UNWIND range(1,1) AS i CREATE (:N {id:i})` | 1 | 1 | 1 |
| `UNWIND range(1,2) AS i CREATE (:N {id:i})` | 2 | **1** | 2 |
| `UNWIND range(1,3) AS i CREATE (:N {id:i})` | 3 | **1** | 3 |
| `UNWIND range(1,8) AS i CREATE (:N {id:i})` | 8 | **1** | 8 |
| 5 nodes pre-existing, then create 3 in-statement | 3 | **6** (5 + 1) | 8 |
| `CREATE (:N),(:N),(:N)` — three nodes, one input row | 3 | **3** ✅ | 3 |
Each row above is `... WITH count(*) AS ig MATCH (n:N) RETURN count(n)`.
The last two rows pin the granularity down: the unit is the command id, one
per **input row**, not per created row. Three nodes created from one input row
are all visible; three nodes created from three input rows yield exactly one
visible. It is not specific to `count(*)` either — `WITH min(i)`, a
non-aggregating `WITH ... LIMIT 1`, and `WITH [1] AS l UNWIND l AS x` all
behave identically.
This query is the three-input-row case, so one node of three is visible,
which is the `bound = 1`.
## Likely mechanism
`exec_cypher_create` calls `CommandCounterIncrement()` once per input row
(`src/backend/executor/cypher_create.c:251`), while
`Increment_Estate_CommandId` bumps `estate->es_snapshot->curcid` once for the
clause (`src/backend/executor/cypher_create.c:148`, macro at
`src/include/executor/cypher_utils.h:40`). `CommandCounterIncrement()` does not
update the pushed snapshot the executor scans with, so `curcid` ends up lagging
the global command id by (input rows − 1) and only the first row's tuples
satisfy `cmin < curcid`.
This is already documented in-tree — see the comment at
`src/backend/executor/cypher_utils.c:248-260`, where `entity_exists()` works
around exactly this with `Max(saved_curcid, GetCurrentCommandId(false))`.
Nothing applies that correction to ordinary `MATCH` scans.
Worth flagging for whoever picks this up: the macro's comment says hiding
writes from sub-clauses is deliberate, which is correct *within* a clause (a
`CREATE` must not see its own output) but wrong *across* a `WITH` boundary. A
fix has to tell those two cases apart rather than raise `curcid`
unconditionally.
#2491 shares this root cause, and additionally hits a second, unrelated
defect.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]