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 dad1294075b..eb1f11b74d0 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 @@ -423,7 +423,9 @@ private static RelDataType sqlType(RelDataTypeFactory typeFactory, int dataType, int precision, int scale, @Nullable String typeString) { // Fall back to ANY if type is unknown final SqlTypeName sqlTypeName = - Util.first(SqlTypeName.getNameForJdbcType(dataType), SqlTypeName.ANY); + Util.first(typeString != null && typeString.toUpperCase(Locale.ROOT).contains("UNSIGNED") + ? SqlTypeName.getNameForUnsignedJdbcType(dataType) + : SqlTypeName.getNameForJdbcType(dataType), SqlTypeName.ANY); switch (sqlTypeName) { case ARRAY: RelDataType component = null; diff --git a/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java b/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java index 1f036986f4d..6bb21e1ae2a 100644 --- a/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java +++ b/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java @@ -793,7 +793,7 @@ private static ColumnMetaData metaData(JavaTypeFactory typeFactory, int ordinal, type.isNullable() ? DatabaseMetaData.columnNullable : DatabaseMetaData.columnNoNulls, - true, + !SqlTypeName.UNSIGNED_TYPES.contains(type.getSqlTypeName()), type.getPrecision(), fieldName, origin(origins, 0), diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java index 79eeaa95bd6..73557737ca5 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java +++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java @@ -289,6 +289,18 @@ public enum SqlTypeName { .put(Types.ARRAY, ARRAY) .build(); + /** + * Mapping between JDBC type codes and their corresponding + * {@link org.apache.calcite.sql.type.SqlTypeName} for unsigned types. + */ + private static final Map UNSIGNED_JDBC_TYPE_TO_NAME = + ImmutableMap.builder() + .put(Types.TINYINT, UTINYINT) + .put(Types.SMALLINT, USMALLINT) + .put(Types.INTEGER, UINTEGER) + .put(Types.BIGINT, UBIGINT) + .build(); + /** * Bitwise-or of flags indicating allowable precision/scale combinations. */ @@ -447,6 +459,16 @@ public int getDefaultScale() { return JDBC_TYPE_TO_NAME.get(jdbcType); } + /** + * Gets the SqlTypeName corresponding to an unsigned JDBC type. + * + * @param jdbcType the unsigned JDBC type of interest + * @return corresponding SqlTypeName, or null if the type is not known + */ + public static @Nullable SqlTypeName getNameForUnsignedJdbcType(int jdbcType) { + return UNSIGNED_JDBC_TYPE_TO_NAME.get(jdbcType); + } + /** * Returns the limit of this datatype. For example, * diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlTypeNameTest.java b/core/src/test/java/org/apache/calcite/sql/test/SqlTypeNameTest.java index 4281179b0d4..457c6272786 100644 --- a/core/src/test/java/org/apache/calcite/sql/test/SqlTypeNameTest.java +++ b/core/src/test/java/org/apache/calcite/sql/test/SqlTypeNameTest.java @@ -40,6 +40,10 @@ import static org.apache.calcite.sql.type.SqlTypeName.TIME; import static org.apache.calcite.sql.type.SqlTypeName.TIMESTAMP; import static org.apache.calcite.sql.type.SqlTypeName.TINYINT; +import static org.apache.calcite.sql.type.SqlTypeName.UBIGINT; +import static org.apache.calcite.sql.type.SqlTypeName.UINTEGER; +import static org.apache.calcite.sql.type.SqlTypeName.USMALLINT; +import static org.apache.calcite.sql.type.SqlTypeName.UTINYINT; import static org.apache.calcite.sql.type.SqlTypeName.VARBINARY; import static org.apache.calcite.sql.type.SqlTypeName.VARCHAR; @@ -81,6 +85,30 @@ class SqlTypeNameTest { assertThat("BIGINT did not map to BIGINT", tn, is(BIGINT)); } + @Test void testUnsignedTinyint() { + SqlTypeName tn = + SqlTypeName.getNameForUnsignedJdbcType(Types.TINYINT); + assertThat("TINYINT did not map to UTINYINT", tn, is(UTINYINT)); + } + + @Test void testUnsignedSmallint() { + SqlTypeName tn = + SqlTypeName.getNameForUnsignedJdbcType(Types.SMALLINT); + assertThat("SMALLINT did not map to USMALLINT", tn, is(USMALLINT)); + } + + @Test void testUnsignedInteger() { + SqlTypeName tn = + SqlTypeName.getNameForUnsignedJdbcType(Types.INTEGER); + assertThat("INTEGER did not map to UINTEGER", tn, is(UINTEGER)); + } + + @Test void testUnsignedBigint() { + SqlTypeName tn = + SqlTypeName.getNameForUnsignedJdbcType(Types.BIGINT); + assertThat("BIGINT did not map to UBIGINT", tn, is(UBIGINT)); + } + @Test void testFloat() { SqlTypeName tn = SqlTypeName.getNameForJdbcType(Types.FLOAT); diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java index 64fb5b0caf3..de66b636f59 100644 --- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java +++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java @@ -9077,6 +9077,39 @@ void checkCalciteSchemaGetSubSchemaMap(boolean cache) { }); } + /** Test case for + * [CALCITE-5094] + * Calcite JDBC Adapter and Avatica should support + * MySQL UNSIGNED types of TINYINT, SMALLINT, INT, BIGINT. */ + @Test void testMySQLUnsignedType() { + CalciteAssert.that() + .with(CalciteAssert.SchemaSpec.UNSIGNED_TYPE) + .with(Lex.MYSQL) + .query("SELECT * FROM test_unsigned WHERE utiny_value = ? " + + "AND usmall_value = ? AND uint_value = ? AND ubig_value = ?") + .consumesPreparedStatement(p -> { + p.setInt(1, 255); + p.setInt(2, 65535); + p.setLong(3, 4294967295L); + p.setBigDecimal(4, new BigDecimal("18446744073709551615")); + }) + .returns(resultSet -> { + try { + assertTrue(resultSet.next()); + final Integer uTinyInt = resultSet.getInt(1); + final Integer uSmallInt = resultSet.getInt(2); + final Long uInteger = resultSet.getLong(3); + final BigDecimal uBigInt = resultSet.getBigDecimal(4); + assertThat(uTinyInt, is(255)); + assertThat(uSmallInt, is(65535)); + assertThat(uInteger, is(4294967295L)); + assertThat(uBigInt, is(new BigDecimal("18446744073709551615"))); + } catch (SQLException e) { + throw TestUtil.rethrow(e); + } + }); + } + @Test void bindByteParameter() { for (SqlTypeName tpe : SqlTypeName.INT_TYPES) { final String sql = diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnsignedType.java b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnsignedType.java index b1cbbcc2e6f..32622315f77 100644 --- a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnsignedType.java +++ b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnsignedType.java @@ -278,7 +278,7 @@ public static ULong toULong(double n) { private static ULong toULongNonNull(Number n, RoundingMode mode) { if (n instanceof BigDecimal) { BigDecimal d = ((BigDecimal) n).setScale(0, mode); - return Unsigned.ulong(d.longValueExact()); + return Unsigned.ulong(d.toBigIntegerExact()); } else if (n instanceof Byte || n instanceof Short || n instanceof Integer || n instanceof Long || n instanceof Float || n instanceof Double || n instanceof UByte || n instanceof UShort || n instanceof UInteger) { diff --git a/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java b/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java index a01501ce23a..8a846da87ea 100644 --- a/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java +++ b/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java @@ -30,6 +30,8 @@ import org.apache.calcite.jdbc.CalciteMetaImpl; import org.apache.calcite.jdbc.CalcitePrepare; import org.apache.calcite.jdbc.CalciteSchema; +import org.apache.calcite.linq4j.Enumerable; +import org.apache.calcite.linq4j.Linq4j; import org.apache.calcite.linq4j.tree.Expression; import org.apache.calcite.materialize.Lattice; import org.apache.calcite.model.ModelHandler; @@ -47,6 +49,7 @@ import org.apache.calcite.runtime.Hook; import org.apache.calcite.runtime.SpatialTypeFunctions; import org.apache.calcite.runtime.UnionOperation; +import org.apache.calcite.schema.ScannableTable; import org.apache.calcite.schema.Schema; import org.apache.calcite.schema.SchemaPlus; import org.apache.calcite.schema.SchemaVersion; @@ -90,6 +93,7 @@ import org.apache.calcite.util.Util; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; @@ -105,6 +109,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.math.BigDecimal; import java.net.URL; import java.sql.Connection; import java.sql.PreparedStatement; @@ -865,7 +870,8 @@ static SchemaPlus addSchema_(SchemaPlus rootSchema, SchemaSpec schema) { case MY_DB: return rootSchema.add(schema.schemaName, MY_DB_SCHEMA); - + case UNSIGNED_TYPE: + return rootSchema.add(schema.schemaName, UNSIGNED_TYPE_SCHEMA); case SCOTT: jdbcScott = addSchemaIfNotExists(rootSchema, SchemaSpec.JDBC_SCOTT); return rootSchema.add(schema.schemaName, new CloneSchema(jdbcScott)); @@ -2077,6 +2083,7 @@ public enum SchemaSpec { GEO("GEO"), HR("hr"), MY_DB("myDb"), + UNSIGNED_TYPE("UNSIGNED_TYPE"), JDBC_SCOTT("JDBC_SCOTT"), SCOTT("scott"), SCOTT_WITH_TEMPORAL("scott_temporal"), @@ -2355,4 +2362,44 @@ static List unwrap(String java) { throw new UnsupportedOperationException("snapshot"); } }; + + /** Schema instance for {@link SchemaSpec#UNSIGNED_TYPE}. */ + private static final Schema UNSIGNED_TYPE_SCHEMA = new AbstractSchema() { + + @Override protected Map getTableMap() { + return ImmutableMap.of("test_unsigned", new UnsingedScannableTable()); + } + }; + + /** Scannable table for unsigned types test. */ + private static class UnsingedScannableTable extends AbstractTable implements ScannableTable { + + /** + * {@inheritDoc} + * + *

Table schema is as follows: + * + *

{@code
+     * test_unsigned(
+     *      utiny_value: TINYINT UNSIGNED,
+     *      usmall_value: SMALLINT UNSIGNED,
+     *      uint_value: INT UNSIGNED,
+     *      ubig_value: BIGINT UNSIGNED)
+     * }
+ */ + @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) { + return typeFactory.builder() + .add("utiny_value", typeFactory.createSqlType(SqlTypeName.UTINYINT)) + .add("usmall_value", typeFactory.createSqlType(SqlTypeName.USMALLINT)) + .add("uint_value", typeFactory.createSqlType(SqlTypeName.UINTEGER)) + .add("ubig_value", typeFactory.createSqlType(SqlTypeName.UBIGINT)) + .build(); + } + + @Override public Enumerable<@Nullable Object[]> scan(DataContext root) { + return Linq4j.asEnumerable(new Object[][] { + {255, 65535, 4294967295L, new BigDecimal("18446744073709551615")} + }); + } + } }