Skip to content

Commit 3760af7

Browse files
committed
feat(isthmus)!: resolve outer references natively by rel_anchor
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>
1 parent a46f546 commit 3760af7

8 files changed

Lines changed: 261 additions & 342 deletions

File tree

isthmus/src/main/java/io/substrait/isthmus/OuterReferenceResolver.java

Lines changed: 66 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -8,199 +8,131 @@
88
import org.apache.calcite.rel.core.CorrelationId;
99
import org.apache.calcite.rel.core.Filter;
1010
import org.apache.calcite.rel.core.Project;
11-
import org.apache.calcite.rex.RexCorrelVariable;
12-
import org.apache.calcite.rex.RexFieldAccess;
1311
import org.apache.calcite.rex.RexNode;
1412
import org.apache.calcite.rex.RexShuttle;
1513
import org.apache.calcite.rex.RexSubQuery;
1614
import org.apache.calcite.rex.RexUtil.SubQueryCollector;
1715

1816
/**
19-
* Resolve correlated variables and compute a depth map for {@link RexFieldAccess}.
17+
* Assigns id-based outer-reference anchors to a {@link RelNode} tree.
2018
*
21-
* <p>Traverses a {@link RelNode} tree and:
19+
* <p>Calcite models correlation with {@link CorrelationId}s, each bound by a single relation (a
20+
* {@link Correlate}'s left input, or the input of a {@link Filter}/{@link Project} that declares
21+
* the id). This maps directly onto Substrait's id-based outer references: the bound relation
22+
* carries a {@code rel_anchor} ({@link io.substrait.relation.Rel#getRelAnchor()}) and every
23+
* correlated field reference carries the matching {@code rel_reference} ({@link
24+
* io.substrait.expression.FieldReference#outerReferenceRelReference()}).
25+
*
26+
* <p>This resolver walks the tree (including subquery relations) and produces:
2227
*
2328
* <ul>
24-
* <li>Tracks nesting depth of {@link CorrelationId}s across filters, projects, subqueries, and
25-
* correlates
26-
* <li>Computes "steps out" for each {@link RexFieldAccess} referencing a {@link
27-
* RexCorrelVariable}
29+
* <li>{@link #anchorForCorrelationId(CorrelationId)} — the anchor a correlated field reference
30+
* should emit;
31+
* <li>{@link #anchorForTarget(RelNode)} — the anchor to stamp on a bound relation.
2832
* </ul>
2933
*
30-
* See OuterReferenceResolver.md for details on how the depth map is computed.
34+
* Anchors are plan-wide unique and assigned per bound relation, so multiple correlation ids sharing
35+
* a relation share its anchor.
3136
*/
3237
public class OuterReferenceResolver extends RelNodeVisitor<RelNode, RuntimeException> {
3338

34-
private final Map<CorrelationId, Integer> nestedDepth;
35-
private final Map<RexFieldAccess, Integer> fieldAccessDepthMap;
39+
private final Map<CorrelationId, Integer> anchorByCorrelationId = new HashMap<>();
40+
private final Map<RelNode, Integer> anchorByTarget = new IdentityHashMap<>();
3641

3742
private final RexVisitor rexVisitor = new RexVisitor(this);
3843

39-
/** Creates a new resolver with empty depth tracking maps. */
40-
public OuterReferenceResolver() {
41-
nestedDepth = new HashMap<>();
42-
fieldAccessDepthMap = new IdentityHashMap<>();
43-
}
44+
private int nextAnchor = 1;
45+
46+
/** Creates a new resolver with empty anchor maps. */
47+
public OuterReferenceResolver() {}
4448

4549
/**
46-
* Applies the resolver to a {@link RelNode} tree, computing the depth map.
50+
* Walks a {@link RelNode} tree, assigning outer-reference anchors.
4751
*
48-
* @param r the root relational node
52+
* @param root the root relational node
4953
* @return the same node after traversal
50-
* @throws RuntimeException if the visitor encounters an unrecoverable condition
5154
*/
52-
public Map<RexFieldAccess, Integer> apply(RelNode r) {
53-
reverseAccept(r);
54-
return fieldAccessDepthMap;
55+
public RelNode resolve(RelNode root) {
56+
reverseAccept(root);
57+
return root;
5558
}
5659

5760
/**
58-
* Visits a {@link Filter}, registering any correlation variables and visiting its condition.
61+
* Returns the anchor a correlated field reference bound to the given correlation id must emit as
62+
* its {@code rel_reference}.
5963
*
60-
* @param filter the filter node
61-
* @return the result of {@link RelNodeVisitor#visit(Filter)}
62-
* @throws RuntimeException if traversal fails
64+
* @param id the correlation id
65+
* @return the anchor, or {@code null} if the id was not resolved
6366
*/
64-
@Override
65-
public RelNode visit(Filter filter) throws RuntimeException {
66-
for (CorrelationId id : filter.getVariablesSet()) {
67-
nestedDepth.putIfAbsent(id, 0);
68-
}
69-
// Also register any CorrelationIds referenced directly in the condition expression.
70-
// This covers partially-decorrelated plans where a Calcite optimizer (e.g. HepPlanner)
71-
// rewrites correlated IN-subqueries into joins but leaves scalar-aggregate subquery
72-
// correlations as RexFieldAccess($corN.col) inside a Filter condition without a
73-
// surrounding Correlate node. In that case getVariablesSet() is empty but the condition
74-
// still contains RexCorrelVariable references whose IDs must be in nestedDepth before
75-
// rexVisitor.visitFieldAccess() is called.
76-
filter
77-
.getCondition()
78-
.accept(
79-
new RexShuttle() {
80-
@Override
81-
public RexNode visitFieldAccess(RexFieldAccess fieldAccess) {
82-
if (fieldAccess.getReferenceExpr() instanceof RexCorrelVariable) {
83-
CorrelationId id = ((RexCorrelVariable) fieldAccess.getReferenceExpr()).id;
84-
nestedDepth.putIfAbsent(id, 0);
85-
}
86-
return fieldAccess;
87-
}
88-
});
89-
filter.getCondition().accept(rexVisitor);
90-
return super.visit(filter);
67+
public Integer anchorForCorrelationId(CorrelationId id) {
68+
return anchorByCorrelationId.get(id);
9169
}
9270

9371
/**
94-
* Visits a {@link Correlate}, handling correlation depth for both sides.
95-
*
96-
* <p>Special case: the right side is a correlated subquery in the rel tree (not a REX), so we
97-
* manually adjust depth before/after visiting it.
72+
* Returns the anchor to stamp on the given relation, if it is the binding point of any outer
73+
* reference.
9874
*
99-
* @param correlate the correlate (correlated join) node
100-
* @return the correlate node
101-
* @throws RuntimeException if traversal fails
75+
* @param rel the relation
76+
* @return the anchor, or {@code null} if the relation binds no outer reference
10277
*/
103-
@Override
104-
public RelNode visit(Correlate correlate) throws RuntimeException {
105-
for (CorrelationId id : correlate.getVariablesSet()) {
106-
nestedDepth.putIfAbsent(id, 0);
107-
}
108-
109-
apply(correlate.getLeft());
110-
111-
// Correlated join is a special case. The right-rel is a correlated sub-query but not a REX. So,
112-
// the RexVisitor cannot be applied to it to correctly compute the depth map. Hence, we need to
113-
// manually compute the depth map for the right-rel.
114-
nestedDepth.replaceAll((k, v) -> v + 1);
115-
116-
apply(correlate.getRight()); // look inside sub-queries
117-
118-
nestedDepth.replaceAll((k, v) -> v - 1);
78+
public Integer anchorForTarget(RelNode rel) {
79+
return anchorByTarget.get(rel);
80+
}
11981

120-
return correlate;
82+
/** Binds {@code id} to {@code target}, allocating {@code target}'s anchor on first use. */
83+
private void bind(CorrelationId id, RelNode target) {
84+
int anchor = anchorByTarget.computeIfAbsent(target, t -> nextAnchor++);
85+
anchorByCorrelationId.put(id, anchor);
12186
}
12287

123-
/**
124-
* Visits a generic {@link RelNode}, applying the resolver to all the node inputs.
125-
*
126-
* @param other the node to visit
127-
* @return the node
128-
* @throws RuntimeException if traversal fails
129-
*/
13088
@Override
131-
public RelNode visitOther(RelNode other) throws RuntimeException {
132-
for (RelNode child : other.getInputs()) {
133-
apply(child);
89+
public RelNode visit(Filter filter) throws RuntimeException {
90+
for (CorrelationId id : filter.getVariablesSet()) {
91+
bind(id, filter.getInput());
13492
}
135-
return other;
93+
filter.getCondition().accept(rexVisitor);
94+
return visitOther(filter);
13695
}
13796

138-
/**
139-
* Visits a {@link Project}, registering correlation variables and visiting any subqueries within
140-
* its expressions.
141-
*
142-
* @param project the project node
143-
* @return the result of {@link RelNodeVisitor#visit(Project)}
144-
* @throws RuntimeException if traversal fails
145-
*/
14697
@Override
14798
public RelNode visit(Project project) throws RuntimeException {
14899
for (CorrelationId id : project.getVariablesSet()) {
149-
nestedDepth.putIfAbsent(id, 0);
100+
bind(id, project.getInput());
150101
}
151-
152102
for (RexSubQuery subQuery : SubQueryCollector.collect(project)) {
153103
subQuery.accept(rexVisitor);
154104
}
105+
return visitOther(project);
106+
}
155107

156-
return super.visit(project);
108+
@Override
109+
public RelNode visit(Correlate correlate) throws RuntimeException {
110+
bind(correlate.getCorrelationId(), correlate.getLeft());
111+
reverseAccept(correlate.getLeft());
112+
reverseAccept(correlate.getRight());
113+
return correlate;
157114
}
158115

159-
/** Rex visitor used to track correlation depth within expressions and subqueries. */
116+
@Override
117+
public RelNode visitOther(RelNode other) throws RuntimeException {
118+
for (RelNode child : other.getInputs()) {
119+
reverseAccept(child);
120+
}
121+
return other;
122+
}
123+
124+
/** Rex visitor that descends into subquery relations to reach nested correlations. */
160125
private static class RexVisitor extends RexShuttle {
161126
final OuterReferenceResolver referenceResolver;
162127

163-
/**
164-
* Creates a new Rex visitor bound to the given reference resolver.
165-
*
166-
* @param referenceResolver the parent resolver maintaining depth maps
167-
*/
168128
RexVisitor(OuterReferenceResolver referenceResolver) {
169129
this.referenceResolver = referenceResolver;
170130
}
171131

172-
/**
173-
* Increments correlation depth when entering a subquery and decrements when exiting.
174-
*
175-
* @param subQuery the subquery expression
176-
* @return the same subquery
177-
*/
178132
@Override
179133
public RexNode visitSubQuery(RexSubQuery subQuery) {
180-
referenceResolver.nestedDepth.replaceAll((k, v) -> v + 1);
181-
182-
referenceResolver.apply(subQuery.rel); // look inside sub-queries
183-
184-
referenceResolver.nestedDepth.replaceAll((k, v) -> v - 1);
134+
referenceResolver.reverseAccept(subQuery.rel);
185135
return subQuery;
186136
}
187-
188-
/**
189-
* Records depth for {@link RexFieldAccess} referencing a {@link RexCorrelVariable}.
190-
*
191-
* @param fieldAccess the field access expression
192-
* @return the same field access
193-
*/
194-
@Override
195-
public RexNode visitFieldAccess(RexFieldAccess fieldAccess) {
196-
if (fieldAccess.getReferenceExpr() instanceof RexCorrelVariable) {
197-
CorrelationId id = ((RexCorrelVariable) fieldAccess.getReferenceExpr()).id;
198-
if (referenceResolver.nestedDepth.containsKey(id)) {
199-
referenceResolver.fieldAccessDepthMap.put(
200-
fieldAccess, referenceResolver.nestedDepth.get(id));
201-
}
202-
}
203-
return fieldAccess;
204-
}
205137
}
206138
}

0 commit comments

Comments
 (0)