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.
- package/README.md +169 -28
- package/package.json +2 -1
- package/src/api/create.ts +26 -8
- package/src/api/down.ts +56 -15
- package/src/api/init.ts +57 -0
- package/src/api/load-migration.ts +45 -0
- package/src/api/lock.ts +50 -0
- package/src/api/mark.ts +43 -0
- package/src/api/options.ts +53 -1
- package/src/api/run-step.ts +4 -2
- package/src/api/run-with-driver.ts +4 -1
- package/src/api/status.ts +20 -0
- package/src/api/up.ts +104 -49
- package/src/cli/main.ts +178 -8
- package/src/core/driver.ts +20 -4
- package/src/core/duration.ts +7 -0
- package/src/core/fs.ts +4 -2
- package/src/core/identifiers.ts +32 -0
- package/src/drivers/mariadb.ts +73 -27
- package/src/drivers/postgres.ts +83 -18
- package/src/drivers/shared.ts +69 -5
- package/src/drivers/sqlite.ts +59 -22
- package/src/index.ts +27 -1
package/src/core/driver.ts
CHANGED
|
@@ -12,10 +12,21 @@ export interface MigrationDriver {
|
|
|
12
12
|
setChecksum(migration: string, checksum: string): Promise<void>;
|
|
13
13
|
remove(migration: string): Promise<void>;
|
|
14
14
|
transaction<T>(run: (tx: SQL) => Promise<T>): Promise<T>;
|
|
15
|
+
trackingTableExists?(): Promise<boolean>;
|
|
16
|
+
tryLock?(timeoutSeconds: number): Promise<boolean>;
|
|
17
|
+
releaseLock?(): Promise<void>;
|
|
15
18
|
close(): Promise<void>;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
export
|
|
21
|
+
export interface DriverTableOptions {
|
|
22
|
+
tableName?: string;
|
|
23
|
+
schema?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function createDriver(
|
|
27
|
+
databaseUrl: string,
|
|
28
|
+
options: DriverTableOptions = {},
|
|
29
|
+
): Promise<MigrationDriver> {
|
|
19
30
|
let protocol: string;
|
|
20
31
|
try {
|
|
21
32
|
protocol = new URL(databaseUrl).protocol.replace(":", "");
|
|
@@ -26,20 +37,25 @@ export async function createDriver(databaseUrl: string): Promise<MigrationDriver
|
|
|
26
37
|
throw new Error(`Cannot parse database URL: ${databaseUrl}`);
|
|
27
38
|
}
|
|
28
39
|
}
|
|
40
|
+
if (options.schema !== undefined && protocol !== "postgres" && protocol !== "postgresql") {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"The schema option is only supported for postgres URLs — MySQL/MariaDB selects the database in the URL, SQLite has no schemas",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
29
45
|
switch (protocol) {
|
|
30
46
|
case "postgres":
|
|
31
47
|
case "postgresql": {
|
|
32
48
|
const mod = await import("./../drivers/postgres.js");
|
|
33
|
-
return mod.create(databaseUrl);
|
|
49
|
+
return mod.create(databaseUrl, options);
|
|
34
50
|
}
|
|
35
51
|
case "sqlite": {
|
|
36
52
|
const mod = await import("./../drivers/sqlite.js");
|
|
37
|
-
return mod.create(databaseUrl);
|
|
53
|
+
return mod.create(databaseUrl, options);
|
|
38
54
|
}
|
|
39
55
|
case "mariadb":
|
|
40
56
|
case "mysql": {
|
|
41
57
|
const mod = await import("./../drivers/mariadb.js");
|
|
42
|
-
return mod.create(databaseUrl);
|
|
58
|
+
return mod.create(databaseUrl, options);
|
|
43
59
|
}
|
|
44
60
|
default:
|
|
45
61
|
throw new Error(
|
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", "up.sql"] 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
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type IdentifierKind = "table" | "schema";
|
|
2
|
+
|
|
3
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
4
|
+
|
|
5
|
+
export class InvalidIdentifierError extends Error {
|
|
6
|
+
readonly kind: IdentifierKind;
|
|
7
|
+
readonly value: string;
|
|
8
|
+
|
|
9
|
+
constructor(kind: IdentifierKind, value: string, maxLength: number) {
|
|
10
|
+
super(
|
|
11
|
+
`Invalid ${kind} name: "${value}" — expected an identifier of letters, digits, underscores and dollar signs ` +
|
|
12
|
+
`starting with a letter or underscore, at most ${maxLength} characters`,
|
|
13
|
+
);
|
|
14
|
+
this.name = "InvalidIdentifierError";
|
|
15
|
+
this.kind = kind;
|
|
16
|
+
this.value = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validateIdentifier(kind: IdentifierKind, value: string, maxLength: number): void {
|
|
21
|
+
if (!IDENTIFIER_PATTERN.test(value) || value.length > maxLength) {
|
|
22
|
+
throw new InvalidIdentifierError(kind, value, maxLength);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function doubleQuoted(name: string): string {
|
|
27
|
+
return `"${name}"`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function backtickQuoted(name: string): string {
|
|
31
|
+
return `\`${name}\``;
|
|
32
|
+
}
|
package/src/drivers/mariadb.ts
CHANGED
|
@@ -1,42 +1,88 @@
|
|
|
1
1
|
import { type SQL } from "bun";
|
|
2
|
-
import type { MigrationDriver } from "../core/driver.js";
|
|
3
|
-
import {
|
|
2
|
+
import type { DriverTableOptions, MigrationDriver } from "../core/driver.js";
|
|
3
|
+
import { backtickQuoted, validateIdentifier } from "../core/identifiers.js";
|
|
4
|
+
import { createReservedLock, createSqlDriver } from "./shared.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const LOCK_NAME_PREFIX = "bunsql-native-migrate:";
|
|
7
|
+
|
|
8
|
+
const TABLE_NAME_MAX_LENGTH = 47;
|
|
9
|
+
const UNIQUE_INDEX_SUFFIX = "_migration_unique";
|
|
10
|
+
|
|
11
|
+
function resolveTableRef(options: DriverTableOptions): {
|
|
12
|
+
table: string;
|
|
13
|
+
index: string;
|
|
14
|
+
name: string;
|
|
15
|
+
} {
|
|
16
|
+
const tableName = options.tableName ?? "migrations";
|
|
17
|
+
validateIdentifier("table", tableName, TABLE_NAME_MAX_LENGTH);
|
|
18
|
+
return {
|
|
19
|
+
table: backtickQuoted(tableName),
|
|
20
|
+
index: backtickQuoted(`${tableName}${UNIQUE_INDEX_SUFFIX}`),
|
|
21
|
+
name: tableName,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function checksumColumnExists(db: SQL, tableName: string): Promise<boolean> {
|
|
6
26
|
const rows = await db`SELECT column_name FROM information_schema.columns
|
|
7
27
|
WHERE table_schema = DATABASE()
|
|
8
|
-
AND table_name =
|
|
28
|
+
AND table_name = ${tableName}
|
|
9
29
|
AND column_name = 'checksum'`;
|
|
10
30
|
return rows.length > 0;
|
|
11
31
|
}
|
|
12
32
|
|
|
13
|
-
async function uniqueIndexExists(db: SQL): Promise<boolean> {
|
|
33
|
+
async function uniqueIndexExists(db: SQL, tableName: string): Promise<boolean> {
|
|
14
34
|
const rows = await db`SELECT index_name FROM information_schema.statistics
|
|
15
35
|
WHERE table_schema = DATABASE()
|
|
16
|
-
AND table_name =
|
|
17
|
-
AND index_name =
|
|
36
|
+
AND table_name = ${tableName}
|
|
37
|
+
AND index_name = ${`${tableName}${UNIQUE_INDEX_SUFFIX}`}`;
|
|
18
38
|
return rows.length > 0;
|
|
19
39
|
}
|
|
20
40
|
|
|
21
|
-
export function create(databaseUrl: string): MigrationDriver {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
export function create(databaseUrl: string, options: DriverTableOptions = {}): MigrationDriver {
|
|
42
|
+
const { table, index, name } = resolveTableRef(options);
|
|
43
|
+
return createSqlDriver(
|
|
44
|
+
databaseUrl,
|
|
45
|
+
{
|
|
46
|
+
async install(db) {
|
|
47
|
+
await db`CREATE TABLE IF NOT EXISTS ${db.unsafe(table)} (
|
|
48
|
+
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
|
49
|
+
migration VARCHAR(255) NOT NULL,
|
|
50
|
+
checksum VARCHAR(64),
|
|
51
|
+
CONSTRAINT ${db.unsafe(index)} UNIQUE (migration)
|
|
52
|
+
)`;
|
|
53
|
+
if (!(await checksumColumnExists(db, name))) {
|
|
54
|
+
await db`ALTER TABLE ${db.unsafe(table)} ADD COLUMN checksum VARCHAR(64)`;
|
|
55
|
+
}
|
|
56
|
+
if (!(await uniqueIndexExists(db, name))) {
|
|
57
|
+
await db`CREATE UNIQUE INDEX ${db.unsafe(index)} ON ${db.unsafe(table)} (migration)`;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
async trackingTableExists(db) {
|
|
61
|
+
const rows = await db`SELECT 1 FROM information_schema.tables
|
|
62
|
+
WHERE table_schema = DATABASE()
|
|
63
|
+
AND table_name = ${name}`;
|
|
64
|
+
return rows.length > 0;
|
|
65
|
+
},
|
|
66
|
+
async record(db, migration, checksum) {
|
|
67
|
+
await db`INSERT IGNORE INTO ${db.unsafe(table)} (migration, checksum)
|
|
68
|
+
VALUES (${migration}, ${checksum})`;
|
|
69
|
+
},
|
|
70
|
+
createLock: (db) =>
|
|
71
|
+
createReservedLock(
|
|
72
|
+
db,
|
|
73
|
+
async (lock) => {
|
|
74
|
+
const rows =
|
|
75
|
+
(await lock`SELECT GET_LOCK(CONCAT(${LOCK_NAME_PREFIX}, MD5(DATABASE())), 0) AS locked`) as Array<{
|
|
76
|
+
locked: number | string | null;
|
|
77
|
+
}>;
|
|
78
|
+
const locked = rows[0]?.locked;
|
|
79
|
+
return locked === 1 || locked === "1";
|
|
80
|
+
},
|
|
81
|
+
async (lock) => {
|
|
82
|
+
await lock`SELECT RELEASE_LOCK(CONCAT(${LOCK_NAME_PREFIX}, MD5(DATABASE())))`;
|
|
83
|
+
},
|
|
84
|
+
),
|
|
40
85
|
},
|
|
41
|
-
|
|
86
|
+
table,
|
|
87
|
+
);
|
|
42
88
|
}
|
package/src/drivers/postgres.ts
CHANGED
|
@@ -1,21 +1,86 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import { type SQL } from "bun";
|
|
2
|
+
import type { DriverTableOptions, MigrationDriver } from "../core/driver.js";
|
|
3
|
+
import { doubleQuoted, validateIdentifier } from "../core/identifiers.js";
|
|
4
|
+
import { createReservedLock, createSqlDriver } from "./shared.js";
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
const LOCK_SCOPE = "bunsql-native-migrate:up";
|
|
7
|
+
|
|
8
|
+
const IDENTIFIER_MAX_LENGTH = 63;
|
|
9
|
+
const UNIQUE_INDEX_SUFFIX = "_migration_unique";
|
|
10
|
+
|
|
11
|
+
interface TableRef {
|
|
12
|
+
table: string;
|
|
13
|
+
index: string;
|
|
14
|
+
name: string;
|
|
15
|
+
schemaName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveTableRef(options: DriverTableOptions): TableRef {
|
|
19
|
+
const tableName = options.tableName ?? "migrations";
|
|
20
|
+
validateIdentifier("table", tableName, IDENTIFIER_MAX_LENGTH);
|
|
21
|
+
const index = doubleQuoted(`${tableName}${UNIQUE_INDEX_SUFFIX}`);
|
|
22
|
+
if (options.schema === undefined) {
|
|
23
|
+
return { table: doubleQuoted(tableName), index, name: tableName };
|
|
24
|
+
}
|
|
25
|
+
validateIdentifier("schema", options.schema, IDENTIFIER_MAX_LENGTH);
|
|
26
|
+
return {
|
|
27
|
+
table: `${doubleQuoted(options.schema)}.${doubleQuoted(tableName)}`,
|
|
28
|
+
index,
|
|
29
|
+
name: tableName,
|
|
30
|
+
schemaName: options.schema,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function advisoryKeyComponents(lock: SQL): Promise<[number, number]> {
|
|
35
|
+
const rows = (await lock`SELECT current_database() AS name`) as Array<{ name: string }>;
|
|
36
|
+
const hash = Bun.hash.wyhash(`${LOCK_SCOPE}:${rows[0]?.name ?? ""}`);
|
|
37
|
+
return [Number((hash >> 32n) & 0x7fffffffn), Number(hash & 0x7fffffffn)];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function create(databaseUrl: string, options: DriverTableOptions = {}): MigrationDriver {
|
|
41
|
+
const { table, index, name, schemaName } = resolveTableRef(options);
|
|
42
|
+
return createSqlDriver(
|
|
43
|
+
databaseUrl,
|
|
44
|
+
{
|
|
45
|
+
async install(db) {
|
|
46
|
+
await db`CREATE TABLE IF NOT EXISTS ${db.unsafe(table)} (
|
|
47
|
+
id SERIAL PRIMARY KEY,
|
|
48
|
+
migration VARCHAR(255) NOT NULL,
|
|
49
|
+
checksum VARCHAR(64)
|
|
50
|
+
)`;
|
|
51
|
+
await db`ALTER TABLE ${db.unsafe(table)} ADD COLUMN IF NOT EXISTS checksum VARCHAR(64)`;
|
|
52
|
+
await db`CREATE UNIQUE INDEX IF NOT EXISTS ${db.unsafe(index)} ON ${db.unsafe(table)} (migration)`;
|
|
53
|
+
},
|
|
54
|
+
async trackingTableExists(db) {
|
|
55
|
+
const rows = schemaName
|
|
56
|
+
? await db`SELECT 1 FROM information_schema.tables
|
|
57
|
+
WHERE table_schema = ${schemaName} AND table_name = ${name}`
|
|
58
|
+
: await db`SELECT 1 FROM information_schema.tables
|
|
59
|
+
WHERE table_schema = current_schema() AND table_name = ${name}`;
|
|
60
|
+
return rows.length > 0;
|
|
61
|
+
},
|
|
62
|
+
async record(db, migration, checksum) {
|
|
63
|
+
await db`INSERT INTO ${db.unsafe(table)} (migration, checksum)
|
|
64
|
+
VALUES (${migration}, ${checksum})
|
|
65
|
+
ON CONFLICT (migration) DO NOTHING`;
|
|
66
|
+
},
|
|
67
|
+
createLock: (db) =>
|
|
68
|
+
createReservedLock(
|
|
69
|
+
db,
|
|
70
|
+
async (lock) => {
|
|
71
|
+
const [first, second] = await advisoryKeyComponents(lock);
|
|
72
|
+
const rows =
|
|
73
|
+
(await lock`SELECT pg_try_advisory_lock(${first}, ${second}) AS locked`) as Array<{
|
|
74
|
+
locked: boolean;
|
|
75
|
+
}>;
|
|
76
|
+
return rows[0]?.locked === true;
|
|
77
|
+
},
|
|
78
|
+
async (lock) => {
|
|
79
|
+
const [first, second] = await advisoryKeyComponents(lock);
|
|
80
|
+
await lock`SELECT pg_advisory_unlock(${first}, ${second})`;
|
|
81
|
+
},
|
|
82
|
+
),
|
|
19
83
|
},
|
|
20
|
-
|
|
84
|
+
table,
|
|
85
|
+
);
|
|
21
86
|
}
|
package/src/drivers/shared.ts
CHANGED
|
@@ -1,18 +1,72 @@
|
|
|
1
|
-
import { SQL } from "bun";
|
|
1
|
+
import { SQL, type ReservedSQL } from "bun";
|
|
2
2
|
import type { ExecutedMigration, MigrationDriver } from "../core/driver.js";
|
|
3
3
|
|
|
4
|
+
export interface SqlLock {
|
|
5
|
+
tryLock(timeoutSeconds: number): Promise<boolean>;
|
|
6
|
+
releaseLock(): Promise<void>;
|
|
7
|
+
dispose(): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
export interface SqlDialect {
|
|
5
11
|
install(db: SQL): Promise<void>;
|
|
6
12
|
record(db: SQL, migration: string, checksum: string): Promise<void>;
|
|
13
|
+
trackingTableExists?(db: SQL): Promise<boolean>;
|
|
14
|
+
createLock?(db: SQL): SqlLock;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createReservedLock(
|
|
18
|
+
db: SQL,
|
|
19
|
+
acquire: (lock: SQL) => Promise<boolean>,
|
|
20
|
+
release: (lock: SQL) => Promise<void>,
|
|
21
|
+
): SqlLock {
|
|
22
|
+
let lockConnection: ReservedSQL | null = null;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
async tryLock() {
|
|
26
|
+
const connection = await db.reserve();
|
|
27
|
+
lockConnection = connection;
|
|
28
|
+
try {
|
|
29
|
+
const acquired = await acquire(connection);
|
|
30
|
+
if (!acquired) {
|
|
31
|
+
connection.release();
|
|
32
|
+
lockConnection = null;
|
|
33
|
+
}
|
|
34
|
+
return acquired;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
connection.release();
|
|
37
|
+
lockConnection = null;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
async releaseLock() {
|
|
42
|
+
const connection = lockConnection;
|
|
43
|
+
if (!connection) return;
|
|
44
|
+
lockConnection = null;
|
|
45
|
+
try {
|
|
46
|
+
await release(connection);
|
|
47
|
+
} finally {
|
|
48
|
+
connection.release();
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
dispose() {
|
|
52
|
+
lockConnection?.release();
|
|
53
|
+
lockConnection = null;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
7
56
|
}
|
|
8
57
|
|
|
9
|
-
export function createSqlDriver(
|
|
58
|
+
export function createSqlDriver(
|
|
59
|
+
databaseUrl: string,
|
|
60
|
+
dialect: SqlDialect,
|
|
61
|
+
table: string,
|
|
62
|
+
): MigrationDriver {
|
|
10
63
|
const db = new SQL(databaseUrl);
|
|
64
|
+
const lock = dialect.createLock?.(db);
|
|
11
65
|
|
|
12
66
|
return {
|
|
13
67
|
install: () => dialect.install(db),
|
|
14
68
|
async listExecuted() {
|
|
15
|
-
const rows = await db`SELECT migration, checksum FROM
|
|
69
|
+
const rows = await db`SELECT migration, checksum FROM ${db.unsafe(table)} ORDER BY id ASC`;
|
|
16
70
|
return rows.map(
|
|
17
71
|
(r: { migration: string; checksum: string | null }): ExecutedMigration => ({
|
|
18
72
|
name: r.migration,
|
|
@@ -22,13 +76,23 @@ export function createSqlDriver(databaseUrl: string, dialect: SqlDialect): Migra
|
|
|
22
76
|
},
|
|
23
77
|
record: (migration, checksum) => dialect.record(db, migration, checksum),
|
|
24
78
|
async setChecksum(migration, checksum) {
|
|
25
|
-
await db`UPDATE
|
|
79
|
+
await db`UPDATE ${db.unsafe(table)} SET checksum = ${checksum} WHERE migration = ${migration}`;
|
|
26
80
|
},
|
|
27
81
|
async remove(migration) {
|
|
28
|
-
await db`DELETE FROM
|
|
82
|
+
await db`DELETE FROM ${db.unsafe(table)} WHERE migration = ${migration}`;
|
|
29
83
|
},
|
|
30
84
|
transaction: (run) => db.begin(run),
|
|
85
|
+
...(dialect.trackingTableExists
|
|
86
|
+
? { trackingTableExists: () => dialect.trackingTableExists!(db) }
|
|
87
|
+
: {}),
|
|
88
|
+
...(lock
|
|
89
|
+
? {
|
|
90
|
+
tryLock: (timeoutSeconds: number) => lock.tryLock(timeoutSeconds),
|
|
91
|
+
releaseLock: () => lock.releaseLock(),
|
|
92
|
+
}
|
|
93
|
+
: {}),
|
|
31
94
|
async close() {
|
|
95
|
+
lock?.dispose();
|
|
32
96
|
db.close({ timeout: 0 });
|
|
33
97
|
},
|
|
34
98
|
};
|
package/src/drivers/sqlite.ts
CHANGED
|
@@ -1,26 +1,63 @@
|
|
|
1
|
-
import type { MigrationDriver } from "../core/driver.js";
|
|
1
|
+
import type { DriverTableOptions, MigrationDriver } from "../core/driver.js";
|
|
2
|
+
import { doubleQuoted, validateIdentifier } from "../core/identifiers.js";
|
|
2
3
|
import { createSqlDriver } from "./shared.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
5
|
+
const TABLE_NAME_MAX_LENGTH = 128;
|
|
6
|
+
const UNIQUE_INDEX_SUFFIX = "_migration_unique";
|
|
7
|
+
|
|
8
|
+
function resolveTableRef(options: DriverTableOptions): {
|
|
9
|
+
table: string;
|
|
10
|
+
index: string;
|
|
11
|
+
name: string;
|
|
12
|
+
} {
|
|
13
|
+
const tableName = options.tableName ?? "migrations";
|
|
14
|
+
validateIdentifier("table", tableName, TABLE_NAME_MAX_LENGTH);
|
|
15
|
+
return {
|
|
16
|
+
table: doubleQuoted(tableName),
|
|
17
|
+
index: doubleQuoted(`${tableName}${UNIQUE_INDEX_SUFFIX}`),
|
|
18
|
+
name: tableName,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function create(databaseUrl: string, options: DriverTableOptions = {}): MigrationDriver {
|
|
23
|
+
const { table, index, name } = resolveTableRef(options);
|
|
24
|
+
return createSqlDriver(
|
|
25
|
+
databaseUrl,
|
|
26
|
+
{
|
|
27
|
+
async install(db) {
|
|
28
|
+
await db`CREATE TABLE IF NOT EXISTS ${db.unsafe(table)} (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
migration TEXT NOT NULL,
|
|
31
|
+
checksum TEXT
|
|
32
|
+
)`;
|
|
33
|
+
const columns = (await db.unsafe(`PRAGMA table_info(${table})`)) as Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
}>;
|
|
36
|
+
const hasChecksum = columns.some((column) => column.name === "checksum");
|
|
37
|
+
if (!hasChecksum) {
|
|
38
|
+
await db`ALTER TABLE ${db.unsafe(table)} ADD COLUMN checksum TEXT`;
|
|
39
|
+
}
|
|
40
|
+
await db`CREATE UNIQUE INDEX IF NOT EXISTS ${db.unsafe(index)} ON ${db.unsafe(table)} (migration)`;
|
|
41
|
+
},
|
|
42
|
+
async trackingTableExists(db) {
|
|
43
|
+
const rows = await db`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${name}`;
|
|
44
|
+
return rows.length > 0;
|
|
45
|
+
},
|
|
46
|
+
async record(db, migration, checksum) {
|
|
47
|
+
await db`INSERT OR IGNORE INTO ${db.unsafe(table)} (migration, checksum)
|
|
48
|
+
VALUES (${migration}, ${checksum})`;
|
|
49
|
+
},
|
|
50
|
+
createLock: (db) => ({
|
|
51
|
+
async tryLock(timeoutSeconds: number) {
|
|
52
|
+
await db.unsafe(`PRAGMA busy_timeout = ${timeoutSeconds * 1000}`);
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
async releaseLock() {
|
|
56
|
+
await db.unsafe("PRAGMA busy_timeout = 0");
|
|
57
|
+
},
|
|
58
|
+
dispose() {},
|
|
59
|
+
}),
|
|
24
60
|
},
|
|
25
|
-
|
|
61
|
+
table,
|
|
62
|
+
);
|
|
26
63
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,29 +1,55 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createDriver,
|
|
3
|
+
type DriverTableOptions,
|
|
4
|
+
type ExecutedMigration,
|
|
5
|
+
type MigrationDriver,
|
|
6
|
+
} from "./core/driver.js";
|
|
7
|
+
import { InvalidIdentifierError } from "./core/identifiers.js";
|
|
2
8
|
import { migrateUp } from "./api/up.js";
|
|
3
9
|
import { migrateDown } from "./api/down.js";
|
|
10
|
+
import { migrateStatus } from "./api/status.js";
|
|
4
11
|
import { installMigrations } from "./api/install.js";
|
|
5
12
|
import { createMigration } from "./api/create.js";
|
|
13
|
+
import { markMigrationsApplied } from "./api/mark.js";
|
|
6
14
|
import {
|
|
15
|
+
type MigrateDownOptions,
|
|
7
16
|
type MigrateDownResult,
|
|
17
|
+
type MarkOptions,
|
|
18
|
+
type MarkResult,
|
|
8
19
|
type MigrateOptions,
|
|
20
|
+
type MigrateStatusResult,
|
|
21
|
+
type MigrateUpOptions,
|
|
9
22
|
type MigrateUpResult,
|
|
10
23
|
ChecksumDriftError,
|
|
11
24
|
GitStageError,
|
|
25
|
+
MigrationLockError,
|
|
26
|
+
MigrationNotFoundError,
|
|
12
27
|
} from "./api/options.js";
|
|
13
28
|
|
|
14
29
|
export {
|
|
15
30
|
createDriver,
|
|
16
31
|
migrateUp,
|
|
17
32
|
migrateDown,
|
|
33
|
+
migrateStatus,
|
|
18
34
|
installMigrations,
|
|
19
35
|
createMigration,
|
|
36
|
+
markMigrationsApplied,
|
|
20
37
|
ChecksumDriftError,
|
|
21
38
|
GitStageError,
|
|
39
|
+
InvalidIdentifierError,
|
|
40
|
+
MigrationLockError,
|
|
41
|
+
MigrationNotFoundError,
|
|
22
42
|
};
|
|
23
43
|
export type {
|
|
44
|
+
DriverTableOptions,
|
|
24
45
|
ExecutedMigration,
|
|
25
46
|
MigrationDriver,
|
|
26
47
|
MigrateOptions,
|
|
48
|
+
MigrateUpOptions,
|
|
27
49
|
MigrateUpResult,
|
|
50
|
+
MigrateDownOptions,
|
|
28
51
|
MigrateDownResult,
|
|
52
|
+
MarkOptions,
|
|
53
|
+
MarkResult,
|
|
54
|
+
MigrateStatusResult,
|
|
29
55
|
};
|