Skip to content

Commit f7c66f6

Browse files
committed
Convert test/qa-tests/jstests/export/fields_csv.js to Go
1 parent c7d9f40 commit f7c66f6

4 files changed

Lines changed: 253 additions & 175 deletions

File tree

.ai-plans/2026-03-13/js-to-go-test-migration/plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ The mongoexport library API: create `mongoexport.MongoExport{Options: opts}`, th
260260

261261
- [ ] **Step 5: Convert `fields_json.js`** (NEW) — `TestExportFieldsJSON`: verify `--fields` limits which fields appear in JSON export output.
262262

263-
- [ ] **Step 6: Convert `fields_csv.js`** (EXTEND) — Add end-to-end case to `mongoexport_test.go`: insert docs, export with `--fields` and `--csv` against a real server, verify only the specified fields appear in CSV output.
263+
- [x] **Step 6: Convert `fields_csv.js`** (EXTEND) — `TestRoundTripFieldsCSV` in `mongoimport/mongoimport_test.go`: round-trip with `--fields a` (verifies b,c excluded) and `--fields a,b,c` (verifies all included, `_id` not exported). `TestExportNestedFieldsCSV` in `mongoexport/mongoexport_test.go`: 6 nested field/projection cases checking CSV output directly.
264264

265265
- [ ] **Step 7: Convert `nested_fields_csv.js`** (EXTEND) — Add to `csv_test.go`: export with dotted field paths in `--fields`, verify CSV flattening behavior.
266266

mongoexport/mongoexport_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ package mongoexport
88

99
import (
1010
"bytes"
11+
"context"
12+
"encoding/csv"
1113
"encoding/json"
1214
"errors"
1315
"fmt"
1416
"io"
1517
"os"
1618
"runtime"
19+
"strings"
1720
"testing"
1821

1922
"github.com/mongodb/mongo-tools/common/bsonutil"
@@ -229,6 +232,143 @@ func TestMongoExportTOOLS1952(t *testing.T) {
229232
})
230233
}
231234

235+
// TestExportNestedFieldsCSV verifies that mongoexport correctly handles nested
236+
// field paths and $ projection in --fields with --csv output.
237+
// Covers the nested field and projection scenarios of fields_csv.js.
238+
func TestExportNestedFieldsCSV(t *testing.T) {
239+
testtype.SkipUnlessTestType(t, testtype.IntegrationTestType)
240+
log.SetWriter(io.Discard)
241+
242+
const dbName = "mongoexport_nestedfieldscsv_test"
243+
const collName = "source"
244+
245+
sessionProvider, _, err := testutil.GetBareSessionProvider()
246+
require.NoError(t, err)
247+
client, err := sessionProvider.GetSession()
248+
require.NoError(t, err)
249+
t.Cleanup(func() {
250+
if err := client.Database(dbName).Drop(context.Background()); err != nil {
251+
t.Errorf("dropping test database: %v", err)
252+
}
253+
})
254+
255+
coll := client.Database(dbName).Collection(collName)
256+
_, err = coll.InsertMany(t.Context(), []any{
257+
bson.D{{"a", bson.A{1, 2, 3, 4, 5}}, {"b", bson.D{{"c", bson.A{-1, -2, -3, -4}}}}},
258+
bson.D{
259+
{"a", int32(1)},
260+
{"b", int32(2)},
261+
{"c", int32(3)},
262+
{"d", bson.D{{"e", bson.A{4, 5, 6}}}},
263+
},
264+
bson.D{
265+
{"a", int32(1)},
266+
{"b", int32(2)},
267+
{"c", int32(3)},
268+
{"d", int32(5)},
269+
{"e", bson.D{{"0", bson.A{"foo", "bar", "baz"}}}},
270+
},
271+
bson.D{
272+
{"a", int32(1)},
273+
{"b", int32(2)},
274+
{"c", int32(3)},
275+
{"d", bson.A{4, 5, 6}},
276+
{"e", bson.A{bson.D{{"0", 0}, {"1", 1}}, bson.D{{"2", 2}, {"3", 3}}}},
277+
},
278+
})
279+
require.NoError(t, err)
280+
281+
toolOptions, err := testutil.GetToolOptions()
282+
require.NoError(t, err)
283+
toolOptions.Namespace = &options.Namespace{DB: dbName, Collection: collName}
284+
285+
rows := parseCSVRows(t, exportNestedCSV(t, toolOptions, "d.e.2", ""))
286+
assert.True(
287+
t, rowsContainValue(rows, "d.e.2", "6"),
288+
"d.e.2 should select the third element of d.e array",
289+
)
290+
291+
rows = parseCSVRows(t, exportNestedCSV(t, toolOptions, "e.0.0", ""))
292+
assert.True(
293+
t, rowsContainValue(rows, "e.0.0", "foo"),
294+
"e.0.0 should select nested numeric array value",
295+
)
296+
297+
rows = parseCSVRows(t, exportNestedCSV(t, toolOptions, "b,d.1,e.1.3", ""))
298+
assert.True(t, rowsContainValue(rows, "b", "2"), "b column should contain 2")
299+
assert.True(t, rowsContainValue(rows, "d.1", "5"), "d.1 column should contain 5")
300+
assert.True(t, rowsContainValue(rows, "e.1.3", "3"), "e.1.3 column should contain 3")
301+
302+
// $ projection strips the trailing .$ from the header name and wraps the result in [].
303+
rows = parseCSVRows(t, exportNestedCSV(t, toolOptions, "d.$", `{"d": 4}`))
304+
assert.True(
305+
t, rowsContainValue(rows, "d", "[4]"),
306+
"d.$ with query {d:4} should select matching element",
307+
)
308+
309+
rows = parseCSVRows(t, exportNestedCSV(t, toolOptions, "a.$", `{"a": {"$gt": 1}}`))
310+
assert.True(
311+
t, rowsContainValue(rows, "a", "[2]"),
312+
"a.$ with query {a:{$gt:1}} should select matching element",
313+
)
314+
315+
rows = parseCSVRows(t, exportNestedCSV(t, toolOptions, "b.c.$", `{"b.c": -1}`))
316+
assert.True(
317+
t, rowsContainValue(rows, "b.c", "[-1]"),
318+
"b.c.$ with query {b.c:-1} should select matching element",
319+
)
320+
}
321+
322+
func exportNestedCSV(t *testing.T, toolOptions *options.ToolOptions, fields, query string) string {
323+
t.Helper()
324+
me, err := New(Options{
325+
ToolOptions: toolOptions,
326+
OutputFormatOptions: &OutputFormatOptions{
327+
Type: "csv",
328+
JSONFormat: "canonical",
329+
Fields: fields,
330+
},
331+
InputOptions: &InputOptions{Query: query},
332+
})
333+
require.NoError(t, err)
334+
defer me.Close()
335+
var buf bytes.Buffer
336+
_, err = me.Export(&buf)
337+
require.NoError(t, err)
338+
return buf.String()
339+
}
340+
341+
func parseCSVRows(t *testing.T, output string) []map[string]string {
342+
t.Helper()
343+
r := csv.NewReader(strings.NewReader(output))
344+
records, err := r.ReadAll()
345+
require.NoError(t, err)
346+
if len(records) == 0 {
347+
return nil
348+
}
349+
headers := records[0]
350+
rows := make([]map[string]string, 0, len(records)-1)
351+
for _, record := range records[1:] {
352+
row := make(map[string]string, len(headers))
353+
for i, h := range headers {
354+
if i < len(record) {
355+
row[h] = record[i]
356+
}
357+
}
358+
rows = append(rows, row)
359+
}
360+
return rows
361+
}
362+
363+
func rowsContainValue(rows []map[string]string, col, val string) bool {
364+
for _, row := range rows {
365+
if row[col] == val {
366+
return true
367+
}
368+
}
369+
return false
370+
}
371+
232372
func TestBadOptions(t *testing.T) {
233373
testtype.SkipUnlessTestType(t, testtype.IntegrationTestType)
234374

mongoimport/mongoimport_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/stretchr/testify/assert"
3131
"github.com/stretchr/testify/require"
3232
"go.mongodb.org/mongo-driver/v2/bson"
33+
"go.mongodb.org/mongo-driver/v2/mongo"
3334
mopt "go.mongodb.org/mongo-driver/v2/mongo/options"
3435
)
3536

@@ -1885,3 +1886,114 @@ func TestRoundTripFieldFile(t *testing.T) {
18851886
require.NoError(t, err)
18861887
assert.EqualValues(t, 0, n, "c=3 should not have been exported (not in fieldFile)")
18871888
}
1889+
1890+
// TestRoundTripFieldsCSV verifies that mongoexport --csv --fields limits which
1891+
// fields are exported, and that mongoimport correctly restores the filtered data.
1892+
// Covers the first two scenarios of fields_csv.js.
1893+
func TestRoundTripFieldsCSV(t *testing.T) {
1894+
testtype.SkipUnlessTestType(t, testtype.IntegrationTestType)
1895+
1896+
const dbName = "mongoimport_roundtrip_fieldscsv_test"
1897+
1898+
sessionProvider, _, err := testutil.GetBareSessionProvider()
1899+
require.NoError(t, err)
1900+
client, err := sessionProvider.GetSession()
1901+
require.NoError(t, err)
1902+
t.Cleanup(func() {
1903+
if err := client.Database(dbName).Drop(context.Background()); err != nil {
1904+
t.Errorf("dropping test database: %v", err)
1905+
}
1906+
})
1907+
1908+
db := client.Database(dbName)
1909+
_, err = db.Collection("source").InsertMany(t.Context(), []any{
1910+
bson.D{{"a", int32(1)}},
1911+
bson.D{{"a", int32(1)}, {"b", int32(1)}},
1912+
bson.D{{"a", int32(1)}, {"b", int32(2)}, {"c", int32(3)}},
1913+
})
1914+
require.NoError(t, err)
1915+
1916+
exportCSVAndImport(t, dbName, "a", db)
1917+
dest := db.Collection("dest")
1918+
n, err := dest.CountDocuments(t.Context(), bson.D{{"a", int32(1)}})
1919+
require.NoError(t, err)
1920+
assert.EqualValues(t, 3, n, "3 documents should have a=1")
1921+
n, err = dest.CountDocuments(t.Context(), bson.D{{"b", int32(1)}})
1922+
require.NoError(t, err)
1923+
assert.EqualValues(t, 0, n, "b=1 should not have been exported")
1924+
n, err = dest.CountDocuments(t.Context(), bson.D{{"b", int32(2)}})
1925+
require.NoError(t, err)
1926+
assert.EqualValues(t, 0, n, "b=2 should not have been exported")
1927+
n, err = dest.CountDocuments(t.Context(), bson.D{{"c", int32(3)}})
1928+
require.NoError(t, err)
1929+
assert.EqualValues(t, 0, n, "c=3 should not have been exported")
1930+
1931+
exportCSVAndImport(t, dbName, "a,b,c", db)
1932+
n, err = dest.CountDocuments(t.Context(), bson.D{{"a", int32(1)}})
1933+
require.NoError(t, err)
1934+
assert.EqualValues(t, 3, n, "3 documents should have a=1")
1935+
n, err = dest.CountDocuments(t.Context(), bson.D{{"b", int32(1)}})
1936+
require.NoError(t, err)
1937+
assert.EqualValues(t, 1, n, "1 document should have b=1")
1938+
n, err = dest.CountDocuments(t.Context(), bson.D{{"b", int32(2)}})
1939+
require.NoError(t, err)
1940+
assert.EqualValues(t, 1, n, "1 document should have b=2")
1941+
n, err = dest.CountDocuments(t.Context(), bson.D{{"c", int32(3)}})
1942+
require.NoError(t, err)
1943+
assert.EqualValues(t, 1, n, "1 document should have c=3")
1944+
1945+
var fromSource, fromDest bson.M
1946+
q := bson.D{{"a", int32(1)}, {"b", int32(1)}}
1947+
err = db.Collection("source").FindOne(t.Context(), q).Decode(&fromSource)
1948+
require.NoError(t, err)
1949+
err = dest.FindOne(t.Context(), q).Decode(&fromDest)
1950+
require.NoError(t, err)
1951+
assert.NotEqual(t, fromSource["_id"], fromDest["_id"], "_id should not have been exported")
1952+
}
1953+
1954+
func exportCSVAndImport(t *testing.T, dbName, exportFields string, db *mongo.Database) {
1955+
t.Helper()
1956+
require.NoError(t, db.Collection("dest").Drop(t.Context()))
1957+
1958+
exportTarget, err := os.CreateTemp(t.TempDir(), "export-*.csv")
1959+
require.NoError(t, err)
1960+
require.NoError(t, exportTarget.Close())
1961+
1962+
exportToolOptions, err := testutil.GetToolOptions()
1963+
require.NoError(t, err)
1964+
exportToolOptions.Namespace = &options.Namespace{DB: dbName, Collection: "source"}
1965+
me, err := mongoexport.New(mongoexport.Options{
1966+
ToolOptions: exportToolOptions,
1967+
OutputFormatOptions: &mongoexport.OutputFormatOptions{
1968+
Type: "csv",
1969+
JSONFormat: "canonical",
1970+
Fields: exportFields,
1971+
},
1972+
InputOptions: &mongoexport.InputOptions{},
1973+
})
1974+
require.NoError(t, err)
1975+
defer me.Close()
1976+
f, err := os.OpenFile(exportTarget.Name(), os.O_WRONLY, 0o644)
1977+
require.NoError(t, err)
1978+
_, err = me.Export(f)
1979+
require.NoError(t, err)
1980+
require.NoError(t, f.Close())
1981+
1982+
importFields := "a,b,c"
1983+
importToolOptions, err := testutil.GetToolOptions()
1984+
require.NoError(t, err)
1985+
importToolOptions.Namespace = &options.Namespace{DB: dbName, Collection: "dest"}
1986+
mi, err := New(Options{
1987+
ToolOptions: importToolOptions,
1988+
InputOptions: &InputOptions{
1989+
File: exportTarget.Name(),
1990+
Type: "csv",
1991+
Fields: &importFields,
1992+
ParseGrace: "stop",
1993+
},
1994+
IngestOptions: &IngestOptions{},
1995+
})
1996+
require.NoError(t, err)
1997+
_, _, err = mi.ImportDocuments()
1998+
require.NoError(t, err)
1999+
}

0 commit comments

Comments
 (0)