//@file indexupdatetests.cpp : mongo/db/index_update.{h,cpp} tests /** * Copyright (C) 2012 10gen Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General 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 GNU Affero General 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/platform/basic.h" #include #include "mongo/db/catalog/collection.h" #include "mongo/db/catalog/index_catalog.h" #include "mongo/db/catalog/index_create.h" #include "mongo/db/client.h" #include "mongo/db/db_raii.h" #include "mongo/db/dbdirectclient.h" #include "mongo/db/index/index_descriptor.h" #include "mongo/db/service_context.h" #include "mongo/db/service_context_d.h" #include "mongo/dbtests/dbtests.h" namespace IndexUpdateTests { namespace { const auto kIndexVersion = IndexDescriptor::IndexVersion::kV2; } // namespace static const char* const _ns = "unittests.indexupdate"; /** * Test fixture for a write locked test using collection _ns. Includes functionality to * partially construct a new IndexDetails in a manner that supports proper cleanup in * dropCollection(). */ class IndexBuildBase { public: IndexBuildBase() : _ctx(&_opCtx, _ns), _client(&_opCtx) { _client.createCollection(_ns); } ~IndexBuildBase() { _client.dropCollection(_ns); getGlobalServiceContext()->unsetKillAllOperations(); } Collection* collection() { return _ctx.getCollection(); } protected: Status createIndex(const std::string& dbname, const BSONObj& indexSpec); bool buildIndexInterrupted(const BSONObj& key, bool allowInterruption) { try { MultiIndexBlock indexer(&_opCtx, collection()); if (allowInterruption) indexer.allowInterruption(); uassertStatusOK(indexer.init(key)); uassertStatusOK(indexer.insertAllDocumentsInCollection()); WriteUnitOfWork wunit(&_opCtx); indexer.commit(); wunit.commit(); } catch (const DBException& e) { if (ErrorCodes::isInterruption(e.code())) return true; throw; } return false; } const ServiceContext::UniqueOperationContext _txnPtr = cc().makeOperationContext(); OperationContext& _opCtx = *_txnPtr; OldClientWriteContext _ctx; DBDirectClient _client; }; /** Index creation ignores unique constraints when told to. */ template class InsertBuildIgnoreUnique : public IndexBuildBase { public: void run() { // Create a new collection. Database* db = _ctx.db(); Collection* coll; { WriteUnitOfWork wunit(&_opCtx); db->dropCollection(&_opCtx, _ns).transitional_ignore(); coll = db->createCollection(&_opCtx, _ns); OpDebug* const nullOpDebug = nullptr; coll->insertDocument(&_opCtx, InsertStatement(BSON("_id" << 1 << "a" << "dup")), nullOpDebug, true) .transitional_ignore(); coll->insertDocument(&_opCtx, InsertStatement(BSON("_id" << 2 << "a" << "dup")), nullOpDebug, true) .transitional_ignore(); wunit.commit(); } MultiIndexBlock indexer(&_opCtx, coll); indexer.allowBackgroundBuilding(); indexer.allowInterruption(); indexer.ignoreUniqueConstraint(); const BSONObj spec = BSON("name" << "a" << "ns" << coll->ns().ns() << "key" << BSON("a" << 1) << "v" << static_cast(kIndexVersion) << "unique" << true << "background" << background); ASSERT_OK(indexer.init(spec).getStatus()); ASSERT_OK(indexer.insertAllDocumentsInCollection()); WriteUnitOfWork wunit(&_opCtx); indexer.commit(); wunit.commit(); } }; /** Index creation enforces unique constraints unless told not to. */ template class InsertBuildEnforceUnique : public IndexBuildBase { public: void run() { // Create a new collection. Database* db = _ctx.db(); Collection* coll; { WriteUnitOfWork wunit(&_opCtx); db->dropCollection(&_opCtx, _ns).transitional_ignore(); coll = db->createCollection(&_opCtx, _ns); OpDebug* const nullOpDebug = nullptr; coll->insertDocument(&_opCtx, InsertStatement(BSON("_id" << 1 << "a" << "dup")), nullOpDebug, true) .transitional_ignore(); coll->insertDocument(&_opCtx, InsertStatement(BSON("_id" << 2 << "a" << "dup")), nullOpDebug, true) .transitional_ignore(); wunit.commit(); } MultiIndexBlock indexer(&_opCtx, coll); indexer.allowBackgroundBuilding(); indexer.allowInterruption(); // indexer.ignoreUniqueConstraint(); // not calling this const BSONObj spec = BSON("name" << "a" << "ns" << coll->ns().ns() << "key" << BSON("a" << 1) << "v" << static_cast(kIndexVersion) << "unique" << true << "background" << background); ASSERT_OK(indexer.init(spec).getStatus()); const Status status = indexer.insertAllDocumentsInCollection(); ASSERT_EQUALS(status.code(), ErrorCodes::DuplicateKey); } }; /** Index creation fills a passed-in set of dups rather than failing. */ template class InsertBuildFillDups : public IndexBuildBase { public: void run() { // Create a new collection. Database* db = _ctx.db(); Collection* coll; RecordId loc1; RecordId loc2; { WriteUnitOfWork wunit(&_opCtx); db->dropCollection(&_opCtx, _ns).transitional_ignore(); coll = db->createCollection(&_opCtx, _ns); OpDebug* const nullOpDebug = nullptr; ASSERT_OK(coll->insertDocument(&_opCtx, InsertStatement(BSON("_id" << 1 << "a" << "dup")), nullOpDebug, true)); ASSERT_OK(coll->insertDocument(&_opCtx, InsertStatement(BSON("_id" << 2 << "a" << "dup")), nullOpDebug, true)); wunit.commit(); } MultiIndexBlock indexer(&_opCtx, coll); indexer.allowBackgroundBuilding(); indexer.allowInterruption(); // indexer.ignoreUniqueConstraint(); // not calling this const BSONObj spec = BSON("name" << "a" << "ns" << coll->ns().ns() << "key" << BSON("a" << 1) << "v" << static_cast(kIndexVersion) << "unique" << true << "background" << background); ASSERT_OK(indexer.init(spec).getStatus()); std::set dups; ASSERT_OK(indexer.insertAllDocumentsInCollection(&dups)); // either loc1 or loc2 should be in dups but not both. ASSERT_EQUALS(dups.size(), 1U); for (auto recordId : dups) { ASSERT_NOT_EQUALS(recordId, RecordId()); BSONObj obj = coll->docFor(&_opCtx, recordId).value(); int id = obj["_id"].Int(); ASSERT(id == 1 || id == 2); } } }; /** Index creation is killed if mayInterrupt is true. */ class InsertBuildIndexInterrupt : public IndexBuildBase { public: void run() { // Create a new collection. Database* db = _ctx.db(); Collection* coll; { WriteUnitOfWork wunit(&_opCtx); db->dropCollection(&_opCtx, _ns).transitional_ignore(); coll = db->createCollection(&_opCtx, _ns); // Drop all indexes including id index. coll->getIndexCatalog()->dropAllIndexes(&_opCtx, true); // Insert some documents with enforceQuota=true. int32_t nDocs = 1000; OpDebug* const nullOpDebug = nullptr; for (int32_t i = 0; i < nDocs; ++i) { coll->insertDocument(&_opCtx, InsertStatement(BSON("a" << i)), nullOpDebug, true) .transitional_ignore(); } wunit.commit(); } // Request an interrupt. getGlobalServiceContext()->setKillAllOperations(); BSONObj indexInfo = BSON("key" << BSON("a" << 1) << "ns" << _ns << "name" << "a_1" << "v" << static_cast(kIndexVersion)); // The call is interrupted because mayInterrupt == true. ASSERT_TRUE(buildIndexInterrupted(indexInfo, true)); // only want to interrupt the index build getGlobalServiceContext()->unsetKillAllOperations(); // The new index is not listed in the index catalog because the index build failed. ASSERT(!coll->getIndexCatalog()->findIndexByName(&_opCtx, "a_1")); } }; /** Index creation is not killed if mayInterrupt is false. */ class InsertBuildIndexInterruptDisallowed : public IndexBuildBase { public: void run() { // Create a new collection. Database* db = _ctx.db(); Collection* coll; { WriteUnitOfWork wunit(&_opCtx); db->dropCollection(&_opCtx, _ns).transitional_ignore(); coll = db->createCollection(&_opCtx, _ns); coll->getIndexCatalog()->dropAllIndexes(&_opCtx, true); // Insert some documents. int32_t nDocs = 1000; OpDebug* const nullOpDebug = nullptr; for (int32_t i = 0; i < nDocs; ++i) { coll->insertDocument(&_opCtx, InsertStatement(BSON("a" << i)), nullOpDebug, true) .transitional_ignore(); } wunit.commit(); } // Request an interrupt. getGlobalServiceContext()->setKillAllOperations(); BSONObj indexInfo = BSON("key" << BSON("a" << 1) << "ns" << _ns << "name" << "a_1" << "v" << static_cast(kIndexVersion)); // The call is not interrupted because mayInterrupt == false. ASSERT_FALSE(buildIndexInterrupted(indexInfo, false)); // only want to interrupt the index build getGlobalServiceContext()->unsetKillAllOperations(); // The new index is listed in the index catalog because the index build completed. ASSERT(coll->getIndexCatalog()->findIndexByName(&_opCtx, "a_1")); } }; /** Index creation is killed when building the _id index. */ class InsertBuildIdIndexInterrupt : public IndexBuildBase { public: void run() { // Recreate the collection as capped, without an _id index. Database* db = _ctx.db(); Collection* coll; { WriteUnitOfWork wunit(&_opCtx); db->dropCollection(&_opCtx, _ns).transitional_ignore(); CollectionOptions options; options.capped = true; options.cappedSize = 10 * 1024; coll = db->createCollection(&_opCtx, _ns, options); coll->getIndexCatalog()->dropAllIndexes(&_opCtx, true); // Insert some documents. int32_t nDocs = 1000; OpDebug* const nullOpDebug = nullptr; for (int32_t i = 0; i < nDocs; ++i) { coll->insertDocument(&_opCtx, InsertStatement(BSON("_id" << i)), nullOpDebug, true) .transitional_ignore(); } wunit.commit(); } // Request an interrupt. getGlobalServiceContext()->setKillAllOperations(); BSONObj indexInfo = BSON("key" << BSON("_id" << 1) << "ns" << _ns << "name" << "_id_" << "v" << static_cast(kIndexVersion)); // The call is interrupted because mayInterrupt == true. ASSERT_TRUE(buildIndexInterrupted(indexInfo, true)); // only want to interrupt the index build getGlobalServiceContext()->unsetKillAllOperations(); // The new index is not listed in the index catalog because the index build failed. ASSERT(!coll->getIndexCatalog()->findIndexByName(&_opCtx, "_id_")); } }; /** Index creation is not killed when building the _id index if mayInterrupt is false. */ class InsertBuildIdIndexInterruptDisallowed : public IndexBuildBase { public: void run() { // Recreate the collection as capped, without an _id index. Database* db = _ctx.db(); Collection* coll; { WriteUnitOfWork wunit(&_opCtx); db->dropCollection(&_opCtx, _ns).transitional_ignore(); CollectionOptions options; options.capped = true; options.cappedSize = 10 * 1024; coll = db->createCollection(&_opCtx, _ns, options); coll->getIndexCatalog()->dropAllIndexes(&_opCtx, true); // Insert some documents. int32_t nDocs = 1000; OpDebug* const nullOpDebug = nullptr; for (int32_t i = 0; i < nDocs; ++i) { coll->insertDocument(&_opCtx, InsertStatement(BSON("_id" << i)), nullOpDebug, true) .transitional_ignore(); } wunit.commit(); } // Request an interrupt. getGlobalServiceContext()->setKillAllOperations(); BSONObj indexInfo = BSON("key" << BSON("_id" << 1) << "ns" << _ns << "name" << "_id_" << "v" << static_cast(kIndexVersion)); // The call is not interrupted because mayInterrupt == false. ASSERT_FALSE(buildIndexInterrupted(indexInfo, false)); // only want to interrupt the index build getGlobalServiceContext()->unsetKillAllOperations(); // The new index is listed in the index catalog because the index build succeeded. ASSERT(coll->getIndexCatalog()->findIndexByName(&_opCtx, "_id_")); } }; Status IndexBuildBase::createIndex(const std::string& dbname, const BSONObj& indexSpec) { MultiIndexBlock indexer(&_opCtx, collection()); Status status = indexer.init(indexSpec).getStatus(); if (status == ErrorCodes::IndexAlreadyExists) { return Status::OK(); } if (!status.isOK()) { return status; } status = indexer.insertAllDocumentsInCollection(); if (!status.isOK()) { return status; } WriteUnitOfWork wunit(&_opCtx); indexer.commit(); wunit.commit(); return Status::OK(); } /** * Fixture class that has a basic compound index. */ class SimpleCompoundIndex : public IndexBuildBase { public: SimpleCompoundIndex() { ASSERT_OK(createIndex("unittest", BSON("name" << "x" << "ns" << _ns << "key" << BSON("x" << 1 << "y" << 1) << "v" << static_cast(kIndexVersion)))); } }; class SameSpecDifferentOption : public SimpleCompoundIndex { public: void run() { // Cannot have same key spec with an option different from the existing one. ASSERT_EQUALS(ErrorCodes::IndexOptionsConflict, createIndex("unittest", BSON("name" << "x" << "ns" << _ns << "unique" << true << "key" << BSON("x" << 1 << "y" << 1) << "v" << static_cast(kIndexVersion)))); } }; class SameSpecSameOptions : public SimpleCompoundIndex { public: void run() { ASSERT_OK(createIndex("unittest", BSON("name" << "x" << "ns" << _ns << "key" << BSON("x" << 1 << "y" << 1) << "v" << static_cast(kIndexVersion)))); } }; class DifferentSpecSameName : public SimpleCompoundIndex { public: void run() { // Cannot create a different index with the same name as the existing one. ASSERT_EQUALS(ErrorCodes::IndexKeySpecsConflict, createIndex("unittest", BSON("name" << "x" << "ns" << _ns << "key" << BSON("y" << 1 << "x" << 1) << "v" << static_cast(kIndexVersion)))); } }; /** * Fixture class for indexes with complex options. */ class ComplexIndex : public IndexBuildBase { public: ComplexIndex() { ASSERT_OK(createIndex("unittests", BSON("name" << "super" << "ns" << _ns << "unique" << 1 << "sparse" << true << "expireAfterSeconds" << 3600 << "key" << BSON("superIdx" << "2d") << "v" << static_cast(kIndexVersion)))); } }; class SameSpecSameOptionDifferentOrder : public ComplexIndex { public: void run() { // Exactly the same specs with the existing one, only // specified in a different order than the original. ASSERT_OK(createIndex("unittests", BSON("name" << "super2" << "ns" << _ns << "expireAfterSeconds" << 3600 << "sparse" << true << "unique" << 1 << "key" << BSON("superIdx" << "2d") << "v" << static_cast(kIndexVersion)))); } }; // The following tests tries to create an index with almost the same // specs as the original, except for one option. class SameSpecDifferentUnique : public ComplexIndex { public: void run() { ASSERT_EQUALS(ErrorCodes::IndexOptionsConflict, createIndex("unittest", BSON("name" << "super2" << "ns" << _ns << "unique" << false << "sparse" << true << "expireAfterSeconds" << 3600 << "key" << BSON("superIdx" << "2d") << "v" << static_cast(kIndexVersion)))); } }; class SameSpecDifferentSparse : public ComplexIndex { public: void run() { ASSERT_EQUALS(ErrorCodes::IndexOptionsConflict, createIndex("unittest", BSON("name" << "super2" << "ns" << _ns << "unique" << 1 << "sparse" << false << "background" << true << "expireAfterSeconds" << 3600 << "key" << BSON("superIdx" << "2d") << "v" << static_cast(kIndexVersion)))); } }; class SameSpecDifferentTTL : public ComplexIndex { public: void run() { ASSERT_EQUALS(ErrorCodes::IndexOptionsConflict, createIndex("unittest", BSON("name" << "super2" << "ns" << _ns << "unique" << 1 << "sparse" << true << "expireAfterSeconds" << 2400 << "key" << BSON("superIdx" << "2d") << "v" << static_cast(kIndexVersion)))); } }; class StorageEngineOptions : public IndexBuildBase { public: void run() { // "storageEngine" field has to be an object if present. ASSERT_NOT_OK(createIndex("unittest", _createSpec(12345))); // 'storageEngine' must not be empty. ASSERT_NOT_OK(createIndex("unittest", _createSpec(BSONObj()))); // Every field under "storageEngine" must match a registered storage engine. ASSERT_NOT_OK(createIndex("unittest", _createSpec(BSON("unknownEngine" << BSONObj())))); // Testing with 'wiredTiger' because the registered storage engine factory // supports custom index options under 'storageEngine'. const std::string storageEngineName = "wiredTiger"; // Run 'wiredTiger' tests if the storage engine is supported. if (getGlobalServiceContext()->isRegisteredStorageEngine(storageEngineName)) { // Every field under "storageEngine" has to be an object. ASSERT_NOT_OK(createIndex("unittest", _createSpec(BSON(storageEngineName << 1)))); // Storage engine options must pass validation by the storage engine factory. // For 'wiredTiger', embedded document must contain 'configString'. ASSERT_NOT_OK(createIndex( "unittest", _createSpec(BSON(storageEngineName << BSON("unknown" << 1))))); // Configuration string for 'wiredTiger' must be a string. ASSERT_NOT_OK(createIndex( "unittest", _createSpec(BSON(storageEngineName << BSON("configString" << 1))))); // Valid 'wiredTiger' configuration. ASSERT_OK(createIndex( "unittest", _createSpec(BSON(storageEngineName << BSON("configString" << "block_compressor=zlib"))))); } } protected: template BSONObj _createSpec(T storageEngineValue) { return BSON("name" << "super2" << "ns" << _ns << "key" << BSON("a" << 1) << "v" << static_cast(kIndexVersion) << "storageEngine" << storageEngineValue); } }; class IndexCatatalogFixIndexKey { public: void run() { ASSERT_BSONOBJ_EQ(BSON("x" << 1), IndexCatalog::fixIndexKey(BSON("x" << 1))); ASSERT_BSONOBJ_EQ(BSON("_id" << 1), IndexCatalog::fixIndexKey(BSON("_id" << 1))); ASSERT_BSONOBJ_EQ(BSON("_id" << 1), IndexCatalog::fixIndexKey(BSON("_id" << true))); } }; class InsertSymbolIntoIndexWithCollationFails { public: void run() { auto opCtx = cc().makeOperationContext(); DBDirectClient client(opCtx.get()); client.dropCollection(_ns); IndexSpec indexSpec; indexSpec.addKey("a").addOptions(BSON("collation" << BSON("locale" << "fr"))); client.createIndex(_ns, indexSpec); client.insert(_ns, BSON("a" << BSONSymbol("mySymbol"))); ASSERT_EQUALS(client.getLastErrorDetailed()["code"].numberInt(), ErrorCodes::CannotBuildIndexKeys); ASSERT_EQUALS(client.count(_ns), 0U); } }; class InsertSymbolIntoIndexWithoutCollationSucceeds { public: void run() { auto opCtx = cc().makeOperationContext(); DBDirectClient client(opCtx.get()); client.dropCollection(_ns); IndexSpec indexSpec; indexSpec.addKey("a"); client.createIndex(_ns, indexSpec); client.insert(_ns, BSON("a" << BSONSymbol("mySymbol"))); ASSERT(client.getLastError().empty()); ASSERT_EQUALS(client.count(_ns), 1U); } }; class InsertSymbolInsideNestedObjectIntoIndexWithCollationFails { public: void run() { auto opCtx = cc().makeOperationContext(); DBDirectClient client(opCtx.get()); client.dropCollection(_ns); IndexSpec indexSpec; indexSpec.addKey("a").addOptions(BSON("collation" << BSON("locale" << "fr"))); client.createIndex(_ns, indexSpec); client.insert(_ns, BSON("a" << BSON("b" << 99 << "c" << BSONSymbol("mySymbol")))); ASSERT_EQUALS(client.getLastErrorDetailed()["code"].numberInt(), ErrorCodes::CannotBuildIndexKeys); ASSERT_EQUALS(client.count(_ns), 0U); } }; class InsertSymbolInsideNestedArrayIntoIndexWithCollationFails { public: void run() { auto opCtx = cc().makeOperationContext(); DBDirectClient client(opCtx.get()); client.dropCollection(_ns); IndexSpec indexSpec; indexSpec.addKey("a").addOptions(BSON("collation" << BSON("locale" << "fr"))); client.createIndex(_ns, indexSpec); client.insert(_ns, BSON("a" << BSON_ARRAY(99 << BSONSymbol("mySymbol")))); ASSERT_EQUALS(client.getLastErrorDetailed()["code"].numberInt(), ErrorCodes::CannotBuildIndexKeys); ASSERT_EQUALS(client.count(_ns), 0U); } }; class BuildingIndexWithCollationWhenSymbolDataExistsShouldFail { public: void run() { auto opCtx = cc().makeOperationContext(); DBDirectClient client(opCtx.get()); client.dropCollection(_ns); client.insert(_ns, BSON("a" << BSON_ARRAY(99 << BSONSymbol("mySymbol")))); ASSERT_EQUALS(client.count(_ns), 1U); IndexSpec indexSpec; indexSpec.addKey("a").addOptions(BSON("collation" << BSON("locale" << "fr"))); ASSERT_THROWS_CODE(client.createIndex(_ns, indexSpec), AssertionException, ErrorCodes::CannotBuildIndexKeys); } }; class IndexingSymbolWithInheritedCollationShouldFail { public: void run() { auto opCtx = cc().makeOperationContext(); DBDirectClient client(opCtx.get()); client.dropCollection(_ns); BSONObj cmdResult; ASSERT_TRUE(client.runCommand("unittests", BSON("create" << "indexupdate" << "collation" << BSON("locale" << "fr")), cmdResult)); IndexSpec indexSpec; indexSpec.addKey("a"); client.createIndex(_ns, indexSpec); client.insert(_ns, BSON("a" << BSON_ARRAY(99 << BSONSymbol("mySymbol")))); ASSERT_EQUALS(client.getLastErrorDetailed()["code"].numberInt(), ErrorCodes::CannotBuildIndexKeys); } }; class IndexUpdateTests : public Suite { public: IndexUpdateTests() : Suite("indexupdate") {} void setupTests() { add>(); add>(); add>(); add>(); add>(); add>(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); } } indexUpdateTests; } // namespace IndexUpdateTests