bunsql-native-migrate 0.3.0 → 0.3.1
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 +2 -2
- package/package.json +1 -1
- package/src/api/down.ts +6 -1
- package/src/api/mark.ts +2 -1
- package/src/api/status.ts +2 -1
- package/src/api/tracking-table.ts +15 -0
- package/src/api/up.ts +2 -9
- package/src/core/driver.ts +1 -0
- package/src/drivers/mariadb.ts +15 -0
- package/src/drivers/postgres.ts +12 -0
- package/src/drivers/shared.ts +4 -0
- package/src/drivers/sqlite.ts +10 -0
package/README.md
CHANGED
|
@@ -261,7 +261,7 @@ Known MySQL/MariaDB limitation of `Bun.SQL` (not used by this package, good to k
|
|
|
261
261
|
|
|
262
262
|
Bun's client also recognizes `mysql2://` and `file://` URLs — `createDriver` deliberately rejects them. Stick to the protocols listed above.
|
|
263
263
|
|
|
264
|
-
`createDriver(url)` returns a `MigrationDriver` (`install`/`listExecuted`/`record`/`setChecksum`/`remove`/`close`) if you need custom tracking logic; `listExecuted` yields `ExecutedMigration` records (`name`, `checksum`). It also accepts the same `{ tableName, schema }` options as the API functions. 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 —
|
|
264
|
+
`createDriver(url)` returns a `MigrationDriver` (`install`/`listExecuted`/`record`/`setChecksum`/`remove`/`close`) if you need custom tracking logic; `listExecuted` yields `ExecutedMigration` records (`name`, `checksum`). It also accepts the same `{ tableName, schema }` options as the API functions. 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 — the optional `trackingTableExists()` used by `up`'s [dry run](#dry-run) to plan against a database that has no tracking table yet, and the optional `trackingTableCurrent()` probe that lets `up`/`down`/`status`/`mark` skip re-running `install()` when the tracking table already has its checksum column and unique index — without the probe, those commands install the table up front as before.
|
|
265
265
|
|
|
266
266
|
### Tracking table
|
|
267
267
|
|
|
@@ -309,7 +309,7 @@ Would apply 2 migration(s).
|
|
|
309
309
|
|
|
310
310
|
- Nothing is executed and nothing is recorded — not even the tracking table is created, so it is safe against any database, production included. A database without the table simply plans everything as pending.
|
|
311
311
|
- The plan honors every option the real run would: `to`, `steps`, `--table`/`--schema`. The checksum drift check runs too — a dry run reports `ChecksumDriftError` exactly where the real run would fail (the legacy NULL-checksum backfill is the one write it skips).
|
|
312
|
-
- `down --dry-run` plans the revert list in reverse apply order; on an empty history it prints `No migrations to rollback.` like the real command.
|
|
312
|
+
- `down --dry-run` plans the revert list in reverse apply order; on an empty history — a database that has never seen an `up`, included — it prints `No migrations to rollback.` like the real command. A real `down` creates the tracking table when it is missing, so it degrades to the same message instead of a driver error.
|
|
313
313
|
- In the API result the plan lands in `planned: string[]` while `applied`/`reverted` stay empty — existing consumers keep working untouched.
|
|
314
314
|
|
|
315
315
|
### Concurrent runs
|
package/package.json
CHANGED
package/src/api/down.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { MigrateDownOptions, MigrateDownResult } from "./options.js";
|
|
|
6
6
|
import { runWithDriver } from "./run-with-driver.js";
|
|
7
7
|
import { runMigrationStep } from "./run-step.js";
|
|
8
8
|
import { isSqlMigration, loadMigration } from "./load-migration.js";
|
|
9
|
+
import { ensureTrackingTable, listExecutedForPlan } from "./tracking-table.js";
|
|
9
10
|
|
|
10
11
|
function resolveStepCount(steps: number | "all" | undefined, appliedCount: number): number {
|
|
11
12
|
if (steps === undefined) return 1;
|
|
@@ -37,7 +38,11 @@ export async function migrateDown(options: MigrateDownOptions = {}): Promise<Mig
|
|
|
37
38
|
const dryRun = options.dryRun ?? false;
|
|
38
39
|
|
|
39
40
|
return runWithDriver(options, async (driver) => {
|
|
40
|
-
|
|
41
|
+
if (!dryRun) {
|
|
42
|
+
await ensureTrackingTable(driver);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const executed = dryRun ? await listExecutedForPlan(driver) : await driver.listExecuted();
|
|
41
46
|
if (executed.length === 0) {
|
|
42
47
|
log({ text: "No migrations to rollback.", type: "warn" });
|
|
43
48
|
return dryRun ? { reverted: [], planned: [] } : { reverted: [] };
|
package/src/api/mark.ts
CHANGED
|
@@ -3,13 +3,14 @@ import { checksumFile, listFiles, MIGRATION_EXTENSIONS, resolveListDir } from ".
|
|
|
3
3
|
import { log } from "../core/console.js";
|
|
4
4
|
import { type MarkOptions, type MarkResult, MigrationNotFoundError } from "./options.js";
|
|
5
5
|
import { runWithDriver } from "./run-with-driver.js";
|
|
6
|
+
import { ensureTrackingTable } from "./tracking-table.js";
|
|
6
7
|
|
|
7
8
|
export async function markMigrationsApplied(options: MarkOptions = {}): Promise<MarkResult> {
|
|
8
9
|
const listDir = resolveListDir(options.listDir);
|
|
9
10
|
const target = options.to;
|
|
10
11
|
|
|
11
12
|
return runWithDriver(options, async (driver) => {
|
|
12
|
-
await driver
|
|
13
|
+
await ensureTrackingTable(driver);
|
|
13
14
|
|
|
14
15
|
const allFiles = await listFiles(listDir, MIGRATION_EXTENSIONS);
|
|
15
16
|
if (target !== undefined && !allFiles.includes(target)) {
|
package/src/api/status.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { listFiles, MIGRATION_EXTENSIONS, resolveListDir } from "../core/fs.js";
|
|
2
2
|
import type { MigrateOptions, MigrateStatusResult } from "./options.js";
|
|
3
3
|
import { runWithDriver } from "./run-with-driver.js";
|
|
4
|
+
import { ensureTrackingTable } from "./tracking-table.js";
|
|
4
5
|
|
|
5
6
|
export async function migrateStatus(options: MigrateOptions = {}): Promise<MigrateStatusResult> {
|
|
6
7
|
const listDir = resolveListDir(options.listDir);
|
|
7
8
|
|
|
8
9
|
return runWithDriver(options, async (driver) => {
|
|
9
|
-
await driver
|
|
10
|
+
await ensureTrackingTable(driver);
|
|
10
11
|
|
|
11
12
|
const files = await listFiles(listDir, MIGRATION_EXTENSIONS);
|
|
12
13
|
const executed = await driver.listExecuted();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ExecutedMigration, MigrationDriver } from "../core/driver.js";
|
|
2
|
+
|
|
3
|
+
export async function ensureTrackingTable(driver: MigrationDriver): Promise<void> {
|
|
4
|
+
if (driver.trackingTableCurrent !== undefined && (await driver.trackingTableCurrent())) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
await driver.install();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function listExecutedForPlan(driver: MigrationDriver): Promise<ExecutedMigration[]> {
|
|
11
|
+
if ((await driver.trackingTableExists?.()) === false) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
return driver.listExecuted();
|
|
15
|
+
}
|
package/src/api/up.ts
CHANGED
|
@@ -2,7 +2,6 @@ import path from "node:path";
|
|
|
2
2
|
import { checksumFile, listFiles, MIGRATION_EXTENSIONS, resolveListDir } from "../core/fs.js";
|
|
3
3
|
import { log } from "../core/console.js";
|
|
4
4
|
import { formatDuration } from "../core/duration.js";
|
|
5
|
-
import type { ExecutedMigration, MigrationDriver } from "../core/driver.js";
|
|
6
5
|
import {
|
|
7
6
|
type MigrateUpOptions,
|
|
8
7
|
type MigrateUpResult,
|
|
@@ -13,13 +12,7 @@ import { runWithDriver } from "./run-with-driver.js";
|
|
|
13
12
|
import { runMigrationStep } from "./run-step.js";
|
|
14
13
|
import { loadMigration } from "./load-migration.js";
|
|
15
14
|
import { resolveLockTimeout, withMigrationLock } from "./lock.js";
|
|
16
|
-
|
|
17
|
-
async function listExecutedForPlan(driver: MigrationDriver): Promise<ExecutedMigration[]> {
|
|
18
|
-
if ((await driver.trackingTableExists?.()) === false) {
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
return driver.listExecuted();
|
|
22
|
-
}
|
|
15
|
+
import { ensureTrackingTable, listExecutedForPlan } from "./tracking-table.js";
|
|
23
16
|
|
|
24
17
|
export async function migrateUp(options: MigrateUpOptions = {}): Promise<MigrateUpResult> {
|
|
25
18
|
const listDir = resolveListDir(options.listDir);
|
|
@@ -29,7 +22,7 @@ export async function migrateUp(options: MigrateUpOptions = {}): Promise<Migrate
|
|
|
29
22
|
|
|
30
23
|
return runWithDriver(options, async (driver) => {
|
|
31
24
|
if (!dryRun) {
|
|
32
|
-
await driver
|
|
25
|
+
await ensureTrackingTable(driver);
|
|
33
26
|
}
|
|
34
27
|
|
|
35
28
|
const run = async (): Promise<MigrateUpResult> => {
|
package/src/core/driver.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface MigrationDriver {
|
|
|
13
13
|
remove(migration: string): Promise<void>;
|
|
14
14
|
transaction<T>(run: (tx: SQL) => Promise<T>): Promise<T>;
|
|
15
15
|
trackingTableExists?(): Promise<boolean>;
|
|
16
|
+
trackingTableCurrent?(): Promise<boolean>;
|
|
16
17
|
tryLock?(timeoutSeconds: number): Promise<boolean>;
|
|
17
18
|
releaseLock?(): Promise<void>;
|
|
18
19
|
close(): Promise<void>;
|
package/src/drivers/mariadb.ts
CHANGED
|
@@ -63,6 +63,21 @@ export function create(databaseUrl: string, options: DriverTableOptions = {}): M
|
|
|
63
63
|
AND table_name = ${name}`;
|
|
64
64
|
return rows.length > 0;
|
|
65
65
|
},
|
|
66
|
+
async trackingTableCurrent(db) {
|
|
67
|
+
const rows = (await db`SELECT EXISTS (
|
|
68
|
+
SELECT 1 FROM information_schema.tables
|
|
69
|
+
WHERE table_schema = DATABASE() AND table_name = ${name}
|
|
70
|
+
) AND EXISTS (
|
|
71
|
+
SELECT 1 FROM information_schema.columns
|
|
72
|
+
WHERE table_schema = DATABASE() AND table_name = ${name} AND column_name = 'checksum'
|
|
73
|
+
) AND EXISTS (
|
|
74
|
+
SELECT 1 FROM information_schema.statistics
|
|
75
|
+
WHERE table_schema = DATABASE() AND table_name = ${name}
|
|
76
|
+
AND index_name = ${`${name}${UNIQUE_INDEX_SUFFIX}`}
|
|
77
|
+
) AS current`) as Array<{ current: number | boolean }>;
|
|
78
|
+
const current = rows[0]?.current;
|
|
79
|
+
return current === 1 || current === true;
|
|
80
|
+
},
|
|
66
81
|
async record(db, migration, checksum) {
|
|
67
82
|
await db`INSERT IGNORE INTO ${db.unsafe(table)} (migration, checksum)
|
|
68
83
|
VALUES (${migration}, ${checksum})`;
|
package/src/drivers/postgres.ts
CHANGED
|
@@ -59,6 +59,18 @@ export function create(databaseUrl: string, options: DriverTableOptions = {}): M
|
|
|
59
59
|
WHERE table_schema = current_schema() AND table_name = ${name}`;
|
|
60
60
|
return rows.length > 0;
|
|
61
61
|
},
|
|
62
|
+
async trackingTableCurrent(db) {
|
|
63
|
+
const rows = (await db`SELECT EXISTS (
|
|
64
|
+
SELECT 1 FROM information_schema.columns
|
|
65
|
+
WHERE table_schema = COALESCE(${schemaName ?? null}, current_schema())
|
|
66
|
+
AND table_name = ${name} AND column_name = 'checksum'
|
|
67
|
+
) AND EXISTS (
|
|
68
|
+
SELECT 1 FROM pg_indexes
|
|
69
|
+
WHERE schemaname = COALESCE(${schemaName ?? null}, current_schema())
|
|
70
|
+
AND indexname = ${`${name}${UNIQUE_INDEX_SUFFIX}`}
|
|
71
|
+
) AS "current"`) as Array<{ current: boolean }>;
|
|
72
|
+
return rows[0]?.current === true;
|
|
73
|
+
},
|
|
62
74
|
async record(db, migration, checksum) {
|
|
63
75
|
await db`INSERT INTO ${db.unsafe(table)} (migration, checksum)
|
|
64
76
|
VALUES (${migration}, ${checksum})
|
package/src/drivers/shared.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface SqlDialect {
|
|
|
11
11
|
install(db: SQL): Promise<void>;
|
|
12
12
|
record(db: SQL, migration: string, checksum: string): Promise<void>;
|
|
13
13
|
trackingTableExists?(db: SQL): Promise<boolean>;
|
|
14
|
+
trackingTableCurrent?(db: SQL): Promise<boolean>;
|
|
14
15
|
createLock?(db: SQL): SqlLock;
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -85,6 +86,9 @@ export function createSqlDriver(
|
|
|
85
86
|
...(dialect.trackingTableExists
|
|
86
87
|
? { trackingTableExists: () => dialect.trackingTableExists!(db) }
|
|
87
88
|
: {}),
|
|
89
|
+
...(dialect.trackingTableCurrent
|
|
90
|
+
? { trackingTableCurrent: () => dialect.trackingTableCurrent!(db) }
|
|
91
|
+
: {}),
|
|
88
92
|
...(lock
|
|
89
93
|
? {
|
|
90
94
|
tryLock: (timeoutSeconds: number) => lock.tryLock(timeoutSeconds),
|
package/src/drivers/sqlite.ts
CHANGED
|
@@ -43,6 +43,16 @@ export function create(databaseUrl: string, options: DriverTableOptions = {}): M
|
|
|
43
43
|
const rows = await db`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${name}`;
|
|
44
44
|
return rows.length > 0;
|
|
45
45
|
},
|
|
46
|
+
async trackingTableCurrent(db) {
|
|
47
|
+
const columns = (await db.unsafe(`PRAGMA table_info(${table})`)) as Array<{
|
|
48
|
+
name: string;
|
|
49
|
+
}>;
|
|
50
|
+
if (!columns.some((column) => column.name === "checksum")) return false;
|
|
51
|
+
const indexName = `${name}${UNIQUE_INDEX_SUFFIX}`;
|
|
52
|
+
const indexes = (await db`SELECT 1 FROM sqlite_master WHERE type = 'index'
|
|
53
|
+
AND name = ${indexName}`) as Array<unknown>;
|
|
54
|
+
return indexes.length > 0;
|
|
55
|
+
},
|
|
46
56
|
async record(db, migration, checksum) {
|
|
47
57
|
await db`INSERT OR IGNORE INTO ${db.unsafe(table)} (migration, checksum)
|
|
48
58
|
VALUES (${migration}, ${checksum})`;
|