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 2021/03/17 23:18:17 UTC

[incubator-age] branch master updated: Add *, optional edges, and VLE grammar components.

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/incubator-age.git


The following commit(s) were added to refs/heads/master by this push:
     new afee553  Add *, optional edges, and VLE grammar components.
afee553 is described below

commit afee55301f61d3bf8ede7ec96682e468e5861e38
Author: John Gemignani <jr...@gmail.com>
AuthorDate: Tue Mar 16 10:46:35 2021 -0700

    Add *, optional edges, and VLE grammar components.
    
    Added * as a valid grammar item for the RETURN command. For example -
    
        MATCH (u) RETURN *;
    
    Added components to mask out our "hidden" variables and aliases.
    
    Added the option to omit edges in the grammar. For example -
    
        MATCH (u)--(v) RETURN u, v;
        MATCH (u)-->(p)<--(v) RETURN u, p, v;
    
    Added the components for Variable Length Edges (Relationships). However,
    VLE commands are disallowed until implementation.
    
    Added regression tests.
---
 regress/expected/expr.out              | 121 ++++++++++++++++++++++++++++++
 regress/sql/expr.sql                   |  22 ++++++
 src/backend/nodes/outfuncs.c           |   2 +
 src/backend/parser/cypher_clause.c     |  43 +++++++++--
 src/backend/parser/cypher_gram.y       | 105 +++++++++++++++++++++++++-
 src/backend/parser/cypher_item.c       | 130 +++++++++++++++++++++++++++++++++
 src/include/nodes/cypher_nodes.h       |   1 +
 src/include/parser/cypher_parse_node.h |   3 +-
 8 files changed, 414 insertions(+), 13 deletions(-)

diff --git a/regress/expected/expr.out b/regress/expected/expr.out
index 5266eeb..d0661c4 100644
--- a/regress/expected/expr.out
+++ b/regress/expected/expr.out
@@ -5023,9 +5023,130 @@ $$) AS (i agtype);
  {"key": "value"}
 (9 rows)
 
+-- RETURN * and (u)--(v) optional forms
+SELECT create_graph('opt_forms');
+NOTICE:  graph "opt_forms" has been created
+ create_graph 
+--------------
+ 
+(1 row)
+
+SELECT * FROM cypher('opt_forms', $$CREATE ({i:1})-[:KNOWS]->({i:2})<-[:KNOWS]-({i:3})$$)AS (result agtype);
+ result 
+--------
+(0 rows)
+
+SELECT * FROM cypher('opt_forms', $$MATCH (u) RETURN u$$) AS (result agtype);
+                                result                                
+----------------------------------------------------------------------
+ {"id": 281474976710657, "label": "", "properties": {"i": 1}}::vertex
+ {"id": 281474976710658, "label": "", "properties": {"i": 2}}::vertex
+ {"id": 281474976710659, "label": "", "properties": {"i": 3}}::vertex
+(3 rows)
+
+SELECT * FROM cypher('opt_forms', $$MATCH (u) RETURN *$$) AS (result agtype);
+                                result                                
+----------------------------------------------------------------------
+ {"id": 281474976710657, "label": "", "properties": {"i": 1}}::vertex
+ {"id": 281474976710658, "label": "", "properties": {"i": 2}}::vertex
+ {"id": 281474976710659, "label": "", "properties": {"i": 3}}::vertex
+(3 rows)
+
+SELECT * FROM cypher('opt_forms', $$MATCH (u)--(v) RETURN u.i, v.i$$) AS (u agtype, v agtype);
+ u | v 
+---+---
+ 2 | 3
+ 3 | 2
+ 1 | 2
+ 2 | 1
+(4 rows)
+
+SELECT * FROM cypher('opt_forms', $$MATCH (u)-->(v) RETURN u.i, v.i$$) AS (u agtype, v agtype);
+ u | v 
+---+---
+ 3 | 2
+ 1 | 2
+(2 rows)
+
+SELECT * FROM cypher('opt_forms', $$MATCH (u)<--(v) RETURN u.i, v.i$$) AS (u agtype, v agtype);
+ u | v 
+---+---
+ 2 | 3
+ 2 | 1
+(2 rows)
+
+SELECT * FROM cypher('opt_forms', $$MATCH (u)-->()<--(v) RETURN u.i, v.i$$) AS (u agtype, v agtype);
+ u | v 
+---+---
+ 3 | 1
+ 1 | 3
+(2 rows)
+
+SELECT * FROM cypher('opt_forms', $$MATCH (u) CREATE (u)-[:edge]->() RETURN *$$) AS (results agtype);
+                               results                                
+----------------------------------------------------------------------
+ {"id": 281474976710657, "label": "", "properties": {"i": 1}}::vertex
+ {"id": 281474976710658, "label": "", "properties": {"i": 2}}::vertex
+ {"id": 281474976710659, "label": "", "properties": {"i": 3}}::vertex
+(3 rows)
+
+SELECT * FROM cypher('opt_forms', $$MATCH (u)-->()<--(v) RETURN *$$) AS (col1 agtype, col2 agtype);
+                                 col1                                 |                                 col2                                 
+----------------------------------------------------------------------+----------------------------------------------------------------------
+ {"id": 281474976710659, "label": "", "properties": {"i": 3}}::vertex | {"id": 281474976710657, "label": "", "properties": {"i": 1}}::vertex
+ {"id": 281474976710657, "label": "", "properties": {"i": 1}}::vertex | {"id": 281474976710659, "label": "", "properties": {"i": 3}}::vertex
+(2 rows)
+
+-- VLE
+SELECT create_graph('VLE');
+NOTICE:  graph "VLE" has been created
+ create_graph 
+--------------
+ 
+(1 row)
+
+-- should fail
+SELECT * FROM cypher('VLE', $$MATCH (u)-[*]-(v) RETURN u, v$$) AS (u agtype, v agtype);
+ERROR:  variable length relationships are not supported
+LINE 1: SELECT * FROM cypher('VLE', $$MATCH (u)-[*]-(v) RETURN u, v$...
+                                                ^
+SELECT * FROM cypher('VLE', $$MATCH (u)-[*0..1]-(v) RETURN u, v$$) AS (u agtype, v agtype);
+ERROR:  variable length relationships are not supported
+LINE 1: SELECT * FROM cypher('VLE', $$MATCH (u)-[*0..1]-(v) RETURN u...
+                                                ^
+SELECT * FROM cypher('VLE', $$MATCH (u)-[*..1]-(v) RETURN u, v$$) AS (u agtype, v agtype);
+ERROR:  variable length relationships are not supported
+LINE 1: SELECT * FROM cypher('VLE', $$MATCH (u)-[*..1]-(v) RETURN u,...
+                                                ^
+SELECT * FROM cypher('VLE', $$MATCH (u)-[*..5]-(v) RETURN u, v$$) AS (u agtype, v agtype);
+ERROR:  variable length relationships are not supported
+LINE 1: SELECT * FROM cypher('VLE', $$MATCH (u)-[*..5]-(v) RETURN u,...
+                                                ^
 --
 -- Cleanup
 --
+SELECT * FROM drop_graph('VLE', true);
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to table "VLE"._ag_label_vertex
+drop cascades to table "VLE"._ag_label_edge
+NOTICE:  graph "VLE" has been dropped
+ drop_graph 
+------------
+ 
+(1 row)
+
+SELECT * FROM drop_graph('opt_forms', true);
+NOTICE:  drop cascades to 4 other objects
+DETAIL:  drop cascades to table opt_forms._ag_label_vertex
+drop cascades to table opt_forms._ag_label_edge
+drop cascades to table opt_forms."KNOWS"
+drop cascades to table opt_forms.edge
+NOTICE:  graph "opt_forms" has been dropped
+ drop_graph 
+------------
+ 
+(1 row)
+
 SELECT * FROM drop_graph('type_coercion', true);
 NOTICE:  drop cascades to 3 other objects
 DETAIL:  drop cascades to table type_coercion._ag_label_vertex
diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql
index dff0702..295c2b3 100644
--- a/regress/sql/expr.sql
+++ b/regress/sql/expr.sql
@@ -2085,9 +2085,31 @@ SELECT * FROM cypher('order_by', $$
 	ORDER BY u.i DESC
 $$) AS (i agtype);
 
+-- RETURN * and (u)--(v) optional forms
+SELECT create_graph('opt_forms');
+SELECT * FROM cypher('opt_forms', $$CREATE ({i:1})-[:KNOWS]->({i:2})<-[:KNOWS]-({i:3})$$)AS (result agtype);
+SELECT * FROM cypher('opt_forms', $$MATCH (u) RETURN u$$) AS (result agtype);
+SELECT * FROM cypher('opt_forms', $$MATCH (u) RETURN *$$) AS (result agtype);
+SELECT * FROM cypher('opt_forms', $$MATCH (u)--(v) RETURN u.i, v.i$$) AS (u agtype, v agtype);
+SELECT * FROM cypher('opt_forms', $$MATCH (u)-->(v) RETURN u.i, v.i$$) AS (u agtype, v agtype);
+SELECT * FROM cypher('opt_forms', $$MATCH (u)<--(v) RETURN u.i, v.i$$) AS (u agtype, v agtype);
+SELECT * FROM cypher('opt_forms', $$MATCH (u)-->()<--(v) RETURN u.i, v.i$$) AS (u agtype, v agtype);
+SELECT * FROM cypher('opt_forms', $$MATCH (u) CREATE (u)-[:edge]->() RETURN *$$) AS (results agtype);
+SELECT * FROM cypher('opt_forms', $$MATCH (u)-->()<--(v) RETURN *$$) AS (col1 agtype, col2 agtype);
+
+-- VLE
+SELECT create_graph('VLE');
+-- should fail
+SELECT * FROM cypher('VLE', $$MATCH (u)-[*]-(v) RETURN u, v$$) AS (u agtype, v agtype);
+SELECT * FROM cypher('VLE', $$MATCH (u)-[*0..1]-(v) RETURN u, v$$) AS (u agtype, v agtype);
+SELECT * FROM cypher('VLE', $$MATCH (u)-[*..1]-(v) RETURN u, v$$) AS (u agtype, v agtype);
+SELECT * FROM cypher('VLE', $$MATCH (u)-[*..5]-(v) RETURN u, v$$) AS (u agtype, v agtype);
+
 --
 -- Cleanup
 --
+SELECT * FROM drop_graph('VLE', true);
+SELECT * FROM drop_graph('opt_forms', true);
 SELECT * FROM drop_graph('type_coercion', true);
 SELECT * FROM drop_graph('order_by', true);
 SELECT * FROM drop_graph('group_by', true);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 347f4fa..eb65ce7 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -167,7 +167,9 @@ void out_cypher_relationship(StringInfo str, const ExtensibleNode *node)
     write_string_field(name);
     write_string_field(label);
     write_node_field(props);
+    write_node_field(varlen);
     write_enum_field(dir, cypher_rel_dir);
+    write_location_field(location);
 }
 
 /*
diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c
index 04cc006..f49379f 100644
--- a/src/backend/parser/cypher_clause.c
+++ b/src/backend/parser/cypher_clause.c
@@ -59,6 +59,25 @@
 #include "utils/agtype.h"
 #include "utils/graphid.h"
 
+/*
+ * Variable string names for makeTargetEntry. As they are going to be variable
+ * names that will be hidden from the user, we need to do our best to make sure
+ * they won't be picked by mistake. Additionally, their form needs to be easily
+ * determined as ours. For now, prefix them as follows -
+ *
+ *     #define AGE_VARNAME_SOMETHING AGE_DEFAULT_VARNAME_PREFIX"something"
+ *
+ * We should probably make an automated variable generator, like for aliases,
+ * for this.
+ *
+ * Also, keep these here as nothing outside of this file needs to know these.
+ */
+#define AGE_VARNAME_CREATE_CLAUSE AGE_DEFAULT_VARNAME_PREFIX"create_clause"
+#define AGE_VARNAME_CREATE_NULL_VALUE AGE_DEFAULT_VARNAME_PREFIX"create_null_value"
+#define AGE_VARNAME_DELETE_CLAUSE AGE_DEFAULT_VARNAME_PREFIX"delete_clause"
+#define AGE_VARNAME_ID AGE_DEFAULT_VARNAME_PREFIX"id"
+#define AGE_VARNAME_SET_CLAUSE AGE_DEFAULT_VARNAME_PREFIX"set_clause"
+
 enum transform_entity_type
 {
     ENT_VERTEX = 0x0,
@@ -348,7 +367,7 @@ static Query *transform_cypher_delete(cypher_parsestate *cpstate,
 
     // Create the target entry
     tle = makeTargetEntry(func_expr, pstate->p_next_resno++,
-                          "cypher_delete_clause", false);
+                          AGE_VARNAME_DELETE_CLAUSE, false);
     query->targetList = lappend(query->targetList, tle);
 
     query->rtable = pstate->p_rtable;
@@ -480,7 +499,7 @@ static Query *transform_cypher_set(cypher_parsestate *cpstate,
 
     // Create the target entry
     tle = makeTargetEntry(func_expr, pstate->p_next_resno++,
-                          SET_CLAUSE_FUNCTION_NAME, false);
+                          AGE_VARNAME_SET_CLAUSE, false);
     query->targetList = lappend(query->targetList, tle);
 
     query->rtable = pstate->p_rtable;
@@ -1295,11 +1314,11 @@ static char *get_next_default_alias(cypher_parsestate *cpstate)
 
     sprintf(alias_num, "%d", cpstate->default_alias_num++);
 
-    base_length = strlen(AG_DEFAULT_ALIAS_BASE);
+    base_length = strlen(AGE_DEFAULT_ALIAS_PREFIX);
 
     alias = palloc0(sizeof(char) * (12 + base_length));
 
-    strncat(alias, AG_DEFAULT_ALIAS_BASE, base_length);
+    strncat(alias, AGE_DEFAULT_ALIAS_PREFIX, base_length);
 
     strncat(alias + base_length, alias_num, 12);
 
@@ -2051,6 +2070,14 @@ static Expr *transform_cypher_edge(cypher_parsestate *cpstate,
     TargetEntry *te;
     Expr *expr;
 
+    if (rel->varlen != NULL)
+    {
+        ereport(ERROR,
+                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                 errmsg("variable length relationships are not supported"),
+                 parser_errposition(pstate, rel->location)));
+    }
+
     if (!rel->label)
         rel->label = AG_DEFAULT_LABEL_EDGE;
     else
@@ -2389,7 +2416,7 @@ static Query *transform_cypher_create(cypher_parsestate *cpstate,
 
     null_const = makeNullConst(AGTYPEOID, -1, InvalidOid);
     tle = makeTargetEntry((Expr *)null_const, pstate->p_next_resno++,
-                          "cypher_create_null_value", false);
+                          AGE_VARNAME_CREATE_NULL_VALUE, false);
     query->targetList = lappend(query->targetList, tle);
 
     /*
@@ -2421,7 +2448,7 @@ static Query *transform_cypher_create(cypher_parsestate *cpstate,
 
     // Create the target entry
     tle = makeTargetEntry(func_expr, pstate->p_next_resno++,
-                          "cypher_create_clause", false);
+                          AGE_VARNAME_CREATE_CLAUSE, false);
     query->targetList = lappend(query->targetList, tle);
 
     query->rtable = pstate->p_rtable;
@@ -2618,7 +2645,7 @@ transform_create_cypher_edge(cypher_parsestate *cpstate, List **target_list,
     id = (Expr *)build_column_default(label_relation,
                                       Anum_ag_label_edge_table_id);
 
-    te = makeTargetEntry(id, InvalidAttrNumber, "id", false);
+    te = makeTargetEntry(id, InvalidAttrNumber, AGE_VARNAME_ID, false);
     targetList = lappend(targetList, te);
 
     rel->targetList = targetList;
@@ -2865,7 +2892,7 @@ transform_create_cypher_new_node(cypher_parsestate *cpstate,
     // id
     id = cypher_create_id_default(cpstate, label_relation, ENT_VERTEX);
 
-    te = makeTargetEntry((Expr *)id, InvalidAttrNumber, "id", false);
+    te = makeTargetEntry((Expr *)id, InvalidAttrNumber, AGE_VARNAME_ID, false);
     rel->targetList = list_make1(te);
     rel->id_var_no = -1;
 
diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y
index debedc7..1db0fbd 100644
--- a/src/backend/parser/cypher_gram.y
+++ b/src/backend/parser/cypher_gram.y
@@ -104,8 +104,9 @@
 %type <integer> order_opt
 
 /* MATCH clause */
-%type <node> match
-
+%type <node> match cypher_varlen_opt cypher_range_opt cypher_range_idx
+             cypher_range_idx_opt
+%type <integer> Iconst
 /* CREATE clause */
 %type <node> create
 
@@ -291,6 +292,71 @@ updating_clause:
     | delete
     ;
 
+cypher_varlen_opt:
+    '*' cypher_range_opt
+        {
+            A_Indices *n = (A_Indices *) $2;
+
+            if (n->lidx == NULL)
+                n->lidx = make_int_const(1, @2);
+
+            if (n->uidx != NULL)
+            {
+                A_Const    *lidx = (A_Const *) n->lidx;
+                A_Const    *uidx = (A_Const *) n->uidx;
+
+                if (lidx->val.val.ival > uidx->val.val.ival)
+                    ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR),
+                                    errmsg("invalid range"),
+                                    ag_scanner_errposition(@2, scanner)));
+            }
+            $$ = (Node *) n;
+        }
+    | /* EMPTY */
+        {
+            $$ = NULL;
+        }
+    ;
+
+cypher_range_opt:
+    cypher_range_idx
+        {
+            A_Indices  *n;
+
+            n = makeNode(A_Indices);
+            n->lidx = copyObject($1);
+            n->uidx = $1;
+            $$ = (Node *) n;
+        }
+    | cypher_range_idx_opt DOT_DOT cypher_range_idx_opt
+        {
+            A_Indices  *n;
+
+            n = makeNode(A_Indices);
+            n->lidx = $1;
+            n->uidx = $3;
+            $$ = (Node *) n;
+        }
+    | /* EMPTY */
+        {
+            $$ = (Node *) makeNode(A_Indices);
+        }
+    ;
+
+cypher_range_idx:
+    Iconst
+        {
+            $$ = make_int_const($1, @1);
+        }
+    ;
+
+cypher_range_idx_opt:
+                        cypher_range_idx
+                        | /* EMPTY */                   { $$ = NULL; }
+                ;
+
+Iconst: INTEGER
+
 /*
  * RETURN and WITH clause
  */
@@ -361,6 +427,23 @@ return_item:
 
             $$ = (Node *)n;
         }
+    | '*'
+        {
+            ColumnRef *cr;
+            ResTarget *rt;
+
+            cr = makeNode(ColumnRef);
+            cr->fields = list_make1(makeNode(A_Star));
+            cr->location = @1;
+
+            rt = makeNode(ResTarget);
+            rt->name = NULL;
+            rt->indirection = NIL;
+            rt->val = (Node *)cr;
+            rt->location = @1;
+
+            $$ = (Node *)rt;
+        }
     ;
 
 order_by_opt:
@@ -794,14 +877,28 @@ path_relationship:
     ;
 
 path_relationship_body:
-    '[' var_name_opt label_opt properties_opt ']'
+    '[' var_name_opt label_opt cypher_varlen_opt properties_opt ']'
         {
             cypher_relationship *n;
 
             n = make_ag_node(cypher_relationship);
             n->name = $2;
             n->label = $3;
-            n->props = $4;
+            n->varlen = $4;
+            n->props = $5;
+
+            $$ = (Node *)n;
+        }
+    |
+    /* empty */
+        {
+            cypher_relationship *n;
+
+            n = make_ag_node(cypher_relationship);
+            n->name = NULL;
+            n->label = NULL;
+            n->varlen = NULL;
+            n->props = NULL;
 
             $$ = (Node *)n;
         }
diff --git a/src/backend/parser/cypher_item.c b/src/backend/parser/cypher_item.c
index 9cfc1d7..5799bf4 100644
--- a/src/backend/parser/cypher_item.c
+++ b/src/backend/parser/cypher_item.c
@@ -26,12 +26,17 @@
 #include "nodes/pg_list.h"
 #include "nodes/primnodes.h"
 #include "parser/parse_node.h"
+#include "parser/parse_relation.h"
 #include "parser/parse_target.h"
 
 #include "parser/cypher_expr.h"
 #include "parser/cypher_item.h"
 #include "parser/cypher_parse_node.h"
 
+static List *ExpandAllTables(ParseState *pstate, int location);
+static List *expand_rel_attrs(ParseState *pstate, RangeTblEntry *rte,
+                              int rtindex, int sublevels_up, int location);
+
 // see transformTargetEntry()
 TargetEntry *transform_cypher_item(cypher_parsestate *cpstate, Node *node,
                                    Node *expr, ParseExprKind expr_kind,
@@ -57,12 +62,41 @@ List *transform_cypher_item_list(cypher_parsestate *cpstate, List *item_list,
     ListCell *li;
     List *group_clause = NIL;
     bool hasAgg = false;
+    bool expand_star;
+
+    expand_star = (expr_kind != EXPR_KIND_UPDATE_SOURCE);
 
     foreach (li, item_list)
     {
         ResTarget *item = lfirst(li);
         TargetEntry *te;
 
+        if (expand_star)
+        {
+            if (IsA(item->val, ColumnRef))
+            {
+                ColumnRef  *cref = (ColumnRef *) item->val;
+
+                if (IsA(llast(cref->fields), A_Star))
+                {
+                    ParseState *pstate = &cpstate->pstate;
+
+                    /* we only allow a bare '*' */
+                    if (list_length(cref->fields) != 1)
+                    {
+                        ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR),
+                                        errmsg("Invalid number of fields for *"),
+                                        parser_errposition(pstate,
+                                                           cref->location)));
+                    }
+
+                    target_list = list_concat(target_list,
+                                              ExpandAllTables(pstate,
+                                                              cref->location));
+                    continue;
+                }
+            }
+        }
         /* clear the exprHasAgg flag to check transform for an aggregate */
         cpstate->exprHasAgg = false;
 
@@ -78,9 +112,13 @@ List *transform_cypher_item_list(cypher_parsestate *cpstate, List *item_list,
          * an aggregate in an expression
          */
         if (!cpstate->exprHasAgg)
+        {
             group_clause = lappend(group_clause, item->val);
+        }
         else
+        {
             hasAgg = true;
+        }
     }
 
     /*
@@ -89,7 +127,99 @@ List *transform_cypher_item_list(cypher_parsestate *cpstate, List *item_list,
      * will verify if it is valid.
      */
     if (hasAgg)
+    {
         *groupClause = group_clause;
+    }
 
     return target_list;
 }
+
+/*
+ * From PG's ExpandAllTables()
+ *     Transforms '*' (in the target list) into a list of targetlist entries.
+ */
+static List *ExpandAllTables(ParseState *pstate, int location)
+{
+    List *target = NIL;
+    bool found_table = false;
+    ListCell *l;
+
+    foreach(l, pstate->p_namespace)
+    {
+        ParseNamespaceItem *nsitem = (ParseNamespaceItem *) lfirst(l);
+        RangeTblEntry *rte = nsitem->p_rte;
+
+        /* Ignore table-only items */
+        if (!nsitem->p_cols_visible)
+            continue;
+        /* Should not have any lateral-only items when parsing targetlist */
+        Assert(!nsitem->p_lateral_only);
+        /* Remember we found a p_cols_visible item */
+        found_table = true;
+
+        target = list_concat(target, expand_rel_attrs(pstate, rte,
+                                                      RTERangeTablePosn(pstate,
+                                                                        rte,
+                                                                        NULL),
+                                                      0, location));
+    }
+
+    /* Check for "RETURN *;" */
+    if (!found_table)
+        ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR),
+                        errmsg("RETURN * without a pattern is not valid"),
+                        parser_errposition(pstate, location)));
+
+    return target;
+}
+
+/*
+ * From PG's expandRelAttrs
+ * Modified to exclude hidden variables and aliases in RETURN *
+ */
+static List *expand_rel_attrs(ParseState *pstate, RangeTblEntry *rte,
+                              int rtindex, int sublevels_up, int location)
+{
+    List *names, *vars;
+    ListCell *name, *var;
+    List *te_list = NIL;
+    int var_prefix_len = strlen(AGE_DEFAULT_VARNAME_PREFIX);
+    int alias_prefix_len = strlen(AGE_DEFAULT_ALIAS_PREFIX);
+
+    expandRTE(rte, rtindex, sublevels_up, location, false, &names, &vars);
+
+    /*
+     * Require read access to the table.  This is normally redundant with the
+     * markVarForSelectPriv calls below, but not if the table has zero
+     * columns.
+     */
+    rte->requiredPerms |= ACL_SELECT;
+
+    /* iterate through the variables */
+    forboth(name, names, var, vars)
+    {
+        char *label = strVal(lfirst(name));
+        Var *varnode = (Var *)lfirst(var);
+        TargetEntry *te;
+
+        /* we want to skip our "hidden" variables */
+        if (strncmp(AGE_DEFAULT_VARNAME_PREFIX, label, var_prefix_len) == 0)
+            continue;
+
+        /* we want to skip out "hidden" aliases */
+        if (strncmp(AGE_DEFAULT_ALIAS_PREFIX, label, alias_prefix_len) == 0)
+            continue;
+
+        /* add this variable to the list */
+        te = makeTargetEntry((Expr *)varnode,
+                             (AttrNumber)pstate->p_next_resno++, label, false);
+        te_list = lappend(te_list, te);
+
+        /* Require read access to each column */
+        markVarForSelectPriv(pstate, varnode, rte);
+    }
+
+    Assert(name == NULL && var == NULL);    /* lists not the same length? */
+
+    return te_list;
+}
diff --git a/src/include/nodes/cypher_nodes.h b/src/include/nodes/cypher_nodes.h
index c4ef763..99b7860 100644
--- a/src/include/nodes/cypher_nodes.h
+++ b/src/include/nodes/cypher_nodes.h
@@ -142,6 +142,7 @@ typedef struct cypher_relationship
     char *name;
     char *label;
     Node *props; // map or parameter
+    Node *varlen; // variable length relationships (A_Indices)
     cypher_rel_dir dir;
     int location;
 } cypher_relationship;
diff --git a/src/include/parser/cypher_parse_node.h b/src/include/parser/cypher_parse_node.h
index 5066c6c..dc7581b 100644
--- a/src/include/parser/cypher_parse_node.h
+++ b/src/include/parser/cypher_parse_node.h
@@ -23,7 +23,8 @@
 #include "nodes/primnodes.h"
 #include "parser/parse_node.h"
 
-#define AG_DEFAULT_ALIAS_BASE "_ag_default_alias_"
+#define AGE_DEFAULT_ALIAS_PREFIX "_age_default_alias_"
+#define AGE_DEFAULT_VARNAME_PREFIX "_age_varname_"
 
 typedef struct cypher_parsestate
 {