feat: infer LateralJoinRel schema with id-based outer references#228
Open
nielspardon wants to merge 1 commit into
Open
feat: infer LateralJoinRel schema with id-based outer references#228nielspardon wants to merge 1 commit into
nielspardon wants to merge 1 commit into
Conversation
nielspardon
marked this pull request as draft
July 20, 2026 11:42
This was referenced Jul 20, 2026
nielspardon
marked this pull request as ready for review
July 20, 2026 14:22
Substrait v0.96.0 (substrait-io#205) adds LateralJoinRel, whose right (dependent) input is evaluated once per left row and references the current left row via OuterReference.rel_reference pointing to the join's RelCommon.rel_anchor. Inference (type_inference.py): - infer_rel_schema handles the "lateral_join" rel type; output columns follow the same per-join-type shapes as a regular JoinRel (INNER/LEFT -> left+right, LEFT_SEMI/LEFT_ANTI -> left only, LEFT_MARK -> +mark). - Add id-based outer reference resolution (OuterReference.rel_reference, introduced in v0.89.0, substrait-io#188) alongside the existing offset-based (steps_out) mechanism: infer_expression_type dispatches on the oneof and resolves rel_reference against a new outer_anchors context map keyed by rel_anchor. The steps_out stack is left untouched -- a lateral join is not a subquery boundary, so offset counting for nested subqueries is unaffected. Builders (handle-based correlation): - builders.plan.lateral_join(left, right, type, ...): right is a function of a LateralInput handle to the left; handle.column(...) emits the id-based outer reference. The builder allocates a rel_anchor, builds the handle, and calls right(handle) while the left schema is registered under the anchor. - DataFrame.lateral_join(right, how, *, on, post_filter) + LateralLeft.col(): the DataFrame-layer handle. Capturing the handle avoids nesting-depth bookkeeping and makes referencing outside a lateral join impossible. - rel_anchor allocation: next_rel_anchor() is unique within a build; fresh_rel_anchors() resets numbering per materialization (to_plan/to_substrait) for reproducible plans. Closes substrait-io#205. Contributes to substrait-io#188 (the DAG/ReferenceRel scope remains open).
nielspardon
force-pushed
the
feat/lateral-join-inference
branch
from
July 20, 2026 14:26
37cab51 to
3f5afe9
Compare
nielspardon
requested review from
andrew-coleman,
benbellick,
bestbeforetoday,
tokoko and
vbarua
July 21, 2026 06:51
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 full support for
LateralJoinRel, the relation introduced in Substrait v0.96.0 (#205): schema inference, the id-based outer reference resolution it depends on (introduced in v0.89.0, #188), and builders across both the low-level and DataFrame layers.A lateral join is semantically identical to a
JoinRelfor output formation, except the right (dependent) input is evaluated once per left row and may reference fields of the current left row viaOuterReference.rel_referencepointing to the join'sRelCommon.rel_anchor.Inference (
type_inference.py)infer_rel_schemahandleslateral_join. Output columns follow the same per-join-type shapes as a regularJoinRel:INNER/LEFT/LEFT_SINGLE→ left+right,LEFT_SEMI/LEFT_ANTI→ left only,LEFT_MARK→ +nullable mark.OuterReferenceis aoneofofsteps_out(offset) andrel_reference(id).infer_expression_typedispatches on it and resolvesrel_referenceagainst a newouter_anchorscontext map keyed byRelCommon.rel_anchor. The existing offset-based (steps_out) mechanism is unchanged, and the lateral join is deliberately not pushed onto thesteps_outstack (it is not a subquery boundary), so offset counting for nested subqueries is unaffected.Builders — handle-based correlation
The right (dependent) input is built as a function of a handle to the left, mirroring substrait-java's
Function<JoinInput, Expression>idiom. Sincerel_referenceis id-based, capturing a handle (identity) fits it better than counting nesting levels.builders.plan.lateral_join(left, right, type, *, expression, post_join_filter, extension)—right: Callable[[LateralInput], PlanOrUnbound]. The builder allocates arel_anchor, builds theLateralInputhandle, and callsright(handle)while the left schema is registered under the anchor (so id references resolve during the right's schema inference).handle.column(field)emits the id-basedOuterReference.DataFrame.lateral_join(right, how, *, on, post_filter)—right: Callable[[LateralLeft], DataFrame];lat.col(name)/lat[name]are the correlated references.next_rel_anchor()draws unique values within a build;fresh_rel_anchors()resets numbering per materialization, and bothDataFrame.to_planandto_substraitwrap in it, so repeated builds are reproducible.Testing
pixi run lint/pixi run format --check— clean.560 passed, 30 skipped.rel_reference→rel_anchorresolution, unknown-anchor error, directinfer_expression_typeid resolution.to_plan/to_substrait, and invalid-join-type error path.Closes #205. Contributes to #188.
🤖 Generated with AI