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;
+}