Skip to content

Commit fb2a794

Browse files
committed
[CALCITE-7616] ProjectToLogicalProjectAndWindowRule should not match non-logical Project nodes with JDBC convention
1 parent 1317946 commit fb2a794

2 files changed

Lines changed: 104 additions & 1 deletion

File tree

‎core/src/main/java/org/apache/calcite/rel/rules/ProjectToWindowRule.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.calcite.rel.core.Project;
2626
import org.apache.calcite.rel.hint.RelHint;
2727
import org.apache.calcite.rel.logical.LogicalCalc;
28+
import org.apache.calcite.rel.logical.LogicalProject;
2829
import org.apache.calcite.rel.logical.LogicalWindow;
2930
import org.apache.calcite.rex.RexBiVisitorImpl;
3031
import org.apache.calcite.rex.RexCall;
@@ -193,7 +194,7 @@ public interface ProjectToLogicalProjectAndWindowRuleConfig
193194
ProjectToLogicalProjectAndWindowRuleConfig DEFAULT =
194195
ImmutableProjectToLogicalProjectAndWindowRuleConfig.of()
195196
.withOperandSupplier(b ->
196-
b.operand(Project.class)
197+
b.operand(LogicalProject.class)
197198
.predicate(Project::containsOver)
198199
.anyInputs())
199200
.withDescription("ProjectToWindowRule:project");

‎core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@
1818

1919
import org.apache.calcite.adapter.enumerable.EnumerableRules;
2020
import org.apache.calcite.adapter.java.ReflectiveSchema;
21+
import org.apache.calcite.adapter.jdbc.JdbcSchema;
2122
import org.apache.calcite.config.CalciteConnectionProperty;
2223
import org.apache.calcite.config.Lex;
2324
import org.apache.calcite.plan.RelOptPlanner;
2425
import org.apache.calcite.prepare.Prepare;
2526
import org.apache.calcite.runtime.Hook;
27+
import org.apache.calcite.schema.Schema;
28+
import org.apache.calcite.schema.SchemaFactory;
29+
import org.apache.calcite.schema.SchemaPlus;
30+
import org.apache.calcite.sql.SqlDialect;
31+
import org.apache.calcite.sql.dialect.MysqlSqlDialect;
2632
import org.apache.calcite.test.CalciteAssert.AssertThat;
2733
import org.apache.calcite.test.CalciteAssert.DatabaseInstance;
2834
import org.apache.calcite.test.schemata.foodmart.FoodmartSchema;
@@ -40,14 +46,17 @@
4046
import java.sql.ResultSet;
4147
import java.sql.SQLException;
4248
import java.sql.Statement;
49+
import java.util.Map;
4350
import java.util.Properties;
4451
import java.util.concurrent.locks.Lock;
4552
import java.util.concurrent.locks.ReentrantLock;
4653
import java.util.function.Consumer;
4754

4855
import static org.hamcrest.CoreMatchers.equalTo;
4956
import static org.hamcrest.CoreMatchers.is;
57+
import static org.hamcrest.CoreMatchers.not;
5058
import static org.hamcrest.MatcherAssert.assertThat;
59+
import static org.hamcrest.Matchers.stringContainsInOrder;
5160
import static org.junit.jupiter.api.Assertions.assertFalse;
5261

5362
/**
@@ -1696,6 +1705,99 @@ private LockWrapper exclusiveCleanDb(Connection c) throws SQLException {
16961705
calciteConnection.close();
16971706
}
16981707

1708+
/** Test case for
1709+
* <a href="https://issues.apache.org/jira/browse/CALCITE-7616">[CALCITE-7616]
1710+
* ProjectToLogicalProjectAndWindowRule should not match non-logical Project
1711+
* nodes with JDBC convention</a>.
1712+
*
1713+
* <p>When a JDBC schema's dialect supports window functions (e.g. MySQL),
1714+
* a query with window functions (RANK, ROW_NUMBER, etc.) should not throw
1715+
* AssertionError because {@code ProjectToLogicalProjectAndWindowRule}
1716+
* fires on {@code JdbcProject} and creates {@code LogicalWindow} with
1717+
* JDBC convention.
1718+
*
1719+
* <p>Uses an in-memory HSQLDB database with a custom schema factory
1720+
* ({@link WindowSupportingJdbcSchemaFactory}) that wraps the HSQLDB
1721+
* connection with MySQL dialect. HSQLDB's own dialect reports
1722+
* {@code supportsWindowFunctions() = false}, so MySQL dialect (which
1723+
* reports {@code true}) is needed to trigger
1724+
* {@code JdbcProjectRule} to convert projects containing OVER expressions
1725+
* into {@code JdbcProject} nodes. */
1726+
@Test void testWindowFunctionJdbcConvention() throws Exception {
1727+
final String jdbcUrl = "jdbc:hsqldb:mem:jdbcwindowconventiontest";
1728+
try (Connection conn = DriverManager.getConnection(jdbcUrl, "SA", "")) {
1729+
try (Statement stmt = conn.createStatement()) {
1730+
stmt.execute("CREATE TABLE emp ("
1731+
+ "empno INT, "
1732+
+ "sal DECIMAL(10,2)"
1733+
+ ")");
1734+
stmt.execute("INSERT INTO emp VALUES "
1735+
+ "(1, 100.00), "
1736+
+ "(2, 200.00), "
1737+
+ "(3, 300.00)");
1738+
}
1739+
}
1740+
1741+
final String model = "{\n"
1742+
+ " version: '1.0',\n"
1743+
+ " defaultSchema: 'TEST',\n"
1744+
+ " schemas: [{\n"
1745+
+ " type: 'custom',\n"
1746+
+ " name: 'TEST',\n"
1747+
+ " factory: '" + WindowSupportingJdbcSchemaFactory.class.getName() + "',\n"
1748+
+ " operand: {\n"
1749+
+ " jdbcUrl: '" + jdbcUrl + "',\n"
1750+
+ " jdbcDriver: 'org.hsqldb.jdbcDriver',\n"
1751+
+ " jdbcUser: 'SA',\n"
1752+
+ " jdbcPassword: ''\n"
1753+
+ " }\n"
1754+
+ " }]\n"
1755+
+ "}";
1756+
1757+
final String sql = "SELECT empno, RANK() OVER (ORDER BY sal DESC) AS rnk\n"
1758+
+ "FROM emp";
1759+
1760+
try {
1761+
CalciteAssert.model(model)
1762+
.query(sql)
1763+
.runs();
1764+
} catch (Exception e) {
1765+
// After fix, the convention AssertionError should NOT occur.
1766+
// The query may fail because HSQLDB does not support RANK(), but
1767+
// that is a runtime SQL error, not a planner convention error.
1768+
assertThat(e.getMessage(),
1769+
not(stringContainsInOrder("calling-convention")));
1770+
}
1771+
}
1772+
1773+
/** Custom JDBC schema factory wrapping HSQLDB with MySQL dialect.
1774+
*
1775+
* <p>HSQLDB's dialect reports {@code supportsWindowFunctions() = false},
1776+
* so MySQL dialect is used to trigger the JdbcProjectRule path that
1777+
* exercises CALCITE-7616. */
1778+
public static class WindowSupportingJdbcSchemaFactory
1779+
implements SchemaFactory {
1780+
1781+
@Override public Schema create(SchemaPlus parentSchema, String name,
1782+
Map<String, Object> operand) {
1783+
final String jdbcUrl = (String) operand.get("jdbcUrl");
1784+
final String jdbcDriver = (String) operand.get("jdbcDriver");
1785+
final String jdbcUser = (String) operand.get("jdbcUser");
1786+
final String jdbcPassword = (String) operand.get("jdbcPassword");
1787+
1788+
final javax.sql.DataSource dataSource =
1789+
JdbcSchema.dataSource(jdbcUrl, jdbcDriver, jdbcUser, jdbcPassword);
1790+
1791+
final SqlDialect dialect =
1792+
new MysqlSqlDialect(
1793+
SqlDialect.EMPTY_CONTEXT
1794+
.withDatabaseProduct(SqlDialect.DatabaseProduct.MYSQL));
1795+
1796+
return JdbcSchema.create(parentSchema, name, dataSource,
1797+
databaseMetaData -> dialect, null, null);
1798+
}
1799+
}
1800+
16991801
/** Acquires a lock, and releases it when closed. */
17001802
static class LockWrapper implements AutoCloseable {
17011803
private final Lock lock;

0 commit comments

Comments
 (0)