This is an automated email from the ASF dual-hosted git repository.
jrgemignani pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/age.git
The following commit(s) were added to refs/heads/master by this push:
new 5a740487 Fix property access on list comprehension / predicate loop
variables (#2402)
5a740487 is described below
commit 5a74048725ac6ddbe7bc15377c3a42cb0e31f22b
Author: Muhammad Taha Naveed <[email protected]>
AuthorDate: Sat Apr 25 05:10:31 2026 +0500
Fix property access on list comprehension / predicate loop variables (#2402)
transform_column_ref_for_indirection errored with "could not find
properties for <name>" when the referenced RTE had no "properties"
column (as is the case for the unnest() RTE used by list comprehension
and the any/all/none/single predicate functions), breaking queries like
[x IN list | x.name] and any(x IN list WHERE x.n > 1).
Return NULL instead so the caller continues transforming the ColumnRef
as an agtype value and applies the indirection via agtype_access_operator.
Co-authored-by: Claude Opus 4.7 <[email protected]>
---
regress/expected/list_comprehension.out | 25 ++++++
regress/expected/predicate_functions.out | 148 +++++++++++++++++++++++++++++++
regress/sql/list_comprehension.sql | 6 ++
regress/sql/predicate_functions.sql | 84 ++++++++++++++++++
src/backend/parser/cypher_expr.c | 15 +---
5 files changed, 267 insertions(+), 11 deletions(-)
diff --git a/regress/expected/list_comprehension.out
b/regress/expected/list_comprehension.out
index e12ad621..e431c0d9 100644
--- a/regress/expected/list_comprehension.out
+++ b/regress/expected/list_comprehension.out
@@ -503,6 +503,31 @@ SELECT * FROM cypher('list_comprehension', $$ CREATE
n=()-[:edge]->() RETURN [n
[{"id": 281474976710664, "label": "", "properties": {}}::vertex, {"id":
281474976710665, "label": "", "properties": {}}::vertex]
(1 row)
+-- Property access on the list-comprehension loop variable
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [{name:'a'},
{name:'b'}] | x.name] $$) AS (result agtype);
+ result
+------------
+ ["a", "b"]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [{n:1}, {n:2},
{n:3}] WHERE x.n > 1 | x.n] $$) AS (result agtype);
+ result
+--------
+ [2, 3]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ MATCH p=()-[:edge]->() RETURN [n
IN nodes(p) | n.name] $$) AS (result agtype);
+ result
+--------------
+ [null, null]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ MATCH (u:csm_match) WITH
collect(u) AS ns RETURN [x IN ns | x.list] $$) AS (result agtype);
+ result
+-------------------------
+ [["abc", "def", "ghi"]]
+(1 row)
+
-- Multiple list comprehensions in RETURN and WITH clause
SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3]], [u IN
[1,2,3]] $$) AS (result agtype, result2 agtype);
result | result2
diff --git a/regress/expected/predicate_functions.out
b/regress/expected/predicate_functions.out
index 47226453..9ee673a4 100644
--- a/regress/expected/predicate_functions.out
+++ b/regress/expected/predicate_functions.out
@@ -351,6 +351,154 @@ $$) AS (result agtype);
"even"
(1 row)
+--
+-- Property access on the loop variable
+--
+-- any: true (2 > 1) / false (none > 5)
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN any(x IN [{n: 1}, {n: 2}] WHERE x.n > 1)
+$$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN any(x IN [{n: 1}, {n: 2}] WHERE x.n > 5)
+$$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+-- all: true (both > 0) / false (not all > 1)
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN all(x IN [{n: 1}, {n: 2}] WHERE x.n > 0)
+$$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN all(x IN [{n: 1}, {n: 2}] WHERE x.n > 1)
+$$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+-- none: true (neither > 2) / false (one matches)
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN none(x IN [{n: 1}, {n: 2}] WHERE x.n > 2)
+$$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN none(x IN [{n: 1}, {n: 2}] WHERE x.n = 1)
+$$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+-- single: true (exactly one) / false (both match)
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN single(x IN [{n: 1}, {n: 2}] WHERE x.n = 1)
+$$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN single(x IN [{n: 1}, {n: 2}] WHERE x.n > 0)
+$$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+-- Property access on vertex loop variables over a collected node list
+-- any: true ('even' exists) / false (no 'missing')
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN any(x IN ns WHERE x.name = 'even')
+$$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN any(x IN ns WHERE x.name = 'missing')
+$$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+-- all: true (all have non-empty vals) / false (not all named 'even')
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN all(x IN ns WHERE size(x.vals) > 0)
+$$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN all(x IN ns WHERE x.name = 'even')
+$$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+-- none: true (none 'missing') / false ('even' matches)
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN none(x IN ns WHERE x.name = 'missing')
+$$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN none(x IN ns WHERE x.name = 'even')
+$$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+-- single: true (only one 'odd') / false (all have non-empty vals)
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN single(x IN ns WHERE x.name = 'odd')
+$$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN single(x IN ns WHERE size(x.vals) > 0)
+$$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
--
-- Predicate functions in boolean expressions
--
diff --git a/regress/sql/list_comprehension.sql
b/regress/sql/list_comprehension.sql
index 572b2e6b..0ef07c97 100644
--- a/regress/sql/list_comprehension.sql
+++ b/regress/sql/list_comprehension.sql
@@ -123,6 +123,12 @@ SELECT * FROM cypher('list_comprehension', $$ WITH 1 AS m
RETURN [m IN [1, 2, 3]
SELECT * FROM cypher('list_comprehension', $$ WITH [m IN [1,2,3]] AS m RETURN
[m IN [1, 2, 3]], m $$) AS (result agtype, result2 agtype);
SELECT * FROM cypher('list_comprehension', $$ CREATE n=()-[:edge]->() RETURN
[n IN nodes(n)] $$) AS (u agtype);
+-- Property access on the list-comprehension loop variable
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [{name:'a'},
{name:'b'}] | x.name] $$) AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [{n:1}, {n:2},
{n:3}] WHERE x.n > 1 | x.n] $$) AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ MATCH p=()-[:edge]->() RETURN [n
IN nodes(p) | n.name] $$) AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ MATCH (u:csm_match) WITH
collect(u) AS ns RETURN [x IN ns | x.list] $$) AS (result agtype);
+
-- Multiple list comprehensions in RETURN and WITH clause
SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3]], [u IN
[1,2,3]] $$) AS (result agtype, result2 agtype);
SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3] WHERE u>1],
[u IN [1,2,3] WHERE u>2] $$) AS (result agtype, result2 agtype);
diff --git a/regress/sql/predicate_functions.sql
b/regress/sql/predicate_functions.sql
index 7466cc2a..82933f87 100644
--- a/regress/sql/predicate_functions.sql
+++ b/regress/sql/predicate_functions.sql
@@ -217,6 +217,90 @@ SELECT * FROM cypher('predicate_functions', $$
ORDER BY u.name
$$) AS (result agtype);
+--
+-- Property access on the loop variable
+--
+-- any: true (2 > 1) / false (none > 5)
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN any(x IN [{n: 1}, {n: 2}] WHERE x.n > 1)
+$$) AS (result agtype);
+
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN any(x IN [{n: 1}, {n: 2}] WHERE x.n > 5)
+$$) AS (result agtype);
+
+-- all: true (both > 0) / false (not all > 1)
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN all(x IN [{n: 1}, {n: 2}] WHERE x.n > 0)
+$$) AS (result agtype);
+
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN all(x IN [{n: 1}, {n: 2}] WHERE x.n > 1)
+$$) AS (result agtype);
+
+-- none: true (neither > 2) / false (one matches)
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN none(x IN [{n: 1}, {n: 2}] WHERE x.n > 2)
+$$) AS (result agtype);
+
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN none(x IN [{n: 1}, {n: 2}] WHERE x.n = 1)
+$$) AS (result agtype);
+
+-- single: true (exactly one) / false (both match)
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN single(x IN [{n: 1}, {n: 2}] WHERE x.n = 1)
+$$) AS (result agtype);
+
+SELECT * FROM cypher('predicate_functions', $$
+ RETURN single(x IN [{n: 1}, {n: 2}] WHERE x.n > 0)
+$$) AS (result agtype);
+
+-- Property access on vertex loop variables over a collected node list
+-- any: true ('even' exists) / false (no 'missing')
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN any(x IN ns WHERE x.name = 'even')
+$$) AS (result agtype);
+
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN any(x IN ns WHERE x.name = 'missing')
+$$) AS (result agtype);
+
+-- all: true (all have non-empty vals) / false (not all named 'even')
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN all(x IN ns WHERE size(x.vals) > 0)
+$$) AS (result agtype);
+
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN all(x IN ns WHERE x.name = 'even')
+$$) AS (result agtype);
+
+-- none: true (none 'missing') / false ('even' matches)
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN none(x IN ns WHERE x.name = 'missing')
+$$) AS (result agtype);
+
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN none(x IN ns WHERE x.name = 'even')
+$$) AS (result agtype);
+
+-- single: true (only one 'odd') / false (all have non-empty vals)
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN single(x IN ns WHERE x.name = 'odd')
+$$) AS (result agtype);
+
+SELECT * FROM cypher('predicate_functions', $$
+ MATCH (u) WITH collect(u) AS ns
+ RETURN single(x IN ns WHERE size(x.vals) > 0)
+$$) AS (result agtype);
+
--
-- Predicate functions in boolean expressions
--
diff --git a/src/backend/parser/cypher_expr.c b/src/backend/parser/cypher_expr.c
index fc0335de..1ed77748 100644
--- a/src/backend/parser/cypher_expr.c
+++ b/src/backend/parser/cypher_expr.c
@@ -1326,21 +1326,14 @@ static Node
*transform_column_ref_for_indirection(cypher_parsestate *cpstate,
}
/* find the properties column of the NSI and return a var for it */
- node = scanNSItemForColumn(pstate, pnsi, levels_up, "properties",
+ node = scanNSItemForColumn(pstate, pnsi, levels_up, "properties",
cr->location);
/*
- * Error out if we couldn't find it.
- *
- * TODO: Should we error out or return NULL for further processing?
- * For now, just error out.
+ * If there's no "properties" column, continue transforming the
+ * ColumnRef as an agtype value and try to apply the indirection via
+ * agtype_access_operator.
*/
- if (!node)
- {
- ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("could not find properties for %s", relname)));
- }
-
return node;
}