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]