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

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new bcaaa5f2936 branch-4.2: [fix](be) Quote JSON_SEARCH path legs that 
need escaping #69170 (#69253)
bcaaa5f2936 is described below

commit bcaaa5f293674f1adba0dfa52c8f8f57fb5e9549
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Oct 20 11:37:40 2026 +0800

    branch-4.2: [fix](be) Quote JSON_SEARCH path legs that need escaping #69170 
(#69253)
    
    Cherry-picked from #69170
    
    Co-authored-by: Jerry Hu <[email protected]>
---
 be/src/exprs/function/function_jsonb.cpp           |  16 +-
 be/src/util/jsonb_document.cpp                     |   6 +-
 be/src/util/jsonb_document.h                       |  56 ++++--
 be/test/core/jsonb/jsonb_document_test.cpp         | 213 +++++++++++++++++++++
 be/test/exprs/function/function_jsonb_test.cpp     |  64 +++++++
 .../sql_functions/json_functions/json_search.out   |  15 ++
 .../json_functions/json_search.groovy              |  58 ++++++
 7 files changed, 405 insertions(+), 23 deletions(-)

diff --git a/be/src/exprs/function/function_jsonb.cpp 
b/be/src/exprs/function/function_jsonb.cpp
index 3b5b9d4417e..840889c2875 100644
--- a/be/src/exprs/function/function_jsonb.cpp
+++ b/be/src/exprs/function/function_jsonb.cpp
@@ -2067,10 +2067,7 @@ public:
                     DCHECK_GT(legs_count, 0);
                     for (size_t j = 0; j + 1 < legs_count; ++j) {
                         auto* current_leg = 
json_path[path_index].get_leg_from_leg_vector(j);
-                        std::unique_ptr<leg_info> leg = 
std::make_unique<leg_info>(
-                                current_leg->leg_ptr, current_leg->leg_len,
-                                current_leg->array_index, current_leg->type);
-                        new_path.add_leg_to_leg_vector(std::move(leg));
+                        
new_path.add_leg_to_leg_vector(std::make_unique<leg_info>(*current_leg));
                     }
 
                     if 
(!build_parents_by_path(json_documents[row_idx]->getValue(), new_path,
@@ -2083,6 +2080,12 @@ public:
                         legs_count > 0
                                 ? 
json_path[path_index].get_leg_from_leg_vector(legs_count - 1)
                                 : nullptr;
+                // A member leg names an object key and never an array 
element, so inserting it
+                // into an array leaves the document unchanged (as MySQL 
does). Otherwise its
+                // default array_index (0) would overwrite the first element.
+                if (!replace && parents.back()->isArray() && last_leg->type == 
MEMBER_CODE) {
+                    continue;
+                }
                 
RETURN_IF_ERROR(write_json_value(json_documents[row_idx]->getValue(), parents, 
0,
                                                  json_value[value_index], 
replace, last_leg,
                                                  writer));
@@ -2135,10 +2138,7 @@ public:
 
         JsonbPath current;
         auto* current_leg = path.get_leg_from_leg_vector(index);
-        std::unique_ptr<leg_info> leg =
-                std::make_unique<leg_info>(current_leg->leg_ptr, 
current_leg->leg_len,
-                                           current_leg->array_index, 
current_leg->type);
-        current.add_leg_to_leg_vector(std::move(leg));
+        
current.add_leg_to_leg_vector(std::make_unique<leg_info>(*current_leg));
 
         auto find_result = root->findValue(current);
         if (!find_result.value) {
diff --git a/be/src/util/jsonb_document.cpp b/be/src/util/jsonb_document.cpp
index 05a44f4b30f..4f85313b55b 100644
--- a/be/src/util/jsonb_document.cpp
+++ b/be/src/util/jsonb_document.cpp
@@ -95,8 +95,7 @@ JsonbFindResult JsonbValue::findValue(JsonbPath& path) const {
             switch (path.get_leg_from_leg_vector(i)->type) {
             case MEMBER_CODE: {
                 if (LIKELY(pval->type == JsonbType::T_Object)) {
-                    if (path.get_leg_from_leg_vector(i)->leg_len == 1 &&
-                        *path.get_leg_from_leg_vector(i)->leg_ptr == WILDCARD) 
{
+                    if (path.get_leg_from_leg_vector(i)->is_wildcard) {
                         is_wildcard = true;
                         for (const auto& it : *pval->unpack<ObjectVal>()) {
                             results.emplace_back(it.value());
@@ -115,8 +114,7 @@ JsonbFindResult JsonbValue::findValue(JsonbPath& path) 
const {
                 continue;
             }
             case ARRAY_CODE: {
-                if (path.get_leg_from_leg_vector(i)->leg_len == 1 &&
-                    *path.get_leg_from_leg_vector(i)->leg_ptr == WILDCARD) {
+                if (path.get_leg_from_leg_vector(i)->is_wildcard) {
                     if (LIKELY(pval->type == JsonbType::T_Array)) {
                         is_wildcard = true;
                         for (const auto& it : *pval->unpack<ArrayVal>()) {
diff --git a/be/src/util/jsonb_document.h b/be/src/util/jsonb_document.h
index 62d013bd0cb..8c9177752c1 100644
--- a/be/src/util/jsonb_document.h
+++ b/be/src/util/jsonb_document.h
@@ -418,16 +418,25 @@ struct leg_info {
     ///type: 0 is member 1 is array
     unsigned int type;
 
+    ///true only for an unquoted `*` member leg or a `[*]` array leg; a quoted 
"*" member leg
+    ///is the literal key `*`
+    bool is_wildcard = false;
+
     // NOLINTNEXTLINE(readability-non-const-parameter): str is an output 
parameter.
     bool to_string(std::string* str) const {
         if (type == MEMBER_CODE) {
             str->push_back(BEGIN_MEMBER);
-            bool contains_space = false;
+            // JsonbPath::parse_member reads an unquoted member leg until the 
next '.' or '[',
+            // rejects unquoted whitespace, treats a leading '*' as a wildcard 
and rejects an
+            // unquoted empty leg. A key that would not read back as this same 
single leg must be
+            // double-quoted so the emitted path can be parsed again (for 
example by
+            // JSON_EXTRACT on the result of JSON_SEARCH). A parsed wildcard 
leg stays `*`.
+            bool needs_quotes = leg_len == 0 || (!is_wildcard && *leg_ptr == 
WILDCARD);
             std::string tmp;
             for (auto* it = leg_ptr; it != (leg_ptr + leg_len); ++it) {
                 auto c = static_cast<unsigned char>(*it);
-                if (std::isspace(c)) {
-                    contains_space = true;
+                if (std::isspace(c) || *it == BEGIN_MEMBER || *it == 
BEGIN_ARRAY) {
+                    needs_quotes = true;
                 }
 
                 switch (*it) {
@@ -464,18 +473,21 @@ struct leg_info {
                     break;
                 }
             }
-            if (contains_space) {
+            if (needs_quotes) {
                 str->push_back(DOUBLE_QUOTE);
             }
             str->append(tmp);
-            if (contains_space) {
+            if (needs_quotes) {
                 str->push_back(DOUBLE_QUOTE);
             }
             return true;
         } else if (type == ARRAY_CODE) {
             str->push_back(BEGIN_ARRAY);
-            std::string int_str = std::to_string(array_index);
-            str->append(int_str);
+            if (is_wildcard) {
+                str->push_back(WILDCARD);
+            } else {
+                str->append(std::to_string(array_index));
+            }
             str->push_back(END_ARRAY);
             return true;
         } else {
@@ -979,7 +991,8 @@ struct ObjectVal : public ContainerVal {
     }
 
     const_iterator search(const char* key, unsigned int klen) const {
-        if (!key || !klen) {
+        // An empty key is a valid object key, so klen == 0 is still searched.
+        if (!key) {
             return end();
         }
         return internalSearch(key, klen);
@@ -1019,6 +1032,16 @@ struct ObjectVal : public ContainerVal {
         return kv->value();
     }
 
+    // find the JSONB value by a key id, as stored when klen() == 0 (sMaxKeyId 
is the empty key)
+    const JsonbValue* find_by_key_id(JsonbKeyValue::keyid_type key_id) const {
+        for (auto it = begin(); it != end(); ++it) {
+            if (it->klen() == 0 && it->getKeyId() == key_id) {
+                return it->value();
+            }
+        }
+        return nullptr;
+    }
+
     const_iterator begin() const { return const_iterator((pointer)payload); }
 
     const_iterator end() const { return const_iterator((pointer)(payload + 
size)); }
@@ -1032,7 +1055,10 @@ private:
 
         while (pch < fence) {
             const auto* pkey = (const JsonbKeyValue*)(pch);
-            if (klen == pkey->klen() && strncmp(key, pkey->getKeyStr(), klen) 
== 0) {
+            // A key id entry is also stored with klen() == 0, so an empty 
name only matches
+            // the sMaxKeyId sentinel that JsonbWriter writes for an empty key.
+            if (klen == 0 ? pkey->klen() == 0 && pkey->getKeyId() == 
JsonbKeyValue::sMaxKeyId
+                          : klen == pkey->klen() && strncmp(key, 
pkey->getKeyStr(), klen) == 0) {
                 return const_iterator(pkey);
             }
             pch += pkey->numPackedBytes();
@@ -1463,7 +1489,11 @@ inline bool JsonbValue::contains(const JsonbValue* rhs) 
const {
             const auto* obj_value1 = unpack<ObjectVal>();
             const auto* obj_value2 = rhs->unpack<ObjectVal>();
             for (auto it = obj_value2->begin(); it != obj_value2->end(); ++it) 
{
-                const JsonbValue* value = obj_value1->find(it->getKeyStr(), 
it->klen());
+                // A key stored as an id (including the empty key sentinel) 
has klen() == 0 and
+                // must be matched by id, not looked up as an empty key name.
+                const JsonbValue* value = it->klen() == 0
+                                                  ? 
obj_value1->find_by_key_id(it->getKeyId())
+                                                  : 
obj_value1->find(it->getKeyStr(), it->klen());
                 if (value == nullptr || !value->contains(it->value())) {
                     return false;
                 }
@@ -1602,6 +1632,7 @@ inline bool JsonbPath::parse_array(Stream* stream, 
JsonbPath* path) {
         if (stream->peek() == END_ARRAY) {
             std::unique_ptr<leg_info> leg(
                     new leg_info(stream->get_leg_ptr(), stream->get_leg_len(), 
0, ARRAY_CODE));
+            leg->is_wildcard = true;
             path->add_leg_to_leg_vector(std::move(leg));
             stream->skip(1);
             path->_is_wildcard = true;
@@ -1690,6 +1721,7 @@ inline bool JsonbPath::parse_member(Stream* stream, 
JsonbPath* path) {
         stream->skip(1);
         std::unique_ptr<leg_info> leg(
                 new leg_info(stream->get_leg_ptr(), stream->get_leg_len(), 0, 
MEMBER_CODE));
+        leg->is_wildcard = true;
         path->add_leg_to_leg_vector(std::move(leg));
         path->_is_wildcard = true;
         return true;
@@ -1736,8 +1768,10 @@ inline bool JsonbPath::parse_member(Stream* stream, 
JsonbPath* path) {
         stream->add_leg_len();
     }
 
+    // An empty member name is only valid when it is written explicitly as 
`""`.
     if ((left_quotation_marks != nullptr && right_quotation_marks == nullptr) 
||
-        stream->get_leg_ptr() == nullptr || stream->get_leg_len() == 0) {
+        stream->get_leg_ptr() == nullptr ||
+        (left_quotation_marks == nullptr && stream->get_leg_len() == 0)) {
         return false; //invalid json path
     }
 
diff --git a/be/test/core/jsonb/jsonb_document_test.cpp 
b/be/test/core/jsonb/jsonb_document_test.cpp
index 4801e1cbfc8..501b3fd2658 100644
--- a/be/test/core/jsonb/jsonb_document_test.cpp
+++ b/be/test/core/jsonb/jsonb_document_test.cpp
@@ -24,6 +24,7 @@
 #include <limits>
 #include <string>
 #include <string_view>
+#include <vector>
 
 #include "core/types.h"
 #include "util/jsonb_utils.h"
@@ -320,6 +321,218 @@ TEST_F(JsonbDocumentTest, 
jsonb_path_member_to_string_escapes_control_characters
     EXPECT_EQ(round_trip, expected_path);
 }
 
+TEST_F(JsonbDocumentTest, jsonb_path_member_to_string_quotes_dotted_key) {
+    std::string key = "a.b";
+
+    leg_info leg(key.data(), static_cast<unsigned int>(key.size()), 0, 
MEMBER_CODE);
+
+    std::string out;
+    ASSERT_TRUE(leg.to_string(&out));
+    // A literal '.' inside the key is a path separator in JsonbPath syntax, 
so the
+    // leg must be double-quoted (".\"a.b\"") or a later JsonbPath::seek on 
this
+    // same string will split it into two legs ("a" then "b") instead of one.
+    EXPECT_EQ(out, ".\"a.b\"");
+
+    const std::string expected_path = "$" + out;
+    std::string parsed_path = expected_path;
+    JsonbPath path;
+    ASSERT_TRUE(path.seek(parsed_path.data(), parsed_path.size()));
+    EXPECT_EQ(path.get_leg_vector_size(), 1);
+
+    std::string round_trip;
+    ASSERT_TRUE(path.to_string(&round_trip));
+    EXPECT_EQ(round_trip, expected_path);
+}
+
+TEST_F(JsonbDocumentTest, 
jsonb_path_member_to_string_quotes_only_unparsable_keys) {
+    struct Case {
+        std::string key;
+        std::string expected;
+    };
+    const std::vector<Case> cases = {
+            {"a[0]", ".\"a[0]\""}, // '[' starts an array leg when unquoted
+            {"*", ".\"*\""},       // a bare '*' is a wildcard when unquoted
+            {"*a", ".\"*a\""},     // a leading '*' is parsed as a wildcard leg
+            {"a*", ".a*"},         // '*' elsewhere is an ordinary character
+            {"a-b", ".a-b"},       // no structural character, stays unquoted
+            {"a]b", ".a]b"},       // ']' is not special for an unquoted member
+            {"a\"b", ".a\\\"b"},   // escaped quote is readable without quoting
+            {"", ".\"\""},         // an empty key is only valid when quoted
+    };
+
+    for (const auto& c : cases) {
+        std::string key = c.key;
+        leg_info leg(key.data(), static_cast<unsigned int>(key.size()), 0, 
MEMBER_CODE);
+
+        std::string out;
+        ASSERT_TRUE(leg.to_string(&out)) << c.key;
+        EXPECT_EQ(out, c.expected) << c.key;
+
+        const std::string expected_path = "$" + out;
+        std::string parsed_path = expected_path;
+        JsonbPath path;
+        ASSERT_TRUE(path.seek(parsed_path.data(), parsed_path.size())) << 
c.key;
+        ASSERT_EQ(path.get_leg_vector_size(), 1) << c.key;
+        const auto* parsed = path.get_leg_from_leg_vector(0);
+        EXPECT_EQ(std::string(parsed->leg_ptr, parsed->leg_len), c.key);
+
+        std::string round_trip;
+        ASSERT_TRUE(path.to_string(&round_trip)) << c.key;
+        EXPECT_EQ(round_trip, expected_path) << c.key;
+    }
+}
+
+TEST_F(JsonbDocumentTest, jsonb_path_quoted_member_is_literal_key) {
+    JsonbWriter writer;
+    writer.writeStartObject();
+    writer.writeKey("", 0);
+    writer.writeInt(1);
+    writer.writeKey("*", 1);
+    writer.writeInt(2);
+    writer.writeKey("other", static_cast<uint8_t>(sizeof("other") - 1));
+    writer.writeInt(3);
+    writer.writeEndObject();
+    const auto* root = jsonb_value(writer);
+
+    // A quoted member leg names exactly that key, including the empty key and 
"*".
+    {
+        std::string path_str = "$.\"\"";
+        JsonbPath path;
+        ASSERT_TRUE(path.seek(path_str.data(), path_str.size()));
+        auto result = root->findValue(path);
+        EXPECT_FALSE(result.is_wildcard);
+        ASSERT_NE(result.value, nullptr);
+        EXPECT_EQ(JsonbToJson {}.to_json_string(result.value), "1");
+    }
+    {
+        std::string path_str = "$.\"*\"";
+        JsonbPath path;
+        ASSERT_TRUE(path.seek(path_str.data(), path_str.size()));
+        EXPECT_FALSE(path.is_wildcard());
+        auto result = root->findValue(path);
+        EXPECT_FALSE(result.is_wildcard);
+        ASSERT_NE(result.value, nullptr);
+        EXPECT_EQ(JsonbToJson {}.to_json_string(result.value), "2");
+    }
+    // An unquoted '*' is still the member wildcard.
+    {
+        std::string path_str = "$.*";
+        JsonbPath path;
+        ASSERT_TRUE(path.seek(path_str.data(), path_str.size()));
+        EXPECT_TRUE(path.is_wildcard());
+        auto result = root->findValue(path);
+        EXPECT_TRUE(result.is_wildcard);
+        ASSERT_NE(result.value, nullptr);
+        EXPECT_EQ(JsonbToJson {}.to_json_string(result.value), "[1,2,3]");
+    }
+
+    // An unquoted empty member leg is still invalid.
+    for (std::string path_str : {"$.", "$.other.", "$..other"}) {
+        JsonbPath path;
+        EXPECT_FALSE(path.seek(path_str.data(), path_str.size())) << path_str;
+    }
+}
+
+TEST_F(JsonbDocumentTest, jsonb_empty_key_lookup_skips_key_ids) {
+    // A key id entry is stored with klen() == 0 like an empty key, but only 
the
+    // sMaxKeyId sentinel is the empty key name.
+    JsonbWriter writer;
+    writer.writeStartObject();
+    writer.writeKey(static_cast<JsonbKeyValue::keyid_type>(42));
+    writer.writeInt(1);
+    writer.writeKey("", 0);
+    writer.writeInt(2);
+    writer.writeEndObject();
+    const auto* root = jsonb_value(writer);
+    const auto* obj = root->unpack<ObjectVal>();
+
+    const auto* found = obj->find("", 0);
+    ASSERT_NE(found, nullptr);
+    EXPECT_EQ(JsonbToJson {}.to_json_string(found), "2");
+
+    std::string path_str = "$.\"\"";
+    JsonbPath path;
+    ASSERT_TRUE(path.seek(path_str.data(), path_str.size()));
+    auto result = root->findValue(path);
+    ASSERT_NE(result.value, nullptr);
+    EXPECT_EQ(JsonbToJson {}.to_json_string(result.value), "2");
+
+    // Without an empty key, a key id entry is never returned for an empty 
name.
+    JsonbWriter id_only_writer;
+    id_only_writer.writeStartObject();
+    id_only_writer.writeKey(static_cast<JsonbKeyValue::keyid_type>(42));
+    id_only_writer.writeInt(1);
+    id_only_writer.writeEndObject();
+    EXPECT_EQ(jsonb_value(id_only_writer)->unpack<ObjectVal>()->find("", 0), 
nullptr);
+}
+
+TEST_F(JsonbDocumentTest, jsonb_path_wildcard_to_string_round_trip) {
+    JsonbWriter writer;
+    writer.writeStartObject();
+    writer.writeKey("*", 1);
+    writer.writeInt(1);
+    writer.writeKey("other", static_cast<uint8_t>(sizeof("other") - 1));
+    writer.writeInt(2);
+    writer.writeEndObject();
+    const auto* root = jsonb_value(writer);
+
+    // A parsed wildcard formats back to the wildcard and a quoted "*" stays 
the literal key.
+    struct Case {
+        std::string path;
+        bool is_wildcard;
+        std::string value;
+    };
+    const std::vector<Case> cases = {
+            {"$.*", true, "[1,2]"},
+            {"$.\"*\"", false, "1"},
+    };
+    for (const auto& c : cases) {
+        std::string path_str = c.path;
+        JsonbPath path;
+        ASSERT_TRUE(path.seek(path_str.data(), path_str.size())) << c.path;
+        std::string formatted;
+        ASSERT_TRUE(path.to_string(&formatted)) << c.path;
+        EXPECT_EQ(formatted, c.path);
+
+        JsonbPath reparsed;
+        ASSERT_TRUE(reparsed.seek(formatted.data(), formatted.size())) << 
c.path;
+        auto result = root->findValue(reparsed);
+        EXPECT_EQ(result.is_wildcard, c.is_wildcard) << c.path;
+        ASSERT_NE(result.value, nullptr) << c.path;
+        EXPECT_EQ(JsonbToJson {}.to_json_string(result.value), c.value) << 
c.path;
+    }
+
+    for (std::string path_str : {"$[*]", "$[1]", "$.a[*].b"}) {
+        JsonbPath path;
+        ASSERT_TRUE(path.seek(path_str.data(), path_str.size())) << path_str;
+        std::string formatted;
+        ASSERT_TRUE(path.to_string(&formatted)) << path_str;
+        EXPECT_EQ(formatted, path_str);
+    }
+}
+
+TEST_F(JsonbDocumentTest, jsonb_object_contains_matches_key_ids_by_id) {
+    JsonbWriter empty_key_writer;
+    empty_key_writer.writeStartObject();
+    empty_key_writer.writeKey("", 0);
+    empty_key_writer.writeInt(1);
+    empty_key_writer.writeEndObject();
+    const auto* empty_key = jsonb_value(empty_key_writer);
+
+    JsonbWriter id_key_writer;
+    id_key_writer.writeStartObject();
+    id_key_writer.writeKey(static_cast<JsonbKeyValue::keyid_type>(42));
+    id_key_writer.writeInt(1);
+    id_key_writer.writeEndObject();
+    const auto* id_key = jsonb_value(id_key_writer);
+
+    // Both keys are stored with klen() == 0, but key id 42 is not the empty 
key.
+    EXPECT_FALSE(empty_key->contains(id_key));
+    EXPECT_FALSE(id_key->contains(empty_key));
+    EXPECT_TRUE(empty_key->contains(empty_key));
+    EXPECT_TRUE(id_key->contains(id_key));
+}
+
 TEST_F(JsonbDocumentTest, 
jsonb_path_member_to_string_escapes_all_control_forms) {
     std::string key = "a";
     key.push_back('\b');
diff --git a/be/test/exprs/function/function_jsonb_test.cpp 
b/be/test/exprs/function/function_jsonb_test.cpp
index 7736362271c..fc6744b8b1c 100644
--- a/be/test/exprs/function/function_jsonb_test.cpp
+++ b/be/test/exprs/function/function_jsonb_test.cpp
@@ -1117,4 +1117,68 @@ TEST(FunctionJsonbTEST, JsonbModifyMissingPathParent) {
     }
 }
 
+TEST(FunctionJsonbTEST, JsonbModifyMemberPathOnArray) {
+    InputTypeSet input_types = {PrimitiveType::TYPE_JSONB, 
PrimitiveType::TYPE_VARCHAR,
+                                PrimitiveType::TYPE_JSONB};
+
+    // A member leg never addresses an array element, so a missing member 
under an array is
+    // not inserted; array legs and quoted literal keys still work.
+    DataSet data_set = {
+            {{STRING("[1]"), STRING("$.a"), STRING("2")}, STRING("[1]")},
+            {{STRING("[]"), STRING("$.a"), STRING("2")}, STRING("[]")},
+            {{STRING("[1]"), STRING(R"($."a.b")"), STRING("2")}, 
STRING("[1]")},
+            {{STRING("[[1]]"), STRING("$[0].a"), STRING("2")}, 
STRING("[[1]]")},
+            {{STRING(R"({"a":[1]})"), STRING("$.a.b"), STRING("2")}, 
STRING(R"({"a":[1]})")},
+            {{STRING("[1]"), STRING("$[1]"), STRING("2")}, STRING("[1,2]")},
+            {{STRING("{}"), STRING(R"($."a.b")"), STRING("2")}, 
STRING(R"({"a.b":2})")},
+            {{STRING("{}"), STRING(R"($."")"), STRING("2")}, 
STRING(R"({"":2})")},
+    };
+    static_cast<void>(check_function<DataTypeJsonb, true>("jsonb_insert", 
input_types, data_set));
+    static_cast<void>(check_function<DataTypeJsonb, true>("jsonb_set", 
input_types, data_set));
+
+    // Quoted "*" and "" name existing literal keys, including as parents of a 
nested leg.
+    DataSet set_existing = {
+            {{STRING(R"({"*":1,"b":1})"), STRING(R"($."*")"), STRING("2")},
+             STRING(R"({"*":2,"b":1})")},
+            {{STRING(R"({"":1,"b":1})"), STRING(R"($."")"), STRING("2")},
+             STRING(R"({"":2,"b":1})")},
+            {{STRING(R"({"*":{"":1},"b":1})"), STRING(R"($."*"."")"), 
STRING("2")},
+             STRING(R"({"*":{"":2},"b":1})")},
+    };
+    static_cast<void>(check_function<DataTypeJsonb, true>("jsonb_set", 
input_types, set_existing));
+    static_cast<void>(
+            check_function<DataTypeJsonb, true>("jsonb_replace", input_types, 
set_existing));
+
+    // jsonb_insert keeps existing literal keys unchanged and only fills a 
missing nested key.
+    DataSet insert_existing = {
+            {{STRING(R"({"*":1,"b":1})"), STRING(R"($."*")"), STRING("2")},
+             STRING(R"({"*":1,"b":1})")},
+            {{STRING(R"({"":1,"b":1})"), STRING(R"($."")"), STRING("2")},
+             STRING(R"({"":1,"b":1})")},
+            {{STRING(R"({"*":{"":1},"b":1})"), STRING(R"($."*"."")"), 
STRING("2")},
+             STRING(R"({"*":{"":1},"b":1})")},
+            {{STRING(R"({"b":1,"*":{}})"), STRING(R"($."*"."")"), STRING("2")},
+             STRING(R"({"b":1,"*":{"":2}})")},
+    };
+    static_cast<void>(
+            check_function<DataTypeJsonb, true>("jsonb_insert", input_types, 
insert_existing));
+
+    // jsonb_replace leaves a missing literal key alone.
+    DataSet replace_missing = {
+            {{STRING(R"({"b":1})"), STRING(R"($."*")"), STRING("2")}, 
STRING(R"({"b":1})")},
+            {{STRING(R"({"b":1})"), STRING(R"($."")"), STRING("2")}, 
STRING(R"({"b":1})")},
+    };
+    static_cast<void>(
+            check_function<DataTypeJsonb, true>("jsonb_replace", input_types, 
replace_missing));
+
+    // An unquoted '*' is still a wildcard and is rejected by every modify 
function.
+    DataSet wildcard = {
+            {{STRING(R"({"*":1,"b":1})"), STRING("$.*"), STRING("2")}, Null()},
+    };
+    for (const auto* func_name : {"jsonb_insert", "jsonb_set", 
"jsonb_replace"}) {
+        static_cast<void>(check_function<DataTypeJsonb, true>(func_name, 
input_types, wildcard, -1,
+                                                              -1, true));
+    }
+}
+
 } // namespace doris
diff --git 
a/regression-test/data/query_p0/sql_functions/json_functions/json_search.out 
b/regression-test/data/query_p0/sql_functions/json_functions/json_search.out
index c5eda451e47..75caae5d1cb 100644
--- a/regression-test/data/query_p0/sql_functions/json_functions/json_search.out
+++ b/regression-test/data/query_p0/sql_functions/json_functions/json_search.out
@@ -170,3 +170,18 @@
 -- !search_control_escape --
 "$.\\"a\\\\nb\\""      "$.\\"a\\\\tb\\""       "$.\\"a\\\\rb\\""       
"$.a\\\\u0001b" "needle"
 
+-- !search_dotted_key --
+"$.\\"a.b\\""  "hit"   "hit"
+
+-- !search_bracket_and_wildcard_key --
+"$.\\"a[0]\\"" "hit"   "$.\\"*a\\""    "hit"
+
+-- !search_empty_key --
+"$.\\"\\""     "hit"   "hit"
+
+-- !search_literal_star_key --
+"$.\\"*\\""    "hit"   ["hit","miss"]  {"other":"miss"}        ["k"]
+
+-- !search_member_path_on_array --
+[1]    [1]     [1]     []      {"x":1,"a.b":2} [1,2]
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/json_functions/json_search.groovy
 
b/regression-test/suites/query_p0/sql_functions/json_functions/json_search.groovy
index 1a4c515a340..06f965116b4 100644
--- 
a/regression-test/suites/query_p0/sql_functions/json_functions/json_search.groovy
+++ 
b/regression-test/suites/query_p0/sql_functions/json_functions/json_search.groovy
@@ -153,4 +153,62 @@ suite("test_json_search") {
                    JSON_UNQUOTE(JSON_SEARCH('{ "a\\\\nb": "needle" }', 'one', 
'needle'))
                );
     """
+
+    // A key that contains a path separator must be quoted so the returned 
path replays
+    // through JSON_EXTRACT and finds the same value JSON_SEARCH matched.
+    qt_search_dotted_key """
+        select JSON_SEARCH('{"a.b":"hit","a":{"b":"miss"}}', 'one', 'hit'),
+               JSON_EXTRACT('{"a.b":"hit","a":{"b":"miss"}}',
+                   JSON_UNQUOTE(JSON_SEARCH('{"a.b":"hit","a":{"b":"miss"}}', 
'one', 'hit'))),
+               JSON_EXTRACT('{"a.b":"hit","a":{"b":"miss"}}', '\$."a.b"');
+    """
+
+    qt_search_bracket_and_wildcard_key """
+        select JSON_SEARCH('{"a[0]":"hit","a":["miss"]}', 'one', 'hit'),
+               JSON_EXTRACT('{"a[0]":"hit","a":["miss"]}',
+                   JSON_UNQUOTE(JSON_SEARCH('{"a[0]":"hit","a":["miss"]}', 
'one', 'hit'))),
+               JSON_SEARCH('{"*a":"hit"}', 'one', 'hit'),
+               JSON_EXTRACT('{"*a":"hit"}',
+                   JSON_UNQUOTE(JSON_SEARCH('{"*a":"hit"}', 'one', 'hit')));
+    """
+
+    // The empty key is emitted as `$.""`, which names exactly that key.
+    qt_search_empty_key """
+        select JSON_SEARCH('{"":"hit","a":"miss"}', 'one', 'hit'),
+               JSON_EXTRACT('{"":"hit","a":"miss"}',
+                   JSON_UNQUOTE(JSON_SEARCH('{"":"hit","a":"miss"}', 'one', 
'hit'))),
+               JSON_EXTRACT('{"":{"":"hit"}}', '\$."".""');
+    """
+
+    test {
+        sql """select JSON_EXTRACT('{"":"hit"}', '\$.');"""
+        exception "Invalid Json Path"
+    }
+
+    // A quoted "*" is the literal key `*`, an unquoted * is still the 
wildcard.
+    qt_search_literal_star_key """
+        select JSON_SEARCH('{"*":"hit","other":"miss"}', 'one', 'hit'),
+               JSON_EXTRACT('{"*":"hit","other":"miss"}',
+                   JSON_UNQUOTE(JSON_SEARCH('{"*":"hit","other":"miss"}', 
'one', 'hit'))),
+               JSON_EXTRACT('{"*":"hit","other":"miss"}', '\$.*'),
+               JSON_REMOVE('{"*":"hit","other":"miss"}',
+                   JSON_UNQUOTE(JSON_SEARCH('{"*":"hit","other":"miss"}', 
'one', 'hit'))),
+               JSON_KEYS('{"*":{"k":"hit"},"other":{"x":"miss"}}', '\$."*"');
+    """
+
+    test {
+        sql """select JSON_REMOVE('{"*":"hit","other":"miss"}', '\$.*');"""
+        exception "path expressions may not contain the * and ** tokens"
+    }
+
+    // A member path never addresses an array element, so JSON_SET / 
JSON_INSERT leave an
+    // array unchanged instead of overwriting or appending an element.
+    qt_search_member_path_on_array """
+        select JSON_SET('[1]', JSON_UNQUOTE(JSON_SEARCH('{"a.b":"hit"}', 
'one', 'hit')), 2),
+               JSON_INSERT('[1]', JSON_UNQUOTE(JSON_SEARCH('{"a.b":"hit"}', 
'one', 'hit')), 2),
+               JSON_SET('[1]', '\$.a', 2),
+               JSON_INSERT('[]', '\$.a', 2),
+               JSON_SET('{"x":1}', JSON_UNQUOTE(JSON_SEARCH('{"a.b":"hit"}', 
'one', 'hit')), 2),
+               JSON_SET('[1]', '\$[1]', 2);
+    """
 }


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

Reply via email to