`tail([1])` and `tail([])` return `null` instead of `[]`
- Dominant language
- C
- Stars
- 4.8k
- Forks
- 523
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 9
Description
### Apache AGE version
Observed on Docker image:
```text
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:
```cypher
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
```pgsql
LOAD 'age';
SET search_path = ag_catalog, '$user', public;
SELECT * FROM ag_catalog.create_graph('test_graph');
```
### Failing queries
```pgsql
SELECT * FROM cypher('test_graph', $$
RETURN tail([1]) AS r
$$) AS (r agtype);
```
AGE returns:
```text
r = null
```
Expected:
```text
r = []
```
```pgsql
SELECT * FROM cypher('test_graph', $$
RETURN tail([]) AS r
$$) AS (r agtype);
```
AGE returns:
```text
r = null
```
Expected:
```text
r = []
```
### Control queries
#### Normal `tail()` works
```pgsql
SELECT * FROM cypher('test_graph', $$
RETURN tail([1,2,3]) AS r
$$) AS (r agtype);
```
AGE returns:
```text
r = [2, 3]
```
This is correct.
#### `tail(null)` should return `null`
```pgsql
SELECT * FROM cypher('test_graph', $$
RETURN tail(null) AS r
$$) AS (r agtype);
```
Expected:
```text
r = null
```
This distinguishes null input from empty-list output.
#### Other list functions show the expected boundary behavior
```pgsql
SELECT * FROM cypher('test_graph', $$
RETURN reverse([]) AS r
$$) AS (r agtype);
```
AGE returns:
```text
r = []
```
This is correct: a list-returning function applied to an empty list returns an empty list.
```pgsql
SELECT * FROM cypher('test_graph', $$
RETURN head([]) AS r
$$) AS (r agtype);
```
AGE returns:
```text
r = null
```
This is correct: `head()` returns an element, and there is no element in an empty list.
```pgsql
SELECT * FROM cypher('test_graph', $$
RETURN last([]) AS r
$$) AS (r agtype);
```
AGE returns:
```text
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:
```pgsql
SELECT * FROM cypher('test_graph', $$
RETURN size(tail([1])) AS r
$$) AS (r agtype);
```
Expected:
```text
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])`.
Contributor guide
Research direction
Start at the ag_catalog.cypher entry point and locate the C implementation of the tail() function, comparing its empty-result handling with the described reverse(), head(), and last() behavior. Run the provided queries for tail([1]), tail([]), tail(null), and size(tail([1])); done means empty inputs produce [] while null input remains null.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, postgresql
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100