# Active Record Doctor runs sixteen database checks and exits non-zero on error

> Active Record Doctor is a Ruby gem that audits a Rails database for the inconsistencies that only surface under load: extraneous indexes, unindexed foreign keys, validations with no matching constraint, short primary key types, tables with no primary key. It is designed as a CI step rather than a dashboard, and its value is that its output is a build failure you can gate on.

**gregnavis/active_record_doctor** — Identify database issues before they hit production.

- Repository: https://github.com/gregnavis/active_record_doctor
- Stars: 1,931 · Forks: 72
- Language: Ruby
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/gregnavis-active-record-doctor

## Sixteen detectors, all of them structural mismatches

The gem's stated purpose is to keep the database in a good shape, and the detector list is where the actual scope lives.

Several checks find indexes that are there but should not be. Extraneous indexes are flagged first, and unindexed deleted_at columns are their own check, which is a separate concern because a soft-delete column only does its job if something is querying on it.

Several check for indexes that should be there. Missing foreign key constraints and mismatched foreign key types are distinct: one is an absent constraint, the other is a constraint whose column types do not line up with the referenced column. Unindexed foreign keys is the one detector that can also fix itself, since the gem can create those indexes.

The largest group is about validations and constraints disagreeing. Uniqueness validations not backed by a unique index is the classic one, where a model validates uniqueness, a race condition inserts a duplicate anyway, and the database never objected. Missing non-NULL constraints and missing presence validations are the mirror pair, one about the database and one about the model. Incorrect presence validations on boolean columns is the subtle version, since validating presence on a boolean behaves differently from what people expect. Mismatches between model length validations and database validation constraints covers the case where a model says a field is limited to 255 characters and the column says something else.

Then there are structural checks. Incorrect values of dependent on associations catches a mistake that only destroys data at the wrong moment. Primary keys having short integer types is the one that will matter most at scale, because a short integer primary key runs out of values. Tables without primary keys and tables without timestamps are both schema hygiene checks. Models referencing undefined tables catches code pointing at a table that is not in the schema, which usually means a migration was never run or a rename was half-finished.

Two are about reach rather than correctness. Unused tables reports tables no model references, which is how you find the leftovers from a feature you removed. And PostgreSQL timestamps without time zone is database-specific, catching timestamp columns that will silently lose offset information.

## The Gemfile line is the whole Rails installation

Installation in a Rails project is one Gemfile entry, and the group restriction is the part worth getting right.

```ruby
gem 'active_record_doctor', group: [:development, :test]
```

The development and test groups are not decoration. This gem reads your schema and your models, which means it needs a database connection, and you do not want a diagnostic tool loading into your production application bundle. Keeping it out of the default and production groups is the mechanism that prevents that.

There is a second form for people who want to live on the edge, which pins to the repository instead of a released version:

```ruby
gem 'active_record_doctor', github: 'gregnavis/active_record_doctor', group: [:development, :test]
```

Bundler will treat that as a git dependency and track the branch, which is how you run against unreleased changes.

For projects that are not Rails but do use Rake, the integration is slightly more involved and the README is explicit about a precondition. You instantiate the Rake task yourself:

```ruby
require "active_record_doctor"

ActiveRecordDoctor::Rake::Task.new do |task|
  task.deps = []
  task.config_path = ::Rails.root.join(".active_record_doctor.rb")
  task.setup = -> { ::Rails.application.eager_load! }
end
```

The task exposes three things. deps is a list of your own Rake dependencies that should run first. config_path points at the configuration file. And setup is a Proc called right before the detectors run, whose job is to make sure models are preloaded and a database connection is available.

The README flags this as IMPORTANT and states the expectation directly: after deps runs and setup is called, your Active Record models must be loaded and a database connection established. That is the gap between the simple Rails case and the non-Rails case, and the example uses Rails.root and Rails.application inside a non-Rails project, so you will need to substitute your own equivalents.

## Rake task names are the API, and help is a sub-task

Usage is through rake or rails, and there are three levels of invocation.

Run everything with the base task:

```
bundle exec rake active_record_doctor
```

Run one detector by appending its name, which is the same name used in the README's detector list:

```
bundle exec rake active_record_doctor:extraneous_indexes
```

And get help on a specific detector with a help sub-task:

```
bundle exec rake active_record_doctor:extraneous_indexes:help
```

That third one is more useful than it looks. The README says it prints the detector help text in the terminal along with the supported configuration options, their meaning, and whether they are global or local. Since every detector carries its own configuration, a single help call tells you both what the check does and how to silence it legitimately.

The Continuous Integration section is where the design intent becomes clear. The README says to ensure the configuration file is committed and run the tool as one of your build steps, and specifies that it returns a non-zero exit status if any errors were reported.

That single sentence is what makes this a CI tool rather than a report generator. A diagnostic that always exits zero has to be read by a human; one that fails the build gets enforced. Committing the configuration file is also load-bearing, because it means the ignore lists travel with the repository and a detector you have justified ignoring stays ignored for everyone.

For the case where the tool fails with an exception and you cannot tell why, there is a debug environment variable. Set ACTIVE_RECORD_DOCTOR_DEBUG and run the base task, and you get more detail for the failure:

```
ACTIVE_RECORD_DOCTOR_DEBUG=1 bundle exec rake active_record_doctor
```

## Configuration is one Ruby file, global and per-detector

The configuration file is named .active_record_doctor.rb and lives in the project root. The README is clear that you do not need it at all if the default behaviour suits you: if you want the default configuration, do nothing and just run active_record_doctor in your project directory.

The file is a single configure block with two kinds of setting, and the distinction is the point.

```ruby
ActiveRecordDoctor.configure do
  global :ignore_tables, [
    "ar_internal_metadata",
    "schema_migrations",
    "active_storage_blobs",
    "active_storage_attachments",
    "action_text_rich_texts",

    "legacy_users"
  ]

  detector :extraneous_indexes,
    ignore_tables: ["users"],
    ignore_indexes: ["accounts_on_email_organization_id"]
end
```

Global settings affect all detectors. The example ignores internal Rails-related tables, which the README notes are ignored by default anyway, and then adds a project-specific table, legacy_users.

Detector-specific settings affect only one detector. Here the extraneous_indexes detector is told to skip the users table entirely, and to skip a specific named index on accounts.

That granularity matters because it turns suppressions from code review arguments into committed, reviewable lines. When an index is genuinely needed for a specific query, recording it as ignored for that one detector says more than disabling the tool does.

The configuration is Ruby rather than YAML, which means it is executable. That is more flexible and it is also a code-review consideration if your configuration is only ever reviewed by people who read YAML.

The repository carries a .rubocop.yml and an AGENTS.md, and the CI setup is three separate workflows by database: one for MySQL, one for PostgreSQL, and one for linting. Testing against both engines matters for a tool like this, because several detectors, including the PostgreSQL timestamp one and the foreign key type one, depend on database-specific behaviour.

## Conclusion

Adopt Active Record Doctor if you have a mature Rails application whose schema has drifted from its models, because the checks it performs are the ones that turn into slow queries and constraint bugs under production load. Do not adopt it as a migration tool, since it detects and can index some problems but writes nothing else and applies no schema changes. Verify first which detectors apply to your project by running them individually with the help sub-task before gating CI on the full suite, since a detector that is correct but noisy on your schema will otherwise block every merge.

## FAQ

### How do I install and run Active Record Doctor in a Rails app?

Add gem 'active_record_doctor', group: [:development, :test] to your Gemfile, run bundle install, then run bundle exec rake active_record_doctor to run every detector or bundle exec rake active_record_doctor:extraneous_indexes for one.

### What kinds of database problems does Active Record Doctor detect?

Sixteen detectors covering extraneous and missing indexes, unindexed foreign keys and deleted_at columns, missing foreign key constraints, uniqueness validations without a unique index, missing non-NULL constraints, incorrect dependent options on associations, short primary key types, tables without primary keys or timestamps, unused tables, and PostgreSQL timestamps without time zone.

### Can Active Record Doctor be used in continuous integration?

Yes. Commit the .active_record_doctor.rb configuration file and run the tool as a build step, since it returns a non-zero exit status when errors are reported. You can also set ACTIVE_RECORD_DOCTOR_DEBUG=1 to get more detail when it fails with an exception.

## Sources

- [gregnavis/active_record_doctor on GitHub](https://github.com/gregnavis/active_record_doctor)
- [Issues](https://github.com/gregnavis/active_record_doctor/issues)
- [License: MIT](https://github.com/gregnavis/active_record_doctor/blob/master/LICENSE)
- [README](https://github.com/gregnavis/active_record_doctor/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/gregnavis-active-record-doctor
