Skip to content
Permalink
Browse files
feat: Implement UNWIND clause. (#173)
* feat: Implement `UNWIND` clause.

* Block types on use `UNWIND` clause.

* Block types on use `UNWIND` clause.

* Block types on use `UNWIND` clause.

* Block types on use `UNWIND` clause.

* Reflect review

* Reflect review

Co-authored-by: Josh Innis <joshinnis@gmail.com>
  • Loading branch information
emotionbug and JoshInnis committed Jan 26, 2022
1 parent a1c1853 commit bb6715b5522f20569ee38e2c11204964cf31a28b
Showing 15 changed files with 394 additions and 1 deletion.
@@ -72,6 +72,7 @@ REGRESS = scan \
expr \
cypher_create \
cypher_match \
cypher_unwind \
cypher_set \
cypher_remove \
cypher_delete \
@@ -3877,6 +3877,13 @@ STABLE
PARALLEL SAFE
AS 'MODULE_PATHNAME';

CREATE FUNCTION ag_catalog.age_unnest(agtype, block_types boolean = false)
RETURNS SETOF agtype
LANGUAGE c
STABLE
PARALLEL SAFE
AS 'MODULE_PATHNAME';

--
-- End
--
@@ -0,0 +1,89 @@
/*
* 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_unwind');
NOTICE: graph "cypher_unwind" has been created
create_graph
--------------

(1 row)

SELECT * FROM cypher('cypher_unwind', $$
UNWIND [1, 2, 3] AS i RETURN i
$$) as (i agtype);
i
---
1
2
3
(3 rows)

SELECT * FROM cypher('cypher_unwind', $$
CREATE ({a: [1, 2, 3]}), ({a: [4, 5, 6]})
$$) as (i agtype);
i
---
(0 rows)

SELECT * FROM cypher('cypher_unwind', $$
MATCH (n) WITH n.a AS a UNWIND a AS i RETURN *
$$) as (i agtype, j agtype);
i | j
-----------+---
[1, 2, 3] | 1
[1, 2, 3] | 2
[1, 2, 3] | 3
[4, 5, 6] | 4
[4, 5, 6] | 5
[4, 5, 6] | 6
(6 rows)

SELECT * FROM cypher('cypher_unwind', $$
WITH [[1, 2], [3, 4], 5] AS nested
UNWIND nested AS x
UNWIND x AS y
RETURN y
$$) as (i agtype);
i
---
1
2
3
4
5
(5 rows)

SELECT * FROM cypher('cypher_unwind', $$
WITH [{id: 0, label:'', properties:{}}::vertex, {id: 1, label:'', properties:{}}::vertex] as n
UNWIND n as a
SET a.i = 1
RETURN a
$$) as (i agtype);
ERROR: UNWIND clause does not support agtype vertex
SELECT drop_graph('cypher_unwind', true);
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table cypher_unwind._ag_label_vertex
drop cascades to table cypher_unwind._ag_label_edge
NOTICE: graph "cypher_unwind" has been dropped
drop_graph
------------

(1 row)

@@ -0,0 +1,51 @@
/*
* 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_unwind');

SELECT * FROM cypher('cypher_unwind', $$
UNWIND [1, 2, 3] AS i RETURN i
$$) as (i agtype);

SELECT * FROM cypher('cypher_unwind', $$
CREATE ({a: [1, 2, 3]}), ({a: [4, 5, 6]})
$$) as (i agtype);

SELECT * FROM cypher('cypher_unwind', $$
MATCH (n) WITH n.a AS a UNWIND a AS i RETURN *
$$) as (i agtype, j agtype);

SELECT * FROM cypher('cypher_unwind', $$
WITH [[1, 2], [3, 4], 5] AS nested
UNWIND nested AS x
UNWIND x AS y
RETURN y
$$) as (i agtype);

SELECT * FROM cypher('cypher_unwind', $$
WITH [{id: 0, label:'', properties:{}}::vertex, {id: 1, label:'', properties:{}}::vertex] as n
UNWIND n as a
SET a.i = 1
RETURN a
$$) as (i agtype);

SELECT drop_graph('cypher_unwind', true);
@@ -40,6 +40,7 @@ const char *node_names[] = {
"cypher_set_item",
"cypher_delete",
"cypher_union",
"cypher_unwind",
"cypher_path",
"cypher_node",
"cypher_relationship",
@@ -100,6 +101,7 @@ const ExtensibleNodeMethods node_methods[] = {
DEFINE_NODE_METHODS(cypher_set_item),
DEFINE_NODE_METHODS(cypher_delete),
DEFINE_NODE_METHODS(cypher_union),
DEFINE_NODE_METHODS(cypher_unwind),
DEFINE_NODE_METHODS(cypher_path),
DEFINE_NODE_METHODS(cypher_node),
DEFINE_NODE_METHODS(cypher_relationship),
@@ -167,6 +167,13 @@ void out_cypher_delete(StringInfo str, const ExtensibleNode *node)
WRITE_NODE_FIELD(exprs);
}

void out_cypher_unwind(StringInfo str, const ExtensibleNode *node)
{
DEFINE_AG_NODE(cypher_unwind);

WRITE_NODE_FIELD(target);
}

// serialization function for the cypher_path ExtensibleNode.
void out_cypher_path(StringInfo str, const ExtensibleNode *node)
{
@@ -292,6 +292,9 @@ Query *cypher_parse_sub_analyze_union(cypher_clause *clause,
bool locked_from_parent,
bool resolve_unknowns);

// unwind
static Query *transform_cypher_unwind(cypher_parsestate *cpstate,
cypher_clause *clause);
// transform
#define PREV_CYPHER_CLAUSE_ALIAS "_"
#define transform_prev_cypher_clause(cpstate, prev_clause) \
@@ -380,6 +383,10 @@ Query *transform_cypher_clause(cypher_parsestate *cpstate,
{
result = transform_cypher_union(cpstate, clause);
}
else if (is_ag_node(self, cypher_unwind))
{
result = transform_cypher_unwind(cpstate, clause);
}
else
{
ereport(ERROR, (errmsg_internal("unexpected Node for cypher_clause")));
@@ -1047,6 +1054,76 @@ static Query *transform_cypher_delete(cypher_parsestate *cpstate,
return query;
}

/*
* transform_cypher_unwind
* It contains logic to convert the form of an array into a row. Here, we
* are simply calling `age_unnest` function, and the actual transformation
* is handled by `age_unnest` function.
*/
static Query *transform_cypher_unwind(cypher_parsestate *cpstate,
cypher_clause *clause)
{
ParseState *pstate = (ParseState *) cpstate;
cypher_unwind *self = (cypher_unwind *) clause->self;
int target_syntax_loc;
Query *query;
Node *expr;
FuncCall *unwind;
ParseExprKind old_expr_kind;
Node *funcexpr;
TargetEntry *te;

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

if (clause->prev)
{
RangeTblEntry *rte;
int rtindex;

rte = transform_prev_cypher_clause(cpstate, clause->prev);
rtindex = list_length(pstate->p_rtable);
Assert(rtindex == 1); // rte is the first RangeTblEntry in pstate
query->targetList = expandRelAttrs(pstate, rte, rtindex, 0, -1);
}

target_syntax_loc = exprLocation((const Node *) self->target);

if (findTarget(query->targetList, self->target->name) != NULL)
{
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_ALIAS),
errmsg("duplicate variable \"%s\"", self->target->name),
parser_errposition((ParseState *) cpstate, target_syntax_loc)));
}

expr = transform_cypher_expr(cpstate, self->target->val, EXPR_KIND_SELECT_TARGET);

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_make2(expr, makeBoolConst(true, false)),
pstate->p_last_srf, unwind, false,
target_syntax_loc);

pstate->p_expr_kind = old_expr_kind;

te = makeTargetEntry((Expr *) funcexpr,
(AttrNumber) pstate->p_next_resno++,
self->target->name, false);

query->targetList = lappend(query->targetList, te);
query->rtable = pstate->p_rtable;
query->jointree = makeFromExpr(pstate->p_joinlist, NULL);
query->hasTargetSRFs = pstate->p_hasTargetSRFs;

assign_query_collations(pstate, query);

return query;
}

/*
* Iterate through the list of items to delete and extract the variable name.
* Then find the resno that the variable name belongs to.
@@ -91,7 +91,7 @@
REMOVE RETURN
SET SKIP STARTS
THEN TRUE_P
UNION
UNION UNWIND
VERBOSE
WHEN WHERE WITH
XOR
@@ -114,6 +114,9 @@
/* CREATE clause */
%type <node> create

/* UNWIND clause */
%type <node> unwind

/* SET and REMOVE clause */
%type <node> set set_item remove remove_item
%type <list> set_item_list remove_item_list
@@ -373,6 +376,7 @@ reading_clause_list:

reading_clause:
match
| unwind
;

updating_clause_list_0:
@@ -721,6 +725,22 @@ match:
}
;

unwind:
UNWIND expr AS var_name
{
ResTarget *res;
cypher_unwind *n;

res = makeNode(ResTarget);
res->name = $4;
res->val = (Node *) $2;
res->location = @2;

n = make_ag_node(cypher_unwind);
n->target = res;
$$ = (Node *) n;
}

/*
* CREATE clause
*/
@@ -77,6 +77,7 @@ const ScanKeyword cypher_keywords[] = {
{"then", THEN, RESERVED_KEYWORD},
{"true", TRUE_P, RESERVED_KEYWORD},
{"union", UNION, RESERVED_KEYWORD},
{"unwind", UNWIND, RESERVED_KEYWORD},
{"verbose", VERBOSE, RESERVED_KEYWORD},
{"when", WHEN, RESERVED_KEYWORD},
{"where", WHERE, RESERVED_KEYWORD},

0 comments on commit bb6715b

Please sign in to comment.