Skip to content

tail([1]) and tail([]) return null instead of [] #2472

Description

@shulei5831sl

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:

    • Neo4j 2026.05.0

Description

AGE's tail() function returns null when the result should be an empty list.

The issue occurs for:

tail([1])
tail([])

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:

r = null

Expected:

r = []
SELECT * FROM cypher('test_graph', $$
  RETURN tail([]) AS r
$$) AS (r agtype);

AGE returns:

r = null

Expected:

r = []

Control queries

Normal tail() works

SELECT * FROM cypher('test_graph', $$
  RETURN tail([1,2,3]) AS r
$$) AS (r agtype);

AGE returns:

r = [2, 3]

This is correct.

tail(null) should return null

SELECT * FROM cypher('test_graph', $$
  RETURN tail(null) AS r
$$) AS (r agtype);

Expected:

r = null

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:

r = []

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:

r = null

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:

r = null

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:

r = 0

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]).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions