apache / apache/age

`tail([1])` and `tail([])` return `null` instead of `[]`

Open
#2,472 0 comments 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.