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

FrankChen021 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 45681539904 fix(sql): avoid reducing constant complex arrays (#20664)
45681539904 is described below

commit 456815399049f74660732b1250d67c0e48a1ee5d
Author: sakshichitnis27 <[email protected]>
AuthorDate: Sun Oct 25 01:36:19 2026 +0000

    fix(sql): avoid reducing constant complex arrays (#20664)
    
    * Fix constant complex array reduction
    
    * fix(sql): handle null and nested constant complex arrays
    
    Address review feedback on #20664 by covering more constant complex array
    cases:
    
    - DruidRexExecutor now looks through nested arrays when deciding whether an
      array constant has complex elements, so ARRAY<ARRAY<COMPLEX<json>>> is 
left
      as an expression instead of failing in RexBuilder.makeLiteral.
    - DruidLogicalValuesRule now accepts null ARRAY literals. A JSON_QUERY_ARRAY
      over a path that does not exist reduces to a null array literal, which was
      previously rejected with "Cannot handle literal ... of unsupported type
      [ARRAY]".
    - Add tests for an empty array, a missing JSON path, a nested array of
      constant JSON, and the null array literal.
    
    Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
    
    * Move issue link comments to test method Javadoc
    
    ---------
    
    Co-authored-by: Claude Sonnet 5.5 <[email protected]>
---
 .../sql/calcite/planner/DruidRexExecutor.java      | 17 +++++
 .../sql/calcite/rule/DruidLogicalValuesRule.java   |  5 ++
 .../sql/calcite/CalciteNestedDataQueryTest.java    | 72 ++++++++++++++++++++++
 .../calcite/rule/DruidLogicalValuesRuleTest.java   | 11 ++++
 4 files changed, 105 insertions(+)

diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidRexExecutor.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidRexExecutor.java
index 13991455f56..e79f4ccd142 100644
--- 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidRexExecutor.java
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidRexExecutor.java
@@ -20,6 +20,7 @@
 package org.apache.druid.sql.calcite.planner;
 
 import com.google.common.annotations.VisibleForTesting;
+import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexExecutor;
@@ -181,6 +182,10 @@ public class DruidRexExecutor implements RexExecutor
           final Object[] array = exprResult.asArray();
           if (array == null) {
             literal = rexBuilder.makeNullLiteral(constExp.getType());
+          } else if (isArrayOfComplex(constExp.getType())) {
+            // arrays of complex constants, including nested arrays, are not 
reducible, so just leave them as an
+            // expression
+            literal = constExp;
           } else if 
(SqlTypeName.NUMERIC_TYPES.contains(constExp.getType().getComponentType().getSqlTypeName()))
 {
             if (exprResult.type().getElementType().is(ExprType.LONG)) {
               List<BigDecimal> resultAsBigDecimalList = new 
ArrayList<>(array.length);
@@ -246,6 +251,18 @@ public class DruidRexExecutor implements RexExecutor
     }
   }
 
+  ///
+  /// Whether the leaf element type of an array type, looking through any 
nested arrays, is a complex type.
+  ///
+  private static boolean isArrayOfComplex(final RelDataType arrayType)
+  {
+    RelDataType elementType = arrayType.getComponentType();
+    while (elementType != null && elementType.getSqlTypeName() == 
SqlTypeName.ARRAY) {
+      elementType = elementType.getComponentType();
+    }
+    return elementType != null && elementType.getSqlTypeName() == 
SqlTypeName.OTHER;
+  }
+
   private static String getInputValueOrExpression(final RexNode constExp)
   {
     if (constExp.isA(SqlKind.CAST)) {
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRule.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRule.java
index 8cf224e2a60..c6c8f0cb6b1 100644
--- 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRule.java
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRule.java
@@ -132,6 +132,11 @@ public class DruidLogicalValuesRule extends RelOptRule
           throw InvalidSqlInput.exception("Expected a NULL literal, but got 
non-null constant [%s]", literal);
         }
         return null;
+      case ARRAY:
+        if (literal.isNull()) {
+          return null;
+        }
+        // fall through to reject non-null array literals
       case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       case TIME:
       case TIME_WITH_LOCAL_TIME_ZONE:
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java
 
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java
index b28bb8b63eb..ea8f4990c66 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java
@@ -6998,6 +6998,78 @@ public abstract class CalciteNestedDataQueryTest extends 
BaseCalciteQueryTest
         .run();
   }
 
+  /**
+   * Unnesting {@code JSON_QUERY_ARRAY} of a constant JSON value used to fail 
planning with a
+   * {@link StackOverflowError}, see 
https://github.com/apache/druid/issues/20631
+   */
+  @Test
+  public void testUnnestJsonQueryArrayOfConstantJson()
+  {
+    testBuilder()
+        .sql(
+            "SELECT TO_JSON_STRING(v) FROM (SELECT 
PARSE_JSON('{\"a\":[{\"b\":1}]}') AS j) t "
+            + "CROSS JOIN UNNEST(JSON_QUERY_ARRAY(t.j, '$.a')) AS u(v)"
+        )
+        .queryContext(QUERY_CONTEXT_NO_STRINGIFY_ARRAY)
+        .expectedResults(ImmutableList.of(new Object[]{"{\"b\":1}"}))
+        .run();
+  }
+
+  /**
+   * Same as {@link #testUnnestJsonQueryArrayOfConstantJson()}, but the 
constant JSON array is empty, see
+   * https://github.com/apache/druid/issues/20631
+   */
+  @Test
+  public void testUnnestJsonQueryArrayOfConstantJsonEmptyArray()
+  {
+    testBuilder()
+        .sql(
+            "SELECT TO_JSON_STRING(v) FROM (SELECT PARSE_JSON('{\"a\":[]}') AS 
j) t "
+            + "CROSS JOIN UNNEST(JSON_QUERY_ARRAY(t.j, '$.a')) AS u(v)"
+        )
+        .queryContext(QUERY_CONTEXT_NO_STRINGIFY_ARRAY)
+        .expectedResults(ImmutableList.of())
+        .run();
+  }
+
+  /**
+   * Same as {@link #testUnnestJsonQueryArrayOfConstantJson()}, but the JSON 
path does not exist, so
+   * {@code JSON_QUERY_ARRAY} reduces to a null array literal, see 
https://github.com/apache/druid/issues/20631
+   */
+  @Test
+  public void testUnnestJsonQueryArrayOfConstantJsonMissingPath()
+  {
+    // Array complex JSON isn't supported
+    msqIncompatible();
+    testBuilder()
+        .sql(
+            "SELECT TO_JSON_STRING(v) FROM (SELECT 
PARSE_JSON('{\"a\":[{\"b\":1}]}') AS j) t "
+            + "CROSS JOIN UNNEST(JSON_QUERY_ARRAY(t.j, '$.missing')) AS u(v)"
+        )
+        .queryContext(QUERY_CONTEXT_NO_STRINGIFY_ARRAY)
+        .expectedResults(ImmutableList.of())
+        .run();
+  }
+
+  /**
+   * An {@code ARRAY<ARRAY<COMPLEX<json>>>} constant used to fail planning 
with a {@link ClassCastException}, see
+   * https://github.com/apache/druid/issues/20631
+   */
+  @Test
+  public void testJsonQueryArrayOfArraysOfConstantJson()
+  {
+    // Array complex JSON isn't supported
+    msqIncompatible();
+    testBuilder()
+        .sql(
+            "SELECT ARRAY[JSON_QUERY_ARRAY(j, '$.a'), JSON_QUERY_ARRAY(j, 
'$.c')] "
+            + "FROM (SELECT 
PARSE_JSON('{\"a\":[{\"b\":1}],\"c\":[{\"d\":2}]}') AS j) t"
+        )
+        .queryContext(QUERY_CONTEXT_DEFAULT)
+        .expectedResults(ImmutableList.of(new 
Object[]{"[[{\"b\":1}],[{\"d\":2}]]"}))
+        .run();
+  }
+
   @Test
   public void testUnnestJsonQueryArrays()
   {
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRuleTest.java
 
b/sql/src/test/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRuleTest.java
index 63d3cbaf63e..9b7781b5bfb 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRuleTest.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRuleTest.java
@@ -150,6 +150,17 @@ public class DruidLogicalValuesRuleTest
       Assertions.assertNull(fromLiteral);
     }
 
+    @Test
+    public void testGetValueFromNullArrayLiteral()
+    {
+      RexLiteral literal = REX_BUILDER.makeNullLiteral(
+          
TYPE_FACTORY.createArrayType(TYPE_FACTORY.createSqlType(SqlTypeName.BIGINT), -1)
+      );
+
+      final Object fromLiteral = 
DruidLogicalValuesRule.getValueFromLiteral(literal, DEFAULT_CONTEXT);
+      Assertions.assertNull(fromLiteral);
+    }
+
     @Test
     public void testGetValueFromTimestampLiteral()
     {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to