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 = std::make_unique( - 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(*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 = - std::make_unique(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(*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()) { 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()) { 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(*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(); const auto* obj_value2 = rhs->unpack(); 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( 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( 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 #include #include +#include #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(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 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(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(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(42)); + writer.writeInt(1); + writer.writeKey("", 0); + writer.writeInt(2); + writer.writeEndObject(); + const auto* root = jsonb_value(writer); + const auto* obj = root->unpack(); + + 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(42)); + id_only_writer.writeInt(1); + id_only_writer.writeEndObject(); + EXPECT_EQ(jsonb_value(id_only_writer)->unpack()->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(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 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(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(check_function("jsonb_insert", input_types, data_set)); + static_cast(check_function("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(check_function("jsonb_set", input_types, set_existing)); + static_cast( + check_function("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( + check_function("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( + check_function("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(check_function(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); + """ }