sqlite-migrate is a data-driven Clojure library that introspects a live SQLite file into a Snapshot, diffs it against a declared target schema, and produces an executable migration Plan — with the Diff as a first-class public surface.
The whole pipeline is pure values between two thin effectful edges:
snapshot and declared-snapshot read schemas in; check and apply! run a
Plan's Gates and Ops against a connection. Everything in between — diff,
plan, the reports — is a pure function over plain EDN.
Clojure teams shipping SQLite evolve their schemas by hand: numbered migration
scripts, ad-hoc ALTER TABLE incantations, and tribal knowledge of SQLite's
12-step rebuild procedure. The developer knows what the schema should be,
but no tool will tell them how the live file differs from that intent, whether
the difference is safe to converge, or exactly what SQL would run. Existing
migration tools either target other databases, hand-roll fragile SQL parsers,
guess at renames heuristically, or silently destroy data on drift resets.
Declaration ──▶ pristine database ──▶ Snapshot ─┐
├─▶ Diff ──▶ Plan ──▶ Apply
live file ───────────────────────────▶ Snapshot ─┘
One Snapshot shape via pristine introspection. The Declaration — canonically plain SQL text — is executed into a throwaway in-memory pristine database and introspected exactly like the live file, so both sides of a diff are the same normalized shape and no SQL parser exists anywhere in the library. Expression text (CHECK bodies, DEFAULT spellings, index WHERE clauses) is opaque: compared token-for-token by SQLite's lexical rules, carried verbatim, never parsed into an AST.
One fixed Equivalence relation. Noise (identifier case and quoting, expression whitespace, sibling order) is erased; Semantic differences (physical column order, declared type text, constraint names) are kept. No configuration knobs. An empty Diff means Equivalent, and vice versa.
The Diff is a product. Flat, self-contained plain-EDN entries you can
filter, store, and dispatch on with ordinary seq functions. It survives
pr-str/read-string byte-identically, so CI can archive and compare it.
Exactly three pure functions operate on it: drift?, drift-report, and
by-object.
The Plan is the review artifact. Planning a Diff under explicit
Capabilities (target SQLite version, :rebuild?) compiles the exact SQL
statements that will run, in execution order. Every Diff entry is either
served by an Op or honestly unhandled with its full Refusal vector:
:incapable (no route exists) or :needs-intent (a route exists but
planning it without explicit intent would risk data).
Intent is explicit, never inferred. Renames and destructive drops require Directives — plain-EDN data supplied to the planner. Directives are conditional: unmatched ones are inert and reported as unused, so one checked-in directive set serves many databases converging at different times. A rename is never guessed from a drop-plus-add.
Data preconditions are Gates, not guesses. Whether rows conform to a
new NOT NULL, UNIQUE, or STRICT shape is undecidable at plan time, so the
planner attaches Gates — plan-compiled SELECTs that sample violating
rows — and the read-only check runs them before any maintenance window.
Apply is strictly atomic. One transaction inside the executor-owned
Frame, all-or-nothing, no partial modes. Apply refuses a database whose
schema_version fingerprint no longer matches the Plan's source Snapshot,
with no override — the remedy is re-diff, re-plan, re-apply. Success
returns an Apply report; every non-success throws.
One error envelope. Every exception carries
:sqlite-migrate/error with structured, reused payload values. All
machine vocabularies — error classes, refusal codes, gate codes, directive
kinds — are open add-only sets: added, never removed or renamed.
Four namespaces (ADR 0013/0014); everything else lives under
sqlite-migrate.impl.* and is not part of the public surface.
| Role | Namespace |
|---|---|
| core pipeline | sqlite-migrate.core |
| executor protocol | sqlite-migrate.protocols |
| JDBC adapter | sqlite-migrate.jdbc |
| EDN schema sugar | sqlite-migrate.schema |
Adapter authors implement the two-op SQLiteExecutor protocol in
sqlite-migrate.protocols; its docstrings are the normative contract.
The design is recorded as ADRs in docs/adr/ (0001–0015) with CONTEXT.md
as the glossary of domain terms (Snapshot, Diff, Plan, Op, Rebuild, Gate,
Directive, Refusal, Executor, Frame, …). Where this overview and an ADR
disagree, the ADR governs.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |