diff --git a/Makefile b/Makefile index b405ff61a..1a176af8e 100644 --- a/Makefile +++ b/Makefile @@ -109,6 +109,7 @@ REGRESS = scan \ graph_generation \ name_validation \ jsonb_operators \ + list_comprehension \ drop srcdir=`pwd` diff --git a/regress/expected/list_comprehension.out b/regress/expected/list_comprehension.out new file mode 100644 index 000000000..8b7a64531 --- /dev/null +++ b/regress/expected/list_comprehension.out @@ -0,0 +1,585 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +LOAD 'age'; +SET search_path TO ag_catalog; +SELECT create_graph('list_comprehension'); +NOTICE: graph "list_comprehension" has been created + create_graph +-------------- + +(1 row) + +SELECT * from cypher('list_comprehension', $$ CREATE (u {list: [0, 2, 4, 6, 8, 10, 12]}) RETURN u $$) AS (result agtype); + result +----------------------------------------------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(1 row) + +SELECT * from cypher('list_comprehension', $$ CREATE (u {list: [1, 3, 5, 7, 9, 11, 13]}) RETURN u $$) AS (result agtype); + result +----------------------------------------------------------------------------------------------- + {"id": 281474976710658, "label": "", "properties": {"list": [1, 3, 5, 7, 9, 11, 13]}}::vertex +(1 row) + +SELECT * from cypher('list_comprehension', $$ CREATE (u {list: []}) RETURN u $$) AS (result agtype); + result +-------------------------------------------------------------------------- + {"id": 281474976710659, "label": "", "properties": {"list": []}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2)] $$) AS (result agtype); + result +--------------------------------------------------------- + [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2)][2] $$) AS (result agtype); + result +-------- + 5 +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2)][1..4] $$) AS (result agtype); + result +----------- + [3, 5, 7] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0] $$) AS (result agtype); + result +-------------------- + [3, 9, 15, 21, 27] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0][2] $$) AS (result agtype); + result +-------- + 15 +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0][0..4] $$) AS (result agtype); + result +---------------- + [3, 9, 15, 21] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0 | u^2 ] $$) AS (result agtype); + result +---------------------------------- + [9.0, 81.0, 225.0, 441.0, 729.0] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0 | u^2 ][3] $$) AS (result agtype); + result +-------- + 441.0 +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0 | u^2 ][1..5] $$) AS (result agtype); + result +----------------------------- + [81.0, 225.0, 441.0, 729.0] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) | u^2 ] $$) AS (result agtype); + result +---------------------------------------------------------------------------------------------------- + [1.0, 9.0, 25.0, 49.0, 81.0, 121.0, 169.0, 225.0, 289.0, 361.0, 441.0, 529.0, 625.0, 729.0, 841.0] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) | u^2 ][0] $$) AS (result agtype); + result +-------- + 1.0 +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) | u^2 ][0..2] $$) AS (result agtype); + result +------------ + [1.0, 9.0] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN (u) $$) AS (result agtype); + result +----------------------------------------------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex + {"id": 281474976710658, "label": "", "properties": {"list": [1, 3, 5, 7, 9, 11, 13]}}::vertex + {"id": 281474976710659, "label": "", "properties": {"list": []}}::vertex +(3 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH () RETURN [i IN range(0, 20, 2) WHERE i % 3 = 0 ] $$) AS (result agtype); + result +---------------- + [0, 6, 12, 18] + [0, 6, 12, 18] + [0, 6, 12, 18] +(3 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH () RETURN [i IN range(0, 20, 2) WHERE i % 3 = 0 | i^2 ] $$) AS (result agtype); + result +--------------------------- + [0.0, 36.0, 144.0, 324.0] + [0.0, 36.0, 144.0, 324.0] + [0.0, 36.0, 144.0, 324.0] +(3 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN range(0, 20, 2) WHERE i % 3 = 0 | i^2 ] $$) AS (result agtype); + result +--------------------------- + [0.0, 36.0, 144.0, 324.0] + [0.0, 36.0, 144.0, 324.0] + [0.0, 36.0, 144.0, 324.0] +(3 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH p=() RETURN [i IN range(0, 20, 2) WHERE i % 3 = 0 | i^2 ] $$) AS (result agtype); + result +--------------------------- + [0.0, 36.0, 144.0, 324.0] + [0.0, 36.0, 144.0, 324.0] + [0.0, 36.0, 144.0, 324.0] +(3 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH p=(u) RETURN [i IN range(0, 20, 2) WHERE i % 3 = 0 | i^2 ] $$) AS (result agtype); + result +--------------------------- + [0.0, 36.0, 144.0, 324.0] + [0.0, 36.0, 144.0, 324.0] + [0.0, 36.0, 144.0, 324.0] +(3 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list] $$) AS (result agtype); + result +------------------------- + [1, 3, 5, 7, 9, 11, 13] + [] + [0, 2, 4, 6, 8, 10, 12] +(3 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list WHERE i % 3 = 0] $$) AS (result agtype); + result +------------ + [0, 6, 12] + [3, 9] +(2 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list WHERE i % 3 = 0 | i/3] $$) AS (result agtype); + result +----------- + [0, 2, 4] + [1, 3] +(2 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list WHERE i % 3 = 0 | i/3][1] $$) AS (result agtype); + result +-------- + 2 + 3 +(2 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list WHERE i % 3 = 0 | i/3][0..2] $$) AS (result agtype); + result +-------- + [0, 2] + [1, 3] +(2 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list WHERE i % 3 = 0 | i/3][0..2][1] $$) AS (result agtype); + result +-------- + 2 + 3 +(2 rows) + +-- Nested cases +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3]]] $$) AS (result agtype); + result +----------- + [1, 2, 3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [i IN [1,2,3]]]] $$) AS (result agtype); + result +----------- + [1, 2, 3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1]] $$) AS (result agtype); + result +-------- + [2, 3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3]] WHERE i>1] $$) AS (result agtype); + result +-------- + [2, 3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1] WHERE i>2] $$) AS (result agtype); + result +-------- + [3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1 | i^2]] $$) AS (result agtype); + result +------------ + [4.0, 9.0] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3]] WHERE i>1 | i^2] $$) AS (result agtype); + result +------------ + [4.0, 9.0] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1] WHERE i>2 | i^2] $$) AS (result agtype); + result +-------- + [9.0] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1 | i^2] WHERE i>4] $$) AS (result agtype); + result +-------- + [9.0] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1 | i^2] WHERE i>4 | i^2] $$) AS (result agtype); + result +-------- + [81.0] +(1 row) + +-- List comprehension inside where and property constraints +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) WHERE u.list=[i IN range(0,12,2)] RETURN u $$) AS (result agtype); + result +----------------------------------------------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) WHERE u.list=[i IN range(1,13,2)] RETURN u $$) AS (result agtype); + result +----------------------------------------------------------------------------------------------- + {"id": 281474976710658, "label": "", "properties": {"list": [1, 3, 5, 7, 9, 11, 13]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) WHERE u.list=[i IN range(0,12,2) WHERE i>4] RETURN u $$) AS (result agtype); + result +-------- +(0 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) WHERE u.list=[i IN range(1,13,2) WHERE i>4 | i^1] RETURN u $$) AS (result agtype); + result +-------- +(0 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) WHERE u.list@>[i IN range(0,6,2)] RETURN u $$) AS (result agtype); + result +----------------------------------------------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) MATCH (v) WHERE v.list=[i IN u.list] RETURN v $$) AS (result agtype); + result +----------------------------------------------------------------------------------------------- + {"id": 281474976710659, "label": "", "properties": {"list": []}}::vertex + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex + {"id": 281474976710658, "label": "", "properties": {"list": [1, 3, 5, 7, 9, 11, 13]}}::vertex +(3 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list:[i IN range(0,12,2)]}) RETURN u $$) AS (result agtype); + result +----------------------------------------------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list:[i IN range(1,13,2)]}) RETURN u $$) AS (result agtype); + result +----------------------------------------------------------------------------------------------- + {"id": 281474976710658, "label": "", "properties": {"list": [1, 3, 5, 7, 9, 11, 13]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list:[i IN range(0,12,2) WHERE i>4]}) RETURN u $$) AS (result agtype); + result +----------------------------------------------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list:[i IN range(1,13,2) WHERE i>4 | i^1]}) RETURN u $$) AS (result agtype); + result +-------- +(0 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) MATCH (v {list:[i IN u.list]}) RETURN v $$) AS (result agtype); + result +----------------------------------------------------------------------------------------------- + {"id": 281474976710658, "label": "", "properties": {"list": [1, 3, 5, 7, 9, 11, 13]}}::vertex + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex + {"id": 281474976710659, "label": "", "properties": {"list": []}}::vertex + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex + {"id": 281474976710658, "label": "", "properties": {"list": [1, 3, 5, 7, 9, 11, 13]}}::vertex +(5 rows) + +SELECT * FROM cypher('list_comprehension', $$ CREATE (u {list:[i IN range(12,24,2)]}) RETURN u $$) AS (result agtype); + result +---------------------------------------------------------------------------------------------------- + {"id": 281474976710660, "label": "", "properties": {"list": [12, 14, 16, 18, 20, 22, 24]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ CREATE (u {list:[i IN range(0,12,2) WHERE i>4]}) RETURN u $$) AS (result agtype); + result +-------------------------------------------------------------------------------------- + {"id": 281474976710661, "label": "", "properties": {"list": [6, 8, 10, 12]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ CREATE (u {list:[i IN range(1,13,2) WHERE i>4 | i^2]}) RETURN u $$) AS (result agtype); + result +-------------------------------------------------------------------------------------------------------- + {"id": 281474976710662, "label": "", "properties": {"list": [25.0, 49.0, 81.0, 121.0, 169.0]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ WITH [u IN [1,2,3]] AS a CREATE (u {list:a}) RETURN u $$) AS (result agtype); + result +--------------------------------------------------------------------------------- + {"id": 281474976710663, "label": "", "properties": {"list": [1, 2, 3]}}::vertex +(1 row) + +-- List comprehension in the WITH WHERE clause +SELECT * FROM cypher('list_comprehension', $$ MATCH(u) WITH u WHERE u.list = [u IN [0, 2, 4, 6, 8, 10, 12]] RETURN u $$) AS (u agtype); + u +----------------------------------------------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ MATCH(u) WITH u WHERE u.list = [u IN [0, 2, 4, 6, 8, 10, 12]] OR size(u.list) = 0 RETURN u $$) AS (u agtype); + u +----------------------------------------------------------------------------------------------- + {"id": 281474976710659, "label": "", "properties": {"list": []}}::vertex + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(2 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH(u) WITH u WHERE u.list = [u IN [0, 2, 4, 6, 8, 10, 12]] OR size(u.list)::bool RETURN u $$) AS (u agtype); + u +-------------------------------------------------------------------------------------------------------- + {"id": 281474976710658, "label": "", "properties": {"list": [1, 3, 5, 7, 9, 11, 13]}}::vertex + {"id": 281474976710661, "label": "", "properties": {"list": [6, 8, 10, 12]}}::vertex + {"id": 281474976710660, "label": "", "properties": {"list": [12, 14, 16, 18, 20, 22, 24]}}::vertex + {"id": 281474976710662, "label": "", "properties": {"list": [25.0, 49.0, 81.0, 121.0, 169.0]}}::vertex + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex + {"id": 281474976710663, "label": "", "properties": {"list": [1, 2, 3]}}::vertex +(6 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH(u) WITH u WHERE u.list = [u IN [0, 2, 4, 6, 8, 10, 12]] OR NOT size(u.list)::bool RETURN u $$) AS (u agtype); + u +----------------------------------------------------------------------------------------------- + {"id": 281474976710659, "label": "", "properties": {"list": []}}::vertex + {"id": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(2 rows) + +SELECT * FROM cypher('list_comprehension', $$ CREATE(u:csm_match {list: ['abc', 'def', 'ghi']}) $$) AS (u agtype); + u +--- +(0 rows) + +SELECT * FROM cypher('list_comprehension', $$ MATCH(u: csm_match) WITH u WHERE [u IN u.list][0] STARTS WITH "ab" RETURN u $$) AS (u agtype); + u +------------------------------------------------------------------------------------------------------ + {"id": 844424930131969, "label": "csm_match", "properties": {"list": ["abc", "def", "ghi"]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ WITH [1, 2, 3] AS u WHERE u = [u IN [1, 2, 3]] RETURN u $$) AS (u agtype); + u +----------- + [1, 2, 3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ WITH 1 AS u WHERE u = [u IN [1, 2, 3]][0] RETURN u $$) AS (u agtype); + u +--- + 1 +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ WITH ['abc', 'defgh'] AS u WHERE [v In u][1] STARTS WITH 'de' RETURN u $$) AS (u agtype); + u +------------------ + ["abc", "defgh"] +(1 row) + +-- List comprehension in UNWIND +SELECT * FROM cypher('list_comprehension', $$ UNWIND [u IN [1, 2, 3]] AS u RETURN u $$) AS (u agtype); + u +--- + 1 + 2 + 3 +(3 rows) + +SELECT * FROM cypher('list_comprehension', $$ UNWIND [u IN [1, 2, 3] WHERE u > 1 | u*2] AS u RETURN u $$) AS (u agtype); + u +--- + 4 + 6 +(2 rows) + +-- invalid use of aggregation in UNWIND +SELECT * FROM cypher('list_comprehension', $$ WITH [1, 2, 3] AS u UNWIND collect(u) AS v RETURN v $$) AS (u agtype); +ERROR: Invalid use of aggregation in this context +LINE 1: ...ist_comprehension', $$ WITH [1, 2, 3] AS u UNWIND collect(u)... + ^ +-- List comprehension in SET +SELECT * FROM cypher('list_comprehension', $$ MATCH(u {list: [0, 2, 4, 6, 8, 10, 12]}) SET u.a = [u IN range(0, 5)] RETURN u $$) AS (u agtype); + u +------------------------------------------------------------------------------------------------------------------------ + {"id": 281474976710657, "label": "", "properties": {"a": [0, 1, 2, 3, 4, 5], "list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ MATCH(u {list: [0, 2, 4, 6, 8, 10, 12]}) SET u.a = [u IN []] RETURN u $$) AS (u agtype); + u +-------------------------------------------------------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {"a": [], "list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ MATCH(u {list: [0, 2, 4, 6, 8, 10, 12]}) SET u += {b: [u IN range(0, 5)]} RETURN u $$) AS (u agtype); + u +--------------------------------------------------------------------------------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {"a": [], "b": [0, 1, 2, 3, 4, 5], "list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ MATCH(u {list: [0, 2, 4, 6, 8, 10, 12]}) WITh u, collect(u.list) AS v SET u += {b: [u IN range(0, 5)]} SET u.c = [u IN v[0]] RETURN u $$) AS (u agtype); + u +--------------------------------------------------------------------------------------------------------------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {"a": [], "b": [0, 1, 2, 3, 4, 5], "c": [0, 2, 4, 6, 8, 10, 12], "list": [0, 2, 4, 6, 8, 10, 12]}}::vertex +(1 row) + +-- invalid use of aggregation in SET +SELECT * FROM cypher('list_comprehension', $$ MATCH(u {list: [0, 2, 4, 6, 8, 10, 12]}) SET u.c = collect(u.list) RETURN u $$) AS (u agtype); +ERROR: Invalid use of aggregation in this context +LINE 1: ..., $$ MATCH(u {list: [0, 2, 4, 6, 8, 10, 12]}) SET u.c = coll... + ^ +-- Known issue +SELECT * FROM cypher('list_comprehension', $$ MERGE (u {list:[i IN [1,2,3]]}) RETURN u $$) AS (result agtype); +ERROR: Aggref found in non-Agg plan node +-- List comprehension variable scoping +SELECT * FROM cypher('list_comprehension', $$ WITH 1 AS m, [m IN [1, 2, 3]] AS n RETURN [m IN [1, 2, 3]] $$) AS (result agtype); + result +----------- + [1, 2, 3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ WITH 1 AS m RETURN [m IN [1, 2, 3]], m $$) AS (result agtype, result2 agtype); + result | result2 +-----------+--------- + [1, 2, 3] | 1 +(1 row) + +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); + result | result2 +-----------+----------- + [1, 2, 3] | [1, 2, 3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ CREATE n=()-[:edge]->() RETURN [n IN nodes(n)] $$) AS (u agtype); + u +---------------------------------------------------------------------------------------------------------------------------------- + [{"id": 281474976710664, "label": "", "properties": {}}::vertex, {"id": 281474976710665, "label": "", "properties": {}}::vertex] +(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 +-----------+----------- + [1, 2, 3] | [1, 2, 3] +(1 row) + +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); + result | result2 +--------+--------- + [2, 3] | [3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3] WHERE u>1 | u^3], [u IN [1,2,3] WHERE u>2 | u^3] $$) AS (result agtype, result2 agtype); + result | result2 +-------------+--------- + [8.0, 27.0] | [27.0] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3]] AS u, [u IN [1,2,3]] AS i $$) AS (result agtype, result2 agtype); + result | result2 +-----------+----------- + [1, 2, 3] | [1, 2, 3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3] WHERE u>1] AS u, [u IN [1,2,3] WHERE u>2] AS i $$) AS (result agtype, result2 agtype); + result | result2 +--------+--------- + [2, 3] | [3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3] WHERE u>1 | u^3] AS u, [u IN [1,2,3] WHERE u>2 | u^3] AS i $$) AS (result agtype, result2 agtype); + result | result2 +-------------+--------- + [8.0, 27.0] | [27.0] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [u IN [1,2,3]]], [u IN [u IN [1,2,3]]] $$) AS (result agtype, result2 agtype); + result | result2 +-----------+----------- + [1, 2, 3] | [1, 2, 3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [u IN [1,2,3] WHERE u>1] WHERE u>2], [u IN [u IN [1,2,3] WHERE u>1] WHERE u>2] $$) AS (result agtype, result2 agtype); + result | result2 +--------+--------- + [3] | [3] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [u IN [1,2,3] WHERE u>1] WHERE u>2 | u^3], [u IN [u IN [1,2,3] WHERE u>1] WHERE u>2 | u^3] $$) AS (result agtype, result2 agtype); + result | result2 +--------+--------- + [27.0] | [27.0] +(1 row) + +SELECT * FROM cypher('list_comprehension', $$ WITH [u IN [1,2,3]] AS u, [u IN [1,2,3]] AS i RETURN u, i $$) AS (result agtype, result2 agtype); + result | result2 +-----------+----------- + [1, 2, 3] | [1, 2, 3] +(1 row) + +-- Should error out +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN range(0, 10, 2)],i $$) AS (result agtype, i agtype); +ERROR: could not find rte for i +LINE 1: ..._comprehension', $$ RETURN [i IN range(0, 10, 2)],i $$) AS (... + ^ +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN range(0, 10, 2) WHERE i>5 | i^2], i $$) AS (result agtype, i agtype); +ERROR: could not find rte for i +LINE 1: ...$$ RETURN [i IN range(0, 10, 2) WHERE i>5 | i^2], i $$) AS (... + ^ +SELECT * FROM drop_graph('list_comprehension', true); +NOTICE: drop cascades to 4 other objects +DETAIL: drop cascades to table list_comprehension._ag_label_vertex +drop cascades to table list_comprehension._ag_label_edge +drop cascades to table list_comprehension.csm_match +drop cascades to table list_comprehension.edge +NOTICE: graph "list_comprehension" has been dropped + drop_graph +------------ + +(1 row) + diff --git a/regress/sql/list_comprehension.sql b/regress/sql/list_comprehension.sql new file mode 100644 index 000000000..b4596e55c --- /dev/null +++ b/regress/sql/list_comprehension.sql @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +LOAD 'age'; +SET search_path TO ag_catalog; + +SELECT create_graph('list_comprehension'); + +SELECT * from cypher('list_comprehension', $$ CREATE (u {list: [0, 2, 4, 6, 8, 10, 12]}) RETURN u $$) AS (result agtype); +SELECT * from cypher('list_comprehension', $$ CREATE (u {list: [1, 3, 5, 7, 9, 11, 13]}) RETURN u $$) AS (result agtype); +SELECT * from cypher('list_comprehension', $$ CREATE (u {list: []}) RETURN u $$) AS (result agtype); + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2)] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2)][2] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2)][1..4] $$) AS (result agtype); + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0][2] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0][0..4] $$) AS (result agtype); + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0 | u^2 ] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0 | u^2 ][3] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) WHERE u % 3 = 0 | u^2 ][1..5] $$) AS (result agtype); + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) | u^2 ] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) | u^2 ][0] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN range(1, 30, 2) | u^2 ][0..2] $$) AS (result agtype); + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN (u) $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH () RETURN [i IN range(0, 20, 2) WHERE i % 3 = 0 ] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH () RETURN [i IN range(0, 20, 2) WHERE i % 3 = 0 | i^2 ] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN range(0, 20, 2) WHERE i % 3 = 0 | i^2 ] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH p=() RETURN [i IN range(0, 20, 2) WHERE i % 3 = 0 | i^2 ] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH p=(u) RETURN [i IN range(0, 20, 2) WHERE i % 3 = 0 | i^2 ] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list WHERE i % 3 = 0] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list WHERE i % 3 = 0 | i/3] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list WHERE i % 3 = 0 | i/3][1] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list WHERE i % 3 = 0 | i/3][0..2] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) RETURN [i IN u.list WHERE i % 3 = 0 | i/3][0..2][1] $$) AS (result agtype); + +-- Nested cases +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3]]] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [i IN [1,2,3]]]] $$) AS (result agtype); + +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1]] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3]] WHERE i>1] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1] WHERE i>2] $$) AS (result agtype); + +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1 | i^2]] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3]] WHERE i>1 | i^2] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1] WHERE i>2 | i^2] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1 | i^2] WHERE i>4] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN [i IN [1,2,3] WHERE i>1 | i^2] WHERE i>4 | i^2] $$) AS (result agtype); + +-- List comprehension inside where and property constraints +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) WHERE u.list=[i IN range(0,12,2)] RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) WHERE u.list=[i IN range(1,13,2)] RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) WHERE u.list=[i IN range(0,12,2) WHERE i>4] RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) WHERE u.list=[i IN range(1,13,2) WHERE i>4 | i^1] RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) WHERE u.list@>[i IN range(0,6,2)] RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) MATCH (v) WHERE v.list=[i IN u.list] RETURN v $$) AS (result agtype); + +SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list:[i IN range(0,12,2)]}) RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list:[i IN range(1,13,2)]}) RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list:[i IN range(0,12,2) WHERE i>4]}) RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list:[i IN range(1,13,2) WHERE i>4 | i^1]}) RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH (u) MATCH (v {list:[i IN u.list]}) RETURN v $$) AS (result agtype); + +SELECT * FROM cypher('list_comprehension', $$ CREATE (u {list:[i IN range(12,24,2)]}) RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ CREATE (u {list:[i IN range(0,12,2) WHERE i>4]}) RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ CREATE (u {list:[i IN range(1,13,2) WHERE i>4 | i^2]}) RETURN u $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ WITH [u IN [1,2,3]] AS a CREATE (u {list:a}) RETURN u $$) AS (result agtype); + +-- List comprehension in the WITH WHERE clause +SELECT * FROM cypher('list_comprehension', $$ MATCH(u) WITH u WHERE u.list = [u IN [0, 2, 4, 6, 8, 10, 12]] RETURN u $$) AS (u agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH(u) WITH u WHERE u.list = [u IN [0, 2, 4, 6, 8, 10, 12]] OR size(u.list) = 0 RETURN u $$) AS (u agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH(u) WITH u WHERE u.list = [u IN [0, 2, 4, 6, 8, 10, 12]] OR size(u.list)::bool RETURN u $$) AS (u agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH(u) WITH u WHERE u.list = [u IN [0, 2, 4, 6, 8, 10, 12]] OR NOT size(u.list)::bool RETURN u $$) AS (u agtype); + +SELECT * FROM cypher('list_comprehension', $$ CREATE(u:csm_match {list: ['abc', 'def', 'ghi']}) $$) AS (u agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH(u: csm_match) WITH u WHERE [u IN u.list][0] STARTS WITH "ab" RETURN u $$) AS (u agtype); + +SELECT * FROM cypher('list_comprehension', $$ WITH [1, 2, 3] AS u WHERE u = [u IN [1, 2, 3]] RETURN u $$) AS (u agtype); +SELECT * FROM cypher('list_comprehension', $$ WITH 1 AS u WHERE u = [u IN [1, 2, 3]][0] RETURN u $$) AS (u agtype); +SELECT * FROM cypher('list_comprehension', $$ WITH ['abc', 'defgh'] AS u WHERE [v In u][1] STARTS WITH 'de' RETURN u $$) AS (u agtype); + +-- List comprehension in UNWIND +SELECT * FROM cypher('list_comprehension', $$ UNWIND [u IN [1, 2, 3]] AS u RETURN u $$) AS (u agtype); +SELECT * FROM cypher('list_comprehension', $$ UNWIND [u IN [1, 2, 3] WHERE u > 1 | u*2] AS u RETURN u $$) AS (u agtype); + +-- invalid use of aggregation in UNWIND +SELECT * FROM cypher('list_comprehension', $$ WITH [1, 2, 3] AS u UNWIND collect(u) AS v RETURN v $$) AS (u agtype); + +-- List comprehension in SET +SELECT * FROM cypher('list_comprehension', $$ MATCH(u {list: [0, 2, 4, 6, 8, 10, 12]}) SET u.a = [u IN range(0, 5)] RETURN u $$) AS (u agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH(u {list: [0, 2, 4, 6, 8, 10, 12]}) SET u.a = [u IN []] RETURN u $$) AS (u agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH(u {list: [0, 2, 4, 6, 8, 10, 12]}) SET u += {b: [u IN range(0, 5)]} RETURN u $$) AS (u agtype); +SELECT * FROM cypher('list_comprehension', $$ MATCH(u {list: [0, 2, 4, 6, 8, 10, 12]}) WITh u, collect(u.list) AS v SET u += {b: [u IN range(0, 5)]} SET u.c = [u IN v[0]] RETURN u $$) AS (u agtype); + +-- invalid use of aggregation in SET +SELECT * FROM cypher('list_comprehension', $$ MATCH(u {list: [0, 2, 4, 6, 8, 10, 12]}) SET u.c = collect(u.list) RETURN u $$) AS (u agtype); + +-- Known issue +SELECT * FROM cypher('list_comprehension', $$ MERGE (u {list:[i IN [1,2,3]]}) RETURN u $$) AS (result agtype); + +-- List comprehension variable scoping +SELECT * FROM cypher('list_comprehension', $$ WITH 1 AS m, [m IN [1, 2, 3]] AS n RETURN [m IN [1, 2, 3]] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ WITH 1 AS m RETURN [m IN [1, 2, 3]], m $$) AS (result agtype, result2 agtype); +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); + +-- 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); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3] WHERE u>1 | u^3], [u IN [1,2,3] WHERE u>2 | u^3] $$) AS (result agtype, result2 agtype); + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3]] AS u, [u IN [1,2,3]] AS i $$) AS (result agtype, result2 agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3] WHERE u>1] AS u, [u IN [1,2,3] WHERE u>2] AS i $$) AS (result agtype, result2 agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [1,2,3] WHERE u>1 | u^3] AS u, [u IN [1,2,3] WHERE u>2 | u^3] AS i $$) AS (result agtype, result2 agtype); + +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [u IN [1,2,3]]], [u IN [u IN [1,2,3]]] $$) AS (result agtype, result2 agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [u IN [1,2,3] WHERE u>1] WHERE u>2], [u IN [u IN [1,2,3] WHERE u>1] WHERE u>2] $$) AS (result agtype, result2 agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [u IN [u IN [1,2,3] WHERE u>1] WHERE u>2 | u^3], [u IN [u IN [1,2,3] WHERE u>1] WHERE u>2 | u^3] $$) AS (result agtype, result2 agtype); + +SELECT * FROM cypher('list_comprehension', $$ WITH [u IN [1,2,3]] AS u, [u IN [1,2,3]] AS i RETURN u, i $$) AS (result agtype, result2 agtype); + +-- Should error out +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN range(0, 10, 2)],i $$) AS (result agtype, i agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN range(0, 10, 2) WHERE i>5 | i^2], i $$) AS (result agtype, i agtype); + +SELECT * FROM drop_graph('list_comprehension', true); \ No newline at end of file diff --git a/sql/agtype_typecast.sql b/sql/agtype_typecast.sql index 08e27fb33..5326182fb 100644 --- a/sql/agtype_typecast.sql +++ b/sql/agtype_typecast.sql @@ -181,7 +181,8 @@ CREATE FUNCTION ag_catalog.age_range(variadic "any") PARALLEL SAFE AS 'MODULE_PATHNAME'; -CREATE FUNCTION ag_catalog.age_unnest(agtype) +CREATE FUNCTION ag_catalog.age_unnest(agtype, + list_comprehension boolean = false) RETURNS SETOF agtype LANGUAGE c IMMUTABLE diff --git a/src/backend/nodes/cypher_outfuncs.c b/src/backend/nodes/cypher_outfuncs.c index 0cf8e8158..a7547286c 100644 --- a/src/backend/nodes/cypher_outfuncs.c +++ b/src/backend/nodes/cypher_outfuncs.c @@ -174,6 +174,7 @@ void out_cypher_unwind(StringInfo str, const ExtensibleNode *node) DEFINE_AG_NODE(cypher_unwind); WRITE_NODE_FIELD(target); + WRITE_NODE_FIELD(collect); } // serialization function for the cypher_delete ExtensibleNode. diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c index c164e194d..5d95b229e 100644 --- a/src/backend/parser/cypher_clause.c +++ b/src/backend/parser/cypher_clause.c @@ -282,7 +282,7 @@ static Query *transform_cypher_call_subquery(cypher_parsestate *cpstate, #define transform_prev_cypher_clause(cpstate, prev_clause, add_rte_to_query) \ transform_cypher_clause_as_subquery(cpstate, transform_cypher_clause, \ prev_clause, NULL, add_rte_to_query) -static ParseNamespaceItem +ParseNamespaceItem *transform_cypher_clause_as_subquery(cypher_parsestate *cpstate, transform_method transform, cypher_clause *clause, @@ -388,7 +388,14 @@ Query *transform_cypher_clause(cypher_parsestate *cpstate, } else if (is_ag_node(self, cypher_unwind)) { - result = transform_cypher_unwind(cpstate, clause); + cypher_unwind *n = (cypher_unwind *) self; + if (n->collect != NULL) + { + cpstate->p_list_comp = true; + } + result = transform_cypher_clause_with_where(cpstate, + transform_cypher_unwind, + clause, n->where); } else if (is_ag_node(self, cypher_call)) { @@ -1320,6 +1327,9 @@ static Query *transform_cypher_unwind(cypher_parsestate *cpstate, Node *funcexpr; TargetEntry *te; ParseNamespaceItem *pnsi; + bool is_list_comp = self->collect != NULL; + bool has_agg = + is_list_comp || has_a_cypher_list_comprehension_node(self->target->val); query = makeNode(Query); query->commandType = CMD_SELECT; @@ -1348,24 +1358,31 @@ static Query *transform_cypher_unwind(cypher_parsestate *cpstate, ereport(ERROR, (errcode(ERRCODE_DUPLICATE_ALIAS), errmsg("duplicate variable \"%s\"", self->target->name), - parser_errposition((ParseState *) cpstate, - target_syntax_loc))); + parser_errposition(pstate, target_syntax_loc))); } expr = transform_cypher_expr(cpstate, self->target->val, EXPR_KIND_SELECT_TARGET); + if (!has_agg && nodeTag(expr) == T_Aggref) + { + ereport(ERROR, errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Invalid use of aggregation in this context"), + parser_errposition(pstate, self->target->location)); + } + unwind = makeFuncCall(list_make1(makeString("age_unnest")), NIL, COERCE_SQL_SYNTAX, -1); old_expr_kind = pstate->p_expr_kind; pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; funcexpr = ParseFuncOrColumn(pstate, unwind->funcname, - list_make1(expr), + list_make2(expr, makeBoolConst(is_list_comp, false)), pstate->p_last_srf, unwind, false, target_syntax_loc); pstate->p_expr_kind = old_expr_kind; + pstate->p_hasAggs = has_agg; te = makeTargetEntry((Expr *) funcexpr, (AttrNumber) pstate->p_next_resno++, @@ -1376,6 +1393,7 @@ static Query *transform_cypher_unwind(cypher_parsestate *cpstate, query->rteperminfos = pstate->p_rteperminfos; query->jointree = makeFromExpr(pstate->p_joinlist, NULL); query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasAggs = pstate->p_hasAggs; assign_query_collations(pstate, query); @@ -1790,6 +1808,19 @@ cypher_update_information *transform_cypher_set_item_list( target_item = transform_cypher_item(cpstate, set_item->expr, NULL, EXPR_KIND_SELECT_TARGET, NULL, false); + + if (has_a_cypher_list_comprehension_node(set_item->expr)) + { + query->hasAggs = true; + } + + if (!query->hasAggs && nodeTag(target_item->expr) == T_Aggref) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Invalid use of aggregation in this context"), + parser_errposition(pstate, set_item->location))); + } + target_item->expr = add_volatile_wrapper(target_item->expr); query->targetList = lappend(query->targetList, target_item); @@ -2316,8 +2347,10 @@ static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate, pnsi = transform_cypher_clause_as_subquery(cpstate, transform, clause, NULL, true); + Assert(pnsi != NULL); rtindex = list_length(pstate->p_rtable); + // rte is the only RangeTblEntry in pstate if (rtindex != 1) { @@ -2338,15 +2371,36 @@ static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate, query->rtable = pstate->p_rtable; query->rteperminfos = pstate->p_rteperminfos; - if (!is_ag_node(self, cypher_match)) + where_qual = transform_cypher_expr(cpstate, where, EXPR_KIND_WHERE); + + where_qual = coerce_to_boolean(pstate, where_qual, "WHERE"); + + // check if we have a list comprehension in the where clause + if (cpstate->p_list_comp && + has_a_cypher_list_comprehension_node(where)) { - where_qual = transform_cypher_expr(cpstate, where, EXPR_KIND_WHERE); + List *groupClause = NIL; + ListCell *li; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + query->havingQual = where_qual; - where_qual = coerce_to_boolean(pstate, where_qual, "WHERE"); - } + foreach (li, ((cypher_return *)self)->items) + { + ResTarget *item = lfirst(li); - query->jointree = makeFromExpr(pstate->p_joinlist, where_qual); - assign_query_collations(pstate, query); + groupClause = lappend(groupClause, item->val); + } + query->groupClause = transform_group_clause(cpstate, groupClause, + &query->groupingSets, + &query->targetList, + query->sortClause, + EXPR_KIND_GROUP_BY); + + } + else + { + query->jointree = makeFromExpr(pstate->p_joinlist, where_qual); + } } else { @@ -2357,6 +2411,8 @@ static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate, query->hasTargetSRFs = pstate->p_hasTargetSRFs; query->hasAggs = pstate->p_hasAggs; + assign_query_collations(pstate, query); + return query; } @@ -2381,9 +2437,7 @@ static Query *transform_cypher_match(cypher_parsestate *cpstate, (Node *)r, -1); } - return transform_cypher_clause_with_where( - cpstate, transform_cypher_match_pattern, clause, - match_self->where); + return transform_cypher_match_pattern(cpstate, clause); } /* @@ -3057,7 +3111,36 @@ static void transform_match_pattern(cypher_parsestate *cpstate, Query *query, query->rtable = cpstate->pstate.p_rtable; query->rteperminfos = cpstate->pstate.p_rteperminfos; - query->jointree = makeFromExpr(cpstate->pstate.p_joinlist, (Node *)expr); + + if (cpstate->p_list_comp) + { + List *groupList = NIL; + + query->jointree = makeFromExpr(cpstate->pstate.p_joinlist, NULL); + query->havingQual = (Node *)expr; + + foreach (lc, query->targetList) + { + TargetEntry *te = lfirst(lc); + ColumnRef *cref = makeNode(ColumnRef); + + cref->fields = list_make1(makeString(te->resname)); + cref->location = exprLocation((Node *)te->expr); + + groupList = lappend(groupList, cref); + } + + query->groupClause = transform_group_clause(cpstate, groupList, + &query->groupingSets, + &query->targetList, + query->sortClause, + EXPR_KIND_GROUP_BY); + } + else + { + query->jointree = makeFromExpr(cpstate->pstate.p_joinlist, + (Node *)expr); + } } /* @@ -5307,6 +5390,7 @@ static Query *transform_cypher_create(cypher_parsestate *cpstate, query->rtable = pstate->p_rtable; query->rteperminfos = pstate->p_rteperminfos; query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + query->hasAggs = pstate->p_hasAggs; return query; } @@ -5942,7 +6026,7 @@ static Expr *cypher_create_properties(cypher_parsestate *cpstate, * This function is similar to transformFromClause() that is called with a * single RangeSubselect. */ -static ParseNamespaceItem * +ParseNamespaceItem * transform_cypher_clause_as_subquery(cypher_parsestate *cpstate, transform_method transform, cypher_clause *clause, @@ -5964,7 +6048,8 @@ transform_cypher_clause_as_subquery(cypher_parsestate *cpstate, pstate->p_expr_kind == EXPR_KIND_OTHER || pstate->p_expr_kind == EXPR_KIND_WHERE || pstate->p_expr_kind == EXPR_KIND_SELECT_TARGET || - pstate->p_expr_kind == EXPR_KIND_FROM_SUBSELECT); + pstate->p_expr_kind == EXPR_KIND_FROM_SUBSELECT || + pstate->p_expr_kind == EXPR_KIND_INSERT_TARGET); /* * As these are all sub queries, if this is just of type NONE, note it as a @@ -6317,6 +6402,7 @@ static Query *transform_cypher_merge(cypher_parsestate *cpstate, query->rtable = pstate->p_rtable; query->rteperminfos = pstate->p_rteperminfos; query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + query->hasAggs = pstate->p_hasAggs; query->hasSubLinks = pstate->p_hasSubLinks; diff --git a/src/backend/parser/cypher_expr.c b/src/backend/parser/cypher_expr.c index f110e8f53..631446586 100644 --- a/src/backend/parser/cypher_expr.c +++ b/src/backend/parser/cypher_expr.c @@ -90,6 +90,8 @@ static Node *transform_WholeRowRef(ParseState *pstate, ParseNamespaceItem *pnsi, static ArrayExpr *make_agtype_array_expr(List *args); static Node *transform_column_ref_for_indirection(cypher_parsestate *cpstate, ColumnRef *cr); +static Node *transform_cypher_list_comprehension(cypher_parsestate *cpstate, + cypher_unwind *expr); /* transform a cypher expression */ Node *transform_cypher_expr(cypher_parsestate *cpstate, Node *expr, @@ -166,34 +168,61 @@ static Node *transform_cypher_expr_recurse(cypher_parsestate *cpstate, case T_CoalesceExpr: return transform_CoalesceExpr(cpstate, (CoalesceExpr *) expr); case T_ExtensibleNode: + { if (is_ag_node(expr, cypher_bool_const)) + { return transform_cypher_bool_const(cpstate, (cypher_bool_const *)expr); + } if (is_ag_node(expr, cypher_integer_const)) + { return transform_cypher_integer_const(cpstate, (cypher_integer_const *)expr); + } if (is_ag_node(expr, cypher_param)) + { return transform_cypher_param(cpstate, (cypher_param *)expr); + } if (is_ag_node(expr, cypher_map)) + { return transform_cypher_map(cpstate, (cypher_map *)expr); + } if (is_ag_node(expr, cypher_list)) + { return transform_cypher_list(cpstate, (cypher_list *)expr); + } if (is_ag_node(expr, cypher_string_match)) + { return transform_cypher_string_match(cpstate, (cypher_string_match *)expr); + } if (is_ag_node(expr, cypher_typecast)) + { return transform_cypher_typecast(cpstate, (cypher_typecast *)expr); + } if (is_ag_node(expr, cypher_comparison_aexpr)) + { return transform_cypher_comparison_aexpr_OP(cpstate, (cypher_comparison_aexpr *)expr); + } if (is_ag_node(expr, cypher_comparison_boolexpr)) + { return transform_cypher_comparison_boolexpr(cpstate, (cypher_comparison_boolexpr *)expr); + } + if (is_ag_node(expr, cypher_unwind)) + { + return transform_cypher_list_comprehension(cpstate, + (cypher_unwind *) expr); + } + ereport(ERROR, (errmsg_internal("unrecognized ExtensibleNode: %s", ((ExtensibleNode *)expr)->extnodename))); + return NULL; + } case T_FuncCall: return transform_FuncCall(cpstate, (FuncCall *)expr); case T_SubLink: @@ -331,8 +360,26 @@ static Node *transform_ColumnRef(cypher_parsestate *cpstate, ColumnRef *cref) Assert(IsA(field1, String)); colname = strVal(field1); - /* Try to identify as an unqualified column */ - node = colNameToVar(pstate, colname, false, cref->location); + if (cpstate->p_list_comp && + (pstate->p_expr_kind == EXPR_KIND_WHERE || + pstate->p_expr_kind == EXPR_KIND_SELECT_TARGET) && + list_length(pstate->p_namespace) > 0) + { + /* + * Just scan through the last pnsi(that is for list comp) + * to find the column. + */ + node = scanNSItemForColumn(pstate, + llast(pstate->p_namespace), + 0, colname, cref->location); + } + else + { + /* Try to identify as an unqualified column */ + node = colNameToVar(pstate, colname, false, + cref->location); + } + if (node != NULL) { break; @@ -1629,6 +1676,9 @@ static Node *transform_SubLink(cypher_parsestate *cpstate, SubLink *sublink) /* Accept sublink here; caller must throw error if wanted */ break; + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_FROM_FUNCTION: case EXPR_KIND_SELECT_TARGET: case EXPR_KIND_FROM_SUBSELECT: case EXPR_KIND_WHERE: @@ -1670,8 +1720,64 @@ static Node *transform_SubLink(cypher_parsestate *cpstate, SubLink *sublink) sublink->testexpr = NULL; sublink->operName = NIL; } + else if (sublink->subLinkType == EXPR_SUBLINK || + sublink->subLinkType == ARRAY_SUBLINK) + { + /* + * Make sure the subselect delivers a single column (ignoring resjunk + * targets). + */ + if (count_nonjunk_tlist_entries(qtree->targetList) != 1) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("subquery must return only one column"), + parser_errposition(pstate, sublink->location))); + } + + /* + * EXPR and ARRAY need no test expression or combining operator. These + * fields should be null already, but make sure. + */ + sublink->testexpr = NULL; + sublink->operName = NIL; + } + else if (sublink->subLinkType == MULTIEXPR_SUBLINK) + { + /* Same as EXPR case, except no restriction on number of columns */ + sublink->testexpr = NULL; + sublink->operName = NIL; + } else elog(ERROR, "unsupported SubLink type"); return result; } + +static Node *transform_cypher_list_comprehension(cypher_parsestate *cpstate, + cypher_unwind *unwind) +{ + cypher_clause cc; + Node* expr; + ParseNamespaceItem *pnsi; + ParseState *pstate = (ParseState *)cpstate; + + cpstate->p_list_comp = true; + pstate->p_lateral_active = true; + + cc.prev = NULL; + cc.next = NULL; + cc.self = (Node *)unwind; + + pnsi = transform_cypher_clause_as_subquery(cpstate, + transform_cypher_clause, + &cc, NULL, true); + + expr = transform_cypher_expr(cpstate, unwind->collect, + EXPR_KIND_SELECT_TARGET); + + pnsi->p_cols_visible = false; + pstate->p_lateral_active = false; + + return expr; +} \ No newline at end of file diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y index a579fe76f..0720a50d6 100644 --- a/src/backend/parser/cypher_gram.y +++ b/src/backend/parser/cypher_gram.y @@ -115,6 +115,9 @@ /* UNWIND clause */ %type unwind +/* list comprehension */ +%type list_comprehension + /* SET and REMOVE clause */ %type set set_item remove remove_item %type set_item_list remove_item_list @@ -133,6 +136,9 @@ /* common */ %type where_opt +/* list comprehension optional mapping expression */ +%type mapping_expr_opt + /* pattern */ %type pattern simple_path_opt_parens simple_path %type path anonymous_path @@ -242,6 +248,13 @@ static bool is_A_Expr_a_comparison_operation(cypher_comparison_aexpr *a); static Node *build_comparison_expression(Node *left_grammar_node, Node *right_grammar_node, char *opr_name, int location); + +// list_comprehension +static Node *build_list_comprehension_node(char *var_name, Node *expr, + Node *where, Node *mapping_expr, + int var_loc, int expr_loc, + int where_loc,int mapping_loc); + %} %% @@ -940,6 +953,8 @@ unwind: n = make_ag_node(cypher_unwind); n->target = res; + n->where = NULL; + n->collect = NULL; $$ = (Node *) n; } @@ -1863,6 +1878,21 @@ list: $$ = (Node *)n; } + | '[' list_comprehension ']' + { + $$ = $2; + } + ; + +mapping_expr_opt: + /* empty */ + { + $$ = NULL; + } + | '|' expr + { + $$ = $2; + } ; expr_case: @@ -1926,6 +1956,14 @@ expr_case_default: } ; +list_comprehension: + var_name IN expr where_opt mapping_expr_opt + { + $$ = build_list_comprehension_node($1, $3, $4, $5, + @1, @3, @4, @5); + } +; + expr_var: var_name { @@ -2890,3 +2928,58 @@ static cypher_relationship *build_VLE_relation(List *left_arg, /* return the VLE relation node */ return cr; } + +/* helper function to build a list_comprehension grammar node */ +static Node *build_list_comprehension_node(char *var_name, Node *expr, + Node *where, Node *mapping_expr, + int var_loc, int expr_loc, + int where_loc, int mapping_loc) +{ + ResTarget *res = NULL; + cypher_unwind *unwind = NULL; + ColumnRef *cref = NULL; + + /* + * Build the ResTarget node for the UNWIND variable var_name attached to + * expr. + */ + res = makeNode(ResTarget); + res->name = var_name; + res->val = (Node *)expr; + res->location = expr_loc; + + /* build the UNWIND node */ + unwind = make_ag_node(cypher_unwind); + unwind->target = res; + + /* + * We need to make a ColumnRef of var_name so that it can be used as an expr + * for the where clause part of unwind. + */ + cref = makeNode(ColumnRef); + cref->fields = list_make1(makeString(var_name)); + cref->location = var_loc; + + unwind->where = where; + + /* if there is a mapping function, add its arg to collect */ + if (mapping_expr != NULL) + { + unwind->collect = make_function_expr(list_make1(makeString("collect")), + list_make1(mapping_expr), + mapping_loc); + } + /* + * Otherwise, we need to add in the ColumnRef of the variable var_name as + * the arg to collect instead. This implies that the RETURN variable is + * var_name. + */ + else + { + unwind->collect = make_function_expr(list_make1(makeString("collect")), + list_make1(cref), mapping_loc); + } + + /* return the UNWIND node */ + return (Node *)unwind; +} \ No newline at end of file diff --git a/src/backend/parser/cypher_item.c b/src/backend/parser/cypher_item.c index e88478c3d..3045d6c05 100644 --- a/src/backend/parser/cypher_item.c +++ b/src/backend/parser/cypher_item.c @@ -27,14 +27,17 @@ #include "nodes/makefuncs.h" #include "parser/parse_relation.h" #include "parser/parse_target.h" +#include "miscadmin.h" #include "parser/cypher_expr.h" #include "parser/cypher_item.h" +#include "parser/cypher_clause.h" static List *ExpandAllTables(ParseState *pstate, int location); static List *expand_pnsi_attrs(ParseState *pstate, ParseNamespaceItem *pnsi, int sublevels_up, bool require_col_privs, int location); +bool has_a_cypher_list_comprehension_node(Node *expr); // see transformTargetEntry() TargetEntry *transform_cypher_item(cypher_parsestate *cpstate, Node *node, @@ -42,10 +45,17 @@ TargetEntry *transform_cypher_item(cypher_parsestate *cpstate, Node *node, char *colname, bool resjunk) { ParseState *pstate = (ParseState *)cpstate; + bool old_p_lateral_active = pstate->p_lateral_active; + + // we want to see lateral variables + pstate->p_lateral_active = true; if (!expr) expr = transform_cypher_expr(cpstate, node, expr_kind); + // set lateral back to what it was + pstate->p_lateral_active = old_p_lateral_active; + if (!colname && !resjunk) colname = FigureColname(node); @@ -53,6 +63,143 @@ TargetEntry *transform_cypher_item(cypher_parsestate *cpstate, Node *node, colname, resjunk); } +/* + * Helper function to determine if the passed node has a list_comprehension + * node embedded in it. + */ +bool has_a_cypher_list_comprehension_node(Node *expr) +{ + // return false on NULL input + if (expr == NULL) + { + return false; + } + + // since this function recurses, it could be driven to stack overflow + check_stack_depth(); + + switch (nodeTag(expr)) + { + case T_A_Expr: + { + /* + * We need to recurse into the left and right nodes + * to check if there is an unwind node in there + */ + A_Expr *a_expr = (A_Expr *)expr; + + return (has_a_cypher_list_comprehension_node(a_expr->lexpr) || + has_a_cypher_list_comprehension_node(a_expr->rexpr)); + } + case T_BoolExpr: + { + BoolExpr *bexpr = (BoolExpr *)expr; + ListCell *lc; + + // is any of the boolean expression argument a list comprehension? + foreach(lc, bexpr->args) + { + Node *arg = lfirst(lc); + + if (has_a_cypher_list_comprehension_node(arg)) + { + return true; + } + } + break; + } + case T_A_Indirection: + { + // set expr to the object of the indirection + expr = ((A_Indirection *)expr)->arg; + + // check the object of the indirection + return has_a_cypher_list_comprehension_node(expr); + } + case T_ExtensibleNode: + { + if (is_ag_node(expr, cypher_unwind)) + { + cypher_unwind *cu = (cypher_unwind *)expr; + + // it is a list comprehension if it has a collect node + return cu->collect != NULL; + } + else if (is_ag_node(expr, cypher_map)) + { + cypher_map *map; + int i; + + map = (cypher_map *)expr; + + if (map->keyvals == NULL || map->keyvals->length == 0) + { + return false; + } + + // check each key and value for a list comprehension + for (i = 0; i < map->keyvals->length; i += 2) + { + Node *val; + + // get the value + val = (Node *)map->keyvals->elements[i + 1].ptr_value; + + // check the value + if (has_a_cypher_list_comprehension_node(val)) + { + return true; + } + } + } + else if (is_ag_node(expr, cypher_string_match)) + { + cypher_string_match *csm_match = (cypher_string_match *)expr; + + // is lhs or rhs of the string match a list comprehension? + return (has_a_cypher_list_comprehension_node(csm_match->lhs) || + has_a_cypher_list_comprehension_node(csm_match->rhs)); + } + else if (is_ag_node(expr, cypher_typecast)) + { + cypher_typecast *ctypecast = (cypher_typecast *)expr; + + // is expr being typecasted a list comprehension? + return has_a_cypher_list_comprehension_node(ctypecast->expr); + } + else if (is_ag_node(expr, cypher_comparison_aexpr)) + { + cypher_comparison_aexpr *aexpr = (cypher_comparison_aexpr *)expr; + + // is left or right argument a list comprehension? + return (has_a_cypher_list_comprehension_node(aexpr->lexpr) || + has_a_cypher_list_comprehension_node(aexpr->rexpr)); + } + else if (is_ag_node(expr, cypher_comparison_boolexpr)) + { + cypher_comparison_boolexpr *bexpr = (cypher_comparison_boolexpr *)expr; + ListCell *lc; + + // is any of the boolean expression argument a list comprehension? + foreach(lc, bexpr->args) + { + Node *arg = lfirst(lc); + + if (has_a_cypher_list_comprehension_node(arg)) + { + return true; + } + } + } + break; + } + default: + break; + } + // otherwise, return false + return false; +} + // see transformTargetList() List *transform_cypher_item_list(cypher_parsestate *cpstate, List *item_list, List **groupClause, ParseExprKind expr_kind) @@ -69,6 +216,7 @@ List *transform_cypher_item_list(cypher_parsestate *cpstate, List *item_list, { ResTarget *item = lfirst(li); TargetEntry *te; + bool has_list_comp = false; if (expand_star) { @@ -96,14 +244,48 @@ List *transform_cypher_item_list(cypher_parsestate *cpstate, List *item_list, } } } - /* clear the exprHasAgg flag to check transform for an aggregate */ + + // Check if we have a list comprehension + has_list_comp = has_a_cypher_list_comprehension_node(item->val); + + // Clear the exprHasAgg flag to check transform for an aggregate cpstate->exprHasAgg = false; - /* transform the item */ - te = transform_cypher_item(cpstate, item->val, NULL, expr_kind, - item->name, false); + if (has_list_comp && item_list->length > 1) + { + /* + * Create a subquery for the list comprehension and transform it + * as a subquery. Then expand the target list of the subquery. + * This is to avoid multiple unnest functions in the same query + * level and collect not able to distinguish correctly. + */ + ParseNamespaceItem *pnsi; + cypher_return *cr; + cypher_clause cc; + + cr = make_ag_node(cypher_return); + cr->items = list_make1(item); + + cc.prev = NULL; + cc.next = NULL; + cc.self = (Node *)cr; + + pnsi = transform_cypher_clause_as_subquery(cpstate, + transform_cypher_clause, + &cc, NULL, true); + + target_list = list_concat(target_list, + expandNSItemAttrs(&cpstate->pstate, pnsi, + 0, true, -1)); + } + else + { + // transform the item + te = transform_cypher_item(cpstate, item->val, NULL, expr_kind, + item->name, false); - target_list = lappend(target_list, te); + target_list = lappend(target_list, te); + } /* * Did the transformed item contain an aggregate function? If it didn't, @@ -118,6 +300,58 @@ List *transform_cypher_item_list(cypher_parsestate *cpstate, List *item_list, { hasAgg = true; } + + /* + * This is for a special case with list comprehension, which is embedded + * in a cypher_unwind node. We need to group the results but not expose + * the grouping expression. + */ + if (has_list_comp) + { + ParseState *pstate = &cpstate->pstate; + ParseNamespaceItem *nsitem = NULL; + RangeTblEntry *rte = NULL; + + /* + * There should be at least 2 entries in p_namespace. One for the + * variable in the reading clause and one for the variable in the + * list_comprehension expression. Otherwise, there is nothing to + * group with. + */ + if (list_length(pstate->p_namespace) > 1) + { + /* + * Get the first namespace item which should be the first + * variable from the reading clause. + */ + nsitem = lfirst(list_head(pstate->p_namespace)); + /* extract the rte */ + rte = nsitem->p_rte; + + /* + * If we have a non-null column name make a ColumnRef to it. + * Otherwise, there wasn't a variable specified in the reading + * clause. If that is the case don't. Because there isn't + * anything to group with. + */ + if (rte->eref->colnames != NULL && nsitem->p_cols_visible) + { + ColumnRef *cref = NULL; + char *colname = NULL; + + // get the name of the column (varname) + colname = strVal(lfirst(list_head(rte->eref->colnames))); + + // create the ColumnRef + cref = makeNode(ColumnRef); + cref->fields = list_make1(makeString(colname)); + cref->location = -1; + + // add the expression for grouping + group_clause = lappend(group_clause, cref); + } + } + } } /* diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c index e65cca410..d0cde4c8f 100644 --- a/src/backend/utils/adt/agtype.c +++ b/src/backend/utils/adt/agtype.c @@ -11411,6 +11411,7 @@ PG_FUNCTION_INFO_V1(age_unnest); Datum age_unnest(PG_FUNCTION_ARGS) { agtype *agtype_arg = NULL; + bool list_comprehension = false; ReturnSetInfo *rsi; Tuplestorestate *tuple_store; TupleDesc tupdesc; @@ -11421,13 +11422,35 @@ Datum age_unnest(PG_FUNCTION_ARGS) agtype_value v; agtype_iterator_token r; - // check for null + /* verify that we have the correct number of args */ + if (PG_NARGS() != 2) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid number of arguments to unnest"))); + } + + /* verify that our flags are not null */ + if (PG_ARGISNULL(1)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid unnest boolean flags passed"))); + } + + /* check for a NULL expr */ if (PG_ARGISNULL(0)) { PG_RETURN_NULL(); } + /* get our flags */ + list_comprehension = PG_GETARG_BOOL(1); + + /* get the input expression */ agtype_arg = AG_GET_ARG_AGTYPE_P(0); + + /* verify that it resolves to an array */ if (!AGT_ROOT_IS_ARRAY(agtype_arg)) { ereport(ERROR, @@ -11484,6 +11507,25 @@ Datum age_unnest(PG_FUNCTION_ARGS) } } + /* + * If this is for list_comprehension, we need to add a NULL as the last row. + * This NULL will allow empty lists (either filtered out by where, creating + * an empty list, or just a generic empty list) to be preserved. + */ + if (list_comprehension) + { + Datum values[1] = {0}; + bool nulls[1] = {true}; + + old_cxt = MemoryContextSwitchTo(tmp_cxt); + + tuplestore_puttuple(tuple_store, + heap_form_tuple(ret_tdesc, values, nulls)); + + MemoryContextSwitchTo(old_cxt); + MemoryContextReset(tmp_cxt); + } + MemoryContextDelete(tmp_cxt); rsi->setResult = tuple_store; diff --git a/src/include/nodes/ag_nodes.h b/src/include/nodes/ag_nodes.h index f74cd135d..598871899 100644 --- a/src/include/nodes/ag_nodes.h +++ b/src/include/nodes/ag_nodes.h @@ -103,5 +103,6 @@ static inline bool _is_ag_node(Node *node, const char *extnodename) } #define is_ag_node(node, type) _is_ag_node((Node *)(node), CppAsString(type)) +#define get_ag_node_tag(node) ((ag_node_tag)(((ExtensibleNode *)(node))->extnodename)) #endif diff --git a/src/include/nodes/cypher_nodes.h b/src/include/nodes/cypher_nodes.h index a3022fea1..c95e84941 100644 --- a/src/include/nodes/cypher_nodes.h +++ b/src/include/nodes/cypher_nodes.h @@ -110,6 +110,10 @@ typedef struct cypher_unwind { ExtensibleNode extensible; ResTarget *target; + + /* for list comprehension */ + Node *where; + Node *collect; } cypher_unwind; typedef struct cypher_merge diff --git a/src/include/parser/cypher_clause.h b/src/include/parser/cypher_clause.h index dd554650b..6ce76c737 100644 --- a/src/include/parser/cypher_clause.h +++ b/src/include/parser/cypher_clause.h @@ -39,4 +39,13 @@ Query *cypher_parse_sub_analyze(Node *parseTree, CommonTableExpr *parentCTE, bool locked_from_parent, bool resolve_unknowns); + +typedef Query *(*transform_method)(cypher_parsestate *cpstate, + cypher_clause *clause); + +ParseNamespaceItem *transform_cypher_clause_as_subquery(cypher_parsestate *cpstate, + transform_method transform, + cypher_clause *clause, + Alias *alias, + bool add_rte_to_query); #endif diff --git a/src/include/parser/cypher_item.h b/src/include/parser/cypher_item.h index 92b6c95f4..ba4e7e9af 100644 --- a/src/include/parser/cypher_item.h +++ b/src/include/parser/cypher_item.h @@ -26,4 +26,6 @@ TargetEntry *transform_cypher_item(cypher_parsestate *cpstate, Node *node, List *transform_cypher_item_list(cypher_parsestate *cpstate, List *item_list, List **groupClause, ParseExprKind expr_kind); +bool has_a_cypher_list_comprehension_node(Node *expr); + #endif diff --git a/src/include/parser/cypher_parse_node.h b/src/include/parser/cypher_parse_node.h index c0eb5a7d7..9f81b2d4c 100644 --- a/src/include/parser/cypher_parse_node.h +++ b/src/include/parser/cypher_parse_node.h @@ -49,6 +49,7 @@ typedef struct cypher_parsestate */ bool exprHasAgg; bool p_opt_match; + bool p_list_comp; } cypher_parsestate; typedef struct errpos_ecb_state