This is an automated email from the ASF dual-hosted git repository.

mrhhsg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new ca632920670 [fix](be) Keep following siblings when inserting nested 
JSON members (#69288)
ca632920670 is described below

commit ca632920670d31bf74bccc2d179d966e2efcf8a6
Author: Jerry Hu <[email protected]>
AuthorDate: Tue Oct 20 18:30:12 2026 +0800

    [fix](be) Keep following siblings when inserting nested JSON members 
(#69288)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary:
    
    `JSON_SET` / `JSON_INSERT` (and the `jsonb_set` / `jsonb_insert`
    spellings) silently dropped every member that followed a nested object
    when they inserted a missing member into that object.
    
    Reproduction:
    
    ```sql
    mysql> SELECT JSON_SET('{"a":{},"b":2}', '$.a.c', 1);
    +-----------------------------------------+
    | json_set('{"a":{},"b":2}', '$.a.c', 1)  |
    +-----------------------------------------+
    | {"a":{"c":1}}                           |
    +-----------------------------------------+
    ```
    
    The sibling member `"b"` is lost and no error is reported. The same
    happens with a non-empty nested object (`{"a":{"d":4},"b":2}` with
    `$.a.c` returns `{"a":{"d":4,"c":1}}`) and at deeper nesting
    (`{"a":{"b":{}},"x":1}` with `$.a.b.c` returns `{"a":{"b":{"c":1}}}`,
    dropping `"x"`).
    
    Root cause: in `FunctionJsonbModify::write_json_value`
    (`be/src/exprs/function/function_jsonb.cpp`), the object branch for
    "last parent, member not present" opened and closed an extra object
    inside the object that was already open:
    
    - The extra `writeStartObject()` is rejected by the `JsonbWriter` state
    machine: the current object is already open and waiting for a key, and
    `verifyValueState()` only accepts `Object + WS_Key` or `Array +
    WS_Value`. Its return value is ignored, so the call is a silent no-op.
    - The extra `writeEndObject()` succeeds and closes the current object
    early. The unconditional `writeEndObject()` at the end of the branch
    then closes the enclosing object as well, so when the caller resumes
    writing the remaining members of that object, its `writeKey` /
    `writeValue` calls fail and their return values are ignored too.
    - The writer's size counters have already been backfilled for the
    prematurely closed object, so the result document passes validation and
    the query succeeds with the fields missing.
    
    Fix: remove the extra `writeStartObject()` / `writeEndObject()` pair and
    append the new key/value directly to the already-open object; the
    existing paired `writeEndObject()` closes it normally. After the fix:
    
    ```sql
    SELECT JSON_SET('{"a":{},"b":2}', '$.a.c', 1);          -- 
{"a":{"c":1},"b":2}
    SELECT JSON_SET('{"a":{"d":4},"b":2}', '$.a.c', 1);     -- 
{"a":{"d":4,"c":1},"b":2}
    SELECT JSON_SET('{"a":{"b":{}},"x":1}', '$.a.b.c', 1);  -- 
{"a":{"b":{"c":1}},"x":1}
    ```
    
    Only the "nested object followed by sibling members" shape changes.
    Inserts at the top level, inserts into an object inside an array,
    `JSON_REPLACE` and `JSON_REMOVE` keep their current output and are
    covered by control cases in the new unit test. The change does not
    affect the JSONB binary format, RPC, metadata, configuration or the FE,
    and existing data needs no migration.
    
    ### Release note
    
    Fix `JSON_SET` and `JSON_INSERT` silently dropping the sibling members
    that follow a nested object when a new member is inserted into that
    nested object.
    
    ### Check List (For Author)
    
    - Test
        - [x] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason (add your reason)
    
    - Behavior changed:
        - [ ] No.
    - [x] Yes. `JSON_SET` / `JSON_INSERT` now preserve all members of the
    enclosing objects instead of dropping the members that follow a nested
    object. This is the reported bug fix; top-level inserts, inserts into an
    object inside an array, `JSON_REPLACE` and `JSON_REMOVE` are unchanged
    and covered by control cases.
    
    - Does this need documentation?
        - [x] No.
    
    ### Testing details
    
    **Unit test (BE)**
    
    - New test
    `FunctionJsonbTEST.JsonbModifyNestedMissingMemberKeepsFollowingSiblings`
    in `be/test/exprs/function/function_jsonb_test.cpp`. It covers 7 data
    rows (empty nested object, non-empty nested object, sibling written
    before the target, two-level nesting, mixed-type siblings, empty key,
    and an object element inside an array as a control) against all four
    names `jsonb_insert`, `jsonb_set`, `json_insert`, `json_set`, plus a
    `jsonb_replace` control.
    - `./run-be-ut.sh --run
    
--filter=FunctionJsonbTEST.JsonbModifyNestedMissingMemberKeepsFollowingSiblings
    -j 48` — PASSED (1 test).
    - `./run-be-ut.sh --run --filter=FunctionJsonbTEST.* -j 48` — PASSED
    (20/20 tests).
    - Verification against the unfixed code: with the product change
    temporarily reverted, the new test reports 20 assertion failures (5 data
    rows x 4 function names), for example `result: {"a":{"c":1}}, expected
    result: {"a":{"c":1},"b":2}`. The two control rows (sibling written
    before the target; object inside an array) and the `jsonb_replace` case
    still pass, which pins the failure to the changed branch.
    
    **Regression test**
    
    - `test_query_json_insert` and `test_query_json_set`
    (`query_p0/sql_functions/json_function`) — both suites pass. Each suite
    gains 5 cases: 4 constant SQL cases (minimal reproduction, non-empty
    nested object, sibling-before control, two-level nesting) and 1 case
    over a real `json` column with `ORDER BY id`.
    - The checked-in `.out` files were generated by the regression framework
    (not written by hand) and pass again on a re-run without regenerating
    them.
    
    **Formatting / static checks**
    
    - `build-support/clang-format.sh` (clang-format 16): exit code 0, no
    file changed.
    - `build-support/check-build-hygiene.sh`: all checks passed.
    - `build-support/run-clang-tidy.sh --build-dir be/build_ASAN --files
    be/src/exprs/function/function_jsonb.cpp
    be/test/exprs/function/function_jsonb_test.cpp`: could not complete in
    the local environment because the installed clang-tidy cannot find
    `stddef.h` (a local toolchain issue). All reported size/complexity
    warnings are in functions that predate this change, and none is on a
    changed line.
---
 be/src/exprs/function/function_jsonb.cpp           |  9 ++++-
 be/test/exprs/function/function_jsonb_test.cpp     | 43 ++++++++++++++++++++++
 .../json_function/test_query_json_insert.out       | 17 +++++++++
 .../json_function/test_query_json_set.out          | 17 +++++++++
 .../json_function/test_query_json_insert.groovy    | 31 ++++++++++++++++
 .../json_function/test_query_json_set.groovy       | 31 ++++++++++++++++
 6 files changed, 146 insertions(+), 2 deletions(-)

diff --git a/be/src/exprs/function/function_jsonb.cpp 
b/be/src/exprs/function/function_jsonb.cpp
index 7fd86167a9a..40150754ba9 100644
--- a/be/src/exprs/function/function_jsonb.cpp
+++ b/be/src/exprs/function/function_jsonb.cpp
@@ -2224,10 +2224,15 @@ public:
 
                 if (is_last_parent && !value_written) {
                     value_written = true;
-                    writer.writeStartObject();
+                    // The current object is already open above and is waiting 
for a key, so the
+                    // new member must be appended to it directly. Opening 
another object here
+                    // would be rejected by the writer, and closing one here 
would end the
+                    // current object early; the writeEndObject below would 
then close its
+                    // parent as well, silently dropping every member that 
follows the current
+                    // one. For example, {"a":{},"b":2} with path $.a.c used 
to return
+                    // {"a":{"c":1}} instead of {"a":{"c":1},"b":2}.
                     writer.writeKey(last_leg->leg_ptr, 
static_cast<uint8_t>(last_leg->leg_len));
                     writer.writeValue(value);
-                    writer.writeEndObject();
                 }
                 writer.writeEndObject();
 
diff --git a/be/test/exprs/function/function_jsonb_test.cpp 
b/be/test/exprs/function/function_jsonb_test.cpp
index 5972505b0c8..18705f4f877 100644
--- a/be/test/exprs/function/function_jsonb_test.cpp
+++ b/be/test/exprs/function/function_jsonb_test.cpp
@@ -1163,6 +1163,49 @@ TEST(FunctionJsonbTEST, JsonbModifyMissingPathParent) {
     }
 }
 
+TEST(FunctionJsonbTEST, JsonbModifyNestedMissingMemberKeepsFollowingSiblings) {
+    InputTypeSet input_types = {PrimitiveType::TYPE_JSONB, 
PrimitiveType::TYPE_VARCHAR,
+                                PrimitiveType::TYPE_JSONB};
+
+    // Adding a missing member to a nested object must keep the members that 
follow that
+    // object in its ancestors. Before the fix the enclosing object was closed 
one level
+    // too early, so every following sibling was silently dropped.
+    DataSet data_set = {
+            // Nested empty object followed by a sibling.
+            {{STRING(R"({"a":{},"b":2})"), STRING(R"($.a.c)"), STRING("1")},
+             STRING(R"({"a":{"c":1},"b":2})")},
+            // Non-empty nested object.
+            {{STRING(R"({"a":{"d":4},"b":2})"), STRING(R"($.a.c)"), 
STRING("1")},
+             STRING(R"({"a":{"d":4,"c":1},"b":2})")},
+            // Control: a sibling written before the nested object was already 
correct.
+            {{STRING(R"({"b":2,"a":{}})"), STRING(R"($.a.c)"), STRING("1")},
+             STRING(R"({"b":2,"a":{"c":1}})")},
+            // Two nested levels: the premature close dropped the sibling one 
level up.
+            {{STRING(R"({"a":{"b":{}},"x":1})"), STRING(R"($.a.b.c)"), 
STRING("1")},
+             STRING(R"({"a":{"b":{"c":1}},"x":1})")},
+            // Following siblings of any type must be preserved.
+            {{STRING(R"({"a":{},"b":[1,2],"c":{"d":3}})"), STRING(R"($.a.e)"), 
STRING("1")},
+             STRING(R"({"a":{"e":1},"b":[1,2],"c":{"d":3}})")},
+            // An empty key can be the inserted member.
+            {{STRING(R"({"a":{},"b":2})"), STRING(R"($.a."")"), STRING("1")},
+             STRING(R"({"a":{"":1},"b":2})")},
+            // An object element inside an array was not affected; keep it as 
a guard.
+            {{STRING(R"({"a":[{"x":1}],"b":2})"), STRING(R"($.a[0].y)"), 
STRING("1")},
+             STRING(R"({"a":[{"x":1,"y":1}],"b":2})")},
+    };
+    for (const auto* func_name : {"jsonb_insert", "jsonb_set", "json_insert", 
"json_set"}) {
+        static_cast<void>(check_function<DataTypeJsonb, true>(func_name, 
input_types, data_set));
+    }
+
+    // jsonb_replace rewrites an existing member and already preserved its 
siblings.
+    DataSet replace_data_set = {
+            {{STRING(R"({"a":{"d":4},"b":2})"), STRING("$.a.d"), STRING("5")},
+             STRING(R"({"a":{"d":5},"b":2})")},
+    };
+    static_cast<void>(
+            check_function<DataTypeJsonb, true>("jsonb_replace", input_types, 
replace_data_set));
+}
+
 TEST(FunctionJsonbTEST, JsonbModifyMemberPathOnArray) {
     InputTypeSet input_types = {PrimitiveType::TYPE_JSONB, 
PrimitiveType::TYPE_VARCHAR,
                                 PrimitiveType::TYPE_JSONB};
diff --git 
a/regression-test/data/query_p0/sql_functions/json_function/test_query_json_insert.out
 
b/regression-test/data/query_p0/sql_functions/json_function/test_query_json_insert.out
index e43d2018d7a..1b539ed3496 100644
--- 
a/regression-test/data/query_p0/sql_functions/json_function/test_query_json_insert.out
+++ 
b/regression-test/data/query_p0/sql_functions/json_function/test_query_json_insert.out
@@ -96,3 +96,20 @@
 -- !insert_odd_arity_ok --
 {"a":1}        {"a":1,"b":2}   {"a":1,"b":2}
 
+-- !insert_nested_sibling --
+{"a":{"c":1},"b":2}
+
+-- !insert_nested_sibling_nonempty --
+{"a":{"d":4,"c":1},"b":2}
+
+-- !insert_nested_sibling_before --
+{"b":2,"a":{"c":1}}
+
+-- !insert_deep_nested_sibling --
+{"a":{"b":{"c":1}},"x":1}
+
+-- !insert_nested_sibling_json_col --
+1      {"a":{"c":1},"b":2}
+2      {"b":2,"a":{"c":1}}
+3      {"a":{"d":4,"c":1},"b":2}
+
diff --git 
a/regression-test/data/query_p0/sql_functions/json_function/test_query_json_set.out
 
b/regression-test/data/query_p0/sql_functions/json_function/test_query_json_set.out
index e198f86db0b..4561d577261 100644
--- 
a/regression-test/data/query_p0/sql_functions/json_function/test_query_json_set.out
+++ 
b/regression-test/data/query_p0/sql_functions/json_function/test_query_json_set.out
@@ -88,3 +88,20 @@ null
 -- !set_odd_arity_ok --
 {"a":1}        {"a":1,"b":2}   {"a":1,"b":2}
 
+-- !set_nested_sibling --
+{"a":{"c":1},"b":2}
+
+-- !set_nested_sibling_nonempty --
+{"a":{"d":4,"c":1},"b":2}
+
+-- !set_nested_sibling_before --
+{"b":2,"a":{"c":1}}
+
+-- !set_deep_nested_sibling --
+{"a":{"b":{"c":1}},"x":1}
+
+-- !set_nested_sibling_json_col --
+1      {"a":{"c":1},"b":2}
+2      {"b":2,"a":{"c":1}}
+3      {"a":{"d":4,"c":1},"b":2}
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_insert.groovy
 
b/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_insert.groovy
index 25d91e8c03c..a61bc842e90 100644
--- 
a/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_insert.groovy
+++ 
b/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_insert.groovy
@@ -150,4 +150,35 @@ suite("test_query_json_insert", "query,arrow_flight_sql") {
         exception "number of arguments must be odd, but got 4"
     }
     qt_insert_odd_arity_ok """select json_insert('{}', '\$.a', 1), 
json_insert('{}', '\$.a', 1, '\$.b', 2), jsonb_insert('{}', '\$.a', 1, '\$.b', 
2);"""
+
+    // Inserting a missing member into a nested object must keep the members 
that follow it.
+    order_qt_insert_nested_sibling """select json_insert('{"a":{},"b":2}', 
'\$.a.c', 1);"""
+    order_qt_insert_nested_sibling_nonempty """select 
json_insert('{"a":{"d":4},"b":2}', '\$.a.c', 1);"""
+    order_qt_insert_nested_sibling_before """select 
json_insert('{"b":2,"a":{}}', '\$.a.c', 1);"""
+    order_qt_insert_deep_nested_sibling """select 
json_insert('{"a":{"b":{}},"x":1}', '\$.a.b.c', 1);"""
+
+    sql "DROP TABLE IF EXISTS test_query_json_insert_nested_sibling"
+    sql """
+            CREATE TABLE test_query_json_insert_nested_sibling (
+              `id` int(11) not null,
+              `j` json
+            ) ENGINE=OLAP
+            DUPLICATE KEY(`id`)
+            DISTRIBUTED BY HASH(`id`) BUCKETS 1
+            PROPERTIES (
+            "replication_allocation" = "tag.location.default: 1",
+            "in_memory" = "false",
+            "storage_format" = "V2"
+            );
+        """
+    sql """
+        insert into test_query_json_insert_nested_sibling values
+            (1, '{"a":{},"b":2}'),
+            (2, '{"b":2,"a":{}}'),
+            (3, '{"a":{"d":4},"b":2}');
+    """
+    order_qt_insert_nested_sibling_json_col """
+        select id, json_insert(j, '\$.a.c', 1) as v
+        from test_query_json_insert_nested_sibling order by id;
+    """
 }
diff --git 
a/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_set.groovy
 
b/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_set.groovy
index 91174fa8827..17f9fe2612a 100644
--- 
a/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_set.groovy
+++ 
b/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_set.groovy
@@ -142,4 +142,35 @@ suite("test_query_json_set", "query") {
         exception "number of arguments must be odd, but got 4"
     }
     qt_set_odd_arity_ok """select json_set('{}', '\$.a', 1), json_set('{}', 
'\$.a', 1, '\$.b', 2), jsonb_set('{}', '\$.a', 1, '\$.b', 2);"""
+
+    // Setting a missing member on a nested object must keep the members that 
follow it.
+    order_qt_set_nested_sibling """select json_set('{"a":{},"b":2}', '\$.a.c', 
1);"""
+    order_qt_set_nested_sibling_nonempty """select 
json_set('{"a":{"d":4},"b":2}', '\$.a.c', 1);"""
+    order_qt_set_nested_sibling_before """select json_set('{"b":2,"a":{}}', 
'\$.a.c', 1);"""
+    order_qt_set_deep_nested_sibling """select 
json_set('{"a":{"b":{}},"x":1}', '\$.a.b.c', 1);"""
+
+    sql "DROP TABLE IF EXISTS test_query_json_set_nested_sibling"
+    sql """
+            CREATE TABLE test_query_json_set_nested_sibling (
+              `id` int(11) not null,
+              `j` json
+            ) ENGINE=OLAP
+            DUPLICATE KEY(`id`)
+            DISTRIBUTED BY HASH(`id`) BUCKETS 1
+            PROPERTIES (
+            "replication_allocation" = "tag.location.default: 1",
+            "in_memory" = "false",
+            "storage_format" = "V2"
+            );
+        """
+    sql """
+        insert into test_query_json_set_nested_sibling values
+            (1, '{"a":{},"b":2}'),
+            (2, '{"b":2,"a":{}}'),
+            (3, '{"a":{"d":4},"b":2}');
+    """
+    order_qt_set_nested_sibling_json_col """
+        select id, json_set(j, '\$.a.c', 1) as v
+        from test_query_json_set_nested_sibling order by id;
+    """
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to