migro

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2026 License: MIT Imports: 17 Imported by: 0

README

Eloquent and Lucid models, Knex-style schema builder, Laravel-style migrations and seeders, on top of GORM.

CI Go reference MIT license Documentation


migro adds what GORM leaves out: Eloquent/Lucid-style models with typed relations, a schema builder, versioned migrations as Go files, DatabaseSeeder-style seeders and factories, events, read replicas, sharding, and an artisan/ace-style CLI. Your GORM models and queries keep working unchanged.

Install

Go 1.25+.

go get github.com/LouisRiverstone/migro@latest
go install github.com/LouisRiverstone/migro/cmd/migro@latest

A quick look

Generate and run a migration, with the app running or not:

migro make:migration create_posts_table
migro migration:run
migro migration:rollback --step=1
// database/migrations/20260101120000_create_posts_table.go
func (CreatePostsTable) Up(ctx context.Context, s *schema.Builder) error {
	return s.CreateTable(ctx, "posts", func(t *schema.Table) {
		t.BigIncrements("id")
		t.ForeignID("user_id").Constrained().OnDelete("CASCADE")
		t.String("title").NotNull().Index()
		t.Timestamps()
	})
}

Declare models with typed relations (names follow Laravel's conventions):

type Post struct {
	orm.BaseModel                                            // ID, CreatedAt, UpdatedAt (SoftDeleteModel adds DeletedAt)
	UserID uint
	Title  string

	Author   orm.BelongsTo[User] `orm:"foreignKey:user_id"`
	Comments orm.MorphMany[Comment] `orm:"morph:commentable"`
	Tags     orm.MorphToMany[Tag] `orm:"morph:taggable"`
}

func (Post) Fillable() []string { return []string{"user_id", "title"} }   // mass-assignment allow-list

Query, load and write like Eloquent/Lucid:

db, _ := migro.Open(migro.Config{Dialect: "postgres", DSN: dsn})
posts := orm.New[Post](db)

page, _ := posts.Query().
	Where("title LIKE ?", "%go%").
	WhereHas("Tags", func(q *gorm.DB) *gorm.DB { return q.Where("name = ?", "orm") }).
	With("Author").With("Comments.User").WithCount("Tags").   // eager loading, never one query per row
	Latest().Paginate(ctx, 1, 20)

post, _ := posts.CreateFrom(ctx, map[string]any{"user_id": 1, "title": "Hello"})
post.Title = "Renamed"
post.IsDirty("title")                                       // true
_ = post.Save(ctx)                                          // UPDATE of the changed columns only
_, _ = post.Comments.Create(ctx, map[string]any{"body": "Nice"})  // fills commentable_type / commentable_id
_, _ = post.Tags.Sync(ctx, 1, 2)                            // pivot writes: Attach, Detach, Sync, Toggle
author, _ := post.Author.Get(ctx)                           // lazy load

Seed it the Laravel way, with a DatabaseSeeder that calls the others in foreign-key order:

type DatabaseSeeder struct{}

func init() { seed.Register(&DatabaseSeeder{}) }

func (DatabaseSeeder) Run(ctx context.Context, db *migro.DB) error {
	return seed.Call(ctx, db, &CountrySeeder{}, &UserSeeder{}, &PostSeeder{})   // one transaction
}

// in PostSeeder: factories with states, sequences, relations and parents
users, _ := UserFactory().Sequence(
	func(i int, u *User) { u.Role = "admin" },
	func(i int, u *User) { u.Role = "editor" },
).For("country").With("posts", 3).CreateMany(ctx, db, 10)
migro db:seed                       # runs DatabaseSeeder
migro migration:fresh --seed        # rebuild the database and seed it

What you get

  • Schema builder and query builder in the style of Knex
  • Models in the style of Eloquent and Lucid: instance API (Save, Delete, Restore, Refresh), dirty tracking, mass assignment, Hidden/Appends serialization, aggregates and Chunk
  • Scopes, casts, resources and collections: local and global query scopes, hashed/encrypted/enum/date/JSON casts, API resources with pagination, and Collection[T] helpers (Filter, KeyBy, GroupBy, Load, Fresh)
  • Relations as typed fields: has-one/many, belongs-to, many-to-many with pivot writes, hasManyThrough, and the polymorphic family (MorphTo, MorphMany, MorphToMany, ...), with With, WithCount, Has/WhereHas and lazy loading
  • Migrations like Laravel: timestamped files, batches, locking, --pretend, migration:diff
  • Seeders and factories like Laravel/Lucid: DatabaseSeeder with seed.Call, --class, --once, states, sequences, For/With, UpdateOrCreateMany and a built-in faker
  • Advanced queries: WhereExists, WhereJSON, full-text search, GroupBy/Having, unions, row locks, Sole, cursor pagination, Cursor/Lazy streaming and relation aggregates (WithSum, WithAvg, WithExists, ...)
  • Lifecycle: type-safe observers, Replicate, WasChanged, quiet saves, default attributes, UUID/ULID keys, Upsert, pruning and Touches
  • Strict mode that fails on lazy loading, query listeners and a query log
  • Schema dump that squashes migrations into one SQL file, plus db:show, db:table, model:show and more make:* generators
  • Test helpers (dbtest): database assertions, query counting, RefreshDatabase
  • Events, named connections, connection pool options, read replicas and sharding

PostgreSQL, CockroachDB, Redshift, MySQL, MariaDB, SQLite, SQL Server and Oracle. PostgreSQL, MySQL and SQLite run in CI; the others are unit-tested but not yet verified against live servers.

Documentation

https://louisriverstone.github.io/migro/: installation, a step-by-step tutorial, how it works, and a page for every feature.

Contributing

Issues and pull requests are welcome. Read CONTRIBUTING.md first.

License

MIT

Documentation

Overview

Package migro is a Lucid/Knex-inspired toolkit on top of GORM: a schema builder, a query builder, a generic model layer and a migration runner.

Register the dialects you need with a blank import:

import _ "github.com/LouisRiverstone/migro/dialect/postgres"

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNotFound           = &Error{Code: NotFound, Message: "record not found"}
	ErrMigrationLocked    = &Error{Code: MigrationLocked}
	ErrUnsupportedFeature = &Error{Code: UnsupportedFeature}
	ErrPoolAcquireTimeout = &Error{Code: PoolAcquireTimeout}
	ErrEventAborted       = &Error{Code: EventAborted}
)

Sentinels for errors.Is.

Functions

func Sticky

func Sticky(ctx context.Context) context.Context

Sticky returns a context that sends reads to the primary once a write has run with it (Laravel's "sticky" connection option). Use one per request or job: reads that come before the first write still go to a replica, reads after it see the data just written.

It only matters with Config.Replicas. Pass the returned context to every call of the unit of work.

Types

type Code

type Code string

Code is a stable, machine-readable error identifier.

const (
	UnsupportedDialect Code = "UNSUPPORTED_DIALECT"
	ConnectionFailed   Code = "CONNECTION_FAILED"
	NotFound           Code = "NOT_FOUND"
	MigrationLocked    Code = "MIGRATION_LOCKED"
	MigrationFailed    Code = "MIGRATION_FAILED"
	ChecksumMismatch   Code = "MIGRATION_CHECKSUM_MISMATCH"
	InvalidSchema      Code = "INVALID_SCHEMA"
	UnsupportedFeature Code = "UNSUPPORTED_FEATURE"
	QueryFailed        Code = "QUERY_FAILED"
	InvalidConfig      Code = "INVALID_CONFIG"
	PoolAcquireTimeout Code = "POOL_ACQUIRE_TIMEOUT"
	EventAborted       Code = "EVENT_ABORTED"
	EventHandlerFailed Code = "EVENT_HANDLER_FAILED"
	SeedFailed         Code = "SEED_FAILED"
	DiffFailed         Code = "DIFF_FAILED"
	ShardNotFound      Code = "SHARD_NOT_FOUND"
	ShardFailed        Code = "SHARD_FAILED"
	UnknownMorphType   Code = "UNKNOWN_MORPH_TYPE"
	ProductionBlocked  Code = "PRODUCTION_BLOCKED"
	MassAssignment     Code = "MASS_ASSIGNMENT"
	UnboundModel       Code = "UNBOUND_MODEL"
	RelationNotLoaded  Code = "RELATION_NOT_LOADED"
	CastFailed         Code = "CAST_FAILED"

	LazyLoadingViolation Code = "LAZY_LOADING_VIOLATION"
	MultipleRecords      Code = "MULTIPLE_RECORDS_FOUND"
	ConnectionNotFound   Code = "CONNECTION_NOT_FOUND"
	PruneFailed          Code = "PRUNE_FAILED"
)

func CodeOf

func CodeOf(err error) Code

CodeOf extracts the migro Code from err, or "" when err is not a migro error.

type Config

type Config struct {
	// Dialect name: postgres, cockroachdb, redshift, mysql, mariadb, sqlite, sqlserver, oracle.
	Dialect string
	DSN     string
	// Gorm is optional; nil uses GORM defaults with a silent logger.
	Gorm *gorm.Config

	// Pool configures the connection pool (Knex-style).
	Pool Pool

	// Replicas are read-only DSNs of the same dialect. Reads go to a replica, writes and
	// transactions to the primary. Replication lag can make a read right after a write stale;
	// use DB.WriteDB() to force the primary, or migro.Sticky(ctx) to do it after the first write of a request.
	Replicas []string
	// ReplicaPolicy is "random" (default) or "round-robin".
	ReplicaPolicy string

	// Deprecated: use Pool.Max.
	MaxOpenConns int
	// Deprecated: caps idle connections; use Pool.Min to keep connections warm.
	MaxIdleConns int
	// Deprecated: use Pool.MaxLifetime.
	ConnMaxLifetime time.Duration
}

Config configures Open.

type Conn

type Conn interface {
	Exec(ctx context.Context, query string) error
}

Conn is what an AfterCreate hook can do with a fresh connection.

type DB

type DB struct {
	Gorm    *gorm.DB
	Dialect dialect.Dialect
	// contains filtered or unexported fields
}

DB bundles a *gorm.DB with the dialect it was opened for.

func Adopt

func Adopt(g *gorm.DB, d dialect.Dialect) *DB

Adopt adopts an existing *gorm.DB.

func Open

func Open(cfg Config) (*DB, error)

Open connects using a registered dialect.

func OpenContext

func OpenContext(ctx context.Context, cfg Config) (*DB, error)

OpenContext is Open with a context bounding connection setup.

func (*DB) Close

func (d *DB) Close() error

Close releases every connection pool (primary and replicas).

func (*DB) DisableQueryLog

func (d *DB) DisableQueryLog()

DisableQueryLog stops recording; the recorded statements are kept until FlushQueryLog.

func (*DB) EnableQueryLog

func (d *DB) EnableQueryLog()

EnableQueryLog starts recording statements (Laravel's DB::enableQueryLog).

func (*DB) FlushQueryLog

func (d *DB) FlushQueryLog()

FlushQueryLog discards the recorded statements.

func (*DB) Listen

func (d *DB) Listen(fn func(QueryEvent)) (off func())

Listen calls fn after every statement the database runs (Laravel's DB::listen). The returned function unsubscribes. fn runs on the goroutine that ran the statement and must not block.

func (*DB) Name

func (d *DB) Name() string

Name is the name the database was registered under in a Manager, or "" for a standalone one.

func (*DB) Ping

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

Ping verifies connectivity.

func (*DB) QueryLog

func (d *DB) QueryLog() []QueryEvent

QueryLog returns a copy of the recorded statements.

func (*DB) SetStrict

func (d *DB) SetStrict(s Strict)

SetStrict replaces the strictness settings. Transactions and WriteDB handles share them.

func (*DB) ShouldBeStrict

func (d *DB) ShouldBeStrict(on bool)

ShouldBeStrict switches every strictness check on or off.

func (*DB) Strictness

func (d *DB) Strictness() Strict

Strictness returns the current settings.

func (*DB) Transaction

func (d *DB) Transaction(ctx context.Context, fn func(tx *DB) error) error

Transaction runs fn in a transaction; a non-nil error rolls back. fn receives a *DB bound to the transaction, usable with schema, query, orm and migrate.

func (*DB) WithGorm

func (d *DB) WithGorm(g *gorm.DB) *DB

WithGorm returns a handle on g that shares this database's dialect, strictness and listeners.

func (*DB) WriteDB

func (d *DB) WriteDB() *DB

WriteDB returns a handle whose queries all go to the primary (read-your-writes).

type Error

type Error struct {
	Code    Code
	Message string
	Field   string
	Err     error
}

Error is the single error type returned across migro boundaries. Match with errors.Is(err, migro.ErrNotFound) or errors.As.

func Newf

func Newf(code Code, format string, args ...any) *Error

Newf builds an *Error without a cause.

func Wrap

func Wrap(code Code, cause error, format string, args ...any) *Error

Wrap builds an *Error around a cause.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches on Code so sentinels work with errors.Is.

func (*Error) Unwrap

func (e *Error) Unwrap() error

type Manager

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

Manager holds named database connections and opens each one the first time it is asked for (Laravel's DB::connection). Use cluster.Cluster instead to route by key across shards.

func NewManager

func NewManager(def string, cfgs map[string]Config) (*Manager, error)

NewManager registers connections by name; def is the one Default and Connection("") return. Nothing connects until a connection is first used.

func (*Manager) Close

func (m *Manager) Close() error

Close closes every opened connection.

func (*Manager) Connection

func (m *Manager) Connection(ctx context.Context, name string) (*DB, error)

Connection returns the named database, opening it on first use. An empty name means the default.

func (*Manager) Default

func (m *Manager) Default(ctx context.Context) (*DB, error)

Default returns the default connection.

func (*Manager) Names

func (m *Manager) Names() []string

Names lists the configured connections, opened or not.

func (*Manager) Purge

func (m *Manager) Purge(name string) error

Purge closes a connection and forgets it; the next Connection call reopens it (Laravel's DB::purge).

type Pool

type Pool struct {
	// Max open connections (Knex pool.max). 0 = unlimited.
	Max int
	// Min connections kept warm (Knex pool.min). Best effort: connections are opened at Open
	// and kept idle, but database/sql may still close them after IdleTimeout or MaxLifetime.
	Min int
	// AcquireTimeout bounds the wait for a free connection (Knex acquireConnectionTimeout).
	// Exceeding it returns POOL_ACQUIRE_TIMEOUT. 0 = wait until the context ends.
	AcquireTimeout time.Duration
	// IdleTimeout closes connections idle longer than this (Knex idleTimeoutMillis).
	IdleTimeout time.Duration
	// MaxLifetime recycles connections older than this.
	MaxLifetime time.Duration
	// Validate pings a pooled connection on checkout and discards it when the ping fails
	// (Knex pool.validate). Only effective for drivers implementing driver.Pinger.
	Validate bool
	// AfterCreate runs once for every new physical connection (Knex pool.afterCreate),
	// e.g. SET TIME ZONE or PRAGMA statements. An error discards the connection.
	AfterCreate func(ctx context.Context, conn Conn) error
	// contains filtered or unexported fields
}

Pool mirrors Knex's pool options on top of database/sql.

type QueryEvent

type QueryEvent struct {
	SQL      string
	Rows     int64
	Duration time.Duration
	Err      error
}

QueryEvent describes one executed statement.

type Strict

type Strict struct {
	// LazyLoading makes a relation's Get fail with LAZY_LOADING_VIOLATION when the relation was not
	// eager loaded (Eloquent's preventLazyLoading). Models that are not persisted yet are exempt.
	LazyLoading bool
	// OnLazyLoad, when set, is called instead of failing. Use it to log the violation in production
	// while still failing in development (Eloquent's handleLazyLoadingViolationUsing).
	OnLazyLoad func(model, relation string)
}

Strict turns silent mistakes into errors. It is off by default, so enabling it is opt-in per database (Eloquent's Model::shouldBeStrict()).

Mass assignment and unknown columns are always strict in migro: Fill fails with MASS_ASSIGNMENT and GetAttribute fails with INVALID_SCHEMA, so the only switch that remains is lazy loading.

Directories

Path Synopsis
Package cluster routes work across several independent databases (application-level sharding).
Package cluster routes work across several independent databases (application-level sharding).
cmd
migro command
Command migro is the artisan/ace-style CLI.
Command migro is the artisan/ace-style CLI.
Package console is the artisan/ace-style command line: migration:run, migration:rollback --step=N, make:migration, db:seed and friends.
Package console is the artisan/ace-style command line: migration:run, migration:rollback --step=N, make:migration, db:seed and friends.
Package dbtest has test helpers for code that uses migro: database assertions, a database that rolls back when the test ends, and a query counter (Laravel's assertDatabaseHas, RefreshDatabase and assertQueryCount).
Package dbtest has test helpers for code that uses migro: database assertions, a database that rolls back when the test ends, and a query counter (Laravel's assertDatabaseHas, RefreshDatabase and assertQueryCount).
Package dialect defines the per-database adapter contract and a registry.
Package dialect defines the per-database adapter contract and a registry.
mysql
Package mysql registers MySQL and MariaDB.
Package mysql registers MySQL and MariaDB.
oracle
Package oracle registers Oracle Database using the pure-Go go-ora driver (no cgo).
Package oracle registers Oracle Database using the pure-Go go-ora driver (no cgo).
postgres
Package postgres registers PostgreSQL, CockroachDB and Redshift.
Package postgres registers PostgreSQL, CockroachDB and Redshift.
sqlite
Package sqlite registers SQLite using the pure-Go (no cgo) glebarez driver.
Package sqlite registers SQLite using the pure-Go (no cgo) glebarez driver.
sqlserver
Package sqlserver registers Microsoft SQL Server.
Package sqlserver registers Microsoft SQL Server.
Package events is a Lucid-style in-process event emitter plus model lifecycle events.
Package events is a Lucid-style in-process event emitter plus model lifecycle events.
integration
database/factories
Package factories holds model factories (Laravel's database/factories, Lucid's factories).
Package factories holds model factories (Laravel's database/factories, Lucid's factories).
database/migrations
Package migrations holds one Laravel-style migration class per file, named <yyyymmddhhmmss>_<snake_case>.go and registered from init().
Package migrations holds one Laravel-style migration class per file, named <yyyymmddhhmmss>_<snake_case>.go and registered from init().
database/seeders
Package seeders holds Laravel-style seeder classes, registered from init().
Package seeders holds Laravel-style seeder classes, registered from init().
models
Package models holds the integration database's models, written the way an application would: BaseModel for the instance API, relations declared as typed fields.
Package models holds the integration database's models, written the way an application would: BaseModel for the instance API, relations declared as typed fields.
internal
testdb
Package testdb opens the database used by tests.
Package testdb opens the database used by tests.
Package migrate runs versioned schema migrations in batches, Knex-style.
Package migrate runs versioned schema migrations in batches, Knex-style.
diff
Package diff compares GORM models with a live database and writes the difference as migration files (the same .up.sql / .down.sql format migrate.FromFS reads).
Package diff compares GORM models with a live database and writes the difference as migration files (the same .up.sql / .down.sql format migrate.FromFS reads).
Package orm is a Lucid-style model layer: a generic repository over GORM models.
Package orm is a Lucid-style model layer: a generic repository over GORM models.
Package query is a Knex-style fluent query builder for tables without a model.
Package query is a Knex-style fluent query builder for tables without a model.
Package schema is a Knex-style DDL builder compiled per dialect.
Package schema is a Knex-style DDL builder compiled per dialect.
Package seed provides Lucid-style factories and seeders.
Package seed provides Lucid-style factories and seeders.

Jump to

Keyboard shortcuts

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