bunsql-native-migrate 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,14 +5,15 @@
5
5
 
6
6
  Zero-ORM SQL file migrations for [Bun](https://bun.sh): PostgreSQL, MySQL/MariaDB and SQLite through the built-in `Bun.SQL` client.
7
7
 
8
- No ORM, no schema diffing, no lock-in — you write plain `.js` migration files with `up()`/`down()` exports (optionally `up(tx)`/`down(tx)` for transactional migrations, see below) and run them with a tiny CLI or the programmatic API.
8
+ No ORM, no schema diffing, no lock-in — you write plain `.js`/`.ts` migration files with `up()`/`down()` exports (optionally `up(tx)`/`down(tx)` for transactional migrations, see below) and run them with a tiny CLI or the programmatic API.
9
9
 
10
10
  ## Features
11
11
 
12
12
  - **Bun-native** — built on the unified [`Bun.SQL`](https://bun.com/docs/runtime/sql) client (PostgreSQL, MySQL/MariaDB, SQLite). No Node.js support.
13
- - **Zero ORM** — migrations are plain JavaScript files; use `sql` tagged templates or any Bun database client you like.
13
+ - **Zero ORM** — migrations are plain JavaScript or TypeScript files; use `sql` tagged templates or any Bun database client you like.
14
14
  - **Zero dependencies**.
15
15
  - **Checksums** — every applied migration is checksummed (SHA-256). A modified applied file fails the run instead of silently drifting.
16
+ - **Concurrent-safe `up`** — an advisory database lock serializes racing `up` runs (two deploy pods, CI + laptop) so a migration body never executes twice.
16
17
  - **Legacy backfill** — records without a checksum are backfilled automatically on the next `up`.
17
18
  - **CLI and library** — use it as `bunx bunsql-native-migrate` or import the functions directly.
18
19
 
@@ -27,7 +28,7 @@ Requires Bun ≥ 1.4.2 — the version this package is developed and tested agai
27
28
  ## Quick start
28
29
 
29
30
  ```bash
30
- # create migrations/<timestamp>_<name>.js from the stub template
31
+ # create migrations/<timestamp>_<name>.ts from the stub template
31
32
  bunx bunsql-native-migrate create add_users_table
32
33
 
33
34
  # create the tracking table (optional — up() does it automatically)
@@ -36,6 +37,12 @@ bunx bunsql-native-migrate install
36
37
  # apply pending migrations
37
38
  bunx bunsql-native-migrate up
38
39
 
40
+ # see what is applied and what is pending
41
+ bunx bunsql-native-migrate status
42
+
43
+ # CI gate: same listing, but exit code 1 while migrations are pending
44
+ bunx bunsql-native-migrate status --strict
45
+
39
46
  # roll back the last applied migration
40
47
  bunx bunsql-native-migrate down
41
48
  ```
@@ -58,10 +65,10 @@ const down = async () => {
58
65
  export { up, down };
59
66
  ```
60
67
 
61
- Files live in the migrations directory (default `./migrations`, override with `--dir` or the `MIGRATION_LIST_DIR` env var) and are applied in descending filename order. `bunsql-native-migrate create` generates names with an inverted timestamp prefix so newer migrations sort first:
68
+ Files live in the migrations directory (default `./migrations`, override with `--dir` or the `MIGRATION_LIST_DIR` env var) and are applied in descending filename order. Both `.js` and `.ts` files work — Bun runs TypeScript natively — and the order is decided purely by the filename, never by the extension. `bunsql-native-migrate create` generates TypeScript stubs by default (`--lang js` for JavaScript) with an inverted timestamp prefix so newer migrations sort first:
62
69
 
63
70
  ```
64
- 9999999999999_2026_09_13_add_users_table.js
71
+ 9999999999999_2026_09_13_add_users_table.ts
65
72
  ```
66
73
 
67
74
  ### Transactional migrations
@@ -81,7 +88,7 @@ const down = async (tx) => {
81
88
  export { up, down };
82
89
  ```
83
90
 
84
- - `bunsql-native-migrate create` generates stubs in this form by default; `tx` is a Bun `SQL` client bound to the same database the runner is connected to.
91
+ - `bunsql-native-migrate create` generates stubs in this form by default (as TypeScript, with `tx` typed as a Bun `SQL` client bound to the same database the runner is connected to).
85
92
  - Migrations declared **without** parameters keep using the global `sql` client and run without a transaction — both styles can coexist in one project, decided per migration by the declared signature.
86
93
  - Engine caveats: PostgreSQL and SQLite roll back everything, DDL included. On MySQL/MariaDB any DDL statement implicitly commits the current transaction, so there only DML gets rollback protection.
87
94
  - The tracking record is written right after the transaction commits. A crash in that single-statement window leaves the migration applied but unrecorded — the next `up` would re-run it, so keep critical migrations idempotent (this window exists for plain `up()` migrations too, just wider).
@@ -101,21 +108,27 @@ This resolution is part of the library contract: `resolveListDir` (and therefore
101
108
  ## CLI reference
102
109
 
103
110
  ```
104
- bunsql-native-migrate <up|down|install|create [name]> [--dir <migrations-dir>] [--git] [--help]
111
+ bunsql-native-migrate <up|down [n]|install|create [name]|status> [--dir <migrations-dir>] [--to <name>] [--lock-timeout <seconds>] [--all] [--lang <js|ts>] [--git] [--strict] [--help]
105
112
  ```
106
113
 
107
- | Command | What it does |
108
- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
109
- | `up` | Applies pending migrations (creating the tracking table if needed); prints `No pending migrations.` when there is nothing to apply. |
110
- | `down` | Reverts the last applied migration; prints `No migrations to rollback.` when there is none. |
111
- | `install` | Creates the tracking table only. |
112
- | `create [name]` | Creates a stub migration file from the template; without a `name` a random `adjective_noun` is generated. |
113
-
114
- | Flag | Applies to | Meaning |
115
- | ------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------- |
116
- | `--dir <migrations-dir>` | all | Migrations directory (default `./migrations`, or the `MIGRATION_LIST_DIR` env var). |
117
- | `--git` | `create` | `git add` the created file. When staging fails, the CLI prints an error and exits 1 — the file itself stays on disk. |
118
- | `--help`, `-h` | — | Prints the usage line and exits with code 0. |
114
+ | Command | What it does |
115
+ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
+ | `up` | Applies pending migrations (creating the tracking table if needed); prints `No pending migrations.` when there is nothing to apply. `up --to <name>` applies pending migrations in order up to and including the named one. |
117
+ | `down` | Reverts the last applied migration; `down <n>` reverts the last `n`, `down --all` reverts everything — always most-recent-first. Prints `No migrations to rollback.` when there is none. |
118
+ | `install` | Creates the tracking table only. |
119
+ | `create [name]` | Creates a stub migration file from the template — TypeScript by default, `--lang js` for JavaScript; without a `name` a random `adjective_noun` is generated. |
120
+ | `status` | Lists applied and pending migrations (no changes to the database except creating the tracking table if missing) and prints an `N applied, M pending` summary. |
121
+
122
+ | Flag | Applies to | Meaning |
123
+ | ------------------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
124
+ | `--dir <migrations-dir>` | all | Migrations directory (default `./migrations`, or the `MIGRATION_LIST_DIR` env var). |
125
+ | `--to <name>` | `up` | Apply pending migrations up to and including the named file. An unknown name is an error (exit 1); a target that is already applied is a no-op. |
126
+ | `--lock-timeout <sec>` | `up` | How long to wait for the migration lock when another `up` is running, in seconds (default `30`, `0` fails immediately). On timeout: exit 1. |
127
+ | `--all` | `down` | Revert every applied migration (most-recent-first). Cannot be combined with a step count. |
128
+ | `--git` | `create` | `git add` the created file. When staging fails, the CLI prints an error and exits 1 — the file itself stays on disk. |
129
+ | `--lang <js\|ts>` | `create` | Language of the created stub. Default: `ts`. An unknown or missing value is an error (exit 1). |
130
+ | `--strict` | `status` | Exit with code 1 when migrations are pending — a gate for CI/CD pipelines. Exit 0 otherwise. |
131
+ | `--help`, `-h` | — | Prints the usage line and exits with code 0. |
119
132
 
120
133
  Any failure (connection errors, a failing migration, a modified applied file) is printed and the CLI exits with code 1; the same happens for an unknown command or a call without a command.
121
134
 
@@ -125,19 +138,30 @@ Any failure (connection errors, a failing migration, a modified applied file) is
125
138
  import {
126
139
  migrateUp,
127
140
  migrateDown,
141
+ migrateStatus,
128
142
  installMigrations,
129
143
  createMigration,
130
144
  createDriver,
131
145
  ChecksumDriftError,
146
+ MigrationLockError,
147
+ MigrationNotFoundError,
132
148
  GitStageError,
133
149
  } from "bunsql-native-migrate";
134
150
 
135
151
  const { applied } = await migrateUp({
136
152
  databaseUrl: "postgres://user:pass@localhost:5432/app", // default: DATABASE_URL env
137
153
  listDir: "./migrations", // default: MIGRATION_LIST_DIR env or ./migrations
154
+ to: "2_add_columns.ts", // optional: apply up to and including this file
155
+ lockTimeout: 60, // optional: seconds to wait for the migration lock (default 30, 0 = fail fast)
138
156
  });
139
157
 
140
- const { reverted } = await migrateDown(); // reverted: string | null
158
+ const { reverted } = await migrateDown(); // reverted: string[] (most-recent-first)
159
+ await migrateDown({ steps: 3 }); // revert the last three
160
+ await migrateDown({ steps: "all" }); // revert everything
161
+
162
+ const status = await migrateStatus();
163
+ // status.applied: ExecutedMigration[] (name + checksum, in apply order)
164
+ // status.pending: string[] (files waiting to be applied, in apply order)
141
165
 
142
166
  await installMigrations(); // creates the tracking table
143
167
 
@@ -146,12 +170,18 @@ const filename = await createMigration({ name: "add_users_table", listDir: "./mi
146
170
 
147
171
  All options are optional unless stated otherwise:
148
172
 
149
- | Option | Where | Default |
150
- | ------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------- |
151
- | `databaseUrl` | `migrateUp`, `migrateDown`, `installMigrations` | `DATABASE_URL` env var |
152
- | `listDir` | all functions | `MIGRATION_LIST_DIR` env var, then `./migrations` (relative paths resolve against the process cwd) |
153
- | `name` | `createMigration` | random `adjective_noun` name |
154
- | `git` | `createMigration` | `false` — `git add` the new file |
173
+ | Option | Where | Default |
174
+ | ------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
175
+ | `databaseUrl` | `migrateUp`, `migrateDown`, `migrateStatus`, `installMigrations` | `DATABASE_URL` env var |
176
+ | `listDir` | all functions | `MIGRATION_LIST_DIR` env var, then `./migrations` (relative paths resolve against the process cwd) |
177
+ | `to` | `migrateUp` | — apply pending migrations in order up to and including the named file; an unknown name throws `MigrationNotFoundError`, an already-applied target applies nothing |
178
+ | `lockTimeout` | `migrateUp` | `30` — seconds to wait for the migration lock while another `up` is running; `0` fails immediately; a timeout throws `MigrationLockError` |
179
+ | `steps` | `migrateDown` | `1` — revert the last `n` applied migrations (`1`–`n` or `"all"`), most-recent-first |
180
+ | `name` | `createMigration` | random `adjective_noun` name |
181
+ | `lang` | `createMigration` | `"ts"` — pass `"js"` for a JavaScript stub |
182
+ | `git` | `createMigration` | `false` — `git add` the new file |
183
+
184
+ `migrateDown` reverts strictly in reverse apply order and stops at the first failing rollback: migrations reverted before the failure stay reverted, the failing one keeps its tracking record, and the error propagates to the caller.
155
185
 
156
186
  ### Drivers
157
187
 
@@ -169,7 +199,7 @@ Known MySQL/MariaDB limitation of `Bun.SQL` (not used by this package, good to k
169
199
 
170
200
  Bun's client also recognizes `mysql2://` and `file://` URLs — `createDriver` deliberately rejects them. Stick to the protocols listed above.
171
201
 
172
- `createDriver(url)` returns a `MigrationDriver` (`install`/`listExecuted`/`record`/`setChecksum`/`remove`/`close`) if you need custom tracking logic; `listExecuted` yields `ExecutedMigration` records (`name`, `checksum`).
202
+ `createDriver(url)` returns a `MigrationDriver` (`install`/`listExecuted`/`record`/`setChecksum`/`remove`/`close`) if you need custom tracking logic; `listExecuted` yields `ExecutedMigration` records (`name`, `checksum`). Custom drivers may also implement the optional `tryLock(timeoutSeconds)` / `releaseLock()` pair to participate in [concurrent-run locking](#concurrent-runs) — without them, `migrateUp` simply runs unlocked.
173
203
 
174
204
  ### Tracking table
175
205
 
@@ -181,9 +211,19 @@ Applied migrations are recorded in a `migrations` table with a unique name and a
181
211
 
182
212
  Records created before checksums existed (checksum `NULL`) are backfilled on the next `up`.
183
213
 
214
+ ### Concurrent runs
215
+
216
+ Two `up` runs racing (two deploy pods, CI and a laptop) deduplicate only the tracking record against each other — without a lock both would read the same pending list and execute the migration bodies twice. `migrateUp` therefore takes an exclusive database-level lock right after creating the tracking table and holds it until the run ends. The lock is released through a `finally` path on any failure, and it dies with the connection even on a crashed process:
217
+
218
+ - **PostgreSQL** — session advisory lock (`pg_try_advisory_lock` / `pg_advisory_unlock`) on a reserved connection, keyed per database.
219
+ - **MySQL/MariaDB** — `GET_LOCK` / `RELEASE_LOCK` on a reserved connection, named per database.
220
+ - **SQLite** — there is no advisory lock; the connection gets `PRAGMA busy_timeout`, so a concurrent run waits on the file write lock and fails with a busy error once the timeout is exceeded. The database file itself is the serialization point.
221
+
222
+ While the lock is held, another `up` polls and waits up to `lockTimeout` seconds (default `30`, CLI `up --lock-timeout <seconds>`, `0` fails immediately). When the wait times out, `migrateUp` throws `MigrationLockError` and the CLI exits with code 1 — rerun after the first run finishes; a waiter that gets through just reports `No pending migrations.`
223
+
184
224
  ### Error handling
185
225
 
186
- The library throws instead of exiting: connection errors, failing migrations and [`ChecksumDriftError`](#tracking-table) propagate to the caller, and the driver connection is always closed. The CLI catches these and exits with code 1.
226
+ The library throws instead of exiting: connection errors, failing migrations, [`ChecksumDriftError`](#tracking-table), `MigrationNotFoundError` (an unknown `to` target, thrown before anything is applied) and [`MigrationLockError`](#concurrent-runs) (another `up` held the lock past `lockTimeout`) propagate to the caller, and the driver connection is always closed. The CLI catches these and exits with code 1.
187
227
 
188
228
  `createMigration` with `git: true` throws `GitStageError` when `git add` fails (no repository, ignored path, …) — the migration file itself is still created on disk.
189
229
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunsql-native-migrate",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Zero-ORM SQL file migrations for Bun: PostgreSQL, MySQL/MariaDB and SQLite through the built-in Bun.SQL client",
5
5
  "keywords": [
6
6
  "bun",
package/src/api/create.ts CHANGED
@@ -5,13 +5,16 @@ import { resolveListDir } from "../core/fs.js";
5
5
  import { log } from "../core/console.js";
6
6
  import { GitStageError } from "./options.js";
7
7
 
8
+ export type MigrationLang = "js" | "ts";
9
+
8
10
  export interface CreateOptions {
9
11
  name?: string;
10
12
  git?: boolean;
13
+ lang?: MigrationLang;
11
14
  listDir: string;
12
15
  }
13
16
 
14
- const STUB_TEMPLATE = `import { sql } from "bun";
17
+ const JS_TEMPLATE = `import { sql } from "bun";
15
18
  // Write your migration SQL here (tx runs inside a transaction)
16
19
  const up = async (tx) => {};
17
20
 
@@ -21,6 +24,20 @@ const down = async (tx) => {};
21
24
  export { up, down };
22
25
  `;
23
26
 
27
+ const TS_TEMPLATE = `import { sql, type SQL } from "bun";
28
+ // Write your migration SQL here (tx runs inside a transaction)
29
+ const up = async (tx: SQL) => {};
30
+
31
+ // Write your rollback SQL here
32
+ const down = async (tx: SQL) => {};
33
+
34
+ export { up, down };
35
+ `;
36
+
37
+ function stubTemplate(lang: MigrationLang): string {
38
+ return lang === "js" ? JS_TEMPLATE : TS_TEMPLATE;
39
+ }
40
+
24
41
  async function stageInGit(filePath: string): Promise<void> {
25
42
  const add = Bun.spawn({
26
43
  cmd: ["git", "add", filePath],
@@ -34,7 +51,7 @@ async function stageInGit(filePath: string): Promise<void> {
34
51
  }
35
52
  }
36
53
 
37
- function migrationFilename(name: string, date: Date): string {
54
+ function migrationFilename(name: string, date: Date, lang: MigrationLang): string {
38
55
  const pad = (value: number) => (value <= 9 ? `0${value}` : `${value}`);
39
56
  const MAX_TIME = 9999999999999;
40
57
  const invertedTime = (MAX_TIME - date.getTime()).toString().padStart(13, "0");
@@ -44,10 +61,10 @@ function migrationFilename(name: string, date: Date): string {
44
61
  pad(date.getUTCMonth() + 1),
45
62
  pad(date.getUTCDate()),
46
63
  ].join("_");
47
- return `${timestamp}_${name}.js`;
64
+ return `${timestamp}_${name}.${lang}`;
48
65
  }
49
66
 
50
- async function writeStubExclusively(filePath: string): Promise<boolean> {
67
+ async function writeStubExclusively(filePath: string, template: string): Promise<boolean> {
51
68
  let handle: FileHandle;
52
69
  try {
53
70
  handle = await open(filePath, "wx");
@@ -58,7 +75,7 @@ async function writeStubExclusively(filePath: string): Promise<boolean> {
58
75
  throw error;
59
76
  }
60
77
  try {
61
- await handle.writeFile(STUB_TEMPLATE);
78
+ await handle.writeFile(template);
62
79
  } finally {
63
80
  await handle.close();
64
81
  }
@@ -67,12 +84,13 @@ async function writeStubExclusively(filePath: string): Promise<boolean> {
67
84
 
68
85
  export async function createMigration(options: CreateOptions): Promise<string> {
69
86
  const name = options.name ?? randomName();
87
+ const lang = options.lang ?? "ts";
70
88
  await mkdir(options.listDir, { recursive: true });
71
- let filename = migrationFilename(name, new Date());
89
+ let filename = migrationFilename(name, new Date(), lang);
72
90
  let filePath = path.join(options.listDir, filename);
73
- while (!(await writeStubExclusively(filePath))) {
91
+ while (!(await writeStubExclusively(filePath, stubTemplate(lang)))) {
74
92
  await Bun.sleep(1);
75
- filename = migrationFilename(name, new Date());
93
+ filename = migrationFilename(name, new Date(), lang);
76
94
  filePath = path.join(options.listDir, filename);
77
95
  }
78
96
 
package/src/api/down.ts CHANGED
@@ -1,33 +1,58 @@
1
1
  import path from "node:path";
2
2
  import { resolveListDir } from "../core/fs.js";
3
3
  import { log } from "../core/console.js";
4
- import type { MigrateDownResult, MigrateOptions } from "./options.js";
4
+ import type { MigrationDriver } from "../core/driver.js";
5
+ import type { MigrateDownOptions, MigrateDownResult } from "./options.js";
5
6
  import { runWithDriver } from "./run-with-driver.js";
6
7
  import { runMigrationStep } from "./run-step.js";
7
8
 
8
- export async function migrateDown(options: MigrateOptions = {}): Promise<MigrateDownResult> {
9
+ function resolveStepCount(steps: number | "all" | undefined, appliedCount: number): number {
10
+ if (steps === undefined) return 1;
11
+ if (steps === "all") return appliedCount;
12
+ if (!Number.isInteger(steps) || steps < 1) {
13
+ throw new Error(`Invalid steps: ${String(steps)} — expected a positive integer or "all"`);
14
+ }
15
+ return steps;
16
+ }
17
+
18
+ async function revertOne(driver: MigrationDriver, listDir: string, file: string): Promise<void> {
19
+ const mod = await import(path.join(listDir, file));
20
+
21
+ if (typeof mod.down !== "function") {
22
+ log({ text: `${file} has no down() export, removing tracking record`, type: "warn" });
23
+ await driver.remove(file);
24
+ log({ text: `${file} tracking record removed`, type: "success" });
25
+ return;
26
+ }
27
+
28
+ await runMigrationStep(driver, mod.down);
29
+ await driver.remove(file);
30
+ log({ text: `${file} rolled back`, type: "success" });
31
+ }
32
+
33
+ export async function migrateDown(options: MigrateDownOptions = {}): Promise<MigrateDownResult> {
9
34
  const listDir = resolveListDir(options.listDir);
10
35
 
11
36
  return runWithDriver(options, async (driver) => {
12
37
  const executed = await driver.listExecuted();
13
38
  if (executed.length === 0) {
14
39
  log({ text: "No migrations to rollback.", type: "warn" });
15
- return { reverted: null };
40
+ return { reverted: [] };
16
41
  }
17
42
 
18
- const file = executed[executed.length - 1]!.name;
19
- const mod = await import(path.join(listDir, file));
43
+ const count = resolveStepCount(options.steps, executed.length);
44
+ const reverted: string[] = [];
20
45
 
21
- if (typeof mod.down !== "function") {
22
- log({ text: `${file} has no down() export, removing tracking record`, type: "warn" });
23
- await driver.remove(file);
24
- log({ text: `${file} tracking record removed`, type: "success" });
25
- return { reverted: file };
46
+ for (const entry of executed.slice(-count).reverse()) {
47
+ try {
48
+ await revertOne(driver, listDir, entry.name);
49
+ } catch (error) {
50
+ log({ text: `${entry.name} rollback failed`, type: "error", error });
51
+ throw error;
52
+ }
53
+ reverted.push(entry.name);
26
54
  }
27
55
 
28
- await runMigrationStep(driver, mod.down);
29
- await driver.remove(file);
30
- log({ text: `${file} rolled back`, type: "success" });
31
- return { reverted: file };
56
+ return { reverted };
32
57
  });
33
58
  }
@@ -0,0 +1,50 @@
1
+ import { log } from "../core/console.js";
2
+ import type { MigrationDriver } from "../core/driver.js";
3
+ import { MigrationLockError } from "./options.js";
4
+
5
+ export const DEFAULT_LOCK_TIMEOUT_SECONDS = 30;
6
+
7
+ const LOCK_RETRY_DELAY_MS = 100;
8
+
9
+ export function resolveLockTimeout(lockTimeout: number | undefined): number {
10
+ if (lockTimeout === undefined) return DEFAULT_LOCK_TIMEOUT_SECONDS;
11
+ if (!Number.isInteger(lockTimeout) || lockTimeout < 0) {
12
+ throw new Error(
13
+ `Invalid lockTimeout: ${String(lockTimeout)} — expected a non-negative integer of seconds`,
14
+ );
15
+ }
16
+ return lockTimeout;
17
+ }
18
+
19
+ export async function withMigrationLock<T>(
20
+ driver: MigrationDriver,
21
+ timeoutSeconds: number,
22
+ run: () => Promise<T>,
23
+ ): Promise<T> {
24
+ const { tryLock, releaseLock } = driver;
25
+ if (tryLock === undefined || releaseLock === undefined) {
26
+ return run();
27
+ }
28
+
29
+ const deadline = Date.now() + timeoutSeconds * 1000;
30
+ let waitingLogged = false;
31
+ while (!(await tryLock(timeoutSeconds))) {
32
+ if (Date.now() >= deadline) {
33
+ throw new MigrationLockError(timeoutSeconds);
34
+ }
35
+ if (!waitingLogged) {
36
+ log({ text: "another migrate up holds the lock — waiting", type: "info" });
37
+ waitingLogged = true;
38
+ }
39
+ await Bun.sleep(LOCK_RETRY_DELAY_MS);
40
+ }
41
+
42
+ try {
43
+ const result = await run();
44
+ await releaseLock();
45
+ return result;
46
+ } catch (error) {
47
+ await releaseLock().catch(() => undefined);
48
+ throw error;
49
+ }
50
+ }
@@ -1,14 +1,30 @@
1
+ import type { ExecutedMigration } from "../core/driver.js";
2
+
1
3
  export interface MigrateOptions {
2
4
  databaseUrl?: string;
3
5
  listDir?: string;
4
6
  }
5
7
 
8
+ export interface MigrateUpOptions extends MigrateOptions {
9
+ to?: string;
10
+ lockTimeout?: number;
11
+ }
12
+
13
+ export interface MigrateDownOptions extends MigrateOptions {
14
+ steps?: number | "all";
15
+ }
16
+
6
17
  export interface MigrateUpResult {
7
18
  applied: string[];
8
19
  }
9
20
 
10
21
  export interface MigrateDownResult {
11
- reverted: string | null;
22
+ reverted: string[];
23
+ }
24
+
25
+ export interface MigrateStatusResult {
26
+ applied: ExecutedMigration[];
27
+ pending: string[];
12
28
  }
13
29
 
14
30
  export class ChecksumDriftError extends Error {
@@ -23,6 +39,28 @@ export class ChecksumDriftError extends Error {
23
39
  }
24
40
  }
25
41
 
42
+ export class MigrationNotFoundError extends Error {
43
+ readonly file: string;
44
+
45
+ constructor(file: string) {
46
+ super(`${file} is not in the migrations directory — nothing was applied`);
47
+ this.name = "MigrationNotFoundError";
48
+ this.file = file;
49
+ }
50
+ }
51
+
52
+ export class MigrationLockError extends Error {
53
+ readonly timeoutSeconds: number;
54
+
55
+ constructor(timeoutSeconds: number) {
56
+ super(
57
+ `could not acquire the migration lock within ${timeoutSeconds}s — another migrate up is probably still running`,
58
+ );
59
+ this.name = "MigrationLockError";
60
+ this.timeoutSeconds = timeoutSeconds;
61
+ }
62
+ }
63
+
26
64
  export class GitStageError extends Error {
27
65
  readonly file: string;
28
66
  readonly exitCode: number;
@@ -0,0 +1,20 @@
1
+ import { listFiles, MIGRATION_EXTENSIONS, resolveListDir } from "../core/fs.js";
2
+ import type { MigrateOptions, MigrateStatusResult } from "./options.js";
3
+ import { runWithDriver } from "./run-with-driver.js";
4
+
5
+ export async function migrateStatus(options: MigrateOptions = {}): Promise<MigrateStatusResult> {
6
+ const listDir = resolveListDir(options.listDir);
7
+
8
+ return runWithDriver(options, async (driver) => {
9
+ await driver.install();
10
+
11
+ const files = await listFiles(listDir, MIGRATION_EXTENSIONS);
12
+ const executed = await driver.listExecuted();
13
+ const appliedNames = new Set(executed.map((entry) => entry.name));
14
+
15
+ return {
16
+ applied: executed,
17
+ pending: files.filter((file) => !appliedNames.has(file)),
18
+ };
19
+ });
20
+ }
package/src/api/up.ts CHANGED
@@ -1,68 +1,92 @@
1
1
  import path from "node:path";
2
- import { checksumFile, listFiles, resolveListDir } from "../core/fs.js";
2
+ import { checksumFile, listFiles, MIGRATION_EXTENSIONS, resolveListDir } from "../core/fs.js";
3
3
  import { log } from "../core/console.js";
4
- import { type MigrateOptions, type MigrateUpResult, ChecksumDriftError } from "./options.js";
4
+ import {
5
+ type MigrateUpOptions,
6
+ type MigrateUpResult,
7
+ ChecksumDriftError,
8
+ MigrationNotFoundError,
9
+ } from "./options.js";
5
10
  import { runWithDriver } from "./run-with-driver.js";
6
11
  import { runMigrationStep } from "./run-step.js";
12
+ import { resolveLockTimeout, withMigrationLock } from "./lock.js";
7
13
 
8
- export async function migrateUp(options: MigrateOptions = {}): Promise<MigrateUpResult> {
14
+ export async function migrateUp(options: MigrateUpOptions = {}): Promise<MigrateUpResult> {
9
15
  const listDir = resolveListDir(options.listDir);
16
+ const target = options.to;
17
+ const lockTimeout = resolveLockTimeout(options.lockTimeout);
10
18
 
11
19
  return runWithDriver(options, async (driver) => {
12
20
  await driver.install();
13
21
 
14
- const allFiles = await listFiles(listDir, "js");
15
- const checksums = new Map(
16
- await Promise.all(
17
- allFiles.map(async (file) => [file, await checksumFile(path.join(listDir, file))] as const),
18
- ),
19
- );
22
+ return withMigrationLock(driver, lockTimeout, async () => {
23
+ const allFiles = await listFiles(listDir, MIGRATION_EXTENSIONS);
24
+ if (target !== undefined && !allFiles.includes(target)) {
25
+ throw new MigrationNotFoundError(target);
26
+ }
27
+
28
+ const checksums = new Map(
29
+ await Promise.all(
30
+ allFiles.map(
31
+ async (file) => [file, await checksumFile(path.join(listDir, file))] as const,
32
+ ),
33
+ ),
34
+ );
35
+
36
+ const executed = await driver.listExecuted();
37
+ const executedByName = new Map(executed.map((entry) => [entry.name, entry]));
20
38
 
21
- const executed = await driver.listExecuted();
22
- const executedByName = new Map(executed.map((entry) => [entry.name, entry]));
39
+ for (const [file, checksum] of checksums) {
40
+ const record = executedByName.get(file);
41
+ if (!record) continue;
23
42
 
24
- for (const [file, checksum] of checksums) {
25
- const record = executedByName.get(file);
26
- if (!record) continue;
43
+ if (record.checksum === null) {
44
+ await driver.setChecksum(file, checksum);
45
+ log({ text: `${file} checksum saved (legacy record)`, type: "info" });
46
+ continue;
47
+ }
27
48
 
28
- if (record.checksum === null) {
29
- await driver.setChecksum(file, checksum);
30
- log({ text: `${file} checksum saved (legacy record)`, type: "info" });
31
- continue;
49
+ if (record.checksum !== checksum) {
50
+ throw new ChecksumDriftError(file);
51
+ }
32
52
  }
33
53
 
34
- if (record.checksum !== checksum) {
35
- throw new ChecksumDriftError(file);
54
+ let pending = allFiles.filter((file) => !executedByName.has(file));
55
+ if (target !== undefined) {
56
+ if (executedByName.has(target)) {
57
+ log({ text: `${target} is already applied.`, type: "info" });
58
+ return { applied: [] };
59
+ }
60
+ pending = pending.slice(0, pending.indexOf(target) + 1);
36
61
  }
37
- }
38
62
 
39
- const pending = allFiles.filter((file) => !executedByName.has(file));
40
- const applied: string[] = [];
63
+ const applied: string[] = [];
41
64
 
42
- if (pending.length === 0) {
43
- log({ text: "No pending migrations.", type: "warn" });
44
- return { applied };
45
- }
65
+ if (pending.length === 0) {
66
+ log({ text: "No pending migrations.", type: "warn" });
67
+ return { applied };
68
+ }
46
69
 
47
- for (const file of pending) {
48
- const checksum = checksums.get(file);
49
- if (!checksum) continue;
50
- try {
51
- const mod = await import(path.join(listDir, file));
52
- if (typeof mod.up !== "function") {
53
- log({ text: `${file} has no up() export, skipping`, type: "warn" });
54
- continue;
70
+ for (const file of pending) {
71
+ const checksum = checksums.get(file);
72
+ if (!checksum) continue;
73
+ try {
74
+ const mod = await import(path.join(listDir, file));
75
+ if (typeof mod.up !== "function") {
76
+ log({ text: `${file} has no up() export, skipping`, type: "warn" });
77
+ continue;
78
+ }
79
+ await runMigrationStep(driver, mod.up);
80
+ await driver.record(file, checksum);
81
+ applied.push(file);
82
+ log({ text: `${file} migrated up`, type: "success" });
83
+ } catch (error) {
84
+ log({ text: `${file} migration failed`, type: "error", error });
85
+ throw error;
55
86
  }
56
- await runMigrationStep(driver, mod.up);
57
- await driver.record(file, checksum);
58
- applied.push(file);
59
- log({ text: `${file} migrated up`, type: "success" });
60
- } catch (error) {
61
- log({ text: `${file} migration failed`, type: "error", error });
62
- throw error;
63
87
  }
64
- }
65
88
 
66
- return { applied };
89
+ return { applied };
90
+ });
67
91
  });
68
92
  }
package/src/cli/main.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
  import { migrateUp } from "../api/up.js";
3
3
  import { migrateDown } from "../api/down.js";
4
+ import { migrateStatus } from "../api/status.js";
4
5
  import { installMigrations } from "../api/install.js";
5
- import { createMigrationCommand } from "../api/create.js";
6
- import { ChecksumDriftError } from "../api/options.js";
6
+ import { createMigrationCommand, type MigrationLang } from "../api/create.js";
7
+ import { ChecksumDriftError, MigrationLockError, MigrationNotFoundError } from "../api/options.js";
7
8
  import { log } from "../core/console.js";
8
9
 
9
10
  interface CliArgs {
@@ -11,6 +12,11 @@ interface CliArgs {
11
12
  positional: string[];
12
13
  dir?: string | undefined;
13
14
  git: boolean;
15
+ lang?: MigrationLang | undefined;
16
+ to?: string | undefined;
17
+ lockTimeout?: number | undefined;
18
+ all: boolean;
19
+ strict: boolean;
14
20
  help: boolean;
15
21
  }
16
22
 
@@ -18,6 +24,11 @@ function parseArgs(argv: string[]): CliArgs {
18
24
  const positional: string[] = [];
19
25
  let dir: string | undefined;
20
26
  let git = false;
27
+ let lang: MigrationLang | undefined;
28
+ let to: string | undefined;
29
+ let lockTimeout: number | undefined;
30
+ let all = false;
31
+ let strict = false;
21
32
  let help = false;
22
33
  for (let i = 0; i < argv.length; i++) {
23
34
  const arg = argv[i]!;
@@ -25,18 +36,60 @@ function parseArgs(argv: string[]): CliArgs {
25
36
  dir = argv[++i];
26
37
  } else if (arg === "--git") {
27
38
  git = true;
39
+ } else if (arg === "--lang") {
40
+ const value = argv[++i];
41
+ if (value !== "js" && value !== "ts") {
42
+ log({
43
+ text: `Unknown --lang value: ${value ?? "(missing)"} (expected js or ts)`,
44
+ type: "error",
45
+ });
46
+ usage(1);
47
+ }
48
+ lang = value;
49
+ } else if (arg === "--to") {
50
+ to = argv[++i];
51
+ if (to === undefined) {
52
+ log({ text: "--to requires a migration file name", type: "error" });
53
+ usage(1);
54
+ }
55
+ } else if (arg === "--lock-timeout") {
56
+ const value = argv[++i];
57
+ const parsed = Number(value);
58
+ if (value === undefined || !Number.isInteger(parsed) || parsed < 0) {
59
+ log({
60
+ text: `Invalid --lock-timeout: ${value ?? "(missing)"} (expected a non-negative integer of seconds)`,
61
+ type: "error",
62
+ });
63
+ usage(1);
64
+ }
65
+ lockTimeout = parsed;
66
+ } else if (arg === "--all") {
67
+ all = true;
68
+ } else if (arg === "--strict") {
69
+ strict = true;
28
70
  } else if (arg === "--help" || arg === "-h") {
29
71
  help = true;
30
72
  } else {
31
73
  positional.push(arg);
32
74
  }
33
75
  }
34
- return { command: positional.shift(), positional, dir, git, help };
76
+ return {
77
+ command: positional.shift(),
78
+ positional,
79
+ dir,
80
+ git,
81
+ lang,
82
+ to,
83
+ lockTimeout,
84
+ all,
85
+ strict,
86
+ help,
87
+ };
35
88
  }
36
89
 
37
90
  function usage(exitCode: number): never {
38
91
  log({
39
- text: "Usage: bunsql-native-migrate <up|down|install|create [name]> [--dir <migrations-dir>] [--git] [--help]",
92
+ text: "Usage: bunsql-native-migrate <up|down [n]|install|create [name]|status> [--dir <migrations-dir>] [--to <name>] [--lock-timeout <seconds>] [--all] [--lang <js|ts>] [--git] [--strict] [--help]",
40
93
  type: "info",
41
94
  });
42
95
  process.exit(exitCode);
@@ -52,14 +105,40 @@ if (args.help) {
52
105
  try {
53
106
  switch (args.command) {
54
107
  case "up": {
55
- const { applied } = await migrateUp(listDirOptions);
108
+ const { applied } = await migrateUp({
109
+ ...listDirOptions,
110
+ ...(args.to ? { to: args.to } : {}),
111
+ ...(args.lockTimeout !== undefined ? { lockTimeout: args.lockTimeout } : {}),
112
+ });
56
113
  if (applied.length > 0) {
57
114
  log({ text: `Applied ${applied.length} migration(s).`, type: "success" });
58
115
  }
59
116
  break;
60
117
  }
61
118
  case "down": {
62
- await migrateDown(listDirOptions);
119
+ const [stepsArg] = args.positional;
120
+ if (args.all && stepsArg !== undefined) {
121
+ log({ text: "Use either --all or a number of steps, not both.", type: "error" });
122
+ usage(1);
123
+ }
124
+ let steps: number | "all" = 1;
125
+ if (args.all) {
126
+ steps = "all";
127
+ } else if (stepsArg !== undefined) {
128
+ const parsed = Number(stepsArg);
129
+ if (!Number.isInteger(parsed) || parsed < 1) {
130
+ log({
131
+ text: `Invalid step count: ${stepsArg} (expected a positive integer)`,
132
+ type: "error",
133
+ });
134
+ usage(1);
135
+ }
136
+ steps = parsed;
137
+ }
138
+ const { reverted } = await migrateDown({ ...listDirOptions, steps });
139
+ if (reverted.length > 0) {
140
+ log({ text: `Reverted ${reverted.length} migration(s).`, type: "success" });
141
+ }
63
142
  break;
64
143
  }
65
144
  case "install": {
@@ -70,16 +149,36 @@ try {
70
149
  const [name] = args.positional;
71
150
  await createMigrationCommand({
72
151
  ...(name ? { name } : {}),
152
+ ...(args.lang ? { lang: args.lang } : {}),
73
153
  git: args.git,
74
154
  ...listDirOptions,
75
155
  });
76
156
  break;
77
157
  }
158
+ case "status": {
159
+ const { applied, pending } = await migrateStatus(listDirOptions);
160
+ for (const entry of applied) {
161
+ log({ text: `${entry.name} applied`, type: "info" });
162
+ }
163
+ for (const file of pending) {
164
+ log({ text: `${file} pending`, type: "warn" });
165
+ }
166
+ log({ text: `${applied.length} applied, ${pending.length} pending`, type: "info" });
167
+ if (args.strict && pending.length > 0) {
168
+ log({ text: `Strict mode: ${pending.length} pending migration(s).`, type: "warn" });
169
+ process.exit(1);
170
+ }
171
+ break;
172
+ }
78
173
  default:
79
174
  usage(1);
80
175
  }
81
176
  } catch (error) {
82
- if (error instanceof ChecksumDriftError) {
177
+ if (
178
+ error instanceof ChecksumDriftError ||
179
+ error instanceof MigrationNotFoundError ||
180
+ error instanceof MigrationLockError
181
+ ) {
83
182
  log({ text: error.message, type: "error" });
84
183
  } else {
85
184
  log({ text: "Migration command failed", type: "error", error });
@@ -12,6 +12,8 @@ export interface MigrationDriver {
12
12
  setChecksum(migration: string, checksum: string): Promise<void>;
13
13
  remove(migration: string): Promise<void>;
14
14
  transaction<T>(run: (tx: SQL) => Promise<T>): Promise<T>;
15
+ tryLock?(timeoutSeconds: number): Promise<boolean>;
16
+ releaseLock?(): Promise<void>;
15
17
  close(): Promise<void>;
16
18
  }
17
19
 
package/src/core/fs.ts CHANGED
@@ -3,11 +3,13 @@ import path from "node:path";
3
3
 
4
4
  export const DEFAULT_MIGRATIONS_DIR = "migrations";
5
5
 
6
- export async function listFiles(dir: string, ext: string): Promise<string[]> {
6
+ export const MIGRATION_EXTENSIONS = ["js", "ts"] as const;
7
+
8
+ export async function listFiles(dir: string, extensions: readonly string[]): Promise<string[]> {
7
9
  const matchedFiles: string[] = [];
8
10
  const entries = await readdir(dir, { withFileTypes: true });
9
11
  for (const entry of entries) {
10
- if (entry.isFile() && entry.name.endsWith(`.${ext}`)) {
12
+ if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(`.${ext}`))) {
11
13
  matchedFiles.push(entry.name);
12
14
  }
13
15
  }
@@ -1,6 +1,8 @@
1
1
  import { type SQL } from "bun";
2
2
  import type { MigrationDriver } from "../core/driver.js";
3
- import { createSqlDriver } from "./shared.js";
3
+ import { createReservedLock, createSqlDriver } from "./shared.js";
4
+
5
+ const LOCK_NAME_PREFIX = "bunsql-native-migrate:";
4
6
 
5
7
  async function checksumColumnExists(db: SQL): Promise<boolean> {
6
8
  const rows = await db`SELECT column_name FROM information_schema.columns
@@ -38,5 +40,20 @@ export function create(databaseUrl: string): MigrationDriver {
38
40
  await db`INSERT IGNORE INTO migrations (migration, checksum)
39
41
  VALUES (${migration}, ${checksum})`;
40
42
  },
43
+ createLock: (db) =>
44
+ createReservedLock(
45
+ db,
46
+ async (lock) => {
47
+ const rows =
48
+ (await lock`SELECT GET_LOCK(CONCAT(${LOCK_NAME_PREFIX}, MD5(DATABASE())), 0) AS locked`) as Array<{
49
+ locked: number | string | null;
50
+ }>;
51
+ const locked = rows[0]?.locked;
52
+ return locked === 1 || locked === "1";
53
+ },
54
+ async (lock) => {
55
+ await lock`SELECT RELEASE_LOCK(CONCAT(${LOCK_NAME_PREFIX}, MD5(DATABASE())))`;
56
+ },
57
+ ),
41
58
  });
42
59
  }
@@ -1,5 +1,14 @@
1
+ import { type SQL } from "bun";
1
2
  import type { MigrationDriver } from "../core/driver.js";
2
- import { createSqlDriver } from "./shared.js";
3
+ import { createReservedLock, createSqlDriver } from "./shared.js";
4
+
5
+ const LOCK_SCOPE = "bunsql-native-migrate:up";
6
+
7
+ async function advisoryKeyComponents(lock: SQL): Promise<[number, number]> {
8
+ const rows = (await lock`SELECT current_database() AS name`) as Array<{ name: string }>;
9
+ const hash = Bun.hash.wyhash(`${LOCK_SCOPE}:${rows[0]?.name ?? ""}`);
10
+ return [Number((hash >> 32n) & 0x7fffffffn), Number(hash & 0x7fffffffn)];
11
+ }
3
12
 
4
13
  export function create(databaseUrl: string): MigrationDriver {
5
14
  return createSqlDriver(databaseUrl, {
@@ -17,5 +26,21 @@ export function create(databaseUrl: string): MigrationDriver {
17
26
  VALUES (${migration}, ${checksum})
18
27
  ON CONFLICT (migration) DO NOTHING`;
19
28
  },
29
+ createLock: (db) =>
30
+ createReservedLock(
31
+ db,
32
+ async (lock) => {
33
+ const [first, second] = await advisoryKeyComponents(lock);
34
+ const rows =
35
+ (await lock`SELECT pg_try_advisory_lock(${first}, ${second}) AS locked`) as Array<{
36
+ locked: boolean;
37
+ }>;
38
+ return rows[0]?.locked === true;
39
+ },
40
+ async (lock) => {
41
+ const [first, second] = await advisoryKeyComponents(lock);
42
+ await lock`SELECT pg_advisory_unlock(${first}, ${second})`;
43
+ },
44
+ ),
20
45
  });
21
46
  }
@@ -1,13 +1,62 @@
1
- import { SQL } from "bun";
1
+ import { SQL, type ReservedSQL } from "bun";
2
2
  import type { ExecutedMigration, MigrationDriver } from "../core/driver.js";
3
3
 
4
+ export interface SqlLock {
5
+ tryLock(timeoutSeconds: number): Promise<boolean>;
6
+ releaseLock(): Promise<void>;
7
+ dispose(): void;
8
+ }
9
+
4
10
  export interface SqlDialect {
5
11
  install(db: SQL): Promise<void>;
6
12
  record(db: SQL, migration: string, checksum: string): Promise<void>;
13
+ createLock?(db: SQL): SqlLock;
14
+ }
15
+
16
+ export function createReservedLock(
17
+ db: SQL,
18
+ acquire: (lock: SQL) => Promise<boolean>,
19
+ release: (lock: SQL) => Promise<void>,
20
+ ): SqlLock {
21
+ let lockConnection: ReservedSQL | null = null;
22
+
23
+ return {
24
+ async tryLock() {
25
+ const connection = await db.reserve();
26
+ lockConnection = connection;
27
+ try {
28
+ const acquired = await acquire(connection);
29
+ if (!acquired) {
30
+ connection.release();
31
+ lockConnection = null;
32
+ }
33
+ return acquired;
34
+ } catch (error) {
35
+ connection.release();
36
+ lockConnection = null;
37
+ throw error;
38
+ }
39
+ },
40
+ async releaseLock() {
41
+ const connection = lockConnection;
42
+ if (!connection) return;
43
+ lockConnection = null;
44
+ try {
45
+ await release(connection);
46
+ } finally {
47
+ connection.release();
48
+ }
49
+ },
50
+ dispose() {
51
+ lockConnection?.release();
52
+ lockConnection = null;
53
+ },
54
+ };
7
55
  }
8
56
 
9
57
  export function createSqlDriver(databaseUrl: string, dialect: SqlDialect): MigrationDriver {
10
58
  const db = new SQL(databaseUrl);
59
+ const lock = dialect.createLock?.(db);
11
60
 
12
61
  return {
13
62
  install: () => dialect.install(db),
@@ -28,7 +77,14 @@ export function createSqlDriver(databaseUrl: string, dialect: SqlDialect): Migra
28
77
  await db`DELETE FROM migrations WHERE migration = ${migration}`;
29
78
  },
30
79
  transaction: (run) => db.begin(run),
80
+ ...(lock
81
+ ? {
82
+ tryLock: (timeoutSeconds: number) => lock.tryLock(timeoutSeconds),
83
+ releaseLock: () => lock.releaseLock(),
84
+ }
85
+ : {}),
31
86
  async close() {
87
+ lock?.dispose();
32
88
  db.close({ timeout: 0 });
33
89
  },
34
90
  };
@@ -22,5 +22,15 @@ export function create(databaseUrl: string): MigrationDriver {
22
22
  await db`INSERT OR IGNORE INTO migrations (migration, checksum)
23
23
  VALUES (${migration}, ${checksum})`;
24
24
  },
25
+ createLock: (db) => ({
26
+ async tryLock(timeoutSeconds: number) {
27
+ await db.unsafe(`PRAGMA busy_timeout = ${timeoutSeconds * 1000}`);
28
+ return true;
29
+ },
30
+ async releaseLock() {
31
+ await db.unsafe("PRAGMA busy_timeout = 0");
32
+ },
33
+ dispose() {},
34
+ }),
25
35
  });
26
36
  }
package/src/index.ts CHANGED
@@ -1,29 +1,41 @@
1
1
  import { createDriver, type ExecutedMigration, type MigrationDriver } from "./core/driver.js";
2
2
  import { migrateUp } from "./api/up.js";
3
3
  import { migrateDown } from "./api/down.js";
4
+ import { migrateStatus } from "./api/status.js";
4
5
  import { installMigrations } from "./api/install.js";
5
6
  import { createMigration } from "./api/create.js";
6
7
  import {
8
+ type MigrateDownOptions,
7
9
  type MigrateDownResult,
8
10
  type MigrateOptions,
11
+ type MigrateStatusResult,
12
+ type MigrateUpOptions,
9
13
  type MigrateUpResult,
10
14
  ChecksumDriftError,
11
15
  GitStageError,
16
+ MigrationLockError,
17
+ MigrationNotFoundError,
12
18
  } from "./api/options.js";
13
19
 
14
20
  export {
15
21
  createDriver,
16
22
  migrateUp,
17
23
  migrateDown,
24
+ migrateStatus,
18
25
  installMigrations,
19
26
  createMigration,
20
27
  ChecksumDriftError,
21
28
  GitStageError,
29
+ MigrationLockError,
30
+ MigrationNotFoundError,
22
31
  };
23
32
  export type {
24
33
  ExecutedMigration,
25
34
  MigrationDriver,
26
35
  MigrateOptions,
36
+ MigrateUpOptions,
27
37
  MigrateUpResult,
38
+ MigrateDownOptions,
28
39
  MigrateDownResult,
40
+ MigrateStatusResult,
29
41
  };