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.
- package/README.md +110 -42
- package/package.json +1 -1
- package/src/api/down.ts +27 -9
- package/src/api/load-migration.ts +31 -6
- package/src/api/lock.ts +2 -7
- package/src/api/mark.ts +18 -14
- package/src/api/options.ts +24 -0
- package/src/api/pending.ts +58 -0
- package/src/api/redo.ts +44 -0
- package/src/api/run-step.ts +22 -4
- package/src/api/run-with-driver.ts +10 -5
- package/src/api/up.ts +9 -16
- package/src/api/wait.ts +67 -0
- package/src/cli/main.ts +181 -26
- package/src/core/console.ts +18 -18
- package/src/core/driver.ts +1 -0
- package/src/core/duration.ts +16 -0
- package/src/drivers/mariadb.ts +11 -18
- package/src/drivers/postgres.ts +30 -20
- package/src/drivers/shared.ts +30 -2
- package/src/drivers/sqlite.ts +6 -18
- package/src/index.ts +8 -0
|
@@ -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
|
+
}
|
package/src/api/redo.ts
ADDED
|
@@ -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
|
+
}
|
package/src/api/run-step.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
+
plan: MigrationStepPlan,
|
|
7
8
|
): Promise<number> {
|
|
8
9
|
const startedAt = performance.now();
|
|
9
|
-
if (
|
|
10
|
-
await driver.
|
|
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 {
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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) {
|
package/src/api/wait.ts
ADDED
|
@@ -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 {
|
|
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 === "--
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
...
|
|
175
|
-
...
|
|
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(
|
|
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
|
-
...
|
|
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(
|
|
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" });
|