Skip to content

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 latest does when one is present
  • Four resolution paths and when to use each
  • How wheels migrate doctor gives a one-command health view
  • Why we recommend per-developer schemas instead

Running wheels migrate info:

Datasource: wheels
Database type: MySQL
Current version: 20260521120100
Total 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 no
matching file in app/migrator/migrations/. This usually means
a 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 in
app/migrator/migrations/. This usually means a peer applied a migration
whose 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.

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.

Terminal window
git pull
wheels 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:

Terminal window
wheels migrate forget 20260521120100 # prints what would happen
wheels migrate forget 20260521120100 --yes # actually removes the row

The 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:

Terminal window
git pull # pull the peer's migration file
wheels migrate pretend 20260521120100 # prints what would happen
wheels migrate pretend 20260521120100 --yes # records the version

The 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().

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.

Terminal window
wheels generate migration AddPhoneToUsers

If for some reason you’ve manually picked an older timestamp, rename the file to use a current timestamp before running wheels migrate latest.

For a single-command view of migrator health, run:

Terminal window
wheels migrate doctor

Sample 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.

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.

  • Migrations — the day-to-day workflow
  • Issue #2780 — the original report that motivated this behavior