diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 6e71fd660f..1d6dc12f5b 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -688,6 +688,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: @@ -2259,6 +2270,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 @@ -2618,6 +2637,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" @@ -2766,6 +2823,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 66f5b137e5..d08f6eba3e 100644 --- a/Makefile +++ b/Makefile @@ -159,6 +159,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/integration -run TestSearchIndexProse -v -timeout $(TEST_TIMEOUT)s >> test.suite + .PHONY: evg-test-ocsp evg-test-ocsp: go test -v ./mongo -run TestOCSP $(OCSP_TLS_SHOULD_SUCCEED) >> test.suite diff --git a/bson/raw.go b/bson/raw.go index fe990a1771..130da61ba0 100644 --- a/bson/raw.go +++ b/bson/raw.go @@ -60,12 +60,19 @@ 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() + 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, 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 6abbea9792..6699a6d4c0 100644 --- a/mongo/collection.go +++ b/mongo/collection.go @@ -1773,6 +1773,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/search_index_prose_test.go b/mongo/integration/search_index_prose_test.go new file mode 100644 index 0000000000..002150c36e --- /dev/null +++ b/mongo/integration/search_index_prose_test.go @@ -0,0 +1,253 @@ +// 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)) + + 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" + opts := options.SearchIndexes().SetName(searchName) + index, err := view.CreateOne(ctx, mongo.SearchIndexModel{ + Definition: definition, + 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, opts) + 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}}}} + models := []mongo.SearchIndexModel{ + { + Definition: definition, + 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 _, model := range models { + require.Contains(mt, indexes, *model.Options.Name) + } + + getDocument := func(opts *options.SearchIndexesOptions) bson.Raw { + for { + cursor, err := view.List(ctx, opts) + 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(len(models)) + for i := range models { + go func(opts *options.SearchIndexesOptions) { + defer wg.Done() + + doc := getDocument(opts) + require.NotNil(mt, doc, "got empty document") + 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") + }(models[i].Options) + } + 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" + opts := options.SearchIndexes().SetName(searchName) + index, err := view.CreateOne(ctx, mongo.SearchIndexModel{ + Definition: definition, + 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, opts) + 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, opts) + 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" + opts := options.SearchIndexes().SetName(searchName) + index, err := view.CreateOne(ctx, mongo.SearchIndexModel{ + Definition: definition, + Options: opts, + }) + require.NoError(mt, err, "failed to create index") + require.Equal(mt, searchName, index, "unmatched name") + + getDocument := func() bson.Raw { + for { + cursor, err := view.List(ctx, opts) + 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() + 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() + 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/integration/unified/collection_operation_execution.go b/mongo/integration/unified/collection_operation_execution.go index 18c6e040fe..83fc736b3a 100644 --- a/mongo/integration/unified/collection_operation_execution.go +++ b/mongo/integration/unified/collection_operation_execution.go @@ -304,6 +304,95 @@ 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": + 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) + } + } + + 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 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: + 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 +611,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 +1126,44 @@ 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 + } + + searchIdxOpts := options.SearchIndexes() + var opts []*options.ListSearchIndexesOptions + + 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": + searchIdxOpts.SetName(val.StringValue()) + case "aggregationOptions": + var opt options.AggregateOptions + err = bson.Unmarshal(val.Document(), &opt) + if err != nil { + return nil, err + } + opts = append(opts, &options.ListSearchIndexesOptions{ + AggregateOpts: &opt, + }) + default: + return nil, fmt.Errorf("unrecognized listSearchIndexes option %q", key) + } + } + + _, err = coll.SearchIndexes().List(ctx, searchIdxOpts, 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 +1300,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 bd46820009..59aa36ae8c 100644 --- a/mongo/integration/unified/operation.go +++ b/mongo/integration/unified/operation.go @@ -150,10 +150,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": @@ -164,6 +168,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": @@ -182,6 +188,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 { @@ -197,6 +205,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 415356d09e..8871a48127 100644 --- a/mongo/integration/unified/unified_spec_test.go +++ b/mongo/integration/unified/unified_spec_test.go @@ -32,6 +32,7 @@ var ( "server-selection/logging", "server-discovery-and-monitoring/unified", "run-command", + "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..9774d615ba --- /dev/null +++ b/mongo/options/searchindexoptions.go @@ -0,0 +1,41 @@ +// 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 + +// 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 { +} + +// 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. +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..6a7871531e --- /dev/null +++ b/mongo/search_index_view.go @@ -0,0 +1,279 @@ +// 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" + "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 search index options. + Options *options.SearchIndexesOptions +} + +// List executes a listSearchIndexes command and returns a cursor over the search indexes in the collection. +// +// 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, + searchIdxOpts *options.SearchIndexesOptions, + opts ...*options.ListSearchIndexesOptions, +) (*Cursor, error) { + if ctx == nil { + ctx = context.Background() + } + + index := bson.D{} + if searchIdxOpts != nil && searchIdxOpts.Name != nil { + index = bson.D{{"name", *searchIdxOpts.Name}} + } + + aggregateOpts := make([]*options.AggregateOptions, len(opts)) + for i, opt := range opts { + aggregateOpts[i] = opt.AggregateOpts + } + + 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.Options != nil && model.Options.Name != nil { + indexes = bsoncore.AppendStringElement(indexes, "name", *model.Options.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) + + 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. +// +// 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/testdata/index-management/createSearchIndex.json b/testdata/index-management/createSearchIndex.json new file mode 100644 index 0000000000..7e27b6eb76 --- /dev/null +++ b/testdata/index-management/createSearchIndex.json @@ -0,0 +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" + } + } + ], + "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, + "errorContains": "Search index commands are only supported with Atlas" + } + } + ], + "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, + "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" + } + } + } + ] + } + ] + } + ] +} \ 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..6aa56f3bc4 --- /dev/null +++ b/testdata/index-management/createSearchIndex.yml @@ -0,0 +1,62 @@ +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: + # 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: + - 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: + # 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: + - 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..558267ace0 --- /dev/null +++ b/testdata/index-management/createSearchIndexes.json @@ -0,0 +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" + } + } + ], + "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" + } + } + } + ] + } + ] + }, + { + "description": "no name provided for an index definition", + "operations": [ + { + "name": "createSearchIndexes", + "object": "collection0", + "arguments": { + "models": [ + { + "definition": { + "mappings": { + "dynamic": 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 + } + } + } + ], + "$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, + "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" + } + } + } + ] + } + ] + } + ] +} \ 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..54a6e84ccb --- /dev/null +++ b/testdata/index-management/createSearchIndexes.yml @@ -0,0 +1,83 @@ +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: + # 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: + - 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: + # 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: + - 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: + # 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: + - 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..9d716b5a58 --- /dev/null +++ b/testdata/index-management/dropSearchIndex.json @@ -0,0 +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" + } + } + ], + "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" + } + } + } + ] + } + ] + } + ] +} \ 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..f47bebe32e --- /dev/null +++ b/testdata/index-management/dropSearchIndex.yml @@ -0,0 +1,42 @@ +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: + # 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: + - commandStartedEvent: + command: + dropSearchIndex: *collection0 + name: *indexName + $db: *database0 \ No newline at end of file diff --git a/testdata/index-management/listSearchIndexes.json b/testdata/index-management/listSearchIndexes.json new file mode 100644 index 0000000000..ca2643ebc1 --- /dev/null +++ b/testdata/index-management/listSearchIndexes.json @@ -0,0 +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" + } + } + ], + "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": {} + } + ] + } + } + } + ] + } + ] + }, + { + "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" + } + } + ], + "$db": "database0" + } + } + } + ] + } + ] + }, + { + "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" + } + } + ], + "$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..a50becdf1d --- /dev/null +++ b/testdata/index-management/listSearchIndexes.yml @@ -0,0 +1,85 @@ +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: + # 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: + - 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: + # 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: + - 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: + # 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: + - 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..4cc3c530f6 --- /dev/null +++ b/testdata/index-management/updateSearchIndex.json @@ -0,0 +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" + } + } + ], + "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" + } + } + } + ] + } + ] + } + ] +} \ 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..7b6f3fbdc4 --- /dev/null +++ b/testdata/index-management/updateSearchIndex.yml @@ -0,0 +1,44 @@ +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: + # 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: + - commandStartedEvent: + command: + updateSearchIndex: *collection0 + name: *indexName + definition: *definition + $db: *database0 \ No newline at end of file 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..25cde8154b --- /dev/null +++ b/x/mongo/driver/operation/drop_search_index.go @@ -0,0 +1,227 @@ +// 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" +) + +// 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 +}