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]