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,