gregfelice opened a new issue, #2494:
URL: https://github.com/apache/age/issues/2494
**Describe the bug**
In a multi-part query, all writes can be silently discarded — not merely
invisible to later clauses, but never executed at all. The statement succeeds,
returns a plausible result, and leaves the database unchanged.
AGE's DML executes inside a `CustomScan` buried in the plan tree rather than
as a top-level `ModifyTable`. The planner is therefore free to place that
subtree on either side of a join, and several executor nodes are entitled to
skip an input entirely when the *other* input turns out to be empty. When the
skipped input is the one containing the `Cypher Create` node, the writes never
happen.
The empty input is typically the scan of the very label the query is writing
to — which is empty precisely because the node that would populate it is the
side being skipped. Chicken-and-egg, and completely silent.
Split out of #2491, which hits this as well as the separate visibility
defect tracked in #2493. This issue is only the discarded-writes half.
**How are you accessing AGE (Command line, driver, etc.)?**
Command line, `psql`.
**What data setup do we need to do?**
None beyond an empty graph.
```pgsql
LOAD 'age';
SET search_path = ag_catalog, public;
SELECT create_graph('g');
```
**What is the necessary configuration info needed?**
None. Default configuration, no extra extensions.
**What is the command that caused the error?**
```pgsql
SELECT * FROM cypher('g', $cypher$
UNWIND [1,2] AS i CREATE (:N)-[:R {id:i}]->(:N)
WITH count(*) AS ig
MATCH ()-[r:R]->() RETURN count(r)
$cypher$) AS (v agtype);
```
```
v
---
0
(1 row)
```
No error is raised. Afterwards the graph is empty — **zero nodes and zero
edges**:
```pgsql
SELECT * FROM cypher('g', $cypher$ MATCH (n) RETURN count(n) $cypher$) AS (c
agtype);
c
---
0
```
`EXPLAIN (ANALYZE)` shows the writes were never executed:
```
Aggregate (actual rows=1.00 loops=1)
-> Merge Join (actual rows=0.00 loops=1)
-> Merge Append (actual rows=0.00 loops=1)
-> Index Only Scan using _ag_label_vertex_pkey ... (actual
rows=0.00 loops=1)
-> Index Only Scan using "N_pkey" on "N" ... (actual
rows=0.00 loops=1)
-> Materialize (never executed)
-> Nested Loop (never executed)
-> Index Scan using "R_end_id_idx" on "R" r (never
executed)
-> ...
-> Custom Scan (Cypher Create) (never executed)
```
The merge join exhausts its left input, concludes the join can produce
nothing, and never pulls the right input — which is where the `CREATE` lives.
It is not specific to merge joins. The query as filed in #2491 reaches the
same outcome through a hash join, where an empty build side lets PostgreSQL
skip reading the outer relation entirely:
```
Hash Join (actual rows=0.00 loops=1)
-> Hash Join (never executed)
...
-> Custom Scan (Cypher Create) (never executed) <- the nodes
...
-> Custom Scan (Cypher Create) (never executed) <- the edges
-> Hash (actual rows=0.00 loops=1)
-> Seq Scan on "N" _age_default_alias_6 (actual rows=0.00 loops=1)
```
Two different join strategies, same failure — so this is not one unlucky
plan shape.
**What decides whether it fires**
Plan shape, not input-row count. Measured on `master` @ 80141740:
| Variant | Writes persisted? |
|---|---|
| Empty graph; later `MATCH` scans the label just written; 2 input rows | ❌
discarded |
| Same, 1 input row (`CREATE (:N)-[:R {id:1}]->(:N)`) | ❌ discarded |
| Same, non-aggregating `WITH i AS keep` instead of `WITH count(*)` | ❌
discarded |
| Same query, but the graph already contains one `:R` edge | ✅ persisted |
| #2491 as filed (three stages, fresh graph) | ❌ discarded |
| Later `MATCH` scans a *different* empty label than the one written | ✅
persisted |
| #2490 as filed (single `CREATE`, later `MATCH` with `WHERE r.id =
item.id`) | ✅ persisted |
The failing cases share one property: the scan of the label being written
back has no visible rows at execution time. Pre-existing rows in that table are
enough to make the same query behave.
**Expected behavior**
A write clause must execute for every input row it receives, regardless of
the join strategy chosen for clauses that read afterwards. Query results may be
debatable under AGE's current visibility rules (see #2493), but a `CREATE` that
the user asked for must never be silently skipped.
**Environment (please complete the following information):**
- Version: `master` @ 80141740 (1.8.0)
- PostgreSQL: 18.4 (Debian), built from source
- OS: Debian, Linux 6.12
Also reproduces on 1.7.0 — the original report in #2491 was against the
1.7.0 Docker image.
**Additional context**
Severity is worth noting: #2493 produces wrong answers, which is bad but
visible. This one silently drops committed-looking work with no error, so an
application has no way to detect that its writes did not happen short of
reading back.
Fixing it likely means preventing the planner from placing the DML subtree
where the executor may skip it, rather than patching any individual join node —
the two reproductions above already show two independent paths to the same
outcome.
Related: #2491 (original report, also covers the visibility defect), #2493
(visibility defect), #2490.
--
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]