diff --git a/README.md b/README.md index d52a2d0..6d72c7f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ import ( "log" "github.com/bytebase/gomongo" + "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) @@ -46,10 +47,27 @@ func main() { log.Fatal(err) } - // Print results (Extended JSON format) - for _, row := range result.Rows { - fmt.Println(row) + // Result.Value contains []any - type depends on operation + // For find(), each element is bson.D + for _, val := range result.Value { + doc, ok := val.(bson.D) + if !ok { + log.Printf("unexpected type %T\n", val) + continue + } + fmt.Printf("%+v\n", doc) } + + // For countDocuments(), single element is int64 + countResult, err := gc.Execute(ctx, "mydb", `db.users.countDocuments({})`) + if err != nil { + log.Fatal(err) + } + count, ok := countResult.Value[0].(int64) + if !ok { + log.Fatalf("unexpected type %T\n", countResult.Value[0]) + } + fmt.Printf("Count: %d\n", count) } ``` @@ -74,16 +92,19 @@ result, err := gc.Execute(ctx, "mydb", `db.users.find()`, gomongo.WithMaxRows(10 ## Output Format -Results are returned in Extended JSON (Relaxed) format: +Results are returned as native Go types in `Result.Value` (a `[]any` slice). Use `Result.Operation` to determine the expected type: -```json -{ - "_id": {"$oid": "507f1f77bcf86cd799439011"}, - "name": "Alice", - "age": 30, - "createdAt": {"$date": "2024-01-01T00:00:00Z"} -} -``` +| Operation | Value Type | +|-----------|-----------| +| `OpFind`, `OpAggregate`, `OpGetIndexes`, `OpGetCollectionInfos` | Each element is `bson.D` | +| `OpFindOne`, `OpFindOneAnd*` | 0 or 1 element of `bson.D` | +| `OpCountDocuments`, `OpEstimatedDocumentCount` | Single `int64` | +| `OpDistinct` | Elements are the distinct values | +| `OpShowDatabases`, `OpShowCollections`, `OpGetCollectionNames` | Each element is `string` | +| `OpInsert*`, `OpUpdate*`, `OpReplace*`, `OpDelete*` | Single `bson.D` with result | +| `OpCreateIndex` | Single `string` (index name) | +| `OpDropIndex`, `OpDropIndexes`, `OpCreateCollection`, `OpDropDatabase`, `OpRenameCollection` | Single `bson.D` with `{ok: 1}` | +| `OpDrop` | Single `bool` (true) | ## Command Reference @@ -191,25 +212,25 @@ Results are returned in Extended JSON (Relaxed) format: *Note: `wtimeout` is parsed but ignored as it's not supported in MongoDB Go driver v2. -### Milestone 3: Administrative Operations (Planned) +### Milestone 3: Administrative Operations #### Index Management | Command | Syntax | Status | |---------|--------|--------| -| db.collection.createIndex() | `createIndex(keys)` | Not yet supported | +| db.collection.createIndex() | `createIndex(keys, options)` | Supported | | db.collection.createIndexes() | `createIndexes(indexSpecs)` | Not yet supported | -| db.collection.dropIndex() | `dropIndex(index)` | Not yet supported | -| db.collection.dropIndexes() | `dropIndexes()` | Not yet supported | +| db.collection.dropIndex() | `dropIndex(index)` | Supported | +| db.collection.dropIndexes() | `dropIndexes()` | Supported | #### Collection Management | Command | Syntax | Status | |---------|--------|--------| -| db.createCollection() | `db.createCollection(name)` | Not yet supported | -| db.collection.drop() | `drop()` | Not yet supported | -| db.collection.renameCollection() | `renameCollection(newName)` | Not yet supported | -| db.dropDatabase() | `db.dropDatabase()` | Not yet supported | +| db.createCollection() | `db.createCollection(name, options)` | Supported | +| db.collection.drop() | `drop()` | Supported | +| db.collection.renameCollection() | `renameCollection(newName, dropTarget)` | Supported | +| db.dropDatabase() | `db.dropDatabase()` | Supported | #### Database Information diff --git a/admin_test.go b/admin_test.go index c7206b6..40e2592 100644 --- a/admin_test.go +++ b/admin_test.go @@ -12,22 +12,36 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" ) -// containsCollectionName checks if the rows contain a JSON object with the given collection name. -func containsCollectionName(rows []string, name string) bool { - for _, row := range rows { - var doc bson.M - if err := bson.UnmarshalExtJSON([]byte(row), false, &doc); err == nil { - if doc["name"] == name { - return true +// containsCollectionName checks if the values contain the given collection name. +// Values can be strings (from show collections) or bson.D documents. +func containsCollectionName(values []any, name string) bool { + for _, v := range values { + // show collections returns strings + if s, ok := v.(string); ok && s == name { + return true + } + // getCollectionInfos returns bson.D + if doc, ok := v.(bson.D); ok { + for _, elem := range doc { + if elem.Key == "name" && elem.Value == name { + return true + } } } } return false } -// containsDatabaseName checks if the rows contain a JSON object with the given database name. -func containsDatabaseName(rows []string, name string) bool { - return containsCollectionName(rows, name) // Same logic +// containsDatabaseName checks if the values contain the given database name. +// Values are strings from show dbs. +func containsDatabaseName(values []any, name string) bool { + for _, v := range values { + // show dbs returns strings + if s, ok := v.(string); ok && s == name { + return true + } + } + return false } func TestCreateIndex(t *testing.T) { @@ -47,8 +61,9 @@ func TestCreateIndex(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.users.createIndex({ name: 1 })`) require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - require.Contains(t, result.Rows[0], "name") + require.Equal(t, 1, len(result.Value)) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, "name") }) } @@ -69,8 +84,11 @@ func TestCreateIndexWithOptions(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.users.createIndex({ email: 1 }, { name: "email_unique_idx" })`) require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) - require.Equal(t, "email_unique_idx", result.Rows[0]) + require.Equal(t, 1, len(result.Value)) + // The returned value is the index name as string + indexName, ok := result.Value[0].(string) + require.True(t, ok) + require.Equal(t, "email_unique_idx", indexName) }) } @@ -96,7 +114,8 @@ func TestDropIndex(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.users.dropIndex("name_idx")`) require.NoError(t, err) require.NotNil(t, result) - require.Contains(t, result.Rows[0], `"ok": 1`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"ok": 1`) }) } @@ -124,12 +143,13 @@ func TestDropIndexes(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.users.dropIndexes()`) require.NoError(t, err) require.NotNil(t, result) - require.Contains(t, result.Rows[0], `"ok": 1`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"ok": 1`) // Verify only _id index remains idxResult, err := gc.Execute(ctx, dbName, `db.users.getIndexes()`) require.NoError(t, err) - require.Equal(t, 1, idxResult.RowCount) // Only _id index + require.Equal(t, 1, len(idxResult.Value)) // Only _id index }) } @@ -149,17 +169,20 @@ func TestDropCollection(t *testing.T) { // Verify collection exists result, err := gc.Execute(ctx, dbName, `show collections`) require.NoError(t, err) - require.Equal(t, 1, result.RowCount) + require.Equal(t, 1, len(result.Value)) // Drop the collection result, err = gc.Execute(ctx, dbName, `db.tobedeleted.drop()`) require.NoError(t, err) - require.Equal(t, "true", result.Rows[0]) + // The returned value is a boolean + dropped, ok := result.Value[0].(bool) + require.True(t, ok) + require.True(t, dropped) // Verify collection is gone result, err = gc.Execute(ctx, dbName, `show collections`) require.NoError(t, err) - require.Equal(t, 0, result.RowCount) + require.Equal(t, 0, len(result.Value)) }) } @@ -174,13 +197,14 @@ func TestCreateCollection(t *testing.T) { // Create a new collection result, err := gc.Execute(ctx, dbName, `db.createCollection("newcollection")`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"ok": 1`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"ok": 1`) // Verify collection exists collResult, err := gc.Execute(ctx, dbName, `show collections`) require.NoError(t, err) - require.Equal(t, 1, collResult.RowCount) - require.True(t, containsCollectionName(collResult.Rows, "newcollection"), "expected 'newcollection' in result") + require.Equal(t, 1, len(collResult.Value)) + require.True(t, containsCollectionName(collResult.Value, "newcollection"), "expected 'newcollection' in result") }) } @@ -200,13 +224,14 @@ func TestDropDatabase(t *testing.T) { // Verify database exists result, err := gc.Execute(ctx, dbName, `show dbs`) require.NoError(t, err) - require.True(t, containsDatabaseName(result.Rows, dbName), "database should exist before drop") + require.True(t, containsDatabaseName(result.Value, dbName), "database should exist before drop") // Drop the database result, err = gc.Execute(ctx, dbName, `db.dropDatabase()`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"ok": 1`) - require.Contains(t, result.Rows[0], fmt.Sprintf(`"dropped": "%s"`, dbName)) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"ok": 1`) + require.Contains(t, row, fmt.Sprintf(`"dropped": "%s"`, dbName)) }) } @@ -227,19 +252,21 @@ func TestRenameCollection(t *testing.T) { // Rename the collection result, err := gc.Execute(ctx, dbName, `db.oldname.renameCollection("newname")`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"ok": 1`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"ok": 1`) // Verify old collection is gone and new one exists collResult, err := gc.Execute(ctx, dbName, `show collections`) require.NoError(t, err) - require.Equal(t, 1, collResult.RowCount) - require.True(t, containsCollectionName(collResult.Rows, "newname"), "expected 'newname' in result") + require.Equal(t, 1, len(collResult.Value)) + require.True(t, containsCollectionName(collResult.Value, "newname"), "expected 'newname' in result") // Verify data is preserved findResult, err := gc.Execute(ctx, dbName, `db.newname.find()`) require.NoError(t, err) - require.Equal(t, 1, findResult.RowCount) - require.Contains(t, findResult.Rows[0], `"x": 1`) + require.Equal(t, 1, len(findResult.Value)) + findRow := valueToJSON(findResult.Value[0]) + require.Contains(t, findRow, `"x": 1`) }) } @@ -263,19 +290,21 @@ func TestRenameCollectionWithDropTarget(t *testing.T) { // Rename with dropTarget = true result, err := gc.Execute(ctx, dbName, `db.source.renameCollection("target", true)`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"ok": 1`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"ok": 1`) // Verify only target exists with source data collResult, err := gc.Execute(ctx, dbName, `show collections`) require.NoError(t, err) - require.Equal(t, 1, collResult.RowCount) - require.True(t, containsCollectionName(collResult.Rows, "target"), "expected 'target' in result") + require.Equal(t, 1, len(collResult.Value)) + require.True(t, containsCollectionName(collResult.Value, "target"), "expected 'target' in result") // Verify it has source data, not old target data findResult, err := gc.Execute(ctx, dbName, `db.target.find()`) require.NoError(t, err) - require.Equal(t, 1, findResult.RowCount) - require.Contains(t, findResult.Rows[0], `"x": 1`) + require.Equal(t, 1, len(findResult.Value)) + findRow := valueToJSON(findResult.Value[0]) + require.Contains(t, findRow, `"x": 1`) }) } @@ -291,13 +320,14 @@ func TestCreateCollectionWithOptions(t *testing.T) { // Create a capped collection result, err := gc.Execute(ctx, dbName, `db.createCollection("cappedcoll", { capped: true, size: 1048576 })`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"ok": 1`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"ok": 1`) // Verify collection exists collResult, err := gc.Execute(ctx, dbName, `show collections`) require.NoError(t, err) - require.Equal(t, 1, collResult.RowCount) - require.True(t, containsCollectionName(collResult.Rows, "cappedcoll"), "expected 'cappedcoll' in result") + require.Equal(t, 1, len(collResult.Value)) + require.True(t, containsCollectionName(collResult.Value, "cappedcoll"), "expected 'cappedcoll' in result") }) } @@ -313,7 +343,8 @@ func TestCreateCollectionWithMaxDocuments(t *testing.T) { // Create a capped collection with max documents result, err := gc.Execute(ctx, dbName, `db.createCollection("cappedmax", { capped: true, size: 1048576, max: 100 })`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"ok": 1`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"ok": 1`) }) } @@ -342,12 +373,13 @@ func TestDropIndexesWithArray(t *testing.T) { // Drop two indexes using an array result, err := gc.Execute(ctx, dbName, `db.users.dropIndexes(["name_idx", "email_idx"])`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"ok": 1`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"ok": 1`) // Verify only _id and age_idx remain idxResult, err := gc.Execute(ctx, dbName, `db.users.getIndexes()`) require.NoError(t, err) - require.Equal(t, 2, idxResult.RowCount) // _id + age_idx + require.Equal(t, 2, len(idxResult.Value)) // _id + age_idx }) } @@ -366,7 +398,9 @@ func TestCreateIndexWithUniqueOption(t *testing.T) { // Create a unique index result, err := gc.Execute(ctx, dbName, `db.users.createIndex({ email: 1 }, { unique: true, name: "email_unique" })`) require.NoError(t, err) - require.Equal(t, "email_unique", result.Rows[0]) + indexName, ok := result.Value[0].(string) + require.True(t, ok) + require.Equal(t, "email_unique", indexName) // Try to insert a duplicate - should fail _, err = gc.Execute(ctx, dbName, `db.users.insertOne({ email: "alice@example.com" })`) @@ -393,7 +427,9 @@ func TestCreateIndexWithSparseOption(t *testing.T) { // Create a sparse index result, err := gc.Execute(ctx, dbName, `db.users.createIndex({ email: 1 }, { sparse: true, name: "email_sparse" })`) require.NoError(t, err) - require.Equal(t, "email_sparse", result.Rows[0]) + indexName, ok := result.Value[0].(string) + require.True(t, ok) + require.Equal(t, "email_sparse", indexName) // Documents without the indexed field should still be insertable _, err = gc.Execute(ctx, dbName, `db.users.insertOne({ name: "bob" })`) @@ -416,7 +452,9 @@ func TestCreateIndexWithTTLOption(t *testing.T) { // Create a TTL index result, err := gc.Execute(ctx, dbName, `db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600, name: "session_ttl" })`) require.NoError(t, err) - require.Equal(t, "session_ttl", result.Rows[0]) + indexName, ok := result.Value[0].(string) + require.True(t, ok) + require.Equal(t, "session_ttl", indexName) }) } @@ -435,6 +473,7 @@ func TestCreateIndexWithBackgroundOption(t *testing.T) { // Create an index with background option (deprecated but should be accepted) result, err := gc.Execute(ctx, dbName, `db.users.createIndex({ name: 1 }, { background: true })`) require.NoError(t, err) - require.Contains(t, result.Rows[0], "name") + row := valueToJSON(result.Value[0]) + require.Contains(t, row, "name") }) } diff --git a/client.go b/client.go index bda4599..53b8f88 100644 --- a/client.go +++ b/client.go @@ -3,6 +3,7 @@ package gomongo import ( "context" + "github.com/bytebase/gomongo/types" "go.mongodb.org/mongo-driver/v2/mongo" ) @@ -17,10 +18,21 @@ func NewClient(client *mongo.Client) *Client { } // Result represents query execution results. +// +// The Value slice contains the operation's return data. The element type varies by operation: +// +// - OpFind, OpAggregate, OpGetIndexes, OpGetCollectionInfos: each element is bson.D (document) +// - OpFindOne, OpFindOneAndUpdate, OpFindOneAndReplace, OpFindOneAndDelete: 0 or 1 element of bson.D +// - OpCountDocuments, OpEstimatedDocumentCount: single element of int64 +// - OpDistinct: elements are the distinct values (various types) +// - OpShowDatabases, OpShowCollections, OpGetCollectionNames: each element is string +// - OpInsertOne, OpInsertMany, OpUpdateOne, OpUpdateMany, OpReplaceOne, OpDeleteOne, OpDeleteMany: single bson.D with operation result +// - OpCreateIndex: single element of string (index name) +// - OpDropIndex, OpDropIndexes, OpCreateCollection, OpDropDatabase, OpRenameCollection: single bson.D with {ok: 1} +// - OpDrop: single element of bool (true) type Result struct { - Rows []string - RowCount int - Statement string + Operation types.OperationType + Value []any } // executeConfig holds configuration for Execute. @@ -41,7 +53,8 @@ func WithMaxRows(n int64) ExecuteOption { } // Execute parses and executes a MongoDB shell statement. -// Returns results as Extended JSON (Relaxed) strings. +// Returns a Result containing the operation type and native Go values. +// Use Result.Operation to determine the expected type of elements in Result.Value. func (c *Client) Execute(ctx context.Context, database, statement string, opts ...ExecuteOption) (*Result, error) { cfg := &executeConfig{} for _, opt := range opts { diff --git a/collection_test.go b/collection_test.go index d529b28..0aaf4ba 100644 --- a/collection_test.go +++ b/collection_test.go @@ -14,6 +14,24 @@ import ( "go.mongodb.org/mongo-driver/v2/mongo" ) +// valueToJSON converts a result value to a JSON string for assertion. +func valueToJSON(v any) string { + bytes, err := bson.MarshalExtJSONIndent(v, false, false, "", " ") + if err != nil { + return fmt.Sprintf("%v", v) + } + return string(bytes) +} + +// valuesToStrings converts result values to JSON strings. +func valuesToStrings(values []any) []string { + result := make([]string, len(values)) + for i, v := range values { + result[i] = valueToJSON(v) + } + return result +} + func TestFindEmptyCollection(t *testing.T) { testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { dbName := fmt.Sprintf("testdb_find_empty_%s", db.Name) @@ -25,8 +43,8 @@ func TestFindEmptyCollection(t *testing.T) { 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) + require.Equal(t, 0, len(result.Value)) + require.Empty(t, result.Value) }) } @@ -49,11 +67,12 @@ func TestFindWithDocuments(t *testing.T) { 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) + require.Equal(t, 2, len(result.Value)) + require.Len(t, result.Value, 2) // Verify JSON format - for _, row := range result.Rows { + rows := valuesToStrings(result.Value) + for _, row := range rows { require.Contains(t, row, "name") require.Contains(t, row, "age") require.Contains(t, row, "_id") @@ -76,7 +95,7 @@ func TestFindWithEmptyFilter(t *testing.T) { result, err := gc.Execute(ctx, dbName, "db.items.find({})") require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) + require.Equal(t, 1, len(result.Value)) }) } @@ -91,8 +110,8 @@ func TestFindOneEmptyCollection(t *testing.T) { 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) + require.Equal(t, 0, len(result.Value)) + require.Empty(t, result.Value) }) } @@ -114,11 +133,12 @@ func TestFindOneWithDocuments(t *testing.T) { 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.Equal(t, 1, len(result.Value)) + require.Len(t, result.Value, 1) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, "name") + require.Contains(t, row, "age") + require.Contains(t, row, "_id") }) } @@ -183,12 +203,12 @@ func TestFindOneWithFilter(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) if tc.expectMatch { - require.Equal(t, 1, result.RowCount) + require.Equal(t, 1, len(result.Value)) if tc.checkResult != nil { - tc.checkResult(t, result.Rows[0]) + tc.checkResult(t, valueToJSON(result.Value[0])) } } else { - require.Equal(t, 0, result.RowCount) + require.Equal(t, 0, len(result.Value)) } }) } @@ -254,8 +274,8 @@ func TestFindOneWithOptions(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]) + require.Equal(t, 1, len(result.Value)) + tc.checkResult(t, valueToJSON(result.Value[0])) }) } }) @@ -340,9 +360,9 @@ func TestFindWithFilter(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) + require.Equal(t, tc.expectedCount, len(result.Value)) + if tc.checkResult != nil && len(result.Value) > 0 { + tc.checkResult(t, valuesToStrings(result.Value)) } }) } @@ -480,9 +500,9 @@ func TestFindWithCursorModifications(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) + require.Equal(t, tc.expectedCount, len(result.Value)) + if tc.checkResult != nil && len(result.Value) > 0 { + tc.checkResult(t, valuesToStrings(result.Value)) } }) } @@ -509,10 +529,11 @@ func TestFindWithProjectionArg(t *testing.T) { // 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) + require.Equal(t, 2, len(result.Value)) // Verify only 'name' field is returned - for _, row := range result.Rows { + rows := valuesToStrings(result.Value) + for _, row := range rows { require.Contains(t, row, "name") require.NotContains(t, row, "age") require.NotContains(t, row, "city") @@ -545,7 +566,7 @@ func TestFindWithHintOption(t *testing.T) { // 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) + require.Equal(t, 2, len(result.Value)) }) } @@ -583,7 +604,7 @@ func TestFindWithMaxMinOptions(t *testing.T) { 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) + require.Equal(t, 2, len(result.Value)) }) } @@ -606,7 +627,7 @@ func TestFindWithMaxTimeMSOption(t *testing.T) { // 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) + require.Equal(t, 2, len(result.Value)) }) } @@ -629,9 +650,10 @@ func TestFindOneWithProjectionAndOptions(t *testing.T) { // 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") + require.Equal(t, 1, len(result.Value)) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, "name") + require.NotContains(t, row, "age") }) } @@ -660,7 +682,7 @@ func TestFindOneWithHintOption(t *testing.T) { // 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) + require.Equal(t, 1, len(result.Value)) }) } @@ -683,7 +705,7 @@ func TestFindOneWithMaxTimeMSOption(t *testing.T) { // 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) + require.Equal(t, 1, len(result.Value)) }) } @@ -797,9 +819,9 @@ func TestAggregateBasic(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) + require.Equal(t, tc.expectedCount, len(result.Value)) + if tc.checkResult != nil && len(result.Value) > 0 { + tc.checkResult(t, valuesToStrings(result.Value)) } }) } @@ -877,9 +899,9 @@ func TestAggregateGroup(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) + require.Equal(t, tc.expectedCount, len(result.Value)) + if tc.checkResult != nil && len(result.Value) > 0 { + tc.checkResult(t, valuesToStrings(result.Value)) } }) } @@ -997,19 +1019,20 @@ func TestAggregateFilteredSubset(t *testing.T) { result, err := gc.Execute(ctx, dbName, statement) require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, 3, result.RowCount) + require.Equal(t, 3, len(result.Value)) + rows := valuesToStrings(result.Value) // Carl (1998) should be first (youngest) - require.Contains(t, result.Rows[0], `"Carl"`) + require.Contains(t, rows[0], `"Carl"`) // Olive (1985) should be second - require.Contains(t, result.Rows[1], `"Olive"`) + require.Contains(t, rows[1], `"Olive"`) // Elise (1972) should be third - require.Contains(t, result.Rows[2], `"Elise"`) + require.Contains(t, 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"`) + require.NotContains(t, rows[0], `"_id"`) + require.NotContains(t, rows[0], `"vocation"`) + require.NotContains(t, rows[0], `"address"`) }) } @@ -1097,16 +1120,17 @@ func TestAggregateGroupAndTotal(t *testing.T) { result, err := gc.Execute(ctx, dbName, statement) require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, 3, result.RowCount) + require.Equal(t, 3, len(result.Value)) + rows := valuesToStrings(result.Value) // oranieri should be first (earliest order in 2020: Jan 1) - require.Contains(t, result.Rows[0], `"oranieri@warmmail.com"`) + require.Contains(t, 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"`) + require.Contains(t, rows[0], `"customer_id"`) + require.Contains(t, rows[0], `"total_value"`) + require.Contains(t, rows[0], `"total_orders"`) + require.NotContains(t, rows[0], `"_id"`) }) } @@ -1172,13 +1196,14 @@ func TestAggregateUnwindArrays(t *testing.T) { 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) + require.Equal(t, 4, len(result.Value)) + rows := valuesToStrings(result.Value) // 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"`) + require.Contains(t, rows[0], `"product_id"`) + require.Contains(t, rows[0], `"product"`) + require.Contains(t, rows[0], `"total_value"`) + require.Contains(t, rows[0], `"quantity"`) }) } @@ -1278,13 +1303,14 @@ func TestAggregateOneToOneJoin(t *testing.T) { 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 + require.Equal(t, 3, len(result.Value)) // Only 2020 orders: elise, oranieri, jjones + rows := valuesToStrings(result.Value) // 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"`) + require.Contains(t, rows[0], `"product_name"`) + require.Contains(t, rows[0], `"product_category"`) + require.NotContains(t, rows[0], `"_id"`) + require.NotContains(t, rows[0], `"product_mapping"`) }) } @@ -1412,13 +1438,14 @@ func TestAggregateMultiFieldJoin(t *testing.T) { 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) + require.Equal(t, 2, len(result.Value)) + rows := valuesToStrings(result.Value) // 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"`) + require.Contains(t, rows[0], `"orders"`) + require.Contains(t, rows[0], `"name"`) + require.Contains(t, rows[0], `"variation"`) + require.NotContains(t, rows[0], `"_id"`) }) } @@ -1441,7 +1468,7 @@ func TestAggregateWithOptions(t *testing.T) { // 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) + require.Equal(t, 2, len(result.Value)) }) } @@ -1470,12 +1497,12 @@ func TestAggregateWithHintOption(t *testing.T) { // 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) + require.Equal(t, 2, len(result.Value)) // 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) + require.Equal(t, 2, len(result.Value)) }) } @@ -1512,11 +1539,12 @@ func TestGetIndexes(t *testing.T) { result, err := gc.Execute(ctx, dbName, "db.users.getIndexes()") require.NoError(t, err) require.NotNil(t, result) - require.GreaterOrEqual(t, result.RowCount, 1) + require.GreaterOrEqual(t, len(result.Value), 1) // Verify the _id index exists found := false - for _, row := range result.Rows { + rows := valuesToStrings(result.Value) + for _, row := range rows { if strings.Contains(row, `"name": "_id_"`) { found = true break @@ -1549,12 +1577,13 @@ func TestGetIndexesWithCustomIndex(t *testing.T) { 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 + require.Equal(t, 2, len(result.Value)) // _id index + email index // Verify both indexes exist hasIdIndex := false hasEmailIndex := false - for _, row := range result.Rows { + rows := valuesToStrings(result.Value) + for _, row := range rows { if strings.Contains(row, `"name": "_id_"`) { hasIdIndex = true } @@ -1585,10 +1614,11 @@ func TestGetIndexesBracketNotation(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db["user-logs"].getIndexes()`) require.NoError(t, err) require.NotNil(t, result) - require.GreaterOrEqual(t, result.RowCount, 1) + require.GreaterOrEqual(t, len(result.Value), 1) // Verify the _id index exists - require.Contains(t, result.Rows[0], `"name": "_id_"`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"name": "_id_"`) }) } @@ -1614,8 +1644,10 @@ func TestCountDocuments(t *testing.T) { 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.Equal(t, 1, len(result.Value)) + count, ok := result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(3), count) }) } @@ -1642,13 +1674,17 @@ func TestCountDocumentsWithFilter(t *testing.T) { 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]) + require.Equal(t, 1, len(result.Value)) + count, ok := result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(3), count) // 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]) + count, ok = result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(2), count) }) } @@ -1665,8 +1701,10 @@ func TestCountDocumentsEmptyCollection(t *testing.T) { 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]) + require.Equal(t, 1, len(result.Value)) + count, ok := result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(0), count) }) } @@ -1691,7 +1729,9 @@ func TestCountDocumentsWithEmptyFilter(t *testing.T) { result, err := gc.Execute(ctx, dbName, "db.items.countDocuments({})") require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, "2", result.Rows[0]) + count, ok := result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(2), count) }) } @@ -1718,17 +1758,23 @@ func TestCountDocumentsWithOptions(t *testing.T) { // 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]) + count, ok := result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(3), count) // 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]) + count, ok = result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(3), count) // 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]) + count, ok = result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(2), count) }) } @@ -1759,12 +1805,16 @@ func TestCountDocumentsWithHint(t *testing.T) { // 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]) + count, ok := result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(2), count) // 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]) + count, ok = result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(2), count) }) } @@ -1786,7 +1836,9 @@ func TestCountDocumentsMaxTimeMS(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.users.countDocuments({}, { maxTimeMS: 5000 })`) require.NoError(t, err) - require.Equal(t, "2", result.Rows[0]) + count, ok := result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(2), count) }) } @@ -1812,8 +1864,10 @@ func TestEstimatedDocumentCount(t *testing.T) { 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.Equal(t, 1, len(result.Value)) + count, ok := result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(3), count) }) } @@ -1830,8 +1884,10 @@ func TestEstimatedDocumentCountEmptyCollection(t *testing.T) { 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]) + require.Equal(t, 1, len(result.Value)) + count, ok := result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(0), count) }) } @@ -1856,7 +1912,9 @@ func TestEstimatedDocumentCountWithEmptyOptions(t *testing.T) { result, err := gc.Execute(ctx, dbName, "db.items.estimatedDocumentCount({})") require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, "2", result.Rows[0]) + count, ok := result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(2), count) }) } @@ -1878,8 +1936,10 @@ func TestEstimatedDocumentCountMaxTimeMS(t *testing.T) { 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]) + require.Equal(t, 1, len(result.Value)) + count, ok := result.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(2), count) }) } @@ -1906,15 +1966,17 @@ func TestDistinct(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.users.distinct("status")`) require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, 2, result.RowCount) + require.Equal(t, 2, len(result.Value)) // Verify both values are present values := make(map[string]bool) - for _, row := range result.Rows { - values[row] = true + for _, v := range result.Value { + if s, ok := v.(string); ok { + values[s] = true + } } - require.True(t, values[`"active"`]) - require.True(t, values[`"inactive"`]) + require.True(t, values["active"]) + require.True(t, values["inactive"]) }) } @@ -1942,17 +2004,19 @@ func TestDistinctWithFilter(t *testing.T) { 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) + require.Equal(t, 2, len(result.Value)) // Verify only electronics brands are returned values := make(map[string]bool) - for _, row := range result.Rows { - values[row] = true + for _, v := range result.Value { + if s, ok := v.(string); ok { + values[s] = true + } } - require.True(t, values[`"Apple"`]) - require.True(t, values[`"Samsung"`]) - require.False(t, values[`"Nike"`]) - require.False(t, values[`"Adidas"`]) + require.True(t, values["Apple"]) + require.True(t, values["Samsung"]) + require.False(t, values["Nike"]) + require.False(t, values["Adidas"]) }) } @@ -1969,8 +2033,8 @@ func TestDistinctEmptyCollection(t *testing.T) { 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) + require.Equal(t, 0, len(result.Value)) + require.Empty(t, result.Value) }) } @@ -1997,7 +2061,7 @@ func TestDistinctBracketNotation(t *testing.T) { 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.Equal(t, 3, len(result.Value)) }) } @@ -2025,7 +2089,7 @@ func TestDistinctNumericValues(t *testing.T) { 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.Equal(t, 3, len(result.Value)) // 100, 85, 90 }) } @@ -2048,7 +2112,7 @@ func TestDistinctMaxTimeMS(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.users.distinct("city", {}, { maxTimeMS: 5000 })`) require.NoError(t, err) - require.Equal(t, 2, result.RowCount) + require.Equal(t, 2, len(result.Value)) }) } @@ -2096,7 +2160,7 @@ func TestCursorHintMethod(t *testing.T) { // 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) + require.Equal(t, 2, len(result.Value)) }) } @@ -2123,7 +2187,7 @@ func TestCursorHintMethodWithDocument(t *testing.T) { // 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) + require.Equal(t, 1, len(result.Value)) }) } @@ -2158,8 +2222,9 @@ func TestCursorMaxMethod(t *testing.T) { // 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"`) + require.Equal(t, 1, len(result.Value)) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"Bob"`) }) } @@ -2194,344 +2259,6 @@ func TestCursorMinMethod(t *testing.T) { // 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) { - 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") - } - - dbName := fmt.Sprintf("testdb_cursor_minmax_%s", db.Name) - defer testutil.CleanupDatabase(t, db.Client, dbName) - - ctx := context.Background() - - gc := gomongo.NewClient(db.Client) - - 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) - - // 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) { - 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() - - // 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(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) - - // 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) { - 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() - - // 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(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) - }) -} - -func TestWithMaxRowsTakesPrecedenceOverLargerLimit(t *testing.T) { - 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() - - // 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(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) - }) -} - -func TestExecuteBackwardCompatibility(t *testing.T) { - 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() - - 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(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) - }) -} - -func TestCountDocumentsWithMaxRows(t *testing.T) { - 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} + require.Equal(t, 2, len(result.Value)) }) } diff --git a/database_test.go b/database_test.go index cb1a001..9a39fb4 100644 --- a/database_test.go +++ b/database_test.go @@ -37,19 +37,17 @@ func TestShowDatabases(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) + require.GreaterOrEqual(t, len(result.Value), 1) - // Check that dbName is in the result (as JSON object with "name" field) + // Check that dbName is in the result (values are strings) found := false - for _, row := range result.Rows { - var doc bson.M - err := bson.UnmarshalExtJSON([]byte(row), false, &doc) - if err == nil && doc["name"] == dbName { + for _, v := range result.Value { + if name, ok := v.(string); ok && name == dbName { found = true break } } - require.True(t, found, "expected database '%s' in result, got: %v", dbName, result.Rows) + require.True(t, found, "expected database '%s' in result", dbName) }) } }) @@ -73,15 +71,12 @@ func TestShowCollections(t *testing.T) { result, err := gc.Execute(ctx, dbName, "show collections") require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, 2, result.RowCount) + require.Equal(t, 2, len(result.Value)) - // Check that both collections are in the result (as JSON objects with "name" field) + // Check that both collections are in the result (values are strings) collectionSet := make(map[string]bool) - for _, row := range result.Rows { - var doc bson.M - err := bson.UnmarshalExtJSON([]byte(row), false, &doc) - require.NoError(t, err, "row should be valid JSON: %s", row) - if name, ok := doc["name"].(string); ok { + for _, v := range result.Value { + if name, ok := v.(string); ok { collectionSet[name] = true } } @@ -108,15 +103,12 @@ func TestGetCollectionNames(t *testing.T) { result, err := gc.Execute(ctx, dbName, "db.getCollectionNames()") require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, 2, result.RowCount) + require.Equal(t, 2, len(result.Value)) - // Check that both collections are in the result (as JSON objects with "name" field) + // Check that both collections are in the result (values are strings) collectionSet := make(map[string]bool) - for _, row := range result.Rows { - var doc bson.M - err := bson.UnmarshalExtJSON([]byte(row), false, &doc) - require.NoError(t, err, "row should be valid JSON: %s", row) - if name, ok := doc["name"].(string); ok { + for _, v := range result.Value { + if name, ok := v.(string); ok { collectionSet[name] = true } } @@ -144,10 +136,11 @@ func TestGetCollectionInfos(t *testing.T) { result, err := gc.Execute(ctx, dbName, "db.getCollectionInfos()") require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, 2, result.RowCount) + require.Equal(t, 2, len(result.Value)) // Verify that results contain collection info structure - for _, row := range result.Rows { + rows := valuesToStrings(result.Value) + for _, row := range rows { require.Contains(t, row, `"name"`) require.Contains(t, row, `"type"`) } @@ -173,11 +166,12 @@ func TestGetCollectionInfosWithFilter(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({ name: "users" })`) require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, 1, result.RowCount) + require.Equal(t, 1, len(result.Value)) // Verify that the returned collection is "users" - require.Contains(t, result.Rows[0], `"name": "users"`) - require.Contains(t, result.Rows[0], `"type": "collection"`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"name": "users"`) + require.Contains(t, row, `"type": "collection"`) }) } @@ -198,8 +192,8 @@ func TestGetCollectionInfosEmptyResult(t *testing.T) { 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) + require.Equal(t, 0, len(result.Value)) + require.Empty(t, result.Value) }) } @@ -218,10 +212,11 @@ func TestGetCollectionInfosNameOnly(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({}, { nameOnly: true })`) require.NoError(t, err) - require.GreaterOrEqual(t, result.RowCount, 1) + require.GreaterOrEqual(t, len(result.Value), 1) // With nameOnly: true, the result should contain "name" field - require.Contains(t, result.Rows[0], `"name"`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"name"`) }) } @@ -240,7 +235,7 @@ func TestGetCollectionInfosAuthorizedCollections(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.getCollectionInfos({}, { authorizedCollections: true })`) require.NoError(t, err) - require.GreaterOrEqual(t, result.RowCount, 1) + require.GreaterOrEqual(t, len(result.Value), 1) }) } diff --git a/executor.go b/executor.go index a78b50c..83361bb 100644 --- a/executor.go +++ b/executor.go @@ -39,8 +39,7 @@ func execute(ctx context.Context, client *mongo.Client, database, statement stri } return &Result{ - Rows: result.Rows, - RowCount: result.RowCount, - Statement: result.Statement, + Operation: result.Operation, + Value: result.Value, }, nil } diff --git a/internal/executor/admin.go b/internal/executor/admin.go index 0ef7efc..5d35853 100644 --- a/internal/executor/admin.go +++ b/internal/executor/admin.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/bytebase/gomongo/internal/translator" + "github.com/bytebase/gomongo/types" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" @@ -49,8 +50,8 @@ func executeCreateIndex(ctx context.Context, client *mongo.Client, database stri } return &Result{ - Rows: []string{indexName}, - RowCount: 1, + Operation: types.OpCreateIndex, + Value: []any{indexName}, }, nil } @@ -94,15 +95,11 @@ func executeDropIndex(ctx context.Context, client *mongo.Client, database string return nil, fmt.Errorf("dropIndex failed: %w", err) } - response := bson.M{"ok": 1} - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } + response := bson.D{{Key: "ok", Value: int32(1)}} return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpDropIndex, + Value: []any{response}, }, nil } @@ -177,15 +174,11 @@ func executeDropIndexes(ctx context.Context, client *mongo.Client, database stri return nil, fmt.Errorf("dropIndexes failed: %w", err) } - response := bson.M{"ok": 1} - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } + response := bson.D{{Key: "ok", Value: int32(1)}} return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpDropIndexes, + Value: []any{response}, }, nil } @@ -199,8 +192,8 @@ func executeDrop(ctx context.Context, client *mongo.Client, database string, op } return &Result{ - Rows: []string{"true"}, - RowCount: 1, + Operation: types.OpDrop, + Value: []any{true}, }, nil } @@ -234,15 +227,11 @@ func executeCreateCollection(ctx context.Context, client *mongo.Client, database return nil, fmt.Errorf("createCollection failed: %w", err) } - response := bson.M{"ok": 1} - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } + response := bson.D{{Key: "ok", Value: int32(1)}} return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpCreateCollection, + Value: []any{response}, }, nil } @@ -253,15 +242,14 @@ func executeDropDatabase(ctx context.Context, client *mongo.Client, database str return nil, fmt.Errorf("dropDatabase failed: %w", err) } - response := bson.M{"ok": 1, "dropped": database} - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) + response := bson.D{ + {Key: "ok", Value: int32(1)}, + {Key: "dropped", Value: database}, } return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpDropDatabase, + Value: []any{response}, }, nil } @@ -282,14 +270,10 @@ func executeRenameCollection(ctx context.Context, client *mongo.Client, database return nil, fmt.Errorf("renameCollection failed: %w", err) } - response := bson.M{"ok": 1} - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } + response := bson.D{{Key: "ok", Value: int32(1)}} return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpRenameCollection, + Value: []any{response}, }, nil } diff --git a/internal/executor/collection.go b/internal/executor/collection.go index 0f6bb12..35f1f43 100644 --- a/internal/executor/collection.go +++ b/internal/executor/collection.go @@ -2,11 +2,11 @@ package executor import ( "context" - "encoding/json" "fmt" "time" "github.com/bytebase/gomongo/internal/translator" + "github.com/bytebase/gomongo/types" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" @@ -81,19 +81,13 @@ func executeFind(ctx context.Context, client *mongo.Client, database string, op } defer func() { _ = cursor.Close(ctx) }() - var rows []string + var values []any for cursor.Next(ctx) { - var doc bson.M + var doc bson.D if err := cursor.Decode(&doc); err != nil { return nil, fmt.Errorf("decode failed: %w", err) } - - // Marshal to Extended JSON (Relaxed) - jsonBytes, err := bson.MarshalExtJSONIndent(doc, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } - rows = append(rows, string(jsonBytes)) + values = append(values, doc) } if err := cursor.Err(); err != nil { @@ -101,8 +95,8 @@ func executeFind(ctx context.Context, client *mongo.Client, database string, op } return &Result{ - Rows: rows, - RowCount: len(rows), + Operation: types.OpFind, + Value: values, }, nil } @@ -142,26 +136,21 @@ func executeFindOne(ctx context.Context, client *mongo.Client, database string, defer cancel() } - var doc bson.M + var doc bson.D err := collection.FindOne(ctx, filter, opts).Decode(&doc) if err != nil { if err == mongo.ErrNoDocuments { return &Result{ - Rows: nil, - RowCount: 0, + Operation: types.OpFindOne, + Value: []any{}, }, nil } return nil, fmt.Errorf("findOne failed: %w", err) } - jsonBytes, err := bson.MarshalExtJSONIndent(doc, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } - return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpFindOne, + Value: []any{doc}, }, nil } @@ -192,18 +181,13 @@ func executeAggregate(ctx context.Context, client *mongo.Client, database string } defer func() { _ = cursor.Close(ctx) }() - var rows []string + var values []any for cursor.Next(ctx) { - var doc bson.M + var doc bson.D if err := cursor.Decode(&doc); err != nil { return nil, fmt.Errorf("decode failed: %w", err) } - - jsonBytes, err := bson.MarshalExtJSONIndent(doc, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } - rows = append(rows, string(jsonBytes)) + values = append(values, doc) } if err := cursor.Err(); err != nil { @@ -211,8 +195,8 @@ func executeAggregate(ctx context.Context, client *mongo.Client, database string } return &Result{ - Rows: rows, - RowCount: len(rows), + Operation: types.OpAggregate, + Value: values, }, nil } @@ -226,18 +210,13 @@ func executeGetIndexes(ctx context.Context, client *mongo.Client, database strin } defer func() { _ = cursor.Close(ctx) }() - var rows []string + var values []any for cursor.Next(ctx) { - var doc bson.M + var doc bson.D if err := cursor.Decode(&doc); err != nil { return nil, fmt.Errorf("decode failed: %w", err) } - - jsonBytes, err := bson.MarshalExtJSONIndent(doc, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } - rows = append(rows, string(jsonBytes)) + values = append(values, doc) } if err := cursor.Err(); err != nil { @@ -245,8 +224,8 @@ func executeGetIndexes(ctx context.Context, client *mongo.Client, database strin } return &Result{ - Rows: rows, - RowCount: len(rows), + Operation: types.OpGetIndexes, + Value: values, }, nil } @@ -285,8 +264,8 @@ func executeCountDocuments(ctx context.Context, client *mongo.Client, database s } return &Result{ - Rows: []string{fmt.Sprintf("%d", count)}, - RowCount: 1, + Operation: types.OpCountDocuments, + Value: []any{count}, }, nil } @@ -307,8 +286,8 @@ func executeEstimatedDocumentCount(ctx context.Context, client *mongo.Client, da } return &Result{ - Rows: []string{fmt.Sprintf("%d", count)}, - RowCount: 1, + Operation: types.OpEstimatedDocumentCount, + Value: []any{count}, }, nil } @@ -338,31 +317,8 @@ func executeDistinct(ctx context.Context, client *mongo.Client, database string, return nil, fmt.Errorf("decode failed: %w", err) } - var rows []string - for _, val := range values { - jsonBytes, err := marshalValue(val) - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } - rows = append(rows, string(jsonBytes)) - } - return &Result{ - Rows: rows, - RowCount: len(rows), + Operation: types.OpDistinct, + Value: values, }, nil } - -// marshalValue marshals a value to JSON. -// bson.MarshalExtJSONIndent only works for documents/arrays at top level, -// so we use encoding/json for primitive values (strings, numbers, booleans). -func marshalValue(val any) ([]byte, error) { - switch v := val.(type) { - case bson.M, bson.D, map[string]any: - return bson.MarshalExtJSONIndent(v, false, false, "", " ") - case bson.A, []any: - return bson.MarshalExtJSONIndent(v, false, false, "", " ") - default: - return json.Marshal(v) - } -} diff --git a/internal/executor/database.go b/internal/executor/database.go index 44a4df9..adc4842 100644 --- a/internal/executor/database.go +++ b/internal/executor/database.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/bytebase/gomongo/internal/translator" + "github.com/bytebase/gomongo/types" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" @@ -17,25 +18,35 @@ func executeShowCollections(ctx context.Context, client *mongo.Client, database return nil, fmt.Errorf("list collections failed: %w", err) } - rows := make([]string, 0, len(names)) - for _, name := range names { - doc := bson.M{"name": name} - jsonBytes, err := bson.MarshalExtJSONIndent(doc, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } - rows = append(rows, string(jsonBytes)) + // Convert []string to []any + values := make([]any, len(names)) + for i, name := range names { + values[i] = name } return &Result{ - Rows: rows, - RowCount: len(rows), + Operation: types.OpShowCollections, + Value: values, }, nil } // executeGetCollectionNames executes a db.getCollectionNames() command. func executeGetCollectionNames(ctx context.Context, client *mongo.Client, database string) (*Result, error) { - return executeShowCollections(ctx, client, database) + names, err := client.Database(database).ListCollectionNames(ctx, bson.D{}) + if err != nil { + return nil, fmt.Errorf("list collections failed: %w", err) + } + + // Convert []string to []any + values := make([]any, len(names)) + for i, name := range names { + values[i] = name + } + + return &Result{ + Operation: types.OpGetCollectionNames, + Value: values, + }, nil } // executeGetCollectionInfos executes a db.getCollectionInfos() command. @@ -59,18 +70,13 @@ func executeGetCollectionInfos(ctx context.Context, client *mongo.Client, databa } defer func() { _ = cursor.Close(ctx) }() - var rows []string + var values []any for cursor.Next(ctx) { - var doc bson.M + var doc bson.D if err := cursor.Decode(&doc); err != nil { return nil, fmt.Errorf("decode failed: %w", err) } - - jsonBytes, err := bson.MarshalExtJSONIndent(doc, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } - rows = append(rows, string(jsonBytes)) + values = append(values, doc) } if err := cursor.Err(); err != nil { @@ -78,7 +84,7 @@ func executeGetCollectionInfos(ctx context.Context, client *mongo.Client, databa } return &Result{ - Rows: rows, - RowCount: len(rows), + Operation: types.OpGetCollectionInfos, + Value: values, }, nil } diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 41c243e..b24cfbd 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -5,75 +5,75 @@ import ( "fmt" "github.com/bytebase/gomongo/internal/translator" + "github.com/bytebase/gomongo/types" "go.mongodb.org/mongo-driver/v2/mongo" ) // Result represents query execution results. type Result struct { - Rows []string - RowCount int - Statement string + Operation types.OperationType + Value []any // slice of results; element types vary by operation } // Execute executes a parsed operation against MongoDB. func Execute(ctx context.Context, client *mongo.Client, database string, op *translator.Operation, statement string, maxRows *int64) (*Result, error) { switch op.OpType { - case translator.OpFind: + case types.OpFind: return executeFind(ctx, client, database, op, maxRows) - case translator.OpFindOne: + case types.OpFindOne: return executeFindOne(ctx, client, database, op) - case translator.OpAggregate: + case types.OpAggregate: return executeAggregate(ctx, client, database, op) - case translator.OpShowDatabases: + case types.OpShowDatabases: return executeShowDatabases(ctx, client) - case translator.OpShowCollections: + case types.OpShowCollections: return executeShowCollections(ctx, client, database) - case translator.OpGetCollectionNames: + case types.OpGetCollectionNames: return executeGetCollectionNames(ctx, client, database) - case translator.OpGetCollectionInfos: + case types.OpGetCollectionInfos: return executeGetCollectionInfos(ctx, client, database, op) - case translator.OpGetIndexes: + case types.OpGetIndexes: return executeGetIndexes(ctx, client, database, op) - case translator.OpCountDocuments: + case types.OpCountDocuments: return executeCountDocuments(ctx, client, database, op, maxRows) - case translator.OpEstimatedDocumentCount: + case types.OpEstimatedDocumentCount: return executeEstimatedDocumentCount(ctx, client, database, op) - case translator.OpDistinct: + case types.OpDistinct: return executeDistinct(ctx, client, database, op) - case translator.OpInsertOne: + case types.OpInsertOne: return executeInsertOne(ctx, client, database, op) - case translator.OpInsertMany: + case types.OpInsertMany: return executeInsertMany(ctx, client, database, op) - case translator.OpUpdateOne: + case types.OpUpdateOne: return executeUpdateOne(ctx, client, database, op) - case translator.OpUpdateMany: + case types.OpUpdateMany: return executeUpdateMany(ctx, client, database, op) - case translator.OpReplaceOne: + case types.OpReplaceOne: return executeReplaceOne(ctx, client, database, op) - case translator.OpDeleteOne: + case types.OpDeleteOne: return executeDeleteOne(ctx, client, database, op) - case translator.OpDeleteMany: + case types.OpDeleteMany: return executeDeleteMany(ctx, client, database, op) - case translator.OpFindOneAndUpdate: + case types.OpFindOneAndUpdate: return executeFindOneAndUpdate(ctx, client, database, op) - case translator.OpFindOneAndReplace: + case types.OpFindOneAndReplace: return executeFindOneAndReplace(ctx, client, database, op) - case translator.OpFindOneAndDelete: + case types.OpFindOneAndDelete: return executeFindOneAndDelete(ctx, client, database, op) // M3: Administrative Operations - case translator.OpCreateIndex: + case types.OpCreateIndex: return executeCreateIndex(ctx, client, database, op) - case translator.OpDropIndex: + case types.OpDropIndex: return executeDropIndex(ctx, client, database, op) - case translator.OpDropIndexes: + case types.OpDropIndexes: return executeDropIndexes(ctx, client, database, op) - case translator.OpDrop: + case types.OpDrop: return executeDrop(ctx, client, database, op) - case translator.OpCreateCollection: + case types.OpCreateCollection: return executeCreateCollection(ctx, client, database, op) - case translator.OpDropDatabase: + case types.OpDropDatabase: return executeDropDatabase(ctx, client, database) - case translator.OpRenameCollection: + case types.OpRenameCollection: return executeRenameCollection(ctx, client, database, op) default: return nil, fmt.Errorf("unsupported operation: %s", statement) diff --git a/internal/executor/server.go b/internal/executor/server.go index 510ed89..f7e3824 100644 --- a/internal/executor/server.go +++ b/internal/executor/server.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/bytebase/gomongo/types" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" ) @@ -15,18 +16,14 @@ func executeShowDatabases(ctx context.Context, client *mongo.Client) (*Result, e return nil, fmt.Errorf("list databases failed: %w", err) } - rows := make([]string, 0, len(names)) - for _, name := range names { - doc := bson.M{"name": name} - jsonBytes, err := bson.MarshalExtJSONIndent(doc, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } - rows = append(rows, string(jsonBytes)) + // Convert []string to []any + values := make([]any, len(names)) + for i, name := range names { + values[i] = name } return &Result{ - Rows: rows, - RowCount: len(rows), + Operation: types.OpShowDatabases, + Value: values, }, nil } diff --git a/internal/executor/write.go b/internal/executor/write.go index 52e50ed..6ee5ac3 100644 --- a/internal/executor/write.go +++ b/internal/executor/write.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/bytebase/gomongo/internal/translator" + "github.com/bytebase/gomongo/types" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" @@ -114,19 +115,14 @@ func executeInsertOne(ctx context.Context, client *mongo.Client, database string } // Build response document matching mongosh format - response := bson.M{ - "acknowledged": true, - "insertedId": result.InsertedID, - } - - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) + response := bson.D{ + {Key: "acknowledged", Value: true}, + {Key: "insertedId", Value: result.InsertedID}, } return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpInsertOne, + Value: []any{response}, }, nil } @@ -157,19 +153,14 @@ func executeInsertMany(ctx context.Context, client *mongo.Client, database strin } // Build response document matching mongosh format - response := bson.M{ - "acknowledged": true, - "insertedIds": result.InsertedIDs, - } - - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) + response := bson.D{ + {Key: "acknowledged", Value: true}, + {Key: "insertedIds", Value: result.InsertedIDs}, } return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpInsertMany, + Value: []any{response}, }, nil } @@ -209,23 +200,18 @@ func executeUpdateOne(ctx context.Context, client *mongo.Client, database string } // Build response document matching mongosh format - response := bson.M{ - "acknowledged": true, - "matchedCount": result.MatchedCount, - "modifiedCount": result.ModifiedCount, + response := bson.D{ + {Key: "acknowledged", Value: true}, + {Key: "matchedCount", Value: result.MatchedCount}, + {Key: "modifiedCount", Value: result.ModifiedCount}, } if result.UpsertedID != nil { - response["upsertedId"] = result.UpsertedID - } - - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) + response = append(response, bson.E{Key: "upsertedId", Value: result.UpsertedID}) } return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpUpdateOne, + Value: []any{response}, }, nil } @@ -261,23 +247,18 @@ func executeUpdateMany(ctx context.Context, client *mongo.Client, database strin return nil, fmt.Errorf("updateMany failed: %w", err) } - response := bson.M{ - "acknowledged": true, - "matchedCount": result.MatchedCount, - "modifiedCount": result.ModifiedCount, + response := bson.D{ + {Key: "acknowledged", Value: true}, + {Key: "matchedCount", Value: result.MatchedCount}, + {Key: "modifiedCount", Value: result.ModifiedCount}, } if result.UpsertedID != nil { - response["upsertedId"] = result.UpsertedID - } - - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) + response = append(response, bson.E{Key: "upsertedId", Value: result.UpsertedID}) } return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpUpdateMany, + Value: []any{response}, }, nil } @@ -313,23 +294,18 @@ func executeReplaceOne(ctx context.Context, client *mongo.Client, database strin return nil, fmt.Errorf("replaceOne failed: %w", err) } - response := bson.M{ - "acknowledged": true, - "matchedCount": result.MatchedCount, - "modifiedCount": result.ModifiedCount, + response := bson.D{ + {Key: "acknowledged", Value: true}, + {Key: "matchedCount", Value: result.MatchedCount}, + {Key: "modifiedCount", Value: result.ModifiedCount}, } if result.UpsertedID != nil { - response["upsertedId"] = result.UpsertedID - } - - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) + response = append(response, bson.E{Key: "upsertedId", Value: result.UpsertedID}) } return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpReplaceOne, + Value: []any{response}, }, nil } @@ -356,19 +332,14 @@ func executeDeleteOne(ctx context.Context, client *mongo.Client, database string return nil, fmt.Errorf("deleteOne failed: %w", err) } - response := bson.M{ - "acknowledged": true, - "deletedCount": result.DeletedCount, - } - - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) + response := bson.D{ + {Key: "acknowledged", Value: true}, + {Key: "deletedCount", Value: result.DeletedCount}, } return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpDeleteOne, + Value: []any{response}, }, nil } @@ -395,19 +366,14 @@ func executeDeleteMany(ctx context.Context, client *mongo.Client, database strin return nil, fmt.Errorf("deleteMany failed: %w", err) } - response := bson.M{ - "acknowledged": true, - "deletedCount": result.DeletedCount, - } - - jsonBytes, err := bson.MarshalExtJSONIndent(response, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) + response := bson.D{ + {Key: "acknowledged", Value: true}, + {Key: "deletedCount", Value: result.DeletedCount}, } return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpDeleteMany, + Value: []any{response}, }, nil } @@ -447,26 +413,21 @@ func executeFindOneAndUpdate(ctx context.Context, client *mongo.Client, database opts.SetComment(op.Comment) } - var doc bson.M + var doc bson.D err := collection.FindOneAndUpdate(ctx, op.Filter, op.Update, opts).Decode(&doc) if err != nil { if err == mongo.ErrNoDocuments { return &Result{ - Rows: []string{"null"}, - RowCount: 1, + Operation: types.OpFindOneAndUpdate, + Value: []any{}, }, nil } return nil, fmt.Errorf("findOneAndUpdate failed: %w", err) } - jsonBytes, err := bson.MarshalExtJSONIndent(doc, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } - return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpFindOneAndUpdate, + Value: []any{doc}, }, nil } @@ -503,26 +464,21 @@ func executeFindOneAndReplace(ctx context.Context, client *mongo.Client, databas opts.SetComment(op.Comment) } - var doc bson.M + var doc bson.D err := collection.FindOneAndReplace(ctx, op.Filter, op.Replacement, opts).Decode(&doc) if err != nil { if err == mongo.ErrNoDocuments { return &Result{ - Rows: []string{"null"}, - RowCount: 1, + Operation: types.OpFindOneAndReplace, + Value: []any{}, }, nil } return nil, fmt.Errorf("findOneAndReplace failed: %w", err) } - jsonBytes, err := bson.MarshalExtJSONIndent(doc, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } - return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpFindOneAndReplace, + Value: []any{doc}, }, nil } @@ -550,25 +506,20 @@ func executeFindOneAndDelete(ctx context.Context, client *mongo.Client, database opts.SetComment(op.Comment) } - var doc bson.M + var doc bson.D err := collection.FindOneAndDelete(ctx, op.Filter, opts).Decode(&doc) if err != nil { if err == mongo.ErrNoDocuments { return &Result{ - Rows: []string{"null"}, - RowCount: 1, + Operation: types.OpFindOneAndDelete, + Value: []any{}, }, nil } return nil, fmt.Errorf("findOneAndDelete failed: %w", err) } - jsonBytes, err := bson.MarshalExtJSONIndent(doc, false, false, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal failed: %w", err) - } - return &Result{ - Rows: []string{string(jsonBytes)}, - RowCount: 1, + Operation: types.OpFindOneAndDelete, + Value: []any{doc}, }, nil } diff --git a/internal/testutil/container_test.go b/internal/testutil/container_test.go index cba268a..c7b2161 100644 --- a/internal/testutil/container_test.go +++ b/internal/testutil/container_test.go @@ -27,7 +27,7 @@ func TestRunOnAllDBsHelper(t *testing.T) { result, err := gc.Execute(ctx, dbName, "db.test.find()") require.NoError(t, err) - require.Equal(t, 0, result.RowCount) + require.Equal(t, 0, len(result.Value)) }) } diff --git a/internal/translator/bson_helpers_test.go b/internal/translator/bson_helpers_test.go index 335a345..bd6fb05 100644 --- a/internal/translator/bson_helpers_test.go +++ b/internal/translator/bson_helpers_test.go @@ -7,11 +7,22 @@ import ( "github.com/bytebase/gomongo" "github.com/bytebase/gomongo/internal/testutil" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/v2/bson" ) // These tests verify BSON helper function conversions through the full pipeline. // Since the helper functions are not exported, we test them end-to-end. +// getDocField extracts the value of a field from a bson.D document. +func getDocField(doc bson.D, key string) any { + for _, elem := range doc { + if elem.Key == key { + return elem.Value + } + } + return nil +} + func TestObjectIdHelper(t *testing.T) { testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) { dbName := "testdb_objectid_helper_" + db.Name @@ -26,8 +37,15 @@ func TestObjectIdHelper(t *testing.T) { 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") + require.Equal(t, 1, len(result.Value)) + doc, ok := result.Value[0].(bson.D) + require.True(t, ok) + id := getDocField(doc, "_id") + require.NotNil(t, id) + // Check the ObjectId hex matches + oid, ok := id.(bson.ObjectID) + require.True(t, ok) + require.Equal(t, "507f1f77bcf86cd799439011", oid.Hex()) }) } @@ -45,9 +63,12 @@ func TestObjectIdHelperGenerated(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.test.findOne({})`) require.NoError(t, err) - require.Equal(t, 1, result.RowCount) + require.Equal(t, 1, len(result.Value)) + doc, ok := result.Value[0].(bson.D) + require.True(t, ok) // Should have an _id field with ObjectId - require.Contains(t, result.Rows[0], `"_id"`) + id := getDocField(doc, "_id") + require.NotNil(t, id) }) } @@ -65,9 +86,11 @@ func TestISODateHelper(t *testing.T) { 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") + require.Equal(t, 1, len(result.Value)) + doc, ok := result.Value[0].(bson.D) + require.True(t, ok) + created := getDocField(doc, "created") + require.NotNil(t, created) }) } @@ -85,8 +108,11 @@ func TestNumberLongHelper(t *testing.T) { 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") + require.Equal(t, 1, len(result.Value)) + doc, ok := result.Value[0].(bson.D) + require.True(t, ok) + bignum := getDocField(doc, "bignum") + require.Equal(t, int64(9007199254740993), bignum) }) } @@ -104,8 +130,11 @@ func TestNumberIntHelper(t *testing.T) { 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") + require.Equal(t, 1, len(result.Value)) + doc, ok := result.Value[0].(bson.D) + require.True(t, ok) + count := getDocField(doc, "count") + require.Equal(t, int32(42), count) }) } @@ -123,9 +152,12 @@ func TestUUIDHelper(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.test.findOne({})`) require.NoError(t, err) - require.Equal(t, 1, result.RowCount) + require.Equal(t, 1, len(result.Value)) + doc, ok := result.Value[0].(bson.D) + require.True(t, ok) // UUID should be in the output (as binary subtype 4) - require.Contains(t, result.Rows[0], "uuid") + uuid := getDocField(doc, "uuid") + require.NotNil(t, uuid) }) } @@ -143,8 +175,11 @@ func TestTimestampHelper(t *testing.T) { 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") + require.Equal(t, 1, len(result.Value)) + doc, ok := result.Value[0].(bson.D) + require.True(t, ok) + ts := getDocField(doc, "ts") + require.NotNil(t, ts) }) } @@ -162,7 +197,10 @@ func TestDecimal128Helper(t *testing.T) { 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") + require.Equal(t, 1, len(result.Value)) + doc, ok := result.Value[0].(bson.D) + require.True(t, ok) + price := getDocField(doc, "price") + require.NotNil(t, price) }) } diff --git a/internal/translator/types.go b/internal/translator/types.go index 027624b..a0206b4 100644 --- a/internal/translator/types.go +++ b/internal/translator/types.go @@ -1,47 +1,13 @@ package translator -import "go.mongodb.org/mongo-driver/v2/bson" - -// OperationType represents the type of MongoDB operation. -type OperationType int - -const ( - OpUnknown OperationType = iota - OpFind - OpFindOne - OpAggregate - OpShowDatabases - OpShowCollections - OpGetCollectionNames - OpGetCollectionInfos - OpGetIndexes - OpCountDocuments - OpEstimatedDocumentCount - OpDistinct - // M2: Write Operations - OpInsertOne - OpInsertMany - OpUpdateOne - OpUpdateMany - OpReplaceOne - OpDeleteOne - OpDeleteMany - OpFindOneAndUpdate - OpFindOneAndReplace - OpFindOneAndDelete - // M3: Administrative Operations - OpCreateIndex - OpDropIndex - OpDropIndexes - OpDrop - OpCreateCollection - OpDropDatabase - OpRenameCollection +import ( + "github.com/bytebase/gomongo/types" + "go.mongodb.org/mongo-driver/v2/bson" ) // Operation represents a parsed MongoDB operation. type Operation struct { - OpType OperationType + OpType types.OperationType Collection string Filter bson.D // Read operation options (find, findOne) diff --git a/internal/translator/visitor.go b/internal/translator/visitor.go index 11c132f..ee9ea9d 100644 --- a/internal/translator/visitor.go +++ b/internal/translator/visitor.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/antlr4-go/antlr/v4" + "github.com/bytebase/gomongo/types" "github.com/bytebase/parser/mongodb" ) @@ -16,7 +17,7 @@ type visitor struct { func newVisitor() *visitor { return &visitor{ - operation: &Operation{OpType: OpUnknown}, + operation: &Operation{OpType: types.OpUnknown}, } } @@ -56,24 +57,24 @@ func (v *visitor) visitDbStatement(ctx mongodb.IDbStatementContext) { case *mongodb.CollectionOperationContext: v.visitCollectionOperation(c) case *mongodb.GetCollectionNamesContext: - v.operation.OpType = OpGetCollectionNames + v.operation.OpType = types.OpGetCollectionNames case *mongodb.GetCollectionInfosContext: - v.operation.OpType = OpGetCollectionInfos + v.operation.OpType = types.OpGetCollectionInfos v.extractGetCollectionInfosArgs(c) case *mongodb.CreateCollectionContext: - v.operation.OpType = OpCreateCollection + v.operation.OpType = types.OpCreateCollection v.extractCreateCollectionArgs(c) case *mongodb.DropDatabaseContext: - v.operation.OpType = OpDropDatabase + v.operation.OpType = types.OpDropDatabase } } func (v *visitor) visitShellCommand(ctx mongodb.IShellCommandContext) { switch ctx.(type) { case *mongodb.ShowDatabasesContext: - v.operation.OpType = OpShowDatabases + v.operation.OpType = types.OpShowDatabases case *mongodb.ShowCollectionsContext: - v.operation.OpType = OpShowCollections + v.operation.OpType = types.OpShowCollections default: v.err = &UnsupportedOperationError{ Operation: ctx.GetText(), @@ -95,12 +96,12 @@ func (v *visitor) visitCollectionOperation(ctx *mongodb.CollectionOperationConte } func (v *visitor) VisitGetCollectionNames(_ *mongodb.GetCollectionNamesContext) any { - v.operation.OpType = OpGetCollectionNames + v.operation.OpType = types.OpGetCollectionNames return nil } func (v *visitor) VisitGetCollectionInfos(ctx *mongodb.GetCollectionInfosContext) any { - v.operation.OpType = OpGetCollectionInfos + v.operation.OpType = types.OpGetCollectionInfos v.extractGetCollectionInfosArgs(ctx) return nil } @@ -138,7 +139,7 @@ func (v *visitor) visitMethodCall(ctx mongodb.IMethodCallContext) { // Determine method context for registry lookup getMethodContext := func() string { - if v.operation.OpType == OpFind || v.operation.OpType == OpFindOne { + if v.operation.OpType == types.OpFind || v.operation.OpType == types.OpFindOne { return "cursor" } return "collection" @@ -147,25 +148,25 @@ func (v *visitor) visitMethodCall(ctx mongodb.IMethodCallContext) { switch { // Supported read operations case mc.FindMethod() != nil: - v.operation.OpType = OpFind + v.operation.OpType = types.OpFind v.extractFindArgs(mc.FindMethod()) case mc.FindOneMethod() != nil: - v.operation.OpType = OpFindOne + v.operation.OpType = types.OpFindOne v.extractFindOneArgs(mc.FindOneMethod()) case mc.CountDocumentsMethod() != nil: - v.operation.OpType = OpCountDocuments + v.operation.OpType = types.OpCountDocuments v.extractCountDocumentsArgsFromMethod(mc.CountDocumentsMethod()) case mc.EstimatedDocumentCountMethod() != nil: - v.operation.OpType = OpEstimatedDocumentCount + v.operation.OpType = types.OpEstimatedDocumentCount v.extractEstimatedDocumentCountArgs(mc.EstimatedDocumentCountMethod()) case mc.DistinctMethod() != nil: - v.operation.OpType = OpDistinct + v.operation.OpType = types.OpDistinct v.extractDistinctArgsFromMethod(mc.DistinctMethod()) case mc.AggregateMethod() != nil: - v.operation.OpType = OpAggregate + v.operation.OpType = types.OpAggregate v.extractAggregationPipelineFromMethod(mc.AggregateMethod()) case mc.GetIndexesMethod() != nil: - v.operation.OpType = OpGetIndexes + v.operation.OpType = types.OpGetIndexes // Supported cursor modifiers case mc.SortMethod() != nil: @@ -185,57 +186,57 @@ func (v *visitor) visitMethodCall(ctx mongodb.IMethodCallContext) { // Supported M2 write operations case mc.InsertOneMethod() != nil: - v.operation.OpType = OpInsertOne + v.operation.OpType = types.OpInsertOne v.extractInsertOneArgs(mc.InsertOneMethod()) case mc.InsertManyMethod() != nil: - v.operation.OpType = OpInsertMany + v.operation.OpType = types.OpInsertMany v.extractInsertManyArgs(mc.InsertManyMethod()) // Supported M2 write operations - updateOne case mc.UpdateOneMethod() != nil: - v.operation.OpType = OpUpdateOne + v.operation.OpType = types.OpUpdateOne v.extractUpdateOneArgs(mc.UpdateOneMethod()) case mc.UpdateManyMethod() != nil: - v.operation.OpType = OpUpdateMany + v.operation.OpType = types.OpUpdateMany v.extractUpdateManyArgs(mc.UpdateManyMethod()) case mc.DeleteOneMethod() != nil: - v.operation.OpType = OpDeleteOne + v.operation.OpType = types.OpDeleteOne v.extractDeleteOneArgs(mc.DeleteOneMethod()) case mc.DeleteManyMethod() != nil: - v.operation.OpType = OpDeleteMany + v.operation.OpType = types.OpDeleteMany v.extractDeleteManyArgs(mc.DeleteManyMethod()) case mc.ReplaceOneMethod() != nil: - v.operation.OpType = OpReplaceOne + v.operation.OpType = types.OpReplaceOne v.extractReplaceOneArgs(mc.ReplaceOneMethod()) case mc.FindOneAndUpdateMethod() != nil: - v.operation.OpType = OpFindOneAndUpdate + v.operation.OpType = types.OpFindOneAndUpdate v.extractFindOneAndUpdateArgs(mc.FindOneAndUpdateMethod()) case mc.FindOneAndReplaceMethod() != nil: - v.operation.OpType = OpFindOneAndReplace + v.operation.OpType = types.OpFindOneAndReplace v.extractFindOneAndReplaceArgs(mc.FindOneAndReplaceMethod()) case mc.FindOneAndDeleteMethod() != nil: - v.operation.OpType = OpFindOneAndDelete + v.operation.OpType = types.OpFindOneAndDelete v.extractFindOneAndDeleteArgs(mc.FindOneAndDeleteMethod()) // Supported M3 index operations case mc.CreateIndexMethod() != nil: - v.operation.OpType = OpCreateIndex + v.operation.OpType = types.OpCreateIndex v.extractCreateIndexArgs(mc.CreateIndexMethod()) case mc.CreateIndexesMethod() != nil: v.handleUnsupportedMethod("collection", "createIndexes") // Lower ROI, keep as planned case mc.DropIndexMethod() != nil: - v.operation.OpType = OpDropIndex + v.operation.OpType = types.OpDropIndex v.extractDropIndexArgs(mc.DropIndexMethod()) case mc.DropIndexesMethod() != nil: - v.operation.OpType = OpDropIndexes + v.operation.OpType = types.OpDropIndexes v.extractDropIndexesArgs(mc.DropIndexesMethod()) // Supported M3 collection management case mc.DropMethod() != nil: - v.operation.OpType = OpDrop + v.operation.OpType = types.OpDrop case mc.RenameCollectionMethod() != nil: - v.operation.OpType = OpRenameCollection + v.operation.OpType = types.OpRenameCollection v.extractRenameCollectionArgs(mc.RenameCollectionMethod()) // Planned M3 stats operations - return PlannedOperationError for fallback diff --git a/types/operation_type.go b/types/operation_type.go new file mode 100644 index 0000000..39000d5 --- /dev/null +++ b/types/operation_type.go @@ -0,0 +1,39 @@ +// Package types provides shared types for the gomongo library. +package types + +// OperationType represents the type of MongoDB operation. +type OperationType int + +const ( + OpUnknown OperationType = iota + OpFind + OpFindOne + OpAggregate + OpShowDatabases + OpShowCollections + OpGetCollectionNames + OpGetCollectionInfos + OpGetIndexes + OpCountDocuments + OpEstimatedDocumentCount + OpDistinct + // Write Operations + OpInsertOne + OpInsertMany + OpUpdateOne + OpUpdateMany + OpReplaceOne + OpDeleteOne + OpDeleteMany + OpFindOneAndUpdate + OpFindOneAndReplace + OpFindOneAndDelete + // Administrative Operations + OpCreateIndex + OpDropIndex + OpDropIndexes + OpDrop + OpCreateCollection + OpDropDatabase + OpRenameCollection +) diff --git a/unicode_test.go b/unicode_test.go index df4c8e0..a17ae47 100644 --- a/unicode_test.go +++ b/unicode_test.go @@ -25,9 +25,10 @@ func TestUnicodeInsertAndQuery(t *testing.T) { // 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], "北京") + require.Equal(t, 1, len(result.Value)) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, "张三") + require.Contains(t, row, "北京") }) } @@ -46,8 +47,9 @@ func TestUnicodeArabic(t *testing.T) { // 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], "محمد") + require.Equal(t, 1, len(result.Value)) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, "محمد") }) } @@ -66,9 +68,10 @@ func TestUnicodeEmoji(t *testing.T) { // 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], "🔥") + require.Equal(t, 1, len(result.Value)) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, "🎉") + require.Contains(t, row, "🔥") }) } @@ -87,7 +90,7 @@ func TestUnicodeInCollectionName(t *testing.T) { // Query unicode-named collection result, err := gc.Execute(ctx, dbName, `db["用户表"].find()`) require.NoError(t, err) - require.Equal(t, 1, result.RowCount) + require.Equal(t, 1, len(result.Value)) }) } @@ -106,7 +109,7 @@ func TestUnicodeEmojiInCollectionName(t *testing.T) { // Query emoji-named collection result, err := gc.Execute(ctx, dbName, `db["users🎉"].find()`) require.NoError(t, err) - require.Equal(t, 1, result.RowCount) + require.Equal(t, 1, len(result.Value)) }) } @@ -130,12 +133,12 @@ func TestUnicodeRoundTrip(t *testing.T) { // 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) + require.Equal(t, len(docs), len(result.Value)) // Spot check specific unicode values allRows := "" - for _, row := range result.Rows { - allRows += row + for _, v := range result.Value { + allRows += valueToJSON(v) } require.Contains(t, allRows, "张三") // Chinese require.Contains(t, allRows, "田中太郎") // Japanese diff --git a/write_test.go b/write_test.go index 61fb505..58ec592 100644 --- a/write_test.go +++ b/write_test.go @@ -22,16 +22,18 @@ func TestInsertOneBasic(t *testing.T) { 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"`) + require.Equal(t, 1, len(result.Value)) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"acknowledged": true`) + require.Contains(t, row, `"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`) + require.Equal(t, 1, len(verifyResult.Value)) + verifyRow := valueToJSON(verifyResult.Value[0]) + require.Contains(t, verifyRow, `"alice"`) + require.Contains(t, verifyRow, `"age": 30`) }) } @@ -47,13 +49,15 @@ func TestInsertOneWithObjectId(t *testing.T) { 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"`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"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"`) + require.Equal(t, 1, len(verifyResult.Value)) + verifyRow := valueToJSON(verifyResult.Value[0]) + require.Contains(t, verifyRow, `"bob"`) }) } @@ -76,8 +80,9 @@ func TestInsertOneWithNestedDocument(t *testing.T) { // 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"`) + verifyRow := valueToJSON(verifyResult.Value[0]) + require.Contains(t, verifyRow, `"city": "NYC"`) + require.Contains(t, verifyRow, `"admin"`) }) } @@ -127,14 +132,17 @@ func TestInsertManyBasic(t *testing.T) { ])`) 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"`) + require.Equal(t, 1, len(result.Value)) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"acknowledged": true`) + require.Contains(t, row, `"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]) + count, ok := verifyResult.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(3), count) }) } @@ -166,14 +174,16 @@ func TestUpdateOneBasic(t *testing.T) { // 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`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"acknowledged": true`) + require.Contains(t, row, `"matchedCount": 1`) + require.Contains(t, row, `"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`) + verifyRow := valueToJSON(verifyResult.Value[0]) + require.Contains(t, verifyRow, `"age": 31`) }) } @@ -187,8 +197,9 @@ func TestUpdateOneNoMatch(t *testing.T) { 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`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"matchedCount": 0`) + require.Contains(t, row, `"modifiedCount": 0`) }) } @@ -206,12 +217,13 @@ func TestUpdateOneUpsert(t *testing.T) { { upsert: true } )`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"upsertedId"`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"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.Equal(t, 1, len(verifyResult.Value)) }) } @@ -237,8 +249,9 @@ func TestUpdateManyBasic(t *testing.T) { { $set: { verified: true } } )`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"matchedCount": 2`) - require.Contains(t, result.Rows[0], `"modifiedCount": 2`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"matchedCount": 2`) + require.Contains(t, row, `"modifiedCount": 2`) }) } @@ -252,8 +265,9 @@ func TestUpdateManyNoMatch(t *testing.T) { 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`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"matchedCount": 0`) + require.Contains(t, row, `"modifiedCount": 0`) }) } @@ -271,12 +285,13 @@ func TestUpdateManyUpsert(t *testing.T) { { upsert: true } )`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"upsertedId"`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"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.Equal(t, 1, len(verifyResult.Value)) }) } @@ -298,14 +313,16 @@ func TestReplaceOneBasic(t *testing.T) { { 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`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"matchedCount": 1`) + require.Contains(t, row, `"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"`) + verifyRow := valueToJSON(verifyResult.Value[0]) + require.Contains(t, verifyRow, `"country": "USA"`) + require.NotContains(t, verifyRow, `"city"`) }) } @@ -323,7 +340,8 @@ func TestReplaceOneUpsert(t *testing.T) { { upsert: true } )`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"upsertedId"`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"upsertedId"`) }) } @@ -346,13 +364,16 @@ func TestDeleteOneBasic(t *testing.T) { // 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`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"acknowledged": true`) + require.Contains(t, row, `"deletedCount": 1`) // Verify countResult, err := gc.Execute(ctx, dbName, `db.users.countDocuments()`) require.NoError(t, err) - require.Equal(t, "2", countResult.Rows[0]) + count, ok := countResult.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(2), count) }) } @@ -366,7 +387,8 @@ func TestDeleteOneNoMatch(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.users.deleteOne({ name: "nobody" })`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"deletedCount": 0`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"deletedCount": 0`) }) } @@ -389,12 +411,15 @@ func TestDeleteManyBasic(t *testing.T) { // 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`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"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]) + count, ok := countResult.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(1), count) }) } @@ -416,7 +441,8 @@ func TestDeleteManyAll(t *testing.T) { // 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`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"deletedCount": 2`) }) } @@ -437,8 +463,9 @@ func TestFindOneAndUpdateBasic(t *testing.T) { { $set: { age: 31 } } )`) require.NoError(t, err) - require.Equal(t, 1, result.RowCount) - require.Contains(t, result.Rows[0], `"age": 30`) + require.Equal(t, 1, len(result.Value)) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"age": 30`) }) } @@ -459,7 +486,8 @@ func TestFindOneAndUpdateReturnAfter(t *testing.T) { { returnDocument: "after" } )`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"age": 31`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"age": 31`) }) } @@ -476,7 +504,8 @@ func TestFindOneAndUpdateNoMatch(t *testing.T) { { $set: { age: 99 } } )`) require.NoError(t, err) - require.Equal(t, "null", result.Rows[0]) + // No document found returns empty slice + require.Equal(t, 0, len(result.Value)) }) } @@ -497,7 +526,8 @@ func TestFindOneAndReplaceBasic(t *testing.T) { { name: "alice", age: 31, country: "USA" } )`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"city": "NYC"`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"city": "NYC"`) }) } @@ -518,7 +548,8 @@ func TestFindOneAndReplaceReturnAfter(t *testing.T) { { returnDocument: "after" } )`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"age": 31`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"age": 31`) }) } @@ -539,13 +570,16 @@ func TestFindOneAndDeleteBasic(t *testing.T) { // 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`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"alice"`) + require.Contains(t, row, `"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]) + count, ok := countResult.Value[0].(int64) + require.True(t, ok) + require.Equal(t, int64(1), count) }) } @@ -559,7 +593,8 @@ func TestFindOneAndDeleteNoMatch(t *testing.T) { result, err := gc.Execute(ctx, dbName, `db.users.findOneAndDelete({ name: "nobody" })`) require.NoError(t, err) - require.Equal(t, "null", result.Rows[0]) + // No document found returns empty slice + require.Equal(t, 0, len(result.Value)) }) } @@ -583,11 +618,13 @@ func TestFindOneAndDeleteWithSort(t *testing.T) { { sort: { score: 1 } } )`) require.NoError(t, err) - require.Contains(t, result.Rows[0], `"score": 10`) + row := valueToJSON(result.Value[0]) + require.Contains(t, row, `"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`) + verifyRow := valueToJSON(verifyResult.Value[0]) + require.Contains(t, verifyRow, `"score": 20`) }) }