bunsql-native-migrate 0.3.1 → 0.4.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,58 @@
1
+ import { listFiles, MIGRATION_EXTENSIONS } from "../core/fs.js";
2
+ import { log } from "../core/console.js";
3
+ import { MigrationNotFoundError } from "./options.js";
4
+
5
+ export interface PendingToTargetResult {
6
+ pending: string[];
7
+ targetApplied: boolean;
8
+ }
9
+
10
+ interface ResolvePendingOptions {
11
+ allFiles: string[];
12
+ executedNames: string[];
13
+ target: string | undefined;
14
+ }
15
+
16
+ export function assertTargetInFiles(allFiles: readonly string[], target: string): void {
17
+ if (!allFiles.includes(target)) {
18
+ throw new MigrationNotFoundError(target);
19
+ }
20
+ }
21
+
22
+ export async function assertTargetOptions(
23
+ command: string,
24
+ listDir: string,
25
+ target: string | undefined,
26
+ steps: unknown,
27
+ ): Promise<void> {
28
+ if (target !== undefined && steps !== undefined) {
29
+ throw new Error(`Invalid ${command} options: "to" and "steps" cannot be combined`);
30
+ }
31
+ if (target !== undefined) {
32
+ assertTargetInFiles(await listFiles(listDir, MIGRATION_EXTENSIONS), target);
33
+ }
34
+ }
35
+
36
+ export function resolvePendingToTarget({
37
+ allFiles,
38
+ executedNames,
39
+ target,
40
+ }: ResolvePendingOptions): PendingToTargetResult {
41
+ const executed = new Set(executedNames);
42
+
43
+ if (target !== undefined) {
44
+ assertTargetInFiles(allFiles, target);
45
+ }
46
+
47
+ let pending = allFiles.filter((file) => !executed.has(file));
48
+ let targetApplied = false;
49
+ if (target !== undefined) {
50
+ if (executed.has(target)) {
51
+ log({ text: `${target} is already applied.`, type: "info" });
52
+ targetApplied = true;
53
+ } else {
54
+ pending = pending.slice(0, pending.indexOf(target) + 1);
55
+ }
56
+ }
57
+ return { pending, targetApplied };
58
+ }
@@ -0,0 +1,44 @@
1
+ import { resolveListDir } from "../core/fs.js";
2
+ import { log } from "../core/console.js";
3
+ import { type RedoOptions, type RedoResult } from "./options.js";
4
+ import { migrateDown, parseSteps } from "./down.js";
5
+ import { migrateUp } from "./up.js";
6
+ import { migrateStatus } from "./status.js";
7
+ import { assertTargetOptions } from "./pending.js";
8
+
9
+ export async function migrateRedo(options: RedoOptions = {}): Promise<RedoResult> {
10
+ const listDir = resolveListDir(options.listDir);
11
+ const target = options.to;
12
+ const steps = parseSteps(options.steps);
13
+
14
+ await assertTargetOptions("redo", listDir, target, options.steps);
15
+
16
+ const { applied } = await migrateStatus(options);
17
+ const lastApplied = applied.at(-1)?.name;
18
+ if (lastApplied === undefined) {
19
+ log({ text: "No migrations to redo.", type: "warn" });
20
+ return { reverted: [], applied: [] };
21
+ }
22
+
23
+ const appliedNames = applied.map((entry) => entry.name);
24
+ if (target !== undefined && !appliedNames.includes(target)) {
25
+ log({ text: `${target} is not applied — nothing to redo.`, type: "warn" });
26
+ return { reverted: [], applied: [] };
27
+ }
28
+
29
+ let reverted: string[] = [];
30
+ try {
31
+ ({ reverted } =
32
+ target !== undefined ? await migrateDown(options) : await migrateDown({ ...options, steps }));
33
+ const up = await migrateUp({ ...options, to: lastApplied });
34
+ return { reverted, applied: up.applied };
35
+ } catch (error) {
36
+ if (reverted.length > 0) {
37
+ log({
38
+ text: "Redo: the up phase failed — the rollbacks above stay reverted; run up to re-apply them",
39
+ type: "warn",
40
+ });
41
+ }
42
+ throw error;
43
+ }
44
+ }
@@ -1,15 +1,33 @@
1
1
  import { type SQL } from "bun";
2
2
  import type { MigrationDriver } from "../core/driver.js";
3
+ import type { MigrationStepPlan } from "./load-migration.js";
3
4
 
4
5
  export async function runMigrationStep(
5
6
  driver: MigrationDriver,
6
- step: (tx?: SQL) => Promise<void>,
7
+ plan: MigrationStepPlan,
7
8
  ): Promise<number> {
8
9
  const startedAt = performance.now();
9
- if (step.length > 0) {
10
- await driver.transaction((tx) => step(tx));
10
+ if (plan.noTransaction) {
11
+ await runOutsideTransaction(driver, plan.step);
12
+ return performance.now() - startedAt;
13
+ }
14
+ if (plan.step.length > 0) {
15
+ await driver.transaction((tx) => plan.step(tx));
11
16
  return performance.now() - startedAt;
12
17
  }
13
- await step();
18
+ await plan.step();
14
19
  return performance.now() - startedAt;
15
20
  }
21
+
22
+ function runOutsideTransaction(
23
+ driver: MigrationDriver,
24
+ step: (tx?: SQL) => Promise<void>,
25
+ ): Promise<void> {
26
+ const client = driver.client?.();
27
+ if (client === undefined) {
28
+ throw new Error(
29
+ "this driver does not expose a non-transactional client — the noTransaction marker is unsupported here",
30
+ );
31
+ }
32
+ return step(client);
33
+ }
@@ -1,16 +1,21 @@
1
1
  import { getDatabaseUrl } from "../core/env.js";
2
- import { createDriver, type MigrationDriver } from "../core/driver.js";
2
+ import type { MigrationDriver } from "../core/driver.js";
3
3
  import type { MigrateOptions } from "./options.js";
4
+ import { connectDriver, resolveWaitTimeout } from "./wait.js";
4
5
 
5
6
  export async function runWithDriver<T>(
6
7
  options: MigrateOptions,
7
8
  run: (driver: MigrationDriver) => Promise<T>,
8
9
  ): Promise<T> {
9
10
  const url = getDatabaseUrl(options.databaseUrl);
10
- const driver = await createDriver(url, {
11
- ...(options.tableName !== undefined ? { tableName: options.tableName } : {}),
12
- ...(options.schema !== undefined ? { schema: options.schema } : {}),
13
- });
11
+ const driver = await connectDriver(
12
+ url,
13
+ {
14
+ ...(options.tableName !== undefined ? { tableName: options.tableName } : {}),
15
+ ...(options.schema !== undefined ? { schema: options.schema } : {}),
16
+ },
17
+ resolveWaitTimeout(options.waitTimeout),
18
+ );
14
19
  try {
15
20
  return await run(driver);
16
21
  } finally {
package/src/api/up.ts CHANGED
@@ -2,15 +2,11 @@ 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 {
6
- type MigrateUpOptions,
7
- type MigrateUpResult,
8
- ChecksumDriftError,
9
- MigrationNotFoundError,
10
- } from "./options.js";
5
+ import { type MigrateUpOptions, type MigrateUpResult, ChecksumDriftError } from "./options.js";
11
6
  import { runWithDriver } from "./run-with-driver.js";
12
7
  import { runMigrationStep } from "./run-step.js";
13
8
  import { loadMigration } from "./load-migration.js";
9
+ import { resolvePendingToTarget } from "./pending.js";
14
10
  import { resolveLockTimeout, withMigrationLock } from "./lock.js";
15
11
  import { ensureTrackingTable, listExecutedForPlan } from "./tracking-table.js";
16
12
 
@@ -27,9 +23,6 @@ export async function migrateUp(options: MigrateUpOptions = {}): Promise<Migrate
27
23
 
28
24
  const run = async (): Promise<MigrateUpResult> => {
29
25
  const allFiles = await listFiles(listDir, MIGRATION_EXTENSIONS);
30
- if (target !== undefined && !allFiles.includes(target)) {
31
- throw new MigrationNotFoundError(target);
32
- }
33
26
 
34
27
  const checksums = new Map(
35
28
  await Promise.all(
@@ -59,13 +52,13 @@ export async function migrateUp(options: MigrateUpOptions = {}): Promise<Migrate
59
52
  }
60
53
  }
61
54
 
62
- let pending = allFiles.filter((file) => !executedByName.has(file));
63
- if (target !== undefined) {
64
- if (executedByName.has(target)) {
65
- log({ text: `${target} is already applied.`, type: "info" });
66
- return dryRun ? { applied: [], planned: [] } : { applied: [] };
67
- }
68
- pending = pending.slice(0, pending.indexOf(target) + 1);
55
+ const { pending, targetApplied } = resolvePendingToTarget({
56
+ allFiles,
57
+ executedNames: executed.map((entry) => entry.name),
58
+ target,
59
+ });
60
+ if (targetApplied) {
61
+ return dryRun ? { applied: [], planned: [] } : { applied: [] };
69
62
  }
70
63
 
71
64
  if (dryRun) {
@@ -0,0 +1,67 @@
1
+ import { log } from "../core/console.js";
2
+ import { createDriver, type DriverTableOptions, type MigrationDriver } from "../core/driver.js";
3
+ import { resolveSecondsOption } from "../core/duration.js";
4
+ import { DatabaseWaitTimeoutError } from "./options.js";
5
+
6
+ export const WAIT_RETRY_DELAY_MS = 500;
7
+
8
+ export function resolveWaitTimeout(waitTimeout: number | undefined): number {
9
+ return resolveSecondsOption("waitTimeout", waitTimeout, 0);
10
+ }
11
+
12
+ export async function waitForDatabase<T>(
13
+ attempt: () => Promise<T>,
14
+ timeoutSeconds: number,
15
+ retryDelayMs: number = WAIT_RETRY_DELAY_MS,
16
+ ): Promise<T> {
17
+ const deadline = Date.now() + timeoutSeconds * 1000;
18
+ let waitingLogged = false;
19
+ while (true) {
20
+ try {
21
+ return await attempt();
22
+ } catch (error) {
23
+ if (Date.now() >= deadline) {
24
+ throw new DatabaseWaitTimeoutError(timeoutSeconds, error);
25
+ }
26
+ if (!waitingLogged) {
27
+ log({ text: `database is not ready — waiting up to ${timeoutSeconds}s`, type: "info" });
28
+ waitingLogged = true;
29
+ }
30
+ await Bun.sleep(retryDelayMs);
31
+ }
32
+ }
33
+ }
34
+
35
+ async function connectProbed(
36
+ databaseUrl: string,
37
+ tableOptions: DriverTableOptions,
38
+ ): Promise<MigrationDriver> {
39
+ const driver = await createDriver(databaseUrl, tableOptions);
40
+ try {
41
+ await driver.transaction(async () => {});
42
+ return driver;
43
+ } catch (error) {
44
+ await driver.close().catch(() => undefined);
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ async function assertDriverConfig(
50
+ databaseUrl: string,
51
+ tableOptions: DriverTableOptions,
52
+ ): Promise<void> {
53
+ const driver = await createDriver(databaseUrl, tableOptions);
54
+ await driver.close();
55
+ }
56
+
57
+ export async function connectDriver(
58
+ databaseUrl: string,
59
+ tableOptions: DriverTableOptions,
60
+ waitTimeout: number,
61
+ ): Promise<MigrationDriver> {
62
+ if (waitTimeout <= 0) {
63
+ return createDriver(databaseUrl, tableOptions);
64
+ }
65
+ await assertDriverConfig(databaseUrl, tableOptions);
66
+ return waitForDatabase(() => connectProbed(databaseUrl, tableOptions), waitTimeout);
67
+ }
package/src/cli/main.ts CHANGED
@@ -1,47 +1,145 @@
1
1
  #!/usr/bin/env bun
2
+ import path from "node:path";
2
3
  import { migrateUp } from "../api/up.js";
3
- import { migrateDown } from "../api/down.js";
4
+ import { migrateDown, parseSteps } from "../api/down.js";
5
+ import { migrateRedo } from "../api/redo.js";
4
6
  import { migrateStatus } from "../api/status.js";
5
7
  import { installMigrations } from "../api/install.js";
6
8
  import { createMigrationCommand, type MigrationLang } from "../api/create.js";
7
9
  import { initMigrations } from "../api/init.js";
8
10
  import { markMigrationsApplied } from "../api/mark.js";
9
- import { ChecksumDriftError, MigrationLockError, MigrationNotFoundError } from "../api/options.js";
11
+ import { resolveSecondsOption } from "../core/duration.js";
12
+ import {
13
+ ChecksumDriftError,
14
+ DatabaseWaitTimeoutError,
15
+ MigrationLockError,
16
+ MigrationNotFoundError,
17
+ } from "../api/options.js";
10
18
  import { InvalidIdentifierError } from "../core/identifiers.js";
11
19
  import { log } from "../core/console.js";
12
20
 
13
21
  interface CliArgs {
14
22
  command: string | undefined;
15
23
  positional: string[];
24
+ url?: string | undefined;
16
25
  dir?: string | undefined;
17
26
  git: boolean;
18
27
  lang?: MigrationLang | undefined;
19
28
  to?: string | undefined;
20
29
  lockTimeout?: number | undefined;
30
+ wait?: number | undefined;
21
31
  table?: string | undefined;
22
32
  schema?: string | undefined;
23
33
  dryRun: boolean;
24
34
  all: boolean;
25
35
  strict: boolean;
26
36
  help: boolean;
37
+ version: boolean;
38
+ }
39
+
40
+ type FlagKey = Exclude<keyof CliArgs, "command" | "positional" | "help" | "version">;
41
+
42
+ const FLAG_NAMES: Record<FlagKey, string> = {
43
+ url: "--url",
44
+ dir: "--dir",
45
+ git: "--git",
46
+ lang: "--lang",
47
+ to: "--to",
48
+ lockTimeout: "--lock-timeout",
49
+ wait: "--wait",
50
+ table: "--table",
51
+ schema: "--schema",
52
+ dryRun: "--dry-run",
53
+ all: "--all",
54
+ strict: "--strict",
55
+ };
56
+
57
+ interface CommandSpec {
58
+ flags: ReadonlySet<FlagKey>;
59
+ positionalLimit: number;
60
+ }
61
+
62
+ const COMMAND_SPECS: Record<string, CommandSpec> = {
63
+ version: { flags: new Set<FlagKey>([]), positionalLimit: 0 },
64
+ up: {
65
+ flags: new Set(["url", "dir", "to", "lockTimeout", "wait", "table", "schema", "dryRun"]),
66
+ positionalLimit: 0,
67
+ },
68
+ down: {
69
+ flags: new Set(["url", "dir", "to", "wait", "table", "schema", "dryRun", "all"]),
70
+ positionalLimit: 1,
71
+ },
72
+ redo: {
73
+ flags: new Set(["url", "dir", "to", "lockTimeout", "wait", "table", "schema"]),
74
+ positionalLimit: 1,
75
+ },
76
+ init: { flags: new Set(["dir", "lang"]), positionalLimit: 0 },
77
+ install: { flags: new Set(["url", "dir", "wait", "table", "schema"]), positionalLimit: 0 },
78
+ create: { flags: new Set(["dir", "lang", "git"]), positionalLimit: 1 },
79
+ mark: { flags: new Set(["url", "dir", "wait", "table", "schema", "all"]), positionalLimit: 1 },
80
+ status: {
81
+ flags: new Set(["url", "dir", "wait", "table", "schema", "strict"]),
82
+ positionalLimit: 0,
83
+ },
84
+ };
85
+
86
+ function rejectDisallowedFlags(command: string, args: CliArgs): void {
87
+ const spec = COMMAND_SPECS[command];
88
+ if (spec === undefined) {
89
+ return;
90
+ }
91
+ const [extra] = args.positional.slice(spec.positionalLimit);
92
+ if (extra !== undefined) {
93
+ log({ text: `Unexpected argument for ${command}: ${extra}`, type: "error" });
94
+ usage(1);
95
+ }
96
+ for (const flag of Object.keys(FLAG_NAMES) as FlagKey[]) {
97
+ const value = args[flag];
98
+ if (value === undefined || value === false || spec.flags.has(flag)) {
99
+ continue;
100
+ }
101
+ log({ text: `${command} does not support ${FLAG_NAMES[flag]}.`, type: "error" });
102
+ usage(1);
103
+ }
104
+ }
105
+
106
+ function parseSecondsValue(flag: string, value: string | undefined): number {
107
+ try {
108
+ return resolveSecondsOption(flag, Number(value), 0);
109
+ } catch {
110
+ log({
111
+ text: `Invalid ${flag}: ${value ?? "(missing)"} (expected a non-negative integer of seconds)`,
112
+ type: "error",
113
+ });
114
+ usage(1);
115
+ }
27
116
  }
28
117
 
29
118
  function parseArgs(argv: string[]): CliArgs {
30
119
  const positional: string[] = [];
120
+ let url: string | undefined;
31
121
  let dir: string | undefined;
32
122
  let git = false;
33
123
  let lang: MigrationLang | undefined;
34
124
  let to: string | undefined;
35
125
  let lockTimeout: number | undefined;
126
+ let wait: number | undefined;
36
127
  let table: string | undefined;
37
128
  let schema: string | undefined;
38
129
  let dryRun = false;
39
130
  let all = false;
40
131
  let strict = false;
41
132
  let help = false;
133
+ let version = false;
42
134
  for (let i = 0; i < argv.length; i++) {
43
135
  const arg = argv[i]!;
44
- if (arg === "--dir") {
136
+ if (arg === "--url") {
137
+ url = argv[++i];
138
+ if (url === undefined) {
139
+ log({ text: "--url requires a database URL", type: "error" });
140
+ usage(1);
141
+ }
142
+ } else if (arg === "--dir") {
45
143
  dir = argv[++i];
46
144
  } else if (arg === "--git") {
47
145
  git = true;
@@ -62,16 +160,9 @@ function parseArgs(argv: string[]): CliArgs {
62
160
  usage(1);
63
161
  }
64
162
  } 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;
163
+ lockTimeout = parseSecondsValue("--lock-timeout", argv[++i]);
164
+ } else if (arg === "--wait") {
165
+ wait = parseSecondsValue("--wait", argv[++i]);
75
166
  } else if (arg === "--table") {
76
167
  table = argv[++i];
77
168
  if (table === undefined) {
@@ -92,6 +183,8 @@ function parseArgs(argv: string[]): CliArgs {
92
183
  strict = true;
93
184
  } else if (arg === "--help" || arg === "-h") {
94
185
  help = true;
186
+ } else if (arg === "--version") {
187
+ version = true;
95
188
  } else {
96
189
  positional.push(arg);
97
190
  }
@@ -99,45 +192,72 @@ function parseArgs(argv: string[]): CliArgs {
99
192
  return {
100
193
  command: positional.shift(),
101
194
  positional,
195
+ url,
102
196
  dir,
103
197
  git,
104
198
  lang,
105
199
  to,
106
200
  lockTimeout,
201
+ wait,
107
202
  table,
108
203
  schema,
109
204
  dryRun,
110
205
  all,
111
206
  strict,
112
207
  help,
208
+ version,
113
209
  };
114
210
  }
115
211
 
116
212
  function usage(exitCode: number): never {
117
213
  log({
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]",
214
+ text: "Usage: bunsql-native-migrate <init|up|down [n]|redo [n]|install|create [name]|mark [name]|status|version> [--url <url>] [--dir <migrations-dir>] [--to <name>] [--lock-timeout <seconds>] [--wait <seconds>] [--table <name>] [--schema <name>] [--dry-run] [--all] [--lang <js|ts>] [--git] [--strict] [--version] [--help]",
119
215
  type: "info",
120
216
  });
121
217
  process.exit(exitCode);
122
218
  }
123
219
 
220
+ async function printVersion(): Promise<void> {
221
+ const manifest = await Bun.file(path.resolve(import.meta.dir, "..", "..", "package.json")).json();
222
+ log({ text: String(manifest.version), type: "info" });
223
+ }
224
+
124
225
  const args = parseArgs(process.argv.slice(2));
226
+ const urlOptions = args.url !== undefined ? { databaseUrl: args.url } : {};
227
+ const waitOptions = args.wait !== undefined && args.wait > 0 ? { waitTimeout: args.wait } : {};
125
228
  const listDirOptions = args.dir ? { listDir: args.dir } : {};
126
229
  const tableOptions = {
127
230
  ...(args.table !== undefined ? { tableName: args.table } : {}),
128
231
  ...(args.schema !== undefined ? { schema: args.schema } : {}),
129
232
  };
233
+ const connectOptions = {
234
+ ...urlOptions,
235
+ ...listDirOptions,
236
+ ...tableOptions,
237
+ ...waitOptions,
238
+ };
130
239
 
131
240
  if (args.help) {
132
241
  usage(0);
133
242
  }
134
243
 
244
+ if (args.version) {
245
+ await printVersion();
246
+ process.exit(0);
247
+ }
248
+
135
249
  try {
250
+ if (args.command !== undefined) {
251
+ rejectDisallowedFlags(args.command, args);
252
+ }
136
253
  switch (args.command) {
254
+ case "version": {
255
+ await printVersion();
256
+ break;
257
+ }
137
258
  case "up": {
138
259
  const { applied, planned } = await migrateUp({
139
- ...listDirOptions,
140
- ...tableOptions,
260
+ ...connectOptions,
141
261
  ...(args.to ? { to: args.to } : {}),
142
262
  ...(args.lockTimeout !== undefined ? { lockTimeout: args.lockTimeout } : {}),
143
263
  ...(args.dryRun ? { dryRun: true } : {}),
@@ -156,24 +276,30 @@ try {
156
276
  log({ text: "Use either --all or a number of steps, not both.", type: "error" });
157
277
  usage(1);
158
278
  }
279
+ if (args.to !== undefined && (args.all || stepsArg !== undefined)) {
280
+ log({
281
+ text: "Use either --to, --all, or a number of steps, not more than one of them.",
282
+ type: "error",
283
+ });
284
+ usage(1);
285
+ }
159
286
  let steps: number | "all" = 1;
160
287
  if (args.all) {
161
288
  steps = "all";
162
289
  } else if (stepsArg !== undefined) {
163
- const parsed = Number(stepsArg);
164
- if (!Number.isInteger(parsed) || parsed < 1) {
290
+ try {
291
+ steps = parseSteps(Number(stepsArg));
292
+ } catch {
165
293
  log({
166
294
  text: `Invalid step count: ${stepsArg} (expected a positive integer)`,
167
295
  type: "error",
168
296
  });
169
297
  usage(1);
170
298
  }
171
- steps = parsed;
172
299
  }
173
300
  const { reverted, planned } = await migrateDown({
174
- ...listDirOptions,
175
- ...tableOptions,
176
- steps,
301
+ ...connectOptions,
302
+ ...(args.to !== undefined ? { to: args.to } : { steps }),
177
303
  ...(args.dryRun ? { dryRun: true } : {}),
178
304
  });
179
305
  if (planned !== undefined && planned.length > 0) {
@@ -184,6 +310,35 @@ try {
184
310
  }
185
311
  break;
186
312
  }
313
+ case "redo": {
314
+ const [stepsArg] = args.positional;
315
+ if (stepsArg !== undefined && args.to !== undefined) {
316
+ log({ text: "Use either --to or a step count, not both.", type: "error" });
317
+ usage(1);
318
+ }
319
+ let steps: number | undefined;
320
+ if (stepsArg !== undefined) {
321
+ try {
322
+ steps = parseSteps(Number(stepsArg));
323
+ } catch {
324
+ log({
325
+ text: `Invalid step count: ${stepsArg} (expected a positive integer)`,
326
+ type: "error",
327
+ });
328
+ usage(1);
329
+ }
330
+ }
331
+ const { reverted } = await migrateRedo({
332
+ ...connectOptions,
333
+ ...(steps !== undefined ? { steps } : {}),
334
+ ...(args.to !== undefined ? { to: args.to } : {}),
335
+ ...(args.lockTimeout !== undefined ? { lockTimeout: args.lockTimeout } : {}),
336
+ });
337
+ if (reverted.length > 0) {
338
+ log({ text: `Redid ${reverted.length} migration(s).`, type: "success" });
339
+ }
340
+ break;
341
+ }
187
342
  case "init": {
188
343
  await initMigrations({
189
344
  ...listDirOptions,
@@ -192,7 +347,7 @@ try {
192
347
  break;
193
348
  }
194
349
  case "install": {
195
- await installMigrations({ ...listDirOptions, ...tableOptions });
350
+ await installMigrations(connectOptions);
196
351
  break;
197
352
  }
198
353
  case "create": {
@@ -216,8 +371,7 @@ try {
216
371
  usage(1);
217
372
  }
218
373
  const { marked } = await markMigrationsApplied({
219
- ...listDirOptions,
220
- ...tableOptions,
374
+ ...connectOptions,
221
375
  ...(name !== undefined ? { to: name } : {}),
222
376
  });
223
377
  if (marked.length > 0) {
@@ -226,7 +380,7 @@ try {
226
380
  break;
227
381
  }
228
382
  case "status": {
229
- const { applied, pending } = await migrateStatus({ ...listDirOptions, ...tableOptions });
383
+ const { applied, pending } = await migrateStatus(connectOptions);
230
384
  for (const entry of applied) {
231
385
  log({ text: `${entry.name} applied`, type: "info" });
232
386
  }
@@ -248,6 +402,7 @@ try {
248
402
  error instanceof ChecksumDriftError ||
249
403
  error instanceof MigrationNotFoundError ||
250
404
  error instanceof MigrationLockError ||
405
+ error instanceof DatabaseWaitTimeoutError ||
251
406
  error instanceof InvalidIdentifierError
252
407
  ) {
253
408
  log({ text: error.message, type: "error" });