How dibs works

dibs keeps your Postgres schema in sync with your Rust types. It constantly reconciles intent (your Rust schema) with reality (the live database), then generates the SQL to make them match.

The pipeline

Most dibs commands follow the same steps:

Load intent
Read your schema + migration registry from the myapp-db crate.
Inspect reality
Query Postgres catalogs to reconstruct the live schema.
Compute diff
Compare "intent vs reality" into a list of typed schema operations.
Solve
Simulate and reorder operations so the SQL can run (FKs, renames, drops).
SQL / apply
Emit ordered DDL, and optionally run it while streaming progress.

Architecture

Your "db crate" is a small process that speaks RPC to the CLI (via Roam):

dibs-cli
Spawns the db process and drives the UX (TUI, prompts, logs, formatting).
myapp-db
Loads your Rust schema + migrations, connects to Postgres, and serves RPC methods.
Postgres
Source of reality (introspection) and target for migrations (DDL + backfills).

A typical exchange looks like:

dibs-cli
What's the intended schema?
myapp-db
Here are the tables and columns I collected from your Rust schema.

dibs scans your crate for registered tables (Facet annotations), builds an internal schema model, and returns it over RPC.

dibs-cli
Given this database URL, what changes are needed?
myapp-db
I connected to Postgres, introspected the current schema, and computed the diff against your Rust schema.

The result is a structured diff - see below.

dibs-cli
What SQL should we run, in what order?
myapp-db
Here's ordered SQL that should execute cleanly.

The solver orders these operations - see below.

dibs-cli
Run migrations and stream logs back to me.
myapp-db
Running pending migrations in transactions - streaming progress as they apply.

Why RPC? So you don't have to boot your entire app just to work with schemas and migrations.

A typical workspace has three crates: myapp-db (schema + migrations), myapp-queries (generated query helpers), and myapp (your application). The CLI talks to myapp-db via RPC; your app imports myapp-queries for typed database access.

The diff

When intent and reality don't match, dibs produces a diff: a typed list of schema operations - create/drop/rename tables, add/alter/drop columns, indexes, constraints, foreign keys. This isn't raw SQL; it's structured data that dibs can reason about for ordering and safety.

The solver

The same set of changes can succeed or fail depending on order - you can't add a foreign key before the referenced table exists, or drop a table while others still reference it.

The solver orders operations by simulating them on a virtual schema:

  1. Start from the current schema state
  2. Pick any change whose preconditions are satisfied
  3. Apply it to the virtual schema
  4. Repeat until all changes are scheduled

After ordering, dibs verifies that applying the changes to "current" produces "desired". If the solver can't make progress, there's either a true dependency cycle or a bug in diff generation.

SQL generation

The solver's ordered operations become concrete DDL: CREATE TABLE, ALTER TABLE, CREATE INDEX, etc. Because dibs knows what it's trying to do (not just the final SQL text), it can provide better errors and tooling.

Running migrations

Migrations are Rust functions. Each runs in its own transaction - if it fails, the transaction rolls back and subsequent migrations don't run.

When something fails, dibs attaches context (what SQL was running, source location when available) so you get actionable errors instead of "postgres said no."

Metadata tables

dibs maintains __dibs_* tables to record source locations (file/line/column), doc comments, and which migration created what. This powers richer tooling in the CLI, editors, and admin UIs - but it's separate from your app schema and ignored during diffing.