Class: Rubino::Database::Migrator
- Inherits:
-
Object
- Object
- Rubino::Database::Migrator
- Defined in:
- lib/rubino/database/migrator.rb
Overview
Handles database schema migrations in order. Migrations are stored as numbered Sequel migration files.
Constant Summary collapse
- MIGRATIONS_PATH =
File.("migrations", __dir__)
Class Method Summary collapse
-
.latest_version ⇒ Object
The highest migration version present on disk (the target schema).
Instance Method Summary collapse
-
#initialize(connection) ⇒ Migrator
constructor
A new instance of Migrator.
-
#migrate!(lock_path: nil) ⇒ Object
Runs all pending migrations, serialized across processes by a file lock.
-
#pending? ⇒ Boolean
Returns true if there are unapplied migrations.
-
#up_to_date? ⇒ Boolean
Side-effect-FREE check that the schema is fully migrated, safe to call OFF the lock and concurrently.
Constructor Details
#initialize(connection) ⇒ Migrator
Returns a new instance of Migrator.
14 15 16 |
# File 'lib/rubino/database/migrator.rb', line 14 def initialize(connection) @connection = connection end |
Class Method Details
.latest_version ⇒ Object
The highest migration version present on disk (the target schema). Read off the numbered filenames, so it needs no DB connection.
20 21 22 23 24 |
# File 'lib/rubino/database/migrator.rb', line 20 def self.latest_version @latest_version ||= Dir.glob(File.join(MIGRATIONS_PATH, "*.rb")) .map { |f| File.basename(f)[/\A(\d+)/, 1].to_i } .max || 0 end |
Instance Method Details
#migrate!(lock_path: nil) ⇒ Object
Runs all pending migrations, serialized across processes by a file lock.
CONCURRENCY (#race): every command boot migrates when the schema isn’t current, so a fan-out of N fresh ‘rubino` processes on a brand-new home would all migrate AT ONCE. Sequel’s IntegerMigrator is NOT concurrency safe at TWO points:
1. Even constructing it (which `pending?` does) runs
`INSERT INTO schema_info VALUES (0) IF empty` — two racers both see
an empty table and both insert → a DUPLICATE version row. That later
makes `sessions list` hard-crash with a raw `no such table`
backtrace, and wedges `pending?` itself ("More than 1 row in
migrator table").
2. The migration steps themselves can interleave → a DB stuck at an
intermediate version.
SQLite ships no migration advisory lock; Rails serializes migrations with a DB advisory lock for exactly this reason, and an OS file lock is the idiomatic Ruby equivalent. So when a lock_path is given we take an EXCLUSIVE flock and run the ENTIRE probe-and-migrate under it (the caller’s fast path uses the side-effect-free ‘up_to_date?` to avoid even constructing a Sequel migrator off the lock). A process that BLOCKED waiting for the lock re-checks `pending?` inside it and no-ops when the winner already migrated (double-checked locking). Without a lock_path (in-memory DBs, tests) the behaviour is unchanged.
49 50 51 52 53 54 55 56 |
# File 'lib/rubino/database/migrator.rb', line 49 def migrate!(lock_path: nil) return run_migrations! if lock_path.nil? || @connection.memory? with_migration_lock(lock_path) do # Double-checked under the lock: the winner migrated while we waited. run_migrations! if pending? end end |
#pending? ⇒ Boolean
Returns true if there are unapplied migrations.
Intentionally does NOT rescue: a connection/schema error here is a real health problem and must propagate so callers (e.g. doctor) can report a failure instead of silently treating an unreachable DB as “up to date”.
80 81 82 |
# File 'lib/rubino/database/migrator.rb', line 80 def pending? !Sequel::Migrator.is_current?(@connection.db, MIGRATIONS_PATH) end |
#up_to_date? ⇒ Boolean
Side-effect-FREE check that the schema is fully migrated, safe to call OFF the lock and concurrently. Reads ‘schema_info` directly rather than constructing a Sequel migrator (whose mere construction inserts the version-0 row and so races, #race). Returns true only when the table exists, holds EXACTLY ONE row, and that row is at the latest version. Anything else (missing table, zero/duplicate rows, behind) returns false → the caller takes the lock and does the real migrate under it.
65 66 67 68 69 70 71 72 73 |
# File 'lib/rubino/database/migrator.rb', line 65 def up_to_date? db = @connection.db return false unless db.table_exists?(:schema_info) versions = db[:schema_info].select_map(:version) versions.size == 1 && versions.first == self.class.latest_version rescue StandardError false end |