// 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 unified import ( "context" "errors" "fmt" "strconv" "strings" "time" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/internal/bsonutil" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/xoptions" ) // This file contains helpers to execute client operations. func executeCreateChangeStream(ctx context.Context, operation *operation) (*operationResult, error) { var watcher interface { Watch(context.Context, any, ...options.Lister[options.ChangeStreamOptions]) (*mongo.ChangeStream, error) } var err error watcher, err = entities(ctx).client(operation.Object) if err != nil { watcher, err = entities(ctx).database(operation.Object) } if err != nil { watcher, err = entities(ctx).collection(operation.Object) } if err != nil { return nil, fmt.Errorf("no client, database, or collection entity found with ID %q", operation.Object) } var pipeline []any opts := options.ChangeStream() elems, _ := operation.Arguments.Elements() for _, elem := range elems { key := elem.Key() val := elem.Value() switch key { case "batchSize": opts.SetBatchSize(val.Int32()) case "collation": collation, err := createCollation(val.Document()) if err != nil { return nil, fmt.Errorf("error creating collation: %w", err) } opts.SetCollation(*collation) case "comment": opts.SetComment(val) case "fullDocument": switch fd := val.StringValue(); fd { case "default": opts.SetFullDocument(options.Default) case "required": opts.SetFullDocument(options.Required) case "updateLookup": opts.SetFullDocument(options.UpdateLookup) case "whenAvailable": opts.SetFullDocument(options.WhenAvailable) default: return nil, fmt.Errorf("unrecognized fullDocument value %q", fd) } case "fullDocumentBeforeChange": switch fdbc := val.StringValue(); fdbc { case "off": opts.SetFullDocumentBeforeChange(options.Off) case "required": opts.SetFullDocumentBeforeChange(options.Required) case "whenAvailable": opts.SetFullDocumentBeforeChange(options.WhenAvailable) } case "maxAwaitTimeMS": opts.SetMaxAwaitTime(time.Duration(val.Int32()) * time.Millisecond) case "pipeline": pipeline = bsonutil.RawToInterfaces(bsonutil.RawArrayToDocuments(val.Array())...) case "resumeAfter": opts.SetResumeAfter(val.Document()) case "showExpandedEvents": opts.SetShowExpandedEvents(val.Boolean()) case "startAfter": opts.SetStartAfter(val.Document()) case "startAtOperationTime": t, i := val.Timestamp() opts.SetStartAtOperationTime(&bson.Timestamp{T: t, I: i}) default: return nil, fmt.Errorf("unrecognized createChangeStream option %q", key) } } if pipeline == nil { return nil, newMissingArgumentError("pipeline") } stream, err := watcher.Watch(ctx, pipeline, opts) if err != nil { return newErrorResult(err), nil } // createChangeStream is sometimes used with no corresponding saveResultAsEntity field. Return an // empty result in this case. if operation.ResultEntityID != nil { if err := entities(ctx).addCursorEntity(*operation.ResultEntityID, stream); err != nil { return nil, fmt.Errorf("error storing result as cursor entity: %w", err) } } return newEmptyResult(), nil } func executeListDatabases(ctx context.Context, operation *operation, nameOnly bool) (*operationResult, error) { client, err := entities(ctx).client(operation.Object) if err != nil { return nil, err } // We set a default filter rather than erroring if the Arguments doc doesn't have a "filter" field because the // spec says drivers should support this field, not must. filter := emptyDocument opts := options.ListDatabases().SetNameOnly(nameOnly) elems, _ := operation.Arguments.Elements() for _, elem := range elems { key := elem.Key() val := elem.Value() switch key { case "authorizedDatabases": opts.SetAuthorizedDatabases(val.Boolean()) case "filter": filter = val.Document() case "nameOnly": opts.SetNameOnly(val.Boolean()) default: return nil, fmt.Errorf("unrecognized listDatabases option %q", key) } } res, err := client.ListDatabases(ctx, filter, opts) if err != nil { return newErrorResult(err), nil } specsArray := bsoncore.NewArrayBuilder() for _, spec := range res.Databases { rawSpec := bsoncore.NewDocumentBuilder(). AppendString("name", spec.Name). AppendInt64("sizeOnDisk", spec.SizeOnDisk). AppendBoolean("empty", spec.Empty). Build() specsArray.AppendDocument(rawSpec) } raw := bsoncore.NewDocumentBuilder(). AppendArray("databases", specsArray.Build()). AppendInt64("totalSize", res.TotalSize). Build() return newDocumentResult(raw, nil), nil } func executeClientBulkWrite(ctx context.Context, operation *operation) (*operationResult, error) { client, err := entities(ctx).client(operation.Object) if err != nil { return nil, err } var writes []mongo.ClientBulkWrite opts := options.ClientBulkWrite() 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": models, err := val.Array().Values() if err != nil { return nil, err } for _, m := range models { model := m.Document().Index(0) var op *mongo.ClientBulkWrite switch key := model.Key(); key { case "insertOne": op, err = createClientInsertOneModel(model.Value().Document()) case "updateOne": op, err = createClientUpdateOneModel(model.Value().Document()) case "updateMany": op, err = createClientUpdateManyModel(model.Value().Document()) case "replaceOne": op, err = createClientReplaceOneModel(model.Value().Document()) case "deleteOne": op, err = createClientDeleteOneModel(model.Value().Document()) case "deleteMany": op, err = createClientDeleteManyModel(model.Value().Document()) default: err = fmt.Errorf("unrecognized bulkWrite model %q", key) } if err != nil { return nil, err } writes = append(writes, *op) } case "bypassDocumentValidation": opts.SetBypassDocumentValidation(val.Boolean()) case "comment": opts.SetComment(val) case "let": opts.SetLet(val.Document()) case "ordered": opts.SetOrdered(val.Boolean()) case "verboseResults": opts.SetVerboseResults(val.Boolean()) case "writeConcern": var wc writeConcern err := bson.Unmarshal(val.Value, &wc) if err != nil { return nil, err } c, err := wc.toWriteConcernOption() if err != nil { return nil, err } opts.SetWriteConcern(c) case "rawData": err = xoptions.SetInternalClientBulkWriteOptions(opts, key, val.Boolean()) if err != nil { return nil, err } default: return nil, fmt.Errorf("unrecognized bulkWrite option %q", key) } } res, err := client.BulkWrite(ctx, writes, opts) var bwe mongo.ClientBulkWriteException if errors.As(err, &bwe) { res = bwe.PartialResult } if res == nil { return newDocumentResult(emptyCoreDocument, err), nil } rawBuilder := bsoncore.NewDocumentBuilder(). AppendBoolean("acknowledged", res.Acknowledged). AppendInt64("deletedCount", res.DeletedCount). AppendInt64("insertedCount", res.InsertedCount). AppendInt64("matchedCount", res.MatchedCount). AppendInt64("modifiedCount", res.ModifiedCount). AppendInt64("upsertedCount", res.UpsertedCount) var resBuilder *bsoncore.DocumentBuilder resBuilder = bsoncore.NewDocumentBuilder() for k, v := range res.DeleteResults { resBuilder.AppendDocument(strconv.Itoa(k), bsoncore.NewDocumentBuilder(). AppendInt64("deletedCount", v.DeletedCount). Build(), ) } rawBuilder.AppendDocument("deleteResults", resBuilder.Build()) resBuilder = bsoncore.NewDocumentBuilder() for k, v := range res.InsertResults { t, d, err := bson.MarshalValue(v.InsertedID) if err != nil { return nil, err } resBuilder.AppendDocument(strconv.Itoa(k), bsoncore.NewDocumentBuilder(). AppendValue("insertedId", bsoncore.Value{Type: bsoncore.Type(t), Data: d}). Build(), ) } rawBuilder.AppendDocument("insertResults", resBuilder.Build()) resBuilder = bsoncore.NewDocumentBuilder() for k, v := range res.UpdateResults { b := bsoncore.NewDocumentBuilder(). AppendInt64("matchedCount", v.MatchedCount). AppendInt64("modifiedCount", v.ModifiedCount) if v.UpsertedID != nil { t, d, err := bson.MarshalValue(v.UpsertedID) if err != nil { return nil, err } b.AppendValue("upsertedId", bsoncore.Value{Type: bsoncore.Type(t), Data: d}) } resBuilder.AppendDocument(strconv.Itoa(k), b.Build()) } rawBuilder.AppendDocument("updateResults", resBuilder.Build()) return newDocumentResult(rawBuilder.Build(), err), nil } func executeAppendMetadata(ctx context.Context, op *operation) (*operationResult, error) { client, err := entities(ctx).client(op.Object) if err != nil { return nil, fmt.Errorf("error getting client entity: %w", err) } elems, err := op.Arguments.Elements() if err != nil { return nil, fmt.Errorf("error getting appendMetadata arguments: %w", err) } driverInfo := options.DriverInfo{} for _, elem := range elems { key := elem.Key() val := elem.Value() if key == "driverInfoOptions" { if err = bson.Unmarshal(val.Value, &driverInfo); err != nil { return nil, fmt.Errorf("error unmarshaling driverInfoOptions: %w", err) } } } client.AppendDriverInfo(driverInfo) return newEmptyResult(), nil } func createClientInsertOneModel(value bson.Raw) (*mongo.ClientBulkWrite, error) { var v struct { Namespace string Document bson.Raw } err := bson.Unmarshal(value, &v) if err != nil { return nil, err } ns := strings.SplitN(v.Namespace, ".", 2) return &mongo.ClientBulkWrite{ Database: ns[0], Collection: ns[1], Model: &mongo.ClientInsertOneModel{ Document: v.Document, }, }, nil } func createClientUpdateOneModel(value bson.Raw) (*mongo.ClientBulkWrite, error) { var v struct { Namespace string Filter bson.Raw Update any ArrayFilters []any Collation *options.Collation Hint *bson.RawValue Upsert *bool Sort *bson.RawValue } err := bson.Unmarshal(value, &v) if err != nil { return nil, err } var hint any if v.Hint != nil { hint, err = createHint(*v.Hint) if err != nil { return nil, err } } var sort any if v.Sort != nil { sort = v.Sort.Document() } model := &mongo.ClientUpdateOneModel{ Filter: v.Filter, Update: v.Update, Collation: v.Collation, Hint: hint, Upsert: v.Upsert, Sort: sort, } if len(v.ArrayFilters) > 0 { model.ArrayFilters = v.ArrayFilters } ns := strings.SplitN(v.Namespace, ".", 2) return &mongo.ClientBulkWrite{ Database: ns[0], Collection: ns[1], Model: model, }, nil } func createClientUpdateManyModel(value bson.Raw) (*mongo.ClientBulkWrite, error) { var v struct { Namespace string Filter bson.Raw Update any ArrayFilters []any Collation *options.Collation Hint *bson.RawValue Upsert *bool } err := bson.Unmarshal(value, &v) if err != nil { return nil, err } var hint any if v.Hint != nil { hint, err = createHint(*v.Hint) if err != nil { return nil, err } } model := &mongo.ClientUpdateManyModel{ Filter: v.Filter, Update: v.Update, Collation: v.Collation, Hint: hint, Upsert: v.Upsert, } if len(v.ArrayFilters) > 0 { model.ArrayFilters = v.ArrayFilters } ns := strings.SplitN(v.Namespace, ".", 2) return &mongo.ClientBulkWrite{ Database: ns[0], Collection: ns[1], Model: model, }, nil } func createClientReplaceOneModel(value bson.Raw) (*mongo.ClientBulkWrite, error) { var v struct { Namespace string Filter bson.Raw Replacement bson.Raw Collation *options.Collation Hint *bson.RawValue Upsert *bool Sort *bson.RawValue } err := bson.Unmarshal(value, &v) if err != nil { return nil, err } var hint any if v.Hint != nil { hint, err = createHint(*v.Hint) if err != nil { return nil, err } } var sort any if v.Sort != nil { sort = v.Sort.Document() } ns := strings.SplitN(v.Namespace, ".", 2) return &mongo.ClientBulkWrite{ Database: ns[0], Collection: ns[1], Model: &mongo.ClientReplaceOneModel{ Filter: v.Filter, Replacement: v.Replacement, Collation: v.Collation, Hint: hint, Upsert: v.Upsert, Sort: sort, }, }, nil } func createClientDeleteOneModel(value bson.Raw) (*mongo.ClientBulkWrite, error) { var v struct { Namespace string Filter bson.Raw Collation *options.Collation Hint *bson.RawValue } err := bson.Unmarshal(value, &v) if err != nil { return nil, err } var hint any if v.Hint != nil { hint, err = createHint(*v.Hint) if err != nil { return nil, err } } ns := strings.SplitN(v.Namespace, ".", 2) return &mongo.ClientBulkWrite{ Database: ns[0], Collection: ns[1], Model: &mongo.ClientDeleteOneModel{ Filter: v.Filter, Collation: v.Collation, Hint: hint, }, }, nil } func createClientDeleteManyModel(value bson.Raw) (*mongo.ClientBulkWrite, error) { var v struct { Namespace string Filter bson.Raw Collation *options.Collation Hint *bson.RawValue } err := bson.Unmarshal(value, &v) if err != nil { return nil, err } var hint any if v.Hint != nil { hint, err = createHint(*v.Hint) if err != nil { return nil, err } } ns := strings.SplitN(v.Namespace, ".", 2) return &mongo.ClientBulkWrite{ Database: ns[0], Collection: ns[1], Model: &mongo.ClientDeleteManyModel{ Filter: v.Filter, Collation: v.Collation, Hint: hint, }, }, nil }