From 8f8778b416f9bb1b3796b8ee4d557acd5ca9e62a Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Sun, 19 Apr 2026 22:52:35 -0700 Subject: [PATCH] agtype: return an empty list (not null) from tail() for empty and singleton lists age_tail() in src/backend/utils/adt/agtype.c had an explicit early return that mapped both the empty-list and the singleton-list cases to SQL NULL: count = AGT_ROOT_COUNT(agt_arg); /* if we have an empty list or only one element in the list, return null */ if (count <= 1) { PG_RETURN_NULL(); } The Cypher specification (and Neo4j / Memgraph, as shown in #2384) both define tail(list) as "the list without its first element". For a zero-or-one-element input that is an empty list [], not null. Differential tests against Neo4j surface this divergence immediately. Remove the early return. The loop below already iterates from i=1 so it naturally emits [] for count <= 1 and emits the correct tail for the general case. No behaviour change for count >= 2. Tests and expected output are updated to match the new, spec-correct result. Fixes #2384 --- regress/expected/expr.out | 6 +++--- regress/sql/expr.sql | 2 +- src/backend/utils/adt/agtype.c | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/regress/expected/expr.out b/regress/expected/expr.out index 6d9341451..e95fe14e3 100644 --- a/regress/expected/expr.out +++ b/regress/expected/expr.out @@ -8039,17 +8039,17 @@ SELECT * FROM cypher('list', $$ RETURN tail(["a","b","c","d","e"]) $$) AS (tail ["b", "c", "d", "e"] (1 row) --- should return null +-- should return an empty list SELECT * FROM cypher('list', $$ RETURN tail([1]) $$) AS (tail agtype); tail ------ - + [] (1 row) SELECT * FROM cypher('list', $$ RETURN tail([]) $$) AS (tail agtype); tail ------ - + [] (1 row) -- should throw errors diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql index 445e2d237..7304d4d65 100644 --- a/regress/sql/expr.sql +++ b/regress/sql/expr.sql @@ -3269,7 +3269,7 @@ SELECT * from cypher('list', $$RETURN range(0, -10.0, -3.0)$$) as (range agtype) -- should return the last elements of the list SELECT * FROM cypher('list', $$ RETURN tail([1,2,3,4,5]) $$) AS (tail agtype); SELECT * FROM cypher('list', $$ RETURN tail(["a","b","c","d","e"]) $$) AS (tail agtype); --- should return null +-- should return an empty list SELECT * FROM cypher('list', $$ RETURN tail([1]) $$) AS (tail agtype); SELECT * FROM cypher('list', $$ RETURN tail([]) $$) AS (tail agtype); -- should throw errors diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c index 386219556..35443d8fc 100644 --- a/src/backend/utils/adt/agtype.c +++ b/src/backend/utils/adt/agtype.c @@ -6056,11 +6056,11 @@ Datum age_tail(PG_FUNCTION_ARGS) count = AGT_ROOT_COUNT(agt_arg); - /* if we have an empty list or only one element in the list, return null */ - if (count <= 1) - { - PG_RETURN_NULL(); - } + /* + * For an empty or singleton list, tail() returns an empty list. The loop + * below already produces that result (i starts at 1 so nothing is pushed + * when count <= 1), so we do not special-case the count here. + */ /* clear the result structure */ MemSet(&agis_result, 0, sizeof(agtype_in_state));