Skip to content

Commit f17d51d

Browse files
committed
TOOLS-4148 Convert mongoimport mode.js to Go integration tests
Adds TestImportModes covering all --mode / --upsertFields combinations from jstests/import/mode.js. Notable implementation details: - Table-driven with importModeCase struct; wantErrContains checks specific error substrings via require.ErrorContains rather than just require.Error - runImportOpts helper returns errors from New() (unlike importWithIngestOpts which uses require.NoError), enabling error-path test cases for invalid option combinations - writeJSONLinesFile marshals []map[string]any to newline-separated JSON instead of hard-coding string literals - map[string]any decode results must be reset to map[string]any{} between FindOne/Decode calls; stale keys from a previous decode persist otherwise (bug found via test failure)
1 parent b62485f commit f17d51d

File tree

2 files changed

+271
-147
lines changed

2 files changed

+271
-147
lines changed

mongoimport/mongoimport_test.go

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)