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
-
#current_version ⇒ Object
Returns the current migration version: the highest version recorded in the ‘schema_info` bookkeeping table, or 0 when it is missing/empty.
-
#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.
-
#pending_migrations ⇒ Object
Returns list of pending migration files.
-
#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
#current_version ⇒ Object
Returns the current migration version: the highest version recorded in the ‘schema_info` bookkeeping table, or 0 when it is missing/empty.
Read MAX(version) DIRECTLY: Sequel 5.105 dropped ‘Sequel::Migrator.get_current_migration_version`, so the old call raised NoMethodError on every invocation. The version row IS the schema version (IntegerMigrator writes the applied number there), so MAX(version) is the authoritative reading.
83 84 85 86 87 88 89 90 |
# File 'lib/rubino/database/migrator.rb', line 83 def current_version db = @connection.db return 0 unless db.table_exists?(:schema_info) db[:schema_info].max(:version).to_i rescue StandardError 0 end |
#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”.
97 98 99 |
# File 'lib/rubino/database/migrator.rb', line 97 def pending? !Sequel::Migrator.is_current?(@connection.db, MIGRATIONS_PATH) end |
#pending_migrations ⇒ Object
Returns list of pending migration files
102 103 104 105 106 107 108 |
# File 'lib/rubino/database/migrator.rb', line 102 def pending_migrations Sequel::Migrator.migrator_class(MIGRATIONS_PATH) .new(@connection.db, MIGRATIONS_PATH) .files rescue StandardError [] 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 |