@@ -3467,6 +3467,116 @@ func checkByIDImport(t *testing.T, coll *mongo.Collection, isMerge bool) {
34673467 }
34683468}
34693469
3470+ // TestImportModeUpsertIDSubdoc verifies that --mode=upsert uses the full
3471+ // subdocument _id as the upsert key, preserving field order through a
3472+ // round-trip of export → upsert-replace → re-import.
3473+ func TestImportModeUpsertIDSubdoc (t * testing.T ) {
3474+ testtype .SkipUnlessTestType (t , testtype .IntegrationTestType )
3475+
3476+ const (
3477+ dbName = "mongoimport_upsert_subdoc_test"
3478+ collName = "c"
3479+ )
3480+
3481+ sessionProvider , _ , err := testutil .GetBareSessionProvider ()
3482+ require .NoError (t , err )
3483+ client , err := sessionProvider .GetSession ()
3484+ require .NoError (t , err )
3485+ t .Cleanup (func () {
3486+ _ = client .Database (dbName ).Drop (context .Background ())
3487+ })
3488+
3489+ coll := client .Database (dbName ).Collection (collName )
3490+ ns := & options.Namespace {DB : dbName , Collection : collName }
3491+
3492+ origDocs := subdocIDDocs ("string" )
3493+ insertDocs := make ([]any , len (origDocs ))
3494+ for i , d := range origDocs {
3495+ insertDocs [i ] = d
3496+ }
3497+ _ , err = coll .InsertMany (t .Context (), insertDocs )
3498+ require .NoError (t , err )
3499+
3500+ exportedFile := exportCollectionToFile (t , ns )
3501+ str2File := writeSubdocIDFile (t , "str2" )
3502+
3503+ t .Run ("upsert with replacement data updates all docs in place" , func (t * testing.T ) {
3504+ require .NoError (t , runImportOpts (t , ns , str2File , IngestOptions {Mode : modeUpsert }))
3505+ n , err := coll .CountDocuments (t .Context (), bson.D {})
3506+ require .NoError (t , err )
3507+ assert .EqualValues (t , 20 , n , "count should be unchanged after upsert" )
3508+ n , err = coll .CountDocuments (t .Context (), bson.D {{"x" , "str2" }})
3509+ require .NoError (t , err )
3510+ assert .EqualValues (t , 20 , n , "all docs should have x=str2 after upsert" )
3511+ })
3512+
3513+ t .Run ("re-import original export reverts all docs" , func (t * testing.T ) {
3514+ require .NoError (t , runImportOpts (t , ns , exportedFile , IngestOptions {Mode : modeUpsert }))
3515+ n , err := coll .CountDocuments (t .Context (), bson.D {})
3516+ require .NoError (t , err )
3517+ assert .EqualValues (t , 20 , n , "count should be unchanged after re-import" )
3518+ n , err = coll .CountDocuments (t .Context (), bson.D {{"x" , "string" }})
3519+ require .NoError (t , err )
3520+ assert .EqualValues (t , 20 , n , "all docs should have x=string after re-import" )
3521+ })
3522+ }
3523+
3524+ func exportCollectionToFile (t * testing.T , ns * options.Namespace ) string {
3525+ t .Helper ()
3526+ exportFile , err := os .CreateTemp (t .TempDir (), "export-*.json" )
3527+ require .NoError (t , err )
3528+ exportToolOptions , err := testutil .GetToolOptions ()
3529+ require .NoError (t , err )
3530+ exportToolOptions .Namespace = ns
3531+ me , err := mongoexport .New (mongoexport.Options {
3532+ ToolOptions : exportToolOptions ,
3533+ OutputFormatOptions : & mongoexport.OutputFormatOptions {
3534+ Type : "json" ,
3535+ JSONFormat : "canonical" ,
3536+ },
3537+ InputOptions : & mongoexport.InputOptions {},
3538+ })
3539+ require .NoError (t , err )
3540+ defer me .Close ()
3541+ _ , err = me .Export (exportFile )
3542+ require .NoError (t , err )
3543+ require .NoError (t , exportFile .Close ())
3544+ return exportFile .Name ()
3545+ }
3546+
3547+ func writeSubdocIDFile (t * testing.T , xFieldValue string ) string {
3548+ t .Helper ()
3549+ f , err := os .CreateTemp (t .TempDir (), "subdoc-*.json" )
3550+ require .NoError (t , err )
3551+ for _ , doc := range subdocIDDocs (xFieldValue ) {
3552+ b , err := bson .MarshalExtJSON (doc , true , false )
3553+ require .NoError (t , err )
3554+ _ , err = f .Write (b )
3555+ require .NoError (t , err )
3556+ _ , err = f .Write ([]byte ("\n " ))
3557+ require .NoError (t , err )
3558+ }
3559+ require .NoError (t , f .Close ())
3560+ return f .Name ()
3561+ }
3562+
3563+ func subdocIDDocs (xFieldValue string ) []bson.D {
3564+ docs := make ([]bson.D , 0 , 20 )
3565+ for i := range int32 (4 ) {
3566+ for j := range int32 (5 ) {
3567+ docs = append (docs , bson.D {
3568+ {"_id" , bson.D {
3569+ {"a" , i },
3570+ {"b" , bson.A {int32 (0 ), int32 (1 ), int32 (2 ), bson.D {{"c" , j }, {"d" , "foo" }}}},
3571+ {"e" , "bar" },
3572+ }},
3573+ {"x" , xFieldValue },
3574+ })
3575+ }
3576+ }
3577+ return docs
3578+ }
3579+
34703580// writeJSONLinesFile marshals each doc in docs as a JSON object and writes them
34713581// as newline-separated lines to a file in dir named name. Returns the file path.
34723582func writeJSONLinesFile (t * testing.T , dir , name string , docs []map [string ]any ) string {
0 commit comments