diff --git a/.mci.yml b/.mci.yml index 478312c514..dd941fcf13 100644 --- a/.mci.yml +++ b/.mci.yml @@ -462,6 +462,7 @@ functions: export URI_OPTIONS_TESTS_PATH="$(pwd)/../data/uri-options" export VERSIONED_API_TESTS_PATH=$(pwd)/../data/versioned-api export WITH_TRANSACTION_TESTS_PATH="$(pwd)/../data/with_transaction" + export INDEX_MANAGEMENT_TESTS_PATH="$(pwd)/../data/index-management" export MONGODB_API_VERSION="${MONGODB_API_VERSION}" diff --git a/data/index-management/createSearchIndex.json b/data/index-management/createSearchIndex.json new file mode 100644 index 0000000000..89e22e9999 --- /dev/null +++ b/data/index-management/createSearchIndex.json @@ -0,0 +1,134 @@ +{ + "description": "createSearchIndex", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "no name provided for an index definition", + "operations": [ + { + "name": "createSearchIndex", + "object": "collection0", + "arguments": { + "model": { + "definition": { + "mappings": { + "dynamic": true + } + } + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [ + { + "definition": { + "mappings": { + "dynamic": true + } + } + } + ], + "$db": "database0" + } + } + } + ] + } + ] + }, + { + "description": "name provided for an index definition", + "operations": [ + { + "name": "createSearchIndex", + "object": "collection0", + "arguments": { + "model": { + "definition": { + "mappings": { + "dynamic": true + } + }, + "name": "test index" + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [ + { + "definition": { + "mappings": { + "dynamic": true + } + }, + "name": "test index" + } + ], + "$db": "database0" + } + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/data/index-management/createSearchIndexes.json b/data/index-management/createSearchIndexes.json new file mode 100644 index 0000000000..1503052667 --- /dev/null +++ b/data/index-management/createSearchIndexes.json @@ -0,0 +1,169 @@ +{ + "description": "createSearchIndexes", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "empty index definition array", + "operations": [ + { + "name": "createSearchIndexes", + "object": "collection0", + "arguments": { + "models": [] + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [], + "$db": "database0" + } + } + } + ] + } + ] + }, + { + "description": "no name provided for an index definition", + "operations": [ + { + "name": "createSearchIndexes", + "object": "collection0", + "arguments": { + "models": [ + { + "definition": { + "mappings": { + "dynamic": true + } + } + } + ] + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [ + { + "definition": { + "mappings": { + "dynamic": true + } + } + } + ], + "$db": "database0" + } + } + } + ] + } + ] + }, + { + "description": "name provided for an index definition", + "operations": [ + { + "name": "createSearchIndexes", + "object": "collection0", + "arguments": { + "models": [ + { + "definition": { + "mappings": { + "dynamic": true + } + }, + "name": "test index" + } + ] + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [ + { + "definition": { + "mappings": { + "dynamic": true + } + }, + "name": "test index" + } + ], + "$db": "database0" + } + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/data/index-management/dropSearchIndex.json b/data/index-management/dropSearchIndex.json new file mode 100644 index 0000000000..4ffa2cdb07 --- /dev/null +++ b/data/index-management/dropSearchIndex.json @@ -0,0 +1,73 @@ +{ + "description": "dropSearchIndex", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "sends the correct command", + "operations": [ + { + "name": "dropSearchIndex", + "object": "collection0", + "arguments": { + "name": "test index" + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "dropSearchIndex": "collection0", + "name": "test index", + "$db": "database0" + } + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/data/index-management/listSearchIndexes.json b/data/index-management/listSearchIndexes.json new file mode 100644 index 0000000000..528f295d7f --- /dev/null +++ b/data/index-management/listSearchIndexes.json @@ -0,0 +1,153 @@ +{ + "description": "listSearchIndexes", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "when no name is provided, it does not populate the filter", + "operations": [ + { + "name": "listSearchIndexes", + "object": "collection0", + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "pipeline": [ + { + "$listSearchIndexes": {} + } + ] + } + } + } + ] + } + ] + }, + { + "description": "when a name is provided, it is present in the filter", + "operations": [ + { + "name": "listSearchIndexes", + "object": "collection0", + "arguments": { + "name": "test index" + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "pipeline": [ + { + "$listSearchIndexes": { + "name": "test index" + } + } + ], + "$db": "database0" + } + } + } + ] + } + ] + }, + { + "description": "aggregation cursor options are supported", + "operations": [ + { + "name": "listSearchIndexes", + "object": "collection0", + "arguments": { + "name": "test index", + "aggregationOptions": { + "batchSize": 10 + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "cursor": { + "batchSize": 10 + }, + "pipeline": [ + { + "$listSearchIndexes": { + "name": "test index" + } + } + ], + "$db": "database0" + } + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/data/index-management/test_files.txt b/data/index-management/test_files.txt new file mode 100644 index 0000000000..11a72aff2c --- /dev/null +++ b/data/index-management/test_files.txt @@ -0,0 +1,5 @@ +createSearchIndex.json +createSearchIndexes.json +dropSearchIndex.json +listSearchIndexes.json +updateSearchIndex.json \ No newline at end of file diff --git a/data/index-management/updateSearchIndex.json b/data/index-management/updateSearchIndex.json new file mode 100644 index 0000000000..00cd7e7541 --- /dev/null +++ b/data/index-management/updateSearchIndex.json @@ -0,0 +1,75 @@ +{ + "description": "updateSearchIndex", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "sends the correct command", + "operations": [ + { + "name": "updateSearchIndex", + "object": "collection0", + "arguments": { + "name": "test index", + "definition": {} + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "updateSearchIndex": "collection0", + "name": "test index", + "definition": {}, + "$db": "database0" + } + } + } + ] + } + ] + } + ] +} diff --git a/src/mongocxx/CMakeLists.txt b/src/mongocxx/CMakeLists.txt index 45fd3ad4f4..eb328587e6 100644 --- a/src/mongocxx/CMakeLists.txt +++ b/src/mongocxx/CMakeLists.txt @@ -178,6 +178,8 @@ set(mongocxx_sources result/replace_one.cpp result/rewrap_many_datakey.cpp result/update.cpp + search_index_model.cpp + search_index_view.cpp uri.cpp validation_criteria.cpp write_concern.cpp @@ -426,6 +428,7 @@ set_local_dist (src_mongocxx_DIST_local pipeline.hpp pool.cpp pool.hpp + private/append_aggregate_options.hh private/bulk_write.hh private/change_stream.hh private/client.hh @@ -448,6 +451,8 @@ set_local_dist (src_mongocxx_DIST_local private/pool.hh private/read_concern.hh private/read_preference.hh + private/search_index_model.hh + private/search_index_view.hh private/uri.hh private/write_concern.hh read_concern.cpp @@ -470,6 +475,10 @@ set_local_dist (src_mongocxx_DIST_local result/rewrap_many_datakey.hpp result/update.cpp result/update.hpp + search_index_model.cpp + search_index_model.hpp + search_index_view.cpp + search_index_view.hpp stdx.hpp test_util/client_helpers.cpp test_util/client_helpers.hh diff --git a/src/mongocxx/client_session.hpp b/src/mongocxx/client_session.hpp index 5e37a4d574..d3b9c64bd9 100644 --- a/src/mongocxx/client_session.hpp +++ b/src/mongocxx/client_session.hpp @@ -191,6 +191,7 @@ class MONGOCXX_API client_session { friend class collection; friend class database; friend class index_view; + friend class search_index_view; class MONGOCXX_PRIVATE impl; diff --git a/src/mongocxx/collection.cpp b/src/mongocxx/collection.cpp index aac7d42bf2..9983b52a04 100644 --- a/src/mongocxx/collection.cpp +++ b/src/mongocxx/collection.cpp @@ -1392,6 +1392,10 @@ class index_view collection::indexes() { return index_view{_get_impl().collection_t, _get_impl().client_impl->client_t}; } +class search_index_view collection::search_indexes() { + return search_index_view{_get_impl().collection_t, _get_impl().client_impl->client_t}; +} + class bulk_write collection::_init_insert_many(const options::insert& options, const client_session* session) { options::bulk_write bulk_write_options; diff --git a/src/mongocxx/collection.hpp b/src/mongocxx/collection.hpp index 9cc2450597..7ceb9fb6ea 100644 --- a/src/mongocxx/collection.hpp +++ b/src/mongocxx/collection.hpp @@ -56,6 +56,7 @@ #include #include #include +#include #include #include @@ -1848,6 +1849,10 @@ class MONGOCXX_API collection { /// @} /// + /// + /// Gets a search_index_view to the collection. + search_index_view search_indexes(); + private: friend mongocxx::bulk_write; friend mongocxx::database; diff --git a/src/mongocxx/cursor.hpp b/src/mongocxx/cursor.hpp index 2823f83751..9e3aa0c0ba 100644 --- a/src/mongocxx/cursor.hpp +++ b/src/mongocxx/cursor.hpp @@ -25,6 +25,7 @@ namespace mongocxx { MONGOCXX_INLINE_NAMESPACE_BEGIN class collection; +class search_index_view; /// /// Class representing a pointer to the result set of a query on a MongoDB server. @@ -84,6 +85,7 @@ class MONGOCXX_API cursor { friend class client_encryption; friend class database; friend class index_view; + friend class search_index_view; friend class cursor::iterator; MONGOCXX_PRIVATE cursor(void* cursor_ptr, diff --git a/src/mongocxx/exception/error_code.cpp b/src/mongocxx/exception/error_code.cpp index 2fb79960bd..ad8cd1ea9c 100644 --- a/src/mongocxx/exception/error_code.cpp +++ b/src/mongocxx/exception/error_code.cpp @@ -86,6 +86,12 @@ class error_category final : public std::error_category { return "an invalid transactions options object was provided"; case error_code::k_create_resource_fail: return "could not create resource"; + case error_code::k_invalid_search_index_model: + return "invalid use of default constructed or moved-from " + "mongocxx::search_index_model object"; + case error_code::k_invalid_search_index_view: + return "invalid use of default constructed or moved-from " + "mongocxx::search_index_view object"; default: return "unknown mongocxx error"; } diff --git a/src/mongocxx/exception/error_code.hpp b/src/mongocxx/exception/error_code.hpp index 8a6c11194b..d3c3cf71e7 100644 --- a/src/mongocxx/exception/error_code.hpp +++ b/src/mongocxx/exception/error_code.hpp @@ -94,6 +94,12 @@ enum class error_code : std::int32_t { // A resource (server API handle, etc.) could not be created: k_create_resource_fail, + // A default-constructed or moved-from mongocxx::search_index_model object has been used. + k_invalid_search_index_model, + + // A default-constructed or moved-from mongocxx::search_index_view object has been used. + k_invalid_search_index_view, + // Add new constant string message to error_code.cpp as well! }; diff --git a/src/mongocxx/options/aggregate.cpp b/src/mongocxx/options/aggregate.cpp index ead8deb1c6..789ce67ffb 100644 --- a/src/mongocxx/options/aggregate.cpp +++ b/src/mongocxx/options/aggregate.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -30,45 +31,7 @@ aggregate& aggregate::allow_disk_use(bool allow_disk_use) { } void aggregate::append(bsoncxx::builder::basic::document& builder) const { - if (const auto& allow_disk_use = this->allow_disk_use()) { - builder.append(kvp("allowDiskUse", *allow_disk_use)); - } - - if (const auto& collation = this->collation()) { - builder.append(kvp("collation", *collation)); - } - - if (const auto& let = this->let()) { - builder.append(kvp("let", *let)); - } - - if (const auto& max_time = this->max_time()) { - builder.append(kvp("maxTimeMS", bsoncxx::types::b_int64{max_time->count()})); - } - - if (const auto& bypass_document_validation = this->bypass_document_validation()) { - builder.append(kvp("bypassDocumentValidation", *bypass_document_validation)); - } - - if (const auto& hint = this->hint()) { - builder.append(kvp("hint", hint->to_value())); - } - - if (const auto& read_concern = this->read_concern()) { - builder.append(kvp("readConcern", read_concern->to_document())); - } - - if (const auto& write_concern = this->write_concern()) { - builder.append(kvp("writeConcern", write_concern->to_document())); - } - - if (const auto& batch_size = this->batch_size()) { - builder.append(kvp("batchSize", *batch_size)); - } - - if (const auto& comment = this->comment()) { - builder.append(kvp("comment", *comment)); - } + append_aggregate_options(builder, *this); } aggregate& aggregate::collation(bsoncxx::document::view_or_value collation) { diff --git a/src/mongocxx/private/append_aggregate_options.hh b/src/mongocxx/private/append_aggregate_options.hh new file mode 100644 index 0000000000..954e2e7771 --- /dev/null +++ b/src/mongocxx/private/append_aggregate_options.hh @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +#include + +namespace mongocxx { +MONGOCXX_INLINE_NAMESPACE_BEGIN + +MONGOCXX_INLINE void append_aggregate_options(bsoncxx::builder::basic::document& builder, + const options::aggregate& options) { + using bsoncxx::builder::basic::kvp; + + if (const auto& allow_disk_use = options.allow_disk_use()) { + builder.append(kvp("allowDiskUse", *allow_disk_use)); + } + + if (const auto& collation = options.collation()) { + builder.append(kvp("collation", *collation)); + } + + if (const auto& let = options.let()) { + builder.append(kvp("let", *let)); + } + + if (const auto& max_time = options.max_time()) { + builder.append(kvp("maxTimeMS", bsoncxx::types::b_int64{max_time->count()})); + } + + if (const auto& bypass_document_validation = options.bypass_document_validation()) { + builder.append(kvp("bypassDocumentValidation", *bypass_document_validation)); + } + + if (const auto& hint = options.hint()) { + builder.append(kvp("hint", hint->to_value())); + } + + if (const auto& read_concern = options.read_concern()) { + builder.append(kvp("readConcern", read_concern->to_document())); + } + + if (const auto& write_concern = options.write_concern()) { + builder.append(kvp("writeConcern", write_concern->to_document())); + } + + if (const auto& batch_size = options.batch_size()) { + builder.append(kvp("batchSize", *batch_size)); + } + + if (const auto& comment = options.comment()) { + builder.append(kvp("comment", *comment)); + } +} +MONGOCXX_INLINE_NAMESPACE_END +} // namespace mongocxx diff --git a/src/mongocxx/private/search_index_model.hh b/src/mongocxx/private/search_index_model.hh new file mode 100644 index 0000000000..c7502ef1d4 --- /dev/null +++ b/src/mongocxx/private/search_index_model.hh @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +#include + +namespace mongocxx { +MONGOCXX_INLINE_NAMESPACE_BEGIN + +class search_index_model::impl { + public: + impl(bsoncxx::document::view_or_value definition) : _definition(definition.view()) {} + impl(bsoncxx::string::view_or_value name, bsoncxx::document::view_or_value definition) + : _name(name), _definition(definition.view()) {} + + bsoncxx::stdx::optional _name; + bsoncxx::document::view_or_value _definition; +}; + +MONGOCXX_INLINE_NAMESPACE_END +} // namespace mongocxx diff --git a/src/mongocxx/private/search_index_view.hh b/src/mongocxx/private/search_index_view.hh new file mode 100644 index 0000000000..f24ddecd55 --- /dev/null +++ b/src/mongocxx/private/search_index_view.hh @@ -0,0 +1,175 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace mongocxx { +MONGOCXX_INLINE_NAMESPACE_BEGIN + +using bsoncxx::builder::basic::kvp; +using bsoncxx::builder::basic::make_document; + +class search_index_view::impl { + public: + impl(mongoc_collection_t* collection, mongoc_client_t* client) + : _coll{collection}, _client{client} {} + + cursor list(const client_session* session, + bsoncxx::string::view_or_value name, + const options::aggregate& options) { + pipeline pipeline{}; + pipeline.append_stage( + make_document(kvp("$listSearchIndexes", make_document(kvp("name", name.view()))))); + return list(session, pipeline, options); + } + + cursor list(const client_session* session, const options::aggregate& options) { + pipeline pipeline{}; + pipeline.append_stage(make_document(kvp("$listSearchIndexes", make_document()))); + return list(session, pipeline, options); + } + + cursor list(const client_session* session, + const pipeline& pipeline, + const options::aggregate& options) { + bsoncxx::builder::basic::document opts_doc; + libbson::scoped_bson_t stages(bsoncxx::document::view(pipeline.view_array())); + + append_aggregate_options(opts_doc, options); + + if (session) { + opts_doc.append(bsoncxx::builder::concatenate_doc{session->_get_impl().to_document()}); + } + + const mongoc_read_prefs_t* const rp_ptr = + options.read_preference() ? options.read_preference()->_impl->read_preference_t + : nullptr; + + libbson::scoped_bson_t opts_bson(opts_doc.view()); + + return libmongoc::collection_aggregate( + _coll, mongoc_query_flags_t(), stages.bson(), opts_bson.bson(), rp_ptr); + } + + std::string create_one(const client_session* session, const search_index_model& model) { + const auto result = create_many(session, std::vector{model}); + return bsoncxx::string::to_string(result["indexesCreated"] + .get_array() + .value.begin() + ->get_document() + .value["name"] + .get_string() + .value); + } + + bsoncxx::document::value create_many(const client_session* session, + const std::vector& search_indexes) { + using namespace bsoncxx; + + builder::basic::array search_index_arr; + + for (auto&& model : search_indexes) { + builder::basic::document search_index_doc; + // model may or may not have a name attached to it. The server will create the name if + // it is not set. + const stdx::optional name = model.name(); + const bsoncxx::document::view& definition = model.definition(); + + if (name) { + search_index_doc.append(kvp("name", name.value())); + } + search_index_doc.append(kvp("definition", definition)); + search_index_arr.append(search_index_doc.view()); + } + + document::view_or_value command = + make_document(kvp("createSearchIndexes", libmongoc::collection_get_name(_coll)), + kvp("indexes", search_index_arr.view())); + + libbson::scoped_bson_t reply; + bson_error_t error; + + builder::basic::document opts_doc; + + if (session) { + opts_doc.append(bsoncxx::builder::concatenate_doc{session->_get_impl().to_document()}); + } + + libbson::scoped_bson_t command_bson{command}; + libbson::scoped_bson_t opts_bson{opts_doc.view()}; + + auto result = libmongoc::collection_write_command_with_opts( + _coll, command_bson.bson(), opts_bson.bson(), reply.bson_for_init(), &error); + + if (!result) { + throw_exception(reply.steal(), error); + } + + return reply.steal(); + } + + void drop_one(const client_session* session, bsoncxx::string::view_or_value name) { + bsoncxx::builder::basic::document opts_doc; + + bsoncxx::document::value command = + make_document(kvp("dropSearchIndex", libmongoc::collection_get_name(_coll)), + kvp("name", name.view())); + + if (session) { + opts_doc.append(bsoncxx::builder::concatenate_doc{session->_get_impl().to_document()}); + } + + libbson::scoped_bson_t reply; + libbson::scoped_bson_t command_bson{command.view()}; + libbson::scoped_bson_t opts_bson{opts_doc.view()}; + bson_error_t error; + + bool result = libmongoc::collection_write_command_with_opts( + _coll, command_bson.bson(), opts_bson.bson(), reply.bson_for_init(), &error); + + if (!result) { + throw_exception(reply.steal(), error); + } + } + + void update_one(const client_session* session, + bsoncxx::string::view_or_value name, + bsoncxx::document::view_or_value definition) { + bsoncxx::builder::basic::document opts_doc; + bsoncxx::document::value command = + make_document(kvp("updateSearchIndex", libmongoc::collection_get_name(_coll)), + kvp("name", name.view()), + kvp("definition", definition.view())); + + if (session) { + opts_doc.append(bsoncxx::builder::concatenate_doc{session->_get_impl().to_document()}); + } + + libbson::scoped_bson_t reply; + libbson::scoped_bson_t command_bson{command.view()}; + libbson::scoped_bson_t opts_bson{opts_doc.view()}; + bson_error_t error; + + bool result = libmongoc::collection_write_command_with_opts( + _coll, command_bson.bson(), opts_bson.bson(), reply.bson_for_init(), &error); + + if (!result) { + throw_exception(reply.steal(), error); + } + } + + mongoc_collection_t* _coll; + mongoc_client_t* _client; +}; +MONGOCXX_INLINE_NAMESPACE_END +} // namespace mongocxx diff --git a/src/mongocxx/read_preference.hpp b/src/mongocxx/read_preference.hpp index 79ed258763..16bc5fdb63 100644 --- a/src/mongocxx/read_preference.hpp +++ b/src/mongocxx/read_preference.hpp @@ -34,6 +34,7 @@ class client; class collection; class database; class uri; +class search_index_view; namespace events { class topology_description; @@ -289,6 +290,7 @@ class MONGOCXX_API read_preference { /// \relates mongocxx::events::topology_description friend mongocxx::events::topology_description; friend uri; + friend search_index_view; /// /// @{ diff --git a/src/mongocxx/search_index_model.cpp b/src/mongocxx/search_index_model.cpp new file mode 100644 index 0000000000..a3e7d4e073 --- /dev/null +++ b/src/mongocxx/search_index_model.cpp @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include + +#include + +namespace mongocxx { +MONGOCXX_INLINE_NAMESPACE_BEGIN + +search_index_model::search_index_model(bsoncxx::document::view_or_value definition) + : _impl{bsoncxx::stdx::make_unique(definition)} {} +search_index_model::search_index_model(bsoncxx::string::view_or_value name, + bsoncxx::document::view_or_value definition) + : _impl{bsoncxx::stdx::make_unique(name, definition)} {} + +search_index_model::search_index_model(search_index_model&&) noexcept = default; + +search_index_model& search_index_model::operator=(search_index_model&&) noexcept = default; + +search_index_model::search_index_model(const search_index_model& other) + : _impl(bsoncxx::stdx::make_unique(other._get_impl())) {} + +search_index_model& search_index_model::operator=(const search_index_model& other) { + _get_impl() = other._get_impl(); + return *this; +} + +search_index_model::~search_index_model() = default; + +bsoncxx::stdx::optional search_index_model::name() const { + return _get_impl()._name; +} + +bsoncxx::document::view search_index_model::definition() const { + return _get_impl()._definition.view(); +} + +const search_index_model::impl& search_index_model::_get_impl() const { + if (!_impl) { + throw logic_error{error_code::k_invalid_search_index_model}; + } + return *_impl; +} + +search_index_model::impl& search_index_model::_get_impl() { + auto cthis = const_cast(this); + return const_cast(cthis->_get_impl()); +} + +MONGOCXX_INLINE_NAMESPACE_END +} // namespace mongocxx diff --git a/src/mongocxx/search_index_model.hpp b/src/mongocxx/search_index_model.hpp new file mode 100644 index 0000000000..e4a10c61ae --- /dev/null +++ b/src/mongocxx/search_index_model.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include + +namespace mongocxx { +MONGOCXX_INLINE_NAMESPACE_BEGIN + +/// +/// Class representing a search index on a MongoDB server. +/// +class MONGOCXX_API search_index_model { + public: + /// + /// Initializes a new search_index_model over a mongocxx::collection. + /// + search_index_model(bsoncxx::document::view_or_value definition); + search_index_model(bsoncxx::string::view_or_value name, + bsoncxx::document::view_or_value definition); + + search_index_model() = delete; + + /// + /// Move constructs a search_index_model. + /// + search_index_model(search_index_model&&) noexcept; + + /// + /// Move assigns a search_index_model. + /// + search_index_model& operator=(search_index_model&&) noexcept; + + /// + /// Copy constructs a search_index_model. + /// + search_index_model(const search_index_model&); + + /// + /// Copy assigns a search_index_model. + /// + search_index_model& operator=(const search_index_model&); + + /// + /// Destroys a search_index_model. + /// + ~search_index_model(); + + /// + /// Retrieves name of a search_index_model. + /// + bsoncxx::stdx::optional name() const; + + /// + /// Retrieves definition of a search_index_model. + /// + bsoncxx::document::view definition() const; + + private: + class MONGOCXX_PRIVATE impl; + + MONGOCXX_PRIVATE const impl& _get_impl() const; + + MONGOCXX_PRIVATE impl& _get_impl(); + + private: + std::unique_ptr _impl; +}; + +MONGOCXX_INLINE_NAMESPACE_END +} // namespace mongocxx + +#include diff --git a/src/mongocxx/search_index_view.cpp b/src/mongocxx/search_index_view.cpp new file mode 100644 index 0000000000..c890c0271c --- /dev/null +++ b/src/mongocxx/search_index_view.cpp @@ -0,0 +1,122 @@ +#include +#include +#include +#include +#include + +#include + +namespace mongocxx { +MONGOCXX_INLINE_NAMESPACE_BEGIN + +search_index_view::search_index_view(void* coll, void* client) + : _impl{stdx::make_unique(static_cast(coll), + static_cast(client))} {} + +search_index_view::search_index_view(search_index_view&&) noexcept = default; +search_index_view& search_index_view::operator=(search_index_view&&) noexcept = default; + +search_index_view::search_index_view(const search_index_view& other) + : _impl(stdx::make_unique(other._get_impl())) {} + +search_index_view& search_index_view::operator=(const search_index_view& other) { + _get_impl() = other._get_impl(); + return *this; +} + +search_index_view::~search_index_view() = default; + +cursor search_index_view::list(const options::aggregate& options) { + return _get_impl().list(nullptr, options); +} + +cursor search_index_view::list(const client_session& session, const options::aggregate& options) { + return _get_impl().list(&session, options); +} + +cursor search_index_view::list(bsoncxx::string::view_or_value name, + const options::aggregate& options) { + return _get_impl().list(nullptr, name, options); +} + +cursor search_index_view::list(const client_session& session, + bsoncxx::string::view_or_value name, + const options::aggregate& options) { + return _get_impl().list(&session, name, options); +} + +std::string search_index_view::create_one(bsoncxx::string::view_or_value name, + bsoncxx::document::view_or_value definition) { + return create_one(search_index_model(name, definition)); +} + +std::string search_index_view::create_one(const client_session& session, + bsoncxx::string::view_or_value name, + bsoncxx::document::view_or_value definition) { + return create_one(session, search_index_model(name, definition)); +} + +std::string search_index_view::create_one(const search_index_model& model) { + return _get_impl().create_one(nullptr, model); +} + +std::string search_index_view::create_one(const client_session& session, + const search_index_model& model) { + return _get_impl().create_one(&session, model); +} + +std::vector search_index_view::create_many( + const std::vector& models) { + auto response = _get_impl().create_many(nullptr, models); + return _create_many_helper(response["indexesCreated"].get_array().value); +} + +std::vector search_index_view::create_many( + const client_session& session, const std::vector& models) { + auto response = _get_impl().create_many(&session, models); + return _create_many_helper(response["indexesCreated"].get_array().value); +} + +std::vector search_index_view::_create_many_helper( + bsoncxx::array::view created_indexes) { + std::vector search_index_names; + for (auto&& index : created_indexes) { + search_index_names.push_back(index.get_document().value["name"].get_string().value); + } + return search_index_names; +} + +void search_index_view::drop_one(bsoncxx::string::view_or_value name) { + _get_impl().drop_one(nullptr, name); +} + +void search_index_view::drop_one(const client_session& session, + bsoncxx::string::view_or_value name) { + _get_impl().drop_one(&session, name); +} + +void search_index_view::update_one(bsoncxx::string::view_or_value name, + bsoncxx::document::view_or_value definition) { + _get_impl().update_one(nullptr, name, definition); +} + +void search_index_view::update_one(const client_session& session, + bsoncxx::string::view_or_value name, + bsoncxx::document::view_or_value definition) { + _get_impl().update_one(&session, name, definition); +} + +const search_index_view::impl& search_index_view::_get_impl() const { + if (!_impl) { + throw mongocxx::logic_error{error_code::k_invalid_search_index_view}; + } + return *_impl; +} + +search_index_view::impl& search_index_view::_get_impl() { + auto cthis = const_cast(this); + return const_cast(cthis->_get_impl()); +} + +MONGOCXX_INLINE_NAMESPACE_END +} // namespace mongocxx diff --git a/src/mongocxx/search_index_view.hpp b/src/mongocxx/search_index_view.hpp new file mode 100644 index 0000000000..c31bc55d95 --- /dev/null +++ b/src/mongocxx/search_index_view.hpp @@ -0,0 +1,249 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include + +namespace mongocxx { +MONGOCXX_INLINE_NAMESPACE_BEGIN + +/// +/// Class representing a MongoDB search index view. +/// +class MONGOCXX_API search_index_view { + public: + search_index_view(search_index_view&&) noexcept; + search_index_view& operator=(search_index_view&&) noexcept; + + search_index_view(const search_index_view&); + search_index_view& operator=(const search_index_view&); + + ~search_index_view(); + + /// + /// @{ + /// + /// Returns a cursor over all the search indexes. + /// + /// @param options + /// Options included in the aggregate operation. + /// + /// @return A cursor to the list of the search indexes returned. + /// + cursor list(const options::aggregate& options = options::aggregate()); + + /// + /// Returns a cursor over all the search indexes. + /// + /// @param session + /// The mongocxx::client_session with which to perform the list operation. + /// @param options + /// Options included in the aggregate operation. + /// + /// @return A cursor to the list of the search indexes returned. + /// + cursor list(const client_session& session, + const options::aggregate& options = options::aggregate()); + + /// + /// Returns a cursor over all the search indexes. + /// + /// @param name + /// The name of the search index to find. + /// @param options + /// Options included in the aggregate operation. + /// + /// @return A cursor to the list of the search indexes returned. + /// + cursor list(bsoncxx::string::view_or_value name, + const options::aggregate& options = options::aggregate()); + + /// + /// Returns a cursor over all the search indexes. + /// + /// @param session + /// The mongocxx::client_session with which to perform the list operation. + /// @param name + /// The name of the search index to find. + /// @param options + /// Options included in the aggregate operation. + /// + /// @return A cursor to the list of the search indexes returned. + /// + cursor list(const client_session& session, + bsoncxx::string::view_or_value name, + const options::aggregate& options = options::aggregate()); + + /// + /// @} + /// + + /// + /// @{ + /// + /// This is a convenience method for creating a single search index. + /// + /// @param name + /// The name of the search index to create. + /// @param definition + /// The document describing the search index to be created. + /// + /// @return The name of the created search index. + /// + std::string create_one(bsoncxx::string::view_or_value name, + bsoncxx::document::view_or_value definition); + + /// + /// This is a convenience method for creating a single search index. + /// + /// @param session + /// The mongocxx::client_session with which to perform the operation. + /// @param name + /// The name of the search index to create. + /// @param definition + /// The document describing the search index to be created. + /// + /// @return The name of the created search index. + /// + std::string create_one(const client_session& session, + bsoncxx::string::view_or_value name, + bsoncxx::document::view_or_value definition); + + /// + /// This is a convenience method for creating a single search index. + /// + /// @param model + /// The search index model to create. + /// + /// @return The name of the created index. + /// + std::string create_one(const search_index_model& model); + + /// + /// This is a convenience method for creating a single search index. + /// + /// @param session + /// The mongocxx::client_session with which to perform the operation. + /// @param model + /// The search index model to create. + /// + /// @return The name of the created index. + /// + std::string create_one(const client_session& session, const search_index_model& model); + + /// + /// @} + /// + + /// + /// @{ + /// + /// Creates multiple search indexes in the collection. + /// + /// @param models + /// The search index models to create. + /// + /// @return The names of the created indexes. + /// + std::vector create_many( + const std::vector& models); + + /// + /// Creates multiple search indexes in the collection. + /// + /// @param session + /// The mongocxx::client_session with which to perform the operation. + /// @param models + /// The search index models to create. + /// + /// @return The names of the created indexes. + /// + std::vector create_many( + const client_session& session, const std::vector& models); + + /// + /// @} + /// + + /// + /// @{ + /// + /// Drops a single search index from the collection by the index name. + /// + /// @param name + /// The name of the search index to drop. + /// + void drop_one(bsoncxx::string::view_or_value name); + + /// + /// Drops a single search index from the collection by the index name. + /// + /// @param session + /// The mongocxx::client_session with which to perform the operation. + /// @param name + /// The name of the search index to drop. + /// + void drop_one(const client_session& session, bsoncxx::string::view_or_value name); + + /// + /// @} + /// + + /// + /// @{ + /// + /// Updates a single search index from the collection by the search index name. + /// + /// @param name + /// The name of the search index to update. + /// @param definition + /// The definition to update the search index to. + /// + void update_one(bsoncxx::string::view_or_value name, + bsoncxx::document::view_or_value definition); + + /// + /// Updates a single search index from the collection by the search index name. + /// + /// @param session + /// The mongocxx::client_session with which to perform the operation. + /// @param name + /// The name of the search index to update. + /// @param definition + /// The definition to update the search index to. + /// + void update_one(const client_session& session, + bsoncxx::string::view_or_value name, + bsoncxx::document::view_or_value definition); + + /// + /// @} + /// + + private: + friend class collection; + class MONGOCXX_PRIVATE impl; + + MONGOCXX_PRIVATE search_index_view(void* coll, void* client); + + MONGOCXX_PRIVATE std::vector _create_many_helper( + bsoncxx::array::view created_indexes); + + MONGOCXX_PRIVATE const impl& _get_impl() const; + + MONGOCXX_PRIVATE impl& _get_impl(); + + private: + std::unique_ptr _impl; +}; + +MONGOCXX_INLINE_NAMESPACE_END +} // namespace mongocxx + +#include diff --git a/src/mongocxx/test/CMakeLists.txt b/src/mongocxx/test/CMakeLists.txt index 720074e923..c52fe744dd 100644 --- a/src/mongocxx/test/CMakeLists.txt +++ b/src/mongocxx/test/CMakeLists.txt @@ -275,6 +275,7 @@ set_property(TEST unified_format_spec APPEND PROPERTY ENVIRONMENT "RETRYABLE_WRITES_UNIFIED_TESTS_PATH=${PROJECT_SOURCE_DIR}/../../data/retryable-writes/unified/" "UNIFIED_FORMAT_TESTS_PATH=${PROJECT_SOURCE_DIR}/../../data/unified-format" "VERSIONED_API_TESTS_PATH=${PROJECT_SOURCE_DIR}/../../data/versioned-api" + "INDEX_MANAGEMENT_TESTS_PATH=${PROJECT_SOURCE_DIR}/../../data/index-management" ) if (MONGOCXX_ENABLE_SLOW_TESTS) diff --git a/src/mongocxx/test/search_index_view.cpp b/src/mongocxx/test/search_index_view.cpp new file mode 100644 index 0000000000..3d71cef15e --- /dev/null +++ b/src/mongocxx/test/search_index_view.cpp @@ -0,0 +1 @@ +#include diff --git a/src/mongocxx/test/spec/unified_tests/operations.cpp b/src/mongocxx/test/spec/unified_tests/operations.cpp index 63e99f7911..51d2066100 100644 --- a/src/mongocxx/test/spec/unified_tests/operations.cpp +++ b/src/mongocxx/test/spec/unified_tests/operations.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -1794,6 +1795,97 @@ document::value create_find_cursor(entity::map& map, return make_document(); } +document::value create_search_index(collection& coll, document::view operation) { + const auto arguments = operation["arguments"]; + const auto raw_model = arguments["model"]; + const auto name = raw_model["name"]; + const auto definition = raw_model["definition"].get_document().value; + + const auto model = name ? search_index_model(name.get_string().value, definition) + : search_index_model(definition); + + return make_document(kvp("result", coll.search_indexes().create_one(model))); +} + +document::value create_search_indexes(collection& coll, document::view operation) { + auto arguments = operation["arguments"]; + + auto raw_models = arguments["models"].get_array().value; + std::vector models; + + for (auto&& m : raw_models) { + search_index_model model = m["name"] + ? search_index_model(m["name"].get_string().value, + m["definition"].get_document().value) + : search_index_model(m["definition"].get_document().value); + models.push_back(model); + } + + std::vector created_indexes = + coll.search_indexes().create_many(models); + + builder::basic::array result; + for (auto s : created_indexes) { + result.append(s); + } + + return make_document(kvp("result", result.view())); +} + +document::value drop_search_index(collection& coll, document::view operation) { + auto arguments = operation["arguments"]; + + auto name = arguments["name"].get_string().value; + + coll.search_indexes().drop_one(name); + + // no return value from drop_one + return make_document(); +} + +document::value list_search_indexes(collection& coll, document::view operation) { + auto arguments = operation["arguments"]; + options::aggregate options; + if (arguments["aggregationOptions"]) { + const auto aggregation_options = arguments["aggregationOptions"].get_document().view(); + for (auto&& element : aggregation_options) { + if (element.key() == stdx::string_view("batchSize")) { + options.batch_size(element.get_int32().value); + } else { + throw std::logic_error{"unsupported aggregateOptions field: " + + std::string(element.key())}; + } + } + } + + cursor c = arguments["name"] + ? coll.search_indexes().list(arguments["name"].get_string().value, options) + : coll.search_indexes().list(options); + + // we must loop over the resulting cursor to trigger server events for the spec tests + auto result = bsoncxx::builder::basic::document{}; + result.append( + bsoncxx::builder::basic::kvp("result", [&c](bsoncxx::builder::basic::sub_array array) { + for (auto&& document : c) { + array.append(document); + } + })); + + return result.extract(); +} + +document::value update_search_index(collection& coll, document::view operation) { + auto arguments = operation["arguments"]; + + auto name = arguments["name"].get_string().value; + auto definition = arguments["definition"].get_document().value; + + coll.search_indexes().update_one(name, definition); + + // no return value from update_one + return make_document(); +} + document::value operations::run(entity::map& entity_map, std::unordered_map& apm_map, const array::element& op, @@ -2129,6 +2221,31 @@ document::value operations::run(entity::map& entity_map, if (name == "removeKeyAltName") { return remove_key_alt_name(entity_map, object, op_view); } + if (name == "createSearchIndex") { + auto& coll = entity_map.get_collection(object); + + return create_search_index(coll, op_view); + } + if (name == "createSearchIndexes") { + auto& coll = entity_map.get_collection(object); + + return create_search_indexes(coll, op_view); + } + if (name == "dropSearchIndex") { + auto& coll = entity_map.get_collection(object); + + return drop_search_index(coll, op_view); + } + if (name == "listSearchIndexes") { + auto& coll = entity_map.get_collection(object); + + return list_search_indexes(coll, op_view); + } + if (name == "updateSearchIndex") { + auto& coll = entity_map.get_collection(object); + + return update_search_index(coll, op_view); + } throw std::logic_error{"unsupported operation: " + name}; } diff --git a/src/mongocxx/test/spec/unified_tests/runner.cpp b/src/mongocxx/test/spec/unified_tests/runner.cpp index b9e2055b14..22f5c83921 100644 --- a/src/mongocxx/test/spec/unified_tests/runner.cpp +++ b/src/mongocxx/test/spec/unified_tests/runner.cpp @@ -1239,6 +1239,10 @@ TEST_CASE("collection management spec automated tests", "[unified_format_spec]") CHECK(run_unified_format_tests_in_env_dir("COLLECTION_MANAGEMENT_TESTS_PATH")); } +TEST_CASE("index management spec automated tests", "[unified_format_spec]") { + CHECK(run_unified_format_tests_in_env_dir("INDEX_MANAGEMENT_TESTS_PATH")); +} + // See: // https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst TEST_CASE("client side encryption unified format spec automated tests", "[unified_format_spec]") {