Skip to content

Repository files navigation

schema_reaper

Find dead columns, dead tables, unused indexes and other schema dead-weight in Rails / ActiveRecord apps — then remove them safely.

schema_reaper reads your live PostgreSQL schema and planner statistics and cross-references them against a static scan of your codebase (Ruby via the Prism AST, plus views and SQL string literals). Optionally it also fuses in a runtime signal — a sampled log of which columns are actually read in production. Every finding is scored by confidence and severity, carries an estimate of the disk it reclaims, and comes with a concrete fix.

Install

# Gemfile
gem "schema_reaper", group: :development
bundle install

Requires Ruby >= 2.7 and PostgreSQL. The database connection is resolved in this order:

  1. database_url: in .schema_reaper.yml
  2. ENV["DATABASE_URL"]
  3. config/database.yml for the current environment (SCHEMA_REAPER_ENV / RAILS_ENV, default development) — ERB and YAML aliases are handled, as are Rails 6+ multi-database sections

So in a Rails app, bundle exec schema_reaper scan works with no setup.

Usage

bundle exec schema_reaper scan                    # grouped terminal report
bundle exec schema_reaper scan --format markdown  # PR-comment table
bundle exec schema_reaper scan --format sarif     # GitHub code scanning
bundle exec schema_reaper scan --format json
bundle exec schema_reaper scan --ci               # exit 1 on new findings
bundle exec schema_reaper scan --min-confidence 0.8
bundle exec schema_reaper scan --no-color         # force plain output
bundle exec schema_reaper baseline                # accept current findings
bundle exec schema_reaper trend                   # snapshot + progress delta
bundle exec schema_reaper generate-migration users legacy_api_token

The scan report rolls up findings that say the same thing about different tables, then groups the rest by table and sorts by confidence:

  schema_reaper  5 findings across 2 tables
  missing_fk_index 3 · always_null_column 1 · dead_column 1
  ~93.8 KB reclaimable

  users
    █████  90%  medium  missing_fk_index     team_id
           team_id is a foreign key with no covering index
           → add_index :users, :team_id
    ████░  85%  high    always_null_column   api_key  46.9 KB
           pg_stats.null_frac = 1.0 across ~3000 row(s) · column carries no data
           → verify with `SELECT count(api_key) FROM users` then stage a removal

  stale_exports
    ████░  85%  high    dead_table
           no model or query reference · table holds ~0 row(s)
           → confirm no external consumer, then `drop_table :stale_exports`

  high 2   medium 1   low 2

Colour is automatic on a terminal, off when piped or NO_COLOR is set.

In a Rails app the railtie also gives you rake schema_reaper:scan|baseline|trend (with FORMAT=).

Analyzers

type what it flags main signal
dead_column column no code path references static scan (+ runtime)
dead_table table with no model/query reference static scan + row count
unused_index non-unique index, idx_scan = 0 pg_stat_user_indexes
duplicate_index index that is a prefix of a wider one schema shape
missing_fk_index *_id / FK column with no index schema shape
always_null_column null_frac = 1.0 — no data at all pg_stats
single_value_column one distinct value on a large table pg_stats

Columns owned by common gems (devise, paper_trail, activestorage, actiontext, friendly_id, audited, pg_search, ahoy_matey, paranoia family) are whitelisted automatically when the gem is in your bundle.

Runtime signal (optional, raises confidence)

Static analysis alone can't see metaprogrammed access, so dead_column confidence is capped at 0.6 without runtime data. To lift the cap:

# config/initializers or manually
SchemaReaper::Runtime::Tracker.install!(
  store: SchemaReaper::Runtime::Store.new(path: ".schema_reaper/runtime.jsonl"),
  sample_rate: 0.05
)

or, in Rails, boot with SCHEMA_REAPER_TRACK=1. Let it run in staging or production for a couple of weeks. A column unseen in both code and

= 14 observed days of runtime data reaches ~0.9 confidence.

Safety model

schema_reaper never drops anything itself. generate-migration emits a pair:

  1. Ignore — you add self.ignored_columns += %w[col] to the model and deploy. Nothing is dropped.
  2. Drop — run only after step 1 has soaked in production and nothing broke.

always_null_column / single_value_column fixes ask you to confirm with a SELECT first.

CI

# .github/workflows/schema_reaper.yml
- run: bundle exec schema_reaper scan --ci --format sarif > reaper.sarif
- uses: github/codeql-action/upload-sarif@v3
  with: { sarif_file: reaper.sarif }

Commit .schema_reaper/baseline.json so the job fails only when a change adds new dead weight.

Custom analyzers

# lib/schema_reaper/analyzers/my_check.rb
class MyCheck < SchemaReaper::Analyzers::Base
  SchemaReaper::Analyzers::Registry.register(self)

  def call
    schema.tables.filter_map { |t| ... finding(type: :my_check, table: t.name, ...) }
  end
end
# .schema_reaper.yml
require:
  - lib/schema_reaper/analyzers/my_check.rb

Roadmap

  • Runtime verdict fusion for index and table findings
  • Orphan-row and schema.rb↔DB drift analyzers
  • Disk/$ reclaim from real pg_total_relation_size
  • Mountable dashboard engine, trend charts
  • MySQL adapter

Pro (for teams)

The gem is free and complete for a single app. schema_reaper Pro adds the team-scale layer: MySQL adapter, multi-database fan-out, orphan-row and schema-drift analyzers, Slack/Jira/PR-comment reporters, real pg_total_relation_size + $ estimates, scheduled scans with alerts, and a mountable dashboard engine. See PRO.md. Waitlist / early access: open an issue tagged pro.

Sponsor

schema_reaper is MIT and maintained in the open. If it saved you disk, money, or a nasty migration, sponsor its development via the Sponsor button on the repo.

Development

bin/setup
bundle exec rake        # rspec + rubocop

License

MIT.

About

Find dead columns, unused indexes and schema dead-weight in Rails/ActiveRecord apps

Resources

Code of conduct

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages