Basics
Shared Development Databases
When several developers share a single development database, the migration tracking table (wheels_migrator_versions) can record a version whose migration file is not yet in your branch — a teammate ran wheels migrate latest against the shared DB before their migration was merged. Wheels calls these orphan versions and handles them transparently.
You’ll learn:
- What an orphan version is and how to spot it
- What
wheels migrate latestdoes when one is present - Four resolution paths and when to use each
- How
wheels migrate doctorgives a one-command health view - Why we recommend per-developer schemas instead
What you’ll see
Section titled “What you’ll see”Running wheels migrate info:
Datasource: wheelsDatabase type: MySQLCurrent version: 20260521120100Total migrations: 3 applied: 2 pending: 1 orphan: 1
Migrations (newest last): [x] 20260520091823 create_users [x] 20260521090300 add_email_to_users [?] 20260521120100 add_phone_to_users (applied 2026-05-21 12:01:00) [ ] 20260521131000 add_address_to_users
Orphan versions are recorded in the database but have nomatching file in app/migrator/migrations/. This usually meansa peer applied a migration whose file isn't yet in your branch.The [?] row is the orphan: tracked as applied in the DB, but the file isn’t on disk in this checkout. The migration name (add_phone_to_users) and apply timestamp come from the name and applied_at columns added to wheels_migrator_versions in 4.0.x — they let you see what the peer applied and when, even though the file isn’t in your branch yet. The [x] and [ ] markers behave as usual — applied and pending respectively.
Older installs that pre-date the schema enrichment will show orphan rows as [?] <version> ********** NO FILE ********** (no name, no timestamp). The columns are added automatically on the first migrator call after upgrade; subsequent applies populate the name and timestamp without any further action.
Running wheels migrate latest:
Running migration: latest...Note: database tracks version(s) 20260521120100 with no matching file inapp/migrator/migrations/. This usually means a peer applied a migrationwhose file isn't yet in your branch.Applying pending migration(s) up to 20260521131000.
-------- 20260521131000_add_phone_to_users ----------------...Your pending local migration still applies. The orphan row stays in the tracking table — Wheels has no way to know whether the peer’s migration was rolled back, will land in your branch later, or was a manual hotfix.
Resolving an orphan
Section titled “Resolving an orphan”Pick the option that matches what actually happened:
Option 1: The peer’s migration is legitimate; pull their file
Section titled “Option 1: The peer’s migration is legitimate; pull their file”This is the common case. Pull the latest from your shared branch and the file appears alongside its tracking row.
git pullwheels migrate info # confirm the file is now present and marked [x]The [?] row becomes [x] once the file lands. No DB change needed.
Option 2: The peer rolled back their migration
Section titled “Option 2: The peer rolled back their migration”If the peer reverted their work but the tracking row stayed (e.g., their wheels migrate down failed mid-flight, or they reverted via git reset --hard without running down()), use wheels migrate forget to remove the stale row:
wheels migrate forget 20260521120100 # prints what would happenwheels migrate forget 20260521120100 --yes # actually removes the rowThe command refuses to forget a version that has a matching local file — for legitimate rollbacks, use wheels migrate down instead. It also refuses if the version isn’t in the tracking table (idempotent).
Option 2b: The migration was applied via direct SQL
Section titled “Option 2b: The migration was applied via direct SQL”If a teammate applied the migration outside of Wheels (e.g., via a SQL client during incident response), the schema is changed but wheels_migrator_versions doesn’t know. Use wheels migrate pretend once the file lands in your branch:
git pull # pull the peer's migration filewheels migrate pretend 20260521120100 # prints what would happenwheels migrate pretend 20260521120100 --yes # records the versionThe command refuses if the version is already applied (idempotent) or if no local file matches — the file must exist so future operations can roll the version back via down().
Option 3: You need to add a new migration
Section titled “Option 3: You need to add a new migration”Just generate it. Wheels uses Now() for the timestamp prefix, so any migration you create today gets a timestamp newer than yesterday’s orphan — your new file will run in the right order automatically.
wheels generate migration AddPhoneToUsersIf for some reason you’ve manually picked an older timestamp, rename the file to use a current timestamp before running wheels migrate latest.
Comprehensive diagnostic
Section titled “Comprehensive diagnostic”For a single-command view of migrator health, run:
wheels migrate doctorSample output when something needs attention:
Migrator needs attention: 1 pending, 1 orphan.
Datasource: wheels Database type: MySQL Current version: 20260521120100 Total migrations: 3 applied: 2 pending: 1 orphan: 1 (20260521120100)
Pending local migrations: [ ] 20260521131000
Orphan versions (no matching file): [?] 20260521120100
Resolve: `wheels migrate forget <version> --yes` to remove an orphan row, or pull the peer's migration file via git.Healthy output is a single line:
Migrator is healthy. 3 migration(s) applied, none pending.doctor is read-only — it never mutates the tracking table or runs migrations.
Alternative: avoid shared dev databases
Section titled “Alternative: avoid shared dev databases”A shared dev database trades schema isolation for “less environment to set up”. When two developers diverge on schema, the symptoms surface exactly when you don’t want them: mid-feature, with someone else’s WIP already applied. Consider these alternatives, all supported by Wheels first-class:
- Per-developer schemas (MSSQL, PostgreSQL) — same physical DB, one schema per developer. Switch via
config/environment.cfm. Best when production-like data matters and you can pay the storage cost once. - Per-developer databases (MySQL, PostgreSQL) — separate logical DBs on the same instance. Each developer’s migrations only affect their own DB.
- Local-only databases — SQLite for dev, the shared instance only for staging and production. Wheels supports SQLite first-class via the bundled JDBC driver. Zero shared-state, zero conflicts.
If a shared dev DB is unavoidable for organisational reasons (e.g., a team relying on production-grade data that can’t be cheaply replicated), accept that orphan handling will fire periodically and design your workflow around regular git pull + wheels migrate info checks.
Related
Section titled “Related”- Migrations — the day-to-day workflow
- Issue #2780 — the original report that motivated this behavior