@@ -3157,3 +3157,274 @@ func importWithIngestOpts(
31573157 _ , _ , err = mi .ImportDocuments ()
31583158 return err
31593159}
3160+
3161+ // TestImportModeUpsertFields tests --mode with --upsertFields a,c (compound key matching).
3162+ // The collection starts with two docs: {a:1234, c:222, x:"original field"} and
3163+ // {a:4567, c:333, x:"original field"}.
3164+ // The import file has five entries:
3165+ // - (a=1234,c=222) appears twice; last write sets b="blah"
3166+ // - (a=4567,c=333) matches the second doc and sets b="yyy"
3167+ // - (a=4567,c=222) has no match and becomes a new doc with b="asdf"
3168+ func TestImportModeUpsertFields (t * testing.T ) {
3169+ testtype .SkipUnlessTestType (t , testtype .IntegrationTestType )
3170+
3171+ const (
3172+ dbName = "mongoimport_modes_upsertfields_test"
3173+ collName = "c"
3174+ )
3175+
3176+ sessionProvider , _ , err := testutil .GetBareSessionProvider ()
3177+ require .NoError (t , err )
3178+ client , err := sessionProvider .GetSession ()
3179+ require .NoError (t , err )
3180+ t .Cleanup (func () {
3181+ _ = client .Database (dbName ).Drop (context .Background ())
3182+ })
3183+
3184+ coll := client .Database (dbName ).Collection (collName )
3185+ ns := & options.Namespace {DB : dbName , Collection : collName }
3186+ dir := t .TempDir ()
3187+
3188+ importFile := writeJSONLinesFile (t , dir , "upsert2.json" , []map [string ]any {
3189+ {"a" : 1234 , "b" : 4567 , "c" : 222 },
3190+ {"a" : 4567 , "b" : "yyy" , "c" : 333 },
3191+ {"a" : 1234 , "b" : "blah" , "c" : 222 },
3192+ {"a" : "xxx" , "b" : "test" , "c" : - 1 },
3193+ {"a" : 4567 , "b" : "asdf" , "c" : 222 },
3194+ })
3195+
3196+ t .Run ("mode=wrong returns error" , func (t * testing.T ) {
3197+ setupUpsertFieldsDocs (t , coll )
3198+ err := runImportOpts (t , ns , importFile , IngestOptions {Mode : "wrong" , UpsertFields : "a,c" })
3199+ require .ErrorContains (t , err , "invalid --mode argument" )
3200+ })
3201+
3202+ t .Run ("mode=insert returns error" , func (t * testing.T ) {
3203+ setupUpsertFieldsDocs (t , coll )
3204+ err := runImportOpts (
3205+ t ,
3206+ ns ,
3207+ importFile ,
3208+ IngestOptions {Mode : modeInsert , UpsertFields : "a,c" },
3209+ )
3210+ require .ErrorContains (t , err , "cannot use --upsertFields with --mode=insert" )
3211+ })
3212+
3213+ // Default mode, mode=upsert, and deprecated --upsert all replace matched docs.
3214+ for _ , tc := range []struct {
3215+ name string
3216+ opts IngestOptions
3217+ }{
3218+ {"default mode" , IngestOptions {UpsertFields : "a,c" }},
3219+ {"deprecated --upsert" , IngestOptions {Upsert : true , UpsertFields : "a,c" }},
3220+ {"mode=upsert" , IngestOptions {Mode : modeUpsert , UpsertFields : "a,c" }},
3221+ } {
3222+ t .Run (tc .name + " replaces matched docs" , func (t * testing.T ) {
3223+ setupUpsertFieldsDocs (t , coll )
3224+ require .NoError (t , runImportOpts (t , ns , importFile , tc .opts ))
3225+
3226+ var doc map [string ]any
3227+ require .NoError (t , coll .FindOne (t .Context (), bson.D {{"b" , "blah" }}).Decode (& doc ))
3228+ assert .Nil (t , doc ["x" ], "upsert replaces doc; original x field gone" )
3229+
3230+ doc = map [string ]any {}
3231+ require .NoError (t , coll .FindOne (t .Context (), bson.D {{"b" , "yyy" }}).Decode (& doc ))
3232+ assert .Nil (t , doc ["x" ], "upsert replaces doc; original x field gone" )
3233+
3234+ doc = map [string ]any {}
3235+ require .NoError (t , coll .FindOne (t .Context (), bson.D {{"b" , "asdf" }}).Decode (& doc ))
3236+ assert .Nil (t , doc ["x" ], "newly inserted doc has no x field" )
3237+ })
3238+ }
3239+
3240+ t .Run ("mode=merge updates matched docs and preserves unset fields" , func (t * testing.T ) {
3241+ setupUpsertFieldsDocs (t , coll )
3242+ require .NoError (
3243+ t ,
3244+ runImportOpts (t , ns , importFile , IngestOptions {Mode : modeMerge , UpsertFields : "a,c" }),
3245+ )
3246+
3247+ var doc map [string ]any
3248+ require .NoError (t , coll .FindOne (t .Context (), bson.D {{"b" , "blah" }}).Decode (& doc ))
3249+ assert .Equal (t , "original field" , doc ["x" ], "merge preserves x on matched doc" )
3250+
3251+ doc = map [string ]any {}
3252+ require .NoError (t , coll .FindOne (t .Context (), bson.D {{"b" , "yyy" }}).Decode (& doc ))
3253+ assert .Equal (t , "original field" , doc ["x" ], "merge preserves x on matched doc" )
3254+
3255+ doc = map [string ]any {}
3256+ require .NoError (t , coll .FindOne (t .Context (), bson.D {{"b" , "asdf" }}).Decode (& doc ))
3257+ assert .Nil (t , doc ["x" ], "newly inserted doc has no x field" )
3258+ })
3259+ }
3260+
3261+ // TestImportModeByID tests --mode without --upsertFields (matches on _id).
3262+ // The collection starts with {_id:"one", a:"original value", x:"original field"} and
3263+ // {_id:"two", a:"original value 2", x:"original field"}.
3264+ // The import file has five entries; _id="one" appears four times and last write wins,
3265+ // leaving it as {a:"unicorns", b:"zebras"}. _id="two" has one entry: {a:"xxx", b:"yyy"}.
3266+ func TestImportModeByID (t * testing.T ) {
3267+ testtype .SkipUnlessTestType (t , testtype .IntegrationTestType )
3268+
3269+ const (
3270+ dbName = "mongoimport_modes_byid_test"
3271+ collName = "c"
3272+ )
3273+
3274+ sessionProvider , _ , err := testutil .GetBareSessionProvider ()
3275+ require .NoError (t , err )
3276+ client , err := sessionProvider .GetSession ()
3277+ require .NoError (t , err )
3278+ t .Cleanup (func () {
3279+ _ = client .Database (dbName ).Drop (context .Background ())
3280+ })
3281+
3282+ coll := client .Database (dbName ).Collection (collName )
3283+ ns := & options.Namespace {DB : dbName , Collection : collName }
3284+ dir := t .TempDir ()
3285+
3286+ importFile := writeJSONLinesFile (t , dir , "upsert1.json" , []map [string ]any {
3287+ {"_id" : "one" , "a" : 1234 , "b" : 4567 },
3288+ {"_id" : "two" , "a" : "xxx" , "b" : "yyy" },
3289+ {"_id" : "one" , "a" : "foo" , "b" : "blah" },
3290+ {"_id" : "one" , "a" : "test" , "b" : "test" },
3291+ {"_id" : "one" , "a" : "unicorns" , "b" : "zebras" },
3292+ })
3293+
3294+ t .Run ("mode=wrong returns error" , func (t * testing.T ) {
3295+ setupByIDDocs (t , coll )
3296+ err := runImportOpts (t , ns , importFile , IngestOptions {Mode : "wrong" })
3297+ require .ErrorContains (t , err , "invalid --mode argument" )
3298+ })
3299+
3300+ // mode=insert and the default mode both skip all duplicates.
3301+ for _ , tc := range []struct {
3302+ name string
3303+ opts IngestOptions
3304+ }{
3305+ {"mode=insert" , IngestOptions {Mode : modeInsert }},
3306+ {"default mode" , IngestOptions {}},
3307+ } {
3308+ t .Run (tc .name + " skips duplicates" , func (t * testing.T ) {
3309+ setupByIDDocs (t , coll )
3310+ require .NoError (t , runImportOpts (t , ns , importFile , tc .opts ))
3311+
3312+ n , err := coll .CountDocuments (t .Context (), bson.D {})
3313+ require .NoError (t , err )
3314+ assert .EqualValues (t , 2 , n , "all entries were duplicates; count unchanged" )
3315+
3316+ var doc map [string ]any
3317+ require .NoError (t , coll .FindOne (t .Context (), bson.D {{"_id" , "one" }}).Decode (& doc ))
3318+ assert .Equal (t , "original value" , doc ["a" ], "_id=one unchanged" )
3319+ assert .Equal (t , "original field" , doc ["x" ], "_id=one unchanged" )
3320+ })
3321+ }
3322+
3323+ // Deprecated --upsert and explicit mode=upsert both replace docs by _id.
3324+ for _ , tc := range []struct {
3325+ name string
3326+ opts IngestOptions
3327+ }{
3328+ {"deprecated --upsert" , IngestOptions {Upsert : true }},
3329+ {"mode=upsert" , IngestOptions {Mode : modeUpsert }},
3330+ } {
3331+ t .Run (tc .name + " replaces docs" , func (t * testing.T ) {
3332+ setupByIDDocs (t , coll )
3333+ require .NoError (t , runImportOpts (t , ns , importFile , tc .opts ))
3334+
3335+ var doc map [string ]any
3336+ require .NoError (t , coll .FindOne (t .Context (), bson.D {{"_id" , "one" }}).Decode (& doc ))
3337+ assert .Equal (t , "unicorns" , doc ["a" ], "last write wins" )
3338+ assert .Equal (t , "zebras" , doc ["b" ], "last write wins" )
3339+ assert .Nil (t , doc ["x" ], "upsert replaces doc; original x field gone" )
3340+
3341+ doc = map [string ]any {}
3342+ require .NoError (t , coll .FindOne (t .Context (), bson.D {{"_id" , "two" }}).Decode (& doc ))
3343+ assert .Equal (t , "xxx" , doc ["a" ])
3344+ assert .Equal (t , "yyy" , doc ["b" ])
3345+ assert .Nil (t , doc ["x" ], "upsert replaces doc; original x field gone" )
3346+ })
3347+ }
3348+
3349+ t .Run ("mode=merge updates docs and preserves unset fields" , func (t * testing.T ) {
3350+ setupByIDDocs (t , coll )
3351+ require .NoError (t , runImportOpts (t , ns , importFile , IngestOptions {Mode : modeMerge }))
3352+
3353+ var doc map [string ]any
3354+ require .NoError (t , coll .FindOne (t .Context (), bson.D {{"_id" , "one" }}).Decode (& doc ))
3355+ assert .Equal (t , "unicorns" , doc ["a" ], "last write wins" )
3356+ assert .Equal (t , "zebras" , doc ["b" ], "last write wins" )
3357+ assert .Equal (t , "original field" , doc ["x" ], "merge preserves x" )
3358+
3359+ doc = map [string ]any {}
3360+ require .NoError (t , coll .FindOne (t .Context (), bson.D {{"_id" , "two" }}).Decode (& doc ))
3361+ assert .Equal (t , "xxx" , doc ["a" ])
3362+ assert .Equal (t , "yyy" , doc ["b" ])
3363+ assert .Equal (t , "original field" , doc ["x" ], "merge preserves x" )
3364+ })
3365+ }
3366+
3367+ func setupUpsertFieldsDocs (t * testing.T , coll * mongo.Collection ) {
3368+ t .Helper ()
3369+ require .NoError (t , coll .Drop (t .Context ()))
3370+ _ , err := coll .InsertMany (
3371+ t .Context (),
3372+ []any {
3373+ bson.D {{"a" , 1234 }, {"b" , "000000" }, {"c" , 222 }, {"x" , "original field" }},
3374+ bson.D {{"a" , 4567 }, {"b" , "111111" }, {"c" , 333 }, {"x" , "original field" }},
3375+ },
3376+ )
3377+ require .NoError (t , err )
3378+ }
3379+
3380+ func setupByIDDocs (t * testing.T , coll * mongo.Collection ) {
3381+ t .Helper ()
3382+ require .NoError (t , coll .Drop (t .Context ()))
3383+ _ , err := coll .InsertMany (
3384+ t .Context (),
3385+ []any {
3386+ bson.D {{"_id" , "one" }, {"a" , "original value" }, {"x" , "original field" }},
3387+ bson.D {{"_id" , "two" }, {"a" , "original value 2" }, {"x" , "original field" }},
3388+ },
3389+ )
3390+ require .NoError (t , err )
3391+ }
3392+
3393+ // runImportOpts is like importWithIngestOpts but also returns errors from New,
3394+ // allowing callers to test for invalid-options errors.
3395+ func runImportOpts (
3396+ t * testing.T ,
3397+ ns * options.Namespace ,
3398+ filePath string ,
3399+ ingestOpts IngestOptions ,
3400+ ) error {
3401+ t .Helper ()
3402+ toolOptions , err := testutil .GetToolOptions ()
3403+ require .NoError (t , err )
3404+ toolOptions .Namespace = ns
3405+ mi , err := New (Options {
3406+ ToolOptions : toolOptions ,
3407+ InputOptions : & InputOptions {File : filePath , ParseGrace : "stop" },
3408+ IngestOptions : & ingestOpts ,
3409+ })
3410+ if err != nil {
3411+ return err
3412+ }
3413+ defer mi .Close ()
3414+ _ , _ , err = mi .ImportDocuments ()
3415+ return err
3416+ }
3417+
3418+ // writeJSONLinesFile marshals each doc in docs as a JSON object and writes them
3419+ // as newline-separated lines to a file in dir named name. Returns the file path.
3420+ func writeJSONLinesFile (t * testing.T , dir , name string , docs []map [string ]any ) string {
3421+ t .Helper ()
3422+ var buf bytes.Buffer
3423+ for _ , doc := range docs {
3424+ b , err := json .Marshal (doc )
3425+ require .NoError (t , err )
3426+ buf .Write (b )
3427+ buf .WriteByte ('\n' )
3428+ }
3429+ return writeTestFile (t , dir , name , buf .String ())
3430+ }
0 commit comments