mirror of
https://github.com/apache/doris.git
synced 2026-11-04 07:31:17 +00:00
[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.
This commit is contained in:
1 parent
f5bb482e2f
commit
fb53e87aa8
7 files changed
+405
-23
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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>()) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
"""
|
||||
}
|
||||
Reference in new issue
Block a user