Describe the bug
Map literals may drop keys whose values are null.
In Cypher, a map literal like {a: null} should still contain the key a with a null value. Instead, Apache AGE returns an empty map {}.
This also affects larger queries that build maps from optional matches: when all projected values are null, AGE returns {} instead of preserving the keys with null values.
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 for the minimal repro 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', $$
RETURN {a: null} AS m
$$) AS (m agtype);
Returned result on AGE:
Expected behavior
The returned map should preserve the key:
Neo4j returns a map containing the key with a null value for the equivalent Cypher query:
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
A two-key control case shows the same behavior:
SELECT * FROM cypher('fuzz_graph', $$
RETURN {companyName: null, sinceYear: null} AS m
$$) AS (m agtype);
AGE returns:
Expected result:
{companyName: null, sinceYear: null}
Two additional expressions suggest this is a semantic loss of null-valued map entries, not just a display issue for one literal form.
keys({a: null}):
SELECT * FROM cypher('fuzz_graph', $$
RETURN keys({a: null}) AS ks
$$) AS (ks agtype);
Expected result on Neo4j and Memgraph:
Observed result on AGE:
coalesce({a: null}, null):
SELECT * FROM cypher('fuzz_graph', $$
RETURN coalesce({a: null}, null) AS m
$$) AS (m agtype);
Expected result on Neo4j and Memgraph:
Observed result on AGE:
This also showed up during automated Neo4j-vs-AGE differential testing in a larger query using OPTIONAL MATCH and collect(...):
MATCH (employee:Person)
OPTIONAL MATCH (employee)-[r:WORKS_FOR]->(company)
WHERE r.since > 2020
WITH employee, collect({companyName: company.name, sinceYear: r.since}) AS workHistory
WHERE size(workHistory) > 0
RETURN employee.name AS name, COUNT { (employee)-[:WORKS_FOR]->() } AS jobCount, workHistory
ORDER BY jobCount DESC
For rows where company.name and r.since were both null, Neo4j returned:
[{companyName: null, sinceYear: null}]
while AGE returned:
That suggests the issue is not just display formatting of a top-level literal, but a semantic loss of null-valued map entries during Cypher evaluation.
Describe the bug
Map literals may drop keys whose values are
null.In Cypher, a map literal like
{a: null}should still contain the keyawith a null value. Instead, Apache AGE returns an empty map{}.This also affects larger queries that build maps from optional matches: when all projected values are null, AGE returns
{}instead of preserving the keys with null values.How are you accessing AGE (Command line, driver, etc.)?
cypher(...)wrapper through the local Python differential-testing harnesspsqlinside the Docker containerWhat data setup do we need to do?
No graph data is required for the minimal repro beyond creating an empty graph:
What is the necessary configuration info needed?
apache/age1.7.018.1fuzz_graphWhat is the command that caused the error?
Returned result on AGE:
Expected behavior
The returned map should preserve the key:
Neo4j returns a map containing the key with a null value for the equivalent Cypher query:
Environment (please complete the following information):
1.7.018.1Additional context
A two-key control case shows the same behavior:
AGE returns:
Expected result:
Two additional expressions suggest this is a semantic loss of null-valued map entries, not just a display issue for one literal form.
keys({a: null}):Expected result on Neo4j and Memgraph:
Observed result on AGE:
coalesce({a: null}, null):Expected result on Neo4j and Memgraph:
Observed result on AGE:
This also showed up during automated Neo4j-vs-AGE differential testing in a larger query using
OPTIONAL MATCHandcollect(...):For rows where
company.nameandr.sincewere both null, Neo4j returned:while AGE returned:
That suggests the issue is not just display formatting of a top-level literal, but a semantic loss of null-valued map entries during Cypher evaluation.