Skip to content

Commit 483f404

Browse files
authored
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.
1 parent bb771b3 commit 483f404

File tree

4 files changed

+66
-18
lines changed

4 files changed

+66
-18
lines changed

docs/querying/sql-functions.md

+1
Original file line numberDiff line numberDiff line change
@@ -3235,6 +3235,7 @@ Returns the following:
32353235
## JSON_MERGE
32363236

32373237
Merges two or more JSON `STRING` or `COMPLEX<json>` expressions into one, preserving the rightmost value when there are key overlaps.
3238+
Returns `NULL` if any argument is `NULL`.
32383239
The function always returns a `COMPLEX<json>` object.
32393240

32403241
* **Syntax:** `JSON_MERGE(expr1, expr2[, expr3 ...])`

docs/querying/sql-json-functions.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ Druid supports nested columns, which provide optimized storage and indexes for n
3434

3535
You can use the following JSON functions to extract, transform, and create `COMPLEX<json>` objects.
3636

37+
3738
| Function | Notes |
3839
| --- | --- |
3940
|`JSON_KEYS(expr, path)`| Returns an array of field names from `expr` at the specified `path`.|
40-
|`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.|
41+
|`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.|
4142
|`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, ...])`.|
4243
|`JSON_PATHS(expr)`| Returns an array of all paths which refer to literal values in `expr` in JSONPath format. |
4344
|`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:
7172
- For a key that contains an array, to return the entire array:<br />
7273
`$['y']` -> `[1, 2, 3]`
7374
- For a key that contains an array, to return an item in the array:<br />
74-
`$.y[1]` -> `2`
75+
`$.y[1]` -> `2`

processing/src/main/java/org/apache/druid/query/expression/NestedDataExpressions.java

+12-15
Original file line numberDiff line numberDiff line change
@@ -138,18 +138,13 @@ public ParseJsonExpr(List<Expr> args)
138138
public ExprEval eval(ObjectBinding bindings)
139139
{
140140
ExprEval arg = args.get(0).eval(bindings);
141-
Object obj;
142-
143-
if (arg.value() == null) {
144-
throw JsonMergeExprMacro.this.validationFailed(
145-
"invalid input expected %s but got %s instead",
146-
ExpressionType.STRING,
147-
arg.type()
148-
);
141+
String argAsJson = getArgAsJson(arg);
142+
if (argAsJson == null) {
143+
return ExprEval.ofComplex(ExpressionType.NESTED_DATA, null);
149144
}
150-
145+
Object obj;
151146
try {
152-
obj = jsonMapper.readValue(getArgAsJson(arg), Object.class);
147+
obj = jsonMapper.readValue(argAsJson, Object.class);
153148
}
154149
catch (JsonProcessingException e) {
155150
throw JsonMergeExprMacro.this.processingFailed(e, "bad string input [%s]", arg.asString());
@@ -159,12 +154,13 @@ public ExprEval eval(ObjectBinding bindings)
159154

160155
for (int i = 1; i < args.size(); i++) {
161156
ExprEval argSub = args.get(i).eval(bindings);
162-
157+
String str = getArgAsJson(argSub);
158+
if (str == null) {
159+
return ExprEval.ofComplex(ExpressionType.NESTED_DATA, null);
160+
}
163161
try {
164-
String str = getArgAsJson(argSub);
165-
if (str != null) {
166-
obj = updater.readValue(str);
167-
}
162+
obj = updater.readValue(str);
163+
updater = jsonMapper.readerForUpdating(obj);
168164
}
169165
catch (JsonProcessingException e) {
170166
throw JsonMergeExprMacro.this.processingFailed(e, "bad string input [%s]", argSub.asString());
@@ -181,6 +177,7 @@ public ExpressionType getOutputType(InputBindingInspector inspector)
181177
return ExpressionType.NESTED_DATA;
182178
}
183179

180+
@Nullable
184181
private String getArgAsJson(ExprEval arg)
185182
{
186183
if (arg.value() == null) {

processing/src/test/java/org/apache/druid/query/expression/NestedDataExpressionsTest.java

+50-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public void testJsonMergeExpression() throws JsonProcessingException
123123

124124
expr = Parser.parse("json_merge('{\"a\":\"x\"}', null)", MACRO_TABLE);
125125
eval = expr.eval(inputBindings);
126-
Assert.assertEquals("{\"a\":\"x\"}", JSON_MAPPER.writeValueAsString(eval.value()));
126+
Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value()));
127127
Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type());
128128

129129
expr = Parser.parse("json_merge('{\"a\":\"x\"}','{\"b\":\"y\"}','{\"c\":[1,2,3]}')", MACRO_TABLE);
@@ -147,6 +147,55 @@ public void testJsonMergeExpression() throws JsonProcessingException
147147
Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type());
148148
}
149149

150+
@Test
151+
public void testJsonMergeWithNullAndEmptyExpressions() throws JsonProcessingException
152+
{
153+
Expr expr = Parser.parse("json_merge(null, null)", MACRO_TABLE);
154+
ExprEval eval = expr.eval(inputBindings);
155+
Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value()));
156+
Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type());
157+
158+
expr = Parser.parse("json_merge(null, '{\"a\":\"x\"}')", MACRO_TABLE);
159+
eval = expr.eval(inputBindings);
160+
Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value()));
161+
Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type());
162+
163+
expr = Parser.parse("json_merge('{\"a\":\"x\"}', null)", MACRO_TABLE);
164+
eval = expr.eval(inputBindings);
165+
Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value()));
166+
Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type());
167+
168+
expr = Parser.parse("json_merge('{\"a\":\"x\"}', null, null, null)", MACRO_TABLE);
169+
eval = expr.eval(inputBindings);
170+
Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value()));
171+
Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type());
172+
173+
expr = Parser.parse("json_merge('{\"a\":\"x\"}', null, null, json_object())", MACRO_TABLE);
174+
eval = expr.eval(inputBindings);
175+
Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value()));
176+
Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type());
177+
178+
expr = Parser.parse("json_merge(json_object(), json_object(), json_object())", MACRO_TABLE);
179+
eval = expr.eval(inputBindings);
180+
Assert.assertEquals("{}", JSON_MAPPER.writeValueAsString(eval.value()));
181+
Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type());
182+
183+
expr = Parser.parse("json_merge(json_object(), json_object(), json_object(), null)", MACRO_TABLE);
184+
eval = expr.eval(inputBindings);
185+
Assert.assertEquals("null", JSON_MAPPER.writeValueAsString(eval.value()));
186+
Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type());
187+
188+
expr = Parser.parse("json_merge(json_object(), json_object(), json_object(), coalesce(null, '{}'))", MACRO_TABLE);
189+
eval = expr.eval(inputBindings);
190+
Assert.assertEquals("{}", JSON_MAPPER.writeValueAsString(eval.value()));
191+
Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type());
192+
193+
expr = Parser.parse("json_merge(coalesce(null, '{}'), '{\"a\":\"x\"}')", MACRO_TABLE);
194+
eval = expr.eval(inputBindings);
195+
Assert.assertEquals("{\"a\":\"x\"}", JSON_MAPPER.writeValueAsString(eval.value()));
196+
Assert.assertEquals(ExpressionType.NESTED_DATA, eval.type());
197+
}
198+
150199
@Test
151200
public void testJsonMergeOverflow() throws JsonProcessingException
152201
{

0 commit comments

Comments
 (0)