From 7289fc06aa2a04908e82f1f926669a25b2363fc8 Mon Sep 17 00:00:00 2001 From: krooswu Date: Tue, 7 Apr 2026 22:58:00 +0800 Subject: [PATCH 1/7] [CALCITE-7437] Type coercion for quantifier operators is incomplete --- .../calcite/sql/fun/SqlQuantifyOperator.java | 10 ++++- .../validate/implicit/TypeCoercionImpl.java | 45 ++++++++++++++----- .../apache/calcite/test/RelOptRulesTest.xml | 6 +-- core/src/test/resources/sql/sub-query.iq | 10 ++--- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java index aeb770acbaac..bc91aed6224b 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java @@ -90,7 +90,15 @@ public class SqlQuantifyOperator extends SqlInOperator { if (typeForCollectionArgument != null) { return typeForCollectionArgument; } - return super.deriveType(validator, scope, call); + // Right-hand side is a subquery. tryDeriveTypeForCollection is skipped, + // so trigger coercion explicitly here. + final RelDataType returnType = super.deriveType(validator, scope, call); + if (validator.config().typeCoercionEnabled()) { + validator.getTypeCoercion() + .quantifyOperationCoercion( + new SqlCallBinding(validator, scope, call)); + } + return returnType; } /** diff --git a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java index f3f5c53469a4..7ae9f7d53dde 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java @@ -652,7 +652,23 @@ private boolean coalesceCoercion(SqlCallBinding callBinding) { @Override public boolean quantifyOperationCoercion(SqlCallBinding binding) { final RelDataType type1 = binding.getOperandType(0); final RelDataType collectionType = binding.getOperandType(1); - final RelDataType type2 = collectionType.getComponentType(); + RelDataType type2 = collectionType.getComponentType(); + + // Determine whether the right-hand side is a subquery or a collection. + final boolean isSubQuery = binding.operand(1) instanceof SqlSelect; + + if (type2 == null) { + if (!isSubQuery) { + return false; + } + // Subquery path: derive type2 from the first output column. + type2 = + SqlTypeUtil.flattenRecordType(binding.getTypeFactory(), collectionType, null) + .getFieldList() + .get(0) + .getType(); + } + requireNonNull(type2, "type2"); final SqlCall sqlCall = binding.getCall(); final SqlValidatorScope scope = binding.getScope(); @@ -674,16 +690,23 @@ private boolean coalesceCoercion(SqlCallBinding callBinding) { } final RelDataType rightWidenType = binding.getTypeFactory().enforceTypeWithNullability(widenType, type2.isNullable()); - RelDataType collectionWidenType = - binding.getTypeFactory().createArrayType(rightWidenType, -1); - collectionWidenType = - binding - .getTypeFactory() - .enforceTypeWithNullability(collectionWidenType, collectionType.isNullable()); - boolean coercedRight = - coerceOperandType(scope, sqlCall, 1, collectionWidenType); - if (coercedRight) { - updateInferredType(node2, collectionWidenType); + boolean coercedRight; + if (isSubQuery) { + // For subquery, cast the output column directly. + // There is no array wrapper to reconstruct. + coercedRight = false; + } else { + // For collection (ARRAY[...]), reconstruct the array type with the + // widened component type before coercing the operand. + RelDataType collectionWidenType = + binding.getTypeFactory().createArrayType(rightWidenType, -1); + collectionWidenType = + binding.getTypeFactory() + .enforceTypeWithNullability(collectionWidenType, collectionType.isNullable()); + coercedRight = coerceOperandType(scope, sqlCall, 1, collectionWidenType); + if (coercedRight) { + updateInferredType(node2, collectionWidenType); + } } return coercedLeft || coercedRight; } diff --git a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml index ece2ad5258aa..16bbe0b03347 100644 --- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml @@ -1520,7 +1520,7 @@ from dept]]> ($2, 0)), AND(<($3, $2), null, <>($2, 0), IS NULL($5)))]) - LogicalJoin(condition=[=($1, $4)], joinType=[left]) + LogicalJoin(condition=[=(CAST($1):INTEGER NOT NULL, $4)], joinType=[left]) LogicalJoin(condition=[true], joinType=[inner]) LogicalTableScan(table=[[CATALOG, SALES, DEPT]]) LogicalAggregate(group=[{}], c=[COUNT()], ck=[COUNT($0)]) @@ -1544,7 +1544,7 @@ LogicalProject(DEPTNO=[$0], EXPR$1=[OR(AND(IS NOT NULL($5), <>($2, 0)), AND(<($3 ($2, 0)), AND(<($3, $2), null, <>($2, 0), IS NULL($5)))]) - LogicalJoin(condition=[=($1, $4)], joinType=[left]) + LogicalJoin(condition=[=(CAST($1):INTEGER NOT NULL, $4)], joinType=[left]) LogicalJoin(condition=[true], joinType=[inner]) LogicalTableScan(table=[[CATALOG, SALES, DEPT]]) LogicalAggregate(group=[{}], c=[COUNT()], ck=[COUNT($0)]) diff --git a/core/src/test/resources/sql/sub-query.iq b/core/src/test/resources/sql/sub-query.iq index 1a79635e640e..68455dab8532 100644 --- a/core/src/test/resources/sql/sub-query.iq +++ b/core/src/test/resources/sql/sub-query.iq @@ -2945,7 +2945,7 @@ where e.empno > ANY( select 2 from "scott".dept e2 where e2.deptno = e.deptno) ; !if (use_old_decorr) { EnumerableCalc(expr#0..6=[{inputs}], EMPNO=[$t5]) - EnumerableHashJoin(condition=[AND(IS NOT DISTINCT FROM($4, $6), OR(AND(>($5, $0), IS NOT TRUE(OR(IS NULL($3), =($1, 0)))), AND(>($5, $0), IS NOT TRUE(OR(IS NULL($3), =($1, 0))), IS NOT TRUE(>($5, $0)), <=($1, $2))))], joinType=[inner]) + EnumerableHashJoin(condition=[AND(IS NOT DISTINCT FROM($4, $6), OR(AND(>(CAST($5):INTEGER NOT NULL, $0), IS NOT TRUE(OR(IS NULL($3), =($1, 0)))), AND(>(CAST($5):INTEGER NOT NULL, $0), IS NOT TRUE(OR(IS NULL($3), =($1, 0))), IS NOT TRUE(>(CAST($5):INTEGER NOT NULL, $0)), <=($1, $2))))], joinType=[inner]) EnumerableCalc(expr#0..4=[{inputs}], expr#5=[IS NOT NULL($t3)], expr#6=[0], expr#7=[CASE($t5, $t3, $t6)], m=[$t2], c=[$t7], d=[$t7], trueLiteral=[$t4], DEPTNO=[$t0]) EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], joinType=[left]) EnumerableAggregate(group=[{7}]) @@ -2983,7 +2983,7 @@ select empno, e.deptno > ANY( select 2 from "scott".dept e2 where e2.deptno = e.empno) from "scott".emp as e; !if (use_old_decorr) { -EnumerableCalc(expr#0..6=[{inputs}], expr#7=[>($t1, $t2)], expr#8=[IS TRUE($t7)], expr#9=[IS NULL($t5)], expr#10=[0], expr#11=[=($t3, $t10)], expr#12=[OR($t9, $t11)], expr#13=[IS NOT TRUE($t12)], expr#14=[AND($t8, $t13)], expr#15=[>($t3, $t4)], expr#16=[IS TRUE($t15)], expr#17=[null:BOOLEAN], expr#18=[IS NOT TRUE($t7)], expr#19=[AND($t16, $t17, $t13, $t18)], expr#20=[IS NOT TRUE($t15)], expr#21=[AND($t7, $t13, $t18, $t20)], expr#22=[OR($t14, $t19, $t21)], EMPNO=[$t0], EXPR$1=[$t22]) +EnumerableCalc(expr#0..6=[{inputs}], expr#7=[CAST($t1):INTEGER], expr#8=[>($t7, $t2)], expr#9=[IS TRUE($t8)], expr#10=[IS NULL($t5)], expr#11=[0], expr#12=[=($t3, $t11)], expr#13=[OR($t10, $t12)], expr#14=[IS NOT TRUE($t13)], expr#15=[AND($t9, $t14)], expr#16=[>($t3, $t4)], expr#17=[IS TRUE($t16)], expr#18=[null:BOOLEAN], expr#19=[IS NOT TRUE($t8)], expr#20=[AND($t17, $t18, $t14, $t19)], expr#21=[IS NOT TRUE($t16)], expr#22=[AND($t8, $t14, $t19, $t21)], expr#23=[OR($t15, $t20, $t22)], EMPNO=[$t0], EXPR$1=[$t23]) EnumerableHashJoin(condition=[=($0, $6)], joinType=[left]) EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0], DEPTNO=[$t7]) EnumerableTableScan(table=[[scott, EMP]]) @@ -3115,7 +3115,7 @@ select * from "scott".emp emp1 where empno <> some (select comm from "scott".emp where deptno = emp1.deptno); !if (use_old_decorr) { -EnumerableCalc(expr#0..13=[{inputs}], expr#14=[<>($t10, $t9)], expr#15=[1], expr#16=[<=($t11, $t15)], expr#17=[AND($t14, $t16)], expr#18=[=($t11, $t15)], expr#19=[OR($t17, $t18)], expr#20=[<>($t0, $t12)], expr#21=[IS NULL($t13)], expr#22=[0], expr#23=[=($t9, $t22)], expr#24=[OR($t21, $t23)], expr#25=[IS NOT TRUE($t24)], expr#26=[AND($t19, $t20, $t25)], expr#27=[IS NOT TRUE($t19)], expr#28=[AND($t25, $t27)], expr#29=[OR($t26, $t28)], proj#0..7=[{exprs}], $condition=[$t29]) +EnumerableCalc(expr#0..13=[{inputs}], expr#14=[<>($t10, $t9)], expr#15=[1], expr#16=[<=($t11, $t15)], expr#17=[AND($t14, $t16)], expr#18=[=($t11, $t15)], expr#19=[OR($t17, $t18)], expr#20=[CAST($t0):DECIMAL(7, 2) NOT NULL], expr#21=[<>($t20, $t12)], expr#22=[IS NULL($t13)], expr#23=[0], expr#24=[=($t9, $t23)], expr#25=[OR($t22, $t24)], expr#26=[IS NOT TRUE($t25)], expr#27=[AND($t19, $t21, $t26)], expr#28=[IS NOT TRUE($t19)], expr#29=[AND($t26, $t28)], expr#30=[OR($t27, $t29)], proj#0..7=[{exprs}], $condition=[$t30]) EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($7, $8)], joinType=[left]) EnumerableTableScan(table=[[scott, EMP]]) EnumerableCalc(expr#0..6=[{inputs}], expr#7=[IS NOT NULL($t2)], expr#8=[0], expr#9=[CASE($t7, $t2, $t8)], expr#10=[IS NOT NULL($t3)], expr#11=[CASE($t10, $t3, $t8)], expr#12=[IS NOT NULL($t4)], expr#13=[CASE($t12, $t4, $t8)], DEPTNO=[$t0], c=[$t9], d=[$t11], dd=[$t13], m=[$t5], trueLiteral=[$t6]) @@ -3174,7 +3174,7 @@ select * from "scott".emp as emp1 where empno <> some (select 2 from "scott".dept dept1 where dept1.deptno = emp1.empno); !if (use_old_decorr) { -EnumerableCalc(expr#0..13=[{inputs}], expr#14=[<>($t9, $t8)], expr#15=[1], expr#16=[<=($t10, $t15)], expr#17=[<>($t0, $t11)], expr#18=[IS NULL($t12)], expr#19=[0], expr#20=[=($t8, $t19)], expr#21=[OR($t18, $t20)], expr#22=[IS NOT TRUE($t21)], expr#23=[AND($t14, $t16, $t17, $t22)], expr#24=[=($t10, $t15)], expr#25=[IS NOT NULL($t10)], expr#26=[AND($t14, $t25)], expr#27=[IS NOT TRUE($t26)], expr#28=[AND($t24, $t17, $t22, $t27)], expr#29=[AND($t14, $t16)], expr#30=[IS NOT TRUE($t29)], expr#31=[IS NOT TRUE($t24)], expr#32=[AND($t22, $t30, $t31)], expr#33=[OR($t23, $t28, $t32)], proj#0..7=[{exprs}], $condition=[$t33]) +EnumerableCalc(expr#0..13=[{inputs}], expr#14=[<>($t9, $t8)], expr#15=[1], expr#16=[<=($t10, $t15)], expr#17=[CAST($t0):INTEGER NOT NULL], expr#18=[<>($t17, $t11)], expr#19=[IS NULL($t12)], expr#20=[0], expr#21=[=($t8, $t20)], expr#22=[OR($t19, $t21)], expr#23=[IS NOT TRUE($t22)], expr#24=[AND($t14, $t16, $t18, $t23)], expr#25=[=($t10, $t15)], expr#26=[IS NOT NULL($t10)], expr#27=[AND($t14, $t26)], expr#28=[IS NOT TRUE($t27)], expr#29=[AND($t25, $t18, $t23, $t28)], expr#30=[AND($t14, $t16)], expr#31=[IS NOT TRUE($t30)], expr#32=[IS NOT TRUE($t25)], expr#33=[AND($t23, $t31, $t32)], expr#34=[OR($t24, $t29, $t33)], proj#0..7=[{exprs}], $condition=[$t34]) EnumerableHashJoin(condition=[=($0, $13)], joinType=[left]) EnumerableTableScan(table=[[scott, EMP]]) EnumerableCalc(expr#0..5=[{inputs}], expr#6=[IS NOT NULL($t2)], expr#7=[0], expr#8=[CASE($t6, $t2, $t7)], expr#9=[IS NOT NULL($t3)], expr#10=[CASE($t9, $t3, $t7)], c=[$t8], d=[$t8], dd=[$t10], m=[$t4], trueLiteral=[$t5], DEPTNO0=[$t0]) @@ -3227,7 +3227,7 @@ select * from "scott".emp as emp1 where comm <> some (select 2 from "scott".dept dept1 where dept1.deptno = emp1.empno); !if (use_old_decorr) { -EnumerableCalc(expr#0..13=[{inputs}], expr#14=[<>($t9, $t8)], expr#15=[1], expr#16=[<=($t10, $t15)], expr#17=[AND($t14, $t16)], expr#18=[=($t10, $t15)], expr#19=[OR($t17, $t18)], expr#20=[<>($t6, $t11)], expr#21=[IS NULL($t12)], expr#22=[IS NULL($t6)], expr#23=[0], expr#24=[=($t8, $t23)], expr#25=[OR($t21, $t22, $t24)], expr#26=[IS NOT TRUE($t25)], expr#27=[AND($t19, $t20, $t26)], expr#28=[IS NOT TRUE($t19)], expr#29=[AND($t26, $t28)], expr#30=[OR($t27, $t29)], proj#0..7=[{exprs}], $condition=[$t30]) +EnumerableCalc(expr#0..13=[{inputs}], expr#14=[<>($t9, $t8)], expr#15=[1], expr#16=[<=($t10, $t15)], expr#17=[AND($t14, $t16)], expr#18=[=($t10, $t15)], expr#19=[OR($t17, $t18)], expr#20=[CAST($t6):DECIMAL(12, 2)], expr#21=[<>($t20, $t11)], expr#22=[IS NULL($t12)], expr#23=[IS NULL($t6)], expr#24=[0], expr#25=[=($t8, $t24)], expr#26=[OR($t22, $t23, $t25)], expr#27=[IS NOT TRUE($t26)], expr#28=[AND($t19, $t21, $t27)], expr#29=[IS NOT TRUE($t19)], expr#30=[AND($t27, $t29)], expr#31=[OR($t28, $t30)], proj#0..7=[{exprs}], $condition=[$t31]) EnumerableHashJoin(condition=[=($0, $13)], joinType=[left]) EnumerableTableScan(table=[[scott, EMP]]) EnumerableCalc(expr#0..5=[{inputs}], expr#6=[IS NOT NULL($t2)], expr#7=[0], expr#8=[CASE($t6, $t2, $t7)], expr#9=[IS NOT NULL($t3)], expr#10=[CASE($t9, $t3, $t7)], c=[$t8], d=[$t8], dd=[$t10], m=[$t4], trueLiteral=[$t5], DEPTNO0=[$t0]) From 501d61ae7bd3c633936c4919b91d6bdba968d0ff Mon Sep 17 00:00:00 2001 From: krooswu Date: Thu, 23 Apr 2026 23:12:33 +0800 Subject: [PATCH 2/7] Fix conflicts --- core/src/test/resources/sql/sub-query.iq | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/core/src/test/resources/sql/sub-query.iq b/core/src/test/resources/sql/sub-query.iq index 68455dab8532..8981e178770a 100644 --- a/core/src/test/resources/sql/sub-query.iq +++ b/core/src/test/resources/sql/sub-query.iq @@ -9205,4 +9205,36 @@ lateral( (2 rows) !ok + +# Test case for [CALCITE-7437] SOME/ANY subquery throws RuntimeException +SELECT deptno, deptno > SOME(SELECT sal FROM emp) AS b FROM dept; ++--------+-------+ +| DEPTNO | B | ++--------+-------+ +| 10 | false | +| 20 | false | +| 30 | false | +| 40 | false | ++--------+-------+ +(4 rows) + +!ok +!if (use_old_decorr) { +EnumerableCalc(expr#0..3=[{inputs}], expr#4=[CAST($t3):DECIMAL(7, 2) NOT NULL], expr#5=[>($t4, $t0)], expr#6=[IS TRUE($t5)], expr#7=[0], expr#8=[<>($t1, $t7)], expr#9=[AND($t6, $t8)], expr#10=[>($t1, $t2)], expr#11=[null:BOOLEAN], expr#12=[IS NOT TRUE($t5)], expr#13=[AND($t10, $t11, $t8, $t12)], expr#14=[<=($t1, $t2)], expr#15=[AND($t5, $t8, $t12, $t14)], expr#16=[OR($t9, $t13, $t15)], DEPTNO=[$t3], B=[$t16]) + EnumerableNestedLoopJoin(condition=[true], joinType=[inner]) + EnumerableAggregate(group=[{}], m=[MIN($5)], c=[COUNT()], d=[COUNT($5)]) + EnumerableTableScan(table=[[scott, EMP]]) + EnumerableCalc(expr#0..2=[{inputs}], DEPTNO=[$t0]) + EnumerableTableScan(table=[[scott, DEPT]]) +!plan +!} + +# Case 2: incompatible types (VARCHAR vs SMALLINT). +# Before fix: java.lang.RuntimeException: while resolving method +# 'gt[class java.lang.String, short]' in class SqlFunctions +# After fix: incompatibleValueType validation error +SELECT deptno, dname > SOME(SELECT empno FROM emp) AS b FROM dept; +For input string: "ACCOUNTING" +!error + # End sub-query.iq From 10c0abfe3d49784f05177335c26c58727ac1a510 Mon Sep 17 00:00:00 2001 From: krooswu Date: Sat, 11 Apr 2026 22:51:19 +0800 Subject: [PATCH 3/7] Test Case --- .../java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java index bc91aed6224b..041d7abf09bb 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java @@ -90,8 +90,7 @@ public class SqlQuantifyOperator extends SqlInOperator { if (typeForCollectionArgument != null) { return typeForCollectionArgument; } - // Right-hand side is a subquery. tryDeriveTypeForCollection is skipped, - // so trigger coercion explicitly here. + // Right-hand side is a subquery (some,any, all) final RelDataType returnType = super.deriveType(validator, scope, call); if (validator.config().typeCoercionEnabled()) { validator.getTypeCoercion() From fb46637f947c5090a7ee63af4c834ea3182529d3 Mon Sep 17 00:00:00 2001 From: krooswu Date: Mon, 20 Apr 2026 23:00:37 +0800 Subject: [PATCH 4/7] Align quantifyOperationCoercion behavior with inOperationCoercion --- .../validate/implicit/TypeCoercionImpl.java | 142 ++++++++++++------ .../apache/calcite/test/RelOptRulesTest.xml | 6 +- core/src/test/resources/sql/sub-query.iq | 4 +- 3 files changed, 99 insertions(+), 53 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java index 7ae9f7d53dde..40b958a254a7 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java @@ -651,64 +651,110 @@ private boolean coalesceCoercion(SqlCallBinding callBinding) { */ @Override public boolean quantifyOperationCoercion(SqlCallBinding binding) { final RelDataType type1 = binding.getOperandType(0); - final RelDataType collectionType = binding.getOperandType(1); - RelDataType type2 = collectionType.getComponentType(); + final RelDataType type2 = binding.getOperandType(1); + final SqlNode node1 = binding.operand(0); + final SqlNode node2 = binding.operand(1); + final SqlValidatorScope scope = binding.getScope(); - // Determine whether the right-hand side is a subquery or a collection. - final boolean isSubQuery = binding.operand(1) instanceof SqlSelect; + // Check column counts match for struct types, consistent with inOperationCoercion. + if (type1.isStruct() + && type2.isStruct() + && type1.getFieldCount() != type2.getFieldCount()) { + return false; + } - if (type2 == null) { - if (!isSubQuery) { + int colCount = type1.isStruct() ? type1.getFieldCount() : 1; + RelDataType[] argTypes = new RelDataType[2]; + argTypes[0] = type1; + final boolean isSubQuery = node2 instanceof SqlSelect; + // For subquery, use the row type directly. + // For collection, use the component type for comparison, not the array type. + if (isSubQuery) { + argTypes[1] = type2; + } else { + RelDataType componentType = type2.getComponentType(); + if (componentType == null) { return false; } - // Subquery path: derive type2 from the first output column. - type2 = - SqlTypeUtil.flattenRecordType(binding.getTypeFactory(), collectionType, null) - .getFieldList() - .get(0) - .getType(); + argTypes[1] = componentType; } + boolean coerced = false; - requireNonNull(type2, "type2"); - final SqlCall sqlCall = binding.getCall(); - final SqlValidatorScope scope = binding.getScope(); - final SqlNode node1 = binding.operand(0); - final SqlNode node2 = binding.operand(1); - RelDataType widenType = commonTypeForBinaryComparison(type1, type2); - if (widenType == null) { - widenType = getTightestCommonType(type1, type2); - } - if (widenType == null) { - return false; + // Find the common types for RHS and LHS columns, + // following the same rules as inOperationCoercion. + List widenTypes = new ArrayList<>(); + for (int i = 0; i < colCount; i++) { + final int i2 = i; + List columnIthTypes = new AbstractList() { + @Override public RelDataType get(int index) { + return argTypes[index].isStruct() + ? argTypes[index].getFieldList().get(i2).getType() + : argTypes[index]; + } + + @Override public int size() { + return argTypes.length; + } + }; + + RelDataType widenType = + commonTypeForBinaryComparison(columnIthTypes.get(0), columnIthTypes.get(1)); + if (widenType == null) { + widenType = getTightestCommonType(columnIthTypes.get(0), columnIthTypes.get(1)); + } + if (widenType == null) { + // Cannot find any common type, return early. + return false; + } + widenTypes.add(widenType); } - final RelDataType leftWidenType = - binding.getTypeFactory().enforceTypeWithNullability(widenType, type1.isNullable()); - boolean coercedLeft = - coerceOperandType(scope, sqlCall, 0, leftWidenType); - if (coercedLeft) { - updateInferredType(node1, leftWidenType); + assert widenTypes.size() == colCount; + + // Coerce LHS operand. + if (!type1.isStruct()) { + coerced = coerceOperandType(scope, binding.getCall(), 0, widenTypes.get(0)) || coerced; } - final RelDataType rightWidenType = - binding.getTypeFactory().enforceTypeWithNullability(widenType, type2.isNullable()); - boolean coercedRight; - if (isSubQuery) { - // For subquery, cast the output column directly. - // There is no array wrapper to reconstruct. - coercedRight = false; - } else { - // For collection (ARRAY[...]), reconstruct the array type with the - // widened component type before coercing the operand. - RelDataType collectionWidenType = - binding.getTypeFactory().createArrayType(rightWidenType, -1); - collectionWidenType = - binding.getTypeFactory() - .enforceTypeWithNullability(collectionWidenType, collectionType.isNullable()); - coercedRight = coerceOperandType(scope, sqlCall, 1, collectionWidenType); - if (coercedRight) { - updateInferredType(node2, collectionWidenType); + + for (int i = 0; i < widenTypes.size(); i++) { + RelDataType desired = widenTypes.get(i); + if (node1.getKind() == SqlKind.ROW) { + assert node1 instanceof SqlCall; + if (coerceOperandType(scope, (SqlCall) node1, i, desired)) { + updateInferredColumnType( + requireNonNull(scope, "scope"), + node1, i, widenTypes.get(i)); + coerced = true; + } + } + + // RHS: subquery uses rowTypeCoercion (consistent with inOperationCoercion), + // collection reconstructs the array type. + if (isSubQuery) { + // Use rowTypeCoercion on the subquery output column, + // consistent with how inOperationCoercion handles the subquery case. + SqlValidatorScope scope1 = validator.getSelectScope((SqlSelect) node2); + coerced = rowTypeCoercion(scope1, node2, i, desired) || coerced; + } else { + // Collection path (ARRAY[...]): coerce the whole array operand once. + // Reconstruct the array type with the widened component type. + // Only single-column comparison is supported for collections. + RelDataType componentType = argTypes[1]; + final RelDataType rightWidenType = + binding.getTypeFactory() + .enforceTypeWithNullability(desired, componentType.isNullable()); + RelDataType collectionWidenType = + binding.getTypeFactory().createArrayType(rightWidenType, -1); + collectionWidenType = + binding.getTypeFactory() + .enforceTypeWithNullability(collectionWidenType, type2.isNullable()); + if (coerceOperandType(scope, binding.getCall(), 1, collectionWidenType)) { + updateInferredType(node2, collectionWidenType); + coerced = true; + } } } - return coercedLeft || coercedRight; + + return coerced; } @Override public boolean builtinFunctionCoercion( diff --git a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml index 16bbe0b03347..099ff741aaab 100644 --- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml @@ -18265,7 +18265,7 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$ @@ -18289,7 +18289,7 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$ LogicalJoin(condition=[=($1, $9)], joinType=[inner]) LogicalTableScan(table=[[CATALOG, SALES, EMP]]) LogicalAggregate(group=[{0}]) - LogicalProject(NAME=[$1]) + LogicalProject(NAME=[CAST($1):VARCHAR(20) NOT NULL]) LogicalTableScan(table=[[CATALOG, SALES, DEPT]]) ]]> diff --git a/core/src/test/resources/sql/sub-query.iq b/core/src/test/resources/sql/sub-query.iq index 8981e178770a..cf5a925f8018 100644 --- a/core/src/test/resources/sql/sub-query.iq +++ b/core/src/test/resources/sql/sub-query.iq @@ -3234,11 +3234,11 @@ EnumerableCalc(expr#0..13=[{inputs}], expr#14=[<>($t9, $t8)], expr#15=[1], expr# EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], joinType=[left]) EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0]) EnumerableTableScan(table=[[scott, EMP]]) - EnumerableCalc(expr#0..4=[{inputs}], expr#5=[CAST($t1):BIGINT NOT NULL], expr#6=[CAST($t3):INTEGER NOT NULL], expr#7=[CAST($t4):BOOLEAN NOT NULL], DEPTNO0=[$t0], c=[$t5], dd=[$t2], m=[$t6], trueLiteral=[$t7]) + EnumerableCalc(expr#0..4=[{inputs}], expr#5=[CAST($t1):BIGINT NOT NULL], expr#6=[CAST($t3):DECIMAL(12, 2) NOT NULL], expr#7=[CAST($t4):BOOLEAN NOT NULL], DEPTNO0=[$t0], c=[$t5], dd=[$t2], m=[$t6], trueLiteral=[$t7]) EnumerableAggregate(group=[{0}], c_g0=[MIN($2) FILTER $6], dd_g0=[COUNT($1) FILTER $5], m_g0=[MIN($3) FILTER $6], trueLiteral_g0=[MIN(true, $4) FILTER $6]) EnumerableCalc(expr#0..5=[{inputs}], expr#6=[0], expr#7=[=($t5, $t6)], expr#8=[1], expr#9=[=($t5, $t8)], proj#0..4=[{exprs}], $g_0=[$t7], $g_1=[$t9]) EnumerableAggregate(group=[{0, 1}], groups=[[{0, 1}, {0}]], c=[COUNT()], m=[MAX($1)], trueLiteral=[LITERAL_AGG(true)], $g=[GROUPING($0, $1)]) - EnumerableCalc(expr#0..2=[{inputs}], expr#3=[CAST($t0):SMALLINT NOT NULL], expr#4=[2], DEPTNO0=[$t3], EXPR$0=[$t4]) + EnumerableCalc(expr#0..2=[{inputs}], expr#3=[CAST($t0):SMALLINT NOT NULL], expr#4=[2.00:DECIMAL(12, 2)], DEPTNO0=[$t3], EXPR$0=[$t4]) EnumerableTableScan(table=[[scott, DEPT]]) !plan !} From ea3e4a0303caa79f758f8bbf9d3d2ca4a02814e9 Mon Sep 17 00:00:00 2001 From: krooswu Date: Wed, 22 Apr 2026 22:31:32 +0800 Subject: [PATCH 5/7] Align quantifyOperationCoercion behavior with inOperationCoercion --- .../calcite/sql/validate/implicit/TypeCoercionImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java index 40b958a254a7..309821356cf2 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java @@ -733,7 +733,10 @@ private boolean coalesceCoercion(SqlCallBinding callBinding) { // Use rowTypeCoercion on the subquery output column, // consistent with how inOperationCoercion handles the subquery case. SqlValidatorScope scope1 = validator.getSelectScope((SqlSelect) node2); - coerced = rowTypeCoercion(scope1, node2, i, desired) || coerced; + RelDataType source = validator.getValidatedNodeType(node2); + RelDataType target = binding.getTypeFactory() + .createTypeWithNullability(desired, source.isNullable() || desired.isNullable()); + coerced = rowTypeCoercion(scope1, node2, i, target) || coerced; } else { // Collection path (ARRAY[...]): coerce the whole array operand once. // Reconstruct the array type with the widened component type. From ee92ed2287164df58487bcb5e29429edca583851 Mon Sep 17 00:00:00 2001 From: krooswu Date: Sat, 25 Apr 2026 10:42:34 +0800 Subject: [PATCH 6/7] More detail --- core/src/test/resources/sql/sub-query.iq | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/resources/sql/sub-query.iq b/core/src/test/resources/sql/sub-query.iq index cf5a925f8018..b301dae413de 100644 --- a/core/src/test/resources/sql/sub-query.iq +++ b/core/src/test/resources/sql/sub-query.iq @@ -9232,7 +9232,7 @@ EnumerableCalc(expr#0..3=[{inputs}], expr#4=[CAST($t3):DECIMAL(7, 2) NOT NULL], # Case 2: incompatible types (VARCHAR vs SMALLINT). # Before fix: java.lang.RuntimeException: while resolving method # 'gt[class java.lang.String, short]' in class SqlFunctions -# After fix: incompatibleValueType validation error +# After fix: incompatibleValueType validation error java.lang.NumberFormatException: For input string: "ACCOUNTING" SELECT deptno, dname > SOME(SELECT empno FROM emp) AS b FROM dept; For input string: "ACCOUNTING" !error From c3acc0fb080fcdf81da8e3e3db1a4876ce7ab754 Mon Sep 17 00:00:00 2001 From: krooswu Date: Sat, 25 Apr 2026 22:01:03 +0800 Subject: [PATCH 7/7] Fix comment --- .../calcite/sql/validate/implicit/TypeCoercionImpl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java index 309821356cf2..232ee6dcbe2b 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java @@ -668,7 +668,7 @@ private boolean coalesceCoercion(SqlCallBinding callBinding) { argTypes[0] = type1; final boolean isSubQuery = node2 instanceof SqlSelect; // For subquery, use the row type directly. - // For collection, use the component type for comparison, not the array type. + // For collection, use the component type for comparison, not the collection. if (isSubQuery) { argTypes[1] = type2; } else { @@ -738,9 +738,9 @@ private boolean coalesceCoercion(SqlCallBinding callBinding) { .createTypeWithNullability(desired, source.isNullable() || desired.isNullable()); coerced = rowTypeCoercion(scope1, node2, i, target) || coerced; } else { - // Collection path (ARRAY[...]): coerce the whole array operand once. - // Reconstruct the array type with the widened component type. - // Only single-column comparison is supported for collections. + // Collection path (e.g. ARRAY, MULTISET): coerce the whole collection + // operand once, reconstructing the collection type with the widened + // component type RelDataType componentType = argTypes[1]; final RelDataType rightWidenType = binding.getTypeFactory()