Skip to content

Commit

Permalink
Update SET clause to support assigning a map to a variable
Browse files Browse the repository at this point in the history
For example, 'SET v = {..}' will remove all properties of
v and set the provided map as its properties.
  • Loading branch information
rafsun42 committed Dec 29, 2022
1 parent ad0a491 commit 43af88d
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 37 deletions.
111 changes: 110 additions & 1 deletion regress/expected/cypher_set.out
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ ERROR: undefined reference to variable wrong_var in SET clause
LINE 1: ...ELECT * FROM cypher('cypher_set', $$MATCH (n) SET wrong_var....
^
SELECT * FROM cypher('cypher_set', $$MATCH (n) SET i = 3$$) AS (a agtype);
ERROR: SET clause expects a variable name
ERROR: SET clause expects a map
LINE 1: ...ELECT * FROM cypher('cypher_set', $$MATCH (n) SET i = 3$$) A...
^
--
Expand Down Expand Up @@ -667,6 +667,100 @@ SELECT * FROM cypher('cypher_set', $$MATCH (n) RETURN n$$) AS (a agtype);
{"id": 2533274790395905, "label": "end", "properties": {"i": {}, "j": 3}}::vertex
(13 rows)

--
-- Test entire property update
--
SELECT * FROM create_graph('cypher_set_1');
NOTICE: graph "cypher_set_1" has been created
create_graph
--------------

(1 row)

SELECT * FROM cypher('cypher_set_1', $$ CREATE (a:Andy {name:'Andy', age:36, hungry:true}) $$) AS (a agtype);
a
---
(0 rows)

SELECT * FROM cypher('cypher_set_1', $$ CREATE (a:Peter {name:'Peter', age:34}) $$) AS (a agtype);
a
---
(0 rows)

SELECT * FROM cypher('cypher_set_1', $$ CREATE (a:Kevin {name:'Kevin', age:32, hungry:false}) $$) AS (a agtype);
a
---
(0 rows)

SELECT * FROM cypher('cypher_set_1', $$ CREATE (a:Matt {name:'Matt', city:'Toronto'}) $$) AS (a agtype);
a
---
(0 rows)

SELECT * FROM cypher('cypher_set_1', $$ CREATE (a:Juan {name:'Juan', role:'admin'}) $$) AS (a agtype);
a
---
(0 rows)

-- test copying properties between entities
SELECT * FROM cypher('cypher_set_1', $$
MATCH (at {name: 'Andy'}), (pn {name: 'Peter'})
SET at = properties(pn)
RETURN at, pn
$$) AS (at agtype, pn agtype);
at | pn
----------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------
{"id": 844424930131969, "label": "Andy", "properties": {"age": 34, "name": "Peter"}}::vertex | {"id": 1125899906842625, "label": "Peter", "properties": {"age": 34, "name": "Peter"}}::vertex
(1 row)

SELECT * FROM cypher('cypher_set_1', $$
MATCH (at {name: 'Kevin'}), (pn {name: 'Matt'})
SET at = pn
RETURN at, pn
$$) AS (at agtype, pn agtype);
at | pn
-------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------
{"id": 1407374883553281, "label": "Kevin", "properties": {"city": "Toronto", "name": "Matt"}}::vertex | {"id": 1688849860263937, "label": "Matt", "properties": {"city": "Toronto", "name": "Matt"}}::vertex
(1 row)

-- test replacing all properties using a map and =
SELECT * FROM cypher('cypher_set_1', $$
MATCH (m {name: 'Matt'})
SET m = {name: 'Peter Smith', position: 'Entrepreneur', city:NULL}
RETURN m
$$) AS (m agtype);
m
-----------------------------------------------------------------------------------------------------------------------
{"id": 1407374883553281, "label": "Kevin", "properties": {"name": "Peter Smith", "position": "Entrepreneur"}}::vertex
{"id": 1688849860263937, "label": "Matt", "properties": {"name": "Peter Smith", "position": "Entrepreneur"}}::vertex
(2 rows)

-- test removing all properties using an empty map and =
SELECT * FROM cypher('cypher_set_1', $$
MATCH (p {name: 'Juan'})
SET p = {}
RETURN p
$$) AS (p agtype);
p
---------------------------------------------------------------------
{"id": 1970324836974593, "label": "Juan", "properties": {}}::vertex
(1 row)

-- test assigning non-map to an enitity
SELECT * FROM cypher('cypher_set_1', $$
MATCH (p {name: 'Peter'})
SET p = "Peter"
RETURN p
$$) AS (p agtype);
ERROR: SET clause expects a map
LINE 3: SET p = "Peter"
^
SELECT * FROM cypher('cypher_set_1', $$
MATCH (p {name: 'Peter'})
SET p = sqrt(4)
RETURN p
$$) AS (p agtype);
ERROR: a map is expected
--
-- Clean up
--
Expand All @@ -689,4 +783,19 @@ NOTICE: graph "cypher_set" has been dropped

(1 row)

SELECT drop_graph('cypher_set_1', true);
NOTICE: drop cascades to 7 other objects
DETAIL: drop cascades to table cypher_set_1._ag_label_vertex
drop cascades to table cypher_set_1._ag_label_edge
drop cascades to table cypher_set_1."Andy"
drop cascades to table cypher_set_1."Peter"
drop cascades to table cypher_set_1."Kevin"
drop cascades to table cypher_set_1."Matt"
drop cascades to table cypher_set_1."Juan"
NOTICE: graph "cypher_set_1" has been dropped
drop_graph
------------

(1 row)

--
52 changes: 52 additions & 0 deletions regress/sql/cypher_set.sql
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,64 @@ SELECT * FROM cypher('cypher_set', $$MATCH (n) SET n.i = {} RETURN n$$) AS (a ag

SELECT * FROM cypher('cypher_set', $$MATCH (n) RETURN n$$) AS (a agtype);

--
-- Test entire property update
--
SELECT * FROM create_graph('cypher_set_1');

SELECT * FROM cypher('cypher_set_1', $$ CREATE (a:Andy {name:'Andy', age:36, hungry:true}) $$) AS (a agtype);
SELECT * FROM cypher('cypher_set_1', $$ CREATE (a:Peter {name:'Peter', age:34}) $$) AS (a agtype);
SELECT * FROM cypher('cypher_set_1', $$ CREATE (a:Kevin {name:'Kevin', age:32, hungry:false}) $$) AS (a agtype);
SELECT * FROM cypher('cypher_set_1', $$ CREATE (a:Matt {name:'Matt', city:'Toronto'}) $$) AS (a agtype);
SELECT * FROM cypher('cypher_set_1', $$ CREATE (a:Juan {name:'Juan', role:'admin'}) $$) AS (a agtype);

-- test copying properties between entities
SELECT * FROM cypher('cypher_set_1', $$
MATCH (at {name: 'Andy'}), (pn {name: 'Peter'})
SET at = properties(pn)
RETURN at, pn
$$) AS (at agtype, pn agtype);

SELECT * FROM cypher('cypher_set_1', $$
MATCH (at {name: 'Kevin'}), (pn {name: 'Matt'})
SET at = pn
RETURN at, pn
$$) AS (at agtype, pn agtype);

-- test replacing all properties using a map and =
SELECT * FROM cypher('cypher_set_1', $$
MATCH (m {name: 'Matt'})
SET m = {name: 'Peter Smith', position: 'Entrepreneur', city:NULL}
RETURN m
$$) AS (m agtype);

-- test removing all properties using an empty map and =
SELECT * FROM cypher('cypher_set_1', $$
MATCH (p {name: 'Juan'})
SET p = {}
RETURN p
$$) AS (p agtype);

-- test assigning non-map to an enitity
SELECT * FROM cypher('cypher_set_1', $$
MATCH (p {name: 'Peter'})
SET p = "Peter"
RETURN p
$$) AS (p agtype);

SELECT * FROM cypher('cypher_set_1', $$
MATCH (p {name: 'Peter'})
SET p = sqrt(4)
RETURN p
$$) AS (p agtype);

--
-- Clean up
--
DROP TABLE tbl;
DROP FUNCTION set_test;
SELECT drop_graph('cypher_set', true);
SELECT drop_graph('cypher_set_1', true);

--

20 changes: 12 additions & 8 deletions src/backend/executor/cypher_set.c
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,18 @@ static void process_update_list(CustomScanState *node)
new_property_value = DATUM_GET_AGTYPE_P(scanTupleSlot->tts_values[update_item->prop_position - 1]);
}

/*
* Alter the properties Agtype value to contain or remove the updated
* property.
*/
altered_properties = alter_property_value(original_properties,
update_item->prop_name,
new_property_value,
remove_property);
// Alter the properties Agtype value.
if (strcmp(update_item->prop_name, ""))
{
altered_properties = alter_property_value(original_properties,
update_item->prop_name,
new_property_value,
remove_property);
}
else
{
altered_properties = get_map_from_agtype(new_property_value);
}

resultRelInfo = create_entity_result_rel_info(estate,
css->set_list->graph_name,
Expand Down
103 changes: 75 additions & 28 deletions src/backend/parser/cypher_clause.c
Original file line number Diff line number Diff line change
Expand Up @@ -1610,17 +1610,52 @@ cypher_update_information *transform_cypher_set_item_list(
A_Indirection *ind;
char *variable_name, *property_name;
Value *property_node, *variable_node;
int is_entire_prop_update = 0; // true if a map is assigned to variable

// ColumnRef may come according to the Parser rule.
if (!IsA(set_item->prop, A_Indirection))
// LHS of set_item must be a variable or an indirection.
if (IsA(set_item->prop, ColumnRef))
{
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
/*
* A variable can only be assigned a map, a function call that
* evaluates to a map, or a variable.
*
* In case of a function call, whether it actually evaluates to
* map is checked in the execution stage.
*/
if (!is_ag_node(set_item->expr, cypher_map) &&
!IsA(set_item->expr, FuncCall) &&
!IsA(set_item->expr, ColumnRef))
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("SET clause expects a map"),
parser_errposition(pstate, set_item->location)));
}

is_entire_prop_update = 1;

/*
* In case of a variable, it is wrapped as an argument to
* the 'properties' function.
*/
if (IsA(set_item->expr, ColumnRef))
{
List *qualified_name, *args;

qualified_name = list_make2(makeString("ag_catalog"),
makeString("age_properties"));
args = list_make1(set_item->expr);
set_item->expr = (Node *)makeFuncCall(qualified_name, args,
-1);
}
}
else if (!IsA(set_item->prop, A_Indirection))
{
ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
errmsg("SET clause expects a variable name"),
parser_errposition(pstate, set_item->location)));
}

ind = (A_Indirection *)set_item->prop;
item = make_ag_node(cypher_update_item);

if (!is_ag_node(lfirst(li), cypher_set_item))
Expand All @@ -1640,9 +1675,42 @@ cypher_update_information *transform_cypher_set_item_list(

item->remove_item = false;

// extract variable name
ref = (ColumnRef *)ind->arg;
// set variable and extract property name
if (is_entire_prop_update)
{
ref = (ColumnRef *)set_item->prop;
item->prop_name = NULL;
}
else
{
ind = (A_Indirection *)set_item->prop;
ref = (ColumnRef *)ind->arg;

// extract property name
if (list_length(ind->indirection) != 1)
{
ereport(
ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg(
"SET clause doesnt not support updating maps or lists in a property"),
parser_errposition(pstate, set_item->location)));
}

property_node = linitial(ind->indirection);
if (!IsA(property_node, String))
{
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
errmsg("SET clause expects a property name"),
parser_errposition(pstate, set_item->location)));
}

property_name = property_node->val.str;
item->prop_name = property_name;
}

// extract variable name
variable_node = linitial(ref->fields);
if (!IsA(variable_node, String))
{
Expand All @@ -1666,27 +1734,6 @@ cypher_update_information *transform_cypher_set_item_list(
parser_errposition(pstate, set_item->location)));
}

// extract property name
if (list_length(ind->indirection) != 1)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("SET clause doesnt not support updating maps or lists in a property"),
parser_errposition(pstate, set_item->location)));
}

property_node = linitial(ind->indirection);
if (!IsA(property_node, String))
{
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
errmsg("SET clause expects a property name"),
parser_errposition(pstate, set_item->location)));
}

property_name = property_node->val.str;
item->prop_name = property_name;

// create target entry for the new property value
item->prop_position = (AttrNumber)pstate->p_next_resno;
target_item = transform_cypher_item(cpstate, set_item->expr, NULL,
Expand Down
Loading

0 comments on commit 43af88d

Please sign in to comment.