This is an automated email from the ASF dual-hosted git repository. mbudiu pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/calcite.git
commit bd3d854cadf37c5f0c0cbd39ce1f583709986765 Author: Mihai Budiu <[email protected]> AuthorDate: Mon Sep 2 14:22:27 2024 -0700 [CALCITE-4918] Add a VARIANT data type - parser and validator Signed-off-by: Mihai Budiu <[email protected]> --- .../org/apache/calcite/test/BabelParserTest.java | 7 +++++ core/src/main/codegen/templates/Parser.jj | 3 +++ .../apache/calcite/sql/fun/SqlCastFunction.java | 6 +++++ .../org/apache/calcite/sql/fun/SqlDotOperator.java | 6 +++++ .../apache/calcite/sql/fun/SqlItemOperator.java | 8 +++++- .../calcite/sql/fun/SqlStdOperatorTable.java | 2 +- .../sql/type/JavaToSqlTypeConversionRules.java | 1 + .../org/apache/calcite/sql/type/OperandTypes.java | 6 +++++ .../calcite/sql/type/SqlTypeCoercionRule.java | 29 +++++++++++++++++--- .../org/apache/calcite/sql/type/SqlTypeFamily.java | 6 +++++ .../org/apache/calcite/sql/type/SqlTypeName.java | 10 ++++--- .../org/apache/calcite/test/SqlValidatorTest.java | 31 ++++++++++++++++++++++ site/_docs/reference.md | 1 + 13 files changed, 107 insertions(+), 9 deletions(-) diff --git a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java index 1b3c218ac6..fa645fe784 100644 --- a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java +++ b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java @@ -330,6 +330,13 @@ class BabelParserTest extends SqlParserTest { sql(sql).ok(expected); } + @Test void testCreateVariantTable() { + final String sql = "create table foo (bar variant not null)"; + final String expected = "CREATE TABLE `FOO` " + + "(`BAR` VARIANT NOT NULL)"; + sql(sql).ok(expected); + } + @Test void testArrayLiteralFromString() { sql("select array '{1,2,3}'") .ok("SELECT (ARRAY[1, 2, 3])"); diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index 09e33643f9..f826c4241e 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -5896,6 +5896,8 @@ SqlTypeNameSpec SqlTypeName1(Span s) : [ <PRECISION> ] { sqlTypeName = SqlTypeName.DOUBLE; } | <FLOAT> { s.add(this); sqlTypeName = SqlTypeName.FLOAT; } + | + <VARIANT> { s.add(this); sqlTypeName = SqlTypeName.VARIANT; } ) { return new SqlBasicTypeNameSpec(sqlTypeName, s.end(this)); @@ -8701,6 +8703,7 @@ SqlPostfixOperator PostfixRowOperator() : | < VAR_SAMP: "VAR_SAMP" > | < VARBINARY: "VARBINARY" > | < VARCHAR: "VARCHAR" > +| < VARIANT: "VARIANT" > | < VARYING: "VARYING" > | < VERSION: "VERSION" > | < VERSIONING: "VERSIONING" > diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java index 7292d0eb44..8e3388236f 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java @@ -190,6 +190,12 @@ public class SqlCastFunction extends SqlFunction { SqlTypeUtil.createMapType(typeFactory, keyType, valueType, isNullable); } + if (expressionType.getSqlTypeName() == SqlTypeName.VARIANT) { + // A variant can be cast to any other type, but the result + // is always nullable, like in the case of a safe cast. + return typeFactory.createTypeWithNullability(targetType, true); + } + return typeFactory.createTypeWithNullability(targetType, isNullable); } diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java index 3d3896f76e..7c9a231579 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java @@ -101,6 +101,12 @@ public class SqlDotOperator extends SqlSpecialOperator { final SqlNode operand = call.getOperandList().get(0); final RelDataType nodeType = requireNonNull(validator.deriveType(scope, operand)); + assert nodeType != null; + if (nodeType.getSqlTypeName() == SqlTypeName.VARIANT) { + // Result is always a nullable VARIANT + return validator.getTypeFactory().createTypeWithNullability(nodeType, true); + } + if (!nodeType.isStruct()) { throw SqlUtil.newContextException(operand.getParserPosition(), Static.RESOURCE.incompatibleTypes()); diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java index 4f5371c9fc..fc832a64cc 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java @@ -125,6 +125,7 @@ public class SqlItemOperator extends SqlSpecialOperator { case ROW: case ANY: case DYNAMIC_STAR: + case VARIANT: return OperandTypes.family(SqlTypeFamily.INTEGER) .or(OperandTypes.family(SqlTypeFamily.CHARACTER)); default: @@ -136,7 +137,8 @@ public class SqlItemOperator extends SqlSpecialOperator { if (name.equals("ITEM")) { return "<ARRAY>[<INTEGER>]\n" + "<MAP>[<ANY>]\n" - + "<ROW>[<CHARACTER>|<INTEGER>]"; + + "<ROW>[<CHARACTER>|<INTEGER>]" + + "<VARIANT>[<CHARACTER>|<INTEGER>]"; } else { return "<ARRAY>[" + name + "(<INTEGER>)]"; } @@ -146,6 +148,10 @@ public class SqlItemOperator extends SqlSpecialOperator { final RelDataTypeFactory typeFactory = opBinding.getTypeFactory(); final RelDataType operandType = opBinding.getOperandType(0); switch (operandType.getSqlTypeName()) { + case VARIANT: + // Return type is always nullable VARIANT + return typeFactory.createTypeWithNullability( + operandType, true); case ARRAY: return typeFactory.createTypeWithNullability( getComponentTypeOrThrow(operandType), true); diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java index a2e3d66504..6aba1cdaa4 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java @@ -2226,7 +2226,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable { * <p>MAP is not standard SQL. */ public static final SqlOperator ITEM = - new SqlItemOperator("ITEM", OperandTypes.ARRAY_OR_MAP, 1, true); + new SqlItemOperator("ITEM", OperandTypes.ARRAY_OR_MAP_OR_VARIANT, 1, true); /** * The ARRAY Value Constructor. e.g. "<code>ARRAY[1, 2, 3]</code>". diff --git a/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java b/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java index f1941b29fe..dc910c0dd1 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java +++ b/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java @@ -81,6 +81,7 @@ public class JavaToSqlTypeConversionRules { .put(List.class, SqlTypeName.ARRAY) .put(Map.class, SqlTypeName.MAP) .put(Void.class, SqlTypeName.NULL) + .put(Object.class, SqlTypeName.VARIANT) .build(); //~ Methods ---------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java index 39bdad21ca..5a82308e91 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java +++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java @@ -545,6 +545,12 @@ public abstract class OperandTypes { .or(OperandTypes.family(SqlTypeFamily.MAP)) .or(OperandTypes.family(SqlTypeFamily.ANY)); + public static final SqlSingleOperandTypeChecker ARRAY_OR_MAP_OR_VARIANT = + OperandTypes.family(SqlTypeFamily.ARRAY) + .or(OperandTypes.family(SqlTypeFamily.MAP)) + .or(OperandTypes.family(SqlTypeFamily.VARIANT)) + .or(OperandTypes.family(SqlTypeFamily.ANY)); + public static final SqlOperandTypeChecker STRING_ARRAY_CHARACTER_OPTIONAL_CHARACTER = new FamilyOperandTypeChecker( ImmutableList.of(SqlTypeFamily.ARRAY, SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER), diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java index ab03b24ce7..e462a11fc7 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java +++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java @@ -138,6 +138,7 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { coerceRules.add(exactType, coerceRules.copyValues(exactType) .addAll(SqlTypeName.INTERVAL_TYPES) + .add(SqlTypeName.VARIANT) .build()); } @@ -152,27 +153,31 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { .add(SqlTypeName.DECIMAL) .add(SqlTypeName.CHAR) .add(SqlTypeName.VARCHAR) + .add(SqlTypeName.VARIANT) .build()); } - // BINARY is castable from VARBINARY, CHARACTERS. + // BINARY is castable from VARBINARY, CHARACTERS, VARIANT. coerceRules.add(SqlTypeName.BINARY, coerceRules.copyValues(SqlTypeName.BINARY) .add(SqlTypeName.VARBINARY) + .add(SqlTypeName.VARIANT) .addAll(SqlTypeName.CHAR_TYPES) .build()); - // VARBINARY is castable from BINARY, CHARACTERS. + // VARBINARY is castable from BINARY, CHARACTERS, VARIANT. coerceRules.add(SqlTypeName.VARBINARY, coerceRules.copyValues(SqlTypeName.VARBINARY) + .add(SqlTypeName.VARIANT) .add(SqlTypeName.BINARY) .addAll(SqlTypeName.CHAR_TYPES) .build()); // VARCHAR is castable from BOOLEAN, DATE, TIME, TIMESTAMP, numeric types, binary and - // intervals + // intervals, VARIANT coerceRules.add(SqlTypeName.VARCHAR, coerceRules.copyValues(SqlTypeName.VARCHAR) + .add(SqlTypeName.VARIANT) .add(SqlTypeName.CHAR) .add(SqlTypeName.BOOLEAN) .add(SqlTypeName.DATE) @@ -187,10 +192,17 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { .addAll(SqlTypeName.INTERVAL_TYPES) .build()); + // VARIANT is castable from anything + coerceRules.add(SqlTypeName.VARIANT, + coerceRules.copyValues(SqlTypeName.CHAR) + .addAll(SqlTypeName.ALL_TYPES) + .build()); + // CHAR is castable from BOOLEAN, DATE, TIME, TIMESTAMP, numeric types, binary and - // intervals + // intervals, VARIANT coerceRules.add(SqlTypeName.CHAR, coerceRules.copyValues(SqlTypeName.CHAR) + .add(SqlTypeName.VARIANT) .add(SqlTypeName.VARCHAR) .add(SqlTypeName.BOOLEAN) .add(SqlTypeName.DATE) @@ -208,6 +220,7 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { // BOOLEAN is castable from ... coerceRules.add(SqlTypeName.BOOLEAN, coerceRules.copyValues(SqlTypeName.BOOLEAN) + .add(SqlTypeName.VARIANT) .add(SqlTypeName.CHAR) .add(SqlTypeName.VARCHAR) .addAll(SqlTypeName.NUMERIC_TYPES) @@ -219,6 +232,7 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { // DATE is castable from... coerceRules.add(SqlTypeName.DATE, coerceRules.copyValues(SqlTypeName.DATE) + .add(SqlTypeName.VARIANT) .add(SqlTypeName.TIMESTAMP) .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE) .add(SqlTypeName.TIMESTAMP_TZ) @@ -230,6 +244,7 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { // TIME is castable from... coerceRules.add(SqlTypeName.TIME, coerceRules.copyValues(SqlTypeName.TIME) + .add(SqlTypeName.VARIANT) .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE) .add(SqlTypeName.TIME_TZ) .add(SqlTypeName.TIMESTAMP) @@ -243,6 +258,7 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { // TIME WITH LOCAL TIME ZONE is castable from... coerceRules.add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE, coerceRules.copyValues(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE) + .add(SqlTypeName.VARIANT) .add(SqlTypeName.TIME_TZ) .add(SqlTypeName.TIME) .add(SqlTypeName.TIMESTAMP) @@ -257,6 +273,7 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { coerceRules.add(SqlTypeName.TIME_TZ, coerceRules.copyValues(SqlTypeName.TIME_TZ) .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE) + .add(SqlTypeName.VARIANT) .add(SqlTypeName.TIME) .add(SqlTypeName.TIMESTAMP) .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE) @@ -269,6 +286,7 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { // TIMESTAMP is castable from... coerceRules.add(SqlTypeName.TIMESTAMP, coerceRules.copyValues(SqlTypeName.TIMESTAMP) + .add(SqlTypeName.VARIANT) .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE) .add(SqlTypeName.TIMESTAMP_TZ) .add(SqlTypeName.DATE) @@ -284,6 +302,7 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { // TIMESTAMP WITH LOCAL TIME ZONE is castable from... coerceRules.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, coerceRules.copyValues(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE) + .add(SqlTypeName.VARIANT) .add(SqlTypeName.TIMESTAMP_TZ) .add(SqlTypeName.TIMESTAMP) .add(SqlTypeName.DATE) @@ -299,6 +318,7 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { // TIMESTAMP WITH TIME ZONE is castable from... coerceRules.add(SqlTypeName.TIMESTAMP_TZ, coerceRules.copyValues(SqlTypeName.TIMESTAMP_TZ) + .add(SqlTypeName.VARIANT) .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE) .add(SqlTypeName.TIMESTAMP) .add(SqlTypeName.DATE) @@ -314,6 +334,7 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { // GEOMETRY is castable from ... coerceRules.add(SqlTypeName.GEOMETRY, coerceRules.copyValues(SqlTypeName.GEOMETRY) + .add(SqlTypeName.VARIANT) .addAll(SqlTypeName.CHAR_TYPES) .build()); diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java index 050902238c..63832873a9 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java +++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java @@ -78,6 +78,7 @@ public enum SqlTypeFamily implements RelDataTypeFamily { COLUMN_LIST, GEO, FUNCTION, + VARIANT, /** Like ANY, but do not even validate the operand. It may not be an * expression. */ IGNORE; @@ -119,6 +120,7 @@ public enum SqlTypeFamily implements RelDataTypeFamily { .put(ExtraSqlTypes.REF_CURSOR, CURSOR) .put(Types.ARRAY, ARRAY) + .put(Types.JAVA_OBJECT, VARIANT) .build(); /** @@ -222,6 +224,8 @@ public enum SqlTypeFamily implements RelDataTypeFamily { return ImmutableList.of(SqlTypeName.COLUMN_LIST); case FUNCTION: return ImmutableList.of(SqlTypeName.FUNCTION); + case VARIANT: + return ImmutableList.of(SqlTypeName.VARIANT); default: throw new IllegalArgumentException(); } @@ -281,6 +285,8 @@ public enum SqlTypeFamily implements RelDataTypeFamily { return factory.createFunctionSqlType( factory.createStructType(ImmutableList.of(), ImmutableList.of()), factory.createSqlType(SqlTypeName.ANY)); + case VARIANT: + return factory.createSqlType(SqlTypeName.VARIANT); default: return null; } 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 e5a30dd581..ed986bb4ca 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 @@ -132,7 +132,10 @@ public enum SqlTypeName { GEOMETRY(PrecScale.NO_NO, false, ExtraSqlTypes.GEOMETRY, SqlTypeFamily.GEO), MEASURE(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.ANY), FUNCTION(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.FUNCTION), - SARG(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.ANY); + SARG(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.ANY), + /** VARIANT data type, a dynamically-typed value that can have at runtime + * any of the other data types in this table. */ + VARIANT(PrecScale.NO_NO, false, Types.OTHER, SqlTypeFamily.VARIANT); public static final int MAX_DATETIME_PRECISION = 3; @@ -165,7 +168,7 @@ public enum SqlTypeName { INTERVAL_HOUR_SECOND, INTERVAL_MINUTE, INTERVAL_MINUTE_SECOND, INTERVAL_SECOND, TIME_WITH_LOCAL_TIME_ZONE, TIME_TZ, TIMESTAMP_WITH_LOCAL_TIME_ZONE, TIMESTAMP_TZ, - FLOAT, MULTISET, DISTINCT, STRUCTURED, ROW, CURSOR, COLUMN_LIST); + FLOAT, MULTISET, DISTINCT, STRUCTURED, ROW, CURSOR, COLUMN_LIST, VARIANT); public static final List<SqlTypeName> BOOLEAN_TYPES = ImmutableList.of(BOOLEAN); @@ -274,6 +277,7 @@ public enum SqlTypeName { .put(Types.DISTINCT, DISTINCT) .put(Types.STRUCT, STRUCTURED) .put(Types.ARRAY, ARRAY) + .put(Types.JAVA_OBJECT, VARIANT) .build(); /** @@ -282,7 +286,7 @@ public enum SqlTypeName { private final int signatures; /** - * Returns true if not of a "pure" standard sql type. "Inpure" types are + * Returns true if not of a "pure" standard sql type. "Inpure" types include * {@link #ANY}, {@link #NULL} and {@link #SYMBOL} */ private final boolean special; diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java index 2c3ca5436f..9434f8b23d 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -1347,6 +1347,37 @@ public class SqlValidatorTest extends SqlValidatorTestCase { .columnType("TIMESTAMP_TZ(3) NOT NULL"); } + @Test void testCastVariant() { + expr("cast(NULL as variant)") + .columnType("VARIANT"); + expr("cast(1 as variant)") + .columnType("VARIANT NOT NULL"); + expr("cast('abc' as variant)") + .columnType("VARIANT NOT NULL"); + expr("cast(TIMESTAMP '2024-09-01 00:00:00' as variant)") + .columnType("VARIANT NOT NULL"); + + expr("cast(cast(NULL as variant) as int)") + .columnType("INTEGER"); + expr("cast(cast(1 as variant) as int)") + .columnType("INTEGER"); + expr("cast(cast(1 as variant) as varchar)") + .columnType("VARCHAR"); + expr("cast(cast('abc' as variant) as varchar)") + .columnType("VARCHAR"); + expr("cast(cast(TIMESTAMP '2024-09-01 00:00:00' as variant) as timestamp)") + .columnType("TIMESTAMP(0)"); + } + + @Test void testAccessVariant() { + expr("cast(1 as variant).field") + .columnType("VARIANT"); + expr("cast(1 as variant)['field']") + .columnType("VARIANT"); + expr("cast(1 as variant)[0]") + .columnType("VARIANT"); + } + @Test void testCastRegisteredType() { expr("cast(123 as ^customBigInt^)") .fails("Unknown identifier 'CUSTOMBIGINT'"); diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 7a2a895025..2fe835f39f 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -1241,6 +1241,7 @@ Note: | ARRAY | Ordered, contiguous collection that may contain duplicates | Example: varchar(10) array | CURSOR | Cursor over the result of executing a query | | FUNCTION | A function definition that is not bound to an identifier, it is not fully supported in CAST or DDL | Example FUNCTION(INTEGER, VARCHAR(30)) -> INTEGER +| VARIANT | Dynamically-typed value that can have at runtime a value of any other type | VARIANT Note:
