summaryrefslogtreecommitdiff
path: root/src/mongo/dbtests
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/dbtests')
-rw-r--r--src/mongo/dbtests/cursor_manager_test.cpp27
-rw-r--r--src/mongo/dbtests/documentsourcetests.cpp10
-rw-r--r--src/mongo/dbtests/extensions_callback_real_test.cpp5
-rw-r--r--src/mongo/dbtests/indexcatalogtests.cpp125
-rw-r--r--src/mongo/dbtests/jsobjtests.cpp36
-rw-r--r--src/mongo/dbtests/jsontests.cpp3
-rw-r--r--src/mongo/dbtests/jstests.cpp2
-rw-r--r--src/mongo/dbtests/query_stage_update.cpp9
-rw-r--r--src/mongo/dbtests/querytests.cpp2
-rw-r--r--src/mongo/dbtests/validate_tests.cpp218
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*/);