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]

Reply via email to