Skip to content

Commit

Permalink
[CALCITE-4918] Add a VARIANT data type - runtime support
Browse files Browse the repository at this point in the history
Signed-off-by: Mihai Budiu <mbudiu@feldera.com>
  • Loading branch information
mihaibudiu committed Dec 17, 2024
1 parent bd3d854 commit 2fefd69
Show file tree
Hide file tree
Showing 22 changed files with 856 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
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;
Expand Down Expand Up @@ -313,7 +315,25 @@ private Expression getConvertExpression(
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;

Expand Down
7 changes: 6 additions & 1 deletion core/src/main/java/org/apache/calcite/rex/RexBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ public SqlStdOperatorTable getOpTab() {
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) {
Expand Down Expand Up @@ -855,7 +859,8 @@ boolean canRemoveCastFromLiteral(RelDataType toType,
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)) {
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/java/org/apache/calcite/rex/RexLiteral.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
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;

Expand Down Expand Up @@ -315,6 +316,8 @@ public static boolean valueMatchesType(
return true;
}
switch (typeName) {
case VARIANT:
return value instanceof Variant;
case BOOLEAN:
// Unlike SqlLiteral, we do not allow boolean null.
return value instanceof Boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
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;
Expand Down Expand Up @@ -5766,6 +5767,9 @@ 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 Map) {
return mapItem((Map) object, index);
}
Expand Down
19 changes: 13 additions & 6 deletions core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,19 @@ private static RelDataType createTypeWithNullabilityFromExpr(RelDataTypeFactory
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();
Expand Down Expand Up @@ -190,12 +203,6 @@ private static RelDataType createTypeWithNullabilityFromExpr(RelDataTypeFactory
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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ private static SqlSingleOperandTypeChecker getChecker(SqlCallBinding callBinding
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>)]";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
coerceRules.add(exactType,
coerceRules.copyValues(exactType)
.addAll(SqlTypeName.INTERVAL_TYPES)
.add(SqlTypeName.VARIANT)
.build());
}

Expand All @@ -153,31 +152,27 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
.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)
Expand All @@ -192,17 +187,10 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
.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)
Expand All @@ -220,7 +208,6 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
// 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)
Expand All @@ -232,7 +219,6 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
// 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)
Expand All @@ -244,7 +230,6 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
// 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)
Expand All @@ -258,7 +243,6 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
// 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)
Expand All @@ -273,7 +257,6 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
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)
Expand All @@ -286,7 +269,6 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
// 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)
Expand All @@ -302,7 +284,6 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
// 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)
Expand All @@ -318,7 +299,6 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
// 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)
Expand All @@ -334,7 +314,6 @@ private SqlTypeCoercionRule(Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
// GEOMETRY is castable from ...
coerceRules.add(SqlTypeName.GEOMETRY,
coerceRules.copyValues(SqlTypeName.GEOMETRY)
.add(SqlTypeName.VARIANT)
.addAll(SqlTypeName.CHAR_TYPES)
.build());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,10 @@ private static boolean isAny(RelDataType t) {
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;
}
Expand Down Expand Up @@ -895,7 +899,7 @@ public static boolean canCastFrom(
return canCastFrom(toType, requireNonNull(fromType.getMeasureElementType()),
typeMappingRule);
}
if (isAny(toType) || isAny(fromType)) {
if (isAny(toType) || isAny(fromType) || isVariant(toType) || isVariant(fromType)) {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
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;

Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 2fefd69

Please sign in to comment.