From 273d2144f7df1533c85270032c49784f5a363064 Mon Sep 17 00:00:00 2001 From: Rafsun Masud Date: Fri, 22 Mar 2024 15:02:18 -0700 Subject: [PATCH] Implement map projection (#1710) 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(-) create mode 100644 regress/expected/map_projection.out create mode 100644 regress/sql/map_projection.sql diff --git a/Makefile b/Makefile index ef97774bc..1224fc2ae 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 000000000..dcb7f0e76 --- /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 000000000..72fc2fb2e --- /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 a41cdd0ba..858ca5a51 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 2904503f4..5c175a30c 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 631446586..de9fcd16d 100644 --- a/src/backend/parser/cypher_expr.c +++ b/src/backend/parser/cypher_expr.c @@ -73,6 +73,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, @@ -187,6 +189,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); @@ -220,7 +227,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: @@ -826,6 +833,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 9a3060917..8806d7ea9 100644 --- a/src/backend/parser/cypher_gram.y +++ b/src/backend/parser/cypher_gram.y @@ -152,6 +152,9 @@ %type expr_case expr_case_when expr_case_default %type expr_case_when_list +%type map_projection map_projection_elem +%type map_projection_elem_list + %type expr_var expr_func expr_func_norm expr_func_subexpr %type expr_list expr_list_opt map_keyval_list_opt map_keyval_list %type property_value @@ -2006,6 +2009,7 @@ expr_literal: $$ = make_null_const(@1); } | map + | map_projection | list ; @@ -2040,6 +2044,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 23d683936..84f541546 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 1af2390fd..b4c363f6d 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 3963be0d0..fc4782d29 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