grove

package module
v1.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 20, 2026 License: MIT Imports: 9 Imported by: 78

README

Grove, a polyglot Go ORM with native query builders for PostgreSQL, MySQL, SQLite, MongoDB, ClickHouse and Elasticsearch

Grove: polyglot Go ORM with native query syntax

Grove is an open-source Go ORM for PostgreSQL, MySQL, SQLite, Turso, MongoDB, ClickHouse and Elasticsearch. Each driver builds queries in its own database's syntax, so you write $1 and DISTINCT ON against Postgres and BSON filters against MongoDB, through one model layer. You also get Go-code database migrations with dependency ordering, query hooks for multi-tenancy and audit logging, a key-value store over Redis, Badger, BoltDB, DynamoDB and Memcached, and CRDT sync for offline-first apps. It runs standalone or as a Forge extension.

CI Go Reference GitHub Stars MIT License

Documentation · Quick start · Drivers · Migrations · Hooks · KV store · CRDT sync · Forge · FAQ

Quick start

You need Go 1.25 or later. This example uses the SQLite driver, which is pure Go, so you don't need a database server or cgo to try it:

mkdir hello-grove
cd hello-grove
go mod init example.com/hello-grove
go get github.com/xraph/grove@latest
go get github.com/xraph/grove/drivers/sqlitedriver@latest

Save this as main.go:

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/xraph/grove"
	"github.com/xraph/grove/drivers/sqlitedriver"
)

type User struct {
	grove.BaseModel `grove:"table:users,alias:u"`

	ID    int64  `grove:"id,pk,autoincrement"`
	Name  string `grove:"name,notnull"`
	Email string `grove:"email,notnull,unique"`
	Role  string `grove:"role,notnull"`
}

func main() {
	ctx := context.Background()

	sqlite := sqlitedriver.New()
	if err := sqlite.Open(ctx, "file:hello.db"); err != nil {
		log.Fatal(err)
	}

	db, err := grove.Open(sqlite)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	q := sqlitedriver.Unwrap(db)

	if _, err := q.NewCreateTable((*User)(nil)).IfNotExists().Exec(ctx); err != nil {
		log.Fatal(err)
	}

	alice := &User{Name: "Alice", Email: "alice@example.com", Role: "admin"}
	if _, err := q.NewInsert(alice).OnConflict("DO NOTHING").Exec(ctx); err != nil {
		log.Fatal(err)
	}

	var admins []User
	err = q.NewSelect(&admins).
		Where("role = ?", "admin").
		OrderExpr("id ASC").
		Limit(10).
		Scan(ctx)
	if err != nil {
		log.Fatal(err)
	}

	for _, u := range admins {
		fmt.Printf("%d %s <%s>\n", u.ID, u.Name, u.Email)
	}
}

Run it:

go run .
# 1 Alice <alice@example.com>

Run it twice and you still get one row, because the second insert hits the unique email and ON CONFLICT DO NOTHING skips it. The data lives in hello.db next to your code.

That's the whole pattern. You open a driver, hand it to grove.Open, and call Unwrap on the *grove.DB to get that driver's typed query builder. Swap sqlitedriver for pgdriver and you get pgdriver.Unwrap(db).NewSelect(...), with Postgres-only methods like DistinctOn, ForUpdate and ForShare on it.

Why Grove?

Most Go ORMs give you one query API and translate it for every database. Grove gives each database its own builder, so the SQL (or BSON, or Elasticsearch DSL) you'd write by hand in a console is what you write in Go, and when a database has a feature the others lack, its builder has a method for it and the other builders don't have one.

You need Grove gives you
Queries that look like your database A query builder per driver: $1 placeholders and DISTINCT ON on Postgres, PREWHERE, SAMPLE and FINAL on ClickHouse, bson.M filters and aggregation pipelines on MongoDB
An easy move off bun grove:"..." struct tags with a bun:"..." fallback, so existing bun models load unchanged
Low query overhead Schema metadata cached per type and pooled byte buffers for query building (see the benchmarks)
Schema changes your modules can share Go-code migrations in named groups, ordered by migrate.DependsOn, behind a migration lock
Tenant isolation and audit trails Pre and post query and mutation hooks that can deny a query or inject WHERE filters
Caching, sessions and rate limits A key-value Store with codecs, middleware and ready-made plugins over five KV backends
Offline-first and collaborative apps CRDT fields (crdt:lww, crdt:counter, crdt:set), a sync server, and TypeScript and Dart clients
One way to wire it into a service A Forge extension that reads extensions.grove and provides *grove.DB through DI

The architecture guide explains how the root package, the driver interfaces and the driver modules fit together.

Database drivers

Each driver is its own Go module. You only download the client libraries for the databases you use, so a service that talks to SQLite never pulls in the MongoDB or ClickHouse drivers, and your go.sum stays short.

Database Module Registered as Docs
PostgreSQL github.com/xraph/grove/drivers/pgdriver postgres, pg PostgreSQL driver
MySQL github.com/xraph/grove/drivers/mysqldriver mysql MySQL driver
SQLite github.com/xraph/grove/drivers/sqlitedriver sqlite SQLite driver
Turso and libSQL github.com/xraph/grove/drivers/tursodriver turso Turso driver
MongoDB github.com/xraph/grove/drivers/mongodriver mongodb, mongo MongoDB driver
ClickHouse github.com/xraph/grove/drivers/clickhousedriver clickhouse ClickHouse driver
Elasticsearch github.com/xraph/grove/drivers/esdriver elasticsearch Elasticsearch driver

Importing a driver package registers it under the names above. That's what lets you open a database by name, from config, without naming the package in your code:

import _ "github.com/xraph/grove/drivers/pgdriver"

drv, err := grove.OpenDriver(ctx, "postgres", "postgres://localhost:5432/app?sslmode=disable")
db, err := grove.Open(drv)

The SQL drivers also accept ? in Where, and pgdriver rewrites each one to the next $n for you.

Migrations

Migrations are Go functions. You put them in a named group, and a group can depend on other groups, which is how a library ships its own tables and still runs after your core schema:

var Migrations = migrate.NewGroup("billing", migrate.DependsOn("core"))

func init() {
	Migrations.MustRegister(&migrate.Migration{
		Name:    "create_invoices",
		Version: "20260101000000",
		Up: func(ctx context.Context, exec migrate.Executor) error {
			_, err := exec.Exec(ctx, `CREATE TABLE invoices (id BIGSERIAL PRIMARY KEY, total BIGINT NOT NULL)`)
			return err
		},
		Down: func(ctx context.Context, exec migrate.Executor) error {
			_, err := exec.Exec(ctx, `DROP TABLE invoices`)
			return err
		},
	})
}

migrate.NewOrchestrator(executor, groups...) plans every group in dependency order and gives you Migrate, Rollback and Status. Each SQL driver and MongoDB has a <name>migrate package (pgmigrate, sqlitemigrate, mongomigrate and so on) with the executor, and importing it lets migrate.NewExecutorFor(drv) find the right one. Applied versions go in a tracking table, and a lock table stops two instances from migrating at once. The migrations guide and multi-module migrations cover the rest.

Query hooks

A hook is any type that implements one or more of hook.PreQueryHook, hook.PostQueryHook, hook.PreMutationHook, hook.PostMutationHook or hook.StreamRowHook. You add it to the database with a scope, and it runs on every matching query the drivers build.

This one scopes reads on the orders table to the tenant in the request context, and refuses the query when there isn't one:

type tenantKey struct{}

type TenantScope struct{}

func (TenantScope) BeforeQuery(ctx context.Context, qc *hook.QueryContext) (*hook.HookResult, error) {
	tenant, _ := ctx.Value(tenantKey{}).(string)
	if tenant == "" {
		return &hook.HookResult{Decision: hook.Deny, Error: grove.ErrHookDenied}, nil
	}

	return &hook.HookResult{
		Decision: hook.Modify,
		Filters:  []hook.ExtraFilter{{Clause: "tenant_id = ?", Args: []any{tenant}}},
	}, nil
}

db.Hooks().AddHook(TenantScope{}, hook.Scope{Tables: []string{"orders"}})

Scopes filter by table and by operation (hook.OpSelect, hook.OpInsert, hook.OpUpdate, hook.OpDelete and the bulk variants), and a lower Priority runs first. The audit package is a hook too, for audit logs. See hooks and tenant isolation.

Key-value store

github.com/xraph/grove/kv is a separate module with its own drivers. It gives you Get, Set, MGet, MSet, Delete, Exists, TTL, Expire and Scan over any of them, with values encoded by a codec (JSON by default):

rdb := redisdriver.New()
if err := rdb.Open(ctx, "redis://localhost:6379/0"); err != nil {
	log.Fatal(err)
}

store, err := kv.Open(rdb)
if err != nil {
	log.Fatal(err)
}
defer store.Close()

err = store.Set(ctx, "session:abc", Session{UserID: "u_1"}, kv.WithTTL(30*time.Minute))
Part What's there
Drivers kv/drivers/redisdriver, badgerdriver, boltdriver, dynamodriver, memcacheddriver
Codecs JSON, MessagePack, gob and Protocol Buffers in kv/codec
Middleware cache, circuit breaker, compression, encryption, logging, namespacing, retry and stampede protection in kv/middleware
Plugins counters, leaderboards, distributed locks, queues, rate limiters and sessions in kv/plugins
Forge kv/extension, configured under extensions.grove_kv

Start with the KV overview.

CRDT sync

Tag a field with a CRDT type and Grove records its merge state in a shadow table next to yours. Your table's schema doesn't change. When two devices edit the same record offline and sync later, the field's type decides the result: the latest write wins for lww, increments from both sides add up for counter, and an element added on one side and removed on the other stays in a set.

type Document struct {
	grove.BaseModel `grove:"table:documents"`

	ID        string   `grove:"id,pk"`
	Title     string   `grove:"title,crdt:lww"`
	ViewCount int64    `grove:"view_count,crdt:counter"`
	Tags      []string `grove:"tags,type:jsonb,crdt:set"`
}

crdt.New(crdt.WithNodeID("node-1")) creates the plugin, which you add as a hook on the CRDT tables. Nodes exchange changes through crdt.NewSyncer over HTTP pull and push, with an SSE stream for live updates, and every change carries a hybrid logical clock so nodes agree on order without coordinating. Beyond LWW registers, counters and sets, there are list, document and collaborative text types, plus presence and rooms.

Your frontend talks to the same sync endpoints through @grove-js/crdt (TypeScript, with React hooks) or grove_crdt (Dart and Flutter). The CRDT guide walks through a full setup.

Use it with Forge

The github.com/xraph/grove/extension module plugs Grove into a Forge app. It opens the database during Register, provides *grove.DB through Forge's DI container, adds your hooks and migration groups, reports health, and closes the connection on shutdown. It needs Go 1.26, the same as Forge.

go get github.com/xraph/grove/extension@latest
go get github.com/xraph/grove/drivers/pgdriver@latest
import (
	"github.com/xraph/forge"
	"github.com/xraph/grove"
	"github.com/xraph/grove/drivers/pgdriver"
	_ "github.com/xraph/grove/drivers/pgdriver/pgmigrate"
	groveext "github.com/xraph/grove/extension"
)

app := forge.NewApp(forge.AppConfig{
	Name:        "accounts",
	HTTPAddress: ":8080",
	Extensions: []forge.Extension{
		groveext.New(
			groveext.WithDSN("postgres", "postgres://localhost:5432/accounts?sslmode=disable"),
			groveext.WithMigrations(Migrations),
			groveext.WithHook(TenantScope{}, hook.Scope{Tables: []string{"orders"}}),
		),
	},
})

err := app.Router().GET("/users/count", func(ctx forge.Context) error {
	db, err := forge.Inject[*grove.DB](app.Container())
	if err != nil {
		return err
	}

	n, err := pgdriver.Unwrap(db).NewSelect((*User)(nil)).Count(ctx.Context())
	if err != nil {
		return err
	}

	return ctx.JSON(http.StatusOK, map[string]int64{"users": n})
})

The extension doesn't import any driver itself. You import the driver you want (blank, or by name as above), plus its <name>migrate package if you have migrations. Otherwise WithDSN fails with an unknown-driver error, or migrations fail with no executor.

You get tracing and metrics for free. Every query runs as a client span (SELECT users, COMMIT) under the request or job that issued it, and /_/metrics gets db_client_operation_duration_seconds, labelled by system, operation, table and error type. Outside Forge, pass grove.WithMetrics(m) to grove.Open. The spans go to whatever OpenTelemetry tracer provider is installed. Grove never records the query text or a bound value. See tracing and metrics.

The extension implements forge.MigratableExtension, but a plain app.Run() doesn't apply migrations. Run your app through Forge's cli.NewAppRunner to get migrate up, migrate down and migrate status commands, and call AutoMigrate() on the runner if you want pending migrations applied before the server starts.

Configuration

The extension reads extensions.grove from your Forge config, then falls back to a top-level grove key. YAML wins for the driver and DSN, and options you pass in code fill whatever YAML leaves empty.

extensions:
  grove:
    driver: postgres
    dsn: postgres://localhost:5432/accounts?sslmode=disable
    lock_timeout: 30s
Key Option What it does
driver, dsn WithDSN(driver, dsn), or WithDriver(drv) for an opened driver The single database to open
databases (list of name, driver, dsn) WithDatabaseDSN, WithDatabase Several named databases, each its own *grove.DB, managed by a DBManager
default WithDefaultDatabase(name) Which named database is the default
disable_migrate WithDisableMigrate() Makes Migrate and Rollback return an error
lock_timeout WithLockTimeout(d) How long migrations wait for the lock. 0 uses the migrate default, negative waits for the context deadline
central_migrations WithCentralMigrations() Hands the migration groups to a shared registry that runs every extension's migrations in one ordered pass. Pair it with CentralMigrations in the Forge AppConfig
disable_routes WithDisableRoutes() Skips the CRDT sync routes
base_path WithBasePath(path) Prefix for the CRDT sync routes, /sync by default

WithCRDT(plugin, scope), WithCRDTEntity, WithSyncer and WithDriverFactory are code-only. With WithCRDT, the extension mounts POST /sync/pull, POST /sync/push, GET /sync/stream (SSE) and /sync/ws (WebSocket) under the base path, plus presence, history and room routes. The Forge extension guide and configuration reference go deeper.

Benchmarks

All benchmarks run against in-memory SQLite, comparing Grove with raw database/sql, bun and GORM. You don't need any external services. Run them yourself with make bench, and make bench-update rewrites the tables below and the benchmarks page.

Benchmark tables: insert, select, update, delete, bulk insert, query building and schema cache

Benchmarks generated on 2026-02-22 with go1.25.7 on darwin/arm64. Each benchmark ran 5 times; values are averages.

Insert
Library ns/op B/op allocs/op vs Raw SQL
Raw SQL 4,015 880 20 baseline
Grove 4,381 1,283 28 +9.1%
Bun 8,459 5,470 27 +110.7%
GORM 10,265 4,954 66 +155.7%
SelectOne
Library ns/op B/op allocs/op vs Raw SQL
Raw SQL 4,747 1,096 39 baseline
Grove 5,575 1,458 34 +17.4%
Bun 6,695 5,944 43 +41.0%
GORM 8,070 4,383 81 +70.0%
SelectMulti
Library ns/op B/op allocs/op vs Raw SQL
Raw SQL 58,079 20,984 388 baseline
Grove 45,686 23,413 584 -21.3%
Bun 67,184 23,192 395 +15.7%
GORM 83,217 28,570 765 +43.3%
Update
Library ns/op B/op allocs/op vs Raw SQL
Raw SQL 2,821 520 13 baseline
Grove 3,617 1,010 21 +28.2%
Bun 4,074 5,205 19 +44.4%
GORM 5,468 4,020 49 +93.8%
Delete
Library ns/op B/op allocs/op vs Raw SQL
Raw SQL 3,479 232 8 baseline
Grove 5,083 553 13 +46.1%
Bun 5,147 4,880 12 +47.9%
GORM 6,660 2,856 36 +91.4%
BulkInsert100
Library ns/op B/op allocs/op vs Raw SQL
Raw SQL 151,006 60,141 1524 baseline
Grove 105,572 42,111 1421 -30.1%
Bun 143,532 30,389 224 -4.9%
GORM 187,914 93,706 1251 +24.4%
BulkInsert1000
Library ns/op B/op allocs/op vs Raw SQL
Raw SQL 1,401,957 605,384 16513 baseline
Grove 967,190 409,753 14021 -31.0%
Bun 1,359,612 408,694 2033 -3.0%
GORM 1,782,186 894,166 12051 +27.1%
BuildSelect
Variant ns/op B/op allocs/op
Grove 453 864 11
Bun 825 1,744 17
BuildInsert
Variant ns/op B/op allocs/op
Grove 842 930 19
Bun 1,012 1,425 17
BuildUpdate
Variant ns/op B/op allocs/op
Grove 380 736 11
Bun 597 1,184 14
SchemaCache
Variant ns/op B/op allocs/op
CacheHit 11 0 0
ColdStart 3,123 3,728 75
TagResolution
Variant ns/op B/op allocs/op
GroveTags 3,101 3,728 75
BunFallback 3,398 3,736 74

Documentation

Topic Docs
Install and run a first query Getting started, basic CRUD
Define models and tags Models, dual tag system
Move an app off bun Migrating from bun
Change your schema Migrations
Stream large result sets Streaming, change feeds
Sync data between nodes and clients CRDT, TypeScript client

The exported Go API is on pkg.go.dev. The docs source lives in docs/ in this repository.

FAQ

Can I use Grove without Forge?

Yes. The root module depends only on github.com/xraph/go-utils, and none of the driver modules import Forge. You need Forge only if you use extension or kv/extension.

How do I migrate from bun to Grove?

Keep your models as they are. Grove reads bun:"..." tags when a field has no grove:"..." tag, so you can switch the query code first and rename tags later, one model at a time. The bun migration guide has the details.

Does Grove work with MongoDB and Elasticsearch, or only SQL?

Both. mongodriver builds BSON filters, find queries and aggregation pipelines, and esdriver builds search, aggregation and bulk requests. They share the model layer and hooks with the SQL drivers.

How do I add multi-tenancy to a Go ORM?

Write a pre-query hook that reads the tenant from the context and returns an extra WHERE filter, as in Query hooks, and scope it to your tenant tables. A hook that returns hook.Deny blocks the query outright.

Does Grove generate IDs for my models?

No. You declare the key in a tag, such as grove:"id,pk,autoincrement", and pick the Go type yourself.

Contributing

Issues and focused pull requests are welcome on GitHub. Every driver and the KV layer are separate Go modules, so run commands from the module you changed, with GOWORK=off:

git clone https://github.com/xraph/grove.git
cd grove
GOWORK=off go test -race -count=1 ./...
(cd drivers/pgdriver && GOWORK=off go test -race -count=1 ./...)
make test-all
golangci-lint run ./...
goimports -l -local github.com/xraph/grove .

The Postgres and Redis container tests need a running Docker daemon and skip without one, so check the -v output for SKIP lines. Skip make build, make run and make dev: they point at a cmd/grove that doesn't exist. AGENTS.md has the module list, lint rules and the release workflow. Commits follow Conventional Commits.

License

Grove is available under the MIT License.

Built by Rex Raphael and contributors, as part of the Forge forgery stack.

Documentation

Overview

Package grove is a polyglot Go ORM with native query syntax per database.

Grove provides near-raw performance with driver-specific query builders that expose each database's native idioms. PostgreSQL queries use $1 placeholders and PG-specific features. MySQL queries use ? placeholders and backtick quoting. MongoDB queries use native BSON syntax.

Key Features

  • Native query syntax per driver (no unified DSL)
  • Dual tag system: grove:"..." primary, bun:"..." fallback
  • Zero-reflection hot path (reflect once at registration)
  • Modular migrations with multi-module dependency ordering
  • Privacy hooks for tenant isolation, PII redaction, audit logging
  • Part of the Forge ecosystem (github.com/xraph/forge)

Quick Start

// Define a model
type User struct {
    grove.BaseModel `grove:"table:users,alias:u"`

    ID    int64  `grove:"id,pk,autoincrement"`
    Name  string `grove:"name,notnull"`
    Email string `grove:"email,notnull,unique"`
}

// Connect and register
db, err := grove.Open(pgdriver.New(), "postgres://localhost:5432/mydb")
db.RegisterModel((*User)(nil))

// Query with native PostgreSQL syntax
var users []User
err = db.NewSelect(&users).
    Where("email ILIKE $1", "%@example.com").
    Scan(ctx)

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoRows is returned when a query expected at least one row but found none.
	ErrNoRows = errors.New("grove: no rows in result set")

	// ErrModelNotRegistered is returned when a query references a model type
	// that was not registered via DB.RegisterModel().
	ErrModelNotRegistered = errors.New("grove: model not registered")

	// ErrNotSupported is returned when an operation is not supported by the
	// current driver (e.g., RETURNING on MySQL, transactions on some NoSQL drivers).
	ErrNotSupported = errors.New("grove: operation not supported by driver")

	// ErrDriverClosed is returned when an operation is attempted on a closed DB.
	ErrDriverClosed = errors.New("grove: driver connection pool has been closed")

	// ErrTxDone is returned when an operation is attempted on a committed
	// or rolled-back transaction.
	ErrTxDone = errors.New("grove: transaction has already been committed or rolled back")

	// ErrInvalidDSN is returned when the data source name is malformed.
	ErrInvalidDSN = errors.New("grove: invalid data source name")

	// ErrHookDenied is returned when a privacy hook denies the operation.
	ErrHookDenied = errors.New("grove: hook denied the operation")

	// ErrHookPanic is returned when a hook panics during execution (recovered).
	ErrHookPanic = errors.New("grove: hook panicked during execution")

	// ErrMigrationFailed is returned when a migration function returns an error.
	ErrMigrationFailed = errors.New("grove: migration failed")

	// ErrMigrationLocked is returned when another process holds the migration lock.
	ErrMigrationLocked = errors.New("grove: migration lock held by another process")

	// ErrCyclicDependency is returned when migration group dependencies form a cycle.
	ErrCyclicDependency = errors.New("grove: cyclic dependency in migration groups")

	// ErrDuplicateVersion is returned when two migrations share the same version string.
	ErrDuplicateVersion = errors.New("grove: duplicate migration version")

	// ErrInvalidTag is returned when a struct tag has invalid syntax.
	ErrInvalidTag = errors.New("grove: invalid struct tag syntax")

	// ErrNoPrimaryKey is returned when a model has no field marked as pk.
	ErrNoPrimaryKey = errors.New("grove: model has no primary key field")

	// ErrInvalidRelation is returned when a relation definition is incomplete or incorrect.
	ErrInvalidRelation = errors.New("grove: invalid relation definition")
)

Sentinel errors returned by Grove operations. Use errors.Is() to check for specific error conditions.

Functions

func Drivers

func Drivers() []string

Drivers returns the names of all registered drivers.

func RegisterDriver

func RegisterDriver(name string, factory DriverFactory)

RegisterDriver registers a named driver factory. It is typically called from a driver package's init() function. Subsequent calls with the same name overwrite the previous registration.

Example (in pgdriver package):

func init() {
    grove.RegisterDriver("postgres", func(ctx context.Context, dsn string) (grove.GroveDriver, error) {
        db := New()
        if err := db.Open(ctx, dsn); err != nil {
            return nil, err
        }
        return db, nil
    })
}

Types

type BaseModel

type BaseModel struct{}

BaseModel is embedded in user structs to mark them as Grove models. It carries table-level metadata via struct tags.

Supports both grove:"..." and bun:"..." tags:

type User struct {
    grove.BaseModel `grove:"table:users,alias:u"`
    ID   int64  `grove:"id,pk,autoincrement"`
    Name string `grove:"name,notnull"`
}

When both tags are present on the same struct, grove takes precedence. When only bun tags are present, they are used as fallback for zero-cost migration from bun.

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB is the top-level database handle. It manages the connection pool, model registry, hook engine, and provides the entry point for all database operations.

Create a DB using Open:

pgdb := pgdriver.New()
pgdb.Open(ctx, "postgres://localhost:5432/mydb")
db, err := grove.Open(pgdb)

func Open

func Open(drv GroveDriver, opts ...Option) (*DB, error)

Open creates a new DB with the given driver. The driver must already be connected (call driver.Open before grove.Open).

pgdb := pgdriver.New()
pgdb.Open(ctx, "postgres://localhost:5432/mydb", driver.WithPoolSize(20))
db, err := grove.Open(pgdb)

func (*DB) BeginTx

func (db *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)

BeginTx starts a new transaction. The returned Tx wraps the driver transaction. Use pgdriver.Unwrap(db).BeginTx() for full driver.Tx access.

tx, err := db.BeginTx(ctx, nil)
defer tx.Rollback()
// ... use pgdriver.Unwrap with tx ...
tx.Commit()

func (*DB) Close

func (db *DB) Close() error

Close closes the database connection pool and releases all resources.

func (*DB) Driver

func (db *DB) Driver() GroveDriver

Driver returns the underlying driver.

func (*DB) Hooks

func (db *DB) Hooks() *hook.Engine

Hooks returns the hook engine for registering pre/post query and mutation hooks.

db.Hooks().AddHook(&TenantIsolation{}, hook.Scope{Tables: []string{"users"}})

func (*DB) NewDelete

func (db *DB) NewDelete(model any) any

NewDelete creates a new DELETE query builder via the driver. The returned value should be type-asserted to the driver-specific builder.

For typed access, prefer: pgdriver.Unwrap(db).NewDelete(model)

func (*DB) NewInsert

func (db *DB) NewInsert(model any) any

NewInsert creates a new INSERT query builder via the driver. The returned value should be type-asserted to the driver-specific builder.

For typed access, prefer: pgdriver.Unwrap(db).NewInsert(model)

func (*DB) NewSelect

func (db *DB) NewSelect(model ...any) any

NewSelect creates a new SELECT query builder via the driver. The returned value should be type-asserted to the driver-specific builder (e.g., *pgdriver.SelectQuery).

For typed access, prefer: pgdriver.Unwrap(db).NewSelect(model)

func (*DB) NewUpdate

func (db *DB) NewUpdate(model any) any

NewUpdate creates a new UPDATE query builder via the driver. The returned value should be type-asserted to the driver-specific builder.

For typed access, prefer: pgdriver.Unwrap(db).NewUpdate(model)

func (*DB) Ping

func (db *DB) Ping(ctx context.Context) error

Ping verifies the database connection is alive.

func (*DB) RegisterModel

func (db *DB) RegisterModel(models ...any)

RegisterModel registers model types for later use. Reflection is performed once per model type and cached by the driver.

db.RegisterModel((*User)(nil), (*Post)(nil))

type DriverFactory

type DriverFactory func(ctx context.Context, dsn string) (GroveDriver, error)

DriverFactory is a function that creates and opens a GroveDriver given a DSN. Drivers register factories via RegisterDriver so that callers can create drivers by name (e.g., from YAML configuration) without importing driver packages directly.

Each driver module should register its factory in an init() function or provide an explicit Register() function.

type GroveDriver

type GroveDriver interface {
	// Name returns the driver identifier (e.g., "pg", "mysql", "mongo").
	Name() string

	// Close terminates all connections.
	Close() error

	// Ping checks connectivity.
	Ping(ctx context.Context) error
}

GroveDriver is the minimal interface that a database driver must satisfy to work with the top-level DB handle. This avoids importing the driver package directly, which would create a circular dependency (schema -> grove -> driver -> schema).

The full driver.Driver interface (in the driver package) extends this with query execution methods.

func OpenDriver

func OpenDriver(ctx context.Context, name, dsn string) (GroveDriver, error)

OpenDriver creates and opens a driver by its registered name. Returns an error if no factory is registered for the given name.

type ModelRegistry

type ModelRegistry interface {
	// Register registers a model and returns its cached metadata.
	Register(model any) (any, error)

	// Get returns cached metadata for a registered model, or nil.
	Get(model any) any
}

ModelRegistry is the interface for model metadata caching. The concrete implementation is schema.Registry. This interface exists to avoid an import cycle between grove and schema packages.

type Option

type Option func(*options)

Option configures a DB instance.

func WithCollectionLabel added in v1.9.0

func WithCollectionLabel(fn func(collection string) string) Option

WithCollectionLabel sets how a collection becomes the db.collection.name label on db.client.operation.duration. fn gets the collection as the driver reported it, never empty: for SQL the first table as written, schema included (tenant_42.orders), for MongoDB and Elasticsearch the collection or index. It returns the label value, "" for no collection, or "other" to fold it in with the rest. Values still count toward WithCollectionLabelLimit. fn runs on every recorded call and must be safe for concurrent use.

Without it, a SQL table loses its schema or database qualifier and other names are used as reported. Spans always carry the collection as reported. It has no effect without WithMetrics.

func WithCollectionLabelLimit added in v1.9.0

func WithCollectionLabelLimit(n int) Option

WithCollectionLabelLimit caps the distinct db.collection.name values the driver puts on db.client.operation.duration. Past n, a collection it hasn't seen records as "other", and the ones it has keep their own label. Default: 256 (observability.DefaultCollectionLabelLimit). It has no effect without WithMetrics.

func WithLogger

func WithLogger(l log.Logger) Option

WithLogger sets a structured logger for the DB instance.

func WithMetrics added in v1.8.0

func WithMetrics(m gometrics.Metrics) Option

WithMetrics records the OpenTelemetry db.client.operation.duration histogram on m for every call the driver makes. In a Forge app pass app.Metrics(), and the series lands on /_/metrics as db_client_operation_duration_seconds. grove's Forge extension does this for you.

Spans need no option: drivers start them on the global tracer provider, which forge.New installs. A nil m records nothing.

func WithPoolSize

func WithPoolSize(n int) Option

WithPoolSize sets the maximum number of connections in the pool. Default: 10.

func WithQueryTimeout

func WithQueryTimeout(d time.Duration) Option

WithQueryTimeout sets the default timeout for query execution. Default: 30s.

type Tx

type Tx struct {
	// contains filtered or unexported fields
}

Tx wraps a driver transaction, exposing Commit and Rollback.

func (*Tx) Commit

func (tx *Tx) Commit() error

Commit commits the transaction.

func (*Tx) Raw

func (tx *Tx) Raw() any

Raw returns the underlying driver transaction for advanced usage. The returned value should be type-asserted to the driver-specific Tx type (e.g., driver.Tx for pgdriver).

func (*Tx) Rollback

func (tx *Tx) Rollback() error

Rollback rolls back the transaction.

type TxOptions

type TxOptions struct {
	IsolationLevel int
	ReadOnly       bool
}

TxOptions holds transaction configuration for BeginTx.

Directories

Path Synopsis
Package audit provides a PostMutationHook that logs all mutations to an audit trail (Chronicle).
Package audit provides a PostMutationHook that logs all mutations to an audit trail (Chronicle).
Package crdt provides an optional CRDT (Conflict-Free Replicated Data Type) layer for Grove.
Package crdt provides an optional CRDT (Conflict-Free Replicated Data Type) layer for Grove.
Package driver defines the interfaces that every database backend must implement to work with Grove.
Package driver defines the interfaces that every database backend must implement to work with Grove.
drivers
esdriver module
mongodriver module
mysqldriver module
pgdriver module
sqlitedriver module
tursodriver module
extension module
Package grovetest provides testing utilities for Grove applications.
Package grovetest provides testing utilities for Grove applications.
Package hook provides the privacy and lifecycle hook system for Grove.
Package hook provides the privacy and lifecycle hook system for Grove.
internal
pool
Package pool provides a sync.Pool-based byte buffer pool for minimizing allocations during query building.
Package pool provides a sync.Pool-based byte buffer pool for minimizing allocations during query building.
safe
Package safe provides SQL identifier quoting and sanitization utilities to prevent SQL injection in dynamically constructed queries.
Package safe provides SQL identifier quoting and sanitization utilities to prevent SQL injection in dynamically constructed queries.
tagparser
Package tagparser provides a high-performance struct tag parser for grove:"..." and bun:"..." tags.
Package tagparser provides a high-performance struct tag parser for grove:"..." and bun:"..." tags.
kv module
extension module
Package migrate provides a database-agnostic migration system with multi-module support, dependency ordering, and distributed locking.
Package migrate provides a database-agnostic migration system with multi-module support, dependency ordering, and distributed locking.
Package observability provides hooks for exposing query timing and error metrics.
Package observability provides hooks for exposing query timing and error metrics.
telemetrytest
Package telemetrytest records the spans and metrics grove's drivers emit, so a driver test can assert on them without a Forge app.
Package telemetrytest records the spans and metrics grove's drivers emit, so a driver test can assert on them without a Forge app.
Package plugin provides the interface for Grove plugins and a registry for managing them.
Package plugin provides the interface for Grove plugins and a registry for managing them.
Package scan maps database result sets to Go structs using cached field metadata from the schema package.
Package scan maps database result sets to Go structs using cached field metadata from the schema package.
Package stream provides a generic streaming iterator for database results.
Package stream provides a generic streaming iterator for database results.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL