crdv7 opened a new issue, #2485:
URL: https://github.com/apache/age/issues/2485

   **Describe the bug**
   
   When the same graph entity is bound to multiple aliases in one result row,
   sequential `SET` and `REMOVE` clauses can build a later update from stale
   entity properties carried by another alias.
   
   The query can return the result of an earlier change successfully while a
   later change through another alias silently overwrites it in storage.
   
   **How are you accessing AGE (Command line, driver, etc.)?**
   
   - `psql`
   
   **What data setup do we need to do?**
   
   ```pgsql
   LOAD 'age';
   SET search_path = ag_catalog, "$user", public;
   
   SELECT create_graph('same_row_alias_set');
   
   SELECT * FROM cypher('same_row_alias_set', $$
       CREATE (:T {v: 1}),
              (:RemoveT {drop_me: 1})
   $$) AS (a agtype);
   ```
   
   **What is the necessary configuration info needed?**
   
   - No additional extension or non-default AGE configuration is required.
   
   **What is the command that caused the error?**
   
   ```pgsql
   SELECT * FROM cypher('same_row_alias_set', $$
       MATCH (n:T), (m:T)
       WHERE id(n) = id(m)
       SET n.a = 1
       SET m.b = n.a + 1
       RETURN n.a, m.a, m.b
   $$) AS (na agtype, ma agtype, mb agtype);
   ```
   
   The query returns:
   
   ```text
    na | ma | mb
   ----+----+----
    1  |    | 2
   ```
   
   Reading the stored entity afterwards:
   
   ```pgsql
   SELECT * FROM cypher('same_row_alias_set', $$
       MATCH (n:T)
       RETURN n.a, n.b
   $$) AS (a agtype, b agtype);
   ```
   
   produces:
   
   ```text
    a | b
   ---+---
      | 2
   ```
   
   Property `a`, which was written by the first `SET`, has been lost.
   
   The same loss occurs with constant right-hand sides:
   
   ```cypher
   SET n.a = 1
   SET m.b = 2
   ```
   
   The dependent expression is included above because the returned `m.b` value
   of `2` shows that `n.a + 1` was evaluated correctly. The lost update occurs
   when the second write is built from the stale properties carried by alias
   `m`.
   
   `REMOVE` exposes the same stale-properties problem:
   
   ```pgsql
   SELECT * FROM cypher('same_row_alias_set', $$
       MATCH (n:RemoveT), (m:RemoveT)
       WHERE id(n) = id(m)
       SET n.changed = true
       REMOVE m.drop_me
       RETURN m
   $$) AS (m agtype);
   ```
   
   The returned and stored entity loses `changed = true` because `REMOVE` builds
   its replacement properties from the original value carried by `m`.
   
   **Expected behavior**
   
   Both aliases identify the same graph entity. A later writable clause should
   preserve the earlier update, and every reference to that entity in the
   current result row should observe its latest state.
   
   The query should return:
   
   ```text
    na | ma | mb
   ----+----+----
    1  | 1  | 2
   ```
   
   The stored entity should contain:
   
   ```text
    a | b
   ---+---
    1 | 2
   ```
   
   For the `REMOVE` reproduction, the stored properties should be:
   
   ```text
   {"changed": true}
   ```
   
   **Environment (please complete the following information):**
   
   - AGE master commit: `801417404978823bd8732452c3f7959017584785`
   - PostgreSQL: 18.4
   - Access method: `psql`
   - OS: Linux x86-64
   
   **Additional context**
   
   ### Root cause
   
   The original `apply_update_list()` processes each writable item using the
   agtype entity value stored at that item's `entity_position` in the current
   `scanTupleSlot`.
   
   It reads `original_properties` directly from that alias-local entity value
   and constructs `altered_properties` before locating the heap tuple used for
   the physical update. That lookup only identifies the row to write; it does 
not
   refresh the properties used to build the replacement entity. Its last-update
   bookkeeping is also indexed by `entity_position`, so two different tuple
   positions are treated as independent update targets even when their entity
   values have the same graphid.
   
   For the result row in the `SET` reproduction:
   
   1. Aliases `n` and `m` initially contain separate agtype values representing
      the same stored entity.
   2. `SET n.a = 1` reads the properties carried by `n`, builds the updated
      entity, and stores that value in `n`'s tuple position.
   3. The original `update_all_paths()` updates occurrences inside paths, but
      does not replace a top-level entity value stored under another alias.
   4. Alias `m` therefore continues to carry the original properties.
   5. `SET m.b = n.a + 1` evaluates the right-hand side correctly as `2`, but
      `apply_update_list()` builds the replacement entity from `m`'s stale
      `original_properties`.
   6. The second physical update preserves `b` but overwrites the earlier
      property `a`.
   
   The returned columns come from their individual alias slots. This explains
   why the query can report the first assignment even though it has been lost
   from storage.
   
   `REMOVE` uses the same `apply_update_list()` executor path and fails for the
   same reason: it removes a property from the stale alias-local property map 
and
   writes that map back over the current entity.
   
   ### Proposed fix
   
   This issue is related to the entity-lookup performance request
   issue [#2484](https://github.com/apache/age/issues/2484)That proposal 
changes how writable clauses locate
   the current heap tuple by introducing CTID as a validated lookup hint, which
   also changes the executor path on which this correctness fix depends.
   Implementing this fix separately would require reworking the same path when
   the CTID lookup is introduced. I therefore plan to address both issues in one
   pull request and link that pull request to each issue.
   
   Cleanup:
   
   ```pgsql
   SELECT drop_graph('same_row_alias_set', true);
   ```
   


-- 
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