bunsql-native-migrate 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -35
- package/package.json +1 -1
- package/src/api/create.ts +2 -1
- package/src/api/down.ts +41 -37
- package/src/api/init.ts +2 -2
- package/src/api/load-migration.ts +46 -7
- package/src/api/mark.ts +6 -9
- package/src/api/options.ts +35 -0
- package/src/api/pending.ts +2 -2
- package/src/api/status.ts +2 -2
- package/src/api/tracking-table.ts +11 -0
- package/src/api/up.ts +8 -19
- package/src/cli/exit-codes.ts +31 -0
- package/src/cli/main.ts +101 -79
- package/src/core/config.ts +131 -0
- package/src/core/console.ts +4 -1
- package/src/core/env.ts +4 -0
- package/src/core/fs.ts +28 -4
- package/src/drivers/mariadb.ts +4 -1
- package/src/drivers/postgres.ts +4 -1
- package/src/drivers/shared.ts +10 -6
- package/src/drivers/sqlite-lock.ts +87 -0
- package/src/drivers/sqlite.ts +28 -11
- package/src/index.ts +8 -0
package/src/core/fs.ts
CHANGED
|
@@ -1,31 +1,55 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { getMigrationListDir } from "./env.js";
|
|
3
4
|
|
|
4
5
|
export const DEFAULT_MIGRATIONS_DIR = "migrations";
|
|
5
6
|
|
|
6
7
|
export const MIGRATION_EXTENSIONS = ["js", "ts", "up.sql"] as const;
|
|
7
8
|
|
|
9
|
+
const MIGRATION_SUFFIXES = MIGRATION_EXTENSIONS.map((extension) => `.${extension}`);
|
|
10
|
+
|
|
11
|
+
const DECLARATION_SUFFIX = ".d.ts";
|
|
12
|
+
|
|
13
|
+
export function isMigrationFileName(name: string): boolean {
|
|
14
|
+
if (name.endsWith(DECLARATION_SUFFIX)) return false;
|
|
15
|
+
return MIGRATION_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
export async function listFiles(dir: string, extensions: readonly string[]): Promise<string[]> {
|
|
19
|
+
const suffixes = extensions.map((extension) => `.${extension}`);
|
|
9
20
|
const matchedFiles: string[] = [];
|
|
10
21
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
11
22
|
for (const entry of entries) {
|
|
12
|
-
if (entry.isFile() &&
|
|
23
|
+
if (entry.isFile() && suffixes.some((suffix) => entry.name.endsWith(suffix))) {
|
|
24
|
+
if (!isMigrationFileName(entry.name)) continue;
|
|
13
25
|
matchedFiles.push(entry.name);
|
|
14
26
|
}
|
|
15
27
|
}
|
|
16
28
|
return matchedFiles.sort().reverse();
|
|
17
29
|
}
|
|
18
30
|
|
|
31
|
+
export async function listMigrationFiles(listDir: string): Promise<string[]> {
|
|
32
|
+
return listFiles(listDir, MIGRATION_EXTENSIONS);
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
export async function checksumFile(filePath: string): Promise<string> {
|
|
20
36
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
21
37
|
hasher.update(await Bun.file(filePath).arrayBuffer());
|
|
22
38
|
return hasher.digest("hex");
|
|
23
39
|
}
|
|
24
40
|
|
|
41
|
+
export async function checksumFiles(
|
|
42
|
+
listDir: string,
|
|
43
|
+
files: readonly string[],
|
|
44
|
+
): Promise<Map<string, string>> {
|
|
45
|
+
const entries = await Promise.all(
|
|
46
|
+
files.map(async (file) => [file, await checksumFile(path.join(listDir, file))] as const),
|
|
47
|
+
);
|
|
48
|
+
return new Map(entries);
|
|
49
|
+
}
|
|
50
|
+
|
|
25
51
|
export function resolveListDir(override?: string): string {
|
|
26
52
|
return path.resolve(
|
|
27
|
-
override ??
|
|
28
|
-
process.env["MIGRATION_LIST_DIR"] ??
|
|
29
|
-
path.join(process.cwd(), DEFAULT_MIGRATIONS_DIR),
|
|
53
|
+
override ?? getMigrationListDir() ?? path.join(process.cwd(), DEFAULT_MIGRATIONS_DIR),
|
|
30
54
|
);
|
|
31
55
|
}
|
package/src/drivers/mariadb.ts
CHANGED
|
@@ -28,7 +28,10 @@ async function uniqueIndexExists(db: SQL, tableName: string): Promise<boolean> {
|
|
|
28
28
|
return rows.length > 0;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function create(
|
|
31
|
+
export async function create(
|
|
32
|
+
databaseUrl: string,
|
|
33
|
+
options: DriverTableOptions = {},
|
|
34
|
+
): Promise<MigrationDriver> {
|
|
32
35
|
const { table, index, name } = resolveTableRef(options, {
|
|
33
36
|
quote: backtickQuoted,
|
|
34
37
|
maxLength: TABLE_NAME_MAX_LENGTH,
|
package/src/drivers/postgres.ts
CHANGED
|
@@ -39,7 +39,10 @@ async function advisoryKeyComponents(lock: SQL): Promise<[number, number]> {
|
|
|
39
39
|
return [Number((hash >> 32n) & 0x7fffffffn), Number(hash & 0x7fffffffn)];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export function create(
|
|
42
|
+
export async function create(
|
|
43
|
+
databaseUrl: string,
|
|
44
|
+
options: DriverTableOptions = {},
|
|
45
|
+
): Promise<MigrationDriver> {
|
|
43
46
|
const { table, index, name, schemaName } = resolvePostgresTableRef(options);
|
|
44
47
|
return createSqlDriver(
|
|
45
48
|
databaseUrl,
|
package/src/drivers/shared.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function resolveTableRef(options: DriverTableOptions, spec: TableRefSpec)
|
|
|
28
28
|
export interface SqlLock {
|
|
29
29
|
tryLock(timeoutSeconds: number): Promise<boolean>;
|
|
30
30
|
releaseLock(): Promise<void>;
|
|
31
|
-
dispose(): void
|
|
31
|
+
dispose(): void | Promise<void>;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface SqlDialect {
|
|
@@ -36,7 +36,8 @@ export interface SqlDialect {
|
|
|
36
36
|
record(db: SQL, migration: string, checksum: string): Promise<void>;
|
|
37
37
|
trackingTableExists?(db: SQL): Promise<boolean>;
|
|
38
38
|
trackingTableCurrent?(db: SQL): Promise<boolean>;
|
|
39
|
-
createLock?(db: SQL): SqlLock;
|
|
39
|
+
createLock?(db: SQL, databaseUrl: string): SqlLock;
|
|
40
|
+
setupConnection?(db: SQL): Promise<void>;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export function createReservedLock(
|
|
@@ -83,13 +84,16 @@ export function createReservedLock(
|
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
export function createSqlDriver(
|
|
87
|
+
export async function createSqlDriver(
|
|
87
88
|
databaseUrl: string,
|
|
88
89
|
dialect: SqlDialect,
|
|
89
90
|
table: string,
|
|
90
|
-
): MigrationDriver {
|
|
91
|
+
): Promise<MigrationDriver> {
|
|
91
92
|
const db = new SQL(databaseUrl);
|
|
92
|
-
const lock = dialect.createLock?.(db);
|
|
93
|
+
const lock = dialect.createLock?.(db, databaseUrl);
|
|
94
|
+
if (dialect.setupConnection) {
|
|
95
|
+
await dialect.setupConnection(db);
|
|
96
|
+
}
|
|
93
97
|
|
|
94
98
|
return {
|
|
95
99
|
install: () => dialect.install(db),
|
|
@@ -124,7 +128,7 @@ export function createSqlDriver(
|
|
|
124
128
|
}
|
|
125
129
|
: {}),
|
|
126
130
|
async close() {
|
|
127
|
-
lock?.dispose();
|
|
131
|
+
await lock?.dispose();
|
|
128
132
|
db.close({ timeout: 0 });
|
|
129
133
|
},
|
|
130
134
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { link, open, readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { SqlLock } from "./shared.js";
|
|
5
|
+
|
|
6
|
+
const LOCK_FILE_SUFFIX = ".bunsql-migrate.lock";
|
|
7
|
+
|
|
8
|
+
const IN_MEMORY_PATH = ":memory:";
|
|
9
|
+
|
|
10
|
+
export function sqliteLockPath(databaseUrl: string): string | null {
|
|
11
|
+
const withoutScheme = databaseUrl.replace(/^sqlite:/, "");
|
|
12
|
+
const withoutAuthority = withoutScheme.startsWith("//") ? withoutScheme.slice(2) : withoutScheme;
|
|
13
|
+
const filePath = withoutAuthority.split(/[?#]/)[0]!;
|
|
14
|
+
if (filePath === IN_MEMORY_PATH) return null;
|
|
15
|
+
return `${path.resolve(filePath)}${LOCK_FILE_SUFFIX}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function lockIsStale(lockPath: string): Promise<boolean> {
|
|
19
|
+
let content: string;
|
|
20
|
+
try {
|
|
21
|
+
content = (await readFile(lockPath, "utf8")).trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const pid = Number(content);
|
|
26
|
+
if (!Number.isInteger(pid)) return false;
|
|
27
|
+
if (pid <= 0) return true;
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return false;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return (error as NodeJS.ErrnoException).code === "ESRCH";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function tryCreateLockFile(lockPath: string): Promise<boolean> {
|
|
37
|
+
const stagingPath = `${lockPath}.${randomUUID()}.tmp`;
|
|
38
|
+
const handle = await open(stagingPath, "wx");
|
|
39
|
+
try {
|
|
40
|
+
await handle.writeFile(String(process.pid));
|
|
41
|
+
} finally {
|
|
42
|
+
await handle.close();
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await link(stagingPath, lockPath);
|
|
46
|
+
return true;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
49
|
+
if (!(await lockIsStale(lockPath))) return false;
|
|
50
|
+
await unlink(lockPath).catch(() => undefined);
|
|
51
|
+
return tryCreateLockFile(lockPath);
|
|
52
|
+
} finally {
|
|
53
|
+
await unlink(stagingPath).catch(() => undefined);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createSqliteFileLock(lockPath: string): SqlLock {
|
|
58
|
+
let held = false;
|
|
59
|
+
return {
|
|
60
|
+
async tryLock() {
|
|
61
|
+
if (held) return true;
|
|
62
|
+
if (await tryCreateLockFile(lockPath)) {
|
|
63
|
+
held = true;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
},
|
|
68
|
+
async releaseLock() {
|
|
69
|
+
if (!held) return;
|
|
70
|
+
held = false;
|
|
71
|
+
await unlink(lockPath).catch(() => undefined);
|
|
72
|
+
},
|
|
73
|
+
async dispose() {
|
|
74
|
+
if (!held) return;
|
|
75
|
+
held = false;
|
|
76
|
+
await unlink(lockPath).catch(() => undefined);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createInMemoryLock(): SqlLock {
|
|
82
|
+
return {
|
|
83
|
+
tryLock: async () => true,
|
|
84
|
+
releaseLock: async () => {},
|
|
85
|
+
dispose() {},
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/drivers/sqlite.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { DriverTableOptions, MigrationDriver } from "../core/driver.js";
|
|
2
2
|
import { doubleQuoted } from "../core/identifiers.js";
|
|
3
3
|
import { createSqlDriver, resolveTableRef, UNIQUE_INDEX_SUFFIX } from "./shared.js";
|
|
4
|
+
import { createInMemoryLock, createSqliteFileLock, sqliteLockPath } from "./sqlite-lock.js";
|
|
4
5
|
|
|
5
6
|
const TABLE_NAME_MAX_LENGTH = 128;
|
|
6
7
|
|
|
7
|
-
export
|
|
8
|
+
export const DEFAULT_BUSY_TIMEOUT_MS = 30_000;
|
|
9
|
+
|
|
10
|
+
export async function create(
|
|
11
|
+
databaseUrl: string,
|
|
12
|
+
options: DriverTableOptions = {},
|
|
13
|
+
): Promise<MigrationDriver> {
|
|
8
14
|
const { table, index, name } = resolveTableRef(options, {
|
|
9
15
|
quote: doubleQuoted,
|
|
10
16
|
maxLength: TABLE_NAME_MAX_LENGTH,
|
|
@@ -45,16 +51,27 @@ export function create(databaseUrl: string, options: DriverTableOptions = {}): M
|
|
|
45
51
|
await db`INSERT OR IGNORE INTO ${db.unsafe(table)} (migration, checksum)
|
|
46
52
|
VALUES (${migration}, ${checksum})`;
|
|
47
53
|
},
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
async setupConnection(db) {
|
|
55
|
+
await db.unsafe(`PRAGMA busy_timeout = ${DEFAULT_BUSY_TIMEOUT_MS}`);
|
|
56
|
+
},
|
|
57
|
+
createLock: (_db) => {
|
|
58
|
+
const lockPath = sqliteLockPath(databaseUrl);
|
|
59
|
+
if (lockPath === null) {
|
|
60
|
+
return createInMemoryLock();
|
|
61
|
+
}
|
|
62
|
+
const fileLock = createSqliteFileLock(lockPath);
|
|
63
|
+
return {
|
|
64
|
+
async tryLock(timeoutSeconds: number) {
|
|
65
|
+
return fileLock.tryLock(timeoutSeconds);
|
|
66
|
+
},
|
|
67
|
+
async releaseLock() {
|
|
68
|
+
await fileLock.releaseLock();
|
|
69
|
+
},
|
|
70
|
+
async dispose() {
|
|
71
|
+
await fileLock.dispose();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
58
75
|
},
|
|
59
76
|
table,
|
|
60
77
|
);
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type MigrationDriver,
|
|
6
6
|
} from "./core/driver.js";
|
|
7
7
|
import { InvalidIdentifierError } from "./core/identifiers.js";
|
|
8
|
+
import { InvalidConfigError, loadProjectConfig, type ProjectConfig } from "./core/config.js";
|
|
8
9
|
import { migrateUp } from "./api/up.js";
|
|
9
10
|
import { migrateDown } from "./api/down.js";
|
|
10
11
|
import { migrateRedo } from "./api/redo.js";
|
|
@@ -26,12 +27,15 @@ import {
|
|
|
26
27
|
ChecksumDriftError,
|
|
27
28
|
DatabaseWaitTimeoutError,
|
|
28
29
|
GitStageError,
|
|
30
|
+
InvalidMigrationNameError,
|
|
31
|
+
MigrationFileMissingError,
|
|
29
32
|
MigrationLockError,
|
|
30
33
|
MigrationNotFoundError,
|
|
31
34
|
} from "./api/options.js";
|
|
32
35
|
|
|
33
36
|
export {
|
|
34
37
|
createDriver,
|
|
38
|
+
loadProjectConfig,
|
|
35
39
|
migrateUp,
|
|
36
40
|
migrateDown,
|
|
37
41
|
migrateRedo,
|
|
@@ -42,7 +46,10 @@ export {
|
|
|
42
46
|
ChecksumDriftError,
|
|
43
47
|
DatabaseWaitTimeoutError,
|
|
44
48
|
GitStageError,
|
|
49
|
+
InvalidConfigError,
|
|
45
50
|
InvalidIdentifierError,
|
|
51
|
+
InvalidMigrationNameError,
|
|
52
|
+
MigrationFileMissingError,
|
|
46
53
|
MigrationLockError,
|
|
47
54
|
MigrationNotFoundError,
|
|
48
55
|
};
|
|
@@ -60,4 +67,5 @@ export type {
|
|
|
60
67
|
MarkOptions,
|
|
61
68
|
MarkResult,
|
|
62
69
|
MigrateStatusResult,
|
|
70
|
+
ProjectConfig,
|
|
63
71
|
};
|