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 5a1e0fee2082..9862620170f5 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 @@ -341,6 +341,54 @@ Expression translateCast( return expressionHandlingSafe(convert3, safe, targetType); } + /** Converts a ROW value to another ROW type, field by field. */ + private Expression getRowConvertExpression( + RelDataType sourceType, + RelDataType targetType, + Expression operand, + ConstantExpression format) { + if (sourceType.getSqlTypeName() == SqlTypeName.NULL) { + return Expressions.constant(null); + } + assert sourceType.getSqlTypeName() == SqlTypeName.ROW; + List targetTypes = targetType.getFieldList(); + List sourceTypes = sourceType.getFieldList(); + assert targetTypes.size() == sourceTypes.size(); + List fields = new ArrayList<>(); + for (int i = 0; i < targetTypes.size(); i++) { + RelDataTypeField targetField = targetTypes.get(i); + RelDataTypeField sourceField = sourceTypes.get(i); + Expression field = Expressions.arrayIndex(operand, Expressions.constant(i)); + // In the generated Java code 'field' is an Object, + // we need to also cast it to the correct type to enable correct method dispatch in Java. + // We force the type to be nullable; this way, instead of (int) we get (Integer). + // Casting an object to an int is not legal. + RelDataType nullableSourceFieldType = + typeFactory.createTypeWithNullability(sourceField.getType(), true); + Type javaType = typeFactory.getJavaClass(nullableSourceFieldType); + if (nullableSourceFieldType.isStruct()) { + // A struct field is represented as Object[] at runtime; + // the recursive conversion below indexes into the field, which + // requires an array-typed operand. + field = Expressions.convert_(field, Object[].class); + } else if (!javaType.getTypeName().equals("java.lang.Void")) { + // Cannot cast to Void - this is the type of NULL literals. + field = Expressions.convert_(field, javaType); + } + Expression convert = + getConvertExpression(sourceField.getType(), targetField.getType(), field, format); + if (sourceField.getType().isNullable()) { + // field == null ? field : convert + convert = + Expressions.condition( + Expressions.equal(field, Expressions.constant(null)), + Expressions.constant(null), convert); + } + fields.add(convert); + } + return Expressions.call(BuiltInMethod.ARRAY.method, fields); + } + private Expression getConvertExpression( RelDataType sourceType, RelDataType targetType, @@ -366,32 +414,7 @@ private Expression getConvertExpression( } if (targetType.getSqlTypeName() == SqlTypeName.ROW) { - assert sourceType.getSqlTypeName() == SqlTypeName.ROW; - List targetTypes = targetType.getFieldList(); - List sourceTypes = sourceType.getFieldList(); - assert targetTypes.size() == sourceTypes.size(); - List fields = new ArrayList<>(); - for (int i = 0; i < targetTypes.size(); i++) { - RelDataTypeField targetField = targetTypes.get(i); - RelDataTypeField sourceField = sourceTypes.get(i); - Expression field = Expressions.arrayIndex(operand, Expressions.constant(i)); - // In the generated Java code 'field' is an Object, - // we need to also cast it to the correct type to enable correct method dispatch in Java. - // We force the type to be nullable; this way, instead of (int) we get (Integer). - // Casting an object ot an int is not legal. - RelDataType nullableSourceFieldType = - typeFactory.createTypeWithNullability(sourceField.getType(), true); - Type javaType = typeFactory.getJavaClass(nullableSourceFieldType); - if (!javaType.getTypeName().equals("java.lang.Void") - && !nullableSourceFieldType.isStruct()) { - // Cannot cast to Void - this is the type of NULL literals. - field = Expressions.convert_(field, javaType); - } - Expression convert = - getConvertExpression(sourceField.getType(), targetField.getType(), field, format); - fields.add(convert); - } - return Expressions.call(BuiltInMethod.ARRAY.method, fields); + return getRowConvertExpression(sourceType, targetType, operand, format); } switch (targetType.getSqlTypeName()) { diff --git a/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java b/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java index 3f276b41e6b2..58882f6a1b2d 100644 --- a/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java +++ b/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java @@ -365,7 +365,33 @@ private Type createSyntheticType(RelRecordType type) { final SyntheticRecordType syntheticType = new SyntheticRecordType(type, name); for (final RelDataTypeField recordField : type.getFieldList()) { - final Type javaClass = getJavaClass(recordField.getType()); + final Type fieldClass = getJavaClass(recordField.getType()); + // A field whose type has no real Java class is stored as Object[] at + // runtime, like all rows in enumerable convention. For example, the + // element type of ARRAY[ROW(ROW(1, 'a'), 10), NULL] becomes a "synthetic" type + // named Record2_0 + // public static class Record2_0 implements java.io.Serializable { + // public Object[] EXPR$0; // the nested row, e.g. {1, 'a'} + // public Integer EXPR$1; + // ...equals, hashCode, compareTo, toString... + // } + // If EXPR$0 would also have a synthetic type, + // this would generate nested synthetic classes, which + // EnumerableRelImplementor#classDecl cannot emit. + // + // A field whose type maps to a real Java class (e.g. a bean from a + // ReflectiveSchema) uses its own Java class. + // + // 'instanceof Class' distinguishes the two cases: getJavaClass + // returns a java.lang.reflect.Type, which is a loaded + // java.lang.Class for most SQL types. For a record type with no + // Java class (here the nested row's type, which maps to its own + // Record2_N), it is a SyntheticRecordType: a description of a class + // that is only generated and compiled together with the query, so no + // Class object exists for it. + final Type javaClass = fieldClass instanceof Class + ? fieldClass + : Object[].class; syntheticType.fields.add( new RecordFieldImpl( syntheticType, diff --git a/core/src/test/resources/sql/cast.iq b/core/src/test/resources/sql/cast.iq index a0ef44885409..ce7b13b8b9f1 100644 --- a/core/src/test/resources/sql/cast.iq +++ b/core/src/test/resources/sql/cast.iq @@ -2031,4 +2031,52 @@ values (cast(multiset[null] as integer multiset)); !ok +# Tests for [CALCITE-7677] CAST between ROW types fails at runtime +# https://issues.apache.org/jira/browse/CALCITE-7677 +!use scott + +SELECT ARRAY[ROW(1, 'Alice'), ROW(NULL, 'Dan')] AS people +FROM (VALUES (0)) AS t(zero); ++---------------------------+ +| PEOPLE | ++---------------------------+ +| [{1, Alice}, {null, Dan}] | ++---------------------------+ +(1 row) + +!ok + +SELECT CAST(ROW(ROW(2, 'b'), 20) AS ROW(a ROW(x INTEGER, y CHAR(1)), b INTEGER)) AS r +FROM (VALUES (0)) AS t(zero); ++--------------+ +| R | ++--------------+ +| {{2, b}, 20} | ++--------------+ +(1 row) + +!ok + +SELECT CAST(ROW(NULL, 30) AS ROW(a ROW(x INTEGER, y CHAR(1)), b INTEGER)) AS r +FROM (VALUES (0)) AS t(zero); ++------------+ +| R | ++------------+ +| {null, 30} | ++------------+ +(1 row) + +!ok + +SELECT ARRAY[ROW(ROW(1, 'a'), 10), NULL] AS xs +FROM (VALUES (0)) AS t(zero); ++----------------------+ +| XS | ++----------------------+ +| [{{1, a}, 10}, null] | ++----------------------+ +(1 row) + +!ok + # End cast.iq