Skip to content

Commit 5a74048

Browse files
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 <noreply@anthropic.com>
1 parent 84e2954 commit 5a74048

5 files changed

Lines changed: 267 additions & 11 deletions

File tree

regress/expected/list_comprehension.out

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,31 @@ SELECT * FROM cypher('list_comprehension', $$ CREATE n=()-[:edge]->() RETURN [n
503503
[{"id": 281474976710664, "label": "", "properties": {}}::vertex, {"id": 281474976710665, "label": "", "properties": {}}::vertex]
504504
(1 row)
505505

506+
-- Property access on the list-comprehension loop variable
507+
SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [{name:'a'}, {name:'b'}] | x.name] $$) AS (result agtype);
508+
result
509+
------------
510+
["a", "b"]
511+
(1 row)
512+
513+
SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [{n:1}, {n:2}, {n:3}] WHERE x.n > 1 | x.n] $$) AS (result agtype);
514+
result
515+
--------
516+
[2, 3]
517+
(1 row)
518+
519+
SELECT * FROM cypher('list_comprehension', $$ MATCH p=()-[:edge]->() RETURN [n IN nodes(p) | n.name] $$) AS (result agtype);
520+
result
521+
--------------
522+
[null, null]
523+
(1 row)
524+
525+
SELECT * FROM cypher('list_comprehension', $$ MATCH (u:csm_match) WITH collect(u) AS ns RETURN [x IN ns | x.list] $$) AS (result agtype);
526+
result
527+
-------------------------
528+
[["abc", "def", "ghi"]]
529+
(1 row)
530+
506531
-- Multiple list comprehensions in RETURN and WITH clause
507532
SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3]], [u IN [1,2,3]] $$) AS (result agtype, result2 agtype);
508533
result | result2

regress/expected/predicate_functions.out

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,154 @@ $$) AS (result agtype);
351351
"even"
352352
(1 row)
353353

354+
--
355+
-- Property access on the loop variable
356+
--
357+
-- any: true (2 > 1) / false (none > 5)
358+
SELECT * FROM cypher('predicate_functions', $$
359+
RETURN any(x IN [{n: 1}, {n: 2}] WHERE x.n > 1)
360+
$$) AS (result agtype);
361+
result
362+
--------
363+
true
364+
(1 row)
365+
366+
SELECT * FROM cypher('predicate_functions', $$
367+
RETURN any(x IN [{n: 1}, {n: 2}] WHERE x.n > 5)
368+
$$) AS (result agtype);
369+
result
370+
--------
371+
false
372+
(1 row)
373+
374+
-- all: true (both > 0) / false (not all > 1)
375+
SELECT * FROM cypher('predicate_functions', $$
376+
RETURN all(x IN [{n: 1}, {n: 2}] WHERE x.n > 0)
377+
$$) AS (result agtype);
378+
result
379+
--------
380+
true
381+
(1 row)
382+
383+
SELECT * FROM cypher('predicate_functions', $$
384+
RETURN all(x IN [{n: 1}, {n: 2}] WHERE x.n > 1)
385+
$$) AS (result agtype);
386+
result
387+
--------
388+
false
389+
(1 row)
390+
391+
-- none: true (neither > 2) / false (one matches)
392+
SELECT * FROM cypher('predicate_functions', $$
393+
RETURN none(x IN [{n: 1}, {n: 2}] WHERE x.n > 2)
394+
$$) AS (result agtype);
395+
result
396+
--------
397+
true
398+
(1 row)
399+
400+
SELECT * FROM cypher('predicate_functions', $$
401+
RETURN none(x IN [{n: 1}, {n: 2}] WHERE x.n = 1)
402+
$$) AS (result agtype);
403+
result
404+
--------
405+
false
406+
(1 row)
407+
408+
-- single: true (exactly one) / false (both match)
409+
SELECT * FROM cypher('predicate_functions', $$
410+
RETURN single(x IN [{n: 1}, {n: 2}] WHERE x.n = 1)
411+
$$) AS (result agtype);
412+
result
413+
--------
414+
true
415+
(1 row)
416+
417+
SELECT * FROM cypher('predicate_functions', $$
418+
RETURN single(x IN [{n: 1}, {n: 2}] WHERE x.n > 0)
419+
$$) AS (result agtype);
420+
result
421+
--------
422+
false
423+
(1 row)
424+
425+
-- Property access on vertex loop variables over a collected node list
426+
-- any: true ('even' exists) / false (no 'missing')
427+
SELECT * FROM cypher('predicate_functions', $$
428+
MATCH (u) WITH collect(u) AS ns
429+
RETURN any(x IN ns WHERE x.name = 'even')
430+
$$) AS (result agtype);
431+
result
432+
--------
433+
true
434+
(1 row)
435+
436+
SELECT * FROM cypher('predicate_functions', $$
437+
MATCH (u) WITH collect(u) AS ns
438+
RETURN any(x IN ns WHERE x.name = 'missing')
439+
$$) AS (result agtype);
440+
result
441+
--------
442+
false
443+
(1 row)
444+
445+
-- all: true (all have non-empty vals) / false (not all named 'even')
446+
SELECT * FROM cypher('predicate_functions', $$
447+
MATCH (u) WITH collect(u) AS ns
448+
RETURN all(x IN ns WHERE size(x.vals) > 0)
449+
$$) AS (result agtype);
450+
result
451+
--------
452+
true
453+
(1 row)
454+
455+
SELECT * FROM cypher('predicate_functions', $$
456+
MATCH (u) WITH collect(u) AS ns
457+
RETURN all(x IN ns WHERE x.name = 'even')
458+
$$) AS (result agtype);
459+
result
460+
--------
461+
false
462+
(1 row)
463+
464+
-- none: true (none 'missing') / false ('even' matches)
465+
SELECT * FROM cypher('predicate_functions', $$
466+
MATCH (u) WITH collect(u) AS ns
467+
RETURN none(x IN ns WHERE x.name = 'missing')
468+
$$) AS (result agtype);
469+
result
470+
--------
471+
true
472+
(1 row)
473+
474+
SELECT * FROM cypher('predicate_functions', $$
475+
MATCH (u) WITH collect(u) AS ns
476+
RETURN none(x IN ns WHERE x.name = 'even')
477+
$$) AS (result agtype);
478+
result
479+
--------
480+
false
481+
(1 row)
482+
483+
-- single: true (only one 'odd') / false (all have non-empty vals)
484+
SELECT * FROM cypher('predicate_functions', $$
485+
MATCH (u) WITH collect(u) AS ns
486+
RETURN single(x IN ns WHERE x.name = 'odd')
487+
$$) AS (result agtype);
488+
result
489+
--------
490+
true
491+
(1 row)
492+
493+
SELECT * FROM cypher('predicate_functions', $$
494+
MATCH (u) WITH collect(u) AS ns
495+
RETURN single(x IN ns WHERE size(x.vals) > 0)
496+
$$) AS (result agtype);
497+
result
498+
--------
499+
false
500+
(1 row)
501+
354502
--
355503
-- Predicate functions in boolean expressions
356504
--

regress/sql/list_comprehension.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ SELECT * FROM cypher('list_comprehension', $$ WITH 1 AS m RETURN [m IN [1, 2, 3]
123123
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);
124124
SELECT * FROM cypher('list_comprehension', $$ CREATE n=()-[:edge]->() RETURN [n IN nodes(n)] $$) AS (u agtype);
125125

126+
-- Property access on the list-comprehension loop variable
127+
SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [{name:'a'}, {name:'b'}] | x.name] $$) AS (result agtype);
128+
SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [{n:1}, {n:2}, {n:3}] WHERE x.n > 1 | x.n] $$) AS (result agtype);
129+
SELECT * FROM cypher('list_comprehension', $$ MATCH p=()-[:edge]->() RETURN [n IN nodes(p) | n.name] $$) AS (result agtype);
130+
SELECT * FROM cypher('list_comprehension', $$ MATCH (u:csm_match) WITH collect(u) AS ns RETURN [x IN ns | x.list] $$) AS (result agtype);
131+
126132
-- Multiple list comprehensions in RETURN and WITH clause
127133
SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3]], [u IN [1,2,3]] $$) AS (result agtype, result2 agtype);
128134
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);

regress/sql/predicate_functions.sql

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,90 @@ SELECT * FROM cypher('predicate_functions', $$
217217
ORDER BY u.name
218218
$$) AS (result agtype);
219219

220+
--
221+
-- Property access on the loop variable
222+
--
223+
-- any: true (2 > 1) / false (none > 5)
224+
SELECT * FROM cypher('predicate_functions', $$
225+
RETURN any(x IN [{n: 1}, {n: 2}] WHERE x.n > 1)
226+
$$) AS (result agtype);
227+
228+
SELECT * FROM cypher('predicate_functions', $$
229+
RETURN any(x IN [{n: 1}, {n: 2}] WHERE x.n > 5)
230+
$$) AS (result agtype);
231+
232+
-- all: true (both > 0) / false (not all > 1)
233+
SELECT * FROM cypher('predicate_functions', $$
234+
RETURN all(x IN [{n: 1}, {n: 2}] WHERE x.n > 0)
235+
$$) AS (result agtype);
236+
237+
SELECT * FROM cypher('predicate_functions', $$
238+
RETURN all(x IN [{n: 1}, {n: 2}] WHERE x.n > 1)
239+
$$) AS (result agtype);
240+
241+
-- none: true (neither > 2) / false (one matches)
242+
SELECT * FROM cypher('predicate_functions', $$
243+
RETURN none(x IN [{n: 1}, {n: 2}] WHERE x.n > 2)
244+
$$) AS (result agtype);
245+
246+
SELECT * FROM cypher('predicate_functions', $$
247+
RETURN none(x IN [{n: 1}, {n: 2}] WHERE x.n = 1)
248+
$$) AS (result agtype);
249+
250+
-- single: true (exactly one) / false (both match)
251+
SELECT * FROM cypher('predicate_functions', $$
252+
RETURN single(x IN [{n: 1}, {n: 2}] WHERE x.n = 1)
253+
$$) AS (result agtype);
254+
255+
SELECT * FROM cypher('predicate_functions', $$
256+
RETURN single(x IN [{n: 1}, {n: 2}] WHERE x.n > 0)
257+
$$) AS (result agtype);
258+
259+
-- Property access on vertex loop variables over a collected node list
260+
-- any: true ('even' exists) / false (no 'missing')
261+
SELECT * FROM cypher('predicate_functions', $$
262+
MATCH (u) WITH collect(u) AS ns
263+
RETURN any(x IN ns WHERE x.name = 'even')
264+
$$) AS (result agtype);
265+
266+
SELECT * FROM cypher('predicate_functions', $$
267+
MATCH (u) WITH collect(u) AS ns
268+
RETURN any(x IN ns WHERE x.name = 'missing')
269+
$$) AS (result agtype);
270+
271+
-- all: true (all have non-empty vals) / false (not all named 'even')
272+
SELECT * FROM cypher('predicate_functions', $$
273+
MATCH (u) WITH collect(u) AS ns
274+
RETURN all(x IN ns WHERE size(x.vals) > 0)
275+
$$) AS (result agtype);
276+
277+
SELECT * FROM cypher('predicate_functions', $$
278+
MATCH (u) WITH collect(u) AS ns
279+
RETURN all(x IN ns WHERE x.name = 'even')
280+
$$) AS (result agtype);
281+
282+
-- none: true (none 'missing') / false ('even' matches)
283+
SELECT * FROM cypher('predicate_functions', $$
284+
MATCH (u) WITH collect(u) AS ns
285+
RETURN none(x IN ns WHERE x.name = 'missing')
286+
$$) AS (result agtype);
287+
288+
SELECT * FROM cypher('predicate_functions', $$
289+
MATCH (u) WITH collect(u) AS ns
290+
RETURN none(x IN ns WHERE x.name = 'even')
291+
$$) AS (result agtype);
292+
293+
-- single: true (only one 'odd') / false (all have non-empty vals)
294+
SELECT * FROM cypher('predicate_functions', $$
295+
MATCH (u) WITH collect(u) AS ns
296+
RETURN single(x IN ns WHERE x.name = 'odd')
297+
$$) AS (result agtype);
298+
299+
SELECT * FROM cypher('predicate_functions', $$
300+
MATCH (u) WITH collect(u) AS ns
301+
RETURN single(x IN ns WHERE size(x.vals) > 0)
302+
$$) AS (result agtype);
303+
220304
--
221305
-- Predicate functions in boolean expressions
222306
--

src/backend/parser/cypher_expr.c

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,21 +1326,14 @@ static Node *transform_column_ref_for_indirection(cypher_parsestate *cpstate,
13261326
}
13271327

13281328
/* find the properties column of the NSI and return a var for it */
1329-
node = scanNSItemForColumn(pstate, pnsi, levels_up, "properties",
1329+
node = scanNSItemForColumn(pstate, pnsi, levels_up, "properties",
13301330
cr->location);
13311331

13321332
/*
1333-
* Error out if we couldn't find it.
1334-
*
1335-
* TODO: Should we error out or return NULL for further processing?
1336-
* For now, just error out.
1333+
* If there's no "properties" column, continue transforming the
1334+
* ColumnRef as an agtype value and try to apply the indirection via
1335+
* agtype_access_operator.
13371336
*/
1338-
if (!node)
1339-
{
1340-
ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT),
1341-
errmsg("could not find properties for %s", relname)));
1342-
}
1343-
13441337
return node;
13451338
}
13461339

0 commit comments

Comments
 (0)