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;

Reply via email to