|
18 | 18 |
|
19 | 19 | import org.apache.calcite.adapter.enumerable.EnumerableRules; |
20 | 20 | import org.apache.calcite.adapter.java.ReflectiveSchema; |
| 21 | +import org.apache.calcite.adapter.jdbc.JdbcSchema; |
21 | 22 | import org.apache.calcite.config.CalciteConnectionProperty; |
22 | 23 | import org.apache.calcite.config.Lex; |
23 | 24 | import org.apache.calcite.plan.RelOptPlanner; |
24 | 25 | import org.apache.calcite.prepare.Prepare; |
25 | 26 | 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; |
26 | 32 | import org.apache.calcite.test.CalciteAssert.AssertThat; |
27 | 33 | import org.apache.calcite.test.CalciteAssert.DatabaseInstance; |
28 | 34 | import org.apache.calcite.test.schemata.foodmart.FoodmartSchema; |
|
40 | 46 | import java.sql.ResultSet; |
41 | 47 | import java.sql.SQLException; |
42 | 48 | import java.sql.Statement; |
| 49 | +import java.util.Map; |
43 | 50 | import java.util.Properties; |
44 | 51 | import java.util.concurrent.locks.Lock; |
45 | 52 | import java.util.concurrent.locks.ReentrantLock; |
46 | 53 | import java.util.function.Consumer; |
47 | 54 |
|
48 | 55 | import static org.hamcrest.CoreMatchers.equalTo; |
49 | 56 | import static org.hamcrest.CoreMatchers.is; |
| 57 | +import static org.hamcrest.CoreMatchers.not; |
50 | 58 | import static org.hamcrest.MatcherAssert.assertThat; |
| 59 | +import static org.hamcrest.Matchers.stringContainsInOrder; |
51 | 60 | import static org.junit.jupiter.api.Assertions.assertFalse; |
52 | 61 |
|
53 | 62 | /** |
@@ -1696,6 +1705,99 @@ private LockWrapper exclusiveCleanDb(Connection c) throws SQLException { |
1696 | 1705 | calciteConnection.close(); |
1697 | 1706 | } |
1698 | 1707 |
|
| 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 | + |
1699 | 1801 | /** Acquires a lock, and releases it when closed. */ |
1700 | 1802 | static class LockWrapper implements AutoCloseable { |
1701 | 1803 | private final Lock lock; |
|
0 commit comments