AnnouncementsIntroducing Arkoren

    Menu

    Overview

    Define, register, apply, inspect, roll back, and test schema or data changes.

    What's on this page
    1. Define ordered schema and data steps with stable migration identities.
    2. Register changes, apply pending work, and inspect recorded history.
    3. Plan reversible changes and package independent migration sequences.
    On this page

    A migration is a named, ordered change to your database schema or data. The migrator executes its steps and records completion in a bookkeeping table, so later runs can identify pending work. Your application owns registration and execution; constructing a database driver does not run migrations.

    Assume an already configured db from Database Drivers. Import arkoren.dev/core/service/database/migrate, arkoren.dev/foundation/query/schema, and arkoren.dev/foundation/query where used.

    Defining Changes

    To introduce a users table, first describe the table:

    users := schema.Create("users", func(table *schema.Table) {
    	table.ID()
    	table.String("email", 255).Unique()
    })

    This prepares a schema operation, not a database call. Attach it to a stable migration identity:

    createUsers := migrate.NewMigration("create-users", func(migration *migrate.MigrationBuilder) {
    	migration.Up(migrate.Schema(users))
    	migration.Down(migrate.Schema(schema.Drop("users")))
    })

    Up is required and contains one or more steps. Down is optional; omitting it marks the migration irreversible. A drop reverses this schema addition but destroys its data, so do not equate reversible schema with recoverable data. Definition construction is synchronous and performs no SQL.

    An ID is 1–191 printable UTF-8 bytes with no whitespace. Prefer a short change name such as add-users-email-index; numeric or date prefixes do not control execution order. Never rename a recorded ID: the new name becomes pending and the old name becomes missing. Keep definitions append-only and preserve their relative order after application. Do not edit an applied Up definition to change existing databases: add a new migration instead.

    Migration Steps

    Use migrate.Schema for schema operations and migrate.Exec for SQL statements. Both accept several arguments and run them in written order. For example, a data change can normalize existing values:

    normalizeEmails := migrate.NewMigration("normalize-user-emails", func(migration *migrate.MigrationBuilder) {
    	migration.Up(migrate.Exec(query.Raw("UPDATE users SET email = LOWER(email)")))
    })

    This irreversible example assumes case normalization is an approved application rule and existing uniqueness constraints permit the result. Test on representative data before deployment; a SQL expression is not a substitute for deciding data policy.

    For logic that needs the current dialect or several facade calls, use a function step with imports context and arkoren.dev/core/service/database:

    step := func(ctx context.Context, db *database.Database) error {
    	_, err := db.Exec(ctx, query.Raw("UPDATE users SET email = LOWER(email)"))
    	return err
    }

    Pass step to migration.Up(step) instead of migrate.Exec. Always forward the supplied context: it carries the migration’s retained session and any active transaction. A fresh context or direct db.Driver() call bypasses the lock-owning connection and can deadlock a one-connection pool. Do not retain the context or use it concurrently. Close streaming cursors before later statements or boundaries.

    Schema and Exec report operation or statement positions on failure. Empty step helpers and nil statements are detected when the step runs, not by Add. Schema operation construction errors are checked before that helper executes any of its operations; dialect-specific failures occur during compilation.

    Use migration.Only(query.Postgres) only when the entire migration is intended for PostgreSQL. Add rejects it on another dialect rather than silently skipping it. For a change with different implementations per dialect, leave it unrestricted and switch on db.Dialect() inside a function step.

    Applying Changes

    Create a migrator and register definitions in dependency order:

    migrator := migrate.New(db)
    err := migrator.Add(createUsers, normalizeEmails)

    New performs no I/O. Add registers all supplied definitions or none; invalid definitions and duplicate IDs return migrate.ErrInvalidMigration and migrate.ErrDuplicateID. After handling registration errors, apply pending work:

    err := migrator.Up(ctx)

    Up takes a named lock, prepares the bookkeeping table, reads history, and runs pending definitions in Add order. The default table and lock name are both arkoren_migrations. A successful run records completed migrations in one new batch; another run skips recorded IDs. Each migration commits independently when transactional DDL is supported. There is no operation-wide rollback: earlier commits survive a later failure. Read Deployment before using this in production or running nontransactional changes.

    Inspecting History

    To compare registered definitions with recorded completion:

    statuses, err := migrator.Status(ctx)

    The result lists registered definitions in Add order, then recorded but unregistered IDs with Missing. Each entry includes ID, Group, Ran, Batch, RanAt, and OutOfOrder. Status takes the same lock and creates the ledger on a fresh database: it is not a read-only connection probe or a dry run. An acquisition, session, inspection, or cleanup failure returns nil rather than an unprotected partial snapshot.

    Reverting Changes

    Before rollback, review the selected Down steps, take an appropriate backup, and ensure running code tolerates the previous schema. To revert only the latest applied migration:

    err := migrator.Rollback(ctx, migrate.Steps(1))

    Without Steps, rollback selects the last batch. It processes newest batches first and reverse Add order within a batch, not timestamp order. It validates all targets before executing any: missing definitions and irreversible targets fail with ErrUnknownMigration and ErrIrreversible. An unknown definition takes precedence over known targets in its batch, including with Steps(1); restore the definitions rather than guessing which known member to revert. Preserve the original relative Add order so rollback can reconstruct application order. Earlier successful Down commits remain final if a later Down fails.

    Package Migrations

    A reusable package can return definitions in its own group:

    definitions := migrate.NewGroup("acme.users").Migrations(createUsers)
    err := migrator.Add(definitions...)

    The group prefixes short IDs, producing acme.users/create-users. Grouped short IDs cannot contain /, and the complete ID must still fit 191 bytes. Definitions already assigned to a group cannot be grouped again. Each group is an independent append-only sequence for out-of-order checks, while overall Add order remains run order. Add package migrations before application changes that use their tables.

    Test definitions against each supported real database engine: apply to an empty database, rerun to check recorded work is skipped, and verify schema and data. If Down exists, test its effects separately. Also exercise failure halfway through nontransactional work; a successful clean install does not establish safe recovery.

    For definitions, steps, and execution, see the migrate API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive