diff --git a/core/src/main/java/org/apache/calcite/runtime/rtti/GenericSqlTypeRtti.java b/core/src/main/java/org/apache/calcite/runtime/rtti/GenericSqlTypeRtti.java index 923537d8ac09..7c8893b3a862 100644 --- a/core/src/main/java/org/apache/calcite/runtime/rtti/GenericSqlTypeRtti.java +++ b/core/src/main/java/org/apache/calcite/runtime/rtti/GenericSqlTypeRtti.java @@ -74,6 +74,10 @@ public GenericSqlTypeRtti(RuntimeSqlTypeName typeName, RuntimeTypeInformation... } public RuntimeTypeInformation getTypeArgument(int index) { - return this.typeArguments[index]; + return typeArguments[index]; + } + + public int getArgumentCount() { + return typeArguments.length; } } diff --git a/core/src/main/java/org/apache/calcite/runtime/rtti/RowSqlTypeRtti.java b/core/src/main/java/org/apache/calcite/runtime/rtti/RowSqlTypeRtti.java index 23259436dd3a..035c7aeb0294 100644 --- a/core/src/main/java/org/apache/calcite/runtime/rtti/RowSqlTypeRtti.java +++ b/core/src/main/java/org/apache/calcite/runtime/rtti/RowSqlTypeRtti.java @@ -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,11 +61,20 @@ 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, @@ -76,13 +85,13 @@ public RowSqlTypeRtti(Map.Entry... fieldNames) { public @Nullable RuntimeTypeInformation getFieldType(Object index) { if (index instanceof Integer) { int intIndex = (Integer) index; - if (intIndex < 0 || intIndex >= this.fieldNames.length) { + if (intIndex < 0 || intIndex >= this.fields.length) { return null; } - return this.fieldNames[intIndex].getValue(); + return this.fields[intIndex].getValue(); } else if (index instanceof String) { String stringIndex = (String) index; - for (Map.Entry field : this.fieldNames) { + for (Map.Entry field : this.fields) { if (field.getKey().equalsIgnoreCase(stringIndex)) { return field.getValue(); } diff --git a/core/src/main/java/org/apache/calcite/runtime/rtti/RuntimeTypeInformation.java b/core/src/main/java/org/apache/calcite/runtime/rtti/RuntimeTypeInformation.java index 08abf4d23e82..bdd811e966c0 100644 --- a/core/src/main/java/org/apache/calcite/runtime/rtti/RuntimeTypeInformation.java +++ b/core/src/main/java/org/apache/calcite/runtime/rtti/RuntimeTypeInformation.java @@ -56,6 +56,8 @@ public enum RuntimeSqlTypeName { TIMESTAMP_TZ(false), INTERVAL_LONG(false), INTERVAL_SHORT(false), + // "Name" is used for structure field names + NAME(false), // CHAR is represented as VARCHAR VARCHAR(false), // BINARY is represented as VARBINARY @@ -118,6 +120,11 @@ public boolean isScalar() { } } + public GenericSqlTypeRtti asGeneric() { + assert this instanceof GenericSqlTypeRtti; + return (GenericSqlTypeRtti) this; + } + /** * Creates and returns an expression that creates a runtime type that * reflects the information in the statically-known type 'type'. 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 index a4c471e7a2aa..9d1c0f678da6 100644 --- a/core/src/main/java/org/apache/calcite/runtime/variant/VariantNonNull.java +++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantNonNull.java @@ -18,7 +18,7 @@ import org.apache.calcite.linq4j.tree.Primitive; import org.apache.calcite.runtime.SqlFunctions; -import org.apache.calcite.runtime.rtti.GenericSqlTypeRtti; +import org.apache.calcite.runtime.rtti.BasicSqlTypeRtti; import org.apache.calcite.runtime.rtti.RowSqlTypeRtti; import org.apache.calcite.runtime.rtti.RuntimeTypeInformation; @@ -26,8 +26,14 @@ 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. */ @@ -37,34 +43,45 @@ public class VariantNonNull extends VariantSqlValue { final Object value; VariantNonNull(RoundingMode roundingMode, Object value, RuntimeTypeInformation runtimeType) { - super(runtimeType); + super(runtimeType.getTypeName()); this.roundingMode = roundingMode; - this.value = value; // 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: @@ -75,20 +92,64 @@ public class VariantNonNull extends VariantSqlValue { case TIMESTAMP_TZ: case INTERVAL_LONG: case INTERVAL_SHORT: + this.value = value; break; case VARCHAR: + this.value = value; assert value instanceof String; break; - case VARBINARY: case NULL: - case MULTISET: - case ARRAY: - case MAP: - case ROW: + 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) { @@ -101,7 +162,7 @@ public class VariantNonNull extends VariantSqlValue { VariantNonNull variant = (VariantNonNull) o; return Objects.equals(value, variant.value) - && runtimeType.equals(variant.runtimeType); + && runtimeType == variant.runtimeType; } @Override public int hashCode() { @@ -116,12 +177,12 @@ public class VariantNonNull extends VariantSqlValue { // This method is invoked from {@link RexToLixTranslator} VARIANT_CAST @Override public @Nullable Object cast(RuntimeTypeInformation type) { if (this.runtimeType.isScalar()) { - if (this.runtimeType.equals(type)) { + if (this.runtimeType == type.getTypeName()) { return this.value; } else { // Convert numeric values @Nullable Primitive target = type.asPrimitive(); - switch (this.runtimeType.getTypeName()) { + switch (this.runtimeType) { case TINYINT: { byte b = (byte) value; switch (type.getTypeName()) { @@ -247,42 +308,90 @@ public class VariantNonNull extends VariantSqlValue { return null; } } else { - // Derived type: ARRAY, MAP, etc. - if (this.runtimeType.equals(type)) { - return this.value; + 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 result = new ArrayList<>(list.size()); + for (VariantSqlValue o : list) { + 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 result = new LinkedHashMap<>(map.size()); + for (Map.Entry e : map.entrySet()) { + Object key = e.getKey().cast(keyType); + 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; + 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; } - // TODO: allow casts that change some of the generic arguments only } return null; } // Implementation of the array index operator for VARIANT values @Override public @Nullable VariantValue item(Object index) { - @Nullable RuntimeTypeInformation fieldType; boolean isInteger = index instanceof Integer; - switch (this.runtimeType.getTypeName()) { + switch (this.runtimeType) { case ROW: - // The type of the field - fieldType = ((RowSqlTypeRtti) this.runtimeType).getFieldType(index); + 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; } - // The type of the elements - fieldType = ((GenericSqlTypeRtti) this.runtimeType).getTypeArgument(0); - break; - case MAP: - // The type of the values - fieldType = ((GenericSqlTypeRtti) this.runtimeType).getTypeArgument(1); break; default: return null; } - if (fieldType == null) { - return null; - } + // If index is VARIANT, leave it unchanged Object result = SqlFunctions.itemOptional(this.value, index); if (result == null) { return null; @@ -291,31 +400,31 @@ public class VariantNonNull extends VariantSqlValue { if (result instanceof VariantValue) { return (VariantValue) result; } - // Otherwise pack the result in a Variant - return VariantSqlValue.create(roundingMode, result, fieldType); + return null; } // This method is called by the testing code. @Override public String toString() { - if (this.runtimeType.getTypeName() == RuntimeTypeInformation.RuntimeSqlTypeName.ROW) { - if (value instanceof Object[]) { - Object[] array = (Object []) value; + 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 (Object o : array) { + for (Map.Entry o : map.entrySet()) { if (!first) { buf.append(", "); } first = false; - buf.append(o.toString()); + buf.append(o.getValue().toString()); } buf.append("}"); return buf.toString(); } } String quote = ""; - switch (this.runtimeType.getTypeName()) { + switch (this.runtimeType) { case TIME: case TIME_WITH_LOCAL_TIME_ZONE: case TIME_TZ: 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 index cb3b12fc8544..85d424461dc3 100644 --- a/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlNull.java +++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlNull.java @@ -24,7 +24,7 @@ /** A VARIANT value that contains a NULL runtime value. */ public class VariantSqlNull extends VariantSqlValue { - VariantSqlNull(RuntimeTypeInformation runtimeType) { + VariantSqlNull(RuntimeTypeInformation.RuntimeSqlTypeName runtimeType) { super(runtimeType); } @@ -55,6 +55,6 @@ public class VariantSqlNull extends VariantSqlValue { } VariantSqlNull variant = (VariantSqlNull) o; - return runtimeType.equals(variant.runtimeType); + 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 index 2c8c38b3f27f..36dc86a26da1 100644 --- a/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlValue.java +++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlValue.java @@ -26,15 +26,14 @@ * (The VARIANT type also has a null value which is different * from any other SQL value). */ public abstract class VariantSqlValue extends VariantValue { - /** Type of the value. */ - final RuntimeTypeInformation runtimeType; + final RuntimeTypeInformation.RuntimeSqlTypeName runtimeType; - protected VariantSqlValue(RuntimeTypeInformation runtimeType) { + protected VariantSqlValue(RuntimeTypeInformation.RuntimeSqlTypeName runtimeType) { this.runtimeType = runtimeType; } @Override public String getTypeString() { - return this.runtimeType.getTypeString(); + return this.runtimeType.toString(); } /** @@ -52,7 +51,7 @@ protected VariantSqlValue(RuntimeTypeInformation runtimeType) { public static VariantValue create( RoundingMode roundingMode, @Nullable Object object, RuntimeTypeInformation type) { if (object == null) { - return new VariantSqlNull(type); + return new VariantSqlNull(type.getTypeName()); } return new VariantNonNull(roundingMode, object, type); } diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 53739a88c7f7..c11854355326 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -1293,9 +1293,7 @@ the runtime types are represented as follows: - `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` do carry runtime - information about the element types -- The `ROW` type does have information about all field types +- 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. 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 e751ab80d8ab..4e033ee393da 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -1795,7 +1795,8 @@ void testCastToBoolean(CastType castType, SqlOperatorFixture f) { // 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"); @@ -1844,7 +1845,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"); }