bunsql-native-migrate 0.1.2 → 0.3.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.
@@ -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
+ }
@@ -0,0 +1,43 @@
1
+ import path from "node:path";
2
+ import { checksumFile, listFiles, MIGRATION_EXTENSIONS, resolveListDir } from "../core/fs.js";
3
+ import { log } from "../core/console.js";
4
+ import { type MarkOptions, type MarkResult, MigrationNotFoundError } from "./options.js";
5
+ import { runWithDriver } from "./run-with-driver.js";
6
+
7
+ export async function markMigrationsApplied(options: MarkOptions = {}): Promise<MarkResult> {
8
+ const listDir = resolveListDir(options.listDir);
9
+ const target = options.to;
10
+
11
+ return runWithDriver(options, async (driver) => {
12
+ await driver.install();
13
+
14
+ const allFiles = await listFiles(listDir, MIGRATION_EXTENSIONS);
15
+ if (target !== undefined && !allFiles.includes(target)) {
16
+ throw new MigrationNotFoundError(target);
17
+ }
18
+
19
+ const executed = await driver.listExecuted();
20
+ const executedNames = new Set(executed.map((entry) => entry.name));
21
+ let pending = allFiles.filter((file) => !executedNames.has(file));
22
+ if (target !== undefined) {
23
+ if (executedNames.has(target)) {
24
+ log({ text: `${target} is already applied.`, type: "info" });
25
+ return { marked: [] };
26
+ }
27
+ pending = pending.slice(0, pending.indexOf(target) + 1);
28
+ }
29
+
30
+ const marked: string[] = [];
31
+ for (const file of pending) {
32
+ const checksum = await checksumFile(path.join(listDir, file));
33
+ await driver.record(file, checksum);
34
+ marked.push(file);
35
+ log({ text: `${file} marked as applied`, type: "success" });
36
+ }
37
+ if (marked.length === 0) {
38
+ log({ text: "No pending migrations to mark.", type: "warn" });
39
+ }
40
+
41
+ return { marked };
42
+ });
43
+ }
@@ -1,14 +1,44 @@
1
+ import type { ExecutedMigration } from "../core/driver.js";
2
+
1
3
  export interface MigrateOptions {
2
4
  databaseUrl?: string;
3
5
  listDir?: string;
6
+ tableName?: string;
7
+ schema?: string;
8
+ }
9
+
10
+ export interface MigrateUpOptions extends MigrateOptions {
11
+ to?: string;
12
+ lockTimeout?: number;
13
+ dryRun?: boolean;
14
+ }
15
+
16
+ export interface MigrateDownOptions extends MigrateOptions {
17
+ steps?: number | "all";
18
+ dryRun?: boolean;
19
+ }
20
+
21
+ export interface MarkOptions extends MigrateOptions {
22
+ to?: string;
4
23
  }
5
24
 
6
25
  export interface MigrateUpResult {
7
26
  applied: string[];
27
+ planned?: string[];
8
28
  }
9
29
 
10
30
  export interface MigrateDownResult {
11
- reverted: string | null;
31
+ reverted: string[];
32
+ planned?: string[];
33
+ }
34
+
35
+ export interface MarkResult {
36
+ marked: string[];
37
+ }
38
+
39
+ export interface MigrateStatusResult {
40
+ applied: ExecutedMigration[];
41
+ pending: string[];
12
42
  }
13
43
 
14
44
  export class ChecksumDriftError extends Error {
@@ -23,6 +53,28 @@ export class ChecksumDriftError extends Error {
23
53
  }
24
54
  }
25
55
 
56
+ export class MigrationNotFoundError extends Error {
57
+ readonly file: string;
58
+
59
+ constructor(file: string) {
60
+ super(`${file} is not in the migrations directory — nothing was applied`);
61
+ this.name = "MigrationNotFoundError";
62
+ this.file = file;
63
+ }
64
+ }
65
+
66
+ export class MigrationLockError extends Error {
67
+ readonly timeoutSeconds: number;
68
+
69
+ constructor(timeoutSeconds: number) {
70
+ super(
71
+ `could not acquire the migration lock within ${timeoutSeconds}s — another migrate up is probably still running`,
72
+ );
73
+ this.name = "MigrationLockError";
74
+ this.timeoutSeconds = timeoutSeconds;
75
+ }
76
+ }
77
+
26
78
  export class GitStageError extends Error {
27
79
  readonly file: string;
28
80
  readonly exitCode: number;
@@ -4,10 +4,12 @@ import type { MigrationDriver } from "../core/driver.js";
4
4
  export async function runMigrationStep(
5
5
  driver: MigrationDriver,
6
6
  step: (tx?: SQL) => Promise<void>,
7
- ): Promise<void> {
7
+ ): Promise<number> {
8
+ const startedAt = performance.now();
8
9
  if (step.length > 0) {
9
10
  await driver.transaction((tx) => step(tx));
10
- return;
11
+ return performance.now() - startedAt;
11
12
  }
12
13
  await step();
14
+ return performance.now() - startedAt;
13
15
  }
@@ -7,7 +7,10 @@ export async function runWithDriver<T>(
7
7
  run: (driver: MigrationDriver) => Promise<T>,
8
8
  ): Promise<T> {
9
9
  const url = getDatabaseUrl(options.databaseUrl);
10
- const driver = await createDriver(url);
10
+ const driver = await createDriver(url, {
11
+ ...(options.tableName !== undefined ? { tableName: options.tableName } : {}),
12
+ ...(options.schema !== undefined ? { schema: options.schema } : {}),
13
+ });
11
14
  try {
12
15
  return await run(driver);
13
16
  } finally {
@@ -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,123 @@
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 { formatDuration } from "../core/duration.js";
5
+ import type { ExecutedMigration, MigrationDriver } from "../core/driver.js";
6
+ import {
7
+ type MigrateUpOptions,
8
+ type MigrateUpResult,
9
+ ChecksumDriftError,
10
+ MigrationNotFoundError,
11
+ } from "./options.js";
5
12
  import { runWithDriver } from "./run-with-driver.js";
6
13
  import { runMigrationStep } from "./run-step.js";
14
+ import { loadMigration } from "./load-migration.js";
15
+ import { resolveLockTimeout, withMigrationLock } from "./lock.js";
7
16
 
8
- export async function migrateUp(options: MigrateOptions = {}): Promise<MigrateUpResult> {
17
+ async function listExecutedForPlan(driver: MigrationDriver): Promise<ExecutedMigration[]> {
18
+ if ((await driver.trackingTableExists?.()) === false) {
19
+ return [];
20
+ }
21
+ return driver.listExecuted();
22
+ }
23
+
24
+ export async function migrateUp(options: MigrateUpOptions = {}): Promise<MigrateUpResult> {
9
25
  const listDir = resolveListDir(options.listDir);
26
+ const target = options.to;
27
+ const dryRun = options.dryRun ?? false;
28
+ const lockTimeout = resolveLockTimeout(options.lockTimeout);
10
29
 
11
30
  return runWithDriver(options, async (driver) => {
12
- await driver.install();
13
-
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
- );
20
-
21
- const executed = await driver.listExecuted();
22
- const executedByName = new Map(executed.map((entry) => [entry.name, entry]));
23
-
24
- for (const [file, checksum] of checksums) {
25
- const record = executedByName.get(file);
26
- if (!record) continue;
27
-
28
- if (record.checksum === null) {
29
- await driver.setChecksum(file, checksum);
30
- log({ text: `${file} checksum saved (legacy record)`, type: "info" });
31
- continue;
32
- }
31
+ if (!dryRun) {
32
+ await driver.install();
33
+ }
33
34
 
34
- if (record.checksum !== checksum) {
35
- throw new ChecksumDriftError(file);
35
+ const run = async (): Promise<MigrateUpResult> => {
36
+ const allFiles = await listFiles(listDir, MIGRATION_EXTENSIONS);
37
+ if (target !== undefined && !allFiles.includes(target)) {
38
+ throw new MigrationNotFoundError(target);
36
39
  }
37
- }
38
40
 
39
- const pending = allFiles.filter((file) => !executedByName.has(file));
40
- const applied: string[] = [];
41
+ const checksums = new Map(
42
+ await Promise.all(
43
+ allFiles.map(
44
+ async (file) => [file, await checksumFile(path.join(listDir, file))] as const,
45
+ ),
46
+ ),
47
+ );
41
48
 
42
- if (pending.length === 0) {
43
- log({ text: "No pending migrations.", type: "warn" });
44
- return { applied };
45
- }
49
+ const executed = dryRun ? await listExecutedForPlan(driver) : await driver.listExecuted();
50
+ const executedByName = new Map(executed.map((entry) => [entry.name, entry]));
46
51
 
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" });
52
+ for (const [file, checksum] of checksums) {
53
+ const record = executedByName.get(file);
54
+ if (!record) continue;
55
+
56
+ if (record.checksum === null) {
57
+ if (!dryRun) {
58
+ await driver.setChecksum(file, checksum);
59
+ log({ text: `${file} checksum saved (legacy record)`, type: "info" });
60
+ }
54
61
  continue;
55
62
  }
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
+
64
+ if (record.checksum !== checksum) {
65
+ throw new ChecksumDriftError(file);
66
+ }
67
+ }
68
+
69
+ let pending = allFiles.filter((file) => !executedByName.has(file));
70
+ if (target !== undefined) {
71
+ if (executedByName.has(target)) {
72
+ log({ text: `${target} is already applied.`, type: "info" });
73
+ return dryRun ? { applied: [], planned: [] } : { applied: [] };
74
+ }
75
+ pending = pending.slice(0, pending.indexOf(target) + 1);
63
76
  }
64
- }
65
77
 
66
- return { applied };
78
+ if (dryRun) {
79
+ log({ text: "Dry run — no changes will be made.", type: "info" });
80
+ if (pending.length === 0) {
81
+ log({ text: "No pending migrations.", type: "warn" });
82
+ }
83
+ for (const file of pending) {
84
+ log({ text: `${file} would be applied`, type: "info" });
85
+ }
86
+ return { applied: [], planned: pending };
87
+ }
88
+
89
+ const applied: string[] = [];
90
+
91
+ if (pending.length === 0) {
92
+ log({ text: "No pending migrations.", type: "warn" });
93
+ return { applied };
94
+ }
95
+
96
+ for (const file of pending) {
97
+ const checksum = checksums.get(file);
98
+ if (!checksum) continue;
99
+ try {
100
+ const { up } = await loadMigration(listDir, file);
101
+ if (up === null) {
102
+ log({ text: `${file} has no up() export, skipping`, type: "warn" });
103
+ continue;
104
+ }
105
+ const durationMs = await runMigrationStep(driver, up);
106
+ await driver.record(file, checksum);
107
+ applied.push(file);
108
+ log({ text: `${file} migrated up (${formatDuration(durationMs)})`, type: "success" });
109
+ } catch (error) {
110
+ log({ text: `${file} migration failed`, type: "error", error });
111
+ throw error;
112
+ }
113
+ }
114
+
115
+ return { applied };
116
+ };
117
+
118
+ if (dryRun) {
119
+ return run();
120
+ }
121
+ return withMigrationLock(driver, lockTimeout, run);
67
122
  });
68
123
  }
package/src/cli/main.ts CHANGED
@@ -1,9 +1,13 @@
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 { initMigrations } from "../api/init.js";
8
+ import { markMigrationsApplied } from "../api/mark.js";
9
+ import { ChecksumDriftError, MigrationLockError, MigrationNotFoundError } from "../api/options.js";
10
+ import { InvalidIdentifierError } from "../core/identifiers.js";
7
11
  import { log } from "../core/console.js";
8
12
 
9
13
  interface CliArgs {
@@ -11,6 +15,14 @@ interface CliArgs {
11
15
  positional: string[];
12
16
  dir?: string | undefined;
13
17
  git: boolean;
18
+ lang?: MigrationLang | undefined;
19
+ to?: string | undefined;
20
+ lockTimeout?: number | undefined;
21
+ table?: string | undefined;
22
+ schema?: string | undefined;
23
+ dryRun: boolean;
24
+ all: boolean;
25
+ strict: boolean;
14
26
  help: boolean;
15
27
  }
16
28
 
@@ -18,6 +30,14 @@ function parseArgs(argv: string[]): CliArgs {
18
30
  const positional: string[] = [];
19
31
  let dir: string | undefined;
20
32
  let git = false;
33
+ let lang: MigrationLang | undefined;
34
+ let to: string | undefined;
35
+ let lockTimeout: number | undefined;
36
+ let table: string | undefined;
37
+ let schema: string | undefined;
38
+ let dryRun = false;
39
+ let all = false;
40
+ let strict = false;
21
41
  let help = false;
22
42
  for (let i = 0; i < argv.length; i++) {
23
43
  const arg = argv[i]!;
@@ -25,18 +45,77 @@ function parseArgs(argv: string[]): CliArgs {
25
45
  dir = argv[++i];
26
46
  } else if (arg === "--git") {
27
47
  git = true;
48
+ } else if (arg === "--lang") {
49
+ const value = argv[++i];
50
+ if (value !== "js" && value !== "ts") {
51
+ log({
52
+ text: `Unknown --lang value: ${value ?? "(missing)"} (expected js or ts)`,
53
+ type: "error",
54
+ });
55
+ usage(1);
56
+ }
57
+ lang = value;
58
+ } else if (arg === "--to") {
59
+ to = argv[++i];
60
+ if (to === undefined) {
61
+ log({ text: "--to requires a migration file name", type: "error" });
62
+ usage(1);
63
+ }
64
+ } else if (arg === "--lock-timeout") {
65
+ const value = argv[++i];
66
+ const parsed = Number(value);
67
+ if (value === undefined || !Number.isInteger(parsed) || parsed < 0) {
68
+ log({
69
+ text: `Invalid --lock-timeout: ${value ?? "(missing)"} (expected a non-negative integer of seconds)`,
70
+ type: "error",
71
+ });
72
+ usage(1);
73
+ }
74
+ lockTimeout = parsed;
75
+ } else if (arg === "--table") {
76
+ table = argv[++i];
77
+ if (table === undefined) {
78
+ log({ text: "--table requires a tracking table name", type: "error" });
79
+ usage(1);
80
+ }
81
+ } else if (arg === "--schema") {
82
+ schema = argv[++i];
83
+ if (schema === undefined) {
84
+ log({ text: "--schema requires a postgres schema name", type: "error" });
85
+ usage(1);
86
+ }
87
+ } else if (arg === "--all") {
88
+ all = true;
89
+ } else if (arg === "--dry-run") {
90
+ dryRun = true;
91
+ } else if (arg === "--strict") {
92
+ strict = true;
28
93
  } else if (arg === "--help" || arg === "-h") {
29
94
  help = true;
30
95
  } else {
31
96
  positional.push(arg);
32
97
  }
33
98
  }
34
- return { command: positional.shift(), positional, dir, git, help };
99
+ return {
100
+ command: positional.shift(),
101
+ positional,
102
+ dir,
103
+ git,
104
+ lang,
105
+ to,
106
+ lockTimeout,
107
+ table,
108
+ schema,
109
+ dryRun,
110
+ all,
111
+ strict,
112
+ help,
113
+ };
35
114
  }
36
115
 
37
116
  function usage(exitCode: number): never {
38
117
  log({
39
- text: "Usage: bunsql-native-migrate <up|down|install|create [name]> [--dir <migrations-dir>] [--git] [--help]",
118
+ text: "Usage: bunsql-native-migrate <init|up|down [n]|install|create [name]|mark [name]|status> [--dir <migrations-dir>] [--to <name>] [--lock-timeout <seconds>] [--table <name>] [--schema <name>] [--dry-run] [--all] [--lang <js|ts>] [--git] [--strict] [--help]",
40
119
  type: "info",
41
120
  });
42
121
  process.exit(exitCode);
@@ -44,6 +123,10 @@ function usage(exitCode: number): never {
44
123
 
45
124
  const args = parseArgs(process.argv.slice(2));
46
125
  const listDirOptions = args.dir ? { listDir: args.dir } : {};
126
+ const tableOptions = {
127
+ ...(args.table !== undefined ? { tableName: args.table } : {}),
128
+ ...(args.schema !== undefined ? { schema: args.schema } : {}),
129
+ };
47
130
 
48
131
  if (args.help) {
49
132
  usage(0);
@@ -52,34 +135,121 @@ if (args.help) {
52
135
  try {
53
136
  switch (args.command) {
54
137
  case "up": {
55
- const { applied } = await migrateUp(listDirOptions);
138
+ const { applied, planned } = await migrateUp({
139
+ ...listDirOptions,
140
+ ...tableOptions,
141
+ ...(args.to ? { to: args.to } : {}),
142
+ ...(args.lockTimeout !== undefined ? { lockTimeout: args.lockTimeout } : {}),
143
+ ...(args.dryRun ? { dryRun: true } : {}),
144
+ });
145
+ if (planned !== undefined && planned.length > 0) {
146
+ log({ text: `Would apply ${planned.length} migration(s).`, type: "info" });
147
+ }
56
148
  if (applied.length > 0) {
57
149
  log({ text: `Applied ${applied.length} migration(s).`, type: "success" });
58
150
  }
59
151
  break;
60
152
  }
61
153
  case "down": {
62
- await migrateDown(listDirOptions);
154
+ const [stepsArg] = args.positional;
155
+ if (args.all && stepsArg !== undefined) {
156
+ log({ text: "Use either --all or a number of steps, not both.", type: "error" });
157
+ usage(1);
158
+ }
159
+ let steps: number | "all" = 1;
160
+ if (args.all) {
161
+ steps = "all";
162
+ } else if (stepsArg !== undefined) {
163
+ const parsed = Number(stepsArg);
164
+ if (!Number.isInteger(parsed) || parsed < 1) {
165
+ log({
166
+ text: `Invalid step count: ${stepsArg} (expected a positive integer)`,
167
+ type: "error",
168
+ });
169
+ usage(1);
170
+ }
171
+ steps = parsed;
172
+ }
173
+ const { reverted, planned } = await migrateDown({
174
+ ...listDirOptions,
175
+ ...tableOptions,
176
+ steps,
177
+ ...(args.dryRun ? { dryRun: true } : {}),
178
+ });
179
+ if (planned !== undefined && planned.length > 0) {
180
+ log({ text: `Would revert ${planned.length} migration(s).`, type: "info" });
181
+ }
182
+ if (reverted.length > 0) {
183
+ log({ text: `Reverted ${reverted.length} migration(s).`, type: "success" });
184
+ }
185
+ break;
186
+ }
187
+ case "init": {
188
+ await initMigrations({
189
+ ...listDirOptions,
190
+ ...(args.lang !== undefined ? { lang: args.lang } : {}),
191
+ });
63
192
  break;
64
193
  }
65
194
  case "install": {
66
- await installMigrations(listDirOptions);
195
+ await installMigrations({ ...listDirOptions, ...tableOptions });
67
196
  break;
68
197
  }
69
198
  case "create": {
70
199
  const [name] = args.positional;
71
200
  await createMigrationCommand({
72
201
  ...(name ? { name } : {}),
202
+ ...(args.lang ? { lang: args.lang } : {}),
73
203
  git: args.git,
74
204
  ...listDirOptions,
75
205
  });
76
206
  break;
77
207
  }
208
+ case "mark": {
209
+ const [name] = args.positional;
210
+ if (args.all && name !== undefined) {
211
+ log({ text: "Use either --all or a migration file name, not both.", type: "error" });
212
+ usage(1);
213
+ }
214
+ if (!args.all && name === undefined) {
215
+ log({ text: "mark requires a migration file name or --all.", type: "error" });
216
+ usage(1);
217
+ }
218
+ const { marked } = await markMigrationsApplied({
219
+ ...listDirOptions,
220
+ ...tableOptions,
221
+ ...(name !== undefined ? { to: name } : {}),
222
+ });
223
+ if (marked.length > 0) {
224
+ log({ text: `Marked ${marked.length} migration(s) as applied.`, type: "success" });
225
+ }
226
+ break;
227
+ }
228
+ case "status": {
229
+ const { applied, pending } = await migrateStatus({ ...listDirOptions, ...tableOptions });
230
+ for (const entry of applied) {
231
+ log({ text: `${entry.name} applied`, type: "info" });
232
+ }
233
+ for (const file of pending) {
234
+ log({ text: `${file} pending`, type: "warn" });
235
+ }
236
+ log({ text: `${applied.length} applied, ${pending.length} pending`, type: "info" });
237
+ if (args.strict && pending.length > 0) {
238
+ log({ text: `Strict mode: ${pending.length} pending migration(s).`, type: "warn" });
239
+ process.exit(1);
240
+ }
241
+ break;
242
+ }
78
243
  default:
79
244
  usage(1);
80
245
  }
81
246
  } catch (error) {
82
- if (error instanceof ChecksumDriftError) {
247
+ if (
248
+ error instanceof ChecksumDriftError ||
249
+ error instanceof MigrationNotFoundError ||
250
+ error instanceof MigrationLockError ||
251
+ error instanceof InvalidIdentifierError
252
+ ) {
83
253
  log({ text: error.message, type: "error" });
84
254
  } else {
85
255
  log({ text: "Migration command failed", type: "error", error });