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 fb53e87aa87 [fix](be) Quote JSON_SEARCH path legs that need escaping
(#69170)
fb53e87aa87 is described below
commit fb53e87aa87fd9f6f7646fe03ea20d239b32a33d
Author: Jerry Hu <[email protected]>
AuthorDate: Mon Oct 19 10:47:41 2026 +0800
[fix](be) Quote JSON_SEARCH path legs that need escaping (#69170)
### What problem does this PR solve?
Issue Number: None
Problem Summary:
`JSON_SEARCH` builds the returned JSON path by concatenating each object
key as a bare member leg (`.key`), only double-quoting the leg when the
key contains whitespace. When the key itself contains a character that
`JsonbPath`'s parser treats as structural when unquoted — `.`, `[`, a
leading `*`, or an empty key — the emitted path does not parse back into
the same single leg, so feeding the result into
`JSON_EXTRACT`/`JSON_SEARCH` again resolves to a different value (or
fails to parse).
For example:
```sql
JSON_SEARCH('{"a.b":"hit","a":{"b":"miss"}}', 'one', 'hit')
-- returns $.a.b, which JSON_EXTRACT parses as a -> b and resolves to
"miss" instead of "hit"
```
The fix makes `leg_info::to_string` double-quote a member leg whenever
the unquoted form would not read back as the same leg (mirroring
`JsonbPath`'s own unquoted-leg grammar: empty key, leading `*`, or
containing whitespace/`.`/`[`), and adds a few supporting changes so the
rest of the path code treats a quoted literal key consistently with a
parsed wildcard:
- A new `leg_info::is_wildcard` flag, set only by the parser for an
unquoted `*` / `[*]` leg, distinguishes a real wildcard from the literal
key `"*"`. `to_string` and `findValue` both switch to this flag instead
of comparing leg bytes, since a quoted `"*"` leg has the same bytes as a
wildcard leg.
- `$.""` (an explicit empty key) is accepted by the parser and can be
looked up; `ObjectVal::search` no longer treats an empty key length as
"not found", and the internal lookup distinguishes an explicit empty key
from an object entry that is stored by numeric key id (which also has
key length 0).
- `JsonbValue::contains` looks up a key-id-encoded right-hand key by id
instead of by name, for the same reason.
- `JSON_SET`/`JSON_INSERT` no longer write a member-style leg into an
array (matching MySQL); previously the leg's default array index (0)
would overwrite or append to the array.
- A pre-existing formatting issue where a wildcard array leg (`[*]`) was
printed back as `[0]` is fixed alongside, since it shares the same root
cause (no explicit wildcard marker).
### Release note
`JSON_SEARCH` now quotes a returned path segment whenever the key
contains `.`, `[`, starts with `*`, or is empty, so the result can be
parsed again by `JSON_EXTRACT`/`JSON_SEARCH`/`JSON_SET`/etc. As a side
effect, `$.""` is now a valid path for an explicit empty key, a quoted
`"*"` key is treated as a literal key instead of a wildcard, and
`JSON_SET`/`JSON_INSERT` no longer modify an array when the path's last
segment is a member (non-array-index) leg.
### Check List (For Author)
- Test
- [x] Unit Test
- [x] Regression test
- Behavior changed:
- [x] Yes. See Release note above.
- Does this need documentation?
- [ ] No.
---
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 9a01cdf357c..7fd86167a9a 100644
--- a/be/src/exprs/function/function_jsonb.cpp
+++ b/be/src/exprs/function/function_jsonb.cpp
@@ -2041,10 +2041,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,
@@ -2057,6 +2054,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));
@@ -2109,10 +2112,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 e0efc375b96..943ee3946f9 100644
--- a/be/src/util/jsonb_document.cpp
+++ b/be/src/util/jsonb_document.cpp
@@ -100,8 +100,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());
@@ -120,8 +119,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 ba4ccb8fe00..5306c2529ec 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();
@@ -1466,7 +1492,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;
}
@@ -1608,6 +1638,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;
@@ -1703,6 +1734,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;
@@ -1749,8 +1781,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 4ef40ee3e1f..5972505b0c8 100644
--- a/be/test/exprs/function/function_jsonb_test.cpp
+++ b/be/test/exprs/function/function_jsonb_test.cpp
@@ -1163,4 +1163,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]