unittest/gunit/item_json_func-t.cc (571 lines of code) (raw):

/* Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include <gtest/gtest.h> #include <cstring> #include "base_mock_field.h" #include "fake_table.h" #include "item_json_func.h" #include "json_diff.h" #include "json_dom.h" #include "test_utils.h" namespace item_json_func_unittest { class ItemJsonFuncTest : public ::testing::Test { protected: void SetUp() override { initializer.SetUp(); m_table.in_use= thd(); init_alloc_root(PSI_NOT_INSTRUMENTED, &m_table.mem_root, 256, 0); } void TearDown() override { m_table.cleanup_partial_update(); initializer.TearDown(); } THD *thd() { return initializer.thd(); } my_testing::Server_initializer initializer; Base_mock_field_json m_field{}; Fake_TABLE m_table{&m_field}; }; /** Parse a JSON text and return its DOM representation. @param json_text null-terminated string of JSON text @return a DOM representing the JSON document */ static Json_dom *parse_json(const char *json_text) { const char *msg; size_t msg_offset; auto dom= Json_dom::parse(json_text, std::strlen(json_text), &msg, &msg_offset); EXPECT_NE(nullptr, dom); return dom; } static Item_string *new_item_string(const char *str) { return new Item_string(str, std::strlen(str), &my_charset_utf8mb4_bin); } static void store_json(Field_json *field, const char *json_text) { if (json_text == nullptr) { EXPECT_EQ(TYPE_OK, set_field_to_null(field)); } else { field->set_notnull(); Json_wrapper doc(parse_json(json_text)); EXPECT_EQ(TYPE_OK, field->store_json(&doc)); } } /** Perform a partial update on a JSON column and verify the result. @param func the JSON function to invoke @param field the JSON column to update @param orig_json text representation of the original JSON value @param new_json text representation of the expected value in the column after the partial update @param binary_update whether binary diffs can be used @param logical_update whether logical diffs can be used */ static void do_partial_update(Item_json_func *func, Field_json *field, const char *orig_json, const char *new_json, bool binary_update, bool logical_update) { const auto table= field->table; if (!func->fixed) { EXPECT_FALSE(func->fix_fields(table->in_use, nullptr)); EXPECT_TRUE(func->supports_partial_update(field)); func->mark_for_partial_update(field); } table->clear_partial_update_diffs(); store_json(field, orig_json); EXPECT_TRUE(table->is_binary_diff_enabled(field)); EXPECT_TRUE(table->is_logical_diff_enabled(field)); Json_wrapper res1; EXPECT_FALSE(func->val_json(&res1)); EXPECT_EQ(new_json == nullptr, func->null_value); EXPECT_EQ(binary_update, table->is_binary_diff_enabled(field)); EXPECT_EQ(logical_update, table->is_logical_diff_enabled(field)); if (new_json == nullptr) return; Json_wrapper new_doc(parse_json(new_json)); EXPECT_EQ(0, res1.compare(new_doc)); if (!logical_update) { EXPECT_EQ(nullptr, table->get_logical_diffs(field)); return; } /* Take a copy of the JSON diffs, since the call to clear_partial_update_diffs() below will clear the original Json_diff_vector. */ const auto thd= table->in_use; Json_diff_vector diffs(Json_diff_vector::allocator_type(thd->mem_root)); for (const auto &diff : *table->get_logical_diffs(field)) diffs.emplace_back(diff.path(), diff.operation(), diff.value().clone_dom(thd)); /* apply_json_diffs() will try to collect binary diffs for the changes that it applies to the column, so we should clear the already collected diffs. */ table->clear_partial_update_diffs(); EXPECT_TRUE(table->is_binary_diff_enabled(field)); EXPECT_EQ(enum_json_diff_status::SUCCESS, apply_json_diffs(field, &diffs)); EXPECT_EQ(binary_update, table->is_binary_diff_enabled(field)); Json_wrapper res2; EXPECT_FALSE(field->val_json(&res2)); EXPECT_EQ(0, res2.compare(new_doc)); // apply_json_diffs() should produce new JSON diffs. EXPECT_TRUE(table->is_logical_diff_enabled(field)); const Json_diff_vector *new_diffs= table->get_logical_diffs(field); EXPECT_NE(nullptr, new_diffs); EXPECT_EQ(diffs.size(), new_diffs->size()); // ... and applying those new diffs should produce the same result again ... diffs.clear(); for (const auto &diff : *new_diffs) diffs.emplace_back(diff.path(), diff.operation(), diff.value().clone_dom(thd)); table->clear_partial_update_diffs(); store_json(field, orig_json); EXPECT_EQ(enum_json_diff_status::SUCCESS, apply_json_diffs(field, &diffs)); Json_wrapper res3; EXPECT_FALSE(field->val_json(&res3)); EXPECT_EQ(0, res3.compare(new_doc)); } /* Test partial update using various JSON functions. */ TEST_F(ItemJsonFuncTest, PartialUpdate) { m_field.make_writable(); auto json_set= new Item_func_json_set(thd(), new Item_field(&m_field), new_item_string("$[1]"), new_item_string("abc"), new_item_string("$[2]"), new Item_int(100)); EXPECT_FALSE(m_table.mark_column_for_partial_update(&m_field)); EXPECT_FALSE(m_table.setup_partial_update(true)); // Logical update OK, but not enough space for binary update. { SCOPED_TRACE(""); do_partial_update(json_set, &m_field, "[1,2,3]", "[1,\"abc\",100]", false, true); } // Both logical update and binary update OK. { SCOPED_TRACE(""); do_partial_update(json_set, &m_field, "[4,\"XYZ\",5]", "[4,\"abc\",100]", true, true); } // The array grows, so only logical update is OK. { SCOPED_TRACE(""); do_partial_update(json_set, &m_field, "[6,\"XYZ\"]", "[6,\"abc\",100]", false, true); } // The root document is auto-wrapped, so no partial update at all. { SCOPED_TRACE(""); do_partial_update(json_set, &m_field, "true", "[true,\"abc\",100]", false, false); } // A sub-document is auto-wrapped. OK for logical update, but not for binary. { SCOPED_TRACE(""); auto wrap_set= new Item_func_json_set(thd(), new Item_field(&m_field), new_item_string("$.x[2]"), new Item_int(2), new_item_string("$.x[1]"), new Item_int(1)); do_partial_update(wrap_set, &m_field, "{\"x\":123}", "{\"x\":[123,1]}", false, true); } // Replacing the root of the document leads to full update. { SCOPED_TRACE(""); auto replace_root= new Item_func_json_replace(thd(), new Item_field(&m_field), new_item_string("$"), new Item_int(1)); do_partial_update(replace_root, &m_field, "{\"a\":[1,2,3]}", "1", false, false); } // A nested call. { auto inner_func= new Item_func_json_set(thd(), new Item_field(&m_field), new_item_string("$.a[1]"), new Item_int(1)); auto outer_func= new Item_func_json_replace(thd(), inner_func, new_item_string("$.b"), new Item_int(2)); { SCOPED_TRACE(""); do_partial_update(outer_func, &m_field, "{\"a\":[1,2,3]}", "{\"a\":[1,1,3]}", true, true); } { SCOPED_TRACE(""); do_partial_update(outer_func, &m_field, "{\"a\":[1,2,3],\"b\":47}", "{\"a\":[1,1,3],\"b\":2}", true, true); } { SCOPED_TRACE(""); do_partial_update(outer_func, &m_field, "{\"a\":8}", "{\"a\":[8,1]}", false, true); } { SCOPED_TRACE(""); do_partial_update(outer_func, &m_field, nullptr, nullptr, false, false); } } // A nested call where the inner function causes a full update. { auto inner_func= new Item_func_json_set(thd(), new Item_field(&m_field), new_item_string("$"), new Item_func_json_array( thd(), new Item_int(1), new Item_int(2))); auto outer_func= new Item_func_json_set(thd(), inner_func, new_item_string("$[1]"), new Item_int(3)); SCOPED_TRACE(""); do_partial_update(outer_func, &m_field, "[true,false]", "[1,3]", false, false); } // Returning NULL should cause full update. { SCOPED_TRACE(""); auto null_path= new Item_func_json_set(thd(), new Item_field(&m_field), new Item_null(), new Item_int(1)); do_partial_update(null_path, &m_field, "[1,2,3]", nullptr, false, false); } // Input document being NULL should cause full update. { SCOPED_TRACE(""); auto null_doc= new Item_func_json_set(thd(), new Item_field(&m_field), new_item_string("$.a.b.c"), new Item_int(1)); do_partial_update(null_doc, &m_field, nullptr, nullptr, false, false); } // Setting object member. { auto set_member= new Item_func_json_set(thd(), new Item_field(&m_field), new_item_string("$.a"), new Item_int(1)); // Existing member can be replaced with both binary and logical update. { SCOPED_TRACE(""); do_partial_update(set_member, &m_field, "{\"a\":\"b\"}", "{\"a\":1}", true, true); } // Non-existing member can be added with logical update. { SCOPED_TRACE(""); do_partial_update(set_member, &m_field, "{}", "{\"a\":1}", false, true); } { SCOPED_TRACE(""); do_partial_update(set_member, &m_field, "[5,6,7]", "[5,6,7]", true, true); } { SCOPED_TRACE(""); do_partial_update(set_member, &m_field, "123", "123", true, true); } { SCOPED_TRACE(""); do_partial_update(set_member, &m_field, nullptr, nullptr, false, false); } } // Replacing object member. { auto replace= new Item_func_json_replace(thd(), new Item_field(&m_field), new_item_string("$.a"), new Item_int(1)); // Existing member can be replaced with both binary and logical update. { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "{\"a\":\"b\"}", "{\"a\":1}", true, true); } // Replacing non-existing member is a no-op. { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "{}", "{}", true, true); } { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "[5,6,7]", "[5,6,7]", true, true); } { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "123", "123", true, true); } { SCOPED_TRACE(""); do_partial_update(replace, &m_field, nullptr, nullptr, false, false); } } // Setting array element. { auto set_element= new Item_func_json_set(thd(), new Item_field(&m_field), new_item_string("$[1]"), new Item_int(1)); { SCOPED_TRACE(""); do_partial_update(set_element, &m_field, "[4,5,6]", "[4,1,6]", true, true); } { SCOPED_TRACE(""); do_partial_update(set_element, &m_field, "[]", "[1]", false, true); } { SCOPED_TRACE(""); do_partial_update(set_element, &m_field, "[2]", "[2,1]", false, true); } { SCOPED_TRACE(""); do_partial_update(set_element, &m_field, "{\"a\":2}", "[{\"a\":2},1]", false, false); } { SCOPED_TRACE(""); do_partial_update(set_element, &m_field, "123", "[123,1]", false, false); } { SCOPED_TRACE(""); do_partial_update(set_element, &m_field, nullptr, nullptr, false, false); } } { auto set_element= new Item_func_json_set(thd(), new Item_field(&m_field), new_item_string("$.a[1]"), new Item_int(1)); { SCOPED_TRACE(""); do_partial_update(set_element, &m_field, "{\"a\":[4,5,6]}", "{\"a\":[4,1,6]}", true, true); } { SCOPED_TRACE(""); do_partial_update(set_element, &m_field, "{\"a\":[]}", "{\"a\":[1]}", false, true); } { SCOPED_TRACE(""); do_partial_update(set_element, &m_field, "{\"a\":{\"b\":2}}", "{\"a\":[{\"b\":2},1]}", false, true); } { SCOPED_TRACE(""); do_partial_update(set_element, &m_field, "{\"a\":123}", "{\"a\":[123,1]}", false, true); } } // Replacing array element. { auto replace= new Item_func_json_replace(thd(), new Item_field(&m_field), new_item_string("$[1]"), new Item_int(1)); { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "[4,5,6]", "[4,1,6]", true, true); } { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "[]", "[]", true, true); } { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "[2]", "[2]", true, true); } { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "{\"a\":2}", "{\"a\":2}", true, true); } { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "123", "123", true, true); } { SCOPED_TRACE(""); do_partial_update(replace, &m_field, nullptr, nullptr, false, false); } } { auto replace= new Item_func_json_replace(thd(), new Item_field(&m_field), new_item_string("$.a[1]"), new Item_int(1)); { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "{\"a\":[4,5,6]}", "{\"a\":[4,1,6]}", true, true); } { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "{\"a\":[]}", "{\"a\":[]}", true, true); } { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "{\"a\":{\"b\":2}}", "{\"a\":{\"b\":2}}", true, true); } { SCOPED_TRACE(""); do_partial_update(replace, &m_field, "{\"a\":123}", "{\"a\":123}", true, true); } } // Remove an element in an array. { auto remove= new Item_func_json_remove(thd(), new Item_field(&m_field), new_item_string("$[1]")); { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{}", "{}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "[]", "[]", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, nullptr, nullptr, false, false); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "[1,2,3]", "[1,3]", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "[1,2]", "[1]", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "[1]", "[1]", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{\"a\":1}", "{\"a\":1}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "123", "123", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, nullptr, nullptr, false, false); } } // Remove a member from an object. { auto remove= new Item_func_json_remove(thd(), new Item_field(&m_field), new_item_string("$.x")); { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{}", "{}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{\"a\":1}", "{\"a\":1}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{\"x\":1}", "{}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{\"a\":\"b\",\"c\":\"d\",\"x\":\"y\",\"z\":\"w\"}", "{\"a\":\"b\",\"c\":\"d\",\"z\":\"w\"}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "[1,2,3]", "[1,2,3]", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "123", "123", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, nullptr, nullptr, false, false); } } // Remove multiple paths. { auto remove= new Item_func_json_remove(thd(), new Item_field(&m_field), new_item_string("$.a.b"), new_item_string("$.c[1]")); { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{}", "{}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{\"a\":{\"b\":\"c\"}, \"b\":{\"c\":\"d\"}, " "\"c\":[1,2,3]}", "{\"a\":{}, \"b\":{\"c\":\"d\"}, \"c\":[1,3]}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{\"a\":{\"b\":\"c\"}, \"b\":{\"c\":\"d\"}}", "{\"a\":{}, \"b\":{\"c\":\"d\"}}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{\"b\":{\"c\":\"d\"}, \"c\":[1,2,3]}", "{\"b\":{\"c\":\"d\"}, \"c\":[1,3]}", true, true); } } // JSON_REMOVE with NULL as path. { SCOPED_TRACE(""); auto remove= new Item_func_json_remove(thd(), new Item_field(&m_field), new Item_null()); do_partial_update(remove, &m_field, "[1,2]", nullptr, false, false); } // Mixed JSON_REMOVE/JSON_SET. { auto set= new Item_func_json_set(thd(), new Item_field(&m_field), new_item_string("$.a"), new_item_string("abc")); auto remove= new Item_func_json_remove(thd(), set, new_item_string("$.b")); { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{}", "{\"a\":\"abc\"}", false, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, nullptr, nullptr, false, false); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{\"b\":123}", "{\"a\":\"abc\"}", false, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{\"a\":\"xyz\",\"b\":123}", "{\"a\":\"abc\"}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, nullptr, nullptr, false, false); } } // Remove with auto-wrap. { auto remove= new Item_func_json_remove(thd(), new Item_field(&m_field), new_item_string("$[0][0].a[0][0].b")); { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{}", "{}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "[]", "[]", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "{\"a\":{\"b\":1,\"c\":2}}", "{\"a\":{\"c\":2}}", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, "[{\"a\":[{\"b\":1,\"c\":2},123]}, 456]", "[{\"a\":[{\"c\":2},123]}, 456]", true, true); } { SCOPED_TRACE(""); do_partial_update(remove, &m_field, nullptr, nullptr, false, false); } } // Append or prepend when setting with out-of-bounds array indexes. { SCOPED_TRACE(""); auto set= new Item_func_json_set(thd(), new Item_field(&m_field), new_item_string("$[2]"), new Item_int(88), new_item_string("$[last-2]"), new Item_int(99)); do_partial_update(set, &m_field, "[]", "[99,88]", false, true); do_partial_update(set, &m_field, "[1]", "[99,1,88]", false, true); do_partial_update(set, &m_field, "[1,2]", "[99,2,88]", false, true); do_partial_update(set, &m_field, "[1,2,3]", "[99,2,88]", true, true); do_partial_update(set, &m_field, "[1,2,3,4]", "[1,99,88,4]", true, true); do_partial_update(set, &m_field, nullptr, nullptr, false, false); } } }