CARLOS rastrillo docs

๐Ÿค– migrate

amadan.net/rastrillo/rastrillo/migrate

One ledgered schema mechanism: an ordered, namespaced set of migrations, applied exactly once each and recorded. It replaced the two mechanisms a Rastrillo app used to run side by side โ€” GORM AutoMigrate for models and a raw Migrations []string for framework subsystems.

Migrations is the guide, and it covers the two failure modes that cost data.

Migrations are forward-only

There is no Down. Production rollback for a CARLOS app is a point-in-time restore of the SQLite file the activator replicates, so none is offered instead of offered and untrustworthy.

Apply

func Apply(ctx context.Context, d *db.DB, s *Set) (Result, error)

Runs every migration the ledger does not already record, in order, each in its own BEGIN IMMEDIATE transaction with its ledger row written inside that same transaction, all on one pinned connection.

Three properties follow, and they matter on a platform that can SIGKILL a hibernating app mid-wake. A killed migration rolls back cleanly and the next wake retries. Progress survives across wakes. And two instances booting at once serialise, the loser re-checking the ledger and skipping instead of re-running or failing.

The pinned connection is correctness, not speed. PRAGMA foreign_keys is per-connection, and SQLite's table rebuild has to toggle it outside the transaction.

Result reports Applied, Skipped and Adopted, so your app can log one line at boot.

Set and Merge

type Set struct{ /* ... */ }

func FromFS(fsys fs.FS, namespace string) (*Set, error)
func MustFromFS(fsys fs.FS, namespace string) *Set
func Merge(sets ...*Set) *Set

Merge's argument order is apply order. That is how a package which must run after another states the requirement at the call site instead of in a comment: auth after sessions, because auth's backfill reads the sessions table.

Your app declares two sets. Schema holds your own migrations, and is what generate and check diff against Models. BootSchema is migrate.Merge(sessions.Schema, Schema), everything App() applies. Add a subsystem to BootSchema, never Schema, or check proposes dropping a table your models do not know about.

Set.Add appends, Set.All returns the migrations in order, and Set.Validate reports a malformed set โ€” a bad id, a duplicate within a namespace. The same id in different namespaces is fine, which is what lets every subsystem number from 0001.

Migration

type Migration struct {
	ID  string
	SQL string
	Fn  func(*gorm.DB) error
}

Exactly one of SQL or Fn is set. SQL is the default and the only thing rastrillo migration generate emits.

Fn is the escape hatch for a change SQL cannot express. It runs on the same pinned connection inside the same transaction as its ledger row, so a failure rolls its writes back too โ€” Apply builds it a *gorm.DB backed by that one connection rather than your pool.

Do not reference your live model structs from a Go migration. A model changes over time and would silently change the meaning of a migration that already ran. Copy the struct into the migration.

Checksum and immutability

func Checksum(sql string) string

Every ledger row records one, and Apply refuses to boot when a shipped migration's SQL no longer matches what was recorded.

Whitespace and formatting are ignored, so reformatting an old migration is safe. Changing what it does is not: add a new migration.

Generating and diffing

func Generate(ctx context.Context, ms []Migration, models []any) ([]Change, error)
func SchemaSQL(ctx context.Context, ms []Migration) (string, error)

Generate is what rastrillo migration generate and rastrillo migration check both run. It replays the migrations into an in-memory database, compares the result against your models, and returns the Change list that would close the gap. A Change carries its SQL and whether it is Destructive.

SchemaSQL returns the schema the migrations produce, without a database.

Reading a real database

func Read(ctx context.Context, q Querier) (Snapshot, error)
func Replay(ctx context.Context, ms []Migration) (*Memory, error)
func Stamp(ctx context.Context, conn *sql.Conn, ms []Migration, through string) error

Read snapshots a live schema โ€” its Table, Column and Index values โ€” through any Querier, the small interface both a *sql.Conn and a *Memory satisfy. Replay builds a *Memory containing what the migrations produce, so the two can be compared: Snapshot.Equal ignores DDL formatting and Snapshot.Diff names what differs.

Stamp records migrations as applied without running them, up to through. It is what rastrillo migration baseline calls, and the guide explains why passing through matters and why bare stamping is dangerous.

LedgerDDL is the ledger table's own DDL, exported so baseline can create the table without duplicating its shape.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.