feat: shared subplans (ReferenceRel) / DataFrame.cache() (CTEs)#235
Draft
nielspardon wants to merge 1 commit into
Draft
feat: shared subplans (ReferenceRel) / DataFrame.cache() (CTEs)#235nielspardon wants to merge 1 commit into
nielspardon wants to merge 1 commit into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds CTEs / shared subplans —
ReferenceRel+subtree_ordinal— to both thesubstrait.builders.planlayer and the nativesubstrait.dataframeAPI (asDataFrame.cache()), closing #211.Per the design agreed in the #204 review, the shared subtrees are carried in-band inside the resolved
Plan— as the leadingrelentries ofPlan.relations, the same way aPlanalready carries its extension declarations — rather than out-of-band via a contextvar. Because the subtrees then live inside the resolvedPlan, the builder-internalinfer_plan_schema(bound_plan)calls have the subtree list in hand directly, so no contextvar is needed.What's in it
type_inferencesubtreeslist throughinfer_rel_schema/infer_expression_type; add areferencecase that resolves aReferenceRel's schema againstsubtrees[subtree_ordinal].infer_plan_schemaextracts the leading subtrees and wraps them in a_SubtreeScopethat memoizes each subtree's schema (a subtree referenced from many places is inferred once, not exponentially) and detects reference cycles (a clear error instead ofRecursionError).builders.planreference(plan)builder promotes a plan to a shared subtree and returns aReferenceRel-rooted plan.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._plan_fromhelper owns subtree propagation +Planassembly, so each builder is a single call.dataframeDataFrame.cache()marks a frame as a reusable common subplan.hint()guard forRelCommon-less relations (e.g. aReferenceRel) is restored (removed alongside the prototype in feat: ergonomic native DataFrame/Expr API (substrait.api) #204).Subqueries
ReferenceRelcannot cross the subquery boundary — keeping the emitted plan valid.Internals
plan_subtrees,rebase_reference_ordinals,inline_reference_rels) live insubstrait.utilsand discover child-Relfields from the protobuf descriptor rather than a hand-maintained table.Notes
subtree_ordinalindexing: subtrees are the leadingrelentries and the query root is last, so the ordinal indexes bothrelationsand the ordered subtree list identically. The proto / substrait-java are underspecified here; this matches the agreed design and the earlier prototype.Rels, which are semantically identical — this is the CTE "compute once, reference many" behavior.Testing
referencebuilder (dedup / rebasing / schema inference),DataFrame.cache()(shared/nested/distinct caches, extension merge, subquery inlining,hint()-after-cache()), and thereferenceinference case (cycle + out-of-range).572 passed),pixi run lintandpixi run formatclean. Existing byte-exact builder comparisons still pass, confirming the no-subtree path is unchanged.🤖 Generated with AI