This is an automated email from the ASF dual-hosted git repository.
karan 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 483f4046269 Add SQL-compliant handling for `json_merge()` when
arguments are null (#17983)
483f4046269 is described below
commit 483f4046269a4640484c916d04af1fb982416322
Author: Abhishek Radhakrishnan <[email protected]>
AuthorDate: Tue May 6 22:15:48 2025 -0700
Add SQL-compliant handling for `json_merge()` when arguments are null
(#17983)
* 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.
* One more doc change.
---
docs/querying/sql-functions.md | 1 +
docs/querying/sql-json-functions.md | 5 ++-
.../query/expression/NestedDataExpressions.java | 27 +++++-------
.../expression/NestedDataExpressionsTest.java | 51 +++++++++++++++++++++-
4 files changed, 66 insertions(+), 18 deletions(-)
diff --git a/docs/querying/sql-functions.md b/docs/querying/sql-functions.md
index 44c849794bb..6fa0c162b03 100644
--- a/docs/querying/sql-functions.md
+++ b/docs/querying/sql-functions.md
@@ -3235,6 +3235,7 @@ Returns the following:
## JSON_MERGE
Merges two or more JSON `STRING` or `COMPLEX<json>` expressions into one,
preserving the rightmost value when there are key overlaps.
+Returns `NULL` if any argument is `NULL`.
The function always returns a `COMPLEX<json>` object.
* **Syntax:** `JSON_MERGE(expr1, expr2[, expr3 ...])`
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]