Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement EXISTS Subquery #1562

Merged
merged 2 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ REGRESS = scan \
cypher_union \
cypher_call \
cypher_merge \
cypher_subquery \
age_global_graph \
age_load \
index \
Expand Down
360 changes: 360 additions & 0 deletions regress/expected/cypher_subquery.out

Large diffs are not rendered by default.

201 changes: 201 additions & 0 deletions regress/sql/cypher_subquery.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
LOAD 'age';
SET search_path TO ag_catalog;

SELECT * FROM create_graph('exists_subquery');

SELECT * FROM cypher('exists_subquery', $$
CREATE (:person {name: "Briggite", age: 32})-[:knows]->(:person {name: "Takeshi", age: 28}),
(:person {name: "Faye", age: 25})-[:knows]->(:person {name: "Tony", age: 34})-[:loved]->(:person {name : "Valerie", age: 33}),
(:person {name: "Calvin", age: 6})-[:knows]->(:pet {name: "Hobbes"}),
(:person {name: "Charlie", age: 8})-[:knows]->(:pet {name : "Snoopy"})
$$) AS (result agtype);

SELECT * FROM cypher('exists_subquery', $$ MATCH (a) RETURN (a) $$) AS (result agtype);

SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {(a:person)-[]->(:pet)}
RETURN (a) $$) AS (result agtype);
--trying to use b when not defined, should fail
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {(a:person)-[]->(b:pet)}
RETURN (a) $$) AS (result agtype);
--query inside
--EXISTS subquery is currently implemented naively, without constraints in the
--subquery. the results of this regression test may change upon implementation
--TODO: implement inner subquery constraints
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {MATCH (a:person)-[]->(b:pet) RETURN b}
RETURN (a) $$) AS (result agtype);

--repeat variable in match
--EXISTS subquery is currently implemented naively, without constraints in the
--subquery. the results of this regression test may change upon implementation
--TODO: implement inner subquery constraints
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {
MATCH (a:person)
WHERE a.name = 'Takeshi'
RETURN a
}
RETURN (a) $$) AS (result agtype);
--query inside, with WHERE
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {MATCH (a:person)-[]->(b:pet)
WHERE b.name = 'Briggite'
RETURN b}
RETURN (a) $$) AS (result agtype);


--no return
--EXISTS subquery is currently implemented naively, without constraints in the
--subquery. the results of this regression test may change upon implementation
--TODO: implement inner subquery constraints
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {MATCH (a:person)-[]->(b:pet)
WHERE a.name = 'Calvin'}
RETURN (a) $$) AS (result agtype);

--union
--EXISTS subquery is currently implemented naively, without constraints in the
--subquery. the results of this regression test may change upon implementation
--TODO: implement inner subquery constraints
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {
MATCH (a:person)-[]->(b:pet)
WHERE b.name = 'Hobbes'
RETURN b
UNION
MATCH (c:person)-[]->(d:person)
RETURN c
}
RETURN (a) $$) AS (result agtype);

-- union, mismatched var, should fail
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {
MATCH (a:person)-[]->(b:pet)
WHERE b.name = 'Snoopy'
RETURN c
UNION
MATCH (c:person)-[]->(d:person)
RETURN c
}
RETURN (a) $$) AS (result agtype);

--union, no returns
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {
MATCH (a:person)-[]->(b:pet)
WHERE a.name = 'Charlie'
UNION
MATCH (c:person)-[]->(d:person)
}
RETURN (a) $$) AS (result agtype);

--union, mismatched returns, should fail
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {
MATCH (a:person)-[]->(b:pet)
WHERE a.name = 'Faye'
RETURN a
UNION
MATCH (c:person)-[]->(d:person)
}
RETURN (a) $$) AS (result agtype);

--nesting
--EXISTS subquery is currently implemented naively, without constraints in the
--subquery. the results of this regression test may change upon implementation
--TODO: implement inner subquery constraints
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {
MATCH (b:person)
WHERE EXISTS {
MATCH (c:person)
WHERE c.name = 'Takeshi'
RETURN c
}
}
RETURN (a) $$) AS (result agtype);

--nesting, accessing var in outer scope
--EXISTS subquery is currently implemented naively, without constraints in the
--subquery. the results of this regression test may change upon implementation
--TODO: implement inner subquery constraints
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {
MATCH (b:person)
WHERE EXISTS {
MATCH (c:person)
WHERE b = c
RETURN c
}
}
RETURN (a) $$) AS (result agtype);

--nesting, accessing indirection in outer scope
--EXISTS subquery is currently implemented naively, without constraints in the
--subquery. the results of this regression test may change upon implementation
--TODO: implement inner subquery constraints
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {
MATCH (b:person)
WHERE EXISTS {
MATCH (c:person)
WHERE b.name = 'Takeshi'
RETURN c
}
}
RETURN (a) $$) AS (result agtype);

--nesting, accessing var 2+ levels up
--EXISTS subquery is currently implemented naively, without constraints in the
--subquery. the results of this regression test may change upon implementation
--TODO: implement inner subquery constraints
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {
MATCH (b:person)
WHERE EXISTS {
MATCH (c:person)
WHERE a.name = 'Takeshi'
RETURN c
}
}
RETURN (a) $$) AS (result agtype);

--nesting, accessing indirection 2+ levels up
--EXISTS subquery is currently implemented naively, without constraints in the
--subquery. the results of this regression test may change upon implementation
--TODO: implement inner subquery constraints
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
WHERE EXISTS {
MATCH (b:person)
WHERE EXISTS {
MATCH (c:person)
WHERE a = b
RETURN c
}
}
RETURN (a) $$) AS (result agtype);

--EXISTS outside of WHERE
SELECT * FROM cypher('exists_subquery', $$ MATCH (a:person)
RETURN a, EXISTS {(a:person)-[]->(:pet)}
$$) AS (a agtype, exists agtype);

--Var doesnt exist in outside scope, should fail
SELECT * FROM cypher('exists_subquery', $$ RETURN 1,
EXISTS {
MATCH (b:person)-[]->(:pet)
RETURN a
}
$$) AS (a agtype, exists agtype);

--
-- Cleanup
--
SELECT * FROM drop_graph('exists_subquery', true);

--
-- End of tests
--
2 changes: 2 additions & 0 deletions src/backend/nodes/ag_nodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const char *node_names[] = {
"cypher_typecast",
"cypher_integer_const",
"cypher_sub_pattern",
"cypher_sub_query",
"cypher_call",
"cypher_create_target_nodes",
"cypher_create_path",
Expand Down Expand Up @@ -117,6 +118,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_sub_query),
DEFINE_NODE_METHODS(cypher_call),
DEFINE_NODE_METHODS_EXTENDED(cypher_create_target_nodes),
DEFINE_NODE_METHODS_EXTENDED(cypher_create_path),
Expand Down
9 changes: 9 additions & 0 deletions src/backend/nodes/cypher_outfuncs.c
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,15 @@ void out_cypher_sub_pattern(StringInfo str, const ExtensibleNode *node)
WRITE_NODE_FIELD(pattern);
}

// serialization function for the cypher_sub_pattern ExtensibleNode.
void out_cypher_sub_query(StringInfo str, const ExtensibleNode *node)
{
DEFINE_AG_NODE(cypher_sub_query);

WRITE_ENUM_FIELD(kind, csp_kind);
WRITE_NODE_FIELD(query);
}

// serialization function for the cypher_call ExtensibleNode.
void out_cypher_call(StringInfo str, const ExtensibleNode *node)
{
Expand Down
1 change: 1 addition & 0 deletions src/backend/parser/cypher_analyze.c
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,7 @@ static Query *analyze_cypher(List *stmt, ParseState *parent_pstate,
cpstate->params = params;
cpstate->default_alias_num = 0;
cpstate->entities = NIL;
cpstate->subquery_where_flag = false;
/*
* install error context callback to adjust an error position since
* locations in stmt are 0 based
Expand Down
85 changes: 83 additions & 2 deletions src/backend/parser/cypher_clause.c
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ static TargetEntry *placeholder_target_entry(cypher_parsestate *cpstate,
char *name);
static Query *transform_cypher_sub_pattern(cypher_parsestate *cpstate,
cypher_clause *clause);
static Query *transform_cypher_sub_query(cypher_parsestate *cpstate,
cypher_clause *clause);
// set and remove clause
static Query *transform_cypher_set(cypher_parsestate *cpstate,
cypher_clause *clause);
Expand All @@ -224,6 +226,8 @@ static List *transform_cypher_delete_item_list(cypher_parsestate *cpstate,
List *delete_item_list,
Query *query);
//set operators
static cypher_clause *make_cypher_clause(List *stmt);

static Query *transform_cypher_union(cypher_parsestate *cpstate,
cypher_clause *clause);

Expand Down Expand Up @@ -389,6 +393,10 @@ Query *transform_cypher_clause(cypher_parsestate *cpstate,
{
result = transform_cypher_sub_pattern(cpstate, clause);
}
else if (is_ag_node(self, cypher_sub_query))
{
result = transform_cypher_sub_query(cpstate, clause);
}
else if (is_ag_node(self, cypher_unwind))
{
cypher_unwind *n = (cypher_unwind *) self;
Expand Down Expand Up @@ -2846,6 +2854,55 @@ static Query *transform_cypher_sub_pattern(cypher_parsestate *cpstate,
return qry;
}

static Query *transform_cypher_sub_query(cypher_parsestate *cpstate,
cypher_clause *clause)
{
cypher_clause *c;
Query *qry;
ParseState *pstate =(ParseState *)cpstate;
cypher_sub_query *sub_query = (cypher_sub_query*)clause->self;
ParseNamespaceItem *pnsi;
cypher_parsestate *child_parse_state = make_cypher_parsestate(cpstate);
ParseState *p_child_parse_state = (ParseState *) child_parse_state;
p_child_parse_state->p_expr_kind = pstate->p_expr_kind;

c = make_cypher_clause((List *)sub_query->query);

qry = makeNode(Query);
qry->commandType = CMD_SELECT;

child_parse_state->subquery_where_flag = true;

pnsi = transform_cypher_clause_as_subquery(child_parse_state,
transform_cypher_clause,
c,
NULL, true);

qry->targetList = makeTargetListFromPNSItem(p_child_parse_state, pnsi);

markTargetListOrigins(p_child_parse_state, qry->targetList);

qry->rtable = p_child_parse_state->p_rtable;
qry->rteperminfos = p_child_parse_state->p_rteperminfos;
qry->jointree = makeFromExpr(p_child_parse_state->p_joinlist, NULL);

/* the state will be destroyed so copy the data we need */
qry->hasSubLinks = p_child_parse_state->p_hasSubLinks;
qry->hasTargetSRFs = p_child_parse_state->p_hasTargetSRFs;
qry->hasAggs = p_child_parse_state->p_hasAggs;

if (qry->hasAggs)
{
parse_check_aggregates(p_child_parse_state, qry);
}

assign_query_collations(p_child_parse_state, qry);

free_cypher_parsestate(child_parse_state);

return qry;
}

/*
* Code borrowed and inspired by PG's transformFromClauseItem. This function
* will transform the VLE function, depending on type. Currently, only
Expand Down Expand Up @@ -4983,8 +5040,20 @@ static Expr *transform_cypher_edge(cypher_parsestate *cpstate,
/*
* If we are in a WHERE clause transform, we don't want to create new
* variables, we want to use the existing ones. So, error if otherwise.
* If we are in a subquery transform, we are allowed to create new variables
* in the match, and all variables outside are visible to
* the subquery. Since there is no existing SQL logic that allows
* subqueries to alter variables of outer queries, we bypass this
* logic we would normally use to process WHERE clauses.
*
* Currently, the EXISTS subquery logic is naive. It returns a boolean
* result on the outer queries, but does not restrict the results set.
*
* TODO: Implement logic to alter outer scope results.
*
*/
if (pstate->p_expr_kind == EXPR_KIND_WHERE)
if (pstate->p_expr_kind == EXPR_KIND_WHERE &&
cpstate->subquery_where_flag == false)
{
cypher_parsestate *parent_cpstate =
(cypher_parsestate *)pstate->parentParseState->parentParseState;
Expand Down Expand Up @@ -5243,8 +5312,20 @@ static Expr *transform_cypher_node(cypher_parsestate *cpstate,
/*
* If we are in a WHERE clause transform, we don't want to create new
* variables, we want to use the existing ones. So, error if otherwise.
* If we are in a subquery transform, we are allowed to create new variables
* in the match, and all variables outside are visible to
* the subquery. Since there is no existing SQL logic that allows
* subqueries to alter variables of outer queries, we bypass this
* logic we would normally use to process WHERE clauses.
*
* Currently, the EXISTS subquery logic is naive. It returns a boolean
* result on the outer queries, but does not restrict the results set.
*
* TODO: Implement logic to alter outer scope results.
*
*/
if (pstate->p_expr_kind == EXPR_KIND_WHERE)
if (pstate->p_expr_kind == EXPR_KIND_WHERE &&
cpstate->subquery_where_flag == false)
{
cypher_parsestate *parent_cpstate =
(cypher_parsestate *)pstate->parentParseState->parentParseState;
Expand Down
Loading
Loading