@@ -10,6 +10,7 @@ import (
1010 "bufio"
1111 "bytes"
1212 "context"
13+ "encoding/csv"
1314 "encoding/json"
1415 "fmt"
1516 "io"
@@ -2772,3 +2773,246 @@ func TestRoundTripDecimal128(t *testing.T) {
27722773 require .NoError (t , err )
27732774 assert .Equal (t , testDoc , result , "imported doc should match original" )
27742775}
2776+
2777+ // TestImportFields verifies --headerline, --fields, --fieldFile, --ignoreBlanks,
2778+ // nested dotted field names, and extra-fields-beyond-header behavior.
2779+ func TestImportFields (t * testing.T ) {
2780+ testtype .SkipUnlessTestType (t , testtype .IntegrationTestType )
2781+
2782+ const dbName = "mongoimport_fields_test"
2783+
2784+ sessionProvider , _ , err := testutil .GetBareSessionProvider ()
2785+ require .NoError (t , err )
2786+ client , err := sessionProvider .GetSession ()
2787+ require .NoError (t , err )
2788+ t .Cleanup (func () {
2789+ if err := client .Database (dbName ).Drop (context .Background ()); err != nil {
2790+ t .Errorf ("dropping test database: %v" , err )
2791+ }
2792+ })
2793+
2794+ tmpDir := t .TempDir ()
2795+ header := []string {"a" , "b" , "c.xyz" , "d.hij.lkm" }
2796+ rows := [][]string {
2797+ {"foo" , "bar" , "blah" , "qwz" },
2798+ {"bob" , "" , "steve" , "sue" },
2799+ {"one" , "two" , "three" , "four" },
2800+ }
2801+
2802+ for _ , format := range []string {"csv" , "tsv" } {
2803+ testImportFieldsForFormat (t , client , dbName , tmpDir , format , header , rows )
2804+ }
2805+ }
2806+
2807+ func testImportFieldsForFormat (
2808+ t * testing.T ,
2809+ client * mongo.Client ,
2810+ dbName , tmpDir , format string ,
2811+ header []string ,
2812+ rows [][]string ,
2813+ ) {
2814+ t .Helper ()
2815+
2816+ separator , ok := map [string ]rune {
2817+ "csv" : ',' ,
2818+ "tsv" : '\t' ,
2819+ }[format ]
2820+ require .True (t , ok , "found a separator for %#q format" , format )
2821+
2822+ headerFile := filepath .Join (tmpDir , "header." + format )
2823+ writeXSVFile (t , headerFile , separator , append ([][]string {header }, rows ... ))
2824+
2825+ noHeaderFile := filepath .Join (tmpDir , "noheader." + format )
2826+ writeXSVFile (t , noHeaderFile , separator , rows )
2827+ fieldFilePath := writeFieldFile (t , tmpDir , "fieldfile." + format , header )
2828+
2829+ t .Run (format + "/headerline" , func (t * testing.T ) {
2830+ importAndCheckFields (t , client , importFieldsOpts {
2831+ dbName : dbName ,
2832+ inputFile : headerFile ,
2833+ format : format ,
2834+ headerLine : true ,
2835+ })
2836+ })
2837+
2838+ t .Run (format + "/fields" , func (t * testing.T ) {
2839+ fields := "a,b,c.xyz,d.hij.lkm"
2840+ importAndCheckFields (t , client , importFieldsOpts {
2841+ dbName : dbName ,
2842+ inputFile : noHeaderFile ,
2843+ format : format ,
2844+ fields : & fields ,
2845+ })
2846+ })
2847+
2848+ t .Run (format + "/fieldFile" , func (t * testing.T ) {
2849+ importAndCheckFields (t , client , importFieldsOpts {
2850+ dbName : dbName ,
2851+ inputFile : noHeaderFile ,
2852+ format : format ,
2853+ fieldFilePath : fieldFilePath ,
2854+ })
2855+ coll := client .Database (dbName ).Collection (format + "testcoll" )
2856+ var bobDoc bson.M
2857+ err := coll .FindOne (t .Context (), bson.D {{"a" , "bob" }}).Decode (& bobDoc )
2858+ require .NoError (t , err )
2859+ assert .Equal (
2860+ t , "" , bobDoc ["b" ],
2861+ "%s: blank field should be empty string without --ignoreBlanks" , format ,
2862+ )
2863+ })
2864+
2865+ t .Run (format + "/ignoreBlanks" , func (t * testing.T ) {
2866+ importAndCheckFields (t , client , importFieldsOpts {
2867+ dbName : dbName ,
2868+ inputFile : noHeaderFile ,
2869+ format : format ,
2870+ fieldFilePath : fieldFilePath ,
2871+ ignoreBlanks : true ,
2872+ })
2873+ coll := client .Database (dbName ).Collection (format + "testcoll" )
2874+ var bobDoc bson.M
2875+ err := coll .FindOne (t .Context (), bson.D {{"a" , "bob" }}).Decode (& bobDoc )
2876+ require .NoError (t , err )
2877+ _ , hasB := bobDoc ["b" ]
2878+ assert .False (t , hasB , "%s: blank field should be omitted with --ignoreBlanks" , format )
2879+ })
2880+
2881+ t .Run (format + "/noFieldSpec" , func (t * testing.T ) {
2882+ toolOpts , err := testutil .GetToolOptions ()
2883+ require .NoError (t , err )
2884+ toolOpts .Namespace = & options.Namespace {DB : dbName , Collection : format + "testcoll" }
2885+ _ , err = New (Options {
2886+ ToolOptions : toolOpts ,
2887+ InputOptions : & InputOptions {File : noHeaderFile , Type : format , ParseGrace : "stop" },
2888+ IngestOptions : & IngestOptions {},
2889+ })
2890+ assert .Error (t , err , "%s: import without field spec should fail" , format )
2891+ })
2892+ }
2893+
2894+ type importFieldsOpts struct {
2895+ dbName string
2896+ inputFile string
2897+ format string
2898+ fieldFilePath string
2899+ fields * string
2900+ headerLine bool
2901+ ignoreBlanks bool
2902+ }
2903+
2904+ func importAndCheckFields (t * testing.T , client * mongo.Client , o importFieldsOpts ) {
2905+ t .Helper ()
2906+ collName := o .format + "testcoll"
2907+ require .NoError (t , client .Database (o .dbName ).Collection (collName ).Drop (t .Context ()))
2908+ toolOpts , err := testutil .GetToolOptions ()
2909+ require .NoError (t , err )
2910+ toolOpts .Namespace = & options.Namespace {DB : o .dbName , Collection : collName }
2911+ var ffPtr * string
2912+ if o .fieldFilePath != "" {
2913+ ffPtr = & o .fieldFilePath
2914+ }
2915+ mi , err := New (Options {
2916+ ToolOptions : toolOpts ,
2917+ InputOptions : & InputOptions {
2918+ File : o .inputFile ,
2919+ Type : o .format ,
2920+ HeaderLine : o .headerLine ,
2921+ FieldFile : ffPtr ,
2922+ Fields : o .fields ,
2923+ ParseGrace : "stop" ,
2924+ },
2925+ IngestOptions : & IngestOptions {IgnoreBlanks : o .ignoreBlanks },
2926+ })
2927+ require .NoError (t , err )
2928+ _ , _ , err = mi .ImportDocuments ()
2929+ require .NoError (t , err )
2930+
2931+ coll := client .Database (o .dbName ).Collection (collName )
2932+ n , err := coll .CountDocuments (t .Context (), bson.D {})
2933+ require .NoError (t , err )
2934+ assert .EqualValues (t , 3 , n , "%s: should import 3 documents" , o .format )
2935+
2936+ nestedQuery := bson.D {
2937+ {"a" , "foo" },
2938+ {"b" , "bar" },
2939+ {"c.xyz" , "blah" },
2940+ {"d.hij.lkm" , "qwz" },
2941+ }
2942+ n , err = coll .CountDocuments (t .Context (), nestedQuery )
2943+ require .NoError (t , err )
2944+ assert .EqualValues (t , 1 , n , "%s: nested fields should be stored correctly" , o .format )
2945+ }
2946+
2947+ // TestImportExtraFields verifies that CSV columns beyond the fieldFile mapping
2948+ // are imported as field4, field5, etc.
2949+ func TestImportExtraFields (t * testing.T ) {
2950+ testtype .SkipUnlessTestType (t , testtype .IntegrationTestType )
2951+
2952+ const dbName = "mongoimport_extrafields_test"
2953+ const collName = "extrafields"
2954+
2955+ sessionProvider , _ , err := testutil .GetBareSessionProvider ()
2956+ require .NoError (t , err )
2957+ client , err := sessionProvider .GetSession ()
2958+ require .NoError (t , err )
2959+ t .Cleanup (func () {
2960+ if err := client .Database (dbName ).Drop (context .Background ()); err != nil {
2961+ t .Errorf ("dropping test database: %v" , err )
2962+ }
2963+ })
2964+
2965+ tmpDir := t .TempDir ()
2966+ extraRows := [][]string {
2967+ {"foo" , "bar" , "blah" , "qwz" },
2968+ {"bob" , "" , "steve" , "sue" },
2969+ {"one" , "two" , "three" , "four" , "extra1" , "extra2" , "extra3" },
2970+ }
2971+ extraFile := filepath .Join (tmpDir , "extrafields.csv" )
2972+ writeXSVFile (t , extraFile , ',' , extraRows )
2973+
2974+ fieldNames := []string {"a" , "b" , "c.xyz" , "d.hij.lkm" }
2975+ fieldFilePath := writeFieldFile (t , tmpDir , "fieldfile" , fieldNames )
2976+
2977+ toolOpts , err := testutil .GetToolOptions ()
2978+ require .NoError (t , err )
2979+ toolOpts .Namespace = & options.Namespace {DB : dbName , Collection : collName }
2980+ mi , err := New (Options {
2981+ ToolOptions : toolOpts ,
2982+ InputOptions : & InputOptions {
2983+ File : extraFile ,
2984+ Type : "csv" ,
2985+ FieldFile : & fieldFilePath ,
2986+ ParseGrace : "stop" ,
2987+ },
2988+ IngestOptions : & IngestOptions {},
2989+ })
2990+ require .NoError (t , err )
2991+ _ , _ , err = mi .ImportDocuments ()
2992+ require .NoError (t , err )
2993+
2994+ var doc bson.M
2995+ err = client .Database (dbName ).Collection (collName ).
2996+ FindOne (t .Context (), bson.D {{"a" , "one" }}).Decode (& doc )
2997+ require .NoError (t , err )
2998+ assert .Equal (t , "extra1" , doc ["field4" ], "field4 should contain first extra value" )
2999+ assert .Equal (t , "extra2" , doc ["field5" ], "field5 should contain second extra value" )
3000+ assert .Equal (t , "extra3" , doc ["field6" ], "field6 should contain third extra value" )
3001+ }
3002+
3003+ func writeXSVFile (t * testing.T , path string , separator rune , records [][]string ) {
3004+ t .Helper ()
3005+ f , err := os .Create (path )
3006+ require .NoError (t , err )
3007+ w := csv .NewWriter (f )
3008+ w .Comma = separator
3009+ require .NoError (t , w .WriteAll (records ))
3010+ require .NoError (t , f .Close ())
3011+ }
3012+
3013+ func writeFieldFile (t * testing.T , dir , name string , fields []string ) string {
3014+ t .Helper ()
3015+ path := filepath .Join (dir , name )
3016+ require .NoError (t , os .WriteFile (path , []byte (strings .Join (fields , "\n " )+ "\n " ), 0o644 ))
3017+ return path
3018+ }
0 commit comments