[Bug]Assertion failure in LATERAL GRAPH_TABLE with multi-label pattern

First seen: 2026-05-07 04:13:20+00:00 · Messages: 1 · Participants: 1

Latest Update

2026-05-08 · opus 4.7

Assertion Failure in LATERAL GRAPH_TABLE with Multi-label Patterns

Context: SQL/PGQ GRAPH_TABLE

This thread concerns a bug in PostgreSQL's emerging SQL/PGQ (Property Graph Queries) implementation, specifically the GRAPH_TABLE table function introduced as part of SQL:2023 graph query support. The feature allows graph pattern matching over property graphs defined via CREATE PROPERTY GRAPH, with results exposed as a relational table that can participate in normal SQL queries including lateral joins.

The report is filed against development code (the feature is still being stabilized), and the assertion failure is a clear planner-level bug triggered by a very specific combination: a LATERAL reference into a GRAPH_TABLE whose pattern uses a label alternation (a IS vl1 | vl2) that expands into multiple underlying path queries.

The Core Problem: Sublevel Bookkeeping in Query Rewriting

The bug is rooted in how GRAPH_TABLE is implemented. Rather than introducing a new executor node, the feature is desugared during rewrite (rewriteGraphTable()) into conventional relational constructs:

  1. A single-label/single-path pattern is rewritten into a SubLink/subquery RTE containing joins over the underlying vertex and edge tables, with graph pattern predicates translated into WHERE clauses.
  2. A multi-label pattern like (a IS vl1 | vl2) expands into multiple path queries — one per concrete label combination — and these are then combined with UNION ALL via generate_setop_from_pathqueries(). Each branch becomes its own subquery RTE, and the whole set operation is wrapped as the final subquery RTE replacing the original GRAPH_TABLE RTE.

When outer Vars (from a LATERAL reference such as v1.vprop1) are pulled into the rewritten subquery, their varlevelsup must be incremented to account for each new subquery-nesting level introduced. rewriteGraphTable() already does one such bump — correct for the single-path case where exactly one subquery wrapper is interposed between the outer query and the pattern predicates.

For the multi-path case, however, two subquery levels are interposed: the per-branch subquery wrapper added by generate_setop_from_pathqueries() sits between the original outer scope and the predicate that references the LATERAL Var. The rewrite code fails to bump varlevelsup the second time. Consequently, the Var that should reference v1 (the outer relation) ends up with varlevelsup pointing to the GRAPH_TABLE's own enclosing RTE — i.e., the lateral dependency collapses onto the GRAPH_TABLE RTE itself.

Where the Assertion Fires

The assertion triggers in create_lateral_join_info() (initsplan.c:1428):

Assert(!bms_is_member(rti, lateral_relids));

This invariant states that a relation cannot laterally depend on itself. When the mis-leveled Var is classified during lateral-relid computation, it is attributed to the GRAPH_TABLE-replaced RTE rather than the outer v1 RTE, producing a self-reference. The planner catches this as an inconsistency because it would cause cycles in join ordering and makes nonsense of the lateral dependency graph.

Notably, non-assert builds would likely not crash outright but would produce wrong plans or silently wrong results — the assertion is guarding a real semantic invariant.

Proposed Fix

Satya's fix is surgical and targets the rewriter, not the planner:

"Tried fixing this by bumping each lquery's sublevels by 1 before the addRangeTableEntryForSubquery() wrap. Single-pathquery queries skip this path entirely."

Concretely: before each per-branch path query is wrapped into its own subquery RTE as part of the UNION ALL construction, walk its expression trees and increment varlevelsup on outer-referencing Vars by one. This compensates for the extra subquery nesting introduced only in the multi-pathquery code path. The single-pathquery path is unaffected because it doesn't introduce the extra wrapper.

This is the correct layer to fix it: sublevel bookkeeping is a rewrite-time concern, and the existing rewriteGraphTable() already performs an analogous bump for the first wrapper. The fix simply extends the invariant — "bump once per subquery wrapper introduced" — consistently across both code paths.

Implementation Considerations Not Addressed in the Post

A reviewer would likely probe:

Broader Architectural Observation

This bug illustrates a recurring fragility in PostgreSQL's rewrite-based feature implementations: whenever a feature is desugared by introducing subquery RTEs, every code path that constructs a wrapper must cooperate with the outer-Var sublevel accounting. The invariant is implicit and easy to violate when new code paths (like multi-path expansion) are bolted on later. A more robust design might centralize the wrap-and-bump operation into a single helper so that varlevelsup adjustments cannot be forgotten.

The single-vs-multi label asymmetry is also notable: it means existing GRAPH_TABLE tests likely exercised only the single-path rewrite thoroughly, letting this escape. It is a standard lesson — set-operation expansion paths in rewriters deserve explicit test matrices against LATERAL.