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);
  }
}
}