This is an automated email from the ASF dual-hosted git repository.

mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 53b11f02cd [CALCITE-7677] CAST between ROW types fails at runtime
53b11f02cd is described below

commit 53b11f02cdec4337f44c5197f15a10e9c2cf1326
Author: Mihai Budiu <[email protected]>
AuthorDate: Mon Jul 27 17:53:02 2026 -0700

    [CALCITE-7677] CAST between ROW types fails at runtime
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../adapter/enumerable/RexToLixTranslator.java     | 78 ++++++++++++++--------
 .../apache/calcite/jdbc/JavaTypeFactoryImpl.java   | 28 +++++++-
 core/src/test/resources/sql/cast.iq                | 48 +++++++++++++
 3 files changed, 124 insertions(+), 30 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
index a39146ac75..9bad3dc3f5 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
@@ -351,6 +351,54 @@ private static boolean valueIsAlwaysNull(RelDataType type) 
{
     return typeName == SqlTypeName.UNKNOWN || typeName == SqlTypeName.NULL;
   }
 
+  /** Converts a ROW value to another ROW type, field by field. */
+  private Expression getRowConvertExpression(
+      RelDataType sourceType,
+      RelDataType targetType,
+      Expression operand,
+      ConstantExpression format) {
+    if (valueIsAlwaysNull(sourceType)) {
+      return Expressions.constant(null);
+    }
+    assert sourceType.getSqlTypeName() == SqlTypeName.ROW;
+    List<RelDataTypeField> targetTypes = targetType.getFieldList();
+    List<RelDataTypeField> sourceTypes = sourceType.getFieldList();
+    assert targetTypes.size() == sourceTypes.size();
+    List<Expression> 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,
@@ -376,35 +424,7 @@ private Expression getConvertExpression(
     }
 
     if (targetType.getSqlTypeName() == SqlTypeName.ROW) {
-      if (valueIsAlwaysNull(sourceType)) {
-        return Expressions.constant(null);
-      }
-      assert sourceType.getSqlTypeName() == SqlTypeName.ROW;
-      List<RelDataTypeField> targetTypes = targetType.getFieldList();
-      List<RelDataTypeField> sourceTypes = sourceType.getFieldList();
-      assert targetTypes.size() == sourceTypes.size();
-      List<Expression> 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 3f276b41e6..58882f6a1b 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 a0ef448854..ce7b13b8b9 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

Reply via email to