From 852b48a3b9a293dad7bde677236a9a0021c8a993 Mon Sep 17 00:00:00 2001 From: Muhammad Taha Naveed Date: Wed, 21 Feb 2024 10:44:22 +0500 Subject: [PATCH 1/2] Implement list comprehension * Add initial code for list comprehension - Additionally, fixed a bug in nested queries. * Resolve variable scope and ambigous variable issue - For scoping the variable, remove the pnsi from namespace list after being used by the list comprehension. - To resolve the ambigous variable issue, only scan the pnsi of list_comp subquery. * Fix non agg plan node issue in list comprehension - Resolved the issue 'aggref found in non-agg plan node' when the list comprehension is used as a property constraint in MATCH and other clauses or when used in WHERE clause. Intead of adding qual in the jointree, we are now adding the qual in having node of query. e.g. MATCH (n {list: [for i in [1,2,3]]}) MATCH (n) WHERE n.list=[for i in [1,2,3]] WITH n WHERE n.list=[for i in [1,2,3]] - Removed redundant call to function tranform_cypher_clause_with_where by tranform_match_clause. * Resolve variable reuse issue in list comprehension - Also, for scoping list comprehension variable, instead of removing the pnsi of list comp from namespace, now we just mark the cols of pnsi as not visible. * Modify grammer to match opencypher list comprehension grammer - Also added initial regression tests for list comprehension * Add regression tests for bugs resolved - Includes tests for nested cases, ambigous variable issue, list comprehension variable scoping and planner node aggref issue. - Should be added more. * Resolve invalid output on multiple list comprehensions - Fix the output of multiple list comprehensions in the RETURN clause and the WHERE clause. - Added regression tests for the above and some for the previous bugs resolved. * Fix aggref found where not expected issue (#189) - Resolved the 'aggref found where not expected' error thrown when using list comprehensions in UNWIND, or as a property update expression in SET or in the comparison expressions, typecasts or string matching in WITH - WHERE clause - Added an error to be thrown when using aggregation functions in UNWIND and property update expr in SET clause - Added regression tests * cleanup: Remove dead code and formatting changes --------- Co-authored-by: John Gemignani Co-authored-by: Zainab Saad <105385638+Zainab-Saad@users.noreply.github.com> --- Makefile | 1 + regress/expected/list_comprehension.out | 585 ++++++++++++++++++++++++ regress/sql/list_comprehension.sql | 148 ++++++ sql/agtype_typecast.sql | 3 +- src/backend/nodes/cypher_outfuncs.c | 1 + src/backend/parser/cypher_clause.c | 122 ++++- src/backend/parser/cypher_expr.c | 110 ++++- src/backend/parser/cypher_gram.y | 93 ++++ src/backend/parser/cypher_item.c | 244 +++++++++- src/backend/utils/adt/agtype.c | 44 +- src/include/nodes/ag_nodes.h | 1 + src/include/nodes/cypher_nodes.h | 4 + src/include/parser/cypher_clause.h | 9 + src/include/parser/cypher_item.h | 2 + src/include/parser/cypher_parse_node.h | 1 + 15 files changed, 1343 insertions(+), 25 deletions(-) create mode 100644 regress/expected/list_comprehension.out create mode 100644 regress/sql/list_comprehension.sql diff --git a/Makefile b/Makefile index 957c9a616..ef97774bc 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,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..2ca3f75ff --- /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": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex + {"id": 281474976710659, "label": "", "properties": {"list": []}}::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": 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 + {"id": 281474976710661, "label": "", "properties": {"list": [6, 8, 10, 12]}}::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": 281474976710657, "label": "", "properties": {"list": [0, 2, 4, 6, 8, 10, 12]}}::vertex + {"id": 281474976710659, "label": "", "properties": {"list": []}}::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 0279e24d2..2904503f4 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 27d9da87a..ef0703a05 100644 --- a/src/backend/parser/cypher_clause.c +++ b/src/backend/parser/cypher_clause.c @@ -289,7 +289,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, @@ -397,7 +397,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)) { @@ -1326,6 +1333,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; @@ -1354,21 +1364,29 @@ 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, -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++, @@ -1378,6 +1396,7 @@ static Query *transform_cypher_unwind(cypher_parsestate *cpstate, query->rtable = pstate->p_rtable; query->jointree = makeFromExpr(pstate->p_joinlist, NULL); query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasAggs = pstate->p_hasAggs; assign_query_collations(pstate, query); @@ -1790,6 +1809,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); @@ -2315,8 +2347,13 @@ 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 + Assert(rtindex == 1); + if (rtindex != 1) { ereport(ERROR, @@ -2335,15 +2372,36 @@ static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate, query->rtable = pstate->p_rtable; - 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 { @@ -2354,6 +2412,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; } @@ -2378,9 +2438,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); } /* @@ -3082,7 +3140,36 @@ static void transform_match_pattern(cypher_parsestate *cpstate, Query *query, } query->rtable = cpstate->pstate.p_rtable; - 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); + } } /* @@ -5447,6 +5534,7 @@ static Query *transform_cypher_create(cypher_parsestate *cpstate, query->rtable = pstate->p_rtable; query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + query->hasAggs = pstate->p_hasAggs; return query; } @@ -6082,7 +6170,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, @@ -6104,7 +6192,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 @@ -6456,6 +6545,7 @@ static Query *transform_cypher_merge(cypher_parsestate *cpstate, query->rtable = pstate->p_rtable; 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 07e95f009..055d51d8e 100644 --- a/src/backend/parser/cypher_expr.c +++ b/src/backend/parser/cypher_expr.c @@ -92,6 +92,8 @@ static ArrayExpr *make_agtype_array_expr(List *args); static Node *transform_column_ref_for_indirection(cypher_parsestate *cpstate, ColumnRef *cr); static bool verify_common_type_coercion(Oid common_type, List *exprs); +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, @@ -168,34 +170,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: @@ -321,8 +350,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; @@ -1640,6 +1687,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: @@ -1681,8 +1731,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 0825c2065..b23252b0b 100644 --- a/src/backend/parser/cypher_gram.y +++ b/src/backend/parser/cypher_gram.y @@ -118,6 +118,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 @@ -136,6 +139,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 @@ -247,6 +253,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); + %} %% @@ -1029,6 +1042,8 @@ unwind: n = make_ag_node(cypher_unwind); n->target = res; + n->where = NULL; + n->collect = NULL; $$ = (Node *) n; } @@ -2031,6 +2046,21 @@ list: $$ = (Node *)n; } + | '[' list_comprehension ']' + { + $$ = $2; + } + ; + +mapping_expr_opt: + /* empty */ + { + $$ = NULL; + } + | '|' expr + { + $$ = $2; + } ; expr_case: @@ -2094,6 +2124,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 { @@ -3049,3 +3087,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 36dfb377b..785a9b17e 100644 --- a/src/backend/parser/cypher_item.c +++ b/src/backend/parser/cypher_item.c @@ -27,13 +27,16 @@ #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_rel_attrs(ParseState *pstate, RangeTblEntry *rte, int rtindex, int sublevels_up, int location); +bool has_a_cypher_list_comprehension_node(Node *expr); // see transformTargetEntry() TargetEntry *transform_cypher_item(cypher_parsestate *cpstate, Node *node, @@ -41,10 +44,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); @@ -52,6 +62,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) @@ -68,6 +215,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) { @@ -95,14 +243,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, -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, @@ -117,6 +299,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 33b986e16..1d9cf3464 100644 --- a/src/backend/utils/adt/agtype.c +++ b/src/backend/utils/adt/agtype.c @@ -11362,6 +11362,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; @@ -11372,13 +11373,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, @@ -11435,6 +11458,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 fad0ae917..23d683936 100644 --- a/src/include/nodes/ag_nodes.h +++ b/src/include/nodes/ag_nodes.h @@ -104,5 +104,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 79070fb02..a825764bf 100644 --- a/src/include/nodes/cypher_nodes.h +++ b/src/include/nodes/cypher_nodes.h @@ -117,6 +117,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 d8ed6765b..75dcc1c01 100644 --- a/src/include/parser/cypher_parse_node.h +++ b/src/include/parser/cypher_parse_node.h @@ -50,6 +50,7 @@ typedef struct cypher_parsestate */ bool exprHasAgg; bool p_opt_match; + bool p_list_comp; } cypher_parsestate; typedef struct errpos_ecb_state From a372b08a7df798b00eba25f8b97c3dee3384d558 Mon Sep 17 00:00:00 2001 From: Muhammad Taha Naveed Date: Fri, 5 Apr 2024 04:36:25 +0500 Subject: [PATCH 2/2] Fix shift/reduce conflict in grammar (#1719) - The grammar had a shift/reduce conflict due to the ambiguity of the `IN` keyword. This is resolved by adding generic rule and manually resolving to the correct specific rule. - Added additional test cases. --- regress/expected/list_comprehension.out | 9 ++ regress/sql/list_comprehension.sql | 4 + src/backend/parser/cypher_gram.y | 122 +++++++++++++++++------- 3 files changed, 103 insertions(+), 32 deletions(-) diff --git a/regress/expected/list_comprehension.out b/regress/expected/list_comprehension.out index 2ca3f75ff..d6d69096e 100644 --- a/regress/expected/list_comprehension.out +++ b/regress/expected/list_comprehension.out @@ -571,6 +571,15 @@ SELECT * FROM cypher('list_comprehension', $$ RETURN [i IN range(0, 10, 2) WHERE ERROR: could not find rte for i LINE 1: ...$$ RETURN [i IN range(0, 10, 2) WHERE i>5 | i^2], i $$) AS (... ^ +-- Invalid list comprehension +SELECT * FROM cypher('list_comprehension', $$ RETURN [1 IN range(0, 10, 2) WHERE 2>5] $$) AS (result agtype); +ERROR: Syntax error at or near IN +LINE 1: SELECT * FROM cypher('list_comprehension', $$ RETURN [1 IN r... + ^ +SELECT * FROM cypher('list_comprehension', $$ RETURN [1 IN range(0, 10, 2) | 1] $$) AS (result agtype); +ERROR: Syntax error at or near IN +LINE 1: SELECT * FROM cypher('list_comprehension', $$ RETURN [1 IN r... + ^ SELECT * FROM drop_graph('list_comprehension', true); NOTICE: drop cascades to 4 other objects DETAIL: drop cascades to table list_comprehension._ag_label_vertex diff --git a/regress/sql/list_comprehension.sql b/regress/sql/list_comprehension.sql index b4596e55c..6a33b6497 100644 --- a/regress/sql/list_comprehension.sql +++ b/regress/sql/list_comprehension.sql @@ -145,4 +145,8 @@ SELECT * FROM cypher('list_comprehension', $$ WITH [u IN [1,2,3]] AS u, [u IN [1 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); +-- Invalid list comprehension +SELECT * FROM cypher('list_comprehension', $$ RETURN [1 IN range(0, 10, 2) WHERE 2>5] $$) AS (result agtype); +SELECT * FROM cypher('list_comprehension', $$ RETURN [1 IN range(0, 10, 2) | 1] $$) AS (result agtype); + SELECT * FROM drop_graph('list_comprehension', true); \ No newline at end of file diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y index b23252b0b..e4c02be66 100644 --- a/src/backend/parser/cypher_gram.y +++ b/src/backend/parser/cypher_gram.y @@ -139,9 +139,6 @@ /* 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 @@ -255,7 +252,12 @@ static Node *build_comparison_expression(Node *left_grammar_node, char *opr_name, int location); // list_comprehension -static Node *build_list_comprehension_node(char *var_name, Node *expr, +static Node *verify_rule_as_list_comprehension(Node *expr, Node *expr2, + Node *where, Node *mapping_expr, + int var_loc, int expr_loc, + int where_loc, int mapping_loc); + +static Node *build_list_comprehension_node(ColumnRef *var_name, Node *expr, Node *where, Node *mapping_expr, int var_loc, int expr_loc, int where_loc,int mapping_loc); @@ -2046,20 +2048,51 @@ list: $$ = (Node *)n; } - | '[' list_comprehension ']' - { - $$ = $2; - } + | list_comprehension ; -mapping_expr_opt: - /* empty */ +/* + * This grammar rule is generic to some extent. It can + * evaluate to either IN operator or list comprehension. + * This avoids shift/reduce errors between the two rules. + */ +list_comprehension: + '[' expr IN expr ']' { - $$ = NULL; + Node *n = $2; + Node *result = NULL; + + /* + * If the first expr is a ColumnRef(variable), then the rule + * should evaluate as a list comprehension. Otherwise, it should + * evaluate as an IN operator. + */ + if (nodeTag(n) == T_ColumnRef) + { + ColumnRef *cref = (ColumnRef *)n; + result = build_list_comprehension_node(cref, $4, NULL, NULL, + @2, @4, 0, 0); + } + else + { + result = (Node *)makeSimpleA_Expr(AEXPR_IN, "=", n, $4, @3); + } + $$ = result; } - | '|' expr + | '[' expr IN expr WHERE expr ']' { - $$ = $2; + $$ = verify_rule_as_list_comprehension($2, $4, $6, NULL, + @2, @4, @6, 0); + } + | '[' expr IN expr '|' expr ']' + { + $$ = verify_rule_as_list_comprehension($2, $4, NULL, $6, + @2, @4, 0, @6); + } + | '[' expr IN expr WHERE expr '|' expr ']' + { + $$ = verify_rule_as_list_comprehension($2, $4, $6, $8, + @2, @4, @6, @8); } ; @@ -2124,14 +2157,6 @@ 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 { @@ -3088,15 +3113,57 @@ static cypher_relationship *build_VLE_relation(List *left_arg, return cr; } +// Helper function to verify that the rule is a list comprehension +static Node *verify_rule_as_list_comprehension(Node *expr, Node *expr2, + Node *where, Node *mapping_expr, + int var_loc, int expr_loc, + int where_loc, int mapping_loc) +{ + Node *result = NULL; + + /* + * If the first expression is a ColumnRef, then we can build a + * list_comprehension node. + * Else its an invalid use of IN operator. + */ + if (nodeTag(expr) == T_ColumnRef) + { + ColumnRef *cref = (ColumnRef *)expr; + result = build_list_comprehension_node(cref, expr2, where, + mapping_expr, var_loc, + expr_loc, where_loc, + mapping_loc); + } + else + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("Syntax error at or near IN"))); + } + return result; +} + /* helper function to build a list_comprehension grammar node */ -static Node *build_list_comprehension_node(char *var_name, Node *expr, +static Node *build_list_comprehension_node(ColumnRef *cref, 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; + char *var_name = NULL; + Value *val; + + // Extract name from cref + val = linitial(cref->fields); + + if (!IsA(val, String)) + { + ereport(ERROR, + (errmsg_internal("unexpected Node for cypher_clause"))); + } + + var_name = val->val.str; /* * Build the ResTarget node for the UNWIND variable var_name attached to @@ -3110,15 +3177,6 @@ static Node *build_list_comprehension_node(char *var_name, Node *expr, /* 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 */