This is an automated email from the ASF dual-hosted git repository.

dehowef pushed a commit to branch PG13
in repository https://gitbox.apache.org/repos/asf/age.git


The following commit(s) were added to refs/heads/PG13 by this push:
     new 58cae209 Implement map projection (#1710) (#1795)
58cae209 is described below

commit 58cae20995623f7ae7290c4b209bf5813d08f1b9
Author: Rafsun Masud <[email protected]>
AuthorDate: Thu Apr 25 16:52:20 2024 -0700

    Implement map projection (#1710) (#1795)
    
    Adds support for openCypher Map Projection specification.
---
 Makefile                            |   3 +-
 regress/expected/map_projection.out | 171 ++++++++++++++++++++++++++++++++++++
 regress/sql/map_projection.sql      |  74 ++++++++++++++++
 src/backend/nodes/ag_nodes.c        |   3 +
 src/backend/nodes/cypher_outfuncs.c |   9 ++
 src/backend/parser/cypher_expr.c    | 169 ++++++++++++++++++++++++++++++++++-
 src/backend/parser/cypher_gram.y    |  80 +++++++++++++++++
 src/include/nodes/ag_nodes.h        |   2 +
 src/include/nodes/cypher_nodes.h    |  34 +++++++
 src/include/nodes/cypher_outfuncs.h |   1 +
 10 files changed, 544 insertions(+), 2 deletions(-)

diff --git a/Makefile b/Makefile
index ef97774b..1224fc2a 100644
--- a/Makefile
+++ b/Makefile
@@ -102,7 +102,7 @@ REGRESS = scan \
           cypher_union \
           cypher_call \
           cypher_merge \
-         cypher_subquery \
+          cypher_subquery \
           age_global_graph \
           age_load \
           index \
@@ -111,6 +111,7 @@ REGRESS = scan \
           name_validation \
           jsonb_operators \
           list_comprehension \
+          map_projection \
           drop
 
 srcdir=`pwd`
diff --git a/regress/expected/map_projection.out 
b/regress/expected/map_projection.out
new file mode 100644
index 00000000..dcb7f0e7
--- /dev/null
+++ b/regress/expected/map_projection.out
@@ -0,0 +1,171 @@
+/*
+ * 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('map_proj');
+NOTICE:  graph "map_proj" has been created
+ create_graph 
+--------------
+ 
+(1 row)
+
+SELECT * FROM cypher('map_proj',
+$$
+    CREATE
+        (tom:Actor {name:'Tom Hanks', age:60}),
+        (bale:Actor {name:'Christian Bale', age:50}),
+        (tom)-[:ACTED_IN {during: 1990}]->(:Movie {title:'Forrest Gump'}),
+        (tom)-[:ACTED_IN {during: 1995}]->(:Movie {title:'Finch'}),
+        (tom)-[:ACTED_IN {during: 1999}]->(:Movie {title:'The Circle'}),
+        (bale)-[:ACTED_IN {during: 2002}]->(:Movie {title:'The Prestige'}),
+        (bale)-[:ACTED_IN {during: 2008}]->(:Movie {title:'The Dark Knight'})
+$$) as (a agtype);
+ a 
+---
+(0 rows)
+
+-- all property selection
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .* } $$) as (a agtype);
+             a              
+----------------------------
+ {"age": 50, "name": "Bob"}
+(1 row)
+
+-- property selector
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .name } $$) as (a agtype);
+        a        
+-----------------
+ {"name": "Bob"}
+(1 row)
+
+-- literal entry
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { name:'Tom' } $$) as (a agtype);
+        a        
+-----------------
+ {"name": "Tom"}
+(1 row)
+
+-- variable selector
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map, 'Tom' as 
name RETURN map { name } $$) as (a agtype);
+        a        
+-----------------
+ {"name": "Tom"}
+(1 row)
+
+-- duplicate all property selector
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .*, .* } $$) as (a agtype);
+             a              
+----------------------------
+ {"age": 50, "name": "Bob"}
+(1 row)
+
+-- name being selected twice
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .name, .* } $$) as (a agtype);
+             a              
+----------------------------
+ {"age": 50, "name": "Bob"}
+(1 row)
+
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .name, .name } $$) as (a agtype);
+        a        
+-----------------
+ {"name": "Bob"}
+(1 row)
+
+-- name being selected twice with different value
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { name:'Tom', .* } $$) as (a agtype);
+             a              
+----------------------------
+ {"age": 50, "name": "Tom"}
+(1 row)
+
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map, 'Tom' as 
name RETURN map { name, .* } $$) as (a agtype);
+             a              
+----------------------------
+ {"age": 50, "name": "Tom"}
+(1 row)
+
+-- new entry added
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .name, .age, height:180 } $$) as (a agtype);
+                     a                     
+-------------------------------------------
+ {"age": 50, "name": "Bob", "height": 180}
+(1 row)
+
+-- NULL as a map
+SELECT * FROM cypher('map_proj', $$ WITH NULL AS map RETURN map { .name } $$) 
as (a agtype);
+ a  
+----
+ {}
+(1 row)
+
+-- vertex as a map
+SELECT * FROM cypher('map_proj', $$ MATCH (n:Actor) RETURN n { .name } $$) as 
(a agtype);
+             a              
+----------------------------
+ {"name": "Tom Hanks"}
+ {"name": "Christian Bale"}
+(2 rows)
+
+-- edge as a map
+SELECT * FROM cypher('map_proj', $$ MATCH ()-[e:ACTED_IN]->() RETURN e { 
.during } $$) as (a agtype);
+        a         
+------------------
+ {"during": 1990}
+ {"during": 1995}
+ {"during": 1999}
+ {"during": 2002}
+ {"during": 2008}
+(5 rows)
+
+-- syntax error
+SELECT * FROM cypher('map_proj', $$ WITH 12 AS map RETURN map { .name } $$) as 
(a agtype);
+ERROR:  properties() argument must be a vertex, an edge or null
+SELECT * FROM cypher('map_proj', $$ WITH [] AS map RETURN map { .name } $$) as 
(a agtype);
+ERROR:  properties() argument must resolve to an object
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob'} AS map RETURN map { 
'name' } $$) as (a agtype);
+ERROR:  syntax error at or near "'name'"
+LINE 1: ...p_proj', $$ WITH {name:'Bob'} AS map RETURN map { 'name' } $...
+                                                             ^
+-- advanced
+SELECT * FROM cypher('map_proj',
+$$
+    MATCH (a:Actor)-[:ACTED_IN]->(m:Movie)
+    WITH a, collect(m { .title }) AS movies
+    RETURN collect(a { .name, movies })
+$$) as (a agtype);
+                                                                               
                       a                                                        
                                               
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"name": "Christian Bale", "movies": [{"title": "The Prestige"}, {"title": 
"The Dark Knight"}]}, {"name": "Tom Hanks", "movies": [{"title": "Forrest 
Gump"}, {"title": "Finch"}, {"title": "The Circle"}]}]
+(1 row)
+
+-- drop
+SELECT drop_graph('map_proj', true);
+NOTICE:  drop cascades to 5 other objects
+DETAIL:  drop cascades to table map_proj._ag_label_vertex
+drop cascades to table map_proj._ag_label_edge
+drop cascades to table map_proj."Actor"
+drop cascades to table map_proj."ACTED_IN"
+drop cascades to table map_proj."Movie"
+NOTICE:  graph "map_proj" has been dropped
+ drop_graph 
+------------
+ 
+(1 row)
+
diff --git a/regress/sql/map_projection.sql b/regress/sql/map_projection.sql
new file mode 100644
index 00000000..72fc2fb2
--- /dev/null
+++ b/regress/sql/map_projection.sql
@@ -0,0 +1,74 @@
+/*
+ * 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('map_proj');
+
+SELECT * FROM cypher('map_proj',
+$$
+    CREATE
+        (tom:Actor {name:'Tom Hanks', age:60}),
+        (bale:Actor {name:'Christian Bale', age:50}),
+        (tom)-[:ACTED_IN {during: 1990}]->(:Movie {title:'Forrest Gump'}),
+        (tom)-[:ACTED_IN {during: 1995}]->(:Movie {title:'Finch'}),
+        (tom)-[:ACTED_IN {during: 1999}]->(:Movie {title:'The Circle'}),
+        (bale)-[:ACTED_IN {during: 2002}]->(:Movie {title:'The Prestige'}),
+        (bale)-[:ACTED_IN {during: 2008}]->(:Movie {title:'The Dark Knight'})
+$$) as (a agtype);
+
+-- all property selection
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .* } $$) as (a agtype);
+-- property selector
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .name } $$) as (a agtype);
+-- literal entry
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { name:'Tom' } $$) as (a agtype);
+-- variable selector
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map, 'Tom' as 
name RETURN map { name } $$) as (a agtype);
+-- duplicate all property selector
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .*, .* } $$) as (a agtype);
+-- name being selected twice
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .name, .* } $$) as (a agtype);
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .name, .name } $$) as (a agtype);
+-- name being selected twice with different value
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { name:'Tom', .* } $$) as (a agtype);
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map, 'Tom' as 
name RETURN map { name, .* } $$) as (a agtype);
+-- new entry added
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob', age:50} AS map RETURN 
map { .name, .age, height:180 } $$) as (a agtype);
+-- NULL as a map
+SELECT * FROM cypher('map_proj', $$ WITH NULL AS map RETURN map { .name } $$) 
as (a agtype);
+-- vertex as a map
+SELECT * FROM cypher('map_proj', $$ MATCH (n:Actor) RETURN n { .name } $$) as 
(a agtype);
+-- edge as a map
+SELECT * FROM cypher('map_proj', $$ MATCH ()-[e:ACTED_IN]->() RETURN e { 
.during } $$) as (a agtype);
+-- syntax error
+SELECT * FROM cypher('map_proj', $$ WITH 12 AS map RETURN map { .name } $$) as 
(a agtype);
+SELECT * FROM cypher('map_proj', $$ WITH [] AS map RETURN map { .name } $$) as 
(a agtype);
+SELECT * FROM cypher('map_proj', $$ WITH {name:'Bob'} AS map RETURN map { 
'name' } $$) as (a agtype);
+-- advanced
+SELECT * FROM cypher('map_proj',
+$$
+    MATCH (a:Actor)-[:ACTED_IN]->(m:Movie)
+    WITH a, collect(m { .title }) AS movies
+    RETURN collect(a { .name, movies })
+$$) as (a agtype);
+
+-- drop
+SELECT drop_graph('map_proj', true);
diff --git a/src/backend/nodes/ag_nodes.c b/src/backend/nodes/ag_nodes.c
index a41cdd0b..858ca5a5 100644
--- a/src/backend/nodes/ag_nodes.c
+++ b/src/backend/nodes/ag_nodes.c
@@ -45,6 +45,8 @@ const char *node_names[] = {
     "cypher_bool_const",
     "cypher_param",
     "cypher_map",
+    "cypher_map_projection",
+    "cypher_map_projection_element",
     "cypher_list",
     "cypher_comparison_aexpr",
     "cypher_comparison_boolexpr",
@@ -111,6 +113,7 @@ const ExtensibleNodeMethods node_methods[] = {
     DEFINE_NODE_METHODS(cypher_bool_const),
     DEFINE_NODE_METHODS(cypher_param),
     DEFINE_NODE_METHODS(cypher_map),
+    DEFINE_NODE_METHODS(cypher_map_projection),
     DEFINE_NODE_METHODS(cypher_list),
     DEFINE_NODE_METHODS(cypher_comparison_aexpr),
     DEFINE_NODE_METHODS(cypher_comparison_boolexpr),
diff --git a/src/backend/nodes/cypher_outfuncs.c 
b/src/backend/nodes/cypher_outfuncs.c
index 2904503f..5c175a30 100644
--- a/src/backend/nodes/cypher_outfuncs.c
+++ b/src/backend/nodes/cypher_outfuncs.c
@@ -251,6 +251,15 @@ void out_cypher_map(StringInfo str, const ExtensibleNode 
*node)
     WRITE_LOCATION_FIELD(location);
 }
 
+void out_cypher_map_projection(StringInfo str, const ExtensibleNode *node)
+{
+    DEFINE_AG_NODE(cypher_map_projection);
+
+    WRITE_NODE_FIELD(map_var);
+    WRITE_NODE_FIELD(map_elements);
+    WRITE_LOCATION_FIELD(location);
+}
+
 // serialization function for the cypher_list ExtensibleNode.
 void out_cypher_list(StringInfo str, const ExtensibleNode *node)
 {
diff --git a/src/backend/parser/cypher_expr.c b/src/backend/parser/cypher_expr.c
index 055d51d8..bb77e876 100644
--- a/src/backend/parser/cypher_expr.c
+++ b/src/backend/parser/cypher_expr.c
@@ -74,6 +74,8 @@ static Node *transform_AEXPR_IN(cypher_parsestate *cpstate, 
A_Expr *a);
 static Node *transform_cypher_param(cypher_parsestate *cpstate,
                                     cypher_param *cp);
 static Node *transform_cypher_map(cypher_parsestate *cpstate, cypher_map *cm);
+static Node *transform_cypher_map_projection(cypher_parsestate *cpstate,
+                                             cypher_map_projection *cmp);
 static Node *transform_cypher_list(cypher_parsestate *cpstate,
                                    cypher_list *cl);
 static Node *transform_cypher_string_match(cypher_parsestate *cpstate,
@@ -189,6 +191,11 @@ static Node 
*transform_cypher_expr_recurse(cypher_parsestate *cpstate,
         {
             return transform_cypher_map(cpstate, (cypher_map *)expr);
         }
+        if (is_ag_node(expr, cypher_map_projection))
+        {
+            return transform_cypher_map_projection(
+                cpstate, (cypher_map_projection *)expr);
+        }
         if (is_ag_node(expr, cypher_list))
         {
             return transform_cypher_list(cpstate, (cypher_list *)expr);
@@ -222,7 +229,7 @@ static Node 
*transform_cypher_expr_recurse(cypher_parsestate *cpstate,
         ereport(ERROR,
                 (errmsg_internal("unrecognized ExtensibleNode: %s",
                                  ((ExtensibleNode *)expr)->extnodename)));
-        
+
         return NULL;
     }
     case T_FuncCall:
@@ -839,6 +846,166 @@ static Node *transform_cypher_param(cypher_parsestate 
*cpstate,
     return (Node *)func_expr;
 }
 
+static Node *transform_cypher_map_projection(cypher_parsestate *cpstate,
+                                             cypher_map_projection *cmp)
+{
+    ParseState *pstate;
+    ListCell *lc;
+    List *keyvals;
+    Oid foid_agtype_build_map;
+    FuncExpr *fexpr_new_map;
+    bool has_all_prop_selector;
+    Node *transformed_map_var;
+    Oid foid_age_properties;
+    FuncExpr *fexpr_orig_map;
+
+    pstate = (ParseState *)cpstate;
+    keyvals = NIL;
+    has_all_prop_selector = false;
+    fexpr_new_map = NULL;
+
+    /*
+     * Builds the original map: `age_properties(cmp->map_var)`. Whether map_var
+     * is compatible (map, vertex or edge) is checked during the execution of
+     * age_properties().
+     */
+    transformed_map_var = transform_cypher_expr_recurse(cpstate,
+                                                        (Node *)cmp->map_var);
+    foid_age_properties = get_ag_func_oid("age_properties", 1, AGTYPEOID);
+    fexpr_orig_map = makeFuncExpr(foid_age_properties, AGTYPEOID,
+                                  list_make1(transformed_map_var), InvalidOid,
+                                  InvalidOid, COERCE_EXPLICIT_CALL);
+    fexpr_orig_map->location = cmp->location;
+
+    /*
+     * Builds a new map. Each map projection element is transformed into a key
+     * value pair (except for the ALL_PROPERTIES_SELECTOR type).
+     */
+    foreach (lc, cmp->map_elements)
+    {
+        cypher_map_projection_element *elem;
+        Const *key;
+        Node *val;
+
+        elem = lfirst(lc);
+        key = NULL;
+        val = NULL;
+
+        if (elem->type == ALL_PROPERTIES_SELECTOR)
+        {
+            has_all_prop_selector = true;
+            continue;
+        }
+
+        /* Makes key and val based on elem->type */
+        switch (elem->type)
+        {
+            case PROPERTY_SELECTOR:
+            {
+                Oid foid_access_op;
+                FuncExpr *fexpr_access_op;
+                ArrayExpr *args_access_op;
+                Const *key_agtype;
+
+                /* Makes key from elem->key */
+                key = makeConst(TEXTOID, -1, InvalidOid, -1,
+                                CStringGetTextDatum(elem->key), false, false);
+
+                /* Makes val from `age_properties(cmp->map_var).key` */
+                key_agtype = makeConst(AGTYPEOID, -1, InvalidOid, -1,
+                                       string_to_agtype(elem->key), false,
+                                       false);
+                foid_access_op = get_ag_func_oid("agtype_access_operator", 1,
+                                                 AGTYPEARRAYOID);
+                args_access_op = make_agtype_array_expr(
+                    list_make2(fexpr_orig_map, key_agtype));
+                fexpr_access_op = makeFuncExpr(foid_access_op, AGTYPEOID,
+                                               list_make1(args_access_op),
+                                               InvalidOid, InvalidOid,
+                                               COERCE_EXPLICIT_CALL);
+                fexpr_access_op->funcvariadic = true;
+                fexpr_access_op->location = -1;
+                val = (Node *)fexpr_access_op;
+
+                break;
+            }
+            case LITERAL_ENTRY:
+            {
+                key = makeConst(TEXTOID, -1, InvalidOid, -1,
+                                CStringGetTextDatum(elem->key), false, false);
+                val = transform_cypher_expr_recurse(cpstate, elem->value);
+                break;
+            }
+            case VARIABLE_SELECTOR:
+            {
+                char *key_str;
+                List *fields;
+
+                Assert(IsA(elem->value, ColumnRef));
+
+                /* Makes key from the ColumnRef's field */
+                fields = ((ColumnRef *)elem->value)->fields;
+                key_str = strVal(lfirst(list_head(fields)));
+                key = makeConst(TEXTOID, -1, InvalidOid, -1,
+                                CStringGetTextDatum(key_str), false, false);
+
+                val = transform_cypher_expr_recurse(cpstate, elem->value);
+                break;
+            }
+            case ALL_PROPERTIES_SELECTOR:
+            {
+                /*
+                 * Key value pairs of the original map are added later outside
+                 * the loop. Control never reaches this block.
+                 */
+                break;
+            }
+            default:
+            {
+                elog(ERROR, "unknown map projection element type");
+            }
+        }
+
+        Assert(key);
+        Assert(val);
+        keyvals = lappend(lappend(keyvals, key), val);
+    }
+
+    if (keyvals)
+    {
+        foid_agtype_build_map = get_ag_func_oid("agtype_build_map_nonull", 1,
+                                                ANYOID);
+        fexpr_new_map = makeFuncExpr(foid_agtype_build_map, AGTYPEOID, keyvals,
+                                     InvalidOid, InvalidOid,
+                                     COERCE_EXPLICIT_CALL);
+        fexpr_new_map->location = cmp->location;
+    }
+
+    /*
+     * In case .* is present, returns age_properties(cmp->map_var) + the new
+     * map. Else, returns the new map.
+     */
+    if (has_all_prop_selector)
+    {
+        if (!keyvals)
+        {
+            return (Node *)fexpr_orig_map;
+        }
+        else
+        {
+            return (Node *)make_op(pstate, list_make1(makeString("+")),
+                                   (Node *)fexpr_orig_map,
+                                   (Node *)fexpr_new_map,
+                                   pstate->p_last_srf, -1);
+        }
+    }
+    else
+    {
+        Assert(!has_all_prop_selector && fexpr_new_map);
+        return (Node *)fexpr_new_map;
+    }
+}
+
 static Node *transform_cypher_map(cypher_parsestate *cpstate, cypher_map *cm)
 {
     ParseState *pstate = (ParseState *)cpstate;
diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y
index e4c02be6..2d9c1947 100644
--- a/src/backend/parser/cypher_gram.y
+++ b/src/backend/parser/cypher_gram.y
@@ -152,6 +152,9 @@
 %type <node> expr_case expr_case_when expr_case_default
 %type <list> expr_case_when_list
 
+%type <node> map_projection map_projection_elem
+%type <list> map_projection_elem_list
+
 %type <node> expr_var expr_func expr_func_norm expr_func_subexpr
 %type <list> expr_list expr_list_opt map_keyval_list_opt map_keyval_list
 %type <node> property_value
@@ -2004,6 +2007,7 @@ expr_literal:
             $$ = make_null_const(@1);
         }
     | map
+    | map_projection
     | list
     ;
 
@@ -2038,6 +2042,82 @@ map_keyval_list:
         }
     ;
 
+map_projection:
+    expr_var '{' map_projection_elem_list '}'
+        {
+            cypher_map_projection *n;
+
+            n = make_ag_node(cypher_map_projection);
+            n->map_var = (ColumnRef *)$1;
+            n->map_elements = $3;
+            n->location = @1;
+
+            $$ = (Node *)n;
+        }
+    ;
+
+map_projection_elem_list:
+    map_projection_elem
+        {
+            $$ = list_make1($1);
+        }
+    | map_projection_elem_list ',' map_projection_elem
+        {
+            $$ = lappend($1, $3);
+        }
+    ;
+
+map_projection_elem:
+    '.' property_key_name
+        {
+            cypher_map_projection_element *n;
+
+            n = make_ag_node(cypher_map_projection_element);
+            n->type = PROPERTY_SELECTOR;
+            n->key = $2;
+            n->value = NULL;
+            n->location = @1;
+
+            $$ = (Node *)n;
+        }
+    | expr_var
+        {
+            cypher_map_projection_element *n;
+
+            n = make_ag_node(cypher_map_projection_element);
+            n->type = VARIABLE_SELECTOR;
+            n->key = NULL;
+            n->value = (Node *)$1;
+            n->location = @1;
+
+            $$ = (Node *)n;
+        }
+    | property_key_name ':' expr
+        {
+            cypher_map_projection_element *n;
+
+            n = make_ag_node(cypher_map_projection_element);
+            n->type = LITERAL_ENTRY;
+            n->key = $1;
+            n->value = (Node *)$3;
+            n->location = @1;
+
+            $$ = (Node *)n;
+        }
+    | '.' '*'
+        {
+            cypher_map_projection_element *n;
+
+            n = make_ag_node(cypher_map_projection_element);
+            n->type = ALL_PROPERTIES_SELECTOR;
+            n->key = NULL;
+            n->value = NULL;
+            n->location = @1;
+
+            $$ = (Node *)n;
+        }
+    ;
+
 list:
     '[' expr_list_opt ']'
         {
diff --git a/src/include/nodes/ag_nodes.h b/src/include/nodes/ag_nodes.h
index 23d68393..84f54154 100644
--- a/src/include/nodes/ag_nodes.h
+++ b/src/include/nodes/ag_nodes.h
@@ -47,6 +47,8 @@ typedef enum ag_node_tag
     cypher_bool_const_t,
     cypher_param_t,
     cypher_map_t,
+    cypher_map_projection_t,
+    cypher_map_projection_element_t,
     cypher_list_t,
     // comparison expression
     cypher_comparison_aexpr_t,
diff --git a/src/include/nodes/cypher_nodes.h b/src/include/nodes/cypher_nodes.h
index a825764b..7af97931 100644
--- a/src/include/nodes/cypher_nodes.h
+++ b/src/include/nodes/cypher_nodes.h
@@ -210,6 +210,40 @@ typedef struct cypher_map
     bool keep_null; // if false, keyvals with null value are removed
 } cypher_map;
 
+typedef struct cypher_map_projection
+{
+    ExtensibleNode extensible;
+    ColumnRef *map_var; /* must be a map, vertex or an edge */
+    List *map_elements; /* list of cypher_map_projection_element */
+    int location;
+} cypher_map_projection;
+
+typedef enum cypher_map_projection_element_type
+{
+    PROPERTY_SELECTOR = 0,  /* map_var { .key } */
+    VARIABLE_SELECTOR,      /* map_var { value } */
+    LITERAL_ENTRY,          /* map_var { key: value } */
+    ALL_PROPERTIES_SELECTOR /* map_var { .* } */
+} cypher_map_projection_element_type;
+
+typedef struct cypher_map_projection_element
+{
+    ExtensibleNode extensible;
+    cypher_map_projection_element_type type;
+
+    /*
+     * key and/or value can be null depending on the type
+     *
+     * For PROPERTY_SELECTOR, value is null.
+     * For VARIABLE_SELECTOR, key is null, and value is a ColumnRef.
+     * For LITERAL_ENTRY, none is null (value is an Expr).
+     * For ALL_PROPERTIES_SELECTOR, both are null.
+     */
+    char *key;
+    Node *value;
+    int location;
+} cypher_map_projection_element;
+
 typedef struct cypher_list
 {
     ExtensibleNode extensible;
diff --git a/src/include/nodes/cypher_outfuncs.h 
b/src/include/nodes/cypher_outfuncs.h
index 3963be0d..fc4782d2 100644
--- a/src/include/nodes/cypher_outfuncs.h
+++ b/src/include/nodes/cypher_outfuncs.h
@@ -46,6 +46,7 @@ void out_cypher_relationship(StringInfo str, const 
ExtensibleNode *node);
 void out_cypher_bool_const(StringInfo str, const ExtensibleNode *node);
 void out_cypher_param(StringInfo str, const ExtensibleNode *node);
 void out_cypher_map(StringInfo str, const ExtensibleNode *node);
+void out_cypher_map_projection(StringInfo str, const ExtensibleNode *node);
 void out_cypher_list(StringInfo str, const ExtensibleNode *node);
 
 // comparison expression

Reply via email to