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;