Skip to content

Repository files navigation

litevolve

Versioned SQLite migrations for Bun, Node, and Deno — usable as a library or a CLI.

CI License: MIT

litevolve-bun litevolve-node Bun Node Deno


what_it_does

litevolve reads a directory of numbered SQL files ({version}_name.sql, {version}_name.down.sql, optional {version}_name.seed.sql) and applies them up or down against a SQLite database to reach a target schema version. Each step runs in a single BEGIN IMMEDIATE transaction so a failed seed rolls back its schema change too. The current schema version is tracked in SQLite's native PRAGMA user_version; a sticky init_seeds flag is recorded in an internal _db_meta table so seed behavior stays consistent across subsequent upgrades.

library_status

package source master copy
litevolve-bun runtimes/bun
litevolve-node / for Deno too runtimes/node

Not distributed yet:

  • standalone binaries — built and smoke-tested in CI, not attached to releases
  • the docker imagescripts/Dockerfile exists but no image is published

comparison

How litevolve stacks up against three other JS/TS-ecosystem SQLite migration tools. drizzle-orm is compared on its migration tooling (drizzle-kit) only, not its query builder / ORM layer.

litevolve sqlite-auto-migrator deno-nessie drizzle-orm (migrations only)
Migration format Hand-written .sql files Auto-generated .mjs from a schema-file diff Function- or class-based (up/down), SQL or JS TS schema to SQL generated by drizzle-kit
Runtimes ✅ Bun, Node, Deno Node, Bun Deno only ✅ Node, Bun, Deno
Rollback (down) .down.sql file per version ✅ Generated down function ✅ Explicit down() per migration Manual
Seeding ✅ Built-in .seed.sql (each version has valid seeds) Not built-in ✅ Built-in Not built-in
Runtime dependencies ✅ No runtime dependencies sqlite3 (Node) or Bun's native SQLite Per-client DB drivers Drizzle ORM + driver of choice
Docker image Coming soon None halvardm/nessie (unmaintained, last updated ~5 years ago) None
Executables Coming soon None None None
Package manager npm + coming soon via most common managers (HomeBrew, Snap, ...) npm only deno.land/x, nest.land, not on npm or Homebrew npm only
Stars 1 7 519 35594
Open issues 2 ✅ 0 8 1983
Latest release ✅ 2026-08-26 2025-05-17 2023-09-24 2026-03-27
Latest commit ✅ 2026-08-26 2025-05-17 2023-09-24 2026-07-23

Comparison stats last fetched: 2026-08-26

install

# Bun
bun add litevolve-bun

# Node: npm / pnpm / yarn
npm install litevolve-node
pnpm add litevolve-node
yarn add litevolve-node

# Deno
deno add npm:litevolve-node

# Homebrew (CLI only) Not available yet
brew install litevolve

Node caveat: litevolve-node uses the built-in node:sqlite (DatabaseSync), added in Node 22.5.0 and still marked experimental (Stability 1.2 — Release Candidate). It emits an ExperimentalWarning and its API may shift in a minor/patch release. Requires node >= 22.5.

library_usage

See the example folder for usage examples in different runtimes.

// Import from the runtime-specific package: litevolve-bun or litevolve-node.
import { migrate_db } from "litevolve-bun"

// Apply migrations up (or down) to reach version 2.
// Returns the open Bun Database handle.
const db = migrate_db(
  "./migrations",       // migrations_path: directory holding the .sql files
  "./data/birds.db",    // db_path: SQLite file (or ":memory:")
  true,                 // init_seeds: only honored on a fresh DB at v0
  2,                    // apply_version: target schema version, or undefined for "latest"
)

migrate_db function is defined per runtime in runtimes/<runtime>/src/index.ts.

It opens the database, sets journal_mode = WAL and foreign_keys = ON, then delegates to the shared migrate_with_adapter in src/core/migrate.ts.

It returns the open handle: a Database from bun:sqlite for the Bun package, a DatabaseSync from node:sqlite for the Node one. The error type is migration_error in src/core/migration_error.ts.

CLI_usage

No binaries are published yet: CI build is available, but binaries are not published yet. bunx and npx commands are available.

The CLI takes the same four inputs as named flags. --apply_version is optional — omit it to migrate up to the highest-numbered migration file in migrations_path:

litevolve \
  --apply_version=2 \
  --db_path=./data/birds.db \
  --migrations_path=./migrations \
  --init_seeds

Each package declares a litevolve bin, so it runs via bunx / npx without installing anything:

bunx litevolve-bun --apply_version=2 --db_path=./data/birds.db --migrations_path=./migrations
npx litevolve-node --apply_version=2 --db_path=./data/birds.db --migrations_path=./migrations

From a clone, or as a binary you compile yourself:

bun run runtimes/bun/src/run_litevolve.ts \
  --apply_version=2 --db_path=./data/birds.db --migrations_path=./migrations

make ci_binary TARGET=bun-darwin-arm64   # -> dist/litevolve
dist/litevolve --apply_version=2 --db_path=./data/birds.db --migrations_path=./migrations

docker_usage

No image is published yetscripts/Dockerfile builds the multi-arch binaries but nothing pushes it to a registry. The intended usage, once it is:

To run migrations during a Docker build without installing litevolve's runtime in your image, copy the binary from the official image in a multi-stage build:

FROM litevolve:latest AS migrator

FROM debian:bookworm-slim
COPY --from=migrator /usr/local/bin/litevolve /usr/local/bin/litevolve
COPY ./migrations /migrations
RUN litevolve --apply_version=3 --db_path=./data/app.db --migrations_path=/migrations

For Alpine-based images, use the musl-linked variant:

FROM litevolve:musl AS migrator

This pattern is suited for baking a pre-seeded read-only SQLite file into an image. For runtime migrations against a writable volume, run litevolve at container startup instead.

migration_file_conventions

Files in the migrations directory are validated by a strict regex:

0*[1-9][0-9]*_([a-z]|[A-Z]|[0-9]|_)+\.(sql|seed\.sql|down\.sql)

Breakdown:

  • 0* — optional leading zeros for zero-padding (padding is not required).
  • [1-9][0-9]* — the numeric version: a non-zero leading digit followed by any digits.
  • _ — separator.
  • ([a-z]|[A-Z]|[0-9]|_)+ — a [a-zA-Z0-9_]+ description.
  • \.(sql|seed\.sql|down\.sql) — one of three extensions.
Extension Direction When applied
.sql up when current_version < N ≤ target
.down.sql down when target < N ≤ current_version
.seed.sql up + init_seeds optional, same transaction as .sql

Files that do not match the regex throw a migration_error and abort the run — the directory is read non-recursively and every entry in it must be a migration. Keep auxiliary files (notes, fixtures, sub-directories) out of the migrations directory, including README.md.

Sort order is numeric after stripping leading zeros, not lexicographic. 0999_x.sql sorts before 01000_y.sql because parseInt("0999") is 999 and parseInt("01000") is 1000. Padding is optional and its width can vary across migrations without breaking the order: 1_…, 0042_…, 0999_…, 01000_… all sort correctly together, and an unpadded 42_… sorts identically to 0042_….

Valid examples: 0001_create_initial_schema.sql, 1234_create_users_table.sql, 0042_add_users_language_column.down.sql, 01000_split_audit_log.seed.sql, 0004_iso8601_timestamps.down.sql.

Invalid: 0000_foo.sql (no non-zero digit), 0_foo.sql (version 0), 0001-foo.sql (hyphen not allowed), 0001_foo.txt (wrong extension).

Notes about the parser (see run_sql_statements in runtimes/bun/src/core/migrate.ts):

  • Line comments -- … are stripped, then the file is split on ; and each statement is run on its own — bun:sqlite only surfaces the error of the last statement in a multi-statement string, so a batched migration could silently COMMIT past a failure
  • The splitter is deliberately naive: no ; or -- inside string literals, no BEGIN … END triggers
  • Down migrations never apply seeds. Each .down.sql is responsible for its own data cleanup before dropping columns or tables

init_seeds_semantics

init_seeds is sticky: it is only honored when the database is at version 0 (fresh or fully rolled back). The chosen value is recorded in _db_meta and reused for every subsequent up-migration on the same database. Passing --init_seeds to a partially-migrated DB is silently ignored — this guarantees that a database either consistently has its seed rows or consistently does not. See the behavior contract in runtimes/bun/src/migrate.test.ts (the init_seeds_* tests).

example_ornithology_database

The migrations/working/ directory in this repository ships a runnable three-version example modelling a bird-observation system (the sibling migrations/broken/ holds an intentionally-invalid migration used only by the test suite):

  • v1 (0001_create_initial_schema.sql) — minimal core, four tables:
    • observation_sites (id, name)
    • birders (id, name, joined_at)
    • time_slots (id, site_id, starts_at, ends_at, reserved)
    • sightings (id, birder_id, site_id, species_common_name, observed_at, status)

Optional seed populates 3 sites, 8 birders (Alice Johnson, Bob Smith, …), 32 two-hour observation windows, and 3 sightings (pending / verified / rejected).

  • v2 (0002_expand_schema.sql) — adds richer metadata via ALTER TABLE ADD COLUMN and creates two intake tables:
    • observation_sites gains latitude, longitude, habitat_type, timezone.
    • birders gains email, skill_level, favorite_species, timezone.
    • time_slots gains weather.
    • sightings gains species_scientific_name, individual_count.
    • New tables incoming_reports (id, source, raw_payload, received_at) and incoming_reports_archive (…, archived_at).

Optional seed back-fills coordinates, skill levels, scientific names, weather notes, and sets timezone = 'America/New_York' for Central Park, Alice, and Bob.

  • v3 (0003_add_birder_mentors.sql) — adds mentor_birder_id TEXT REFERENCES birders(id) to birders (a self-referential FK). Optional seed marks Alice as the mentor of Carol/Dan/Eve and Bob as the mentor of Frank/Grace.

The down migration demonstrates the NULL-before-drop pattern for foreign-key removal:

-- 0003_add_birder_mentors.down.sql
UPDATE birders SET mentor_birder_id = NULL;
ALTER TABLE birders DROP COLUMN mentor_birder_id;

Clearing the FK values first is the right habit even when DROP COLUMN would also strip the inline REFERENCES constraint — it's the pattern you must use when removing a FK constraint while keeping the column, since SQLite has no ALTER TABLE DROP CONSTRAINT and the alternative is a CREATE TABLE … / INSERT SELECT / DROP / RENAME table-rebuild that would otherwise copy stale references into the new table.

Drive it from the Makefile:

make migrate_seeds DB_PATH=./birds.db VERSION=2 MIGRATIONS_PATH=./migrations/working
sqlite3 ./birds.db "SELECT name, timezone FROM observation_sites;"

MIGRATIONS_PATH is required here: it defaults to the repository root, which holds no migrations and would throw on the first non-matching filename.

repository_layout

runtimes/bun/     litevolve-bun   — master copy of src/core, plus the bun:sqlite adapter
runtimes/node/    litevolve-node  — generated src/core, plus the node:sqlite adapter
migrations/       working/ example database, broken/ fixture used by the test suite
scripts/          every CI step, as plain shell — the Makefile only calls into these
                  (plus check_bun_version.sh, which is local-only)
examples/         useful examples on how to use the library in Bun, Node, and Deno

contributing_guidelines

Refer to Makefile for a comprehensive list of available helping commands. make ci_checks runs everything CI does, plus make check_version — which checks your installed Bun against .bun-version and has no CI equivalent, since CI installs Bun from that file.

  • OSX is recommended for development
    • if you have any experience contributing to this library under Linux please share your setup
  • litevolve basic ecosystem is Bun, so you need Bun installed in your machine, possibly to the .bun-version version
  • a fix to src/core goes into runtimes/bun and reaches the others through make align_artifacts — editing a generated copy directly will fail make ci_check_align
  • dependency and toolchain versions are Renovate's job
  • the engines field is a floor: the oldest runtime the package supports, not the version we build with. It is raised by hand, only when a breaking change raises the real minimum
  • Makefile approach is opinionated (sorry)
  • Use any editor but don't push any related configuration of it, keep it in your machine

release_process

Versions are driven by changesets, one project per runtime.

# on a dev branch: bump package.json + CHANGELOG.md from the changeset files
make yield_version

# open a PR, get it reviewed, rebase main

# on main, after the merge: tag the merged commit <package>@<semver>
make yield_new_version
git push --follow-tags

The tag is what publishes. .github/workflows/publish.yml triggers on litevolve-{bun,node}@* tags, refuses any tag that is not an ancestor of main, re-checks the tag against package.json, then builds, tests, packs and publishes that one package to npm with trusted publishing and a provenance attestation.

license

MIT

About

A smart way to migrate and version your SQLite database

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages