This is an automated email from the ASF dual-hosted git repository. abhishekrb pushed a commit to branch json_merge_null_bug in repository https://gitbox.apache.org/repos/asf/druid.git
commit cbf03782bd1c995714c6cc9e7d1247be7af68c85 Author: Abhishek Balaji Radhakrishnan <[email protected]> AuthorDate: Tue May 6 16:49:51 2025 -0700 Add SQL-compliant handling of json_merge() function with null expressions. The json_merge() function fail with a query out with JSON_MERGE(null, null): Function[json_merge] invalid input expected STRING but got STRING instead The behavior in both mysql and postgres is that the function returns NULL if any argument is NULL. This change adds that to align and make it SQL-compliant. --- docs/querying/sql-json-functions.md | 5 ++- .../query/expression/NestedDataExpressions.java | 27 +++++------- .../expression/NestedDataExpressionsTest.java | 51 +++++++++++++++++++++- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/docs/querying/sql-json-functions.md b/docs/querying/sql-json-functions.md index 892c348b5c8..ea090ae2c28 100644 --- a/docs/querying/sql-json-functions.md +++ b/docs/querying/sql-json-functions.md @@ -34,10 +34,11 @@ Druid supports nested columns, which provide optimized storage and indexes for n You can use the following JSON functions to extract, transform, and create `COMPLEX<json>` objects. + | Function | Notes | | --- | --- | |`JSON_KEYS(expr, path)`| Returns an array of field names from `expr` at the specified `path`.| -|`JSON_MERGE(expr1, expr2[, expr3 ...])`| Merges two or more JSON `STRING` or `COMPLEX<json>` values into one, preserving the rightmost value when there are key overlaps. Always returns a `COMPLEX<json>` object.| +|`JSON_MERGE(expr1, expr2[, expr3 ...])`| Merges two or more JSON `STRING` or `COMPLEX<json>` values into one, preserving the rightmost value when there are key overlaps. Returns `NULL` if any argument is `NULL`. Always returns a `COMPLEX<json>` object.| |`JSON_OBJECT(KEY expr1 VALUE expr2[, KEY expr3 VALUE expr4, ...])` | Constructs a new `COMPLEX<json>` object from one or more expressions. The `KEY` expressions must evaluate to string types. The `VALUE` expressions can be composed of any input type, including other `COMPLEX<json>` objects. The function 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<json>` value from `expr`, at the specified `path`. | @@ -71,4 +72,4 @@ Consider the following example input JSON: - For a key that contains an array, to return the entire array:<br /> `$['y']` -> `[1, 2, 3]` - For a key that contains an array, to return an item in the array:<br /> - `$.y[1]` -> `2` \ No newline at end of file + `$.y[1]` -> `2` 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 0926ce78e0a..5a384994f69 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 @@ -138,18 +138,13 @@ public class NestedDataExpressions public ExprEval eval(ObjectBinding bindings) { ExprEval arg = args.get(0).eval(bindings); - Object obj; - - if (arg.value() == null) { - throw JsonMergeExprMacro.this.validationFailed( - "invalid input expected %s but got %s instead", - ExpressionType.STRING, - arg.type() - ); + String argAsJson = getArgAsJson(arg); + if (argAsJson == null) { + return ExprEval.ofComplex(ExpressionType.NESTED_DATA, null); } - + Object obj; try { - obj = jsonMapper.readValue(getArgAsJson(arg), Object.class); + obj = jsonMapper.readValue(argAsJson, Object.class); } catch (JsonProcessingException e) { throw JsonMergeExprMacro.this.processingFailed(e, "bad string input [%s]", arg.asString()); @@ -159,12 +154,13 @@ public class NestedDataExpressions for (int i = 1; i < args.size(); i++) { ExprEval argSub = args.get(i).eval(bindings); - + String str = getArgAsJson(argSub); + if (str == null) { + return ExprEval.ofComplex(ExpressionType.NESTED_DATA, null); + } try { - String str = getArgAsJson(argSub); - if (str != null) { - obj = updater.readValue(str); - } + obj = updater.readValue(str); + updater = jsonMapper.readerForUpdating(obj); } catch (JsonProcessingException e) { throw JsonMergeExprMacro.this.processingFailed(e, "bad string input [%s]", argSub.asString()); @@ -181,6 +177,7 @@ public class NestedDataExpressions return ExpressionType.NESTED_DATA; } + @Nullable private String getArgAsJson(ExprEval arg) { if (arg.value() == null) { 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 269f4b63372..b2267b4b06d 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 @@ -123,7 +123,7 @@ public class NestedDataExpressionsTest extends InitializedNullHandlingTest expr = Parser.parse("json_merge('{\"a\":\"x\"}', null)", MACRO_TABLE); eval = expr.eval(inputBindings); - Assert.assertEquals("{\"a\":\"x\"}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value())); Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); expr = Parser.parse("json_merge('{\"a\":\"x\"}','{\"b\":\"y\"}','{\"c\":[1,2,3]}')", MACRO_TABLE); @@ -147,6 +147,55 @@ public class NestedDataExpressionsTest extends InitializedNullHandlingTest Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); } + @Test + public void testJsonMergeWithNullAndEmptyExpressions() throws JsonProcessingException + { + Expr expr = Parser.parse("json_merge(null, null)", MACRO_TABLE); + ExprEval eval = expr.eval(inputBindings); + Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge(null, '{\"a\":\"x\"}')", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge('{\"a\":\"x\"}', null)", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge('{\"a\":\"x\"}', null, null, null)", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge('{\"a\":\"x\"}', null, null, json_object())", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge(json_object(), json_object(), json_object())", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge(json_object(), json_object(), json_object(), null)", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge(json_object(), json_object(), json_object(), coalesce(null, '{}'))", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + + expr = Parser.parse("json_merge(coalesce(null, '{}'), '{\"a\":\"x\"}')", MACRO_TABLE); + eval = expr.eval(inputBindings); + Assert.assertEquals("{\"a\":\"x\"}", JSON_MAPPER.writeValueAsString(eval.value())); + Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type()); + } + @Test public void testJsonMergeOverflow() throws JsonProcessingException { --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
