Skip to content

feat: shared subplans (ReferenceRel) / DataFrame.cache() (CTEs) - #235

Merged
nielspardon merged 1 commit into
substrait-io:mainfrom
nielspardon:feat/reference-rel-ctes
Jul 28, 2026
Merged

feat: shared subplans (ReferenceRel) / DataFrame.cache() (CTEs)#235
nielspardon merged 1 commit into
substrait-io:mainfrom
nielspardon:feat/reference-rel-ctes

Conversation

@nielspardon

Copy link
Copy Markdown
Member

Summary

Adds CTEs / shared subplansReferenceRel + subtree_ordinal — to both the substrait.builders.plan layer and the native substrait.dataframe API (as DataFrame.cache()), closing #211.

Per the design agreed in the #204 review, the shared subtrees are carried in-band inside the resolved Plan — as the leading rel entries of Plan.relations, the same way a Plan already carries its extension declarations — rather than out-of-band via a contextvar. Because the subtrees then live inside the resolved Plan, the builder-internal infer_plan_schema(bound_plan) calls have the subtree list in hand directly, so no contextvar is needed.

What's in it

type_inference

  • Thread an optional subtrees list through infer_rel_schema / infer_expression_type; add a reference case that resolves a ReferenceRel's schema against subtrees[subtree_ordinal].
  • infer_plan_schema extracts the leading subtrees and wraps them in a _SubtreeScope that memoizes each subtree's schema (a subtree referenced from many places is inferred once, not exponentially) and detects reference cycles (a clear error instead of RecursionError).

builders.plan

  • A reference(plan) builder promotes a plan to a shared subtree and returns a ReferenceRel-rooted plan.
  • Every relational builder propagates its inputs' shared subtrees upward and rebases subtree_ordinals when merging inputs, structurally deduping identical subtrees so a cached frame reused across branches that later meet at a join/union is emitted once and referenced many times.
  • A shared _plan_from helper owns subtree propagation + Plan assembly, so each builder is a single call.

dataframe

Subqueries

  • A cached frame consumed inside a subquery is inlined into that subquery — a plan-global ReferenceRel cannot cross the subquery boundary — keeping the emitted plan valid.

Internals

  • Rel-walking primitives (plan_subtrees, rebase_reference_ordinals, inline_reference_rels) live in substrait.utils and discover child-Rel fields from the protobuf descriptor rather than a hand-maintained table.

Notes

  • subtree_ordinal indexing: subtrees are the leading rel entries and the query root is last, so the ordinal indexes both relations and the ordered subtree list identically. The proto / substrait-java are underspecified here; this matches the agreed design and the earlier prototype.
  • Structural dedup collapses byte-equal serialized Rels, which are semantically identical — this is the CTE "compute once, reference many" behavior.

Testing

  • New tests for the reference builder (dedup / rebasing / schema inference), DataFrame.cache() (shared/nested/distinct caches, extension merge, subquery inlining, hint()-after-cache()), and the reference inference case (cycle + out-of-range).
  • Full suite green (572 passed), pixi run lint and pixi run format clean. Existing byte-exact builder comparisons still pass, confirming the no-subtree path is unchanged.

🤖 Generated with AI

@nielspardon
nielspardon force-pushed the feat/reference-rel-ctes branch from 4d87e95 to 5dd720d Compare July 27, 2026 16:10
Add CTE / shared-subplan support at both the builders.plan and native
DataFrame layers, carrying the shared subtrees in-band inside the resolved
Plan (the leading `rel` entries of Plan.relations, the way a Plan already
carries its extension declarations) rather than out-of-band via a contextvar.

type_inference: thread an optional `subtrees` list through infer_rel_schema /
infer_expression_type and add a `reference` case that resolves a ReferenceRel's
schema against subtrees[subtree_ordinal]. infer_plan_schema extracts the leading
subtrees and wraps them in a _SubtreeScope that memoizes each subtree's schema
(so a subtree referenced from many places is inferred once, not exponentially)
and detects reference cycles (a clear error instead of RecursionError).

builders: a `reference(plan)` builder promotes a plan to a shared subtree and
returns a ReferenceRel-rooted plan; every relational builder propagates its
inputs' shared subtrees upward and rebases subtree_ordinals when merging inputs,
structurally deduping identical subtrees so a cached frame reused across
branches is emitted once and referenced many times. A shared _plan_from helper
owns subtree propagation + Plan assembly, so each builder is a single call.

dataframe: DataFrame.cache() marks a frame as a reusable common subplan; the
hint() guard for RelCommon-less relations (e.g. a ReferenceRel) is restored.

A cached frame consumed inside a subquery is inlined into that subquery (a
plan-global ReferenceRel cannot cross the subquery boundary), keeping the
emitted plan valid.

Rel-walking primitives (plan_subtrees, rebase_reference_ordinals,
inline_reference_rels) live in substrait.utils and discover child-Rel fields
from the protobuf descriptor rather than a hand-maintained table.

Closes substrait-io#211
@nielspardon
nielspardon force-pushed the feat/reference-rel-ctes branch from 5dd720d to 7d1a455 Compare July 27, 2026 16:13
@nielspardon
nielspardon marked this pull request as ready for review July 27, 2026 16:15
@nielspardon
nielspardon merged commit 4436530 into substrait-io:main Jul 28, 2026
21 checks passed
@nielspardon
nielspardon deleted the feat/reference-rel-ctes branch July 28, 2026 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants