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;

Reply via email to