diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 086f114480e4..349778ac8830 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -42,6 +42,8 @@ val testH2 by configurations.creating(integrationTestConfig) val testOracle by configurations.creating(integrationTestConfig) val testPostgresql by configurations.creating(integrationTestConfig) val testMysql by configurations.creating(integrationTestConfig) +val testMssql by configurations.creating(integrationTestConfig) +val testPostgresql42 by configurations.creating(integrationTestConfig) dependencies { api(project(":linq4j")) @@ -86,6 +88,8 @@ dependencies { testMysql("mysql:mysql-connector-java") testOracle("com.oracle.ojdbc:ojdbc8") testPostgresql("org.postgresql:postgresql") + testMssql("com.microsoft.sqlserver:mssql-jdbc:${property("mssql-jdbc.version")}") + testPostgresql42("org.postgresql:postgresql:${property("postgresql-test.version")}") testImplementation(project(":testkit")) testImplementation("net.bytebuddy:byte-buddy") @@ -95,6 +99,7 @@ dependencies { testImplementation("org.apache.commons:commons-pool2") testImplementation("org.hsqldb:hsqldb::jdk8") testImplementation("sqlline:sqlline") + testImplementation("com.oracle.ojdbc:ojdbc8") testImplementation(kotlin("stdlib-jdk8")) testImplementation(kotlin("test")) testImplementation(kotlin("test-junit5")) @@ -296,3 +301,13 @@ for (db in listOf("h2", "mysql", "oracle", "postgresql")) { dependsOn(task) } } + +tasks.register("integTestCalcite6654", Test::class) { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = "Integration tests for CALCITE-6654 decimal precision fix" + include("org/apache/calcite/jdbc/JdbcSchemaDecimalBugTest.class") + systemProperty("calcite.integration.tests", "true") + classpath += configurations.getAt("testPostgresql42") + classpath += configurations.getAt("testMssql") + classpath += configurations.getAt("testOracle") +} diff --git a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcSchema.java b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcSchema.java index eb1f11b74d0b..db88542050dc 100644 --- a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcSchema.java +++ b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcSchema.java @@ -445,6 +445,14 @@ private static RelDataType sqlType(RelDataTypeFactory typeFactory, int dataType, default: break; } + // CALCITE-6654: Some JDBC drivers (Oracle, PostgreSQL, MSSQL) report + // DECIMAL/NUMERIC columns without explicit precision as precision=0. + // Treat this as unspecified precision/scale to avoid IllegalArgumentException. + if (precision == 0 && sqlTypeName == SqlTypeName.DECIMAL) { + precision = RelDataType.PRECISION_NOT_SPECIFIED; + scale = RelDataType.SCALE_NOT_SPECIFIED; + } + if (precision >= 0 && scale >= 0 && sqlTypeName.allowsPrecScale(true, true)) { diff --git a/core/src/test/java/org/apache/calcite/jdbc/JdbcSchemaDecimalBugTest.java b/core/src/test/java/org/apache/calcite/jdbc/JdbcSchemaDecimalBugTest.java new file mode 100644 index 000000000000..02fe6acc8106 --- /dev/null +++ b/core/src/test/java/org/apache/calcite/jdbc/JdbcSchemaDecimalBugTest.java @@ -0,0 +1,298 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.jdbc; + +import org.apache.calcite.adapter.jdbc.JdbcSchema; +import org.apache.calcite.schema.SchemaPlus; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Statement; +import java.util.Properties; +import java.util.logging.Logger; +import javax.sql.DataSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Integration tests for + * [CALCITE-6654] + * JdbcSchema throws error for NUMERIC/DECIMAL columns without explicit + * precision (Oracle, PostgreSQL, MSSQL). + * + *
These are integration tests that require live database instances. + * Run with: + *
{@code ./gradlew :core:integTestCalcite6654}
+ *
+ * Required Docker containers – start with: + *
{@code
+ * docker compose -f core/src/test/resources/docker-compose.yml up -d
+ * }
+ *
+ * Tests are skipped automatically if a container is unreachable.
+ */
+
+@Tag("integration")
+@EnabledIfSystemProperty(named = "calcite.integration.tests", matches = "true")
+class JdbcSchemaDecimalBugTest {
+
+ // ---------------------------------------------------------------------------
+ // DataSource factory
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Minimal {@link DataSource} backed by a JDBC URL.
+ * Sufficient for integration tests; not for production use.
+ */
+ private static class DriverManagerDataSource implements DataSource {
+ private final String url;
+ private final String user;
+ private final String password;
+
+ DriverManagerDataSource(String url, String user, String password) {
+ this.url = url;
+ this.user = user;
+ this.password = password;
+ }
+
+ @Override public Connection getConnection() throws SQLException {
+ return DriverManager.getConnection(url, user, password);
+ }
+
+ @Override public Connection getConnection(String u, String p) throws SQLException {
+ return getConnection();
+ }
+
+ @Override public PrintWriter getLogWriter() {
+ return null;
+ }
+
+ @Override public void setLogWriter(PrintWriter w) {
+ }
+
+ @Override public void setLoginTimeout(int seconds) {
+ }
+
+ @Override public int getLoginTimeout() {
+ return 0;
+ }
+
+ @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+ throw new SQLFeatureNotSupportedException("getParentLogger");
+ }
+
+ @Override public Oracle reports {@code precision=0, scale=-127} for such columns.
+ */
+ @Test void oracleNumberWithoutPrecisionShouldNotThrow() throws Exception {
+ DataSource ds = oracleDataSource();
+ assumeReachable(ds, "Oracle");
+
+ // Oracle stores unquoted identifiers in UPPERCASE
+ createTestTable(ds,
+ "BEGIN EXECUTE IMMEDIATE 'DROP TABLE TEST_NUMBERS';"
+ + " EXCEPTION WHEN OTHERS THEN NULL; END;",
+ "CREATE TABLE TEST_NUMBERS (id NUMBER, val NUMBER)",
+ "INSERT INTO TEST_NUMBERS VALUES (1, 42.5)");
+
+ try (Connection conn = DriverManager.getConnection("jdbc:calcite:", new Properties())) {
+ CalciteConnection calciteConn = conn.unwrap(CalciteConnection.class);
+ SchemaPlus root = calciteConn.getRootSchema();
+ root.add("oracle", JdbcSchema.create(root, "oracle", ds, null, "SYSTEM"));
+ try (Statement st = conn.createStatement();
+ ResultSet rs =
+ st.executeQuery("SELECT * FROM \"oracle\".\"TEST_NUMBERS\"")) {
+ while (rs.next()) {
+ //drain
+ }
+ }
+ }
+ }
+
+ /**
+ * PostgreSQL {@code NUMERIC} without precision (e.g. {@code col NUMERIC})
+ * must not cause Calcite to throw.
+ * PostgreSQL reports {@code precision=0, scale=0} for such columns.
+ */
+ @Test void postgresNumericWithoutPrecisionShouldNotThrow() throws Exception {
+ DataSource ds = postgresDataSource();
+ assumeReachable(ds, "PostgreSQL");
+
+ // Create the test table directly in the test — with NUMERIC (no precision)
+ createTestTable(ds,
+ "DROP TABLE IF EXISTS test_numbers",
+ "CREATE TABLE test_numbers (id INTEGER, val NUMERIC)",
+ "INSERT INTO test_numbers VALUES (1, 42.5)");
+
+ try (Connection conn = DriverManager.getConnection("jdbc:calcite:", new Properties())) {
+ CalciteConnection calciteConn = conn.unwrap(CalciteConnection.class);
+ SchemaPlus root = calciteConn.getRootSchema();
+ root.add("postgres",
+ JdbcSchema.create(root, "postgres", ds, "testdb", "public"));
+ try (Statement st = conn.createStatement();
+ ResultSet rs =
+ st.executeQuery("SELECT * FROM \"postgres\".\"test_numbers\"")) {
+ while (rs.next()) {
+ // drain
+ }
+ }
+ }
+ }
+
+ /**
+ * SQL Server {@code DECIMAL} without precision (e.g. {@code col DECIMAL})
+ * must not cause Calcite to throw.
+ */
+ @Test void mssqlDecimalWithoutPrecisionShouldNotThrow() throws Exception {
+ DataSource ds = mssqlDataSource();
+ assumeReachable(ds, "MSSQL");
+
+ // Create the test table directly in the test — with DECIMAL (no precision)
+ createTestTable(ds,
+ "DROP TABLE IF EXISTS test_numbers",
+ "CREATE TABLE test_numbers (id INT, val DECIMAL)",
+ "INSERT INTO test_numbers VALUES (1, 42.5)");
+
+ try (Connection conn = DriverManager.getConnection("jdbc:calcite:", new Properties())) {
+ CalciteConnection calciteConn = conn.unwrap(CalciteConnection.class);
+ SchemaPlus root = calciteConn.getRootSchema();
+ root.add("mssql", JdbcSchema.create(root, "mssql", ds, null, "dbo"));
+ try (Statement st = conn.createStatement();
+ ResultSet rs =
+ st.executeQuery("SELECT * FROM \"mssql\".\"test_numbers\"")) {
+ while (rs.next()) {
+ // drain
+ }
+ }
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // Tests - Regression: explicit precision/scale must be preserved
+ // ---------------------------------------------------------------------------
+
+ /**
+ * A column declared as {@code DECIMAL(10,2)} must still report the correct
+ * type, precision, and scale after the fix — i.e. the fix must not break
+ * columns that have explicit precision.
+ */
+ @Test void postgresDecimalWithExplicitPrecisionShouldRemainUnchanged()
+ throws Exception {
+ DataSource ds = postgresDataSource();
+ assumeReachable(ds, "PostgreSQL");
+
+ // Create a table with EXPLICIT precision — regression test
+ createTestTable(ds,
+ "DROP TABLE IF EXISTS test_explicit",
+ "CREATE TABLE test_explicit (amount DECIMAL(10, 2))",
+ "INSERT INTO test_explicit VALUES (12345.67)");
+
+ try (Connection conn = DriverManager.getConnection("jdbc:calcite:", new Properties())) {
+ CalciteConnection calciteConn = conn.unwrap(CalciteConnection.class);
+ SchemaPlus root = calciteConn.getRootSchema();
+ root.add("postgres",
+ JdbcSchema.create(root, "postgres", ds, "testdb", "public"));
+ try (Statement st = conn.createStatement();
+ ResultSet rs =
+ st.executeQuery("SELECT * FROM \"postgres\".\"test_explicit\"")) {
+ ResultSetMetaData meta = rs.getMetaData();
+ assertEquals(10, meta.getPrecision(1),
+ "Precision of DECIMAL(10,2) column must be 10");
+ assertEquals(2, meta.getScale(1),
+ "Scale of DECIMAL(10,2) column must be 2");
+ while (rs.next()) {
+ // drain
+ }
+ }
+ }
+ }
+}
diff --git a/core/src/test/resources/docker-compose.yml b/core/src/test/resources/docker-compose.yml
new file mode 100644
index 000000000000..f2df0002f6e1
--- /dev/null
+++ b/core/src/test/resources/docker-compose.yml
@@ -0,0 +1,44 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+services:
+
+ postgres:
+ image: postgres:15
+ container_name: pg-calcite
+ environment:
+ POSTGRES_DB: testdb
+ POSTGRES_USER: testuser
+ POSTGRES_PASSWORD: testpass
+ ports:
+ - "5432:5432"
+
+ mssql:
+ image: mcr.microsoft.com/mssql/server:2022-latest
+ container_name: mssql-calcite
+ environment:
+ ACCEPT_EULA: "Y"
+ SA_PASSWORD: TestPass123!
+ ports:
+ - "1433:1433"
+
+ oracle:
+ image: gvenzl/oracle-free:slim
+ container_name: oracle-calcite
+ environment:
+ ORACLE_PASSWORD: testpass
+ ports:
+ - "1521:1521"
diff --git a/gradle.properties b/gradle.properties
index 8c7e62d40aea..1e0cb8e6be83 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -170,6 +170,8 @@ tpch.version=1.0
uzaygezen.version=0.2
xalan.version=2.7.1
xercesImpl.version=2.9.1
+mssql-jdbc.version=12.4.2.jre11
+postgresql-test.version=42.7.3
# sonar properties
systemProp.sonar.gradle.skipCompile=true