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