Implement chained expression order of operations (#1402)

Implements functionality for order of operations (parentheses) in
chained operations.

Added new nodes and functions to accomodate this feature.

Added relevant regression tests for order of operations in chained
expressions.

Modified a previous regression test case that had its location
reporting changed by this implementation.
diff --git a/regress/expected/cypher_call.out b/regress/expected/cypher_call.out
index 8c5c5fc..6980abe 100644
--- a/regress/expected/cypher_call.out
+++ b/regress/expected/cypher_call.out
@@ -84,8 +84,8 @@
 /* CALL YIELD WHERE, should fail */
 SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1$$) as (sqrt agtype);
 ERROR:  Cannot use standalone CALL with WHERE
-LINE 2: ...r('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1$$...
-                                                             ^
+LINE 2: SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sq...
+                                             ^
 HINT:  Instead use `CALL ... WITH * WHERE ... RETURN *`
 /*
  * subquery
diff --git a/regress/expected/expr.out b/regress/expected/expr.out
index 43b4080..e55285a 100644
--- a/regress/expected/expr.out
+++ b/regress/expected/expr.out
@@ -808,6 +808,135 @@
  {"id": 844424930131973, "label": "people", "properties": {"age": 15, "name": "David"}}::vertex
 (3 rows)
 
+-- order of operations
+-- expressions
+SELECT * FROM cypher('chained', $$ RETURN 1 = 1 = 1 $$) AS (result agtype);
+ result 
+--------
+ true
+(1 row)
+
+SELECT * FROM cypher('chained', $$ RETURN 1 = 2 = 1 $$) AS (result agtype);
+ result 
+--------
+ false
+(1 row)
+
+SELECT * FROM cypher('chained', $$ RETURN (1 = 1) = 1 $$) AS (result agtype);
+ result 
+--------
+ false
+(1 row)
+
+SELECT * FROM cypher('chained', $$ RETURN 1 = (1 = 1) $$) AS (result agtype);
+ result 
+--------
+ false
+(1 row)
+
+SELECT * FROM cypher('chained', $$ RETURN 1 = 1 = true $$) AS (result agtype);
+ result 
+--------
+ false
+(1 row)
+
+SELECT * FROM cypher('chained', $$ RETURN (1 = 1) = true $$) AS (result agtype);
+ result 
+--------
+ true
+(1 row)
+
+SELECT * FROM cypher('chained', $$ RETURN true = ((1 = 1) = true) $$) AS (result agtype);
+ result 
+--------
+ true
+(1 row)
+
+SELECT * FROM cypher('chained', $$ RETURN ((1 = 1) = 1) = 1 $$) AS (result agtype);
+ result 
+--------
+ false
+(1 row)
+
+SELECT * FROM cypher('chained', $$ RETURN (1 = (1 = 1)) = 1 $$) AS (result agtype);
+ result 
+--------
+ false
+(1 row)
+
+SELECT * FROM cypher('chained', $$ RETURN ((1 = (1 = 1)) = 1) = 1 $$) AS (result agtype);
+ result 
+--------
+ false
+(1 row)
+
+-- in clause
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 35 = u.age = 35  RETURN u $$) AS (result agtype);
+                                              result                                               
+---------------------------------------------------------------------------------------------------
+ {"id": 844424930131971, "label": "people", "properties": {"age": 35, "name": "Samantha"}}::vertex
+(1 row)
+
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE (35 = u.age) = 35  RETURN u $$) AS (result agtype);
+ result 
+--------
+(0 rows)
+
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE (35 = 35) = u.age  RETURN u $$) AS (result agtype);
+ result 
+--------
+(0 rows)
+
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE u.age = u.age = u.age  RETURN u $$) AS (result agtype);
+                                              result                                               
+---------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "people", "properties": {"age": 50, "name": "Jason"}}::vertex
+ {"id": 844424930131970, "label": "people", "properties": {"age": 25, "name": "Amy"}}::vertex
+ {"id": 844424930131971, "label": "people", "properties": {"age": 35, "name": "Samantha"}}::vertex
+ {"id": 844424930131972, "label": "people", "properties": {"age": 40, "name": "Mark"}}::vertex
+ {"id": 844424930131973, "label": "people", "properties": {"age": 15, "name": "David"}}::vertex
+(5 rows)
+
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE (u.age = u.age) = u.age  RETURN u $$) AS (result agtype);
+ result 
+--------
+(0 rows)
+
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE u.age = (u.age = u.age)  RETURN u $$) AS (result agtype);
+ result 
+--------
+(0 rows)
+
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE (u.age = u.age) = (u.age = u.age)  RETURN u $$) AS (result agtype);
+                                              result                                               
+---------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "people", "properties": {"age": 50, "name": "Jason"}}::vertex
+ {"id": 844424930131970, "label": "people", "properties": {"age": 25, "name": "Amy"}}::vertex
+ {"id": 844424930131971, "label": "people", "properties": {"age": 35, "name": "Samantha"}}::vertex
+ {"id": 844424930131972, "label": "people", "properties": {"age": 40, "name": "Mark"}}::vertex
+ {"id": 844424930131973, "label": "people", "properties": {"age": 15, "name": "David"}}::vertex
+(5 rows)
+
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE u.age = (u.age = (u.age = u.age))  RETURN u $$) AS (result agtype);
+ result 
+--------
+(0 rows)
+
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE u.age = 35 = ((u.age = u.age) = u.age) RETURN u $$) AS (result agtype);
+ result 
+--------
+(0 rows)
+
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE ((u.age = u.age) = (u.age = u.age)) = (u.age = u.age)  RETURN u $$) AS (result agtype);
+                                              result                                               
+---------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "people", "properties": {"age": 50, "name": "Jason"}}::vertex
+ {"id": 844424930131970, "label": "people", "properties": {"age": 25, "name": "Amy"}}::vertex
+ {"id": 844424930131971, "label": "people", "properties": {"age": 35, "name": "Samantha"}}::vertex
+ {"id": 844424930131972, "label": "people", "properties": {"age": 40, "name": "Mark"}}::vertex
+ {"id": 844424930131973, "label": "people", "properties": {"age": 15, "name": "David"}}::vertex
+(5 rows)
+
 --
 -- Test transform logic for IS NULL & IS NOT NULL
 --
diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql
index 6f7c585..85065e0 100644
--- a/regress/sql/expr.sql
+++ b/regress/sql/expr.sql
@@ -328,6 +328,31 @@
 -- should return 3
 SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE NOT 35 < u.age + 1 <= 50 RETURN u $$) AS (result agtype);
 
+-- order of operations
+-- expressions
+SELECT * FROM cypher('chained', $$ RETURN 1 = 1 = 1 $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ RETURN 1 = 2 = 1 $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ RETURN (1 = 1) = 1 $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ RETURN 1 = (1 = 1) $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ RETURN 1 = 1 = true $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ RETURN (1 = 1) = true $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ RETURN true = ((1 = 1) = true) $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ RETURN ((1 = 1) = 1) = 1 $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ RETURN (1 = (1 = 1)) = 1 $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ RETURN ((1 = (1 = 1)) = 1) = 1 $$) AS (result agtype);
+
+-- in clause
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 35 = u.age = 35  RETURN u $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE (35 = u.age) = 35  RETURN u $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE (35 = 35) = u.age  RETURN u $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE u.age = u.age = u.age  RETURN u $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE (u.age = u.age) = u.age  RETURN u $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE u.age = (u.age = u.age)  RETURN u $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE (u.age = u.age) = (u.age = u.age)  RETURN u $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE u.age = (u.age = (u.age = u.age))  RETURN u $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE u.age = 35 = ((u.age = u.age) = u.age) RETURN u $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE ((u.age = u.age) = (u.age = u.age)) = (u.age = u.age)  RETURN u $$) AS (result agtype);
+
 --
 -- Test transform logic for IS NULL & IS NOT NULL
 --
diff --git a/src/backend/nodes/ag_nodes.c b/src/backend/nodes/ag_nodes.c
index 14715bd..dee460b 100644
--- a/src/backend/nodes/ag_nodes.c
+++ b/src/backend/nodes/ag_nodes.c
@@ -48,6 +48,8 @@
     "cypher_param",
     "cypher_map",
     "cypher_list",
+    "cypher_comparison_aexpr",
+    "cypher_comparison_boolexpr",
     "cypher_string_match",
     "cypher_typecast",
     "cypher_integer_const",
@@ -111,6 +113,8 @@
     DEFINE_NODE_METHODS(cypher_param),
     DEFINE_NODE_METHODS(cypher_map),
     DEFINE_NODE_METHODS(cypher_list),
+    DEFINE_NODE_METHODS(cypher_comparison_aexpr),
+    DEFINE_NODE_METHODS(cypher_comparison_boolexpr),
     DEFINE_NODE_METHODS(cypher_string_match),
     DEFINE_NODE_METHODS(cypher_typecast),
     DEFINE_NODE_METHODS(cypher_integer_const),
diff --git a/src/backend/nodes/cypher_outfuncs.c b/src/backend/nodes/cypher_outfuncs.c
index 4053a39..594c615 100644
--- a/src/backend/nodes/cypher_outfuncs.c
+++ b/src/backend/nodes/cypher_outfuncs.c
@@ -259,6 +259,28 @@
     WRITE_LOCATION_FIELD(location);
 }
 
+// serialization function for the cypher_comparison_aexpr ExtensibleNode.
+void out_cypher_comparison_aexpr(StringInfo str, const ExtensibleNode *node)
+{
+    DEFINE_AG_NODE(cypher_comparison_aexpr);
+
+    WRITE_ENUM_FIELD(kind, A_Expr_Kind);
+    WRITE_NODE_FIELD(name);
+    WRITE_NODE_FIELD(lexpr);
+    WRITE_NODE_FIELD(rexpr);
+    WRITE_LOCATION_FIELD(location);
+}
+
+// serialization function for the cypher_comparison_boolexpr ExtensibleNode.
+void out_cypher_comparison_boolexpr(StringInfo str, const ExtensibleNode *node)
+{
+    DEFINE_AG_NODE(cypher_comparison_boolexpr);
+
+    WRITE_ENUM_FIELD(boolop, BoolExprType);
+    WRITE_NODE_FIELD(args);
+    WRITE_LOCATION_FIELD(location);
+}
+
 // serialization function for the cypher_string_match ExtensibleNode.
 void out_cypher_string_match(StringInfo str, const ExtensibleNode *node)
 {
diff --git a/src/backend/parser/cypher_expr.c b/src/backend/parser/cypher_expr.c
index 4e3e9d4..ac08164 100644
--- a/src/backend/parser/cypher_expr.c
+++ b/src/backend/parser/cypher_expr.c
@@ -73,7 +73,11 @@
 static Node *transform_A_Indirection(cypher_parsestate *cpstate,
                                      A_Indirection *a_ind);
 static Node *transform_AEXPR_OP(cypher_parsestate *cpstate, A_Expr *a);
+static Node *transform_cypher_comparison_aexpr_OP(cypher_parsestate *cpstate,
+                                                  cypher_comparison_aexpr *a);
 static Node *transform_BoolExpr(cypher_parsestate *cpstate, BoolExpr *expr);
+static Node *transform_cypher_comparison_boolexpr(cypher_parsestate *cpstate,
+                                                  cypher_comparison_boolexpr *b);
 static Node *transform_cypher_bool_const(cypher_parsestate *cpstate,
                                          cypher_bool_const *bc);
 static Node *transform_cypher_integer_const(cypher_parsestate *cpstate,
@@ -193,6 +197,12 @@
         if (is_ag_node(expr, cypher_typecast))
             return transform_cypher_typecast(cpstate,
                                              (cypher_typecast *)expr);
+        if (is_ag_node(expr, cypher_comparison_aexpr))
+            return transform_cypher_comparison_aexpr_OP(cpstate,
+                                             (cypher_comparison_aexpr *)expr);
+        if (is_ag_node(expr, cypher_comparison_boolexpr))
+            return transform_cypher_comparison_boolexpr(cpstate,
+                                             (cypher_comparison_boolexpr *)expr);
         ereport(ERROR,
                 (errmsg_internal("unrecognized ExtensibleNode: %s",
                                  ((ExtensibleNode *)expr)->extnodename)));
@@ -488,6 +498,25 @@
                            a->location);
 }
 
+/*
+ * function for transforming cypher comparision A_Expr. Since this node is a
+ * wrapper to let us know when a comparison occurs in a chained comparison,
+ * we convert it to a regular A_expr and transform it.
+ */
+static Node *transform_cypher_comparison_aexpr_OP(cypher_parsestate *cpstate,
+                                                  cypher_comparison_aexpr *a)
+{
+    A_Expr *n = makeNode(A_Expr);
+    n->kind = a->kind;
+    n->name = a->name;
+    n->lexpr = a->lexpr;
+    n->rexpr = a->rexpr;
+    n->location = a->location;
+
+    return (Node *)transform_AEXPR_OP(cpstate, n);
+}
+
+
 static Node *transform_AEXPR_IN(cypher_parsestate *cpstate, A_Expr *a)
 {
     ParseState *pstate = (ParseState *)cpstate;
@@ -660,6 +689,24 @@
     return (Node *)makeBoolExpr(expr->boolop, args, expr->location);
 }
 
+/*
+ * function for transforming cypher_comparison_boolexpr. Since this node is a
+ * wrapper to let us know when a comparison occurs in a chained comparison,
+ * we convert it to a PG BoolExpr and transform it.
+ */
+static Node *transform_cypher_comparison_boolexpr(cypher_parsestate *cpstate,
+                                                  cypher_comparison_boolexpr *b)
+{
+    BoolExpr *n = makeNode(BoolExpr);
+
+    n->boolop = b->boolop;
+    n->args = b->args;
+    n->location = b->location;
+
+    return transform_BoolExpr(cpstate, n);
+}
+
+
 static Node *transform_cypher_bool_const(cypher_parsestate *cpstate,
                                          cypher_bool_const *bc)
 {
diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y
index 4f7fddf..1e3adb3 100644
--- a/src/backend/parser/cypher_gram.y
+++ b/src/backend/parser/cypher_gram.y
@@ -205,6 +205,12 @@
 static Node *make_and_expr(Node *lexpr, Node *rexpr, int location);
 static Node *make_xor_expr(Node *lexpr, Node *rexpr, int location);
 static Node *make_not_expr(Node *expr, int location);
+static Node *make_comparison_and_expr(Node *lexpr, Node *rexpr, int location);
+static Node *make_cypher_comparison_aexpr(A_Expr_Kind kind, char *name,
+                                          Node *lexpr, Node *rexpr,
+                                          int location);
+static Node *make_cypher_comparison_boolexpr(BoolExprType boolop, List *args,
+                                               int location);
 
 // arithmetic operators
 static Node *do_negate(Node *n, int location);
@@ -240,7 +246,7 @@
                                                int left_arg_location,
                                                int cr_location);
 // comparison
-static bool is_A_Expr_a_comparison_operation(A_Expr *a);
+static bool is_A_Expr_a_comparison_operation(cypher_comparison_aexpr *a);
 static Node *build_comparison_expression(Node *left_grammar_node,
                                          Node *right_grammar_node,
                                          char *opr_name, int location);
@@ -1776,7 +1782,14 @@
         }
     | '(' expr ')'
         {
-            $$ = $2;
+            Node *n = $2;
+
+            if (is_ag_node(n, cypher_comparison_aexpr) ||
+                is_ag_node(n, cypher_comparison_boolexpr))
+            {
+                n = (Node *)node_to_agtype(n, "boolean", @2);
+            }
+            $$ = n;
         }
     | expr_case
     | expr_var
@@ -2116,6 +2129,51 @@
 }
 
 /*
+ * chained expression comparison operators
+ */
+
+static Node *make_cypher_comparison_aexpr(A_Expr_Kind kind, char *name,
+                 Node *lexpr, Node *rexpr, int location)
+{
+    cypher_comparison_aexpr *a = make_ag_node(cypher_comparison_aexpr);
+
+    a->kind = kind;
+    a->name = list_make1(makeString((char *) name));
+    a->lexpr = lexpr;
+    a->rexpr = rexpr;
+    a->location = location;
+    return (Node *)a;
+}
+
+static Node *make_cypher_comparison_boolexpr(BoolExprType boolop, List *args, int location)
+{
+    cypher_comparison_boolexpr *b = make_ag_node(cypher_comparison_boolexpr);
+
+    b->boolop = boolop;
+    b->args = args;
+    b->location = location;
+    return (Node *)b;
+}
+
+static Node *make_comparison_and_expr(Node *lexpr, Node *rexpr, int location)
+{
+    // flatten "a AND b AND c ..." to a single BoolExpr on sight
+    if (is_ag_node(lexpr, cypher_comparison_boolexpr))
+    {
+        cypher_comparison_boolexpr *bexpr = (cypher_comparison_boolexpr *)lexpr;
+
+        if (bexpr->boolop == AND_EXPR)
+        {
+            bexpr->args = lappend(bexpr->args, rexpr);
+
+            return (Node *)bexpr;
+        }
+    }
+
+    return (Node *)make_cypher_comparison_boolexpr(AND_EXPR, list_make2(lexpr, rexpr), location);
+}
+
+/*
  * arithmetic operators
  */
 
@@ -2535,7 +2593,7 @@
 }
 
 /* check if A_Expr is a comparison expression */
-static bool is_A_Expr_a_comparison_operation(A_Expr *a)
+static bool is_A_Expr_a_comparison_operation(cypher_comparison_aexpr *a)
 {
     String *v = NULL;
     char *opr_name = NULL;
@@ -2601,75 +2659,77 @@
 
     /*
      * Case 1:
-     *    If the previous expression is an A_Expr and it is also a
+     *    If the left expression is an A_Expr and it is also a
      *    comparison, then this is part of a chained comparison. In this
      *    specific case, the second chained element.
      */
-    if (IsA(left_grammar_node, A_Expr) &&
-        is_A_Expr_a_comparison_operation((A_Expr *)left_grammar_node))
+    if (is_ag_node(left_grammar_node, cypher_comparison_aexpr) &&
+        is_A_Expr_a_comparison_operation((cypher_comparison_aexpr *)left_grammar_node))
     {
-        A_Expr *aexpr = NULL;
+        cypher_comparison_aexpr *aexpr = NULL;
         Node *lexpr = NULL;
         Node *n = NULL;
 
         /* get the A_Expr on the left side */
-        aexpr = (A_Expr *) left_grammar_node;
+        aexpr = (cypher_comparison_aexpr *)left_grammar_node;
         /* get its rexpr which will be our lexpr */
         lexpr = aexpr->rexpr;
         /* build our comparison operator */
-        n = (Node *)makeSimpleA_Expr(AEXPR_OP, opr_name, lexpr,
+        n = (Node *)make_cypher_comparison_aexpr(AEXPR_OP, opr_name, lexpr,
                                      right_grammar_node, location);
 
         /* now add it (AND) to the other comparison */
-        result_expr = make_and_expr(left_grammar_node, n, location);
+        result_expr = make_comparison_and_expr(left_grammar_node, n, location);
     }
 
     /*
      * Case 2:
-     *    If the previous expression is a boolean AND and its right most
+     *    If the left expression is a boolean AND and its right most
      *    expression is an A_Expr and a comparison, then this is part of
      *    a chained comparison. In this specific case, the third and
      *    beyond chained element.
      */
-    if (IsA(left_grammar_node, BoolExpr) &&
-        ((BoolExpr*)left_grammar_node)->boolop == AND_EXPR)
+    else if (is_ag_node(left_grammar_node, cypher_comparison_boolexpr) &&
+        ((cypher_comparison_boolexpr*)left_grammar_node)->boolop == AND_EXPR)
     {
-        BoolExpr *bexpr = NULL;
+        cypher_comparison_boolexpr *bexpr = NULL;
         Node *last = NULL;
 
         /* cast the left to a boolean */
-        bexpr = (BoolExpr *)left_grammar_node;
+        bexpr = (cypher_comparison_boolexpr *)left_grammar_node;
         /* extract the last node - ANDs are chained in a flat list */
         last = llast(bexpr->args);
 
         /* is the last node an A_Expr and a comparison operator */
-        if (IsA(last, A_Expr) &&
-            is_A_Expr_a_comparison_operation((A_Expr *)last))
+        if (is_ag_node(last, cypher_comparison_aexpr) &&
+            is_A_Expr_a_comparison_operation((cypher_comparison_aexpr *)last))
         {
-            A_Expr *aexpr = NULL;
+            cypher_comparison_aexpr *aexpr = NULL;
             Node *lexpr = NULL;
             Node *n = NULL;
 
             /* get the last expressions right expression */
-            aexpr = (A_Expr *) last;
+            aexpr = (cypher_comparison_aexpr *) last;
             lexpr = aexpr->rexpr;
             /* make our comparison operator */
-            n = (Node *)makeSimpleA_Expr(AEXPR_OP, opr_name, lexpr,
+            n = (Node *)make_cypher_comparison_aexpr(AEXPR_OP, opr_name, lexpr,
                                          right_grammar_node, location);
 
             /* now add it (AND) to the other comparisons */
-            result_expr = make_and_expr(left_grammar_node, n, location);
+            result_expr = make_comparison_and_expr(left_grammar_node, n, location);
         }
     }
 
+
     /*
      * Case 3:
-     *    The previous expression isn't a chained comparison. So, treat
-     *    it as a regular comparison expression.
+     *    The left expression isn't a chained comparison. So, treat
+     *    it as a regular comparison expression. This is usually an initial
+     *    comparison expression.
      */
-    if (result_expr == NULL)
+    else if (result_expr == NULL)
     {
-        result_expr = (Node *)makeSimpleA_Expr(AEXPR_OP, opr_name,
+        result_expr = (Node *)make_cypher_comparison_aexpr(AEXPR_OP, opr_name,
                                                left_grammar_node,
                                                right_grammar_node, location);
     }
diff --git a/src/include/nodes/ag_nodes.h b/src/include/nodes/ag_nodes.h
index e51a8de..481c75b 100644
--- a/src/include/nodes/ag_nodes.h
+++ b/src/include/nodes/ag_nodes.h
@@ -51,6 +51,9 @@
     cypher_param_t,
     cypher_map_t,
     cypher_list_t,
+    // comparison expression
+    cypher_comparison_aexpr_t,
+    cypher_comparison_boolexpr_t,
     // string match
     cypher_string_match_t,
     // typecast
diff --git a/src/include/nodes/cypher_nodes.h b/src/include/nodes/cypher_nodes.h
index 0b2f653..c80c082 100644
--- a/src/include/nodes/cypher_nodes.h
+++ b/src/include/nodes/cypher_nodes.h
@@ -240,6 +240,29 @@
 } cypher_create_path;
 
 /*
+ * comparison expressions
+ */
+
+typedef struct cypher_comparison_aexpr
+{
+    ExtensibleNode extensible;
+    A_Expr_Kind kind; /* see above */
+    List *name; /* possibly-qualified name of operator */
+    Node *lexpr; /* left argument, or NULL if none */
+    Node *rexpr; /* right argument, or NULL if none */
+    int location; /* token location, or -1 if unknown */
+} cypher_comparison_aexpr;
+
+typedef struct cypher_comparison_boolexpr
+{
+    ExtensibleNode extensible;
+    BoolExprType boolop;
+    List       *args;           /* arguments to this expression */
+    int         location;       /* token location, or -1 if unknown */
+} cypher_comparison_boolexpr;
+
+
+/*
  * procedure call
  */
 
diff --git a/src/include/nodes/cypher_outfuncs.h b/src/include/nodes/cypher_outfuncs.h
index 836765b..4071b28 100644
--- a/src/include/nodes/cypher_outfuncs.h
+++ b/src/include/nodes/cypher_outfuncs.h
@@ -53,6 +53,10 @@
 void out_cypher_map(StringInfo str, const ExtensibleNode *node);
 void out_cypher_list(StringInfo str, const ExtensibleNode *node);
 
+// comparison expression
+void out_cypher_comparison_aexpr(StringInfo str, const ExtensibleNode *node);
+void out_cypher_comparison_boolexpr(StringInfo str, const ExtensibleNode *node);
+
 // string match
 void out_cypher_string_match(StringInfo str, const ExtensibleNode *node);