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 ce51f0e527 [CALCITE-7886] Subquery expansion fails for IN subquery 
with ROW types
ce51f0e527 is described below

commit ce51f0e52753af382b66c9089ec79cdccd920c06
Author: Mihai Budiu <[email protected]>
AuthorDate: Sat Oct 24 18:26:31 2026 -0700

    [CALCITE-7886] Subquery expansion fails for IN subquery with ROW types
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../sql/validate/implicit/TypeCoercionImpl.java    | 14 +++++
 .../apache/calcite/sql2rel/SqlToRelConverter.java  | 57 +++++++++++---------
 core/src/test/resources/sql/row-equality.iq        | 61 ++++++++++++++++++++++
 3 files changed, 108 insertions(+), 24 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 7f4d0d7356..463216b006 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
@@ -553,6 +553,20 @@ private boolean coalesceCoercion(SqlCallBinding 
callBinding) {
         // A "scalar" (non-ROW) type is in the LHS of the IN
         coerced = coerceOperandType(scope, binding.getCall(), 0, 
widenTypes.get(0))
             || coerced;
+      } else if (node1.getKind() != SqlKind.ROW) {
+        // An expression of a ROW type is in the LHS of the IN, e.g.,
+        // SELECT r IN (ROW(1.5, 2), ...)
+        //        ^
+        // In this case cast it to a ROW of the desired field types
+        final List<RelDataTypeField> fields = type1.getFieldList();
+        final List<RelDataType> fieldTypes = new ArrayList<>();
+        for (int i = 0; i < colCount; i++) {
+          fieldTypes.add(syncAttributes(fields.get(i).getType(), 
widenTypes.get(i)));
+        }
+        coerced =
+            coerceOperandType(scope, binding.getCall(), 0,
+                factory.createStructType(fieldTypes, type1.getFieldNames()))
+            || coerced;
       }
 
       for (int i = 0; i < widenTypes.size(); i++) {
diff --git 
a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java 
b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
index f380de8d82..7ff2a824a9 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -1443,7 +1443,7 @@ private void substituteSubQuery(Blackboard bb, SubQuery 
subQuery) {
 
       if (!config.isExpand()) {
         if (query instanceof SqlNodeList) {
-          convertNodeListToSubQuery(bb, subQuery, query, leftSqlKeys, 
targetRowType, call);
+          convertNodeListToSubQuery(bb, subQuery, query, leftKeyNode, 
targetRowType, call);
           return;
         }
         return;
@@ -1486,7 +1486,7 @@ private void substituteSubQuery(Blackboard bb, SubQuery 
subQuery) {
       // Instead, we convert to a RexSubQuery which can be optimized later.
       if (bb.root == null) {
         if (query instanceof SqlNodeList) {
-          convertNodeListToSubQuery(bb, subQuery, query, leftSqlKeys, 
targetRowType, call);
+          convertNodeListToSubQuery(bb, subQuery, query, leftKeyNode, 
targetRowType, call);
         }
         return;
       }
@@ -2069,7 +2069,7 @@ private RexNode ensureComparisonTypes(RexNode node) {
    * @param bb              Blackboard containing the context
    * @param subQuery        The SubQuery to populate with the converted 
expression
    * @param query           The query node (expected to be a SqlNodeList)
-   * @param leftSqlKeys     Left-hand key expressions for IN/SOME/ALL
+   * @param leftKeyNode     Left-hand operand of IN/SOME/ALL
    * @param targetRowType   The target row type for the conversion
    * @param call            The original SQL call
    */
@@ -2077,19 +2077,42 @@ private void convertNodeListToSubQuery(
       Blackboard bb,
       SubQuery subQuery,
       SqlNode query,
-      List<SqlNode> leftSqlKeys,
+      SqlNode leftKeyNode,
       RelDataType targetRowType,
       SqlCall call) {
     RelNode relNode = convertRowValues(bb, query, (SqlNodeList) query, false, 
targetRowType);
-    final ImmutableList.Builder<RexNode> builder = ImmutableList.builder();
-    for (SqlNode node : leftSqlKeys) {
-      builder.add(bb.convertExpression(node));
-    }
-    final ImmutableList<RexNode> list = builder.build();
+    final ImmutableList<RexNode> list = convertInKeys(bb, leftKeyNode);
     assert relNode != null;
     subQuery.expr = createSubquery(subQuery.node.getKind(), relNode, list, 
call);
   }
 
+  /**
+   * Converts the left-hand operand of IN/SOME/ALL to a list of expressions,
+   * one for each column of the right-hand operand.
+   *
+   * <p>For a ROW constructor, the list contains its operands:
+   * for {@code (a, b) IN ((1, 2))} the list is [a, b].
+   * For any other operand of a ROW type, the list contains its fields, as in
+   * the validator (see {@link SqlInOperator#deriveType}): if {@code r} has
+   * type {@code ROW(x INTEGER, y INTEGER)}, for {@code r IN (ROW(1, 2))}
+   * the list is [r.x, r.y]. For any other operand, the list contains only
+   * the operand.
+   */
+  private ImmutableList<RexNode> convertInKeys(Blackboard bb, SqlNode 
leftKeyNode) {
+    if (leftKeyNode.getKind() == SqlKind.ROW) {
+      return ((SqlCall) leftKeyNode).getOperandList().stream()
+          .map(bb::convertExpression)
+          .collect(toImmutableList());
+    }
+    final RexNode key = bb.convertExpression(leftKeyNode);
+    if (!key.getType().isStruct()) {
+      return ImmutableList.of(key);
+    }
+    return key.getType().getFieldList().stream()
+        .map(field -> rexBuilder.makeFieldAccess(key, field.getIndex()))
+        .collect(toImmutableList());
+  }
+
   /**
    * Gets the list size threshold under which {@link #convertInToOr} is used.
    * Lists of this size or greater will instead be converted to use a join
@@ -6170,21 +6193,7 @@ private RexNode convertExpression0(SqlNode expr) {
           query = call.operand(1);
           if (!(query instanceof SqlNodeList)) {
             root = convertQueryRecursive(query, false, null);
-            final SqlNode operand = call.operand(0);
-            List<SqlNode> nodes;
-            switch (operand.getKind()) {
-            case ROW:
-              nodes = ((SqlCall) operand).getOperandList();
-              break;
-            default:
-              nodes = ImmutableList.of(operand);
-            }
-            final ImmutableList.Builder<RexNode> builder =
-                ImmutableList.builder();
-            for (SqlNode node : nodes) {
-              builder.add(convertExpression(node));
-            }
-            final ImmutableList<RexNode> list = builder.build();
+            final ImmutableList<RexNode> list = convertInKeys(this, 
call.operand(0));
             RelNode rel = root.rel;
             // Fix the correlation namespaces and de-duplicate the correlation 
variables.
             CorrelationUse correlationUse = getCorrelationUse(this, root.rel);
diff --git a/core/src/test/resources/sql/row-equality.iq 
b/core/src/test/resources/sql/row-equality.iq
index aa9de0d6fc..a3cd23c293 100644
--- a/core/src/test/resources/sql/row-equality.iq
+++ b/core/src/test/resources/sql/row-equality.iq
@@ -507,6 +507,67 @@ SELECT (1, CAST(NULL AS VARCHAR))
 
 !ok
 
+# Test cases for https://issues.apache.org/jira/browse/CALCITE-7886
+# [CALCITE-7886] Subquery expansion fails for IN subquery with ROW types
+!set insubquerythreshold 0
+
+# Validated on H2 2.4.240 and PostgreSQL 14: same result.
+SELECT * FROM (SELECT ROW(1, 2) AS r) AS t WHERE r IN (ROW(1, 2), ROW(3, 4));
++--------+
+| R      |
++--------+
+| {1, 2} |
++--------+
+(1 row)
+
+!ok
+
+# IN_DECIMAL casts "a" to ROW(DECIMAL, CHAR(1)) before it compares.
+WITH t(id, a) AS (
+  SELECT id, ROW(x, y)
+  FROM (VALUES (1, 1, 'a'), (2, 1, NULL), (3, 2, 'c')) AS v(id, x, y))
+SELECT id,
+  a IN (ROW(1, 'a'), ROW(2, 'b')) AS in_list,
+  a NOT IN (ROW(1, 'a'), ROW(2, 'b')) AS notin_list,
+  a IN (ROW(1.0, 'a'), ROW(2.5, 'b')) AS in_decimal,
+  a IN (VALUES (1, 'a'), (2, 'b')) AS in_values,
+  a NOT IN (SELECT 1, 'a') AS notin_subq
+FROM t
+ORDER BY id;
++----+---------+------------+------------+-----------+------------+
+| ID | IN_LIST | NOTIN_LIST | IN_DECIMAL | IN_VALUES | NOTIN_SUBQ |
++----+---------+------------+------------+-----------+------------+
+|  1 | true    | false      | true       | true      | false      |
+|  2 |         |            |            |           |            |
+|  3 | false   | true       | false      | false     | true       |
++----+---------+------------+------------+-----------+------------+
+(3 rows)
+
+!ok
+
+# A window function of a ROW type on the left of IN.
+# Validated on H2 2.4.240: same result.
+WITH t(id, a) AS (
+  SELECT id, ROW(x, y)
+  FROM (VALUES (1, 1, 'a'), (2, 1, NULL), (3, 2, 'c')) AS v(id, x, y))
+SELECT id,
+  LAG(a) OVER (ORDER BY id) IN (ROW(1, 'a'), ROW(2, 'b')) AS lag_in_list,
+  LEAD(a) OVER (ORDER BY id) IN (ROW(1, 'a'), ROW(2, 'b')) AS lead_in_list,
+  LAG(a) OVER (ORDER BY id) IN (SELECT 1, 'a') AS lag_in_subq
+FROM t
+ORDER BY id;
++----+-------------+--------------+-------------+
+| ID | LAG_IN_LIST | LEAD_IN_LIST | LAG_IN_SUBQ |
++----+-------------+--------------+-------------+
+|  1 |             |              |             |
+|  2 | true        | false        | true        |
+|  3 |             |              |             |
++----+-------------+--------------+-------------+
+(3 rows)
+
+!ok
+!set insubquerythreshold 20
+
 #####################################################################
 # ROW values inside a collection
 #

Reply via email to