Skip to content

chore(db): drop the phantom levelcode_stripe_id column - #408

Merged
ndemianc merged 1 commit into
developfrom
chore/drop-phantom-levelcode-stripe-id
Jul 29, 2026
Merged

chore(db): drop the phantom levelcode_stripe_id column#408
ndemianc merged 1 commit into
developfrom
chore/drop-phantom-levelcode-stripe-id

Conversation

@ndemianc

Copy link
Copy Markdown
Member

Closes the schema drift found while reconciling orphaned Stripe customers (#404, #406, #407).

The problem

users.levelcode_stripe_id never had a migration:

$ grep -rl levelcode_stripe_id db/migrate/
(nothing)

It entered the tree in b7b2cfc ("Add annotations", 2026-07-22), a commit touching only db/schema.rb and 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:

Built by Column
db:schema:load (development, test, CI) present, always NULL
migrations (production) absent

And db:migrate could 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 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.

Why both directions are guarded

That asymmetry is the entire reason this migration isn't a one-liner:

  • up skips remove_column when the column is absent, so it's a clean no-op on production instead of an exception during deploy.
  • down re-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:

Path Result
With the column dropped; schema.rb + all 3 annotation blocks lose it; version → 20260729022150
db:rollback column and index_users_on_levelcode_stripe_id both restored
Production shape (column dropped by hand, migration unrun) column_exists? → false, remove_column skipped, migration records without raising

That third row is the one that matters — it's exactly what production will do, and it's the case an unguarded remove_column would have failed.

1018 examples green, rubocop clean.

Note on deploying

Migrations are not run automatically here — there's no db:migrate in .platform/ or .ebextensions/, and dump_schema_after_migration = false in production, so running it there won't rewrite db/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.

`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.
Copilot AI review requested due to automatic review settings July 29, 2026 02:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_id when present and restore it (with index) on rollback.
  • Update db/schema.rb to 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.

@ndemianc
ndemianc merged commit 52b41d4 into develop Jul 29, 2026
4 checks passed
@ndemianc
ndemianc deleted the chore/drop-phantom-levelcode-stripe-id branch July 29, 2026 02:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants