This is an automated email from the ASF dual-hosted git repository.
jrgemignani pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/age.git
The following commit(s) were added to refs/heads/master by this push:
new 774e781b Propagate null through agtype arithmetic operators (#2405)
774e781b is described below
commit 774e781b43878fadf829e185be202dd51eb6def8
Author: Muhammad Taha Naveed <[email protected]>
AuthorDate: Mon Apr 27 23:35:00 2026 +0500
Propagate null through agtype arithmetic operators (#2405)
The scalar branch of agtype_add lacked an AGTV_NULL case and fell through
to the scalar-scalar concat path, so `null + 1` produced the two-element
list `[null, 1]` instead of null. This surfaced most visibly inside a
list comprehension projection (e.g. `[x IN [1, null, 2] | x + 1]` yielded
`[2, [null, 1], 3]`) because there both operands are typed agtype and
bypass the agtype_any_add wrapper that already short-circuits on null.
The sibling operators (-, *, /, %, ^, unary -) had the same gap but
surfaced as "Invalid input parameter types" errors rather than concats.
Short-circuit all seven functions to SQL NULL when any operand is
AGTV_NULL. SQL NULL (not a wrapped AGTV_NULL scalar) matches AGE's
existing convention: `RETURN null AS v` already yields SQL NULL at the
row level, and agtype_any_add already returns PG NULL for AGTV_NULL
input, so the two operator-resolution paths stay indistinguishable.
List aggregation re-packs SQL NULL entries as AGTV_NULL, so
`[x IN [1, null, 2] | x + 1]` now renders as `[2, null, 3]`.
Co-authored-by: Claude Opus 4.7 <[email protected]>
---
regress/expected/agtype.out | 44 +++++++++++++++++++++--
regress/expected/list_comprehension.out | 63 +++++++++++++++++++++++++++++++++
regress/sql/agtype.sql | 8 ++++-
regress/sql/list_comprehension.sql | 14 ++++++++
src/backend/utils/adt/agtype_ops.c | 42 ++++++++++++++++++++++
5 files changed, 168 insertions(+), 3 deletions(-)
diff --git a/regress/expected/agtype.out b/regress/expected/agtype.out
index 065f357f..cb97b3bf 100644
--- a/regress/expected/agtype.out
+++ b/regress/expected/agtype.out
@@ -704,6 +704,48 @@ SELECT '3.14::numeric'::agtype + '3.14::numeric'::agtype;
6.28::numeric
(1 row)
+SELECT 'null'::agtype - '1';
+ ?column?
+----------
+
+(1 row)
+
+SELECT 'null'::agtype + '1';
+ ?column?
+----------
+
+(1 row)
+
+SELECT 'null'::agtype * '1';
+ ?column?
+----------
+
+(1 row)
+
+SELECT 'null'::agtype / '1';
+ ?column?
+----------
+
+(1 row)
+
+SELECT 'null'::agtype % '1';
+ ?column?
+----------
+
+(1 row)
+
+SELECT 'null'::agtype ^ '1';
+ ?column?
+----------
+
+(1 row)
+
+SELECT -'null'::agtype;
+ ?column?
+----------
+
+(1 row)
+
--
-- Test operator - for extended functionality
--
@@ -1065,8 +1107,6 @@ SELECT '{"a":1 , "b":2, "c":3}'::agtype - 'null';
ERROR: expected agtype string, not agtype NULL
SELECT '{"a":1 , "b":2, "c":3}'::agtype - '["c","b"]' - '[1]' - '["a"]';
ERROR: expected agtype string, not agtype integer
-SELECT 'null'::agtype - '1';
-ERROR: Invalid input parameter types for agtype_sub
SELECT 'null'::agtype - '[1]';
ERROR: must be object or array, not a scalar value
SELECT '{"id": 1125899906842625, "label": "Vertex", "properties": {"a": "xyz",
"b": true, "c": -19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i":
{"j": 199, "k": {"l": "mnopq"}}}}::vertex'::agtype - '"a"';
diff --git a/regress/expected/list_comprehension.out
b/regress/expected/list_comprehension.out
index e431c0d9..e47b2d56 100644
--- a/regress/expected/list_comprehension.out
+++ b/regress/expected/list_comprehension.out
@@ -746,6 +746,69 @@ SELECT * FROM cypher('list_comprehension', $$ MATCH (u
{list: [0, 2, 4, 6, 8, 10
{"id": 281474976710668, "label": "", "properties": {"b": [0, 1, 2, 3, 4, 5],
"c": [0, 2, 4, 6, 8, 10, 12], "list": [0, 2, 4, 6, 8, 10, 12]}}::vertex
(2 rows)
+-- Issue 2394 - projection over a null element should propagate null,
+-- not concatenate the null and the projection result into a sublist and
+-- not error out for operators other than '+'.
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [null] | x + 1] $$)
AS (result agtype);
+ result
+--------
+ [null]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x +
1] $$) AS (result agtype);
+ result
+--------------
+ [2, null, 3]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | 1 +
x] $$) AS (result agtype);
+ result
+--------------
+ [2, null, 3]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x]
$$) AS (result agtype);
+ result
+--------------
+ [1, null, 2]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x -
1] $$) AS (result agtype);
+ result
+--------------
+ [0, null, 1]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x *
2] $$) AS (result agtype);
+ result
+--------------
+ [2, null, 4]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x /
1] $$) AS (result agtype);
+ result
+--------------
+ [1, null, 2]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x %
2] $$) AS (result agtype);
+ result
+--------------
+ [1, null, 0]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x ^
2] $$) AS (result agtype);
+ result
+------------------
+ [1.0, null, 4.0]
+(1 row)
+
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | -x]
$$) AS (result agtype);
+ result
+----------------
+ [-1, null, -2]
+(1 row)
+
-- Clean up
SELECT * FROM drop_graph('list_comprehension', true);
NOTICE: drop cascades to 4 other objects
diff --git a/regress/sql/agtype.sql b/regress/sql/agtype.sql
index 6dab6bc3..50509776 100644
--- a/regress/sql/agtype.sql
+++ b/regress/sql/agtype.sql
@@ -229,6 +229,13 @@ SELECT '3'::agtype + '3.14'::agtype;
SELECT '3'::agtype + '3.14::numeric'::agtype;
SELECT '3.14'::agtype + '3.14::numeric'::agtype;
SELECT '3.14::numeric'::agtype + '3.14::numeric'::agtype;
+SELECT 'null'::agtype - '1';
+SELECT 'null'::agtype + '1';
+SELECT 'null'::agtype * '1';
+SELECT 'null'::agtype / '1';
+SELECT 'null'::agtype % '1';
+SELECT 'null'::agtype ^ '1';
+SELECT -'null'::agtype;
--
-- Test operator - for extended functionality
@@ -305,7 +312,6 @@ SELECT '{"a":1 , "b":2, "c":3}'::agtype - '[null]';
SELECT '{"a":1 , "b":2, "c":3}'::agtype - '1';
SELECT '{"a":1 , "b":2, "c":3}'::agtype - 'null';
SELECT '{"a":1 , "b":2, "c":3}'::agtype - '["c","b"]' - '[1]' - '["a"]';
-SELECT 'null'::agtype - '1';
SELECT 'null'::agtype - '[1]';
SELECT '{"id": 1125899906842625, "label": "Vertex", "properties": {"a": "xyz",
"b": true, "c": -19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i":
{"j": 199, "k": {"l": "mnopq"}}}}::vertex'::agtype - '"a"';
SELECT '{"id": 1125899906842625, "label": "Vertex", "properties": {"a": "xyz",
"b": true, "c": -19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i":
{"j": 199, "k": {"l": "mnopq"}}}}::vertex'::agtype - '["a"]';
diff --git a/regress/sql/list_comprehension.sql
b/regress/sql/list_comprehension.sql
index 0ef07c97..57b43221 100644
--- a/regress/sql/list_comprehension.sql
+++ b/regress/sql/list_comprehension.sql
@@ -180,5 +180,19 @@ SELECT * FROM cypher('list_comprehension', $$ MATCH (u
{list: [0, 2, 4, 6, 8, 10
SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list: [0, 2, 4, 6, 8,
10, 12]}) WHERE u.list = [u IN [1, u]] RETURN u $$) AS (u agtype);
SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list: [0, 2, 4, 6, 8,
10, 12]}) WHERE u.list IN [u IN [1, u.list]] RETURN u $$) AS (u agtype);
+-- Issue 2394 - projection over a null element should propagate null,
+-- not concatenate the null and the projection result into a sublist and
+-- not error out for operators other than '+'.
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [null] | x + 1] $$)
AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x +
1] $$) AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | 1 +
x] $$) AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x]
$$) AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x -
1] $$) AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x *
2] $$) AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x /
1] $$) AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x %
2] $$) AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | x ^
2] $$) AS (result agtype);
+SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, null, 2] | -x]
$$) AS (result agtype);
+
-- Clean up
SELECT * FROM drop_graph('list_comprehension', true);
\ No newline at end of file
diff --git a/src/backend/utils/adt/agtype_ops.c
b/src/backend/utils/adt/agtype_ops.c
index d831447b..f7f45b46 100644
--- a/src/backend/utils/adt/agtype_ops.c
+++ b/src/backend/utils/adt/agtype_ops.c
@@ -165,6 +165,12 @@ Datum agtype_add(PG_FUNCTION_ARGS)
agtv_lhs = get_ith_agtype_value_from_container(&lhs->root, 0);
agtv_rhs = get_ith_agtype_value_from_container(&rhs->root, 0);
+ /* openCypher: arithmetic over null yields null. */
+ if (agtv_lhs->type == AGTV_NULL || agtv_rhs->type == AGTV_NULL)
+ {
+ PG_RETURN_NULL();
+ }
+
/*
* One or both values is a string OR one is a string and the other is
* either an integer, float, or numeric. If so, concatenate them.
@@ -525,6 +531,12 @@ Datum agtype_sub(PG_FUNCTION_ARGS)
agtv_lhs = get_ith_agtype_value_from_container(&lhs->root, 0);
agtv_rhs = get_ith_agtype_value_from_container(&rhs->root, 0);
+ /* openCypher: arithmetic over null yields null. */
+ if (agtv_lhs->type == AGTV_NULL || agtv_rhs->type == AGTV_NULL)
+ {
+ PG_RETURN_NULL();
+ }
+
if (agtv_lhs->type == AGTV_INTEGER && agtv_rhs->type == AGTV_INTEGER)
{
agtv_result.type = AGTV_INTEGER;
@@ -615,6 +627,12 @@ Datum agtype_neg(PG_FUNCTION_ARGS)
agtv_value = get_ith_agtype_value_from_container(&v->root, 0);
+ /* openCypher: arithmetic over null yields null. */
+ if (agtv_value->type == AGTV_NULL)
+ {
+ PG_RETURN_NULL();
+ }
+
if (agtv_value->type == AGTV_INTEGER)
{
agtv_result.type = AGTV_INTEGER;
@@ -666,6 +684,12 @@ Datum agtype_mul(PG_FUNCTION_ARGS)
agtv_lhs = get_ith_agtype_value_from_container(&lhs->root, 0);
agtv_rhs = get_ith_agtype_value_from_container(&rhs->root, 0);
+ /* openCypher: arithmetic over null yields null. */
+ if (agtv_lhs->type == AGTV_NULL || agtv_rhs->type == AGTV_NULL)
+ {
+ PG_RETURN_NULL();
+ }
+
if (agtv_lhs->type == AGTV_INTEGER && agtv_rhs->type == AGTV_INTEGER)
{
agtv_result.type = AGTV_INTEGER;
@@ -756,6 +780,12 @@ Datum agtype_div(PG_FUNCTION_ARGS)
agtv_lhs = get_ith_agtype_value_from_container(&lhs->root, 0);
agtv_rhs = get_ith_agtype_value_from_container(&rhs->root, 0);
+ /* openCypher: arithmetic over null yields null. */
+ if (agtv_lhs->type == AGTV_NULL || agtv_rhs->type == AGTV_NULL)
+ {
+ PG_RETURN_NULL();
+ }
+
if (agtv_lhs->type == AGTV_INTEGER && agtv_rhs->type == AGTV_INTEGER)
{
if (agtv_rhs->val.int_value == 0)
@@ -874,6 +904,12 @@ Datum agtype_mod(PG_FUNCTION_ARGS)
agtv_lhs = get_ith_agtype_value_from_container(&lhs->root, 0);
agtv_rhs = get_ith_agtype_value_from_container(&rhs->root, 0);
+ /* openCypher: arithmetic over null yields null. */
+ if (agtv_lhs->type == AGTV_NULL || agtv_rhs->type == AGTV_NULL)
+ {
+ PG_RETURN_NULL();
+ }
+
if (agtv_lhs->type == AGTV_INTEGER && agtv_rhs->type == AGTV_INTEGER)
{
agtv_result.type = AGTV_INTEGER;
@@ -964,6 +1000,12 @@ Datum agtype_pow(PG_FUNCTION_ARGS)
agtv_lhs = get_ith_agtype_value_from_container(&lhs->root, 0);
agtv_rhs = get_ith_agtype_value_from_container(&rhs->root, 0);
+ /* openCypher: arithmetic over null yields null. */
+ if (agtv_lhs->type == AGTV_NULL || agtv_rhs->type == AGTV_NULL)
+ {
+ PG_RETURN_NULL();
+ }
+
if (agtv_lhs->type == AGTV_INTEGER && agtv_rhs->type == AGTV_INTEGER)
{
agtv_result.type = AGTV_FLOAT;