Skip to content

Commit 5447f29

Browse files
committed
[CALCITE-7440] Preserve rel2sql correlation fallback without JDBC regressions
1 parent c628e68 commit 5447f29

File tree

3 files changed

+80
-3
lines changed

3 files changed

+80
-3
lines changed

core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,7 @@ private Builder visitAggregate(Aggregate e, List<Integer> groupKeyList,
772772
// "select a, b, sum(x) from ( ... ) group by a, b"
773773
final boolean ignoreClauses = e.getInput() instanceof Project;
774774
final Result x = visitInput(e, 0, isAnon(), ignoreClauses, clauseSet);
775+
parseCorrelTable(e, x);
775776
final Builder builder = x.builder(e);
776777
final List<SqlNode> selectList = new ArrayList<>();
777778
final List<SqlNode> groupByList =

core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,9 +1587,42 @@ public static SqlNode toSql(RexLiteral literal) {
15871587
}
15881588

15891589
protected Context getAliasContext(RexCorrelVariable variable) {
1590-
return requireNonNull(
1591-
correlTableMap.get(variable.id),
1592-
() -> "variable " + variable.id + " is not found");
1590+
Context context = correlTableMap.get(variable.id);
1591+
if (context == null) {
1592+
// Some rewrites can introduce a new correlation id that still points to
1593+
// the same outer scope. Reuse a compatible known context before failing.
1594+
context = findCompatibleCorrelContext(variable.id, variable.getType().getFieldCount());
1595+
if (context == null && correlTableMap.isEmpty()) {
1596+
context =
1597+
aliasContext(ImmutableMap.of(variable.id.getName(), variable.getType()), true);
1598+
}
1599+
if (context != null) {
1600+
correlTableMap.put(variable.id, context);
1601+
}
1602+
}
1603+
return requireNonNull(context, () -> "variable " + variable.id + " is not found");
1604+
}
1605+
1606+
private @Nullable Context findCompatibleCorrelContext(CorrelationId missingId,
1607+
int expectedFieldCount) {
1608+
Context fallback = null;
1609+
String fallbackName = null;
1610+
for (Map.Entry<CorrelationId, Context> entry : correlTableMap.entrySet()) {
1611+
final CorrelationId id = entry.getKey();
1612+
final Context context = entry.getValue();
1613+
if (id.equals(missingId)) {
1614+
continue;
1615+
}
1616+
if (context.fieldCount == expectedFieldCount) {
1617+
return context;
1618+
}
1619+
final String name = id.getName();
1620+
if (fallbackName == null || name.compareTo(fallbackName) < 0) {
1621+
fallbackName = name;
1622+
fallback = context;
1623+
}
1624+
}
1625+
return fallback;
15931626
}
15941627

15951628
/** Simple implementation of {@link Context} that cannot handle sub-queries

core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11887,6 +11887,49 @@ public Sql schema(CalciteAssert.SchemaSpec schemaSpec) {
1188711887
sql(sql).schema(CalciteAssert.SchemaSpec.JDBC_SCOTT).ok(expected);
1188811888
}
1188911889

11890+
/** Test case for
11891+
* <a href="https://issues.apache.org/jira/browse/CALCITE-7440">[CALCITE-7440]
11892+
* RelToSqlConverter throws NPE when correlation scope is missing after
11893+
* semi-join rewrites.</a>. */
11894+
@Test void testPostgresqlRoundTripCorrelatedProjectWithSemiJoinRules() {
11895+
final String query = "WITH product_keys AS (\n"
11896+
+ " SELECT p.\"product_id\",\n"
11897+
+ " (SELECT MAX(p3.\"product_id\")\n"
11898+
+ " FROM \"foodmart\".\"product\" p3\n"
11899+
+ " WHERE p3.\"product_id\" = p.\"product_id\") AS \"mx\"\n"
11900+
+ " FROM \"foodmart\".\"product\" p\n"
11901+
+ ")\n"
11902+
+ "SELECT DISTINCT pk.\"product_id\"\n"
11903+
+ "FROM product_keys pk\n"
11904+
+ "LEFT JOIN \"foodmart\".\"product\" p2 USING (\"product_id\")\n"
11905+
+ "WHERE pk.\"product_id\" IN (\n"
11906+
+ " SELECT p4.\"product_id\"\n"
11907+
+ " FROM \"foodmart\".\"product\" p4\n"
11908+
+ ")";
11909+
11910+
final RuleSet rules =
11911+
RuleSets.ofList(CoreRules.FILTER_SUB_QUERY_TO_CORRELATE,
11912+
CoreRules.PROJECT_SUB_QUERY_TO_CORRELATE,
11913+
CoreRules.JOIN_SUB_QUERY_TO_CORRELATE,
11914+
CoreRules.FILTER_SUB_QUERY_TO_MARK_CORRELATE,
11915+
CoreRules.PROJECT_SUB_QUERY_TO_MARK_CORRELATE,
11916+
CoreRules.MARK_TO_SEMI_OR_ANTI_JOIN_RULE,
11917+
CoreRules.PROJECT_TO_SEMI_JOIN,
11918+
CoreRules.JOIN_TO_SEMI_JOIN,
11919+
CoreRules.SEMI_JOIN_FILTER_TRANSPOSE,
11920+
CoreRules.SEMI_JOIN_JOIN_TRANSPOSE);
11921+
11922+
final String generated = sql(query).withPostgresql().optimize(rules, null).exec();
11923+
try {
11924+
sql(generated).withPostgresql().exec();
11925+
} catch (Exception e) {
11926+
throw new AssertionError(
11927+
"Generated SQL failed PostgreSQL round-trip validation:\n"
11928+
+ generated,
11929+
e);
11930+
}
11931+
}
11932+
1189011933
@Test void testNotBetween() {
1189111934
Sql f = fixture().withConvertletTable(new SqlRexConvertletTable() {
1189211935
@Override public @Nullable SqlRexConvertlet get(SqlCall call) {

0 commit comments

Comments
 (0)