From a09e00c31ea0f61611e4bd35c9e681ca2ef4ea59 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 6 Jul 2023 11:29:40 -0400 Subject: [PATCH 01/11] GODRIVER-2859 WIP --- .../index-management/createSearchIndex.json | 134 ++++++++++++++ .../index-management/createSearchIndex.yml | 60 +++++++ .../index-management/createSearchIndexes.json | 169 ++++++++++++++++++ .../index-management/createSearchIndexes.yml | 80 +++++++++ .../index-management/dropSearchIndex.json | 73 ++++++++ testdata/index-management/dropSearchIndex.yml | 41 +++++ .../index-management/listSearchIndexes.json | 153 ++++++++++++++++ .../index-management/listSearchIndexes.yml | 82 +++++++++ .../index-management/updateSearchIndex.json | 75 ++++++++ .../index-management/updateSearchIndex.yml | 43 +++++ 10 files changed, 910 insertions(+) create mode 100644 testdata/index-management/createSearchIndex.json create mode 100644 testdata/index-management/createSearchIndex.yml create mode 100644 testdata/index-management/createSearchIndexes.json create mode 100644 testdata/index-management/createSearchIndexes.yml create mode 100644 testdata/index-management/dropSearchIndex.json create mode 100644 testdata/index-management/dropSearchIndex.yml create mode 100644 testdata/index-management/listSearchIndexes.json create mode 100644 testdata/index-management/listSearchIndexes.yml create mode 100644 testdata/index-management/updateSearchIndex.json create mode 100644 testdata/index-management/updateSearchIndex.yml diff --git a/testdata/index-management/createSearchIndex.json b/testdata/index-management/createSearchIndex.json new file mode 100644 index 0000000000..07d7b3f666 --- /dev/null +++ b/testdata/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/testdata/index-management/createSearchIndex.yml b/testdata/index-management/createSearchIndex.yml new file mode 100644 index 0000000000..0eb5c1ab1d --- /dev/null +++ b/testdata/index-management/createSearchIndex.yml @@ -0,0 +1,60 @@ +description: "createSearchIndex" +schemaVersion: "1.4" +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: + - commandStartedEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: *database0 + - collection: + id: &collection0 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: &definition { mappings: { dynamic: true } } } + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + createSearchIndexes: *collection0 + indexes: [ { definition: *definition } ] + $db: *database0 + + - description: "name provided for an index definition" + operations: + - name: createSearchIndex + object: *collection0 + arguments: + model: { definition: &definition { mappings: { dynamic: true } } , name: 'test index' } + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + createSearchIndexes: *collection0 + indexes: [ { definition: *definition, name: 'test index' } ] + $db: *database0 \ No newline at end of file diff --git a/testdata/index-management/createSearchIndexes.json b/testdata/index-management/createSearchIndexes.json new file mode 100644 index 0000000000..86b200679a --- /dev/null +++ b/testdata/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/testdata/index-management/createSearchIndexes.yml b/testdata/index-management/createSearchIndexes.yml new file mode 100644 index 0000000000..dc01c1b166 --- /dev/null +++ b/testdata/index-management/createSearchIndexes.yml @@ -0,0 +1,80 @@ +description: "createSearchIndexes" +schemaVersion: "1.4" +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: + - commandStartedEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: *database0 + - collection: + id: &collection0 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: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + 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: &definition { mappings: { dynamic: true } } } ] + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + createSearchIndexes: *collection0 + indexes: [ { definition: *definition } ] + $db: *database0 + + - description: "name provided for an index definition" + operations: + - name: createSearchIndexes + object: *collection0 + arguments: + models: [ { definition: &definition { mappings: { dynamic: true } } , name: 'test index' } ] + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + createSearchIndexes: *collection0 + indexes: [ { definition: *definition, name: 'test index' } ] + $db: *database0 \ No newline at end of file diff --git a/testdata/index-management/dropSearchIndex.json b/testdata/index-management/dropSearchIndex.json new file mode 100644 index 0000000000..0b34a6ff45 --- /dev/null +++ b/testdata/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/testdata/index-management/dropSearchIndex.yml b/testdata/index-management/dropSearchIndex.yml new file mode 100644 index 0000000000..9c93cbe02b --- /dev/null +++ b/testdata/index-management/dropSearchIndex.yml @@ -0,0 +1,41 @@ +description: "dropSearchIndex" +schemaVersion: "1.4" +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: + - commandStartedEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: *database0 + - collection: + id: &collection0 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: &indexName 'test index' + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + dropSearchIndex: *collection0 + name: *indexName + $db: *database0 diff --git a/testdata/index-management/listSearchIndexes.json b/testdata/index-management/listSearchIndexes.json new file mode 100644 index 0000000000..84d4eee295 --- /dev/null +++ b/testdata/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/testdata/index-management/listSearchIndexes.yml b/testdata/index-management/listSearchIndexes.yml new file mode 100644 index 0000000000..7d3c3187b9 --- /dev/null +++ b/testdata/index-management/listSearchIndexes.yml @@ -0,0 +1,82 @@ +description: "listSearchIndexes" +schemaVersion: "1.4" +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: + - commandStartedEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: *database0 + - collection: + id: &collection0 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: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + 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: &indexName "test index" + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + aggregate: *collection0 + pipeline: + - $listSearchIndexes: { name: *indexName } + $db: *database0 + + - description: aggregation cursor options are supported + operations: + - name: listSearchIndexes + object: *collection0 + arguments: + name: &indexName "test index" + aggregationOptions: + batchSize: 10 + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + aggregate: *collection0 + cursor: { batchSize: 10 } + pipeline: + - $listSearchIndexes: { name: *indexName } + $db: *database0 \ No newline at end of file diff --git a/testdata/index-management/updateSearchIndex.json b/testdata/index-management/updateSearchIndex.json new file mode 100644 index 0000000000..a3edc1f16d --- /dev/null +++ b/testdata/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" + } + } + } + ] + } + ] + } + ] + } \ No newline at end of file diff --git a/testdata/index-management/updateSearchIndex.yml b/testdata/index-management/updateSearchIndex.yml new file mode 100644 index 0000000000..748b7ef61b --- /dev/null +++ b/testdata/index-management/updateSearchIndex.yml @@ -0,0 +1,43 @@ +description: "updateSearchIndex" +schemaVersion: "1.4" +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: + - commandStartedEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: *database0 + - collection: + id: &collection0 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: &indexName 'test index' + definition: &definition {} + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + updateSearchIndex: *collection0 + name: *indexName + definition: *definition + $db: *database0 From a154b3afc0c946fd5d269f835f872ebb408e17d7 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 13 Jul 2023 10:49:29 -0400 Subject: [PATCH 02/11] GODRIVER-2859 Add search index management helpers. --- bson/raw.go | 16 +- mongo/collection.go | 7 + .../unified/collection_operation_execution.go | 173 ++++++++++++ mongo/integration/unified/operation.go | 14 +- .../integration/unified/unified_spec_test.go | 1 + mongo/options/searchindexoptions.go | 24 ++ mongo/search_index_view.go | 251 ++++++++++++++++++ .../{createIndexes.go => create_indexes.go} | 0 .../driver/operation/create_search_indexes.go | 245 +++++++++++++++++ x/mongo/driver/operation/drop_search_index.go | 227 ++++++++++++++++ .../driver/operation/update_search_index.go | 240 +++++++++++++++++ 11 files changed, 1191 insertions(+), 7 deletions(-) create mode 100644 mongo/options/searchindexoptions.go create mode 100644 mongo/search_index_view.go rename x/mongo/driver/operation/{createIndexes.go => create_indexes.go} (100%) create mode 100644 x/mongo/driver/operation/create_search_indexes.go create mode 100644 x/mongo/driver/operation/drop_search_index.go create mode 100644 x/mongo/driver/operation/update_search_index.go diff --git a/bson/raw.go b/bson/raw.go index fe990a1771..773080c876 100644 --- a/bson/raw.go +++ b/bson/raw.go @@ -60,12 +60,18 @@ func (r Raw) LookupErr(key ...string) (RawValue, error) { // elements. If the document is not valid, the elements up to the invalid point will be returned // along with an error. func (r Raw) Elements() ([]RawElement, error) { - elems, err := bsoncore.Document(r).Elements() - relems := make([]RawElement, 0, len(elems)) - for _, elem := range elems { - relems = append(relems, RawElement(elem)) + var relems []RawElement + if doc := bsoncore.Document(r); len(doc) > 0 { + elems, err := doc.Elements() + if err != nil { + return relems, err + } + relems = make([]RawElement, 0, len(elems)) + for _, elem := range elems { + relems = append(relems, RawElement(elem)) + } } - return relems, err + return relems, nil } // Values returns this document as a slice of values. The returned slice will contain valid values. diff --git a/mongo/collection.go b/mongo/collection.go index 1e696ded96..218c6d702a 100644 --- a/mongo/collection.go +++ b/mongo/collection.go @@ -1767,6 +1767,13 @@ func (coll *Collection) Indexes() IndexView { return IndexView{coll: coll} } +// SearchIndexes returns a SearchIndexView instance that can be used to perform operations on the search indexes for the collection. +func (coll *Collection) SearchIndexes() SearchIndexView { + return SearchIndexView{ + coll: coll, + } +} + // Drop drops the collection on the server. This method ignores "namespace not found" errors so it is safe to drop // a collection that does not exist on the server. func (coll *Collection) Drop(ctx context.Context) error { diff --git a/mongo/integration/unified/collection_operation_execution.go b/mongo/integration/unified/collection_operation_execution.go index d41c0da8cc..14de61130b 100644 --- a/mongo/integration/unified/collection_operation_execution.go +++ b/mongo/integration/unified/collection_operation_execution.go @@ -304,6 +304,80 @@ func executeCreateIndex(ctx context.Context, operation *operation) (*operationRe return newValueResult(bsontype.String, bsoncore.AppendString(nil, name), err), nil } +func executeCreateSearchIndex(ctx context.Context, operation *operation) (*operationResult, error) { + coll, err := entities(ctx).collection(operation.Object) + if err != nil { + return nil, err + } + + var model mongo.SearchIndexModel + + elems, err := operation.Arguments.Elements() + if err != nil { + return nil, err + } + for _, elem := range elems { + key := elem.Key() + val := elem.Value() + + switch key { + case "model": + err = bson.Unmarshal(val.Document(), &model) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unrecognized createSearchIndex option %q", key) + } + } + + name, err := coll.SearchIndexes().CreateOne(ctx, model) + return newValueResult(bsontype.String, bsoncore.AppendString(nil, name), err), nil +} + +func executeCreateSearchIndexes(ctx context.Context, operation *operation) (*operationResult, error) { + coll, err := entities(ctx).collection(operation.Object) + if err != nil { + return nil, err + } + + var models []mongo.SearchIndexModel + + elems, err := operation.Arguments.Elements() + if err != nil { + return nil, err + } + for _, elem := range elems { + key := elem.Key() + val := elem.Value() + + switch key { + case "models": + vals, err := val.Array().Values() + if err != nil { + return nil, err + } + for _, val := range vals { + var model mongo.SearchIndexModel + err = bson.Unmarshal(val.Value, &model) + if err != nil { + return nil, err + } + models = append(models, model) + } + default: + return nil, fmt.Errorf("unrecognized createSearchIndexes option %q", key) + } + } + + names, err := coll.SearchIndexes().CreateMany(ctx, models) + builder := bsoncore.NewArrayBuilder() + for _, name := range names { + builder.AppendString(name) + } + return newValueResult(bsontype.Array, builder.Build(), err), nil +} + func executeDeleteOne(ctx context.Context, operation *operation) (*operationResult, error) { coll, err := entities(ctx).collection(operation.Object) if err != nil { @@ -522,6 +596,34 @@ func executeDropIndexes(ctx context.Context, operation *operation) (*operationRe return newDocumentResult(res, err), nil } +func executeDropSearchIndex(ctx context.Context, operation *operation) (*operationResult, error) { + coll, err := entities(ctx).collection(operation.Object) + if err != nil { + return nil, err + } + + var name string + + elems, err := operation.Arguments.Elements() + if err != nil { + return nil, err + } + for _, elem := range elems { + key := elem.Key() + val := elem.Value() + + switch key { + case "name": + name = val.StringValue() + default: + return nil, fmt.Errorf("unrecognized dropSearchIndex option %q", key) + } + } + + err = coll.SearchIndexes().DropOne(ctx, name) + return newValueResult(bsontype.Null, nil, err), nil +} + func executeEstimatedDocumentCount(ctx context.Context, operation *operation) (*operationResult, error) { coll, err := entities(ctx).collection(operation.Object) if err != nil { @@ -1009,6 +1111,43 @@ func executeListIndexes(ctx context.Context, operation *operation) (*operationRe return newCursorResult(docs), nil } +func executeListSearchIndexes(ctx context.Context, operation *operation) (*operationResult, error) { + coll, err := entities(ctx).collection(operation.Object) + if err != nil { + return nil, err + } + + var name *string + var opts []*options.AggregateOptions + + elems, err := operation.Arguments.Elements() + if err != nil { + return nil, err + } + for _, elem := range elems { + key := elem.Key() + val := elem.Value() + + switch key { + case "name": + n := val.StringValue() + name = &n + case "aggregationOptions": + var opt options.AggregateOptions + err = bson.Unmarshal(val.Document(), &opt) + if err != nil { + return nil, err + } + opts = append(opts, &opt) + default: + return nil, fmt.Errorf("unrecognized listSearchIndexes option %q", key) + } + } + + _, err = coll.SearchIndexes().List(ctx, name, opts) + return newValueResult(bsontype.Null, nil, err), nil +} + func executeRenameCollection(ctx context.Context, operation *operation) (*operationResult, error) { coll, err := entities(ctx).collection(operation.Object) if err != nil { @@ -1145,6 +1284,40 @@ func executeUpdateMany(ctx context.Context, operation *operation) (*operationRes return newDocumentResult(raw, err), nil } +func executeUpdateSearchIndex(ctx context.Context, operation *operation) (*operationResult, error) { + coll, err := entities(ctx).collection(operation.Object) + if err != nil { + return nil, err + } + + var name string + var definition interface{} + + elems, err := operation.Arguments.Elements() + if err != nil { + return nil, err + } + for _, elem := range elems { + key := elem.Key() + val := elem.Value() + + switch key { + case "name": + name = val.StringValue() + case "definition": + err = bson.Unmarshal(val.Value, &definition) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unrecognized updateSearchIndex option %q", key) + } + } + + err = coll.SearchIndexes().UpdateOne(ctx, name, definition) + return newValueResult(bsontype.Null, nil, err), nil +} + func buildUpdateResultDocument(res *mongo.UpdateResult) (bsoncore.Document, error) { if res == nil { return emptyCoreDocument, nil diff --git a/mongo/integration/unified/operation.go b/mongo/integration/unified/operation.go index 4034543e14..6a4e323482 100644 --- a/mongo/integration/unified/operation.go +++ b/mongo/integration/unified/operation.go @@ -146,10 +146,14 @@ func (op *operation) run(ctx context.Context, loopDone <-chan struct{}) (*operat return executeBulkWrite(ctx, op) case "countDocuments": return executeCountDocuments(ctx, op) - case "createIndex": - return executeCreateIndex(ctx, op) case "createFindCursor": return executeCreateFindCursor(ctx, op) + case "createIndex": + return executeCreateIndex(ctx, op) + case "createSearchIndex": + return executeCreateSearchIndex(ctx, op) + case "createSearchIndexes": + return executeCreateSearchIndexes(ctx, op) case "deleteOne": return executeDeleteOne(ctx, op) case "deleteMany": @@ -160,6 +164,8 @@ func (op *operation) run(ctx context.Context, loopDone <-chan struct{}) (*operat return executeDropIndex(ctx, op) case "dropIndexes": return executeDropIndexes(ctx, op) + case "dropSearchIndex": + return executeDropSearchIndex(ctx, op) case "estimatedDocumentCount": return executeEstimatedDocumentCount(ctx, op) case "find": @@ -178,6 +184,8 @@ func (op *operation) run(ctx context.Context, loopDone <-chan struct{}) (*operat return executeInsertOne(ctx, op) case "listIndexes": return executeListIndexes(ctx, op) + case "listSearchIndexes": + return executeListSearchIndexes(ctx, op) case "rename": // "rename" can either target a collection or a GridFS bucket. if _, err := entities(ctx).collection(op.Object); err == nil { @@ -193,6 +201,8 @@ func (op *operation) run(ctx context.Context, loopDone <-chan struct{}) (*operat return executeUpdateOne(ctx, op) case "updateMany": return executeUpdateMany(ctx, op) + case "updateSearchIndex": + return executeUpdateSearchIndex(ctx, op) // GridFS operations case "delete": diff --git a/mongo/integration/unified/unified_spec_test.go b/mongo/integration/unified/unified_spec_test.go index edfa481255..66d848d970 100644 --- a/mongo/integration/unified/unified_spec_test.go +++ b/mongo/integration/unified/unified_spec_test.go @@ -29,6 +29,7 @@ var ( "client-side-encryption/unified", "client-side-operations-timeout", "gridfs", + "index-management", } failDirectories = []string{ "unified-test-format/valid-fail", diff --git a/mongo/options/searchindexoptions.go b/mongo/options/searchindexoptions.go new file mode 100644 index 0000000000..64e585c49f --- /dev/null +++ b/mongo/options/searchindexoptions.go @@ -0,0 +1,24 @@ +// Copyright (C) MongoDB, Inc. 2023-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package options + +// CreateSearchIndexesOptions represents options that can be used to configure a SearchIndexView.CreateOne or +// SearchIndexView.CreateMany operation. +type CreateSearchIndexesOptions struct { +} + +// ListSearchIndexesOptions represents options that can be used to configure a SearchIndexView.List operation. +type ListSearchIndexesOptions struct { +} + +// DropSearchIndexOptions represents options that can be used to configure a SearchIndexView.DropOne operation. +type DropSearchIndexOptions struct { +} + +// UpdateSearchIndexOptions represents options that can be used to configure a SearchIndexView.UpdateOne operation. +type UpdateSearchIndexOptions struct { +} diff --git a/mongo/search_index_view.go b/mongo/search_index_view.go new file mode 100644 index 0000000000..d7a65c7ce8 --- /dev/null +++ b/mongo/search_index_view.go @@ -0,0 +1,251 @@ +// Copyright (C) MongoDB, Inc. 2023-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package mongo + +import ( + "context" + "fmt" + "strconv" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/writeconcern" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" + "go.mongodb.org/mongo-driver/x/mongo/driver/operation" + "go.mongodb.org/mongo-driver/x/mongo/driver/session" +) + +// SearchIndexView is a type that can be used to create, drop, list and update search indexes on a collection. A SearchIndexView for +// a collection can be created by a call to Collection.SearchIndexes(). +type SearchIndexView struct { + coll *Collection +} + +// SearchIndexModel represents a new search index to be created. +type SearchIndexModel struct { + // A document describing the definition for the search index. It cannot be nil. + // See https://www.mongodb.com/docs/atlas/atlas-search/create-index/ for reference. + Definition interface{} + + // The name for the index, if present. + Name *string +} + +// List executes a listSearchIndexes command and returns a cursor over the search indexes in the collection. +// +// The aggregateOpts parameter is the aggregation options. +// +// The opts parameter can be used to specify options for this operation (see the options.ListSearchIndexesOptions +// documentation). +func (siv SearchIndexView) List(ctx context.Context, name *string, + aggregateOpts []*options.AggregateOptions, _ ...*options.ListSearchIndexesOptions) (*Cursor, error) { + if ctx == nil { + ctx = context.Background() + } + + var index bson.D + if name != nil && len(*name) > 0 { + index = bson.D{{"name", *name}} + } else { + index = bson.D{} + } + + return siv.coll.Aggregate(ctx, Pipeline{{{"$listSearchIndexes", index}}}, aggregateOpts...) +} + +// CreateOne executes a createSearchIndexes command to create a search index on the collection and returns the name of the new +// search index. See the SearchIndexView.CreateMany documentation for more information and an example. +func (siv SearchIndexView) CreateOne(ctx context.Context, model SearchIndexModel, opts ...*options.CreateSearchIndexesOptions) (string, error) { + names, err := siv.CreateMany(ctx, []SearchIndexModel{model}, opts...) + if err != nil { + return "", err + } + + return names[0], nil +} + +// CreateMany executes a createSearchIndexes command to create multiple search indexes on the collection and returns +// the names of the new search indexes. +// +// For each SearchIndexModel in the models parameter, the index name can be specified. +// +// The opts parameter can be used to specify options for this operation (see the options.CreateSearchIndexesOptions +// documentation). +func (siv SearchIndexView) CreateMany(ctx context.Context, models []SearchIndexModel, _ ...*options.CreateSearchIndexesOptions) ([]string, error) { + var indexes bsoncore.Document + aidx, indexes := bsoncore.AppendArrayStart(indexes) + + for i, model := range models { + if model.Definition == nil { + return nil, fmt.Errorf("search index model definition cannot be nil") + } + + definition, err := marshal(model.Definition, siv.coll.bsonOpts, siv.coll.registry) + if err != nil { + return nil, err + } + + var iidx int32 + iidx, indexes = bsoncore.AppendDocumentElementStart(indexes, strconv.Itoa(i)) + if model.Name != nil && len(*model.Name) > 0 { + indexes = bsoncore.AppendStringElement(indexes, "name", *model.Name) + } + indexes = bsoncore.AppendDocumentElement(indexes, "definition", definition) + + indexes, err = bsoncore.AppendDocumentEnd(indexes, iidx) + if err != nil { + return nil, err + } + } + + indexes, err := bsoncore.AppendArrayEnd(indexes, aidx) + if err != nil { + return nil, err + } + + sess := sessionFromContext(ctx) + + if sess == nil && siv.coll.client.sessionPool != nil { + sess = session.NewImplicitClientSession(siv.coll.client.sessionPool, siv.coll.client.id) + defer sess.EndSession() + } + + err = siv.coll.client.validSession(sess) + if err != nil { + return nil, err + } + + wc := siv.coll.writeConcern + if sess.TransactionRunning() { + wc = nil + } + if !writeconcern.AckWrite(wc) { + sess = nil + } + + selector := makePinnedSelector(sess, siv.coll.writeSelector) + + op := operation.NewCreateSearchIndexes(indexes). + Session(sess).WriteConcern(wc).ClusterClock(siv.coll.client.clock). + Database(siv.coll.db.name).Collection(siv.coll.name).CommandMonitor(siv.coll.client.monitor). + Deployment(siv.coll.client.deployment).ServerSelector(selector).ServerAPI(siv.coll.client.serverAPI). + Timeout(siv.coll.client.timeout) + + err = op.Execute(ctx) + if err != nil { + _, err = processWriteError(err) + return nil, err + } + + indexesCreated := op.Result().IndexesCreated + names := make([]string, 0, len(indexesCreated)) + for _, index := range indexesCreated { + names = append(names, index.Name) + } + + return names, nil +} + +// DropOne executes a dropSearchIndexes operation to drop a search index on the collection. +// +// The name parameter should be the name of the search index to drop. If the name is "*", ErrMultipleIndexDrop will be returned +// without running the command because doing so would drop all search indexes. +// +// The opts parameter can be used to specify options for this operation (see the options.DropSearchIndexOptions +// documentation). +func (siv SearchIndexView) DropOne(ctx context.Context, name string, _ ...*options.DropSearchIndexOptions) error { + if name == "*" { + return ErrMultipleIndexDrop + } + + if ctx == nil { + ctx = context.Background() + } + + sess := sessionFromContext(ctx) + if sess == nil && siv.coll.client.sessionPool != nil { + sess = session.NewImplicitClientSession(siv.coll.client.sessionPool, siv.coll.client.id) + defer sess.EndSession() + } + + err := siv.coll.client.validSession(sess) + if err != nil { + return err + } + + wc := siv.coll.writeConcern + if sess.TransactionRunning() { + wc = nil + } + if !writeconcern.AckWrite(wc) { + sess = nil + } + + selector := makePinnedSelector(sess, siv.coll.writeSelector) + + op := operation.NewDropSearchIndex(name). + Session(sess).WriteConcern(wc).CommandMonitor(siv.coll.client.monitor). + ServerSelector(selector).ClusterClock(siv.coll.client.clock). + Database(siv.coll.db.name).Collection(siv.coll.name). + Deployment(siv.coll.client.deployment).ServerAPI(siv.coll.client.serverAPI). + Timeout(siv.coll.client.timeout) + + return op.Execute(ctx) +} + +// UpdateOne executes a updateSearchIndex operation to update a search index on the collection. +// +// The name parameter should be the name of the search index to update. +// +// The definition parameter is a document describing the definition for the search index. It cannot be nil. +// +// The opts parameter can be used to specify options for this operation (see the options.UpdateSearchIndexOptions +// documentation). +func (siv SearchIndexView) UpdateOne(ctx context.Context, name string, definition interface{}, _ ...*options.UpdateSearchIndexOptions) error { + if definition == nil { + return fmt.Errorf("search index definition cannot be nil") + } + + indexDefinition, err := marshal(definition, siv.coll.bsonOpts, siv.coll.registry) + if err != nil { + return err + } + + if ctx == nil { + ctx = context.Background() + } + + sess := sessionFromContext(ctx) + if sess == nil && siv.coll.client.sessionPool != nil { + sess = session.NewImplicitClientSession(siv.coll.client.sessionPool, siv.coll.client.id) + defer sess.EndSession() + } + + err = siv.coll.client.validSession(sess) + if err != nil { + return err + } + + wc := siv.coll.writeConcern + if sess.TransactionRunning() { + wc = nil + } + if !writeconcern.AckWrite(wc) { + sess = nil + } + + selector := makePinnedSelector(sess, siv.coll.writeSelector) + + op := operation.NewUpdateSearchIndex(name, indexDefinition). + Session(sess).WriteConcern(wc).CommandMonitor(siv.coll.client.monitor). + ServerSelector(selector).ClusterClock(siv.coll.client.clock). + Database(siv.coll.db.name).Collection(siv.coll.name). + Deployment(siv.coll.client.deployment).ServerAPI(siv.coll.client.serverAPI). + Timeout(siv.coll.client.timeout) + + return op.Execute(ctx) +} diff --git a/x/mongo/driver/operation/createIndexes.go b/x/mongo/driver/operation/create_indexes.go similarity index 100% rename from x/mongo/driver/operation/createIndexes.go rename to x/mongo/driver/operation/create_indexes.go diff --git a/x/mongo/driver/operation/create_search_indexes.go b/x/mongo/driver/operation/create_search_indexes.go new file mode 100644 index 0000000000..a16f9d716b --- /dev/null +++ b/x/mongo/driver/operation/create_search_indexes.go @@ -0,0 +1,245 @@ +// Copyright (C) MongoDB, Inc. 2023-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package operation + +import ( + "context" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/event" + "go.mongodb.org/mongo-driver/mongo/description" + "go.mongodb.org/mongo-driver/mongo/writeconcern" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" + "go.mongodb.org/mongo-driver/x/mongo/driver" + "go.mongodb.org/mongo-driver/x/mongo/driver/session" +) + +// CreateSearchIndexes performs a createSearchIndexes operation. +type CreateSearchIndexes struct { + indexes bsoncore.Document + session *session.Client + clock *session.ClusterClock + collection string + monitor *event.CommandMonitor + crypt driver.Crypt + database string + deployment driver.Deployment + selector description.ServerSelector + writeConcern *writeconcern.WriteConcern + result CreateSearchIndexesResult + serverAPI *driver.ServerAPIOptions + timeout *time.Duration +} + +// CreateSearchIndexResult represents a single search index result in CreateSearchIndexesResult. +type CreateSearchIndexResult struct { + Name string +} + +// CreateSearchIndexesResult represents a createSearchIndexes result returned by the server. +type CreateSearchIndexesResult struct { + IndexesCreated []CreateSearchIndexResult +} + +func buildCreateSearchIndexesResult(response bsoncore.Document) (CreateSearchIndexesResult, error) { + elements, err := response.Elements() + if err != nil { + return CreateSearchIndexesResult{}, err + } + csir := CreateSearchIndexesResult{} + for _, element := range elements { + switch element.Key() { + case "indexesCreated": + arr, ok := element.Value().ArrayOK() + if !ok { + return csir, fmt.Errorf("response field 'indexesCreated' is type array, but received BSON type %s", element.Value().Type) + } + + var values []bsoncore.Value + values, err = arr.Values() + if err != nil { + break + } + + for _, val := range values { + valDoc, ok := val.DocumentOK() + if !ok { + return csir, fmt.Errorf("indexesCreated value is type document, but received BSON type %s", val.Type) + } + var indexesCreated CreateSearchIndexResult + if err = bson.Unmarshal(valDoc, &indexesCreated); err != nil { + return csir, err + } + csir.IndexesCreated = append(csir.IndexesCreated, indexesCreated) + } + } + } + return csir, nil +} + +// NewCreateSearchIndexes constructs and returns a new CreateSearchIndexes. +func NewCreateSearchIndexes(indexes bsoncore.Document) *CreateSearchIndexes { + return &CreateSearchIndexes{ + indexes: indexes, + } +} + +// Result returns the result of executing this operation. +func (csi *CreateSearchIndexes) Result() CreateSearchIndexesResult { return csi.result } + +func (csi *CreateSearchIndexes) processResponse(info driver.ResponseInfo) error { + var err error + csi.result, err = buildCreateSearchIndexesResult(info.ServerResponse) + return err +} + +// Execute runs this operations and returns an error if the operation did not execute successfully. +func (csi *CreateSearchIndexes) Execute(ctx context.Context) error { + if csi.deployment == nil { + return errors.New("the CreateSearchIndexes operation must have a Deployment set before Execute can be called") + } + + return driver.Operation{ + CommandFn: csi.command, + ProcessResponseFn: csi.processResponse, + CommandMonitor: csi.monitor, + Database: csi.database, + Deployment: csi.deployment, + }.Execute(ctx) + +} + +func (csi *CreateSearchIndexes) command(dst []byte, _ description.SelectedServer) ([]byte, error) { + dst = bsoncore.AppendStringElement(dst, "createSearchIndexes", csi.collection) + if csi.indexes != nil { + dst = bsoncore.AppendArrayElement(dst, "indexes", csi.indexes) + } + return dst, nil +} + +// Indexes specifies an array containing index specification documents for the indexes being created. +func (csi *CreateSearchIndexes) Indexes(indexes bsoncore.Document) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.indexes = indexes + return csi +} + +// Session sets the session for this operation. +func (csi *CreateSearchIndexes) Session(session *session.Client) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.session = session + return csi +} + +// ClusterClock sets the cluster clock for this operation. +func (csi *CreateSearchIndexes) ClusterClock(clock *session.ClusterClock) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.clock = clock + return csi +} + +// Collection sets the collection that this command will run against. +func (csi *CreateSearchIndexes) Collection(collection string) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.collection = collection + return csi +} + +// CommandMonitor sets the monitor to use for APM events. +func (csi *CreateSearchIndexes) CommandMonitor(monitor *event.CommandMonitor) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.monitor = monitor + return csi +} + +// Crypt sets the Crypt object to use for automatic encryption and decryption. +func (csi *CreateSearchIndexes) Crypt(crypt driver.Crypt) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.crypt = crypt + return csi +} + +// Database sets the database to run this operation against. +func (csi *CreateSearchIndexes) Database(database string) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.database = database + return csi +} + +// Deployment sets the deployment to use for this operation. +func (csi *CreateSearchIndexes) Deployment(deployment driver.Deployment) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.deployment = deployment + return csi +} + +// ServerSelector sets the selector used to retrieve a server. +func (csi *CreateSearchIndexes) ServerSelector(selector description.ServerSelector) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.selector = selector + return csi +} + +// WriteConcern sets the write concern for this operation. +func (csi *CreateSearchIndexes) WriteConcern(writeConcern *writeconcern.WriteConcern) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.writeConcern = writeConcern + return csi +} + +// ServerAPI sets the server API version for this operation. +func (csi *CreateSearchIndexes) ServerAPI(serverAPI *driver.ServerAPIOptions) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.serverAPI = serverAPI + return csi +} + +// Timeout sets the timeout for this operation. +func (csi *CreateSearchIndexes) Timeout(timeout *time.Duration) *CreateSearchIndexes { + if csi == nil { + csi = new(CreateSearchIndexes) + } + + csi.timeout = timeout + return csi +} diff --git a/x/mongo/driver/operation/drop_search_index.go b/x/mongo/driver/operation/drop_search_index.go new file mode 100644 index 0000000000..bf079b3ce9 --- /dev/null +++ b/x/mongo/driver/operation/drop_search_index.go @@ -0,0 +1,227 @@ +// Copyright (C) MongoDB, Inc. 2019-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package operation + +import ( + "context" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/event" + "go.mongodb.org/mongo-driver/mongo/description" + "go.mongodb.org/mongo-driver/mongo/writeconcern" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" + "go.mongodb.org/mongo-driver/x/mongo/driver" + "go.mongodb.org/mongo-driver/x/mongo/driver/session" +) + +// DropSearchIndex performs an dropSearchIndex operation. +type DropSearchIndex struct { + index string + session *session.Client + clock *session.ClusterClock + collection string + monitor *event.CommandMonitor + crypt driver.Crypt + database string + deployment driver.Deployment + selector description.ServerSelector + writeConcern *writeconcern.WriteConcern + result DropSearchIndexResult + serverAPI *driver.ServerAPIOptions + timeout *time.Duration +} + +// DropSearchIndexResult represents a dropSearchIndex result returned by the server. +type DropSearchIndexResult struct { + Ok int32 +} + +func buildDropSearchIndexResult(response bsoncore.Document) (DropSearchIndexResult, error) { + elements, err := response.Elements() + if err != nil { + return DropSearchIndexResult{}, err + } + dsir := DropSearchIndexResult{} + for _, element := range elements { + switch element.Key() { + case "ok": + var ok bool + dsir.Ok, ok = element.Value().AsInt32OK() + if !ok { + return dsir, fmt.Errorf("response field 'ok' is type int32, but received BSON type %s", element.Value().Type) + } + } + } + return dsir, nil +} + +// NewDropSearchIndex constructs and returns a new DropSearchIndex. +func NewDropSearchIndex(index string) *DropSearchIndex { + return &DropSearchIndex{ + index: index, + } +} + +// Result returns the result of executing this operation. +func (dsi *DropSearchIndex) Result() DropSearchIndexResult { return dsi.result } + +func (dsi *DropSearchIndex) processResponse(info driver.ResponseInfo) error { + var err error + dsi.result, err = buildDropSearchIndexResult(info.ServerResponse) + return err +} + +// Execute runs this operations and returns an error if the operation did not execute successfully. +func (dsi *DropSearchIndex) Execute(ctx context.Context) error { + if dsi.deployment == nil { + return errors.New("the DropSearchIndex operation must have a Deployment set before Execute can be called") + } + + return driver.Operation{ + CommandFn: dsi.command, + ProcessResponseFn: dsi.processResponse, + Client: dsi.session, + Clock: dsi.clock, + CommandMonitor: dsi.monitor, + Crypt: dsi.crypt, + Database: dsi.database, + Deployment: dsi.deployment, + Selector: dsi.selector, + WriteConcern: dsi.writeConcern, + ServerAPI: dsi.serverAPI, + Timeout: dsi.timeout, + }.Execute(ctx) + +} + +func (dsi *DropSearchIndex) command(dst []byte, _ description.SelectedServer) ([]byte, error) { + dst = bsoncore.AppendStringElement(dst, "dropSearchIndex", dsi.collection) + dst = bsoncore.AppendStringElement(dst, "name", dsi.index) + return dst, nil +} + +// Index specifies the name of the index to drop. If '*' is specified, all indexes will be dropped. +func (dsi *DropSearchIndex) Index(index string) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.index = index + return dsi +} + +// Session sets the session for this operation. +func (dsi *DropSearchIndex) Session(session *session.Client) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.session = session + return dsi +} + +// ClusterClock sets the cluster clock for this operation. +func (dsi *DropSearchIndex) ClusterClock(clock *session.ClusterClock) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.clock = clock + return dsi +} + +// Collection sets the collection that this command will run against. +func (dsi *DropSearchIndex) Collection(collection string) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.collection = collection + return dsi +} + +// CommandMonitor sets the monitor to use for APM events. +func (dsi *DropSearchIndex) CommandMonitor(monitor *event.CommandMonitor) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.monitor = monitor + return dsi +} + +// Crypt sets the Crypt object to use for automatic encryption and decryption. +func (dsi *DropSearchIndex) Crypt(crypt driver.Crypt) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.crypt = crypt + return dsi +} + +// Database sets the database to run this operation against. +func (dsi *DropSearchIndex) Database(database string) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.database = database + return dsi +} + +// Deployment sets the deployment to use for this operation. +func (dsi *DropSearchIndex) Deployment(deployment driver.Deployment) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.deployment = deployment + return dsi +} + +// ServerSelector sets the selector used to retrieve a server. +func (dsi *DropSearchIndex) ServerSelector(selector description.ServerSelector) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.selector = selector + return dsi +} + +// WriteConcern sets the write concern for this operation. +func (dsi *DropSearchIndex) WriteConcern(writeConcern *writeconcern.WriteConcern) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.writeConcern = writeConcern + return dsi +} + +// ServerAPI sets the server API version for this operation. +func (dsi *DropSearchIndex) ServerAPI(serverAPI *driver.ServerAPIOptions) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.serverAPI = serverAPI + return dsi +} + +// Timeout sets the timeout for this operation. +func (dsi *DropSearchIndex) Timeout(timeout *time.Duration) *DropSearchIndex { + if dsi == nil { + dsi = new(DropSearchIndex) + } + + dsi.timeout = timeout + return dsi +} diff --git a/x/mongo/driver/operation/update_search_index.go b/x/mongo/driver/operation/update_search_index.go new file mode 100644 index 0000000000..ba807986c9 --- /dev/null +++ b/x/mongo/driver/operation/update_search_index.go @@ -0,0 +1,240 @@ +// Copyright (C) MongoDB, Inc. 2023-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package operation + +import ( + "context" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/event" + "go.mongodb.org/mongo-driver/mongo/description" + "go.mongodb.org/mongo-driver/mongo/writeconcern" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" + "go.mongodb.org/mongo-driver/x/mongo/driver" + "go.mongodb.org/mongo-driver/x/mongo/driver/session" +) + +// UpdateSearchIndex performs a updateSearchIndex operation. +type UpdateSearchIndex struct { + index string + definition bsoncore.Document + session *session.Client + clock *session.ClusterClock + collection string + monitor *event.CommandMonitor + crypt driver.Crypt + database string + deployment driver.Deployment + selector description.ServerSelector + writeConcern *writeconcern.WriteConcern + result UpdateSearchIndexResult + serverAPI *driver.ServerAPIOptions + timeout *time.Duration +} + +// UpdateSearchIndexResult represents a single index in the updateSearchIndexResult result. +type UpdateSearchIndexResult struct { + Ok int32 +} + +func buildUpdateSearchIndexResult(response bsoncore.Document) (UpdateSearchIndexResult, error) { + elements, err := response.Elements() + if err != nil { + return UpdateSearchIndexResult{}, err + } + usir := UpdateSearchIndexResult{} + for _, element := range elements { + switch element.Key() { + case "ok": + var ok bool + usir.Ok, ok = element.Value().AsInt32OK() + if !ok { + return usir, fmt.Errorf("response field 'ok' is type int32, but received BSON type %s", element.Value().Type) + } + } + } + return usir, nil +} + +// NewUpdateSearchIndex constructs and returns a new UpdateSearchIndex. +func NewUpdateSearchIndex(index string, definition bsoncore.Document) *UpdateSearchIndex { + return &UpdateSearchIndex{ + index: index, + definition: definition, + } +} + +// Result returns the result of executing this operation. +func (usi *UpdateSearchIndex) Result() UpdateSearchIndexResult { return usi.result } + +func (usi *UpdateSearchIndex) processResponse(info driver.ResponseInfo) error { + var err error + usi.result, err = buildUpdateSearchIndexResult(info.ServerResponse) + return err +} + +// Execute runs this operations and returns an error if the operation did not execute successfully. +func (usi *UpdateSearchIndex) Execute(ctx context.Context) error { + if usi.deployment == nil { + return errors.New("the UpdateSearchIndex operation must have a Deployment set before Execute can be called") + } + + return driver.Operation{ + CommandFn: usi.command, + ProcessResponseFn: usi.processResponse, + Client: usi.session, + Clock: usi.clock, + CommandMonitor: usi.monitor, + Crypt: usi.crypt, + Database: usi.database, + Deployment: usi.deployment, + Selector: usi.selector, + WriteConcern: usi.writeConcern, + ServerAPI: usi.serverAPI, + Timeout: usi.timeout, + }.Execute(ctx) + +} + +func (usi *UpdateSearchIndex) command(dst []byte, _ description.SelectedServer) ([]byte, error) { + dst = bsoncore.AppendStringElement(dst, "updateSearchIndex", usi.collection) + dst = bsoncore.AppendStringElement(dst, "name", usi.index) + dst = bsoncore.AppendDocumentElement(dst, "definition", usi.definition) + return dst, nil +} + +// Index specifies the index of the document being updated. +func (usi *UpdateSearchIndex) Index(name string) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.index = name + return usi +} + +// Definition specifies the definition for the document being created. +func (usi *UpdateSearchIndex) Definition(definition bsoncore.Document) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.definition = definition + return usi +} + +// Session sets the session for this operation. +func (usi *UpdateSearchIndex) Session(session *session.Client) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.session = session + return usi +} + +// ClusterClock sets the cluster clock for this operation. +func (usi *UpdateSearchIndex) ClusterClock(clock *session.ClusterClock) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.clock = clock + return usi +} + +// Collection sets the collection that this command will run against. +func (usi *UpdateSearchIndex) Collection(collection string) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.collection = collection + return usi +} + +// CommandMonitor sets the monitor to use for APM events. +func (usi *UpdateSearchIndex) CommandMonitor(monitor *event.CommandMonitor) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.monitor = monitor + return usi +} + +// Crypt sets the Crypt object to use for automatic encryption and decryption. +func (usi *UpdateSearchIndex) Crypt(crypt driver.Crypt) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.crypt = crypt + return usi +} + +// Database sets the database to run this operation against. +func (usi *UpdateSearchIndex) Database(database string) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.database = database + return usi +} + +// Deployment sets the deployment to use for this operation. +func (usi *UpdateSearchIndex) Deployment(deployment driver.Deployment) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.deployment = deployment + return usi +} + +// ServerSelector sets the selector used to retrieve a server. +func (usi *UpdateSearchIndex) ServerSelector(selector description.ServerSelector) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.selector = selector + return usi +} + +// WriteConcern sets the write concern for this operation. +func (usi *UpdateSearchIndex) WriteConcern(writeConcern *writeconcern.WriteConcern) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.writeConcern = writeConcern + return usi +} + +// ServerAPI sets the server API version for this operation. +func (usi *UpdateSearchIndex) ServerAPI(serverAPI *driver.ServerAPIOptions) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.serverAPI = serverAPI + return usi +} + +// Timeout sets the timeout for this operation. +func (usi *UpdateSearchIndex) Timeout(timeout *time.Duration) *UpdateSearchIndex { + if usi == nil { + usi = new(UpdateSearchIndex) + } + + usi.timeout = timeout + return usi +} From a8fa83a36efb1e3130aedbc1f5d9a8bfff0c9cdf Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 10 Aug 2023 19:20:50 -0400 Subject: [PATCH 03/11] GODRIVER-2875 Add e2e testing. --- .evergreen/config.yml | 63 ++++ Makefile | 4 + mongo/search_index_prose_test.go | 238 ++++++++++++++ mongo/search_index_view.go | 7 +- .../index-management/createSearchIndex.json | 240 +++++++------- .../index-management/createSearchIndex.yml | 10 +- .../index-management/createSearchIndexes.json | 299 +++++++++--------- .../index-management/createSearchIndexes.yml | 15 +- .../index-management/dropSearchIndex.json | 135 ++++---- testdata/index-management/dropSearchIndex.yml | 7 +- .../index-management/listSearchIndexes.json | 277 ++++++++-------- .../index-management/listSearchIndexes.yml | 15 +- .../index-management/updateSearchIndex.json | 139 ++++---- .../index-management/updateSearchIndex.yml | 9 +- 14 files changed, 894 insertions(+), 564 deletions(-) create mode 100644 mongo/search_index_prose_test.go diff --git a/.evergreen/config.yml b/.evergreen/config.yml index ab6289576a..f3f9d615ff 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -706,6 +706,17 @@ functions: params: file: lb-expansion.yml + run-search-index-tests: + - command: shell.exec + type: test + params: + shell: "bash" + working_dir: src/go.mongodb.org/mongo-driver + script: | + ${PREPARE_SHELL} + TEST_INDEX_URI="${TEST_INDEX_URI}" \ + make evg-test-search-index + stop-load-balancer: - command: shell.exec params: @@ -2271,6 +2282,14 @@ tasks: ${PREPARE_SHELL} ./.evergreen/run-deployed-lambda-aws-tests.sh + - name: "test-search-index" + commands: + - func: "bootstrap-mongo-orchestration" + vars: + VERSION: "latest" + TOPOLOGY: "replica_set" + - func: "run-search-index-tests" + axes: - id: version display_name: MongoDB Version @@ -2628,6 +2647,44 @@ task_groups: tasks: - test-aws-lambda-deployed + - name: test-search-index-task-group + setup_group: + - func: fetch-source + - func: prepare-resources + - command: subprocess.exec + params: + working_dir: src/go.mongodb.org/mongo-driver + binary: bash + add_expansions_to_env: true + env: + MONGODB_VERSION: "7.0" + args: + - ${DRIVERS_TOOLS}/.evergreen/atlas/setup-atlas-cluster.sh + - command: expansions.update + params: + file: src/go.mongodb.org/mongo-driver/atlas-expansion.yml + - command: shell.exec + params: + working_dir: src/go.mongodb.org/mongo-driver + shell: bash + script: |- + echo "TEST_INDEX_URI: ${MONGODB_URI}" > atlas-expansion.yml + - command: expansions.update + params: + file: src/go.mongodb.org/mongo-driver/atlas-expansion.yml + teardown_group: + - command: subprocess.exec + params: + working_dir: src/go.mongodb.org/mongo-driver + binary: bash + add_expansions_to_env: true + args: + - ${DRIVERS_TOOLS}/.evergreen/atlas/teardown-atlas-cluster.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - test-search-index + buildvariants: - name: static-analysis display_name: "Static Analysis" @@ -2777,6 +2834,12 @@ buildvariants: tasks: - test-aws-lambda-task-group + - matrix_name: "searchindex-test" + matrix_spec: { version: ["7.0"], os-faas-80: ["rhel80-large-go-1-20"] } + display_name: "Search Index ${version} ${os-faas-80}" + tasks: + - test-search-index-task-group + - name: testgcpkms-variant display_name: "GCP KMS" run_on: diff --git a/Makefile b/Makefile index f3d3b88292..8c7b494e93 100644 --- a/Makefile +++ b/Makefile @@ -160,6 +160,10 @@ evg-test-load-balancers: go test $(BUILD_TAGS) ./mongo/integration -run TestLoadBalancerSupport -v -timeout $(TEST_TIMEOUT)s >> test.suite go test $(BUILD_TAGS) ./mongo/integration/unified -run TestUnifiedSpec -v -timeout $(TEST_TIMEOUT)s >> test.suite +.PHONY: evg-test-search-index +evg-test-search-index: + go test ./mongo -run TestSearchIndexProse -v -timeout $(TEST_TIMEOUT)s + .PHONY: evg-test-ocsp evg-test-ocsp: go test -v ./mongo -run TestOCSP $(OCSP_TLS_SHOULD_SUCCEED) >> test.suite diff --git a/mongo/search_index_prose_test.go b/mongo/search_index_prose_test.go new file mode 100644 index 0000000000..1b8731f599 --- /dev/null +++ b/mongo/search_index_prose_test.go @@ -0,0 +1,238 @@ +// Copyright (C) MongoDB, Inc. 2023-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package mongo + +import ( + "context" + "fmt" + "os" + "sync" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/internal/assert" + "go.mongodb.org/mongo-driver/internal/uuid" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func newTestCollection(t *testing.T, client *Client) *Collection { + t.Helper() + + id, err := uuid.New() + assert.NoError(t, err) + collName := fmt.Sprintf("col%s", id.String()) + return client.Database("test").Collection(collName) +} + +func getDocument(t *testing.T, ctx context.Context, view SearchIndexView, index string) bson.Raw { + t.Helper() + + for { + cursor, err := view.List(ctx, &index, nil) + assert.NoError(t, err, "failed to list") + + if !cursor.Next(ctx) { + return nil + } + if cursor.Current.Lookup("queryable").Boolean() { + return cursor.Current + } + t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } +} + +func TestSearchIndexProse(t *testing.T) { + const timeout = 5 * time.Minute + + ctx := context.Background() + + uri := os.Getenv("TEST_INDEX_URI") + if uri == "" { + t.Skip("skipping") + } + clientOptions := options.Client().ApplyURI(uri).SetTimeout(timeout) + + client, err := Connect(ctx, clientOptions) + assert.NoError(t, err, "failed to connect %s", uri) + defer client.Disconnect(ctx) + + t.Run("case 1: Driver can successfully create and list search indexes", func(t *testing.T) { + ctx := context.Background() + + collection := newTestCollection(t, client) + defer collection.Drop(ctx) + + _, err = collection.InsertOne(ctx, bson.D{}) + assert.NoError(t, err, "failed to insert") + + view := collection.SearchIndexes() + + definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} + searchName := "test-search-index" + model := SearchIndexModel{ + Definition: definition, + Name: &searchName, + } + index, err := view.CreateOne(ctx, model) + assert.NoError(t, err, "failed to create index") + assert.Equal(t, searchName, index, "unmatched name") + + doc := getDocument(t, ctx, view, searchName) + assert.NotNil(t, doc, "got empty document") + assert.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") + expected, err := bson.Marshal(definition) + assert.NoError(t, err, "failed to marshal definition") + actual := doc.Lookup("latestDefinition").Value + assert.Equal(t, expected, actual, "unmatched definition") + }) + + t.Run("case 2: Driver can successfully create multiple indexes in batch", func(t *testing.T) { + ctx := context.Background() + + collection := newTestCollection(t, client) + defer collection.Drop(ctx) + + _, err = collection.InsertOne(ctx, bson.D{}) + assert.NoError(t, err, "failed to insert") + + view := collection.SearchIndexes() + + definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} + searchName0 := "test-search-index-1" + searchName1 := "test-search-index-2" + models := []SearchIndexModel{ + { + Definition: definition, + Name: &searchName0, + }, + { + Definition: definition, + Name: &searchName1, + }, + } + indexes, err := view.CreateMany(ctx, models) + assert.NoError(t, err, "failed to create index") + assert.Equal(t, len(indexes), 2, "expected 2 indexes") + assert.Contains(t, indexes, searchName0) + assert.Contains(t, indexes, searchName1) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + + doc := getDocument(t, ctx, view, searchName0) + assert.NotNil(t, doc, "got empty document") + assert.Equal(t, searchName0, doc.Lookup("name").StringValue(), "unmatched name") + expected, err := bson.Marshal(definition) + assert.NoError(t, err, "failed to marshal definition") + actual := doc.Lookup("latestDefinition").Value + assert.Equal(t, expected, actual, "unmatched definition") + }() + go func() { + defer wg.Done() + + doc := getDocument(t, ctx, view, searchName1) + assert.NotNil(t, doc, "got empty document") + assert.Equal(t, searchName1, doc.Lookup("name").StringValue(), "unmatched name") + expected, err := bson.Marshal(definition) + assert.NoError(t, err, "failed to marshal definition") + actual := doc.Lookup("latestDefinition").Value + assert.Equal(t, expected, actual, "unmatched definition") + }() + wg.Wait() + }) + + t.Run("case 3: Driver can successfully drop search indexes", func(t *testing.T) { + ctx := context.Background() + + collection := newTestCollection(t, client) + defer collection.Drop(ctx) + + _, err = collection.InsertOne(ctx, bson.D{}) + assert.NoError(t, err, "failed to insert") + + view := collection.SearchIndexes() + + definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} + searchName := "test-search-index" + model := SearchIndexModel{ + Definition: definition, + Name: &searchName, + } + index, err := view.CreateOne(ctx, model) + assert.NoError(t, err, "failed to create index") + assert.Equal(t, searchName, index, "unmatched name") + + doc := getDocument(t, ctx, view, searchName) + assert.NotNil(t, doc, "got empty document") + assert.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") + + err = view.DropOne(ctx, searchName) + assert.NoError(t, err, "failed to drop index") + for { + cursor, err := view.List(ctx, &index, nil) + assert.NoError(t, err, "failed to list") + + if !cursor.Next(ctx) { + break + } + t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } + }) + + t.Run("case 4: Driver can update a search index", func(t *testing.T) { + ctx := context.Background() + + collection := newTestCollection(t, client) + defer collection.Drop(ctx) + + _, err = collection.InsertOne(ctx, bson.D{}) + assert.NoError(t, err, "failed to insert") + + view := collection.SearchIndexes() + + definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} + searchName := "test-search-index" + model := SearchIndexModel{ + Definition: definition, + Name: &searchName, + } + index, err := view.CreateOne(ctx, model) + assert.NoError(t, err, "failed to create index") + assert.Equal(t, searchName, index, "unmatched name") + + doc := getDocument(t, ctx, view, searchName) + assert.NotNil(t, doc, "got empty document") + assert.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") + + definition = bson.D{{"mappings", bson.D{{"dynamic", true}}}} + err = view.UpdateOne(ctx, searchName, definition) + assert.NoError(t, err, "failed to drop index") + doc = getDocument(t, ctx, view, searchName) + assert.NotNil(t, doc, "got empty document") + assert.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") + assert.Equal(t, "READY", doc.Lookup("status").StringValue(), "unexpected status") + expected, err := bson.Marshal(definition) + assert.NoError(t, err, "failed to marshal definition") + actual := doc.Lookup("latestDefinition").Value + assert.Equal(t, expected, actual, "unmatched definition") + }) + + t.Run("case 5: dropSearchIndex suppresses namespace not found errors", func(t *testing.T) { + ctx := context.Background() + + collection := newTestCollection(t, client) + defer collection.Drop(ctx) + + err = collection.SearchIndexes().DropOne(ctx, "foo") + assert.NoError(t, err) + }) +} diff --git a/mongo/search_index_view.go b/mongo/search_index_view.go index d7a65c7ce8..4d0fa104ff 100644 --- a/mongo/search_index_view.go +++ b/mongo/search_index_view.go @@ -15,6 +15,7 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/writeconcern" "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" + "go.mongodb.org/mongo-driver/x/mongo/driver" "go.mongodb.org/mongo-driver/x/mongo/driver/operation" "go.mongodb.org/mongo-driver/x/mongo/driver/session" ) @@ -194,7 +195,11 @@ func (siv SearchIndexView) DropOne(ctx context.Context, name string, _ ...*optio Deployment(siv.coll.client.deployment).ServerAPI(siv.coll.client.serverAPI). Timeout(siv.coll.client.timeout) - return op.Execute(ctx) + err = op.Execute(ctx) + if de, ok := err.(driver.Error); ok && de.NamespaceNotFound() { + return nil + } + return err } // UpdateOne executes a updateSearchIndex operation to update a search index on the collection. diff --git a/testdata/index-management/createSearchIndex.json b/testdata/index-management/createSearchIndex.json index 07d7b3f666..7e27b6eb76 100644 --- a/testdata/index-management/createSearchIndex.json +++ b/testdata/index-management/createSearchIndex.json @@ -1,134 +1,136 @@ { - "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" - } + "description": "createSearchIndex", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" } - ], - "runOnRequirements": [ - { - "minServerVersion": "7.0.0", - "topologies": [ - "replicaset", - "load-balanced", - "sharded" - ], - "serverless": "forbid" + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" } - ], - "tests": [ - { - "description": "no name provided for an index definition", - "operations": [ - { - "name": "createSearchIndex", - "object": "collection0", - "arguments": { - "model": { - "definition": { - "mappings": { - "dynamic": true - } + } + ], + "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 } + }, + "expectError": { + "isError": true, + "errorContains": "Search index commands are only supported with Atlas" } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "createSearchIndexes": "collection0", - "indexes": [ - { - "definition": { - "mappings": { - "dynamic": true - } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [ + { + "definition": { + "mappings": { + "dynamic": true } } - ], - "$db": "database0" - } + } + ], + "$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 } + ] + } + ] + }, + { + "description": "name provided for an index definition", + "operations": [ + { + "name": "createSearchIndex", + "object": "collection0", + "arguments": { + "model": { + "definition": { + "mappings": { + "dynamic": true + } + }, + "name": "test index" + } + }, + "expectError": { + "isError": true, + "errorContains": "Search index commands are only supported with Atlas" } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "createSearchIndexes": "collection0", - "indexes": [ - { - "definition": { - "mappings": { - "dynamic": true - } - }, - "name": "test index" - } - ], - "$db": "database0" - } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [ + { + "definition": { + "mappings": { + "dynamic": true + } + }, + "name": "test index" + } + ], + "$db": "database0" } } - ] - } - ] - } - ] - } \ No newline at end of file + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/testdata/index-management/createSearchIndex.yml b/testdata/index-management/createSearchIndex.yml index 0eb5c1ab1d..6aa56f3bc4 100644 --- a/testdata/index-management/createSearchIndex.yml +++ b/testdata/index-management/createSearchIndex.yml @@ -28,9 +28,10 @@ tests: arguments: model: { definition: &definition { mappings: { dynamic: true } } } expectError: - # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing - # against an Atlas cluster and the expectError will be removed. + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. isError: true + errorContains: Search index commands are only supported with Atlas expectEvents: - client: *client0 events: @@ -47,9 +48,10 @@ tests: arguments: model: { definition: &definition { mappings: { dynamic: true } } , name: 'test index' } expectError: - # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing - # against an Atlas cluster and the expectError will be removed. + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. isError: true + errorContains: Search index commands are only supported with Atlas expectEvents: - client: *client0 events: diff --git a/testdata/index-management/createSearchIndexes.json b/testdata/index-management/createSearchIndexes.json index 86b200679a..558267ace0 100644 --- a/testdata/index-management/createSearchIndexes.json +++ b/testdata/index-management/createSearchIndexes.json @@ -1,169 +1,172 @@ { - "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" - } + "description": "createSearchIndexes", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] } - ], - "runOnRequirements": [ - { - "minServerVersion": "7.0.0", - "topologies": [ - "replicaset", - "load-balanced", - "sharded" - ], - "serverless": "forbid" + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" } - ], - "tests": [ - { - "description": "empty index definition array", - "operations": [ - { - "name": "createSearchIndexes", - "object": "collection0", - "arguments": { - "models": [] - }, - "expectError": { - "isError": true - } + }, + { + "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, + "errorContains": "Search index commands are only supported with Atlas" } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "createSearchIndexes": "collection0", - "indexes": [], - "$db": "database0" - } + } + ], + "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": [ + ] + } + ] + }, + { + "description": "no name provided for an index definition", + "operations": [ + { + "name": "createSearchIndexes", + "object": "collection0", + "arguments": { + "models": [ { - "commandStartedEvent": { - "command": { - "createSearchIndexes": "collection0", - "indexes": [ - { - "definition": { - "mappings": { - "dynamic": true - } - } - } - ], - "$db": "database0" + "definition": { + "mappings": { + "dynamic": true } } } ] + }, + "expectError": { + "isError": true, + "errorContains": "Search index commands are only supported with Atlas" } - ] - }, - { - "description": "name provided for an index definition", - "operations": [ - { - "name": "createSearchIndexes", - "object": "collection0", - "arguments": { - "models": [ - { - "definition": { - "mappings": { - "dynamic": true + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [ + { + "definition": { + "mappings": { + "dynamic": true + } + } } - }, - "name": "test index" + ], + "$db": "database0" } - ] - }, - "expectError": { - "isError": true + } } - } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ + ] + } + ] + }, + { + "description": "name provided for an index definition", + "operations": [ + { + "name": "createSearchIndexes", + "object": "collection0", + "arguments": { + "models": [ { - "commandStartedEvent": { - "command": { - "createSearchIndexes": "collection0", - "indexes": [ - { - "definition": { - "mappings": { - "dynamic": true - } - }, - "name": "test index" - } - ], - "$db": "database0" + "definition": { + "mappings": { + "dynamic": true } - } + }, + "name": "test index" } ] + }, + "expectError": { + "isError": true, + "errorContains": "Search index commands are only supported with Atlas" } - ] - } - ] - } \ No newline at end of file + } + ], + "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/testdata/index-management/createSearchIndexes.yml b/testdata/index-management/createSearchIndexes.yml index dc01c1b166..54a6e84ccb 100644 --- a/testdata/index-management/createSearchIndexes.yml +++ b/testdata/index-management/createSearchIndexes.yml @@ -28,9 +28,10 @@ tests: arguments: models: [] expectError: - # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing - # against an Atlas cluster and the expectError will be removed. + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. isError: true + errorContains: Search index commands are only supported with Atlas expectEvents: - client: *client0 events: @@ -48,9 +49,10 @@ tests: arguments: models: [ { definition: &definition { mappings: { dynamic: true } } } ] expectError: - # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing - # against an Atlas cluster and the expectError will be removed. + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. isError: true + errorContains: Search index commands are only supported with Atlas expectEvents: - client: *client0 events: @@ -67,9 +69,10 @@ tests: arguments: models: [ { definition: &definition { mappings: { dynamic: true } } , name: 'test index' } ] expectError: - # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing - # against an Atlas cluster and the expectError will be removed. + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. isError: true + errorContains: Search index commands are only supported with Atlas expectEvents: - client: *client0 events: diff --git a/testdata/index-management/dropSearchIndex.json b/testdata/index-management/dropSearchIndex.json index 0b34a6ff45..9d716b5a58 100644 --- a/testdata/index-management/dropSearchIndex.json +++ b/testdata/index-management/dropSearchIndex.json @@ -1,73 +1,74 @@ { - "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" - } + "description": "dropSearchIndex", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] } - ], - "runOnRequirements": [ - { - "minServerVersion": "7.0.0", - "topologies": [ - "replicaset", - "load-balanced", - "sharded" - ], - "serverless": "forbid" + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" } - ], - "tests": [ - { - "description": "sends the correct command", - "operations": [ - { - "name": "dropSearchIndex", - "object": "collection0", - "arguments": { - "name": "test index" - }, - "expectError": { - "isError": true - } + }, + { + "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, + "errorContains": "Search index commands are only supported with Atlas" } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "dropSearchIndex": "collection0", - "name": "test index", - "$db": "database0" - } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "dropSearchIndex": "collection0", + "name": "test index", + "$db": "database0" } } - ] - } - ] - } - ] - } \ No newline at end of file + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/testdata/index-management/dropSearchIndex.yml b/testdata/index-management/dropSearchIndex.yml index 9c93cbe02b..f47bebe32e 100644 --- a/testdata/index-management/dropSearchIndex.yml +++ b/testdata/index-management/dropSearchIndex.yml @@ -28,9 +28,10 @@ tests: arguments: name: &indexName 'test index' expectError: - # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing - # against an Atlas cluster and the expectError will be removed. + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. isError: true + errorContains: Search index commands are only supported with Atlas expectEvents: - client: *client0 events: @@ -38,4 +39,4 @@ tests: command: dropSearchIndex: *collection0 name: *indexName - $db: *database0 + $db: *database0 \ No newline at end of file diff --git a/testdata/index-management/listSearchIndexes.json b/testdata/index-management/listSearchIndexes.json index 84d4eee295..ca2643ebc1 100644 --- a/testdata/index-management/listSearchIndexes.json +++ b/testdata/index-management/listSearchIndexes.json @@ -1,153 +1,156 @@ { - "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" - } + "description": "listSearchIndexes", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] } - ], - "runOnRequirements": [ - { - "minServerVersion": "7.0.0", - "topologies": [ - "replicaset", - "load-balanced", - "sharded" - ], - "serverless": "forbid" + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" } - ], - "tests": [ - { - "description": "when no name is provided, it does not populate the filter", - "operations": [ - { - "name": "listSearchIndexes", - "object": "collection0", - "expectError": { - "isError": true - } + }, + { + "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, + "errorContains": "Search index commands are only supported with Atlas" } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "aggregate": "collection0", - "pipeline": [ - { - "$listSearchIndexes": {} - } - ] - } + } + ], + "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 } + ] + } + ] + }, + { + "description": "when a name is provided, it is present in the filter", + "operations": [ + { + "name": "listSearchIndexes", + "object": "collection0", + "arguments": { + "name": "test index" + }, + "expectError": { + "isError": true, + "errorContains": "Search index commands are only supported with Atlas" } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "aggregate": "collection0", - "pipeline": [ - { - "$listSearchIndexes": { - "name": "test index" - } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "pipeline": [ + { + "$listSearchIndexes": { + "name": "test index" } - ], - "$db": "database0" - } + } + ], + "$db": "database0" } } - ] - } - ] - }, - { - "description": "aggregation cursor options are supported", - "operations": [ - { - "name": "listSearchIndexes", - "object": "collection0", - "arguments": { - "name": "test index", - "aggregationOptions": { - "batchSize": 10 - } - }, - "expectError": { - "isError": true } + ] + } + ] + }, + { + "description": "aggregation cursor options are supported", + "operations": [ + { + "name": "listSearchIndexes", + "object": "collection0", + "arguments": { + "name": "test index", + "aggregationOptions": { + "batchSize": 10 + } + }, + "expectError": { + "isError": true, + "errorContains": "Search index commands are only supported with Atlas" } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "aggregate": "collection0", - "cursor": { - "batchSize": 10 - }, - "pipeline": [ - { - "$listSearchIndexes": { - "name": "test index" - } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "cursor": { + "batchSize": 10 + }, + "pipeline": [ + { + "$listSearchIndexes": { + "name": "test index" } - ], - "$db": "database0" - } + } + ], + "$db": "database0" } } - ] - } - ] - } - ] - } \ No newline at end of file + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/testdata/index-management/listSearchIndexes.yml b/testdata/index-management/listSearchIndexes.yml index 7d3c3187b9..a50becdf1d 100644 --- a/testdata/index-management/listSearchIndexes.yml +++ b/testdata/index-management/listSearchIndexes.yml @@ -26,9 +26,10 @@ tests: - name: listSearchIndexes object: *collection0 expectError: - # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing - # against an Atlas cluster and the expectError will be removed. + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. isError: true + errorContains: Search index commands are only supported with Atlas expectEvents: - client: *client0 events: @@ -45,9 +46,10 @@ tests: arguments: name: &indexName "test index" expectError: - # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing - # against an Atlas cluster and the expectError will be removed. + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. isError: true + errorContains: Search index commands are only supported with Atlas expectEvents: - client: *client0 events: @@ -67,9 +69,10 @@ tests: aggregationOptions: batchSize: 10 expectError: - # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing - # against an Atlas cluster and the expectError will be removed. + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. isError: true + errorContains: Search index commands are only supported with Atlas expectEvents: - client: *client0 events: diff --git a/testdata/index-management/updateSearchIndex.json b/testdata/index-management/updateSearchIndex.json index a3edc1f16d..4cc3c530f6 100644 --- a/testdata/index-management/updateSearchIndex.json +++ b/testdata/index-management/updateSearchIndex.json @@ -1,75 +1,76 @@ { - "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" - } + "description": "updateSearchIndex", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] } - ], - "runOnRequirements": [ - { - "minServerVersion": "7.0.0", - "topologies": [ - "replicaset", - "load-balanced", - "sharded" - ], - "serverless": "forbid" + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" } - ], - "tests": [ - { - "description": "sends the correct command", - "operations": [ - { - "name": "updateSearchIndex", - "object": "collection0", - "arguments": { - "name": "test index", - "definition": {} - }, - "expectError": { - "isError": true - } + }, + { + "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, + "errorContains": "Search index commands are only supported with Atlas" } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "updateSearchIndex": "collection0", - "name": "test index", - "definition": {}, - "$db": "database0" - } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "updateSearchIndex": "collection0", + "name": "test index", + "definition": {}, + "$db": "database0" } } - ] - } - ] - } - ] - } \ No newline at end of file + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/testdata/index-management/updateSearchIndex.yml b/testdata/index-management/updateSearchIndex.yml index 748b7ef61b..7b6f3fbdc4 100644 --- a/testdata/index-management/updateSearchIndex.yml +++ b/testdata/index-management/updateSearchIndex.yml @@ -28,10 +28,11 @@ tests: arguments: name: &indexName 'test index' definition: &definition {} - expectError: - # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing - # against an Atlas cluster and the expectError will be removed. + expectError: + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. isError: true + errorContains: Search index commands are only supported with Atlas expectEvents: - client: *client0 events: @@ -40,4 +41,4 @@ tests: updateSearchIndex: *collection0 name: *indexName definition: *definition - $db: *database0 + $db: *database0 \ No newline at end of file From 064428a025dbf6093c3d03eb1aabcd9a613cca64 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 10 Aug 2023 19:38:20 -0400 Subject: [PATCH 04/11] fix typo --- x/mongo/driver/operation/drop_search_index.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/mongo/driver/operation/drop_search_index.go b/x/mongo/driver/operation/drop_search_index.go index bf079b3ce9..25cde8154b 100644 --- a/x/mongo/driver/operation/drop_search_index.go +++ b/x/mongo/driver/operation/drop_search_index.go @@ -1,4 +1,4 @@ -// Copyright (C) MongoDB, Inc. 2019-present. +// Copyright (C) MongoDB, Inc. 2023-present. // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain From 45855377b2b5dada25716041081f62b8f5a2fc97 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 11 Aug 2023 09:50:57 -0400 Subject: [PATCH 05/11] update prose test --- mongo/search_index_prose_test.go | 184 +++++++++++++++++++++---------- 1 file changed, 124 insertions(+), 60 deletions(-) diff --git a/mongo/search_index_prose_test.go b/mongo/search_index_prose_test.go index 1b8731f599..a2320b1fb3 100644 --- a/mongo/search_index_prose_test.go +++ b/mongo/search_index_prose_test.go @@ -16,6 +16,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/internal/assert" + "go.mongodb.org/mongo-driver/internal/require" "go.mongodb.org/mongo-driver/internal/uuid" "go.mongodb.org/mongo-driver/mongo/options" ) @@ -24,30 +25,14 @@ func newTestCollection(t *testing.T, client *Client) *Collection { t.Helper() id, err := uuid.New() - assert.NoError(t, err) + require.NoError(t, err) collName := fmt.Sprintf("col%s", id.String()) return client.Database("test").Collection(collName) } -func getDocument(t *testing.T, ctx context.Context, view SearchIndexView, index string) bson.Raw { - t.Helper() - - for { - cursor, err := view.List(ctx, &index, nil) - assert.NoError(t, err, "failed to list") - - if !cursor.Next(ctx) { - return nil - } - if cursor.Current.Lookup("queryable").Boolean() { - return cursor.Current - } - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) - time.Sleep(5 * time.Second) - } -} - func TestSearchIndexProse(t *testing.T) { + t.Parallel() + const timeout = 5 * time.Minute ctx := context.Background() @@ -66,10 +51,13 @@ func TestSearchIndexProse(t *testing.T) { ctx := context.Background() collection := newTestCollection(t, client) - defer collection.Drop(ctx) + defer func() { + err = collection.Drop(ctx) + assert.NoError(t, err, "failed to drop collection") + }() _, err = collection.InsertOne(ctx, bson.D{}) - assert.NoError(t, err, "failed to insert") + require.NoError(t, err, "failed to insert") view := collection.SearchIndexes() @@ -80,14 +68,28 @@ func TestSearchIndexProse(t *testing.T) { Name: &searchName, } index, err := view.CreateOne(ctx, model) - assert.NoError(t, err, "failed to create index") - assert.Equal(t, searchName, index, "unmatched name") + require.NoError(t, err, "failed to create index") + require.Equal(t, searchName, index, "unmatched name") + + var doc bson.Raw + for doc == nil { + cursor, err := view.List(ctx, &index, nil) + require.NoError(t, err, "failed to list") - doc := getDocument(t, ctx, view, searchName) - assert.NotNil(t, doc, "got empty document") + if !cursor.Next(ctx) { + break + } + if cursor.Current.Lookup("queryable").Boolean() { + doc = cursor.Current + } else { + t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } + } + require.NotNil(t, doc, "got empty document") assert.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") expected, err := bson.Marshal(definition) - assert.NoError(t, err, "failed to marshal definition") + require.NoError(t, err, "failed to marshal definition") actual := doc.Lookup("latestDefinition").Value assert.Equal(t, expected, actual, "unmatched definition") }) @@ -96,10 +98,13 @@ func TestSearchIndexProse(t *testing.T) { ctx := context.Background() collection := newTestCollection(t, client) - defer collection.Drop(ctx) + defer func() { + err = collection.Drop(ctx) + assert.NoError(t, err, "failed to drop collection") + }() _, err = collection.InsertOne(ctx, bson.D{}) - assert.NoError(t, err, "failed to insert") + require.NoError(t, err, "failed to insert") view := collection.SearchIndexes() @@ -117,32 +122,50 @@ func TestSearchIndexProse(t *testing.T) { }, } indexes, err := view.CreateMany(ctx, models) - assert.NoError(t, err, "failed to create index") - assert.Equal(t, len(indexes), 2, "expected 2 indexes") - assert.Contains(t, indexes, searchName0) - assert.Contains(t, indexes, searchName1) + require.NoError(t, err, "failed to create index") + require.Equal(t, len(indexes), 2, "expected 2 indexes") + require.Contains(t, indexes, searchName0) + require.Contains(t, indexes, searchName1) + + getDocument := func(index string) bson.Raw { + t.Helper() + + for { + cursor, err := view.List(ctx, &index, nil) + require.NoError(t, err, "failed to list") + + if !cursor.Next(ctx) { + return nil + } + if cursor.Current.Lookup("queryable").Boolean() { + return cursor.Current + } + t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } + } var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() - doc := getDocument(t, ctx, view, searchName0) - assert.NotNil(t, doc, "got empty document") + doc := getDocument(searchName0) + require.NotNil(t, doc, "got empty document") assert.Equal(t, searchName0, doc.Lookup("name").StringValue(), "unmatched name") expected, err := bson.Marshal(definition) - assert.NoError(t, err, "failed to marshal definition") + require.NoError(t, err, "failed to marshal definition") actual := doc.Lookup("latestDefinition").Value assert.Equal(t, expected, actual, "unmatched definition") }() go func() { defer wg.Done() - doc := getDocument(t, ctx, view, searchName1) - assert.NotNil(t, doc, "got empty document") + doc := getDocument(searchName1) + require.NotNil(t, doc, "got empty document") assert.Equal(t, searchName1, doc.Lookup("name").StringValue(), "unmatched name") expected, err := bson.Marshal(definition) - assert.NoError(t, err, "failed to marshal definition") + require.NoError(t, err, "failed to marshal definition") actual := doc.Lookup("latestDefinition").Value assert.Equal(t, expected, actual, "unmatched definition") }() @@ -153,10 +176,13 @@ func TestSearchIndexProse(t *testing.T) { ctx := context.Background() collection := newTestCollection(t, client) - defer collection.Drop(ctx) + defer func() { + err = collection.Drop(ctx) + assert.NoError(t, err, "failed to drop collection") + }() _, err = collection.InsertOne(ctx, bson.D{}) - assert.NoError(t, err, "failed to insert") + require.NoError(t, err, "failed to insert") view := collection.SearchIndexes() @@ -167,18 +193,32 @@ func TestSearchIndexProse(t *testing.T) { Name: &searchName, } index, err := view.CreateOne(ctx, model) - assert.NoError(t, err, "failed to create index") - assert.Equal(t, searchName, index, "unmatched name") + require.NoError(t, err, "failed to create index") + require.Equal(t, searchName, index, "unmatched name") - doc := getDocument(t, ctx, view, searchName) - assert.NotNil(t, doc, "got empty document") - assert.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") + var doc bson.Raw + for doc == nil { + cursor, err := view.List(ctx, &index, nil) + require.NoError(t, err, "failed to list") + + if !cursor.Next(ctx) { + break + } + if cursor.Current.Lookup("queryable").Boolean() { + doc = cursor.Current + } else { + t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } + } + require.NotNil(t, doc, "got empty document") + require.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") err = view.DropOne(ctx, searchName) - assert.NoError(t, err, "failed to drop index") + require.NoError(t, err, "failed to drop index") for { cursor, err := view.List(ctx, &index, nil) - assert.NoError(t, err, "failed to list") + require.NoError(t, err, "failed to list") if !cursor.Next(ctx) { break @@ -192,10 +232,13 @@ func TestSearchIndexProse(t *testing.T) { ctx := context.Background() collection := newTestCollection(t, client) - defer collection.Drop(ctx) + defer func() { + err = collection.Drop(ctx) + assert.NoError(t, err, "failed to drop collection") + }() _, err = collection.InsertOne(ctx, bson.D{}) - assert.NoError(t, err, "failed to insert") + require.NoError(t, err, "failed to insert") view := collection.SearchIndexes() @@ -206,22 +249,40 @@ func TestSearchIndexProse(t *testing.T) { Name: &searchName, } index, err := view.CreateOne(ctx, model) - assert.NoError(t, err, "failed to create index") - assert.Equal(t, searchName, index, "unmatched name") + require.NoError(t, err, "failed to create index") + require.Equal(t, searchName, index, "unmatched name") + + getDocument := func(index string) bson.Raw { + t.Helper() + + for { + cursor, err := view.List(ctx, &index, nil) + require.NoError(t, err, "failed to list") + + if !cursor.Next(ctx) { + return nil + } + if cursor.Current.Lookup("queryable").Boolean() { + return cursor.Current + } + t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } + } - doc := getDocument(t, ctx, view, searchName) - assert.NotNil(t, doc, "got empty document") - assert.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") + doc := getDocument(searchName) + require.NotNil(t, doc, "got empty document") + require.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") definition = bson.D{{"mappings", bson.D{{"dynamic", true}}}} err = view.UpdateOne(ctx, searchName, definition) - assert.NoError(t, err, "failed to drop index") - doc = getDocument(t, ctx, view, searchName) - assert.NotNil(t, doc, "got empty document") + require.NoError(t, err, "failed to drop index") + doc = getDocument(searchName) + require.NotNil(t, doc, "got empty document") assert.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") assert.Equal(t, "READY", doc.Lookup("status").StringValue(), "unexpected status") expected, err := bson.Marshal(definition) - assert.NoError(t, err, "failed to marshal definition") + require.NoError(t, err, "failed to marshal definition") actual := doc.Lookup("latestDefinition").Value assert.Equal(t, expected, actual, "unmatched definition") }) @@ -230,9 +291,12 @@ func TestSearchIndexProse(t *testing.T) { ctx := context.Background() collection := newTestCollection(t, client) - defer collection.Drop(ctx) + defer func() { + err = collection.Drop(ctx) + assert.NoError(t, err, "failed to drop collection") + }() err = collection.SearchIndexes().DropOne(ctx, "foo") - assert.NoError(t, err) + require.NoError(t, err) }) } From 73d66337eb72808827883d200b9f84b0b988096e Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 18 Aug 2023 18:09:34 -0400 Subject: [PATCH 06/11] Update prose tests. --- Makefile | 2 +- mongo/integration/search_index_prose_test.go | 268 ++++++++++++++++ mongo/search_index_prose_test.go | 302 ------------------- 3 files changed, 269 insertions(+), 303 deletions(-) create mode 100644 mongo/integration/search_index_prose_test.go delete mode 100644 mongo/search_index_prose_test.go diff --git a/Makefile b/Makefile index 7618dc5b28..e04c86269c 100644 --- a/Makefile +++ b/Makefile @@ -166,7 +166,7 @@ evg-test-load-balancers: .PHONY: evg-test-search-index evg-test-search-index: - go test ./mongo -run TestSearchIndexProse -v -timeout $(TEST_TIMEOUT)s + go test ./mongo/integration -run TestSearchIndexProse -v -timeout $(TEST_TIMEOUT)s .PHONY: evg-test-ocsp evg-test-ocsp: diff --git a/mongo/integration/search_index_prose_test.go b/mongo/integration/search_index_prose_test.go new file mode 100644 index 0000000000..facc16c60b --- /dev/null +++ b/mongo/integration/search_index_prose_test.go @@ -0,0 +1,268 @@ +// Copyright (C) MongoDB, Inc. 2023-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package integration + +import ( + "context" + "os" + "sync" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/internal/assert" + "go.mongodb.org/mongo-driver/internal/require" + "go.mongodb.org/mongo-driver/internal/uuid" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/integration/mtest" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func TestSearchIndexProse(t *testing.T) { + t.Parallel() + + const timeout = 5 * time.Minute + + uri := os.Getenv("TEST_INDEX_URI") + if uri == "" { + t.Skip("skipping") + } + + opts := options.Client().ApplyURI(uri).SetTimeout(timeout) + mt := mtest.New(t, mtest.NewOptions().ClientOptions(opts).MinServerVersion("7.0").Topologies(mtest.ReplicaSet)) + defer mt.Close() + + mt.Run("case 1: Driver can successfully create and list search indexes", func(mt *mtest.T) { + ctx := context.Background() + + _, err := mt.Coll.InsertOne(ctx, bson.D{}) + require.NoError(mt, err, "failed to insert") + + view := mt.Coll.SearchIndexes() + + definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} + searchName := "test-search-index" + model := mongo.SearchIndexModel{ + Definition: definition, + Name: &searchName, + } + index, err := view.CreateOne(ctx, model) + require.NoError(mt, err, "failed to create index") + require.Equal(mt, searchName, index, "unmatched name") + + var doc bson.Raw + for doc == nil { + cursor, err := view.List(ctx, &index, nil) + require.NoError(mt, err, "failed to list") + + if !cursor.Next(ctx) { + break + } + if cursor.Current.Lookup("queryable").Boolean() { + doc = cursor.Current + } else { + t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } + } + require.NotNil(mt, doc, "got empty document") + assert.Equal(mt, searchName, doc.Lookup("name").StringValue(), "unmatched name") + expected, err := bson.Marshal(definition) + require.NoError(mt, err, "failed to marshal definition") + actual := doc.Lookup("latestDefinition").Value + assert.Equal(mt, expected, actual, "unmatched definition") + }) + + mt.Run("case 2: Driver can successfully create multiple indexes in batch", func(mt *mtest.T) { + ctx := context.Background() + + _, err := mt.Coll.InsertOne(ctx, bson.D{}) + require.NoError(mt, err, "failed to insert") + + view := mt.Coll.SearchIndexes() + + definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} + searchName0 := "test-search-index-1" + searchName1 := "test-search-index-2" + models := []mongo.SearchIndexModel{ + { + Definition: definition, + Name: &searchName0, + }, + { + Definition: definition, + Name: &searchName1, + }, + } + indexes, err := view.CreateMany(ctx, models) + require.NoError(mt, err, "failed to create index") + require.Equal(mt, len(indexes), 2, "expected 2 indexes") + require.Contains(mt, indexes, searchName0) + require.Contains(mt, indexes, searchName1) + + getDocument := func(index string) bson.Raw { + t.Helper() + + for { + cursor, err := view.List(ctx, &index, nil) + require.NoError(mt, err, "failed to list") + + if !cursor.Next(ctx) { + return nil + } + if cursor.Current.Lookup("queryable").Boolean() { + return cursor.Current + } + t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } + } + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + + doc := getDocument(searchName0) + require.NotNil(mt, doc, "got empty document") + assert.Equal(mt, searchName0, doc.Lookup("name").StringValue(), "unmatched name") + expected, err := bson.Marshal(definition) + require.NoError(mt, err, "failed to marshal definition") + actual := doc.Lookup("latestDefinition").Value + assert.Equal(mt, expected, actual, "unmatched definition") + }() + go func() { + defer wg.Done() + + doc := getDocument(searchName1) + require.NotNil(mt, doc, "got empty document") + assert.Equal(mt, searchName1, doc.Lookup("name").StringValue(), "unmatched name") + expected, err := bson.Marshal(definition) + require.NoError(mt, err, "failed to marshal definition") + actual := doc.Lookup("latestDefinition").Value + assert.Equal(mt, expected, actual, "unmatched definition") + }() + wg.Wait() + }) + + mt.Run("case 3: Driver can successfully drop search indexes", func(mt *mtest.T) { + ctx := context.Background() + + _, err := mt.Coll.InsertOne(ctx, bson.D{}) + require.NoError(mt, err, "failed to insert") + + view := mt.Coll.SearchIndexes() + + definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} + searchName := "test-search-index" + model := mongo.SearchIndexModel{ + Definition: definition, + Name: &searchName, + } + index, err := view.CreateOne(ctx, model) + require.NoError(mt, err, "failed to create index") + require.Equal(mt, searchName, index, "unmatched name") + + var doc bson.Raw + for doc == nil { + cursor, err := view.List(ctx, &index, nil) + require.NoError(mt, err, "failed to list") + + if !cursor.Next(ctx) { + break + } + if cursor.Current.Lookup("queryable").Boolean() { + doc = cursor.Current + } else { + t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } + } + require.NotNil(mt, doc, "got empty document") + require.Equal(mt, searchName, doc.Lookup("name").StringValue(), "unmatched name") + + err = view.DropOne(ctx, searchName) + require.NoError(mt, err, "failed to drop index") + for { + cursor, err := view.List(ctx, &index, nil) + require.NoError(mt, err, "failed to list") + + if !cursor.Next(ctx) { + break + } + t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } + }) + + mt.Run("case 4: Driver can update a search index", func(mt *mtest.T) { + ctx := context.Background() + + _, err := mt.Coll.InsertOne(ctx, bson.D{}) + require.NoError(mt, err, "failed to insert") + + view := mt.Coll.SearchIndexes() + + definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} + searchName := "test-search-index" + model := mongo.SearchIndexModel{ + Definition: definition, + Name: &searchName, + } + index, err := view.CreateOne(ctx, model) + require.NoError(mt, err, "failed to create index") + require.Equal(mt, searchName, index, "unmatched name") + + getDocument := func(index string) bson.Raw { + t.Helper() + + for { + cursor, err := view.List(ctx, &index, nil) + require.NoError(mt, err, "failed to list") + + if !cursor.Next(ctx) { + return nil + } + if cursor.Current.Lookup("queryable").Boolean() { + return cursor.Current + } + t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } + } + + doc := getDocument(searchName) + require.NotNil(mt, doc, "got empty document") + require.Equal(mt, searchName, doc.Lookup("name").StringValue(), "unmatched name") + + definition = bson.D{{"mappings", bson.D{{"dynamic", true}}}} + err = view.UpdateOne(ctx, searchName, definition) + require.NoError(mt, err, "failed to drop index") + doc = getDocument(searchName) + require.NotNil(mt, doc, "got empty document") + assert.Equal(mt, searchName, doc.Lookup("name").StringValue(), "unmatched name") + assert.Equal(mt, "READY", doc.Lookup("status").StringValue(), "unexpected status") + expected, err := bson.Marshal(definition) + require.NoError(mt, err, "failed to marshal definition") + actual := doc.Lookup("latestDefinition").Value + assert.Equal(mt, expected, actual, "unmatched definition") + }) + + mt.Run("case 5: dropSearchIndex suppresses namespace not found errors", func(mt *mtest.T) { + ctx := context.Background() + + id, err := uuid.New() + require.NoError(mt, err) + + collection := mt.CreateCollection(mtest.Collection{ + Name: id.String(), + }, false) + + err = collection.SearchIndexes().DropOne(ctx, "foo") + require.NoError(mt, err) + }) +} diff --git a/mongo/search_index_prose_test.go b/mongo/search_index_prose_test.go deleted file mode 100644 index a2320b1fb3..0000000000 --- a/mongo/search_index_prose_test.go +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright (C) MongoDB, Inc. 2023-present. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - -package mongo - -import ( - "context" - "fmt" - "os" - "sync" - "testing" - "time" - - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/internal/assert" - "go.mongodb.org/mongo-driver/internal/require" - "go.mongodb.org/mongo-driver/internal/uuid" - "go.mongodb.org/mongo-driver/mongo/options" -) - -func newTestCollection(t *testing.T, client *Client) *Collection { - t.Helper() - - id, err := uuid.New() - require.NoError(t, err) - collName := fmt.Sprintf("col%s", id.String()) - return client.Database("test").Collection(collName) -} - -func TestSearchIndexProse(t *testing.T) { - t.Parallel() - - const timeout = 5 * time.Minute - - ctx := context.Background() - - uri := os.Getenv("TEST_INDEX_URI") - if uri == "" { - t.Skip("skipping") - } - clientOptions := options.Client().ApplyURI(uri).SetTimeout(timeout) - - client, err := Connect(ctx, clientOptions) - assert.NoError(t, err, "failed to connect %s", uri) - defer client.Disconnect(ctx) - - t.Run("case 1: Driver can successfully create and list search indexes", func(t *testing.T) { - ctx := context.Background() - - collection := newTestCollection(t, client) - defer func() { - err = collection.Drop(ctx) - assert.NoError(t, err, "failed to drop collection") - }() - - _, err = collection.InsertOne(ctx, bson.D{}) - require.NoError(t, err, "failed to insert") - - view := collection.SearchIndexes() - - definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} - searchName := "test-search-index" - model := SearchIndexModel{ - Definition: definition, - Name: &searchName, - } - index, err := view.CreateOne(ctx, model) - require.NoError(t, err, "failed to create index") - require.Equal(t, searchName, index, "unmatched name") - - var doc bson.Raw - for doc == nil { - cursor, err := view.List(ctx, &index, nil) - require.NoError(t, err, "failed to list") - - if !cursor.Next(ctx) { - break - } - if cursor.Current.Lookup("queryable").Boolean() { - doc = cursor.Current - } else { - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) - time.Sleep(5 * time.Second) - } - } - require.NotNil(t, doc, "got empty document") - assert.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") - expected, err := bson.Marshal(definition) - require.NoError(t, err, "failed to marshal definition") - actual := doc.Lookup("latestDefinition").Value - assert.Equal(t, expected, actual, "unmatched definition") - }) - - t.Run("case 2: Driver can successfully create multiple indexes in batch", func(t *testing.T) { - ctx := context.Background() - - collection := newTestCollection(t, client) - defer func() { - err = collection.Drop(ctx) - assert.NoError(t, err, "failed to drop collection") - }() - - _, err = collection.InsertOne(ctx, bson.D{}) - require.NoError(t, err, "failed to insert") - - view := collection.SearchIndexes() - - definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} - searchName0 := "test-search-index-1" - searchName1 := "test-search-index-2" - models := []SearchIndexModel{ - { - Definition: definition, - Name: &searchName0, - }, - { - Definition: definition, - Name: &searchName1, - }, - } - indexes, err := view.CreateMany(ctx, models) - require.NoError(t, err, "failed to create index") - require.Equal(t, len(indexes), 2, "expected 2 indexes") - require.Contains(t, indexes, searchName0) - require.Contains(t, indexes, searchName1) - - getDocument := func(index string) bson.Raw { - t.Helper() - - for { - cursor, err := view.List(ctx, &index, nil) - require.NoError(t, err, "failed to list") - - if !cursor.Next(ctx) { - return nil - } - if cursor.Current.Lookup("queryable").Boolean() { - return cursor.Current - } - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) - time.Sleep(5 * time.Second) - } - } - - var wg sync.WaitGroup - wg.Add(2) - go func() { - defer wg.Done() - - doc := getDocument(searchName0) - require.NotNil(t, doc, "got empty document") - assert.Equal(t, searchName0, doc.Lookup("name").StringValue(), "unmatched name") - expected, err := bson.Marshal(definition) - require.NoError(t, err, "failed to marshal definition") - actual := doc.Lookup("latestDefinition").Value - assert.Equal(t, expected, actual, "unmatched definition") - }() - go func() { - defer wg.Done() - - doc := getDocument(searchName1) - require.NotNil(t, doc, "got empty document") - assert.Equal(t, searchName1, doc.Lookup("name").StringValue(), "unmatched name") - expected, err := bson.Marshal(definition) - require.NoError(t, err, "failed to marshal definition") - actual := doc.Lookup("latestDefinition").Value - assert.Equal(t, expected, actual, "unmatched definition") - }() - wg.Wait() - }) - - t.Run("case 3: Driver can successfully drop search indexes", func(t *testing.T) { - ctx := context.Background() - - collection := newTestCollection(t, client) - defer func() { - err = collection.Drop(ctx) - assert.NoError(t, err, "failed to drop collection") - }() - - _, err = collection.InsertOne(ctx, bson.D{}) - require.NoError(t, err, "failed to insert") - - view := collection.SearchIndexes() - - definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} - searchName := "test-search-index" - model := SearchIndexModel{ - Definition: definition, - Name: &searchName, - } - index, err := view.CreateOne(ctx, model) - require.NoError(t, err, "failed to create index") - require.Equal(t, searchName, index, "unmatched name") - - var doc bson.Raw - for doc == nil { - cursor, err := view.List(ctx, &index, nil) - require.NoError(t, err, "failed to list") - - if !cursor.Next(ctx) { - break - } - if cursor.Current.Lookup("queryable").Boolean() { - doc = cursor.Current - } else { - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) - time.Sleep(5 * time.Second) - } - } - require.NotNil(t, doc, "got empty document") - require.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") - - err = view.DropOne(ctx, searchName) - require.NoError(t, err, "failed to drop index") - for { - cursor, err := view.List(ctx, &index, nil) - require.NoError(t, err, "failed to list") - - if !cursor.Next(ctx) { - break - } - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) - time.Sleep(5 * time.Second) - } - }) - - t.Run("case 4: Driver can update a search index", func(t *testing.T) { - ctx := context.Background() - - collection := newTestCollection(t, client) - defer func() { - err = collection.Drop(ctx) - assert.NoError(t, err, "failed to drop collection") - }() - - _, err = collection.InsertOne(ctx, bson.D{}) - require.NoError(t, err, "failed to insert") - - view := collection.SearchIndexes() - - definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} - searchName := "test-search-index" - model := SearchIndexModel{ - Definition: definition, - Name: &searchName, - } - index, err := view.CreateOne(ctx, model) - require.NoError(t, err, "failed to create index") - require.Equal(t, searchName, index, "unmatched name") - - getDocument := func(index string) bson.Raw { - t.Helper() - - for { - cursor, err := view.List(ctx, &index, nil) - require.NoError(t, err, "failed to list") - - if !cursor.Next(ctx) { - return nil - } - if cursor.Current.Lookup("queryable").Boolean() { - return cursor.Current - } - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) - time.Sleep(5 * time.Second) - } - } - - doc := getDocument(searchName) - require.NotNil(t, doc, "got empty document") - require.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") - - definition = bson.D{{"mappings", bson.D{{"dynamic", true}}}} - err = view.UpdateOne(ctx, searchName, definition) - require.NoError(t, err, "failed to drop index") - doc = getDocument(searchName) - require.NotNil(t, doc, "got empty document") - assert.Equal(t, searchName, doc.Lookup("name").StringValue(), "unmatched name") - assert.Equal(t, "READY", doc.Lookup("status").StringValue(), "unexpected status") - expected, err := bson.Marshal(definition) - require.NoError(t, err, "failed to marshal definition") - actual := doc.Lookup("latestDefinition").Value - assert.Equal(t, expected, actual, "unmatched definition") - }) - - t.Run("case 5: dropSearchIndex suppresses namespace not found errors", func(t *testing.T) { - ctx := context.Background() - - collection := newTestCollection(t, client) - defer func() { - err = collection.Drop(ctx) - assert.NoError(t, err, "failed to drop collection") - }() - - err = collection.SearchIndexes().DropOne(ctx, "foo") - require.NoError(t, err) - }) -} From b0d6b4f8c315fb1d559eb359ee5d9e7344514d08 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 25 Aug 2023 11:45:12 -0400 Subject: [PATCH 07/11] Embed an entire AggregateOptions struct into the ListSearchIndexOptions struct. --- mongo/integration/search_index_prose_test.go | 68 ++++++++----------- .../unified/collection_operation_execution.go | 8 ++- mongo/options/searchindexoptions.go | 1 + mongo/search_index_view.go | 18 ++--- 4 files changed, 44 insertions(+), 51 deletions(-) diff --git a/mongo/integration/search_index_prose_test.go b/mongo/integration/search_index_prose_test.go index facc16c60b..281ec46436 100644 --- a/mongo/integration/search_index_prose_test.go +++ b/mongo/integration/search_index_prose_test.go @@ -56,7 +56,7 @@ func TestSearchIndexProse(t *testing.T) { var doc bson.Raw for doc == nil { - cursor, err := view.List(ctx, &index, nil) + cursor, err := view.List(ctx, &index) require.NoError(mt, err, "failed to list") if !cursor.Next(ctx) { @@ -86,29 +86,26 @@ func TestSearchIndexProse(t *testing.T) { view := mt.Coll.SearchIndexes() definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} - searchName0 := "test-search-index-1" - searchName1 := "test-search-index-2" - models := []mongo.SearchIndexModel{ - { + searchNames := []string{"test-search-index-1", "test-search-index-2"} + models := make([]mongo.SearchIndexModel, len(searchNames)) + for i := range searchNames { + models[i] = mongo.SearchIndexModel{ Definition: definition, - Name: &searchName0, - }, - { - Definition: definition, - Name: &searchName1, - }, + Name: &searchNames[i], + } } indexes, err := view.CreateMany(ctx, models) require.NoError(mt, err, "failed to create index") require.Equal(mt, len(indexes), 2, "expected 2 indexes") - require.Contains(mt, indexes, searchName0) - require.Contains(mt, indexes, searchName1) + for _, searchName := range searchNames { + require.Contains(mt, indexes, searchName) + } getDocument := func(index string) bson.Raw { t.Helper() for { - cursor, err := view.List(ctx, &index, nil) + cursor, err := view.List(ctx, &index) require.NoError(mt, err, "failed to list") if !cursor.Next(ctx) { @@ -123,29 +120,20 @@ func TestSearchIndexProse(t *testing.T) { } var wg sync.WaitGroup - wg.Add(2) - go func() { - defer wg.Done() - - doc := getDocument(searchName0) - require.NotNil(mt, doc, "got empty document") - assert.Equal(mt, searchName0, doc.Lookup("name").StringValue(), "unmatched name") - expected, err := bson.Marshal(definition) - require.NoError(mt, err, "failed to marshal definition") - actual := doc.Lookup("latestDefinition").Value - assert.Equal(mt, expected, actual, "unmatched definition") - }() - go func() { - defer wg.Done() - - doc := getDocument(searchName1) - require.NotNil(mt, doc, "got empty document") - assert.Equal(mt, searchName1, doc.Lookup("name").StringValue(), "unmatched name") - expected, err := bson.Marshal(definition) - require.NoError(mt, err, "failed to marshal definition") - actual := doc.Lookup("latestDefinition").Value - assert.Equal(mt, expected, actual, "unmatched definition") - }() + wg.Add(len(searchNames)) + for i := range searchNames { + go func(name string) { + defer wg.Done() + + doc := getDocument(name) + require.NotNil(mt, doc, "got empty document") + assert.Equal(mt, name, doc.Lookup("name").StringValue(), "unmatched name") + expected, err := bson.Marshal(definition) + require.NoError(mt, err, "failed to marshal definition") + actual := doc.Lookup("latestDefinition").Value + assert.Equal(mt, expected, actual, "unmatched definition") + }(searchNames[i]) + } wg.Wait() }) @@ -169,7 +157,7 @@ func TestSearchIndexProse(t *testing.T) { var doc bson.Raw for doc == nil { - cursor, err := view.List(ctx, &index, nil) + cursor, err := view.List(ctx, &index) require.NoError(mt, err, "failed to list") if !cursor.Next(ctx) { @@ -188,7 +176,7 @@ func TestSearchIndexProse(t *testing.T) { err = view.DropOne(ctx, searchName) require.NoError(mt, err, "failed to drop index") for { - cursor, err := view.List(ctx, &index, nil) + cursor, err := view.List(ctx, &index) require.NoError(mt, err, "failed to list") if !cursor.Next(ctx) { @@ -221,7 +209,7 @@ func TestSearchIndexProse(t *testing.T) { t.Helper() for { - cursor, err := view.List(ctx, &index, nil) + cursor, err := view.List(ctx, &index) require.NoError(mt, err, "failed to list") if !cursor.Next(ctx) { diff --git a/mongo/integration/unified/collection_operation_execution.go b/mongo/integration/unified/collection_operation_execution.go index 08768ce694..6da5a9d401 100644 --- a/mongo/integration/unified/collection_operation_execution.go +++ b/mongo/integration/unified/collection_operation_execution.go @@ -1118,7 +1118,7 @@ func executeListSearchIndexes(ctx context.Context, operation *operation) (*opera } var name *string - var opts []*options.AggregateOptions + var opts []*options.ListSearchIndexesOptions elems, err := operation.Arguments.Elements() if err != nil { @@ -1138,13 +1138,15 @@ func executeListSearchIndexes(ctx context.Context, operation *operation) (*opera if err != nil { return nil, err } - opts = append(opts, &opt) + opts = append(opts, &options.ListSearchIndexesOptions{ + AggregateOpts: &opt, + }) default: return nil, fmt.Errorf("unrecognized listSearchIndexes option %q", key) } } - _, err = coll.SearchIndexes().List(ctx, name, opts) + _, err = coll.SearchIndexes().List(ctx, name, opts...) return newValueResult(bsontype.Null, nil, err), nil } diff --git a/mongo/options/searchindexoptions.go b/mongo/options/searchindexoptions.go index 64e585c49f..5f7b771153 100644 --- a/mongo/options/searchindexoptions.go +++ b/mongo/options/searchindexoptions.go @@ -13,6 +13,7 @@ type CreateSearchIndexesOptions struct { // ListSearchIndexesOptions represents options that can be used to configure a SearchIndexView.List operation. type ListSearchIndexesOptions struct { + AggregateOpts *AggregateOptions } // DropSearchIndexOptions represents options that can be used to configure a SearchIndexView.DropOne operation. diff --git a/mongo/search_index_view.go b/mongo/search_index_view.go index 4d0fa104ff..fdd12d2202 100644 --- a/mongo/search_index_view.go +++ b/mongo/search_index_view.go @@ -38,21 +38,23 @@ type SearchIndexModel struct { // List executes a listSearchIndexes command and returns a cursor over the search indexes in the collection. // -// The aggregateOpts parameter is the aggregation options. +// The name parameter specifies the index name. A nil pointer matches all indexes. // // The opts parameter can be used to specify options for this operation (see the options.ListSearchIndexesOptions // documentation). -func (siv SearchIndexView) List(ctx context.Context, name *string, - aggregateOpts []*options.AggregateOptions, _ ...*options.ListSearchIndexesOptions) (*Cursor, error) { +func (siv SearchIndexView) List(ctx context.Context, name *string, opts ...*options.ListSearchIndexesOptions) (*Cursor, error) { if ctx == nil { ctx = context.Background() } - var index bson.D - if name != nil && len(*name) > 0 { + index := bson.D{} + if name != nil { index = bson.D{{"name", *name}} - } else { - index = bson.D{} + } + + aggregateOpts := make([]*options.AggregateOptions, len(opts)) + for i, opt := range opts { + aggregateOpts[i] = opt.AggregateOpts } return siv.coll.Aggregate(ctx, Pipeline{{{"$listSearchIndexes", index}}}, aggregateOpts...) @@ -92,7 +94,7 @@ func (siv SearchIndexView) CreateMany(ctx context.Context, models []SearchIndexM var iidx int32 iidx, indexes = bsoncore.AppendDocumentElementStart(indexes, strconv.Itoa(i)) - if model.Name != nil && len(*model.Name) > 0 { + if model.Name != nil { indexes = bsoncore.AppendStringElement(indexes, "name", *model.Name) } indexes = bsoncore.AppendDocumentElement(indexes, "definition", definition) From 05250ef525c377714278bc76d51b6ac0b44a49e9 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 25 Aug 2023 14:07:46 -0400 Subject: [PATCH 08/11] Update prose test. --- mongo/integration/search_index_prose_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/mongo/integration/search_index_prose_test.go b/mongo/integration/search_index_prose_test.go index 281ec46436..b1663585e5 100644 --- a/mongo/integration/search_index_prose_test.go +++ b/mongo/integration/search_index_prose_test.go @@ -34,7 +34,6 @@ func TestSearchIndexProse(t *testing.T) { opts := options.Client().ApplyURI(uri).SetTimeout(timeout) mt := mtest.New(t, mtest.NewOptions().ClientOptions(opts).MinServerVersion("7.0").Topologies(mtest.ReplicaSet)) - defer mt.Close() mt.Run("case 1: Driver can successfully create and list search indexes", func(mt *mtest.T) { ctx := context.Background() From ceebe17bcabbc2e56dcc342f3d86e1ccc48cf163 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Tue, 5 Sep 2023 11:15:57 -0400 Subject: [PATCH 09/11] Add options.SearchIndexesOptions. --- Makefile | 2 +- bson/raw.go | 21 ++--- mongo/integration/search_index_prose_test.go | 76 +++++++++---------- .../unified/collection_operation_execution.go | 7 +- mongo/options/searchindexoptions.go | 16 ++++ mongo/search_index_view.go | 14 ++-- 6 files changed, 75 insertions(+), 61 deletions(-) diff --git a/Makefile b/Makefile index e04c86269c..a62cc378bb 100644 --- a/Makefile +++ b/Makefile @@ -166,7 +166,7 @@ evg-test-load-balancers: .PHONY: evg-test-search-index evg-test-search-index: - go test ./mongo/integration -run TestSearchIndexProse -v -timeout $(TEST_TIMEOUT)s + go test ./mongo/integration -run TestSearchIndexProse -v -timeout $(TEST_TIMEOUT)s >> test.suite .PHONY: evg-test-ocsp evg-test-ocsp: diff --git a/bson/raw.go b/bson/raw.go index 773080c876..130da61ba0 100644 --- a/bson/raw.go +++ b/bson/raw.go @@ -60,16 +60,17 @@ func (r Raw) LookupErr(key ...string) (RawValue, error) { // elements. If the document is not valid, the elements up to the invalid point will be returned // along with an error. func (r Raw) Elements() ([]RawElement, error) { - var relems []RawElement - if doc := bsoncore.Document(r); len(doc) > 0 { - elems, err := doc.Elements() - if err != nil { - return relems, err - } - relems = make([]RawElement, 0, len(elems)) - for _, elem := range elems { - relems = append(relems, RawElement(elem)) - } + doc := bsoncore.Document(r) + if len(doc) == 0 { + return nil, nil + } + elems, err := doc.Elements() + if err != nil { + return nil, err + } + relems := make([]RawElement, 0, len(elems)) + for _, elem := range elems { + relems = append(relems, RawElement(elem)) } return relems, nil } diff --git a/mongo/integration/search_index_prose_test.go b/mongo/integration/search_index_prose_test.go index b1663585e5..002150c36e 100644 --- a/mongo/integration/search_index_prose_test.go +++ b/mongo/integration/search_index_prose_test.go @@ -45,17 +45,17 @@ func TestSearchIndexProse(t *testing.T) { definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} searchName := "test-search-index" - model := mongo.SearchIndexModel{ + opts := options.SearchIndexes().SetName(searchName) + index, err := view.CreateOne(ctx, mongo.SearchIndexModel{ Definition: definition, - Name: &searchName, - } - index, err := view.CreateOne(ctx, model) + Options: opts, + }) require.NoError(mt, err, "failed to create index") require.Equal(mt, searchName, index, "unmatched name") var doc bson.Raw for doc == nil { - cursor, err := view.List(ctx, &index) + cursor, err := view.List(ctx, opts) require.NoError(mt, err, "failed to list") if !cursor.Next(ctx) { @@ -85,26 +85,26 @@ func TestSearchIndexProse(t *testing.T) { view := mt.Coll.SearchIndexes() definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} - searchNames := []string{"test-search-index-1", "test-search-index-2"} - models := make([]mongo.SearchIndexModel, len(searchNames)) - for i := range searchNames { - models[i] = mongo.SearchIndexModel{ + models := []mongo.SearchIndexModel{ + { Definition: definition, - Name: &searchNames[i], - } + Options: options.SearchIndexes().SetName("test-search-index-1"), + }, + { + Definition: definition, + Options: options.SearchIndexes().SetName("test-search-index-2"), + }, } indexes, err := view.CreateMany(ctx, models) require.NoError(mt, err, "failed to create index") require.Equal(mt, len(indexes), 2, "expected 2 indexes") - for _, searchName := range searchNames { - require.Contains(mt, indexes, searchName) + for _, model := range models { + require.Contains(mt, indexes, *model.Options.Name) } - getDocument := func(index string) bson.Raw { - t.Helper() - + getDocument := func(opts *options.SearchIndexesOptions) bson.Raw { for { - cursor, err := view.List(ctx, &index) + cursor, err := view.List(ctx, opts) require.NoError(mt, err, "failed to list") if !cursor.Next(ctx) { @@ -119,19 +119,19 @@ func TestSearchIndexProse(t *testing.T) { } var wg sync.WaitGroup - wg.Add(len(searchNames)) - for i := range searchNames { - go func(name string) { + wg.Add(len(models)) + for i := range models { + go func(opts *options.SearchIndexesOptions) { defer wg.Done() - doc := getDocument(name) + doc := getDocument(opts) require.NotNil(mt, doc, "got empty document") - assert.Equal(mt, name, doc.Lookup("name").StringValue(), "unmatched name") + assert.Equal(mt, *opts.Name, doc.Lookup("name").StringValue(), "unmatched name") expected, err := bson.Marshal(definition) require.NoError(mt, err, "failed to marshal definition") actual := doc.Lookup("latestDefinition").Value assert.Equal(mt, expected, actual, "unmatched definition") - }(searchNames[i]) + }(models[i].Options) } wg.Wait() }) @@ -146,17 +146,17 @@ func TestSearchIndexProse(t *testing.T) { definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} searchName := "test-search-index" - model := mongo.SearchIndexModel{ + opts := options.SearchIndexes().SetName(searchName) + index, err := view.CreateOne(ctx, mongo.SearchIndexModel{ Definition: definition, - Name: &searchName, - } - index, err := view.CreateOne(ctx, model) + Options: opts, + }) require.NoError(mt, err, "failed to create index") require.Equal(mt, searchName, index, "unmatched name") var doc bson.Raw for doc == nil { - cursor, err := view.List(ctx, &index) + cursor, err := view.List(ctx, opts) require.NoError(mt, err, "failed to list") if !cursor.Next(ctx) { @@ -175,7 +175,7 @@ func TestSearchIndexProse(t *testing.T) { err = view.DropOne(ctx, searchName) require.NoError(mt, err, "failed to drop index") for { - cursor, err := view.List(ctx, &index) + cursor, err := view.List(ctx, opts) require.NoError(mt, err, "failed to list") if !cursor.Next(ctx) { @@ -196,19 +196,17 @@ func TestSearchIndexProse(t *testing.T) { definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}} searchName := "test-search-index" - model := mongo.SearchIndexModel{ + opts := options.SearchIndexes().SetName(searchName) + index, err := view.CreateOne(ctx, mongo.SearchIndexModel{ Definition: definition, - Name: &searchName, - } - index, err := view.CreateOne(ctx, model) + Options: opts, + }) require.NoError(mt, err, "failed to create index") require.Equal(mt, searchName, index, "unmatched name") - getDocument := func(index string) bson.Raw { - t.Helper() - + getDocument := func() bson.Raw { for { - cursor, err := view.List(ctx, &index) + cursor, err := view.List(ctx, opts) require.NoError(mt, err, "failed to list") if !cursor.Next(ctx) { @@ -222,14 +220,14 @@ func TestSearchIndexProse(t *testing.T) { } } - doc := getDocument(searchName) + doc := getDocument() require.NotNil(mt, doc, "got empty document") require.Equal(mt, searchName, doc.Lookup("name").StringValue(), "unmatched name") definition = bson.D{{"mappings", bson.D{{"dynamic", true}}}} err = view.UpdateOne(ctx, searchName, definition) require.NoError(mt, err, "failed to drop index") - doc = getDocument(searchName) + doc = getDocument() require.NotNil(mt, doc, "got empty document") assert.Equal(mt, searchName, doc.Lookup("name").StringValue(), "unmatched name") assert.Equal(mt, "READY", doc.Lookup("status").StringValue(), "unexpected status") diff --git a/mongo/integration/unified/collection_operation_execution.go b/mongo/integration/unified/collection_operation_execution.go index 6da5a9d401..e91658ca55 100644 --- a/mongo/integration/unified/collection_operation_execution.go +++ b/mongo/integration/unified/collection_operation_execution.go @@ -1117,7 +1117,7 @@ func executeListSearchIndexes(ctx context.Context, operation *operation) (*opera return nil, err } - var name *string + searchIdxOpts := options.SearchIndexes() var opts []*options.ListSearchIndexesOptions elems, err := operation.Arguments.Elements() @@ -1130,8 +1130,7 @@ func executeListSearchIndexes(ctx context.Context, operation *operation) (*opera switch key { case "name": - n := val.StringValue() - name = &n + searchIdxOpts.SetName(val.StringValue()) case "aggregationOptions": var opt options.AggregateOptions err = bson.Unmarshal(val.Document(), &opt) @@ -1146,7 +1145,7 @@ func executeListSearchIndexes(ctx context.Context, operation *operation) (*opera } } - _, err = coll.SearchIndexes().List(ctx, name, opts...) + _, err = coll.SearchIndexes().List(ctx, searchIdxOpts, opts...) return newValueResult(bsontype.Null, nil, err), nil } diff --git a/mongo/options/searchindexoptions.go b/mongo/options/searchindexoptions.go index 5f7b771153..9774d615ba 100644 --- a/mongo/options/searchindexoptions.go +++ b/mongo/options/searchindexoptions.go @@ -6,6 +6,22 @@ package options +// SearchIndexesOptions represents options that can be used to configure a SearchIndexView. +type SearchIndexesOptions struct { + Name *string +} + +// SearchIndexes creates a new SearchIndexesOptions instance. +func SearchIndexes() *SearchIndexesOptions { + return &SearchIndexesOptions{} +} + +// SetName sets the value for the Name field. +func (sio *SearchIndexesOptions) SetName(name string) *SearchIndexesOptions { + sio.Name = &name + return sio +} + // CreateSearchIndexesOptions represents options that can be used to configure a SearchIndexView.CreateOne or // SearchIndexView.CreateMany operation. type CreateSearchIndexesOptions struct { diff --git a/mongo/search_index_view.go b/mongo/search_index_view.go index fdd12d2202..7e10843275 100644 --- a/mongo/search_index_view.go +++ b/mongo/search_index_view.go @@ -32,8 +32,8 @@ type SearchIndexModel struct { // See https://www.mongodb.com/docs/atlas/atlas-search/create-index/ for reference. Definition interface{} - // The name for the index, if present. - Name *string + // The search index options. + Options *options.SearchIndexesOptions } // List executes a listSearchIndexes command and returns a cursor over the search indexes in the collection. @@ -42,14 +42,14 @@ type SearchIndexModel struct { // // The opts parameter can be used to specify options for this operation (see the options.ListSearchIndexesOptions // documentation). -func (siv SearchIndexView) List(ctx context.Context, name *string, opts ...*options.ListSearchIndexesOptions) (*Cursor, error) { +func (siv SearchIndexView) List(ctx context.Context, searchIdxOpts *options.SearchIndexesOptions, opts ...*options.ListSearchIndexesOptions) (*Cursor, error) { if ctx == nil { ctx = context.Background() } index := bson.D{} - if name != nil { - index = bson.D{{"name", *name}} + if searchIdxOpts != nil && searchIdxOpts.Name != nil { + index = bson.D{{"name", *searchIdxOpts.Name}} } aggregateOpts := make([]*options.AggregateOptions, len(opts)) @@ -94,8 +94,8 @@ func (siv SearchIndexView) CreateMany(ctx context.Context, models []SearchIndexM var iidx int32 iidx, indexes = bsoncore.AppendDocumentElementStart(indexes, strconv.Itoa(i)) - if model.Name != nil { - indexes = bsoncore.AppendStringElement(indexes, "name", *model.Name) + if model.Options != nil && model.Options.Name != nil { + indexes = bsoncore.AppendStringElement(indexes, "name", *model.Options.Name) } indexes = bsoncore.AppendDocumentElement(indexes, "definition", definition) From 0a4389cf1a0a3830b934cb5f26e0d7e408d1816a Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Tue, 5 Sep 2023 13:14:54 -0400 Subject: [PATCH 10/11] update unified test suite. --- .../unified/collection_operation_execution.go | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/mongo/integration/unified/collection_operation_execution.go b/mongo/integration/unified/collection_operation_execution.go index e91658ca55..83fc736b3a 100644 --- a/mongo/integration/unified/collection_operation_execution.go +++ b/mongo/integration/unified/collection_operation_execution.go @@ -322,10 +322,17 @@ func executeCreateSearchIndex(ctx context.Context, operation *operation) (*opera switch key { case "model": - err = bson.Unmarshal(val.Document(), &model) + var m struct { + Definition interface{} + Name *string + } + err = bson.Unmarshal(val.Document(), &m) if err != nil { return nil, err } + model.Definition = m.Definition + model.Options = options.SearchIndexes() + model.Options.Name = m.Name default: return nil, fmt.Errorf("unrecognized createSearchIndex option %q", key) } @@ -358,11 +365,19 @@ func executeCreateSearchIndexes(ctx context.Context, operation *operation) (*ope return nil, err } for _, val := range vals { - var model mongo.SearchIndexModel - err = bson.Unmarshal(val.Value, &model) + var m struct { + Definition interface{} + Name *string + } + err = bson.Unmarshal(val.Value, &m) if err != nil { return nil, err } + model := mongo.SearchIndexModel{ + Definition: m.Definition, + Options: options.SearchIndexes(), + } + model.Options.Name = m.Name models = append(models, model) } default: From 0c53f6f2fd2021f5f56184b63418deb3f71a27ff Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Tue, 19 Sep 2023 10:15:44 -0400 Subject: [PATCH 11/11] Format code --- mongo/search_index_view.go | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/mongo/search_index_view.go b/mongo/search_index_view.go index 7e10843275..6a7871531e 100644 --- a/mongo/search_index_view.go +++ b/mongo/search_index_view.go @@ -42,7 +42,11 @@ type SearchIndexModel struct { // // The opts parameter can be used to specify options for this operation (see the options.ListSearchIndexesOptions // documentation). -func (siv SearchIndexView) List(ctx context.Context, searchIdxOpts *options.SearchIndexesOptions, opts ...*options.ListSearchIndexesOptions) (*Cursor, error) { +func (siv SearchIndexView) List( + ctx context.Context, + searchIdxOpts *options.SearchIndexesOptions, + opts ...*options.ListSearchIndexesOptions, +) (*Cursor, error) { if ctx == nil { ctx = context.Background() } @@ -62,7 +66,11 @@ func (siv SearchIndexView) List(ctx context.Context, searchIdxOpts *options.Sear // CreateOne executes a createSearchIndexes command to create a search index on the collection and returns the name of the new // search index. See the SearchIndexView.CreateMany documentation for more information and an example. -func (siv SearchIndexView) CreateOne(ctx context.Context, model SearchIndexModel, opts ...*options.CreateSearchIndexesOptions) (string, error) { +func (siv SearchIndexView) CreateOne( + ctx context.Context, + model SearchIndexModel, + opts ...*options.CreateSearchIndexesOptions, +) (string, error) { names, err := siv.CreateMany(ctx, []SearchIndexModel{model}, opts...) if err != nil { return "", err @@ -78,7 +86,11 @@ func (siv SearchIndexView) CreateOne(ctx context.Context, model SearchIndexModel // // The opts parameter can be used to specify options for this operation (see the options.CreateSearchIndexesOptions // documentation). -func (siv SearchIndexView) CreateMany(ctx context.Context, models []SearchIndexModel, _ ...*options.CreateSearchIndexesOptions) ([]string, error) { +func (siv SearchIndexView) CreateMany( + ctx context.Context, + models []SearchIndexModel, + _ ...*options.CreateSearchIndexesOptions, +) ([]string, error) { var indexes bsoncore.Document aidx, indexes := bsoncore.AppendArrayStart(indexes) @@ -160,7 +172,11 @@ func (siv SearchIndexView) CreateMany(ctx context.Context, models []SearchIndexM // // The opts parameter can be used to specify options for this operation (see the options.DropSearchIndexOptions // documentation). -func (siv SearchIndexView) DropOne(ctx context.Context, name string, _ ...*options.DropSearchIndexOptions) error { +func (siv SearchIndexView) DropOne( + ctx context.Context, + name string, + _ ...*options.DropSearchIndexOptions, +) error { if name == "*" { return ErrMultipleIndexDrop } @@ -212,7 +228,12 @@ func (siv SearchIndexView) DropOne(ctx context.Context, name string, _ ...*optio // // The opts parameter can be used to specify options for this operation (see the options.UpdateSearchIndexOptions // documentation). -func (siv SearchIndexView) UpdateOne(ctx context.Context, name string, definition interface{}, _ ...*options.UpdateSearchIndexOptions) error { +func (siv SearchIndexView) UpdateOne( + ctx context.Context, + name string, + definition interface{}, + _ ...*options.UpdateSearchIndexOptions, +) error { if definition == nil { return fmt.Errorf("search index definition cannot be nil") }