Renaming an ActiveRecord Column Without Breaking Everything: What RubyMine Actually Catches
RubyMine's Rails-aware rename refactoring propagates column renames across models, migrations, views, and SQL strings — but metaprogramming and database constraints escape it. Here's how to use it safely.
10 Oct 2026, 02:55 UTC

Renaming a database column in a Rails app sounds trivial until you remember that the column name lives in a dozen places: the migration history, model scopes, strong parameters, form builders in ERB partials, factories, serializers, and the occasional raw SQL string someone wrote three years ago. Miss one and you get a production ActiveRecord::StatementInvalid instead of a failed test.
RubyMine's Rails-aware rename refactoring exists precisely for this problem. It won't catch everything — and knowing exactly where it stops is the difference between a five-minute refactor and a Friday-afternoon incident.
How the cross-language index works
When you hit Shift+F6 on a model attribute, RubyMine isn't doing a text search. It maintains a usage index built from several sources: schema.rb or structure.sql for the actual columns, the Ruby AST for associations, validations, and scopes, and parsed ERB/Haml/Slim templates for form helpers and display calls. That index is what lets a single rename propagate across Ruby, SQL, ERB, and YAML in one atomic operation.
Because it's structural rather than textual, it can distinguish user.name (the attribute) from a local variable that happens to be called name. It also scopes migration generation correctly in multi-database setups — a model connected to a non-primary database gets its migration in the right db/migrate subdirectory, which matters more as Rails 7 apps adopt multiple databases.
A worked example
Say you're renaming users.display_name to users.public_name. The column is referenced in a scope, a form partial, a factory, and a raw SQL fragment:
# app/models/user.rb
scope :searchable, -> { where("display_name ILIKE ?", "%#{term}%") }
# app/views/users/_form.html.erb
<%= form.text_field :display_name %>
# spec/factories/users.rb
factory :user do
display_name { "Ada" }
endThe workflow that works reliably:
- Wait for indexing to finish (watch the status bar — on a large codebase this can take 10–30 seconds; refactoring mid-index gives incomplete results).
- Right-click the attribute usage in the model → Refactor → Rename.
- In the preview pane, inspect every affected file. Exclude
vendor/and generated files from the scope dialog — false positives there are common. - Apply. RubyMine generates a migration containing
rename_column :users, :display_name, :public_name— a real rename, not a remove/add pair that would silently drop data. - Run
bin/rails db:migratelocally, then the test suite.
In the example above, all four locations — scope SQL string, form helper, factory, and model — should appear in the preview diff. If one doesn't, that's a signal to investigate before applying, not after.
Where static analysis gives up
The refactoring engine parses code; it can't execute it. Anything assembled at runtime escapes detection:
- Metaprogrammed accessors:
send("#{old_column}")ordefine_method-generated readers. alias_attributechains that reference the old name indirectly.- Keys inside JSON/JSONB columns, which look like data, not schema.
- Database-level dependents: foreign keys and indexes referencing the old column name are not renamed by the generated migration — you handle those yourself.
The safety net is a plain grep after the refactor:
grep -rn "display_name" --include="*.rb" --include="*.erb" --include="*.yml" app/ spec/ config/Run this in the project root; it needs no special permissions. Any hit is either a dynamic reference the IDE missed or a comment worth updating. Then run the full test suite — runtime failures from missed references almost always surface there first.
Trust, but verify
The honest trade-off: RubyMine's rename is the fastest correct way to handle the 90% of references that are statically visible, and its Safe Delete preview (which checks for remaining usages before generating a remove_column migration) adds a real guardrail for schema cleanup. But it's an accelerator, not an authority. Treat the preview diff as a code review, grep for the leftovers, and let the test suite be the final word. That combination — IDE for breadth, grep for depth, tests for truth — is what makes a column rename boring, which is exactly what it should be.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.