chore(db): drop the phantom levelcode_stripe_id column - #408
Merged
Conversation
`users.levelcode_stripe_id` never had a migration behind it. It entered the tree in b7b2cfc ("Add annotations", 2026-07-22), a commit touching only db/schema.rb and the annotation comments: an annotate run regenerated the schema from a local database where the column had been added by hand, and that snapshot was committed. Its existence therefore depended on how each database was built — db:schema:load (development, test, CI) -> present, always NULL migrations (production) -> absent entirely — and `db:migrate` could never reconcile them, because nothing defined the column. This was not academic: it broke the first production run of the orphan-customer reconciliation with `PG::UndefinedColumn`, and until now app/models/user.rb told every reader that production had a column, and a UNIQUE index, that it did not. Nothing reads or writes it. `users.stripe_id` is, and always was, the only column holding a Stripe customer id. Both directions are guarded, which is the whole point of the migration: * `up` skips `remove_column` when the column is absent, so it is a clean no-op on production rather than an exception on deploy. * `down` re-adds the column AND the index, since Postgres drops a dependent index along with its column. Verified all three paths against a real database: * with the column: migrate drops it, schema.rb and all three annotation blocks lose it, `db/schema.rb` version moves to 20260729022150 * rollback: column and `index_users_on_levelcode_stripe_id` both restored * production shape: column dropped by hand with the migration unrun, then `db:migrate` — `column_exists?` returns false, `remove_column` is skipped, the migration records without raising 1018 examples green; rubocop clean. The cleanup runbook's note is updated, since this commit falsifies its claim that the column is still in the schema.
Contributor
There was a problem hiding this comment.
Pull request overview
Removes a schema/annotation/documentation drift artifact (users.levelcode_stripe_id) that was present in db/schema.rb and annotations but never existed in production (no migration), by adding a guarded migration and updating related schema dumps/annotations/runbook text.
Changes:
- Add a guarded migration to drop
users.levelcode_stripe_idwhen present and restore it (with index) on rollback. - Update
db/schema.rbto remove the phantom column and bump schema version. - Remove stale annotate blocks referencing the column/index and update the Stripe orphan cleanup runbook to reflect the new reality.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| STRIPE_ORPHAN_CUSTOMER_CLEANUP.md | Updates the runbook narrative to reflect that levelcode_stripe_id has been removed, while keeping the defensive column_names intersection for compatibility. |
| app/models/user.rb | Removes stale schema annotation lines for the phantom column/index. |
| db/migrate/20260729022150_remove_levelcode_stripe_id_from_users.rb | Adds guarded up/down migration to drop the column when present and recreate column+index on rollback. |
| db/schema.rb | Removes the levelcode_stripe_id column and its index from the schema dump; updates schema version. |
| spec/models/user_spec.rb | Removes stale schema annotation lines for the phantom column/index. |
| spec/factories/users.rb | Removes stale schema annotation lines for the phantom column/index. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the schema drift found while reconciling orphaned Stripe customers (#404, #406, #407).
The problem
users.levelcode_stripe_idnever had a migration:It entered the tree in
b7b2cfc("Add annotations", 2026-07-22), a commit touching onlydb/schema.rband annotation comments — an annotate run regenerated the schema from a local database where the column had been added by hand.So its existence depended entirely on how a given database was built:
db:schema:load(development, test, CI)NULLAnd
db:migratecould never reconcile the two, because nothing defined it.This was not academic. It broke the first production run of the orphan-customer reconciliation with
PG::UndefinedColumn, and until nowapp/models/user.rbtold every reader that production had a column — and aUNIQUEindex — that it did not.Nothing reads or writes it.
users.stripe_idis, and always was, the only column holding a Stripe customer id.Why both directions are guarded
That asymmetry is the entire reason this migration isn't a one-liner:
upskipsremove_columnwhen the column is absent, so it's a clean no-op on production instead of an exception during deploy.downre-adds the column and the index, because Postgres drops a dependent index along with its column.Verification
All three paths exercised against a real database, not reasoned about:
schema.rb+ all 3 annotation blocks lose it; version →20260729022150db:rollbackindex_users_on_levelcode_stripe_idboth restoredcolumn_exists?→ false,remove_columnskipped, migration records without raisingThat third row is the one that matters — it's exactly what production will do, and it's the case an unguarded
remove_columnwould have failed.1018 examples green, rubocop clean.
Note on deploying
Migrations are not run automatically here — there's no
db:migratein.platform/or.ebextensions/, anddump_schema_after_migration = falsein production, so running it there won't rewritedb/schema.rb. It needs to be run by hand after deploy, and it will report as a no-op.The runbook's note is updated in the same commit, since this change falsifies its claim that the column is still in the schema.