diff options
Diffstat (limited to 'src/mongo/dbtests')
| -rw-r--r-- | src/mongo/dbtests/cursor_manager_test.cpp | 27 | ||||
| -rw-r--r-- | src/mongo/dbtests/documentsourcetests.cpp | 10 | ||||
| -rw-r--r-- | src/mongo/dbtests/extensions_callback_real_test.cpp | 5 | ||||
| -rw-r--r-- | src/mongo/dbtests/indexcatalogtests.cpp | 125 | ||||
| -rw-r--r-- | src/mongo/dbtests/jsobjtests.cpp | 36 | ||||
| -rw-r--r-- | src/mongo/dbtests/jsontests.cpp | 3 | ||||
| -rw-r--r-- | src/mongo/dbtests/jstests.cpp | 2 | ||||
| -rw-r--r-- | src/mongo/dbtests/query_stage_update.cpp | 9 | ||||
| -rw-r--r-- | src/mongo/dbtests/querytests.cpp | 2 | ||||
| -rw-r--r-- | src/mongo/dbtests/validate_tests.cpp | 218 |
10 files changed, 399 insertions, 38 deletions
diff --git a/src/mongo/dbtests/cursor_manager_test.cpp b/src/mongo/dbtests/cursor_manager_test.cpp index f71e45fe2ec..52d543e9157 100644 --- a/src/mongo/dbtests/cursor_manager_test.cpp +++ b/src/mongo/dbtests/cursor_manager_test.cpp @@ -708,6 +708,33 @@ TEST_F(CursorManagerTestCustomOpCtx, MultipleCursorsMultipleOperationKeys) { ASSERT(cursors.find(cursor2) != cursors.end()); } +TEST_F(CursorManagerTestCustomOpCtx, MultipleCursorsSameOperationKey) { + auto opKey = UUID::gen(); + + auto opCtx = _queryServiceContext->makeOperationContext(); + opCtx->setOperationKey(opKey); + auto cursor1 = makeCursor(opCtx.get()).getCursor()->cursorid(); + auto cursor2 = makeCursor(opCtx.get()).getCursor()->cursorid(); + + // Retrieve cursors for operation key - should be both cursors. + auto cursors = useCursorManager()->getCursorsForOpKeys({opKey}); + ASSERT_EQ(cursors.size(), size_t(2)); + ASSERT(cursors.find(cursor1) != cursors.end()); + ASSERT(cursors.find(cursor2) != cursors.end()); + + // Now delete first one. The other should remain. + ASSERT_OK(useCursorManager()->killCursor(opCtx.get(), cursor1)); + cursors = useCursorManager()->getCursorsForOpKeys({opKey}); + ASSERT_EQ(cursors.size(), size_t(1)); + ASSERT(cursors.find(cursor1) == cursors.end()); + ASSERT(cursors.find(cursor2) != cursors.end()); + + // Now delete the other. None should remain. + ASSERT_OK(useCursorManager()->killCursor(opCtx.get(), cursor2)); + cursors = useCursorManager()->getCursorsForOpKeys({opKey}); + ASSERT_EQ(cursors.size(), size_t(0)); +} + TEST_F(CursorManagerTestCustomOpCtx, TimedOutCursorShouldNotBeReturnedForOpKeyLookup) { auto opKey = UUID::gen(); auto opCtx = _queryServiceContext->makeOperationContext(); diff --git a/src/mongo/dbtests/documentsourcetests.cpp b/src/mongo/dbtests/documentsourcetests.cpp index ca5d8b519d1..08349d40c15 100644 --- a/src/mongo/dbtests/documentsourcetests.cpp +++ b/src/mongo/dbtests/documentsourcetests.cpp @@ -240,7 +240,7 @@ TEST_F(DocumentSourceCursorTest, SerializationQueryPlannerExplainLevel) { ctx()->explain = verb; createSource(); - auto explainResult = source()->serialize(verb); + auto explainResult = source()->serialize(SerializationOptions{boost::make_optional(verb)}); ASSERT_FALSE(explainResult["$cursor"]["queryPlanner"].missing()); ASSERT_TRUE(explainResult["$cursor"]["executionStats"].missing()); @@ -255,7 +255,7 @@ TEST_F(DocumentSourceCursorTest, SerializationExecStatsExplainLevel) { // Execute the plan so that the source populates its internal execution stats. exhaustCursor(); - auto explainResult = source()->serialize(verb); + auto explainResult = source()->serialize(SerializationOptions{boost::make_optional(verb)}); ASSERT_FALSE(explainResult["$cursor"]["queryPlanner"].missing()); ASSERT_FALSE(explainResult["$cursor"]["executionStats"].missing()); ASSERT_TRUE(explainResult["$cursor"]["executionStats"]["allPlansExecution"].missing()); @@ -271,7 +271,8 @@ TEST_F(DocumentSourceCursorTest, SerializationExecAllPlansExplainLevel) { // Execute the plan so that the source populates its internal executionStats. exhaustCursor(); - auto explainResult = source()->serialize(verb).getDocument(); + auto explainResult = + source()->serialize(SerializationOptions{boost::make_optional(verb)}).getDocument(); ASSERT_FALSE(explainResult["$cursor"]["queryPlanner"].missing()); ASSERT_FALSE(explainResult["$cursor"]["executionStats"].missing()); ASSERT_FALSE(explainResult["$cursor"]["executionStats"]["allPlansExecution"].missing()); @@ -288,7 +289,8 @@ TEST_F(DocumentSourceCursorTest, ExpressionContextAndSerializeVerbosityMismatch) // Execute the plan so that the source populates its internal executionStats. exhaustCursor(); - ASSERT_THROWS_CODE(source()->serialize(verb2), DBException, 50660); + ASSERT_THROWS_CODE( + source()->serialize(SerializationOptions{boost::make_optional(verb2)}), DBException, 50660); } TEST_F(DocumentSourceCursorTest, TailableAwaitDataCursorShouldErrorAfterTimeout) { diff --git a/src/mongo/dbtests/extensions_callback_real_test.cpp b/src/mongo/dbtests/extensions_callback_real_test.cpp index a2d88117969..9475d0e2779 100644 --- a/src/mongo/dbtests/extensions_callback_real_test.cpp +++ b/src/mongo/dbtests/extensions_callback_real_test.cpp @@ -255,13 +255,10 @@ TEST_F(ExtensionsCallbackRealTest, WhereExpressionDesugarsToExprAndInternalJs) { auto expr1 = unittest::assertGet( ExtensionsCallbackReal(&_opCtx, &_nss).parseWhere(expCtx, query1.firstElement())); - BSONObjBuilder gotMatch; - expr1->serialize(&gotMatch); - auto expectedMatch = fromjson( "{$expr: {$function: {'body': 'function() { return this.x == 10; }', 'args': " "['$$CURRENT'], 'lang': 'js', '_internalSetObjToThis': true}}}"); - ASSERT_BSONOBJ_EQ(gotMatch.obj(), expectedMatch); + ASSERT_BSONOBJ_EQ(expr1->serialize(), expectedMatch); } } diff --git a/src/mongo/dbtests/indexcatalogtests.cpp b/src/mongo/dbtests/indexcatalogtests.cpp index 7a648f654f0..d80fd3a471d 100644 --- a/src/mongo/dbtests/indexcatalogtests.cpp +++ b/src/mongo/dbtests/indexcatalogtests.cpp @@ -229,6 +229,129 @@ public: } }; +class PrepareUniqueIndexRecords : IndexCatalogTestBase { +public: + ~PrepareUniqueIndexRecords() { + auto opCtx = cc().makeOperationContext(); + AutoGetDb db{opCtx.get(), _nss.db(), LockMode::MODE_X}; + WriteUnitOfWork wuow{opCtx.get()}; + ASSERT_OK(db.getDb()->dropCollection(opCtx.get(), _nss)); + wuow.commit(); + } + + void run() { + auto opCtx = cc().makeOperationContext(); + dbtests::WriteContextForTests ctx{opCtx.get(), _nss.ns()}; + + ASSERT_OK(dbtests::createIndexFromSpec( + opCtx.get(), + _nss.ns(), + BSON(IndexDescriptor::kIndexVersionFieldName + << static_cast<int>(kIndexVersion) << IndexDescriptor::kIndexNameFieldName << "a_1" + << IndexDescriptor::kKeyPatternFieldName << BSON("a" << 1) + << IndexDescriptor::kPrepareUniqueFieldName << true))); + + AutoGetCollection coll{opCtx.get(), _nss, LockMode::MODE_X}; + auto doc1 = BSON("_id" << 1 << "a" << 1); + auto doc2 = BSON("_id" << 2 << "a" << 1); + + { + WriteUnitOfWork wuow{opCtx.get()}; + ASSERT_OK(indexCatalog(opCtx.get()) + ->indexRecords(opCtx.get(), *coll, {{RecordId{1}, {}, &doc1}}, nullptr)); + wuow.commit(); + } + + { + WriteUnitOfWork wuow{opCtx.get()}; + ASSERT_NOT_OK( + indexCatalog(opCtx.get()) + ->indexRecords(opCtx.get(), *coll, {{RecordId{2}, {}, &doc2}}, nullptr)); + } + + opCtx->setEnforceConstraints(false); + + { + WriteUnitOfWork wuow{opCtx.get()}; + ASSERT_OK(indexCatalog(opCtx.get()) + ->indexRecords(opCtx.get(), *coll, {{RecordId{2}, {}, &doc2}}, nullptr)); + wuow.commit(); + } + } +}; + +class PrepareUniqueUpdateRecord : IndexCatalogTestBase { +public: + ~PrepareUniqueUpdateRecord() { + auto opCtx = cc().makeOperationContext(); + AutoGetDb db{opCtx.get(), _nss.db(), LockMode::MODE_X}; + WriteUnitOfWork wuow{opCtx.get()}; + ASSERT_OK(db.getDb()->dropCollection(opCtx.get(), _nss)); + wuow.commit(); + } + + void run() { + auto opCtx = cc().makeOperationContext(); + dbtests::WriteContextForTests ctx{opCtx.get(), _nss.ns()}; + + ASSERT_OK(dbtests::createIndexFromSpec( + opCtx.get(), + _nss.ns(), + BSON(IndexDescriptor::kIndexVersionFieldName + << static_cast<int>(kIndexVersion) << IndexDescriptor::kIndexNameFieldName << "a_1" + << IndexDescriptor::kKeyPatternFieldName << BSON("a" << 1) + << IndexDescriptor::kPrepareUniqueFieldName << true))); + + AutoGetCollection coll{opCtx.get(), _nss, LockMode::MODE_X}; + auto doc1 = BSON("_id" << 1 << "a" << 1); + auto doc2 = BSON("_id" << 2 << "a" << 2); + auto updatedDoc2 = BSON("_id" << 2 << "a" << 1); + + { + WriteUnitOfWork wuow{opCtx.get()}; + ASSERT_OK(indexCatalog(opCtx.get()) + ->indexRecords(opCtx.get(), + *coll, + {{RecordId{1}, {}, &doc1}, {RecordId{2}, {}, &doc2}}, + nullptr)); + wuow.commit(); + } + + { + WriteUnitOfWork wuow{opCtx.get()}; + int64_t keysInsertedOut, keysDeletedOut; + ASSERT_NOT_OK(indexCatalog(opCtx.get()) + ->updateRecord(opCtx.get(), + *coll, + doc2, + updatedDoc2, + RecordId{2}, + &keysInsertedOut, + &keysDeletedOut)); + ASSERT_EQ(keysInsertedOut, 0); + ASSERT_EQ(keysDeletedOut, 0); + } + + opCtx->setEnforceConstraints(false); + + { + WriteUnitOfWork wuow{opCtx.get()}; + int64_t keysInsertedOut, keysDeletedOut; + ASSERT_OK(indexCatalog(opCtx.get()) + ->updateRecord(opCtx.get(), + *coll, + doc2, + updatedDoc2, + RecordId{2}, + &keysInsertedOut, + &keysDeletedOut)); + ASSERT_EQ(keysInsertedOut, 1); + ASSERT_EQ(keysDeletedOut, 1); + wuow.commit(); + } + } +}; + class IndexCatalogTests : public OldStyleSuiteSpecification { public: IndexCatalogTests() : OldStyleSuiteSpecification("indexcatalogtests") {} @@ -236,6 +359,8 @@ public: add<IndexIteratorTests>(); add<IndexCatalogEntryDroppedTest>(); add<RefreshEntry>(); + add<PrepareUniqueIndexRecords>(); + add<PrepareUniqueUpdateRecord>(); } }; diff --git a/src/mongo/dbtests/jsobjtests.cpp b/src/mongo/dbtests/jsobjtests.cpp index 0c20c6f0f9b..16baa542c18 100644 --- a/src/mongo/dbtests/jsobjtests.cpp +++ b/src/mongo/dbtests/jsobjtests.cpp @@ -179,13 +179,13 @@ public: void run() { { BufBuilder b(0); - b.appendStr("foo"); + b.appendCStr("foo"); ASSERT_EQUALS(4, b.len()); ASSERT(strcmp("foo", b.buf()) == 0); } { mongo::StackBufBuilder b; - b.appendStr("foo"); + b.appendCStr("foo"); ASSERT_EQUALS(4, b.len()); ASSERT(strcmp("foo", b.buf()) == 0); } @@ -200,7 +200,7 @@ public: try { for (; written <= 64 * 1024 * 1024 + 1; ++written) // (re)alloc past the buffer 64mb limit - b.appendStr("a"); + b.appendCStr("a"); } catch (const AssertionException&) { } // assert half of max buffer size was allocated before exception is thrown @@ -505,7 +505,7 @@ public: bb << "a" << 1; BSONObj tmp = bb.asTempObj(); ASSERT(tmp.objsize() == 4 + (1 + 2 + 4) + 1); - ASSERT(tmp.valid()); + ASSERT_OK(mongo::validateBSON(tmp)); ASSERT(tmp.hasField("a")); ASSERT(!tmp.hasField("b")); ASSERT_BSONOBJ_EQ(tmp, BSON("a" << 1)); @@ -513,7 +513,7 @@ public: bb << "b" << 2; BSONObj obj = bb.obj(); ASSERT_EQUALS(obj.objsize(), 4 + (1 + 2 + 4) + (1 + 2 + 4) + 1); - ASSERT(obj.valid()); + ASSERT_OK(mongo::validateBSON(obj)); ASSERT(obj.hasField("a")); ASSERT(obj.hasField("b")); ASSERT_BSONOBJ_EQ(obj, BSON("a" << 1 << "b" << 2)); @@ -523,7 +523,7 @@ public: bb << "a" << GT << 1; BSONObj tmp = bb.asTempObj(); ASSERT(tmp.objsize() == 4 + (1 + 2 + (4 + 1 + 4 + 4 + 1)) + 1); - ASSERT(tmp.valid()); + ASSERT_OK(mongo::validateBSON(tmp)); ASSERT(tmp.hasField("a")); ASSERT(!tmp.hasField("b")); ASSERT_BSONOBJ_EQ(tmp, BSON("a" << BSON("$gt" << 1))); @@ -532,7 +532,7 @@ public: BSONObj obj = bb.obj(); ASSERT(obj.objsize() == 4 + (1 + 2 + (4 + 1 + 4 + 4 + 1)) + (1 + 2 + (4 + 1 + 4 + 4 + 1)) + 1); - ASSERT(obj.valid()); + ASSERT_OK(mongo::validateBSON(obj)); ASSERT(obj.hasField("a")); ASSERT(obj.hasField("b")); ASSERT_BSONOBJ_EQ(obj, BSON("a" << BSON("$gt" << 1) << "b" << BSON("$lt" << 2))); @@ -542,7 +542,7 @@ public: bb << "a" << 1; BSONObj tmp = bb.asTempObj(); ASSERT(tmp.objsize() == 4 + (1 + 2 + 4) + 1); - ASSERT(tmp.valid()); + ASSERT_OK(mongo::validateBSON(tmp)); ASSERT(tmp.hasField("a")); ASSERT(!tmp.hasField("b")); ASSERT_BSONOBJ_EQ(tmp, BSON("a" << 1)); @@ -554,7 +554,7 @@ public: } bb << "b" << arr.arr(); BSONObj obj = bb.obj(); - ASSERT(obj.valid()); + ASSERT_OK(mongo::validateBSON(obj)); ASSERT(obj.hasField("a")); ASSERT(obj.hasField("b")); } @@ -756,8 +756,8 @@ class Base { public: virtual ~Base() {} void run() { - ASSERT(valid().valid()); - ASSERT(!invalid().valid()); + ASSERT_OK(mongo::validateBSON(valid())); + ASSERT(!mongo::validateBSON(invalid()).isOK()); } protected: @@ -806,7 +806,7 @@ public: b.appendNull("a"); BSONObj o = b.done(); set(o, 4, mongo::Undefined); - ASSERT(o.valid()); + ASSERT_OK(mongo::validateBSON(o)); } }; @@ -990,7 +990,7 @@ public: void run() { const char data[] = {0x07, 0x00, 0x00, 0x00, char(type_), 'a', 0x00}; BSONObj o(data); - ASSERT(!o.valid()); + ASSERT(!mongo::validateBSON(o).isOK()); } private: @@ -1329,7 +1329,7 @@ public: b2.done(); b1.append("f", 10.0); BSONObj ret = b1.done(); - ASSERT(ret.valid()); + ASSERT_OK(mongo::validateBSON(ret)); ASSERT(ret.woCompare(fromjson("{a:'bcd',foo:{ggg:44},f:10}")) == 0); } }; @@ -1350,7 +1350,7 @@ public: BSONObj o = BSON("now" << DATENOW); Date_t after = jsTime(); - ASSERT(o.valid()); + ASSERT_OK(mongo::validateBSON(o)); BSONElement e = o["now"]; ASSERT(e.type() == Date); @@ -1368,7 +1368,7 @@ public: b.appendTimeT("now", aTime); BSONObj o = b.obj(); - ASSERT(o.valid()); + ASSERT_OK(mongo::validateBSON(o)); BSONElement e = o["now"]; ASSERT_EQUALS(Date, e.type()); @@ -1382,8 +1382,8 @@ public: BSONObj min = BSON("a" << MINKEY); BSONObj max = BSON("b" << MAXKEY); - ASSERT(min.valid()); - ASSERT(max.valid()); + ASSERT_OK(mongo::validateBSON(min)); + ASSERT_OK(mongo::validateBSON(max)); BSONElement minElement = min["a"]; BSONElement maxElement = max["b"]; diff --git a/src/mongo/dbtests/jsontests.cpp b/src/mongo/dbtests/jsontests.cpp index e175afbf22d..c4410a8c45e 100644 --- a/src/mongo/dbtests/jsontests.cpp +++ b/src/mongo/dbtests/jsontests.cpp @@ -625,7 +625,7 @@ void assertEquals(const std::string& json, } void checkEquivalence(const std::string& json, const BSONObj& bson) { - ASSERT(fromjson(json).valid()); + ASSERT_OK(mongo::validateBSON(fromjson(json))); assertEquals(json, bson, fromjson(json), "mode: json-to-bson"); assertEquals(json, bson, fromjson(tojson(bson)), "mode: <default>"); assertEquals(json, bson, fromjson(tojson(bson, LegacyStrict)), "mode: strict"); @@ -836,6 +836,7 @@ TEST(FromJsonTest, BinDataTypes) { {0x05, MD5Type}, {0x06, Encrypt}, {0x07, Column}, + {0x08, Sensitive}, {0x80, bdtCustom}, }; for (const auto& ts : specs) { diff --git a/src/mongo/dbtests/jstests.cpp b/src/mongo/dbtests/jstests.cpp index 9e48d81b23e..9a396d240f5 100644 --- a/src/mongo/dbtests/jstests.cpp +++ b/src/mongo/dbtests/jstests.cpp @@ -785,7 +785,7 @@ public: { BSONObjBuilder b; b.bb().appendNum(static_cast<char>(bsonTimestamp)); - b.bb().appendStr("a"); + b.bb().appendCStr("a"); b.bb().appendNum(std::numeric_limits<unsigned long long>::max()); in = b.obj(); diff --git a/src/mongo/dbtests/query_stage_update.cpp b/src/mongo/dbtests/query_stage_update.cpp index 2ae8011e9c8..1c6d2302260 100644 --- a/src/mongo/dbtests/query_stage_update.cpp +++ b/src/mongo/dbtests/query_stage_update.cpp @@ -55,15 +55,6 @@ #include "mongo/db/update/update_driver.h" #include "mongo/dbtests/dbtests.h" -#define ASSERT_DOES_NOT_THROW(EXPRESSION) \ - try { \ - EXPRESSION; \ - } catch (const AssertionException& e) { \ - ::str::stream err; \ - err << "Threw an exception incorrectly: " << e.toString(); \ - ::mongo::unittest::TestAssertionFailure(__FILE__, __LINE__, err).stream(); \ - } - namespace QueryStageUpdate { using std::make_unique; diff --git a/src/mongo/dbtests/querytests.cpp b/src/mongo/dbtests/querytests.cpp index f7aefecaf55..2fe9e828355 100644 --- a/src/mongo/dbtests/querytests.cpp +++ b/src/mongo/dbtests/querytests.cpp @@ -1205,7 +1205,7 @@ public: std::unique_ptr<DBClientCursor> cursor = _client.find(std::move(findRequest)); while (cursor->more()) { BSONObj o = cursor->next(); - verify(o.valid()); + verify(validateBSON(o).isOK()); } } void run() { diff --git a/src/mongo/dbtests/validate_tests.cpp b/src/mongo/dbtests/validate_tests.cpp index f4e81c264e2..f85755ac87e 100644 --- a/src/mongo/dbtests/validate_tests.cpp +++ b/src/mongo/dbtests/validate_tests.cpp @@ -184,6 +184,23 @@ protected: dumpOnErrorGuard.dismiss(); } + void ensureValidateWarned() { + ValidateResults results = runValidate(); + + ScopeGuard dumpOnErrorGuard([&] { + StorageDebugUtil::printValidateResults(results); + StorageDebugUtil::printCollectionAndIndexTableEntries(&_opCtx, _nss); + }); + + ASSERT_TRUE(results.valid) << "Validation failed when it should've worked."; + ASSERT_TRUE(results.errors.empty()) + << "Validation reported errors when it should not have."; + ASSERT_FALSE(results.warnings.empty()) + << "Validation did not report a warning when it should have."; + + dumpOnErrorGuard.dismiss(); + } + void ensureValidateFailed() { ValidateResults results = runValidate(); @@ -4243,6 +4260,205 @@ public: } }; +/** + * Validate detects duplicate keys in a secondary unique index {a: 1} when the index is + * on a clustered collection. + * Two cases are tested: + * 1. The false negative case: when validate says there isn't a uniqueness + * violation even though there is one. + * 2. The false positive case: when validate says there is a uniqueness + * violation even though there isn't one. + * + * False negative case: + * Suppose we have two documents {_id: "1000000000", a: 1} and {_id: "1000000000", a: 1} + * that live in a collection. Since they have the same value for field 'a', they violate + * the uniqueness constraint of the index. + * The key strings for index {a: 1} for the two docs look something like this. + * They map from the value of 'a' in the document to the recordId. + * Buffer for keystring1: 1,1000000000 + * Buffer for keystring2: 1,2000000000 + * + * When we compareWithoutRecordIdLong(), we chop off only the number of + * bytes used in a long before making the comparison in the buffer. Since a long + * is 8 bytes, we cut 8 characters off. + * Truncated buffer 1: 1,10 + * Truncated buffer 2: 1,20 + * + * And we can see that the two truncated buffers above still aren't equal. But instead, + * if we used compareWithoutRecordIdStr(), we first figure out how many bytes we need + * to chop to exclude the recordId, and that way only the index entry value is compared. + * Now the unique index violation can be detected, as both the truncated buffers are + * equal. + * Truncated buffer 1: 1 + * Truncated buffer 2: 1 + * + * False positive case: + * Suppose we have two documents {_id: "1", a: 10000001} and {_id: "2", a: 10000002}. + * Clearly they don't violate any constraints. However it is possible, if we truncate + * more bytes than necessary, that we will end up truncating some of the bytes of the + * field 'a'. For example, + * Pre-truncation: + * Buffer for keystring1: 10000001,1 + * Buffer for keystring2: 10000002,2 + * Post-truncation: + * Buffer for keystring1: 10000 + * Buffer for keystring2: 10000 + * This can lead to a false positive uniqueness violation. + */ +template <bool falsePositiveCase> +class ValidateDuplicateKeyOnClusteredCollection : public ValidateBase { +public: + ValidateDuplicateKeyOnClusteredCollection() + : ValidateBase(/*full=*/true, /*background=*/false, /*clustered=*/true) {} + + void run() { + // Cannot run validate with {background:true} if the storage engine does not support + // checkpoints. + if (_background && !_supportsBackgroundValidation) { + return; + } + + SharedBufferFragmentBuilder pooledBuilder( + KeyString::HeapBuilder::kHeapAllocatorDefaultBytes); + + lockDb(MODE_X); + ASSERT(coll()); + + // Create a unique index on {a: 1} + const auto indexName = "a"; + const auto indexKey = BSON("a" << 1); + auto status = dbtests::createIndexFromSpec( + &_opCtx, + coll()->ns().ns(), + BSON("name" << indexName << "key" << indexKey << "v" << static_cast<int>(kIndexVersion) + << "unique" << true)); + ASSERT_OK(status); + + + // Insert documents. + auto firstDoc = BSON("_id" + << "1000000000000" + << "a" << 1); + auto secondDoc = BSON("_id" + << "2000000000000" + << "a" << 1); + if (falsePositiveCase) { + firstDoc = BSON("_id" + << "1" + << "a" << 10000001); + secondDoc = BSON("_id" + << "2" + << "a" << 10000002); + } + OpDebug* const nullOpDebug = nullptr; + lockDb(MODE_X); + { + WriteUnitOfWork wunit(&_opCtx); + ASSERT_OK( + coll()->insertDocument(&_opCtx, InsertStatement(firstDoc), nullOpDebug, true)); + if (falsePositiveCase) { + ASSERT_OK( + coll()->insertDocument(&_opCtx, InsertStatement(secondDoc), nullOpDebug, true)); + } + wunit.commit(); + } + releaseDb(); + ensureValidateWorked(); + + // Insert a document with a duplicate key for "a". + if (!falsePositiveCase) { + lockDb(MODE_X); + + const IndexCatalog* indexCatalog = coll()->getIndexCatalog(); + + InsertDeleteOptions options; + options.dupsAllowed = true; + + WriteUnitOfWork wunit(&_opCtx); + + // Insert a record and its keys separately. We do this to bypass duplicate constraint + // checking. Inserting a record and all of its keys ensures that validation fails + // because there are duplicate keys, and not just because there are keys without + // corresponding records. + auto swRecordId = + coll()->getRecordStore()->insertRecord(&_opCtx, + record_id_helpers::keyForObj(secondDoc), + secondDoc.objdata(), + secondDoc.objsize(), + Timestamp()); + ASSERT_OK(swRecordId); + wunit.commit(); + + // Insert the key on "a". + { + auto descriptor = indexCatalog->findIndexByName(&_opCtx, indexName); + auto entry = const_cast<IndexCatalogEntry*>(indexCatalog->getEntry(descriptor)); + auto iam = entry->accessMethod()->asSortedData(); + auto interceptor = std::make_unique<IndexBuildInterceptor>(&_opCtx, entry); + + KeyStringSet keys; + iam->getKeys(&_opCtx, + coll(), + pooledBuilder, + secondDoc, + InsertDeleteOptions::ConstraintEnforcementMode::kRelaxConstraints, + SortedDataIndexAccessMethod::GetKeysContext::kAddingKeys, + &keys, + nullptr, + nullptr, + swRecordId.getValue()); + ASSERT_EQ(1, keys.size()); + + { + WriteUnitOfWork wunit(&_opCtx); + + int64_t numInserted; + auto insertStatus = iam->insertKeysAndUpdateMultikeyPaths( + &_opCtx, + coll(), + {keys.begin(), keys.end()}, + {}, + MultikeyPaths{}, + options, + [this, &interceptor](const KeyString::Value& duplicateKey) { + return interceptor->recordDuplicateKey(&_opCtx, duplicateKey); + }, + &numInserted); + + ASSERT_EQUALS(numInserted, 1); + ASSERT_OK(insertStatus); + + wunit.commit(); + } + + ASSERT_NOT_OK(interceptor->checkDuplicateKeyConstraints(&_opCtx)); + } + + releaseDb(); + } + + ValidateResults results = runValidate(); + + ScopeGuard dumpOnErrorGuard([&] { + StorageDebugUtil::printValidateResults(results); + StorageDebugUtil::printCollectionAndIndexTableEntries(&_opCtx, coll()->ns()); + }); + + if (falsePositiveCase) { + ASSERT(results.valid) << "Validation failed when it should have worked."; + ASSERT_EQ(static_cast<size_t>(0), results.errors.size()); + } else { + ASSERT_FALSE(results.valid) << "Validation worked when it should have failed."; + ASSERT_EQ(static_cast<size_t>(1), results.errors.size()); + } + ASSERT_EQ(static_cast<size_t>(0), omitTransientWarningsFromCount(results)); + ASSERT_EQ(static_cast<size_t>(0), results.extraIndexEntries.size()); + ASSERT_EQ(static_cast<size_t>(0), results.missingIndexEntries.size()); + + dumpOnErrorGuard.dismiss(); + } +}; + class ValidateRepairOnClusteredCollection : public ValidateBase { public: ValidateRepairOnClusteredCollection() @@ -4568,6 +4784,8 @@ public: add<ValidateInvalidBSONOnClusteredCollection<true>>(); add<ValidateReportInfoOnClusteredCollection<false>>(); add<ValidateReportInfoOnClusteredCollection<true>>(); + add<ValidateDuplicateKeyOnClusteredCollection<true /*falsePositiveCase*/>>(); + add<ValidateDuplicateKeyOnClusteredCollection<false /*falsePositiveCase*/>>(); add<ValidateRepairOnClusteredCollection>(); add<ValidateInvalidRecordIdOnClusteredCollection<false>>(false /*withSecondaryIndex*/); |
