|
7 | 7 | import org.apache.calcite.rel.core.JoinRelType; |
8 | 8 | import org.apache.calcite.rex.RexCorrelVariable; |
9 | 9 | import org.apache.calcite.rex.RexFieldAccess; |
| 10 | +import org.apache.calcite.rex.RexNode; |
10 | 11 | import org.apache.calcite.sql.parser.SqlParseException; |
11 | 12 | import org.apache.calcite.tools.RelBuilder; |
12 | 13 | import org.apache.calcite.util.Holder; |
@@ -142,4 +143,69 @@ void nestedApplyJoinQuery() throws SqlParseException { |
142 | 143 | validateOuterRef(fieldAccessDepthMap, "$cor0", "SS_ITEM_SK", 2); |
143 | 144 | validateOuterRef(fieldAccessDepthMap, "$cor1", "I_ITEM_SK", 1); |
144 | 145 | } |
| 146 | + |
| 147 | + /** |
| 148 | + * Regression test for the partially-decorrelated Filter case. |
| 149 | + * |
| 150 | + * <p>When a Calcite optimizer (e.g. HepPlanner) rewrites a correlated IN-subquery into a join, it |
| 151 | + * can produce a {@link org.apache.calcite.rel.core.Filter} whose condition contains a {@link |
| 152 | + * RexFieldAccess} referencing a {@link RexCorrelVariable} ({@code $cor0.field}), but whose {@link |
| 153 | + * RelNode#getVariablesSet()} is empty — because the enclosing {@link |
| 154 | + * org.apache.calcite.rel.core.Correlate} node has already been replaced by a regular join. |
| 155 | + * |
| 156 | + * <p>Before the fix, {@code OuterReferenceResolver.visit(Filter)} only iterated {@code |
| 157 | + * filter.getVariablesSet()}, so the {@link org.apache.calcite.rel.core.CorrelationId} was never |
| 158 | + * registered in {@code nestedDepth}. The subsequent {@code rexVisitor.visitFieldAccess()} call |
| 159 | + * found {@code !nestedDepth.containsKey(id)} and silently skipped the access, producing an empty |
| 160 | + * {@code fieldAccessDepthMap} instead of the expected entry. |
| 161 | + * |
| 162 | + * <p>The fix pre-scans the Filter condition for {@link RexCorrelVariable} references and |
| 163 | + * registers their IDs before delegating to the rex visitor. |
| 164 | + */ |
| 165 | + @Test |
| 166 | + void filterWithCorrelVariableButEmptyVariablesSet() throws SqlParseException { |
| 167 | + // Build the partially-decorrelated pattern: |
| 168 | + // JOIN (inner side has a Filter whose condition references $cor0 but variablesSet is empty) |
| 169 | + // |
| 170 | + // SQL equivalent of what a post-optimisation plan looks like: |
| 171 | + // SELECT ss_sold_date_sk FROM store_sales |
| 172 | + // JOIN item ON item.i_item_sk = store_sales.ss_item_sk ← decorrelated join |
| 173 | + // WHERE item.i_item_sk = $cor0.SS_ITEM_SK ← Filter still has the correl ref |
| 174 | + // |
| 175 | + // We deliberately call .filter(condition) without passing the CorrelationId so that |
| 176 | + // getVariablesSet() returns an empty set, reproducing the post-decorrelation shape. |
| 177 | + |
| 178 | + final Holder<RexCorrelVariable> cor0 = Holder.empty(); |
| 179 | + |
| 180 | + // Push STORE_SALES and capture the correlation variable for it. |
| 181 | + tpcDsRelBuilder.scan("tpcds", "STORE_SALES").variable(cor0::set); |
| 182 | + |
| 183 | + // Push ITEM on top, then build the filter condition while ITEM is the top-of-stack. |
| 184 | + // The condition equates item.I_ITEM_SK to the correlated $cor0.SS_ITEM_SK. |
| 185 | + tpcDsRelBuilder.scan("tpcds", "ITEM"); |
| 186 | + RexNode correlCondition = |
| 187 | + tpcDsRelBuilder.equals( |
| 188 | + tpcDsRelBuilder.field("I_ITEM_SK"), tpcDsRelBuilder.field(cor0.get(), "SS_ITEM_SK")); |
| 189 | + |
| 190 | + final RelNode calciteRel = |
| 191 | + tpcDsRelBuilder |
| 192 | + // Intentionally NOT passing cor0.get().id to filter() → getVariablesSet() is empty. |
| 193 | + .filter(correlCondition) |
| 194 | + .join(JoinRelType.INNER, tpcDsRelBuilder.literal(true)) |
| 195 | + .project(tpcDsRelBuilder.field("SS_SOLD_DATE_SK")) |
| 196 | + .build(); |
| 197 | + |
| 198 | + // Resolver registers the dangling correlation (no longer silently dropped → no NPE). |
| 199 | + final Map<RexFieldAccess, Integer> fieldAccessDepthMap = buildOuterFieldRefMap(calciteRel); |
| 200 | + Assertions.assertEquals(1, fieldAccessDepthMap.size()); |
| 201 | + validateOuterRef(fieldAccessDepthMap, "$cor0", "SS_ITEM_SK", 0); |
| 202 | + |
| 203 | + // steps_out=0 is not a valid Substrait outer reference (proto requires >= 1), |
| 204 | + // so conversion must fail clearly rather than emit an invalid plan. |
| 205 | + UnsupportedOperationException ex = |
| 206 | + Assertions.assertThrows( |
| 207 | + UnsupportedOperationException.class, |
| 208 | + () -> SubstraitRelVisitor.convert(calciteRel, converterProvider)); |
| 209 | + Assertions.assertTrue(ex.getMessage().contains("steps_out")); |
| 210 | + } |
145 | 211 | } |
0 commit comments