v3.migrations #
V3 ORM migrations
v3.migrations provides ordered, reversible ORM migrations for the V3 compiler. It records applied versions in schema_migrations, uses transactions when the database supports transactional DDL, and supports migrate, rollback, redo, reset, target-version, and status workflows.
Mutating workflows hold a database-level lock from before the applied-version snapshot through all callbacks and history updates. PostgreSQL uses an advisory lock, MySQL uses a named lock, and SQLite uses an immediate transaction so concurrent runners cannot apply the same migration. MySQL lock names use a qualified history table's database, or otherwise the current database, so independent databases on one server do not contend, and follow the server's lower_case_table_names mode. PostgreSQL lock keys use the effective history schema and the full signed 64-bit advisory-lock space. A migrator retains its resolved database or schema across workflow calls, so callback namespace changes cannot redirect later locks or history access. Unqualified SQLite history tables are pinned to main, preventing TEMP tables from shadowing persistent history.
import db.sqlite
import v3.migrations
fn create_users(mut ctx migrations.Context) ! {
ctx.create_table(migrations.Table{
name: 'users'
columns: [
migrations.Column{
name: 'email'
kind: .varchar
nullable: false
},
]
})!
}
fn drop_users(mut ctx migrations.Context) ! {
ctx.drop_table('users')!
}
mut db := sqlite.connect('app.db')!
mut migrator := migrations.new(mut db, [
migrations.Migration{
version: 20260816143000
name: 'create_users'
up: create_users
down: drop_users
},
], migrations.Config{
dialect: .sqlite
})!
migrator.migrate()!
Migration callbacks receive a context that implements orm.Connection. Existing ORM DDL and DML can therefore be mixed with migration helpers:
fn create_users(mut ctx migrations.Context) ! {
sql ctx {
create table User
}!
}
The schema helpers include table and column creation/removal/renaming, indexes, inline or altered foreign keys, and trusted raw SQL via ctx.execute(). SQLite cannot directly change a column or add/remove a foreign key on an existing table; those helpers return an error so the migration can explicitly rebuild the table. SQLite add_column also rejects primary-key, unique, and auto-increment columns, non-nullable columns without a non-NULL default, and nonconstant defaults even when prohibited expressions have unary signs, SQL comments, or postfix clauses. Parenthesized and signed literal defaults remain allowed. Numeric defaults require a mantissa and a complete exponent; digit separators require SQLite 3.46.0 or newer. Constant CAST expressions, including signed casts, are also accepted, while casts resolving to NULL are rejected for NOT NULL columns. Casts of functions, column identifiers, or current-time values remain rejected. SQLite index tables and foreign-key targets must be unqualified; index removal derives the index schema from a qualified table or resolves an unqualified table using SQLite lookup order. Index creation resolves the table and qualifies an unqualified index name with the same schema; an explicitly qualified index name selects its attached database. PostgreSQL change_column supports type-related fields only and rejects explicitly supplied constraint options, including false or empty values, before executing SQL; use ctx.execute() for explicit constraint DDL. PostgreSQL serial columns reject explicit defaults, and index removal derives the index schema from a qualified table; PostgreSQL index names are unqualified when adding them. SQLite non-integer primary keys are explicitly non-nullable. Decimal scale requires a positive precision. MySQL change_column requires nullable, default, and auto-increment attributes; omitted key options, including those on auto-increment columns, are preserved, additions use true, and removals must use remove_index() or raw SQL. MySQL auto-increment columns must be primary keys or unique, MySQL index names must be unqualified when adding or removing them, and tables cannot contain more than one auto-increment column. MySQL foreign keys reject SET DEFAULT. Column-level identifiers must be unqualified. Generated PostgreSQL and MySQL index and foreign-key names are shortened deterministically to their dialect limits; every generated index name and every generated PostgreSQL or MySQL foreign-key identity receives a deterministic component-aware hash suffix, and overlong explicit names are rejected. Caller-supplied table, column, and history-table name components are also checked against those dialect limits, and qualified table, history, or index names may contain at most two components. SQLite and MySQL reject case-insensitive duplicate table columns. PostgreSQL and SQLite table rename targets must also be unqualified; MySQL keeps support for qualified table targets. MySQL migrations default to non-transactional execution because MySQL DDL implicitly commits. MySQL history strings use hex literals, while PostgreSQL uses explicit escape strings with doubled backslashes, avoiding session-mode-sensitive escaping. The migrator accepts orm.TransactionalConnection implementations, and Config.transaction_mode can override whether per-migration transaction methods are used for DDL. SQLite's immediate lock transaction still covers each mutating workflow; failed acquisition and commit paths roll back and remove their temporary transaction probes. Migration names containing NUL bytes are rejected before any database access. PostgreSQL migrations reject orm.DB decorators without probing their transactions; pass a direct session-pinned pg.Conn without an active transaction. Existing transactions, including pg.Tx, are rejected in every transaction mode so the session lock cannot be released before their work commits. Unqualified PostgreSQL history tables resolve an existing persistent relation before falling back to the normal creation schema for inspection, creation, and lock namespacing. Temporary relations are ignored unless explicitly qualified in Config.table. PostgreSQL transactions opened by callbacks in never mode are rolled back and rejected before the advisory migration lock is released, including when an aborted transaction makes the history write fail. In transactional modes, callbacks cannot end the migrator-owned PostgreSQL transaction before history is written. MySQL always mode verifies an owned savepoint before writing history. SQLite callbacks likewise cannot end or replace the original immediate lock transaction before the history write. MySQL migrations and inspections also reject connections with active transactions or disabled session autocommit, using a unique savepoint name for each transaction-state probe. An unqualified MySQL history table is resolved and retained on first use, so later database changes on the same connection cannot redirect history operations or lock namespacing. Callback-created MySQL transaction state is rolled back and rejected before the named migration lock is released.
fn create_orm_table #
fn create_orm_table[T](mut ctx Context) !
create_orm_table creates the table represented by T through V's ORM metadata.
fn drop_orm_table #
fn drop_orm_table[T](mut ctx Context) !
drop_orm_table drops the table represented by T through V's ORM metadata.
fn new #
fn new(mut conn orm.TransactionalConnection, registered []Migration, config Config) !Migrator
new creates and validates a migrator. It does not access the database until one of the migration or inspection methods is called.
fn ColumnType.from #
fn ColumnType.from[W](input W) !ColumnType
fn Dialect.from #
fn Dialect.from[W](input W) !Dialect
fn MigrationOperation.from #
fn MigrationOperation.from[W](input W) !MigrationOperation
fn MigrationState.from #
fn MigrationState.from[W](input W) !MigrationState
fn TransactionMode.from #
fn TransactionMode.from[W](input W) !TransactionMode
type MigrationFn #
type MigrationFn = fn (mut Context) !
MigrationFn changes the schema through a migration Context.
enum ColumnType #
enum ColumnType {
boolean
integer
bigint
real
double_precision
text
varchar
blob
date
datetime
timestamp
json
jsonb
uuid
decimal
}
ColumnType is a portable subset of common SQL column types.
enum Dialect #
enum Dialect {
sqlite
pg
mysql
}
Dialect selects the SQL emitted by schema helpers.
enum MigrationState #
enum MigrationState {
applied
pending
missing
}
MigrationState describes the relationship between code and database history.
enum TransactionMode #
enum TransactionMode {
automatic
always
never
}
TransactionMode controls whether each migration is wrapped in a transaction.
struct AppliedMigration #
struct AppliedMigration {
pub:
version i64
name string
applied_at string
}
AppliedMigration is a migration recorded in the database history table.
struct Column #
struct Column {
pub:
name string
kind ColumnType
limit int
precision int
scale int
nullable ?bool
default_sql ?string
primary_key ?bool
auto_increment ?bool
unique ?bool
}
Column describes a column used by create_table, add_column, or change_column. default_sql is inserted verbatim and should contain a trusted SQL expression. Constraint fields are optional so change_column can distinguish omitted options from explicit false or empty values.
struct Config #
struct Config {
pub:
dialect Dialect
table string = 'schema_migrations'
transaction_mode TransactionMode = .automatic
}
Config configures a Migrator.
struct Context #
struct Context {
mut:
conn orm.Connection
sqlite_runtime_version ?int
pub:
dialect Dialect
}
Context is passed to migration callbacks. It implements orm.Connection, so normal V3 sql ctx { ... } ORM statements can be used alongside schema helpers.
fn (Context) execute #
fn (mut ctx Context) execute(query string) ![]orm.Row
execute runs trusted raw SQL through the migration connection.
fn (Context) create_table #
fn (mut ctx Context) create_table(table Table) !
create_table creates a table from a Rails-style table definition.
fn (Context) drop_table #
fn (mut ctx Context) drop_table(name string) !
drop_table drops a table by name.
fn (Context) rename_table #
fn (mut ctx Context) rename_table(from string, to string) !
rename_table renames a table. PostgreSQL and SQLite targets must be unqualified.
fn (Context) add_column #
fn (mut ctx Context) add_column(table string, column Column) !
add_column adds a column to an existing table.
fn (Context) remove_column #
fn (mut ctx Context) remove_column(table string, column string) !
remove_column removes a column from an existing table.
fn (Context) rename_column #
fn (mut ctx Context) rename_column(table string, from string, to string) !
rename_column renames a column.
fn (Context) change_column #
fn (mut ctx Context) change_column(table string, column Column) !
change_column changes a column definition. SQLite requires a table rebuild. PostgreSQL supports type-related fields only and rejects constraint changes before executing SQL; use execute for explicit PostgreSQL constraint DDL. MySQL requires nullable, default_sql, and auto_increment to be supplied because MODIFY COLUMN replaces those attributes. Omitted key options are preserved.
fn (Context) add_index #
fn (mut ctx Context) add_index(index Index) !
add_index adds an index. When name is empty, a deterministic Rails-style index_<table>_on_<columns> name is used. SQLite tables and PostgreSQL/MySQL index names must be unqualified.
fn (Context) remove_index #
fn (mut ctx Context) remove_index(table string, name string) !
remove_index removes an index by name. PostgreSQL and SQLite derive the index schema from a qualified table unless name is already qualified. MySQL index names must be unqualified.
fn (Context) add_foreign_key #
fn (mut ctx Context) add_foreign_key(key ForeignKey) !
add_foreign_key adds a named foreign-key constraint. SQLite cannot add one to an existing table without rebuilding the table.
fn (Context) remove_foreign_key #
fn (mut ctx Context) remove_foreign_key(table string, name string) !
remove_foreign_key removes a named foreign-key constraint.
fn (Context) select #
fn (mut ctx Context) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive
select forwards ORM queries through the migration connection.
fn (Context) insert #
fn (mut ctx Context) insert(table orm.Table, data orm.QueryData) !
insert forwards ORM inserts through the migration connection.
fn (Context) update #
fn (mut ctx Context) update(table orm.Table, data orm.QueryData, where orm.QueryData) !
update forwards ORM updates through the migration connection.
fn (Context) delete #
fn (mut ctx Context) delete(table orm.Table, where orm.QueryData) !
delete forwards ORM deletes through the migration connection.
fn (Context) create #
fn (mut ctx Context) create(table orm.Table, fields []orm.TableField) !
create forwards ORM table creation through the migration connection.
fn (Context) drop #
fn (mut ctx Context) drop(table orm.Table) !
drop forwards ORM table removal through the migration connection.
fn (Context) last_id #
fn (mut ctx Context) last_id() int
last_id forwards the last inserted id from the migration connection.
struct ForeignKey #
struct ForeignKey {
pub:
from_table string
column string
to_table string
primary_key string = 'id'
name string
on_delete string
on_update string
}
ForeignKey describes a foreign-key constraint.
struct Index #
struct Index {
pub:
table string
columns []string
name string
unique bool
}
Index describes an index for Context.add_index.
struct Migration #
struct Migration {
pub:
version i64
name string
up MigrationFn @[required]
down MigrationFn @[required]
}
Migration is one reversible, versioned database schema change.
Versions are positive integers. Timestamp-shaped versions such as 20260816143000 make migrations naturally sortable, like Rails migrations.
struct Migrator #
struct Migrator {
mut:
conn orm.TransactionalConnection
migrations []Migration
config Config
pg_lock_key ?i64
mysql_lock_name string
sqlite_lock_active bool
sqlite_transaction_probe string
resolved_history_namespace string
resolved_history_table_sql string
}
Migrator applies an ordered set of migrations and records their versions.
fn (Migrator) migrate #
fn (mut m Migrator) migrate() ![]AppliedMigration
migrate applies every pending migration in ascending version order.
fn (Migrator) migrate_to #
fn (mut m Migrator) migrate_to(target_version i64) ![]AppliedMigration
migrate_to moves the schema to target_version. Pending migrations at or below the target are applied; applied migrations above it are rolled back.
fn (Migrator) rollback #
fn (mut m Migrator) rollback(steps int) ![]AppliedMigration
rollback reverts the newest steps applied migrations.
fn (Migrator) rollback_last #
fn (mut m Migrator) rollback_last() ![]AppliedMigration
rollback_last reverts the newest applied migration.
fn (Migrator) redo #
fn (mut m Migrator) redo(steps int) ![]AppliedMigration
redo rolls back the newest steps migrations and applies them again.
fn (Migrator) redo_last #
fn (mut m Migrator) redo_last() ![]AppliedMigration
redo_last rolls back and reapplies the newest migration.
fn (Migrator) reset #
fn (mut m Migrator) reset() ![]AppliedMigration
reset rolls back every applied migration while preserving the history table.
fn (Migrator) applied #
fn (mut m Migrator) applied() ![]AppliedMigration
applied returns the migrations recorded in the database, oldest first.
fn (Migrator) pending #
fn (mut m Migrator) pending() ![]Migration
pending returns registered migrations that have not been applied yet.
fn (Migrator) current_version #
fn (mut m Migrator) current_version() !i64
current_version returns the greatest applied version, or zero for an empty schema.
fn (Migrator) status #
fn (mut m Migrator) status() ![]Status
status reports applied and pending migrations, plus applied versions whose migration definitions are missing from the program.
struct Status #
struct Status {
pub:
version i64
name string
state MigrationState
applied_at string
}
Status is one row returned by Migrator.status. A missing row was applied to the database but is no longer registered in code.
struct Table #
struct Table {
pub:
name string
columns []Column
foreign_keys []ForeignKey
id bool = true
id_name string = 'id'
if_not_exists bool
}
Table describes a table for Context.create_table. An auto-incrementing bigint id primary key is added unless id is false.
- README
- fn create_orm_table
- fn drop_orm_table
- fn new
- fn ColumnType.from
- fn Dialect.from
- fn MigrationOperation.from
- fn MigrationState.from
- fn TransactionMode.from
- type MigrationFn
- enum ColumnType
- enum Dialect
- enum MigrationState
- enum TransactionMode
- struct AppliedMigration
- struct Column
- struct Config
- struct Context
- struct ForeignKey
- struct Index
- struct Migration
- struct Migrator
- struct Status
- struct Table