How to Renumber a Migration So It Doesn't Collide With the Contractor's
Two engineers, two migrations, the same number, one production database. Here is the renumbering pattern I use so the contractor's migrations and mine cannot collide.
How to Renumber a Migration So It Doesn't Collide With the Contractor's
When you fork a contractor's codebase to migrate it to in-house infrastructure, you usually fork it at a specific migration number — let's say 045 — and then start writing your own additions from 046 onward. The contractor's also still at 045 because they haven't shipped a schema change in a while. So far, so good.
Three weeks into the fork, the contractor ships migrations 046, 047, 048, 049, 050, 051, 052, 053, 054 to their production. You ship your own migrations 046 (add_lead_dedup_columns) and 047 (add_clinic_timezone) to staging.
Cutover day: you import the contractor's production database into your new database. The dump includes their schema_migrations table, which records that migrations 046 through 054 have already been applied (under the contractor's filenames). Your migration runner starts up, sees that the database has prior history, and tries to apply your 046 and 047. Both fail, because they're not idempotent.
You've just discovered the migration number collision. Here is how to resolve it without losing any of either side's schema changes.
The mechanics of the collision
Most migration frameworks track applied migrations by filename, not by number. Sequelize's umzug-based migrator, for example, stores rows like:
+----------------------------------------+
| name |
+----------------------------------------+
| 045_create_recordings_table |
| 046_create_activity_logs_table | ← contractor's
| 047_create_two_fa_codes_table | ← contractor's
| ... |
| 054_add_activity_notes_permissions | ← contractor's
+----------------------------------------+
Your fork's migration directory contains:
045_create_recordings_table.js
046_add_lead_dedup_columns.js ← yours
047_add_clinic_timezone.js ← yours
After import, your migrator looks at the imported schema_migrations table, sees 045_create_recordings_table as applied (good, names match), and looks for the next file. It finds 046_add_lead_dedup_columns.js. That name is not in schema_migrations. The migrator concludes the migration is pending and runs it.
The migration runs against a database where the contractor's 046 has already been applied. The contractor's 046 created an activity_logs table. Your 046 adds dedup columns to the leads table. Different schemas, different changes — and on its own, your 046 succeeds.
The problem isn't your 046. The problem is the contractor's 046, 047, 048, 049, 050, 051, 052, 053, 054. The migrator looks at their filenames and they don't exist in your codebase. From the migrator's perspective, the contractor's migrations don't exist as files, so they can't be applied or unapplied. The migrator ignores them.
That part is fine. What's not fine is that the contractor's 046 created tables that your codebase doesn't have models for. So those tables sit in your database, fully populated with the contractor's last three weeks of data, and your application has no idea they exist. Users complain that their 2FA codes are gone, their activity logs are missing, their assigned email addresses don't work — because the data is there, just unreachable from your application's ORM.
The fix, in three steps
Step 1: port the contractor's migrations into your codebase, with the exact same names
Add files to your migration directory that match the names recorded in the imported schema_migrations table, byte-for-byte:
apps/api/config/migrations/
046_create_activity_logs_table.js
047_create_two_fa_codes_table.js
048_create_email_addresses_table.js
049_add_user_assigned_email_addresses.js
050_add_view_activity_notes_permission.js
051_add_view_ai_call_logs_permission.js
052_create_email_aliases_table.js
053_add_user_email_alias_id.js
054_add_activity_notes_and_ai_call_logs_permissions.js
The contents of each file should match what the contractor's migration did. Read the contractor's migration source if you have access. If you don't, you can reconstruct each migration from the database schema diff between your last common ancestor (045) and the imported state — mysqldump --no-data of both, run diff, work out what changed and which migration did it.
Why the exact names? Because the migrator skips a migration whose name is already in schema_migrations. After import, your new files match the recorded names, the migrator sees "already applied," and they don't run again. The files exist so that future fresh databases (a new staging environment, a CI fixture) will run them in the right order.
This is the single most important step. Get the names right.
Step 2: renumber your own additions to 055 and 056
Move your own migrations to numbers after the contractor's last:
mv 046_add_lead_dedup_columns.js 055_add_lead_dedup_columns.js
mv 047_add_clinic_timezone.js 056_add_clinic_timezone.js
Their content doesn't change. Only the file name. Since these migrations have never been applied to the production database (because the production database is the contractor's, and the contractor never had your migrations), renumbering them is safe — they appear as "pending" to the migrator either way, and they apply cleanly under their new names.
If your migrations had already been applied to a staging environment you control, that staging's schema_migrations will have the old 046 and 047 names. You need to either reset that staging or update its schema_migrations to match the new names:
UPDATE schema_migrations
SET name = '055_add_lead_dedup_columns'
WHERE name = '046_add_lead_dedup_columns';
UPDATE schema_migrations
SET name = '056_add_clinic_timezone'
WHERE name = '047_add_clinic_timezone';
Do this before the next migrator run on staging.
Step 3: add models and associations for the contractor's tables
Now that the contractor's tables exist in your database and the migrator knows they exist, your application also needs to know about them. Create the model files (ActivityLog, TwoFaCode, EmailAddress, etc.), wire them into models/index.js with the right associations, and add any new columns the contractor introduced to existing tables (users.assignedEmailAddresses, etc.) to your existing models.
This is where forks usually leak features. The contractor shipped 2FA. The mailbox-management UI shipped. The activity-log UI shipped. If you skip step 3, the features stop working when you cut over to your fork. They look like "regressions caused by the migration" to users; they're actually missed model wiring.
The lesson, going forward: make migrations idempotent
The deeper fix is to never rely on "this migration won't be re-run" as a correctness property. Every migration should be safe to run twice. The same INSERT IGNORE/IF NOT EXISTS defensiveness that protects against the collision case also protects against any future scenario where a migration accidentally runs against an unexpected schema state.
MySQL 8.4 doesn't accept ADD COLUMN IF NOT EXISTS, so the pattern looks like:
const [exists] = await sequelize.query(
`SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'leads'
AND COLUMN_NAME = 'emailHash'`
);
if (exists.length === 0) {
await sequelize.query('ALTER TABLE leads ADD COLUMN emailHash VARCHAR(64)');
}
A columnExists / tableExists / indexExists helper makes this readable. Use them. Every new migration should pass the "this is safe to run on a database in any state" test.
Tying off
Migration number collisions are entirely a forking artifact. Two parallel histories of the same codebase will pick the same numbers. The fix is to pick the contractor's numbers (because they're already in production), keep yours (because you also need them to apply somewhere), and renumber to whatever comes after the merge point.
In the next post in the series, I'll get into the other contractor-shipped surprise: full plaintext passwords in Cloud Logging, why they got there, and the one-line redaction helper that prevents the next one.
Run the same audit on your own stack. Open the 30-question checklist →
Next in the series: Stop Logging req.body →
Run the audit on your own stack
A 30-question self-audit. P0/P1/P2 severity. Takes about an hour.
Open the checklist →