Skip to content

Commit

Permalink
Add openCypher range() function AGE2-422
Browse files Browse the repository at this point in the history
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 [].
  • Loading branch information
jrgemignani committed Oct 6, 2021
1 parent c1d84dd commit 486c850
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 8 deletions.
7 changes: 7 additions & 0 deletions age--0.6.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
--
12 changes: 6 additions & 6 deletions regress/expected/catalog.out
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
78 changes: 77 additions & 1 deletion regress/expected/expr.out
Original file line number Diff line number Diff line change
Expand Up @@ -5378,14 +5378,15 @@ 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
--------------

(1 row)

-- relationships()
SELECT * from cypher('list', $$CREATE p=()-[:knows]->() RETURN p$$) as (path agtype);
path
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -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
--
Expand Down
20 changes: 19 additions & 1 deletion regress/sql/expr.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down
181 changes: 181 additions & 0 deletions src/backend/utils/adt/agtype.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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));
}

0 comments on commit 486c850

Please sign in to comment.