You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@age.apache.org by jg...@apache.org on 2023/01/04 18:30:45 UTC
[age] branch master updated: Update SET clause to support assigning a map to a variable (#468)
This is an automated email from the ASF dual-hosted git repository.
jgemignani pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/age.git
The following commit(s) were added to refs/heads/master by this push:
new f7bb3b1 Update SET clause to support assigning a map to a variable (#468)
f7bb3b1 is described below
commit f7bb3b1e3c4b1fb003a36685fd9724fc1094ad71
Author: Rafsun Masud <ra...@gmail.com>
AuthorDate: Wed Jan 4 13:30:39 2023 -0500
Update SET clause to support assigning a map to a variable (#468)
For example, 'SET v = {..}' will remove all properties of
v and set the provided map as its properties.
---
regress/expected/cypher_set.out | 111 ++++++++++++++++++++++++++++++++++++-
regress/sql/cypher_set.sql | 52 +++++++++++++++++
src/backend/executor/cypher_set.c | 20 ++++---
src/backend/parser/cypher_clause.c | 103 ++++++++++++++++++++++++----------
src/backend/utils/adt/agtype.c | 51 +++++++++++++++++
src/include/utils/agtype.h | 1 +
6 files changed, 301 insertions(+), 37 deletions(-)
diff --git a/regress/expected/cypher_set.out b/regress/expected/cypher_set.out
index 5cd42ee..74b3c17 100644
--- a/regress/expected/cypher_set.out
+++ b/regress/expected/cypher_set.out
@@ -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...
^
--
@@ -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
--
@@ -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)
+
--
diff --git a/regress/sql/cypher_set.sql b/regress/sql/cypher_set.sql
index 76a3a02..b408c55 100644
--- a/regress/sql/cypher_set.sql
+++ b/regress/sql/cypher_set.sql
@@ -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);
--
diff --git a/src/backend/executor/cypher_set.c b/src/backend/executor/cypher_set.c
index 8baea71..e095cb6 100644
--- a/src/backend/executor/cypher_set.c
+++ b/src/backend/executor/cypher_set.c
@@ -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,
diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c
index a541fb4..4c10327 100644
--- a/src/backend/parser/cypher_clause.c
+++ b/src/backend/parser/cypher_clause.c
@@ -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))
@@ -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))
{
@@ -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 does 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,
diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c
index 6c1a500..ff97378 100644
--- a/src/backend/utils/adt/agtype.c
+++ b/src/backend/utils/adt/agtype.c
@@ -8395,6 +8395,57 @@ agtype_value *alter_property_value(agtype_value *properties, char *var_name,
return parsed_agtype_value;
}
+/**
+ * Returns the map contained within the provided agtype.
+ */
+agtype_value *get_map_from_agtype(agtype *a)
+{
+ agtype_iterator *it;
+ agtype_iterator_token tok = WAGT_DONE;
+ agtype_parse_state *parse_state = NULL;
+ agtype_value *key;
+ agtype_value *value;
+ agtype_value *parsed_agtype_value = NULL;
+
+ key = palloc0(sizeof(agtype_value));
+ value = palloc0(sizeof(agtype_value));
+ it = agtype_iterator_init(&a->root);
+ tok = agtype_iterator_next(&it, key, true);
+
+ if (tok != WAGT_BEGIN_OBJECT)
+ {
+ ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("a map is expected")));
+ }
+
+ parsed_agtype_value = push_agtype_value(&parse_state, WAGT_BEGIN_OBJECT,
+ NULL);
+
+ while (true)
+ {
+ tok = agtype_iterator_next(&it, key, true);
+
+ if (tok == WAGT_DONE || tok == WAGT_END_OBJECT)
+ {
+ break;
+ }
+
+ agtype_iterator_next(&it, value, true);
+
+ if (value->type != AGTV_NULL)
+ {
+ parsed_agtype_value = push_agtype_value(&parse_state, WAGT_KEY,
+ key);
+ parsed_agtype_value = push_agtype_value(&parse_state, WAGT_VALUE,
+ value);
+ }
+ }
+
+ parsed_agtype_value = push_agtype_value(&parse_state, WAGT_END_OBJECT,
+ NULL);
+ return parsed_agtype_value;
+}
+
/*
* Helper function to extract 1 datum from a variadic "any" and convert, if
* possible, to an agtype, if it isn't already.
diff --git a/src/include/utils/agtype.h b/src/include/utils/agtype.h
index c7eab03..5f45eac 100644
--- a/src/include/utils/agtype.h
+++ b/src/include/utils/agtype.h
@@ -523,6 +523,7 @@ bool is_decimal_needed(char *numstr);
int compare_agtype_scalar_values(agtype_value *a, agtype_value *b);
agtype_value *alter_property_value(agtype_value *properties, char *var_name,
agtype *new_v, bool remove_property);
+agtype_value *get_map_from_agtype(agtype *a);
agtype *get_one_agtype_from_variadic_args(FunctionCallInfo fcinfo,
int variadic_offset,