diff --git a/common/idx/index_catalog.go b/common/idx/index_catalog.go index 3cbd9d8e9..d526f2215 100644 --- a/common/idx/index_catalog.go +++ b/common/idx/index_catalog.go @@ -267,18 +267,6 @@ func (i *IndexCatalog) DeleteIndexes(database, collection string, dropCmd bson.D } } -func updateExpireAfterSeconds(index *IndexDocument, expire int64) error { - if _, ok := index.Options["expireAfterSeconds"]; !ok { - return errors.Errorf("missing \"expireAfterSeconds\" in matching index: %v", index) - } - index.Options["expireAfterSeconds"] = expire - return nil -} - -func updateHidden(index *IndexDocument, hidden bool) { - index.Options["hidden"] = hidden -} - // GetIndexByIndexMod returns an index that matches the name or key pattern specified in // a collMod command. func (i *IndexCatalog) GetIndexByIndexMod( @@ -337,39 +325,18 @@ func (i *IndexCatalog) collMod(database, collection string, indexModValue any) e return errors.Errorf("cannot find index in indexCatalog for collMod: %v", indexMod) } - expireValue, expireKeyError := bsonutil.FindValueByKey("expireAfterSeconds", &indexMod) - if expireKeyError == nil { - newExpire, ok := expireValue.(int64) - if !ok { - return errors.Errorf( - "expireAfterSeconds must be a number (found %v of type %T): %v", - expireValue, - expireValue, - indexMod, - ) + for _, element := range indexMod { + k := element.Key + if k == "keyPattern" || k == "name" { + continue } - err = updateExpireAfterSeconds(matchingIndex, newExpire) - if err != nil { - return err - } - } - expireValue, hiddenKeyError := bsonutil.FindValueByKey("hidden", &indexMod) - if hiddenKeyError == nil { - newHidden, ok := expireValue.(bool) - if !ok { - return errors.Errorf( - "hidden must be a boolean (found %v of type %T): %v", - expireValue, - expireValue, - indexMod, - ) - } - updateHidden(matchingIndex, newHidden) - } + if k == "expireAfterSeconds" || k == "hidden" || k == "prepareUnique" || k == "unique" { + matchingIndex.Options[k] = element.Value - if expireKeyError != nil && hiddenKeyError != nil { - return errors.Errorf("must specify expireAfterSeconds or hidden: %v", indexMod) + } else { + return errors.Errorf("unknown index option: %v", k) + } } // Update the index. diff --git a/mongodump/oplog_dump_test.go b/mongodump/oplog_dump_test.go index f11f445b4..8a86b6e65 100644 --- a/mongodump/oplog_dump_test.go +++ b/mongodump/oplog_dump_test.go @@ -14,6 +14,7 @@ import ( "testing" "github.com/mongodb/mongo-tools/common/bsonutil" + "github.com/mongodb/mongo-tools/common/db" "github.com/mongodb/mongo-tools/common/failpoint" "github.com/mongodb/mongo-tools/common/log" "github.com/mongodb/mongo-tools/common/testtype" @@ -22,6 +23,7 @@ import ( . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) @@ -131,3 +133,146 @@ func vectoredInsert(ctx context.Context) error { return nil } + +func TestOplogDumpCollModPrepareUnique(t *testing.T) { + testtype.SkipUnlessTestType(t, testtype.IntegrationTestType) + // Oplog is not available in a standalone topology. + testtype.SkipUnlessTestType(t, testtype.ReplSetTestType) + + ctx := t.Context() + + session, err := testutil.GetBareSession() + if err != nil { + t.Fatalf("Failed to get session: %v", err) + } + fcv := testutil.GetFCV(session) + if cmp, err := testutil.CompareFCV(fcv, "6.0"); err != nil || cmp < 0 { + if err != nil { + t.Errorf("error getting FCV: %v", err) + } + t.Skipf("Requires server with FCV 6.0 or later; found %v", fcv) + } + + testCollName := testCollectionNames[0] + + err = session.Database(testDB).CreateCollection(ctx, testCollName) + require.NoError(t, err) + //nolint:errcheck + defer session.Database(testDB).Collection(testCollName).Drop(ctx) + + md, err := simpleMongoDumpInstance() + require.NoError(t, err) + + md.ToolOptions.DB = "" + md.OutputOptions.Oplog = true + md.OutputOptions.Out = "collMod_prepareUnique" + + require.NoError(t, md.Init()) + + // Enable a failpoint so that the test can create oplogs during dump. + failpoint.ParseFailpoints(failpoint.PauseBeforeDumping) + defer failpoint.Reset() + + go func() { + require.NoError(t, createIndexesAndRunCollModPrepareUnique(ctx)) + }() + + //nolint:errcheck + defer tearDownMongoDumpTestData(t) + + require.NoError(t, md.Dump()) + + path, err := os.Getwd() + require.NoError(t, err) + + dumpDir := util.ToUniversalPath(filepath.Join(path, "collMod_prepareUnique")) + dumpDBDir := util.ToUniversalPath(filepath.Join(dumpDir, testDB)) + oplogFilePath := util.ToUniversalPath(filepath.Join(dumpDir, "oplog.bson")) + require.True(t, fileDirExists(dumpDir)) + require.True(t, fileDirExists(dumpDBDir)) + require.True(t, fileDirExists(oplogFilePath)) + + defer os.RemoveAll(dumpDir) + + oplogFile, err := os.Open(oplogFilePath) + require.NoError(t, err) + defer oplogFile.Close() + + bsonSrc := db.NewDecodedBSONSource(db.NewBufferlessBSONSource(oplogFile)) + prepareUniqueTrueCount := 0 + prepareUniqueFalseCount := 0 + + var oplog db.Oplog + for bsonSrc.Next(&oplog) { + require.NoError(t, bsonSrc.Err()) + + if oplog.Namespace == testDB+".$cmd" { + indexDoc, ok := bsonutil.ToMap(oplog.Object)["index"].(bson.D) + if ok { + if bsonutil.ToMap(indexDoc)["prepareUnique"] == true { + prepareUniqueTrueCount++ + } else { + prepareUniqueFalseCount++ + } + } + } + } + require.NoError(t, oplogFile.Close()) + require.Equal(t, 8, prepareUniqueTrueCount) + require.Equal(t, 4, prepareUniqueFalseCount) +} + +func createIndexesAndRunCollModPrepareUnique(ctx context.Context) error { + client, err := testutil.GetBareSession() + if err != nil { + return err + } + + testCollName := testCollectionNames[0] + + indexes := []mongo.IndexModel{ + { + Keys: bson.D{{"a", 1}}, + }, + { + Keys: bson.D{{"b", 1}}, + Options: options.Index().SetHidden(true), + }, + { + Keys: bson.D{{"c", 1}}, + Options: options.Index().SetExpireAfterSeconds(100000), + }, + { + Keys: bson.D{{"d", 1}}, + Options: options.Index().SetExpireAfterSeconds(100000).SetHidden(true), + }, + } + + _, err = client.Database(testDB).Collection(testCollName).Indexes().CreateMany( + ctx, + indexes, + ) + if err != nil { + return err + } + + for _, index := range indexes { + for _, prepareUnique := range []bool{true, false, true} { + res := client.Database(testDB).RunCommand( + ctx, + bson.D{ + {"collMod", testCollName}, + {"index", bson.D{ + {"keyPattern", index.Keys}, + {"prepareUnique", prepareUnique}, + }}, + }, + ) + if res.Err() != nil { + return res.Err() + } + } + } + + return nil +} diff --git a/mongorestore/oplog_test.go b/mongorestore/oplog_test.go index 0a3ce2243..c8b1d6af1 100644 --- a/mongorestore/oplog_test.go +++ b/mongorestore/oplog_test.go @@ -767,3 +767,66 @@ func testOplogRestoreVectoredInsert(t *testing.T, linked bool) { } require.Equal(t, len(expectedDocs), i) } + +func TestOplogRestoreCollModPrepareUnique(t *testing.T) { + testtype.SkipUnlessTestType(t, testtype.IntegrationTestType) + + ctx := t.Context() + + session, err := testutil.GetBareSession() + if err != nil { + t.Fatalf("Failed to get session: %v", err) + } + //nolint:errcheck + defer session.Disconnect(ctx) + + fcv := testutil.GetFCV(session) + if cmp, err := testutil.CompareFCV(fcv, "6.0"); err != nil || cmp < 0 { + if err != nil { + t.Errorf("error getting FCV: %v", err) + } + t.Skipf("Requires server with FCV 6.0 or later; found %v", fcv) + } + + // Prepare the test by creating the necessary collection. + require.NoError(t, session.Database("mongodump_test_db").Drop(ctx)) + require.NoError(t, session.Database("mongodump_test_db").CreateCollection(ctx, "coll1")) + + oplogFileName := "testdata/oplogs/bson/collMod_prepareUnique.bson" + + args := []string{ + DirectoryOption, "testdata/coll_without_index", + OplogReplayOption, + DropOption, + OplogFileOption, oplogFileName, + } + + restore, err := getRestoreWithArgs(args...) + require.NoError(t, err) + defer restore.Close() + + // Run mongorestore + result := restore.Restore() + require.NoError(t, result.Err) + require.Equal(t, int64(0), result.Failures) + + db := session.Database("mongodump_test_db") + + cursor, err := db.RunCommandCursor(ctx, bson.D{ + {"listIndexes", "coll1"}, + }) + require.NoError(t, err) + + var indexSpecs []bson.M + require.NoError(t, cursor.All(ctx, &indexSpecs)) + + require.Len(t, indexSpecs, 5) + + for _, indexSpec := range indexSpecs { + if indexSpec["name"] != "_id_" { + prepareUnique, ok := indexSpec["prepareUnique"].(bool) + require.True(t, ok) + require.True(t, prepareUnique) + } + } +} diff --git a/mongorestore/testdata/oplogs/bson/collMod_prepareUnique.bson b/mongorestore/testdata/oplogs/bson/collMod_prepareUnique.bson new file mode 100644 index 000000000..a19a6cb57 Binary files /dev/null and b/mongorestore/testdata/oplogs/bson/collMod_prepareUnique.bson differ