sow

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 12 Imported by: 0

README

sow

A small, general-purpose SQLite migration runner — a library and a CLI. Point it at any SQLite database and a directory of .sql migration files; it applies the pending ones in order, once, inside a transaction, and records what it applied so re-running is a no-op. Rollbacks run each migration's down section. No ORM, no codegen, no framework.

Read migrations, apply them, sow it up.

Migration files

A migration is a plain .sql file with two sections:

-- +sow up
CREATE TABLE widgets (id INTEGER PRIMARY KEY, name TEXT);

-- +sow down
DROP TABLE widgets;

The file name without .sql is its version and sort key, so use a zero-padded or timestamp prefix to keep order deterministic:

migrations/
  001_create_widgets.sql
  002_add_index.sql
  20260708_backfill.sql

A file with no markers is treated as all-up (no rollback). Statements are split on top-level ; (quotes and -- / /* */ comments are respected).

CLI

sow up     <db> <dir> [--dry-run]     apply all pending migrations
sow down   <db> <dir> [--steps N]     roll back the last N applied (default 1)
sow status <db> <dir>                 show applied / pending
sow create <dir> <name>               scaffold a new timestamped migration file
sow version

Example:

sow create ./migrations "create users"   # writes ./migrations/<ts>_create_users.sql
# ...edit the file...
sow status ./app.db ./migrations
sow up     ./app.db ./migrations
sow down   ./app.db ./migrations --steps 1

Install:

go install github.com/SirNiklas9/sow/cmd/sow@latest

Library

import "github.com/SirNiklas9/sow"

migs, _ := sow.Load("./migrations")
db, _   := sow.Open("./app.db")
defer db.Close()

res, err := db.Up(migs, sow.Options{Logf: log.Printf})
// db.Down(migs, 1, opt) · db.Status(migs) · db.Pending(migs) · db.AppliedVersions()

Applied versions are tracked in a sow_migrations table created automatically. Each migration runs in its own transaction together with its ledger write, so a failure leaves earlier migrations committed and the failing one fully rolled back.

How it works

  • Pure Go — uses modernc.org/sqlite (no CGo).
  • Idempotent — a version already in sow_migrations is skipped.
  • Ordered — migrations apply in version (filename) order; duplicate versions are an error.
  • Reversibledown sections enable sow down; a migration with no down refuses to roll back.

Limitations

  • SQLite only.
  • Statement splitting is on top-level ;. A body containing an inner ; (e.g. a trigger with BEGIN … ; … ; END) should be authored so that block is the file's only statement.

License

MIT © 2026 SirNiklas9.

Documentation

Overview

Package sow is a small, general-purpose SQL migration runner: point it at a SQLite or Postgres database and a directory of `.sql` migration files, and it applies the pending ones in order, once, recording what it applied so re-running is a no-op. Rollbacks run each migration's `down` section. No ORM, no codegen, no framework — read migrations, apply them, sow it up.

The core owns NO SQLite driver: it runs over an injected Conn (see Conn/TxConn), so it compiles to wasm and can run as a Pulp cell that gets SQLite from the host capability, exactly like a native binary gets it from modernc. Use Open (native, CLI) to get a modernc-backed DB, or New(conn) to inject any Conn — e.g. a host-backed one in a cell.

A migration file is plain SQL with two sections:

-- +sow up
CREATE TABLE widgets (id INTEGER PRIMARY KEY, name TEXT);

-- +sow down
DROP TABLE widgets;

The file name (without .sql) is its VERSION and sort key: 001_init.sql, 20260708_x.sql.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Conn

type Conn interface {
	Exec(query string, args ...any) error
	Query(query string, args ...any) ([][]any, error)
}

Conn is the minimal SQLite capability sow runs over. Inject a native (modernc) conn via Open, or a Pulp host-backed conn in a cell — the core imports no driver, so it builds to wasm. Query returns rows as [][]any (one []any per row), matching the host wire shape.

type DB

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

DB is a migration target: any Conn with the sow ledger ensured, speaking one Dialect.

func New

func New(conn Conn) (*DB, error)

New wraps an injected Conn (native, host-backed, or a test fake) and ensures the ledger table exists, speaking SQLite. This is the driver-agnostic constructor; Open is the native convenience. Use NewWithDialect to inject a Conn that talks to something else.

func NewWithDialect

func NewWithDialect(conn Conn, d Dialect) (*DB, error)

NewWithDialect is New over a caller-chosen Dialect — the seam for an injected Conn that is not SQLite (a Pulp cell whose host capability is Postgres, say). The zero Dialect means SQLite, so NewWithDialect(conn, Dialect{}) is exactly New(conn).

func Open

func Open(path string) (*DB, error)

Open opens (or creates) a SQLite database at path (":memory:" allowed) and returns a DB with the ledger ensured, backed by modernc over database/sql.

func OpenPostgres

func OpenPostgres(dsn string) (*DB, error)

OpenPostgres opens a Postgres database at dsn (a URL like "postgres://user:pass@host:5432/db?sslmode=require", or a libpq key/value string) and returns a DB with the ledger ensured, backed by pgx over database/sql. It is the Postgres sibling of Open; the DB it returns speaks Postgres SQL for its ledger.

Note that sql.Open does not dial — a bad host surfaces on the ledger's first statement, which is inside this call, so a returned DB has proven it can reach the database.

func (*DB) AppliedVersions

func (d *DB) AppliedVersions() ([]string, error)

AppliedVersions returns the applied versions in application order.

func (*DB) Close

func (d *DB) Close() error

Close closes the underlying Conn if it is a Closer (the native adapter is); a host-backed Conn the caller owns is left untouched.

func (*DB) Dialect

func (d *DB) Dialect() Dialect

Dialect reports the SQL flavour this DB speaks.

func (*DB) Down

func (d *DB) Down(ms []Migration, n int, opt Options) (Result, error)

Down rolls back the last n applied migrations (n<=0 means 1), newest first. A migration with no Down section is an error (irreversible).

func (*DB) MarkApplied

func (d *DB) MarkApplied(version, name string, at int64) error

MarkApplied records a version as already applied WITHOUT running it — the hook a caller uses to adopt sow over a database migrated by an older mechanism (seed the ledger from a prior version counter so those steps don't re-run). Idempotent.

func (*DB) Pending

func (d *DB) Pending(ms []Migration) ([]Migration, error)

Pending returns migrations not yet applied, in version order.

func (*DB) Status

func (d *DB) Status(ms []Migration) ([]StatusRow, error)

Status reports each migration's applied state, in version order.

func (*DB) Up

func (d *DB) Up(ms []Migration, opt Options) (Result, error)

Up applies every pending migration in order. Each runs atomically when the Conn is a TxConn (native), together with its ledger insert. DryRun reports the plan, changes nothing.

type Dialect

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

Dialect is the SQL flavour sow speaks to a target database. sow ships two: SQLite (the default) and Postgres. The zero value is SQLite, so a DB built without naming a dialect behaves exactly as sow did before dialects existed.

Its fields are unexported on purpose: a Dialect is a value sow hands you (SQLite() or Postgres()), not a plugin point. That keeps the published surface two functions wide and leaves sow free to change what a dialect must know without breaking callers.

func Postgres

func Postgres() Dialect

Postgres returns the Postgres dialect: `ON CONFLICT (version) DO NOTHING` and `$n` placeholders. Pair it with OpenPostgres, or with NewWithDialect over an injected Conn.

func SQLite

func SQLite() Dialect

SQLite returns the SQLite dialect: `INSERT OR IGNORE` and `?` placeholders. This is what Open and New use, and what the zero Dialect resolves to.

func (Dialect) String

func (d Dialect) String() string

String names the dialect, for error messages.

type Manifest added in v0.3.0

type Manifest struct {
	Migrations []string `json:"migrations"`
}

Manifest is an explicit, shippable migration plan. Entries are paths relative to the manifest file and run in the authored order; the filename still supplies each migration's stable version and name.

type Migration

type Migration struct {
	Version string // sort key / unique id (the file name without .sql)
	Name    string // human label (the part after the leading version token)
	Up      string // SQL applied when migrating forward
	Down    string // SQL applied when rolling back ("" = irreversible)
	Path    string // source file, for error messages ("" for in-memory)
}

Migration is one versioned step: its Up SQL, optional Down SQL, and identity.

func Load

func Load(dir string) ([]Migration, error)

Load reads every *.sql file in dir, parses it, and returns them sorted by version.

func LoadFS

func LoadFS(fsys fs.FS, dir string) ([]Migration, error)

LoadFS reads every *.sql migration under dir in an fs.FS — an embed.FS in a wasm cell, os.DirFS on disk, or any other fs.FS — parses them, and returns them sorted by version. This is the driver-agnostic sibling of Load(dir): it uses only io/fs, so it works inside a Pulp cell where there is no os filesystem and migrations ship embedded in the binary.

func LoadManifestFS added in v0.3.0

func LoadManifestFS(fsys fs.FS, manifestPath string) ([]Migration, error)

LoadManifestFS loads migrations in the exact order declared by a JSON manifest. It works with embed.FS, os.DirFS, or any other fs.FS, so an application can ship its migration plan and SQL together without hard-coding schema steps in Go.

func Parse

func Parse(fileName, content string) Migration

Parse turns a file name + contents into a Migration.

type Options

type Options struct {
	DryRun bool                          // plan only — writes nothing
	Logf   func(format string, a ...any) // progress sink (nil = silent)
	Now    func() int64                  // applied_at clock (nil = 0); injectable for tests
}

Options tune an Up/Down run.

type Result

type Result struct {
	Applied []Migration // migrations whose Up ran
	Rolled  []Migration // migrations whose Down ran
}

Result summarizes what a run did.

type StatusRow

type StatusRow struct {
	Migration Migration
	Applied   bool
}

StatusRow pairs a migration with whether it's applied.

type TxConn

type TxConn interface {
	Conn
	RunTx(fn func(Conn) error) error
}

TxConn is a Conn that can run a unit of work atomically. Backends that support transactions (the native modernc adapter) implement it, so each migration is all-or-nothing; a backend that doesn't (a bare host capability) is driven best-effort — statements then ledger, no rollback — which is how a seam-only store already migrates.

Directories

Path Synopsis
cmd
sow command
Command sow is a general-purpose SQLite and PostgreSQL migration runner.
Command sow is a general-purpose SQLite and PostgreSQL migration runner.

Jump to

Keyboard shortcuts

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