rails2qdbd
Turn a Rails application's db/schema.rb and app/models into
qdbd schema files: plain-text QuickDBD syntax that
qdbd checks, lays out and serves as an interactive ER diagram.
rails2qdbd path/to/app -o schema/ # write schema/*.txt
qdbd serve schema/ # live diagram in the browser- Ruby files are parsed with Prism, never executed. No app boot, no database, none of the app's gems needed.
- Relationships come from database foreign keys and from model
associations (
belongs_to,has_many,has_one,has_and_belongs_to_many, STI, concerns, namespaces, project inflections). - What qdbd cannot draw (composite, expression and partial indexes, enums, polymorphic associations, generated columns, check constraints, attachments) becomes documentation that qdbd shows on hover. Anything the tool cannot resolve is reported as a warning.
- Output is deterministic, and
--checkexits 1 when the committed files are stale.
Installation
gem install rails2qdbdor in the app's Gemfile (it never loads into the app; it only reads files):
gem "rails2qdbd", group: :development, require: falseRequirements: Ruby ≥ 3.2 (tested on 3.2 – 4.0), with prism and
activesupport ≥ 7.1 as its only runtime dependencies, so it fits Rails 7.1 –
8.1 apps. To look at the result you need
qdbd (pipx install quickdbd-cli).
Usage
rails2qdbd [APP_ROOT] [options] # APP_ROOT defaults to "."
-o, --output PATH a .txt file or a directory (default: config, else stdout)
--schema FILE schema file, repeatable (default: db/schema.rb)
--models DIR models directory (default: app/models)
--no-models schema only, no model associations
-c, --config FILE config file (default: APP_ROOT/.rails2qdbd.yml)
--exclude GLOB leave out matching tables, repeatable
--check write nothing; exit 1 if the output is out of date
-q, --quiet no warnings or summary
-v, --version
Paths on the command line are relative to the current directory; paths in the
config file are relative to the app root. Exit codes: 0 ok, 1 errors or
out of date, 2 usage error. Warnings go to stderr as
path:line: warning: message.
Multiple databases (Rails 8's db/queue_schema.rb, db/cache_schema.rb, …)
can be combined: --schema db/schema.rb --schema db/queue_schema.rb.
Configuration: .rails2qdbd.yml
Optional, in the app root:
output: schema/ # .txt file or directory
schemas: [db/schema.rb]
models: app/models # false: schema only
inflections: config/initializers/inflections.rb # false: ignore
exclude: [ar_internal_metadata, "solid_*"]
default_group: app
groups: # one file per group (qdbd shows each file as a group)
storage: ["active_storage_*"]
auth: [users, sessions]Groups split the output into one file per group when the output is a
directory; the first matching glob wins. Without groups, or with a .txt
output, everything goes into one file.
Keeping the diagram current in CI
rails2qdbd -o schema/ --check && qdbd check --strict schema/Generated files begin with # Generated by rails2qdbd …. Only files with
that header are ever replaced or deleted, so hand-written qdbd files can live
alongside them; if an output file exists without it, nothing is written and the
command exits 1. qdbd's layout file (qdbd.layout.json) is
never touched, so table positions you arrange are kept across regenerations.
What you get
# unique index (tenant_id, email_address)
users
-
id PK bigint
email_address string
invalidated_at datetime NULL
# enum: member, owner
role string default=member
tenant_id bigint INDEX FK >- tenants.id
| Rails | qdbd |
|---|---|
implicit id, id: :uuid, primary_key: "code", composite primary_key: [...], id: false
|
PK on the key column(s) |
column type and limit: / precision: / scale: / array:
|
string(50), decimal(10,2), string[] (Rails type names) |
| nullable (Rails default) |
NULL (qdbd's default is NOT NULL) |
default: (including -> { "now()" }) |
default=…, quoted when needed |
| single-column unique / plain index |
UNIQUE / INDEX
|
composite, expression, partial index; index on one column of a composite primary key; check_constraint
|
documentation line above the table |
add_foreign_key, t.references foreign_key: true
|
FK >- table.column |
| association without a database FK |
FK >- … plus the note relationship from model association
|
unique FK column, or has_one (without has_many) |
one-to-one FK - …
|
has_and_belongs_to_many |
FKs on the join table |
polymorphic belongs_to / has_many as: / Active Storage |
note: polymorphic (type in x_type): Model, …
|
enum (model) / create_enum (PostgreSQL) |
note: enum: a, b
|
t.virtual … as: |
note: generated: …
|
STI subclasses, has_*_attached
|
table notes |
comment: on tables and columns |
documentation lines |
Limitations
-
db/structure.sqlis not read (useschema.rb). - Association options must be literals;
class_name: some_methodis skipped with a warning. - Relationships are never guessed from column names alone.
- Module-level
table_name_prefixis not followed; useself.table_name. - Only the literal
irregular / plural / singular / uncountable / acronymcalls ofconfig/initializers/inflections.rbare applied.
Development
bin/setup # gems into .bundle/, qdbd into .venv/; nothing installed globally
bundle exec rake test # unit + integration tests (qdbd checks skip if qdbd is missing;
# REQUIRE_QDBD=1 makes them fail instead, as in CI)
bundle exec rake golden # regenerate test/fixtures/apps/*/expected after an intended output change
bundle exec exe/rails2qdbd --helpTests are written first (Minitest). test/fixtures/apps/shop is a synthetic
app covering every supported feature; its golden output is validated with
qdbd check --strict, and qdbd's own parser is used to assert what qdbd
understood. CI (workflow) runs Ruby 3.3 – 4.0 on
the locked dependencies and Ruby 3.2 on the oldest allowed ones
(gemfiles/minimum.gemfile).
Releases are published to RubyGems.org by the
release workflow when a vX.Y.Z tag matching
Rails2qdbd::VERSION is pushed.
License
MIT