Apache AGE version
Observed with the official Docker image:
Image: apache/age:latest
Apache AGE: 1.7.0
PostgreSQL: 18.1
Environment
- Deployment: Docker
- Query interface: PostgreSQL SQL client
- Query language: Cypher through
cypher()
- Differential comparison targets:
- Neo4j
2026.05
- Neo4j
5.26
- Memgraph
3.11.0
- FalkorDB
4.18.11
Description
A standalone OPTIONAL MATCH returns zero rows when its pattern does not
match.
OPTIONAL MATCH should preserve its input row and bind variables introduced
by the unmatched optional pattern to null. At the beginning of a query,
there is an implicit initial input row. Therefore, a standalone unmatched
OPTIONAL MATCH should return one null-filled row.
Apache AGE instead drops the initial row and returns an empty result.
When the same OPTIONAL MATCH follows a successful MATCH, AGE correctly
preserves the preceding row and binds the unmatched variable to null.
The issue therefore appears specific to the initial input row of a standalone
OPTIONAL MATCH.
Steps to reproduce
Use an existing empty graph named test_graph.
Test 1: standalone OPTIONAL MATCH
SELECT *
FROM cypher('test_graph', $$
OPTIONAL MATCH (n:DefinitelyMissing)
RETURN n AS result
$$) AS (result agtype);
Expected behavior
The query should return one row:
Equivalent logical result:
Actual behavior
Apache AGE returns zero rows:
The initial input row is lost when the optional pattern does not match.
Control: OPTIONAL MATCH after a successful MATCH
Create one seed node:
SELECT *
FROM cypher('test_graph', $$
CREATE (:V1 {val: 1})
$$) AS (result agtype);
Run an unmatched OPTIONAL MATCH after matching the seed node:
SELECT *
FROM cypher('test_graph', $$
MATCH (v:V1 {val: 1})
OPTIONAL MATCH (n:DefinitelyMissing)
RETURN v.val AS seed, n AS result
$$) AS (seed agtype, result agtype);
Apache AGE correctly returns:
seed | result
-----+-------
1 | null
This shows that AGE implements null-preserving behavior correctly when an
input row is supplied by a preceding MATCH. The failure occurs when
OPTIONAL MATCH is the first clause and must operate on the query's initial
input row.
Cross-engine comparison
| Engine |
Standalone unmatched OPTIONAL MATCH |
Neo4j 2026.05 |
One row: {result: null} |
Neo4j 5.26 |
One row: {result: null} |
Memgraph 3.11.0 |
One row: {result: null} |
FalkorDB 4.18.11 |
One row: {result: null} |
Apache AGE 1.7.0 |
Zero rows |
Why this looks like a bug
The following two query shapes should apply the same null-preserving
OPTIONAL MATCH semantics:
OPTIONAL MATCH (n:DefinitelyMissing)
RETURN n
and:
MATCH (v:V1)
OPTIONAL MATCH (n:DefinitelyMissing)
RETURN v, n
In the second query, AGE correctly preserves each incoming row and binds
n to null.
In the first query, the query begins with one implicit input row. AGE should
preserve that row in the same way, but instead removes it entirely.
This suggests that the standalone OPTIONAL MATCH execution path starts
with an empty input relation rather than the required single initial row.
Related but distinct issue
Apache AGE issue #2378 reports a different OPTIONAL MATCH null-preservation
problem involving a preceding MATCH and correlated subquery predicates.
This report is distinct because:
- there is no preceding clause;
- there is no correlated subquery;
- no graph data is required for the failing query;
- the failure is specifically the loss of the initial input row for a
standalone OPTIONAL MATCH.
Apache AGE version
Observed with the official Docker image:
Environment
cypher()2026.055.263.11.04.18.11Description
A standalone
OPTIONAL MATCHreturns zero rows when its pattern does notmatch.
OPTIONAL MATCHshould preserve its input row and bind variables introducedby the unmatched optional pattern to
null. At the beginning of a query,there is an implicit initial input row. Therefore, a standalone unmatched
OPTIONAL MATCHshould return one null-filled row.Apache AGE instead drops the initial row and returns an empty result.
When the same
OPTIONAL MATCHfollows a successfulMATCH, AGE correctlypreserves the preceding row and binds the unmatched variable to
null.The issue therefore appears specific to the initial input row of a standalone
OPTIONAL MATCH.Steps to reproduce
Use an existing empty graph named
test_graph.Test 1: standalone
OPTIONAL MATCHExpected behavior
The query should return one row:
Equivalent logical result:
Actual behavior
Apache AGE returns zero rows:
The initial input row is lost when the optional pattern does not match.
Control:
OPTIONAL MATCHafter a successfulMATCHCreate one seed node:
Run an unmatched
OPTIONAL MATCHafter matching the seed node:Apache AGE correctly returns:
This shows that AGE implements null-preserving behavior correctly when an
input row is supplied by a preceding
MATCH. The failure occurs whenOPTIONAL MATCHis the first clause and must operate on the query's initialinput row.
Cross-engine comparison
OPTIONAL MATCH2026.05{result: null}5.26{result: null}3.11.0{result: null}4.18.11{result: null}1.7.0Why this looks like a bug
The following two query shapes should apply the same null-preserving
OPTIONAL MATCHsemantics:and:
In the second query, AGE correctly preserves each incoming row and binds
ntonull.In the first query, the query begins with one implicit input row. AGE should
preserve that row in the same way, but instead removes it entirely.
This suggests that the standalone
OPTIONAL MATCHexecution path startswith an empty input relation rather than the required single initial row.
Related but distinct issue
Apache AGE issue #2378 reports a different
OPTIONAL MATCHnull-preservationproblem involving a preceding
MATCHand correlated subquery predicates.This report is distinct because:
standalone
OPTIONAL MATCH.