class Rotor::Migrator
Runs migrations (Rotor::Migration classes) against a database and
records which ran in a table (schema_migrations unless table:
says otherwise: version bigint PRIMARY KEY, inserted_at), which
migrate and rollback create when it is missing.
class MigCreateUsers < Rotor::Migration
version 20261107120000
def change
create_table :users do |t|
t.column :name, String
end
end
end
class MigAddAge < Rotor::Migration
version 20261108090000
def change
add_column :users, :age, Int, default: 0
end
end
PG::Pool.open("postgres://app@localhost/shop") do |pool|
migrator = Rotor::Migrator.new(Rotor::Repo.new(pool: pool), [MigCreateUsers, MigAddAge], log: ->(line: String) { puts line })
migrator.migrate # [20261107120000, 20261108090000]
migrator.status # [[20261107120000, "MigCreateUsers", :up], [20261108090000, "MigAddAge", :up]]
migrator.rollback # [20261108090000]
end
- Each migration runs in a transaction of its own, its version's row
written in it (PostgreSQL's DDL is transactional): one that fails
is rolled back whole, raising
Rotor::MigrationError, and those before it stay applied. One whose class saysdisable_transaction!runs outside one, its row written after it succeeds. migrateandrollbacktake one connection of the repository's for their whole run (Rotor::Adapter#hold), take a session-levelpg_advisory_lockon it (its key derived from the table's name), and run every migration there: two processes migrating at once run one after the other, the second finding nothing pending.lock_timeout:bounds the wait (PostgreSQL'slock_timeout, during the lock alone); without it the wait has no bound.- Versions are unique among the migrations given.
migraterefuses a pending version lower than the highest one applied (Rotor::MigrationError, before running anything), unlessallow_out_of_order: true: versions are timestamps, and an older one appearing late is usually a merge that went wrong, to look at before it runs. Applied versions without a migration in the list are shown bystatusas:unknown;rollbackrefuses them. - The repository must not be inside a transaction: each migration
opens its own (
ArgumentErrorotherwise). When the run ends, a transaction a migration left open is rolled back and the lock released; a connection taken from a pool has its settings reset (RESET ALL), so that a migration's SET does not reach the pool, while a repository over one connection keeps its session as the migrations left it (theirs and the program's settings alike). A migration without a transaction that leaves one open fails.
/home/runner/work/rotor/rotor/src/migrator.qz:60
Class methods
def self.new(repo: Rotor::Repo, migrations: Array[Type[Rotor::Migration]], *, table: String = …, allow_out_of_order: Bool = …, lock_timeout: Duration? = …, log: shareable (String) -> Void? = …) -> Rotor::Migrator
A migrator of migrations (in any order) over repo. log gets a
line as each migration starts and ends. Raises ArgumentError for a
migration without a version, or two with one version.
def self.release(r: Rotor::Repo, key: Int, reset: Bool) -> StandardError?
Leaves the connection of r as the migrator found it: no
transaction, the lock key released (a warning when this session
does not hold it), and the settings reset when reset. The error
that stopped it, or nil.
def self.attempt(&: () -> Object?) -> StandardError?
The error the block raised, or nil.
def self.lock_key(table: String) -> Int
The advisory lock's key for the table table: the first 60 bits of
SHA-256 of rotor.migrations.<table>, so that two programs agree.
def self.render(op: Rotor::Operation) -> Array[String]
The SQL statements that do op, in order.
op = Rotor::Operation::RenameColumn.new("posts", "body", "content")
Rotor::Migrator.render(op) # => ["ALTER TABLE \"posts\" RENAME COLUMN \"body\" TO \"content\""]
def self.column(c: Rotor::ColumnDef) -> String
A column's definition in CREATE TABLE and ADD COLUMN.
def self.references(r: Rotor::Reference) -> String
REFERENCES "t" ("id"), with its ON DELETE.
def self.index(i: Rotor::IndexDef, if_not_exists: Bool) -> String
CREATE [UNIQUE] INDEX ....
def self.literal(v: Int | Float | String | Bool | Bytes | Rotor::SQL::Raw) -> String
A default as SQL: a literal of its value, or the SQL given.
Rotor::Migrator.literal("it's") # => "'it''s'"
Rotor::Migrator.literal(1.5) # => "1.5"
Rotor::Migrator.literal(Rotor::SQL.raw("now()")) # => "now()"
Methods
def applied -> Array[Int]
The versions applied, ascending (none when the table is missing).
def pending -> Array[Int]
The versions of the migrations given that are not applied, ascending.
def status -> Array[[Int, String, Symbol]]
Each version, ascending: the migration's class name and :up
(applied) or :pending; an applied version none of the migrations
has is :unknown, with an empty name.
def migrate(*, to: Int? = …) -> Array[Int]
Runs the pending migrations, ascending, up to and including to
when it is given: the versions run.
def rollback(*, steps: Int = …) -> Array[Int]
Rolls back the steps highest versions applied, highest first (as
Ecto: by version, not by when they were applied, which differ only
after allow_out_of_order:): the versions rolled back.
def rollback(*, to: Int) -> Array[Int]
Rolls back the versions applied above to (which stays applied),
highest first: the versions rolled back.