Skip to content

feat!: id-based outer-reference resolution (Substrait 0.89.0)#982

Open
nielspardon wants to merge 4 commits into
substrait-io:mainfrom
nielspardon:feat/id-based-outer-references
Open

feat!: id-based outer-reference resolution (Substrait 0.89.0)#982
nielspardon wants to merge 4 commits into
substrait-io:mainfrom
nielspardon:feat/id-based-outer-references

Conversation

@nielspardon

@nielspardon nielspardon commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

Updates the substrait submodule to v0.89.0 and adds support for its new id-based outer-reference resolution (RelCommon.rel_anchor + OuterReference.rel_reference) alongside the existing offset-based steps_out mechanism, then makes the isthmus integration resolve correlations natively by id.

Implements #869.

Core (feat(core)!)

  • Model: Rel.getRelAnchor() / Rel.withRelAnchor(...) and FieldReference.outerReferenceRelReference().
  • Mutual exclusivity: steps_out and rel_reference are a single protobuf oneof, so they are mutually exclusive — a FieldReference carries at most one (enforced by FieldReference#check()), and both proto converters emit/read whichever form is set. (Unlike the URI→URN case in the breaking-change policy, these are not parallel fields, so there is no "dual write / prefer" behavior.)
  • OuterReferenceConverter rewrites a plan's outer references from one form to the other (toIdBased / toStepsOut). Assigning plan-wide-unique rel_anchors needs whole-plan context, so it lives in core.

Isthmus (feat(isthmus)feat(isthmus)!)

Calcite's correlation model is itself id-based (CorrelationId is an identity bound to one relation), so isthmus now resolves outer references natively by anchor rather than by subquery depth:

  • Produce: OuterReferenceResolver maps each CorrelationId to a plan-wide rel_anchor and its binding relation; SubstraitRelVisitor stamps the anchor on that relation and RexExpressionConverter emits rel_reference directly. The offset-based depth computation is gone.
  • Consume: SubstraitRelNodeConverter.Context resolves rel_reference by anchor (no subquery-depth arithmetic). Offset-based (steps_out) plans are still accepted — they are normalized to the id-based form once at the conversion boundary, so the internal machinery is purely id-based.

This replaces the earlier depth/steps_out bridge with a closer, simpler fit to Calcite's own model.

Breaking changes

  • Rel gains an abstract getRelAnchor(); non-Immutables Rel implementations must implement it (withRelAnchor(...) is a throwing default the Immutables override).
  • Isthmus's outer-reference API changed: OuterReferenceResolver.applyresolve + anchorForCorrelationId/anchorForTarget; SubstraitRelVisitor.popFieldAccessDepthMap/getFieldAccessDepthresolveOuterReferences/getOuterReferenceAnchor; the SubstraitRelNodeConverter.Context correlation API is now anchor-based.

Known limitation

OuterReferenceConverter supports outer references whose binding relation is the input of a single-input host (Filter/Project), covering correlated scalar/IN/EXISTS subqueries. A correlated reference into a multi-input (join/set) scope, or a shared subtree (ReferenceRel), throws UnsupportedOperationException. isthmus does not produce those shapes.

Testing

  • New core round-trip tests for both outer-reference encodings, rel_anchor, OuterReferenceConverter (round-trip identity, nesting, shared-scope dedup, unsupported cases), plus a mutual-exclusivity test.
  • Isthmus SubqueryConversionTest covers native id-based consume and offset-based (boundary-normalized) consume; SubqueryPlanTest covers id-based produce; OuterReferenceResolverTest covers anchor assignment and the dangling/partially-decorrelated case.
  • :core:test, :isthmus:test (incl. TPC-H / TPC-DS correlated-subquery round-trips), :core:javadoc, :isthmus:javadoc, spotlessCheck, and Spark compilation all pass.

🤖 Generated with AI

@nielspardon nielspardon force-pushed the feat/id-based-outer-references branch from 08eae97 to 4e98b39 Compare July 2, 2026 15:44
Update the substrait submodule to v0.89.0 and model its new id-based
outer-reference mechanism alongside the existing offset-based one:

- RelCommon.rel_anchor  -> Rel.getRelAnchor() / Rel.withRelAnchor(...)
- OuterReference.rel_reference -> FieldReference.outerReferenceRelReference()

steps_out and rel_reference share a single OuterReference oneof, so they
are mutually exclusive: a FieldReference carries at most one (enforced by
FieldReference#check()), and both proto converters emit/read whichever
form is set.

Add OuterReferenceConverter to rewrite a plan's outer references from one
form to the other (toIdBased / toStepsOut) so an integration can work in
a single encoding regardless of which form a plan arrives in. Assigning
plan-wide unique rel_anchors requires whole-plan context, so this lives
in core rather than being duplicated per integration.

BREAKING CHANGE: Rel gains an abstract getRelAnchor(); non-Immutables Rel
implementations must implement it. withRelAnchor(...) is added as a
throwing default method that the generated Immutables override.

Signed-off-by: Niels Pardon <par@zurich.ibm.com>
Wrap isthmus conversion with the core OuterReferenceConverter:

- Producing: convert offset-based outer references (steps_out) emitted by
  the depth-based visitor into the id-based form (rel_anchor /
  rel_reference).
- Consuming: normalize id-based plans back to steps_out before the
  existing depth-based correlation logic runs, so both encodings are
  accepted while that logic stays unchanged. Offset-based plans pass
  through untouched.

Calcite's correlation model is itself id-based (CorrelationId), so this
aligns isthmus' external interface with the direction Substrait is
migrating towards without touching its correlation wiring.

SubstraitRepeatRel, a non-Immutables test Rel, now implements the new
Rel.getRelAnchor() accessor.

Signed-off-by: Niels Pardon <par@zurich.ibm.com>
@nielspardon nielspardon force-pushed the feat/id-based-outer-references branch from 4e98b39 to a46f546 Compare July 8, 2026 13:38
Replace the steps_out bridge with native id-based correlation handling so
isthmus no longer works on offset-based outer references internally.

Producing: OuterReferenceResolver now maps each Calcite CorrelationId to a
plan-wide rel_anchor and its binding relation. SubstraitRelVisitor stamps
that anchor on the binding relation and RexExpressionConverter emits
rel_reference directly. The offset-based depth computation and the
OuterReferenceConverter.toIdBased post-pass are removed.

Consuming: SubstraitRelNodeConverter.Context resolves rel_reference by
anchor, dropping the subquery-depth arithmetic. Incoming offset-based
plans are normalized to the id-based form once at the boundary
(OuterReferenceConverter.toIdBased), so both encodings are still accepted
while the internal machinery is purely id-based.

Calcite's CorrelationId is itself an identity bound to a single relation,
so id-based resolution is a closer and simpler fit than the depth model it
replaces.

BREAKING CHANGE: OuterReferenceResolver no longer computes a
RexFieldAccess->depth map; its apply(RelNode) is replaced by
resolve(RelNode) plus anchorForCorrelationId/anchorForTarget.
SubstraitRelVisitor.popFieldAccessDepthMap/getFieldAccessDepth are
replaced by resolveOuterReferences/getOuterReferenceAnchor, and the
SubstraitRelNodeConverter.Context correlation API is now anchor-based.

Signed-off-by: Niels Pardon <par@zurich.ibm.com>
@nielspardon nielspardon force-pushed the feat/id-based-outer-references branch from 3d42703 to 3760af7 Compare July 8, 2026 14:46
Address review feedback on the id-based outer-reference work:

- core: OuterReferenceConverter clears currentInput inside a subquery
  scope so a correlated reference reached through a multi-input host
  (join/set) is rejected as unsupported instead of silently binding to
  the wrong relation; share one anchor counter across all plan roots so
  assigned rel_anchors are unique plan-wide.
- core: simplify FieldReference#check() to an int sum and drop
  fully-qualified names in OuterReferenceConverter.
- isthmus: reject duplicate rel_anchors on consume, surface an
  unresolvable rel_reference as UnsupportedOperationException
  (consistent with core), and report an actionable error when a custom
  Rel binding target cannot carry an anchor.

Add regression tests for multi-root anchor uniqueness, multi-input scope
rejection, duplicate-anchor rejection, and custom-Rel anchor support.
@nielspardon nielspardon marked this pull request as ready for review July 9, 2026 08:44
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.

1 participant