Skip to content

feat: add LateralJoinRel support (inference + builders) - #228

Merged
nielspardon merged 1 commit into
substrait-io:mainfrom
nielspardon:feat/lateral-join-inference
Jul 29, 2026
Merged

feat: add LateralJoinRel support (inference + builders)#228
nielspardon merged 1 commit into
substrait-io:mainfrom
nielspardon:feat/lateral-join-inference

Conversation

@nielspardon

@nielspardon nielspardon commented Jul 20, 2026

Copy link
Copy Markdown
Member

Summary

Adds LateralJoinRel support (Substrait v0.96.0, #205) to schema inference and the builder / DataFrame layers. A lateral join evaluates its right (dependent) input once per left row; the right input references the current left row via OuterReference.rel_reference pointing at the join's RelCommon.rel_anchor.

Rebased onto main and refactored to build on the id-based outer-reference resolution merged in #239, so there is now a single resolution mechanism (_AnchorScope) — this PR no longer adds its own outer_anchors map.

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).
  • Id-based outer references resolve through feat!: id-based outer-reference resolution (Substrait v0.89.0) #239's plan-wide _AnchorScope. A lateral-join anchor denotes the left row, so _AnchorScope resolves it against the left input's schema (one small special case). While the right input is inferred, the left schema is bound to the anchor via a new _outer_anchor_binding context manager (a scoped _AnchorScope chained to any enclosing one), so:
    • inferring a lateral join on its own is self-contained (no whole-plan context required), and
    • nested lateral joins compose — an inner right input can reference an outer anchor via the parent chain.

Builders

  • 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, infers the right input under _outer_anchor_binding, and assembles via _plan_from so shared subtrees (CTEs) propagate.
  • 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. Only INNER and left-oriented join types are valid.
  • Anchor allocation: next_rel_anchor() is unique within a build; fresh_rel_anchors() resets numbering per materialization. DataFrame._finalize builds under fresh_rel_anchors, then normalizes any correlated-subquery steps_out to id-based via to_id_based_outer_references (feat!: id-based outer-reference resolution (Substrait v0.89.0) #239) — whose anchor pre-scan keeps DataFrame-correlation anchors clear of the lateral-join ones.

Testing

  • Inference: lateral-join output shapes (inner / left_semi / left_mark), a correlated rel_reference resolving against the left row, and the unknown-anchor error.
  • Builders: plan.lateral_join and DataFrame.lateral_join (correlated right input, join types, post-filter).
  • Full suite green; ruff check + ruff format --check clean.

Closes #205.

🤖 Generated with AI

@nielspardon
nielspardon marked this pull request as draft July 20, 2026 11:42
@nielspardon
nielspardon marked this pull request as ready for review July 20, 2026 14:22
@nielspardon
nielspardon force-pushed the feat/lateral-join-inference branch from 37cab51 to 3f5afe9 Compare July 20, 2026 14:26
@nielspardon
nielspardon force-pushed the feat/lateral-join-inference branch 2 times, most recently from 68922fd to d926068 Compare July 27, 2026 15:54
@nielspardon
nielspardon marked this pull request as draft July 27, 2026 15:56
@nielspardon
nielspardon force-pushed the feat/lateral-join-inference branch from d926068 to d802db1 Compare July 29, 2026 11:10
@nielspardon
nielspardon marked this pull request as ready for review July 29, 2026 11:14
@nielspardon nielspardon changed the title feat: infer LateralJoinRel schema with id-based outer references feat: add LateralJoinRel support (inference + builders) Jul 29, 2026
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).
- id-based outer references resolve through the plan-wide _AnchorScope added in
  substrait-io#239. A lateral join's anchor denotes its *left* row, so _AnchorScope resolves
  it against the left input's schema. While the right input is inferred the left
  schema is bound to the anchor via _outer_anchor_binding (a scoped _AnchorScope
  chained to any enclosing one, so nested lateral joins compose); this also lets
  the builder infer the right input before the join relation is assembled. No
  separate outer_anchors map -- id-based resolution is unified on _AnchorScope.

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, infers the right input under
  _outer_anchor_binding, and assembles via _plan_from so shared subtrees
  propagate.
- 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. DataFrame._finalize
  builds under fresh_rel_anchors, then normalizes any correlated-subquery
  steps_out to id-based via to_id_based_outer_references (substrait-io#239); its anchor
  pre-scan keeps DataFrame-correlation anchors clear of the lateral-join ones.

Closes substrait-io#205.
@nielspardon
nielspardon force-pushed the feat/lateral-join-inference branch from d802db1 to 06d8230 Compare July 29, 2026 12:11
@nielspardon
nielspardon merged commit 68df5fb into substrait-io:main Jul 29, 2026
21 checks passed
@nielspardon
nielspardon deleted the feat/lateral-join-inference branch July 29, 2026 13:10
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.

Update to Substrait v0.96.0

2 participants