Skip to content

Commit 0012ec3

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 37dd895 commit 0012ec3

2 files changed

Lines changed: 271 additions & 147 deletions

File tree

mongoimport/mongoimport_test.go

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

0 commit comments

Comments
 (0)