diff --git a/docs/querying/math-expr.md b/docs/querying/math-expr.md index 2de004618d2c..4389dab120dc 100644 --- a/docs/querying/math-expr.md +++ b/docs/querying/math-expr.md @@ -237,6 +237,7 @@ JSON functions provide facilities to extract, transform, and create `COMPLEX`, `ARRAY`, or `ARRAY`) value from `expr` using JSONPath syntax of `path`. The optional `type` argument can be set to `'LONG'`,`'DOUBLE'`, `'STRING'`, `'ARRAY'`, `'ARRAY'`, or `'ARRAY'` to cast values to that type. | | json_query(expr, path) | Extract a `COMPLEX` value from `expr` using JSONPath syntax of `path` | +| json_query_array(expr, path) | Extract an `ARRAY>` value from `expr` using JSONPath syntax of `path`. If value is not an `ARRAY`, it will be translated into a single element `ARRAY` containing the value at `path`. | | json_object(expr1, expr2[, expr3, expr4 ...]) | Construct a `COMPLEX` with alternating 'key' and 'value' arguments| | parse_json(expr) | Deserialize a JSON `STRING` into a `COMPLEX`. If the input is not a `STRING` or it is invalid JSON, this function will result in an error.| | try_parse_json(expr) | Deserialize a JSON `STRING` into a `COMPLEX`. If the input is not a `STRING` or it is invalid JSON, this function will result in a `NULL` value. | diff --git a/docs/querying/sql-json-functions.md b/docs/querying/sql-json-functions.md index 1f3c681392e8..d499fef5087f 100644 --- a/docs/querying/sql-json-functions.md +++ b/docs/querying/sql-json-functions.md @@ -40,6 +40,7 @@ You can use the following JSON functions to extract, transform, and create `COMP |`JSON_OBJECT(KEY expr1 VALUE expr2[, KEY expr3 VALUE expr4, ...])` | Constructs a new `COMPLEX` object. The `KEY` expressions must evaluate to string types. The `VALUE` expressions can be composed of any input type, including other `COMPLEX` values. `JSON_OBJECT` can accept colon-separated key-value pairs. The following syntax is equivalent: `JSON_OBJECT(expr1:expr2[, expr3:expr4, ...])`.| |`JSON_PATHS(expr)`| Returns an array of all paths which refer to literal values in `expr` in JSONPath format. | |`JSON_QUERY(expr, path)`| Extracts a `COMPLEX` value from `expr`, at the specified `path`. | +|`JSON_QUERY_ARRAY(expr, path)`| Extracts an `ARRAY>` value from `expr`, at the specified `path`. If value is not an `ARRAY`, it will be translated into a single element `ARRAY` containing the value at `path`.| |`JSON_VALUE(expr, path [RETURNING sqlType])`| Extracts a literal value from `expr` at the specified `path`. If you specify `RETURNING` and an SQL type name (such as `VARCHAR`, `BIGINT`, `DOUBLE`, etc) the function plans the query using the suggested type. Otherwise, it attempts to infer the type based on the context. If it can't infer the type, it defaults to `VARCHAR`.| |`PARSE_JSON(expr)`|Parses `expr` into a `COMPLEX` object. This operator deserializes JSON values when processing them, translating stringified JSON into a nested structure. If the input is not a `VARCHAR` or it is invalid JSON, this function will result in an error.| |`TRY_PARSE_JSON(expr)`|Parses `expr` into a `COMPLEX` object. This operator deserializes JSON values when processing them, translating stringified JSON into a nested structure. If the input is not a `VARCHAR` or it is invalid JSON, this function will result in a `NULL` value.| diff --git a/processing/src/main/java/org/apache/druid/guice/ExpressionModule.java b/processing/src/main/java/org/apache/druid/guice/ExpressionModule.java index db5aeaf17c21..5387daead4bb 100644 --- a/processing/src/main/java/org/apache/druid/guice/ExpressionModule.java +++ b/processing/src/main/java/org/apache/druid/guice/ExpressionModule.java @@ -83,6 +83,7 @@ public class ExpressionModule implements Module .add(NestedDataExpressions.JsonPathsExprMacro.class) .add(NestedDataExpressions.JsonValueExprMacro.class) .add(NestedDataExpressions.JsonQueryExprMacro.class) + .add(NestedDataExpressions.JsonQueryArrayExprMacro.class) .add(NestedDataExpressions.ToJsonStringExprMacro.class) .add(NestedDataExpressions.ParseJsonExprMacro.class) .add(NestedDataExpressions.TryParseJsonExprMacro.class) diff --git a/processing/src/main/java/org/apache/druid/query/expression/NestedDataExpressions.java b/processing/src/main/java/org/apache/druid/query/expression/NestedDataExpressions.java index fec16ad99b1e..f7476adf599e 100644 --- a/processing/src/main/java/org/apache/druid/query/expression/NestedDataExpressions.java +++ b/processing/src/main/java/org/apache/druid/query/expression/NestedDataExpressions.java @@ -28,6 +28,7 @@ import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.math.expr.ExprType; import org.apache.druid.math.expr.ExpressionType; +import org.apache.druid.math.expr.ExpressionTypeFactory; import org.apache.druid.math.expr.NamedFunction; import org.apache.druid.segment.nested.NestedPathFinder; import org.apache.druid.segment.nested.NestedPathPart; @@ -44,6 +45,8 @@ public class NestedDataExpressions { + private static ExpressionType JSON_ARRAY = ExpressionTypeFactory.getInstance().ofArray(ExpressionType.NESTED_DATA); + public static class JsonObjectExprMacro implements ExprMacroTable.ExprMacro { public static final String NAME = "json_object"; @@ -591,6 +594,120 @@ public ExpressionType getOutputType(InputBindingInspector inspector) } } + public static class JsonQueryArrayExprMacro implements ExprMacroTable.ExprMacro + { + public static final String NAME = "json_query_array"; + + @Override + public String name() + { + return NAME; + } + + @Override + public Expr apply(List args) + { + if (args.get(1).isLiteral()) { + return new JsonQueryArrayExpr(args); + } else { + return new JsonQueryArrayDynamicExpr(args); + } + } + + final class JsonQueryArrayExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr + { + private final List parts; + + public JsonQueryArrayExpr(List args) + { + super(name(), args); + this.parts = getJsonPathPartsFromLiteral(JsonQueryArrayExprMacro.this, args.get(1)); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + ExprEval input = args.get(0).eval(bindings); + final Object value = NestedPathFinder.find(unwrap(input), parts); + if (value instanceof List) { + return ExprEval.ofArray( + JSON_ARRAY, + ExprEval.bestEffortArray((List) value).asArray() + ); + } + return ExprEval.ofArray( + JSON_ARRAY, + ExprEval.bestEffortOf(value).asArray() + ); + } + + @Override + public Expr visit(Shuttle shuttle) + { + List newArgs = args.stream().map(x -> x.visit(shuttle)).collect(Collectors.toList()); + if (newArgs.get(1).isLiteral()) { + return shuttle.visit(new JsonQueryArrayExpr(newArgs)); + } else { + return shuttle.visit(new JsonQueryArrayDynamicExpr(newArgs)); + } + } + + @Nullable + @Override + public ExpressionType getOutputType(InputBindingInspector inspector) + { + // call all the output JSON typed + return ExpressionType.NESTED_DATA; + } + } + + final class JsonQueryArrayDynamicExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr + { + public JsonQueryArrayDynamicExpr(List args) + { + super(name(), args); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + ExprEval input = args.get(0).eval(bindings); + ExprEval path = args.get(1).eval(bindings); + final List parts = NestedPathFinder.parseJsonPath(path.asString()); + final Object value = NestedPathFinder.find(unwrap(input), parts); + if (value instanceof List) { + return ExprEval.ofArray( + JSON_ARRAY, + ExprEval.bestEffortArray((List) value).asArray() + ); + } + return ExprEval.ofArray( + JSON_ARRAY, + ExprEval.bestEffortOf(value).asArray() + ); + } + + @Override + public Expr visit(Shuttle shuttle) + { + List newArgs = args.stream().map(x -> x.visit(shuttle)).collect(Collectors.toList()); + if (newArgs.get(1).isLiteral()) { + return shuttle.visit(new JsonQueryArrayExpr(newArgs)); + } else { + return shuttle.visit(new JsonQueryArrayDynamicExpr(newArgs)); + } + } + + @Nullable + @Override + public ExpressionType getOutputType(InputBindingInspector inspector) + { + // call all the output ARRAY> typed + return JSON_ARRAY; + } + } + } + public static class JsonPathsExprMacro implements ExprMacroTable.ExprMacro { public static final String NAME = "json_paths"; diff --git a/processing/src/main/java/org/apache/druid/segment/column/ColumnTypeFactory.java b/processing/src/main/java/org/apache/druid/segment/column/ColumnTypeFactory.java index 49793fba4163..c9b153bf1211 100644 --- a/processing/src/main/java/org/apache/druid/segment/column/ColumnTypeFactory.java +++ b/processing/src/main/java/org/apache/druid/segment/column/ColumnTypeFactory.java @@ -64,7 +64,7 @@ public static ColumnType ofType(TypeSignature type) case STRING: return ColumnType.STRING_ARRAY; default: - throw new ISE("Unsupported expression type[%s]", type.asTypeString()); + return ColumnType.ofArray(ofType(type.getElementType())); } case COMPLEX: return INTERNER.intern(new ColumnType(ValueType.COMPLEX, type.getComplexTypeName(), null)); diff --git a/processing/src/main/java/org/apache/druid/segment/virtual/NestedFieldVirtualColumn.java b/processing/src/main/java/org/apache/druid/segment/virtual/NestedFieldVirtualColumn.java index e486e7b77388..0a282f5e4bc6 100644 --- a/processing/src/main/java/org/apache/druid/segment/virtual/NestedFieldVirtualColumn.java +++ b/processing/src/main/java/org/apache/druid/segment/virtual/NestedFieldVirtualColumn.java @@ -1228,6 +1228,13 @@ public ColumnCapabilities capabilities(String columnName) public ColumnCapabilities capabilities(ColumnInspector inspector, String columnName) { if (processFromRaw) { + if (expectedType != null && expectedType.isArray() && ColumnType.NESTED_DATA.equals(expectedType.getElementType())) { + // arrays of objects! + return ColumnCapabilitiesImpl.createDefault() + .setType(ColumnType.ofArray(ColumnType.NESTED_DATA)) + .setHasMultipleValues(false) + .setHasNulls(true); + } // JSON_QUERY always returns a StructuredData return ColumnCapabilitiesImpl.createDefault() .setType(ColumnType.NESTED_DATA) diff --git a/processing/src/test/java/org/apache/druid/query/expression/NestedDataExpressionsTest.java b/processing/src/test/java/org/apache/druid/query/expression/NestedDataExpressionsTest.java index d8bd2d93841b..b14edb2d17b8 100644 --- a/processing/src/test/java/org/apache/druid/query/expression/NestedDataExpressionsTest.java +++ b/processing/src/test/java/org/apache/druid/query/expression/NestedDataExpressionsTest.java @@ -29,6 +29,7 @@ import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.math.expr.ExpressionProcessingException; import org.apache.druid.math.expr.ExpressionType; +import org.apache.druid.math.expr.ExpressionTypeFactory; import org.apache.druid.math.expr.InputBindings; import org.apache.druid.math.expr.Parser; import org.apache.druid.segment.nested.StructuredData; @@ -37,6 +38,7 @@ import org.junit.Test; import java.util.Arrays; +import java.util.List; import java.util.Map; public class NestedDataExpressionsTest extends InitializedNullHandlingTest @@ -49,6 +51,7 @@ public class NestedDataExpressionsTest extends InitializedNullHandlingTest new NestedDataExpressions.JsonObjectExprMacro(), new NestedDataExpressions.JsonValueExprMacro(), new NestedDataExpressions.JsonQueryExprMacro(), + new NestedDataExpressions.JsonQueryArrayExprMacro(), new NestedDataExpressions.ToJsonStringExprMacro(JSON_MAPPER), new NestedDataExpressions.ParseJsonExprMacro(JSON_MAPPER), new NestedDataExpressions.TryParseJsonExprMacro(JSON_MAPPER) @@ -329,6 +332,37 @@ public void testJsonQueryExpression() Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); } + @Test + public void testJsonQueryArrayExpression() + { + final ExpressionType nestedArray = ExpressionTypeFactory.getInstance().ofArray(ExpressionType.NESTED_DATA); + + Expr expr = Parser.parse("json_query_array(nest, '$.x')", MACRO_TABLE); + ExprEval eval = expr.eval(inputBindings); + Assert.assertArrayEquals(new Object[]{100L}, (Object[]) eval.value()); + Assert.assertEquals(nestedArray, eval.type()); + + expr = Parser.parse("json_query_array(nester, '$.x')", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertArrayEquals(((List) NESTER.get("x")).toArray(), (Object[]) eval.value()); + Assert.assertEquals(nestedArray, eval.type()); + + expr = Parser.parse("json_query_array(nester, array_offset(json_paths(nester), 0))", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertArrayEquals(((List) NESTER.get("x")).toArray(), (Object[]) eval.value()); + Assert.assertEquals(nestedArray, eval.type()); + + expr = Parser.parse("json_query_array(nesterer, '$.y')", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertArrayEquals(((List) NESTERER.get("y")).toArray(), (Object[]) eval.value()); + Assert.assertEquals(nestedArray, eval.type()); + + expr = Parser.parse("array_length(json_query_array(nesterer, '$.y'))", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals(3L, eval.value()); + Assert.assertEquals(ExpressionType.LONG, eval.type()); + } + @Test public void testParseJsonTryParseJson() throws JsonProcessingException { diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/NestedDataOperatorConversions.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/NestedDataOperatorConversions.java index bfa8f47e56ec..a51a3d713757 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/NestedDataOperatorConversions.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/NestedDataOperatorConversions.java @@ -48,11 +48,13 @@ import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.math.expr.Expr; import org.apache.druid.math.expr.InputBindings; +import org.apache.druid.query.expression.NestedDataExpressions; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.column.RowSignature; import org.apache.druid.segment.nested.NestedPathFinder; import org.apache.druid.segment.nested.NestedPathPart; import org.apache.druid.segment.virtual.NestedFieldVirtualColumn; +import org.apache.druid.sql.calcite.expression.DirectOperatorConversion; import org.apache.druid.sql.calcite.expression.DruidExpression; import org.apache.druid.sql.calcite.expression.Expressions; import org.apache.druid.sql.calcite.expression.OperatorConversions; @@ -78,6 +80,16 @@ public class NestedDataOperatorConversions true ); + public static final SqlReturnTypeInference NESTED_ARRAY_RETURN_TYPE_INFERENCE = opBinding -> + opBinding.getTypeFactory().createArrayType( + RowSignatures.makeComplexType( + opBinding.getTypeFactory(), + ColumnType.NESTED_DATA, + true + ), + -1 + ); + public static class JsonPathsOperatorConversion implements SqlOperatorConversion { private static final SqlFunction SQL_FUNCTION = OperatorConversions @@ -231,6 +243,26 @@ public DruidExpression toDruidExpression( } } + public static class JsonQueryArrayOperatorConversion extends DirectOperatorConversion + { + private static final SqlFunction SQL_FUNCTION = OperatorConversions + .operatorBuilder(StringUtils.toUpperCase(NestedDataExpressions.JsonQueryArrayExprMacro.NAME)) + .operandTypeChecker( + OperandTypes.family( + SqlTypeFamily.ANY, + SqlTypeFamily.CHARACTER + ) + ) + .returnTypeInference(NESTED_ARRAY_RETURN_TYPE_INFERENCE) + .functionCategory(SqlFunctionCategory.SYSTEM) + .build(); + + public JsonQueryArrayOperatorConversion() + { + super(SQL_FUNCTION, NestedDataExpressions.JsonQueryArrayExprMacro.NAME); + } + } + /** * The {@link org.apache.calcite.sql2rel.StandardConvertletTable} converts json_value(.. RETURNING type) into * cast(json_value_any(..), type). diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java index 6c432c800d89..cc9779a30c63 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java @@ -333,6 +333,7 @@ public class DruidOperatorTable implements SqlOperatorTable .add(new NestedDataOperatorConversions.JsonKeysOperatorConversion()) .add(new NestedDataOperatorConversions.JsonPathsOperatorConversion()) .add(new NestedDataOperatorConversions.JsonQueryOperatorConversion()) + .add(new NestedDataOperatorConversions.JsonQueryArrayOperatorConversion()) .add(new NestedDataOperatorConversions.JsonValueAnyOperatorConversion()) .add(new NestedDataOperatorConversions.JsonValueBigintOperatorConversion()) .add(new NestedDataOperatorConversions.JsonValueDoubleOperatorConversion()) 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 cea1bdbef7e1..8261694df5b5 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 @@ -6523,4 +6523,218 @@ public void testJsonQueryDynamicArg() ); } + + @Test + public void testJsonQueryArrays() + { + cannotVectorize(); + testBuilder() + .sql("SELECT JSON_QUERY_ARRAY(arrayObject, '$') FROM druid.arrays") + .queryContext(QUERY_CONTEXT_DEFAULT) + .expectedQueries( + ImmutableList.of( + Druids.newScanQueryBuilder() + .dataSource(DATA_SOURCE_ARRAYS) + .intervals(querySegmentSpec(Filtration.eternity())) + .virtualColumns( + expressionVirtualColumn("v0", "json_query_array(\"arrayObject\",'$')", ColumnType.ofArray(ColumnType.NESTED_DATA)) + ) + .columns("v0") + .context(QUERY_CONTEXT_DEFAULT) + .legacy(false) + .resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST) + .build() + ) + ) + .expectedResults( + ImmutableList.of( + new Object[]{"[{\"x\":1000},{\"y\":2000}]"}, + new Object[]{"[{\"x\":1},{\"x\":2}]"}, + new Object[]{"[{\"x\":null},{\"x\":2}]"}, + new Object[]{"[{\"a\":1},{\"b\":2}]"}, + new Object[]{"[{\"x\":1},{\"x\":2}]"}, + new Object[]{"[null,{\"x\":2}]"}, + new Object[]{"[{\"x\":3},{\"x\":4}]"}, + new Object[]{"[{\"x\":1000},{\"y\":2000}]"}, + new Object[]{"[{\"x\":1},{\"x\":2}]"}, + new Object[]{"[{\"x\":null},{\"x\":2}]"}, + new Object[]{"[{\"a\":1},{\"b\":2}]"}, + new Object[]{"[{\"x\":1},{\"x\":2}]"}, + new Object[]{"[null,{\"x\":2}]"}, + new Object[]{"[{\"x\":3},{\"x\":4}]"} + ) + ) + .expectedSignature( + RowSignature.builder() + .add("EXPR$0", ColumnType.ofArray(ColumnType.NESTED_DATA)) + .build() + ) + .run(); + } + @Test + public void testUnnestJsonQueryArrays() + { + cannotVectorize(); + testBuilder() + .sql("SELECT objects FROM druid.arrays, UNNEST(JSON_QUERY_ARRAY(arrayObject, '$')) as u(objects)") + .queryContext(QUERY_CONTEXT_NO_STRINGIFY_ARRAY) + .expectedQueries( + ImmutableList.of( + Druids.newScanQueryBuilder() + .dataSource( + UnnestDataSource.create( + TableDataSource.create(DATA_SOURCE_ARRAYS), + expressionVirtualColumn("j0.unnest", "json_query_array(\"arrayObject\",'$')", ColumnType.ofArray(ColumnType.NESTED_DATA)), + null + ) + ) + .intervals(querySegmentSpec(Filtration.eternity())) + .columns("j0.unnest") + .context(QUERY_CONTEXT_NO_STRINGIFY_ARRAY) + .legacy(false) + .resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST) + .build() + ) + ) + .expectedResults( + ImmutableList.of( + new Object[]{"{\"x\":1000}"}, + new Object[]{"{\"y\":2000}"}, + new Object[]{"{\"x\":1}"}, + new Object[]{"{\"x\":2}"}, + new Object[]{"{\"x\":null}"}, + new Object[]{"{\"x\":2}"}, + new Object[]{"{\"a\":1}"}, + new Object[]{"{\"b\":2}"}, + new Object[]{"{\"x\":1}"}, + new Object[]{"{\"x\":2}"}, + new Object[]{null}, + new Object[]{"{\"x\":2}"}, + new Object[]{"{\"x\":3}"}, + new Object[]{"{\"x\":4}"}, + new Object[]{"{\"x\":1000}"}, + new Object[]{"{\"y\":2000}"}, + new Object[]{"{\"x\":1}"}, + new Object[]{"{\"x\":2}"}, + new Object[]{"{\"x\":null}"}, + new Object[]{"{\"x\":2}"}, + new Object[]{"{\"a\":1}"}, + new Object[]{"{\"b\":2}"}, + new Object[]{"{\"x\":1}"}, + new Object[]{"{\"x\":2}"}, + new Object[]{null}, + new Object[]{"{\"x\":2}"}, + new Object[]{"{\"x\":3}"}, + new Object[]{"{\"x\":4}"} + ) + ) + .expectedSignature( + RowSignature.builder() + .add("objects", ColumnType.NESTED_DATA) + .build() + ) + .run(); + } + + @Test + public void testUnnestJsonQueryArraysJsonValue() + { + cannotVectorize(); + testBuilder() + .sql( + "SELECT" + + " json_value(objects, '$.x' returning bigint) as x," + + " count(*)" + + " FROM druid.arrays, UNNEST(JSON_QUERY_ARRAY(arrayObject, '$')) as u(objects)" + + " GROUP BY 1" + ) + .queryContext(QUERY_CONTEXT_NO_STRINGIFY_ARRAY) + .expectedQueries( + ImmutableList.of( + GroupByQuery.builder() + .setDataSource( + UnnestDataSource.create( + TableDataSource.create(DATA_SOURCE_ARRAYS), + expressionVirtualColumn("j0.unnest", "json_query_array(\"arrayObject\",'$')", ColumnType.ofArray(ColumnType.NESTED_DATA)), + null + ) + ) + .setInterval(querySegmentSpec(Filtration.eternity())) + .setGranularity(Granularities.ALL) + .setVirtualColumns( + new NestedFieldVirtualColumn("j0.unnest", "$.x", "v0", ColumnType.LONG) + ) + .setDimensions( + dimensions( + new DefaultDimensionSpec("v0", "d0", ColumnType.LONG) + ) + ) + .setAggregatorSpecs(new CountAggregatorFactory("a0")) + .setContext(QUERY_CONTEXT_DEFAULT) + .build() + ) + ) + .expectedResults( + ImmutableList.of( + new Object[]{NullHandling.defaultLongValue(), 10L}, + new Object[]{1L, 4L}, + new Object[]{2L, 8L}, + new Object[]{3L, 2L}, + new Object[]{4L, 2L}, + new Object[]{1000L, 2L} + ) + ) + .expectedSignature( + RowSignature.builder() + .add("x", ColumnType.LONG) + .add("EXPR$1", ColumnType.LONG) + .build() + ) + .run(); + } + + @Test + public void testUnnestJsonQueryArraysJsonValueSum() + { + cannotVectorize(); + testBuilder() + .sql( + "SELECT" + + " sum(json_value(objects, '$.x' returning bigint)) as xs" + + " FROM druid.arrays, UNNEST(JSON_QUERY_ARRAY(arrayObject, '$')) as u(objects)" + ) + .queryContext(QUERY_CONTEXT_NO_STRINGIFY_ARRAY) + .expectedQueries( + ImmutableList.of( + Druids.newTimeseriesQueryBuilder() + .intervals(querySegmentSpec(Filtration.eternity())) + .dataSource( + UnnestDataSource.create( + TableDataSource.create(DATA_SOURCE_ARRAYS), + expressionVirtualColumn("j0.unnest", "json_query_array(\"arrayObject\",'$')", ColumnType.ofArray(ColumnType.NESTED_DATA)), + null + ) + ) + .virtualColumns( + new NestedFieldVirtualColumn("j0.unnest", "$.x", "v0", ColumnType.LONG) + ) + .aggregators( + new LongSumAggregatorFactory("a0", "v0") + ) + .context(QUERY_CONTEXT_DEFAULT) + .build() + ) + ) + .expectedResults( + ImmutableList.of( + new Object[]{2034L} + ) + ) + .expectedSignature( + RowSignature.builder() + .add("xs", ColumnType.LONG) + .build() + ) + .run(); + } } diff --git a/website/.spelling b/website/.spelling index b7e7bc74e501..8653746e786c 100644 --- a/website/.spelling +++ b/website/.spelling @@ -360,6 +360,7 @@ json_keys json_object json_paths json_query +json_query_array json_value karlkfi kerberos