Skip to content

Standalone OPTIONAL MATCH drops the initial input row when the pattern has no match #2473

Description

@shulei5831sl

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:

result
------
null

Equivalent logical result:

[{result: null}]

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions