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 2fefd69b01f9658bdede98c7a18d1a5f32f0b412 Author: Mihai Budiu <[email protected]> AuthorDate: Tue Sep 3 23:34:05 2024 -0700 [CALCITE-4918] Add a VARIANT data type - runtime support Signed-off-by: Mihai Budiu <[email protected]> --- .../adapter/enumerable/RexToLixTranslator.java | 20 ++ .../java/org/apache/calcite/rex/RexBuilder.java | 7 +- .../java/org/apache/calcite/rex/RexLiteral.java | 3 + .../org/apache/calcite/runtime/SqlFunctions.java | 4 + .../apache/calcite/sql/fun/SqlCastFunction.java | 19 +- .../apache/calcite/sql/fun/SqlItemOperator.java | 4 +- .../sql/type/JavaToSqlTypeConversionRules.java | 1 - .../calcite/sql/type/SqlTypeCoercionRule.java | 29 +-- .../org/apache/calcite/sql/type/SqlTypeName.java | 1 - .../org/apache/calcite/sql/type/SqlTypeUtil.java | 6 +- .../org/apache/calcite/util/BuiltInMethod.java | 4 +- .../main/java/org/apache/calcite/util/Variant.java | 181 ++++++++++++++++ .../apache/calcite/util/rtti/BasicSqlTypeRtti.java | 113 ++++++++++ .../calcite/util/rtti/GenericSqlTypeRtti.java | 75 +++++++ .../apache/calcite/util/rtti/RowSqlTypeRtti.java | 70 +++++++ .../calcite/util/rtti/RuntimeTypeInformation.java | 228 +++++++++++++++++++++ .../org/apache/calcite/util/rtti/package-info.java | 21 ++ .../calcite/jdbc/CalciteRemoteDriverTest.java | 2 +- .../org/apache/calcite/test/SqlValidatorTest.java | 11 +- site/_docs/reference.md | 31 +++ .../apache/calcite/sql/parser/SqlParserTest.java | 1 + .../org/apache/calcite/test/SqlOperatorTest.java | 66 +++++- 22 files changed, 856 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java index 206da0307b..0cebb750e2 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java @@ -66,6 +66,8 @@ import org.apache.calcite.util.BuiltInMethod; import org.apache.calcite.util.ControlFlowException; import org.apache.calcite.util.Pair; import org.apache.calcite.util.Util; +import org.apache.calcite.util.Variant; +import org.apache.calcite.util.rtti.RuntimeTypeInformation; import com.google.common.base.CaseFormat; import com.google.common.collect.ImmutableList; @@ -313,7 +315,25 @@ public class RexToLixTranslator implements RexVisitor<RexToLixTranslator.Result> final Supplier<Expression> defaultExpression = () -> EnumUtils.convert(operand, typeFactory.getJavaClass(targetType)); + if (sourceType.getSqlTypeName() == SqlTypeName.VARIANT) { + // Converting VARIANT to VARIANT uses the default conversion + if (targetType.getSqlTypeName() == SqlTypeName.VARIANT) { + return defaultExpression.get(); + } + // Converting a VARIANT to any other type calls the Variant.cast method + Expression cast = + Expressions.call(BuiltInMethod.VARIANT_CAST.method, operand, + RuntimeTypeInformation.createExpression(targetType)); + // The cast returns an Object, so we need a convert too + RelDataType nullableTarget = typeFactory.createTypeWithNullability(targetType, true); + return Expressions.convert_(cast, typeFactory.getJavaClass(nullableTarget)); + } + switch (targetType.getSqlTypeName()) { + case VARIANT: + // Converting any type to a VARIANT invokes the Variant constructor + Expression rtti = RuntimeTypeInformation.createExpression(sourceType); + return Expressions.new_(Variant.class, operand, rtti); case ANY: return operand; diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java index b1291af74c..a3a5248ba5 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java +++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java @@ -196,6 +196,10 @@ public class RexBuilder { public RexNode makeFieldAccess(RexNode expr, String fieldName, boolean caseSensitive) { final RelDataType type = expr.getType(); + if (type.getSqlTypeName() == SqlTypeName.VARIANT) { + // VARIANT.field is rewritten as an VARIANT[field] + return this.makeCall(SqlStdOperatorTable.ITEM, expr, this.makeLiteral(fieldName)); + } final RelDataTypeField field = type.getField(fieldName, caseSensitive, false); if (field == null) { @@ -855,7 +859,8 @@ public class RexBuilder { return true; } final SqlTypeName sqlType = toType.getSqlTypeName(); - if (sqlType == SqlTypeName.MEASURE) { + if (sqlType == SqlTypeName.MEASURE + || sqlType == SqlTypeName.VARIANT) { return false; } if (!RexLiteral.valueMatchesType(value, sqlType, false)) { diff --git a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java index 1d95b0b3ed..69230a1ec9 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java +++ b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java @@ -43,6 +43,7 @@ import org.apache.calcite.util.TimeWithTimeZoneString; import org.apache.calcite.util.TimestampString; import org.apache.calcite.util.TimestampWithTimeZoneString; import org.apache.calcite.util.Util; +import org.apache.calcite.util.Variant; import com.google.common.collect.ImmutableList; @@ -315,6 +316,8 @@ public class RexLiteral extends RexNode { return true; } switch (typeName) { + case VARIANT: + return value instanceof Variant; case BOOLEAN: // Unlike SqlLiteral, we do not allow boolean null. return value instanceof Boolean; diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java index 9946d6ab84..a5959c3a3a 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -47,6 +47,7 @@ import org.apache.calcite.util.TimestampWithTimeZoneString; import org.apache.calcite.util.TryThreadLocal; import org.apache.calcite.util.Unsafe; import org.apache.calcite.util.Util; +import org.apache.calcite.util.Variant; import org.apache.calcite.util.format.FormatElement; import org.apache.calcite.util.format.FormatModel; import org.apache.calcite.util.format.FormatModels; @@ -5766,6 +5767,9 @@ public class SqlFunctions { * known until runtime. */ public static @Nullable Object item(Object object, Object index) { + if (object instanceof Variant) { + return ((Variant) object).item(index); + } if (object instanceof Map) { return mapItem((Map) object, index); } 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 8e3388236f..93bcf284a9 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 @@ -142,6 +142,19 @@ public class SqlCastFunction extends SqlFunction { RelDataType expressionType, RelDataType targetType, boolean safe) { boolean isNullable = expressionType.isNullable() || safe; + if (targetType.getSqlTypeName() == SqlTypeName.VARIANT) { + // A variant can be cast from any other type, and it inherits + // the nullability of the source. + // Note that the order of this test and the next one is important. + return typeFactory.createTypeWithNullability(targetType, expressionType.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); + } + if (isCollection(expressionType)) { RelDataType expressionElementType = expressionType.getComponentType(); RelDataType targetElementType = targetType.getComponentType(); @@ -190,12 +203,6 @@ 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/SqlItemOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java index fc832a64cc..dffaa91389 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 @@ -137,8 +137,8 @@ public class SqlItemOperator extends SqlSpecialOperator { if (name.equals("ITEM")) { return "<ARRAY>[<INTEGER>]\n" + "<MAP>[<ANY>]\n" - + "<ROW>[<CHARACTER>|<INTEGER>]" - + "<VARIANT>[<CHARACTER>|<INTEGER>]"; + + "<ROW>[<CHARACTER>|<INTEGER>]\n" + + "<VARIANT>[<ANY>]"; } else { return "<ARRAY>[" + name + "(<INTEGER>)]"; } 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 dc910c0dd1..f1941b29fe 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,7 +81,6 @@ 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/SqlTypeCoercionRule.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java index e462a11fc7..ab03b24ce7 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,7 +138,6 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { coerceRules.add(exactType, coerceRules.copyValues(exactType) .addAll(SqlTypeName.INTERVAL_TYPES) - .add(SqlTypeName.VARIANT) .build()); } @@ -153,31 +152,27 @@ public class SqlTypeCoercionRule implements SqlTypeMappingRule { .add(SqlTypeName.DECIMAL) .add(SqlTypeName.CHAR) .add(SqlTypeName.VARCHAR) - .add(SqlTypeName.VARIANT) .build()); } - // BINARY is castable from VARBINARY, CHARACTERS, VARIANT. + // BINARY is castable from VARBINARY, CHARACTERS. 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, VARIANT. + // VARBINARY is castable from BINARY, CHARACTERS. 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, VARIANT + // intervals coerceRules.add(SqlTypeName.VARCHAR, coerceRules.copyValues(SqlTypeName.VARCHAR) - .add(SqlTypeName.VARIANT) .add(SqlTypeName.CHAR) .add(SqlTypeName.BOOLEAN) .add(SqlTypeName.DATE) @@ -192,17 +187,10 @@ 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, VARIANT + // intervals coerceRules.add(SqlTypeName.CHAR, coerceRules.copyValues(SqlTypeName.CHAR) - .add(SqlTypeName.VARIANT) .add(SqlTypeName.VARCHAR) .add(SqlTypeName.BOOLEAN) .add(SqlTypeName.DATE) @@ -220,7 +208,6 @@ 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) @@ -232,7 +219,6 @@ 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) @@ -244,7 +230,6 @@ 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) @@ -258,7 +243,6 @@ 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) @@ -273,7 +257,6 @@ 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) @@ -286,7 +269,6 @@ 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) @@ -302,7 +284,6 @@ 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) @@ -318,7 +299,6 @@ 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) @@ -334,7 +314,6 @@ 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/SqlTypeName.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java index ed986bb4ca..522d4639da 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 @@ -277,7 +277,6 @@ public enum SqlTypeName { .put(Types.DISTINCT, DISTINCT) .put(Types.STRUCT, STRUCTURED) .put(Types.ARRAY, ARRAY) - .put(Types.JAVA_OBJECT, VARIANT) .build(); /** 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 126e76e365..245756824e 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 @@ -772,6 +772,10 @@ public abstract class SqlTypeUtil { return t.getFamily() == SqlTypeFamily.ANY; } + private static boolean isVariant(RelDataType t) { + return t.getFamily() == SqlTypeFamily.VARIANT; + } + public static boolean isMeasure(RelDataType t) { return t instanceof MeasureSqlType; } @@ -895,7 +899,7 @@ public abstract class SqlTypeUtil { return canCastFrom(toType, requireNonNull(fromType.getMeasureElementType()), typeMappingRule); } - if (isAny(toType) || isAny(fromType)) { + if (isAny(toType) || isAny(fromType) || isVariant(toType) || isVariant(fromType)) { return true; } diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java index 6236e0e556..ab6e2c3ce0 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -116,6 +116,7 @@ import org.apache.calcite.sql.SqlJsonExistsErrorBehavior; import org.apache.calcite.sql.SqlJsonQueryEmptyOrErrorBehavior; import org.apache.calcite.sql.SqlJsonQueryWrapperBehavior; import org.apache.calcite.sql.SqlJsonValueEmptyOrErrorBehavior; +import org.apache.calcite.util.rtti.RuntimeTypeInformation; import com.google.common.collect.ImmutableMap; @@ -935,7 +936,8 @@ public enum BuiltInMethod { long.class), BIG_DECIMAL_ADD(BigDecimal.class, "add", BigDecimal.class), BIG_DECIMAL_NEGATE(BigDecimal.class, "negate"), - COMPARE_TO(Comparable.class, "compareTo", Object.class); + COMPARE_TO(Comparable.class, "compareTo", Object.class), + VARIANT_CAST(Variant.class, "cast", Object.class, RuntimeTypeInformation.class); @SuppressWarnings("ImmutableEnumChecker") public final Method method; diff --git a/core/src/main/java/org/apache/calcite/util/Variant.java b/core/src/main/java/org/apache/calcite/util/Variant.java new file mode 100644 index 0000000000..05c91cf186 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/Variant.java @@ -0,0 +1,181 @@ +/* + * 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.util; + +import org.apache.calcite.runtime.SqlFunctions; +import org.apache.calcite.sql.type.BasicSqlType; +import org.apache.calcite.util.rtti.BasicSqlTypeRtti; +import org.apache.calcite.util.rtti.RuntimeTypeInformation; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Objects; + +/** This class is the runtime support for values of the VARIANT SQL type. */ +public class Variant { + /** Actual value. */ + final @Nullable Object value; + /** Type of the value. */ + final RuntimeTypeInformation runtimeType; + /** The VARIANT type has its own notion of null, which is + * different from the SQL NULL value. For example, two variant nulls are equal + * with each other. This flag is 'true' if this value represents a variant null. */ + final boolean isVariantNull; + + private Variant(@Nullable Object value, RuntimeTypeInformation runtimeType, + boolean isVariantNull) { + this.value = value; + this.runtimeType = runtimeType; + this.isVariantNull = isVariantNull; + } + + public Variant(@Nullable Object value, RuntimeTypeInformation runtimeType) { + this(value, runtimeType, false); + } + + /** Create a variant object with a null value. */ + public Variant() { + this(null, + new BasicSqlTypeRtti(RuntimeTypeInformation.RuntimeSqlTypeName.VARIANT, + BasicSqlType.PRECISION_NOT_SPECIFIED, BasicSqlType.SCALE_NOT_SPECIFIED), + true); + } + + public String getType() { + return this.runtimeType.getTypeString(); + } + + public boolean isVariantNull() { + return this.isVariantNull; + } + + @Override public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Variant variant = (Variant) o; + return isVariantNull == variant.isVariantNull + && Objects.equals(value, variant.value) + && runtimeType.equals(variant.runtimeType); + } + + @Override public int hashCode() { + int result = Objects.hashCode(value); + result = 31 * result + runtimeType.hashCode(); + result = 31 * result + Boolean.hashCode(isVariantNull); + return result; + } + + /** Cast this value to the specified type. Currently, the rule is: + * if the value has the specified type, the value field is returned, otherwise a SQL + * NULL is returned. */ + @Nullable Object cast(RuntimeTypeInformation type) { + if (this.runtimeType.isScalar()) { + if (this.runtimeType.equals(type)) { + return this.value; + } else { + return null; + } + } else { + if (this.runtimeType.equals(type)) { + return this.value; + } + // TODO: allow casts that change some of the generic arguments only + } + return null; + } + + // This method is invoked from {@link RexToLixTranslator} VARIANT_CAST + public static @Nullable Object cast(@Nullable Object variant, RuntimeTypeInformation type) { + if (variant == null) { + return null; + } + if (!(variant instanceof Variant)) { + throw new RuntimeException("Expected a variant value " + variant); + } + return ((Variant) variant).cast(type); + } + + // Implementation of the array index operator for VARIANT values + public @Nullable Object item(Object index) { + if (this.value == null) { + return null; + } + switch (this.runtimeType.getTypeName()) { + case ROW: + case ARRAY: + case MAP: + return SqlFunctions.item(this.value, index); + default: + return null; + } + } + + // This method is called by the testing code. + @Override public String toString() { + if (value == null) { + return "NULL"; + } + if (this.isVariantNull()) { + return "null"; + } + if (this.runtimeType.getTypeName() == RuntimeTypeInformation.RuntimeSqlTypeName.ROW) { + if (value instanceof Object[]) { + Object[] array = (Object []) value; + StringBuilder buf = new StringBuilder("{"); + + boolean first = true; + for (Object o : array) { + if (!first) { + buf.append(", "); + } + first = false; + buf.append(o.toString()); + } + buf.append("}"); + return buf.toString(); + } + } + String quote = ""; + switch (this.runtimeType.getTypeName()) { + case TIME: + case TIME_WITH_LOCAL_TIME_ZONE: + case TIME_TZ: + case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + case TIMESTAMP_TZ: + case INTERVAL_LONG: + case INTERVAL_SHORT: + case CHAR: + case VARCHAR: + case BINARY: + case VARBINARY: + // At least in Snowflake VARIANT values that are strings + // are printed with double quotes + // https://docs.snowflake.com/en/sql-reference/data-types-semistructured + quote = "\""; + break; + default: + break; + } + return quote + value + quote; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/rtti/BasicSqlTypeRtti.java b/core/src/main/java/org/apache/calcite/util/rtti/BasicSqlTypeRtti.java new file mode 100644 index 0000000000..63f7afe60a --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/rtti/BasicSqlTypeRtti.java @@ -0,0 +1,113 @@ +/* + * 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.util.rtti; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Runtime type information about a base (primitive) SQL type. */ +public class BasicSqlTypeRtti extends RuntimeTypeInformation { + private final int precision; + private final int scale; + + public BasicSqlTypeRtti(RuntimeSqlTypeName typeName, int precision, int scale) { + super(typeName); + assert typeName.isScalar() : "Base SQL type must be a scalar type " + typeName; + this.precision = precision; + this.scale = scale; + } + + @Override public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BasicSqlTypeRtti that = (BasicSqlTypeRtti) o; + return typeName == that.typeName && precision == that.precision && scale == that.scale; + } + + @Override public int hashCode() { + int result = precision; + result = 31 * result + scale; + return result; + } + + @Override public String getTypeString() { + switch (this.typeName) { + case BOOLEAN: + return "BOOLEAN"; + case TINYINT: + return "TINYINT"; + case SMALLINT: + return "SMALLINT"; + case INTEGER: + return "INTEGER"; + case BIGINT: + return "BIGINT"; + case DECIMAL: + return "DECIMAL"; + case FLOAT: + return "FLOAT"; + case REAL: + return "REAL"; + case DOUBLE: + return "DOUBLE"; + case DATE: + return "DATE"; + case TIME: + return "TIME"; + case TIME_WITH_LOCAL_TIME_ZONE: + return "TIME_WITH_LOCAL_TIME_ZONE"; + case TIME_TZ: + return "TIME_WITH_TIME_ZONE"; + case TIMESTAMP: + return "TIMESTAMP"; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + return "TIMESTAMP_WITH_LOCAL_TIME_ZONE"; + case TIMESTAMP_TZ: + return "TIMESTAMP_WITH_TIME_ZONE"; + case INTERVAL_LONG: + case INTERVAL_SHORT: + return "INTERVAL"; + case CHAR: + return "CHAR"; + case VARCHAR: + return "VARCHAR"; + case BINARY: + return "BINARY"; + case VARBINARY: + return "VARBINARY"; + case NULL: + return "NULL"; + case GEOMETRY: + return "GEOMETRY"; + case VARIANT: + return "VARIANT"; + default: + throw new RuntimeException("Unexpected type " + this.typeName); + } + } + + // This method is used to serialize the type in Java code implementations, + // so it should produce a computation that reconstructs the type at runtime + @Override public String toString() { + return "new BasicSqlTypeRtti(" + + this.getTypeString() + ", " + this.precision + ", " + this.scale + ")"; + } +} diff --git a/core/src/main/java/org/apache/calcite/util/rtti/GenericSqlTypeRtti.java b/core/src/main/java/org/apache/calcite/util/rtti/GenericSqlTypeRtti.java new file mode 100644 index 0000000000..25f3be33e8 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/rtti/GenericSqlTypeRtti.java @@ -0,0 +1,75 @@ +/* + * 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.util.rtti; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Arrays; + +/** Runtime type information which contains some type parameters. */ +public class GenericSqlTypeRtti extends RuntimeTypeInformation { + final RuntimeTypeInformation[] typeArguments; + + public GenericSqlTypeRtti(RuntimeSqlTypeName typeName, RuntimeTypeInformation... typeArguments) { + super(typeName); + assert !typeName.isScalar() : "Generic type cannot be a scalar type " + typeName; + this.typeArguments = typeArguments; + } + + @Override public String getTypeString() { + switch (this.typeName) { + case ARRAY: + return "ARRAY"; + case MAP: + return "MAP"; + case MULTISET: + return "MULTISET"; + default: + throw new RuntimeException("Unexpected type " + this.typeName); + } + } + + // This method is used to serialize the type in Java code implementations, + // so it should produce a computation that reconstructs the type at runtime + @Override public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("new GenericSqlTypeRtti(") + .append(this.getTypeString()); + for (RuntimeTypeInformation arg : this.typeArguments) { + builder.append(", "); + builder.append(arg.toString()); + } + builder.append(")"); + return builder.toString(); + } + + @Override public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + GenericSqlTypeRtti that = (GenericSqlTypeRtti) o; + return typeName == that.typeName && Arrays.equals(typeArguments, that.typeArguments); + } + + @Override public int hashCode() { + return Arrays.hashCode(typeArguments); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/rtti/RowSqlTypeRtti.java b/core/src/main/java/org/apache/calcite/util/rtti/RowSqlTypeRtti.java new file mode 100644 index 0000000000..d876e7ea76 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/rtti/RowSqlTypeRtti.java @@ -0,0 +1,70 @@ +/* + * 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.util.rtti; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Arrays; +import java.util.Map; + +/** Runtime type information for a ROW type. */ +public class RowSqlTypeRtti extends RuntimeTypeInformation { + private final Map.Entry<String, RuntimeTypeInformation>[] fieldNames; + + @SafeVarargs + public RowSqlTypeRtti(Map.Entry<String, RuntimeTypeInformation>... fieldNames) { + super(RuntimeSqlTypeName.ROW); + this.fieldNames = fieldNames; + } + + @Override public String getTypeString() { + return "ROW"; + } + + // This method is used to serialize the type in Java code implementations, + // so it should produce a computation that reconstructs the type at runtime + @Override public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("new RowSqlTypeRtti("); + boolean first = true; + for (Map.Entry<String, RuntimeTypeInformation> arg : this.fieldNames) { + if (!first) { + builder.append(", "); + } + first = false; + builder.append(arg.toString()); + } + builder.append(")"); + return builder.toString(); + } + + @Override public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + RowSqlTypeRtti that = (RowSqlTypeRtti) o; + return Arrays.equals(fieldNames, that.fieldNames); + } + + @Override public int hashCode() { + return Arrays.hashCode(fieldNames); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/rtti/RuntimeTypeInformation.java b/core/src/main/java/org/apache/calcite/util/rtti/RuntimeTypeInformation.java new file mode 100644 index 0000000000..73527408d7 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/rtti/RuntimeTypeInformation.java @@ -0,0 +1,228 @@ +/* + * 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.util.rtti; + +import org.apache.calcite.linq4j.tree.Expression; +import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeField; + +import java.util.AbstractMap; + +import static java.util.Objects.requireNonNull; + +/** + * This class represents the type of a SQL expression at runtime. + * Normally SQL is a statically-typed language, and there is no need for + * runtime-type information. However, the VARIANT data type is actually + * a dynamically-typed value, and needs this kind of information. + * We cannot use the very similar RelDataType type since it carries extra + * baggage, like the type system, which is not available at runtime. */ +public abstract class RuntimeTypeInformation { + /** Names of SQL types as represented at runtime. */ + public enum RuntimeSqlTypeName { + BOOLEAN(false), + TINYINT(false), + SMALLINT(false), + INTEGER(false), + BIGINT(false), + DECIMAL(false), + FLOAT(false), + REAL(false), + DOUBLE(false), + DATE(false), + TIME(false), + TIME_WITH_LOCAL_TIME_ZONE(false), + TIME_TZ(false), + TIMESTAMP(false), + TIMESTAMP_WITH_LOCAL_TIME_ZONE(false), + TIMESTAMP_TZ(false), + INTERVAL_LONG(false), + INTERVAL_SHORT(false), + CHAR(false), + VARCHAR(false), + BINARY(false), + VARBINARY(false), + NULL(false), + MULTISET(true), + ARRAY(true), + MAP(true), + ROW(true), + GEOMETRY(false), + // used only for VARIANT.null value + VARIANT(false); + + private final boolean composite; + + RuntimeSqlTypeName(boolean composite) { + this.composite = composite; + } + + public boolean isScalar() { + return !this.composite; + } + } + + final RuntimeSqlTypeName typeName; + + protected RuntimeTypeInformation(RuntimeSqlTypeName typeName) { + this.typeName = typeName; + } + + public abstract String getTypeString(); + + public RuntimeSqlTypeName getTypeName() { + return this.typeName; + } + + public boolean isScalar() { + return this.typeName.isScalar(); + } + + /** + * Create and return an expression that creates a runtime type that + * reflects the information in the statically-known type 'type'. + * + * @param type The static type of an expression. + */ + public static Expression createExpression(RelDataType type) { + final Expression precision = Expressions.constant(type.getPrecision()); + final Expression scale = Expressions.constant(type.getScale()); + switch (type.getSqlTypeName()) { + case BOOLEAN: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.BOOLEAN), precision, scale); + case TINYINT: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.TINYINT), precision, scale); + case SMALLINT: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.SMALLINT), precision, scale); + case INTEGER: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.INTEGER), precision, scale); + case BIGINT: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.BIGINT), precision, scale); + case DECIMAL: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.DECIMAL), precision, scale); + case FLOAT: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.FLOAT), precision, scale); + case REAL: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.REAL), precision, scale); + case DOUBLE: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.DOUBLE), precision, scale); + case DATE: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.DATE), precision, scale); + case TIME: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.TIME), precision, scale); + case TIME_WITH_LOCAL_TIME_ZONE: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.TIME_WITH_LOCAL_TIME_ZONE), + precision, scale); + case TIME_TZ: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.TIME_TZ), precision, scale); + case TIMESTAMP: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.TIMESTAMP), precision, scale); + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE), + precision, scale); + case TIMESTAMP_TZ: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.TIMESTAMP_TZ), precision, scale); + case INTERVAL_YEAR: + case INTERVAL_YEAR_MONTH: + case INTERVAL_MONTH: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.INTERVAL_LONG), precision, scale); + case INTERVAL_DAY: + case INTERVAL_DAY_HOUR: + case INTERVAL_DAY_MINUTE: + case INTERVAL_DAY_SECOND: + case INTERVAL_HOUR: + case INTERVAL_HOUR_MINUTE: + case INTERVAL_HOUR_SECOND: + case INTERVAL_MINUTE: + case INTERVAL_MINUTE_SECOND: + case INTERVAL_SECOND: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.INTERVAL_SHORT), precision, scale); + case CHAR: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.CHAR), precision, scale); + case VARCHAR: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.VARCHAR), precision, scale); + case BINARY: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.BINARY), precision, scale); + case VARBINARY: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.VARBINARY), precision, scale); + case NULL: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.NULL), precision, scale); + case MULTISET: { + Expression comp = createExpression(requireNonNull(type.getComponentType())); + return Expressions.new_(GenericSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.MULTISET), comp); + } + case ARRAY: { + Expression comp = createExpression(requireNonNull(type.getComponentType())); + return Expressions.new_(GenericSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.ARRAY), comp); + } + case MAP: { + Expression key = createExpression(requireNonNull(type.getValueType())); + Expression value = createExpression(requireNonNull(type.getKeyType())); + return Expressions.new_(GenericSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.MAP), key, value); + } + case ROW: { + Expression[] fields = new Expression[type.getFieldCount()]; + int index = 0; + for (RelDataTypeField field : type.getFieldList()) { + String name = field.getName(); + RelDataType fieldType = field.getType(); + Expression fieldTypeExpression = createExpression(fieldType); + Expression nameExpression = Expressions.constant(name); + Expression entry = + Expressions.new_(AbstractMap.SimpleEntry.class, nameExpression, fieldTypeExpression); + fields[index++] = entry; + } + return Expressions.new_(RowSqlTypeRtti.class, fields); + } + case GEOMETRY: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.GEOMETRY), precision, scale); + case VARIANT: + return Expressions.new_(BasicSqlTypeRtti.class, + Expressions.constant(RuntimeSqlTypeName.VARIANT), precision, scale); + default: + throw new RuntimeException("Unexpected type " + type); + } + } +} diff --git a/core/src/main/java/org/apache/calcite/util/rtti/package-info.java b/core/src/main/java/org/apache/calcite/util/rtti/package-info.java new file mode 100644 index 0000000000..7113279765 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/rtti/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Support for runtime type information. + */ +package org.apache.calcite.util.rtti; diff --git a/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java b/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java index 44c0cee15a..1e11113bc4 100644 --- a/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java +++ b/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java @@ -327,7 +327,7 @@ class CalciteRemoteDriverTest { CalciteAssert.hr() .with(CalciteRemoteDriverTest::getRemoteConnection) .metaData(CalciteRemoteDriverTest::getTypeInfo) - .returns(CalciteAssert.checkResultCount(is(43))); + .returns(CalciteAssert.checkResultCount(is(44))); } @Test void testRemoteTableTypes() { 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 9434f8b23d..1e9ae827a7 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -1356,6 +1356,10 @@ public class SqlValidatorTest extends SqlValidatorTestCase { .columnType("VARIANT NOT NULL"); expr("cast(TIMESTAMP '2024-09-01 00:00:00' as variant)") .columnType("VARIANT NOT NULL"); + expr("cast(ARRAY[1,2,3] AS VARIANT)") + .columnType("VARIANT NOT NULL"); + expr("cast(MAP[1, 2, 3, 4] AS VARIANT)") + .columnType("VARIANT NOT NULL"); expr("cast(cast(NULL as variant) as int)") .columnType("INTEGER"); @@ -1367,6 +1371,10 @@ public class SqlValidatorTest extends SqlValidatorTestCase { .columnType("VARCHAR"); expr("cast(cast(TIMESTAMP '2024-09-01 00:00:00' as variant) as timestamp)") .columnType("TIMESTAMP(0)"); + expr("cast(ARRAY[1,2,3] AS VARIANT ARRAY)") + .columnType("VARIANT NOT NULL ARRAY NOT NULL"); + expr("cast(MAP['a','b','c','d'] AS MAP<VARCHAR, VARIANT>)") + .columnType("(VARCHAR NOT NULL, VARIANT NOT NULL) MAP NOT NULL"); } @Test void testAccessVariant() { @@ -8636,7 +8644,8 @@ public class SqlValidatorTest extends SqlValidatorTestCase { .fails("Cannot apply 'ITEM' to arguments of type 'ITEM\\(<VARCHAR\\(10\\)>, " + "<INTEGER>\\)'\\. Supported form\\(s\\): <ARRAY>\\[<INTEGER>\\]\n" + "<MAP>\\[<ANY>\\]\n" - + "<ROW>\\[<CHARACTER>\\|<INTEGER>\\].*"); + + "<ROW>\\[<CHARACTER>\\|<INTEGER>\\]\n" + + "<VARIANT>\\[<ANY>\\].*"); } /** Test case for diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 2fe835f39f..8795333b58 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -1131,6 +1131,7 @@ UTF8, **VALUE_OF**, **VARBINARY**, **VARCHAR**, +**VARIANT**, **VARYING**, **VAR_POP**, **VAR_SAMP**, @@ -1248,6 +1249,36 @@ Note: * Every `ROW` column type can have an optional [ NULL | NOT NULL ] suffix to indicate if this column type is nullable, default is not nullable. +### The `VARIANT` type + +Values of `VARIANT` type are dynamically-typed. +Any such value holds at runtime two pieces of information: +- the data type +- the data value + +Values of `VARIANT` type can be created by casting any other value to a `VARIANT`: e.g. +`SELECT CAST(x AS VARIANT)`. Conversely, values of type `VARIANT` can be cast to any other data type +`SELECT CAST(variant AS INT)`. A cast of a value of type `VARIANT` to target type T +will compare the runtime type with T. If the types are identical, the +original value is returned. Otherwise the `CAST` returns `NULL`. + +Values of type `ARRAY`, `MAP`, and `ROW` type can be cast to `VARIANT`. `VARIANT` values +also offer the following operations: + +- indexing using array indexing notation `variant[index]`. If the `VARIANT` is + obtained from an `ARRAY` value, the indexing operation returns a `VARIANT` whose value element + is the element at the specified index. Otherwise this operation returns `NULL` +- indexing using map element access notation `variant[key]`, where `key` can have + any legal `MAP` key type. If the `VARIANT` is obtained from a `MAP` value + that has en element with this key, a `VARIANT` value holding the associated value in + the `MAP` is returned. Otherwise `NULL` is returned. If the `VARIANT` is obtained from `ROW` value + which has a field with the name `key`, this operation returns a `VARIANT` value holding + the corresponding field value. Otherwise `NULL` is returned. +- field access using the dot notation: `variant.field`. This operation is interpreted + as equivalent to `variant['field']`. Note, however, that the field notation + is subject to the capitalization rules of the SQL dialect, so for correct + operation the field may need to be quoted: `variant."field"` + ### Spatial types Spatial data is represented as character strings encoded as diff --git a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java index c6f06280aa..39faceb01b 100644 --- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java +++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java @@ -576,6 +576,7 @@ public class SqlParserTest { "VALUE_OF", "2014", "c", "VARBINARY", "2011", "2014", "c", "VARCHAR", "92", "99", "2003", "2011", "2014", "c", + "VARIANT", "c", "VARYING", "92", "99", "2003", "2011", "2014", "c", "VAR_POP", "2011", "2014", "c", "VAR_SAMP", "2011", "2014", "c", diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java index 1bb7e3058b..a75cf71c1e 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -1802,6 +1802,69 @@ public class SqlOperatorTest { f.checkNull("cast(null as row(f0 varchar, f1 varchar))"); } + /** Test cases for + * <a href="https://issues.apache.org/jira/browse/CALCITE-4918"> + * [CALCITE-4918] Add a VARIANT data type</a>. */ + @Test public void testVariant() { + SqlOperatorFixture f = fixture(); + f.checkScalar("cast(1 as VARIANT)", "1", "VARIANT NOT NULL"); + // String variants include quotes when output + f.checkScalar("cast('abc' as VARIANT)", "\"abc\"", "VARIANT NOT NULL"); + f.checkScalar("cast(ARRAY[1,2,3] as VARIANT)", "[1, 2, 3]", "VARIANT NOT NULL"); + f.checkScalar("cast(MAP['a',1,'b',2] as VARIANT)", "{a=1, b=2}", "VARIANT NOT NULL"); + f.checkScalar("cast((1, 2) as row(f0 integer, f1 bigint))", "{1, 2}", + "RecordType(INTEGER NOT NULL F0, BIGINT NOT NULL F1) NOT NULL"); + f.checkScalar("cast(row(1, 2) AS VARIANT)", "{1, 2}", "VARIANT NOT NULL"); + f.checkNull("cast(NULL AS VARIANT)"); + + // Converting a VARIANT back to the original type produces the original value + f.checkScalar("cast(cast(1 as VARIANT) AS INTEGER)", "1", "INTEGER"); + // no quotes printed, since the result is a VARCHAR + f.checkScalar("cast(cast(CAST('abc' AS VARCHAR) as VARIANT) AS VARCHAR)", "abc", + "VARCHAR"); + f.checkScalar("cast(cast(ARRAY[1,2,3] as VARIANT) AS INTEGER ARRAY)", "[1, 2, 3]", + "INTEGER NOT NULL ARRAY"); + // If the type is not exaclty the same the conversion fails (here CHAR to VARCHAR) + f.checkNull("cast(cast('abc' as VARIANT) AS VARCHAR)"); + f.checkScalar("cast(cast('abc' as VARIANT) AS CHAR(3))", "abc", "CHAR(3)"); + + // Converting a variant to anything that does not match the runtime type returns null + f.checkScalar("cast(cast(1 as VARIANT) as INTEGER)", "1", "INTEGER"); + f.checkNull("cast(cast(1 as VARIANT) as VARCHAR)"); + f.checkNull("cast(cast(1 as VARIANT) as INT ARRAY)"); + + // Arrays of variant objects + f.checkScalar("ARRAY[CAST(1 AS VARIANT), CAST('abc' AS VARIANT)]", "[1, \"abc\"]", + "VARIANT NOT NULL ARRAY NOT NULL"); + // Arrays can even contain other arrays + f.checkScalar("ARRAY[CAST(1 AS VARIANT), CAST('abc' AS VARIANT), CAST(ARRAY[2] AS VARIANT)]", + "[1, \"abc\", [2]]", "VARIANT NOT NULL ARRAY NOT NULL"); + // Field access in a VARIANT ARRAY + f.checkScalar("CAST(ARRAY[CAST(1 AS VARIANT), CAST('abc' AS VARIANT)][1] AS INTEGER)", "1", + "INTEGER"); + // Field access in a VARIANT MAP + f.checkScalar("cast(MAP['a',1,'b',2] as VARIANT)['a']", "1", "VARIANT"); + // Alternative field access in a VARIANT MAP. Field names have to be quoted, though + f.checkScalar("cast(MAP['a',1,'b',2] as VARIANT).\"a\"", "1", "VARIANT"); + + // Here is a possible representation of a JSON document { "a": 1, "b": [ { "c": 2.3 }, 5 ] } + f.checkScalar("MAP[" + + "CAST('a' AS VARIANT), CAST(1 AS VARIANT), " + + "CAST('b' AS VARIANT), CAST(ARRAY[" + + "CAST(MAP[CAST('c' AS VARIANT), CAST(2.3 AS VARIANT)] AS VARIANT), CAST(5 AS VARIANT)]" + + " AS VARIANT)]", + "{\"a\"=1, \"b\"=[{\"c\"=2.3}, 5]}", + "(VARIANT NOT NULL, VARIANT NOT NULL) MAP NOT NULL"); + // Another possible representation using CHAR instead of VARIANT for MAP keys + f.checkScalar("MAP[" + + "'a', CAST(1 AS VARIANT), " + + "'b', CAST(ARRAY[" + + "CAST(MAP['c', CAST(2.3 AS VARIANT)] AS VARIANT), CAST(5 AS VARIANT)]" + + " AS VARIANT)]", + "{a=1, b=[{c=2.3}, 5]}", + "(CHAR(1) NOT NULL, VARIANT NOT NULL) MAP NOT NULL"); + } + /** Test case for * <a href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6095"> * [CALCITE-6095] Arithmetic expression with VARBINARY value causes AssertionFailure</a>. @@ -12907,7 +12970,8 @@ public class SqlOperatorTest { "Cannot apply 'ITEM' to arguments of type 'ITEM\\(<CHAR\\(3\\) ARRAY>, " + "<CHAR\\(3\\)>\\)'\\. Supported form\\(s\\): <ARRAY>\\[<INTEGER>\\]\n" + "<MAP>\\[<ANY>\\]\n" - + "<ROW>\\[<CHARACTER>\\|<INTEGER>\\]", + + "<ROW>\\[<CHARACTER>\\|<INTEGER>\\]\n" + + "<VARIANT>\\[<ANY>\\]", false); // Array of INTEGER NOT NULL is interesting because we might be tempted
