diff --git a/core/src/main/java/org/apache/calcite/rel/rules/ProjectToWindowRule.java b/core/src/main/java/org/apache/calcite/rel/rules/ProjectToWindowRule.java index 931169135fd..536f5834e46 100644 --- a/core/src/main/java/org/apache/calcite/rel/rules/ProjectToWindowRule.java +++ b/core/src/main/java/org/apache/calcite/rel/rules/ProjectToWindowRule.java @@ -25,6 +25,7 @@ import org.apache.calcite.rel.core.Project; import org.apache.calcite.rel.hint.RelHint; import org.apache.calcite.rel.logical.LogicalCalc; +import org.apache.calcite.rel.logical.LogicalProject; import org.apache.calcite.rel.logical.LogicalWindow; import org.apache.calcite.rex.RexBiVisitorImpl; import org.apache.calcite.rex.RexCall; @@ -193,7 +194,7 @@ public interface ProjectToLogicalProjectAndWindowRuleConfig ProjectToLogicalProjectAndWindowRuleConfig DEFAULT = ImmutableProjectToLogicalProjectAndWindowRuleConfig.of() .withOperandSupplier(b -> - b.operand(Project.class) + b.operand(LogicalProject.class) .predicate(Project::containsOver) .anyInputs()) .withDescription("ProjectToWindowRule:project"); diff --git a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java index df12fbe8a8d..7bed49bb103 100644 --- a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java +++ b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java @@ -18,11 +18,17 @@ import org.apache.calcite.adapter.enumerable.EnumerableRules; import org.apache.calcite.adapter.java.ReflectiveSchema; +import org.apache.calcite.adapter.jdbc.JdbcSchema; import org.apache.calcite.config.CalciteConnectionProperty; import org.apache.calcite.config.Lex; import org.apache.calcite.plan.RelOptPlanner; import org.apache.calcite.prepare.Prepare; import org.apache.calcite.runtime.Hook; +import org.apache.calcite.schema.Schema; +import org.apache.calcite.schema.SchemaFactory; +import org.apache.calcite.schema.SchemaPlus; +import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.dialect.MysqlSqlDialect; import org.apache.calcite.test.CalciteAssert.AssertThat; import org.apache.calcite.test.CalciteAssert.DatabaseInstance; import org.apache.calcite.test.schemata.foodmart.FoodmartSchema; @@ -40,6 +46,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.Map; import java.util.Properties; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -47,7 +54,9 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.stringContainsInOrder; import static org.junit.jupiter.api.Assertions.assertFalse; /** @@ -1696,6 +1705,99 @@ private LockWrapper exclusiveCleanDb(Connection c) throws SQLException { calciteConnection.close(); } + /** Test case for + * [CALCITE-7616] + * ProjectToLogicalProjectAndWindowRule should not match non-logical Project + * nodes with JDBC convention. + * + *
When a JDBC schema's dialect supports window functions (e.g. MySQL), + * a query with window functions (RANK, ROW_NUMBER, etc.) should not throw + * AssertionError because {@code ProjectToLogicalProjectAndWindowRule} + * fires on {@code JdbcProject} and creates {@code LogicalWindow} with + * JDBC convention. + * + *
Uses an in-memory HSQLDB database with a custom schema factory + * ({@link WindowSupportingJdbcSchemaFactory}) that wraps the HSQLDB + * connection with MySQL dialect. HSQLDB's own dialect reports + * {@code supportsWindowFunctions() = false}, so MySQL dialect (which + * reports {@code true}) is needed to trigger + * {@code JdbcProjectRule} to convert projects containing OVER expressions + * into {@code JdbcProject} nodes. */ + @Test void testWindowFunctionJdbcConvention() throws Exception { + final String jdbcUrl = "jdbc:hsqldb:mem:jdbcwindowconventiontest"; + try (Connection conn = DriverManager.getConnection(jdbcUrl, "SA", "")) { + try (Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE emp (" + + "empno INT, " + + "sal DECIMAL(10,2)" + + ")"); + stmt.execute("INSERT INTO emp VALUES " + + "(1, 100.00), " + + "(2, 200.00), " + + "(3, 300.00)"); + } + } + + final String model = "{\n" + + " version: '1.0',\n" + + " defaultSchema: 'TEST',\n" + + " schemas: [{\n" + + " type: 'custom',\n" + + " name: 'TEST',\n" + + " factory: '" + WindowSupportingJdbcSchemaFactory.class.getName() + "',\n" + + " operand: {\n" + + " jdbcUrl: '" + jdbcUrl + "',\n" + + " jdbcDriver: 'org.hsqldb.jdbcDriver',\n" + + " jdbcUser: 'SA',\n" + + " jdbcPassword: ''\n" + + " }\n" + + " }]\n" + + "}"; + + final String sql = "SELECT empno, RANK() OVER (ORDER BY sal DESC) AS rnk\n" + + "FROM emp"; + + try { + CalciteAssert.model(model) + .query(sql) + .runs(); + } catch (Exception e) { + // After fix, the convention AssertionError should NOT occur. + // The query may fail because HSQLDB does not support RANK(), but + // that is a runtime SQL error, not a planner convention error. + assertThat(e.getMessage(), + not(stringContainsInOrder("calling-convention"))); + } + } + + /** Custom JDBC schema factory wrapping HSQLDB with MySQL dialect. + * + *
HSQLDB's dialect reports {@code supportsWindowFunctions() = false},
+ * so MySQL dialect is used to trigger the JdbcProjectRule path that
+ * exercises CALCITE-7616. */
+ public static class WindowSupportingJdbcSchemaFactory
+ implements SchemaFactory {
+
+ @Override public Schema create(SchemaPlus parentSchema, String name,
+ Map