From 226eac01a2910053ba63f30c82872ab109ab0632 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Sun, 19 Apr 2026 23:04:26 -0700 Subject: [PATCH 1/2] agtype: propagate null through list slice bounds instead of treating AGTV_NULL as omitted agtype_access_slice() decides what to do with each bound in two steps: 1. If the SQL argument is NULL (`PG_ARGISNULL(1|2)`), the caller did not supply that bound at all (e.g. `list[..2]`) - treat it as "no bound" and fall back to 0 / array_size. This is correct. 2. Otherwise the argument is non-NULL agtype. If *the agtype value* inside happens to be AGTV_NULL (e.g. `list[null..2]`), the existing code also treated it as "no bound" and returned the full slice. This is wrong. Under Cypher null-propagation semantics `list[a..b]` is null whenever either bound is null; Neo4j and Memgraph both return null, and differential testing against both flagged the divergence (#2392). Change step 2 to PG_RETURN_NULL() when the supplied bound is AGTV_NULL. Step 1 is untouched, so `[..n]` / `[n..]` still work as before. Regression tests are updated accordingly: * The two pre-existing agtype_access_slice() calls with an explicit 'null'::agtype argument now expect the null result. * New bracket-syntax cases cover `[1,2,3][1..null]` and `[1,2,3][null..2]`, which were the minimal repros on the issue. Fixes #2392 --- regress/expected/expr.out | 19 +++++++++++++++++-- regress/sql/expr.sql | 5 +++++ src/backend/utils/adt/agtype.c | 14 ++++++++------ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/regress/expected/expr.out b/regress/expected/expr.out index 6d9341451..7195ea983 100644 --- a/regress/expected/expr.out +++ b/regress/expected/expr.out @@ -489,13 +489,28 @@ $$RETURN [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10][-1..10]$$) AS r(c agtype); SELECT agtype_access_slice('[0]'::agtype, 'null'::agtype, '1'::agtype); agtype_access_slice --------------------- - [0] + (1 row) SELECT agtype_access_slice('[0]'::agtype, '0'::agtype, 'null'::agtype); agtype_access_slice --------------------- - [0] + +(1 row) + +-- null bounds propagate through bracket-style slicing too +SELECT * FROM cypher('expr', +$$RETURN [1, 2, 3][1..null]$$) AS r(c agtype); + c +--- + +(1 row) + +SELECT * FROM cypher('expr', +$$RETURN [1, 2, 3][null..2]$$) AS r(c agtype); + c +--- + (1 row) -- should error - ERROR: slice must access a list diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql index 445e2d237..965cf78e2 100644 --- a/regress/sql/expr.sql +++ b/regress/sql/expr.sql @@ -216,6 +216,11 @@ SELECT * FROM cypher('expr', $$RETURN [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10][-1..10]$$) AS r(c agtype); SELECT agtype_access_slice('[0]'::agtype, 'null'::agtype, '1'::agtype); SELECT agtype_access_slice('[0]'::agtype, '0'::agtype, 'null'::agtype); +-- null bounds propagate through bracket-style slicing too +SELECT * FROM cypher('expr', +$$RETURN [1, 2, 3][1..null]$$) AS r(c agtype); +SELECT * FROM cypher('expr', +$$RETURN [1, 2, 3][null..2]$$) AS r(c agtype); -- should error - ERROR: slice must access a list SELECT * from cypher('expr', $$RETURN 0[0..1]$$) as r(a agtype); diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c index 386219556..986d068fd 100644 --- a/src/backend/utils/adt/agtype.c +++ b/src/backend/utils/adt/agtype.c @@ -4324,11 +4324,14 @@ Datum agtype_access_slice(PG_FUNCTION_ARGS) { agt_lidx = AG_GET_ARG_AGTYPE_P(1); lidx_value = get_ith_agtype_value_from_container(&agt_lidx->root, 0); - /* adjust for AGTV_NULL */ + /* + * Under Cypher null-propagation semantics, list[a..b] is null when + * either bound is null. Return null directly instead of silently + * treating AGTV_NULL as an omitted bound. + */ if (lidx_value->type == AGTV_NULL) { - lower_index = 0; - lidx_value = NULL; + PG_RETURN_NULL(); } } @@ -4341,11 +4344,10 @@ Datum agtype_access_slice(PG_FUNCTION_ARGS) { agt_uidx = AG_GET_ARG_AGTYPE_P(2); uidx_value = get_ith_agtype_value_from_container(&agt_uidx->root, 0); - /* adjust for AGTV_NULL */ + /* Symmetric to the lower bound: null propagates to a null result. */ if (uidx_value->type == AGTV_NULL) { - upper_index = array_size; - uidx_value = NULL; + PG_RETURN_NULL(); } } From 57ed094a0a0c73edca9c3a8cd8a79338792741fc Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Mon, 20 Apr 2026 14:58:53 -0700 Subject: [PATCH 2/2] regress: narrow expr tests to the agtype_access_slice call sites Drop the Cypher-level `[1,2,3][1..null]` / `[null..2]` regression cases. The list-index execution path goes through a separate function from agtype_access_slice and still treats AGTV_NULL as an omitted bound (tracking that fix separately); keeping those cases in the regression file without a matching source change makes the build job fail on diff. Keep the two direct agtype_access_slice tests that actually exercise the fixed code path. Raised by @MuhammadTahaNaveed on #2400. Co-developed with an AI coding assistant (Claude Code). Signed-off-by: SAY-5 --- regress/expected/expr.out | 15 --------------- regress/sql/expr.sql | 5 ----- 2 files changed, 20 deletions(-) diff --git a/regress/expected/expr.out b/regress/expected/expr.out index 7195ea983..8ba64d16f 100644 --- a/regress/expected/expr.out +++ b/regress/expected/expr.out @@ -498,21 +498,6 @@ SELECT agtype_access_slice('[0]'::agtype, '0'::agtype, 'null'::agtype); (1 row) --- null bounds propagate through bracket-style slicing too -SELECT * FROM cypher('expr', -$$RETURN [1, 2, 3][1..null]$$) AS r(c agtype); - c ---- - -(1 row) - -SELECT * FROM cypher('expr', -$$RETURN [1, 2, 3][null..2]$$) AS r(c agtype); - c ---- - -(1 row) - -- should error - ERROR: slice must access a list SELECT * from cypher('expr', $$RETURN 0[0..1]$$) as r(a agtype); diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql index 965cf78e2..445e2d237 100644 --- a/regress/sql/expr.sql +++ b/regress/sql/expr.sql @@ -216,11 +216,6 @@ SELECT * FROM cypher('expr', $$RETURN [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10][-1..10]$$) AS r(c agtype); SELECT agtype_access_slice('[0]'::agtype, 'null'::agtype, '1'::agtype); SELECT agtype_access_slice('[0]'::agtype, '0'::agtype, 'null'::agtype); --- null bounds propagate through bracket-style slicing too -SELECT * FROM cypher('expr', -$$RETURN [1, 2, 3][1..null]$$) AS r(c agtype); -SELECT * FROM cypher('expr', -$$RETURN [1, 2, 3][null..2]$$) AS r(c agtype); -- should error - ERROR: slice must access a list SELECT * from cypher('expr', $$RETURN 0[0..1]$$) as r(a agtype);