This is an automated email from the ASF dual-hosted git repository.
mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 7424c0ae3f [CALCITE-7616] ProjectToLogicalProjectAndWindowRule should
only match logical Project nodes
7424c0ae3f is described below
commit 7424c0ae3fe4641925460935522fbc8b4600b8fe
Author: leishp <[email protected]>
AuthorDate: Tue Jun 23 20:12:21 2026 +0800
[CALCITE-7616] ProjectToLogicalProjectAndWindowRule should only match
logical Project nodes
---
.../calcite/rel/rules/ProjectToWindowRule.java | 3 +-
.../org/apache/calcite/test/JdbcAdapterTest.java | 102 +++++++++++++++++++++
2 files changed, 104 insertions(+), 1 deletion(-)
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 931169135f..536f5834e4 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 df12fbe8a8..7bed49bb10 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
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7616">[CALCITE-7616]
+ * ProjectToLogicalProjectAndWindowRule should not match non-logical Project
+ * nodes with JDBC convention</a>.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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<String, Object> operand) {
+ final String jdbcUrl = (String) operand.get("jdbcUrl");
+ final String jdbcDriver = (String) operand.get("jdbcDriver");
+ final String jdbcUser = (String) operand.get("jdbcUser");
+ final String jdbcPassword = (String) operand.get("jdbcPassword");
+
+ final javax.sql.DataSource dataSource =
+ JdbcSchema.dataSource(jdbcUrl, jdbcDriver, jdbcUser, jdbcPassword);
+
+ final SqlDialect dialect =
+ new MysqlSqlDialect(
+ SqlDialect.EMPTY_CONTEXT
+ .withDatabaseProduct(SqlDialect.DatabaseProduct.MYSQL));
+
+ return JdbcSchema.create(parentSchema, name, dataSource,
+ databaseMetaData -> dialect, null, null);
+ }
+ }
+
/** Acquires a lock, and releases it when closed. */
static class LockWrapper implements AutoCloseable {
private final Lock lock;