@wess/atlas/migrate#
Database migration manager using @wess/atlas/db for Postgres and SQLite.
Exports#
Migration Operations#
up(db: Connection, dir?: string) => Promise<string[]>- run pending migrations, returns names of applieddown(db: Connection, dir?: string) => Promise<string | null>- rollback most recent migrationstatus(db: Connection, dir?: string) => Promise<MigrationStatus[]>- list all migrations with applied statusensureTable(db: Connection) => Promise<void>- createschema_migrationstable if missing
File Operations#
createMigration(dir: string, name: string) => MigrationFile- scaffold new migration folder with up.sql/down.sqlscanMigrations(dir: string) => MigrationFile[]- scan directory for migration folders, sorted by name
Diff (schema → migration)#
plan(db, schemas) => Promise<DiffPlan>— compute up/down SQL by introspecting the live DB and comparing it to a list ofdefineSchema()results. Read-only.writeDiff(db, schemas, { dir?, name? }) => Promise<DiffWriteResult>— same plan, but writes a new timestamped migration folder withup.sql/down.sqlif the diff is non-empty. Returns{ noop: true, path: null }when the schema is in sync.
import { migrate } from "@wess/atlas/migrate"
import { users, posts } from "./schema"
const result = await migrate.diff(db, [users, posts], { name: "add_users" })
if (result.noop) console.log("schema in sync; nothing to write")
else console.log(`wrote ${result.path}`)
Postgres uses information_schema; SQLite uses pragma table_info. Type/nullability
mismatches are emitted as -- ALTER table.col … comments so destructive migrations
land in front of the user instead of running silently (line breaks and control
characters in live names are replaced with spaces so a name can't end the comment). serial/integer and PK
nullability quirks are normalized — they won't show up as spurious diffs.
Convenience#
migrate- namespace object with{ up, down, status, create, ensureTable, diff, plan }
Types#
type MigrationFile = {
name: string // "20260402_add_users"
timestamp: string // "20260402"
upPath: string // full path to up.sql
downPath: string // full path to down.sql
}
type MigrationStatus = {
name: string
appliedAt: Date | null // null = pending
}
Migration Directory Structure#
migrations/
20260402_add_users/
up.sql
down.sql
20260403_add_posts/
up.sql
down.sql
Usage#
import { connect } from "@wess/atlas/db"
import { migrate } from "@wess/atlas/migrate"
const db = connect({ driver: "sqlite", path: "./app.db" })
// create a new migration
migrate.create("./migrations", "add_users")
// run all pending
const ran = await migrate.up(db, "./migrations")
// check status
const statuses = await migrate.status(db, "./migrations")
// rollback last
const rolled = await migrate.down(db, "./migrations")
await db.close()
migrate.diff writes .unique() as a constraint on new tables and as CREATE UNIQUE INDEX {table}_{column}_key on existing ones, including tables migrated before this was generated. That index fails on apply if the data has duplicates. Removing .unique() is reported as a review comment. .default(value) and .defaultRaw(sql) are both written to the DDL, and columns compare by stored type, so SQLite booleans report in sync.
On SQLite each migration file is split into statements (quotes, comments, and trigger bodies are respected) and applied with its schema_migrations row in one transaction, so a failure leaves nothing half-applied; don't put BEGIN/COMMIT in a SQLite migration. On Postgres the file runs as one implicit transaction.
Dependencies#
Sibling: @wess/atlas/db. Runtime: bun:sqlite, node:fs.