Apache AGE version
Observed on Docker image:
docker pull apache/age:latest
Pulled: 2026-07-09
Apache AGE: PostgreSQL 18.1
Endpoint:
postgres://127.0.0.1:5432
How are you accessing AGE
psql and Python psycopg2 driver
Environment
-
Host OS: Linux 5.4.0-216-generic
-
Deployment: Docker apache/age:latest
-
Query interface: psql and psycopg2
-
Query language: Cypher via ag_catalog.cypher
-
Configuration: default AGE on PostgreSQL 18
-
Differential comparison targets:
Description
AGE's tail() function returns null when the result should be an empty list.
The issue occurs for:
Both should return [].
tail() is a list-returning function: it returns all elements of the input list except the first one. Therefore, dropping the only element from [1] should produce an empty list, and applying tail() to an already empty list should also produce an empty list.
Neo4j returns [] for both cases.
Setup
LOAD 'age';
SET search_path = ag_catalog, '$user', public;
SELECT * FROM ag_catalog.create_graph('test_graph');
Failing queries
SELECT * FROM cypher('test_graph', $$
RETURN tail([1]) AS r
$$) AS (r agtype);
AGE returns:
Expected:
SELECT * FROM cypher('test_graph', $$
RETURN tail([]) AS r
$$) AS (r agtype);
AGE returns:
Expected:
Control queries
Normal tail() works
SELECT * FROM cypher('test_graph', $$
RETURN tail([1,2,3]) AS r
$$) AS (r agtype);
AGE returns:
This is correct.
tail(null) should return null
SELECT * FROM cypher('test_graph', $$
RETURN tail(null) AS r
$$) AS (r agtype);
Expected:
This distinguishes null input from empty-list output.
Other list functions show the expected boundary behavior
SELECT * FROM cypher('test_graph', $$
RETURN reverse([]) AS r
$$) AS (r agtype);
AGE returns:
This is correct: a list-returning function applied to an empty list returns an empty list.
SELECT * FROM cypher('test_graph', $$
RETURN head([]) AS r
$$) AS (r agtype);
AGE returns:
This is correct: head() returns an element, and there is no element in an empty list.
SELECT * FROM cypher('test_graph', $$
RETURN last([]) AS r
$$) AS (r agtype);
AGE returns:
This is correct: last() returns an element, and there is no element in an empty list.
Cross-engine comparison
| Expression |
Apache AGE |
Neo4j 2026.05.0 |
tail([1]) |
null |
[] |
tail([]) |
null |
[] |
tail([1,2,3]) |
[2,3] |
[2,3] |
tail(null) |
null |
null |
reverse([]) |
[] |
[] |
head([]) |
null |
null |
last([]) |
null |
null |
Impact
This can silently change query logic when tail() is used in list pipelines.
For example:
SELECT * FROM cypher('test_graph', $$
RETURN size(tail([1])) AS r
$$) AS (r agtype);
Expected:
AGE may return null because tail([1]) returns null.
This also affects predicates and CASE expressions that distinguish an empty list from null.
Summary
tail() should return an empty list when all elements are removed from the input list. AGE currently returns null for tail([1]) and tail([]), even though it correctly returns lists for non-empty outputs such as tail([1,2,3]).
Apache AGE version
Observed on Docker image:
How are you accessing AGE
psql and Python
psycopg2driverEnvironment
Host OS: Linux
5.4.0-216-genericDeployment: Docker
apache/age:latestQuery interface: psql and psycopg2
Query language: Cypher via
ag_catalog.cypherConfiguration: default AGE on PostgreSQL 18
Differential comparison targets:
2026.05.0Description
AGE's
tail()function returnsnullwhen the result should be an empty list.The issue occurs for:
Both should return
[].tail()is a list-returning function: it returns all elements of the input list except the first one. Therefore, dropping the only element from[1]should produce an empty list, and applyingtail()to an already empty list should also produce an empty list.Neo4j returns
[]for both cases.Setup
Failing queries
AGE returns:
Expected:
AGE returns:
Expected:
Control queries
Normal
tail()worksAGE returns:
This is correct.
tail(null)should returnnullExpected:
This distinguishes null input from empty-list output.
Other list functions show the expected boundary behavior
AGE returns:
This is correct: a list-returning function applied to an empty list returns an empty list.
AGE returns:
This is correct:
head()returns an element, and there is no element in an empty list.AGE returns:
This is correct:
last()returns an element, and there is no element in an empty list.Cross-engine comparison
2026.05.0tail([1])null[]tail([])null[]tail([1,2,3])[2,3][2,3]tail(null)nullnullreverse([])[][]head([])nullnulllast([])nullnullImpact
This can silently change query logic when
tail()is used in list pipelines.For example:
Expected:
AGE may return
nullbecausetail([1])returnsnull.This also affects predicates and CASE expressions that distinguish an empty list from
null.Summary
tail()should return an empty list when all elements are removed from the input list. AGE currently returnsnullfortail([1])andtail([]), even though it correctly returns lists for non-empty outputs such astail([1,2,3]).