kern-migrations 0.1.0 → 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/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +2 -2
- package/dist/{migrate-Bd9skEp-.mjs → migrate-CxWPJJmr.mjs} +29 -9
- package/dist/{target-Bxf6BZub.mjs → target-BHaRgZJG.mjs} +51 -24
- package/dist/{target-GAW46krF.d.mts → target-iyJah0hO.d.mts} +4 -4
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +1 -1
package/dist/cli.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as MigrationHistory, t as MigrationTarget } from "./target-
|
|
1
|
+
import { i as MigrationHistory, t as MigrationTarget } from "./target-iyJah0hO.mjs";
|
|
2
2
|
/** Options of the list module that the command `new` writes. */
|
|
3
3
|
interface MigrationListModuleOptions {
|
|
4
4
|
/** Path of the module, absolute or relative to the working directory, or its `file:` URL. */
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { d as moduleExtensions, i as isMigrationTargetDb, l as getMigrationNumber, s as compareMigrationNumbers, w as getErrorMessage } from "./target-
|
|
2
|
-
import { i as rollback, n as migrate, r as redo, t as getMigrationStatus } from "./migrate-
|
|
1
|
+
import { d as moduleExtensions, i as isMigrationTargetDb, l as getMigrationNumber, s as compareMigrationNumbers, w as getErrorMessage } from "./target-BHaRgZJG.mjs";
|
|
2
|
+
import { i as rollback, n as migrate, r as redo, t as getMigrationStatus } from "./migrate-CxWPJJmr.mjs";
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { parseArgs } from "node:util";
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as MigrationError, t as MigrationDirection } from "./error-BOZbdvKf.mjs";
|
|
2
|
-
import { a as MigrationHistoryOptions, c as Migration, d as MigrationStep, f as defineMigration, i as MigrationHistory, l as MigrationChange, n as MigrationTargetDb, o as MigrationLoader, r as MigrationTargetOptions, s as defineMigrations, t as MigrationTarget, u as MigrationDatabase } from "./target-
|
|
2
|
+
import { a as MigrationHistoryOptions, c as Migration, d as MigrationStep, f as defineMigration, i as MigrationHistory, l as MigrationChange, n as MigrationTargetDb, o as MigrationLoader, r as MigrationTargetOptions, s as defineMigrations, t as MigrationTarget, u as MigrationDatabase } from "./target-iyJah0hO.mjs";
|
|
3
3
|
/** Builds a step that adds a column of a SQL definition, such as `"text NOT NULL DEFAULT ''"`, to a table, and drops it on rollback. */
|
|
4
4
|
export declare function addColumn(table: string, column: string, definition: string): MigrationStep;
|
|
5
5
|
/**
|
|
@@ -149,6 +149,7 @@ interface MigrationStatus {
|
|
|
149
149
|
* Inside a transaction, it runs in a nested transaction, so that the migrations commit or roll back with the enclosing one, and every migration runs in it.
|
|
150
150
|
* A target that is up to date costs two statements outside a transaction; otherwise, it locks the target, so that processes that migrate it at once apply each migration once.
|
|
151
151
|
* It rejects when the target has applied a migration that the history lacks before its last migration, or a migration after a pending one; applied migrations after the last one, such as of a newer version of the application, are allowed.
|
|
152
|
+
* In a transaction at `repeatable read` or `serializable`, it rejects when it has migrations to apply, before it locks the target, since the snapshot of the transaction misses the migrations that another process applies meanwhile.
|
|
152
153
|
*/
|
|
153
154
|
export declare function migrate(target: MigrationTarget, history: MigrationHistory, options?: MigrateOptions): Promise<string[]>;
|
|
154
155
|
/**
|
|
@@ -156,6 +157,7 @@ export declare function migrate(target: MigrationTarget, history: MigrationHisto
|
|
|
156
157
|
*
|
|
157
158
|
* It rolls them back in one transaction, unless a migration runs outside a transaction, as `migrate` applies them.
|
|
158
159
|
* It rejects when the target has applied a migration that the history lacks, since it cannot roll it back.
|
|
160
|
+
* In a transaction at `repeatable read` or `serializable`, it rejects when the target has applied migrations, before it locks the target.
|
|
159
161
|
*/
|
|
160
162
|
export declare function rollback(target: MigrationTarget, history: MigrationHistory, options?: RollbackOptions): Promise<string[]>;
|
|
161
163
|
/**
|
|
@@ -163,6 +165,7 @@ export declare function rollback(target: MigrationTarget, history: MigrationHist
|
|
|
163
165
|
*
|
|
164
166
|
* It runs them in one transaction, unless a migration runs outside a transaction, as `migrate` applies them.
|
|
165
167
|
* It rejects when the target has applied a migration that the history lacks, since it cannot roll it back.
|
|
168
|
+
* In a transaction at `repeatable read` or `serializable`, it rejects when the target has applied migrations, before it locks the target.
|
|
166
169
|
*/
|
|
167
170
|
export declare function redo(target: MigrationTarget, history: MigrationHistory, options?: RedoOptions): Promise<string[]>;
|
|
168
171
|
/** Lists the applied and the pending migrations of `history` on `target`, and the applied migrations that the history lacks. */
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as MigrationError, S as quoteLiteral, b as quoteDollar, c as defineMigrations, f as defineMigration, g as runChange, m as findStepProblem, p as findSharedStepProblem, x as quoteIdentifier } from "./target-
|
|
2
|
-
import { i as rollback, n as migrate, r as redo, t as getMigrationStatus } from "./migrate-
|
|
1
|
+
import { C as MigrationError, S as quoteLiteral, b as quoteDollar, c as defineMigrations, f as defineMigration, g as runChange, m as findStepProblem, p as findSharedStepProblem, x as quoteIdentifier } from "./target-BHaRgZJG.mjs";
|
|
2
|
+
import { i as rollback, n as migrate, r as redo, t as getMigrationStatus } from "./migrate-CxWPJJmr.mjs";
|
|
3
3
|
// src/helpers/step.ts
|
|
4
4
|
/**
|
|
5
5
|
* Inverts a step: its `down` becomes `up`, and its `up` becomes `down`, also of its changes outside a transaction, such as to build a helper that drops what another helper creates.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as runMigrationSteps, a as openTarget, h as getOutsideTransactionChange, l as getMigrationNumber, n as deleteVersion, o as readVersions, r as insertVersion, t as createVersionsTable, v as runOutsideTransaction, y as toMigrationDatabase } from "./target-
|
|
1
|
+
import { _ as runMigrationSteps, a as openTarget, h as getOutsideTransactionChange, l as getMigrationNumber, n as deleteVersion, o as readVersions, r as insertVersion, t as createVersionsTable, v as runOutsideTransaction, y as toMigrationDatabase } from "./target-BHaRgZJG.mjs";
|
|
2
2
|
// src/migrate.ts
|
|
3
3
|
/**
|
|
4
4
|
* Applies the pending migrations of `history` to `target`, or the first `count` of them, and resolves to their keys.
|
|
@@ -7,12 +7,13 @@ import { _ as runMigrationSteps, a as openTarget, h as getOutsideTransactionChan
|
|
|
7
7
|
* Inside a transaction, it runs in a nested transaction, so that the migrations commit or roll back with the enclosing one, and every migration runs in it.
|
|
8
8
|
* A target that is up to date costs two statements outside a transaction; otherwise, it locks the target, so that processes that migrate it at once apply each migration once.
|
|
9
9
|
* It rejects when the target has applied a migration that the history lacks before its last migration, or a migration after a pending one; applied migrations after the last one, such as of a newer version of the application, are allowed.
|
|
10
|
+
* In a transaction at `repeatable read` or `serializable`, it rejects when it has migrations to apply, before it locks the target, since the snapshot of the transaction misses the migrations that another process applies meanwhile.
|
|
10
11
|
*/
|
|
11
12
|
async function migrate(target, history, options = {}) {
|
|
12
13
|
const count = options.count === void 0 ? Number.POSITIVE_INFINITY : getCount(options.count, false);
|
|
13
14
|
const opened = await openTarget(target, history, options);
|
|
14
15
|
if (getPendingKeys(history, opened.target, opened.versions).length === 0) return [];
|
|
15
|
-
return await runLocked(opened
|
|
16
|
+
return await runLocked(opened, history, "migrate", (versions) => {
|
|
16
17
|
const keys = getPendingKeys(history, opened.target, versions).slice(0, count);
|
|
17
18
|
return {
|
|
18
19
|
keys,
|
|
@@ -28,13 +29,14 @@ async function migrate(target, history, options = {}) {
|
|
|
28
29
|
*
|
|
29
30
|
* It rolls them back in one transaction, unless a migration runs outside a transaction, as `migrate` applies them.
|
|
30
31
|
* It rejects when the target has applied a migration that the history lacks, since it cannot roll it back.
|
|
32
|
+
* In a transaction at `repeatable read` or `serializable`, it rejects when the target has applied migrations, before it locks the target.
|
|
31
33
|
*/
|
|
32
34
|
async function rollback(target, history, options = {}) {
|
|
33
35
|
const count = getCount(options.count, true);
|
|
34
36
|
const opened = await openTarget(target, history, options);
|
|
35
37
|
checkAppliedKeys(history, opened.target, opened.versions, "roll back the migrations of");
|
|
36
38
|
if (opened.versions.applied.length === 0) return [];
|
|
37
|
-
return await runLocked(opened
|
|
39
|
+
return await runLocked(opened, history, "roll back the migrations of", (versions) => {
|
|
38
40
|
checkAppliedKeys(history, opened.target, versions, "roll back the migrations of");
|
|
39
41
|
const keys = getLastKeys(versions, count).reverse();
|
|
40
42
|
return {
|
|
@@ -51,13 +53,14 @@ async function rollback(target, history, options = {}) {
|
|
|
51
53
|
*
|
|
52
54
|
* It runs them in one transaction, unless a migration runs outside a transaction, as `migrate` applies them.
|
|
53
55
|
* It rejects when the target has applied a migration that the history lacks, since it cannot roll it back.
|
|
56
|
+
* In a transaction at `repeatable read` or `serializable`, it rejects when the target has applied migrations, before it locks the target.
|
|
54
57
|
*/
|
|
55
58
|
async function redo(target, history, options = {}) {
|
|
56
59
|
const count = getCount(options.count, false);
|
|
57
60
|
const opened = await openTarget(target, history, options);
|
|
58
61
|
checkAppliedKeys(history, opened.target, opened.versions, "redo the migrations of");
|
|
59
62
|
if (opened.versions.applied.length === 0) return [];
|
|
60
|
-
return await runLocked(opened
|
|
63
|
+
return await runLocked(opened, history, "redo the migrations of", (versions) => {
|
|
61
64
|
checkAppliedKeys(history, opened.target, versions, "redo the migrations of");
|
|
62
65
|
const keys = getLastKeys(versions, count);
|
|
63
66
|
const runs = [];
|
|
@@ -119,16 +122,19 @@ function checkAppliedKeys(history, target, versions, action) {
|
|
|
119
122
|
if (unknown !== void 0) throw new Error(`Cannot ${action} schema "${target.schemaName}": migration ${unknown} is applied, but the history lacks it`);
|
|
120
123
|
}
|
|
121
124
|
/**
|
|
122
|
-
* Locks `
|
|
125
|
+
* Locks the target of `opened`, reads its versions again, loads the migrations that `plan` chooses from them, runs them, and resolves to the keys of the plan.
|
|
123
126
|
*
|
|
124
|
-
* The lock belongs to the qualified name of the table of versions, so that different targets migrate at once.
|
|
127
|
+
* The lock belongs to the qualified name of the table of versions, so that different targets and histories of one target migrate at once.
|
|
125
128
|
* In a transaction block, such as of an enclosing transaction or a test, it runs every migration in a nested transaction that locks the target, with the search path of the migrations.
|
|
126
129
|
* Outside one, it runs them in a session with that search path, which polls for the lock, since a transaction that waits for it would keep a snapshot, which `CREATE INDEX CONCURRENTLY` of the session that holds the lock waits for.
|
|
127
130
|
* It decides which migrations run outside a transaction only from the steps that it loads under the lock, since another process may change the applied migrations before the lock.
|
|
131
|
+
* `action` names the call in its errors, such as `roll back the migrations of`.
|
|
128
132
|
*/
|
|
129
|
-
async function runLocked(
|
|
133
|
+
async function runLocked(opened, history, action, plan) {
|
|
134
|
+
const { target } = opened;
|
|
130
135
|
const { database } = target;
|
|
131
136
|
const inTransaction = database.isInTransactionBlock();
|
|
137
|
+
if (inTransaction && snapshotIsolationLevels.has(opened.isolation)) throw new Error(`Cannot ${action} schema "${target.schemaName}" in a transaction at isolation level ${opened.isolation}, whose snapshot misses the migrations that another process applies or rolls back meanwhile; run the call outside a transaction or in a transaction at read committed`);
|
|
132
138
|
const run = async () => {
|
|
133
139
|
await (inTransaction ? database.lock(target.table) : database.pollLock(target.table));
|
|
134
140
|
const versions = await readVersions(target);
|
|
@@ -140,6 +146,18 @@ async function runLocked(target, history, plan) {
|
|
|
140
146
|
const options = { searchPath: target.searchPath };
|
|
141
147
|
return inTransaction ? await database.transaction(run, options) : await database.session(run, options);
|
|
142
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Isolation levels whose transaction reads from one snapshot, which misses what other transactions commit after it.
|
|
151
|
+
*
|
|
152
|
+
* A call in a transaction block reads the versions under the lock from that snapshot, so it would apply again the migrations that another process has applied meanwhile, or roll back others than the last ones.
|
|
153
|
+
*/
|
|
154
|
+
const snapshotIsolationLevels = /* @__PURE__ */ new Set(["repeatable read", "serializable"]);
|
|
155
|
+
/**
|
|
156
|
+
* Options of the transactions of a call in a session, whatever the default isolation level of the database.
|
|
157
|
+
*
|
|
158
|
+
* Creating the target schema and `createExtension` check again under a lock what another transaction has committed meanwhile, which a transaction at `read committed` sees, since each statement takes a new snapshot there.
|
|
159
|
+
*/
|
|
160
|
+
const sessionTransactionOptions = { isolation: "read committed" };
|
|
143
161
|
/** Runs `runs` in order in the transaction of a call, creating the schema and the table of versions that `versions` lacks before them. */
|
|
144
162
|
async function runInTransaction(target, versions, runs) {
|
|
145
163
|
if (runs.length === 0) return;
|
|
@@ -149,6 +167,8 @@ async function runInTransaction(target, versions, runs) {
|
|
|
149
167
|
/**
|
|
150
168
|
* Runs `runs` in order in the session of a call: a migration that has a change of its direction outside a transaction runs it alone and is recorded after it in a statement of its own, and each sequence of other migrations between such migrations runs in one transaction.
|
|
151
169
|
*
|
|
170
|
+
* Before a first migration outside a transaction, it creates the schema and the table of versions in a transaction of their own.
|
|
171
|
+
*
|
|
152
172
|
* The migrations before a failure stay committed, and a migration whose change fails outside a transaction keeps its record, so that `migrate` runs its `up` again, and `rollback` its `down`.
|
|
153
173
|
*/
|
|
154
174
|
async function runInSession(target, versions, runs) {
|
|
@@ -157,7 +177,7 @@ async function runInSession(target, versions, runs) {
|
|
|
157
177
|
const run = runs[start];
|
|
158
178
|
const change = getOutsideTransactionChange(run.steps, run.direction);
|
|
159
179
|
if (change !== void 0) {
|
|
160
|
-
if (start === 0) await createVersionsTable(target, versions);
|
|
180
|
+
if (start === 0) await target.database.transaction(() => createVersionsTable(target, versions), sessionTransactionOptions);
|
|
161
181
|
await runOutsideTransaction(toMigrationDatabase(target.database), run.key, run.direction, change);
|
|
162
182
|
await recordRun(target, run);
|
|
163
183
|
start++;
|
|
@@ -170,7 +190,7 @@ async function runInSession(target, versions, runs) {
|
|
|
170
190
|
await target.database.transaction(async () => {
|
|
171
191
|
if (first) await createVersionsTable(target, versions);
|
|
172
192
|
await runMigrations(target, batch);
|
|
173
|
-
});
|
|
193
|
+
}, sessionTransactionOptions);
|
|
174
194
|
start = end;
|
|
175
195
|
}
|
|
176
196
|
}
|
|
@@ -108,10 +108,14 @@ async function runOutsideTransaction(database, key, direction, change) {
|
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
/**
|
|
112
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Returns the steps of migration `key`, rejecting a value that is not a migration.
|
|
113
|
+
*
|
|
114
|
+
* `requirement` completes the message that rejects a value that is neither a step nor a non-empty array of steps, such as `be a step or a non-empty array of steps`.
|
|
115
|
+
*/
|
|
116
|
+
function toMigrationSteps(key, value, requirement) {
|
|
113
117
|
const steps = Array.isArray(value) ? value : [value];
|
|
114
|
-
if (typeof value !== "object" || value === null || steps.length === 0) throw new MigrationError(`Migration ${key} must
|
|
118
|
+
if (typeof value !== "object" || value === null || steps.length === 0) throw new MigrationError(`Migration ${key} must ${requirement}`, { key });
|
|
115
119
|
for (let index = 0; index < steps.length; index++) {
|
|
116
120
|
const step = steps[index];
|
|
117
121
|
const problem = findStepProblem(step) ?? (steps.length > 1 ? findSharedStepProblem(step) : void 0);
|
|
@@ -198,14 +202,14 @@ function compareMigrationNumbers(left, right) {
|
|
|
198
202
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
199
203
|
}
|
|
200
204
|
/**
|
|
201
|
-
* Defines a history of migrations from the loaders of their modules, by key or by path
|
|
205
|
+
* Defines a history of migrations from the loaders of their modules or the migrations themselves, by key or by path as `import.meta.glob` returns them.
|
|
202
206
|
*
|
|
203
207
|
* A key is the file name without directories and extension, of the form `<number>_<name>`, such as `0001_create_form`, and the numbers order the history.
|
|
204
|
-
* It rejects a key of another form, a path of a file of migration tests,
|
|
208
|
+
* It rejects a key of another form, a path of a file of migration tests, two keys of one number, such as migrations of two branches that took one number, and a value that is neither a loader nor a migration.
|
|
205
209
|
*/
|
|
206
|
-
function defineMigrations(
|
|
210
|
+
function defineMigrations(migrations, options = {}) {
|
|
207
211
|
const entries = [];
|
|
208
|
-
for (const [name,
|
|
212
|
+
for (const [name, value] of Object.entries(migrations)) {
|
|
209
213
|
const isPath = name.includes("/");
|
|
210
214
|
if (isPath && getMigrationTestKey(name.slice(name.lastIndexOf("/") + 1)) !== void 0) throw new Error(`${name} is a file of migration tests rather than a migration: exclude migration tests from the modules of the history, such as with "!./migrations/*.migration-test.ts" in the patterns of import.meta.glob`);
|
|
211
215
|
const key = isPath ? getFileKey(name) : name;
|
|
@@ -214,7 +218,7 @@ function defineMigrations(loaders, options = {}) {
|
|
|
214
218
|
entries.push({
|
|
215
219
|
key,
|
|
216
220
|
number,
|
|
217
|
-
load
|
|
221
|
+
load: toEntryLoader(key, value)
|
|
218
222
|
});
|
|
219
223
|
}
|
|
220
224
|
entries.sort((left, right) => compareMigrationNumbers(left.number, right.number));
|
|
@@ -232,19 +236,29 @@ function defineMigrations(loaders, options = {}) {
|
|
|
232
236
|
async load(key) {
|
|
233
237
|
const entry = byKey.get(key);
|
|
234
238
|
if (entry === void 0) throw new Error(`The history has no migration ${key}`);
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
module = await entry.load();
|
|
238
|
-
} catch (error) {
|
|
239
|
-
throw new MigrationError(`Cannot load migration ${key}: ${getErrorMessage(error)}`, {
|
|
240
|
-
key,
|
|
241
|
-
cause: error
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
return toMigrationSteps(key, typeof module === "object" && module !== null ? module.default : void 0);
|
|
239
|
+
return await entry.load();
|
|
245
240
|
}
|
|
246
241
|
};
|
|
247
242
|
}
|
|
243
|
+
/** Returns the function that gives the steps of migration `key`: it checks a migration given as a value at once, and loads the module of a loader only when the function is called. */
|
|
244
|
+
function toEntryLoader(key, value) {
|
|
245
|
+
if (typeof value === "function") return () => loadModule(key, value);
|
|
246
|
+
const steps = toMigrationSteps(key, value, "be the loader of its module, a step, or a non-empty array of steps");
|
|
247
|
+
return async () => steps;
|
|
248
|
+
}
|
|
249
|
+
/** Loads the module of migration `key` with `loader` and returns the steps of its default export. */
|
|
250
|
+
async function loadModule(key, loader) {
|
|
251
|
+
let module;
|
|
252
|
+
try {
|
|
253
|
+
module = await loader();
|
|
254
|
+
} catch (error) {
|
|
255
|
+
throw new MigrationError(`Cannot load migration ${key}: ${getErrorMessage(error)}`, {
|
|
256
|
+
key,
|
|
257
|
+
cause: error
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return toMigrationSteps(key, typeof module === "object" && module !== null ? module.default : void 0, "export a step or a non-empty array of steps as its default export");
|
|
261
|
+
}
|
|
248
262
|
/** Returns the key of a migration module at `path`: its file name without directories and extension. */
|
|
249
263
|
function getFileKey(path) {
|
|
250
264
|
const fileName = path.slice(path.lastIndexOf("/") + 1);
|
|
@@ -274,7 +288,8 @@ async function openTarget(target, history, options) {
|
|
|
274
288
|
};
|
|
275
289
|
return {
|
|
276
290
|
target: resolved,
|
|
277
|
-
versions: await readApplied(resolved, state)
|
|
291
|
+
versions: await readApplied(resolved, state),
|
|
292
|
+
isolation: state.isolation
|
|
278
293
|
};
|
|
279
294
|
}
|
|
280
295
|
/** Reads the versions of a resolved target again, such as after locking it. */
|
|
@@ -288,9 +303,20 @@ async function readVersions(target) {
|
|
|
288
303
|
* Checking `versions` first keeps PostgreSQL from sending a notice of an existing schema, which postgres.js logs by default.
|
|
289
304
|
*/
|
|
290
305
|
async function createVersionsTable(target, versions) {
|
|
291
|
-
if (!versions.schemaExists) await execute(target.database,
|
|
306
|
+
if (!versions.schemaExists) await execute(target.database, buildCreateSchema(target.schemaName));
|
|
292
307
|
if (!versions.tableExists) await execute(target.database, `CREATE TABLE IF NOT EXISTS ${target.table} (key text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`);
|
|
293
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Builds a statement that creates schema `schemaName` unless it exists, checking it again under a transaction-level advisory lock of the schema.
|
|
311
|
+
*
|
|
312
|
+
* Histories of one target do not share the lock of the target, and `IF NOT EXISTS` misses a schema that another transaction has created and not committed, so without this lock a history that creates the schema while another one does fails on the unique index of `pg_namespace` once the other commits.
|
|
313
|
+
* The lock lasts until the transaction of the statement ends, and the check under it sees a schema that another history committed meanwhile under `READ COMMITTED`, since each statement of the block takes a new snapshot.
|
|
314
|
+
* The key of the lock includes the schema, so that targets of different schemas, such as new tenants, do not wait for each other.
|
|
315
|
+
*/
|
|
316
|
+
function buildCreateSchema(schemaName) {
|
|
317
|
+
const name = quoteLiteral(schemaName);
|
|
318
|
+
return `DO ${quoteDollar(`BEGIN PERFORM pg_advisory_xact_lock(hashtextextended(${quoteLiteral(`kern-migrations schema ${schemaName}`)}, 0)); IF NOT EXISTS (SELECT FROM pg_catalog.pg_namespace WHERE nspname = ${name}) THEN CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier(schemaName)}; END IF; END`)}`;
|
|
319
|
+
}
|
|
294
320
|
/** Records migration `key` as applied. */
|
|
295
321
|
async function insertVersion(target, key) {
|
|
296
322
|
await execute(target.database, `INSERT INTO ${target.table} (key) VALUES ($1)`, [key]);
|
|
@@ -300,14 +326,14 @@ async function deleteVersion(target, key) {
|
|
|
300
326
|
await execute(target.database, `DELETE FROM ${target.table} WHERE key = $1`, [key]);
|
|
301
327
|
}
|
|
302
328
|
/**
|
|
303
|
-
* Resolves `schemaName`, or `current_schema()` when it is `null`,
|
|
329
|
+
* Resolves `schemaName`, or `current_schema()` when it is `null`, checks that the schema and its table of versions exist, and reads the isolation level of the transaction, in one statement.
|
|
304
330
|
*
|
|
305
331
|
* It reads `pg_catalog` rather than calling `to_regclass`, whose lookups in the catalog cache of the connection miss a schema that another transaction committed while this one waited for the lock of the target.
|
|
306
332
|
* It rejects a schema or table name longer than `max_identifier_length` bytes, which PostgreSQL would truncate, so that the check never misses a schema or a table that the migrator created.
|
|
307
333
|
*/
|
|
308
334
|
async function readState(database, schemaName, tableName) {
|
|
309
|
-
const { rows } = await execute(database, `SELECT target.name, namespace.oid IS NOT NULL, EXISTS (SELECT FROM pg_catalog.pg_class WHERE relnamespace = namespace.oid AND relname = $2::text), current_setting('max_identifier_length'), octet_length(target.name), octet_length($2::text) FROM (SELECT coalesce($1::text, current_schema()) AS name) AS target LEFT JOIN pg_catalog.pg_namespace AS namespace ON namespace.nspname = target.name`, [schemaName, tableName]);
|
|
310
|
-
const [name, schemaExists, tableExists, maxLength, schemaLength, tableLength] = rows[0];
|
|
335
|
+
const { rows } = await execute(database, `SELECT target.name, namespace.oid IS NOT NULL, EXISTS (SELECT FROM pg_catalog.pg_class WHERE relnamespace = namespace.oid AND relname = $2::text), current_setting('max_identifier_length'), octet_length(target.name), octet_length($2::text), current_setting('transaction_isolation') FROM (SELECT coalesce($1::text, current_schema()) AS name) AS target LEFT JOIN pg_catalog.pg_namespace AS namespace ON namespace.nspname = target.name`, [schemaName, tableName]);
|
|
336
|
+
const [name, schemaExists, tableExists, maxLength, schemaLength, tableLength, isolation] = rows[0];
|
|
311
337
|
if (name === null || name === void 0) throw new Error("Cannot resolve the target schema: current_schema() is NULL, since no schema of the search path exists; pass schemaName");
|
|
312
338
|
const limit = Number(maxLength);
|
|
313
339
|
for (const [kind, value, length] of [[
|
|
@@ -322,7 +348,8 @@ async function readState(database, schemaName, tableName) {
|
|
|
322
348
|
return {
|
|
323
349
|
schemaName: name,
|
|
324
350
|
schemaExists: schemaExists === "t",
|
|
325
|
-
tableExists: tableExists === "t"
|
|
351
|
+
tableExists: tableExists === "t",
|
|
352
|
+
isolation
|
|
326
353
|
};
|
|
327
354
|
}
|
|
328
355
|
/** Reads the applied keys of a target whose table of versions `state` found. */
|
|
@@ -51,19 +51,19 @@ interface MigrationHistory {
|
|
|
51
51
|
/** PostgreSQL schemas that the search path of the migrations lists after the target schema. */
|
|
52
52
|
readonly searchPath: readonly string[];
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
54
|
+
* Returns the steps of the migration of `key`, loading its module when the history has its loader.
|
|
55
55
|
*
|
|
56
56
|
* It rejects with a `MigrationError` when the module fails to load or does not export a migration.
|
|
57
57
|
*/
|
|
58
58
|
load(key: string): Promise<readonly MigrationStep[]>;
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* Defines a history of migrations from the loaders of their modules, by key or by path
|
|
61
|
+
* Defines a history of migrations from the loaders of their modules or the migrations themselves, by key or by path as `import.meta.glob` returns them.
|
|
62
62
|
*
|
|
63
63
|
* A key is the file name without directories and extension, of the form `<number>_<name>`, such as `0001_create_form`, and the numbers order the history.
|
|
64
|
-
* It rejects a key of another form, a path of a file of migration tests,
|
|
64
|
+
* It rejects a key of another form, a path of a file of migration tests, two keys of one number, such as migrations of two branches that took one number, and a value that is neither a loader nor a migration.
|
|
65
65
|
*/
|
|
66
|
-
declare function defineMigrations(
|
|
66
|
+
declare function defineMigrations(migrations: Readonly<Record<string, MigrationLoader | Migration>>, options?: MigrationHistoryOptions): MigrationHistory;
|
|
67
67
|
/**
|
|
68
68
|
* Target of migrations: a database and a PostgreSQL schema, the target schema.
|
|
69
69
|
*
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as MigrationError } from "./error-BOZbdvKf.mjs";
|
|
2
|
-
import { i as MigrationHistory, t as MigrationTarget, u as MigrationDatabase } from "./target-
|
|
2
|
+
import { i as MigrationHistory, t as MigrationTarget, u as MigrationDatabase } from "./target-iyJah0hO.mjs";
|
|
3
3
|
/** Callback of a migration test that prepares or checks the data of the schema through the database of the migration. */
|
|
4
4
|
type MigrationTestCallback = (database: MigrationDatabase) => unknown;
|
|
5
5
|
/** Type that rejects `assertUpError` of a migration test together with `assertUp` or `assertDown`. */
|
package/dist/testing.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as MigrationError, _ as runMigrationSteps, i as isMigrationTargetDb, u as getMigrationTestKey, w as getErrorMessage, x as quoteIdentifier, y as toMigrationDatabase } from "./target-
|
|
1
|
+
import { C as MigrationError, _ as runMigrationSteps, i as isMigrationTargetDb, u as getMigrationTestKey, w as getErrorMessage, x as quoteIdentifier, y as toMigrationDatabase } from "./target-BHaRgZJG.mjs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
4
|
import { readdir } from "node:fs/promises";
|