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 7da17ab  Add chained comparisons to openCypher
7da17ab is described below

commit 7da17ab97d0a1ecacf403eb77a1bb193dca402ab
Author: John Gemignani <[email protected]>
AuthorDate: Thu Apr 7 15:13:32 2022 -0700

    Add chained comparisons to openCypher
    
    Added chained comparisons to AGE's openCypher specification.
    
    Added regression tests.
---
 regress/expected/expr.out        | 111 +++++++++++++++++++++++++
 regress/sql/expr.sql             |  27 +++++++
 src/backend/parser/cypher_gram.y | 171 ++++++++++++++++++++++++++++++++++++---
 3 files changed, 298 insertions(+), 11 deletions(-)

diff --git a/regress/expected/expr.out b/regress/expected/expr.out
index 73dde0a..5c8df1f 100644
--- a/regress/expected/expr.out
+++ b/regress/expected/expr.out
@@ -456,6 +456,106 @@ $$) AS r(result boolean);
  t
 (1 row)
 
+--
+-- Test chained comparisons
+--
+SELECT * FROM create_graph('chained');
+NOTICE:  graph "chained" has been created
+ create_graph 
+--------------
+ 
+(1 row)
+
+SELECT * FROM cypher('chained', $$ CREATE (:people {name: "Jason", age:50}) 
$$) AS (result agtype);
+ result 
+--------
+(0 rows)
+
+SELECT * FROM cypher('chained', $$ CREATE (:people {name: "Amy", age:25}) $$) 
AS (result agtype);
+ result 
+--------
+(0 rows)
+
+SELECT * FROM cypher('chained', $$ CREATE (:people {name: "Samantha", age:35}) 
$$) AS (result agtype);
+ result 
+--------
+(0 rows)
+
+SELECT * FROM cypher('chained', $$ CREATE (:people {name: "Mark", age:40}) $$) 
AS (result agtype);
+ result 
+--------
+(0 rows)
+
+SELECT * FROM cypher('chained', $$ CREATE (:people {name: "David", age:15}) 
$$) AS (result agtype);
+ result 
+--------
+(0 rows)
+
+-- should return 1
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 35 < u.age <= 49  
RETURN u $$) AS (result agtype);
+                                            result                             
                
+-----------------------------------------------------------------------------------------------
+ {"id": 844424930131972, "label": "people", "properties": {"age": 40, "name": 
"Mark"}}::vertex
+(1 row)
+
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 25 <= u.age <= 25  
RETURN u $$) AS (result agtype);
+                                            result                             
               
+----------------------------------------------------------------------------------------------
+ {"id": 844424930131970, "label": "people", "properties": {"age": 25, "name": 
"Amy"}}::vertex
+(1 row)
+
+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 50 > u.age > 35  
RETURN u $$) AS (result agtype);
+                                            result                             
                
+-----------------------------------------------------------------------------------------------
+ {"id": 844424930131972, "label": "people", "properties": {"age": 40, "name": 
"Mark"}}::vertex
+(1 row)
+
+-- should return 3
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 40 <> u.age <> 35 
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": 844424930131973, "label": "people", "properties": {"age": 15, "name": 
"David"}}::vertex
+(3 rows)
+
+-- should return 2
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 30 <= u.age <= 49 > 
u.age RETURN u $$) AS (result agtype);
+                                              result                           
                    
+---------------------------------------------------------------------------------------------------
+ {"id": 844424930131971, "label": "people", "properties": {"age": 35, "name": 
"Samantha"}}::vertex
+ {"id": 844424930131972, "label": "people", "properties": {"age": 40, "name": 
"Mark"}}::vertex
+(2 rows)
+
+-- should return 0
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 30 <= u.age <= 49 = 
u.age RETURN u $$) AS (result agtype);
+ result 
+--------
+(0 rows)
+
+-- should return 2
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 35 < u.age + 1 <= 50 
RETURN u $$) AS (result agtype);
+                                              result                           
                    
+---------------------------------------------------------------------------------------------------
+ {"id": 844424930131971, "label": "people", "properties": {"age": 35, "name": 
"Samantha"}}::vertex
+ {"id": 844424930131972, "label": "people", "properties": {"age": 40, "name": 
"Mark"}}::vertex
+(2 rows)
+
+-- should return 3
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE NOT 35 < u.age + 1 
<= 50 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": 844424930131973, "label": "people", "properties": {"age": 15, "name": 
"David"}}::vertex
+(3 rows)
+
 --
 -- Test transform logic for IS NULL & IS NOT NULL
 --
@@ -5912,6 +6012,17 @@ ERROR:  labels() argument must be a vertex
 --
 -- Cleanup
 --
+SELECT * FROM drop_graph('chained', true);
+NOTICE:  drop cascades to 3 other objects
+DETAIL:  drop cascades to table chained._ag_label_vertex
+drop cascades to table chained._ag_label_edge
+drop cascades to table chained.people
+NOTICE:  graph "chained" has been dropped
+ drop_graph 
+------------
+ 
+(1 row)
+
 SELECT * FROM drop_graph('VLE', true);
 NOTICE:  drop cascades to 9 other objects
 DETAIL:  drop cascades to table "VLE"._ag_label_vertex
diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql
index 45a95d4..745388d 100644
--- a/regress/sql/expr.sql
+++ b/regress/sql/expr.sql
@@ -235,6 +235,32 @@ SELECT * FROM cypher('expr', $$
 RETURN (1 + 1.0) = (7 % 5)
 $$) AS r(result boolean);
 
+--
+-- Test chained comparisons
+--
+
+SELECT * FROM create_graph('chained');
+SELECT * FROM cypher('chained', $$ CREATE (:people {name: "Jason", age:50}) 
$$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ CREATE (:people {name: "Amy", age:25}) $$) 
AS (result agtype);
+SELECT * FROM cypher('chained', $$ CREATE (:people {name: "Samantha", age:35}) 
$$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ CREATE (:people {name: "Mark", age:40}) $$) 
AS (result agtype);
+SELECT * FROM cypher('chained', $$ CREATE (:people {name: "David", age:15}) 
$$) AS (result agtype);
+-- should return 1
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 35 < u.age <= 49  
RETURN u $$) AS (result agtype);
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 25 <= u.age <= 25  
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 50 > u.age > 35  
RETURN u $$) AS (result agtype);
+-- should return 3
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 40 <> u.age <> 35 
RETURN u $$) AS (result agtype);
+-- should return 2
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 30 <= u.age <= 49 > 
u.age RETURN u $$) AS (result agtype);
+-- should return 0
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 30 <= u.age <= 49 = 
u.age RETURN u $$) AS (result agtype);
+-- should return 2
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE 35 < u.age + 1 <= 50 
RETURN u $$) AS (result agtype);
+-- should return 3
+SELECT * FROM cypher('chained', $$ MATCH (u:people) WHERE NOT 35 < u.age + 1 
<= 50 RETURN u $$) AS (result agtype);
+
 --
 -- Test transform logic for IS NULL & IS NOT NULL
 --
@@ -2374,6 +2400,7 @@ SELECT * from cypher('list', $$RETURN labels("string")$$) 
as (Labels agtype);
 --
 -- Cleanup
 --
+SELECT * FROM drop_graph('chained', true);
 SELECT * FROM drop_graph('VLE', true);
 SELECT * FROM drop_graph('case_statement', true);
 SELECT * FROM drop_graph('opt_forms', true);
diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y
index c08f01d..906cd6e 100644
--- a/src/backend/parser/cypher_gram.y
+++ b/src/backend/parser/cypher_gram.y
@@ -162,7 +162,7 @@
 %left AND
 %left XOR
 %right NOT
-%nonassoc '=' NOT_EQ '<' LT_EQ '>' GT_EQ
+%left '=' NOT_EQ '<' LT_EQ '>' GT_EQ
 %left '+' '-'
 %left '*' '/' '%'
 %left '^'
@@ -210,7 +210,14 @@ static Node *make_typecast_expr(Node *expr, char 
*typecast, int location);
 static Node *make_function_expr(List *func_name, List *exprs, int location);
 
 // setops
-static Node *make_set_op(SetOperation op, bool all_or_distinct, List *larg, 
List *rarg);
+static Node *make_set_op(SetOperation op, bool all_or_distinct, List *larg,
+                         List *rarg);
+
+// comparison
+static bool is_A_Expr_a_comparison_operation(A_Expr *a);
+static Node *build_comparison_expression(Node *left_grammar_node,
+                                         Node *right_grammar_node,
+                                         char *opr_name, int location);
 %}
 %%
 
@@ -1280,27 +1287,27 @@ expr:
         }
     | expr '=' expr
         {
-            $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "=", $1, $3, @2);
+            $$ = build_comparison_expression($1, $3, "=", @2);
         }
     | expr NOT_EQ expr
         {
-            $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "<>", $1, $3, @2);
+            $$ = build_comparison_expression($1, $3, "<>", @2);
         }
     | expr '<' expr
         {
-            $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "<", $1, $3, @2);
+            $$ = build_comparison_expression($1, $3, "<", @2);
         }
     | expr LT_EQ expr
         {
-            $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "<=", $1, $3, @2);
+            $$ = build_comparison_expression($1, $3, "<=", @2);
         }
     | expr '>' expr
         {
-            $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, ">", $1, $3, @2);
+            $$ = build_comparison_expression($1, $3, ">", @2);
         }
     | expr GT_EQ expr
         {
-            $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, ">=", $1, $3, @2);
+            $$ = build_comparison_expression($1, $3, ">=", @2);
         }
     | expr '+' expr
         {
@@ -2197,9 +2204,8 @@ static unsigned long get_a_unique_number(void)
 }
 
 /*set operation function node to make a set op node*/
-
-static Node *
-make_set_op(SetOperation op, bool all_or_distinct, List *larg, List *rarg)
+static Node *make_set_op(SetOperation op, bool all_or_distinct, List *larg,
+                         List *rarg)
 {
     cypher_return *n = make_ag_node(cypher_return);
 
@@ -2209,3 +2215,146 @@ make_set_op(SetOperation op, bool all_or_distinct, List 
*larg, List *rarg)
     n->rarg = (List *) rarg;
     return (Node *) n;
 }
+
+/* check if A_Expr is a comparison expression */
+static bool is_A_Expr_a_comparison_operation(A_Expr *a)
+{
+    Value *v = NULL;
+    char *opr_name = NULL;
+
+    /* we don't support qualified comparison operators */
+    if (list_length(a->name) != 1)
+    {
+        ereport(ERROR,
+                (errcode(ERRCODE_SYNTAX_ERROR),
+                 errmsg("qualified comparison operator names are not 
permitted")));
+    }
+
+    /* get the value and verify that it is a string */
+    v = linitial(a->name);
+    Assert(v->type == T_String);
+
+    /* get the string value */
+    opr_name = v->val.str;
+
+    /* verify it is a comparison operation */
+    if (strcmp(opr_name, "<") == 0)
+    {
+        return true;
+    }
+    if (strcmp(opr_name, ">") == 0)
+    {
+        return true;
+    }
+    if (strcmp(opr_name, "<=") == 0)
+    {
+        return true;
+    }
+    if (strcmp(opr_name, "=>") == 0)
+    {
+        return true;
+    }
+    if (strcmp(opr_name, "=") == 0)
+    {
+        return true;
+    }
+    if (strcmp(opr_name, "<>") == 0)
+    {
+        return true;
+    }
+
+    return false;
+}
+
+/*
+ * Helper function to build the comparison operator expression. It will also
+ * build a chained comparison operator expression if it detects a chained
+ * comparison.
+ */
+static Node *build_comparison_expression(Node *left_grammar_node,
+                                         Node *right_grammar_node,
+                                         char *opr_name, int location)
+{
+    Node *result_expr = NULL;
+
+    Assert(left_grammar_node != NULL);
+    Assert(right_grammar_node != NULL);
+    Assert(opr_name != NULL);
+
+    /*
+     * Case 1:
+     *    If the previous 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))
+    {
+        A_Expr *aexpr = NULL;
+        Node *lexpr = NULL;
+        Node *n = NULL;
+
+        /* get the A_Expr on the left side */
+        aexpr = (A_Expr *) 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,
+                                     right_grammar_node, location);
+
+        /* now add it (AND) to the other comparison */
+        result_expr = make_and_expr(left_grammar_node, n, location);
+    }
+
+    /*
+     * Case 2:
+     *    If the previous 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)
+    {
+        BoolExpr *bexpr = NULL;
+        Node *last = NULL;
+
+        /* cast the left to a boolean */
+        bexpr = (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))
+        {
+            A_Expr *aexpr = NULL;
+            Node *lexpr = NULL;
+            Node *n = NULL;
+
+            /* get the last expressions right expression */
+            aexpr = (A_Expr *) last;
+            lexpr = aexpr->rexpr;
+            /* make our comparison operator */
+            n = (Node *)makeSimpleA_Expr(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);
+        }
+    }
+
+    /*
+     * Case 3:
+     *    The previous expression isn't a chained comparison. So, treat
+     *    it as a regular comparison expression.
+     */
+    if (result_expr == NULL)
+    {
+        result_expr = (Node *)makeSimpleA_Expr(AEXPR_OP, opr_name,
+                                               left_grammar_node,
+                                               right_grammar_node, location);
+    }
+
+    return result_expr;
+}

Reply via email to