From e7af5fb45bef72ddeb8f8dd5c7e7b3223e8c4830 Mon Sep 17 00:00:00 2001 From: Shivam Garg Date: Wed, 9 Oct 2024 19:24:12 +0530 Subject: [PATCH] Added basic framework for json_merge_aggr --- .../apache/druid/guice/ExpressionModule.java | 1 + .../expression/NestedDataExpressions.java | 257 ++++++++++++++++-- .../expression/NestedDataExpressionsTest.java | 98 +++++++ .../NestedDataOperatorConversions.java | 47 ++++ .../calcite/planner/DruidOperatorTable.java | 1 + .../calcite/CalciteNestedDataQueryTest.java | 49 ++++ website/.spelling | 1 + 7 files changed, 430 insertions(+), 24 deletions(-) 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 e1064234e56e6..65e5e98a2cda2 100644 --- a/processing/src/main/java/org/apache/druid/guice/ExpressionModule.java +++ b/processing/src/main/java/org/apache/druid/guice/ExpressionModule.java @@ -82,6 +82,7 @@ public class ExpressionModule implements Module .add(HyperUniqueExpressions.HllRoundEstimateExprMacro.class) .add(NestedDataExpressions.JsonObjectExprMacro.class) .add(NestedDataExpressions.JsonMergeExprMacro.class) + .add(NestedDataExpressions.JsonMergeAggrExprMacro.class) .add(NestedDataExpressions.JsonKeysExprMacro.class) .add(NestedDataExpressions.JsonPathsExprMacro.class) .add(NestedDataExpressions.JsonValueExprMacro.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 0926ce78e0a55..b52a486340bd4 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 @@ -21,9 +21,13 @@ package org.apache.druid.query.expression; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import org.apache.druid.guice.annotations.Json; +import org.apache.druid.java.util.common.StringUtils; +import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.math.expr.Expr; import org.apache.druid.math.expr.ExprEval; import org.apache.druid.math.expr.ExprMacroTable; @@ -38,10 +42,7 @@ import javax.annotation.Nullable; import javax.inject.Inject; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; public class NestedDataExpressions @@ -149,7 +150,7 @@ public ExprEval eval(ObjectBinding bindings) } try { - obj = jsonMapper.readValue(getArgAsJson(arg), Object.class); + obj = jsonMapper.readValue(getArgAsJson(JsonMergeExprMacro.this, jsonMapper, arg), Object.class); } catch (JsonProcessingException e) { throw JsonMergeExprMacro.this.processingFailed(e, "bad string input [%s]", arg.asString()); @@ -161,7 +162,7 @@ public ExprEval eval(ObjectBinding bindings) ExprEval argSub = args.get(i).eval(bindings); try { - String str = getArgAsJson(argSub); + String str = getArgAsJson(JsonMergeExprMacro.this, jsonMapper, argSub); if (str != null) { obj = updater.readValue(str); } @@ -181,36 +182,217 @@ public ExpressionType getOutputType(InputBindingInspector inspector) return ExpressionType.NESTED_DATA; } - private String getArgAsJson(ExprEval arg) + + } + return new ParseJsonExpr(args); + } + } + + public static class JsonMergeAggrExprMacro implements ExprMacroTable.ExprMacro + { + public static final String NAME = "json_merge_aggr"; + static final Logger logger = new Logger(NestedDataExpressions.class); + + private final ObjectMapper jsonMapper; + + @Inject + public JsonMergeAggrExprMacro( + @Json ObjectMapper jsonMapper + ) + { + this.jsonMapper = jsonMapper; + } + + public enum Aggregate + { + ADD, + MULTIPLY + } + + interface Aggregator + { + ExprEval aggregate(ExprEval sourceValue, ExprEval targetValue); + } + + public static class AddAggregator implements Aggregator + { + @Override + public ExprEval aggregate(ExprEval sourceValue, ExprEval targetValue) + { + if (sourceValue.type().isNumeric() && targetValue.type().isNumeric()) { + if (sourceValue.type().equals(ExpressionType.LONG) && targetValue.type().equals(ExpressionType.LONG)) { + return ExprEval.of(sourceValue.asLong() + targetValue.asLong()); + } + + return ExprEval.of(sourceValue.asDouble() + targetValue.asDouble()); + } else if (sourceValue.isArray() && targetValue.isArray()) { + ArrayList sourceList = new ArrayList<>(Arrays.asList(sourceValue.asArray())); + sourceList.addAll(Arrays.asList(targetValue.asArray())); + + return ExprEval.ofComplex(ExpressionType.NESTED_DATA, sourceList); + } + return targetValue; + } + } + + public static class MultiplyAggregator implements Aggregator + { + @Override + public ExprEval aggregate(ExprEval sourceValue, ExprEval targetValue) + { + if (sourceValue.type().isNumeric() && targetValue.type().isNumeric()) { + if (sourceValue.type().equals(ExpressionType.LONG) && targetValue.type().equals(ExpressionType.LONG)) { + return ExprEval.of(sourceValue.asLong() * targetValue.asLong()); + } + + return ExprEval.of(sourceValue.asDouble() * targetValue.asDouble()); + } else if (sourceValue.isArray() && targetValue.isArray()) { + ArrayList sourceList = new ArrayList<>(Arrays.asList(sourceValue.asArray())); + sourceList.addAll(Arrays.asList(targetValue.asArray())); + + return ExprEval.ofComplex(ExpressionType.NESTED_DATA, sourceList); + } + return targetValue; + } + } + + private Aggregator getAggregator(Aggregate aggregate) + { + switch (aggregate) { + case ADD: + return new AddAggregator(); + case MULTIPLY: + return new MultiplyAggregator(); + default: + throw new IllegalArgumentException("Unknown aggregator: " + aggregate); + } + } + + private boolean isMap(ExprEval exprEval) { + return !exprEval.type().isPrimitive() && !exprEval.isArray(); + } + + private void merge(Object source, Object target, Aggregator aggregator) + { + ExprEval sourceValue = ExprEval.bestEffortOf(source); + ExprEval targetValue = ExprEval.bestEffortOf(target); + + if (isMap(sourceValue) && isMap(targetValue)) { + mergeJson((Map) source, (Map) target, aggregator); + } else if (sourceValue.isArray() && targetValue.isArray()) { + mergeArray((List) source, (List) target); + } else { + throw JsonMergeAggrExprMacro.this.validationFailed( + "Unsupported type for merge" + ); + } + } + + private void mergeArray(List source, List target) { + source.addAll(target); + } + + private void mergeJson(Map source, Map target, Aggregator aggregator) + { + for (String key : target.keySet()) { + if (source.containsKey(key)) { + if (source.get(key) instanceof Map && target.get(key) instanceof Map) { + mergeJson((Map) source.get(key), (Map) target.get(key), aggregator); + } else { + ExprEval sourceValue = ExprEval.bestEffortOf(source.get(key)); + ExprEval targetValue = ExprEval.bestEffortOf(target.get(key)); + + ExprEval newValue = aggregator.aggregate(sourceValue, targetValue); + source.put(key, unwrap(newValue)); + } + } else { + source.put(key, target.get(key)); + } + } + } + + @Override + public Expr apply(List args) + { + if (args.size() < 3) { + throw validationFailed("must have at least three arguments"); + } + + final class JsonMergeAggExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr + { + public JsonMergeAggExpr(List args) + { + super(JsonMergeAggrExprMacro.this, args); + } + + @Override + public ExprEval eval(ObjectBinding bindings) { + if (!args.get(0).isLiteral() || args.get(0).getLiteralValue() == null) { + throw validationFailed("aggregator arg must be literal"); + } + + final Aggregator aggregator = getAggregator(Aggregate.valueOf( + StringUtils.toUpperCase((String) args.get(0).getLiteralValue()) + )); + + ExprEval arg = args.get(1).eval(bindings); + if (arg.value() == null) { - return null; + throw JsonMergeAggrExprMacro.this.validationFailed( + "invalid input expected %s but got %s instead", + ExpressionType.STRING, + arg.type() + ); } - if (arg.type().is(ExprType.STRING)) { - return arg.asString(); - } - - if (arg.type().is(ExprType.COMPLEX)) { + Object source; + try { + String str = getArgAsJson(JsonMergeAggrExprMacro.this, jsonMapper, arg); + source = jsonMapper.readValue(str, Object.class); + } + catch (JsonProcessingException e) { + throw JsonMergeAggrExprMacro.this.processingFailed(e, + "bad string input [%s]", arg.asString()); + } + + for (int i = 2; i < args.size(); i++) { + ExprEval argSub = args.get(i).eval(bindings); + try { - return jsonMapper.writeValueAsString(unwrap(arg)); + String str = getArgAsJson(JsonMergeAggrExprMacro.this, jsonMapper, argSub); + if (str != null) { + Object target = jsonMapper.readValue(str, Object.class); + merge(source, target, aggregator); + } } catch (JsonProcessingException e) { - throw JsonMergeExprMacro.this.processingFailed(e, "bad complex input [%s]", arg.asString()); - } - } - - throw JsonMergeExprMacro.this.validationFailed( - "invalid input expected %s but got %s instead", - ExpressionType.STRING, - arg.type() - ); + throw JsonMergeAggrExprMacro.this.processingFailed(e, "bad string input [%s]", argSub.asString()); + } + } + return ExprEval.ofComplex(ExpressionType.NESTED_DATA, source); + } + + @Nullable + @Override + public ExpressionType getOutputType(InputBindingInspector inspector) + { + return ExpressionType.NESTED_DATA; } } - return new ParseJsonExpr(args); + return new JsonMergeAggExpr(args); + } + + @Override + public String name() + { + return NAME; } + + } + public static class ToJsonStringExprMacro implements ExprMacroTable.ExprMacro { public static final String NAME = "to_json_string"; @@ -853,4 +1035,31 @@ static List getJsonPathPartsFromLiteral(NamedFunction fn, Expr a ); return parts; } + + + static String getArgAsJson(NamedFunction fn, ObjectMapper jsonMapper, ExprEval arg) + { + if (arg.value() == null) { + return null; + } + + if (arg.type().is(ExprType.STRING)) { + return arg.asString(); + } + + if (arg.type().is(ExprType.COMPLEX)) { + try { + return jsonMapper.writeValueAsString(unwrap(arg)); + } + catch (JsonProcessingException e) { + throw fn.processingFailed(e, "bad complex input [%s]", arg.asString()); + } + } + + throw fn.validationFailed( + "invalid input expected %s but got %s instead", + ExpressionType.STRING, + arg.type() + ); + } } 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 c9fe553469a71..46a88eaf2b50f 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 @@ -50,6 +50,7 @@ public class NestedDataExpressionsTest extends InitializedNullHandlingTest new NestedDataExpressions.JsonKeysExprMacro(), new NestedDataExpressions.JsonObjectExprMacro(), new NestedDataExpressions.JsonMergeExprMacro(JSON_MAPPER), + new NestedDataExpressions.JsonMergeAggrExprMacro(JSON_MAPPER), new NestedDataExpressions.JsonValueExprMacro(), new NestedDataExpressions.JsonQueryExprMacro(), new NestedDataExpressions.JsonQueryArrayExprMacro(), @@ -113,6 +114,103 @@ public void testJsonObjectExpression() Assert.assertEquals(ImmutableMap.of("a", "hello", "b", "world"), ((Map) eval.value()).get("y")); } + @Test + public void testJsonMergeAggrExpression() throws JsonProcessingException + { + Expr expr = Parser.parse("json_merge_aggr('ADD','{\"a\":\"x\"}','{\"b\":\"y\"}')", MACRO_TABLE); + ExprEval eval = expr.eval(inputBindings); + Assert.assertEquals("{\"a\":\"x\",\"b\":\"y\"}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('ADD','{\"a\":2}',json_merge_aggr('ADD','{\"a\":3}','{\"a\":4}'))", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"a\":9}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('MULTIPLY','{\"a\":2}',json_merge('{\"a\":3}','{\"a\":4}'))", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"a\":8}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('ADD','{\"a\":2.1}',json_merge('{\"a\":3.2}','{\"a\":4.3}'))", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"a\":6.4}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('ADD','{\"a\":2.1}',json_merge('{\"a\":3}','{\"a\":4}'))", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"a\":6.1}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('ADD','{\"a\":{\"b\":2}}','{\"a\":{\"b\":3}}')", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"a\":{\"b\":5}}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('ADD','{\"a\":{\"b\":2}}','{\"a\":{\"c\":3}}')", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"a\":{\"b\":2,\"c\":3}}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('ADD',json_object('a', 2),json_object('a', 3))", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"a\":5}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('ADD',json_object('a', '2'),json_object('a', '3'))", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"a\":\"3\"}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('ADD','{\"key\": [1,2]}', '{\"key\": [3,4]}')", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"key\":[1,2,3,4]}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('MULTIPLY','{\"key\": [1,2]}', '{\"key\": [3,4]}')", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"key\":[1,2,3,4]}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('ADD','{\"key\":[1,2]}', '{\"key\":3}')", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"key\":3}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('ADD','[1,2]', '[3,4]')", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("[1,2,3,4]", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge_aggr('ADD','[1,2]', '[3,4,5]')", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("[1,2,3,4,5]", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + } + + @Test + public void testJsonMergeAggrOverflow() throws JsonProcessingException + { + Expr.ObjectBinding input1 = InputBindings.forInputSuppliers( + new ImmutableMap.Builder>() + .put("attr", InputBindings.inputSupplier(ExpressionType.NESTED_DATA, () -> ImmutableMap.of("key", "a", "value", 3))) + .build() + ); + Expr.ObjectBinding input2 = InputBindings.forInputSuppliers( + new ImmutableMap.Builder>() + .put("attr", InputBindings.inputSupplier(ExpressionType.NESTED_DATA, () -> ImmutableMap.of("key", "a", "value", 5))) + .build() + ); + + Expr expr = Parser.parse("json_merge_aggr('ADD', json_object(), json_object(json_value(attr, '$.key'), json_value(attr, '$.value')))", MACRO_TABLE); + ExprEval eval = expr.eval(input1); + Assert.assertEquals("{\"a\":3}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + eval = expr.eval(input2); + Assert.assertEquals("{\"a\":5}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + } + @Test public void testJsonMergeExpression() 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 9c6cfb0448bbd..a07f9fd483082 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 @@ -826,6 +826,53 @@ public DruidExpression toDruidExpression( } } + public static class JsonMergeAggrOperatorConversion implements SqlOperatorConversion + { + private static final String FUNCTION_NAME = "json_merge_aggr"; + private static final SqlFunction SQL_FUNCTION = OperatorConversions + .operatorBuilder(FUNCTION_NAME) + .operandTypeChecker(OperandTypes.variadic(SqlOperandCountRanges.from(2))) + .operandTypeInference((callBinding, returnType, operandTypes) -> { + RelDataTypeFactory typeFactory = callBinding.getTypeFactory(); + operandTypes[0] = typeFactory.createSqlType(SqlTypeName.VARCHAR); + for (int i = 1; i < operandTypes.length; i++) { + operandTypes[i] = typeFactory.createTypeWithNullability( + typeFactory.createSqlType(SqlTypeName.ANY), + true + ); + } + }) + .returnTypeInference(NestedDataOperatorConversions.NESTED_RETURN_TYPE_INFERENCE) + .functionCategory(SqlFunctionCategory.SYSTEM) + .build(); + + @Override + public SqlOperator calciteOperator() + { + return SQL_FUNCTION; + } + + @Nullable + @Override + public DruidExpression toDruidExpression( + PlannerContext plannerContext, + RowSignature rowSignature, + RexNode rexNode + ) + { + return OperatorConversions.convertCall( + plannerContext, + rowSignature, + rexNode, + druidExpressions -> DruidExpression.ofExpression( + ColumnType.NESTED_DATA, + DruidExpression.functionCall("json_merge_aggr"), + druidExpressions + ) + ); + } + } + public static class ToJsonStringOperatorConversion implements SqlOperatorConversion { private static final String FUNCTION_NAME = "to_json_string"; 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 0fb1c9fb9ff02..f0e3a6267a183 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 @@ -354,6 +354,7 @@ public class DruidOperatorTable implements SqlOperatorTable .add(new NestedDataOperatorConversions.JsonValueReturningArrayVarcharOperatorConversion()) .add(new NestedDataOperatorConversions.JsonObjectOperatorConversion()) .add(new NestedDataOperatorConversions.JsonMergeOperatorConversion()) + .add(new NestedDataOperatorConversions.JsonMergeAggrOperatorConversion()) .add(new NestedDataOperatorConversions.ToJsonStringOperatorConversion()) .add(new NestedDataOperatorConversions.ParseJsonOperatorConversion()) .add(new NestedDataOperatorConversions.TryParseJsonOperatorConversion()) 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 ec4d5812dd5d7..517b0526adfb5 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 @@ -4972,6 +4972,55 @@ public void testJsonMerging() ); } + @Test + public void testJsonAggrMerging() + { + testQuery( + "SELECT " + + "JSON_MERGE_AGGR('ADD','{\"x\":100.10}',JSON_OBJECT(KEY 'x' VALUE JSON_VALUE(nest, '$.x' RETURNING DECIMAL)))\n" + + "FROM druid.nested", + ImmutableList.of( + Druids.newScanQueryBuilder() + .dataSource(DATA_SOURCE) + .intervals(querySegmentSpec(Filtration.eternity())) + .virtualColumns( + new ExpressionVirtualColumn( + "v0", + "json_merge_aggr('ADD','{\\u0022x\\u0022:100.10}',json_object('x',\"v1\"))", + ColumnType.NESTED_DATA, + queryFramework().macroTable() + ), + new NestedFieldVirtualColumn( + "nest", + "v1", + ColumnType.DOUBLE, + ImmutableList.of( + new NestedPathField("x") + ), + false, + null, + false + ) + ) + .columns("v0") + .resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST) + .build() + ), + ImmutableList.of( + new Object[]{"{\"x\":200.1}"}, + new Object[]{"{\"x\":null}"}, + new Object[]{"{\"x\":300.1}"}, + new Object[]{"{\"x\":null}"}, + new Object[]{"{\"x\":null}"}, + new Object[]{"{\"x\":200.1}"}, + new Object[]{"{\"x\":null}"} + ), + RowSignature.builder() + .add("EXPR$0", ColumnType.NESTED_DATA) + .build() + ); + } + @Test public void testCompositionTyping() { diff --git a/website/.spelling b/website/.spelling index ebfbaa6a87551..a07e427badc39 100644 --- a/website/.spelling +++ b/website/.spelling @@ -386,6 +386,7 @@ json_query json_query_array json_value json_merge +json_merge_aggr karlkfi kbps kerberos