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

mbudiu 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 506950a3eb [CALCITE-7358] Casts involving MAP and ROW types cause 
compile-time exceptions
506950a3eb is described below

commit 506950a3ebd4807b36901bededc64f7c60497712
Author: Mihai Budiu <[email protected]>
AuthorDate: Thu Jan 8 17:42:47 2026 -0800

    [CALCITE-7358] Casts involving MAP and ROW types cause compile-time 
exceptions
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../apache/calcite/sql/fun/SqlItemOperator.java    | 20 +++++++++++++
 .../calcite/sql/type/SqlTypeCoercionRule.java      |  2 ++
 .../org/apache/calcite/sql/type/SqlTypeUtil.java   | 30 +++++++++++++++++++
 .../calcite/sql/type/SqlTypeFactoryTest.java       |  3 +-
 server/src/test/resources/sql/type.iq              | 35 ++++++++++++++++++++++
 .../java/org/apache/calcite/test/QuidemTest.java   | 22 ++++++++++++++
 6 files changed, 111 insertions(+), 1 deletion(-)

diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
index 2285b0804a..5b21c8560e 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
@@ -24,6 +24,7 @@
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlOperandCountRange;
+import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlOperatorBinding;
 import org.apache.calcite.sql.SqlSpecialOperator;
 import org.apache.calcite.sql.SqlWriter;
@@ -163,6 +164,25 @@ private static SqlSingleOperandTypeChecker 
getChecker(SqlCallBinding callBinding
       if (sqlTypeName == SqlTypeName.VARIANT) {
         // Allow any key type to be used when the map keys have a VARIANT type
         return OperandTypes.family(SqlTypeFamily.ANY);
+      } else if (sqlTypeName == SqlTypeName.ROW) {
+        // Check that the type of the argument is exactly the key type
+        return new SqlSingleOperandTypeChecker() {
+          @Override public boolean checkSingleOperandType(
+              SqlCallBinding callBinding, SqlNode operand,
+              int iFormalOperand, boolean throwOnFailure) {
+            // operand 0 of ITEM is the indexed object, operand 1 is the key 
value
+            RelDataType operandType = callBinding.getOperandType(1);
+            boolean match = operandType.equals(keyType);
+            if (!match && throwOnFailure) {
+              throw callBinding.newValidationSignatureError();
+            }
+            return match;
+          }
+
+          @Override public String getAllowedSignatures(SqlOperator op, String 
opName) {
+            return "[" + keyType.getSqlTypeName() + "]";
+          }
+        };
       }
       return OperandTypes.family(
           requireNonNull(sqlTypeName.getFamily(),
diff --git 
a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java 
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
index c3ba413bf3..59fb82fd19 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
@@ -173,6 +173,7 @@ private SqlTypeCoercionRule(Map<SqlTypeName, 
ImmutableSet<SqlTypeName>> map) {
             .add(SqlTypeName.VARBINARY)
             .addAll(SqlTypeName.CHAR_TYPES)
             .add(SqlTypeName.UUID)
+            .addAll(SqlTypeName.INT_TYPES)
             .build());
 
     // VARBINARY is castable from BINARY, CHARACTERS.
@@ -181,6 +182,7 @@ private SqlTypeCoercionRule(Map<SqlTypeName, 
ImmutableSet<SqlTypeName>> map) {
             .add(SqlTypeName.BINARY)
             .addAll(SqlTypeName.CHAR_TYPES)
             .add(SqlTypeName.UUID)
+            .addAll(SqlTypeName.INT_TYPES)
             .build());
 
     // VARCHAR is castable from BOOLEAN, DATE, TIME, TIMESTAMP, numeric types, 
binary, uuid, and
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java 
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
index 2808c89514..1be36fa18b 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
@@ -1120,6 +1120,36 @@ public static boolean canCastFrom(
           || fromType.getSqlTypeName() == SqlTypeName.UUID
           || fromType.getFamily() == SqlTypeFamily.CHARACTER
           || fromType.getFamily() == SqlTypeFamily.BINARY;
+    } else if (toType.getSqlTypeName() == SqlTypeName.ARRAY) {
+      if (fromType.getSqlTypeName() == SqlTypeName.ARRAY
+          || fromType.getSqlTypeName() == SqlTypeName.MULTISET) {
+        return canCastFrom(
+            requireNonNull(toType.getComponentType(), "componentType"),
+            requireNonNull(fromType.getComponentType(), "componentType"),
+            typeMappingRule);
+      } else if (fromType.getFamily() == SqlTypeFamily.CHARACTER
+          || fromType.getSqlTypeName() == SqlTypeName.NULL) {
+        // Cast from NULL or string to array is legal
+        return true;
+      }
+      return false;
+    } else if (toType.getSqlTypeName() == SqlTypeName.MAP) {
+      if (fromType.getSqlTypeName() == SqlTypeName.MAP) {
+        // It is not clear whether this is sufficient, but it is clearly 
necessary
+        return canCastFrom(
+            requireNonNull(toType.getKeyType(), "keyType"),
+            requireNonNull(fromType.getKeyType(), "keyType"),
+            typeMappingRule)
+            && canCastFrom(
+            requireNonNull(toType.getValueType(), "valueType"),
+            requireNonNull(fromType.getValueType(), "valueType"),
+            typeMappingRule);
+      } else if (fromType.getFamily() == SqlTypeFamily.CHARACTER
+          || fromType.getSqlTypeName() == SqlTypeName.NULL) {
+        // Cast from NULL or string to map is legal
+        return true;
+      }
+      return false;
     }
     if (toType.isStruct() || fromType.isStruct()) {
       if (toTypeName == SqlTypeName.DISTINCT) {
diff --git 
a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java 
b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java
index 09731bf3a5..8f5e5e4018 100644
--- a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java
@@ -99,7 +99,8 @@ class SqlTypeFactoryTest {
     RelDataType leastRestrictive =
         f.typeFactory.leastRestrictive(
             Lists.newArrayList(f.arraySqlChar10, f.sqlChar));
-    assertNull(leastRestrictive);
+    // Some SQL dialects, like Postgres, allow casts between strings and arrays
+    assertThat(leastRestrictive, is(f.arraySqlChar10));
   }
 
   @Test void testLeastRestrictiveForArrays() {
diff --git a/server/src/test/resources/sql/type.iq 
b/server/src/test/resources/sql/type.iq
index a63b8c58c1..160f5769a5 100644
--- a/server/src/test/resources/sql/type.iq
+++ b/server/src/test/resources/sql/type.iq
@@ -18,6 +18,7 @@
 !use server
 !set outputformat mysql
 
+# Test case for [CALCITE-7363] Improve error message for ASOF JOIN
 CREATE TABLE asof_tbl(intt INT, arr VARCHAR ARRAY );
 (0 rows modified)
 
@@ -34,6 +35,40 @@ ON t1.arr[2] = t2.arr[2]": From line 4, column 4 to line 4, 
column 24: ASOF JOIN
 
 !error
 
+# Test case for [CALCITE-7358] Casts involving MAP and ROW types cause 
compile-time exceptions
+CREATE TYPE user_def AS(i1 INT, v1 VARCHAR NULL);
+(0 rows modified)
+
+!update
+
+CREATE TABLE tbl(mapp1 MAP<user_def, ROW(v VARCHAR NULL)>);
+(0 rows modified)
+
+!update
+
+# Index in a map with a user-defined type
+SELECT mapp1[user_def(1, 'a')] as field FROM tbl;
++-------+
+| FIELD |
++-------+
++-------+
+(0 rows)
+
+!ok
+
+# Test case for [CALCITE-7358] Casts involving MAP and ROW types cause 
compile-time exceptions
+SELECT CAST(mapp1[user_def(1, 'a')] AS INT) FROM tbl;
+java.sql.SQLException: Error while executing SQL "SELECT 
CAST(mapp1[user_def(1, 'a')] AS INT) FROM tbl": From line 1, column 8 to line 
1, column 43: Cast function cannot convert value of type RecordType(VARCHAR V) 
to type INTEGER NOT NULL
+
+!error
+
+SELECT CAST(mapp1 AS MAP<VARCHAR, INT>) AS to_map
+FROM tbl;
+java.sql.SQLException: Error while executing SQL "SELECT CAST(mapp1 AS 
MAP<VARCHAR, INT>) AS to_map
+FROM tbl": From line 1, column 8 to line 1, column 39: Cast function cannot 
convert value of type (RecordType(INTEGER I1, VARCHAR V1) NOT NULL, 
RecordType(VARCHAR V)) MAP to type (VARCHAR NOT NULL, INTEGER) MAP NOT NULL
+
+!error
+
 create type myint1 as int;
 (0 rows modified)
 
diff --git a/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java 
b/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
index 67b43ff462..632d24cfea 100644
--- a/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
@@ -83,6 +83,7 @@
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -213,6 +214,27 @@ protected static Collection<String> data(String first) {
     return paths;
   }
 
+  /** Debugging helper which returns only a subset of the files produced by 
{@code data(first)}.
+   *
+   * @param first       File path indicating where IQ files are searched.
+   * @param substring   Only files that contain this substring are returned.
+   * @return            The list of IQ files produced by data(first) which 
match the restriction.
+   *
+   * <p>I find that often when I debug quidem tests it is handy to only run 
the currently modified
+   * file.  By replacing the call to data(first) with a call to data(first, 
restricted) one
+   * can easily just run the new tests.  But do not forget to undo this change 
when submitting the
+   * final PR! */
+  protected static Collection<String> data(String first, String substring) {
+    List<String> result = data(first)
+        .stream()
+        .filter(s -> s.contains(substring))
+        .collect(Collectors.toList());
+    if (result.isEmpty()) {
+      throw new RuntimeException("Filter is too strict, result is empty");
+    }
+    return result;
+  }
+
   protected void checkRun(String path) throws Exception {
     final File inFile;
     final File outFile;

Reply via email to