Skip to content

Commit 175d199

Browse files
authored
[BugFix] Fix json_set crash and json_delete no-op with $.key paths (#5167) (#5339)
convertToJsonPath() unconditionally prepends "$." to input paths. When users pass standard JSONPath syntax like "$.name", the path becomes "$.$.name" (double-prefixed), causing json_set to crash with HTTP 500 and json_delete to silently return unchanged JSON. Strip any existing "$." or "$" prefix before processing to prevent double-prefixing while preserving the curly-brace-to-bracket conversion. Signed-off-by: Heng Qian <qianheng@amazon.com>
1 parent c26897d commit 175d199

4 files changed

Lines changed: 156 additions & 0 deletions

File tree

core/src/main/java/org/opensearch/sql/expression/function/jsonUDF/JsonUtils.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ public class JsonUtils {
2323
public static String convertToJsonPath(String input) {
2424
if (input == null || input.isEmpty()) return "$";
2525

26+
// Strip leading "$." or "$" to avoid double-prefixing (issue #5167)
27+
if (input.startsWith("$.")) {
28+
input = input.substring(2);
29+
} else if (input.startsWith("$")) {
30+
input = input.substring(1);
31+
}
32+
33+
if (input.isEmpty()) return "$";
34+
2635
StringBuilder sb = new StringBuilder("$.");
2736
int i = 0;
2837
while (i < input.length()) {

core/src/test/java/org/opensearch/sql/expression/json/JsonFunctionsTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import org.opensearch.sql.exception.SemanticCheckException;
2020
import org.opensearch.sql.expression.DSL;
2121
import org.opensearch.sql.expression.LiteralExpression;
22+
import org.opensearch.sql.expression.function.jsonUDF.JsonDeleteFunctionImpl;
23+
import org.opensearch.sql.expression.function.jsonUDF.JsonSetFunctionImpl;
2224
import org.opensearch.sql.expression.function.jsonUDF.JsonUtils;
2325

2426
@ExtendWith(MockitoExtension.class)
@@ -65,6 +67,18 @@ void test_convertToJsonPath() {
6567
assertEquals(targetJsonPath, convertedJsonPath);
6668
}
6769

70+
@Test
71+
void test_convertToJsonPathWithDollarPrefix() {
72+
// Issue #5167: paths already starting with $ or $. should not be double-prefixed
73+
assertEquals("$.name", convertToJsonPath("$.name"));
74+
assertEquals("$.a.b.c", convertToJsonPath("$.a.b.c"));
75+
assertEquals("$.[*]", convertToJsonPath("$.[*]"));
76+
assertEquals("$.a[2].c", convertToJsonPath("$.a[2].c"));
77+
assertEquals("$.[3].bc[*].d[1]", convertToJsonPath("$.[3].bc[*].d[1]"));
78+
// Bare $ should return $
79+
assertEquals("$", convertToJsonPath("$"));
80+
}
81+
6882
@Test
6983
void test_convertToJsonPathWithWrongPath() {
7084
IllegalArgumentException e =
@@ -100,6 +114,23 @@ void test_jsonPathExpand() {
100114
assertEquals(expandJsonPath(node, candidate4), target4);
101115
}
102116

117+
@Test
118+
void test_jsonSetWithDollarPrefixedPath() {
119+
// Issue #5167: json_set with $.key path should work correctly
120+
Object result =
121+
JsonSetFunctionImpl.eval(
122+
"{\"name\":\"alice\",\"scores\":[90,85,92]}", "$.name", "modified_alice");
123+
assertEquals("{\"name\":\"modified_alice\",\"scores\":[90,85,92]}", result);
124+
}
125+
126+
@Test
127+
void test_jsonDeleteWithDollarPrefixedPath() throws Exception {
128+
// Issue #5167: json_delete with $.key path should remove the key
129+
Object result =
130+
JsonDeleteFunctionImpl.eval("{\"name\":\"alice\",\"scores\":[90,85,92]}", "$.name");
131+
assertEquals("{\"scores\":[90,85,92]}", result);
132+
}
133+
103134
@Test
104135
void test_jsonPathExpandAtArray() {
105136
String jsonStr = "[{\"c\": 1}, {\"c\": 1}, {\"c\": 1}]";

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJsonBuiltinFunctionIT.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,38 @@ public void testJsonSetPartialSet() throws IOException {
296296
verifyDataRows(actual, rows("{\"a\":[{\"b\":1},{\"b\":{\"c\":\"3\"}}]}"));
297297
}
298298

299+
@Test
300+
public void testJsonSetWithDollarPrefixedPath() throws IOException {
301+
// Issue #5167: json_set with $.key path should not double-prefix
302+
JSONObject actual =
303+
executeQuery(
304+
String.format(
305+
"source=%s | eval a"
306+
+ " =json_set('{\\\"name\\\":\\\"alice\\\",\\\"scores\\\":[90,85,92]}',"
307+
+ " '$.name', 'modified_alice')| fields a | head 1",
308+
TEST_INDEX_PEOPLE2));
309+
310+
verifySchema(actual, schema("a", "string"));
311+
312+
verifyDataRows(actual, rows("{\"name\":\"modified_alice\",\"scores\":[90,85,92]}"));
313+
}
314+
315+
@Test
316+
public void testJsonDeleteWithDollarPrefixedPath() throws IOException {
317+
// Issue #5167: json_delete with $.key path should remove the key
318+
JSONObject actual =
319+
executeQuery(
320+
String.format(
321+
"source=%s | eval a"
322+
+ " =json_delete('{\\\"name\\\":\\\"alice\\\",\\\"scores\\\":[90,85,92]}',"
323+
+ " '$.name')| fields a | head 1",
324+
TEST_INDEX_PEOPLE2));
325+
326+
verifySchema(actual, schema("a", "string"));
327+
328+
verifyDataRows(actual, rows("{\"scores\":[90,85,92]}"));
329+
}
330+
299331
@Test
300332
public void testJsonDelete() throws IOException {
301333
JSONObject actual =
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
setup:
2+
- do:
3+
query.settings:
4+
body:
5+
transient:
6+
plugins.calcite.enabled: true
7+
8+
- do:
9+
indices.create:
10+
index: issue5167
11+
body:
12+
settings:
13+
number_of_shards: 1
14+
number_of_replicas: 0
15+
mappings:
16+
properties:
17+
int_field:
18+
type: integer
19+
json_data:
20+
type: keyword
21+
22+
- do:
23+
bulk:
24+
refresh: true
25+
body:
26+
- '{"index": {"_index": "issue5167", "_id": "1"}}'
27+
- '{"int_field": 42, "json_data": "{\"name\":\"alice\",\"scores\":[90,85,92]}"}'
28+
29+
---
30+
teardown:
31+
- do:
32+
indices.delete:
33+
index: issue5167
34+
ignore_unavailable: true
35+
- do:
36+
query.settings:
37+
body:
38+
transient:
39+
plugins.calcite.enabled: false
40+
41+
---
42+
"Issue 5167: json_set with $.key path should update the value":
43+
- skip:
44+
features:
45+
- headers
46+
- do:
47+
headers:
48+
Content-Type: 'application/json'
49+
ppl:
50+
body:
51+
query: "source=issue5167 | where int_field = 42 | eval modified = json_set(json_data, '$.name', 'modified_alice') | fields modified"
52+
53+
- match: { total: 1 }
54+
- match: { datarows: [ [ "{\"name\":\"modified_alice\",\"scores\":[90,85,92]}" ] ] }
55+
56+
---
57+
"Issue 5167: json_delete with $.key path should remove the key":
58+
- skip:
59+
features:
60+
- headers
61+
- do:
62+
headers:
63+
Content-Type: 'application/json'
64+
ppl:
65+
body:
66+
query: "source=issue5167 | where int_field = 42 | eval deleted = json_delete(json_data, '$.name') | fields deleted"
67+
68+
- match: { total: 1 }
69+
- match: { datarows: [ [ "{\"scores\":[90,85,92]}" ] ] }
70+
71+
---
72+
"Issue 5167: json_set with unprefixed path still works":
73+
- skip:
74+
features:
75+
- headers
76+
- do:
77+
headers:
78+
Content-Type: 'application/json'
79+
ppl:
80+
body:
81+
query: "source=issue5167 | where int_field = 42 | eval modified = json_set(json_data, 'name', 'bob') | fields modified"
82+
83+
- match: { total: 1 }
84+
- match: { datarows: [ [ "{\"name\":\"bob\",\"scores\":[90,85,92]}" ] ] }

0 commit comments

Comments
 (0)