diff options
author | Gregory Noma <gregory.noma@gmail.com> | 2021-05-24 18:24:17 -0400 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-05-25 04:22:29 +0000 |
commit | 61710dae9c72f603f15882f3005b729c7f84bcf6 (patch) | |
tree | f0b3117b9775888235d934249de0c9ae3452d14c | |
parent | fcddb1393fa511f68706b1ff8e74c65f40c086bc (diff) | |
download | mongo-61710dae9c72f603f15882f3005b729c7f84bcf6.tar.gz |
SERVER-56933 Return options to create an identical time-series collection from listCollections
(cherry picked from commit 8d5547cdd6c45dd68c95dcf353e459e62da5222b)
20 files changed, 472 insertions, 94 deletions
diff --git a/jstests/core/list_collections_no_views.js b/jstests/core/list_collections_no_views.js index 378b15f1085..45e796c50e5 100644 --- a/jstests/core/list_collections_no_views.js +++ b/jstests/core/list_collections_no_views.js @@ -52,7 +52,7 @@ assert.eq(allExpected, return 0; })); -// {type: {$exists: false}} is needed for versions <= 3.2 +// TODO (SERVER-25493): {type: {$exists: false}} is needed for versions <= 3.2 let collOnlyCommand = { listCollections: 1, filter: {$or: [{type: 'collection'}, {type: {$exists: false}}]} diff --git a/jstests/core/timeseries/timeseries_list_collections.js b/jstests/core/timeseries/timeseries_list_collections.js new file mode 100644 index 00000000000..9038fb53e05 --- /dev/null +++ b/jstests/core/timeseries/timeseries_list_collections.js @@ -0,0 +1,120 @@ +/** + * Tests the result of running listCollections when there are time-series collections present. + * + * @tags: [ + * assumes_no_implicit_collection_creation_after_drop, + * does_not_support_transactions, + * requires_fcv_49, + * requires_find_command, + * requires_getmore, + * ] + */ +(function() { +'use strict'; + +const testDB = db.getSiblingDB(jsTestName()); +assert.commandWorked(testDB.dropDatabase()); + +const timeFieldName = 'time'; +const metaFieldName = 'meta'; +const coll = testDB.getCollection('t'); + +const getBucketMaxSpanSeconds = function(granularity) { + switch (granularity) { + case 'seconds': + return 60 * 60; + case 'minutes': + return 60 * 60 * 24; + case 'hours': + return 60 * 60 * 24 * 30; + default: + assert(false, 'Invalid granularity: ' + granularity); + } +}; + +const testOptions = function(options) { + coll.drop(); + jsTestLog('Creating time-series collection with options: ' + tojson(options)); + assert.commandWorked(testDB.createCollection(coll.getName(), options)); + + if (!options.timeseries.hasOwnProperty('granularity')) { + Object.assign(options.timeseries, {granularity: 'seconds'}); + } + if (!options.timeseries.hasOwnProperty('bucketMaxSpanSeconds')) { + Object.assign( + options.timeseries, + {bucketMaxSpanSeconds: getBucketMaxSpanSeconds(options.timeseries.granularity)}); + } + + if (options.hasOwnProperty('collation')) { + Object.assign(options.collation, { + caseLevel: false, + caseFirst: 'off', + strength: 3, + numericOrdering: false, + alternate: 'non-ignorable', + maxVariable: 'punct', + normalization: false, + backwards: false, + version: '57.1', + }); + } + + const collections = + assert.commandWorked(testDB.runCommand({listCollections: 1})).cursor.firstBatch; + jsTestLog('Checking listCollections result: ' + tojson(collections)); + assert.eq(collections.length, 3); + assert(collections.find(entry => entry.name === 'system.views')); + assert(collections.find(entry => entry.name === 'system.buckets.' + coll.getName())); + assert.docEq( + collections.find(entry => entry.name === coll.getName()), + {name: coll.getName(), type: 'timeseries', options: options, info: {readOnly: false}}); +}; + +testOptions({timeseries: {timeField: timeFieldName}}); +testOptions({timeseries: {timeField: timeFieldName, metaField: metaFieldName}}); +testOptions({ + timeseries: { + timeField: timeFieldName, + granularity: 'minutes', + } +}); +testOptions({ + timeseries: { + timeField: timeFieldName, + granularity: 'minutes', + bucketMaxSpanSeconds: 60 * 60 * 24, + } +}); +testOptions({ + timeseries: { + timeField: timeFieldName, + }, + storageEngine: {wiredTiger: {}}, +}); +testOptions({ + timeseries: { + timeField: timeFieldName, + }, + indexOptionDefaults: {storageEngine: {wiredTiger: {}}}, +}); +testOptions({ + timeseries: { + timeField: timeFieldName, + }, + collation: {locale: 'ja'}, +}); +testOptions({timeseries: {timeField: timeFieldName}, expireAfterSeconds: NumberLong(100)}); +testOptions({ + timeseries: { + timeField: timeFieldName, + metaField: metaFieldName, + granularity: 'minutes', + bucketMaxSpanSeconds: 60 * 60 * 24, + }, + storageEngine: {wiredTiger: {}}, + indexOptionDefaults: {storageEngine: {wiredTiger: {}}}, + collation: {locale: 'ja'}, + expireAfterSeconds: NumberLong(100), +}); +})(); diff --git a/jstests/core/timeseries/timeseries_list_collections_filter_name.js b/jstests/core/timeseries/timeseries_list_collections_filter_name.js new file mode 100644 index 00000000000..6f88fe1fe82 --- /dev/null +++ b/jstests/core/timeseries/timeseries_list_collections_filter_name.js @@ -0,0 +1,37 @@ +/** + * Tests that listCollections includes time-series collections and their options when filtering on + * name. + * + * @tags: [ + * assumes_no_implicit_collection_creation_after_drop, + * does_not_support_transactions, + * requires_fcv_49, + * requires_find_command, + * requires_getmore, + * ] + */ +(function() { +'use strict'; + +const testDB = db.getSiblingDB(jsTestName()); +assert.commandWorked(testDB.dropDatabase()); + +const timeFieldName = 'time'; +const coll = testDB.getCollection('t'); + +assert.commandWorked( + testDB.createCollection(coll.getName(), {timeseries: {timeField: timeFieldName}})); + +const collections = + assert.commandWorked(testDB.runCommand({listCollections: 1, filter: {name: coll.getName()}})) + .cursor.firstBatch; +assert.eq(collections, [{ + name: coll.getName(), + type: 'timeseries', + options: { + timeseries: + {timeField: timeFieldName, granularity: 'seconds', bucketMaxSpanSeconds: 3600} + }, + info: {readOnly: false}, + }]); +})(); diff --git a/jstests/core/timeseries/timeseries_list_collections_invalid_view.js b/jstests/core/timeseries/timeseries_list_collections_invalid_view.js new file mode 100644 index 00000000000..0ef1c0874ca --- /dev/null +++ b/jstests/core/timeseries/timeseries_list_collections_invalid_view.js @@ -0,0 +1,51 @@ +/** + * Tests the behavior of listCollections in the presence of both a time-series collection and an + * invalid view definition. + * + * @tags: [ + * assumes_against_mongod_not_mongos, + * assumes_no_implicit_collection_creation_after_drop, + * does_not_support_transactions, + * requires_fcv_49, + * requires_find_command, + * requires_getmore, + * ] + */ +(function() { +'use strict'; + +const testDB = db.getSiblingDB(jsTestName()); +assert.commandWorked(testDB.dropDatabase()); + +const timeFieldName = 'time'; +const coll = testDB.getCollection('t'); + +assert.commandWorked( + testDB.createCollection(coll.getName(), {timeseries: {timeField: timeFieldName}})); +assert.commandWorked(testDB.adminCommand({ + applyOps: [ + {op: 'i', ns: testDB.getName() + '.system.views', o: {_id: 'invalid', pipeline: 'invalid'}} + ] +})); + +assert.commandFailedWithCode(testDB.runCommand({listCollections: 1}), + ErrorCodes.InvalidViewDefinition); +assert.commandFailedWithCode(testDB.runCommand({listCollections: 1, filter: {type: 'timeseries'}}), + ErrorCodes.InvalidViewDefinition); +assert.commandFailedWithCode( + testDB.runCommand({listCollections: 1, filter: {name: coll.getName()}}), + ErrorCodes.InvalidViewDefinition); + +// TODO (SERVER-25493): Change filter to {type: 'collection'}. +const collections = + assert + .commandWorked(testDB.runCommand( + {listCollections: 1, filter: {$or: [{type: 'collection'}, {type: {$exists: false}}]}})) + .cursor.firstBatch; +jsTestLog('Checking listCollections result: ' + tojson(collections)); +assert.eq(collections.length, 2); +assert(collections.find(entry => entry.name === 'system.views')); +assert(collections.find(entry => entry.name === 'system.buckets.' + coll.getName())); + +assert(testDB.system.views.drop()); +})(); diff --git a/jstests/core/timeseries/timeseries_list_collections_missing_buckets_collection.js b/jstests/core/timeseries/timeseries_list_collections_missing_buckets_collection.js new file mode 100644 index 00000000000..c0f779425f9 --- /dev/null +++ b/jstests/core/timeseries/timeseries_list_collections_missing_buckets_collection.js @@ -0,0 +1,39 @@ +/** + * Tests that listCollections shows the time-series view, but not the buckets collection, if the + * backing time-series buckets collection is missing. + * + * @tags: [ + * assumes_no_implicit_collection_creation_after_drop, + * does_not_support_transactions, + * requires_fcv_49, + * requires_find_command, + * requires_getmore, + * ] + */ +(function() { +'use strict'; + +const testDB = db.getSiblingDB(jsTestName()); +assert.commandWorked(testDB.dropDatabase()); + +const timeFieldName = 'time'; +const coll = testDB.getCollection('t'); +const bucketsColl = testDB.getCollection('system.buckets.' + coll.getName()); + +assert.commandWorked( + testDB.createCollection(coll.getName(), {timeseries: {timeField: timeFieldName}})); +assert(bucketsColl.drop()); + +let collections = assert.commandWorked(testDB.runCommand({listCollections: 1})).cursor.firstBatch; +jsTestLog('Checking listCollections result: ' + tojson(collections)); +assert.eq(collections.length, 2); +assert(collections.find(entry => entry.name === 'system.views')); +assert.docEq(collections.find(entry => entry.name === coll.getName()), + {name: coll.getName(), type: 'timeseries', options: {}, info: {readOnly: false}}); + +collections = + assert.commandWorked(testDB.runCommand({listCollections: 1, filter: {name: coll.getName()}})) + .cursor.firstBatch; +assert.eq(collections, + [{name: coll.getName(), type: 'timeseries', options: {}, info: {readOnly: false}}]); +})(); diff --git a/jstests/core/timeseries/timeseries_list_collections_missing_view.js b/jstests/core/timeseries/timeseries_list_collections_missing_view.js new file mode 100644 index 00000000000..44f74ad31ef --- /dev/null +++ b/jstests/core/timeseries/timeseries_list_collections_missing_view.js @@ -0,0 +1,30 @@ +/** + * Tests that listCollections shows the time-series buckets collection, but not the view, if the + * time-series view is missing. + * + * @tags: [ + * assumes_no_implicit_collection_creation_after_drop, + * does_not_support_transactions, + * requires_fcv_49, + * requires_find_command, + * requires_getmore, + * ] + */ +(function() { +'use strict'; + +const testDB = db.getSiblingDB(jsTestName()); +assert.commandWorked(testDB.dropDatabase()); + +const timeFieldName = 'time'; +const coll = testDB.getCollection('t'); + +assert.commandWorked( + testDB.createCollection(coll.getName(), {timeseries: {timeField: timeFieldName}})); +assert(testDB.system.views.drop()); + +const collections = assert.commandWorked(testDB.runCommand({listCollections: 1})).cursor.firstBatch; +jsTestLog('Checking listCollections result: ' + tojson(collections)); +assert.eq(collections.length, 1); +assert(collections.find(entry => entry.name === 'system.buckets.' + coll.getName())); +})(); diff --git a/jstests/hooks/run_validate_collections_background.js b/jstests/hooks/run_validate_collections_background.js index 31c46298768..fc5b928d8aa 100644 --- a/jstests/hooks/run_validate_collections_background.js +++ b/jstests/hooks/run_validate_collections_background.js @@ -94,6 +94,7 @@ const validateCollectionsBackgroundThread = function validateCollectionsBackgrou for (let dbName of dbNames) { let db = conn.getDB(dbName); + // TODO (SERVER-25493): Change filter to {type: 'collection'}. const listCollRes = assert.commandWorked(db.runCommand({ "listCollections": 1, "nameOnly": true, diff --git a/src/mongo/db/catalog/collection_options.cpp b/src/mongo/db/catalog/collection_options.cpp index b5f1d32afed..2eadc1b11f8 100644 --- a/src/mongo/db/catalog/collection_options.cpp +++ b/src/mongo/db/catalog/collection_options.cpp @@ -316,81 +316,90 @@ CollectionOptions CollectionOptions::fromCreateCommand(const CreateCommand& cmd) return options; } -BSONObj CollectionOptions::toBSON(bool includeUUID) const { +BSONObj CollectionOptions::toBSON(bool includeUUID, const StringDataSet& includeFields) const { BSONObjBuilder b; - appendBSON(&b, includeUUID); + appendBSON(&b, includeUUID, includeFields); return b.obj(); } -void CollectionOptions::appendBSON(BSONObjBuilder* builder, bool includeUUID) const { +void CollectionOptions::appendBSON(BSONObjBuilder* builder, + bool includeUUID, + const StringDataSet& includeFields) const { if (uuid && includeUUID) { builder->appendElements(uuid->toBSON()); } - if (capped) { - builder->appendBool("capped", true); - builder->appendNumber("size", cappedSize); + auto shouldAppend = [&](StringData option) { + return includeFields.empty() || includeFields.contains(option); + }; + + if (capped && shouldAppend(CreateCommand::kCappedFieldName)) { + builder->appendBool(CreateCommand::kCappedFieldName, true); + builder->appendNumber(CreateCommand::kSizeFieldName, cappedSize); if (cappedMaxDocs) - builder->appendNumber("max", cappedMaxDocs); + builder->appendNumber(CreateCommand::kMaxFieldName, cappedMaxDocs); } - if (autoIndexId != DEFAULT) - builder->appendBool("autoIndexId", autoIndexId == YES); + if (autoIndexId != DEFAULT && shouldAppend(CreateCommand::kAutoIndexIdFieldName)) + builder->appendBool(CreateCommand::kAutoIndexIdFieldName, autoIndexId == YES); - if (temp) - builder->appendBool("temp", true); + if (temp && shouldAppend(CreateCommand::kTempFieldName)) + builder->appendBool(CreateCommand::kTempFieldName, true); - if (recordPreImages) { - builder->appendBool("recordPreImages", true); + if (recordPreImages && shouldAppend(CreateCommand::kRecordPreImagesFieldName)) { + builder->appendBool(CreateCommand::kRecordPreImagesFieldName, true); } - if (!storageEngine.isEmpty()) { - builder->append("storageEngine", storageEngine); + if (!storageEngine.isEmpty() && shouldAppend(CreateCommand::kStorageEngineFieldName)) { + builder->append(CreateCommand::kStorageEngineFieldName, storageEngine); } - if (indexOptionDefaults.getStorageEngine()) { - builder->append("indexOptionDefaults", indexOptionDefaults.toBSON()); + if (indexOptionDefaults.getStorageEngine() && + shouldAppend(CreateCommand::kIndexOptionDefaultsFieldName)) { + builder->append(CreateCommand::kIndexOptionDefaultsFieldName, indexOptionDefaults.toBSON()); } - if (!validator.isEmpty()) { - builder->append("validator", validator); + if (!validator.isEmpty() && shouldAppend(CreateCommand::kValidatorFieldName)) { + builder->append(CreateCommand::kValidatorFieldName, validator); } - if (validationLevel) { - builder->append("validationLevel", ValidationLevel_serializer(*validationLevel)); + if (validationLevel && shouldAppend(CreateCommand::kValidationLevelFieldName)) { + builder->append(CreateCommand::kValidationLevelFieldName, + ValidationLevel_serializer(*validationLevel)); } - if (validationAction) { - builder->append("validationAction", ValidationAction_serializer(*validationAction)); + if (validationAction && shouldAppend(CreateCommand::kValidationActionFieldName)) { + builder->append(CreateCommand::kValidationActionFieldName, + ValidationAction_serializer(*validationAction)); } - if (!collation.isEmpty()) { - builder->append("collation", collation); + if (!collation.isEmpty() && shouldAppend(CreateCommand::kCollationFieldName)) { + builder->append(CreateCommand::kCollationFieldName, collation); } - if (clusteredIndex) { - builder->append("clusteredIndex", true); + if (clusteredIndex && shouldAppend(CreateCommand::kClusteredIndexFieldName)) { + builder->append(CreateCommand::kClusteredIndexFieldName, true); } - if (expireAfterSeconds) { - builder->append("expireAfterSeconds", *expireAfterSeconds); + if (expireAfterSeconds && shouldAppend(CreateCommand::kExpireAfterSecondsFieldName)) { + builder->append(CreateCommand::kExpireAfterSecondsFieldName, *expireAfterSeconds); } - if (!viewOn.empty()) { - builder->append("viewOn", viewOn); + if (!viewOn.empty() && shouldAppend(CreateCommand::kViewOnFieldName)) { + builder->append(CreateCommand::kViewOnFieldName, viewOn); } - if (!pipeline.isEmpty()) { - builder->appendArray("pipeline", pipeline); + if (!pipeline.isEmpty() && shouldAppend(CreateCommand::kPipelineFieldName)) { + builder->appendArray(CreateCommand::kPipelineFieldName, pipeline); } - if (!idIndex.isEmpty()) { - builder->append("idIndex", idIndex); + if (!idIndex.isEmpty() && shouldAppend(CreateCommand::kIdIndexFieldName)) { + builder->append(CreateCommand::kIdIndexFieldName, idIndex); } - if (timeseries) { - builder->append("timeseries", timeseries->toBSON()); + if (timeseries && shouldAppend(CreateCommand::kTimeseriesFieldName)) { + builder->append(CreateCommand::kTimeseriesFieldName, timeseries->toBSON()); } } diff --git a/src/mongo/db/catalog/collection_options.h b/src/mongo/db/catalog/collection_options.h index 16be9667804..7a901f79c28 100644 --- a/src/mongo/db/catalog/collection_options.h +++ b/src/mongo/db/catalog/collection_options.h @@ -92,10 +92,13 @@ struct CollectionOptions { /** * Serialize to BSON. The 'includeUUID' parameter is used for the listCollections command to do - * special formatting for the uuid. + * special formatting for the uuid. Aside from the UUID, if 'includeFields' is non-empty, only + * the specified fields will be included. */ - void appendBSON(BSONObjBuilder* builder, bool includeUUID) const; - BSONObj toBSON(bool includeUUID = true) const; + void appendBSON(BSONObjBuilder* builder, + bool includeUUID, + const StringDataSet& includeFields) const; + BSONObj toBSON(bool includeUUID = true, const StringDataSet& includeFields = {}) const; /** * Returns true if given options matches to this. diff --git a/src/mongo/db/commands/create_command.cpp b/src/mongo/db/commands/create_command.cpp index 40442e764dc..d94c77f5268 100644 --- a/src/mongo/db/commands/create_command.cpp +++ b/src/mongo/db/commands/create_command.cpp @@ -39,6 +39,7 @@ #include "mongo/db/query/collation/collator_factory_interface.h" #include "mongo/db/s/operation_sharding_state.h" #include "mongo/db/storage/storage_parameters_gen.h" +#include "mongo/db/timeseries/timeseries_constants.h" #include "mongo/logv2/log.h" namespace mongo { @@ -148,47 +149,33 @@ public: feature_flags::gTimeseriesCollection.isEnabled( serverGlobalParams.featureCompatibility)); - const auto timeseriesNotAllowedWith = [&cmd](StringData option) -> std::string { - return str::stream() << cmd.getNamespace() - << ": 'timeseries' is not allowed with '" << option << "'"; - }; + for (auto&& option : cmd.toBSON({})) { + auto fieldName = option.fieldNameStringData(); + + if (fieldName == CreateCommand::kCommandName) { + continue; + } + + // The 'capped' option defaults to false. We accept it unless it is set to true. + if (fieldName == CreateCommand::kCappedFieldName && !option.Bool()) { + continue; + } + + // The 'timeseries' option may be passed with a 'validator' or 'clusteredIndex' + // if a buckets collection is being restored. We assume the caller knows what + // they are doing. + if ((fieldName == CreateCommand::kValidatorFieldName || + fieldName == CreateCommand::kClusteredIndexFieldName) && + cmd.getNamespace().isTimeseriesBucketsCollection()) { + continue; + } - uassert(ErrorCodes::InvalidOptions, - timeseriesNotAllowedWith("capped"), - !cmd.getCapped()); - uassert(ErrorCodes::InvalidOptions, - timeseriesNotAllowedWith("autoIndexId"), - !cmd.getAutoIndexId()); - uassert(ErrorCodes::InvalidOptions, - timeseriesNotAllowedWith("idIndex"), - !cmd.getIdIndex()); - uassert( - ErrorCodes::InvalidOptions, timeseriesNotAllowedWith("size"), !cmd.getSize()); - uassert(ErrorCodes::InvalidOptions, timeseriesNotAllowedWith("max"), !cmd.getMax()); - - // The 'timeseries' option may be passed with a 'validator' or 'clusteredIndex' if a - // buckets collection is being restored. We assume the caller knows what they are - // doing. - if (!cmd.getNamespace().isTimeseriesBucketsCollection()) { - uassert(ErrorCodes::InvalidOptions, - timeseriesNotAllowedWith("validator"), - !cmd.getValidator()); uassert(ErrorCodes::InvalidOptions, - timeseriesNotAllowedWith("clusteredIndex"), - !cmd.getClusteredIndex()); + str::stream() + << cmd.getNamespace() << ": 'timeseries' is not allowed with '" + << fieldName << "'", + timeseries::kAllowedCollectionCreationOptions.contains(fieldName)); } - uassert(ErrorCodes::InvalidOptions, - timeseriesNotAllowedWith("validationLevel"), - !cmd.getValidationLevel()); - uassert(ErrorCodes::InvalidOptions, - timeseriesNotAllowedWith("validationAction"), - !cmd.getValidationAction()); - uassert(ErrorCodes::InvalidOptions, - timeseriesNotAllowedWith("viewOn"), - !cmd.getViewOn()); - uassert(ErrorCodes::InvalidOptions, - timeseriesNotAllowedWith("pipeline"), - !cmd.getPipeline()); auto hasDot = [](StringData field) -> bool { return field.find('.') != std::string::npos; diff --git a/src/mongo/db/commands/list_collections.cpp b/src/mongo/db/commands/list_collections.cpp index 03c012f06fa..dd3c3192776 100644 --- a/src/mongo/db/commands/list_collections.cpp +++ b/src/mongo/db/commands/list_collections.cpp @@ -63,6 +63,7 @@ #include "mongo/db/service_context.h" #include "mongo/db/storage/storage_engine.h" #include "mongo/db/storage/storage_options.h" +#include "mongo/db/timeseries/timeseries_constants.h" #include "mongo/db/views/view_catalog.h" #include "mongo/logv2/log.h" @@ -142,9 +143,11 @@ void _addWorkingSetMember(OperationContext* opCtx, } BSONObj buildViewBson(const ViewDefinition& view, bool nameOnly) { + invariant(!view.timeseries()); + BSONObjBuilder b; b.append("name", view.name().coll()); - b.append("type", view.timeseries() ? "timeseries" : "view"); + b.append("type", "view"); if (nameOnly) { return b.obj(); @@ -158,11 +161,47 @@ BSONObj buildViewBson(const ViewDefinition& view, bool nameOnly) { } optionsBuilder.doneFast(); - BSONObj info = BSON("readOnly" << !view.timeseries()); + BSONObj info = BSON("readOnly" << true); b.append("info", info); return b.obj(); } +BSONObj buildTimeseriesBson(OperationContext* opCtx, + const CollectionPtr& collection, + bool nameOnly) { + invariant(collection); + + BSONObjBuilder builder; + builder.append("name", collection->ns().getTimeseriesViewNamespace().coll()); + builder.append("type", "timeseries"); + + if (nameOnly) { + return builder.obj(); + } + + builder.append("options", + collection->getCollectionOptions().toBSON( + false /* includeUUID */, timeseries::kAllowedCollectionCreationOptions)); + builder.append("info", BSON("readOnly" << false)); + + return builder.obj(); +} + +BSONObj buildTimeseriesBson(StringData collName, bool nameOnly) { + BSONObjBuilder builder; + builder.append("name", collName); + builder.append("type", "timeseries"); + + if (nameOnly) { + return builder.obj(); + } + + builder.append("options", BSONObj{}); + builder.append("info", BSON("readOnly" << false)); + + return builder.obj(); +} + /** * Return an object describing the collection. Takes a collection lock if nameOnly is false. */ @@ -323,11 +362,32 @@ public: clk.emplace(opCtx, nss, MODE_IS); } - CollectionPtr collection = - CollectionCatalog::get(opCtx)->lookupCollectionByNamespace(opCtx, - nss); - BSONObj collBson = buildCollectionBson( - opCtx, collection, includePendingDrops, nameOnly); + auto collBson = [&] { + if (auto collection = + CollectionCatalog::get(opCtx)->lookupCollectionByNamespace( + opCtx, nss)) { + return buildCollectionBson( + opCtx, collection, includePendingDrops, nameOnly); + } + + auto view = viewCatalog->lookupWithoutValidatingDurableViews( + opCtx, nss.ns()); + if (view && view->timeseries()) { + if (auto bucketsCollection = CollectionCatalog::get(opCtx) + ->lookupCollectionByNamespace( + opCtx, view->viewOn())) { + return buildTimeseriesBson( + opCtx, bucketsCollection, nameOnly); + } else { + // The buckets collection does not exist, so the time-series + // view will be appended when we iterate through the view + // catalog below. + } + } + + return BSONObj{}; + }(); + if (!collBson.isEmpty()) { _addWorkingSetMember( opCtx, collBson, matcher.get(), ws.get(), root.get()); @@ -340,12 +400,27 @@ public: ResourcePattern::forExactNamespace(collection->ns())))) { return true; } + BSONObj collBson = buildCollectionBson( opCtx, collection, includePendingDrops, nameOnly); if (!collBson.isEmpty()) { _addWorkingSetMember( opCtx, collBson, matcher.get(), ws.get(), root.get()); } + + if (collection && collection->getTimeseriesOptions() && + viewCatalog->lookupWithoutValidatingDurableViews( + opCtx, collection->ns().getTimeseriesViewNamespace().ns())) { + // The time-series view for this buckets namespace exists, so add it + // here while we have the collection options. + _addWorkingSetMember( + opCtx, + buildTimeseriesBson(opCtx, collection, nameOnly), + matcher.get(), + ws.get(), + root.get()); + } + return true; }; @@ -379,6 +454,23 @@ public: return true; } + if (view.timeseries()) { + if (!CollectionCatalog::get(opCtx) + ->lookupCollectionByNamespaceForRead(opCtx, + view.viewOn())) { + // There is no buckets collection backing this time-series view, + // which means that it was not already added along with the + // buckets collection above. + _addWorkingSetMember( + opCtx, + buildTimeseriesBson(view.name().coll(), nameOnly), + matcher.get(), + ws.get(), + root.get()); + } + return true; + } + BSONObj viewBson = buildViewBson(view, nameOnly); if (!viewBson.isEmpty()) { _addWorkingSetMember( diff --git a/src/mongo/db/exec/bucket_unpacker.cpp b/src/mongo/db/exec/bucket_unpacker.cpp index 1e884dc1714..829e490213b 100644 --- a/src/mongo/db/exec/bucket_unpacker.cpp +++ b/src/mongo/db/exec/bucket_unpacker.cpp @@ -30,7 +30,7 @@ #include "mongo/platform/basic.h" #include "mongo/db/exec/bucket_unpacker.h" -#include "mongo/db/timeseries/timeseries_field_names.h" +#include "mongo/db/timeseries/timeseries_constants.h" namespace mongo { diff --git a/src/mongo/db/exec/sample_from_timeseries_bucket.cpp b/src/mongo/db/exec/sample_from_timeseries_bucket.cpp index 2d0fe7e72aa..06bdba2e77a 100644 --- a/src/mongo/db/exec/sample_from_timeseries_bucket.cpp +++ b/src/mongo/db/exec/sample_from_timeseries_bucket.cpp @@ -28,7 +28,7 @@ */ #include "mongo/db/exec/sample_from_timeseries_bucket.h" -#include "mongo/db/timeseries/timeseries_field_names.h" +#include "mongo/db/timeseries/timeseries_constants.h" namespace mongo { const char* SampleFromTimeseriesBucket::kStageType = "SAMPLE_FROM_TIMESERIES_BUCKET"; diff --git a/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.cpp b/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.cpp index 216472accc4..3e523696eb8 100644 --- a/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.cpp +++ b/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.cpp @@ -36,7 +36,7 @@ #include "mongo/db/list_indexes_gen.h" #include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" -#include "mongo/db/timeseries/timeseries_field_names.h" +#include "mongo/db/timeseries/timeseries_constants.h" #include "mongo/db/timeseries/timeseries_gen.h" #include "mongo/db/timeseries/timeseries_index_schema_conversion_functions.h" diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp index 0b63d0aadb5..2fa8291fee2 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp @@ -52,7 +52,7 @@ #include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" #include "mongo/db/query/util/make_data_structure.h" -#include "mongo/db/timeseries/timeseries_field_names.h" +#include "mongo/db/timeseries/timeseries_constants.h" #include "mongo/logv2/log.h" #include "mongo/util/duration.h" #include "mongo/util/time_support.h" diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/unpack_bucket_exec_test.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/unpack_bucket_exec_test.cpp index 83f2fa6e894..35667959188 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/unpack_bucket_exec_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/unpack_bucket_exec_test.cpp @@ -31,7 +31,7 @@ #include "mongo/db/pipeline/aggregation_context_fixture.h" #include "mongo/db/pipeline/document_source_internal_unpack_bucket.h" #include "mongo/db/pipeline/document_source_mock.h" -#include "mongo/db/timeseries/timeseries_field_names.h" +#include "mongo/db/timeseries/timeseries_constants.h" namespace mongo { namespace { diff --git a/src/mongo/db/timeseries/timeseries_field_names.h b/src/mongo/db/timeseries/timeseries_constants.h index b6d24a78bd8..e9a6ebf80ba 100644 --- a/src/mongo/db/timeseries/timeseries_field_names.h +++ b/src/mongo/db/timeseries/timeseries_constants.h @@ -30,6 +30,8 @@ #pragma once #include "mongo/base/string_data.h" +#include "mongo/db/commands/create_gen.h" +#include "mongo/util/string_map.h" namespace mongo { namespace timeseries { @@ -49,5 +51,12 @@ static constexpr StringData kMetaFieldName = "metaField"_sd; // These are hard-coded field names in index specs. static constexpr StringData kKeyFieldName = "key"_sd; +static const StringDataSet kAllowedCollectionCreationOptions{ + CreateCommand::kStorageEngineFieldName, + CreateCommand::kIndexOptionDefaultsFieldName, + CreateCommand::kCollationFieldName, + CreateCommand::kTimeseriesFieldName, + CreateCommand::kExpireAfterSecondsFieldName}; + } // namespace timeseries } // namespace mongo diff --git a/src/mongo/db/timeseries/timeseries_index_schema_conversion_functions.cpp b/src/mongo/db/timeseries/timeseries_index_schema_conversion_functions.cpp index 1aa910b73e9..5909670ff08 100644 --- a/src/mongo/db/timeseries/timeseries_index_schema_conversion_functions.cpp +++ b/src/mongo/db/timeseries/timeseries_index_schema_conversion_functions.cpp @@ -33,7 +33,7 @@ #include "mongo/db/timeseries/timeseries_index_schema_conversion_functions.h" -#include "mongo/db/timeseries/timeseries_field_names.h" +#include "mongo/db/timeseries/timeseries_constants.h" #include "mongo/db/timeseries/timeseries_gen.h" #include "mongo/logv2/log.h" #include "mongo/logv2/redaction.h" diff --git a/src/mongo/db/timeseries/timeseries_index_schema_conversion_functions_test.cpp b/src/mongo/db/timeseries/timeseries_index_schema_conversion_functions_test.cpp index 9eac6a67236..2763b5a3385 100644 --- a/src/mongo/db/timeseries/timeseries_index_schema_conversion_functions_test.cpp +++ b/src/mongo/db/timeseries/timeseries_index_schema_conversion_functions_test.cpp @@ -32,7 +32,7 @@ #include "mongo/db/timeseries/timeseries_index_schema_conversion_functions.h" #include "mongo/bson/bsonobj.h" -#include "mongo/db/timeseries/timeseries_field_names.h" +#include "mongo/db/timeseries/timeseries_constants.h" #include "mongo/db/timeseries/timeseries_gen.h" #include "mongo/unittest/unittest.h" diff --git a/src/mongo/db/views/resolved_view.cpp b/src/mongo/db/views/resolved_view.cpp index 2c1e9900fa6..9c754c559e7 100644 --- a/src/mongo/db/views/resolved_view.cpp +++ b/src/mongo/db/views/resolved_view.cpp @@ -36,7 +36,7 @@ #include "mongo/db/pipeline/document_source_index_stats.h" #include "mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.h" #include "mongo/db/pipeline/document_source_internal_unpack_bucket.h" -#include "mongo/db/timeseries/timeseries_field_names.h" +#include "mongo/db/timeseries/timeseries_constants.h" #include "mongo/rpc/get_status_from_command_result.h" namespace mongo { |