crprashant opened a new pull request, #2411:
URL: https://github.com/apache/age/pull/2411
Fixes #2383.
## Problem
Per Neo4j / openCypher semantics, `count()` must ignore nulls. On Apache AGE:
```cypher
UNWIND [null] AS x RETURN count(x) -- returns 1 (buggy), should be 0
```
The control cases already returned the correct `0`:
```cypher
RETURN count(null)
WITH null AS x RETURN count(x)
```
## Root cause
`age_unnest()` in `src/backend/utils/adt/agtype.c` materialized every
top-level array element into a heap tuple with `nulls[0] = false`, even when
the element's `agtype_value.type == AGTV_NULL`. The row that reached `count()`
therefore carried a non-NULL datum wrapping an agtype JSON null, so the
aggregate's strict null check never skipped it.
## Fix
A naïve fix inside `age_unnest()` breaks other callers.
`construct_age_function_name()` in `src/backend/parser/cypher_expr.c` rewrites
`unnest` → `age_unnest`, so list comprehensions and the predicate functions
(`all` / `any` / `none` / `single`) also route through `age_unnest`. Those
callers deliberately rely on the quirk that AGE's `agtype_null >
agtype_integer` evaluates to `agtype_null` (truthy), not SQL NULL. Changing the
SRF broke five `predicate_functions` expected results.
Introduce a dedicated SRF `age_unwind()` with UNWIND-specific null semantics:
- Top-level `AGTV_NULL` elements → SQL NULL rows so null-strict operators
(`count`, `IS NULL`, `WHERE x IS NOT NULL`, …) match `WITH null AS x`.
- Non-null elements are returned unchanged.
- Nested nulls inside arrays/objects are preserved as agtype-null.
`transform_cypher_unwind()` is the only call site changed from `age_unnest`
to `age_unwind`. List comprehensions and predicate functions continue to use
`age_unnest` and their behavior is untouched.
## Before / after
```text
-- before
UNWIND [null] AS x RETURN count(x) -- 1
UNWIND [1, null, 2, null, 3] AS x RETURN count(x) -- 5
UNWIND [null] AS x RETURN x IS NULL -- false
-- after
UNWIND [null] AS x RETURN count(x) -- 0 ✓
UNWIND [1, null, 2, null, 3] AS x RETURN count(x) -- 3 ✓
UNWIND [null] AS x RETURN x IS NULL -- true ✓
```
## Regression tests (`regress/sql/cypher_unwind.sql`)
- `UNWIND [null] RETURN count(x) = 0`
- `UNWIND [1, null, 2, null, 3] RETURN count(x) = 3`
- `UNWIND [null] RETURN x IS NULL`
- `UNWIND [1, null, 2] WITH x WHERE x IS NOT NULL RETURN count(x) = 2`
- `UNWIND [null] RETURN count(*) = 1` (row still produced)
- `UNWIND [[null], {k: null}] RETURN count(x) = 2` (nested nulls preserved)
- `UNWIND [null] RETURN x` — asserts the row value itself is SQL NULL
## Files changed
- `src/backend/utils/adt/agtype.c` — new `age_unwind` SRF
- `src/backend/parser/cypher_clause.c` — UNWIND calls `age_unwind`
- `sql/agtype_typecast.sql` — declare `age_unwind`
- `age--1.7.0--y.y.y.sql` — add `age_unwind` for in-place upgrades
- `regress/sql/cypher_unwind.sql` + `regress/expected/cypher_unwind.out`
## Test results
`make installcheck` with PostgreSQL 18 on Ubuntu 24.04:
- **32/33 tests pass.**
- `age_upgrade` fails identically on `apache/age` master `HEAD`
(pre-existing, unrelated; same `age_prepare_pg_upgrade already exists` error).
- `cypher_unwind`, `predicate_functions`, `list_comprehension` all pass.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]