Skip to content

Commit 1472099

Browse files
committed
add Apply func
1 parent 35bd989 commit 1472099

8 files changed

+256
-17
lines changed

collection.go

+10-11
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@ package qmgo
1616
import (
1717
"context"
1818
"fmt"
19-
"github.com/qiniu/qmgo/field"
20-
"github.com/qiniu/qmgo/hook"
21-
opts "github.com/qiniu/qmgo/options"
19+
"reflect"
20+
"strings"
21+
2222
"go.mongodb.org/mongo-driver/bson"
2323
"go.mongodb.org/mongo-driver/mongo"
2424
"go.mongodb.org/mongo-driver/mongo/options"
2525
"go.mongodb.org/mongo-driver/x/bsonx"
26-
"reflect"
27-
"strings"
26+
27+
"github.com/qiniu/qmgo/field"
28+
"github.com/qiniu/qmgo/hook"
29+
opts "github.com/qiniu/qmgo/options"
2830
)
2931

3032
// Collection is a handle to a MongoDB collection
@@ -309,7 +311,6 @@ func (c *Collection) Aggregate(ctx context.Context, pipeline interface{}) Aggreg
309311
func (c *Collection) ensureIndex(ctx context.Context, indexes []opts.IndexModel) error {
310312
var indexModels []mongo.IndexModel
311313

312-
// 组建[]mongo.IndexModel
313314
for _, idx := range indexes {
314315
var model mongo.IndexModel
315316
var keysDoc bsonx.Doc
@@ -375,17 +376,15 @@ func (c *Collection) EnsureIndexes(ctx context.Context, uniques []string, indexe
375376
// If the Key in opts.IndexModel is []string{"name"}, means create index: name
376377
// If the Key in opts.IndexModel is []string{"name","-age"} means create Compound indexes: name and -age
377378
func (c *Collection) CreateIndexes(ctx context.Context, indexes []opts.IndexModel) (err error) {
378-
// 创建普通索引
379379
err = c.ensureIndex(ctx, indexes)
380380
return
381381
}
382382

383383
// CreateIndex creates one index
384384
// If the Key in opts.IndexModel is []string{"name"}, means create index name
385-
// If the Key in opts.IndexModel is []string{"name","-age"} means drop Compound indexes: name and -age
386-
func (c *Collection) CreateOneIndex(ctx context.Context, indexes opts.IndexModel) error {
387-
// 创建普通索引
388-
return c.ensureIndex(ctx, []opts.IndexModel{indexes})
385+
// If the Key in opts.IndexModel is []string{"name","-age"} means create Compound index: name and -age
386+
func (c *Collection) CreateOneIndex(ctx context.Context, index opts.IndexModel) error {
387+
return c.ensureIndex(ctx, []opts.IndexModel{index})
389388

390389
}
391390

collection_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ package qmgo
1515

1616
import (
1717
"context"
18-
"github.com/qiniu/qmgo/options"
1918
"testing"
2019

21-
"github.com/qiniu/qmgo/operator"
2220
"github.com/stretchr/testify/require"
2321
"go.mongodb.org/mongo-driver/bson"
2422
"go.mongodb.org/mongo-driver/bson/primitive"
23+
24+
"github.com/qiniu/qmgo/operator"
25+
"github.com/qiniu/qmgo/options"
2526
)
2627

2728
func TestCollection_EnsureIndex(t *testing.T) {

cursor.go

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package qmgo
1515

1616
import (
1717
"context"
18+
1819
"go.mongodb.org/mongo-driver/mongo"
1920
)
2021

errors.go

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ var (
4141
ErrNotSupportedPassword = errors.New("password not supported")
4242
// ErrNotValidSliceToInsert return if insert argument is not valid slice
4343
ErrNotValidSliceToInsert = errors.New("must be valid slice to insert")
44+
// ErrReplacementContainUpdateOperators return if replacement document contain update operators
45+
ErrReplacementContainUpdateOperators = errors.New("replacement document cannot contain keys beginning with '$'")
4446
)
4547

4648
// IsErrNoDocuments check if err is no documents, both mongo-go-driver error and qmgo custom error

interface.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,21 @@ package qmgo
2828
// EnsureIndexes(uniques []string, indexes []string)
2929
//}
3030

31+
// Change holds fields for running a findAndModify command via the Query.Apply method.
32+
type Change struct {
33+
Update interface{} // update/replace document
34+
Replace bool // Whether to replace the document rather than updating
35+
Remove bool // Whether to remove the document found rather than updating
36+
Upsert bool // Whether to insert in case the document isn't found, take effect when Remove is false
37+
ReturnNew bool // Should the modified document be returned rather than the old one, take effect when Remove is false
38+
}
39+
3140
// CursorI Cursor interface
3241
type CursorI interface {
3342
Next(result interface{}) bool
3443
Close() error
3544
Err() error
36-
All(reuslts interface{}) error
45+
All(results interface{}) error
3746
//ID() int64
3847
}
3948

@@ -48,6 +57,7 @@ type QueryI interface {
4857
Count() (n int64, err error)
4958
Distinct(key string, result interface{}) error
5059
Cursor() CursorI
60+
Apply(change Change, result interface{}) error
5161
}
5262

5363
// AggregateI define the interface of aggregate

query.go

+91-2
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import (
1818
"fmt"
1919
"reflect"
2020

21-
"github.com/qiniu/qmgo/hook"
22-
qOpts "github.com/qiniu/qmgo/options"
2321
"go.mongodb.org/mongo-driver/bson"
2422
"go.mongodb.org/mongo-driver/mongo"
2523
"go.mongodb.org/mongo-driver/mongo/options"
24+
25+
"github.com/qiniu/qmgo/hook"
26+
qOpts "github.com/qiniu/qmgo/options"
2627
)
2728

2829
// Query struct definition
@@ -274,3 +275,91 @@ func (q *Query) Cursor() CursorI {
274275
err: err,
275276
}
276277
}
278+
279+
// Apply runs the findAndModify command, which allows updating, replacing
280+
// or removing a document matching a query and atomically returning either the old
281+
// version (the default) or the new version of the document (when ReturnNew is true)
282+
//
283+
// The Sort and Select query methods affect the result of Apply. In case
284+
// multiple documents match the query, Sort enables selecting which document to
285+
// act upon by ordering it first. Select enables retrieving only a selection
286+
// of fields of the new or old document.
287+
//
288+
// When Change.Replace is true, it means replace at most one document in the collection
289+
// and the update parameter must be a document and cannot contain any update operators;
290+
// if no objects are found and Change.Upsert is false, it will returns ErrNoDocuments.
291+
// When Change.Remove is true, it means delete at most one document in the collection
292+
// and returns the document as it appeared before deletion; if no objects are found,
293+
// it will returns ErrNoDocuments.
294+
// When both Change.Replace and Change.Remove are false,it means update at most one document
295+
// in the collection and the update parameter must be a document containing update operators;
296+
// if no objects are found and Change.Upsert is false, it will returns ErrNoDocuments.
297+
//
298+
// reference: https://docs.mongodb.com/manual/reference/command/findAndModify/
299+
func (q *Query) Apply(change Change, result interface{}) error {
300+
var err error
301+
302+
if change.Remove {
303+
err = q.findOneAndDelete(change, result)
304+
} else if change.Replace {
305+
err = q.findOneAndReplace(change, result)
306+
} else {
307+
err = q.findOneAndUpdate(change, result)
308+
}
309+
310+
return err
311+
}
312+
313+
// findOneAndDelete
314+
// reference: https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndDelete/
315+
func (q *Query) findOneAndDelete(change Change, result interface{}) error {
316+
opts := options.FindOneAndDelete()
317+
if q.sort != nil {
318+
opts.SetSort(q.sort)
319+
}
320+
if q.project != nil {
321+
opts.SetProjection(q.project)
322+
}
323+
324+
return q.collection.FindOneAndDelete(q.ctx, q.filter, opts).Decode(result)
325+
}
326+
327+
// findOneAndReplace
328+
// reference: https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/
329+
func (q *Query) findOneAndReplace(change Change, result interface{}) error {
330+
opts := options.FindOneAndReplace()
331+
if q.sort != nil {
332+
opts.SetSort(q.sort)
333+
}
334+
if q.project != nil {
335+
opts.SetProjection(q.project)
336+
}
337+
if change.Upsert {
338+
opts.SetUpsert(change.Upsert)
339+
}
340+
if change.ReturnNew {
341+
opts.SetReturnDocument(options.After)
342+
}
343+
344+
return q.collection.FindOneAndReplace(q.ctx, q.filter, change.Update, opts).Decode(result)
345+
}
346+
347+
// findOneAndUpdate
348+
// reference: https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndUpdate/
349+
func (q *Query) findOneAndUpdate(change Change, result interface{}) error {
350+
opts := options.FindOneAndUpdate()
351+
if q.sort != nil {
352+
opts.SetSort(q.sort)
353+
}
354+
if q.project != nil {
355+
opts.SetProjection(q.project)
356+
}
357+
if change.Upsert {
358+
opts.SetUpsert(change.Upsert)
359+
}
360+
if change.ReturnNew {
361+
opts.SetReturnDocument(options.After)
362+
}
363+
364+
return q.collection.FindOneAndUpdate(q.ctx, q.filter, change.Update, opts).Decode(result)
365+
}

query_test.go

+136
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import (
2020
"github.com/stretchr/testify/require"
2121
"go.mongodb.org/mongo-driver/bson"
2222
"go.mongodb.org/mongo-driver/bson/primitive"
23+
"go.mongodb.org/mongo-driver/mongo"
24+
25+
"github.com/qiniu/qmgo/operator"
2326
)
2427

2528
type QueryTestItem struct {
@@ -563,3 +566,136 @@ func TestQuery_Cursor(t *testing.T) {
563566
cursor = cli.Find(context.Background(), filter3).Cursor()
564567
ast.Error(cursor.Err())
565568
}
569+
570+
func TestQuery_Apply(t *testing.T) {
571+
ast := require.New(t)
572+
cli := initClient("test")
573+
defer cli.Close(context.Background())
574+
defer cli.DropCollection(context.Background())
575+
cli.EnsureIndexes(context.Background(), nil, []string{"name"})
576+
577+
id1 := primitive.NewObjectID()
578+
id2 := primitive.NewObjectID()
579+
id3 := primitive.NewObjectID()
580+
docs := []interface{}{
581+
bson.M{"_id": id1, "name": "Alice", "age": 18},
582+
bson.M{"_id": id2, "name": "Alice", "age": 19},
583+
bson.M{"_id": id3, "name": "Lucas", "age": 20},
584+
}
585+
_, _ = cli.InsertMany(context.Background(), docs)
586+
587+
var err error
588+
res1 := QueryTestItem{}
589+
filter1 := bson.M{
590+
"name": "Tom",
591+
}
592+
change1 := Change{
593+
}
594+
595+
err = cli.Find(context.Background(), filter1).Apply(change1, &res1)
596+
ast.EqualError(err, mongo.ErrNilDocument.Error())
597+
598+
change1.Update = bson.M{
599+
operator.Set: bson.M{
600+
"name": "Tom",
601+
"age": 18,
602+
},
603+
}
604+
err = cli.Find(context.Background(), filter1).Apply(change1, &res1)
605+
ast.EqualError(err, mongo.ErrNoDocuments.Error())
606+
607+
change1.ReturnNew = true
608+
err = cli.Find(context.Background(), filter1).Apply(change1, &res1)
609+
ast.EqualError(err, mongo.ErrNoDocuments.Error())
610+
611+
change1.Upsert = true
612+
err = cli.Find(context.Background(), filter1).Apply(change1, &res1)
613+
ast.NoError(err)
614+
ast.Equal( "Tom", res1.Name)
615+
ast.Equal(18, res1.Age)
616+
617+
res2 := QueryTestItem{}
618+
filter2 := bson.M{
619+
"name": "Alice",
620+
}
621+
change2 := Change{
622+
ReturnNew: true,
623+
Update: bson.M{
624+
operator.Set: bson.M{
625+
"name": "Alice",
626+
"age": 22,
627+
},
628+
},
629+
}
630+
projection2 := bson.M{
631+
"age": 1,
632+
}
633+
err = cli.Find(context.Background(), filter2).Sort("age").Select(projection2).Apply(change2, &res2)
634+
ast.NoError(err)
635+
ast.Equal("", res2.Name)
636+
ast.Equal(22, res2.Age)
637+
638+
res3 := QueryTestItem{}
639+
filter3 := bson.M{
640+
"name": "Bob",
641+
}
642+
change3 := Change{
643+
Remove: true,
644+
}
645+
err = cli.Find(context.Background(), filter3).Apply(change3, &res3)
646+
ast.EqualError(err, mongo.ErrNoDocuments.Error())
647+
648+
res3 = QueryTestItem{}
649+
filter3 = bson.M{
650+
"name": "Alice",
651+
}
652+
projection3 := bson.M{
653+
"age": 1,
654+
}
655+
err = cli.Find(context.Background(), filter3).Sort("age").Select(projection3).Apply(change3, &res3)
656+
ast.NoError(err)
657+
ast.Equal("", res3.Name)
658+
ast.Equal(19, res3.Age)
659+
660+
res4 := QueryTestItem{}
661+
filter4 := bson.M{
662+
"name": "Bob",
663+
}
664+
change4 := Change{
665+
Replace: true,
666+
Update: bson.M{
667+
operator.Set: bson.M{
668+
"name": "Bob",
669+
"age": 23,
670+
},
671+
},
672+
}
673+
err = cli.Find(context.Background(), filter4).Apply(change4, &res4)
674+
ast.EqualError(err, ErrReplacementContainUpdateOperators.Error())
675+
676+
change4.Update = bson.M{"name": "Bob", "age": 23}
677+
err = cli.Find(context.Background(), filter4).Apply(change4, &res4)
678+
ast.EqualError(err, mongo.ErrNoDocuments.Error())
679+
680+
change4.Upsert = true
681+
change4.ReturnNew = true
682+
err = cli.Find(context.Background(), filter4).Apply(change4, &res4)
683+
ast.NoError(err)
684+
ast.Equal("Bob", res4.Name)
685+
ast.Equal(23, res4.Age)
686+
687+
change4 = Change{
688+
Replace: true,
689+
Update: bson.M{"name": "Bob", "age": 25},
690+
Upsert: true,
691+
ReturnNew: false,
692+
}
693+
projection4 := bson.M{
694+
"age": 1,
695+
"name": 1,
696+
}
697+
err = cli.Find(context.Background(), filter4).Sort("age").Select(projection4).Apply(change4, &res4)
698+
ast.NoError(err)
699+
ast.Equal("Bob", res4.Name)
700+
ast.Equal(23, res4.Age)
701+
}

util_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ package qmgo
1515

1616
import (
1717
"fmt"
18-
"github.com/stretchr/testify/require"
1918
"testing"
2019
"time"
20+
21+
"github.com/stretchr/testify/require"
2122
)
2223

2324
func TestNow(t *testing.T) {

0 commit comments

Comments
 (0)