summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLouis Williams <louis.williams@mongodb.com>2021-11-05 15:08:40 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2021-11-05 16:09:45 +0000
commit7bcb659c013aaf44e1edab7b8a102a738c0bfb8c (patch)
tree89d69d8c8bc4eb13bc6523ef89cba8f64b5b6e94
parent43cba1cc995ffc6436493246b10146365d593b8e (diff)
downloadmongo-7bcb659c013aaf44e1edab7b8a102a738c0bfb8c.tar.gz
SERVER-54360 Support secondary unique indexes on clustered collections
-rw-r--r--jstests/libs/clustered_indexes_utils.js191
-rw-r--r--src/mongo/db/catalog/index_catalog_impl.cpp4
-rw-r--r--src/mongo/db/index/duplicate_key_tracker.cpp7
-rw-r--r--src/mongo/db/index/index_access_method.cpp8
-rw-r--r--src/mongo/db/storage/key_string.cpp9
-rw-r--r--src/mongo/db/storage/key_string.h38
-rw-r--r--src/mongo/db/storage/sorted_data_interface_test_keyformat_string.cpp133
-rw-r--r--src/mongo/db/storage/wiredtiger/wiredtiger_index.cpp31
-rw-r--r--src/mongo/db/storage/wiredtiger/wiredtiger_index.h1
-rw-r--r--src/mongo/db/storage/wiredtiger/wiredtiger_kv_engine.cpp6
-rw-r--r--src/mongo/db/storage/wiredtiger/wiredtiger_standard_index_test.cpp4
-rw-r--r--src/mongo/db/timeseries/timeseries_commands_conversion_helper.cpp7
12 files changed, 420 insertions, 19 deletions
diff --git a/jstests/libs/clustered_indexes_utils.js b/jstests/libs/clustered_indexes_utils.js
new file mode 100644
index 00000000000..7d99c0df71b
--- /dev/null
+++ b/jstests/libs/clustered_indexes_utils.js
@@ -0,0 +1,191 @@
+/**
+ * Utilies for clustered collections.
+ */
+
+function areClusteredIndexesEnabled(conn) {
+ const clusteredIndexesEnabled =
+ assert.commandWorked(conn.adminCommand({getParameter: 1, featureFlagClusteredIndexes: 1}))
+ .featureFlagClusteredIndexes.value;
+
+ if (!clusteredIndexesEnabled) {
+ return false;
+ }
+ return true;
+}
+
+function validateClusteredCollection(db, collName, clusterKey) {
+ const lengths = [100, 1024, 1024 * 1024, 3 * 1024 * 1024];
+ const coll = db[collName];
+ const clusterKeyString = new String(clusterKey);
+
+ assert.commandWorked(
+ db.createCollection(collName, {clusteredIndex: {key: {[clusterKey]: 1}, unique: true}}));
+
+ // Expect that duplicates are rejected.
+ for (let len of lengths) {
+ let id = 'x'.repeat(len);
+ assert.commandWorked(coll.insert({[clusterKey]: id}));
+ assert.commandFailedWithCode(coll.insert({[clusterKey]: id}), ErrorCodes.DuplicateKey);
+ assert.eq(1, coll.find({[clusterKey]: id}).itcount());
+ }
+
+ // Updates should work.
+ for (let len of lengths) {
+ let id = 'x'.repeat(len);
+
+ // Validate the below for _id-clustered collection only until the following tickets are
+ // addressed:
+ // * TODO SERVER-60734 replacement updates should preserve the cluster key
+ // * TODO SERVER-60702 enable bounded collscans for arbitary cluster keys
+ if (clusterKey == "_id") {
+ assert.commandWorked(coll.update({[clusterKey]: id}, {a: len}));
+
+ assert.eq(1, coll.find({[clusterKey]: id}).itcount());
+ assert.eq(len, coll.findOne({[clusterKey]: id})['a']);
+ }
+ }
+
+ // This section is based on jstests/core/timeseries/clustered_index_crud.js with
+ // specific additions for general-purpose (non-timeseries) clustered collections
+ assert.commandWorked(coll.insert({[clusterKey]: 0, a: 1}));
+ assert.commandWorked(coll.insert({[clusterKey]: 1, a: 1}));
+ assert.eq(1, coll.find({[clusterKey]: 0}).itcount());
+ assert.commandWorked(coll.insert({[clusterKey]: "", a: 2}));
+ assert.eq(1, coll.find({[clusterKey]: ""}).itcount());
+ assert.commandWorked(coll.insert({[clusterKey]: NumberLong("9223372036854775807"), a: 3}));
+ assert.eq(1, coll.find({[clusterKey]: NumberLong("9223372036854775807")}).itcount());
+ assert.commandWorked(coll.insert({[clusterKey]: {a: 1, b: 1}, a: 4}));
+ assert.eq(1, coll.find({[clusterKey]: {a: 1, b: 1}}).itcount());
+ assert.commandWorked(coll.insert({[clusterKey]: {a: {b: 1}, c: 1}, a: 5}));
+ assert.commandWorked(coll.insert({[clusterKey]: -1, a: 6}));
+ assert.eq(1, coll.find({[clusterKey]: -1}).itcount());
+ assert.commandWorked(coll.insert({[clusterKey]: "123456789012", a: 7}));
+ assert.eq(1, coll.find({[clusterKey]: "123456789012"}).itcount());
+ if (clusterKey == "_id") {
+ assert.commandWorked(coll.insert({a: 8}));
+ } else {
+ // Missing required cluster key field.
+ assert.commandFailedWithCode(coll.insert({a: 8}), 2);
+ assert.commandWorked(coll.insert({[clusterKey]: "withFieldA", a: 8}));
+ }
+ assert.eq(1, coll.find({a: 8}).itcount());
+ assert.commandWorked(coll.insert({[clusterKey]: null, a: 9}));
+ assert.eq(1, coll.find({[clusterKey]: null}).itcount());
+ assert.commandWorked(coll.insert({[clusterKey]: 'x'.repeat(99), a: 10}));
+
+ if (clusterKey == "_id") {
+ assert.commandWorked(coll.insert({}));
+ } else {
+ // Missing required ts field.
+ assert.commandFailedWithCode(coll.insert({}), 2);
+ assert.commandWorked(coll.insert({[clusterKey]: 'missingFieldA'}));
+ }
+ // Can build a secondary index with a 3MB RecordId doc.
+ assert.commandWorked(coll.createIndex({a: 1}));
+ // Can drop the secondary index
+ assert.commandWorked(coll.dropIndex({a: 1}));
+
+ // This key is too large.
+ assert.commandFailedWithCode(coll.insert({[clusterKey]: 'x'.repeat(8 * 1024 * 1024), a: 11}),
+ 5894900);
+
+ // Look up using the secondary index on {a: 1}
+ assert.commandWorked(coll.createIndex({a: 1}));
+
+ // TODO remove the branch once SERVER-60734 "replacement updates should preserve the cluster
+ // key" is resolved.
+ if (clusterKey == "_id") {
+ assert.eq(1, coll.find({a: null}).itcount());
+ } else {
+ assert.eq(5, coll.find({a: null}).itcount());
+ }
+ assert.eq(0, coll.find({a: 0}).itcount());
+ assert.eq(2, coll.find({a: 1}).itcount());
+ assert.eq(1, coll.find({a: 2}).itcount());
+ assert.eq(1, coll.find({a: 8}).itcount());
+ assert.eq(1, coll.find({a: 9}).itcount());
+ assert.eq(null, coll.findOne({a: 9})[clusterKeyString]);
+ assert.eq(1, coll.find({a: 10}).itcount());
+ assert.eq(99, coll.findOne({a: 10})[clusterKeyString].length);
+
+ // todo make it unconditional once server-60734 "replacement updates should preserve the cluster
+ // key" is resolved.
+ if (clusterKey == "_id") {
+ for (let len of lengths) {
+ // Secondary index lookups for documents with large RecordId's.
+ assert.eq(1, coll.find({a: len}).itcount());
+ assert.eq(len, coll.findOne({a: len})[clusterKeyString].length);
+ }
+ }
+
+ let res = coll.validate();
+ assert(res.valid, res);
+ assert.commandWorked(coll.dropIndex({a: 1}));
+
+ // Test secondary unique indexes constraints.
+ assert.commandFailedWithCode(coll.createIndex({a: 1}, {unique: true}), ErrorCodes.DuplicateKey);
+ assert.commandWorked(coll.deleteOne({a: 1}));
+
+ // TODO remove the branch once SERVER-60734 "replacement updates should preserve the cluster
+ // key" is resolved.
+ if (clusterKey == "_id") {
+ assert.eq(1, coll.find({a: null}).itcount());
+ assert.commandWorked(coll.removeOne({a: null}));
+ } else {
+ assert.eq(5, coll.find({a: null}).itcount());
+ assert.commandWorked(coll.remove({a: null}, {multi: true}));
+ }
+
+ assert.commandWorked(coll.createIndex({a: 1}, {unique: true}));
+
+ // Check that document retrieval using the unique index key behaves correctly for cluster keys
+ // of different types.
+ assert.eq(0, coll.find({a: 0}).itcount());
+ assert.commandWorked(coll.insert({[clusterKey]: 100, a: 0}));
+
+ assert.eq(1, coll.find({a: 1}).itcount());
+ assert.commandFailedWithCode(coll.insert({[clusterKey]: -1, a: 1}), ErrorCodes.DuplicateKey);
+
+ assert.eq(1, coll.find({a: 2}).itcount());
+ assert.commandFailedWithCode(coll.insert({[clusterKey]: -1, a: 2}), ErrorCodes.DuplicateKey);
+
+ assert.eq(1, coll.find({a: 8}).itcount());
+ assert.commandFailedWithCode(coll.insert({[clusterKey]: -1, a: 8}), ErrorCodes.DuplicateKey);
+
+ assert.eq(1, coll.find({a: 9}).itcount());
+ assert.eq(null, coll.findOne({a: 9})['_id']);
+ assert.commandFailedWithCode(coll.insert({[clusterKey]: -1, a: 9}), ErrorCodes.DuplicateKey);
+
+ assert.eq(1, coll.find({a: 10}).itcount());
+ assert.eq(99, coll.findOne({a: 10})['_id'].length);
+ assert.commandFailedWithCode(coll.insert({[clusterKey]: -1, a: 10}), ErrorCodes.DuplicateKey);
+
+ // Cycle values of 'a' around to different documents. This intended to ensure correctness of
+ // batch application when this test is run in replication passthrough suites.
+ assert.commandWorked(coll.insert({[clusterKey]: 10, a: 110}));
+ assert.commandWorked(coll.insert({[clusterKey]: 11, a: 111}));
+ assert.commandWorked(coll.insert({[clusterKey]: 12, a: 112}));
+
+ assert.commandFailedWithCode(coll.update({[clusterKey]: 12}, {a: 110}),
+ ErrorCodes.DuplicateKey);
+ assert.commandWorked(coll.update({[clusterKey]: 12}, {a: 113}));
+ assert.commandWorked(coll.update({[clusterKey]: 11}, {a: 112}));
+ assert.commandWorked(coll.update({[clusterKey]: 10}, {a: 111}));
+ assert.commandWorked(coll.update({[clusterKey]: 12}, {a: 110}));
+
+ // Unique index lookups for documents with large RecordIds.
+ for (let len of lengths) {
+ assert.eq(1, coll.find({a: len}).itcount());
+ assert.eq(len, coll.findOne({a: len})['_id'].length);
+ assert.commandFailedWithCode(coll.insert({a: len}), ErrorCodes.DuplicateKey);
+ }
+
+ // No support for numeric type differentiation.
+ assert.commandWorked(coll.insert({[clusterKey]: 42.0}));
+ assert.commandFailedWithCode(coll.insert({[clusterKey]: 42}), ErrorCodes.DuplicateKey);
+ assert.commandFailedWithCode(coll.insert({[clusterKey]: NumberLong("42")}),
+ ErrorCodes.DuplicateKey);
+ assert.eq(1, coll.find({[clusterKey]: 42.0}).itcount());
+ assert.eq(1, coll.find({[clusterKey]: 42}).itcount());
+ assert.eq(1, coll.find({[clusterKey]: NumberLong("42")}).itcount());
+}
diff --git a/src/mongo/db/catalog/index_catalog_impl.cpp b/src/mongo/db/catalog/index_catalog_impl.cpp
index 261f0f8fb71..fd63fb5d40e 100644
--- a/src/mongo/db/catalog/index_catalog_impl.cpp
+++ b/src/mongo/db/catalog/index_catalog_impl.cpp
@@ -796,10 +796,6 @@ Status IndexCatalogImpl::_isSpecOk(OperationContext* opCtx,
}
}
- uassert(ErrorCodes::InvalidOptions,
- "Unique indexes are not supported on clustered collections",
- !collection->isClustered() || !spec[IndexDescriptor::kUniqueFieldName].trueValue());
-
if (IndexDescriptor::isIdIndexPattern(key)) {
if (collection->isClustered()) {
return Status(ErrorCodes::CannotCreateIndex,
diff --git a/src/mongo/db/index/duplicate_key_tracker.cpp b/src/mongo/db/index/duplicate_key_tracker.cpp
index d5c75435764..67ccb738c20 100644
--- a/src/mongo/db/index/duplicate_key_tracker.cpp
+++ b/src/mongo/db/index/duplicate_key_tracker.cpp
@@ -85,7 +85,12 @@ Status DuplicateKeyTracker::recordKey(OperationContext* opCtx, const KeyString::
// store the TypeBits for error reporting later on. The RecordId does not need to be stored, so
// we exclude it from the serialization.
BufBuilder builder;
- key.serializeWithoutRecordIdLong(builder);
+ if (KeyFormat::Long ==
+ _indexCatalogEntry->accessMethod()->getSortedDataInterface()->rsKeyFormat()) {
+ key.serializeWithoutRecordIdLong(builder);
+ } else {
+ key.serializeWithoutRecordIdStr(builder);
+ }
auto status =
_keyConstraintsTable->rs()->insertRecord(opCtx, builder.buf(), builder.len(), Timestamp());
diff --git a/src/mongo/db/index/index_access_method.cpp b/src/mongo/db/index/index_access_method.cpp
index 1a2fe8fe133..b7fa09bd0c4 100644
--- a/src/mongo/db/index/index_access_method.cpp
+++ b/src/mongo/db/index/index_access_method.cpp
@@ -810,7 +810,9 @@ Status AbstractIndexAccessMethod::commitBulk(OperationContext* opCtx,
// builds since this check can be expensive.
int cmpData;
if (_descriptor->unique()) {
- cmpData = data.first.compareWithoutRecordIdLong(previousKey);
+ cmpData = (_newInterface->rsKeyFormat() == KeyFormat::Long)
+ ? data.first.compareWithoutRecordIdLong(previousKey)
+ : data.first.compareWithoutRecordIdStr(previousKey);
}
if (kDebugBuild && data.first.compare(previousKey) < 0) {
@@ -988,7 +990,9 @@ std::string nextFileName() {
Status AbstractIndexAccessMethod::_handleDuplicateKey(OperationContext* opCtx,
const KeyString::Value& dataKey,
const RecordIdHandlerFn& onDuplicateRecord) {
- RecordId recordId = KeyString::decodeRecordIdLongAtEnd(dataKey.getBuffer(), dataKey.getSize());
+ RecordId recordId = (KeyFormat::Long == _newInterface->rsKeyFormat())
+ ? KeyString::decodeRecordIdLongAtEnd(dataKey.getBuffer(), dataKey.getSize())
+ : KeyString::decodeRecordIdStrAtEnd(dataKey.getBuffer(), dataKey.getSize());
if (onDuplicateRecord) {
return onDuplicateRecord(recordId);
}
diff --git a/src/mongo/db/storage/key_string.cpp b/src/mongo/db/storage/key_string.cpp
index 0a93caa0c9a..9d1d88121b2 100644
--- a/src/mongo/db/storage/key_string.cpp
+++ b/src/mongo/db/storage/key_string.cpp
@@ -2705,6 +2705,15 @@ void Value::serializeWithoutRecordIdLong(BufBuilder& buf) const {
buf.appendBuf(_buffer.get() + _ksSize, _buffer.size() - _ksSize); // Serialize TypeBits
}
+void Value::serializeWithoutRecordIdStr(BufBuilder& buf) const {
+ dassert(decodeRecordIdStrAtEnd(_buffer.get(), _ksSize).isValid());
+
+ const int32_t sizeWithoutRecordId = sizeWithoutRecordIdStrAtEnd(_buffer.get(), _ksSize);
+ buf.appendNum(sizeWithoutRecordId); // Serialize size of KeyString
+ buf.appendBuf(_buffer.get(), sizeWithoutRecordId); // Serialize KeyString
+ buf.appendBuf(_buffer.get() + _ksSize, _buffer.size() - _ksSize); // Serialize TypeBits
+}
+
size_t Value::getApproximateSize() const {
auto size = sizeof(Value);
size += !_buffer.isShared() ? SharedBuffer::kHolderSize + _buffer.size() : 0;
diff --git a/src/mongo/db/storage/key_string.h b/src/mongo/db/storage/key_string.h
index d8db757213e..f0e1441004c 100644
--- a/src/mongo/db/storage/key_string.h
+++ b/src/mongo/db/storage/key_string.h
@@ -335,13 +335,21 @@ public:
return *this;
}
+ /**
+ * Compare with another KeyString::Value or Builder.
+ */
template <class T>
int compare(const T& other) const;
int compareWithTypeBits(const Value& other) const;
+ /**
+ * Compare with another KeyString::Value or Builder, ignoring the RecordId part of both.
+ */
template <class T>
int compareWithoutRecordIdLong(const T& other) const;
+ template <class T>
+ int compareWithoutRecordIdStr(const T& other) const;
// Returns the size of the stored KeyString.
size_t getSize() const {
@@ -383,11 +391,12 @@ public:
}
/**
- * Serializes this Value, excluing the RecordId, into a storable format with TypeBits
+ * Serializes this Value, excluding the RecordId, into a storable format with TypeBits
* information. The serialized format takes the following form:
* [keystring size][keystring encoding][typebits encoding]
*/
void serializeWithoutRecordIdLong(BufBuilder& buf) const;
+ void serializeWithoutRecordIdStr(BufBuilder& buf) const;
// Deserialize the Value from a serialized format.
static Value deserialize(BufReader& buf, KeyString::Version version) {
@@ -616,11 +625,19 @@ public:
return _typeBits;
}
+ /**
+ * Compare with another KeyString::Value or Builder.
+ */
template <class T>
int compare(const T& other) const;
+ /**
+ * Compare with another KeyString::Value or Builder, ignoring the RecordId part of both.
+ */
template <class T>
int compareWithoutRecordIdLong(const T& other) const;
+ template <class T>
+ int compareWithoutRecordIdStr(const T& other) const;
/**
* @return a hex encoding of this key
@@ -1030,6 +1047,16 @@ int BuilderBase<BufferT>::compareWithoutRecordIdLong(const T& other) const {
!other.isEmpty() ? sizeWithoutRecordIdLongAtEnd(other.getBuffer(), other.getSize()) : 0);
}
+template <class BufferT>
+template <class T>
+int BuilderBase<BufferT>::compareWithoutRecordIdStr(const T& other) const {
+ return KeyString::compare(
+ getBuffer(),
+ other.getBuffer(),
+ !isEmpty() ? sizeWithoutRecordIdStrAtEnd(getBuffer(), getSize()) : 0,
+ !other.isEmpty() ? sizeWithoutRecordIdStrAtEnd(other.getBuffer(), other.getSize()) : 0);
+}
+
template <class T>
int Value::compare(const T& other) const {
return KeyString::compare(getBuffer(), other.getBuffer(), getSize(), other.getSize());
@@ -1044,6 +1071,15 @@ int Value::compareWithoutRecordIdLong(const T& other) const {
!other.isEmpty() ? sizeWithoutRecordIdLongAtEnd(other.getBuffer(), other.getSize()) : 0);
}
+template <class T>
+int Value::compareWithoutRecordIdStr(const T& other) const {
+ return KeyString::compare(
+ getBuffer(),
+ other.getBuffer(),
+ !isEmpty() ? sizeWithoutRecordIdStrAtEnd(getBuffer(), getSize()) : 0,
+ !other.isEmpty() ? sizeWithoutRecordIdStrAtEnd(other.getBuffer(), other.getSize()) : 0);
+}
+
/**
* Takes key string and key pattern information and uses it to present human-readable information
* about an index or collection entry.
diff --git a/src/mongo/db/storage/sorted_data_interface_test_keyformat_string.cpp b/src/mongo/db/storage/sorted_data_interface_test_keyformat_string.cpp
index 604c4ae0168..b3a75c6f593 100644
--- a/src/mongo/db/storage/sorted_data_interface_test_keyformat_string.cpp
+++ b/src/mongo/db/storage/sorted_data_interface_test_keyformat_string.cpp
@@ -110,6 +110,87 @@ TEST(SortedDataInterface, KeyFormatStringInsertDuplicates) {
}
}
+TEST(SortedDataInterface, KeyFormatStringUniqueInsertRemoveDuplicates) {
+ const auto harnessHelper(newSortedDataInterfaceHarnessHelper());
+ const std::unique_ptr<SortedDataInterface> sorted(harnessHelper->newSortedDataInterface(
+ /*unique=*/true, /*partial=*/false, KeyFormat::String));
+ const ServiceContext::UniqueOperationContext opCtx(harnessHelper->newOperationContext());
+ ASSERT(sorted->isEmpty(opCtx.get()));
+
+ std::string buf1(12, 0);
+ std::string buf2(12, 1);
+ std::string buf3(12, 0xff);
+
+ RecordId rid1(buf1.c_str(), 12);
+ RecordId rid2(buf2.c_str(), 12);
+ RecordId rid3(buf3.c_str(), 12);
+
+ {
+ WriteUnitOfWork uow(opCtx.get());
+ ASSERT_OK(sorted->insert(opCtx.get(),
+ makeKeyString(sorted.get(), key1, rid1),
+ /*dupsAllowed*/ true));
+ Status status = sorted->insert(opCtx.get(),
+ makeKeyString(sorted.get(), key1, rid2),
+ /*dupsAllowed*/ false);
+ ASSERT_EQ(ErrorCodes::DuplicateKey, status.code());
+
+ ASSERT_OK(sorted->insert(opCtx.get(),
+ makeKeyString(sorted.get(), key1, rid3),
+ /*dupsAllowed*/ true));
+ uow.commit();
+ }
+
+ ASSERT_EQUALS(2, sorted->numEntries(opCtx.get()));
+
+ {
+ WriteUnitOfWork uow(opCtx.get());
+ sorted->unindex(opCtx.get(),
+ makeKeyString(sorted.get(), key1, rid1),
+ /*dupsAllowed*/ true);
+
+ ASSERT_OK(sorted->insert(opCtx.get(),
+ makeKeyString(sorted.get(), key2, rid1),
+ /*dupsAllowed*/ true));
+ uow.commit();
+ }
+
+ ASSERT_EQUALS(2, sorted->numEntries(opCtx.get()));
+
+ auto ksSeek = makeKeyStringForSeek(sorted.get(), key1, true, true);
+ {
+ auto cursor = sorted->newCursor(opCtx.get());
+ auto entry = cursor->seek(ksSeek);
+ ASSERT(entry);
+ ASSERT_EQ(*entry, IndexKeyEntry(key1, rid3));
+
+ entry = cursor->next();
+ ASSERT(entry);
+ ASSERT_EQ(*entry, IndexKeyEntry(key2, rid1));
+
+ entry = cursor->next();
+ ASSERT_FALSE(entry);
+ }
+
+ {
+ auto cursor = sorted->newCursor(opCtx.get());
+ auto entry = cursor->seekForKeyString(ksSeek);
+ ASSERT(entry);
+ ASSERT_EQ(entry->loc, rid3);
+ auto ks1 = makeKeyString(sorted.get(), key1, rid3);
+ ASSERT_EQ(entry->keyString, ks1);
+
+ entry = cursor->nextKeyString();
+ ASSERT(entry);
+ ASSERT_EQ(entry->loc, rid1);
+ auto ks2 = makeKeyString(sorted.get(), key2, rid1);
+ ASSERT_EQ(entry->keyString, ks2);
+
+ entry = cursor->nextKeyString();
+ ASSERT_FALSE(entry);
+ }
+}
+
TEST(SortedDataInterface, KeyFormatStringSetEndPosition) {
const auto harnessHelper(newSortedDataInterfaceHarnessHelper());
const std::unique_ptr<SortedDataInterface> sorted(harnessHelper->newSortedDataInterface(
@@ -228,6 +309,58 @@ TEST(SortedDataInterface, KeyFormatStringUnindex) {
ASSERT_EQUALS(0, sorted->numEntries(opCtx.get()));
}
+TEST(SortedDataInterface, KeyFormatStringUniqueUnindex) {
+ const auto harnessHelper(newSortedDataInterfaceHarnessHelper());
+ const std::unique_ptr<SortedDataInterface> sorted(harnessHelper->newSortedDataInterface(
+ /*unique=*/true, /*partial=*/false, KeyFormat::String));
+ const ServiceContext::UniqueOperationContext opCtx(harnessHelper->newOperationContext());
+ ASSERT(sorted->isEmpty(opCtx.get()));
+
+ std::string buf1(12, 0);
+ std::string buf2(12, 1);
+ std::string buf3(12, 0xff);
+
+ RecordId rid1(buf1.c_str(), 12);
+ RecordId rid2(buf2.c_str(), 12);
+ RecordId rid3(buf3.c_str(), 12);
+
+ {
+ WriteUnitOfWork uow(opCtx.get());
+ ASSERT_OK(sorted->insert(opCtx.get(),
+ makeKeyString(sorted.get(), key1, rid1),
+ /*dupsAllowed*/ false));
+ ASSERT_OK(sorted->insert(opCtx.get(),
+ makeKeyString(sorted.get(), key2, rid2),
+ /*dupsAllowed*/ false));
+ ASSERT_OK(sorted->insert(opCtx.get(),
+ makeKeyString(sorted.get(), key3, rid3),
+ /*dupsAllowed*/ false));
+ uow.commit();
+ }
+ ASSERT_EQUALS(3, sorted->numEntries(opCtx.get()));
+
+ {
+ WriteUnitOfWork uow(opCtx.get());
+ // Does not exist, does nothing.
+ sorted->unindex(opCtx.get(),
+ makeKeyString(sorted.get(), key1, rid3),
+ /*dupsAllowed*/ false);
+
+ sorted->unindex(opCtx.get(),
+ makeKeyString(sorted.get(), key1, rid1),
+ /*dupsAllowed*/ false);
+ sorted->unindex(opCtx.get(),
+ makeKeyString(sorted.get(), key2, rid2),
+ /*dupsAllowed*/ false);
+ sorted->unindex(opCtx.get(),
+ makeKeyString(sorted.get(), key3, rid3),
+ /*dupsAllowed*/ false);
+
+ uow.commit();
+ }
+ ASSERT_EQUALS(0, sorted->numEntries(opCtx.get()));
+}
+
TEST(SortedDataInterface, InsertReservedRecordIdStr) {
const auto harnessHelper(newSortedDataInterfaceHarnessHelper());
const std::unique_ptr<SortedDataInterface> sorted(harnessHelper->newSortedDataInterface(
diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_index.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_index.cpp
index 8ad8a557afd..b3fe456cbb2 100644
--- a/src/mongo/db/storage/wiredtiger/wiredtiger_index.cpp
+++ b/src/mongo/db/storage/wiredtiger/wiredtiger_index.cpp
@@ -654,11 +654,13 @@ public:
}
Status addKey(const KeyString::Value& newKeyString) override {
- dassertRecordIdAtEnd(newKeyString, KeyFormat::Long);
+ dassertRecordIdAtEnd(newKeyString, _idx->rsKeyFormat());
// Do a duplicate check, but only if dups aren't allowed.
if (!_dupsAllowed) {
- const int cmp = newKeyString.compareWithoutRecordIdLong(_previousKeyString);
+ const int cmp = (_idx->_rsKeyFormat == KeyFormat::Long)
+ ? newKeyString.compareWithoutRecordIdLong(_previousKeyString)
+ : newKeyString.compareWithoutRecordIdStr(_previousKeyString);
if (cmp == 0) {
// Duplicate found!
auto newKey = KeyString::toBson(newKeyString, _idx->_ordering);
@@ -842,6 +844,8 @@ public:
dassert(KeyString::decodeDiscriminator(
key.getBuffer(), key.getSize(), _idx.getOrdering(), key.getTypeBits()) ==
KeyString::Discriminator::kInclusive);
+ // seekExact is only used on the _id index, which only uses the Long format.
+ invariant(KeyFormat::Long == _idx.rsKeyFormat());
auto ksEntry = [&]() {
if (_forward) {
@@ -1303,6 +1307,9 @@ private:
// Must not throw WriteConflictException, throwing a WriteConflictException will retry the
// operation effectively skipping over this key.
void _updateIdAndTypeBitsFromValue() {
+ // Old-format unique index keys always use the Long format.
+ invariant(_idx.rsKeyFormat() == KeyFormat::Long);
+
// We assume that cursors can only ever see unique indexes in their "pristine" state,
// where no duplicates are possible. The cases where dups are allowed should hold
// sufficient locks to ensure that no cursor ever sees them.
@@ -1340,6 +1347,9 @@ public:
// Must not throw WriteConflictException, throwing a WriteConflictException will retry the
// operation effectively skipping over this key.
void updateIdAndTypeBits() override {
+ // _id index keys always use the Long format.
+ invariant(_idx.rsKeyFormat() == KeyFormat::Long);
+
WT_CURSOR* c = _cursor->get();
WT_ITEM item;
auto ret = c->get_value(c, &item);
@@ -1365,10 +1375,10 @@ public:
WiredTigerIndexUnique::WiredTigerIndexUnique(OperationContext* ctx,
const std::string& uri,
StringData ident,
+ KeyFormat rsKeyFormat,
const IndexDescriptor* desc,
bool isReadOnly)
- : WiredTigerIndex(ctx, uri, ident, KeyFormat::Long, desc, isReadOnly),
- _partial(desc->isPartial()) {
+ : WiredTigerIndex(ctx, uri, ident, rsKeyFormat, desc, isReadOnly), _partial(desc->isPartial()) {
// _id indexes must use WiredTigerIdIndex
invariant(!isIdIndex());
// All unique indexes should be in the timestamp-safe format version as of version 4.2.
@@ -1488,6 +1498,7 @@ Status WiredTigerIdIndex::_insert(OperationContext* opCtx,
WT_CURSOR* c,
const KeyString::Value& keyString,
bool dupsAllowed) {
+ invariant(KeyFormat::Long == _rsKeyFormat);
invariant(!dupsAllowed);
const RecordId id =
KeyString::decodeRecordIdLongAtEnd(keyString.getBuffer(), keyString.getSize());
@@ -1534,8 +1545,9 @@ Status WiredTigerIndexUnique::_insert(OperationContext* opCtx,
if (!dupsAllowed) {
// A prefix key is KeyString of index key. It is the component of the index entry that
// should be unique.
- auto sizeWithoutRecordId =
- KeyString::sizeWithoutRecordIdLongAtEnd(keyString.getBuffer(), keyString.getSize());
+ auto sizeWithoutRecordId = (_rsKeyFormat == KeyFormat::Long)
+ ? KeyString::sizeWithoutRecordIdLongAtEnd(keyString.getBuffer(), keyString.getSize())
+ : KeyString::sizeWithoutRecordIdStrAtEnd(keyString.getBuffer(), keyString.getSize());
WiredTigerItem prefixKeyItem(keyString.getBuffer(), sizeWithoutRecordId);
// First phase inserts the prefix key to prohibit concurrent insertions of same key
@@ -1611,6 +1623,7 @@ void WiredTigerIdIndex::_unindex(OperationContext* opCtx,
WT_CURSOR* c,
const KeyString::Value& keyString,
bool dupsAllowed) {
+ invariant(KeyFormat::Long == _rsKeyFormat);
const RecordId id =
KeyString::decodeRecordIdLongAtEnd(keyString.getBuffer(), keyString.getSize());
invariant(id.isValid());
@@ -1701,6 +1714,12 @@ void WiredTigerIndexUnique::_unindex(OperationContext* opCtx,
return;
}
+ if (KeyFormat::String == _rsKeyFormat) {
+ // This is a unique index on a clustered collection. These indexes will only have keys
+ // in the timestamp safe format where the RecordId is appended at the end of the key.
+ return;
+ }
+
// After a rolling upgrade an index can have keys from both timestamp unsafe (old) and
// timestamp safe (new) unique indexes. Old format keys just had the index key while new
// format key has index key + Record id. WT_NOTFOUND is possible if index key is in old format.
diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_index.h b/src/mongo/db/storage/wiredtiger/wiredtiger_index.h
index f37605e784f..a109b3ae5f4 100644
--- a/src/mongo/db/storage/wiredtiger/wiredtiger_index.h
+++ b/src/mongo/db/storage/wiredtiger/wiredtiger_index.h
@@ -201,6 +201,7 @@ public:
WiredTigerIndexUnique(OperationContext* ctx,
const std::string& uri,
StringData ident,
+ KeyFormat rsKeyFormat,
const IndexDescriptor* desc,
bool readOnly = false);
diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_kv_engine.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_kv_engine.cpp
index bcc74d25591..b46954184ea 100644
--- a/src/mongo/db/storage/wiredtiger/wiredtiger_kv_engine.cpp
+++ b/src/mongo/db/storage/wiredtiger/wiredtiger_kv_engine.cpp
@@ -1579,12 +1579,12 @@ std::unique_ptr<SortedDataInterface> WiredTigerKVEngine::getSortedDataInterface(
invariant(!collOptions.clusteredIndex);
return std::make_unique<WiredTigerIdIndex>(opCtx, _uri(ident), ident, desc, _readOnly);
}
+ auto keyFormat = (collOptions.clusteredIndex) ? KeyFormat::String : KeyFormat::Long;
if (desc->unique()) {
- invariant(!collOptions.clusteredIndex);
- return std::make_unique<WiredTigerIndexUnique>(opCtx, _uri(ident), ident, desc, _readOnly);
+ return std::make_unique<WiredTigerIndexUnique>(
+ opCtx, _uri(ident), ident, keyFormat, desc, _readOnly);
}
- auto keyFormat = (collOptions.clusteredIndex) ? KeyFormat::String : KeyFormat::Long;
return std::make_unique<WiredTigerIndexStandard>(
opCtx, _uri(ident), ident, keyFormat, desc, _readOnly);
}
diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_standard_index_test.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_standard_index_test.cpp
index 09c654ca822..0ecb7d90806 100644
--- a/src/mongo/db/storage/wiredtiger/wiredtiger_standard_index_test.cpp
+++ b/src/mongo/db/storage/wiredtiger/wiredtiger_standard_index_test.cpp
@@ -130,8 +130,8 @@ public:
invariantWTOK(WiredTigerIndex::Create(&opCtx, uri, result.getValue()));
if (unique) {
- invariant(keyFormat == KeyFormat::Long);
- return std::make_unique<WiredTigerIndexUnique>(&opCtx, uri, "" /* ident */, &desc);
+ return std::make_unique<WiredTigerIndexUnique>(
+ &opCtx, uri, "" /* ident */, keyFormat, &desc);
}
return std::make_unique<WiredTigerIndexStandard>(
&opCtx, uri, "" /* ident */, keyFormat, &desc);
diff --git a/src/mongo/db/timeseries/timeseries_commands_conversion_helper.cpp b/src/mongo/db/timeseries/timeseries_commands_conversion_helper.cpp
index 79d8483ab6f..db4e0295c61 100644
--- a/src/mongo/db/timeseries/timeseries_commands_conversion_helper.cpp
+++ b/src/mongo/db/timeseries/timeseries_commands_conversion_helper.cpp
@@ -112,6 +112,13 @@ CreateIndexesCommand makeTimeseriesCreateIndexesCommand(OperationContext* opCtx,
"TTL indexes are not supported on time-series collections");
}
+
+ if (elem.fieldNameStringData() == IndexDescriptor::kUniqueFieldName) {
+ uassert(ErrorCodes::InvalidOptions,
+ "Unique indexes are not supported on time-series collections",
+ !elem.trueValue());
+ }
+
if (elem.fieldNameStringData() == NewIndexSpec::kKeyFieldName) {
auto pluginName = IndexNames::findPluginName(elem.Obj());
uassert(ErrorCodes::InvalidOptions,