From 87485a90737ded1f7324870e761b94d78a41b5e0 Mon Sep 17 00:00:00 2001 From: Mihai Budiu Date: Thu, 5 Sep 2024 23:43:38 -0700 Subject: [PATCH] Implement VARIANT functions TYPEOF, VARIANTNULL; add variant.iq Signed-off-by: Mihai Budiu --- .../adapter/enumerable/RexImpTable.java | 6 + .../enumerable/RexToLixTranslator.java | 13 +- .../org/apache/calcite/rex/RexBuilder.java | 3 +- .../org/apache/calcite/rex/RexLiteral.java | 4 +- .../apache/calcite/runtime/SqlFunctions.java | 6 +- .../rtti/BasicSqlTypeRtti.java | 26 +- .../rtti/GenericSqlTypeRtti.java | 10 +- .../rtti/RowSqlTypeRtti.java | 46 +- .../rtti/RuntimeTypeInformation.java | 106 ++-- .../{util => runtime}/rtti/package-info.java | 2 +- .../runtime/variant/VariantNonNull.java | 452 ++++++++++++++++++ .../calcite/runtime/variant/VariantNull.java | 64 +++ .../runtime/variant/VariantSqlNull.java | 60 +++ .../runtime/variant/VariantSqlValue.java | 58 +++ .../calcite/runtime/variant/VariantValue.java | 44 ++ .../calcite/runtime/variant/package-info.java | 21 + .../calcite/sql/fun/SqlStdOperatorTable.java | 10 + .../apache/calcite/sql/type/OperandTypes.java | 3 + .../apache/calcite/sql/type/ReturnTypes.java | 6 + .../calcite/sql2rel/ConvertToChecked.java | 6 +- .../apache/calcite/util/BuiltInMethod.java | 12 +- .../java/org/apache/calcite/util/Variant.java | 181 ------- core/src/test/resources/sql/variant.iq | 197 ++++++++ site/_docs/reference.md | 27 +- .../apache/calcite/test/SqlOperatorTest.java | 10 +- 25 files changed, 1099 insertions(+), 274 deletions(-) rename core/src/main/java/org/apache/calcite/{util => runtime}/rtti/BasicSqlTypeRtti.java (81%) rename core/src/main/java/org/apache/calcite/{util => runtime}/rtti/GenericSqlTypeRtti.java (92%) rename core/src/main/java/org/apache/calcite/{util => runtime}/rtti/RowSqlTypeRtti.java (64%) rename core/src/main/java/org/apache/calcite/{util => runtime}/rtti/RuntimeTypeInformation.java (72%) rename core/src/main/java/org/apache/calcite/{util => runtime}/rtti/package-info.java (95%) create mode 100644 core/src/main/java/org/apache/calcite/runtime/variant/VariantNonNull.java create mode 100644 core/src/main/java/org/apache/calcite/runtime/variant/VariantNull.java create mode 100644 core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlNull.java create mode 100644 core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlValue.java create mode 100644 core/src/main/java/org/apache/calcite/runtime/variant/VariantValue.java create mode 100644 core/src/main/java/org/apache/calcite/runtime/variant/package-info.java delete mode 100644 core/src/main/java/org/apache/calcite/util/Variant.java create mode 100644 core/src/test/resources/sql/variant.iq diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index f766feb40231..fd7aa0cf76c8 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -520,10 +520,12 @@ import static org.apache.calcite.sql.fun.SqlStdOperatorTable.TRIM; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.TRUNCATE; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.TUMBLE; +import static org.apache.calcite.sql.fun.SqlStdOperatorTable.TYPEOF; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.UNARY_MINUS; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.UNARY_PLUS; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.UPPER; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.USER; +import static org.apache.calcite.sql.fun.SqlStdOperatorTable.VARIANTNULL; import static org.apache.calcite.util.ReflectUtil.isStatic; import static java.util.Objects.requireNonNull; @@ -866,6 +868,8 @@ void populate1() { defineMethod(TRUNC_BIG_QUERY, BuiltInMethod.STRUNCATE.method, NullPolicy.STRICT); defineMethod(TRUNCATE, BuiltInMethod.STRUNCATE.method, NullPolicy.STRICT); defineMethod(LOG1P, BuiltInMethod.LOG1P.method, NullPolicy.STRICT); + defineMethod(TYPEOF, BuiltInMethod.TYPEOF.method, NullPolicy.STRICT); + defineMethod(VARIANTNULL, BuiltInMethod.VARIANTNULL.method, NullPolicy.STRICT); define(SAFE_ADD, new SafeArithmeticImplementor(BuiltInMethod.SAFE_ADD.method)); @@ -3825,6 +3829,8 @@ private static class ItemImplementor extends AbstractRexCallImplementor { // use the general MethodImplementor. private AbstractRexCallImplementor getImplementor(SqlTypeName sqlTypeName) { switch (sqlTypeName) { + case VARIANT: + return new MethodImplementor(BuiltInMethod.VARIANT_ITEM.method, nullPolicy, false); case ARRAY: return new ArrayItemImplementor(); case MAP: 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 0cebb750e2ee..d3411654fec4 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 @@ -53,6 +53,8 @@ import org.apache.calcite.rex.RexUtil; import org.apache.calcite.rex.RexVisitor; import org.apache.calcite.runtime.SpatialTypeFunctions; +import org.apache.calcite.runtime.rtti.RuntimeTypeInformation; +import org.apache.calcite.runtime.variant.VariantValue; import org.apache.calcite.schema.FunctionContext; import org.apache.calcite.sql.SqlIntervalQualifier; import org.apache.calcite.sql.SqlOperator; @@ -66,8 +68,6 @@ 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; @@ -321,10 +321,12 @@ private Expression getConvertExpression( return defaultExpression.get(); } // Converting a VARIANT to any other type calls the Variant.cast method + // First cast operand to a VariantValue (it may be an Object) + Expression operandCast = Expressions.convert_(operand, VariantValue.class); Expression cast = - Expressions.call(BuiltInMethod.VARIANT_CAST.method, operand, + Expressions.call(operandCast, BuiltInMethod.VARIANT_CAST.method, RuntimeTypeInformation.createExpression(targetType)); - // The cast returns an Object, so we need a convert too + // The cast returns an Object, so we need a convert to the expected Java type RelDataType nullableTarget = typeFactory.createTypeWithNullability(targetType, true); return Expressions.convert_(cast, typeFactory.getJavaClass(nullableTarget)); } @@ -333,7 +335,8 @@ private Expression getConvertExpression( case VARIANT: // Converting any type to a VARIANT invokes the Variant constructor Expression rtti = RuntimeTypeInformation.createExpression(sourceType); - return Expressions.new_(Variant.class, operand, rtti); + Expression roundingMode = Expressions.constant(typeFactory.getTypeSystem().roundingMode()); + return Expressions.call(BuiltInMethod.VARIANT_CREATE.method, roundingMode, 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 a3a5248ba5b4..cb4285e376da 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java +++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java @@ -859,8 +859,7 @@ boolean canRemoveCastFromLiteral(RelDataType toType, return true; } final SqlTypeName sqlType = toType.getSqlTypeName(); - if (sqlType == SqlTypeName.MEASURE - || sqlType == SqlTypeName.VARIANT) { + 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 69230a1ec9c7..d2722719e49c 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java +++ b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java @@ -26,6 +26,7 @@ import org.apache.calcite.rel.type.RelDataTypeField; import org.apache.calcite.runtime.FlatLists; import org.apache.calcite.runtime.SpatialTypeFunctions; +import org.apache.calcite.runtime.variant.VariantValue; import org.apache.calcite.sql.SqlCollation; import org.apache.calcite.sql.SqlKind; import org.apache.calcite.sql.SqlOperator; @@ -43,7 +44,6 @@ 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; @@ -317,7 +317,7 @@ public static boolean valueMatchesType( } switch (typeName) { case VARIANT: - return value instanceof Variant; + return value instanceof VariantValue; 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 a5959c3a3a05..ba6cf04f734f 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -38,6 +38,7 @@ import org.apache.calcite.rel.type.TimeFrame; import org.apache.calcite.rel.type.TimeFrameSet; import org.apache.calcite.runtime.FlatLists.ComparableList; +import org.apache.calcite.runtime.variant.VariantValue; import org.apache.calcite.sql.SqlIntervalQualifier; import org.apache.calcite.sql.SqlUtil; import org.apache.calcite.sql.fun.SqlLibraryOperators; @@ -47,7 +48,6 @@ 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; @@ -5767,8 +5767,8 @@ public static String replace(String s, String search, String replacement) { * known until runtime. */ public static @Nullable Object item(Object object, Object index) { - if (object instanceof Variant) { - return ((Variant) object).item(index); + if (object instanceof VariantValue) { + return ((VariantValue) object).item(index); } if (object instanceof Map) { return mapItem((Map) object, index); diff --git a/core/src/main/java/org/apache/calcite/util/rtti/BasicSqlTypeRtti.java b/core/src/main/java/org/apache/calcite/runtime/rtti/BasicSqlTypeRtti.java similarity index 81% rename from core/src/main/java/org/apache/calcite/util/rtti/BasicSqlTypeRtti.java rename to core/src/main/java/org/apache/calcite/runtime/rtti/BasicSqlTypeRtti.java index 63f7afe60a12..cb2281ac6840 100644 --- a/core/src/main/java/org/apache/calcite/util/rtti/BasicSqlTypeRtti.java +++ b/core/src/main/java/org/apache/calcite/runtime/rtti/BasicSqlTypeRtti.java @@ -14,20 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.util.rtti; +package org.apache.calcite.runtime.rtti; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** 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) { + public BasicSqlTypeRtti(RuntimeSqlTypeName typeName) { 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) { @@ -39,13 +36,11 @@ public BasicSqlTypeRtti(RuntimeSqlTypeName typeName, int precision, int scale) { } BasicSqlTypeRtti that = (BasicSqlTypeRtti) o; - return typeName == that.typeName && precision == that.precision && scale == that.scale; + return typeName == that.typeName; } @Override public int hashCode() { - int result = precision; - result = 31 * result + scale; - return result; + return Objects.hashCode(typeName); } @Override public String getTypeString() { @@ -62,8 +57,6 @@ public BasicSqlTypeRtti(RuntimeSqlTypeName typeName, int precision, int scale) { return "BIGINT"; case DECIMAL: return "DECIMAL"; - case FLOAT: - return "FLOAT"; case REAL: return "REAL"; case DOUBLE: @@ -85,12 +78,8 @@ public BasicSqlTypeRtti(RuntimeSqlTypeName typeName, int precision, int scale) { 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: @@ -107,7 +96,6 @@ public BasicSqlTypeRtti(RuntimeSqlTypeName typeName, int precision, int scale) { // 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 + ")"; + return "new BasicSqlTypeRtti(" + this.getTypeString() + ")"; } } diff --git a/core/src/main/java/org/apache/calcite/util/rtti/GenericSqlTypeRtti.java b/core/src/main/java/org/apache/calcite/runtime/rtti/GenericSqlTypeRtti.java similarity index 92% rename from core/src/main/java/org/apache/calcite/util/rtti/GenericSqlTypeRtti.java rename to core/src/main/java/org/apache/calcite/runtime/rtti/GenericSqlTypeRtti.java index 25f3be33e8c4..7c8893b3a862 100644 --- a/core/src/main/java/org/apache/calcite/util/rtti/GenericSqlTypeRtti.java +++ b/core/src/main/java/org/apache/calcite/runtime/rtti/GenericSqlTypeRtti.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.util.rtti; +package org.apache.calcite.runtime.rtti; import org.checkerframework.checker.nullness.qual.Nullable; @@ -72,4 +72,12 @@ public GenericSqlTypeRtti(RuntimeSqlTypeName typeName, RuntimeTypeInformation... @Override public int hashCode() { return Arrays.hashCode(typeArguments); } + + public RuntimeTypeInformation getTypeArgument(int index) { + return typeArguments[index]; + } + + public int getArgumentCount() { + return typeArguments.length; + } } diff --git a/core/src/main/java/org/apache/calcite/util/rtti/RowSqlTypeRtti.java b/core/src/main/java/org/apache/calcite/runtime/rtti/RowSqlTypeRtti.java similarity index 64% rename from core/src/main/java/org/apache/calcite/util/rtti/RowSqlTypeRtti.java rename to core/src/main/java/org/apache/calcite/runtime/rtti/RowSqlTypeRtti.java index d876e7ea7676..035c7aeb0294 100644 --- a/core/src/main/java/org/apache/calcite/util/rtti/RowSqlTypeRtti.java +++ b/core/src/main/java/org/apache/calcite/runtime/rtti/RowSqlTypeRtti.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.util.rtti; +package org.apache.calcite.runtime.rtti; import org.checkerframework.checker.nullness.qual.Nullable; @@ -23,12 +23,12 @@ /** Runtime type information for a ROW type. */ public class RowSqlTypeRtti extends RuntimeTypeInformation { - private final Map.Entry[] fieldNames; + private final Map.Entry[] fields; @SafeVarargs - public RowSqlTypeRtti(Map.Entry... fieldNames) { + public RowSqlTypeRtti(Map.Entry... fields) { super(RuntimeSqlTypeName.ROW); - this.fieldNames = fieldNames; + this.fields = fields; } @Override public String getTypeString() { @@ -41,7 +41,7 @@ public RowSqlTypeRtti(Map.Entry... fieldNames) { StringBuilder builder = new StringBuilder(); builder.append("new RowSqlTypeRtti("); boolean first = true; - for (Map.Entry arg : this.fieldNames) { + for (Map.Entry arg : this.fields) { if (!first) { builder.append(", "); } @@ -61,10 +61,42 @@ public RowSqlTypeRtti(Map.Entry... fieldNames) { } RowSqlTypeRtti that = (RowSqlTypeRtti) o; - return Arrays.equals(fieldNames, that.fieldNames); + return Arrays.equals(fields, that.fields); } @Override public int hashCode() { - return Arrays.hashCode(fieldNames); + return Arrays.hashCode(fields); + } + + /** Get the field with the specified index. */ + public Map.Entry getField(int index) { + return this.fields[index]; + } + + public int size() { + return this.fields.length; + } + + /** Return the runtime type information of the associated field, + * or null if no such field exists. + * + * @param index Field index, starting from 0 + */ + public @Nullable RuntimeTypeInformation getFieldType(Object index) { + if (index instanceof Integer) { + int intIndex = (Integer) index; + if (intIndex < 0 || intIndex >= this.fields.length) { + return null; + } + return this.fields[intIndex].getValue(); + } else if (index instanceof String) { + String stringIndex = (String) index; + for (Map.Entry field : this.fields) { + if (field.getKey().equalsIgnoreCase(stringIndex)) { + return field.getValue(); + } + } + } + return null; } } diff --git a/core/src/main/java/org/apache/calcite/util/rtti/RuntimeTypeInformation.java b/core/src/main/java/org/apache/calcite/runtime/rtti/RuntimeTypeInformation.java similarity index 72% rename from core/src/main/java/org/apache/calcite/util/rtti/RuntimeTypeInformation.java rename to core/src/main/java/org/apache/calcite/runtime/rtti/RuntimeTypeInformation.java index 73527408d78f..2bb9bed6a5de 100644 --- a/core/src/main/java/org/apache/calcite/util/rtti/RuntimeTypeInformation.java +++ b/core/src/main/java/org/apache/calcite/runtime/rtti/RuntimeTypeInformation.java @@ -14,21 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.util.rtti; +package org.apache.calcite.runtime.rtti; import org.apache.calcite.linq4j.tree.Expression; import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.linq4j.tree.Primitive; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeField; +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.AbstractMap; import static java.util.Objects.requireNonNull; /** - * This class represents the type of a SQL expression at runtime. + * 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 + * 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. */ @@ -41,8 +44,8 @@ public enum RuntimeSqlTypeName { INTEGER(false), BIGINT(false), DECIMAL(false), - FLOAT(false), REAL(false), + // FLOAT is represented as DOUBLE DOUBLE(false), DATE(false), TIME(false), @@ -53,9 +56,11 @@ public enum RuntimeSqlTypeName { TIMESTAMP_TZ(false), INTERVAL_LONG(false), INTERVAL_SHORT(false), - CHAR(false), + // "Name" is used for structure field names + NAME(false), + // CHAR is represented as VARCHAR VARCHAR(false), - BINARY(false), + // BINARY is represented as VARBINARY VARBINARY(false), NULL(false), MULTISET(true), @@ -93,71 +98,92 @@ public boolean isScalar() { return this.typeName.isScalar(); } + /** If this type is a Primitive, return it, otherwise return null. */ + public @Nullable Primitive asPrimitive() { + switch (typeName) { + case BOOLEAN: + return Primitive.BOOLEAN; + case TINYINT: + return Primitive.BYTE; + case SMALLINT: + return Primitive.SHORT; + case INTEGER: + return Primitive.INT; + case BIGINT: + return Primitive.LONG; + case REAL: + return Primitive.FLOAT; + case DOUBLE: + return Primitive.DOUBLE; + default: + return null; + } + } + + public GenericSqlTypeRtti asGeneric() { + assert this instanceof GenericSqlTypeRtti; + return (GenericSqlTypeRtti) this; + } + /** - * Create and return an expression that creates a runtime type that + * Creates and returns 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); + Expressions.constant(RuntimeSqlTypeName.BOOLEAN)); case TINYINT: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.TINYINT), precision, scale); + Expressions.constant(RuntimeSqlTypeName.TINYINT)); case SMALLINT: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.SMALLINT), precision, scale); + Expressions.constant(RuntimeSqlTypeName.SMALLINT)); case INTEGER: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.INTEGER), precision, scale); + Expressions.constant(RuntimeSqlTypeName.INTEGER)); case BIGINT: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.BIGINT), precision, scale); + Expressions.constant(RuntimeSqlTypeName.BIGINT)); 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); + Expressions.constant(RuntimeSqlTypeName.DECIMAL)); case REAL: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.REAL), precision, scale); + Expressions.constant(RuntimeSqlTypeName.REAL)); + case FLOAT: case DOUBLE: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.DOUBLE), precision, scale); + Expressions.constant(RuntimeSqlTypeName.DOUBLE)); case DATE: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.DATE), precision, scale); + Expressions.constant(RuntimeSqlTypeName.DATE)); case TIME: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.TIME), precision, scale); + Expressions.constant(RuntimeSqlTypeName.TIME)); case TIME_WITH_LOCAL_TIME_ZONE: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.TIME_WITH_LOCAL_TIME_ZONE), - precision, scale); + Expressions.constant(RuntimeSqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)); case TIME_TZ: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.TIME_TZ), precision, scale); + Expressions.constant(RuntimeSqlTypeName.TIME_TZ)); case TIMESTAMP: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.TIMESTAMP), precision, scale); + Expressions.constant(RuntimeSqlTypeName.TIMESTAMP)); case TIMESTAMP_WITH_LOCAL_TIME_ZONE: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE), - precision, scale); + Expressions.constant(RuntimeSqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)); case TIMESTAMP_TZ: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.TIMESTAMP_TZ), precision, scale); + Expressions.constant(RuntimeSqlTypeName.TIMESTAMP_TZ)); case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: case INTERVAL_MONTH: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.INTERVAL_LONG), precision, scale); + Expressions.constant(RuntimeSqlTypeName.INTERVAL_LONG)); case INTERVAL_DAY: case INTERVAL_DAY_HOUR: case INTERVAL_DAY_MINUTE: @@ -169,22 +195,18 @@ public static Expression createExpression(RelDataType type) { case INTERVAL_MINUTE_SECOND: case INTERVAL_SECOND: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.INTERVAL_SHORT), precision, scale); + Expressions.constant(RuntimeSqlTypeName.INTERVAL_SHORT)); 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); + Expressions.constant(RuntimeSqlTypeName.VARCHAR)); 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); + Expressions.constant(RuntimeSqlTypeName.VARBINARY)); case NULL: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.NULL), precision, scale); + Expressions.constant(RuntimeSqlTypeName.NULL)); case MULTISET: { Expression comp = createExpression(requireNonNull(type.getComponentType())); return Expressions.new_(GenericSqlTypeRtti.class, @@ -196,8 +218,8 @@ public static Expression createExpression(RelDataType type) { Expressions.constant(RuntimeSqlTypeName.ARRAY), comp); } case MAP: { - Expression key = createExpression(requireNonNull(type.getValueType())); - Expression value = createExpression(requireNonNull(type.getKeyType())); + Expression key = createExpression(requireNonNull(type.getKeyType())); + Expression value = createExpression(requireNonNull(type.getValueType())); return Expressions.new_(GenericSqlTypeRtti.class, Expressions.constant(RuntimeSqlTypeName.MAP), key, value); } @@ -217,10 +239,10 @@ public static Expression createExpression(RelDataType type) { } case GEOMETRY: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.GEOMETRY), precision, scale); + Expressions.constant(RuntimeSqlTypeName.GEOMETRY)); case VARIANT: return Expressions.new_(BasicSqlTypeRtti.class, - Expressions.constant(RuntimeSqlTypeName.VARIANT), precision, scale); + Expressions.constant(RuntimeSqlTypeName.VARIANT)); 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/runtime/rtti/package-info.java similarity index 95% rename from core/src/main/java/org/apache/calcite/util/rtti/package-info.java rename to core/src/main/java/org/apache/calcite/runtime/rtti/package-info.java index 7113279765da..548b4ba62452 100644 --- a/core/src/main/java/org/apache/calcite/util/rtti/package-info.java +++ b/core/src/main/java/org/apache/calcite/runtime/rtti/package-info.java @@ -18,4 +18,4 @@ /** * Support for runtime type information. */ -package org.apache.calcite.util.rtti; +package org.apache.calcite.runtime.rtti; diff --git a/core/src/main/java/org/apache/calcite/runtime/variant/VariantNonNull.java b/core/src/main/java/org/apache/calcite/runtime/variant/VariantNonNull.java new file mode 100644 index 000000000000..939785f74f27 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantNonNull.java @@ -0,0 +1,452 @@ +/* + * 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.runtime.variant; + +import org.apache.calcite.linq4j.tree.Primitive; +import org.apache.calcite.runtime.SqlFunctions; +import org.apache.calcite.runtime.rtti.BasicSqlTypeRtti; +import org.apache.calcite.runtime.rtti.RowSqlTypeRtti; +import org.apache.calcite.runtime.rtti.RuntimeTypeInformation; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.apache.calcite.runtime.rtti.RuntimeTypeInformation.RuntimeSqlTypeName.NAME; + +import static java.util.Objects.requireNonNull; + +/** A VARIANT value that contains a non-null value. */ +public class VariantNonNull extends VariantSqlValue { + final RoundingMode roundingMode; + /** Actual value - can have any SQL type. */ + final Object value; + + VariantNonNull(RoundingMode roundingMode, Object value, RuntimeTypeInformation runtimeType) { + super(runtimeType.getTypeName()); + this.roundingMode = roundingMode; + // sanity check + switch (runtimeType.getTypeName()) { + case NAME: + assert value instanceof String; + this.value = value; + break; + case BOOLEAN: + assert value instanceof Boolean; + this.value = value; + break; + case TINYINT: + assert value instanceof Byte; + this.value = value; + break; + case SMALLINT: + assert value instanceof Short; + this.value = value; + break; + case INTEGER: + assert value instanceof Integer; + this.value = value; + break; + case BIGINT: + assert value instanceof Long; + this.value = value; + break; + case DECIMAL: + assert value instanceof BigDecimal; + this.value = value; + break; + case REAL: + assert value instanceof Float; + this.value = value; + break; + case DOUBLE: + assert value instanceof Double; + this.value = value; + break; + case DATE: + 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: + this.value = value; + break; + case VARCHAR: + this.value = value; + assert value instanceof String; + break; + case NULL: + default: + throw new RuntimeException("Unreachable"); + case VARBINARY: + case GEOMETRY: + case VARIANT: + this.value = value; + break; + case MAP: { + RuntimeTypeInformation keyType = runtimeType.asGeneric().getTypeArgument(0); + RuntimeTypeInformation valueType = runtimeType.asGeneric().getTypeArgument(1); + assert value instanceof Map; + Map map = (Map) value; + LinkedHashMap converted = new LinkedHashMap<>(map.size()); + for (Map.Entry o : map.entrySet()) { + VariantValue key = VariantSqlValue.create(roundingMode, o.getKey(), keyType); + VariantValue val = VariantSqlValue.create(roundingMode, o.getValue(), valueType); + converted.put(key, val); + } + this.value = converted; + break; + } + case ROW: { + assert value instanceof Object[]; + Object[] a = (Object[]) value; + assert runtimeType instanceof RowSqlTypeRtti; + RowSqlTypeRtti rowType = (RowSqlTypeRtti) runtimeType; + LinkedHashMap converted = new LinkedHashMap<>(a.length); + RuntimeTypeInformation name = new BasicSqlTypeRtti(NAME); + for (int i = 0; i < a.length; i++) { + Map.Entry fieldType = rowType.getField(i); + VariantValue key = VariantSqlValue.create(roundingMode, fieldType.getKey(), name); + VariantValue val = VariantSqlValue.create(roundingMode, a[i], fieldType.getValue()); + converted.put(key, val); + } + this.value = converted; + break; + } + case MULTISET: + case ARRAY: { + RuntimeTypeInformation elementType = runtimeType.asGeneric().getTypeArgument(0); + assert value instanceof List; + List list = (List) value; + List converted = new ArrayList<>(list.size()); + for (Object o : list) { + VariantValue element = VariantSqlValue.create(roundingMode, o, elementType); + converted.add(element); + } + this.value = converted; + break; + } + } + } + + @Override public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + VariantNonNull variant = (VariantNonNull) o; + return Objects.equals(value, variant.value) + && runtimeType == variant.runtimeType; + } + + @Override public int hashCode() { + int result = Objects.hashCode(value); + result = 31 * result + runtimeType.hashCode(); + 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. */ + // This method is invoked from {@link RexToLixTranslator} VARIANT_CAST + @Override public @Nullable Object cast(RuntimeTypeInformation type) { + if (this.runtimeType.isScalar()) { + if (this.runtimeType == type.getTypeName()) { + return this.value; + } else { + // Convert numeric values + @Nullable Primitive target = type.asPrimitive(); + switch (this.runtimeType) { + case TINYINT: { + byte b = (byte) value; + switch (type.getTypeName()) { + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case REAL: + case DOUBLE: + return requireNonNull(target, "target").numberValue(b, roundingMode); + case DECIMAL: + return BigDecimal.valueOf(b); + default: + break; + } + break; + } + case SMALLINT: { + short s = (short) value; + switch (type.getTypeName()) { + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case REAL: + case DOUBLE: + return requireNonNull(target, "target").numberValue(s, roundingMode); + case DECIMAL: + return BigDecimal.valueOf(s); + default: + break; + } + break; + } + case INTEGER: { + int i = (int) value; + switch (type.getTypeName()) { + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case REAL: + case DOUBLE: + return requireNonNull(target, "target").numberValue(i, roundingMode); + case DECIMAL: + return BigDecimal.valueOf(i); + default: + break; + } + break; + } + case BIGINT: { + long l = (int) value; + switch (type.getTypeName()) { + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case REAL: + case DOUBLE: + return requireNonNull(target, "target").numberValue(l, roundingMode); + case DECIMAL: + return BigDecimal.valueOf(l); + default: + break; + } + break; + } + case DECIMAL: { + BigDecimal d = (BigDecimal) value; + switch (type.getTypeName()) { + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case REAL: + case DOUBLE: + return requireNonNull(target, "target").numberValue(d, roundingMode); + case DECIMAL: + return d; + default: + break; + } + break; + } + case REAL: { + float f = (float) value; + switch (type.getTypeName()) { + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case REAL: + case DOUBLE: + return requireNonNull(target, "target").numberValue(f, roundingMode); + case DECIMAL: + return BigDecimal.valueOf(f); + default: + break; + } + break; + } + case DOUBLE: { + double d = (double) value; + switch (type.getTypeName()) { + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case REAL: + case DOUBLE: + return requireNonNull(target, "target").numberValue(d, roundingMode); + case DECIMAL: + return BigDecimal.valueOf(d); + default: + break; + } + break; + } + default: + break; + } + return null; + } + } else { + switch (this.runtimeType) { + case ARRAY: + if (type.getTypeName() == RuntimeTypeInformation.RuntimeSqlTypeName.ARRAY) { + RuntimeTypeInformation elementType = type.asGeneric().getTypeArgument(0); + assert value instanceof List; + List list = (List) value; + List<@Nullable Object> result = new ArrayList<>(list.size()); + for (VariantSqlValue o : list) { + @Nullable Object converted = o.cast(elementType); + result.add(converted); + } + return result; + } + break; + case MAP: + assert value instanceof Map; + Map map = (Map) value; + if (type.getTypeName() == RuntimeTypeInformation.RuntimeSqlTypeName.MAP) { + // Convert map to map: cast keys and values recursively + RuntimeTypeInformation keyType = type.asGeneric().getTypeArgument(0); + RuntimeTypeInformation valueType = type.asGeneric().getTypeArgument(0); + LinkedHashMap<@Nullable Object, @Nullable Object> result = + new LinkedHashMap<>(map.size()); + for (Map.Entry e : map.entrySet()) { + @Nullable Object key = e.getKey().cast(keyType); + @Nullable Object value = e.getValue().cast(valueType); + result.put(key, value); + } + return result; + } else if (type.getTypeName() == RuntimeTypeInformation.RuntimeSqlTypeName.ROW) { + // Convert map to row: lookup the row's fields in the map + RowSqlTypeRtti rowType = (RowSqlTypeRtti) type; + @Nullable Object [] result = new Object[rowType.size()]; + for (int i = 0; i < rowType.size(); i++) { + Map.Entry field = rowType.getField(i); + Object fieldValue = null; + VariantValue v = this.item(field.getKey()); + if (v != null) { + fieldValue = v.cast(field.getValue()); + } + result[i] = fieldValue; + } + return result; + } + break; + default: + break; + } + } + return null; + } + + // Implementation of the array index operator for VARIANT values + @Override public @Nullable VariantValue item(Object index) { + boolean isInteger = index instanceof Integer; + switch (this.runtimeType) { + case ROW: + if (index instanceof String) { + RuntimeTypeInformation string = + new BasicSqlTypeRtti(RuntimeTypeInformation.RuntimeSqlTypeName.NAME); + index = VariantSqlValue.create(roundingMode, index, string); + } + break; + case MAP: + if (index instanceof String) { + RuntimeTypeInformation string = + new BasicSqlTypeRtti(RuntimeTypeInformation.RuntimeSqlTypeName.VARCHAR); + index = VariantSqlValue.create(roundingMode, index, string); + } else if (isInteger) { + RuntimeTypeInformation i = + new BasicSqlTypeRtti(RuntimeTypeInformation.RuntimeSqlTypeName.INTEGER); + index = VariantSqlValue.create(roundingMode, index, i); + } + break; + case ARRAY: + if (!isInteger) { + // Arrays only support integer indexes + return null; + } + break; + default: + return null; + } + + // If index is VARIANT, leave it unchanged + Object result = SqlFunctions.itemOptional(this.value, index); + if (result == null) { + return null; + } + // If result is a variant, return as is + if (result instanceof VariantValue) { + return (VariantValue) result; + } + return null; + } + + // This method is called by the testing code. + @Override public String toString() { + if (this.runtimeType == RuntimeTypeInformation.RuntimeSqlTypeName.ROW) { + if (value instanceof Map) { + // Do not print field names, only their values + Map map = (Map) value; + StringBuilder buf = new StringBuilder("{"); + + boolean first = true; + for (Map.Entry o : map.entrySet()) { + if (!first) { + buf.append(", "); + } + first = false; + if (o.getValue() != null) { + // This should always be true + buf.append(o.getValue()); + } + } + buf.append("}"); + return buf.toString(); + } + } + String quote = ""; + switch (this.runtimeType) { + 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 VARCHAR: + 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/runtime/variant/VariantNull.java b/core/src/main/java/org/apache/calcite/runtime/variant/VariantNull.java new file mode 100644 index 000000000000..a11dbbed6a57 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantNull.java @@ -0,0 +1,64 @@ +/* + * 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.runtime.variant; + +import org.apache.calcite.runtime.rtti.RuntimeTypeInformation; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** 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 class represents such values. */ +public class VariantNull extends VariantValue { + // Protected constructor to enforce a singleton pattern + protected VariantNull() {} + + @Override public String getTypeString() { + return "VARIANT"; + } + + @Override public @Nullable Object cast(RuntimeTypeInformation type) { + // Result is always null + return null; + } + + @Override public @Nullable Object item(Object index) { + // Result is always null + return null; + } + + public static final VariantNull INSTANCE = new VariantNull(); + + /** Get the single instance of this type. */ + // Called from BuiltInMethod.VARIANTNULL + public static VariantNull getInstance() { + return INSTANCE; + } + + @Override public int hashCode() { + return 0; + } + + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + @Override public boolean equals(@Nullable Object other) { + return other == INSTANCE; + } + + @Override public String toString() { + return "null"; + } +} diff --git a/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlNull.java b/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlNull.java new file mode 100644 index 000000000000..85d424461dc3 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlNull.java @@ -0,0 +1,60 @@ +/* + * 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.runtime.variant; + +import org.apache.calcite.runtime.rtti.RuntimeTypeInformation; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Objects; + +/** A VARIANT value that contains a NULL runtime value. */ +public class VariantSqlNull extends VariantSqlValue { + VariantSqlNull(RuntimeTypeInformation.RuntimeSqlTypeName runtimeType) { + super(runtimeType); + } + + @Override public @Nullable Object item(Object index) { + // Result is always null + return null; + } + + @Override public @Nullable Object cast(RuntimeTypeInformation type) { + // Result is always null + return null; + } + + @Override public String toString() { + return "NULL"; + } + + @Override public int hashCode() { + return Objects.hashCode(runtimeType); + } + + @Override public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + VariantSqlNull variant = (VariantSqlNull) o; + return runtimeType == variant.runtimeType; + } +} diff --git a/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlValue.java b/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlValue.java new file mode 100644 index 000000000000..36dc86a26da1 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlValue.java @@ -0,0 +1,58 @@ +/* + * 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.runtime.variant; + +import org.apache.calcite.runtime.rtti.RuntimeTypeInformation; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.math.RoundingMode; + +/** A value of VARIANT type that represents a SQL value + * (The VARIANT type also has a null value which is different + * from any other SQL value). */ +public abstract class VariantSqlValue extends VariantValue { + final RuntimeTypeInformation.RuntimeSqlTypeName runtimeType; + + protected VariantSqlValue(RuntimeTypeInformation.RuntimeSqlTypeName runtimeType) { + this.runtimeType = runtimeType; + } + + @Override public String getTypeString() { + return this.runtimeType.toString(); + } + + /** + * Create a VariantValue from a specified SQL value and the runtime type information. + * + * @param object SQL runtime value. + * @param roundingMode Rounding mode used for converting numeric values. + * @param type Runtime type information. + * @return The created VariantValue. + */ + // Normally this method should be in the VariantValue class, but the Janino + // compiler used by Calcite compiles to a Java version that does not + // support static methods in interfaces. + // This method is called from BuiltInMethods.VARIANT_CREATE. + public static VariantValue create( + RoundingMode roundingMode, @Nullable Object object, RuntimeTypeInformation type) { + if (object == null) { + return new VariantSqlNull(type.getTypeName()); + } + return new VariantNonNull(roundingMode, object, type); + } +} diff --git a/core/src/main/java/org/apache/calcite/runtime/variant/VariantValue.java b/core/src/main/java/org/apache/calcite/runtime/variant/VariantValue.java new file mode 100644 index 000000000000..483b34db668b --- /dev/null +++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantValue.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.runtime.variant; + +import org.apache.calcite.runtime.rtti.RuntimeTypeInformation; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Base class for the runtime support for values of the VARIANT SQL type. */ +public abstract class VariantValue { + // We made this an abstract class rather than an interface, + // because Janino does not like static methods in interfaces. + public static @Nullable String getTypeString(Object object) { + if (object instanceof VariantValue) { + return ((VariantValue) object).getTypeString(); + } + return null; + } + + /** A string describing the runtime type information of this value. */ + public abstract String getTypeString(); + + /** 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. */ + public abstract @Nullable Object cast(RuntimeTypeInformation type); + + // Implementation of the array index operator for VARIANT values + public abstract @Nullable Object item(Object index); +} diff --git a/core/src/main/java/org/apache/calcite/runtime/variant/package-info.java b/core/src/main/java/org/apache/calcite/runtime/variant/package-info.java new file mode 100644 index 000000000000..f8910457bf72 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/runtime/variant/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. + */ + +/** + * Runtime support for values of the VARIANT data type. + */ +package org.apache.calcite.runtime.variant; 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 6aba1cdaa4fd..9e0ca8bb0ae3 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 @@ -1935,6 +1935,16 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable { SqlFunctionCategory.NUMERIC) .withSyntax(SqlSyntax.FUNCTION_ID_CONSTANT); + /** The {@code TYPEOF} function. */ + public static final SqlFunction TYPEOF = + SqlBasicFunction.create("TYPEOF", ReturnTypes.VARCHAR, OperandTypes.VARIANT, + SqlFunctionCategory.STRING); + + /** The {@code VARIANTNULL} function. */ + public static final SqlFunction VARIANTNULL = + SqlBasicFunction.create("VARIANTNULL", ReturnTypes.VARIANT, OperandTypes.NILADIC, + SqlFunctionCategory.SYSTEM); + /** {@code FIRST} function to be used within {@code MATCH_RECOGNIZE}. */ public static final SqlFunction FIRST = SqlBasicFunction.create(SqlKind.FIRST, 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 5a82308e916e..6e129f588835 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 @@ -401,6 +401,9 @@ public static SqlOperandTypeChecker variadic( public static final SqlSingleOperandTypeChecker INTEGER_INTEGER = family(SqlTypeFamily.INTEGER, SqlTypeFamily.INTEGER); + public static final SqlSingleOperandTypeChecker VARIANT = + family(SqlTypeFamily.VARIANT); + public static final SqlSingleOperandTypeChecker NUMERIC_OPTIONAL_NUMERIC = family(ImmutableList.of(SqlTypeFamily.NUMERIC, SqlTypeFamily.NUMERIC), // Second operand optional (operand index 0, 1) diff --git a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java index 12849a60dbd6..c61aebd0b265 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java +++ b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java @@ -532,6 +532,12 @@ public static SqlCall stripSeparator(SqlCall call) { public static final SqlReturnTypeInference VARCHAR = ReturnTypes.explicit(SqlTypeName.VARCHAR); + /** + * Type-inference strategy that always returns "VARIANT". + */ + public static final SqlReturnTypeInference VARIANT = + ReturnTypes.explicit(SqlTypeName.VARIANT); + /** * Type-inference strategy that always returns "VARCHAR" with nulls * allowed if any of the operands allow nulls. diff --git a/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java b/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java index d7754b4801dd..f2a44602d109 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java @@ -60,7 +60,11 @@ class ConvertRexToChecked extends RexShuttle { @Override public RexNode visitSubQuery(RexSubQuery subQuery) { RelNode result = subQuery.rel.accept(ConvertToChecked.this); - return subQuery.clone(result); + if (result != subQuery.rel) { + return subQuery.clone(result); + } else { + return subQuery; + } } @Override public RexNode visitCall(final RexCall call) { 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 ab6e2c3ce000..6ea6701a65da 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -101,6 +101,10 @@ import org.apache.calcite.runtime.UrlFunctions; import org.apache.calcite.runtime.Utilities; import org.apache.calcite.runtime.XmlFunctions; +import org.apache.calcite.runtime.rtti.RuntimeTypeInformation; +import org.apache.calcite.runtime.variant.VariantNull; +import org.apache.calcite.runtime.variant.VariantSqlValue; +import org.apache.calcite.runtime.variant.VariantValue; import org.apache.calcite.schema.FilterableTable; import org.apache.calcite.schema.ModifiableTable; import org.apache.calcite.schema.ProjectableFilterableTable; @@ -116,7 +120,6 @@ 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; @@ -937,7 +940,12 @@ public enum BuiltInMethod { BIG_DECIMAL_ADD(BigDecimal.class, "add", BigDecimal.class), BIG_DECIMAL_NEGATE(BigDecimal.class, "negate"), COMPARE_TO(Comparable.class, "compareTo", Object.class), - VARIANT_CAST(Variant.class, "cast", Object.class, RuntimeTypeInformation.class); + VARIANT_CREATE(VariantSqlValue.class, "create", RoundingMode.class, + Object.class, RuntimeTypeInformation.class), + VARIANT_CAST(VariantValue.class, "cast", RuntimeTypeInformation.class), + TYPEOF(VariantValue.class, "getTypeString", VariantValue.class), + VARIANT_ITEM(SqlFunctions.class, "item", VariantValue.class, Object.class), + VARIANTNULL(VariantNull.class, "getInstance"); @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 deleted file mode 100644 index 05c91cf18618..000000000000 --- a/core/src/main/java/org/apache/calcite/util/Variant.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * 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/test/resources/sql/variant.iq b/core/src/test/resources/sql/variant.iq new file mode 100644 index 000000000000..486e77514dab --- /dev/null +++ b/core/src/test/resources/sql/variant.iq @@ -0,0 +1,197 @@ +# variant.iq - VARIANT type examples +# +# 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. +# + +# We don't really need scott, but we need to have a connection +!use scott +!set outputformat csv + +SELECT CAST(1 AS VARIANT) as C; +C +1 +!ok + +SELECT TYPEOF(CAST(1 AS VARIANT)) AS C; +C +INTEGER +!ok + +SELECT CAST(CAST(1 AS TINYINT) AS VARIANT) AS C; +C +1 +!ok + +# The runtime knows that this is a TINYINT +SELECT TYPEOF(CAST(CAST(1 AS TINYINT) AS VARIANT)) AS C; +C +TINYINT +!ok + +# Converting something to VARIANT and back works +SELECT CAST(CAST(1 AS VARIANT) AS INT) AS C; +C +1 +!ok + +# Variant converts between numeric types +SELECT CAST(CAST(1 AS VARIANT) AS TINYINT) AS C; +C +1 +!ok + +# Some VARIANT objects when output receive double quotes +select CAST('string' as VARIANT) as C; +C +"string" +!ok + +# CHAR(3) values are represented as VARCHAR in variants +SELECT CAST(CAST('abc' AS VARIANT) AS VARCHAR) AS C; +C +abc +!ok + +# VARCHAR and CHAR(N) have the same underlying runtime type +SELECT CAST(CAST('abc' AS VARIANT) AS CHAR(3)) AS C; +C +abc +!ok + +# The value representing a VARIANT null value (think of a JSON null) +SELECT VARIANTNULL() AS C; +C +null +!ok + +# VARIANT null is not the same as SQL NULL +SELECT VARIANTNULL() IS NULL AS C; +C +false +!ok + +# Two VARIANT nulls are equal, unlike SQL NULL +SELECT VARIANTNULL() = VARIANTNULL() AS C; +C +true +!ok + +SELECT TYPEOF(VARIANTNULL()) AS C; +C +VARIANT +!ok + +# Variants delegate equality to the underlying values +SELECT CAST(1 AS VARIANT) = CAST(1 AS VARIANT) AS C; +C +true +!ok + +# To be equal two variants must have the same value and the same runtime type +SELECT CAST(1 AS VARIANT) = CAST(CAST(1 AS TINYINT) AS VARIANT) AS C; +C +false +!ok + +# An array of variant values can have values with any underlying type +SELECT ARRAY[CAST(1 AS VARIANT), CAST('abc' AS VARIANT)] AS C; +C +[1, "abc"] +!ok + +# A map with VARCHAR keys and VARIANT values +SELECT MAP['a', CAST(1 AS VARIANT), 'b', CAST('abc' AS VARIANT), 'c', CAST(ARRAY[1,2,3] AS VARIANT)] AS C; +C +{a=1, b="abc", c=[1, 2, 3]} +!ok + +# Variant values allow access by index, but return null if they are not arrays +SELECT (CAST(1 AS VARIANT))[1] AS C; +C +null +!ok + +SELECT CAST(ARRAY[1,2,3] AS VARIANT)[1] AS C; +C +1 +!ok + +# Acessing items in a VARIANT array returns VARIANT values, +# even if the array itself does not contain VARIANT values +# (Otherwise TYPEOF would not compile) +SELECT TYPEOF(CAST(ARRAY[1,2,3] AS VARIANT)[1]) AS C; +C +INTEGER +!ok + +# One can access fields by name in a VARIANT, even if the +# variant does not have named fields +SELECT CAST(ARRAY[1,2,3] AS VARIANT)['name'] AS C; +C +null +!ok + +# One can access fields by name in a VARIANT, even if the +# variant does not have named fields +SELECT CAST(ARRAY[1,2,3] AS VARIANT)."name" AS C; +C +null +!ok + +# One can access fields by index in a VARIANT +SELECT CAST(Map[1,'a',2,'b',3,'c'] AS VARIANT)[1] AS C; +C +"a" +!ok + +SELECT TYPEOF(CAST(Map[1,'a',2,'b',3,'c'] AS VARIANT)[1]) AS C; +C +VARCHAR +!ok + +# Note that field name is quoted to match the case of the key +SELECT CAST(Map['a',1,'b',2,'c',3] AS VARIANT)."a" AS C; +C +1 +!ok + +# Unquoted field may not match, depending on dialect +SELECT CAST(Map['a',1,'b',2,'c',3] AS VARIANT).a AS C; +C +null +!ok + +# The safest way is to use an index +SELECT CAST(Map['a',1,'b',2,'c',3] AS VARIANT)['a'] AS C; +C +1 +!ok + +# Maps can have variant keys too +# (but you have to index with a variant). +SELECT (Map[CAST('a' AS VARIANT), 1, CAST(1 AS VARIANT), 2])[CAST(1 AS VARIANT)] as C; +C +2 +!ok + +# Navigating a JSON-like object +SELECT CAST(MAP['a', CAST(1 AS VARIANT), 'b', CAST('abc' AS VARIANT), 'c', CAST(ARRAY[1,2,3] AS VARIANT)] + ['c'][1] AS INTEGER) AS C; +C +1 +!ok + +# End variant.iq diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 8795333b58c6..e48d6afc6b28 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -1259,15 +1259,16 @@ Any such value holds at runtime two pieces of information: 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`. +will compare the runtime type with T. If the types are identical or the types are +numeric and there is a natural conversion between the two types, the +original value is converted to the target type and 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` + 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 @@ -1279,6 +1280,24 @@ also offer the following operations: is subject to the capitalization rules of the SQL dialect, so for correct operation the field may need to be quoted: `variant."field"` +The runtime types do not need to match exactly the compile-time types. +As a compiler front-end, Calcite does not mandate exactly how the runtime types +are represented. Calcite does include one particular implementation in +Java runtime, which is used for testing. In this representation +the runtime types are represented as follows: + +- The scalar types do not include information about precision and scale. Thus all `DECIMAL` + compile-time types are represented by a single run-time type. +- `CHAR(N)` and `VARCHAR` are both represented by a single runtime `VARCHAR` type. +- `BINARY(N)` and `VARBINARY` are both represented by a single runtime `VARBINARY` type. +- `FLOAT` and `DOUBLE` are both represented by the same runtime type. +- All "short interval" types (from days to seconds) are represented by a single type. +- All "long interval" types (from years to months) are represented by a single type. +- Generic types such as `INT ARRAY`, `MULTISET`, and `MAP` convert all their elements to VARIANT values + +The function VARIANTNULL() can be used to create an instance +of the `VARIANT` `null` value. + ### Spatial types Spatial data is represented as character strings encoded as @@ -1542,6 +1561,8 @@ Algorithms for implicit conversion are subject to change across Calcite releases | CONVERT(string, charSet1, charSet2) | Converts *string* from *charSet1* to *charSet2* | CONVERT(value USING transcodingName) | Alter *value* from one base character set to *transcodingName* | TRANSLATE(value USING transcodingName) | Alter *value* from one base character set to *transcodingName* +| TYPEOF(variant) | Returns a string that describes the runtime type of *variant*, where variant has a `VARIANT` type +| VARIANTNULL() | Returns an instance of the `VARIANT` null value (constructor) Converting a string to a **BINARY** or **VARBINARY** type produces the list of bytes of the string's encoding in the strings' charset. A 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 a75cf71c1e22..0bb72cff54f1 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -1804,14 +1804,15 @@ void testCastToBoolean(CastType castType, SqlOperatorFixture f) { /** Test cases for * - * [CALCITE-4918] Add a VARIANT data type. */ + * [CALCITE-4918] Add a VARIANT data type. */ @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(MULTISET[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"); @@ -1824,8 +1825,7 @@ void testCastToBoolean(CastType castType, SqlOperatorFixture f) { "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 VARCHAR)", "abc", "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 @@ -1861,7 +1861,7 @@ void testCastToBoolean(CastType castType, SqlOperatorFixture f) { + "'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]}", + "{a=1, b=[{\"c\"=2.3}, 5]}", "(CHAR(1) NOT NULL, VARIANT NOT NULL) MAP NOT NULL"); }