You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
UNWIND [null] may produce a row whose value prints as null, but count(x) still counts it as 1. #2383
Describe the bug UNWIND [null] may produce a row whose value prints as null, but count(x) still counts it as 1.
In Cypher, count(expr) should count only non-null values. After UNWIND [null] AS x, the single row has x = null, so count(x) should be 0.
Instead, Apache AGE returns 1.
How are you accessing AGE (Command line, driver, etc.)?
PostgreSQL cypher(...) wrapper through the local Python differential-testing harness
Reproducible directly in psql inside the Docker container
What data setup do we need to do?
No graph data is required beyond creating an empty graph:
SELECT create_graph('fuzz_graph');
What is the necessary configuration info needed?
Plain Apache AGE Docker image was enough
Docker image in local repro: apache/age
AGE extension version: 1.7.0
PostgreSQL version: 18.1
Graph name used in repro: fuzz_graph
No extra extensions or special configuration were required
What is the command that caused the error?
SELECT*FROM cypher('fuzz_graph', $$
UNWIND [null] AS x
RETURN count(x) AS c
$$) AS (c agtype);
Returned result on AGE:
c
---
1
Expected behavior
The result should be:
c
---
0
Neo4j returns 0 for the equivalent Cypher query:
UNWIND [null] ASxRETURNcount(x) ASc
Environment (please complete the following information):
Version: Apache AGE 1.7.0
PostgreSQL: 18.1
Host OS: Windows
Architecture: x86_64
Deployment: Docker
Additional context
This appears to be a counting/null-semantics issue specifically around UNWIND, not a general count(null) problem.
Direct control cases on the same AGE instance:
Plain count(null) behaves correctly:
SELECT*FROM cypher('fuzz_graph', $$
RETURN count(null) AS c
$$) AS (c agtype);
Observed result:
0
WITH null AS x RETURN count(x) also behaves correctly:
SELECT*FROM cypher('fuzz_graph', $$
WITH nullAS x
RETURN count(x) AS c
$$) AS (c agtype);
Observed result:
0
UNWIND [null] AS x RETURN x shows that the produced row is indeed null:
SELECT*FROM cypher('fuzz_graph', $$
UNWIND [null] AS x
RETURN x
$$) AS (x agtype);
Observed result:
null
A larger differential-testing case first exposed this via:
MATCH (p:Person)
OPTIONALMATCHpath= (p)-[:KNOWS]->(friend:Person)
WHEREp.age>25UNWIND [path] ASpthRETURNp.nameASperson_name, count(pth) ASfriend_count
For rows where path was null, AGE returned friend_count = 1 instead of 0. Reducing the case to UNWIND [null] reproduced the same incorrect counting behavior.
Hi! I'd like to work on this. Could a maintainer please assign it to me?
Initial hypothesis: UNWIND may be emitting an agtype null value rather than a true SQL NULL, so the null-check inside the count() aggregate doesn't trigger. The reporter's control cases confirm WITH null AS x RETURN count(x) works correctly, which suggests the divergence is specifically in UNWIND's row materialization path. I'll reproduce locally, trace the executor output, compare against the WITH path, and follow up with a fix proposal plus a regression test.
@jrgemignani — we have two open PRs fixing this bug via opposite approaches, and they conflict, so whichever merges first forces a rework of the other. Flagging for a maintainer call on direction before either author spends more effort (e.g. #2406 currently needs a rebase I've just asked for):
#2411 (@crprashant) — narrow. Adds a separate age_unwind SRF and repoints only transform_cypher_unwind to it; age_unnest is untouched. Low blast radius, no risk to list comprehensions / predicate functions. Deliberately fixes only this issue (#2383) — it leaves the sibling list-comprehension null bug (#2393) and the single() / predicate three-valued-logic quirk unfixed, since those callers still rely on agtype_null > agtype_integer being truthy. (Currently conflicting + no green CI run.)
#2406 (@MuhammadTahaNaveed) — root-cause. Fixes age_unnest in place (emit SQL NULL for AGTV_NULL) and rewrites the predicate-function 3VL, so it fixes #2383 + #2393 + single() together and makes null > 0 yield null per openCypher instead of truthy. Larger surface, but I've reviewed it and the approach is sound; it needs a rebase + two small tidy-ups (INT8PASSBYVAL, a comment).
They're mutually redundant on the age_unnest/UNWIND path — we should merge one and close the other.
My lean is #2406 as the more correct end state (fixes the whole null-handling family, not just the UNWIND symptom), with #2411 as the lower-risk fallback if we'd rather ship the narrow fix now and defer the 3VL rework. Your call on the direction — I'll shepherd whichever you pick to green + approval.
Activity
crprashant commentedon Apr 22, 2026
Hi! I'd like to work on this. Could a maintainer please assign it to me?
Initial hypothesis:
UNWINDmay be emitting an agtypenullvalue rather than a true SQL NULL, so the null-check inside thecount()aggregate doesn't trigger. The reporter's control cases confirmWITH null AS x RETURN count(x)works correctly, which suggests the divergence is specifically in UNWIND's row materialization path. I'll reproduce locally, trace the executor output, compare against theWITHpath, and follow up with a fix proposal plus a regression test.Fix UNWIND [null] causing count() to miscount null rows (apache#2383)
Fix UNWIND [null] causing count() to miscount null rows (apache#2383)
gregfelice commentedon Jul 3, 2026
@jrgemignani — we have two open PRs fixing this bug via opposite approaches, and they conflict, so whichever merges first forces a rework of the other. Flagging for a maintainer call on direction before either author spends more effort (e.g. #2406 currently needs a rebase I've just asked for):
#2411 (@crprashant) — narrow. Adds a separate
age_unwindSRF and repoints onlytransform_cypher_unwindto it;age_unnestis untouched. Low blast radius, no risk to list comprehensions / predicate functions. Deliberately fixes only this issue (#2383) — it leaves the sibling list-comprehension null bug (#2393) and thesingle()/ predicate three-valued-logic quirk unfixed, since those callers still rely onagtype_null > agtype_integerbeing truthy. (Currently conflicting + no green CI run.)#2406 (@MuhammadTahaNaveed) — root-cause. Fixes
age_unnestin place (emit SQL NULL forAGTV_NULL) and rewrites the predicate-function 3VL, so it fixes #2383 + #2393 +single()together and makesnull > 0yieldnullper openCypher instead of truthy. Larger surface, but I've reviewed it and the approach is sound; it needs a rebase + two small tidy-ups (INT8PASSBYVAL, a comment).They're mutually redundant on the
age_unnest/UNWIND path — we should merge one and close the other.My lean is #2406 as the more correct end state (fixes the whole null-handling family, not just the UNWIND symptom), with #2411 as the lower-risk fallback if we'd rather ship the narrow fix now and defer the 3VL rework. Your call on the direction — I'll shepherd whichever you pick to green + approval.
Propagate null through unnest and single() three-valued logic (apache…
Propagate null through unnest and single() three-valued logic (#2406)
Propagate null through unnest and single() three-valued logic (#2406)