From e97533c98921989daf9a09c044945d6e6505f9e0 Mon Sep 17 00:00:00 2001 From: h3n4l Date: Fri, 23 Jan 2026 14:27:17 +0800 Subject: [PATCH 1/2] feat(test): add comprehensive multi-database testing infrastructure - Add multi-container support for MongoDB 4.4, MongoDB 8.0, and DocumentDB - Migrate all existing tests to run against all three databases - Add unicode test suite covering CJK, Arabic, emoji, and special characters - Add complex query tests (nested $and/$or, $elemMatch, cursor modifiers) - Add BSON helper function tests (ObjectId, ISODate, NumberLong, etc.) - Add fuzz tests for edge case discovery - Add shared test fixtures (users, unicode samples, complex documents) - Add fixture loader utility for test data management Co-Authored-By: Claude Opus 4.5 --- collection_test.go | 3884 ++++++++++++---------- database_test.go | 448 +-- error_test.go | 190 +- fuzz_test.go | 64 + go.mod | 2 +- internal/testutil/container.go | 182 +- internal/testutil/container_test.go | 39 + internal/testutil/fixtures.go | 48 + internal/translator/bson_helpers_test.go | 168 + testdata/complex_documents.json | 67 + testdata/unicode_samples.json | 18 + testdata/users.json | 7 + unicode_test.go | 146 + write_test.go | 742 +++-- 14 files changed, 3506 insertions(+), 2499 deletions(-) create mode 100644 fuzz_test.go create mode 100644 internal/testutil/container_test.go create mode 100644 internal/testutil/fixtures.go create mode 100644 internal/translator/bson_helpers_test.go create mode 100644 testdata/complex_documents.json create mode 100644 testdata/unicode_samples.json create mode 100644 testdata/users.json create mode 100644 unicode_test.go diff --git a/collection_test.go b/collection_test.go index 37ef0f2..b2044b9 100644 --- a/collection_test.go +++ b/collection_test.go @@ -2,6 +2,7 @@ package gomongo_test import ( "context" + "fmt" "strings" "testing" "time" @@ -14,818 +15,840 @@ import ( ) func TestFindEmptyCollection(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_empty" - defer testutil.CleanupDatabase(t, client, dbName) - - gc := gomongo.NewClient(client) - ctx := context.Background() - - result, err := gc.Execute(ctx, dbName, "db.users.find()") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 0, result.RowCount) - require.Empty(t, result.Rows) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_empty_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + result, err := gc.Execute(ctx, dbName, "db.users.find()") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 0, result.RowCount) + require.Empty(t, result.Rows) + }) } func TestFindWithDocuments(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_docs" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_docs_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Insert test documents - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "alice", "age": 30}, - bson.M{"name": "bob", "age": 25}, + // Insert test documents + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "alice", "age": 30}, + bson.M{"name": "bob", "age": 25}, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + result, err := gc.Execute(ctx, dbName, "db.users.find()") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 2, result.RowCount) + require.Len(t, result.Rows, 2) + + // Verify JSON format + for _, row := range result.Rows { + require.Contains(t, row, "name") + require.Contains(t, row, "age") + require.Contains(t, row, "_id") + } }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - result, err := gc.Execute(ctx, dbName, "db.users.find()") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 2, result.RowCount) - require.Len(t, result.Rows, 2) - - // Verify JSON format - for _, row := range result.Rows { - require.Contains(t, row, "name") - require.Contains(t, row, "age") - require.Contains(t, row, "_id") - } } func TestFindWithEmptyFilter(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_empty_filter" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_empty_filter_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - collection := client.Database(dbName).Collection("items") - _, err := collection.InsertOne(ctx, bson.M{"item": "test"}) - require.NoError(t, err) + collection := db.Client.Database(dbName).Collection("items") + _, err := collection.InsertOne(ctx, bson.M{"item": "test"}) + require.NoError(t, err) - gc := gomongo.NewClient(client) - result, err := gc.Execute(ctx, dbName, "db.items.find({})") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) + gc := gomongo.NewClient(db.Client) + result, err := gc.Execute(ctx, dbName, "db.items.find({})") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 1, result.RowCount) + }) } func TestFindOneEmptyCollection(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_findone_empty" - defer testutil.CleanupDatabase(t, client, dbName) - - gc := gomongo.NewClient(client) - ctx := context.Background() - - result, err := gc.Execute(ctx, dbName, "db.users.findOne()") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 0, result.RowCount) - require.Empty(t, result.Rows) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_findone_empty_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + result, err := gc.Execute(ctx, dbName, "db.users.findOne()") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 0, result.RowCount) + require.Empty(t, result.Rows) + }) } func TestFindOneWithDocuments(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_findone_docs" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_findone_docs_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "alice", "age": 30}, - bson.M{"name": "bob", "age": 25}, + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "alice", "age": 30}, + bson.M{"name": "bob", "age": 25}, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + result, err := gc.Execute(ctx, dbName, "db.users.findOne()") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 1, result.RowCount) + require.Len(t, result.Rows, 1) + require.Contains(t, result.Rows[0], "name") + require.Contains(t, result.Rows[0], "age") + require.Contains(t, result.Rows[0], "_id") }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - result, err := gc.Execute(ctx, dbName, "db.users.findOne()") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - require.Len(t, result.Rows, 1) - require.Contains(t, result.Rows[0], "name") - require.Contains(t, result.Rows[0], "age") - require.Contains(t, result.Rows[0], "_id") } func TestFindOneWithFilter(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_findone_filter" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "alice", "age": 30}, - bson.M{"name": "bob", "age": 25}, - bson.M{"name": "carol", "age": 35}, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - tests := []struct { - name string - statement string - expectMatch bool - checkResult func(t *testing.T, row string) - }{ - { - name: "filter by string", - statement: `db.users.findOne({ name: "bob" })`, - expectMatch: true, - checkResult: func(t *testing.T, row string) { - require.Contains(t, row, `"bob"`) - require.Contains(t, row, `"age": 25`) - }, - }, - { - name: "filter by number", - statement: `db.users.findOne({ age: 35 })`, - expectMatch: true, - checkResult: func(t *testing.T, row string) { - require.Contains(t, row, `"carol"`) - }, - }, - { - name: "filter with no match", - statement: `db.users.findOne({ name: "nobody" })`, - expectMatch: false, - }, - { - name: "filter with $gt operator", - statement: `db.users.findOne({ age: { $gt: 30 } })`, - expectMatch: true, - checkResult: func(t *testing.T, row string) { - require.Contains(t, row, `"carol"`) - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result, err := gc.Execute(ctx, dbName, tc.statement) - require.NoError(t, err) - require.NotNil(t, result) - if tc.expectMatch { - require.Equal(t, 1, result.RowCount) - if tc.checkResult != nil { - tc.checkResult(t, result.Rows[0]) - } - } else { - require.Equal(t, 0, result.RowCount) - } + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_findone_filter_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "alice", "age": 30}, + bson.M{"name": "bob", "age": 25}, + bson.M{"name": "carol", "age": 35}, }) - } + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + tests := []struct { + name string + statement string + expectMatch bool + checkResult func(t *testing.T, row string) + }{ + { + name: "filter by string", + statement: `db.users.findOne({ name: "bob" })`, + expectMatch: true, + checkResult: func(t *testing.T, row string) { + require.Contains(t, row, `"bob"`) + require.Contains(t, row, `"age": 25`) + }, + }, + { + name: "filter by number", + statement: `db.users.findOne({ age: 35 })`, + expectMatch: true, + checkResult: func(t *testing.T, row string) { + require.Contains(t, row, `"carol"`) + }, + }, + { + name: "filter with no match", + statement: `db.users.findOne({ name: "nobody" })`, + expectMatch: false, + }, + { + name: "filter with $gt operator", + statement: `db.users.findOne({ age: { $gt: 30 } })`, + expectMatch: true, + checkResult: func(t *testing.T, row string) { + require.Contains(t, row, `"carol"`) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := gc.Execute(ctx, dbName, tc.statement) + require.NoError(t, err) + require.NotNil(t, result) + if tc.expectMatch { + require.Equal(t, 1, result.RowCount) + if tc.checkResult != nil { + tc.checkResult(t, result.Rows[0]) + } + } else { + require.Equal(t, 0, result.RowCount) + } + }) + } + }) } func TestFindOneWithOptions(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_findone_options" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - collection := client.Database(dbName).Collection("items") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "apple", "price": 1}, - bson.M{"name": "banana", "price": 2}, - bson.M{"name": "carrot", "price": 3}, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - tests := []struct { - name string - statement string - checkResult func(t *testing.T, row string) - }{ - { - name: "sort ascending - returns first", - statement: `db.items.findOne().sort({ price: 1 })`, - checkResult: func(t *testing.T, row string) { - require.Contains(t, row, `"apple"`) - }, - }, - { - name: "sort descending - returns first", - statement: `db.items.findOne().sort({ price: -1 })`, - checkResult: func(t *testing.T, row string) { - require.Contains(t, row, `"carrot"`) - }, - }, - { - name: "skip", - statement: `db.items.findOne().sort({ price: 1 }).skip(1)`, - checkResult: func(t *testing.T, row string) { - require.Contains(t, row, `"banana"`) - }, - }, - { - name: "projection include", - statement: `db.items.findOne().projection({ name: 1, _id: 0 })`, - checkResult: func(t *testing.T, row string) { - require.Contains(t, row, `"name"`) - require.NotContains(t, row, `"_id"`) - require.NotContains(t, row, `"price"`) - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result, err := gc.Execute(ctx, dbName, tc.statement) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - tc.checkResult(t, result.Rows[0]) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_findone_options_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + collection := db.Client.Database(dbName).Collection("items") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "apple", "price": 1}, + bson.M{"name": "banana", "price": 2}, + bson.M{"name": "carrot", "price": 3}, }) - } + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + tests := []struct { + name string + statement string + checkResult func(t *testing.T, row string) + }{ + { + name: "sort ascending - returns first", + statement: `db.items.findOne().sort({ price: 1 })`, + checkResult: func(t *testing.T, row string) { + require.Contains(t, row, `"apple"`) + }, + }, + { + name: "sort descending - returns first", + statement: `db.items.findOne().sort({ price: -1 })`, + checkResult: func(t *testing.T, row string) { + require.Contains(t, row, `"carrot"`) + }, + }, + { + name: "skip", + statement: `db.items.findOne().sort({ price: 1 }).skip(1)`, + checkResult: func(t *testing.T, row string) { + require.Contains(t, row, `"banana"`) + }, + }, + { + name: "projection include", + statement: `db.items.findOne().projection({ name: 1, _id: 0 })`, + checkResult: func(t *testing.T, row string) { + require.Contains(t, row, `"name"`) + require.NotContains(t, row, `"_id"`) + require.NotContains(t, row, `"price"`) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := gc.Execute(ctx, dbName, tc.statement) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 1, result.RowCount) + tc.checkResult(t, result.Rows[0]) + }) + } + }) } func TestFindWithFilter(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_filter" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "alice", "age": 30, "active": true}, - bson.M{"name": "bob", "age": 25, "active": false}, - bson.M{"name": "carol", "age": 35, "active": true}, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - tests := []struct { - name string - statement string - expectedCount int - checkResult func(t *testing.T, rows []string) - }{ - { - name: "filter by string", - statement: `db.users.find({ name: "alice" })`, - expectedCount: 1, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"alice"`) - }, - }, - { - name: "filter by number", - statement: `db.users.find({ age: 25 })`, - expectedCount: 1, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"bob"`) - }, - }, - { - name: "filter by boolean", - statement: `db.users.find({ active: true })`, - expectedCount: 2, - }, - { - name: "filter with $gt operator", - statement: `db.users.find({ age: { $gt: 28 } })`, - expectedCount: 2, - }, - { - name: "filter with $lte operator", - statement: `db.users.find({ age: { $lte: 25 } })`, - expectedCount: 1, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"bob"`) - }, - }, - { - name: "filter with multiple conditions", - statement: `db.users.find({ active: true, age: { $gte: 30 } })`, - expectedCount: 2, - }, - { - name: "filter with $in operator", - statement: `db.users.find({ name: { $in: ["alice", "bob"] } })`, - expectedCount: 2, - }, - { - name: "filter with no matches", - statement: `db.users.find({ name: "nobody" })`, - expectedCount: 0, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result, err := gc.Execute(ctx, dbName, tc.statement) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, tc.expectedCount, result.RowCount) - if tc.checkResult != nil && result.RowCount > 0 { - tc.checkResult(t, result.Rows) - } + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_filter_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "alice", "age": 30, "active": true}, + bson.M{"name": "bob", "age": 25, "active": false}, + bson.M{"name": "carol", "age": 35, "active": true}, }) - } + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + tests := []struct { + name string + statement string + expectedCount int + checkResult func(t *testing.T, rows []string) + }{ + { + name: "filter by string", + statement: `db.users.find({ name: "alice" })`, + expectedCount: 1, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"alice"`) + }, + }, + { + name: "filter by number", + statement: `db.users.find({ age: 25 })`, + expectedCount: 1, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"bob"`) + }, + }, + { + name: "filter by boolean", + statement: `db.users.find({ active: true })`, + expectedCount: 2, + }, + { + name: "filter with $gt operator", + statement: `db.users.find({ age: { $gt: 28 } })`, + expectedCount: 2, + }, + { + name: "filter with $lte operator", + statement: `db.users.find({ age: { $lte: 25 } })`, + expectedCount: 1, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"bob"`) + }, + }, + { + name: "filter with multiple conditions", + statement: `db.users.find({ active: true, age: { $gte: 30 } })`, + expectedCount: 2, + }, + { + name: "filter with $in operator", + statement: `db.users.find({ name: { $in: ["alice", "bob"] } })`, + expectedCount: 2, + }, + { + name: "filter with no matches", + statement: `db.users.find({ name: "nobody" })`, + expectedCount: 0, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := gc.Execute(ctx, dbName, tc.statement) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, tc.expectedCount, result.RowCount) + if tc.checkResult != nil && result.RowCount > 0 { + tc.checkResult(t, result.Rows) + } + }) + } + }) } func TestFindWithCursorModifications(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_cursor" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - collection := client.Database(dbName).Collection("items") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "apple", "price": 1, "category": "fruit"}, - bson.M{"name": "banana", "price": 2, "category": "fruit"}, - bson.M{"name": "carrot", "price": 3, "category": "vegetable"}, - bson.M{"name": "date", "price": 4, "category": "fruit"}, - bson.M{"name": "eggplant", "price": 5, "category": "vegetable"}, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - tests := []struct { - name string - statement string - expectedCount int - checkResult func(t *testing.T, rows []string) - }{ - { - name: "sort ascending", - statement: `db.items.find().sort({ price: 1 })`, - expectedCount: 5, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"apple"`) - require.Contains(t, rows[4], `"eggplant"`) - }, - }, - { - name: "sort descending", - statement: `db.items.find().sort({ price: -1 })`, - expectedCount: 5, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"eggplant"`) - require.Contains(t, rows[4], `"apple"`) - }, - }, - { - name: "limit", - statement: `db.items.find().limit(2)`, - expectedCount: 2, - }, - { - name: "skip", - statement: `db.items.find().sort({ price: 1 }).skip(2)`, - expectedCount: 3, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"carrot"`) - }, - }, - { - name: "sort and limit", - statement: `db.items.find().sort({ price: -1 }).limit(3)`, - expectedCount: 3, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"eggplant"`) - require.Contains(t, rows[2], `"carrot"`) - }, - }, - { - name: "skip and limit", - statement: `db.items.find().sort({ price: 1 }).skip(1).limit(2)`, - expectedCount: 2, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"banana"`) - require.Contains(t, rows[1], `"carrot"`) - }, - }, - { - name: "projection include", - statement: `db.items.find().projection({ name: 1 })`, - expectedCount: 5, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"name"`) - require.Contains(t, rows[0], `"_id"`) - require.NotContains(t, rows[0], `"price"`) - require.NotContains(t, rows[0], `"category"`) - }, - }, - { - name: "projection exclude", - statement: `db.items.find().projection({ _id: 0, category: 0 })`, - expectedCount: 5, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"name"`) - require.Contains(t, rows[0], `"price"`) - require.NotContains(t, rows[0], `"_id"`) - require.NotContains(t, rows[0], `"category"`) - }, - }, - { - name: "filter with sort and limit", - statement: `db.items.find({ category: "fruit" }).sort({ price: -1 }).limit(2)`, - expectedCount: 2, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"date"`) - require.Contains(t, rows[1], `"banana"`) - }, - }, - { - name: "all modifiers combined", - statement: `db.items.find({ category: "fruit" }).sort({ price: 1 }).skip(1).limit(2).projection({ name: 1, price: 1, _id: 0 })`, - expectedCount: 2, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"banana"`) - require.Contains(t, rows[1], `"date"`) - require.NotContains(t, rows[0], `"_id"`) - require.NotContains(t, rows[0], `"category"`) - }, - }, - { - name: "method chain order: limit before sort", - statement: `db.items.find().limit(3).sort({ price: -1 })`, - expectedCount: 3, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"eggplant"`) - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result, err := gc.Execute(ctx, dbName, tc.statement) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, tc.expectedCount, result.RowCount) - if tc.checkResult != nil && result.RowCount > 0 { - tc.checkResult(t, result.Rows) - } + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_cursor_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + collection := db.Client.Database(dbName).Collection("items") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "apple", "price": 1, "category": "fruit"}, + bson.M{"name": "banana", "price": 2, "category": "fruit"}, + bson.M{"name": "carrot", "price": 3, "category": "vegetable"}, + bson.M{"name": "date", "price": 4, "category": "fruit"}, + bson.M{"name": "eggplant", "price": 5, "category": "vegetable"}, }) - } + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + tests := []struct { + name string + statement string + expectedCount int + checkResult func(t *testing.T, rows []string) + }{ + { + name: "sort ascending", + statement: `db.items.find().sort({ price: 1 })`, + expectedCount: 5, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"apple"`) + require.Contains(t, rows[4], `"eggplant"`) + }, + }, + { + name: "sort descending", + statement: `db.items.find().sort({ price: -1 })`, + expectedCount: 5, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"eggplant"`) + require.Contains(t, rows[4], `"apple"`) + }, + }, + { + name: "limit", + statement: `db.items.find().limit(2)`, + expectedCount: 2, + }, + { + name: "skip", + statement: `db.items.find().sort({ price: 1 }).skip(2)`, + expectedCount: 3, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"carrot"`) + }, + }, + { + name: "sort and limit", + statement: `db.items.find().sort({ price: -1 }).limit(3)`, + expectedCount: 3, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"eggplant"`) + require.Contains(t, rows[2], `"carrot"`) + }, + }, + { + name: "skip and limit", + statement: `db.items.find().sort({ price: 1 }).skip(1).limit(2)`, + expectedCount: 2, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"banana"`) + require.Contains(t, rows[1], `"carrot"`) + }, + }, + { + name: "projection include", + statement: `db.items.find().projection({ name: 1 })`, + expectedCount: 5, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"name"`) + require.Contains(t, rows[0], `"_id"`) + require.NotContains(t, rows[0], `"price"`) + require.NotContains(t, rows[0], `"category"`) + }, + }, + { + name: "projection exclude", + statement: `db.items.find().projection({ _id: 0, category: 0 })`, + expectedCount: 5, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"name"`) + require.Contains(t, rows[0], `"price"`) + require.NotContains(t, rows[0], `"_id"`) + require.NotContains(t, rows[0], `"category"`) + }, + }, + { + name: "filter with sort and limit", + statement: `db.items.find({ category: "fruit" }).sort({ price: -1 }).limit(2)`, + expectedCount: 2, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"date"`) + require.Contains(t, rows[1], `"banana"`) + }, + }, + { + name: "all modifiers combined", + statement: `db.items.find({ category: "fruit" }).sort({ price: 1 }).skip(1).limit(2).projection({ name: 1, price: 1, _id: 0 })`, + expectedCount: 2, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"banana"`) + require.Contains(t, rows[1], `"date"`) + require.NotContains(t, rows[0], `"_id"`) + require.NotContains(t, rows[0], `"category"`) + }, + }, + { + name: "method chain order: limit before sort", + statement: `db.items.find().limit(3).sort({ price: -1 })`, + expectedCount: 3, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"eggplant"`) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := gc.Execute(ctx, dbName, tc.statement) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, tc.expectedCount, result.RowCount) + if tc.checkResult != nil && result.RowCount > 0 { + tc.checkResult(t, result.Rows) + } + }) + } + }) } func TestFindWithProjectionArg(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_proj_arg" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_proj_arg_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Insert test data - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "age": 30, "city": "NYC"}, - bson.M{"name": "Bob", "age": 25, "city": "LA"}, - }) - require.NoError(t, err) + // Insert test data + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "age": 30, "city": "NYC"}, + bson.M{"name": "Bob", "age": 25, "city": "LA"}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // find with projection as 2nd argument - result, err := gc.Execute(ctx, dbName, `db.users.find({}, { name: 1, _id: 0 })`) - require.NoError(t, err) - require.Equal(t, 2, result.RowCount) + // find with projection as 2nd argument + result, err := gc.Execute(ctx, dbName, `db.users.find({}, { name: 1, _id: 0 })`) + require.NoError(t, err) + require.Equal(t, 2, result.RowCount) - // Verify only 'name' field is returned - for _, row := range result.Rows { - require.Contains(t, row, "name") - require.NotContains(t, row, "age") - require.NotContains(t, row, "city") - } + // Verify only 'name' field is returned + for _, row := range result.Rows { + require.Contains(t, row, "name") + require.NotContains(t, row, "age") + require.NotContains(t, row, "city") + } + }) } func TestFindWithHintOption(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_hint" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_hint_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "age": 30}, - bson.M{"name": "Bob", "age": 25}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "age": 30}, + bson.M{"name": "Bob", "age": 25}, + }) + require.NoError(t, err) - // Create index - _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "name", Value: 1}}, - }) - require.NoError(t, err) + // Create index + _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "name", Value: 1}}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // find with hint option (index name) - result, err := gc.Execute(ctx, dbName, `db.users.find({}, {}, { hint: "name_1" })`) - require.NoError(t, err) - require.Equal(t, 2, result.RowCount) + // find with hint option (index name) + result, err := gc.Execute(ctx, dbName, `db.users.find({}, {}, { hint: "name_1" })`) + require.NoError(t, err) + require.Equal(t, 2, result.RowCount) + }) } func TestFindWithMaxMinOptions(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_maxmin" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + // DocumentDB doesn't support min/max options + if db.Name == "documentdb" { + t.Skip("DocumentDB doesn't support min/max options") + } - ctx := context.Background() + dbName := fmt.Sprintf("testdb_find_maxmin_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - coll := client.Database(dbName).Collection("items") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"price": 10}, - bson.M{"price": 20}, - bson.M{"price": 30}, - bson.M{"price": 40}, - bson.M{"price": 50}, - }) - require.NoError(t, err) + ctx := context.Background() - // Create index on price field (required for min/max) - _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "price", Value: 1}}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("items") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"price": 10}, + bson.M{"price": 20}, + bson.M{"price": 30}, + bson.M{"price": 40}, + bson.M{"price": 50}, + }) + require.NoError(t, err) + + // Create index on price field (required for min/max) + _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "price", Value: 1}}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // find with min and max options (requires hint) - result, err := gc.Execute(ctx, dbName, `db.items.find({}, {}, { hint: { price: 1 }, min: { price: 20 }, max: { price: 40 } })`) - require.NoError(t, err) - // Should return items with price 20 and 30 (max is exclusive) - require.Equal(t, 2, result.RowCount) + // find with min and max options (requires hint) + result, err := gc.Execute(ctx, dbName, `db.items.find({}, {}, { hint: { price: 1 }, min: { price: 20 }, max: { price: 40 } })`) + require.NoError(t, err) + // Should return items with price 20 and 30 (max is exclusive) + require.Equal(t, 2, result.RowCount) + }) } func TestFindWithMaxTimeMSOption(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_maxtime" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_maxtime_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice"}, - bson.M{"name": "Bob"}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice"}, + bson.M{"name": "Bob"}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // find with maxTimeMS option - result, err := gc.Execute(ctx, dbName, `db.users.find({}, {}, { maxTimeMS: 5000 })`) - require.NoError(t, err) - require.Equal(t, 2, result.RowCount) + // find with maxTimeMS option + result, err := gc.Execute(ctx, dbName, `db.users.find({}, {}, { maxTimeMS: 5000 })`) + require.NoError(t, err) + require.Equal(t, 2, result.RowCount) + }) } func TestFindOneWithProjectionAndOptions(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_findone_proj_opts" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_findone_proj_opts_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "age": 30, "city": "NYC"}, - bson.M{"name": "Bob", "age": 25, "city": "LA"}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "age": 30, "city": "NYC"}, + bson.M{"name": "Bob", "age": 25, "city": "LA"}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // findOne with projection as 2nd argument - result, err := gc.Execute(ctx, dbName, `db.users.findOne({}, { name: 1, _id: 0 })`) - require.NoError(t, err) - require.Equal(t, 1, result.RowCount) - require.Contains(t, result.Rows[0], "name") - require.NotContains(t, result.Rows[0], "age") + // findOne with projection as 2nd argument + result, err := gc.Execute(ctx, dbName, `db.users.findOne({}, { name: 1, _id: 0 })`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], "name") + require.NotContains(t, result.Rows[0], "age") + }) } func TestFindOneWithHintOption(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_findone_hint" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_findone_hint_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "age": 30}, - bson.M{"name": "Bob", "age": 25}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "age": 30}, + bson.M{"name": "Bob", "age": 25}, + }) + require.NoError(t, err) - // Create index - _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "name", Value: 1}}, - }) - require.NoError(t, err) + // Create index + _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "name", Value: 1}}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // findOne with hint option (index name) - result, err := gc.Execute(ctx, dbName, `db.users.findOne({}, {}, { hint: "name_1" })`) - require.NoError(t, err) - require.Equal(t, 1, result.RowCount) + // findOne with hint option (index name) + result, err := gc.Execute(ctx, dbName, `db.users.findOne({}, {}, { hint: "name_1" })`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + }) } func TestFindOneWithMaxTimeMSOption(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_findone_maxtime" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_findone_maxtime_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice"}, - bson.M{"name": "Bob"}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice"}, + bson.M{"name": "Bob"}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // findOne with maxTimeMS option - result, err := gc.Execute(ctx, dbName, `db.users.findOne({}, {}, { maxTimeMS: 5000 })`) - require.NoError(t, err) - require.Equal(t, 1, result.RowCount) + // findOne with maxTimeMS option + result, err := gc.Execute(ctx, dbName, `db.users.findOne({}, {}, { maxTimeMS: 5000 })`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + }) } func TestAggregateBasic(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_basic" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - collection := client.Database(dbName).Collection("items") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "apple", "price": 1, "category": "fruit"}, - bson.M{"name": "banana", "price": 2, "category": "fruit"}, - bson.M{"name": "carrot", "price": 3, "category": "vegetable"}, - bson.M{"name": "date", "price": 4, "category": "fruit"}, - bson.M{"name": "eggplant", "price": 5, "category": "vegetable"}, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - tests := []struct { - name string - statement string - expectedCount int - checkResult func(t *testing.T, rows []string) - }{ - { - name: "empty pipeline", - statement: `db.items.aggregate([])`, - expectedCount: 5, - }, - { - name: "empty pipeline no args", - statement: `db.items.aggregate()`, - expectedCount: 5, - }, - { - name: "$match stage", - statement: `db.items.aggregate([{ $match: { category: "fruit" } }])`, - expectedCount: 3, - }, - { - name: "$sort ascending", - statement: `db.items.aggregate([{ $sort: { price: 1 } }])`, - expectedCount: 5, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"apple"`) - require.Contains(t, rows[4], `"eggplant"`) - }, - }, - { - name: "$sort descending", - statement: `db.items.aggregate([{ $sort: { price: -1 } }])`, - expectedCount: 5, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"eggplant"`) - require.Contains(t, rows[4], `"apple"`) - }, - }, - { - name: "$limit stage", - statement: `db.items.aggregate([{ $limit: 2 }])`, - expectedCount: 2, - }, - { - name: "$skip stage", - statement: `db.items.aggregate([{ $sort: { price: 1 } }, { $skip: 3 }])`, - expectedCount: 2, - }, - { - name: "$project include", - statement: `db.items.aggregate([{ $project: { name: 1, _id: 0 } }])`, - expectedCount: 5, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"name"`) - require.NotContains(t, rows[0], `"_id"`) - require.NotContains(t, rows[0], `"price"`) - }, - }, - { - name: "$count stage", - statement: `db.items.aggregate([{ $count: "total" }])`, - expectedCount: 1, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"total": 5`) - }, - }, - { - name: "multi-stage: match and sort", - statement: `db.items.aggregate([{ $match: { category: "fruit" } }, { $sort: { price: -1 } }])`, - expectedCount: 3, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"date"`) - }, - }, - { - name: "multi-stage: match, sort, limit", - statement: `db.items.aggregate([{ $match: { category: "fruit" } }, { $sort: { price: 1 } }, { $limit: 2 }])`, - expectedCount: 2, - checkResult: func(t *testing.T, rows []string) { - require.Contains(t, rows[0], `"apple"`) - require.Contains(t, rows[1], `"banana"`) - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result, err := gc.Execute(ctx, dbName, tc.statement) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, tc.expectedCount, result.RowCount) - if tc.checkResult != nil && result.RowCount > 0 { - tc.checkResult(t, result.Rows) - } + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_agg_basic_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + collection := db.Client.Database(dbName).Collection("items") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "apple", "price": 1, "category": "fruit"}, + bson.M{"name": "banana", "price": 2, "category": "fruit"}, + bson.M{"name": "carrot", "price": 3, "category": "vegetable"}, + bson.M{"name": "date", "price": 4, "category": "fruit"}, + bson.M{"name": "eggplant", "price": 5, "category": "vegetable"}, }) - } + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + tests := []struct { + name string + statement string + expectedCount int + checkResult func(t *testing.T, rows []string) + }{ + { + name: "empty pipeline", + statement: `db.items.aggregate([])`, + expectedCount: 5, + }, + { + name: "empty pipeline no args", + statement: `db.items.aggregate()`, + expectedCount: 5, + }, + { + name: "$match stage", + statement: `db.items.aggregate([{ $match: { category: "fruit" } }])`, + expectedCount: 3, + }, + { + name: "$sort ascending", + statement: `db.items.aggregate([{ $sort: { price: 1 } }])`, + expectedCount: 5, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"apple"`) + require.Contains(t, rows[4], `"eggplant"`) + }, + }, + { + name: "$sort descending", + statement: `db.items.aggregate([{ $sort: { price: -1 } }])`, + expectedCount: 5, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"eggplant"`) + require.Contains(t, rows[4], `"apple"`) + }, + }, + { + name: "$limit stage", + statement: `db.items.aggregate([{ $limit: 2 }])`, + expectedCount: 2, + }, + { + name: "$skip stage", + statement: `db.items.aggregate([{ $sort: { price: 1 } }, { $skip: 3 }])`, + expectedCount: 2, + }, + { + name: "$project include", + statement: `db.items.aggregate([{ $project: { name: 1, _id: 0 } }])`, + expectedCount: 5, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"name"`) + require.NotContains(t, rows[0], `"_id"`) + require.NotContains(t, rows[0], `"price"`) + }, + }, + { + name: "$count stage", + statement: `db.items.aggregate([{ $count: "total" }])`, + expectedCount: 1, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"total": 5`) + }, + }, + { + name: "multi-stage: match and sort", + statement: `db.items.aggregate([{ $match: { category: "fruit" } }, { $sort: { price: -1 } }])`, + expectedCount: 3, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"date"`) + }, + }, + { + name: "multi-stage: match, sort, limit", + statement: `db.items.aggregate([{ $match: { category: "fruit" } }, { $sort: { price: 1 } }, { $limit: 2 }])`, + expectedCount: 2, + checkResult: func(t *testing.T, rows []string) { + require.Contains(t, rows[0], `"apple"`) + require.Contains(t, rows[1], `"banana"`) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := gc.Execute(ctx, dbName, tc.statement) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, tc.expectedCount, result.RowCount) + if tc.checkResult != nil && result.RowCount > 0 { + tc.checkResult(t, result.Rows) + } + }) + } + }) } func TestAggregateGroup(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_group" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - collection := client.Database(dbName).Collection("sales") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"item": "apple", "quantity": 10, "price": 1.5}, - bson.M{"item": "banana", "quantity": 5, "price": 2.0}, - bson.M{"item": "apple", "quantity": 8, "price": 1.5}, - bson.M{"item": "banana", "quantity": 3, "price": 2.0}, - bson.M{"item": "carrot", "quantity": 15, "price": 0.5}, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - tests := []struct { - name string - statement string - expectedCount int - checkResult func(t *testing.T, rows []string) - }{ - { - name: "$group by field", - statement: `db.sales.aggregate([{ $group: { _id: "$item" } }])`, - expectedCount: 3, - }, - { - name: "$group with $sum", - statement: `db.sales.aggregate([{ $group: { _id: "$item", totalQuantity: { $sum: "$quantity" } } }])`, - expectedCount: 3, - }, - { - name: "$group with $avg", - statement: `db.sales.aggregate([{ $group: { _id: "$item", avgQuantity: { $avg: "$quantity" } } }])`, - expectedCount: 3, - }, - { - name: "$group with multiple accumulators", - statement: `db.sales.aggregate([ + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_agg_group_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + collection := db.Client.Database(dbName).Collection("sales") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"item": "apple", "quantity": 10, "price": 1.5}, + bson.M{"item": "banana", "quantity": 5, "price": 2.0}, + bson.M{"item": "apple", "quantity": 8, "price": 1.5}, + bson.M{"item": "banana", "quantity": 3, "price": 2.0}, + bson.M{"item": "carrot", "quantity": 15, "price": 0.5}, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + tests := []struct { + name string + statement string + expectedCount int + checkResult func(t *testing.T, rows []string) + }{ + { + name: "$group by field", + statement: `db.sales.aggregate([{ $group: { _id: "$item" } }])`, + expectedCount: 3, + }, + { + name: "$group with $sum", + statement: `db.sales.aggregate([{ $group: { _id: "$item", totalQuantity: { $sum: "$quantity" } } }])`, + expectedCount: 3, + }, + { + name: "$group with $avg", + statement: `db.sales.aggregate([{ $group: { _id: "$item", avgQuantity: { $avg: "$quantity" } } }])`, + expectedCount: 3, + }, + { + name: "$group with multiple accumulators", + statement: `db.sales.aggregate([ { $group: { _id: "$item", totalQuantity: { $sum: "$quantity" }, @@ -833,223 +856,226 @@ func TestAggregateGroup(t *testing.T) { count: { $sum: 1 } }} ])`, - expectedCount: 3, - }, - { - name: "$group then $sort", - statement: `db.sales.aggregate([ + expectedCount: 3, + }, + { + name: "$group then $sort", + statement: `db.sales.aggregate([ { $group: { _id: "$item", total: { $sum: "$quantity" } } }, { $sort: { total: -1 } } ])`, - expectedCount: 3, - checkResult: func(t *testing.T, rows []string) { - // apple has 18 total, should be first - require.Contains(t, rows[0], `"apple"`) - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result, err := gc.Execute(ctx, dbName, tc.statement) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, tc.expectedCount, result.RowCount) - if tc.checkResult != nil && result.RowCount > 0 { - tc.checkResult(t, result.Rows) - } - }) - } + expectedCount: 3, + checkResult: func(t *testing.T, rows []string) { + // apple has 18 total, should be first + require.Contains(t, rows[0], `"apple"`) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := gc.Execute(ctx, dbName, tc.statement) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, tc.expectedCount, result.RowCount) + if tc.checkResult != nil && result.RowCount > 0 { + tc.checkResult(t, result.Rows) + } + }) + } + }) } func TestAggregateCollectionAccess(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_coll_access" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - collection := client.Database(dbName).Collection("my-items") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "test1"}, - bson.M{"name": "test2"}, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - tests := []struct { - name string - statement string - }{ - {"dot notation", `db.users.aggregate([])`}, - {"bracket notation", `db["my-items"].aggregate([{ $limit: 1 }])`}, - {"getCollection", `db.getCollection("my-items").aggregate([{ $limit: 1 }])`}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result, err := gc.Execute(ctx, dbName, tc.statement) - require.NoError(t, err) - require.NotNil(t, result) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_agg_coll_access_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + collection := db.Client.Database(dbName).Collection("my-items") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "test1"}, + bson.M{"name": "test2"}, }) - } + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + tests := []struct { + name string + statement string + }{ + {"dot notation", `db.users.aggregate([])`}, + {"bracket notation", `db["my-items"].aggregate([{ $limit: 1 }])`}, + {"getCollection", `db.getCollection("my-items").aggregate([{ $limit: 1 }])`}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := gc.Execute(ctx, dbName, tc.statement) + require.NoError(t, err) + require.NotNil(t, result) + }) + } + }) } // TestAggregateFilteredSubset tests the "Filtered Subset" example from MongoDB docs // https://www.mongodb.com/docs/manual/tutorial/aggregation-examples/filtered-subset/ func TestAggregateFilteredSubset(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_filtered" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - collection := client.Database(dbName).Collection("persons") - _, err := collection.InsertMany(ctx, []any{ - bson.M{ - "person_id": "6392529400", - "firstname": "Elise", - "lastname": "Smith", - "dateofbirth": time.Date(1972, 1, 13, 9, 32, 7, 0, time.UTC), - "vocation": "ENGINEER", - "address": bson.M{"number": 5625, "street": "Tipa Circle", "city": "Wojzinmoj"}, - }, - bson.M{ - "person_id": "1723338115", - "firstname": "Olive", - "lastname": "Ranieri", - "dateofbirth": time.Date(1985, 5, 12, 23, 14, 30, 0, time.UTC), - "gender": "FEMALE", - "vocation": "ENGINEER", - "address": bson.M{"number": 9303, "street": "Mele Circle", "city": "Tobihbo"}, - }, - bson.M{ - "person_id": "8732762874", - "firstname": "Toni", - "lastname": "Jones", - "dateofbirth": time.Date(1991, 11, 23, 16, 53, 56, 0, time.UTC), - "vocation": "POLITICIAN", - "address": bson.M{"number": 1, "street": "High Street", "city": "Upper Abbeywoodington"}, - }, - bson.M{ - "person_id": "7363629563", - "firstname": "Bert", - "lastname": "Gooding", - "dateofbirth": time.Date(1941, 4, 7, 22, 11, 52, 0, time.UTC), - "vocation": "FLORIST", - "address": bson.M{"number": 13, "street": "Upper Bold Road", "city": "Redringtonville"}, - }, - bson.M{ - "person_id": "1029648329", - "firstname": "Sophie", - "lastname": "Celements", - "dateofbirth": time.Date(1959, 7, 6, 17, 35, 45, 0, time.UTC), - "vocation": "ENGINEER", - "address": bson.M{"number": 5, "street": "Innings Close", "city": "Basilbridge"}, - }, - bson.M{ - "person_id": "7363626383", - "firstname": "Carl", - "lastname": "Simmons", - "dateofbirth": time.Date(1998, 12, 26, 13, 13, 55, 0, time.UTC), - "vocation": "ENGINEER", - "address": bson.M{"number": 187, "street": "Hillside Road", "city": "Kenningford"}, - }, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Find 3 youngest engineers - statement := `db.persons.aggregate([ + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_agg_filtered_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + collection := db.Client.Database(dbName).Collection("persons") + _, err := collection.InsertMany(ctx, []any{ + bson.M{ + "person_id": "6392529400", + "firstname": "Elise", + "lastname": "Smith", + "dateofbirth": time.Date(1972, 1, 13, 9, 32, 7, 0, time.UTC), + "vocation": "ENGINEER", + "address": bson.M{"number": 5625, "street": "Tipa Circle", "city": "Wojzinmoj"}, + }, + bson.M{ + "person_id": "1723338115", + "firstname": "Olive", + "lastname": "Ranieri", + "dateofbirth": time.Date(1985, 5, 12, 23, 14, 30, 0, time.UTC), + "gender": "FEMALE", + "vocation": "ENGINEER", + "address": bson.M{"number": 9303, "street": "Mele Circle", "city": "Tobihbo"}, + }, + bson.M{ + "person_id": "8732762874", + "firstname": "Toni", + "lastname": "Jones", + "dateofbirth": time.Date(1991, 11, 23, 16, 53, 56, 0, time.UTC), + "vocation": "POLITICIAN", + "address": bson.M{"number": 1, "street": "High Street", "city": "Upper Abbeywoodington"}, + }, + bson.M{ + "person_id": "7363629563", + "firstname": "Bert", + "lastname": "Gooding", + "dateofbirth": time.Date(1941, 4, 7, 22, 11, 52, 0, time.UTC), + "vocation": "FLORIST", + "address": bson.M{"number": 13, "street": "Upper Bold Road", "city": "Redringtonville"}, + }, + bson.M{ + "person_id": "1029648329", + "firstname": "Sophie", + "lastname": "Celements", + "dateofbirth": time.Date(1959, 7, 6, 17, 35, 45, 0, time.UTC), + "vocation": "ENGINEER", + "address": bson.M{"number": 5, "street": "Innings Close", "city": "Basilbridge"}, + }, + bson.M{ + "person_id": "7363626383", + "firstname": "Carl", + "lastname": "Simmons", + "dateofbirth": time.Date(1998, 12, 26, 13, 13, 55, 0, time.UTC), + "vocation": "ENGINEER", + "address": bson.M{"number": 187, "street": "Hillside Road", "city": "Kenningford"}, + }, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + // Find 3 youngest engineers + statement := `db.persons.aggregate([ { $match: { vocation: "ENGINEER" } }, { $sort: { dateofbirth: -1 } }, { $limit: 3 }, { $unset: ["_id", "vocation", "address"] } ])` - result, err := gc.Execute(ctx, dbName, statement) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 3, result.RowCount) - - // Carl (1998) should be first (youngest) - require.Contains(t, result.Rows[0], `"Carl"`) - // Olive (1985) should be second - require.Contains(t, result.Rows[1], `"Olive"`) - // Elise (1972) should be third - require.Contains(t, result.Rows[2], `"Elise"`) - - // Verify _id, vocation, and address are excluded - require.NotContains(t, result.Rows[0], `"_id"`) - require.NotContains(t, result.Rows[0], `"vocation"`) - require.NotContains(t, result.Rows[0], `"address"`) + result, err := gc.Execute(ctx, dbName, statement) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 3, result.RowCount) + + // Carl (1998) should be first (youngest) + require.Contains(t, result.Rows[0], `"Carl"`) + // Olive (1985) should be second + require.Contains(t, result.Rows[1], `"Olive"`) + // Elise (1972) should be third + require.Contains(t, result.Rows[2], `"Elise"`) + + // Verify _id, vocation, and address are excluded + require.NotContains(t, result.Rows[0], `"_id"`) + require.NotContains(t, result.Rows[0], `"vocation"`) + require.NotContains(t, result.Rows[0], `"address"`) + }) } // TestAggregateGroupAndTotal tests the "Group and Total" example from MongoDB docs // https://www.mongodb.com/docs/manual/tutorial/aggregation-examples/group-and-total/ func TestAggregateGroupAndTotal(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_group_total" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - collection := client.Database(dbName).Collection("orders") - _, err := collection.InsertMany(ctx, []any{ - bson.M{ - "customer_id": "elise_smith@myemail.com", - "orderdate": time.Date(2020, 5, 30, 8, 35, 52, 0, time.UTC), - "value": 231.43, - }, - bson.M{ - "customer_id": "elise_smith@myemail.com", - "orderdate": time.Date(2020, 1, 13, 9, 32, 7, 0, time.UTC), - "value": 99.99, - }, - bson.M{ - "customer_id": "oranieri@warmmail.com", - "orderdate": time.Date(2020, 1, 1, 8, 25, 37, 0, time.UTC), - "value": 63.13, - }, - bson.M{ - "customer_id": "tj@wheresmyemail.com", - "orderdate": time.Date(2019, 5, 28, 19, 13, 32, 0, time.UTC), - "value": 2.01, - }, - bson.M{ - "customer_id": "tj@wheresmyemail.com", - "orderdate": time.Date(2020, 11, 23, 22, 56, 53, 0, time.UTC), - "value": 187.99, - }, - bson.M{ - "customer_id": "tj@wheresmyemail.com", - "orderdate": time.Date(2020, 8, 18, 23, 4, 48, 0, time.UTC), - "value": 4.59, - }, - bson.M{ - "customer_id": "elise_smith@myemail.com", - "orderdate": time.Date(2020, 12, 26, 8, 55, 46, 0, time.UTC), - "value": 48.50, - }, - bson.M{ - "customer_id": "tj@wheresmyemail.com", - "orderdate": time.Date(2021, 2, 28, 7, 49, 32, 0, time.UTC), - "value": 1024.89, - }, - bson.M{ - "customer_id": "elise_smith@myemail.com", - "orderdate": time.Date(2020, 10, 3, 13, 49, 44, 0, time.UTC), - "value": 102.24, - }, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Group orders by customer for year 2020 - statement := `db.orders.aggregate([ + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_agg_group_total_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + collection := db.Client.Database(dbName).Collection("orders") + _, err := collection.InsertMany(ctx, []any{ + bson.M{ + "customer_id": "elise_smith@myemail.com", + "orderdate": time.Date(2020, 5, 30, 8, 35, 52, 0, time.UTC), + "value": 231.43, + }, + bson.M{ + "customer_id": "elise_smith@myemail.com", + "orderdate": time.Date(2020, 1, 13, 9, 32, 7, 0, time.UTC), + "value": 99.99, + }, + bson.M{ + "customer_id": "oranieri@warmmail.com", + "orderdate": time.Date(2020, 1, 1, 8, 25, 37, 0, time.UTC), + "value": 63.13, + }, + bson.M{ + "customer_id": "tj@wheresmyemail.com", + "orderdate": time.Date(2019, 5, 28, 19, 13, 32, 0, time.UTC), + "value": 2.01, + }, + bson.M{ + "customer_id": "tj@wheresmyemail.com", + "orderdate": time.Date(2020, 11, 23, 22, 56, 53, 0, time.UTC), + "value": 187.99, + }, + bson.M{ + "customer_id": "tj@wheresmyemail.com", + "orderdate": time.Date(2020, 8, 18, 23, 4, 48, 0, time.UTC), + "value": 4.59, + }, + bson.M{ + "customer_id": "elise_smith@myemail.com", + "orderdate": time.Date(2020, 12, 26, 8, 55, 46, 0, time.UTC), + "value": 48.50, + }, + bson.M{ + "customer_id": "tj@wheresmyemail.com", + "orderdate": time.Date(2021, 2, 28, 7, 49, 32, 0, time.UTC), + "value": 1024.89, + }, + bson.M{ + "customer_id": "elise_smith@myemail.com", + "orderdate": time.Date(2020, 10, 3, 13, 49, 44, 0, time.UTC), + "value": 102.24, + }, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + // Group orders by customer for year 2020 + statement := `db.orders.aggregate([ { $match: { orderdate: { $gte: ISODate("2020-01-01T00:00:00Z"), @@ -1068,67 +1094,68 @@ func TestAggregateGroupAndTotal(t *testing.T) { { $unset: ["_id"] } ])` - result, err := gc.Execute(ctx, dbName, statement) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 3, result.RowCount) + result, err := gc.Execute(ctx, dbName, statement) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 3, result.RowCount) - // oranieri should be first (earliest order in 2020: Jan 1) - require.Contains(t, result.Rows[0], `"oranieri@warmmail.com"`) + // oranieri should be first (earliest order in 2020: Jan 1) + require.Contains(t, result.Rows[0], `"oranieri@warmmail.com"`) - // Verify structure - require.Contains(t, result.Rows[0], `"customer_id"`) - require.Contains(t, result.Rows[0], `"total_value"`) - require.Contains(t, result.Rows[0], `"total_orders"`) - require.NotContains(t, result.Rows[0], `"_id"`) + // Verify structure + require.Contains(t, result.Rows[0], `"customer_id"`) + require.Contains(t, result.Rows[0], `"total_value"`) + require.Contains(t, result.Rows[0], `"total_orders"`) + require.NotContains(t, result.Rows[0], `"_id"`) + }) } // TestAggregateUnwindArrays tests the "Unpack Arrays" example from MongoDB docs // https://www.mongodb.com/docs/manual/tutorial/aggregation-examples/unpack-arrays/ func TestAggregateUnwindArrays(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_unwind" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - collection := client.Database(dbName).Collection("orders") - _, err := collection.InsertMany(ctx, []any{ - bson.M{ - "order_id": 6363763262239, - "products": []bson.M{ - {"prod_id": "abc12345", "name": "Asus Laptop", "price": 431.43}, - {"prod_id": "def45678", "name": "Karcher Hose Set", "price": 22.13}, - }, - }, - bson.M{ - "order_id": 1197372932325, - "products": []bson.M{ - {"prod_id": "abc12345", "name": "Asus Laptop", "price": 429.99}, - }, - }, - bson.M{ - "order_id": 9812343774839, - "products": []bson.M{ - {"prod_id": "pqr88223", "name": "Morphy Richards Food Mixer", "price": 431.43}, - {"prod_id": "def45678", "name": "Karcher Hose Set", "price": 21.78}, - }, - }, - bson.M{ - "order_id": 4433997244387, - "products": []bson.M{ - {"prod_id": "def45678", "name": "Karcher Hose Set", "price": 23.43}, - {"prod_id": "jkl77336", "name": "Picky Pencil Sharpener", "price": 0.67}, - {"prod_id": "xyz11228", "name": "Russell Hobbs Chrome Kettle", "price": 15.76}, - }, - }, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Unpack products, filter by price > 15, group by product - statement := `db.orders.aggregate([ + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_agg_unwind_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + collection := db.Client.Database(dbName).Collection("orders") + _, err := collection.InsertMany(ctx, []any{ + bson.M{ + "order_id": 6363763262239, + "products": []bson.M{ + {"prod_id": "abc12345", "name": "Asus Laptop", "price": 431.43}, + {"prod_id": "def45678", "name": "Karcher Hose Set", "price": 22.13}, + }, + }, + bson.M{ + "order_id": 1197372932325, + "products": []bson.M{ + {"prod_id": "abc12345", "name": "Asus Laptop", "price": 429.99}, + }, + }, + bson.M{ + "order_id": 9812343774839, + "products": []bson.M{ + {"prod_id": "pqr88223", "name": "Morphy Richards Food Mixer", "price": 431.43}, + {"prod_id": "def45678", "name": "Karcher Hose Set", "price": 21.78}, + }, + }, + bson.M{ + "order_id": 4433997244387, + "products": []bson.M{ + {"prod_id": "def45678", "name": "Karcher Hose Set", "price": 23.43}, + {"prod_id": "jkl77336", "name": "Picky Pencil Sharpener", "price": 0.67}, + {"prod_id": "xyz11228", "name": "Russell Hobbs Chrome Kettle", "price": 15.76}, + }, + }, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + // Unpack products, filter by price > 15, group by product + statement := `db.orders.aggregate([ { $unwind: { path: "$products" } }, { $match: { "products.price": { $gt: 15 } } }, { $group: { @@ -1141,92 +1168,93 @@ func TestAggregateUnwindArrays(t *testing.T) { { $unset: ["_id"] } ])` - result, err := gc.Execute(ctx, dbName, statement) - require.NoError(t, err) - require.NotNil(t, result) - // Should have: abc12345 (2x), def45678 (3x but all > 15), pqr88223 (1x), xyz11228 (1x) - require.Equal(t, 4, result.RowCount) - - // Verify structure - require.Contains(t, result.Rows[0], `"product_id"`) - require.Contains(t, result.Rows[0], `"product"`) - require.Contains(t, result.Rows[0], `"total_value"`) - require.Contains(t, result.Rows[0], `"quantity"`) + result, err := gc.Execute(ctx, dbName, statement) + require.NoError(t, err) + require.NotNil(t, result) + // Should have: abc12345 (2x), def45678 (3x but all > 15), pqr88223 (1x), xyz11228 (1x) + require.Equal(t, 4, result.RowCount) + + // Verify structure + require.Contains(t, result.Rows[0], `"product_id"`) + require.Contains(t, result.Rows[0], `"product"`) + require.Contains(t, result.Rows[0], `"total_value"`) + require.Contains(t, result.Rows[0], `"quantity"`) + }) } // TestAggregateOneToOneJoin tests the "One-to-One Join" example from MongoDB docs // https://www.mongodb.com/docs/manual/tutorial/aggregation-examples/one-to-one-join/ func TestAggregateOneToOneJoin(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_join_1to1" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create products collection - productsCollection := client.Database(dbName).Collection("products") - _, err := productsCollection.InsertMany(ctx, []any{ - bson.M{ - "id": "a1b2c3d4", - "name": "Asus Laptop", - "category": "ELECTRONICS", - "description": "Good value laptop for students", - }, - bson.M{ - "id": "z9y8x7w6", - "name": "The Day Of The Triffids", - "category": "BOOKS", - "description": "Classic post-apocalyptic novel", - }, - bson.M{ - "id": "ff11gg22hh33", - "name": "Morphy Richards Food Mixer", - "category": "KITCHENWARE", - "description": "Luxury mixer turning good cakes into great", - }, - bson.M{ - "id": "pqr678st", - "name": "Karcher Hose Set", - "category": "GARDEN", - "description": "Hose + nozzles + winder for tidy storage", - }, - }) - require.NoError(t, err) - - // Create orders collection - ordersCollection := client.Database(dbName).Collection("orders") - _, err = ordersCollection.InsertMany(ctx, []any{ - bson.M{ - "customer_id": "elise_smith@myemail.com", - "orderdate": time.Date(2020, 5, 30, 8, 35, 52, 0, time.UTC), - "product_id": "a1b2c3d4", - "value": 431.43, - }, - bson.M{ - "customer_id": "tj@wheresmyemail.com", - "orderdate": time.Date(2019, 5, 28, 19, 13, 32, 0, time.UTC), - "product_id": "z9y8x7w6", - "value": 5.01, - }, - bson.M{ - "customer_id": "oranieri@warmmail.com", - "orderdate": time.Date(2020, 1, 1, 8, 25, 37, 0, time.UTC), - "product_id": "ff11gg22hh33", - "value": 63.13, - }, - bson.M{ - "customer_id": "jjones@tepidmail.com", - "orderdate": time.Date(2020, 12, 26, 8, 55, 46, 0, time.UTC), - "product_id": "a1b2c3d4", - "value": 429.65, - }, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Join orders to products - statement := `db.orders.aggregate([ + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_agg_join_1to1_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create products collection + productsCollection := db.Client.Database(dbName).Collection("products") + _, err := productsCollection.InsertMany(ctx, []any{ + bson.M{ + "id": "a1b2c3d4", + "name": "Asus Laptop", + "category": "ELECTRONICS", + "description": "Good value laptop for students", + }, + bson.M{ + "id": "z9y8x7w6", + "name": "The Day Of The Triffids", + "category": "BOOKS", + "description": "Classic post-apocalyptic novel", + }, + bson.M{ + "id": "ff11gg22hh33", + "name": "Morphy Richards Food Mixer", + "category": "KITCHENWARE", + "description": "Luxury mixer turning good cakes into great", + }, + bson.M{ + "id": "pqr678st", + "name": "Karcher Hose Set", + "category": "GARDEN", + "description": "Hose + nozzles + winder for tidy storage", + }, + }) + require.NoError(t, err) + + // Create orders collection + ordersCollection := db.Client.Database(dbName).Collection("orders") + _, err = ordersCollection.InsertMany(ctx, []any{ + bson.M{ + "customer_id": "elise_smith@myemail.com", + "orderdate": time.Date(2020, 5, 30, 8, 35, 52, 0, time.UTC), + "product_id": "a1b2c3d4", + "value": 431.43, + }, + bson.M{ + "customer_id": "tj@wheresmyemail.com", + "orderdate": time.Date(2019, 5, 28, 19, 13, 32, 0, time.UTC), + "product_id": "z9y8x7w6", + "value": 5.01, + }, + bson.M{ + "customer_id": "oranieri@warmmail.com", + "orderdate": time.Date(2020, 1, 1, 8, 25, 37, 0, time.UTC), + "product_id": "ff11gg22hh33", + "value": 63.13, + }, + bson.M{ + "customer_id": "jjones@tepidmail.com", + "orderdate": time.Date(2020, 12, 26, 8, 55, 46, 0, time.UTC), + "product_id": "a1b2c3d4", + "value": 429.65, + }, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + // Join orders to products + statement := `db.orders.aggregate([ { $match: { orderdate: { $gte: ISODate("2020-01-01T00:00:00Z"), @@ -1247,107 +1275,113 @@ func TestAggregateOneToOneJoin(t *testing.T) { { $unset: ["_id", "product_id", "product_mapping"] } ])` - result, err := gc.Execute(ctx, dbName, statement) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 3, result.RowCount) // Only 2020 orders: elise, oranieri, jjones + result, err := gc.Execute(ctx, dbName, statement) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 3, result.RowCount) // Only 2020 orders: elise, oranieri, jjones - // Verify joined fields exist - require.Contains(t, result.Rows[0], `"product_name"`) - require.Contains(t, result.Rows[0], `"product_category"`) - require.NotContains(t, result.Rows[0], `"_id"`) - require.NotContains(t, result.Rows[0], `"product_mapping"`) + // Verify joined fields exist + require.Contains(t, result.Rows[0], `"product_name"`) + require.Contains(t, result.Rows[0], `"product_category"`) + require.NotContains(t, result.Rows[0], `"_id"`) + require.NotContains(t, result.Rows[0], `"product_mapping"`) + }) } // TestAggregateMultiFieldJoin tests the "Multi-Field Join" example from MongoDB docs // https://www.mongodb.com/docs/manual/tutorial/aggregation-examples/multi-field-join/ func TestAggregateMultiFieldJoin(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_join_multi" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create products collection - productsCollection := client.Database(dbName).Collection("products") - _, err := productsCollection.InsertMany(ctx, []any{ - bson.M{ - "name": "Asus Laptop", - "variation": "Ultra HD", - "category": "ELECTRONICS", - "description": "Great for watching movies", - }, - bson.M{ - "name": "Asus Laptop", - "variation": "Normal Display", - "category": "ELECTRONICS", - "description": "Good value laptop for students", - }, - bson.M{ - "name": "The Day Of The Triffids", - "variation": "1st Edition", - "category": "BOOKS", - "description": "Classic post-apocalyptic novel", - }, - bson.M{ - "name": "The Day Of The Triffids", - "variation": "2nd Edition", - "category": "BOOKS", - "description": "Classic post-apocalyptic novel", - }, - bson.M{ - "name": "Morphy Richards Food Mixer", - "variation": "Deluxe", - "category": "KITCHENWARE", - "description": "Luxury mixer turning good cakes into great", - }, - bson.M{ - "name": "Karcher Hose Set", - "variation": "Full Monty", - "category": "GARDEN", - "description": "Hose + nozzles + winder for tidy storage", - }, - }) - require.NoError(t, err) - - // Create orders collection - ordersCollection := client.Database(dbName).Collection("orders") - _, err = ordersCollection.InsertMany(ctx, []any{ - bson.M{ - "customer_id": "elise_smith@myemail.com", - "orderdate": time.Date(2020, 5, 30, 8, 35, 52, 0, time.UTC), - "product_name": "Asus Laptop", - "product_variation": "Normal Display", - "value": 431.43, - }, - bson.M{ - "customer_id": "tj@wheresmyemail.com", - "orderdate": time.Date(2019, 5, 28, 19, 13, 32, 0, time.UTC), - "product_name": "The Day Of The Triffids", - "product_variation": "2nd Edition", - "value": 5.01, - }, - bson.M{ - "customer_id": "oranieri@warmmail.com", - "orderdate": time.Date(2020, 1, 1, 8, 25, 37, 0, time.UTC), - "product_name": "Morphy Richards Food Mixer", - "product_variation": "Deluxe", - "value": 63.13, - }, - bson.M{ - "customer_id": "jjones@tepidmail.com", - "orderdate": time.Date(2020, 12, 26, 8, 55, 46, 0, time.UTC), - "product_name": "Asus Laptop", - "product_variation": "Normal Display", - "value": 429.65, - }, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Multi-field join using $lookup with let and pipeline - statement := `db.products.aggregate([ + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + // DocumentDB doesn't support $lookup with let/pipeline + if db.Name == "documentdb" { + t.Skip("DocumentDB doesn't support $lookup with let/pipeline") + } + + dbName := fmt.Sprintf("testdb_agg_join_multi_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create products collection + productsCollection := db.Client.Database(dbName).Collection("products") + _, err := productsCollection.InsertMany(ctx, []any{ + bson.M{ + "name": "Asus Laptop", + "variation": "Ultra HD", + "category": "ELECTRONICS", + "description": "Great for watching movies", + }, + bson.M{ + "name": "Asus Laptop", + "variation": "Normal Display", + "category": "ELECTRONICS", + "description": "Good value laptop for students", + }, + bson.M{ + "name": "The Day Of The Triffids", + "variation": "1st Edition", + "category": "BOOKS", + "description": "Classic post-apocalyptic novel", + }, + bson.M{ + "name": "The Day Of The Triffids", + "variation": "2nd Edition", + "category": "BOOKS", + "description": "Classic post-apocalyptic novel", + }, + bson.M{ + "name": "Morphy Richards Food Mixer", + "variation": "Deluxe", + "category": "KITCHENWARE", + "description": "Luxury mixer turning good cakes into great", + }, + bson.M{ + "name": "Karcher Hose Set", + "variation": "Full Monty", + "category": "GARDEN", + "description": "Hose + nozzles + winder for tidy storage", + }, + }) + require.NoError(t, err) + + // Create orders collection + ordersCollection := db.Client.Database(dbName).Collection("orders") + _, err = ordersCollection.InsertMany(ctx, []any{ + bson.M{ + "customer_id": "elise_smith@myemail.com", + "orderdate": time.Date(2020, 5, 30, 8, 35, 52, 0, time.UTC), + "product_name": "Asus Laptop", + "product_variation": "Normal Display", + "value": 431.43, + }, + bson.M{ + "customer_id": "tj@wheresmyemail.com", + "orderdate": time.Date(2019, 5, 28, 19, 13, 32, 0, time.UTC), + "product_name": "The Day Of The Triffids", + "product_variation": "2nd Edition", + "value": 5.01, + }, + bson.M{ + "customer_id": "oranieri@warmmail.com", + "orderdate": time.Date(2020, 1, 1, 8, 25, 37, 0, time.UTC), + "product_name": "Morphy Richards Food Mixer", + "product_variation": "Deluxe", + "value": 63.13, + }, + bson.M{ + "customer_id": "jjones@tepidmail.com", + "orderdate": time.Date(2020, 12, 26, 8, 55, 46, 0, time.UTC), + "product_name": "Asus Laptop", + "product_variation": "Normal Display", + "value": 429.65, + }, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + // Multi-field join using $lookup with let and pipeline + statement := `db.products.aggregate([ { $lookup: { from: "orders", let: { prdname: "$name", prdvartn: "$variation" }, @@ -1374,911 +1408,1129 @@ func TestAggregateMultiFieldJoin(t *testing.T) { { $unset: ["_id"] } ])` - result, err := gc.Execute(ctx, dbName, statement) - require.NoError(t, err) - require.NotNil(t, result) - // Should have: Asus Laptop Normal Display (2 orders), Morphy Richards (1 order) - require.Equal(t, 2, result.RowCount) - - // Verify structure - require.Contains(t, result.Rows[0], `"orders"`) - require.Contains(t, result.Rows[0], `"name"`) - require.Contains(t, result.Rows[0], `"variation"`) - require.NotContains(t, result.Rows[0], `"_id"`) + result, err := gc.Execute(ctx, dbName, statement) + require.NoError(t, err) + require.NotNil(t, result) + // Should have: Asus Laptop Normal Display (2 orders), Morphy Richards (1 order) + require.Equal(t, 2, result.RowCount) + + // Verify structure + require.Contains(t, result.Rows[0], `"orders"`) + require.Contains(t, result.Rows[0], `"name"`) + require.Contains(t, result.Rows[0], `"variation"`) + require.NotContains(t, result.Rows[0], `"_id"`) + }) } func TestAggregateWithOptions(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_options" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_agg_options_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "age": 30}, - bson.M{"name": "Bob", "age": 25}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "age": 30}, + bson.M{"name": "Bob", "age": 25}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // aggregate with maxTimeMS option - result, err := gc.Execute(ctx, dbName, `db.users.aggregate([{ $match: { age: { $gt: 20 } } }], { maxTimeMS: 5000 })`) - require.NoError(t, err) - require.Equal(t, 2, result.RowCount) + // aggregate with maxTimeMS option + result, err := gc.Execute(ctx, dbName, `db.users.aggregate([{ $match: { age: { $gt: 20 } } }], { maxTimeMS: 5000 })`) + require.NoError(t, err) + require.Equal(t, 2, result.RowCount) + }) } func TestAggregateWithHintOption(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_hint" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_agg_hint_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "age": 30}, - bson.M{"name": "Bob", "age": 25}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "age": 30}, + bson.M{"name": "Bob", "age": 25}, + }) + require.NoError(t, err) - // Create index on age field - _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "age", Value: 1}}, - }) - require.NoError(t, err) + // Create index on age field + _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "age", Value: 1}}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // aggregate with hint option (index name) - result, err := gc.Execute(ctx, dbName, `db.users.aggregate([{ $match: { age: { $gt: 20 } } }], { hint: "age_1" })`) - require.NoError(t, err) - require.Equal(t, 2, result.RowCount) + // aggregate with hint option (index name) + result, err := gc.Execute(ctx, dbName, `db.users.aggregate([{ $match: { age: { $gt: 20 } } }], { hint: "age_1" })`) + require.NoError(t, err) + require.Equal(t, 2, result.RowCount) - // aggregate with hint option (index spec) - result, err = gc.Execute(ctx, dbName, `db.users.aggregate([{ $match: { age: { $gt: 20 } } }], { hint: { age: 1 } })`) - require.NoError(t, err) - require.Equal(t, 2, result.RowCount) + // aggregate with hint option (index spec) + result, err = gc.Execute(ctx, dbName, `db.users.aggregate([{ $match: { age: { $gt: 20 } } }], { hint: { age: 1 } })`) + require.NoError(t, err) + require.Equal(t, 2, result.RowCount) + }) } func TestAggregateTooManyArguments(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_too_many_args" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_agg_too_many_args_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.aggregate([], {}, "extra")`) - require.Error(t, err) - require.Contains(t, err.Error(), "aggregate() takes at most 2 arguments") + _, err := gc.Execute(ctx, dbName, `db.users.aggregate([], {}, "extra")`) + require.Error(t, err) + require.Contains(t, err.Error(), "aggregate() takes at most 2 arguments") + }) } func TestGetIndexes(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_get_indexes" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create a collection with a document (this creates the default _id index) - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertOne(ctx, bson.M{"name": "alice", "email": "alice@example.com"}) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Test getIndexes - should return at least the _id index - result, err := gc.Execute(ctx, dbName, "db.users.getIndexes()") - require.NoError(t, err) - require.NotNil(t, result) - require.GreaterOrEqual(t, result.RowCount, 1) - - // Verify the _id index exists - found := false - for _, row := range result.Rows { - if strings.Contains(row, `"name": "_id_"`) { - found = true - break + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_get_indexes_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create a collection with a document (this creates the default _id index) + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertOne(ctx, bson.M{"name": "alice", "email": "alice@example.com"}) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + // Test getIndexes - should return at least the _id index + result, err := gc.Execute(ctx, dbName, "db.users.getIndexes()") + require.NoError(t, err) + require.NotNil(t, result) + require.GreaterOrEqual(t, result.RowCount, 1) + + // Verify the _id index exists + found := false + for _, row := range result.Rows { + if strings.Contains(row, `"name": "_id_"`) { + found = true + break + } } - } - require.True(t, found, "expected _id_ index") + require.True(t, found, "expected _id_ index") + }) } func TestGetIndexesWithCustomIndex(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_indexes_custom" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_indexes_custom_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Create a collection and add a custom index - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertOne(ctx, bson.M{"name": "alice", "email": "alice@example.com"}) - require.NoError(t, err) + // Create a collection and add a custom index + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertOne(ctx, bson.M{"name": "alice", "email": "alice@example.com"}) + require.NoError(t, err) - // Create an index on the email field - _, err = collection.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "email", Value: 1}}, - }) - require.NoError(t, err) + // Create an index on the email field + _, err = collection.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "email", Value: 1}}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, "db.users.getIndexes()") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 2, result.RowCount) // _id index + email index + result, err := gc.Execute(ctx, dbName, "db.users.getIndexes()") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 2, result.RowCount) // _id index + email index - // Verify both indexes exist - hasIdIndex := false - hasEmailIndex := false - for _, row := range result.Rows { - if strings.Contains(row, `"name": "_id_"`) { - hasIdIndex = true - } - if strings.Contains(row, `"email"`) { - hasEmailIndex = true + // Verify both indexes exist + hasIdIndex := false + hasEmailIndex := false + for _, row := range result.Rows { + if strings.Contains(row, `"name": "_id_"`) { + hasIdIndex = true + } + if strings.Contains(row, `"email"`) { + hasEmailIndex = true + } } - } - require.True(t, hasIdIndex, "expected _id_ index") - require.True(t, hasEmailIndex, "expected email index") + require.True(t, hasIdIndex, "expected _id_ index") + require.True(t, hasEmailIndex, "expected email index") + }) } func TestGetIndexesBracketNotation(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_indexes_bracket" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_indexes_bracket_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Create a collection with hyphenated name - collection := client.Database(dbName).Collection("user-logs") - _, err := collection.InsertOne(ctx, bson.M{"message": "test"}) - require.NoError(t, err) + // Create a collection with hyphenated name + collection := db.Client.Database(dbName).Collection("user-logs") + _, err := collection.InsertOne(ctx, bson.M{"message": "test"}) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Test with bracket notation - result, err := gc.Execute(ctx, dbName, `db["user-logs"].getIndexes()`) - require.NoError(t, err) - require.NotNil(t, result) - require.GreaterOrEqual(t, result.RowCount, 1) + // Test with bracket notation + result, err := gc.Execute(ctx, dbName, `db["user-logs"].getIndexes()`) + require.NoError(t, err) + require.NotNil(t, result) + require.GreaterOrEqual(t, result.RowCount, 1) - // Verify the _id index exists - require.Contains(t, result.Rows[0], `"name": "_id_"`) + // Verify the _id index exists + require.Contains(t, result.Rows[0], `"name": "_id_"`) + }) } func TestCountDocuments(t *testing.T) { - dbName := "testdb_count" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_count_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create a collection with documents + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "alice", "age": 30}, + bson.M{"name": "bob", "age": 25}, + bson.M{"name": "charlie", "age": 35}, + }) + require.NoError(t, err) - ctx := context.Background() + gc := gomongo.NewClient(db.Client) - // Create a collection with documents - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "alice", "age": 30}, - bson.M{"name": "bob", "age": 25}, - bson.M{"name": "charlie", "age": 35}, + // Test countDocuments without filter + result, err := gc.Execute(ctx, dbName, "db.users.countDocuments()") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 1, result.RowCount) + require.Equal(t, "3", result.Rows[0]) }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Test countDocuments without filter - result, err := gc.Execute(ctx, dbName, "db.users.countDocuments()") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - require.Equal(t, "3", result.Rows[0]) } func TestCountDocumentsWithFilter(t *testing.T) { - dbName := "testdb_count_filter" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create a collection with documents - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "alice", "age": 30, "status": "active"}, - bson.M{"name": "bob", "age": 25, "status": "inactive"}, - bson.M{"name": "charlie", "age": 35, "status": "active"}, - bson.M{"name": "diana", "age": 28, "status": "active"}, - }) - require.NoError(t, err) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_count_filter_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create a collection with documents + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "alice", "age": 30, "status": "active"}, + bson.M{"name": "bob", "age": 25, "status": "inactive"}, + bson.M{"name": "charlie", "age": 35, "status": "active"}, + bson.M{"name": "diana", "age": 28, "status": "active"}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Test countDocuments with filter - result, err := gc.Execute(ctx, dbName, `db.users.countDocuments({ status: "active" })`) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - require.Equal(t, "3", result.Rows[0]) + // Test countDocuments with filter + result, err := gc.Execute(ctx, dbName, `db.users.countDocuments({ status: "active" })`) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 1, result.RowCount) + require.Equal(t, "3", result.Rows[0]) - // Test with comparison operator - result, err = gc.Execute(ctx, dbName, `db.users.countDocuments({ age: { $gte: 30 } })`) - require.NoError(t, err) - require.Equal(t, "2", result.Rows[0]) + // Test with comparison operator + result, err = gc.Execute(ctx, dbName, `db.users.countDocuments({ age: { $gte: 30 } })`) + require.NoError(t, err) + require.Equal(t, "2", result.Rows[0]) + }) } func TestCountDocumentsEmptyCollection(t *testing.T) { - dbName := "testdb_count_empty" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_count_empty_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Test countDocuments on empty/non-existent collection - result, err := gc.Execute(ctx, dbName, "db.users.countDocuments()") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - require.Equal(t, "0", result.Rows[0]) + // Test countDocuments on empty/non-existent collection + result, err := gc.Execute(ctx, dbName, "db.users.countDocuments()") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 1, result.RowCount) + require.Equal(t, "0", result.Rows[0]) + }) } func TestCountDocumentsWithEmptyFilter(t *testing.T) { - dbName := "testdb_count_empty_filter" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_count_empty_filter_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Create a collection with documents - collection := client.Database(dbName).Collection("items") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"item": "a"}, - bson.M{"item": "b"}, - }) - require.NoError(t, err) + // Create a collection with documents + collection := db.Client.Database(dbName).Collection("items") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"item": "a"}, + bson.M{"item": "b"}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Test countDocuments with empty filter {} - result, err := gc.Execute(ctx, dbName, "db.items.countDocuments({})") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, "2", result.Rows[0]) + // Test countDocuments with empty filter {} + result, err := gc.Execute(ctx, dbName, "db.items.countDocuments({})") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, "2", result.Rows[0]) + }) } func TestCountDocumentsWithOptions(t *testing.T) { - dbName := "testdb_count_options" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create a collection with documents - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "alice", "age": 30}, - bson.M{"name": "bob", "age": 25}, - bson.M{"name": "charlie", "age": 35}, - bson.M{"name": "diana", "age": 28}, - bson.M{"name": "eve", "age": 32}, - }) - require.NoError(t, err) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_count_options_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create a collection with documents + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "alice", "age": 30}, + bson.M{"name": "bob", "age": 25}, + bson.M{"name": "charlie", "age": 35}, + bson.M{"name": "diana", "age": 28}, + bson.M{"name": "eve", "age": 32}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Test with limit option - result, err := gc.Execute(ctx, dbName, `db.users.countDocuments({}, { limit: 3 })`) - require.NoError(t, err) - require.Equal(t, "3", result.Rows[0]) + // Test with limit option + result, err := gc.Execute(ctx, dbName, `db.users.countDocuments({}, { limit: 3 })`) + require.NoError(t, err) + require.Equal(t, "3", result.Rows[0]) - // Test with skip option - result, err = gc.Execute(ctx, dbName, `db.users.countDocuments({}, { skip: 2 })`) - require.NoError(t, err) - require.Equal(t, "3", result.Rows[0]) + // Test with skip option + result, err = gc.Execute(ctx, dbName, `db.users.countDocuments({}, { skip: 2 })`) + require.NoError(t, err) + require.Equal(t, "3", result.Rows[0]) - // Test with both limit and skip - result, err = gc.Execute(ctx, dbName, `db.users.countDocuments({}, { skip: 1, limit: 2 })`) - require.NoError(t, err) - require.Equal(t, "2", result.Rows[0]) + // Test with both limit and skip + result, err = gc.Execute(ctx, dbName, `db.users.countDocuments({}, { skip: 1, limit: 2 })`) + require.NoError(t, err) + require.Equal(t, "2", result.Rows[0]) + }) } func TestCountDocumentsWithHint(t *testing.T) { - dbName := "testdb_count_hint" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create a collection with documents and an index - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "alice", "status": "active"}, - bson.M{"name": "bob", "status": "inactive"}, - bson.M{"name": "charlie", "status": "active"}, - }) - require.NoError(t, err) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_count_hint_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create a collection with documents and an index + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "alice", "status": "active"}, + bson.M{"name": "bob", "status": "inactive"}, + bson.M{"name": "charlie", "status": "active"}, + }) + require.NoError(t, err) - // Create an index on status - _, err = collection.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "status", Value: 1}}, - }) - require.NoError(t, err) + // Create an index on status + _, err = collection.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "status", Value: 1}}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Test with hint using index name - result, err := gc.Execute(ctx, dbName, `db.users.countDocuments({ status: "active" }, { hint: "status_1" })`) - require.NoError(t, err) - require.Equal(t, "2", result.Rows[0]) + // Test with hint using index name + result, err := gc.Execute(ctx, dbName, `db.users.countDocuments({ status: "active" }, { hint: "status_1" })`) + require.NoError(t, err) + require.Equal(t, "2", result.Rows[0]) - // Test with hint using index specification document - result, err = gc.Execute(ctx, dbName, `db.users.countDocuments({ status: "active" }, { hint: { status: 1 } })`) - require.NoError(t, err) - require.Equal(t, "2", result.Rows[0]) + // Test with hint using index specification document + result, err = gc.Execute(ctx, dbName, `db.users.countDocuments({ status: "active" }, { hint: { status: 1 } })`) + require.NoError(t, err) + require.Equal(t, "2", result.Rows[0]) + }) } func TestCountDocumentsMaxTimeMS(t *testing.T) { - dbName := "testdb_count_maxtime" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_count_maxtime_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice"}, - bson.M{"name": "Bob"}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice"}, + bson.M{"name": "Bob"}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.users.countDocuments({}, { maxTimeMS: 5000 })`) - require.NoError(t, err) - require.Equal(t, "2", result.Rows[0]) + result, err := gc.Execute(ctx, dbName, `db.users.countDocuments({}, { maxTimeMS: 5000 })`) + require.NoError(t, err) + require.Equal(t, "2", result.Rows[0]) + }) } func TestEstimatedDocumentCount(t *testing.T) { - dbName := "testdb_est_count" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_est_count_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create a collection with documents + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "alice"}, + bson.M{"name": "bob"}, + bson.M{"name": "charlie"}, + }) + require.NoError(t, err) - ctx := context.Background() + gc := gomongo.NewClient(db.Client) - // Create a collection with documents - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "alice"}, - bson.M{"name": "bob"}, - bson.M{"name": "charlie"}, + // Test estimatedDocumentCount + result, err := gc.Execute(ctx, dbName, "db.users.estimatedDocumentCount()") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 1, result.RowCount) + require.Equal(t, "3", result.Rows[0]) }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Test estimatedDocumentCount - result, err := gc.Execute(ctx, dbName, "db.users.estimatedDocumentCount()") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - require.Equal(t, "3", result.Rows[0]) } func TestEstimatedDocumentCountEmptyCollection(t *testing.T) { - dbName := "testdb_est_count_empty" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_est_count_empty_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Test estimatedDocumentCount on empty/non-existent collection - result, err := gc.Execute(ctx, dbName, "db.users.estimatedDocumentCount()") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - require.Equal(t, "0", result.Rows[0]) + // Test estimatedDocumentCount on empty/non-existent collection + result, err := gc.Execute(ctx, dbName, "db.users.estimatedDocumentCount()") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 1, result.RowCount) + require.Equal(t, "0", result.Rows[0]) + }) } func TestEstimatedDocumentCountWithEmptyOptions(t *testing.T) { - dbName := "testdb_est_count_opts" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_est_count_opts_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Create a collection with documents - collection := client.Database(dbName).Collection("items") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"item": "a"}, - bson.M{"item": "b"}, - }) - require.NoError(t, err) + // Create a collection with documents + collection := db.Client.Database(dbName).Collection("items") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"item": "a"}, + bson.M{"item": "b"}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Test estimatedDocumentCount with empty options {} - result, err := gc.Execute(ctx, dbName, "db.items.estimatedDocumentCount({})") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, "2", result.Rows[0]) + // Test estimatedDocumentCount with empty options {} + result, err := gc.Execute(ctx, dbName, "db.items.estimatedDocumentCount({})") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, "2", result.Rows[0]) + }) } func TestEstimatedDocumentCountMaxTimeMS(t *testing.T) { - dbName := "testdb_est_count_maxtime" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_est_count_maxtime_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice"}, - bson.M{"name": "Bob"}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice"}, + bson.M{"name": "Bob"}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.users.estimatedDocumentCount({ maxTimeMS: 5000 })`) - require.NoError(t, err) - require.Equal(t, 1, result.RowCount) - require.Equal(t, "2", result.Rows[0]) + result, err := gc.Execute(ctx, dbName, `db.users.estimatedDocumentCount({ maxTimeMS: 5000 })`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Equal(t, "2", result.Rows[0]) + }) } func TestDistinct(t *testing.T) { - dbName := "testdb_distinct" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_distinct_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create a collection with documents + collection := db.Client.Database(dbName).Collection("users") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "alice", "status": "active"}, + bson.M{"name": "bob", "status": "inactive"}, + bson.M{"name": "charlie", "status": "active"}, + bson.M{"name": "diana", "status": "active"}, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) - ctx := context.Background() + // Test distinct on status field + result, err := gc.Execute(ctx, dbName, `db.users.distinct("status")`) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 2, result.RowCount) - // Create a collection with documents - collection := client.Database(dbName).Collection("users") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "alice", "status": "active"}, - bson.M{"name": "bob", "status": "inactive"}, - bson.M{"name": "charlie", "status": "active"}, - bson.M{"name": "diana", "status": "active"}, + // Verify both values are present + values := make(map[string]bool) + for _, row := range result.Rows { + values[row] = true + } + require.True(t, values[`"active"`] || values[`"inactive"`]) }) - require.NoError(t, err) +} - gc := gomongo.NewClient(client) +func TestDistinctWithFilter(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_distinct_filter_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create a collection with documents + collection := db.Client.Database(dbName).Collection("products") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"category": "electronics", "brand": "Apple", "price": 999}, + bson.M{"category": "electronics", "brand": "Samsung", "price": 799}, + bson.M{"category": "electronics", "brand": "Apple", "price": 1299}, + bson.M{"category": "clothing", "brand": "Nike", "price": 99}, + bson.M{"category": "clothing", "brand": "Adidas", "price": 89}, + }) + require.NoError(t, err) - // Test distinct on status field - result, err := gc.Execute(ctx, dbName, `db.users.distinct("status")`) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 2, result.RowCount) + gc := gomongo.NewClient(db.Client) - // Verify both values are present - values := make(map[string]bool) - for _, row := range result.Rows { - values[row] = true - } - require.True(t, values[`"active"`] || values[`"inactive"`]) -} + // Test distinct with filter + result, err := gc.Execute(ctx, dbName, `db.products.distinct("brand", { category: "electronics" })`) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 2, result.RowCount) -func TestDistinctWithFilter(t *testing.T) { - dbName := "testdb_distinct_filter" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create a collection with documents - collection := client.Database(dbName).Collection("products") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"category": "electronics", "brand": "Apple", "price": 999}, - bson.M{"category": "electronics", "brand": "Samsung", "price": 799}, - bson.M{"category": "electronics", "brand": "Apple", "price": 1299}, - bson.M{"category": "clothing", "brand": "Nike", "price": 99}, - bson.M{"category": "clothing", "brand": "Adidas", "price": 89}, - }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Test distinct with filter - result, err := gc.Execute(ctx, dbName, `db.products.distinct("brand", { category: "electronics" })`) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 2, result.RowCount) - - // Verify only electronics brands are returned - values := make(map[string]bool) - for _, row := range result.Rows { - values[row] = true - } - require.True(t, values[`"Apple"`]) - require.True(t, values[`"Samsung"`]) - require.False(t, values[`"Nike"`]) - require.False(t, values[`"Adidas"`]) + // Verify only electronics brands are returned + values := make(map[string]bool) + for _, row := range result.Rows { + values[row] = true + } + require.True(t, values[`"Apple"`]) + require.True(t, values[`"Samsung"`]) + require.False(t, values[`"Nike"`]) + require.False(t, values[`"Adidas"`]) + }) } func TestDistinctEmptyCollection(t *testing.T) { - dbName := "testdb_distinct_empty" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_distinct_empty_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Test distinct on empty/non-existent collection - result, err := gc.Execute(ctx, dbName, `db.users.distinct("status")`) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 0, result.RowCount) - require.Empty(t, result.Rows) + // Test distinct on empty/non-existent collection + result, err := gc.Execute(ctx, dbName, `db.users.distinct("status")`) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 0, result.RowCount) + require.Empty(t, result.Rows) + }) } func TestDistinctBracketNotation(t *testing.T) { - dbName := "testdb_distinct_bracket" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_distinct_bracket_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create a collection with hyphenated name + collection := db.Client.Database(dbName).Collection("user-logs") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"level": "info"}, + bson.M{"level": "warn"}, + bson.M{"level": "error"}, + bson.M{"level": "info"}, + }) + require.NoError(t, err) - ctx := context.Background() + gc := gomongo.NewClient(db.Client) - // Create a collection with hyphenated name - collection := client.Database(dbName).Collection("user-logs") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"level": "info"}, - bson.M{"level": "warn"}, - bson.M{"level": "error"}, - bson.M{"level": "info"}, + // Test with bracket notation + result, err := gc.Execute(ctx, dbName, `db["user-logs"].distinct("level")`) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 3, result.RowCount) }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Test with bracket notation - result, err := gc.Execute(ctx, dbName, `db["user-logs"].distinct("level")`) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 3, result.RowCount) } func TestDistinctNumericValues(t *testing.T) { - dbName := "testdb_distinct_numeric" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_distinct_numeric_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create a collection with numeric values + collection := db.Client.Database(dbName).Collection("scores") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"score": 100}, + bson.M{"score": 85}, + bson.M{"score": 100}, + bson.M{"score": 90}, + bson.M{"score": 85}, + }) + require.NoError(t, err) - ctx := context.Background() + gc := gomongo.NewClient(db.Client) - // Create a collection with numeric values - collection := client.Database(dbName).Collection("scores") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"score": 100}, - bson.M{"score": 85}, - bson.M{"score": 100}, - bson.M{"score": 90}, - bson.M{"score": 85}, + // Test distinct on numeric field + result, err := gc.Execute(ctx, dbName, `db.scores.distinct("score")`) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 3, result.RowCount) // 100, 85, 90 }) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Test distinct on numeric field - result, err := gc.Execute(ctx, dbName, `db.scores.distinct("score")`) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 3, result.RowCount) // 100, 85, 90 } func TestDistinctMaxTimeMS(t *testing.T) { - dbName := "testdb_distinct_maxtime" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_distinct_maxtime_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "city": "NYC"}, - bson.M{"name": "Bob", "city": "LA"}, - bson.M{"name": "Charlie", "city": "NYC"}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "city": "NYC"}, + bson.M{"name": "Bob", "city": "LA"}, + bson.M{"name": "Charlie", "city": "NYC"}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.users.distinct("city", {}, { maxTimeMS: 5000 })`) - require.NoError(t, err) - require.Equal(t, 2, result.RowCount) + result, err := gc.Execute(ctx, dbName, `db.users.distinct("city", {}, { maxTimeMS: 5000 })`) + require.NoError(t, err) + require.Equal(t, 2, result.RowCount) + }) } func TestCursorCountUnsupported(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_cursor_count" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_cursor_count_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // cursor.count() is not in the planned registry, should return UnsupportedOperationError - _, err := gc.Execute(ctx, dbName, "db.users.find().count()") - require.Error(t, err) + // cursor.count() is not in the planned registry, should return UnsupportedOperationError + _, err := gc.Execute(ctx, dbName, "db.users.find().count()") + require.Error(t, err) - var unsupportedErr *gomongo.UnsupportedOperationError - require.ErrorAs(t, err, &unsupportedErr) - require.Equal(t, "count()", unsupportedErr.Operation) + var unsupportedErr *gomongo.UnsupportedOperationError + require.ErrorAs(t, err, &unsupportedErr) + require.Equal(t, "count()", unsupportedErr.Operation) + }) } func TestCursorHintMethod(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_cursor_hint" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_cursor_hint_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "age": 30}, - bson.M{"name": "Bob", "age": 25}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "age": 30}, + bson.M{"name": "Bob", "age": 25}, + }) + require.NoError(t, err) - // Create index - _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "name", Value: 1}}, - }) - require.NoError(t, err) + // Create index + _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "name", Value: 1}}, + }) + require.NoError(t, err) - // Use hint() cursor method with string - result, err := gc.Execute(ctx, dbName, `db.users.find({}).hint("name_1")`) - require.NoError(t, err) - require.Equal(t, 2, result.RowCount) + // Use hint() cursor method with string + result, err := gc.Execute(ctx, dbName, `db.users.find({}).hint("name_1")`) + require.NoError(t, err) + require.Equal(t, 2, result.RowCount) + }) } func TestCursorHintMethodWithDocument(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_cursor_hint_doc" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_cursor_hint_doc_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "age": 30}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "age": 30}, + }) + require.NoError(t, err) - _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "name", Value: 1}}, - }) - require.NoError(t, err) + _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "name", Value: 1}}, + }) + require.NoError(t, err) - // Use hint() cursor method with document - result, err := gc.Execute(ctx, dbName, `db.users.find({}).hint({ name: 1 })`) - require.NoError(t, err) - require.Equal(t, 1, result.RowCount) + // Use hint() cursor method with document + result, err := gc.Execute(ctx, dbName, `db.users.find({}).hint({ name: 1 })`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + }) } func TestCursorMaxMethod(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_cursor_max" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + // DocumentDB doesn't support min/max cursor methods + if db.Name == "documentdb" { + t.Skip("DocumentDB doesn't support min/max cursor methods") + } - ctx := context.Background() + dbName := fmt.Sprintf("testdb_cursor_max_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - gc := gomongo.NewClient(client) + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "age": 30}, - bson.M{"name": "Bob", "age": 25}, - bson.M{"name": "Carol", "age": 35}, - }) - require.NoError(t, err) + gc := gomongo.NewClient(db.Client) - // Create index on age for max() to work - _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "age", Value: 1}}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "age": 30}, + bson.M{"name": "Bob", "age": 25}, + bson.M{"name": "Carol", "age": 35}, + }) + require.NoError(t, err) + + // Create index on age for max() to work + _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "age", Value: 1}}, + }) + require.NoError(t, err) - // Use max() cursor method - returns documents with age < 30 - result, err := gc.Execute(ctx, dbName, `db.users.find({}).hint({ age: 1 }).max({ age: 30 })`) - require.NoError(t, err) - require.Equal(t, 1, result.RowCount) - require.Contains(t, result.Rows[0], `"Bob"`) + // Use max() cursor method - returns documents with age < 30 + result, err := gc.Execute(ctx, dbName, `db.users.find({}).hint({ age: 1 }).max({ age: 30 })`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], `"Bob"`) + }) } func TestCursorMinMethod(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_cursor_min" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + // DocumentDB doesn't support min/max cursor methods + if db.Name == "documentdb" { + t.Skip("DocumentDB doesn't support min/max cursor methods") + } - ctx := context.Background() + dbName := fmt.Sprintf("testdb_cursor_min_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - gc := gomongo.NewClient(client) + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "age": 30}, - bson.M{"name": "Bob", "age": 25}, - bson.M{"name": "Carol", "age": 35}, - }) - require.NoError(t, err) + gc := gomongo.NewClient(db.Client) - // Create index on age for min() to work - _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "age", Value: 1}}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "age": 30}, + bson.M{"name": "Bob", "age": 25}, + bson.M{"name": "Carol", "age": 35}, + }) + require.NoError(t, err) + + // Create index on age for min() to work + _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "age", Value: 1}}, + }) + require.NoError(t, err) - // Use min() cursor method - returns documents with age >= 30 - result, err := gc.Execute(ctx, dbName, `db.users.find({}).hint({ age: 1 }).min({ age: 30 })`) - require.NoError(t, err) - require.Equal(t, 2, result.RowCount) + // Use min() cursor method - returns documents with age >= 30 + result, err := gc.Execute(ctx, dbName, `db.users.find({}).hint({ age: 1 }).min({ age: 30 })`) + require.NoError(t, err) + require.Equal(t, 2, result.RowCount) + }) } func TestCursorMinMaxCombined(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_cursor_minmax" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + // DocumentDB doesn't support min/max cursor methods + if db.Name == "documentdb" { + t.Skip("DocumentDB doesn't support min/max cursor methods") + } - ctx := context.Background() + dbName := fmt.Sprintf("testdb_cursor_minmax_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - gc := gomongo.NewClient(client) + ctx := context.Background() - coll := client.Database(dbName).Collection("users") - _, err := coll.InsertMany(ctx, []any{ - bson.M{"name": "Alice", "age": 30}, - bson.M{"name": "Bob", "age": 25}, - bson.M{"name": "Carol", "age": 35}, - bson.M{"name": "Dave", "age": 40}, - }) - require.NoError(t, err) + gc := gomongo.NewClient(db.Client) - // Create index on age - _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "age", Value: 1}}, - }) - require.NoError(t, err) + coll := db.Client.Database(dbName).Collection("users") + _, err := coll.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "age": 30}, + bson.M{"name": "Bob", "age": 25}, + bson.M{"name": "Carol", "age": 35}, + bson.M{"name": "Dave", "age": 40}, + }) + require.NoError(t, err) - // Use min() and max() together - returns documents with 30 <= age < 40 - result, err := gc.Execute(ctx, dbName, `db.users.find({}).hint({ age: 1 }).min({ age: 30 }).max({ age: 40 })`) - require.NoError(t, err) - require.Equal(t, 2, result.RowCount) + // Create index on age + _, err = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "age", Value: 1}}, + }) + require.NoError(t, err) + + // Use min() and max() together - returns documents with 30 <= age < 40 + result, err := gc.Execute(ctx, dbName, `db.users.find({}).hint({ age: 1 }).min({ age: 30 }).max({ age: 40 })`) + require.NoError(t, err) + require.Equal(t, 2, result.RowCount) + }) } func TestWithMaxRowsCapsResults(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_maxrows_cap" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_maxrows_cap_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Insert 20 documents - collection := client.Database(dbName).Collection("items") - docs := make([]any, 20) - for i := 0; i < 20; i++ { - docs[i] = bson.M{"index": i} - } - _, err := collection.InsertMany(ctx, docs) - require.NoError(t, err) + // Insert 20 documents + collection := db.Client.Database(dbName).Collection("items") + docs := make([]any, 20) + for i := range 20 { + docs[i] = bson.M{"index": i} + } + _, err := collection.InsertMany(ctx, docs) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Without MaxRows - returns all 20 - result, err := gc.Execute(ctx, dbName, "db.items.find()") - require.NoError(t, err) - require.Equal(t, 20, result.RowCount) + // Without MaxRows - returns all 20 + result, err := gc.Execute(ctx, dbName, "db.items.find()") + require.NoError(t, err) + require.Equal(t, 20, result.RowCount) - // With MaxRows(10) - caps at 10 - result, err = gc.Execute(ctx, dbName, "db.items.find()", gomongo.WithMaxRows(10)) - require.NoError(t, err) - require.Equal(t, 10, result.RowCount) + // With MaxRows(10) - caps at 10 + result, err = gc.Execute(ctx, dbName, "db.items.find()", gomongo.WithMaxRows(10)) + require.NoError(t, err) + require.Equal(t, 10, result.RowCount) + }) } func TestWithMaxRowsQueryLimitTakesPrecedence(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_maxrows_query_limit" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_maxrows_query_limit_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Insert 20 documents - collection := client.Database(dbName).Collection("items") - docs := make([]any, 20) - for i := 0; i < 20; i++ { - docs[i] = bson.M{"index": i} - } - _, err := collection.InsertMany(ctx, docs) - require.NoError(t, err) + // Insert 20 documents + collection := db.Client.Database(dbName).Collection("items") + docs := make([]any, 20) + for i := range 20 { + docs[i] = bson.M{"index": i} + } + _, err := collection.InsertMany(ctx, docs) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Query limit(5) is smaller than MaxRows(100) - should return 5 - result, err := gc.Execute(ctx, dbName, "db.items.find().limit(5)", gomongo.WithMaxRows(100)) - require.NoError(t, err) - require.Equal(t, 5, result.RowCount) + // Query limit(5) is smaller than MaxRows(100) - should return 5 + result, err := gc.Execute(ctx, dbName, "db.items.find().limit(5)", gomongo.WithMaxRows(100)) + require.NoError(t, err) + require.Equal(t, 5, result.RowCount) + }) } func TestWithMaxRowsTakesPrecedenceOverLargerLimit(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_maxrows_precedence" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_maxrows_precedence_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Insert 20 documents - collection := client.Database(dbName).Collection("items") - docs := make([]any, 20) - for i := 0; i < 20; i++ { - docs[i] = bson.M{"index": i} - } - _, err := collection.InsertMany(ctx, docs) - require.NoError(t, err) + // Insert 20 documents + collection := db.Client.Database(dbName).Collection("items") + docs := make([]any, 20) + for i := range 20 { + docs[i] = bson.M{"index": i} + } + _, err := collection.InsertMany(ctx, docs) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Query limit(100) is larger than MaxRows(5) - should return 5 - result, err := gc.Execute(ctx, dbName, "db.items.find().limit(100)", gomongo.WithMaxRows(5)) - require.NoError(t, err) - require.Equal(t, 5, result.RowCount) + // Query limit(100) is larger than MaxRows(5) - should return 5 + result, err := gc.Execute(ctx, dbName, "db.items.find().limit(100)", gomongo.WithMaxRows(5)) + require.NoError(t, err) + require.Equal(t, 5, result.RowCount) + }) } func TestExecuteBackwardCompatibility(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_backward_compat" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_backward_compat_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - collection := client.Database(dbName).Collection("items") - _, err := collection.InsertMany(ctx, []any{ - bson.M{"name": "a"}, - bson.M{"name": "b"}, - bson.M{"name": "c"}, - }) - require.NoError(t, err) + collection := db.Client.Database(dbName).Collection("items") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "a"}, + bson.M{"name": "b"}, + bson.M{"name": "c"}, + }) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Execute without options should work (backward compatible) - result, err := gc.Execute(ctx, dbName, "db.items.find()") - require.NoError(t, err) - require.Equal(t, 3, result.RowCount) + // Execute without options should work (backward compatible) + result, err := gc.Execute(ctx, dbName, "db.items.find()") + require.NoError(t, err) + require.Equal(t, 3, result.RowCount) + }) } func TestCountDocumentsWithMaxRows(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_count_maxrows" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Insert 100 documents - collection := client.Database(dbName).Collection("items") - docs := make([]any, 100) - for i := 0; i < 100; i++ { - docs[i] = bson.M{"index": i} - } - _, err := collection.InsertMany(ctx, docs) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Without MaxRows - counts all 100 - result, err := gc.Execute(ctx, dbName, "db.items.countDocuments()") - require.NoError(t, err) - require.Equal(t, "100", result.Rows[0]) - - // With MaxRows(50) - counts up to 50 - result, err = gc.Execute(ctx, dbName, "db.items.countDocuments()", gomongo.WithMaxRows(50)) - require.NoError(t, err) - require.Equal(t, "50", result.Rows[0]) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_count_maxrows_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Insert 100 documents + collection := db.Client.Database(dbName).Collection("items") + docs := make([]any, 100) + for i := range 100 { + docs[i] = bson.M{"index": i} + } + _, err := collection.InsertMany(ctx, docs) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + // Without MaxRows - counts all 100 + result, err := gc.Execute(ctx, dbName, "db.items.countDocuments()") + require.NoError(t, err) + require.Equal(t, "100", result.Rows[0]) + + // With MaxRows(50) - counts up to 50 + result, err = gc.Execute(ctx, dbName, "db.items.countDocuments()", gomongo.WithMaxRows(50)) + require.NoError(t, err) + require.Equal(t, "50", result.Rows[0]) + }) +} + +func TestFindWithNestedAndOr(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_nested_andor_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + collection := db.Client.Database(dbName).Collection("items") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"a": 1, "b": 2, "c": 3}, + bson.M{"a": 1, "b": 9, "c": 3}, + bson.M{"a": 9, "b": 2, "c": 3}, + bson.M{"a": 9, "b": 9, "c": 9}, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + // {$and: [{$or: [{a: 1}, {b: 2}]}, {c: 3}]} + result, err := gc.Execute(ctx, dbName, `db.items.find({$and: [{$or: [{a: 1}, {b: 2}]}, {c: 3}]})`) + require.NoError(t, err) + require.Equal(t, 3, result.RowCount) + }) +} + +func TestFindWithMultipleOperatorsSameField(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_multi_op_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + collection := db.Client.Database(dbName).Collection("items") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"age": 15}, + bson.M{"age": 25}, + bson.M{"age": 30}, + bson.M{"age": 35}, + bson.M{"age": 55}, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + // {age: {$gt: 20, $lt: 50, $ne: 30}} + result, err := gc.Execute(ctx, dbName, `db.items.find({age: {$gt: 20, $lt: 50, $ne: 30}})`) + require.NoError(t, err) + require.Equal(t, 2, result.RowCount) // 25 and 35 + }) +} + +func TestFindWithElemMatch(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_elemmatch_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + collection := db.Client.Database(dbName).Collection("students") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"name": "Alice", "scores": []bson.M{{"subject": "math", "score": 95}, {"subject": "english", "score": 80}}}, + bson.M{"name": "Bob", "scores": []bson.M{{"subject": "math", "score": 70}, {"subject": "english", "score": 75}}}, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + result, err := gc.Execute(ctx, dbName, `db.students.find({scores: {$elemMatch: {score: {$gt: 90}}}})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], "Alice") + }) +} + +func TestFindAllCursorModifiers(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_all_modifiers_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + collection := db.Client.Database(dbName).Collection("items") + docs := make([]any, 20) + for i := range 20 { + docs[i] = bson.M{"idx": i, "name": fmt.Sprintf("item%02d", i)} + } + _, err := collection.InsertMany(ctx, docs) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + // sort descending, skip 5, limit 3, project only idx + result, err := gc.Execute(ctx, dbName, `db.items.find().sort({idx: -1}).skip(5).limit(3).projection({idx: 1, _id: 0})`) + require.NoError(t, err) + require.Equal(t, 3, result.RowCount) + // Should get idx 14, 13, 12 (sorted desc, skip top 5: 19,18,17,16,15) + require.Contains(t, result.Rows[0], "14") + require.Contains(t, result.Rows[1], "13") + require.Contains(t, result.Rows[2], "12") + }) +} + +func TestAggregateWithJSFunction(t *testing.T) { + // Skip: The ANTLR-based MongoDB parser does not support JavaScript function literals. + // The parser fails with "no viable alternative at input '...body: function'" when + // encountering inline JavaScript functions in $function operators. + // This is a parser limitation, not a gomongo limitation. + t.Skip("Parser does not support JavaScript function literals in $function operator") + + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + // DocumentDB may not support $function + if db.Name == "documentdb" { + t.Skip("DocumentDB does not support $function") + } + + dbName := fmt.Sprintf("testdb_js_func_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + collection := db.Client.Database(dbName).Collection("numbers") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"value": 2}, + bson.M{"value": 3}, + bson.M{"value": 4}, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + result, err := gc.Execute(ctx, dbName, `db.numbers.aggregate([ + {$addFields: { + isEven: { + $function: { + body: function(x) { return x % 2 === 0; }, + args: ["$value"], + lang: "js" + } + } + }} + ])`) + require.NoError(t, err) + require.Equal(t, 3, result.RowCount) + }) +} + +func TestFindWithWhere(t *testing.T) { + // Skip: The ANTLR-based MongoDB parser does not support JavaScript function literals. + // The parser fails with "no viable alternative at input 'find({$where: function'" when + // encountering inline JavaScript functions in $where clauses. + // This is a parser limitation, not a gomongo limitation. + t.Skip("Parser does not support JavaScript function literals in $where clause") + + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + // DocumentDB may not support $where + if db.Name == "documentdb" { + t.Skip("DocumentDB does not support $where") + } + + dbName := fmt.Sprintf("testdb_where_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + collection := db.Client.Database(dbName).Collection("items") + _, err := collection.InsertMany(ctx, []any{ + bson.M{"a": 5, "b": 10}, + bson.M{"a": 10, "b": 5}, + bson.M{"a": 3, "b": 3}, + }) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + result, err := gc.Execute(ctx, dbName, `db.items.find({$where: function() { return this.a > this.b; }})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) // Only {a: 10, b: 5} + }) } diff --git a/database_test.go b/database_test.go index c868d0e..b271f5f 100644 --- a/database_test.go +++ b/database_test.go @@ -2,6 +2,7 @@ package gomongo_test import ( "context" + "fmt" "slices" "testing" @@ -12,267 +13,278 @@ import ( ) func TestShowDatabases(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_show_dbs" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create a database by inserting a document - _, err := client.Database(dbName).Collection("test").InsertOne(ctx, bson.M{"x": 1}) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - tests := []struct { - name string - statement string - }{ - {"show dbs", "show dbs"}, - {"show databases", "show databases"}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result, err := gc.Execute(ctx, dbName, tc.statement) - require.NoError(t, err) - require.NotNil(t, result) - require.GreaterOrEqual(t, result.RowCount, 1) - - // Check that dbName is in the result - require.True(t, slices.Contains(result.Rows, dbName), "expected '%s' in database list, got: %v", dbName, result.Rows) - }) - } + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_show_dbs_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create a database by inserting a document + _, err := db.Client.Database(dbName).Collection("test").InsertOne(ctx, bson.M{"x": 1}) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + tests := []struct { + name string + statement string + }{ + {"show dbs", "show dbs"}, + {"show databases", "show databases"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := gc.Execute(ctx, dbName, tc.statement) + require.NoError(t, err) + require.NotNil(t, result) + require.GreaterOrEqual(t, result.RowCount, 1) + + // Check that dbName is in the result + require.True(t, slices.Contains(result.Rows, dbName), "expected '%s' in database list, got: %v", dbName, result.Rows) + }) + } + }) } func TestShowCollections(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_show_colls" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create collections by inserting documents - _, err := client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"}) - require.NoError(t, err) - _, err = client.Database(dbName).Collection("orders").InsertOne(ctx, bson.M{"item": "book"}) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - result, err := gc.Execute(ctx, dbName, "show collections") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 2, result.RowCount) - - // Check that both collections are in the result - collectionSet := make(map[string]bool) - for _, row := range result.Rows { - collectionSet[row] = true - } - require.True(t, collectionSet["users"], "expected 'users' collection") - require.True(t, collectionSet["orders"], "expected 'orders' collection") + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_show_colls_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create collections by inserting documents + _, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"}) + require.NoError(t, err) + _, err = db.Client.Database(dbName).Collection("orders").InsertOne(ctx, bson.M{"item": "book"}) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + result, err := gc.Execute(ctx, dbName, "show collections") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 2, result.RowCount) + + // Check that both collections are in the result + collectionSet := make(map[string]bool) + for _, row := range result.Rows { + collectionSet[row] = true + } + require.True(t, collectionSet["users"], "expected 'users' collection") + require.True(t, collectionSet["orders"], "expected 'orders' collection") + }) } func TestGetCollectionNames(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_get_coll_names" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create collections by inserting documents - _, err := client.Database(dbName).Collection("products").InsertOne(ctx, bson.M{"name": "widget"}) - require.NoError(t, err) - _, err = client.Database(dbName).Collection("categories").InsertOne(ctx, bson.M{"name": "electronics"}) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - result, err := gc.Execute(ctx, dbName, "db.getCollectionNames()") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 2, result.RowCount) - - // Check that both collections are in the result - collectionSet := make(map[string]bool) - for _, row := range result.Rows { - collectionSet[row] = true - } - require.True(t, collectionSet["products"], "expected 'products' collection") - require.True(t, collectionSet["categories"], "expected 'categories' collection") + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_get_coll_names_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create collections by inserting documents + _, err := db.Client.Database(dbName).Collection("products").InsertOne(ctx, bson.M{"name": "widget"}) + require.NoError(t, err) + _, err = db.Client.Database(dbName).Collection("categories").InsertOne(ctx, bson.M{"name": "electronics"}) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + result, err := gc.Execute(ctx, dbName, "db.getCollectionNames()") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 2, result.RowCount) + + // Check that both collections are in the result + collectionSet := make(map[string]bool) + for _, row := range result.Rows { + collectionSet[row] = true + } + require.True(t, collectionSet["products"], "expected 'products' collection") + require.True(t, collectionSet["categories"], "expected 'categories' collection") + }) } func TestGetCollectionInfos(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_get_coll_infos" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create collections by inserting documents - _, err := client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"}) - require.NoError(t, err) - _, err = client.Database(dbName).Collection("orders").InsertOne(ctx, bson.M{"item": "book"}) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Test without filter - should return all collections - result, err := gc.Execute(ctx, dbName, "db.getCollectionInfos()") - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 2, result.RowCount) - - // Verify that results contain collection info structure - for _, row := range result.Rows { - require.Contains(t, row, `"name"`) - require.Contains(t, row, `"type"`) - } + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_get_coll_infos_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create collections by inserting documents + _, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"}) + require.NoError(t, err) + _, err = db.Client.Database(dbName).Collection("orders").InsertOne(ctx, bson.M{"item": "book"}) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + // Test without filter - should return all collections + result, err := gc.Execute(ctx, dbName, "db.getCollectionInfos()") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 2, result.RowCount) + + // Verify that results contain collection info structure + for _, row := range result.Rows { + require.Contains(t, row, `"name"`) + require.Contains(t, row, `"type"`) + } + }) } func TestGetCollectionInfosWithFilter(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_coll_infos_filter" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Create collections by inserting documents - _, err := client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"}) - require.NoError(t, err) - _, err = client.Database(dbName).Collection("orders").InsertOne(ctx, bson.M{"item": "book"}) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - // Test with filter - should return only matching collection - result, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({ name: "users" })`) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - - // Verify that the returned collection is "users" - require.Contains(t, result.Rows[0], `"name": "users"`) - require.Contains(t, result.Rows[0], `"type": "collection"`) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_coll_infos_filter_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Create collections by inserting documents + _, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"}) + require.NoError(t, err) + _, err = db.Client.Database(dbName).Collection("orders").InsertOne(ctx, bson.M{"item": "book"}) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + // Test with filter - should return only matching collection + result, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({ name: "users" })`) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 1, result.RowCount) + + // Verify that the returned collection is "users" + require.Contains(t, result.Rows[0], `"name": "users"`) + require.Contains(t, result.Rows[0], `"type": "collection"`) + }) } func TestGetCollectionInfosEmptyResult(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_coll_infos_empty" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_coll_infos_empty_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Create a collection - _, err := client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"}) - require.NoError(t, err) + // Create a collection + _, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"}) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // Test with filter that matches no collections - result, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({ name: "nonexistent" })`) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 0, result.RowCount) - require.Empty(t, result.Rows) + // Test with filter that matches no collections + result, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({ name: "nonexistent" })`) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 0, result.RowCount) + require.Empty(t, result.Rows) + }) } func TestGetCollectionInfosNameOnly(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_coll_infos_nameonly" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_coll_infos_nameonly_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Create a collection - _, err := client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "test"}) - require.NoError(t, err) + // Create a collection + _, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "test"}) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({}, { nameOnly: true })`) - require.NoError(t, err) - require.GreaterOrEqual(t, result.RowCount, 1) + result, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({}, { nameOnly: true })`) + require.NoError(t, err) + require.GreaterOrEqual(t, result.RowCount, 1) - // With nameOnly: true, the result should contain "name" field - require.Contains(t, result.Rows[0], `"name"`) + // With nameOnly: true, the result should contain "name" field + require.Contains(t, result.Rows[0], `"name"`) + }) } func TestGetCollectionInfosAuthorizedCollections(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_coll_infos_auth" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_coll_infos_auth_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - // Create a collection - _, err := client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "test"}) - require.NoError(t, err) + // Create a collection + _, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "test"}) + require.NoError(t, err) - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({}, { authorizedCollections: true })`) - require.NoError(t, err) - require.GreaterOrEqual(t, result.RowCount, 1) + result, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({}, { authorizedCollections: true })`) + require.NoError(t, err) + require.GreaterOrEqual(t, result.RowCount, 1) + }) } func TestGetCollectionInfosUnsupportedOption(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_coll_infos_unsup" - defer testutil.CleanupDatabase(t, client, dbName) - - gc := gomongo.NewClient(client) - ctx := context.Background() - - _, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({}, { unknownOption: true })`) - var optErr *gomongo.UnsupportedOptionError - require.ErrorAs(t, err, &optErr) - require.Equal(t, "getCollectionInfos()", optErr.Method) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_coll_infos_unsup_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + _, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({}, { unknownOption: true })`) + var optErr *gomongo.UnsupportedOptionError + require.ErrorAs(t, err, &optErr) + require.Equal(t, "getCollectionInfos()", optErr.Method) + }) } func TestGetCollectionInfosTooManyArgs(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_coll_infos_args" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_coll_infos_args_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - gc := gomongo.NewClient(client) - ctx := context.Background() + gc := gomongo.NewClient(db.Client) + ctx := context.Background() - _, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({}, {}, {})`) - require.Error(t, err) - require.Contains(t, err.Error(), "takes at most 2 arguments") + _, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({}, {}, {})`) + require.Error(t, err) + require.Contains(t, err.Error(), "takes at most 2 arguments") + }) } func TestCollectionAccessPatterns(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_coll_access" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - - // Insert a document - collection := client.Database(dbName).Collection("my-collection") - _, err := collection.InsertOne(ctx, bson.M{"data": "test"}) - require.NoError(t, err) - - gc := gomongo.NewClient(client) - - tests := []struct { - name string - statement string - }{ - {"dot access", "db.users.find()"}, - {"bracket double quote", `db["my-collection"].find()`}, - {"bracket single quote", `db['my-collection'].find()`}, - {"getCollection", `db.getCollection("my-collection").find()`}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result, err := gc.Execute(ctx, dbName, tc.statement) - require.NoError(t, err) - require.NotNil(t, result) - }) - } + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_coll_access_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + + // Insert a document + collection := db.Client.Database(dbName).Collection("my-collection") + _, err := collection.InsertOne(ctx, bson.M{"data": "test"}) + require.NoError(t, err) + + gc := gomongo.NewClient(db.Client) + + tests := []struct { + name string + statement string + }{ + {"dot access", "db.users.find()"}, + {"bracket double quote", `db["my-collection"].find()`}, + {"bracket single quote", `db['my-collection'].find()`}, + {"getCollection", `db.getCollection("my-collection").find()`}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := gc.Execute(ctx, dbName, tc.statement) + require.NoError(t, err) + require.NotNil(t, result) + }) + } + }) } diff --git a/error_test.go b/error_test.go index 0ce873e..3675b02 100644 --- a/error_test.go +++ b/error_test.go @@ -2,6 +2,7 @@ package gomongo_test import ( "context" + "fmt" "testing" "github.com/bytebase/gomongo" @@ -10,69 +11,73 @@ import ( ) func TestParseError(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_parse_error" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_parse_error_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - gc := gomongo.NewClient(client) - ctx := context.Background() + gc := gomongo.NewClient(db.Client) + ctx := context.Background() - _, err := gc.Execute(ctx, dbName, "db.users.find({ name: })") - require.Error(t, err) + _, err := gc.Execute(ctx, dbName, "db.users.find({ name: })") + require.Error(t, err) - var parseErr *gomongo.ParseError - require.ErrorAs(t, err, &parseErr) + var parseErr *gomongo.ParseError + require.ErrorAs(t, err, &parseErr) + }) } func TestPlannedOperation(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_planned_op" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_planned_op_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - gc := gomongo.NewClient(client) - ctx := context.Background() + gc := gomongo.NewClient(db.Client) + ctx := context.Background() - // createIndex is a planned M3 operation - should return PlannedOperationError - _, err := gc.Execute(ctx, dbName, "db.users.createIndex({ name: 1 })") - require.Error(t, err) + // createIndex is a planned M3 operation - should return PlannedOperationError + _, err := gc.Execute(ctx, dbName, "db.users.createIndex({ name: 1 })") + require.Error(t, err) - var plannedErr *gomongo.PlannedOperationError - require.ErrorAs(t, err, &plannedErr) - require.Equal(t, "createIndex()", plannedErr.Operation) + var plannedErr *gomongo.PlannedOperationError + require.ErrorAs(t, err, &plannedErr) + require.Equal(t, "createIndex()", plannedErr.Operation) + }) } func TestUnsupportedOperation(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_unsup_op" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_unsup_op_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - gc := gomongo.NewClient(client) - ctx := context.Background() + gc := gomongo.NewClient(db.Client) + ctx := context.Background() - // createSearchIndex is NOT in the registry - should return UnsupportedOperationError - _, err := gc.Execute(ctx, dbName, `db.movies.createSearchIndex({ name: "default", definition: { mappings: { dynamic: true } } })`) - require.Error(t, err) + // createSearchIndex is NOT in the registry - should return UnsupportedOperationError + _, err := gc.Execute(ctx, dbName, `db.movies.createSearchIndex({ name: "default", definition: { mappings: { dynamic: true } } })`) + require.Error(t, err) - var unsupportedErr *gomongo.UnsupportedOperationError - require.ErrorAs(t, err, &unsupportedErr) - require.Equal(t, "createSearchIndex()", unsupportedErr.Operation) + var unsupportedErr *gomongo.UnsupportedOperationError + require.ErrorAs(t, err, &unsupportedErr) + require.Equal(t, "createSearchIndex()", unsupportedErr.Operation) + }) } func TestUnsupportedOptionError(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_unsup_opt_err" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_unsup_opt_err_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - // find() with unsupported option 'collation' - _, err := gc.Execute(ctx, dbName, `db.users.find({}, {}, { collation: { locale: "en" } })`) - var optErr *gomongo.UnsupportedOptionError - require.ErrorAs(t, err, &optErr) - require.Equal(t, "find()", optErr.Method) - require.Equal(t, "collation", optErr.Option) + // find() with unsupported option 'collation' + _, err := gc.Execute(ctx, dbName, `db.users.find({}, {}, { collation: { locale: "en" } })`) + var optErr *gomongo.UnsupportedOptionError + require.ErrorAs(t, err, &optErr) + require.Equal(t, "find()", optErr.Method) + require.Equal(t, "collation", optErr.Option) + }) } func TestMethodRegistryStats(t *testing.T) { @@ -87,79 +92,84 @@ func TestMethodRegistryStats(t *testing.T) { } func TestFindOneUnsupportedOption(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_findone_unsup_opt" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_findone_unsup_opt_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.findOne({}, {}, { collation: { locale: "en" } })`) - var optErr *gomongo.UnsupportedOptionError - require.ErrorAs(t, err, &optErr) - require.Equal(t, "findOne()", optErr.Method) - require.Equal(t, "collation", optErr.Option) + _, err := gc.Execute(ctx, dbName, `db.users.findOne({}, {}, { collation: { locale: "en" } })`) + var optErr *gomongo.UnsupportedOptionError + require.ErrorAs(t, err, &optErr) + require.Equal(t, "findOne()", optErr.Method) + require.Equal(t, "collation", optErr.Option) + }) } func TestAggregateUnsupportedOption(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_agg_unsup_opt" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_agg_unsup_opt_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.aggregate([], { allowDiskUse: true })`) - var optErr *gomongo.UnsupportedOptionError - require.ErrorAs(t, err, &optErr) - require.Equal(t, "aggregate()", optErr.Method) - require.Equal(t, "allowDiskUse", optErr.Option) + _, err := gc.Execute(ctx, dbName, `db.users.aggregate([], { allowDiskUse: true })`) + var optErr *gomongo.UnsupportedOptionError + require.ErrorAs(t, err, &optErr) + require.Equal(t, "aggregate()", optErr.Method) + require.Equal(t, "allowDiskUse", optErr.Option) + }) } func TestCountDocumentsUnsupportedOption(t *testing.T) { - dbName := "testdb_count_unsup" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_count_unsup_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.countDocuments({}, { collation: { locale: "en" } })`) - var optErr *gomongo.UnsupportedOptionError - require.ErrorAs(t, err, &optErr) - require.Equal(t, "countDocuments()", optErr.Method) + _, err := gc.Execute(ctx, dbName, `db.users.countDocuments({}, { collation: { locale: "en" } })`) + var optErr *gomongo.UnsupportedOptionError + require.ErrorAs(t, err, &optErr) + require.Equal(t, "countDocuments()", optErr.Method) + }) } func TestDistinctUnsupportedOption(t *testing.T) { - dbName := "testdb_distinct_unsup" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_distinct_unsup_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.distinct("city", {}, { collation: { locale: "en" } })`) - var optErr *gomongo.UnsupportedOptionError - require.ErrorAs(t, err, &optErr) - require.Equal(t, "distinct()", optErr.Method) + _, err := gc.Execute(ctx, dbName, `db.users.distinct("city", {}, { collation: { locale: "en" } })`) + var optErr *gomongo.UnsupportedOptionError + require.ErrorAs(t, err, &optErr) + require.Equal(t, "distinct()", optErr.Method) + }) } func TestEstimatedDocumentCountUnsupportedOption(t *testing.T) { - dbName := "testdb_est_count_unsup" - client := testutil.GetClient(t) - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_est_count_unsup_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() + ctx := context.Background() - gc := gomongo.NewClient(client) + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.estimatedDocumentCount({ comment: "test" })`) - var optErr *gomongo.UnsupportedOptionError - require.ErrorAs(t, err, &optErr) - require.Equal(t, "estimatedDocumentCount()", optErr.Method) - require.Equal(t, "comment", optErr.Option) + _, err := gc.Execute(ctx, dbName, `db.users.estimatedDocumentCount({ comment: "test" })`) + var optErr *gomongo.UnsupportedOptionError + require.ErrorAs(t, err, &optErr) + require.Equal(t, "estimatedDocumentCount()", optErr.Method) + require.Equal(t, "comment", optErr.Option) + }) } diff --git a/fuzz_test.go b/fuzz_test.go new file mode 100644 index 0000000..dc92c19 --- /dev/null +++ b/fuzz_test.go @@ -0,0 +1,64 @@ +package gomongo_test + +import ( + "context" + "fmt" + "testing" + + "github.com/bytebase/gomongo" + "github.com/bytebase/gomongo/internal/testutil" +) + +func FuzzFindFilter(f *testing.F) { + // Seed corpus with valid filters + f.Add(`{}`) + f.Add(`{"name": "test"}`) + f.Add(`{"age": 25}`) + f.Add(`{"age": {"$gt": 10}}`) + f.Add(`{"$and": [{"a": 1}, {"b": 2}]}`) + f.Add(`{"name": {"$regex": "^test"}}`) + + f.Fuzz(func(t *testing.T, filter string) { + // Get first available client (don't iterate all for fuzz) + dbs := testutil.GetAllClients(t) + if len(dbs) == 0 { + t.Skip("no database available") + } + db := dbs[0] + + dbName := "fuzz_test" + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + // Should not panic - errors are OK + query := fmt.Sprintf(`db.test.find(%s)`, filter) + _, _ = gc.Execute(ctx, dbName, query) + }) +} + +func FuzzInsertDocument(f *testing.F) { + // Seed corpus with valid documents + f.Add(`{"name": "test"}`) + f.Add(`{"a": 1, "b": "two", "c": true}`) + f.Add(`{"nested": {"deep": {"value": 1}}}`) + f.Add(`{"arr": [1, 2, 3]}`) + + f.Fuzz(func(t *testing.T, doc string) { + dbs := testutil.GetAllClients(t) + if len(dbs) == 0 { + t.Skip("no database available") + } + db := dbs[0] + + dbName := "fuzz_insert" + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + query := fmt.Sprintf(`db.test.insertOne(%s)`, doc) + _, _ = gc.Execute(ctx, dbName, query) + }) +} diff --git a/go.mod b/go.mod index 937fe5a..7d853cf 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/bytebase/parser v0.0.0-20260121030202-698704919f24 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.40.0 github.com/testcontainers/testcontainers-go/modules/mongodb v0.40.0 go.mongodb.org/mongo-driver/v2 v2.4.1 ) @@ -51,7 +52,6 @@ require ( github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v4 v4.25.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/testcontainers/testcontainers-go v0.40.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect diff --git a/internal/testutil/container.go b/internal/testutil/container.go index 7b79403..9333b7e 100644 --- a/internal/testutil/container.go +++ b/internal/testutil/container.go @@ -2,50 +2,187 @@ package testutil import ( "context" + "crypto/tls" + "fmt" "sync" "testing" "time" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mongodb" + "github.com/testcontainers/testcontainers-go/wait" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) +// TestDB represents a test database connection. +type TestDB struct { + Name string + Client *mongo.Client +} + var ( - container *mongodb.MongoDBContainer - client *mongo.Client - containerOnce sync.Once - containerErr error + testDBs []TestDB + setupOnce sync.Once + setupErr error ) -// GetClient returns a shared MongoDB client for testing. -// The container is started once and reused across all tests. -// Each test should use a unique database name to avoid interference. -func GetClient(t *testing.T) *mongo.Client { +// GetAllClients returns all test database clients. +// Containers are started once and reused across all tests. +func GetAllClients(t *testing.T) []TestDB { t.Helper() - containerOnce.Do(func() { + setupOnce.Do(func() { ctx := context.Background() + testDBs, setupErr = setupContainers(ctx) + }) + + if setupErr != nil { + t.Fatalf("failed to setup test containers: %v", setupErr) + } + + return testDBs +} + +// GetClient returns the first available MongoDB client (for backward compatibility). +func GetClient(t *testing.T) *mongo.Client { + t.Helper() + dbs := GetAllClients(t) + if len(dbs) == 0 { + t.Fatal("no test databases available") + } + return dbs[0].Client +} - container, containerErr = mongodb.Run(ctx, "mongo:7") - if containerErr != nil { +func setupContainers(ctx context.Context) ([]TestDB, error) { + var dbs []TestDB + var mu sync.Mutex + var wg sync.WaitGroup + errCh := make(chan error, 3) + + // MongoDB 4.4 (Go driver v2 requires at least MongoDB 4.2) + wg.Add(1) + go func() { + defer wg.Done() + db, err := setupMongoDB(ctx, "mongo4", "mongo:4.4") + if err != nil { + errCh <- fmt.Errorf("mongo4: %w", err) + return + } + mu.Lock() + dbs = append(dbs, db) + mu.Unlock() + }() + + // MongoDB 8.0 + wg.Add(1) + go func() { + defer wg.Done() + db, err := setupMongoDB(ctx, "mongo8", "mongo:8.0") + if err != nil { + errCh <- fmt.Errorf("mongo8: %w", err) return } + mu.Lock() + dbs = append(dbs, db) + mu.Unlock() + }() - connectionString, err := container.ConnectionString(ctx) + // DocumentDB + wg.Add(1) + go func() { + defer wg.Done() + db, err := setupDocumentDB(ctx) if err != nil { - containerErr = err + errCh <- fmt.Errorf("documentdb: %w", err) return } + mu.Lock() + dbs = append(dbs, db) + mu.Unlock() + }() - client, containerErr = mongo.Connect(options.Client().ApplyURI(connectionString)) + wg.Wait() + close(errCh) + + // Collect errors - fail if any container failed + var errs []error + for err := range errCh { + errs = append(errs, err) + } + + if len(errs) > 0 { + return nil, fmt.Errorf("container setup failed: %v", errs) + } + + return dbs, nil +} + +func setupMongoDB(ctx context.Context, name, image string) (TestDB, error) { + container, err := mongodb.Run(ctx, image) + if err != nil { + return TestDB{}, err + } + + connStr, err := container.ConnectionString(ctx) + if err != nil { + return TestDB{}, err + } + + client, err := mongo.Connect(options.Client().ApplyURI(connStr)) + if err != nil { + return TestDB{}, err + } + + return TestDB{Name: name, Client: client}, nil +} + +func setupDocumentDB(ctx context.Context) (TestDB, error) { + req := testcontainers.ContainerRequest{ + Image: "ghcr.io/documentdb/documentdb/documentdb-local:latest", + ExposedPorts: []string{"10260/tcp"}, + Cmd: []string{"--username", "test", "--password", "testpass"}, + WaitingFor: wait.ForLog("documentdb").WithStartupTimeout(60 * time.Second), + } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, }) + if err != nil { + return TestDB{}, err + } - if containerErr != nil { - t.Fatalf("failed to setup test container: %v", containerErr) + host, err := container.Host(ctx) + if err != nil { + return TestDB{}, err } - return client + port, err := container.MappedPort(ctx, "10260") + if err != nil { + return TestDB{}, err + } + + connStr := fmt.Sprintf("mongodb://test:testpass@%s:%s/?tls=true&tlsInsecure=true", host, port.Port()) + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + client, err := mongo.Connect( + options.Client(). + ApplyURI(connStr). + SetTLSConfig(tlsConfig), + ) + if err != nil { + return TestDB{}, err + } + + // Verify connection + pingCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + if err := client.Ping(pingCtx, nil); err != nil { + return TestDB{}, fmt.Errorf("ping failed: %w", err) + } + + return TestDB{Name: "documentdb", Client: client}, nil } // CleanupDatabase drops the specified database after a test. @@ -57,3 +194,14 @@ func CleanupDatabase(t *testing.T, client *mongo.Client, dbName string) { t.Logf("warning: failed to drop database %s: %v", dbName, err) } } + +// RunOnAllDBs runs a test function against all available databases. +func RunOnAllDBs(t *testing.T, testFn func(t *testing.T, db TestDB)) { + t.Helper() + dbs := GetAllClients(t) + for _, db := range dbs { + t.Run(db.Name, func(t *testing.T) { + testFn(t, db) + }) + } +} diff --git a/internal/testutil/container_test.go b/internal/testutil/container_test.go new file mode 100644 index 0000000..77d4b1a --- /dev/null +++ b/internal/testutil/container_test.go @@ -0,0 +1,39 @@ +package testutil + +import ( + "context" + "fmt" + "testing" + + "github.com/bytebase/gomongo" + "github.com/stretchr/testify/require" +) + +func TestMultiContainer(t *testing.T) { + dbs := GetAllClients(t) + require.GreaterOrEqual(t, len(dbs), 2) // At least mongo4 and mongo8 + for _, db := range dbs { + require.NotEmpty(t, db.Name) + require.NotNil(t, db.Client) + } +} + +func TestRunOnAllDBsHelper(t *testing.T) { + RunOnAllDBs(t, func(t *testing.T, db TestDB) { + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + dbName := fmt.Sprintf("test_%s_helper", db.Name) + defer CleanupDatabase(t, db.Client, dbName) + + result, err := gc.Execute(ctx, dbName, "db.test.find()") + require.NoError(t, err) + require.Equal(t, 0, result.RowCount) + }) +} + +func TestLoadFixture(t *testing.T) { + docs, err := LoadFixture("users.json") + require.NoError(t, err) + require.Len(t, docs, 5) + require.Equal(t, "user1", docs[0]["_id"]) +} diff --git a/internal/testutil/fixtures.go b/internal/testutil/fixtures.go new file mode 100644 index 0000000..4663ebe --- /dev/null +++ b/internal/testutil/fixtures.go @@ -0,0 +1,48 @@ +package testutil + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// LoadFixture loads a JSON fixture file and returns it as a slice of bson.M. +func LoadFixture(filename string) ([]bson.M, error) { + path := fixturePath(filename) + + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read fixture %s: %w", filename, err) + } + + var docs []bson.M + if err := json.Unmarshal(data, &docs); err != nil { + return nil, fmt.Errorf("unmarshal fixture %s: %w", filename, err) + } + + return docs, nil +} + +// LoadFixtureAsAny loads a fixture and returns []any for InsertMany. +func LoadFixtureAsAny(filename string) ([]any, error) { + docs, err := LoadFixture(filename) + if err != nil { + return nil, err + } + + result := make([]any, len(docs)) + for i, doc := range docs { + result[i] = doc + } + return result, nil +} + +func fixturePath(filename string) string { + _, currentFile, _, _ := runtime.Caller(0) + projectRoot := filepath.Dir(filepath.Dir(filepath.Dir(currentFile))) + return filepath.Join(projectRoot, "testdata", filename) +} diff --git a/internal/translator/bson_helpers_test.go b/internal/translator/bson_helpers_test.go new file mode 100644 index 0000000..335a345 --- /dev/null +++ b/internal/translator/bson_helpers_test.go @@ -0,0 +1,168 @@ +package translator_test + +import ( + "context" + "testing" + + "github.com/bytebase/gomongo" + "github.com/bytebase/gomongo/internal/testutil" + "github.com/stretchr/testify/require" +) + +// These tests verify BSON helper function conversions through the full pipeline. +// Since the helper functions are not exported, we test them end-to-end. + +func TestObjectIdHelper(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := "testdb_objectid_helper_" + db.Name + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + // Test ObjectId with valid hex string + _, err := gc.Execute(ctx, dbName, `db.test.insertOne({_id: ObjectId("507f1f77bcf86cd799439011"), name: "test"})`) + require.NoError(t, err) + + result, err := gc.Execute(ctx, dbName, `db.test.findOne({_id: ObjectId("507f1f77bcf86cd799439011")})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], "507f1f77bcf86cd799439011") + }) +} + +func TestObjectIdHelperGenerated(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := "testdb_objectid_gen_" + db.Name + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + // Test ObjectId() without arguments (generates new ObjectId) + _, err := gc.Execute(ctx, dbName, `db.test.insertOne({name: "test"})`) + require.NoError(t, err) + + result, err := gc.Execute(ctx, dbName, `db.test.findOne({})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + // Should have an _id field with ObjectId + require.Contains(t, result.Rows[0], `"_id"`) + }) +} + +func TestISODateHelper(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := "testdb_isodate_helper_" + db.Name + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + // Test ISODate with valid ISO string + _, err := gc.Execute(ctx, dbName, `db.test.insertOne({created: ISODate("2024-01-15T10:30:00Z")})`) + require.NoError(t, err) + + result, err := gc.Execute(ctx, dbName, `db.test.findOne({})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + // Extended JSON format for dates + require.Contains(t, result.Rows[0], "2024-01-15") + }) +} + +func TestNumberLongHelper(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := "testdb_numberlong_helper_" + db.Name + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + // Test NumberLong with large number (beyond JS safe integer) + _, err := gc.Execute(ctx, dbName, `db.test.insertOne({bignum: NumberLong("9007199254740993")})`) + require.NoError(t, err) + + result, err := gc.Execute(ctx, dbName, `db.test.findOne({})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], "9007199254740993") + }) +} + +func TestNumberIntHelper(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := "testdb_numberint_helper_" + db.Name + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + // Test NumberInt + _, err := gc.Execute(ctx, dbName, `db.test.insertOne({count: NumberInt(42)})`) + require.NoError(t, err) + + result, err := gc.Execute(ctx, dbName, `db.test.findOne({})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], "42") + }) +} + +func TestUUIDHelper(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := "testdb_uuid_helper_" + db.Name + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + // Test UUID helper + _, err := gc.Execute(ctx, dbName, `db.test.insertOne({uuid: UUID("550e8400-e29b-41d4-a716-446655440000")})`) + require.NoError(t, err) + + result, err := gc.Execute(ctx, dbName, `db.test.findOne({})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + // UUID should be in the output (as binary subtype 4) + require.Contains(t, result.Rows[0], "uuid") + }) +} + +func TestTimestampHelper(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := "testdb_timestamp_helper_" + db.Name + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + // Test Timestamp(t, i) format + _, err := gc.Execute(ctx, dbName, `db.test.insertOne({ts: Timestamp(1234567890, 1)})`) + require.NoError(t, err) + + result, err := gc.Execute(ctx, dbName, `db.test.findOne({})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], "1234567890") + }) +} + +func TestDecimal128Helper(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := "testdb_decimal128_helper_" + db.Name + defer testutil.CleanupDatabase(t, db.Client, dbName) + + gc := gomongo.NewClient(db.Client) + ctx := context.Background() + + // Test Decimal128 (NumberDecimal) + _, err := gc.Execute(ctx, dbName, `db.test.insertOne({price: NumberDecimal("123.456")})`) + require.NoError(t, err) + + result, err := gc.Execute(ctx, dbName, `db.test.findOne({})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], "123.456") + }) +} diff --git a/testdata/complex_documents.json b/testdata/complex_documents.json new file mode 100644 index 0000000..9d197ae --- /dev/null +++ b/testdata/complex_documents.json @@ -0,0 +1,67 @@ +[ + { + "_id": "deeply_nested", + "level1": { + "level2": { + "level3": { + "level4": { + "level5": {"value": "deep"} + } + } + } + } + }, + { + "_id": "array_variations", + "empty_array": [], + "single_item": ["only"], + "numbers": [1, 2, 3, 4, 5], + "mixed_types": [1, "two", true, null, {"nested": "obj"}], + "nested_arrays": [[1, 2], [3, 4], [[5, 6], [7, 8]]] + }, + { + "_id": "numeric_edge_cases", + "zero": 0, + "negative": -42, + "float": 3.14159265358979, + "large_int": 9007199254740991, + "small_float": 0.0000001, + "scientific": 1.23e10 + }, + { + "_id": "boolean_null", + "true_val": true, + "false_val": false, + "null_val": null, + "missing_field_test": "other fields exist" + }, + { + "_id": "field_name_edge", + "normal": "ok", + "with-dash": "dashed field", + "with_underscore": "underscored", + "with space": "spaced field", + "123numeric": "numeric start" + }, + { + "_id": "orders_sample", + "customer": {"name": "Alice", "tier": "gold"}, + "items": [ + {"product": "Widget", "qty": 2, "price": 25.00}, + {"product": "Gadget", "qty": 1, "price": 99.99} + ], + "shipping": {"address": {"city": "Tokyo", "zip": "100-0001"}, "method": "express"}, + "status": "shipped", + "total": 149.99 + }, + { + "_id": "empty_nested", + "empty_obj": {}, + "obj_with_empty": {"a": {}, "b": {"c": {}}}, + "array_of_empty": [{}, {}, {}] + }, + { + "_id": "large_array", + "items": ["item0", "item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item9", "item10", "item11", "item12", "item13", "item14", "item15", "item16", "item17", "item18", "item19"] + } +] diff --git a/testdata/unicode_samples.json b/testdata/unicode_samples.json new file mode 100644 index 0000000..3376f7e --- /dev/null +++ b/testdata/unicode_samples.json @@ -0,0 +1,18 @@ +[ + {"_id": "zh_simplified", "name": "张三", "city": "北京", "bio": "软件工程师,喜欢编程", "lang": "zh-CN"}, + {"_id": "zh_traditional", "name": "李四", "city": "台北", "bio": "資深開發者", "lang": "zh-TW"}, + {"_id": "ja", "name": "田中太郎", "city": "東京", "bio": "プログラマーです。趣味は読書。", "lang": "ja"}, + {"_id": "ko", "name": "김철수", "city": "서울", "bio": "백엔드 개발자입니다", "lang": "ko"}, + {"_id": "ar", "name": "محمد أحمد", "city": "القاهرة", "bio": "مهندس برمجيات متخصص في قواعد البيانات", "lang": "ar"}, + {"_id": "he", "name": "דוד כהן", "city": "תל אביב", "bio": "מפתח תוכנה בכיר", "lang": "he"}, + {"_id": "th", "name": "สมชาย", "city": "กรุงเทพ", "bio": "นักพัฒนาซอฟต์แวร์", "lang": "th"}, + {"_id": "ru", "name": "Иван Петров", "city": "Москва", "bio": "Программист баз данных", "lang": "ru"}, + {"_id": "emoji", "name": "Test User 🎉", "city": "Fun City 🌈", "bio": "Love coding 💻 and coffee ☕!", "tags": ["🔥", "✨", "🚀", "❤️"], "lang": "en"}, + {"_id": "mixed_cjk", "name": "Alice 张三 田中", "city": "Tokyo/東京/东京", "bio": "Bilingual: 日本語と中文", "lang": "mixed"}, + {"_id": "mixed_rtl", "name": "David דוד محمد", "city": "Global", "bio": "English עברית العربية together", "lang": "mixed"}, + {"_id": "special_chars", "name": "O'Brien-Smith", "city": "San José", "bio": "Café owner & \"entrepreneur\"", "lang": "en"}, + {"_id": "math_symbols", "name": "Dr. π", "city": "∞ City", "bio": "∑(1/n²) = π²/6, √2 ≈ 1.414", "lang": "math"}, + {"_id": "currency", "name": "Trader", "city": "Finance Hub", "bio": "Deals in $, €, £, ¥, ₹, ₿", "lang": "en"}, + {"_id": "newlines", "name": "Multi\nLine", "city": "Test", "bio": "Line1\nLine2\tTabbed\rCarriage", "lang": "en"}, + {"_id": "zero_width", "name": "Zero​Width​", "city": "Invisible", "bio": "Has zero-width spaces", "lang": "en"} +] diff --git a/testdata/users.json b/testdata/users.json new file mode 100644 index 0000000..27b8460 --- /dev/null +++ b/testdata/users.json @@ -0,0 +1,7 @@ +[ + {"_id": "user1", "name": "Alice", "age": 30, "city": "Tokyo", "active": true, "score": 95.5, "tags": ["admin", "vip"]}, + {"_id": "user2", "name": "Bob", "age": 25, "city": "Paris", "active": false, "score": 88.0, "tags": ["user"]}, + {"_id": "user3", "name": "Charlie", "age": 35, "city": "Tokyo", "active": true, "score": 72.3, "tags": []}, + {"_id": "user4", "name": "Diana", "age": 28, "city": "London", "active": true, "score": null, "tags": ["user", "beta"]}, + {"_id": "user5", "name": "Eve", "age": 45, "city": "Berlin", "active": false, "score": 100.0, "tags": ["admin"]} +] diff --git a/unicode_test.go b/unicode_test.go new file mode 100644 index 0000000..948dba0 --- /dev/null +++ b/unicode_test.go @@ -0,0 +1,146 @@ +package gomongo_test + +import ( + "context" + "fmt" + "testing" + + "github.com/bytebase/gomongo" + "github.com/bytebase/gomongo/internal/testutil" + "github.com/stretchr/testify/require" +) + +func TestUnicodeInsertAndQuery(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_unicode_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + // Insert CJK document + _, err := gc.Execute(ctx, dbName, `db.users.insertOne({"name": "张三", "city": "北京"})`) + require.NoError(t, err) + + // Query by unicode field value + result, err := gc.Execute(ctx, dbName, `db.users.findOne({"name": "张三"})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], "张三") + require.Contains(t, result.Rows[0], "北京") + }) +} + +func TestUnicodeArabic(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_arabic_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + // Insert Arabic document + _, err := gc.Execute(ctx, dbName, `db.users.insertOne({"name": "محمد", "city": "القاهرة"})`) + require.NoError(t, err) + + // Query by Arabic field value + result, err := gc.Execute(ctx, dbName, `db.users.findOne({"name": "محمد"})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], "محمد") + }) +} + +func TestUnicodeEmoji(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_emoji_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + // Insert document with emoji + _, err := gc.Execute(ctx, dbName, `db.users.insertOne({"name": "Test 🎉", "tags": ["🔥", "✨"]})`) + require.NoError(t, err) + + // Query and verify emoji preserved + result, err := gc.Execute(ctx, dbName, `db.users.findOne({})`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], "🎉") + require.Contains(t, result.Rows[0], "🔥") + }) +} + +func TestUnicodeInCollectionName(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_unicode_coll_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + // Insert into unicode-named collection + _, err := gc.Execute(ctx, dbName, `db["用户表"].insertOne({"name": "test"})`) + require.NoError(t, err) + + // Query unicode-named collection + result, err := gc.Execute(ctx, dbName, `db["用户表"].find()`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + }) +} + +func TestUnicodeEmojiInCollectionName(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_emoji_coll_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + // Insert into emoji-named collection + _, err := gc.Execute(ctx, dbName, `db["users🎉"].insertOne({"name": "test"})`) + require.NoError(t, err) + + // Query emoji-named collection + result, err := gc.Execute(ctx, dbName, `db["users🎉"].find()`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + }) +} + +func TestUnicodeRoundTrip(t *testing.T) { + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_roundtrip_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + // Load unicode fixture + docs, err := testutil.LoadFixtureAsAny("unicode_samples.json") + require.NoError(t, err) + + // Insert all unicode samples using driver directly + collection := db.Client.Database(dbName).Collection("samples") + _, err = collection.InsertMany(ctx, docs) + require.NoError(t, err) + + // Query each and verify round-trip integrity + result, err := gc.Execute(ctx, dbName, `db.samples.find()`) + require.NoError(t, err) + require.Equal(t, len(docs), result.RowCount) + + // Spot check specific unicode values + allRows := "" + for _, row := range result.Rows { + allRows += row + } + require.Contains(t, allRows, "张三") // Chinese + require.Contains(t, allRows, "田中太郎") // Japanese + require.Contains(t, allRows, "김철수") // Korean + require.Contains(t, allRows, "محمد") // Arabic + require.Contains(t, allRows, "🎉") // Emoji + }) +} diff --git a/write_test.go b/write_test.go index 1eae494..61fb505 100644 --- a/write_test.go +++ b/write_test.go @@ -2,6 +2,7 @@ package gomongo_test import ( "context" + "fmt" "testing" "github.com/bytebase/gomongo" @@ -10,556 +11,583 @@ import ( ) func TestInsertOneBasic(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_insert_one" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - gc := gomongo.NewClient(client) - - // Insert a document - result, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30 })`) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - require.Contains(t, result.Rows[0], `"acknowledged": true`) - require.Contains(t, result.Rows[0], `"insertedId"`) - - // Verify document was inserted - verifyResult, err := gc.Execute(ctx, dbName, `db.users.find({ name: "alice" })`) - require.NoError(t, err) - require.Equal(t, 1, verifyResult.RowCount) - require.Contains(t, verifyResult.Rows[0], `"alice"`) - require.Contains(t, verifyResult.Rows[0], `"age": 30`) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_insert_one_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + // Insert a document + result, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30 })`) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], `"acknowledged": true`) + require.Contains(t, result.Rows[0], `"insertedId"`) + + // Verify document was inserted + verifyResult, err := gc.Execute(ctx, dbName, `db.users.find({ name: "alice" })`) + require.NoError(t, err) + require.Equal(t, 1, verifyResult.RowCount) + require.Contains(t, verifyResult.Rows[0], `"alice"`) + require.Contains(t, verifyResult.Rows[0], `"age": 30`) + }) } func TestInsertOneWithObjectId(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_insert_one_oid" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - gc := gomongo.NewClient(client) - - // Insert with explicit ObjectId - result, err := gc.Execute(ctx, dbName, `db.users.insertOne({ _id: ObjectId("507f1f77bcf86cd799439011"), name: "bob" })`) - require.NoError(t, err) - require.NotNil(t, result) - require.Contains(t, result.Rows[0], `"507f1f77bcf86cd799439011"`) - - // Verify - verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ _id: ObjectId("507f1f77bcf86cd799439011") })`) - require.NoError(t, err) - require.Equal(t, 1, verifyResult.RowCount) - require.Contains(t, verifyResult.Rows[0], `"bob"`) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_insert_one_oid_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + // Insert with explicit ObjectId + result, err := gc.Execute(ctx, dbName, `db.users.insertOne({ _id: ObjectId("507f1f77bcf86cd799439011"), name: "bob" })`) + require.NoError(t, err) + require.NotNil(t, result) + require.Contains(t, result.Rows[0], `"507f1f77bcf86cd799439011"`) + + // Verify + verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ _id: ObjectId("507f1f77bcf86cd799439011") })`) + require.NoError(t, err) + require.Equal(t, 1, verifyResult.RowCount) + require.Contains(t, verifyResult.Rows[0], `"bob"`) + }) } func TestInsertOneWithNestedDocument(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_insert_one_nested" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_insert_one_nested_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.users.insertOne({ + result, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "carol", address: { city: "NYC", zip: "10001" }, tags: ["admin", "user"] })`) - require.NoError(t, err) - require.NotNil(t, result) - - // Verify nested structure - verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ name: "carol" })`) - require.NoError(t, err) - require.Contains(t, verifyResult.Rows[0], `"city": "NYC"`) - require.Contains(t, verifyResult.Rows[0], `"admin"`) + require.NoError(t, err) + require.NotNil(t, result) + + // Verify nested structure + verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ name: "carol" })`) + require.NoError(t, err) + require.Contains(t, verifyResult.Rows[0], `"city": "NYC"`) + require.Contains(t, verifyResult.Rows[0], `"admin"`) + }) } func TestInsertOneMissingDocument(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_insert_one_missing" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - gc := gomongo.NewClient(client) - - // Note: When insertOne() is called without arguments, the parser may not - // recognize it as InsertOneMethod (grammar limitation). The error message - // varies based on parser behavior - it may be "unsupported operation" or - // "requires a document". Either way, it should be an error. - _, err := gc.Execute(ctx, dbName, `db.users.insertOne()`) - require.Error(t, err) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_insert_one_missing_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + // Note: When insertOne() is called without arguments, the parser may not + // recognize it as InsertOneMethod (grammar limitation). The error message + // varies based on parser behavior - it may be "unsupported operation" or + // "requires a document". Either way, it should be an error. + _, err := gc.Execute(ctx, dbName, `db.users.insertOne()`) + require.Error(t, err) + }) } func TestInsertOneInvalidDocument(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_insert_one_invalid" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_insert_one_invalid_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.insertOne("not a document")`) - require.Error(t, err) - require.Contains(t, err.Error(), "must be an object") + _, err := gc.Execute(ctx, dbName, `db.users.insertOne("not a document")`) + require.Error(t, err) + require.Contains(t, err.Error(), "must be an object") + }) } func TestInsertManyBasic(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_insert_many" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_insert_many_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.users.insertMany([ + result, err := gc.Execute(ctx, dbName, `db.users.insertMany([ { name: "alice", age: 30 }, { name: "bob", age: 25 }, { name: "carol", age: 35 } ])`) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - require.Contains(t, result.Rows[0], `"acknowledged": true`) - require.Contains(t, result.Rows[0], `"insertedIds"`) - - // Verify all documents were inserted - verifyResult, err := gc.Execute(ctx, dbName, `db.users.countDocuments()`) - require.NoError(t, err) - require.Equal(t, "3", verifyResult.Rows[0]) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], `"acknowledged": true`) + require.Contains(t, result.Rows[0], `"insertedIds"`) + + // Verify all documents were inserted + verifyResult, err := gc.Execute(ctx, dbName, `db.users.countDocuments()`) + require.NoError(t, err) + require.Equal(t, "3", verifyResult.Rows[0]) + }) } func TestInsertManyEmpty(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_insert_many_empty" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_insert_many_empty_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.insertMany([])`) - require.Error(t, err) // MongoDB doesn't allow empty array + _, err := gc.Execute(ctx, dbName, `db.users.insertMany([])`) + require.Error(t, err) // MongoDB doesn't allow empty array + }) } func TestUpdateOneBasic(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_update_one" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - gc := gomongo.NewClient(client) - - // Insert test data - _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30 })`) - require.NoError(t, err) - - // Update - result, err := gc.Execute(ctx, dbName, `db.users.updateOne({ name: "alice" }, { $set: { age: 31 } })`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"acknowledged": true`) - require.Contains(t, result.Rows[0], `"matchedCount": 1`) - require.Contains(t, result.Rows[0], `"modifiedCount": 1`) - - // Verify - verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ name: "alice" })`) - require.NoError(t, err) - require.Contains(t, verifyResult.Rows[0], `"age": 31`) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_update_one_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + // Insert test data + _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30 })`) + require.NoError(t, err) + + // Update + result, err := gc.Execute(ctx, dbName, `db.users.updateOne({ name: "alice" }, { $set: { age: 31 } })`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"acknowledged": true`) + require.Contains(t, result.Rows[0], `"matchedCount": 1`) + require.Contains(t, result.Rows[0], `"modifiedCount": 1`) + + // Verify + verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ name: "alice" })`) + require.NoError(t, err) + require.Contains(t, verifyResult.Rows[0], `"age": 31`) + }) } func TestUpdateOneNoMatch(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_update_one_no_match" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - gc := gomongo.NewClient(client) - - result, err := gc.Execute(ctx, dbName, `db.users.updateOne({ name: "nobody" }, { $set: { age: 99 } })`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"matchedCount": 0`) - require.Contains(t, result.Rows[0], `"modifiedCount": 0`) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_update_one_no_match_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + result, err := gc.Execute(ctx, dbName, `db.users.updateOne({ name: "nobody" }, { $set: { age: 99 } })`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"matchedCount": 0`) + require.Contains(t, result.Rows[0], `"modifiedCount": 0`) + }) } func TestUpdateOneUpsert(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_update_one_upsert" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_update_one_upsert_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.users.updateOne( + result, err := gc.Execute(ctx, dbName, `db.users.updateOne( { name: "newuser" }, { $set: { age: 25 } }, { upsert: true } )`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"upsertedId"`) - - // Verify upserted document - verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ name: "newuser" })`) - require.NoError(t, err) - require.Equal(t, 1, verifyResult.RowCount) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"upsertedId"`) + + // Verify upserted document + verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ name: "newuser" })`) + require.NoError(t, err) + require.Equal(t, 1, verifyResult.RowCount) + }) } func TestUpdateManyBasic(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_update_many" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_update_many_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - // Insert test data - _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ + // Insert test data + _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ { name: "alice", status: "active" }, { name: "bob", status: "active" }, { name: "carol", status: "inactive" } ])`) - require.NoError(t, err) + require.NoError(t, err) - // Update all active users - result, err := gc.Execute(ctx, dbName, `db.users.updateMany( + // Update all active users + result, err := gc.Execute(ctx, dbName, `db.users.updateMany( { status: "active" }, { $set: { verified: true } } )`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"matchedCount": 2`) - require.Contains(t, result.Rows[0], `"modifiedCount": 2`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"matchedCount": 2`) + require.Contains(t, result.Rows[0], `"modifiedCount": 2`) + }) } func TestUpdateManyNoMatch(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_update_many_no_match" - defer testutil.CleanupDatabase(t, client, dbName) - - ctx := context.Background() - gc := gomongo.NewClient(client) - - result, err := gc.Execute(ctx, dbName, `db.users.updateMany({ status: "nonexistent" }, { $set: { verified: true } })`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"matchedCount": 0`) - require.Contains(t, result.Rows[0], `"modifiedCount": 0`) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_update_many_no_match_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) + + ctx := context.Background() + gc := gomongo.NewClient(db.Client) + + result, err := gc.Execute(ctx, dbName, `db.users.updateMany({ status: "nonexistent" }, { $set: { verified: true } })`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"matchedCount": 0`) + require.Contains(t, result.Rows[0], `"modifiedCount": 0`) + }) } func TestUpdateManyUpsert(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_update_many_upsert" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_update_many_upsert_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.users.updateMany( + result, err := gc.Execute(ctx, dbName, `db.users.updateMany( { status: "pending" }, { $set: { verified: false } }, { upsert: true } )`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"upsertedId"`) - - // Verify upserted document - verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ status: "pending" })`) - require.NoError(t, err) - require.Equal(t, 1, verifyResult.RowCount) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"upsertedId"`) + + // Verify upserted document + verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ status: "pending" })`) + require.NoError(t, err) + require.Equal(t, 1, verifyResult.RowCount) + }) } func TestReplaceOneBasic(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_replace_one" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_replace_one_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - // Insert test data - _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30, city: "NYC" })`) - require.NoError(t, err) + // Insert test data + _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30, city: "NYC" })`) + require.NoError(t, err) - // Replace entire document - result, err := gc.Execute(ctx, dbName, `db.users.replaceOne( + // Replace entire document + result, err := gc.Execute(ctx, dbName, `db.users.replaceOne( { name: "alice" }, { name: "alice", age: 31, country: "USA" } )`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"matchedCount": 1`) - require.Contains(t, result.Rows[0], `"modifiedCount": 1`) - - // Verify - city should be gone, country should exist - verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ name: "alice" })`) - require.NoError(t, err) - require.Contains(t, verifyResult.Rows[0], `"country": "USA"`) - require.NotContains(t, verifyResult.Rows[0], `"city"`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"matchedCount": 1`) + require.Contains(t, result.Rows[0], `"modifiedCount": 1`) + + // Verify - city should be gone, country should exist + verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ name: "alice" })`) + require.NoError(t, err) + require.Contains(t, verifyResult.Rows[0], `"country": "USA"`) + require.NotContains(t, verifyResult.Rows[0], `"city"`) + }) } func TestReplaceOneUpsert(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_replace_one_upsert" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_replace_one_upsert_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.users.replaceOne( + result, err := gc.Execute(ctx, dbName, `db.users.replaceOne( { name: "newuser" }, { name: "newuser", age: 25 }, { upsert: true } )`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"upsertedId"`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"upsertedId"`) + }) } func TestDeleteOneBasic(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_delete_one" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_delete_one_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - // Insert test data - _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ + // Insert test data + _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ { name: "alice" }, { name: "bob" }, { name: "carol" } ])`) - require.NoError(t, err) - - // Delete one - result, err := gc.Execute(ctx, dbName, `db.users.deleteOne({ name: "bob" })`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"acknowledged": true`) - require.Contains(t, result.Rows[0], `"deletedCount": 1`) - - // Verify - countResult, err := gc.Execute(ctx, dbName, `db.users.countDocuments()`) - require.NoError(t, err) - require.Equal(t, "2", countResult.Rows[0]) + require.NoError(t, err) + + // Delete one + result, err := gc.Execute(ctx, dbName, `db.users.deleteOne({ name: "bob" })`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"acknowledged": true`) + require.Contains(t, result.Rows[0], `"deletedCount": 1`) + + // Verify + countResult, err := gc.Execute(ctx, dbName, `db.users.countDocuments()`) + require.NoError(t, err) + require.Equal(t, "2", countResult.Rows[0]) + }) } func TestDeleteOneNoMatch(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_delete_one_no_match" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_delete_one_no_match_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.users.deleteOne({ name: "nobody" })`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"deletedCount": 0`) + result, err := gc.Execute(ctx, dbName, `db.users.deleteOne({ name: "nobody" })`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"deletedCount": 0`) + }) } func TestDeleteManyBasic(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_delete_many" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_delete_many_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - // Insert test data - _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ + // Insert test data + _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ { name: "alice", status: "inactive" }, { name: "bob", status: "inactive" }, { name: "carol", status: "active" } ])`) - require.NoError(t, err) - - // Delete all inactive - result, err := gc.Execute(ctx, dbName, `db.users.deleteMany({ status: "inactive" })`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"deletedCount": 2`) - - // Verify only carol remains - countResult, err := gc.Execute(ctx, dbName, `db.users.countDocuments()`) - require.NoError(t, err) - require.Equal(t, "1", countResult.Rows[0]) + require.NoError(t, err) + + // Delete all inactive + result, err := gc.Execute(ctx, dbName, `db.users.deleteMany({ status: "inactive" })`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"deletedCount": 2`) + + // Verify only carol remains + countResult, err := gc.Execute(ctx, dbName, `db.users.countDocuments()`) + require.NoError(t, err) + require.Equal(t, "1", countResult.Rows[0]) + }) } func TestDeleteManyAll(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_delete_many_all" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_delete_many_all_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - // Insert test data - _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ + // Insert test data + _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ { name: "alice" }, { name: "bob" } ])`) - require.NoError(t, err) + require.NoError(t, err) - // Delete all with empty filter - result, err := gc.Execute(ctx, dbName, `db.users.deleteMany({})`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"deletedCount": 2`) + // Delete all with empty filter + result, err := gc.Execute(ctx, dbName, `db.users.deleteMany({})`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"deletedCount": 2`) + }) } func TestFindOneAndUpdateBasic(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_one_and_update" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_one_and_update_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30 })`) - require.NoError(t, err) + _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30 })`) + require.NoError(t, err) - // Returns document BEFORE update by default - result, err := gc.Execute(ctx, dbName, `db.users.findOneAndUpdate( + // Returns document BEFORE update by default + result, err := gc.Execute(ctx, dbName, `db.users.findOneAndUpdate( { name: "alice" }, { $set: { age: 31 } } )`) - require.NoError(t, err) - require.Equal(t, 1, result.RowCount) - require.Contains(t, result.Rows[0], `"age": 30`) + require.NoError(t, err) + require.Equal(t, 1, result.RowCount) + require.Contains(t, result.Rows[0], `"age": 30`) + }) } func TestFindOneAndUpdateReturnAfter(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_one_and_update_after" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_one_and_update_after_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30 })`) - require.NoError(t, err) + _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30 })`) + require.NoError(t, err) - result, err := gc.Execute(ctx, dbName, `db.users.findOneAndUpdate( + result, err := gc.Execute(ctx, dbName, `db.users.findOneAndUpdate( { name: "alice" }, { $set: { age: 31 } }, { returnDocument: "after" } )`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"age": 31`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"age": 31`) + }) } func TestFindOneAndUpdateNoMatch(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_one_and_update_no_match" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_one_and_update_no_match_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.users.findOneAndUpdate( + result, err := gc.Execute(ctx, dbName, `db.users.findOneAndUpdate( { name: "nobody" }, { $set: { age: 99 } } )`) - require.NoError(t, err) - require.Equal(t, "null", result.Rows[0]) + require.NoError(t, err) + require.Equal(t, "null", result.Rows[0]) + }) } func TestFindOneAndReplaceBasic(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_one_and_replace" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_one_and_replace_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30, city: "NYC" })`) - require.NoError(t, err) + _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30, city: "NYC" })`) + require.NoError(t, err) - // Returns document BEFORE replacement - result, err := gc.Execute(ctx, dbName, `db.users.findOneAndReplace( + // Returns document BEFORE replacement + result, err := gc.Execute(ctx, dbName, `db.users.findOneAndReplace( { name: "alice" }, { name: "alice", age: 31, country: "USA" } )`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"city": "NYC"`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"city": "NYC"`) + }) } func TestFindOneAndReplaceReturnAfter(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_one_and_replace_after" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_one_and_replace_after_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30 })`) - require.NoError(t, err) + _, err := gc.Execute(ctx, dbName, `db.users.insertOne({ name: "alice", age: 30 })`) + require.NoError(t, err) - result, err := gc.Execute(ctx, dbName, `db.users.findOneAndReplace( + result, err := gc.Execute(ctx, dbName, `db.users.findOneAndReplace( { name: "alice" }, { name: "alice", age: 31 }, { returnDocument: "after" } )`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"age": 31`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"age": 31`) + }) } func TestFindOneAndDeleteBasic(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_one_and_delete" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_one_and_delete_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ + _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ { name: "alice", age: 30 }, { name: "bob", age: 25 } ])`) - require.NoError(t, err) - - // Returns the deleted document - result, err := gc.Execute(ctx, dbName, `db.users.findOneAndDelete({ name: "alice" })`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"alice"`) - require.Contains(t, result.Rows[0], `"age": 30`) - - // Verify alice is deleted - countResult, err := gc.Execute(ctx, dbName, `db.users.countDocuments()`) - require.NoError(t, err) - require.Equal(t, "1", countResult.Rows[0]) + require.NoError(t, err) + + // Returns the deleted document + result, err := gc.Execute(ctx, dbName, `db.users.findOneAndDelete({ name: "alice" })`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"alice"`) + require.Contains(t, result.Rows[0], `"age": 30`) + + // Verify alice is deleted + countResult, err := gc.Execute(ctx, dbName, `db.users.countDocuments()`) + require.NoError(t, err) + require.Equal(t, "1", countResult.Rows[0]) + }) } func TestFindOneAndDeleteNoMatch(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_one_and_delete_no_match" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_one_and_delete_no_match_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - result, err := gc.Execute(ctx, dbName, `db.users.findOneAndDelete({ name: "nobody" })`) - require.NoError(t, err) - require.Equal(t, "null", result.Rows[0]) + result, err := gc.Execute(ctx, dbName, `db.users.findOneAndDelete({ name: "nobody" })`) + require.NoError(t, err) + require.Equal(t, "null", result.Rows[0]) + }) } func TestFindOneAndDeleteWithSort(t *testing.T) { - client := testutil.GetClient(t) - dbName := "testdb_find_one_and_delete_sort" - defer testutil.CleanupDatabase(t, client, dbName) + testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { + dbName := fmt.Sprintf("testdb_find_one_and_delete_sort_%s", db.Name) + defer testutil.CleanupDatabase(t, db.Client, dbName) - ctx := context.Background() - gc := gomongo.NewClient(client) + ctx := context.Background() + gc := gomongo.NewClient(db.Client) - _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ + _, err := gc.Execute(ctx, dbName, `db.users.insertMany([ { name: "alice", score: 10 }, { name: "alice", score: 20 } ])`) - require.NoError(t, err) + require.NoError(t, err) - // Delete the alice with lowest score - result, err := gc.Execute(ctx, dbName, `db.users.findOneAndDelete( + // Delete the alice with lowest score + result, err := gc.Execute(ctx, dbName, `db.users.findOneAndDelete( { name: "alice" }, { sort: { score: 1 } } )`) - require.NoError(t, err) - require.Contains(t, result.Rows[0], `"score": 10`) - - // Verify only score=20 remains - verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ name: "alice" })`) - require.NoError(t, err) - require.Contains(t, verifyResult.Rows[0], `"score": 20`) + require.NoError(t, err) + require.Contains(t, result.Rows[0], `"score": 10`) + + // Verify only score=20 remains + verifyResult, err := gc.Execute(ctx, dbName, `db.users.findOne({ name: "alice" })`) + require.NoError(t, err) + require.Contains(t, verifyResult.Rows[0], `"score": 20`) + }) } From 3c78f66a2da2cb91a37a63c5a5f3bfad897d5fb8 Mon Sep 17 00:00:00 2001 From: h3n4l Date: Fri, 23 Jan 2026 14:40:26 +0800 Subject: [PATCH 2/2] fix: address PR review comments - Pin DocumentDB image to version 1.0.0 - Fix port spec consistency ("10260/tcp") - Add slices.SortFunc for stable DB ordering - Add ping verification to setupMongoDB - Require all 3 DBs in TestMultiContainer - Fix distinct test to assert both values explicitly - Remove fuzz tests Co-Authored-By: Claude Opus 4.5 --- collection_test.go | 3 +- fuzz_test.go | 64 ----------------------------- internal/testutil/container.go | 21 +++++++++- internal/testutil/container_test.go | 2 +- 4 files changed, 23 insertions(+), 67 deletions(-) delete mode 100644 fuzz_test.go diff --git a/collection_test.go b/collection_test.go index b2044b9..d529b28 100644 --- a/collection_test.go +++ b/collection_test.go @@ -1913,7 +1913,8 @@ func TestDistinct(t *testing.T) { for _, row := range result.Rows { values[row] = true } - require.True(t, values[`"active"`] || values[`"inactive"`]) + require.True(t, values[`"active"`]) + require.True(t, values[`"inactive"`]) }) } diff --git a/fuzz_test.go b/fuzz_test.go deleted file mode 100644 index dc92c19..0000000 --- a/fuzz_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package gomongo_test - -import ( - "context" - "fmt" - "testing" - - "github.com/bytebase/gomongo" - "github.com/bytebase/gomongo/internal/testutil" -) - -func FuzzFindFilter(f *testing.F) { - // Seed corpus with valid filters - f.Add(`{}`) - f.Add(`{"name": "test"}`) - f.Add(`{"age": 25}`) - f.Add(`{"age": {"$gt": 10}}`) - f.Add(`{"$and": [{"a": 1}, {"b": 2}]}`) - f.Add(`{"name": {"$regex": "^test"}}`) - - f.Fuzz(func(t *testing.T, filter string) { - // Get first available client (don't iterate all for fuzz) - dbs := testutil.GetAllClients(t) - if len(dbs) == 0 { - t.Skip("no database available") - } - db := dbs[0] - - dbName := "fuzz_test" - defer testutil.CleanupDatabase(t, db.Client, dbName) - - gc := gomongo.NewClient(db.Client) - ctx := context.Background() - - // Should not panic - errors are OK - query := fmt.Sprintf(`db.test.find(%s)`, filter) - _, _ = gc.Execute(ctx, dbName, query) - }) -} - -func FuzzInsertDocument(f *testing.F) { - // Seed corpus with valid documents - f.Add(`{"name": "test"}`) - f.Add(`{"a": 1, "b": "two", "c": true}`) - f.Add(`{"nested": {"deep": {"value": 1}}}`) - f.Add(`{"arr": [1, 2, 3]}`) - - f.Fuzz(func(t *testing.T, doc string) { - dbs := testutil.GetAllClients(t) - if len(dbs) == 0 { - t.Skip("no database available") - } - db := dbs[0] - - dbName := "fuzz_insert" - defer testutil.CleanupDatabase(t, db.Client, dbName) - - gc := gomongo.NewClient(db.Client) - ctx := context.Background() - - query := fmt.Sprintf(`db.test.insertOne(%s)`, doc) - _, _ = gc.Execute(ctx, dbName, query) - }) -} diff --git a/internal/testutil/container.go b/internal/testutil/container.go index 9333b7e..aed6adb 100644 --- a/internal/testutil/container.go +++ b/internal/testutil/container.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + "slices" "sync" "testing" "time" @@ -115,6 +116,17 @@ func setupContainers(ctx context.Context) ([]TestDB, error) { return nil, fmt.Errorf("container setup failed: %v", errs) } + // Sort by name for stable ordering + slices.SortFunc(dbs, func(a, b TestDB) int { + if a.Name < b.Name { + return -1 + } + if a.Name > b.Name { + return 1 + } + return 0 + }) + return dbs, nil } @@ -134,6 +146,13 @@ func setupMongoDB(ctx context.Context, name, image string) (TestDB, error) { return TestDB{}, err } + // Verify connection + pingCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + if err := client.Ping(pingCtx, nil); err != nil { + return TestDB{}, fmt.Errorf("ping failed: %w", err) + } + return TestDB{Name: name, Client: client}, nil } @@ -158,7 +177,7 @@ func setupDocumentDB(ctx context.Context) (TestDB, error) { return TestDB{}, err } - port, err := container.MappedPort(ctx, "10260") + port, err := container.MappedPort(ctx, "10260/tcp") if err != nil { return TestDB{}, err } diff --git a/internal/testutil/container_test.go b/internal/testutil/container_test.go index 77d4b1a..cba268a 100644 --- a/internal/testutil/container_test.go +++ b/internal/testutil/container_test.go @@ -11,7 +11,7 @@ import ( func TestMultiContainer(t *testing.T) { dbs := GetAllClients(t) - require.GreaterOrEqual(t, len(dbs), 2) // At least mongo4 and mongo8 + require.Equal(t, 3, len(dbs)) // All three databases must be available: documentdb, mongo4, mongo8 for _, db := range dbs { require.NotEmpty(t, db.Name) require.NotNil(t, db.Client)