herd

package
v0.4.44 Latest Latest
Warning

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

Go to latest
Published: Apr 21, 2026 License: MIT Imports: 18 Imported by: 0

README

Herd

Herd Logo

Herd is a library for applying migrations to a SQL database. Currently, only PostgreSQL is supported, but other database engines may be added in the future.

At this time, only up-migrations are supported. This means that once a migration has been applied, it cannot be un-done. Problematic migrations should therefore be corrected by creating an additional migration.

Migrations

Migrations must implement the herd.Migration interface, providing Version and Migrate methods.

type MyMigration struct {}

// The unique version number for the migration. Migrations are applied in ascending order.
func (m *MyMigration) Version() int64 { /* ... */ }

// Migrate is called to appky the migration.
func (m *MyMigration) Migrate(ctx context.Context, db herd.DB) error { /* ... */ }

Migrations made up of SQL files only can use the built-in herd.FileMigration.

migrations := []herd.Migration{
	herd.NewFileMigration(1, "-- your migration here"),
	herd.NewFileMigration(2, "-- your migration here"),
	herd.NewFileMigration(3, "-- your migration here"),
}

Files in a directory and named in the format {version}_{name}.sql can be automatically gathered.

//go:embed migrations/*.sql
var migrationFS embed.FS

migrations, err := herd.CollectFileMigrations(migrationFS)
if err != nil {
	// handle error
}

Version numbers must be positive integers, but do not need to be consecutive. This is to allow date or time based versions to be used if desired. However, a new version should not be inserted between 2 existing versions. Versions less than the largest applied version are skipped.

When more complex migration logic is required, custom implementations of herd.Migration can be used. These can be used alongside herd.FileMigration instances. Each migration must continue to have a unique version number, regardless of how it is implemented.

Applying Migrations

Once the migrations have been gathered, they can be executed via herd.Herd. This determines which migrations have not yet been applied, and then executes them within a transaction. As PostgreSQL supports schema changes within a transaction, this means the pending migrations are either all applied, or none of them are.

On success, the version before and after the applied migrations is returned.

herder := herd.New(migrations)

result, err := herder.Migrate(ctx, db)
if err != nil {
	// handle error
}

fmt.Println(result.Before, "->", result.After)

To migrate to a specific version and stop, use herd.WithTargetVersion. This causes any pending migrations with a version greater than the target version to be skipped, such that they can be applied at a later time.

herder := herd.New(migrations, herd.WithTargetVersion(100))

result, err := herder.Migrate(ctx, db)
if err != nil {
	// handle error
}

fmt.Println(result.Before, "->", result.After)

Migration Metadata

Herd uses 2 database tables for tracking migrations: herd_system_migrations and herd_user_migrations. herd_system_migrations is for tracking the schema changes to these 2 tables, while herd_user_migrations is for tracking the application of the provided migrations.

-- TODO: add example for each table

The primary use of these tables is for auditing, track when each migration was applied. Additionally, they track the version and commit of the application that executed them. This refers to the application that is importing and using Herd, not Herd itself.

Documentation

Overview

Package herd provides support for running migrations against a PostgreSQL database.

Example
package main

import (
	"context"
	"database/sql"
	"embed"
	"log/slog"

	"github.com/mattdowdell/sandbox/pkg/herd"
)

//go:embed testdata/migrations/*.sql
var migrationFS embed.FS

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

	migrations, _ := herd.CollectFileMigrations(migrationFS)
	migrator, _ := herd.New(migrations)

	db, _ := sql.Open("example", "dsn")

	result, err := migrator.Migrate(ctx, db)
	if err != nil {
		slog.Error("failed to run migrate", slog.Any("error", err))
		return
	}

	slog.Info("migration completed", slog.Any("result", result))
}

Index

Examples

Constants

View Source
const (
	TableNameSystem = "herd_system_migrations"
	TableNameUser   = "herd_user_migrations"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type DB

type DB interface {
	ExecContext(context.Context, string, ...any) (sql.Result, error)
	QueryContext(context.Context, string, ...any) (*sql.Rows, error)
	QueryRowContext(context.Context, string, ...any) *sql.Row
}

DB contains the methods from sql.DB that migrations can use.

type FileMigration

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

FileMigration uses the contents of a SQL file as a migration.

func NewFileMigration

func NewFileMigration(version int64, contents string) *FileMigration

NewFileMigration creates a new FileMigration.

func NewFileMigrationFromFS

func NewFileMigrationFromFS(filesystem fs.FS, path string) (*FileMigration, error)

NewFileMigrationFromFS reads the file at the path from the filesystem. The last element of the path is taken as the filename, and is passed with the contents to NewFileMigrationFromFilename.

func NewFileMigrationFromFilename

func NewFileMigrationFromFilename(filename, contents string) (*FileMigration, error)

NewFileMigrationFromFilename extracts the migration version from the given filename and calls NewFileMigration.

The version must be at the start of the filename followed by a underscore ("_"). For example, 1_initial.sql would have a version of 1. Versions can be padded with 0s to make them easier to view in a file browser. 0001_initial.sql and 1_initial.sql would have the same version.

func (*FileMigration) Migrate

func (m *FileMigration) Migrate(ctx context.Context, db DB) error

Migrate executes the SQL file contents against the database.

func (*FileMigration) Version

func (m *FileMigration) Version() int64

Version returns the migration version.

type Herd added in v0.2.55

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

Herd is used to migrate a database using a set of migrations.

Migrations are applied in order of version, starting from the lowest to highest. Versions do not need to be consecutive. This allows version numbers to be based on dates.

func New added in v0.2.55

func New(migrations []Migration, options ...Option) (*Herd, error)

New creates a new Herd instance with the given migrations.

func (*Herd) Migrate added in v0.2.55

func (h *Herd) Migrate(ctx context.Context, db *sql.DB) (*Result, error)

Migrate migrates the database using the configured migrations.

type Migration

type Migration interface {
	// Version returns the migration version. It must be greater than 0 and unique across all
	// migrations.
	Version() int64

	// Migrate applies the schema or data changes for the migration.
	Migrate(context.Context, DB) error
}

Migration implementations apply schema or data changes to a database.

func CollectFileMigrations

func CollectFileMigrations(filesystem fs.FS) ([]Migration, error)

CollectFileMigrations walks the filesystem, searching for files with a ".sql" extension. For each found file, NewFileMigrationFromFS is called to create the migration.

type Option added in v0.2.55

type Option interface {
	// contains filtered or unexported methods
}

Option adjust the behaviour of Herd.

func WithBuildInfo added in v0.2.55

func WithBuildInfo(info *debug.BuildInfo) Option

WithBuildInfo overrides the soure of the code version and revision used in migration records. Defaults to the output of runtime/debug.ReadBuildInfo.

This option is intended to support unit testing, or when build info is otherwise unavailable.

func WithBuildInfoValues added in v0.4.36

func WithBuildInfoValues(version, revision string) Option

WithBuildInfoValues wraps WithBuildInfo, constructing a runtime/debug.BuildInfo instance from the given values.

  • version should be the version of the application using Herd, e.g. a git tag.
  • revision should be the VCS revision at build time, e.g. a git commit.

Both values must be non-empty.

func WithNowFunc added in v0.2.55

func WithNowFunc(fn func() time.Time) Option

WithNowFunc overrides the use of time.Now for recording when a migration was applied.

This option is intended to support unit testing only.

func WithTargetVersion added in v0.4.36

func WithTargetVersion(version int64) Option

WithTargetVersion causes pending migrations with a version greater than the given value to be skipped. By default, all pending migrations are applied, regardless of version.

This option is intended to allow a number of migrations to be applied, before inserting data and applying the final migration(s). This is useful when simulating a migration on a production database using representative data.

type Result added in v0.2.51

type Result struct {
	// Before is the migration version before applying any migrations. A value of 0 indicates that
	// no migrations had been applied previously.
	Before int64

	// After is the migration version after applying any migrations. After will equal Before when no
	// pending migrations were found.
	After int64
}

Result contains the migration version before and after the pending migrations were applied.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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