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]