The MongoDB driver provides native BSON query syntax and aggregation pipeline support, built on the official Go driver v2. Unlike the SQL-based drivers (pgdriver, mysqldriver, sqlitedriver), this driver uses MongoDB-native operations (Find, InsertOne, UpdateOne, Aggregate, etc.) and implements grove.GroveDriver.
Installation01
go get github.com/xraph/grove
go get go.mongodb.org/mongo-driver/v2Connection02
import (
"context"
"log"
"github.com/xraph/grove"
"github.com/xraph/grove/drivers/mongodriver"
)
ctx := context.Background()
// 1. Create an unconnected driver
mdb := mongodriver.New()
// 2. Open a connection (database name is extracted from the URI path)
err := mdb.Open(ctx, "mongodb://localhost:27017/mydb")
if err != nil {
log.Fatal(err)
}
// 3. Wrap in a grove.DB handle
db, err := grove.Open(mdb)
if err != nil {
log.Fatal(err)
}
defer db.Close()Overriding the Database Name
If your URI does not contain a database path, or you want to target a different database, use WithDatabase:
err := mdb.Open(ctx, "mongodb://localhost:27017",
mongodriver.WithDatabase("mydb"),
)Unwrap Pattern
Many MongoDB-specific query builders live on *mongodriver.MongoDB. Use Unwrap to extract the driver from a *grove.DB handle:
mdb := mongodriver.Unwrap(db)
// Now you can use MongoDB-native query builders:
mdb.NewFind(&users).Filter(bson.M{"role": "admin"}).Scan(ctx)Accessing the Underlying Client
mdb := mongodriver.Unwrap(db)
client := mdb.Client() // *mongo.Client
database := mdb.Database() // *mongo.Database
coll := mdb.Collection("users") // *mongo.CollectionBSON Types03
The mongodriver package re-exports common BSON types so you can avoid importing the bson package directly if you prefer:
import "github.com/xraph/grove/drivers/mongodriver"
filter := mongodriver.M{"status": "active"} // = bson.M
sort := mongodriver.D{{"name", 1}} // = bson.D
arr := mongodriver.A{"a", "b", "c"} // = bson.AThe package also provides ObjectID helpers:
id := mongodriver.NewObjectID()
id, err := mongodriver.ObjectIDFromHex("507f1f77bcf86cd799439011")
ok := mongodriver.IsValidObjectID("507f1f77bcf86cd799439011")All code examples below use bson.M and bson.D directly:
import "go.mongodb.org/mongo-driver/v2/bson"Defining Models04
Models are plain Go structs with grove struct tags. The collection name is derived from the table name in the schema (typically the pluralized, snake-cased struct name).
type User struct {
ID mongodriver.ObjectID `grove:"id,pk"`
Name string `grove:"name"`
Email string `grove:"email"`
Role string `grove:"role"`
Age int `grove:"age"`
CreatedAt time.Time `grove:"created_at"`
}The id field is automatically mapped to MongoDB's _id field. If the primary key has a zero value on insert, it is omitted so MongoDB generates an _id automatically.
Collection Management05
Create Collection with Schema Validation
NewCreateCollection creates a MongoDB collection with $jsonSchema validation derived from the model. This is the MongoDB equivalent of CREATE TABLE in SQL drivers.
mdb := mongodriver.Unwrap(db)
err := mdb.NewCreateCollection((*User)(nil)).
ValidationLevel("strict").
ValidationAction("error").
AdditionalProperties(false).
IfNotExists().
Exec(ctx)| Method | Default | Description |
Collection(name) | derived from model | Override collection name |
ValidationLevel(level) | "strict" | "strict", "moderate", or "off" |
ValidationAction(action) | "error" | "error" or "warn" |
AdditionalProperties(bool) | unset (MongoDB default: true) | Whether documents can have fields not in the schema |
IfNotExists() | false | No-op if the collection already exists |
Inspect the Generated Schema
Use BuildSchema() to get the $jsonSchema document without executing:
q := mdb.NewCreateCollection((*User)(nil)).
AdditionalProperties(false)
schema, err := q.BuildSchema()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", schema)Go Type to BSON Type Mapping
The schema generator maps Go types to BSON types automatically:
| Go Type | BSON Type |
string | "string" |
bool | "bool" |
int, int8, int16, int32 | "int" |
int64 | "long" |
uint, uint8, uint16, uint32 | "int" |
uint64 | "long" |
float32, float64 | "double" |
time.Time | "date" |
bson.ObjectID | "objectId" |
[]byte | "binData" |
| other slices | "array" |
| maps, structs | "object" |
The type: tag overrides this mapping. For example, grove:"data,type:decimal" forces the BSON type to "decimal" regardless of Go type.
Schema Behavior
Fields tagged
pkornotnullbecome entries in therequiredarrayA primary key with column name
idis remapped to_idFields tagged
scanonlyor-are excluded from the schemaPointer types are dereferenced before mapping (e.g.,
*stringmaps to"string")
Drop Collection
err := mdb.NewDropCollection((*User)(nil)).Exec(ctx)
// With a custom collection name:
err = mdb.NewDropCollection((*User)(nil)).
Collection("archived_users").
Exec(ctx)Find Queries06
Create find queries with NewFind. Pass a pointer to a struct (single result) or a pointer to a slice (multiple results).
Find Multiple Documents
var users []User
mdb := mongodriver.Unwrap(db)
err := mdb.NewFind(&users).
Filter(bson.M{"role": "admin"}).
Sort(bson.D{{"created_at", -1}}).
Limit(20).
Skip(0).
Scan(ctx)Find a Single Document
var user User
err := mdb.NewFind(&user).
Filter(bson.M{"email": "[email protected]"}).
Scan(ctx)Projection
Use Project to include or exclude specific fields:
var users []User
err := mdb.NewFind(&users).
Filter(bson.M{"role": "admin"}).
Project(bson.M{"name": 1, "email": 1}).
Scan(ctx)Count Documents
count, err := mdb.NewFind(&users).
Filter(bson.M{"role": "admin"}).
Count(ctx)Override Collection Name
err := mdb.NewFind(&users).
Collection("archived_users").
Filter(bson.M{"role": "admin"}).
Scan(ctx)Insert07
Create insert queries with NewInsert. The driver auto-detects single vs bulk insert based on whether the model is a struct pointer or a slice pointer.
Insert a Single Document
user := User{
Name: "Alice",
Email: "[email protected]",
Role: "admin",
CreatedAt: time.Now(),
}
result, err := mdb.NewInsert(&user).Exec(ctx)
if err != nil {
log.Fatal(err)
}
// Get the generated ObjectID
insertedID := result.InsertedID()Bulk Insert
users := []User{
{Name: "Bob", Email: "[email protected]", Role: "user"},
{Name: "Carol", Email: "[email protected]", Role: "user"},
}
result, err := mdb.NewInsert(&users).Exec(ctx)Update08
Create update queries with NewUpdate. Use Set to update individual fields, or SetUpdate for complex update operations.
Update with Set
Use Set(field, value) to set individual fields via the $set operator:
result, err := mdb.NewUpdate(&User{}).
Filter(bson.M{"_id": userID}).
Set("name", "New Name").
Set("role", "moderator").
Exec(ctx)
fmt.Println(result.MatchedCount()) // documents matched
fmt.Println(result.ModifiedCount()) // documents actually modifiedComplex Updates with SetUpdate
Use SetUpdate for MongoDB update operators like $inc, $push, $unset, etc.:
result, err := mdb.NewUpdate(&User{}).
Filter(bson.M{"role": "user"}).
SetUpdate(bson.M{
"$inc": bson.M{"login_count": 1},
"$set": bson.M{"last_login": time.Now()},
}).
Exec(ctx)Upsert
Insert the document if no match is found:
result, err := mdb.NewUpdate(&User{}).
Filter(bson.M{"email": "[email protected]"}).
Set("name", "Alice").
Set("role", "admin").
Upsert().
Exec(ctx)
fmt.Println(result.UpsertedCount()) // 1 if a new document was createdUpdate Many
Update all matching documents instead of just the first:
result, err := mdb.NewUpdate(&User{}).
Filter(bson.M{"role": "guest"}).
Set("role", "user").
Many().
Exec(ctx)Delete09
Create delete queries with NewDelete.
Delete a Single Document
result, err := mdb.NewDelete(&User{}).
Filter(bson.M{"_id": userID}).
Exec(ctx)
fmt.Println(result.DeletedCount())Delete Many
Delete all matching documents:
result, err := mdb.NewDelete(&User{}).
Filter(bson.M{"role": "inactive"}).
Many().
Exec(ctx)Aggregation Pipeline10
Create aggregation pipelines with NewAggregate. Unlike the other query builders, NewAggregate takes a collection name string (not a model pointer).
Basic Aggregation
type DeptStats struct {
Department string `bson:"_id"`
Count int `bson:"count"`
AvgSalary float64 `bson:"avg_salary"`
}
var results []DeptStats
err := mdb.NewAggregate("users").
Match(bson.M{"status": "active"}).
Group(bson.M{
"_id": "$department",
"count": bson.M{"$sum": 1},
"avg_salary": bson.M{"$avg": "$salary"},
}).
Sort(bson.D{{"count", -1}}).
Scan(ctx, &results)Projection
err := mdb.NewAggregate("users").
Match(bson.M{"role": "admin"}).
Project(bson.M{
"name": 1,
"email": 1,
"year": bson.M{"$year": "$created_at"},
}).
Scan(ctx, &results)Lookup (Join)
Use Lookup with a bson.M document matching MongoDB's $lookup syntax:
type UserWithOrders struct {
ID mongodriver.ObjectID `bson:"_id"`
Name string `bson:"name"`
Orders []Order `bson:"user_orders"`
}
var results []UserWithOrders
err := mdb.NewAggregate("users").
Match(bson.M{"status": "active"}).
Lookup(bson.M{
"from": "orders",
"localField": "_id",
"foreignField": "user_id",
"as": "user_orders",
}).
Scan(ctx, &results)Unwind
Deconstruct an array field into separate documents:
err := mdb.NewAggregate("users").
Match(bson.M{"status": "active"}).
Lookup(bson.M{
"from": "orders",
"localField": "_id",
"foreignField": "user_id",
"as": "user_orders",
}).
Unwind("$user_orders").
Scan(ctx, &results)Limit and Skip
err := mdb.NewAggregate("logs").
Match(bson.M{"level": "error"}).
Sort(bson.D{{"timestamp", -1}}).
Skip(100).
Limit(50).
Scan(ctx, &results)Custom Pipeline Stages
Use Stage for any pipeline operator not covered by the convenience methods:
err := mdb.NewAggregate("orders").
Match(bson.M{"status": "completed"}).
Stage(bson.M{
"$addFields": bson.M{
"total_with_tax": bson.M{"$multiply": []any{"$total", 1.1}},
},
}).
Scan(ctx, &results)Transactions11
MongoDB transactions require a replica set or sharded cluster. The driver wraps sessions and transactions into MongoTx.
mdb := mongodriver.Unwrap(db)
tx, err := mdb.GroveTx(ctx, 0, false)
if err != nil {
log.Fatal(err)
}
// Use the transaction-scoped query builders
_, err = tx.NewInsert(&User{Name: "Alice", Email: "[email protected]"}).Exec(ctx)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
_, err = tx.NewUpdate(&User{}).
Filter(bson.M{"email": "[email protected]"}).
Set("role", "admin").
Exec(ctx)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
err = tx.Commit()
if err != nil {
log.Fatal(err)
}Session Context
If you need to call the underlying MongoDB client directly within a transaction, use SessionContext to obtain a context that carries the session:
sessCtx := tx.SessionContext(ctx)
// Use the session context with raw MongoDB operations
_, err = mdb.Collection("audit_log").InsertOne(sessCtx, bson.M{
"action": "user_created",
"timestamp": time.Now(),
})Transaction Query Builders
MongoTx provides the same query builders as *MongoDB, but all operations execute within the transaction:
tx.NewFind(model...)-- find within transactiontx.NewInsert(model)-- insert within transactiontx.NewUpdate(model)-- update within transactiontx.NewDelete(model)-- delete within transaction
Migrations12
The mongomigrate package provides a MongoDB-specific migration executor that integrates with Grove's migration system.
Setup
import (
"github.com/xraph/grove/migrate"
"github.com/xraph/grove/drivers/mongodriver"
"github.com/xraph/grove/drivers/mongodriver/mongomigrate"
)
mdb := mongodriver.Unwrap(db)
executor := mongomigrate.New(mdb)Migration Tracking
The executor stores migration state in two collections:
grove_migrations-- tracks applied migrations with a unique index on(version, group)grove_migration_locks-- distributed locking via atomicfindOneAndUpdate
Writing Migrations
MongoDB migrations use the driver API directly rather than SQL. The executor's Exec and Query methods return ErrNotSupported -- use the DB() method to access the *mongodriver.MongoDB instance within migration functions:
migrations := []*migrate.Migration{
{
Version: "20240101120000",
Name: "create_users_indexes",
Group: "default",
Up: func(ctx context.Context, exec migrate.Executor) error {
mdb := exec.(*mongomigrate.Executor).DB()
// Create indexes using the MongoDB driver directly
_, err := mdb.Collection("users").Indexes().CreateOne(ctx,
mongo.IndexModel{
Keys: bson.D{{"email", 1}},
Options: options.Index().SetUnique(true),
},
)
return err
},
Down: func(ctx context.Context, exec migrate.Executor) error {
mdb := exec.(*mongomigrate.Executor).DB()
_, err := mdb.Collection("users").Indexes().DropOne(ctx, "email_1")
return err
},
},
}Running Migrations
var Migrations = migrate.NewGroup("app")
func init() {
Migrations.MustRegister(migrations...)
}
orchestrator := migrate.NewOrchestrator(executor, Migrations)
// Apply all pending migrations
result, err := orchestrator.Migrate(ctx)
// Rollback the last batch
result, err = orchestrator.Rollback(ctx)Result Type13
All write operations (insert, update, delete) return a result object with the following methods:
| Method | Description |
RowsAffected() | Number of documents affected (inserted, modified, or deleted) |
InsertedID() | The _id of the inserted document (typically an ObjectID) |
MatchedCount() | Number of documents matched by the filter |
ModifiedCount() | Number of documents actually modified |
DeletedCount() | Number of documents deleted |
UpsertedCount() | Number of documents upserted |
LastInsertId() always returns an error because MongoDB does not use auto-incrementing integer IDs. Use InsertedID() instead.
Compatible Databases14
FerretDB
FerretDB implements the MongoDB wire protocol on top of PostgreSQL or SQLite. The mongodriver works with FerretDB using a standard MongoDB connection string:
mdb := mongodriver.New()
err := mdb.Open(ctx, "mongodb://localhost:27017/mydb") // FerretDB endpointFerretDB supports the core MongoDB operations used by Grove's mongodriver (Find, Insert, Update, Delete, Aggregate). Some advanced MongoDB features may have limited support -- consult the FerretDB compatibility matrix for details.