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/10/07 01:04:34 UTC

[incubator-age] branch master updated: Add openCypher range() function AGE2-422

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 486c850  Add openCypher range() function AGE2-422
486c850 is described below

commit 486c8500bd86356bbb5166684bb1da9d0939c291
Author: John Gemignani <jr...@gmail.com>
AuthorDate: Tue Oct 5 17:31:40 2021 -0700

    Add openCypher range() function AGE2-422
    
    Added the openCypher range() function. This is Jira ticket
    AGE2-422.
    
    Added regression tests.
    
    NOTE:
    
    This range() function differs slightly from some other
    implementations in that it will return an empty list for start,
    end, and step combinations that wouldn't result in anything being
    generated. Some other implementations will generate errors instead.
    
    For example: range(0, -10, 1) or range(0, -10) will return [].
---
 age--0.6.0.sql                 |   7 ++
 regress/expected/catalog.out   |  12 +--
 regress/expected/expr.out      |  78 +++++++++++++++++-
 regress/sql/expr.sql           |  20 ++++-
 src/backend/utils/adt/agtype.c | 181 +++++++++++++++++++++++++++++++++++++++++
 5 files changed, 290 insertions(+), 8 deletions(-)

diff --git a/age--0.6.0.sql b/age--0.6.0.sql
index 314288f..9a8b957 100644
--- a/age--0.6.0.sql
+++ b/age--0.6.0.sql
@@ -3744,6 +3744,13 @@ STABLE
 PARALLEL SAFE
 AS 'MODULE_PATHNAME';
 
+CREATE FUNCTION ag_catalog.age_range(variadic "any")
+RETURNS agtype
+LANGUAGE c
+STABLE
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
 --
 -- End
 --
diff --git a/regress/expected/catalog.out b/regress/expected/catalog.out
index 3e7a90f..f79e217 100644
--- a/regress/expected/catalog.out
+++ b/regress/expected/catalog.out
@@ -337,10 +337,10 @@ NOTICE:  ELabel "r" has been created
 SELECT * FROM ag_label;
        name       | graph | id | kind |      relation      
 ------------------+-------+----+------+--------------------
- _ag_label_vertex | 17625 |  1 | v    | g._ag_label_vertex
- _ag_label_edge   | 17625 |  2 | e    | g._ag_label_edge
- n                | 17625 |  3 | v    | g.n
- r                | 17625 |  4 | e    | g.r
+ _ag_label_vertex | 17627 |  1 | v    | g._ag_label_vertex
+ _ag_label_edge   | 17627 |  2 | e    | g._ag_label_edge
+ n                | 17627 |  3 | v    | g.n
+ r                | 17627 |  4 | e    | g.r
 (4 rows)
 
 -- try to create duplicate labels
@@ -367,8 +367,8 @@ NOTICE:  label "g"."r" has been dropped
 SELECT * FROM ag_label;
        name       | graph | id | kind |      relation      
 ------------------+-------+----+------+--------------------
- _ag_label_vertex | 17625 |  1 | v    | g._ag_label_vertex
- _ag_label_edge   | 17625 |  2 | e    | g._ag_label_edge
+ _ag_label_vertex | 17627 |  1 | v    | g._ag_label_vertex
+ _ag_label_edge   | 17627 |  2 | e    | g._ag_label_edge
 (2 rows)
 
 -- try to remove labels that is not there
diff --git a/regress/expected/expr.out b/regress/expected/expr.out
index d888509..724bc76 100644
--- a/regress/expected/expr.out
+++ b/regress/expected/expr.out
@@ -5378,7 +5378,7 @@ SELECT * FROM cypher('VLE', $$MATCH (u)-[*..5]-(v) RETURN u, v$$) AS (u agtype,
 ERROR:  variable length relationships are not supported
 LINE 1: SELECT * FROM cypher('VLE', $$MATCH (u)-[*..5]-(v) RETURN u,...
                                                 ^
--- list functions
+-- list functions relationships(), range()
 SELECT create_graph('list');
 NOTICE:  graph "list" has been created
  create_graph 
@@ -5386,6 +5386,7 @@ NOTICE:  graph "list" has been created
  
 (1 row)
 
+-- relationships()
 SELECT * from cypher('list', $$CREATE p=()-[:knows]->() RETURN p$$) as (path agtype);
                                                                                                                                path                                                                                                                                
 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -5434,6 +5435,81 @@ SELECT * from cypher('list', $$MATCH (u) RETURN relationships(u)$$) as (relation
 ERROR:  relationships() argument must be a path
 SELECT * from cypher('list', $$MATCH ()-[e]->() RETURN relationships(e)$$) as (relationships agtype);
 ERROR:  relationships() argument must be a path
+-- range()
+SELECT * from cypher('list', $$RETURN range(0, 10)$$) as (range agtype);
+               range                
+------------------------------------
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+(1 row)
+
+SELECT * from cypher('list', $$RETURN range(0, 10, null)$$) as (range agtype);
+               range                
+------------------------------------
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+(1 row)
+
+SELECT * from cypher('list', $$RETURN range(0, 10, 1)$$) as (range agtype);
+               range                
+------------------------------------
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+(1 row)
+
+SELECT * from cypher('list', $$RETURN range(0, 10, 3)$$) as (range agtype);
+    range     
+--------------
+ [0, 3, 6, 9]
+(1 row)
+
+SELECT * from cypher('list', $$RETURN range(0, -10, -1)$$) as (range agtype);
+                    range                     
+----------------------------------------------
+ [0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10]
+(1 row)
+
+SELECT * from cypher('list', $$RETURN range(0, -10, -3)$$) as (range agtype);
+      range      
+-----------------
+ [0, -3, -6, -9]
+(1 row)
+
+SELECT * from cypher('list', $$RETURN range(0, 10, 11)$$) as (range agtype);
+ range 
+-------
+ [0]
+(1 row)
+
+SELECT * from cypher('list', $$RETURN range(-20, 10, 5)$$) as (range agtype);
+             range             
+-------------------------------
+ [-20, -15, -10, -5, 0, 5, 10]
+(1 row)
+
+-- should return an empty list []
+SELECT * from cypher('list', $$RETURN range(0, -10)$$) as (range agtype);
+ range 
+-------
+ []
+(1 row)
+
+SELECT * from cypher('list', $$RETURN range(0, 10, -1)$$) as (range agtype);
+ range 
+-------
+ []
+(1 row)
+
+SELECT * from cypher('list', $$RETURN range(-10, 10, -1)$$) as (range agtype);
+ range 
+-------
+ []
+(1 row)
+
+-- should return an error
+SELECT * from cypher('list', $$RETURN range(null, -10, -3)$$) as (range agtype);
+ERROR:  range(): neither start or end can be NULL
+SELECT * from cypher('list', $$RETURN range(0, null, -3)$$) as (range agtype);
+ERROR:  range(): neither start or end can be NULL
+SELECT * from cypher('list', $$RETURN range(0, -10.0, -3.0)$$) as (range agtype);
+ERROR:  range() unsupported argument type
 --
 -- Cleanup
 --
diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql
index 4935c69..8f1205b 100644
--- a/regress/sql/expr.sql
+++ b/regress/sql/expr.sql
@@ -2213,8 +2213,9 @@ SELECT * FROM cypher('VLE', $$MATCH (u)-[*0..1]-(v) RETURN u, v$$) AS (u 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);
 
--- list functions
+-- list functions relationships(), range()
 SELECT create_graph('list');
+-- relationships()
 SELECT * from cypher('list', $$CREATE p=()-[:knows]->() RETURN p$$) as (path agtype);
 SELECT * from cypher('list', $$CREATE p=()-[:knows]->()-[:knows]->() RETURN p$$) as (path agtype);
 SELECT * from cypher('list', $$MATCH p=()-[]->() RETURN relationships(p)$$) as (relationships agtype);
@@ -2228,6 +2229,23 @@ SELECT * from cypher('list', $$MATCH (u) RETURN relationships([1,2,3])$$) as (re
 SELECT * from cypher('list', $$MATCH (u) RETURN relationships("string")$$) as (relationships agtype);
 SELECT * from cypher('list', $$MATCH (u) RETURN relationships(u)$$) as (relationships agtype);
 SELECT * from cypher('list', $$MATCH ()-[e]->() RETURN relationships(e)$$) as (relationships agtype);
+-- range()
+SELECT * from cypher('list', $$RETURN range(0, 10)$$) as (range agtype);
+SELECT * from cypher('list', $$RETURN range(0, 10, null)$$) as (range agtype);
+SELECT * from cypher('list', $$RETURN range(0, 10, 1)$$) as (range agtype);
+SELECT * from cypher('list', $$RETURN range(0, 10, 3)$$) as (range agtype);
+SELECT * from cypher('list', $$RETURN range(0, -10, -1)$$) as (range agtype);
+SELECT * from cypher('list', $$RETURN range(0, -10, -3)$$) as (range agtype);
+SELECT * from cypher('list', $$RETURN range(0, 10, 11)$$) as (range agtype);
+SELECT * from cypher('list', $$RETURN range(-20, 10, 5)$$) as (range agtype);
+-- should return an empty list []
+SELECT * from cypher('list', $$RETURN range(0, -10)$$) as (range agtype);
+SELECT * from cypher('list', $$RETURN range(0, 10, -1)$$) as (range agtype);
+SELECT * from cypher('list', $$RETURN range(-10, 10, -1)$$) as (range agtype);
+-- should return an error
+SELECT * from cypher('list', $$RETURN range(null, -10, -3)$$) as (range agtype);
+SELECT * from cypher('list', $$RETURN range(0, null, -3)$$) as (range agtype);
+SELECT * from cypher('list', $$RETURN range(0, -10.0, -3.0)$$) as (range agtype);
 
 --
 -- Cleanup
diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c
index a0b4284..ec01445 100644
--- a/src/backend/utils/adt/agtype.c
+++ b/src/backend/utils/adt/agtype.c
@@ -153,6 +153,8 @@ agtype *get_one_agtype_from_variadic_args(FunctionCallInfo fcinfo,
                                                  int variadic_offset,
                                                  int expected_nargs);
 static agtype_value *integer_to_agtype_value(int64 int_value);
+static int64 get_int64_from_int_datums(Datum d, Oid type, char *funcname,
+                                       bool *is_agnull);
 
 PG_FUNCTION_INFO_V1(agtype_in);
 
@@ -8341,3 +8343,182 @@ Datum age_relationships(PG_FUNCTION_ARGS)
     /* convert the agtype_value to a datum to return to the caller */
     PG_RETURN_POINTER(agtype_value_to_agtype(agis_result.res));
 }
+
+/*
+ * Helper function to convert an integer type (PostgreSQL or agtype) datum into
+ * an int64. The function will flag if an agtype null was found. The function
+ * will error out on invalid information, printing out the funcname passed.
+ */
+static int64 get_int64_from_int_datums(Datum d, Oid type, char *funcname,
+                                       bool *is_agnull)
+{
+    int64 result = 0;
+
+    /* test for PG integer types */
+    if (type == INT2OID)
+    {
+        result = (int64) DatumGetInt16(d);
+    }
+    else if (type == INT4OID)
+    {
+        result = (int64) DatumGetInt32(d);
+    }
+    else if (type == INT8OID)
+    {
+        result = (int64) DatumGetInt64(d);
+    }
+    /* test for agtype integer */
+    else if (type == AGTYPEOID)
+    {
+        agtype *agt_arg = NULL;
+        agtype_value *agtv_value = NULL;
+        agtype_container *agtc = NULL;
+
+        /* get the agtype argument */
+        agt_arg = DATUM_GET_AGTYPE_P(d);
+
+        if (!AGT_ROOT_IS_SCALAR(agt_arg))
+        {
+            ereport(ERROR,
+                    (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                     errmsg("%s() only supports scalar arguments", funcname)));
+        }
+        /* check for agtype null*/
+        agtc = &agt_arg->root;
+        if (AGTE_IS_NULL(agtc->children[0]))
+        {
+            *is_agnull = true;
+            return 0;
+        }
+
+        /* extract it from the scalar array */
+        agtv_value = get_ith_agtype_value_from_container(&agt_arg->root, 0);
+
+        /* check for agtype integer */
+        if (agtv_value->type == AGTV_INTEGER)
+        {
+            result = agtv_value->val.int_value;
+        }
+        else
+        {
+            ereport(ERROR,
+                    (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                     errmsg("%s() unsupported argument type", funcname)));
+        }
+    }
+    else
+    {
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                 errmsg("%s() unsupported argument type", funcname)));
+    }
+
+    /* return the result */
+    *is_agnull = false;
+    return result;
+}
+
+PG_FUNCTION_INFO_V1(age_range);
+/*
+ * Execution function to implement openCypher range() function
+ */
+Datum age_range(PG_FUNCTION_ARGS)
+{
+    Datum *args = NULL;
+    bool *nulls = NULL;
+    Oid *types = NULL;
+    int nargs;
+    int64 start_idx = 0;
+    int64 end_idx = 0;
+    /* step defaults to 1 */
+    int64 step = 1;
+    bool is_agnull = false;
+    agtype_in_state agis_result;
+    int64 i = 0;
+
+    /* get the arguments */
+    nargs = extract_variadic_args(fcinfo, 0, false, &args, &types, &nulls);
+
+    /* throw an error if the number of args is not the expected number */
+    if (nargs != 2 && nargs != 3)
+    {
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                 errmsg("range(): invalid number of input parameters")));
+    }
+
+    /* check for NULL start and end input */
+    if (nulls[0] || nulls[1])
+    {
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                 errmsg("range(): neither start or end can be NULL")));
+    }
+
+    /* get the start index */
+    start_idx = get_int64_from_int_datums(args[0], types[0], "range",
+                                          &is_agnull);
+    if (is_agnull)
+    {
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                 errmsg("range(): start cannot be NULL")));
+    }
+
+    /* get the end index */
+    end_idx = get_int64_from_int_datums(args[1], types[1], "range", &is_agnull);
+    if (is_agnull)
+    {
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                 errmsg("range(): end cannot be NULL")));
+    }
+
+    /* get the step */
+    if (nargs == 3 && !nulls[2])
+    {
+        step = get_int64_from_int_datums(args[2], types[2], "range",
+                                         &is_agnull);
+        if (is_agnull)
+        {
+            step = 1;
+        }
+    }
+
+    /* the step cannot be zero */
+    if (step == 0)
+    {
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                 errmsg("range(): step cannot be zero")));
+    }
+
+    /* clear the result structure */
+    MemSet(&agis_result, 0, sizeof(agtype_in_state));
+
+    /* push the beginning of the array */
+    agis_result.res = push_agtype_value(&agis_result.parse_state,
+                                        WAGT_BEGIN_ARRAY, NULL);
+
+    /* push in each agtype integer in the range */
+    for (i = start_idx;
+         (step > 0 && i <= end_idx) || (step < 0 && i >= end_idx);
+         i += step)
+    {
+        agtype_value agtv;
+
+        /* build the integer */
+        agtv.type = AGTV_INTEGER;
+        agtv.val.int_value = i;
+        /* add the value to the array */
+        agis_result.res = push_agtype_value(&agis_result.parse_state, WAGT_ELEM,
+                                            &agtv);
+    }
+
+    /* push the end of the array */
+    agis_result.res = push_agtype_value(&agis_result.parse_state,
+                                        WAGT_END_ARRAY, NULL);
+
+    /* convert the agtype_value to a datum to return to the caller */
+    PG_RETURN_POINTER(agtype_value_to_agtype(agis_result.res));
+}