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]

Reply via email to