You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
504 lines
16 KiB
504 lines
16 KiB
// Copyright (C) MongoDB, Inc. 2017-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 ( |
|
"bytes" |
|
"context" |
|
"errors" |
|
"fmt" |
|
"strconv" |
|
"time" |
|
|
|
"go.mongodb.org/mongo-driver/bson" |
|
"go.mongodb.org/mongo-driver/bson/bsontype" |
|
"go.mongodb.org/mongo-driver/mongo/description" |
|
"go.mongodb.org/mongo-driver/mongo/options" |
|
"go.mongodb.org/mongo-driver/mongo/readpref" |
|
"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" |
|
) |
|
|
|
// ErrInvalidIndexValue is returned if an index is created with a keys document that has a value that is not a number |
|
// or string. |
|
var ErrInvalidIndexValue = errors.New("invalid index value") |
|
|
|
// ErrNonStringIndexName is returned if an index is created with a name that is not a string. |
|
var ErrNonStringIndexName = errors.New("index name must be a string") |
|
|
|
// ErrMultipleIndexDrop is returned if multiple indexes would be dropped from a call to IndexView.DropOne. |
|
var ErrMultipleIndexDrop = errors.New("multiple indexes would be dropped") |
|
|
|
// IndexView is a type that can be used to create, drop, and list indexes on a collection. An IndexView for a collection |
|
// can be created by a call to Collection.Indexes(). |
|
type IndexView struct { |
|
coll *Collection |
|
} |
|
|
|
// IndexModel represents a new index to be created. |
|
type IndexModel struct { |
|
// A document describing which keys should be used for the index. It cannot be nil. This must be an order-preserving |
|
// type such as bson.D. Map types such as bson.M are not valid. See https://docs.mongodb.com/manual/indexes/#indexes |
|
// for examples of valid documents. |
|
Keys interface{} |
|
|
|
// The options to use to create the index. |
|
Options *options.IndexOptions |
|
} |
|
|
|
func isNamespaceNotFoundError(err error) bool { |
|
if de, ok := err.(driver.Error); ok { |
|
return de.Code == 26 |
|
} |
|
return false |
|
} |
|
|
|
// List executes a listIndexes command and returns a cursor over the indexes in the collection. |
|
// |
|
// The opts parameter can be used to specify options for this operation (see the options.ListIndexesOptions |
|
// documentation). |
|
// |
|
// For more information about the command, see https://docs.mongodb.com/manual/reference/command/listIndexes/. |
|
func (iv IndexView) List(ctx context.Context, opts ...*options.ListIndexesOptions) (*Cursor, error) { |
|
if ctx == nil { |
|
ctx = context.Background() |
|
} |
|
|
|
sess := sessionFromContext(ctx) |
|
if sess == nil && iv.coll.client.sessionPool != nil { |
|
var err error |
|
sess, err = session.NewClientSession(iv.coll.client.sessionPool, iv.coll.client.id, session.Implicit) |
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
|
|
err := iv.coll.client.validSession(sess) |
|
if err != nil { |
|
closeImplicitSession(sess) |
|
return nil, err |
|
} |
|
|
|
selector := description.CompositeSelector([]description.ServerSelector{ |
|
description.ReadPrefSelector(readpref.Primary()), |
|
description.LatencySelector(iv.coll.client.localThreshold), |
|
}) |
|
selector = makeReadPrefSelector(sess, selector, iv.coll.client.localThreshold) |
|
op := operation.NewListIndexes(). |
|
Session(sess).CommandMonitor(iv.coll.client.monitor). |
|
ServerSelector(selector).ClusterClock(iv.coll.client.clock). |
|
Database(iv.coll.db.name).Collection(iv.coll.name). |
|
Deployment(iv.coll.client.deployment).ServerAPI(iv.coll.client.serverAPI) |
|
|
|
cursorOpts := iv.coll.client.createBaseCursorOptions() |
|
lio := options.MergeListIndexesOptions(opts...) |
|
if lio.BatchSize != nil { |
|
op = op.BatchSize(*lio.BatchSize) |
|
cursorOpts.BatchSize = *lio.BatchSize |
|
} |
|
if lio.MaxTime != nil { |
|
op = op.MaxTimeMS(int64(*lio.MaxTime / time.Millisecond)) |
|
} |
|
retry := driver.RetryNone |
|
if iv.coll.client.retryReads { |
|
retry = driver.RetryOncePerCommand |
|
} |
|
op.Retry(retry) |
|
|
|
err = op.Execute(ctx) |
|
if err != nil { |
|
// for namespaceNotFound errors, return an empty cursor and do not throw an error |
|
closeImplicitSession(sess) |
|
if isNamespaceNotFoundError(err) { |
|
return newEmptyCursor(), nil |
|
} |
|
|
|
return nil, replaceErrors(err) |
|
} |
|
|
|
bc, err := op.Result(cursorOpts) |
|
if err != nil { |
|
closeImplicitSession(sess) |
|
return nil, replaceErrors(err) |
|
} |
|
cursor, err := newCursorWithSession(bc, iv.coll.registry, sess) |
|
return cursor, replaceErrors(err) |
|
} |
|
|
|
// ListSpecifications executes a List command and returns a slice of returned IndexSpecifications |
|
func (iv IndexView) ListSpecifications(ctx context.Context, opts ...*options.ListIndexesOptions) ([]*IndexSpecification, error) { |
|
cursor, err := iv.List(ctx, opts...) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
var results []*IndexSpecification |
|
err = cursor.All(ctx, &results) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
ns := iv.coll.db.Name() + "." + iv.coll.Name() |
|
for _, res := range results { |
|
// Pre-4.4 servers report a namespace in their responses, so we only set Namespace manually if it was not in |
|
// the response. |
|
res.Namespace = ns |
|
} |
|
|
|
return results, nil |
|
} |
|
|
|
// CreateOne executes a createIndexes command to create an index on the collection and returns the name of the new |
|
// index. See the IndexView.CreateMany documentation for more information and an example. |
|
func (iv IndexView) CreateOne(ctx context.Context, model IndexModel, opts ...*options.CreateIndexesOptions) (string, error) { |
|
names, err := iv.CreateMany(ctx, []IndexModel{model}, opts...) |
|
if err != nil { |
|
return "", err |
|
} |
|
|
|
return names[0], nil |
|
} |
|
|
|
// CreateMany executes a createIndexes command to create multiple indexes on the collection and returns the names of |
|
// the new indexes. |
|
// |
|
// For each IndexModel in the models parameter, the index name can be specified via the Options field. If a name is not |
|
// given, it will be generated from the Keys document. |
|
// |
|
// The opts parameter can be used to specify options for this operation (see the options.CreateIndexesOptions |
|
// documentation). |
|
// |
|
// For more information about the command, see https://docs.mongodb.com/manual/reference/command/createIndexes/. |
|
func (iv IndexView) CreateMany(ctx context.Context, models []IndexModel, opts ...*options.CreateIndexesOptions) ([]string, error) { |
|
names := make([]string, 0, len(models)) |
|
|
|
var indexes bsoncore.Document |
|
aidx, indexes := bsoncore.AppendArrayStart(indexes) |
|
|
|
for i, model := range models { |
|
if model.Keys == nil { |
|
return nil, fmt.Errorf("index model keys cannot be nil") |
|
} |
|
|
|
keys, err := transformBsoncoreDocument(iv.coll.registry, model.Keys, false, "keys") |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
name, err := getOrGenerateIndexName(keys, model) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
names = append(names, name) |
|
|
|
var iidx int32 |
|
iidx, indexes = bsoncore.AppendDocumentElementStart(indexes, strconv.Itoa(i)) |
|
indexes = bsoncore.AppendDocumentElement(indexes, "key", keys) |
|
|
|
if model.Options == nil { |
|
model.Options = options.Index() |
|
} |
|
model.Options.SetName(name) |
|
|
|
optsDoc, err := iv.createOptionsDoc(model.Options) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
indexes = bsoncore.AppendDocument(indexes, optsDoc) |
|
|
|
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 && iv.coll.client.sessionPool != nil { |
|
sess, err = session.NewClientSession(iv.coll.client.sessionPool, iv.coll.client.id, session.Implicit) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer sess.EndSession() |
|
} |
|
|
|
err = iv.coll.client.validSession(sess) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
wc := iv.coll.writeConcern |
|
if sess.TransactionRunning() { |
|
wc = nil |
|
} |
|
if !writeconcern.AckWrite(wc) { |
|
sess = nil |
|
} |
|
|
|
selector := makePinnedSelector(sess, iv.coll.writeSelector) |
|
|
|
option := options.MergeCreateIndexesOptions(opts...) |
|
|
|
op := operation.NewCreateIndexes(indexes). |
|
Session(sess).WriteConcern(wc).ClusterClock(iv.coll.client.clock). |
|
Database(iv.coll.db.name).Collection(iv.coll.name).CommandMonitor(iv.coll.client.monitor). |
|
Deployment(iv.coll.client.deployment).ServerSelector(selector).ServerAPI(iv.coll.client.serverAPI) |
|
|
|
if option.MaxTime != nil { |
|
op.MaxTimeMS(int64(*option.MaxTime / time.Millisecond)) |
|
} |
|
if option.CommitQuorum != nil { |
|
commitQuorum, err := transformValue(iv.coll.registry, option.CommitQuorum, true, "commitQuorum") |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
op.CommitQuorum(commitQuorum) |
|
} |
|
|
|
err = op.Execute(ctx) |
|
if err != nil { |
|
_, err = processWriteError(err) |
|
return nil, err |
|
} |
|
|
|
return names, nil |
|
} |
|
|
|
func (iv IndexView) createOptionsDoc(opts *options.IndexOptions) (bsoncore.Document, error) { |
|
optsDoc := bsoncore.Document{} |
|
if opts.Background != nil { |
|
optsDoc = bsoncore.AppendBooleanElement(optsDoc, "background", *opts.Background) |
|
} |
|
if opts.ExpireAfterSeconds != nil { |
|
optsDoc = bsoncore.AppendInt32Element(optsDoc, "expireAfterSeconds", *opts.ExpireAfterSeconds) |
|
} |
|
if opts.Name != nil { |
|
optsDoc = bsoncore.AppendStringElement(optsDoc, "name", *opts.Name) |
|
} |
|
if opts.Sparse != nil { |
|
optsDoc = bsoncore.AppendBooleanElement(optsDoc, "sparse", *opts.Sparse) |
|
} |
|
if opts.StorageEngine != nil { |
|
doc, err := transformBsoncoreDocument(iv.coll.registry, opts.StorageEngine, true, "storageEngine") |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
optsDoc = bsoncore.AppendDocumentElement(optsDoc, "storageEngine", doc) |
|
} |
|
if opts.Unique != nil { |
|
optsDoc = bsoncore.AppendBooleanElement(optsDoc, "unique", *opts.Unique) |
|
} |
|
if opts.Version != nil { |
|
optsDoc = bsoncore.AppendInt32Element(optsDoc, "v", *opts.Version) |
|
} |
|
if opts.DefaultLanguage != nil { |
|
optsDoc = bsoncore.AppendStringElement(optsDoc, "default_language", *opts.DefaultLanguage) |
|
} |
|
if opts.LanguageOverride != nil { |
|
optsDoc = bsoncore.AppendStringElement(optsDoc, "language_override", *opts.LanguageOverride) |
|
} |
|
if opts.TextVersion != nil { |
|
optsDoc = bsoncore.AppendInt32Element(optsDoc, "textIndexVersion", *opts.TextVersion) |
|
} |
|
if opts.Weights != nil { |
|
doc, err := transformBsoncoreDocument(iv.coll.registry, opts.Weights, true, "weights") |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
optsDoc = bsoncore.AppendDocumentElement(optsDoc, "weights", doc) |
|
} |
|
if opts.SphereVersion != nil { |
|
optsDoc = bsoncore.AppendInt32Element(optsDoc, "2dsphereIndexVersion", *opts.SphereVersion) |
|
} |
|
if opts.Bits != nil { |
|
optsDoc = bsoncore.AppendInt32Element(optsDoc, "bits", *opts.Bits) |
|
} |
|
if opts.Max != nil { |
|
optsDoc = bsoncore.AppendDoubleElement(optsDoc, "max", *opts.Max) |
|
} |
|
if opts.Min != nil { |
|
optsDoc = bsoncore.AppendDoubleElement(optsDoc, "min", *opts.Min) |
|
} |
|
if opts.BucketSize != nil { |
|
optsDoc = bsoncore.AppendInt32Element(optsDoc, "bucketSize", *opts.BucketSize) |
|
} |
|
if opts.PartialFilterExpression != nil { |
|
doc, err := transformBsoncoreDocument(iv.coll.registry, opts.PartialFilterExpression, true, "partialFilterExpression") |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
optsDoc = bsoncore.AppendDocumentElement(optsDoc, "partialFilterExpression", doc) |
|
} |
|
if opts.Collation != nil { |
|
optsDoc = bsoncore.AppendDocumentElement(optsDoc, "collation", bsoncore.Document(opts.Collation.ToDocument())) |
|
} |
|
if opts.WildcardProjection != nil { |
|
doc, err := transformBsoncoreDocument(iv.coll.registry, opts.WildcardProjection, true, "wildcardProjection") |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
optsDoc = bsoncore.AppendDocumentElement(optsDoc, "wildcardProjection", doc) |
|
} |
|
if opts.Hidden != nil { |
|
optsDoc = bsoncore.AppendBooleanElement(optsDoc, "hidden", *opts.Hidden) |
|
} |
|
|
|
return optsDoc, nil |
|
} |
|
|
|
func (iv IndexView) drop(ctx context.Context, name string, opts ...*options.DropIndexesOptions) (bson.Raw, error) { |
|
if ctx == nil { |
|
ctx = context.Background() |
|
} |
|
|
|
sess := sessionFromContext(ctx) |
|
if sess == nil && iv.coll.client.sessionPool != nil { |
|
var err error |
|
sess, err = session.NewClientSession(iv.coll.client.sessionPool, iv.coll.client.id, session.Implicit) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer sess.EndSession() |
|
} |
|
|
|
err := iv.coll.client.validSession(sess) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
wc := iv.coll.writeConcern |
|
if sess.TransactionRunning() { |
|
wc = nil |
|
} |
|
if !writeconcern.AckWrite(wc) { |
|
sess = nil |
|
} |
|
|
|
selector := makePinnedSelector(sess, iv.coll.writeSelector) |
|
|
|
dio := options.MergeDropIndexesOptions(opts...) |
|
op := operation.NewDropIndexes(name). |
|
Session(sess).WriteConcern(wc).CommandMonitor(iv.coll.client.monitor). |
|
ServerSelector(selector).ClusterClock(iv.coll.client.clock). |
|
Database(iv.coll.db.name).Collection(iv.coll.name). |
|
Deployment(iv.coll.client.deployment).ServerAPI(iv.coll.client.serverAPI) |
|
if dio.MaxTime != nil { |
|
op.MaxTimeMS(int64(*dio.MaxTime / time.Millisecond)) |
|
} |
|
|
|
err = op.Execute(ctx) |
|
if err != nil { |
|
return nil, replaceErrors(err) |
|
} |
|
|
|
// TODO: it's weird to return a bson.Raw here because we have to convert the result back to BSON |
|
ridx, res := bsoncore.AppendDocumentStart(nil) |
|
res = bsoncore.AppendInt32Element(res, "nIndexesWas", op.Result().NIndexesWas) |
|
res, _ = bsoncore.AppendDocumentEnd(res, ridx) |
|
return res, nil |
|
} |
|
|
|
// DropOne executes a dropIndexes operation to drop an index on the collection. If the operation succeeds, this returns |
|
// a BSON document in the form {nIndexesWas: <int32>}. The "nIndexesWas" field in the response contains the number of |
|
// indexes that existed prior to the drop. |
|
// |
|
// The name parameter should be the name of the index to drop. If the name is "*", ErrMultipleIndexDrop will be returned |
|
// without running the command because doing so would drop all indexes. |
|
// |
|
// The opts parameter can be used to specify options for this operation (see the options.DropIndexesOptions |
|
// documentation). |
|
// |
|
// For more information about the command, see https://docs.mongodb.com/manual/reference/command/dropIndexes/. |
|
func (iv IndexView) DropOne(ctx context.Context, name string, opts ...*options.DropIndexesOptions) (bson.Raw, error) { |
|
if name == "*" { |
|
return nil, ErrMultipleIndexDrop |
|
} |
|
|
|
return iv.drop(ctx, name, opts...) |
|
} |
|
|
|
// DropAll executes a dropIndexes operation to drop all indexes on the collection. If the operation succeeds, this |
|
// returns a BSON document in the form {nIndexesWas: <int32>}. The "nIndexesWas" field in the response contains the |
|
// number of indexes that existed prior to the drop. |
|
// |
|
// The opts parameter can be used to specify options for this operation (see the options.DropIndexesOptions |
|
// documentation). |
|
// |
|
// For more information about the command, see https://docs.mongodb.com/manual/reference/command/dropIndexes/. |
|
func (iv IndexView) DropAll(ctx context.Context, opts ...*options.DropIndexesOptions) (bson.Raw, error) { |
|
return iv.drop(ctx, "*", opts...) |
|
} |
|
|
|
func getOrGenerateIndexName(keySpecDocument bsoncore.Document, model IndexModel) (string, error) { |
|
if model.Options != nil && model.Options.Name != nil { |
|
return *model.Options.Name, nil |
|
} |
|
|
|
name := bytes.NewBufferString("") |
|
first := true |
|
|
|
elems, err := keySpecDocument.Elements() |
|
if err != nil { |
|
return "", err |
|
} |
|
for _, elem := range elems { |
|
if !first { |
|
_, err := name.WriteRune('_') |
|
if err != nil { |
|
return "", err |
|
} |
|
} |
|
|
|
_, err := name.WriteString(elem.Key()) |
|
if err != nil { |
|
return "", err |
|
} |
|
|
|
_, err = name.WriteRune('_') |
|
if err != nil { |
|
return "", err |
|
} |
|
|
|
var value string |
|
|
|
bsonValue := elem.Value() |
|
switch bsonValue.Type { |
|
case bsontype.Int32: |
|
value = fmt.Sprintf("%d", bsonValue.Int32()) |
|
case bsontype.Int64: |
|
value = fmt.Sprintf("%d", bsonValue.Int64()) |
|
case bsontype.String: |
|
value = bsonValue.StringValue() |
|
default: |
|
return "", ErrInvalidIndexValue |
|
} |
|
|
|
_, err = name.WriteString(value) |
|
if err != nil { |
|
return "", err |
|
} |
|
|
|
first = false |
|
} |
|
|
|
return name.String(), nil |
|
}
|
|
|