/** * Copyright (C) 2020-present MongoDB, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the Server Side Public License, version 1, * as published by MongoDB, Inc. * * 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 * Server Side Public License for more details. * * You should have received a copy of the Server Side Public License * along with this program. If not, see * . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the Server Side Public License in all respects for * all of the code used other than as permitted herein. If you modify file(s) * with this exception, you may extend this exception to your version of the * file(s), but you are not obligated to do so. If you do not wish to do so, * delete this exception statement from your version. If you delete this * exception statement from all source files in the program, then also delete * it in the license file. */ #include "mongo/db/update/v2_log_builder.h" #include "mongo/base/status.h" #include "mongo/bson/bsonobj.h" #include "mongo/bson/mutable/mutable_bson_test_utils.h" #include "mongo/db/json.h" #include "mongo/unittest/death_test.h" #include "mongo/unittest/unittest.h" #include "mongo/util/safe_num.h" namespace mongo::v2_log_builder { namespace { namespace mmb = mongo::mutablebson; /** * Given a FieldRef, creates a RuntimeUpdatePath based on it, assuming that every numeric component * is an array index. */ RuntimeUpdatePath makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes(StringData path) { FieldRef fr(path); std::vector types; for (int i = 0; i < fr.numParts(); ++i) { types.push_back(fr.isNumericPathComponentStrict(i) ? RuntimeUpdatePath::ComponentType::kArrayIndex : RuntimeUpdatePath::ComponentType::kFieldName); } return RuntimeUpdatePath(fr, std::move(types)); } /** * Given a FieldRef, creates a RuntimeUpdatePath based on it, assuming that every component is a * field name. */ RuntimeUpdatePath makeRuntimeUpdatePathAssumeAllComponentsFieldNames(std::string path) { FieldRef fieldRef(path); RuntimeUpdatePath::ComponentTypeVector types(fieldRef.numParts(), RuntimeUpdatePath::ComponentType::kFieldName); return RuntimeUpdatePath(std::move(fieldRef), std::move(types)); } TEST(V2LogBuilder, UpdateFieldWithTopLevelMutableBsonElement) { mmb::Document doc; // Element is an array. Modifying a sub-element of mmb::Element does not store the data // serialized. So we need to call Element::writeToArray() in those cases to serialize the array. // We explicity modify the sub-element so that this logic can be tested. const mmb::Element eltArr = doc.makeElementArray("", BSON_ARRAY(1 << BSON("sub" << "obj") << 3)); ASSERT_OK(eltArr.leftChild().setValueString("val")); // Element is an obj. Modifying a sub-element of mmb::Element does not store the data // serialized. So we need to call Element::writeChildren() in those cases to serialize the // object. We explicity modify the sub-element so that this logic can be tested. mmb::Element eltObj = doc.makeElementObject("", fromjson("{arr: [{a: [0]}, 2]}")); ASSERT_OK(eltObj.leftChild().rightChild().setValueString("val")); // Element is a scalar. const mmb::Element eltScalar = doc.makeElementString("c.d", "val"); ASSERT_TRUE(eltScalar.ok()); V2LogBuilder lb; ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.b"), eltArr)); ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("c.d"), eltObj)); ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("c.e"), eltScalar)); ASSERT_BSONOBJ_BINARY_EQ( mongo::fromjson("{ $v: 2, diff: { sa: {u: {b: ['val', {sub: 'obj'}, 3]}}, sc: {u: {d: " "{arr: [{a: [0]}, 'val']}, e: 'val'}} }}"), lb.serialize()); } TEST(V2LogBuilder, UpdateFieldMutableBson) { auto storage = fromjson("{subArray: [{a: [0]}], array: [{}], scalar: 'val'}"); mmb::Document doc( fromjson("{topLevel: {obj: {subArray: 1}, array: [[], [0], {}], scalar: 'val'}}")); // Element is an obj. Modifying a sub-element of mmb::Element does not store the data // serialized. So we need to call Element::writeChildren() in those cases to serialize the // object. We explicity modify the sub-element so that this logic can be tested. mmb::Element eltObj = doc.root().leftChild().leftChild(); ASSERT_OK(eltObj.leftChild().setValueBSONElement(storage["subArray"])); // Element is an array. const mmb::Element eltArr = eltObj.rightSibling(); ASSERT_OK(eltArr.leftChild().setValueBSONElement(storage["array"])); // Element is a scalar. const mmb::Element eltScalar = eltArr.rightSibling(); ASSERT_TRUE(eltScalar.ok()); V2LogBuilder lb; ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.b"), eltArr)); ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("c.d"), eltObj)); ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("c.e"), eltScalar)); ASSERT_BSONOBJ_BINARY_EQ( mongo::fromjson("{ $v: 2, diff: { sa: {u: {b: [[{}], [0], {}]}}, sc: {u: {d: " "{subArray: [{a: [0]}]}, e: 'val'}} }}"), lb.serialize()); } TEST(V2LogBuilder, CreateFieldBSONElt) { BSONObj storage = fromjson("{arr: ['v','a','l'], obj: {}, scalar: 2}"); V2LogBuilder lb; ASSERT_OK(lb.logCreatedField( makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.b.d"), 0, storage["arr"])); ASSERT_OK(lb.logCreatedField( makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.c.e"), 0, storage["obj"])); ASSERT_OK(lb.logCreatedField( makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.b.c"), 0, storage["scalar"])); ASSERT_BSONOBJ_BINARY_EQ( mongo::fromjson("{ $v: 2, diff: {i : {a: {b: {d: ['v','a','l'], c: 2}, c: {e: {}}} }}}"), lb.serialize()); } TEST(V2LogBuilder, CreateFieldMutableBson) { mmb::Document doc; V2LogBuilder lb; const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); ASSERT_TRUE(elt_ab.ok()); ASSERT_OK(lb.logCreatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.b"), 0, // idxOfFirstNewComponent (unused) elt_ab)); ASSERT_BSONOBJ_BINARY_EQ(mongo::fromjson("{ $v: 2, diff: { i: {a: {b: 1}}}}"), lb.serialize()); } TEST(V2LogBuilder, CreateFieldMergeInOrder) { mmb::Document doc; const mmb::Element eltScalar = doc.makeElementInt("scalar", 1); const mmb::Element eltObj = doc.makeElementObject("obj", fromjson("{arr: [{a: [0]}]}")); ASSERT_TRUE(eltObj.ok()); ASSERT_TRUE(eltScalar.ok()); // Test where only a 'root' object exists and logCreateField() needs to create sub-objects // recursively. const int idxOfFirstNewComponent = 1; V2LogBuilder lb; ASSERT_OK(lb.logCreatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("root.d"), idxOfFirstNewComponent, eltScalar)); ASSERT_OK(lb.logCreatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("root.b.a"), idxOfFirstNewComponent, eltScalar)); ASSERT_OK(lb.logCreatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("root.c.b"), idxOfFirstNewComponent, eltObj)); ASSERT_OK(lb.logCreatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("root.c.e"), idxOfFirstNewComponent, eltScalar)); ASSERT_OK(lb.logCreatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("root.a"), idxOfFirstNewComponent, eltObj)); ASSERT_BSONOBJ_BINARY_EQ(mongo::fromjson("{ $v: 2, diff: { sroot: {i: {d: 1, b: {a: 1}, c: {b: " "{arr: [{a: [0]}]}, e: 1}, a: {arr: [{a: [0]}]}} }}}"), lb.serialize()); } TEST(V2LogBuilder, BasicDelete) { mmb::Document doc; V2LogBuilder lb; ASSERT_OK(lb.logDeletedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("x"))); ASSERT_BSONOBJ_BINARY_EQ(mongo::fromjson("{ $v: 2, diff: { d: {x: false}}}"), lb.serialize()); } TEST(V2LogBuilder, VerifyDeletesOrder) { mmb::Document doc; V2LogBuilder lb; ASSERT_OK(lb.logDeletedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.c"))); ASSERT_OK(lb.logDeletedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.b"))); ASSERT_OK(lb.logDeletedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("x.z"))); ASSERT_OK(lb.logDeletedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("x.y"))); ASSERT_BSONOBJ_BINARY_EQ( mongo::fromjson( "{ $v: 2, diff: { sa: {d: {c: false, b: false}}, sx: {d: {z: false, y: false}} }}"), lb.serialize()); } TEST(V2LogBuilder, WithAllModificationTypes) { mmb::Document doc; V2LogBuilder lb; const mmb::Element elt_ab = doc.makeElementInt("", 1); ASSERT_TRUE(elt_ab.ok()); ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.b.c"), elt_ab)); const mmb::Element elt_cd = doc.makeElementInt("", 2); ASSERT_TRUE(elt_cd.ok()); ASSERT_OK(lb.logCreatedField( makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.b.d.e"), 3, elt_cd)); ASSERT_OK(lb.logDeletedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.b.e"))); ASSERT_BSONOBJ_BINARY_EQ( mongo::fromjson("{ $v: 2, diff: {sa: {sb: {d: {e: false}, u: {c: 1}, sd: {i: {e: 2}} }}}}"), lb.serialize()); } TEST(V2LogBuilder, VerifySubDiffsAreOrderedCorrectly) { mmb::Document doc; V2LogBuilder lb; const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); ASSERT_TRUE(elt_ab.ok()); ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.b.c"), elt_ab)); ASSERT_OK( lb.logCreatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("a.c.d"), 2, elt_ab)); const mmb::Element elt_xy = doc.makeElementInt("x.y", 2); ASSERT_TRUE(elt_xy.ok()); ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("x.y.z"), elt_xy)); ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeAllComponentsFieldNames("x.z.a"), elt_xy)); ASSERT_BSONOBJ_BINARY_EQ( mongo::fromjson("{ $v: 2, diff: { sa: {sb: {u: {c: 1}}, sc: {i: {d: 1}}}, " "sx: {sy: {u: {z: 2}}, sz: {u: {a: 2}}} }}"), lb.serialize()); } TEST(V2LogBuilder, ArrayModifications) { mmb::Document doc; V2LogBuilder lb; const mmb::Element elt_ab = doc.makeElementInt("", 1); ASSERT_TRUE(elt_ab.ok()); ASSERT_OK(lb.logUpdatedField( makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes("a.0.c.update"), elt_ab)); // Element is an obj. const mmb::Element eltObj = doc.makeElementObject("", fromjson("{arr: [{a: [0]}]}")); ASSERT_TRUE(eltObj.ok()); const mmb::Element eltScalar = doc.makeElementInt("", 2); ASSERT_TRUE(eltScalar.ok()); // The order of the update of array elements need not be maintained by the caller. ASSERT_OK(lb.logCreatedField( makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes("a.10.insert.a"), 2, eltScalar)); ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes("a.3"), elt_ab)); ASSERT_OK(lb.logCreatedField( makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes("a.0.c.insert.a"), 3, eltScalar)); ASSERT_OK(lb.logCreatedField( makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes("a.0.c.insert.f"), 3, eltScalar)); ASSERT_OK(lb.logCreatedField( makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes("a.1"), 1, eltObj)); ASSERT_OK( lb.logDeletedField(makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes("a.0.c.del"))); ASSERT_BSONOBJ_BINARY_EQ( mongo::fromjson("{$v: 2, diff: {sa: {a: true, s0: {sc: {d: {del: false}, u: {update: 1}, " "i: {insert: {a: 2, f: 2}}}}, u1: {arr: [{a: [0]}]}, u3: 1, s10: {i: " "{insert: {a: 2}}} }}}"), lb.serialize()); } TEST(V2LogBuilder, ImplicityArrayElementCreationAllowed) { mmb::Document doc; V2LogBuilder lb; const mmb::Element elt = doc.makeElementInt("", 1); ASSERT_TRUE(elt.ok()); ASSERT_OK(lb.logCreatedField( makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes("a.10.b.a"), 1, elt)); ASSERT_OK(lb.logCreatedField( makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes("a.2.b.a"), 1, elt)); ASSERT_OK( lb.logUpdatedField(makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes("a.0.b.a"), elt)); ASSERT_BSONOBJ_BINARY_EQ( mongo::fromjson("{$v: 2, diff: {sa: {a: true, s0: {sb: {u: {a: 1}}}, u2: " "{b: {a: 1}}, u10: {b: {a: 1}} }}}"), lb.serialize()); } DEATH_TEST(V2LogBuilder, ImplicityArrayCreationDisallowed, "invariant") { mmb::Document doc; V2LogBuilder lb; const mmb::Element elt = doc.makeElementInt("", 1); ASSERT_TRUE(elt.ok()); auto status = lb.logCreatedField( makeRuntimeUpdatePathAssumeNumericComponentsAreIndexes("a.10.insert.a"), 0, elt); } } // namespace } // namespace mongo::v2_log_builder