1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-05-06 19:29:15 +08:00
Aleksandar Novaković 58cdf2cddc MF-312 - Implement basic MongoDB reader (#344)
* Add mongodb reader service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add tests for mongodb reader service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add documentation for mongodb reader service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix test function name

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update comment in docker-compose for mongodb-reader service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-08-08 13:38:34 +02:00

1115 lines
28 KiB
Go

// 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 (
"context"
"errors"
"strings"
"github.com/mongodb/mongo-go-driver/bson"
"github.com/mongodb/mongo-go-driver/core/command"
"github.com/mongodb/mongo-go-driver/core/description"
"github.com/mongodb/mongo-go-driver/core/dispatch"
"github.com/mongodb/mongo-go-driver/core/option"
"github.com/mongodb/mongo-go-driver/core/readconcern"
"github.com/mongodb/mongo-go-driver/core/readpref"
"github.com/mongodb/mongo-go-driver/core/session"
"github.com/mongodb/mongo-go-driver/core/writeconcern"
"github.com/mongodb/mongo-go-driver/mongo/aggregateopt"
"github.com/mongodb/mongo-go-driver/mongo/changestreamopt"
"github.com/mongodb/mongo-go-driver/mongo/collectionopt"
"github.com/mongodb/mongo-go-driver/mongo/countopt"
"github.com/mongodb/mongo-go-driver/mongo/deleteopt"
"github.com/mongodb/mongo-go-driver/mongo/distinctopt"
"github.com/mongodb/mongo-go-driver/mongo/dropcollopt"
"github.com/mongodb/mongo-go-driver/mongo/findopt"
"github.com/mongodb/mongo-go-driver/mongo/insertopt"
"github.com/mongodb/mongo-go-driver/mongo/replaceopt"
"github.com/mongodb/mongo-go-driver/mongo/updateopt"
)
// Collection performs operations on a given collection.
type Collection struct {
client *Client
db *Database
name string
readConcern *readconcern.ReadConcern
writeConcern *writeconcern.WriteConcern
readPreference *readpref.ReadPref
readSelector description.ServerSelector
writeSelector description.ServerSelector
}
func newCollection(db *Database, name string, opts ...collectionopt.Option) *Collection {
collOpt, err := collectionopt.BundleCollection(opts...).Unbundle()
if err != nil {
return nil
}
rc := db.readConcern
if collOpt.ReadConcern != nil {
rc = collOpt.ReadConcern
}
wc := db.writeConcern
if collOpt.WriteConcern != nil {
wc = collOpt.WriteConcern
}
rp := db.readPreference
if collOpt.ReadPreference != nil {
rp = collOpt.ReadPreference
}
coll := &Collection{
client: db.client,
db: db,
name: name,
readPreference: rp,
readConcern: rc,
writeConcern: wc,
readSelector: db.readSelector,
writeSelector: db.writeSelector,
}
return coll
}
func (coll *Collection) copy() *Collection {
return &Collection{
client: coll.client,
db: coll.db,
name: coll.name,
readConcern: coll.readConcern,
writeConcern: coll.writeConcern,
readPreference: coll.readPreference,
readSelector: coll.readSelector,
writeSelector: coll.writeSelector,
}
}
// Clone creates a copy of this collection with updated options, if any are given.
func (coll *Collection) Clone(opts ...collectionopt.Option) (*Collection, error) {
copyColl := coll.copy()
optsColl, err := collectionopt.BundleCollection(opts...).Unbundle()
if err != nil {
return nil, err
}
if optsColl.ReadConcern != nil {
copyColl.readConcern = optsColl.ReadConcern
}
if optsColl.WriteConcern != nil {
copyColl.writeConcern = optsColl.WriteConcern
}
if optsColl.ReadPreference != nil {
copyColl.readPreference = optsColl.ReadPreference
}
return copyColl, nil
}
// Name provides access to the name of the collection.
func (coll *Collection) Name() string {
return coll.name
}
// namespace returns the namespace of the collection.
func (coll *Collection) namespace() command.Namespace {
return command.NewNamespace(coll.db.name, coll.name)
}
// InsertOne inserts a single document into the collection. A user can supply
// a custom context to this method, or nil to default to context.Background().
//
// This method uses TransformDocument to turn the document parameter into a
// *bson.Document. See TransformDocument for the list of valid types for
// document.
//
// TODO(skriptble): Determine if we should unwrap the value for the
// InsertOneResult or just return the bson.Element or a bson.Value.
func (coll *Collection) InsertOne(ctx context.Context, document interface{},
opts ...insertopt.One) (*InsertOneResult, error) {
if ctx == nil {
ctx = context.Background()
}
doc, err := TransformDocument(document)
if err != nil {
return nil, err
}
insertedID, err := ensureID(doc)
if err != nil {
return nil, err
}
// convert options into []option.InsertOptioner and dedup
oneOpts, sess, err := insertopt.BundleOne(opts...).Unbundle(true)
if err != nil {
return nil, err
}
err = coll.client.ValidSession(sess)
if err != nil {
return nil, err
}
oldns := coll.namespace()
cmd := command.Insert{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Docs: []*bson.Document{doc},
Opts: oneOpts,
WriteConcern: coll.writeConcern,
Session: sess,
Clock: coll.client.clock,
}
res, err := dispatch.Insert(
ctx, cmd,
coll.client.topology,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
rr, err := processWriteError(res.WriteConcernError, res.WriteErrors, err)
if rr&rrOne == 0 {
return nil, err
}
return &InsertOneResult{InsertedID: insertedID}, err
}
// InsertMany inserts the provided documents. A user can supply a custom context to this
// method.
//
// Currently, batching is not implemented for this operation. Because of this, extremely large
// sets of documents will not fit into a single BSON document to be sent to the server, so the
// operation will fail.
//
// This method uses TransformDocument to turn the documents parameter into a
// *bson.Document. See TransformDocument for the list of valid types for
// documents.
func (coll *Collection) InsertMany(ctx context.Context, documents []interface{},
opts ...insertopt.Many) (*InsertManyResult, error) {
if ctx == nil {
ctx = context.Background()
}
result := make([]interface{}, len(documents))
docs := make([]*bson.Document, len(documents))
for i, doc := range documents {
bdoc, err := TransformDocument(doc)
if err != nil {
return nil, err
}
insertedID, err := ensureID(bdoc)
if err != nil {
return nil, err
}
docs[i] = bdoc
result[i] = insertedID
}
// convert options into []option.InsertOptioner and dedup
manyOpts, sess, err := insertopt.BundleMany(opts...).Unbundle(true)
if err != nil {
return nil, err
}
err = coll.client.ValidSession(sess)
if err != nil {
return nil, err
}
oldns := coll.namespace()
cmd := command.Insert{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Docs: docs,
Opts: manyOpts,
WriteConcern: coll.writeConcern,
Session: sess,
Clock: coll.client.clock,
}
res, err := dispatch.Insert(
ctx, cmd,
coll.client.topology,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
switch err {
case nil:
case command.ErrUnacknowledgedWrite:
return &InsertManyResult{InsertedIDs: result}, ErrUnacknowledgedWrite
default:
return nil, err
}
if len(res.WriteErrors) > 0 || res.WriteConcernError != nil {
err = BulkWriteError{
WriteErrors: writeErrorsFromResult(res.WriteErrors),
WriteConcernError: convertWriteConcernError(res.WriteConcernError),
}
}
return &InsertManyResult{InsertedIDs: result}, err
}
// DeleteOne deletes a single document from the collection. A user can supply
// a custom context to this method, or nil to default to context.Background().
//
// This method uses TransformDocument to turn the filter parameter into a
// *bson.Document. See TransformDocument for the list of valid types for
// filter.
func (coll *Collection) DeleteOne(ctx context.Context, filter interface{},
opts ...deleteopt.Delete) (*DeleteResult, error) {
if ctx == nil {
ctx = context.Background()
}
f, err := TransformDocument(filter)
if err != nil {
return nil, err
}
deleteDocs := []*bson.Document{
bson.NewDocument(
bson.EC.SubDocument("q", f),
bson.EC.Int32("limit", 1)),
}
deleteOpts, sess, err := deleteopt.BundleDelete(opts...).Unbundle(true)
if err != nil {
return nil, err
}
err = coll.client.ValidSession(sess)
if err != nil {
return nil, err
}
oldns := coll.namespace()
cmd := command.Delete{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Deletes: deleteDocs,
Opts: deleteOpts,
WriteConcern: coll.writeConcern,
Session: sess,
Clock: coll.client.clock,
}
res, err := dispatch.Delete(
ctx, cmd,
coll.client.topology,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
rr, err := processWriteError(res.WriteConcernError, res.WriteErrors, err)
if rr&rrOne == 0 {
return nil, err
}
return &DeleteResult{DeletedCount: int64(res.N)}, err
}
// DeleteMany deletes multiple documents from the collection. A user can
// supply a custom context to this method, or nil to default to
// context.Background().
//
// This method uses TransformDocument to turn the filter parameter into a
// *bson.Document. See TransformDocument for the list of valid types for
// filter.
func (coll *Collection) DeleteMany(ctx context.Context, filter interface{},
opts ...deleteopt.Delete) (*DeleteResult, error) {
if ctx == nil {
ctx = context.Background()
}
f, err := TransformDocument(filter)
if err != nil {
return nil, err
}
deleteDocs := []*bson.Document{bson.NewDocument(bson.EC.SubDocument("q", f), bson.EC.Int32("limit", 0))}
deleteOpts, sess, err := deleteopt.BundleDelete(opts...).Unbundle(true)
if err != nil {
return nil, err
}
err = coll.client.ValidSession(sess)
if err != nil {
return nil, err
}
oldns := coll.namespace()
cmd := command.Delete{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Deletes: deleteDocs,
Opts: deleteOpts,
WriteConcern: coll.writeConcern,
Session: sess,
Clock: coll.client.clock,
}
res, err := dispatch.Delete(
ctx, cmd,
coll.client.topology,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
rr, err := processWriteError(res.WriteConcernError, res.WriteErrors, err)
if rr&rrMany == 0 {
return nil, err
}
return &DeleteResult{DeletedCount: int64(res.N)}, err
}
func (coll *Collection) updateOrReplaceOne(ctx context.Context, filter,
update *bson.Document, sess *session.Client, opts ...option.UpdateOptioner) (*UpdateResult, error) {
if ctx == nil {
ctx = context.Background()
}
updateDocs := []*bson.Document{
bson.NewDocument(
bson.EC.SubDocument("q", filter),
bson.EC.SubDocument("u", update),
bson.EC.Boolean("multi", false),
),
}
oldns := coll.namespace()
cmd := command.Update{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Docs: updateDocs,
Opts: opts,
WriteConcern: coll.writeConcern,
Session: sess,
Clock: coll.client.clock,
}
r, err := dispatch.Update(
ctx, cmd,
coll.client.topology,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
if err != nil && err != command.ErrUnacknowledgedWrite {
return nil, err
}
res := &UpdateResult{
MatchedCount: r.MatchedCount,
ModifiedCount: r.ModifiedCount,
}
if len(r.Upserted) > 0 {
res.UpsertedID = r.Upserted[0].ID
res.MatchedCount--
}
rr, err := processWriteError(r.WriteConcernError, r.WriteErrors, err)
if rr&rrOne == 0 {
return nil, err
}
return res, err
}
// UpdateOne updates a single document in the collection. A user can supply a
// custom context to this method, or nil to default to context.Background().
//
// This method uses TransformDocument to turn the filter and update parameter
// into a *bson.Document. See TransformDocument for the list of valid types for
// filter and update.
func (coll *Collection) UpdateOne(ctx context.Context, filter interface{}, update interface{},
options ...updateopt.Update) (*UpdateResult, error) {
if ctx == nil {
ctx = context.Background()
}
f, err := TransformDocument(filter)
if err != nil {
return nil, err
}
u, err := TransformDocument(update)
if err != nil {
return nil, err
}
if err := ensureDollarKey(u); err != nil {
return nil, err
}
updOpts, sess, err := updateopt.BundleUpdate(options...).Unbundle(true)
if err != nil {
return nil, err
}
err = coll.client.ValidSession(sess)
if err != nil {
return nil, err
}
return coll.updateOrReplaceOne(ctx, f, u, sess, updOpts...)
}
// UpdateMany updates multiple documents in the collection. A user can supply
// a custom context to this method, or nil to default to context.Background().
//
// This method uses TransformDocument to turn the filter and update parameter
// into a *bson.Document. See TransformDocument for the list of valid types for
// filter and update.
func (coll *Collection) UpdateMany(ctx context.Context, filter interface{}, update interface{},
opts ...updateopt.Update) (*UpdateResult, error) {
if ctx == nil {
ctx = context.Background()
}
f, err := TransformDocument(filter)
if err != nil {
return nil, err
}
u, err := TransformDocument(update)
if err != nil {
return nil, err
}
if err = ensureDollarKey(u); err != nil {
return nil, err
}
updateDocs := []*bson.Document{
bson.NewDocument(
bson.EC.SubDocument("q", f),
bson.EC.SubDocument("u", u),
bson.EC.Boolean("multi", true),
),
}
updOpts, sess, err := updateopt.BundleUpdate(opts...).Unbundle(true)
if err != nil {
return nil, err
}
err = coll.client.ValidSession(sess)
if err != nil {
return nil, err
}
oldns := coll.namespace()
cmd := command.Update{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Docs: updateDocs,
Opts: updOpts,
WriteConcern: coll.writeConcern,
Session: sess,
Clock: coll.client.clock,
}
r, err := dispatch.Update(
ctx, cmd,
coll.client.topology,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
if err != nil && err != command.ErrUnacknowledgedWrite {
return nil, err
}
res := &UpdateResult{
MatchedCount: r.MatchedCount,
ModifiedCount: r.ModifiedCount,
}
// TODO(skriptble): Is this correct? Do we only return the first upserted ID for an UpdateMany?
if len(r.Upserted) > 0 {
res.UpsertedID = r.Upserted[0].ID
res.MatchedCount--
}
rr, err := processWriteError(r.WriteConcernError, r.WriteErrors, err)
if rr&rrMany == 0 {
return nil, err
}
return res, err
}
// ReplaceOne replaces a single document in the collection. A user can supply
// a custom context to this method, or nil to default to context.Background().
//
// This method uses TransformDocument to turn the filter and replacement
// parameter into a *bson.Document. See TransformDocument for the list of
// valid types for filter and replacement.
func (coll *Collection) ReplaceOne(ctx context.Context, filter interface{},
replacement interface{}, opts ...replaceopt.Replace) (*UpdateResult, error) {
if ctx == nil {
ctx = context.Background()
}
f, err := TransformDocument(filter)
if err != nil {
return nil, err
}
r, err := TransformDocument(replacement)
if err != nil {
return nil, err
}
if elem, ok := r.ElementAtOK(0); ok && strings.HasPrefix(elem.Key(), "$") {
return nil, errors.New("replacement document cannot contains keys beginning with '$")
}
repOpts, sess, err := replaceopt.BundleReplace(opts...).Unbundle(true)
if err != nil {
return nil, err
}
err = coll.client.ValidSession(sess)
if err != nil {
return nil, err
}
updateOptions := make([]option.UpdateOptioner, 0, len(opts))
for _, opt := range repOpts {
updateOptions = append(updateOptions, opt)
}
return coll.updateOrReplaceOne(ctx, f, r, sess, updateOptions...)
}
// Aggregate runs an aggregation framework pipeline. A user can supply a custom context to
// this method.
//
// See https://docs.mongodb.com/manual/aggregation/.
//
// This method uses TransformDocument to turn the pipeline parameter into a
// *bson.Document. See TransformDocument for the list of valid types for
// pipeline.
func (coll *Collection) Aggregate(ctx context.Context, pipeline interface{},
opts ...aggregateopt.Aggregate) (Cursor, error) {
if ctx == nil {
ctx = context.Background()
}
pipelineArr, err := transformAggregatePipeline(pipeline)
if err != nil {
return nil, err
}
// convert options into []option.Optioner and dedup
aggOpts, sess, err := aggregateopt.BundleAggregate(opts...).Unbundle(true)
if err != nil {
return nil, err
}
err = coll.client.ValidSession(sess)
if err != nil {
return nil, err
}
oldns := coll.namespace()
cmd := command.Aggregate{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Pipeline: pipelineArr,
Opts: aggOpts,
ReadPref: coll.readPreference,
WriteConcern: coll.writeConcern,
ReadConcern: coll.readConcern,
Session: sess,
Clock: coll.client.clock,
}
return dispatch.Aggregate(
ctx, cmd,
coll.client.topology,
coll.readSelector,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
}
// Count gets the number of documents matching the filter. A user can supply a
// custom context to this method, or nil to default to context.Background().
//
// This method uses TransformDocument to turn the filter parameter into a
// *bson.Document. See TransformDocument for the list of valid types for
// filter.
func (coll *Collection) Count(ctx context.Context, filter interface{},
opts ...countopt.Count) (int64, error) {
if ctx == nil {
ctx = context.Background()
}
f, err := TransformDocument(filter)
if err != nil {
return 0, err
}
countOpts, sess, err := countopt.BundleCount(opts...).Unbundle(true)
if err != nil {
return 0, err
}
err = coll.client.ValidSession(sess)
if err != nil {
return 0, err
}
oldns := coll.namespace()
cmd := command.Count{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Query: f,
Opts: countOpts,
ReadPref: coll.readPreference,
ReadConcern: coll.readConcern,
Session: sess,
Clock: coll.client.clock,
}
return dispatch.Count(
ctx, cmd,
coll.client.topology,
coll.readSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
}
// Distinct finds the distinct values for a specified field across a single
// collection. A user can supply a custom context to this method, or nil to
// default to context.Background().
//
// This method uses TransformDocument to turn the filter parameter into a
// *bson.Document. See TransformDocument for the list of valid types for
// filter.
func (coll *Collection) Distinct(ctx context.Context, fieldName string, filter interface{},
opts ...distinctopt.Distinct) ([]interface{}, error) {
if ctx == nil {
ctx = context.Background()
}
var f *bson.Document
var err error
if filter != nil {
f, err = TransformDocument(filter)
if err != nil {
return nil, err
}
}
distinctOpts, sess, err := distinctopt.BundleDistinct(opts...).Unbundle(true)
if err != nil {
return nil, err
}
err = coll.client.ValidSession(sess)
if err != nil {
return nil, err
}
oldns := coll.namespace()
cmd := command.Distinct{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Field: fieldName,
Query: f,
Opts: distinctOpts,
ReadPref: coll.readPreference,
ReadConcern: coll.readConcern,
Session: sess,
Clock: coll.client.clock,
}
res, err := dispatch.Distinct(
ctx, cmd,
coll.client.topology,
coll.readSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
if err != nil {
return nil, err
}
return res.Values, nil
}
// Find finds the documents matching a model. A user can supply a custom context to this
// method.
//
// This method uses TransformDocument to turn the filter parameter into a
// *bson.Document. See TransformDocument for the list of valid types for
// filter.
func (coll *Collection) Find(ctx context.Context, filter interface{},
opts ...findopt.Find) (Cursor, error) {
if ctx == nil {
ctx = context.Background()
}
var f *bson.Document
var err error
if filter != nil {
f, err = TransformDocument(filter)
if err != nil {
return nil, err
}
}
findOpts, sess, err := findopt.BundleFind(opts...).Unbundle(true)
if err != nil {
return nil, err
}
err = coll.client.ValidSession(sess)
if err != nil {
return nil, err
}
oldns := coll.namespace()
cmd := command.Find{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Filter: f,
Opts: findOpts,
ReadPref: coll.readPreference,
ReadConcern: coll.readConcern,
Session: sess,
Clock: coll.client.clock,
}
return dispatch.Find(
ctx, cmd,
coll.client.topology,
coll.readSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
}
// FindOne returns up to one document that matches the model. A user can
// supply a custom context to this method, or nil to default to
// context.Background().
//
// This method uses TransformDocument to turn the filter parameter into a
// *bson.Document. See TransformDocument for the list of valid types for
// filter.
func (coll *Collection) FindOne(ctx context.Context, filter interface{},
opts ...findopt.One) *DocumentResult {
if ctx == nil {
ctx = context.Background()
}
var f *bson.Document
var err error
if filter != nil {
f, err = TransformDocument(filter)
if err != nil {
return &DocumentResult{err: err}
}
}
findOneOpts, sess, err := findopt.BundleOne(opts...).Unbundle(true)
if err != nil {
return &DocumentResult{err: err}
}
findOneOpts = append(findOneOpts, findopt.Limit(1).ConvertFindOption())
err = coll.client.ValidSession(sess)
if err != nil {
return &DocumentResult{err: err}
}
oldns := coll.namespace()
cmd := command.Find{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Filter: f,
Opts: findOneOpts,
ReadPref: coll.readPreference,
ReadConcern: coll.readConcern,
Session: sess,
Clock: coll.client.clock,
}
cursor, err := dispatch.Find(
ctx, cmd,
coll.client.topology,
coll.readSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
if err != nil {
return &DocumentResult{err: err}
}
return &DocumentResult{cur: cursor}
}
// FindOneAndDelete find a single document and deletes it, returning the
// original in result. The document to return may be nil.
//
// A user can supply a custom context to this method, or nil to default to
// context.Background().
//
// This method uses TransformDocument to turn the filter parameter into a
// *bson.Document. See TransformDocument for the list of valid types for
// filter.
func (coll *Collection) FindOneAndDelete(ctx context.Context, filter interface{},
opts ...findopt.DeleteOne) *DocumentResult {
if ctx == nil {
ctx = context.Background()
}
var f *bson.Document
var err error
if filter != nil {
f, err = TransformDocument(filter)
if err != nil {
return &DocumentResult{err: err}
}
}
findOpts, sess, err := findopt.BundleDeleteOne(opts...).Unbundle(true)
if err != nil {
return &DocumentResult{err: err}
}
err = coll.client.ValidSession(sess)
if err != nil {
return &DocumentResult{err: err}
}
oldns := coll.namespace()
cmd := command.FindOneAndDelete{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Query: f,
Opts: findOpts,
WriteConcern: coll.writeConcern,
Session: sess,
Clock: coll.client.clock,
}
res, err := dispatch.FindOneAndDelete(
ctx, cmd,
coll.client.topology,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
if err != nil {
return &DocumentResult{err: err}
}
return &DocumentResult{rdr: res.Value}
}
// FindOneAndReplace finds a single document and replaces it, returning either
// the original or the replaced document. The document to return may be nil.
//
// A user can supply a custom context to this method, or nil to default to
// context.Background().
//
// This method uses TransformDocument to turn the filter and replacement
// parameter into a *bson.Document. See TransformDocument for the list of
// valid types for filter and replacement.
func (coll *Collection) FindOneAndReplace(ctx context.Context, filter interface{},
replacement interface{}, opts ...findopt.ReplaceOne) *DocumentResult {
if ctx == nil {
ctx = context.Background()
}
f, err := TransformDocument(filter)
if err != nil {
return &DocumentResult{err: err}
}
r, err := TransformDocument(replacement)
if err != nil {
return &DocumentResult{err: err}
}
if elem, ok := r.ElementAtOK(0); ok && strings.HasPrefix(elem.Key(), "$") {
return &DocumentResult{err: errors.New("replacement document cannot contains keys beginning with '$")}
}
findOpts, sess, err := findopt.BundleReplaceOne(opts...).Unbundle(true)
if err != nil {
return &DocumentResult{err: err}
}
err = coll.client.ValidSession(sess)
if err != nil {
return &DocumentResult{err: err}
}
oldns := coll.namespace()
cmd := command.FindOneAndReplace{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Query: f,
Replacement: r,
Opts: findOpts,
WriteConcern: coll.writeConcern,
Session: sess,
Clock: coll.client.clock,
}
res, err := dispatch.FindOneAndReplace(
ctx, cmd,
coll.client.topology,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
if err != nil {
return &DocumentResult{err: err}
}
return &DocumentResult{rdr: res.Value}
}
// FindOneAndUpdate finds a single document and updates it, returning either
// the original or the updated. The document to return may be nil.
//
// A user can supply a custom context to this method, or nil to default to
// context.Background().
//
// This method uses TransformDocument to turn the filter and update parameter
// into a *bson.Document. See TransformDocument for the list of valid types for
// filter and update.
func (coll *Collection) FindOneAndUpdate(ctx context.Context, filter interface{},
update interface{}, opts ...findopt.UpdateOne) *DocumentResult {
if ctx == nil {
ctx = context.Background()
}
f, err := TransformDocument(filter)
if err != nil {
return &DocumentResult{err: err}
}
u, err := TransformDocument(update)
if err != nil {
return &DocumentResult{err: err}
}
if elem, ok := u.ElementAtOK(0); !ok || !strings.HasPrefix(elem.Key(), "$") {
return &DocumentResult{err: errors.New("update document must contain key beginning with '$")}
}
findOpts, sess, err := findopt.BundleUpdateOne(opts...).Unbundle(true)
if err != nil {
return &DocumentResult{err: err}
}
err = coll.client.ValidSession(sess)
if err != nil {
return &DocumentResult{err: err}
}
oldns := coll.namespace()
cmd := command.FindOneAndUpdate{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Query: f,
Update: u,
Opts: findOpts,
WriteConcern: coll.writeConcern,
Session: sess,
Clock: coll.client.clock,
}
res, err := dispatch.FindOneAndUpdate(
ctx, cmd,
coll.client.topology,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
if err != nil {
return &DocumentResult{err: err}
}
return &DocumentResult{rdr: res.Value}
}
// Watch returns a change stream cursor used to receive notifications of changes to the collection.
// This method is preferred to running a raw aggregation with a $changeStream stage because it
// supports resumability in the case of some errors.
func (coll *Collection) Watch(ctx context.Context, pipeline interface{},
opts ...changestreamopt.ChangeStream) (Cursor, error) {
return newChangeStream(ctx, coll, pipeline, opts...)
}
// Indexes returns the index view for this collection.
func (coll *Collection) Indexes() IndexView {
return IndexView{coll: coll}
}
// Drop drops this collection from database.
func (coll *Collection) Drop(ctx context.Context, opts ...dropcollopt.DropColl) error {
if ctx == nil {
ctx = context.Background()
}
var sess *session.Client
for _, opt := range opts {
if conv, ok := opt.(dropcollopt.DropCollSession); ok {
sess = conv.ConvertDropCollSession()
}
}
err := coll.client.ValidSession(sess)
if err != nil {
return err
}
cmd := command.DropCollection{
DB: coll.db.name,
Collection: coll.name,
WriteConcern: coll.writeConcern,
Session: sess,
Clock: coll.client.clock,
}
_, err = dispatch.DropCollection(
ctx, cmd,
coll.client.topology,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
)
if err != nil && !command.IsNotFound(err) {
return err
}
return nil
}