This is an automated email from the ASF dual-hosted git repository. zabetak pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/master by this push: new 48126d3 [CALCITE-4706] JDBC adapter generates casts exceeding Redshift's data types bounds 48126d3 is described below commit 48126d3d3834e28e84ae3fc4c65b86831b5d6aef Author: Stamatis Zampetakis <zabe...@gmail.com> AuthorDate: Thu Jul 29 14:16:20 2021 +0300 [CALCITE-4706] JDBC adapter generates casts exceeding Redshift's data types bounds 1. Add Redshift type system ensuring precision/scale bounds are satisfied when casting. 2. Consider max precision for DECIMAL and CHAR data types in SqlDialect#getCastSpec. 3. Consider max scale for DECIMAL data types in SqlDialect#getCastSpec. 4. Extend RelToSqlConverterTest.Sql with custom type system to allow writting tests with non-default type system (e.g., simulate systems with large numeric precision). 5. Set typesystem explicitly in SqlDialectFactoryImpl since it cannot be derived from the metadata and the default is not appropriate. 6. Add unit tests casting to DECIMAL, CHAR, VARCHAR, with precision/scale exceeding Redshift's bounds. Close apache/calcite#2470 --- .../java/org/apache/calcite/sql/SqlDialect.java | 7 +- .../apache/calcite/sql/SqlDialectFactoryImpl.java | 3 +- .../calcite/sql/dialect/RedshiftSqlDialect.java | 28 ++++++- .../org/apache/calcite/sql/type/SqlTypeUtil.java | 8 +- .../rel/rel2sql/RelToSqlConverterStructsTest.java | 3 +- .../calcite/rel/rel2sql/RelToSqlConverterTest.java | 97 ++++++++++++++++++---- 6 files changed, 124 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java index 0942973..06d5de9 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java @@ -789,10 +789,15 @@ public class SqlDialect { * {@code CAST(NULL AS <nulltype>)} is rendered as {@code NULL}. */ public @Nullable SqlNode getCastSpec(RelDataType type) { int maxPrecision = -1; + int maxScale = -1; if (type instanceof AbstractSqlType) { switch (type.getSqlTypeName()) { case NULL: return null; + case DECIMAL: + maxScale = getTypeSystem().getMaxScale(type.getSqlTypeName()); + // fall through + case CHAR: case VARCHAR: // if needed, adjust varchar length to max length supported by the system maxPrecision = getTypeSystem().getMaxPrecision(type.getSqlTypeName()); @@ -803,7 +808,7 @@ public class SqlDialect { String charSet = type.getCharset() != null && supportsCharSet() ? type.getCharset().name() : null; - return SqlTypeUtil.convertTypeToSpec(type, charSet, maxPrecision); + return SqlTypeUtil.convertTypeToSpec(type, charSet, maxPrecision, maxScale); } return SqlTypeUtil.convertTypeToSpec(type); } diff --git a/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java b/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java index 91f0f31..b7d025a 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java @@ -129,7 +129,8 @@ public class SqlDialectFactoryImpl implements SqlDialectFactory { return new MysqlSqlDialect( c.withDataTypeSystem(MysqlSqlDialect.MYSQL_TYPE_SYSTEM)); case "REDSHIFT": - return new RedshiftSqlDialect(c); + return new RedshiftSqlDialect( + c.withDataTypeSystem(RedshiftSqlDialect.TYPE_SYSTEM)); case "SNOWFLAKE": return new SnowflakeSqlDialect(c); case "SPARK": diff --git a/core/src/main/java/org/apache/calcite/sql/dialect/RedshiftSqlDialect.java b/core/src/main/java/org/apache/calcite/sql/dialect/RedshiftSqlDialect.java index 5785520..0d498de 100644 --- a/core/src/main/java/org/apache/calcite/sql/dialect/RedshiftSqlDialect.java +++ b/core/src/main/java/org/apache/calcite/sql/dialect/RedshiftSqlDialect.java @@ -18,12 +18,15 @@ package org.apache.calcite.sql.dialect; import org.apache.calcite.avatica.util.Casing; import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeSystem; +import org.apache.calcite.rel.type.RelDataTypeSystemImpl; import org.apache.calcite.sql.SqlDataTypeSpec; import org.apache.calcite.sql.SqlDialect; import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.SqlUserDefinedTypeNameSpec; import org.apache.calcite.sql.SqlWriter; import org.apache.calcite.sql.parser.SqlParserPos; +import org.apache.calcite.sql.type.SqlTypeName; import org.checkerframework.checker.nullness.qual.Nullable; @@ -31,12 +34,35 @@ import org.checkerframework.checker.nullness.qual.Nullable; * A <code>SqlDialect</code> implementation for the Redshift database. */ public class RedshiftSqlDialect extends SqlDialect { + public static final RelDataTypeSystem TYPE_SYSTEM = + new RelDataTypeSystemImpl() { + @Override public int getMaxPrecision(SqlTypeName typeName) { + switch (typeName) { + case VARCHAR: + return 65535; + case CHAR: + return 4096; + default: + return super.getMaxPrecision(typeName); + } + } + + @Override public int getMaxNumericPrecision() { + return 38; + } + + @Override public int getMaxNumericScale() { + return 37; + } + }; + public static final SqlDialect.Context DEFAULT_CONTEXT = SqlDialect.EMPTY_CONTEXT .withDatabaseProduct(SqlDialect.DatabaseProduct.REDSHIFT) .withIdentifierQuoteString("\"") .withQuotedCasing(Casing.TO_LOWER) .withUnquotedCasing(Casing.TO_LOWER) - .withCaseSensitive(false); + .withCaseSensitive(false) + .withDataTypeSystem(TYPE_SYSTEM); public static final SqlDialect DEFAULT = new RedshiftSqlDialect(DEFAULT_CONTEXT); diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java index 714371b..3fdd416 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java +++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java @@ -1021,10 +1021,11 @@ public abstract class SqlTypeUtil { * @param type type descriptor * @param charSetName charSet name * @param maxPrecision The max allowed precision. + * @param maxScale max allowed scale * @return corresponding parse representation */ public static SqlDataTypeSpec convertTypeToSpec(RelDataType type, - @Nullable String charSetName, int maxPrecision) { + @Nullable String charSetName, int maxPrecision, int maxScale) { SqlTypeName typeName = type.getSqlTypeName(); // TODO jvs 28-Dec-2004: support row types, user-defined types, @@ -1039,6 +1040,9 @@ public abstract class SqlTypeUtil { precision = maxPrecision; } int scale = typeName.allowsScale() ? type.getScale() : -1; + if (maxScale > 0 && scale > maxScale) { + scale = maxScale; + } typeNameSpec = new SqlBasicTypeNameSpec( typeName, @@ -1084,7 +1088,7 @@ public abstract class SqlTypeUtil { public static SqlDataTypeSpec convertTypeToSpec(RelDataType type) { // TODO jvs 28-Dec-2004: collation String charSetName = inCharFamily(type) ? type.getCharset().name() : null; - return convertTypeToSpec(type, charSetName, -1); + return convertTypeToSpec(type, charSetName, -1, -1); } public static RelDataType createMultisetType( diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterStructsTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterStructsTest.java index 284bc10..e92f102 100644 --- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterStructsTest.java +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterStructsTest.java @@ -16,6 +16,7 @@ */ package org.apache.calcite.rel.rel2sql; +import org.apache.calcite.rel.type.RelDataTypeSystem; import org.apache.calcite.sql.dialect.CalciteSqlDialect; import org.apache.calcite.sql.parser.SqlParser; import org.apache.calcite.test.CalciteAssert; @@ -36,7 +37,7 @@ class RelToSqlConverterStructsTest { private RelToSqlConverterTest.Sql sql(String sql) { return new RelToSqlConverterTest.Sql(CalciteAssert.SchemaSpec.MY_DB, sql, CalciteSqlDialect.DEFAULT, SqlParser.Config.DEFAULT, ImmutableSet.of(), - UnaryOperator.identity(), null, ImmutableList.of()); + UnaryOperator.identity(), null, ImmutableList.of(), RelDataTypeSystem.DEFAULT); } @Test void testNestedSchemaSelectStar() { diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java index 51ed1f3..94ef0c0 100644 --- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java @@ -91,6 +91,7 @@ import org.junit.jupiter.api.Test; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -115,7 +116,7 @@ class RelToSqlConverterTest { private Sql sql(String sql) { return new Sql(CalciteAssert.SchemaSpec.JDBC_FOODMART, sql, CalciteSqlDialect.DEFAULT, SqlParser.Config.DEFAULT, ImmutableSet.of(), - UnaryOperator.identity(), null, ImmutableList.of()); + UnaryOperator.identity(), null, ImmutableList.of(), RelDataTypeSystem.DEFAULT); } /** Initiates a test case with a given {@link RelNode} supplier. */ @@ -128,7 +129,7 @@ class RelToSqlConverterTest { private static Planner getPlanner(List<RelTraitDef> traitDefs, SqlParser.Config parserConfig, SchemaPlus schema, SqlToRelConverter.Config sqlToRelConf, Collection<SqlLibrary> librarySet, - Program... programs) { + RelDataTypeSystem typeSystem, Program... programs) { final MockSqlOperatorTable operatorTable = new MockSqlOperatorTable( SqlOperatorTables.chain(SqlStdOperatorTable.instance(), @@ -142,6 +143,7 @@ class RelToSqlConverterTest { .sqlToRelConverterConfig(sqlToRelConf) .programs(programs) .operatorTable(operatorTable) + .typeSystem(typeSystem) .build(); return Frameworks.getPlanner(config); } @@ -711,6 +713,59 @@ class RelToSqlConverterTest { sql(query).ok(expected); } + /** + * Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-4706">[CALCITE-4706] + * JDBC adapter generates casts exceeding Redshift's data types bounds</a>. + */ + @Test void testCastDecimalBigPrecision() { + final String query = "select cast(\"product_id\" as decimal(60,2)) " + + "from \"product\" "; + final String expectedRedshift = "SELECT CAST(\"product_id\" AS DECIMAL(38, 2))\n" + + "FROM \"foodmart\".\"product\""; + sql(query) + .withTypeSystem(new RelDataTypeSystemImpl() { + @Override public int getMaxNumericPrecision() { + // Ensures that parsed decimal will not be truncated during SQL to Rel transformation + // The default type system sets precision to 19 so it is not sufficient to test + // this change. + return 100; + } + }) + .withRedshift() + .ok(expectedRedshift); + } + + /** + * Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-4706">[CALCITE-4706] + * JDBC adapter generates casts exceeding Redshift's data types bounds</a>. + */ + @Test void testCastDecimalBigScale() { + final String query = "select cast(\"product_id\" as decimal(2,90)) " + + "from \"product\" "; + final String expectedRedshift = "SELECT CAST(\"product_id\" AS DECIMAL(2, 37))\n" + + "FROM \"foodmart\".\"product\""; + sql(query) + .withRedshift() + .ok(expectedRedshift); + } + + /** + * Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-4706">[CALCITE-4706] + * JDBC adapter generates casts exceeding Redshift's data types bounds</a>. + */ + @Test void testCastLongChar() { + final String query = "select cast(\"product_id\" as char(9999999)) " + + "from \"product\" "; + final String expectedRedshift = "SELECT CAST(\"product_id\" AS CHAR(4096))\n" + + "FROM \"foodmart\".\"product\""; + sql(query) + .withRedshift() + .ok(expectedRedshift); + } + /** Test case for * <a href="https://issues.apache.org/jira/browse/CALCITE-2713">[CALCITE-2713] * JDBC adapter may generate casts on PostgreSQL for VARCHAR type exceeding @@ -720,15 +775,17 @@ class RelToSqlConverterTest { + " from \"expense_fact\""; final String expectedPostgreSQL = "SELECT CAST(\"store_id\" AS VARCHAR(256))\n" + "FROM \"foodmart\".\"expense_fact\""; - sql(query) - .withPostgresqlModifiedTypeSystem() - .ok(expectedPostgreSQL); - final String expectedOracle = "SELECT CAST(\"store_id\" AS VARCHAR(512))\n" + "FROM \"foodmart\".\"expense_fact\""; + final String expectedRedshift = "SELECT CAST(\"store_id\" AS VARCHAR(65535))\n" + + "FROM \"foodmart\".\"expense_fact\""; sql(query) + .withPostgresqlModifiedTypeSystem() + .ok(expectedPostgreSQL) .withOracleModifiedTypeSystem() - .ok(expectedOracle); + .ok(expectedOracle) + .withRedshift() + .ok(expectedRedshift); } /** Test case for @@ -5996,12 +6053,14 @@ class RelToSqlConverterTest { private final List<Function<RelNode, RelNode>> transforms; private final SqlParser.Config parserConfig; private final UnaryOperator<SqlToRelConverter.Config> config; + private final RelDataTypeSystem typeSystem; Sql(CalciteAssert.SchemaSpec schemaSpec, String sql, SqlDialect dialect, SqlParser.Config parserConfig, Set<SqlLibrary> librarySet, UnaryOperator<SqlToRelConverter.Config> config, @Nullable Function<RelBuilder, RelNode> relFn, - List<Function<RelNode, RelNode>> transforms) { + List<Function<RelNode, RelNode>> transforms, + RelDataTypeSystem typeSystem) { this.schemaSpec = schemaSpec; this.sql = sql; this.dialect = dialect; @@ -6010,16 +6069,17 @@ class RelToSqlConverterTest { this.transforms = ImmutableList.copyOf(transforms); this.parserConfig = parserConfig; this.config = config; + this.typeSystem = Objects.requireNonNull(typeSystem, "typeSystem"); } Sql dialect(SqlDialect dialect) { return new Sql(schemaSpec, sql, dialect, parserConfig, librarySet, config, - relFn, transforms); + relFn, transforms, typeSystem); } Sql relFn(Function<RelBuilder, RelNode> relFn) { return new Sql(schemaSpec, sql, dialect, parserConfig, librarySet, config, - relFn, transforms); + relFn, transforms, typeSystem); } Sql withCalcite() { @@ -6146,12 +6206,17 @@ class RelToSqlConverterTest { Sql parserConfig(SqlParser.Config parserConfig) { return new Sql(schemaSpec, sql, dialect, parserConfig, librarySet, config, - relFn, transforms); + relFn, transforms, typeSystem); } Sql withConfig(UnaryOperator<SqlToRelConverter.Config> config) { return new Sql(schemaSpec, sql, dialect, parserConfig, librarySet, config, - relFn, transforms); + relFn, transforms, typeSystem); + } + + Sql withTypeSystem(RelDataTypeSystem typeSystem) { + return new Sql(schemaSpec, sql, dialect, parserConfig, librarySet, config, + relFn, transforms, typeSystem); } final Sql withLibrary(SqlLibrary library) { @@ -6160,7 +6225,7 @@ class RelToSqlConverterTest { Sql withLibrarySet(Iterable<? extends SqlLibrary> librarySet) { return new Sql(schemaSpec, sql, dialect, parserConfig, - ImmutableSet.copyOf(librarySet), config, relFn, transforms); + ImmutableSet.copyOf(librarySet), config, relFn, transforms, typeSystem); } Sql optimize(final RuleSet ruleSet, @@ -6177,7 +6242,7 @@ class RelToSqlConverterTest { ImmutableList.of(), ImmutableList.of()); }); return new Sql(schemaSpec, sql, dialect, parserConfig, librarySet, config, - relFn, transforms); + relFn, transforms, typeSystem); } Sql ok(String expectedQuery) { @@ -6212,7 +6277,7 @@ class RelToSqlConverterTest { final SqlToRelConverter.Config config = this.config.apply(SqlToRelConverter.config() .withTrimUnusedFields(false)); final Planner planner = - getPlanner(null, parserConfig, defaultSchema, config, librarySet); + getPlanner(null, parserConfig, defaultSchema, config, librarySet, typeSystem); SqlNode parse = planner.parse(sql); SqlNode validate = planner.validate(parse); rel = planner.rel(validate).rel; @@ -6228,7 +6293,7 @@ class RelToSqlConverterTest { public Sql schema(CalciteAssert.SchemaSpec schemaSpec) { return new Sql(schemaSpec, sql, dialect, parserConfig, librarySet, config, - relFn, transforms); + relFn, transforms, typeSystem); } } }