From e53a9850a09af2bf76a1c1a49db680ca79d11d0c Mon Sep 17 00:00:00 2001 From: Dehowe Feng Date: Tue, 19 Jul 2022 15:28:33 +0900 Subject: [PATCH] Implement CALL ...[YIELD] for cypher functions. Queries can now call functions using the form CALL ... YIELD. CALL ... YIELD can be used in some of the following forms: Individual: CALL CALL YIELD CALL YIELD WHERE UPDATE/RETURN Subqueries: READING_CLAUSE CALL YIELD UPDATE/RETURN CALL YIELD READING_CLAUSE UPDATE/RETURN In the future, CALL YIELD support for record returning functions and multiple variable output functions can be added. Known Issue with WHERE clause where a WHERE in a MATCH + CALL subquery does not filter results is known. --- Makefile | 1 + regress/expected/cypher_call.out | 203 ++++++++++++++++++++++ regress/sql/cypher_call.sql | 99 +++++++++++ src/backend/nodes/ag_nodes.c | 2 + src/backend/nodes/cypher_outfuncs.c | 11 ++ src/backend/parser/cypher_clause.c | 257 +++++++++++++++++++++++++++- src/backend/parser/cypher_gram.y | 120 ++++++++++++- src/include/nodes/ag_nodes.h | 2 + src/include/nodes/cypher_nodes.h | 14 ++ src/include/nodes/cypher_outfuncs.h | 4 + 10 files changed, 701 insertions(+), 12 deletions(-) create mode 100644 regress/expected/cypher_call.out create mode 100644 regress/sql/cypher_call.sql diff --git a/Makefile b/Makefile index 00063d530..f3750117e 100644 --- a/Makefile +++ b/Makefile @@ -89,6 +89,7 @@ REGRESS = scan \ cypher_with \ cypher_vle \ cypher_union \ + cypher_call \ cypher_merge \ age_load \ index \ diff --git a/regress/expected/cypher_call.out b/regress/expected/cypher_call.out new file mode 100644 index 000000000..e9f9e346e --- /dev/null +++ b/regress/expected/cypher_call.out @@ -0,0 +1,203 @@ +/* + * 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('cypher_call'); +NOTICE: graph "cypher_call" has been created + create_graph +-------------- + +(1 row) + +SELECT * FROM cypher('cypher_call', $$CREATE ({n: 'a'})$$) as (a agtype); + a +--- +(0 rows) + +SELECT * FROM cypher('cypher_call', $$CREATE ({n: 'b'})$$) as (a agtype); + a +--- +(0 rows) + +CREATE SCHEMA call_stmt_test; +CREATE FUNCTION call_stmt_test.add_agtype(agtype, agtype) RETURNS agtype + AS 'select $1 + $2;' + LANGUAGE SQL + IMMUTABLE + RETURNS NULL ON NULL INPUT; +/* + * CALL (solo) + */ +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64)$$) as (sqrt agtype); + sqrt +------ + 8.0 +(1 row) + +/* CALL RETURN, should fail */ +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) RETURN sqrt$$) as (sqrt agtype); +ERROR: Procedure call inside a query does not support naming results implicitly +LINE 2: SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) RETURN s... + ^ +HINT: Name explicitly using `YIELD` instead +/* CALL YIELD */ +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt$$) as (sqrt agtype); + sqrt +------ + 8.0 +(1 row) + +/* incorrect variable should fail */ +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD squirt$$) as (sqrt agtype); +ERROR: Unknown CALL output +LINE 2: ... FROM cypher('cypher_call', $$CALL sqrt(64) YIELD squirt$$) ... + ^ +/* qualified name */ +SELECT * FROM cypher('cypher_call', $$CALL call_stmt_test.add_agtype(1,2)$$) as (sqrt agtype); + sqrt +------ + 3 +(1 row) + +/* non-existent schema should fail */ +SELECT * FROM cypher('cypher_call', $$CALL ag_catalog.add_agtype(1,2)$$) as (sqrt agtype); +ERROR: function ag_catalog.add_agtype(agtype, agtype) does not exist +LINE 2: ...cypher('cypher_call', $$CALL ag_catalog.add_agtype(1,2)$$) a... + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +/* CALL YIELD WHERE, should fail */ +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1$$) as (sqrt agtype); +ERROR: Cannot use standalone CALL with WHERE +LINE 2: ...r('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1$$... + ^ +HINT: Instead use `CALL ... WITH * WHERE ... RETURN *` +/* + * subquery + */ + /* CALL YIELD UPDATE/RETURN */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt RETURN sqrt $$) as (sqrt agtype); + sqrt +------ + 8.0 +(1 row) + +/* Unrecognized YIELD, correct RETURN, should fail*/ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD squirt RETURN sqrt $$) as (sqrt agtype); +ERROR: Unknown CALL output +LINE 2: ...FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD squirt RET... + ^ +/* CALL YIELD WHERE RETURN */ +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 RETURN sqrt $$) as (a agtype); + a +----- + 8.0 +(1 row) + +/* MATCH CALL RETURN, should fail */ +SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) RETURN sqrt $$) as (sqrt agtype); +ERROR: Procedure call inside a query does not support naming results implicitly +LINE 2: SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(6... + ^ +HINT: Name explicitly using `YIELD` instead +/* MATCH CALL YIELD RETURN */ +SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt RETURN a, sqrt $$) as (a agtype, sqrt agtype); + a | sqrt +------------------------------------------------------------------------+------ + {"id": 281474976710657, "label": "", "properties": {"n": "a"}}::vertex | 8.0 + {"id": 281474976710658, "label": "", "properties": {"n": "b"}}::vertex | 8.0 +(2 rows) + +/* MATCH CALL YIELD WHERE UPDATE/RETURN */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 CREATE ({n:'c'}) $$) as (a agtype); + a +--- +(0 rows) + +SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE a.n = 'c' DELETE (a) $$) as (a agtype); + a +--- +(0 rows) + +SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE a.n = 'c' RETURN a $$) as (a agtype); + a +--- +(0 rows) + +/* CALL MATCH YIELD WHERE UPDATE/RETURN */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 CREATE ({n:'c'}) $$) as (a agtype); + a +--- +(0 rows) + +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE a.n = 'c' DELETE (a) $$) as (a agtype); + a +--- +(0 rows) + +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE a.n = 'c' RETURN a $$) as (a agtype); + a +--- +(0 rows) + +/* Multiple Calls: CALL YIELD CALL YIELD... RETURN */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum RETURN sqrt, agtype_sum $$) as (sqrt agtype, agtype_sum agtype); + sqrt | agtype_sum +------+------------ + 8.0 | 4 +(1 row) + +/* should fail */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL sqrt(81) YIELD sqrt RETURN sqrt, sqrt $$) as (a agtype, b agtype); +ERROR: duplicate variable "sqrt" +LINE 2: ..., $$ CALL sqrt(64) YIELD sqrt CALL sqrt(81) YIELD sqrt RETUR... + ^ +/* Aliasing: CALL YIELD AS CALL YIELD AS ... UPDATE/RETURN */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL sqrt(81) YIELD sqrt AS sqrt1 RETURN sqrt, sqrt1 $$) as (sqrt agtype, sqrt1 agtype); + sqrt | sqrt1 +------+------- + 8.0 | 9.0 +(1 row) + +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt AS sqrt1 CALL sqrt(81) YIELD sqrt RETURN sqrt, sqrt1 $$) as (sqrt agtype, sqrt1 agtype); + sqrt | sqrt1 +------+------- + 9.0 | 8.0 +(1 row) + +/* duplicated alias should fail */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt AS sqrt1 CALL sqrt(81) YIELD sqrt AS sqrt1 RETURN sqrt1, sqrt1 $$) as (a agtype, b agtype); +ERROR: duplicate variable "sqrt1" +LINE 2: ... sqrt(64) YIELD sqrt AS sqrt1 CALL sqrt(81) YIELD sqrt AS sq... + ^ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum AS sqrt RETURN sqrt, sqrt $$) as (a agtype, b agtype); +ERROR: duplicate variable "sqrt" +LINE 1: ...LL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum... + ^ +DROP SCHEMA call_stmt_test CASCADE; +NOTICE: drop cascades to function call_stmt_test.add_agtype(agtype,agtype) +SELECT drop_graph('cypher_call', true); +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table cypher_call._ag_label_vertex +drop cascades to table cypher_call._ag_label_edge +NOTICE: graph "cypher_call" has been dropped + drop_graph +------------ + +(1 row) + diff --git a/regress/sql/cypher_call.sql b/regress/sql/cypher_call.sql new file mode 100644 index 000000000..403049a10 --- /dev/null +++ b/regress/sql/cypher_call.sql @@ -0,0 +1,99 @@ +/* + * 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('cypher_call'); + +SELECT * FROM cypher('cypher_call', $$CREATE ({n: 'a'})$$) as (a agtype); +SELECT * FROM cypher('cypher_call', $$CREATE ({n: 'b'})$$) as (a agtype); + +CREATE SCHEMA call_stmt_test; + +CREATE FUNCTION call_stmt_test.add_agtype(agtype, agtype) RETURNS agtype + AS 'select $1 + $2;' + LANGUAGE SQL + IMMUTABLE + RETURNS NULL ON NULL INPUT; + +/* + * CALL (solo) + */ + +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64)$$) as (sqrt agtype); +/* CALL RETURN, should fail */ +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) RETURN sqrt$$) as (sqrt agtype); +/* CALL YIELD */ +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt$$) as (sqrt agtype); +/* incorrect variable should fail */ +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD squirt$$) as (sqrt agtype); + +/* qualified name */ +SELECT * FROM cypher('cypher_call', $$CALL call_stmt_test.add_agtype(1,2)$$) as (sqrt agtype); +/* non-existent schema should fail */ +SELECT * FROM cypher('cypher_call', $$CALL ag_catalog.add_agtype(1,2)$$) as (sqrt agtype); + +/* CALL YIELD WHERE, should fail */ +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1$$) as (sqrt agtype); + +/* + * subquery + */ + + /* CALL YIELD UPDATE/RETURN */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt RETURN sqrt $$) as (sqrt agtype); +/* Unrecognized YIELD, correct RETURN, should fail*/ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD squirt RETURN sqrt $$) as (sqrt agtype); + +/* CALL YIELD WHERE RETURN */ +SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 RETURN sqrt $$) as (a agtype); + +/* MATCH CALL RETURN, should fail */ +SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) RETURN sqrt $$) as (sqrt agtype); + +/* MATCH CALL YIELD RETURN */ +SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt RETURN a, sqrt $$) as (a agtype, sqrt agtype); + +/* MATCH CALL YIELD WHERE UPDATE/RETURN */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 CREATE ({n:'c'}) $$) as (a agtype); +SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE a.n = 'c' DELETE (a) $$) as (a agtype); +SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE a.n = 'c' RETURN a $$) as (a agtype); + +/* CALL MATCH YIELD WHERE UPDATE/RETURN */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 CREATE ({n:'c'}) $$) as (a agtype); +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE a.n = 'c' DELETE (a) $$) as (a agtype); +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE a.n = 'c' RETURN a $$) as (a agtype); + +/* Multiple Calls: CALL YIELD CALL YIELD... RETURN */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum RETURN sqrt, agtype_sum $$) as (sqrt agtype, agtype_sum agtype); +/* should fail */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL sqrt(81) YIELD sqrt RETURN sqrt, sqrt $$) as (a agtype, b agtype); + +/* Aliasing: CALL YIELD AS CALL YIELD AS ... UPDATE/RETURN */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL sqrt(81) YIELD sqrt AS sqrt1 RETURN sqrt, sqrt1 $$) as (sqrt agtype, sqrt1 agtype); +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt AS sqrt1 CALL sqrt(81) YIELD sqrt RETURN sqrt, sqrt1 $$) as (sqrt agtype, sqrt1 agtype); +/* duplicated alias should fail */ +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt AS sqrt1 CALL sqrt(81) YIELD sqrt AS sqrt1 RETURN sqrt1, sqrt1 $$) as (a agtype, b agtype); +SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum AS sqrt RETURN sqrt, sqrt $$) as (a agtype, b agtype); + +DROP SCHEMA call_stmt_test CASCADE; +SELECT drop_graph('cypher_call', true); \ No newline at end of file diff --git a/src/backend/nodes/ag_nodes.c b/src/backend/nodes/ag_nodes.c index d65bb5038..14715bd2e 100644 --- a/src/backend/nodes/ag_nodes.c +++ b/src/backend/nodes/ag_nodes.c @@ -52,6 +52,7 @@ const char *node_names[] = { "cypher_typecast", "cypher_integer_const", "cypher_sub_pattern", + "cypher_call", "cypher_create_target_nodes", "cypher_create_path", "cypher_target_node", @@ -114,6 +115,7 @@ const ExtensibleNodeMethods node_methods[] = { DEFINE_NODE_METHODS(cypher_typecast), DEFINE_NODE_METHODS(cypher_integer_const), DEFINE_NODE_METHODS(cypher_sub_pattern), + DEFINE_NODE_METHODS(cypher_call), DEFINE_NODE_METHODS_EXTENDED(cypher_create_target_nodes), DEFINE_NODE_METHODS_EXTENDED(cypher_create_path), DEFINE_NODE_METHODS_EXTENDED(cypher_target_node), diff --git a/src/backend/nodes/cypher_outfuncs.c b/src/backend/nodes/cypher_outfuncs.c index 1d84bc289..531ce2163 100644 --- a/src/backend/nodes/cypher_outfuncs.c +++ b/src/backend/nodes/cypher_outfuncs.c @@ -296,6 +296,17 @@ void out_cypher_sub_pattern(StringInfo str, const ExtensibleNode *node) WRITE_NODE_FIELD(pattern); } +// serialization function for the cypher_call ExtensibleNode. +void out_cypher_call(StringInfo str, const ExtensibleNode *node) +{ + DEFINE_AG_NODE(cypher_call); + + WRITE_NODE_FIELD(funccall); + WRITE_NODE_FIELD(funcexpr); + WRITE_NODE_FIELD(where); + WRITE_NODE_FIELD(yield_items); +} + // serialization function for the cypher_create_target_nodes ExtensibleNode. void out_cypher_create_target_nodes(StringInfo str, const ExtensibleNode *node) { diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c index e07d07a83..f6db5899c 100644 --- a/src/backend/parser/cypher_clause.c +++ b/src/backend/parser/cypher_clause.c @@ -207,6 +207,9 @@ static void handle_prev_clause(cypher_parsestate *cpstate, Query *query, cypher_clause *clause, bool first_rte); static TargetEntry *placeholder_target_entry(cypher_parsestate *cpstate, char *name); + +static List *makeTargetListFromRTE(ParseState *pstate, RangeTblEntry *rte); + static Query *transform_cypher_sub_pattern(cypher_parsestate *cpstate, cypher_clause *clause); // set and remove clause @@ -270,6 +273,12 @@ static void transform_cypher_merge_mark_tuple_position(List *target_list, cypher_create_path *path); +//call...[yield] +static Query *transform_cypher_call_stmt(cypher_parsestate *cpstate, + cypher_clause *clause); +static Query *transform_cypher_call_subquery(cypher_parsestate *cpstate, + cypher_clause *clause); + // transform #define PREV_CYPHER_CLAUSE_ALIAS "_" #define CYPHER_OPT_RIGHT_ALIAS "_R" @@ -381,6 +390,10 @@ Query *transform_cypher_clause(cypher_parsestate *cpstate, { result = transform_cypher_unwind(cpstate, clause); } + else if (is_ag_node(self, cypher_call)) + { + result = transform_cypher_call_stmt(cpstate, clause); + } else { ereport(ERROR, (errmsg_internal("unexpected Node for cypher_clause"))); @@ -1010,6 +1023,211 @@ transform_cypher_union_tree(cypher_parsestate *cpstate, cypher_clause *clause, }//end else (is not leaf) } +/* + * Function that takes a cypher call and returns the yielded result + * This function also catches some cases that should fail that could not + * be picked up by the grammar. transform_cypher_call_subquery handles the + * call transformation itself. + */ +static Query * transform_cypher_call_stmt(cypher_parsestate *cpstate, + cypher_clause *clause) +{ + ParseState *pstate = (ParseState *)cpstate; + cypher_call *self = (cypher_call *)clause->self; + + if (!clause->prev && !clause->next) /* CALL [YIELD] -- the most simple call */ + { + if (self->where) /* Error check for WHERE clause after YIELD without RETURN */ + { + Assert(self->yield_items); + + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("Cannot use standalone CALL with WHERE"), + errhint("Instead use `CALL ... WITH * WHERE ... RETURN *`"), + parser_errposition(pstate, + exprLocation((Node *) self->where)))); + } + + return transform_cypher_call_subquery(cpstate, clause); + } + else /* subqueries */ + { + if (!self->yield_items) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("Procedure call inside a query does not support naming results implicitly"), + errhint("Name explicitly using `YIELD` instead"), + parser_errposition(pstate, + exprLocation((Node *) self)))); + } + Assert(self->yield_items); + + if (!clause->next) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("Query cannot conclude with CALL"), + errhint("Must be RETURN or an update clause"), + parser_errposition(pstate, + exprLocation((Node *) self)))); + } + + return transform_cypher_clause_with_where(cpstate, + transform_cypher_call_subquery, + clause); + } + + return NULL; +} + +/* + * Helper routine for transform_cypher_call_stmt. This routine transforms the + * call statement and handles the YIELD clause. + */ +static Query *transform_cypher_call_subquery(cypher_parsestate *cpstate, + cypher_clause *clause) +{ + ParseState *pstate = (ParseState *)cpstate; + ParseState *p_child_parse_state = make_parsestate(NULL); + cypher_call *self = (cypher_call *)clause->self; + Query *query; + char *colName; + FuncExpr *node = NULL; + TargetEntry *tle; + + Expr *where_qual = NULL; + + query = makeNode(Query); + query->commandType = CMD_SELECT; + + if (clause->prev) + { + /* we want to retain all previous range table entries */ + handle_prev_clause(cpstate, query, clause->prev, false); + } + + /* transform the funccall and store it in a funcexpr node */ + node = castNode( FuncExpr, transform_cypher_expr(cpstate, (Node *) self->funccall, + EXPR_KIND_FROM_FUNCTION)); + + /* retrieve the column name from funccall */ + colName = strVal(linitial(self->funccall->funcname)); + + /* make a targetentry from the funcexpr node */ + tle = makeTargetEntry((Expr *) node, + (AttrNumber) p_child_parse_state->p_next_resno++, + colName, + false); + + if (self->yield_items) /* if there are yield items, we need to check them */ + { + List *yield_targetList; + ListCell *lc; + + yield_targetList = list_make1(tle); + + foreach (lc, self->yield_items) + { + ResTarget *target = NULL; + ColumnRef *var = NULL; + TargetEntry *yielded_tle = NULL; + + target = (ResTarget *) lfirst(lc); + + if (!IsA(target->val, ColumnRef)) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("YIELD item must be ColumnRef"), + parser_errposition(&cpstate->pstate, 0))); + } + + var = (ColumnRef *) target->val; + + /* check if the restarget variable exists in the yield_targetList*/ + if (findTarget(yield_targetList, strVal(linitial(var->fields))) != NULL) + { + /* check if an alias exists. if one does, we check if it is + already declared in the targetlist */ + if (target->name) + { + if (findTarget(query->targetList, target->name) != NULL) + { + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate variable \"%s\"", target->name), + parser_errposition((ParseState *) cpstate, exprLocation((Node *) target)))); + } + else + { + yielded_tle = makeTargetEntry((Expr *) node, + (AttrNumber) pstate->p_next_resno++, + target->name, + false); + query->targetList = lappend(query->targetList, yielded_tle); + } + } + else/* if there is no alias, we check if the variable is already declared */ + { + if (findTarget(query->targetList, strVal(linitial(var->fields))) != NULL) + { + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate variable \"%s\"", colName), + parser_errposition((ParseState *) cpstate, exprLocation((Node *) target)))); + } + else + { + yielded_tle = makeTargetEntry((Expr *) node, + (AttrNumber) pstate->p_next_resno++, + colName, + false); + query->targetList = lappend(query->targetList, yielded_tle); + } + } + } + else + { + /* if the yield_item is not found and we return an error */ + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("Unknown CALL output"), + parser_errposition(pstate, exprLocation((Node *) target)))); + } + } + } + else /* if there are no yield items this must be a solo call */ + { + tle = makeTargetEntry((Expr *) node, + (AttrNumber) pstate->p_next_resno++, + colName, + false); + query->targetList = list_make1(tle); + } + + + + markTargetListOrigins(pstate, query->targetList); + + query->rtable = cpstate->pstate.p_rtable; + query->jointree = makeFromExpr(cpstate->pstate.p_joinlist, (Node *)where_qual); + query->hasAggs = pstate->p_hasAggs; + + assign_query_collations(pstate, query); + + /* this must be done after collations, for reliable comparison of exprs */ + if (pstate->p_hasAggs || + query->groupClause || query->groupingSets || query->havingQual) + { + parse_check_aggregates(pstate, query); + } + + free_parsestate(p_child_parse_state); + + return query; +} + /* * Transform the Delete clause. Creates a _cypher_delete_clause * and passes the necessary information that is needed in the @@ -1936,8 +2154,22 @@ static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate, { ParseState *pstate = (ParseState *)cpstate; Query *query; - cypher_match *self = (cypher_match *)clause->self; - Node *where = self->where; + Node *self = clause->self; + cypher_match *match_self; + cypher_call *call_self; + Node *where; + + + if (is_ag_node(self, cypher_call)) + { + call_self = (cypher_call*) clause->self; + where = call_self->where; + } + else + { + match_self = (cypher_match*) clause->self; + where = match_self->where; + } if (where) { @@ -1957,8 +2189,27 @@ static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate, markTargetListOrigins(pstate, query->targetList); query->rtable = pstate->p_rtable; - query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + if (is_ag_node(clause, cypher_call)) + { + cypher_call *call = (cypher_call *)clause->self; + + if (call->where != NULL) + { + Expr *where_qual = NULL; + + where_qual = (Expr *)transform_cypher_expr(cpstate, call->where, + EXPR_KIND_WHERE); + + where_qual = (Expr *)coerce_to_boolean(pstate, (Node *)where_qual, + "WHERE"); + query->jointree = makeFromExpr(pstate->p_joinlist, (Node *)where_qual); + } + } + else + { + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + } assign_query_collations(pstate, query); } else diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y index b751cdbe6..84eb957a1 100644 --- a/src/backend/parser/cypher_gram.y +++ b/src/backend/parser/cypher_gram.y @@ -331,17 +331,98 @@ cypher_stmt: call_stmt: CALL expr_func_norm { - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("CALL not supported yet"), - ag_scanner_errposition(@1, scanner))); + cypher_call *n = make_ag_node(cypher_call); + n->funccall = castNode (FuncCall, $2); + + $$ = (Node *)n; + } + | CALL expr '.' expr + { + cypher_call *n = make_ag_node(cypher_call); + + if (IsA($4, FuncCall) && IsA($2, ColumnRef)) + { + FuncCall *fc = (FuncCall*)$4; + ColumnRef *cr = (ColumnRef*)$2; + List *fields = cr->fields; + Value *string = linitial(fields); + + /* + * A function can only be qualified with a single schema. So, we + * check to see that the function isn't already qualified. There + * may be unforeseen cases where we might need to remove this in + * the future. + */ + if (list_length(fc->funcname) == 1) + { + fc->funcname = lcons(string, fc->funcname); + $$ = (Node*)fc; + } + else + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("function already qualified"), + ag_scanner_errposition(@1, scanner))); + + n->funccall = fc; + $$ = (Node *)n; + } + else + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("CALL statement must be a qualified function"), + ag_scanner_errposition(@1, scanner))); + } } | CALL expr_func_norm YIELD yield_item_list where_opt { - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("CALL... [YIELD] not supported yet"), - ag_scanner_errposition(@1, scanner))); + cypher_call *n = make_ag_node(cypher_call); + n->funccall = castNode (FuncCall, $2); + n->yield_items = $4; + n->where = $5; + $$ = (Node *)n; + } + | CALL expr '.' expr YIELD yield_item_list where_opt + { + cypher_call *n = make_ag_node(cypher_call); + + if (IsA($4, FuncCall) && IsA($2, ColumnRef)) + { + FuncCall *fc = (FuncCall*)$4; + ColumnRef *cr = (ColumnRef*)$2; + List *fields = cr->fields; + Value *string = linitial(fields); + + /* + * A function can only be qualified with a single schema. So, we + * check to see that the function isn't already qualified. There + * may be unforeseen cases where we might need to remove this in + * the future. + */ + if (list_length(fc->funcname) == 1) + { + fc->funcname = lcons(string, fc->funcname); + $$ = (Node*)fc; + } + else + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("function already qualified"), + ag_scanner_errposition(@1, scanner))); + + n->funccall = fc; + n->yield_items = $6; + n->where = $7; + $$ = (Node *)n; + } + else + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("CALL statement must be a qualified function"), + ag_scanner_errposition(@1, scanner))); + } } ; @@ -359,14 +440,31 @@ yield_item_list: yield_item: expr AS var_name { + ResTarget *n; + n = makeNode(ResTarget); + n->name = $3; + n->indirection = NIL; + n->val = $1; + n->location = @1; + + $$ = (Node *)n; } | expr { + ResTarget *n; + n = makeNode(ResTarget); + n->name = NULL; + n->indirection = NIL; + n->val = $1; + n->location = @1; + + $$ = (Node *)n; } ; + semicolon_opt: /* empty */ | ';' @@ -419,6 +517,10 @@ query_part_last: { $$ = lappend(list_concat($1, $2), $3); } + | reading_clause_list call_stmt + { + $$ = list_concat($1, list_make1($2)); + } ; reading_clause_list: @@ -435,6 +537,7 @@ reading_clause_list: reading_clause: match | unwind + | call_stmt ; updating_clause_list_0: @@ -462,7 +565,6 @@ updating_clause: | remove | delete | merge - | call_stmt ; cypher_varlen_opt: diff --git a/src/include/nodes/ag_nodes.h b/src/include/nodes/ag_nodes.h index 15ff409f1..e51a8de43 100644 --- a/src/include/nodes/ag_nodes.h +++ b/src/include/nodes/ag_nodes.h @@ -59,6 +59,8 @@ typedef enum ag_node_tag cypher_integer_const_t, // sub patterns cypher_sub_pattern_t, + // procedure calls + cypher_call_t, // create data structures cypher_create_target_nodes_t, cypher_create_path_t, diff --git a/src/include/nodes/cypher_nodes.h b/src/include/nodes/cypher_nodes.h index 533515da1..c23d0dc99 100644 --- a/src/include/nodes/cypher_nodes.h +++ b/src/include/nodes/cypher_nodes.h @@ -236,6 +236,20 @@ typedef struct cypher_create_path char *var_name; } cypher_create_path; +/* + * procedure call + */ + +typedef struct cypher_call +{ + ExtensibleNode extensible; + FuncCall *funccall; /*from the parser */ + FuncExpr *funcexpr; /*transformed */ + + Node *where; + List *yield_items; // optional yield subclause +} cypher_call; + #define CYPHER_CLAUSE_FLAG_NONE 0x0000 #define CYPHER_CLAUSE_FLAG_TERMINAL 0x0001 #define CYPHER_CLAUSE_FLAG_PREVIOUS_CLAUSE 0x0002 diff --git a/src/include/nodes/cypher_outfuncs.h b/src/include/nodes/cypher_outfuncs.h index 20ef73fa9..836765bc9 100644 --- a/src/include/nodes/cypher_outfuncs.h +++ b/src/include/nodes/cypher_outfuncs.h @@ -65,6 +65,10 @@ void out_cypher_integer_const(StringInfo str, const ExtensibleNode *node); // sub pattern void out_cypher_sub_pattern(StringInfo str, const ExtensibleNode *node); +// procedure call + +void out_cypher_call(StringInfo str, const ExtensibleNode *node); + // create private data structures void out_cypher_create_target_nodes(StringInfo str, const ExtensibleNode *node); void out_cypher_create_path(StringInfo str, const ExtensibleNode *node);