bunsql-native-migrate 0.1.1 → 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 +89 -27
- package/package.json +1 -1
- package/src/api/create.ts +29 -11
- package/src/api/down.ts +40 -14
- package/src/api/lock.ts +50 -0
- package/src/api/options.ts +39 -1
- package/src/api/run-step.ts +13 -0
- package/src/api/status.ts +20 -0
- package/src/api/up.ts +68 -45
- package/src/cli/main.ts +106 -7
- package/src/core/console.ts +1 -1
- package/src/core/driver.ts +5 -0
- package/src/core/fs.ts +4 -2
- package/src/core/random-name.ts +0 -68
- package/src/drivers/mariadb.ts +48 -34
- package/src/drivers/postgres.ts +31 -31
- package/src/drivers/shared.ts +91 -0
- package/src/drivers/sqlite.ts +16 -31
- package/src/index.ts +12 -0
package/src/api/up.ts
CHANGED
|
@@ -1,69 +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 {
|
|
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";
|
|
11
|
+
import { runMigrationStep } from "./run-step.js";
|
|
12
|
+
import { resolveLockTimeout, withMigrationLock } from "./lock.js";
|
|
6
13
|
|
|
7
|
-
export async function migrateUp(options:
|
|
14
|
+
export async function migrateUp(options: MigrateUpOptions = {}): Promise<MigrateUpResult> {
|
|
8
15
|
const listDir = resolveListDir(options.listDir);
|
|
16
|
+
const target = options.to;
|
|
17
|
+
const lockTimeout = resolveLockTimeout(options.lockTimeout);
|
|
9
18
|
|
|
10
19
|
return runWithDriver(options, async (driver) => {
|
|
11
20
|
await driver.install();
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
}
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
);
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
if (!record) continue;
|
|
36
|
+
const executed = await driver.listExecuted();
|
|
37
|
+
const executedByName = new Map(executed.map((entry) => [entry.name, entry]));
|
|
25
38
|
|
|
26
|
-
const checksum
|
|
27
|
-
|
|
39
|
+
for (const [file, checksum] of checksums) {
|
|
40
|
+
const record = executedByName.get(file);
|
|
41
|
+
if (!record) continue;
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
if (record.checksum === null) {
|
|
44
|
+
await driver.setChecksum(file, checksum);
|
|
45
|
+
log({ text: `${file} checksum saved (legacy record)`, type: "info" });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (record.checksum !== checksum) {
|
|
50
|
+
throw new ChecksumDriftError(file);
|
|
51
|
+
}
|
|
33
52
|
}
|
|
34
53
|
|
|
35
|
-
|
|
36
|
-
|
|
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);
|
|
37
61
|
}
|
|
38
|
-
}
|
|
39
62
|
|
|
40
|
-
|
|
41
|
-
const applied: string[] = [];
|
|
63
|
+
const applied: string[] = [];
|
|
42
64
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
if (pending.length === 0) {
|
|
66
|
+
log({ text: "No pending migrations.", type: "warn" });
|
|
67
|
+
return { applied };
|
|
68
|
+
}
|
|
47
69
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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;
|
|
56
86
|
}
|
|
57
|
-
await mod.up();
|
|
58
|
-
await driver.record(file, checksum);
|
|
59
|
-
applied.push(file);
|
|
60
|
-
log({ text: `${file} migrated up`, type: "success" });
|
|
61
|
-
} catch (error) {
|
|
62
|
-
log({ text: `${file} migration failed`, type: "error", error });
|
|
63
|
-
throw error;
|
|
64
87
|
}
|
|
65
|
-
}
|
|
66
88
|
|
|
67
|
-
|
|
89
|
+
return { applied };
|
|
90
|
+
});
|
|
68
91
|
});
|
|
69
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 {
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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 });
|
package/src/core/console.ts
CHANGED
package/src/core/driver.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type SQL } from "bun";
|
|
2
|
+
|
|
1
3
|
export interface ExecutedMigration {
|
|
2
4
|
name: string;
|
|
3
5
|
checksum: string | null;
|
|
@@ -9,6 +11,9 @@ export interface MigrationDriver {
|
|
|
9
11
|
record(migration: string, checksum: string): Promise<void>;
|
|
10
12
|
setChecksum(migration: string, checksum: string): Promise<void>;
|
|
11
13
|
remove(migration: string): Promise<void>;
|
|
14
|
+
transaction<T>(run: (tx: SQL) => Promise<T>): Promise<T>;
|
|
15
|
+
tryLock?(timeoutSeconds: number): Promise<boolean>;
|
|
16
|
+
releaseLock?(): Promise<void>;
|
|
12
17
|
close(): Promise<void>;
|
|
13
18
|
}
|
|
14
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
|
|
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
|
}
|
package/src/core/random-name.ts
CHANGED
|
@@ -1,54 +1,20 @@
|
|
|
1
1
|
const adjectives = [
|
|
2
2
|
"brave",
|
|
3
3
|
"calm",
|
|
4
|
-
"dusty",
|
|
5
4
|
"eager",
|
|
6
|
-
"fair",
|
|
7
5
|
"golden",
|
|
8
|
-
"hasty",
|
|
9
6
|
"jolly",
|
|
10
7
|
"keen",
|
|
11
8
|
"lucky",
|
|
12
9
|
"merry",
|
|
13
10
|
"noble",
|
|
14
|
-
"proud",
|
|
15
11
|
"quick",
|
|
16
12
|
"sharp",
|
|
17
13
|
"swift",
|
|
18
|
-
"tall",
|
|
19
14
|
"vivid",
|
|
20
15
|
"warm",
|
|
21
|
-
"young",
|
|
22
16
|
"bold",
|
|
23
17
|
"crisp",
|
|
24
|
-
"dry",
|
|
25
|
-
"fine",
|
|
26
|
-
"glad",
|
|
27
|
-
"kind",
|
|
28
|
-
"light",
|
|
29
|
-
"mild",
|
|
30
|
-
"neat",
|
|
31
|
-
"prime",
|
|
32
|
-
"rare",
|
|
33
|
-
"safe",
|
|
34
|
-
"true",
|
|
35
|
-
"vast",
|
|
36
|
-
"wise",
|
|
37
|
-
"apt",
|
|
38
|
-
"bright",
|
|
39
|
-
"deep",
|
|
40
|
-
"free",
|
|
41
|
-
"grand",
|
|
42
|
-
"honest",
|
|
43
|
-
"just",
|
|
44
|
-
"lean",
|
|
45
|
-
"open",
|
|
46
|
-
"plain",
|
|
47
|
-
"still",
|
|
48
|
-
"wild",
|
|
49
|
-
"cool",
|
|
50
|
-
"soft",
|
|
51
|
-
"dark",
|
|
52
18
|
];
|
|
53
19
|
|
|
54
20
|
const nouns = [
|
|
@@ -61,47 +27,13 @@ const nouns = [
|
|
|
61
27
|
"breeze",
|
|
62
28
|
"creek",
|
|
63
29
|
"ember",
|
|
64
|
-
"glacier",
|
|
65
|
-
"harbor",
|
|
66
|
-
"island",
|
|
67
|
-
"jasper",
|
|
68
|
-
"kettle",
|
|
69
30
|
"lantern",
|
|
70
31
|
"meadow",
|
|
71
|
-
"nectar",
|
|
72
|
-
"orchid",
|
|
73
|
-
"prism",
|
|
74
32
|
"quartz",
|
|
75
33
|
"ridge",
|
|
76
34
|
"tide",
|
|
77
35
|
"valley",
|
|
78
36
|
"willow",
|
|
79
|
-
"zephyr",
|
|
80
|
-
"bolt",
|
|
81
|
-
"cliff",
|
|
82
|
-
"dawn",
|
|
83
|
-
"fern",
|
|
84
|
-
"grove",
|
|
85
|
-
"haze",
|
|
86
|
-
"iris",
|
|
87
|
-
"jet",
|
|
88
|
-
"kite",
|
|
89
|
-
"lark",
|
|
90
|
-
"marsh",
|
|
91
|
-
"nest",
|
|
92
|
-
"opal",
|
|
93
|
-
"peak",
|
|
94
|
-
"reed",
|
|
95
|
-
"sage",
|
|
96
|
-
"thorn",
|
|
97
|
-
"vine",
|
|
98
|
-
"wolf",
|
|
99
|
-
"ash",
|
|
100
|
-
"bay",
|
|
101
|
-
"cape",
|
|
102
|
-
"dune",
|
|
103
|
-
"flint",
|
|
104
|
-
"gleam",
|
|
105
37
|
];
|
|
106
38
|
|
|
107
39
|
export function randomName(): string {
|
package/src/drivers/mariadb.ts
CHANGED
|
@@ -1,45 +1,59 @@
|
|
|
1
|
-
import { SQL } from "bun";
|
|
2
|
-
import type {
|
|
1
|
+
import { type SQL } from "bun";
|
|
2
|
+
import type { MigrationDriver } from "../core/driver.js";
|
|
3
|
+
import { createReservedLock, createSqlDriver } from "./shared.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
const LOCK_NAME_PREFIX = "bunsql-native-migrate:";
|
|
6
|
+
|
|
7
|
+
async function checksumColumnExists(db: SQL): Promise<boolean> {
|
|
8
|
+
const rows = await db`SELECT column_name FROM information_schema.columns
|
|
9
|
+
WHERE table_schema = DATABASE()
|
|
10
|
+
AND table_name = 'migrations'
|
|
11
|
+
AND column_name = 'checksum'`;
|
|
12
|
+
return rows.length > 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function uniqueIndexExists(db: SQL): Promise<boolean> {
|
|
16
|
+
const rows = await db`SELECT index_name FROM information_schema.statistics
|
|
17
|
+
WHERE table_schema = DATABASE()
|
|
18
|
+
AND table_name = 'migrations'
|
|
19
|
+
AND index_name = 'migrations_migration_unique'`;
|
|
20
|
+
return rows.length > 0;
|
|
21
|
+
}
|
|
6
22
|
|
|
7
|
-
|
|
8
|
-
|
|
23
|
+
export function create(databaseUrl: string): MigrationDriver {
|
|
24
|
+
return createSqlDriver(databaseUrl, {
|
|
25
|
+
async install(db) {
|
|
9
26
|
await db`CREATE TABLE IF NOT EXISTS migrations (
|
|
10
27
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
|
11
28
|
migration VARCHAR(255) NOT NULL,
|
|
12
|
-
checksum VARCHAR(64)
|
|
29
|
+
checksum VARCHAR(64),
|
|
30
|
+
CONSTRAINT migrations_migration_unique UNIQUE (migration)
|
|
13
31
|
)`;
|
|
14
|
-
await db
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return rows.map(
|
|
21
|
-
(r: { migration: string; checksum: string | null }): ExecutedMigration => ({
|
|
22
|
-
name: r.migration,
|
|
23
|
-
checksum: r.checksum ?? null,
|
|
24
|
-
}),
|
|
25
|
-
);
|
|
32
|
+
if (!(await checksumColumnExists(db))) {
|
|
33
|
+
await db`ALTER TABLE migrations ADD COLUMN checksum VARCHAR(64)`;
|
|
34
|
+
}
|
|
35
|
+
if (!(await uniqueIndexExists(db))) {
|
|
36
|
+
await db`CREATE UNIQUE INDEX migrations_migration_unique ON migrations (migration)`;
|
|
37
|
+
}
|
|
26
38
|
},
|
|
27
|
-
|
|
28
|
-
async record(migration: string, checksum: string) {
|
|
39
|
+
async record(db, migration, checksum) {
|
|
29
40
|
await db`INSERT IGNORE INTO migrations (migration, checksum)
|
|
30
41
|
VALUES (${migration}, ${checksum})`;
|
|
31
42
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
),
|
|
58
|
+
});
|
|
45
59
|
}
|
package/src/drivers/postgres.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { SQL } from "bun";
|
|
2
|
-
import type {
|
|
1
|
+
import { type SQL } from "bun";
|
|
2
|
+
import type { MigrationDriver } from "../core/driver.js";
|
|
3
|
+
import { createReservedLock, createSqlDriver } from "./shared.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
}
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
export function create(databaseUrl: string): MigrationDriver {
|
|
14
|
+
return createSqlDriver(databaseUrl, {
|
|
15
|
+
async install(db) {
|
|
9
16
|
await db`CREATE TABLE IF NOT EXISTS migrations (
|
|
10
17
|
id SERIAL PRIMARY KEY,
|
|
11
18
|
migration VARCHAR(255) NOT NULL,
|
|
@@ -14,33 +21,26 @@ export function create(databaseUrl: string): MigrationDriver {
|
|
|
14
21
|
await db`ALTER TABLE migrations ADD COLUMN IF NOT EXISTS checksum VARCHAR(64)`;
|
|
15
22
|
await db`CREATE UNIQUE INDEX IF NOT EXISTS migrations_migration_unique ON migrations (migration)`;
|
|
16
23
|
},
|
|
17
|
-
|
|
18
|
-
async listExecuted() {
|
|
19
|
-
const rows = await db`SELECT migration, checksum FROM migrations ORDER BY id ASC`;
|
|
20
|
-
return rows.map(
|
|
21
|
-
(r: { migration: string; checksum: string | null }): ExecutedMigration => ({
|
|
22
|
-
name: r.migration,
|
|
23
|
-
checksum: r.checksum ?? null,
|
|
24
|
-
}),
|
|
25
|
-
);
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
async record(migration: string, checksum: string) {
|
|
24
|
+
async record(db, migration, checksum) {
|
|
29
25
|
await db`INSERT INTO migrations (migration, checksum)
|
|
30
26
|
VALUES (${migration}, ${checksum})
|
|
31
27
|
ON CONFLICT (migration) DO NOTHING`;
|
|
32
28
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
),
|
|
45
|
+
});
|
|
46
46
|
}
|