@softure-ai/db 0.1.5 → 0.1.7
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/CHANGELOG.md +25 -0
- package/README.md +261 -22
- package/dist/cli/run.d.ts +8 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +71 -21
- package/dist/cli/run.js.map +1 -1
- package/dist/client.d.ts +39 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +64 -8
- package/dist/client.js.map +1 -1
- package/dist/configured.d.ts +31 -0
- package/dist/configured.d.ts.map +1 -0
- package/dist/configured.js +74 -0
- package/dist/configured.js.map +1 -0
- package/dist/driver-errors.d.ts +21 -0
- package/dist/driver-errors.d.ts.map +1 -0
- package/dist/driver-errors.js +25 -0
- package/dist/driver-errors.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/migrations/adopt.d.ts +8 -1
- package/dist/migrations/adopt.d.ts.map +1 -1
- package/dist/migrations/adopt.js +53 -87
- package/dist/migrations/adopt.js.map +1 -1
- package/dist/migrations/apply.d.ts +35 -0
- package/dist/migrations/apply.d.ts.map +1 -0
- package/dist/migrations/apply.js +115 -0
- package/dist/migrations/apply.js.map +1 -0
- package/dist/migrations/migrator.d.ts +55 -27
- package/dist/migrations/migrator.d.ts.map +1 -1
- package/dist/migrations/migrator.js +187 -103
- package/dist/migrations/migrator.js.map +1 -1
- package/dist/migrations/problems.d.ts +9 -0
- package/dist/migrations/problems.d.ts.map +1 -1
- package/dist/migrations/problems.js +9 -3
- package/dist/migrations/problems.js.map +1 -1
- package/dist/migrations/reference.d.ts +23 -0
- package/dist/migrations/reference.d.ts.map +1 -0
- package/dist/migrations/reference.js +64 -0
- package/dist/migrations/reference.js.map +1 -0
- package/dist/migrations/session.d.ts +2 -1
- package/dist/migrations/session.d.ts.map +1 -1
- package/dist/migrations/session.js +19 -2
- package/dist/migrations/session.js.map +1 -1
- package/dist/migrations/stubs.d.ts +23 -0
- package/dist/migrations/stubs.d.ts.map +1 -0
- package/dist/migrations/stubs.js +99 -0
- package/dist/migrations/stubs.js.map +1 -0
- package/dist/process.d.ts +23 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +82 -0
- package/dist/process.js.map +1 -0
- package/dist/shared.d.ts +2 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +9 -0
- package/dist/shared.js.map +1 -1
- package/dist/testing.d.ts +11 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +28 -9
- package/dist/testing.js.map +1 -1
- package/package.json +22 -5
- package/src/cli/run.ts +84 -23
- package/src/client.ts +88 -16
- package/src/configured.ts +109 -0
- package/src/driver-errors.ts +40 -0
- package/src/index.ts +12 -1
- package/src/migrations/adopt.ts +65 -100
- package/src/migrations/apply.ts +135 -0
- package/src/migrations/migrator.ts +246 -128
- package/src/migrations/problems.ts +13 -5
- package/src/migrations/reference.ts +78 -0
- package/src/migrations/session.ts +28 -2
- package/src/migrations/stubs.ts +139 -0
- package/src/process.ts +110 -0
- package/src/shared.ts +9 -0
- package/src/testing.ts +38 -10
package/src/migrations/adopt.ts
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
// Adoption (docs/02-module-standard.md §4, docs/05-adoption-playbook.md step 3): an existing app
|
|
2
2
|
// has moved its own tables into a module's schema with its own migration. `adoptModule` checks
|
|
3
|
-
// that the schema now equals what the module's
|
|
4
|
-
//
|
|
5
|
-
// migrated when it is not is the one mistake a later
|
|
3
|
+
// that the schema now equals what the module's files 1..through would create, then records those
|
|
4
|
+
// files as `adopted` without running them; the later files stay pending for `migrate`. Any
|
|
5
|
+
// difference refuses: marking a schema as migrated when it is not is the one mistake a later
|
|
6
|
+
// migration cannot recover from. The pending files of the modules it depends on run first.
|
|
6
7
|
import { ok, type AnySoftureModule } from "@softure-ai/core";
|
|
7
|
-
import {
|
|
8
|
+
import type { DatabaseHandle } from "../client.js";
|
|
9
|
+
import { applyFile, recordAdoption, withMigrationLock, type MigrationUnit } from "./apply.js";
|
|
8
10
|
import { describeSchema, diffSchemas } from "./introspect.js";
|
|
9
|
-
import { LEDGER_MODULE_ID, readJournal,
|
|
10
|
-
import {
|
|
11
|
-
applyFile,
|
|
12
|
-
compareJournal,
|
|
13
|
-
migrate,
|
|
14
|
-
prepareUnits,
|
|
15
|
-
rollBack,
|
|
16
|
-
withMigrationLock,
|
|
17
|
-
type MigrationStep,
|
|
18
|
-
type MigrationUnit,
|
|
19
|
-
} from "./migrator.js";
|
|
11
|
+
import { LEDGER_MODULE_ID, readJournal, type JournalRow } from "./ledger.js";
|
|
12
|
+
import { checkThrough, compareJournal, prepareUnits, toStep, type MigrationStep } from "./migrator.js";
|
|
20
13
|
import { failWith, type MigrationProblem, type MigrationResult } from "./problems.js";
|
|
14
|
+
import { buildReferenceSchema, collectWithDependencies, listOwnedSchemas } from "./reference.js";
|
|
21
15
|
import { withSession, type MigrationSession } from "./session.js";
|
|
16
|
+
import { readAppTables } from "./stubs.js";
|
|
22
17
|
|
|
23
18
|
export interface AdoptOptions {
|
|
24
19
|
/** The enabled modules, as for `migrate`. */
|
|
@@ -27,6 +22,11 @@ export interface AdoptOptions {
|
|
|
27
22
|
readonly module: string;
|
|
28
23
|
/** Its version; must equal the enabled module's manifest version. */
|
|
29
24
|
readonly version: string;
|
|
25
|
+
/**
|
|
26
|
+
* The last file to adopt (1..n); default every file. The later files stay pending and the next
|
|
27
|
+
* `migrate` applies them.
|
|
28
|
+
*/
|
|
29
|
+
readonly through?: number;
|
|
30
30
|
/** Compare and report only; write nothing (also no ledger). */
|
|
31
31
|
readonly dryRun?: boolean;
|
|
32
32
|
readonly migrationsDir?: URL;
|
|
@@ -38,6 +38,8 @@ export interface AdoptionReport {
|
|
|
38
38
|
readonly dryRun: boolean;
|
|
39
39
|
/** Ledger migrations applied first (or that would be, on a dry run). */
|
|
40
40
|
readonly ledger: readonly MigrationStep[];
|
|
41
|
+
/** Pending files of the modules it depends on, applied before the comparison (or that would be). */
|
|
42
|
+
readonly dependencies: readonly MigrationStep[];
|
|
41
43
|
/** The module's migrations recorded as adopted (or that would be). */
|
|
42
44
|
readonly adopted: readonly MigrationStep[];
|
|
43
45
|
}
|
|
@@ -58,114 +60,77 @@ export async function adoptModule(handle: DatabaseHandle, options: AdoptOptions)
|
|
|
58
60
|
if (unit === undefined) {
|
|
59
61
|
return failWith([{ code: "db.adopt_no_schema", module: target.id }]);
|
|
60
62
|
}
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
return
|
|
63
|
+
const through = options.through ?? unit.files.length;
|
|
64
|
+
const throughProblems = checkThrough(unit, through);
|
|
65
|
+
if (throughProblems.length > 0) {
|
|
66
|
+
return failWith(throughProblems);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
return withSession(handle, (session) =>
|
|
68
70
|
withMigrationLock(session, async () => {
|
|
69
71
|
const journal = await readJournal(session);
|
|
70
72
|
const comparison = compareJournal(units.value, journal);
|
|
71
|
-
const problems = [...comparison.problems, ...checkAdoptable(
|
|
73
|
+
const problems = [...comparison.problems, ...checkAdoptable(unit, journal)];
|
|
72
74
|
if (problems.length > 0) {
|
|
73
75
|
return failWith(problems);
|
|
74
76
|
}
|
|
77
|
+
const dependencyIds = collectWithDependencies(options.modules, target.id);
|
|
78
|
+
dependencyIds.delete(target.id);
|
|
79
|
+
const ledger = comparison.pending.filter((step) => step.module === LEDGER_MODULE_ID);
|
|
80
|
+
const dependencies = comparison.pending.filter((step) => dependencyIds.has(step.module));
|
|
81
|
+
const files = unit.files.filter((file) => file.version <= through);
|
|
82
|
+
const adopted = files.map((file) => toStep(unit, file));
|
|
83
|
+
const report = { module: unit.module, version: options.version, dryRun: options.dryRun === true, ledger, dependencies, adopted };
|
|
84
|
+
|
|
85
|
+
// Built from the live database's app tables (module SQL may reference them, issue #170), so
|
|
86
|
+
// inside the lock and only once the checks above passed.
|
|
87
|
+
const appTables = await readAppTables(session, listOwnedSchemas(units.value));
|
|
88
|
+
const reference = await buildReferenceSchema({ modules: options.modules, units: units.value, target: unit, through, appTables });
|
|
89
|
+
if (!reference.ok) {
|
|
90
|
+
return reference;
|
|
91
|
+
}
|
|
92
|
+
// The target's own schema only, compared before anything is written: applying a dependency
|
|
93
|
+
// never changes it.
|
|
75
94
|
const differences = diffSchemas(reference.value, await describeSchema(session, unit.schema));
|
|
76
95
|
if (differences.length > 0) {
|
|
77
96
|
return failWith([{ code: "db.schema_mismatch", module: unit.module, schema: unit.schema, differences }]);
|
|
78
97
|
}
|
|
79
|
-
|
|
80
|
-
const ledger = comparison.pending.filter((step) => step.module === LEDGER_MODULE_ID);
|
|
81
|
-
const adopted = unit.files.map((file) => ({ module: unit.module, schema: unit.schema, version: file.version, name: file.name, checksum: file.checksum }));
|
|
82
|
-
const report = { module: unit.module, version: options.version, dryRun: options.dryRun === true, ledger, adopted };
|
|
83
98
|
if (options.dryRun === true) {
|
|
84
99
|
return ok(report);
|
|
85
100
|
}
|
|
86
|
-
const
|
|
101
|
+
const ledgerFailure = await applyPending(session, units.value, ledger);
|
|
102
|
+
if (ledgerFailure !== null) {
|
|
103
|
+
return failWith([ledgerFailure.problem]);
|
|
104
|
+
}
|
|
105
|
+
const dependencyFailure = await applyPending(session, units.value, dependencies);
|
|
106
|
+
if (dependencyFailure !== null) {
|
|
107
|
+
return failWith([dependencyFailure.problem, { code: "db.adopt_dependency_pending", module: unit.module, dependency: dependencyFailure.step.module }]);
|
|
108
|
+
}
|
|
109
|
+
const failure = await recordAdoption(session, unit, files);
|
|
87
110
|
return failure === null ? ok(report) : failWith([failure]);
|
|
88
111
|
}),
|
|
89
112
|
);
|
|
90
113
|
}
|
|
91
114
|
|
|
92
|
-
/** The adopted module must be new to the ledger
|
|
93
|
-
function checkAdoptable(
|
|
94
|
-
|
|
95
|
-
unit: MigrationUnit,
|
|
96
|
-
journal: readonly JournalRow[],
|
|
97
|
-
pending: readonly MigrationStep[],
|
|
98
|
-
): MigrationProblem[] {
|
|
99
|
-
if (journal.some((row) => row.module === unit.module)) {
|
|
100
|
-
return [{ code: "db.adopt_already_applied", module: unit.module }];
|
|
101
|
-
}
|
|
102
|
-
return Object.keys(target.manifest.dependsOn)
|
|
103
|
-
.filter((dependency) => pending.some((step) => step.module === dependency))
|
|
104
|
-
.map((dependency) => ({ code: "db.adopt_dependency_pending", module: unit.module, dependency }));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* The schema the module's migrations create: they run on a scratch PGlite together with the
|
|
109
|
-
* enabled modules the target depends on (directly or not), because its SQL may reference them.
|
|
110
|
-
*/
|
|
111
|
-
async function describeReference(options: AdoptOptions, target: AnySoftureModule): Promise<MigrationResult<string[]>> {
|
|
112
|
-
const modules = collectWithDependencies(options.modules, target);
|
|
113
|
-
const scratch = await createDatabase("pglite://");
|
|
114
|
-
try {
|
|
115
|
-
const result = await migrate(scratch, { modules, ...(options.migrationsDir ? { migrationsDir: options.migrationsDir } : {}) });
|
|
116
|
-
if (!result.ok) {
|
|
117
|
-
const reason = result.problems.map((problem) => problem.code === "db.migration_failed" ? `${problem.module} ${problem.name}: ${problem.reason}` : problem.code).join("; ");
|
|
118
|
-
return failWith([{ code: "db.adopt_reference_failed", module: target.id, reason }]);
|
|
119
|
-
}
|
|
120
|
-
const schema = target.manifest.dbSchema ?? "";
|
|
121
|
-
return ok(await withSession(scratch, (session) => describeSchema(session, schema)));
|
|
122
|
-
} finally {
|
|
123
|
-
await scratch.close();
|
|
124
|
-
}
|
|
115
|
+
/** The adopted module must be new to the ledger. */
|
|
116
|
+
function checkAdoptable(unit: MigrationUnit, journal: readonly JournalRow[]): MigrationProblem[] {
|
|
117
|
+
return journal.some((row) => row.module === unit.module) ? [{ code: "db.adopt_already_applied", module: unit.module }] : [];
|
|
125
118
|
}
|
|
126
119
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
visit(target);
|
|
139
|
-
return [...collected.values()];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Applies a pending ledger migration, then records every module file as adopted in one transaction. */
|
|
143
|
-
async function writeAdoption(session: MigrationSession, units: readonly MigrationUnit[], unit: MigrationUnit): Promise<MigrationProblem | null> {
|
|
144
|
-
const ledger = units.find((candidate) => candidate.module === LEDGER_MODULE_ID);
|
|
145
|
-
const journal = await readJournal(session);
|
|
146
|
-
for (const file of ledger?.files ?? []) {
|
|
147
|
-
if (ledger !== undefined && !journal.some((row) => row.module === LEDGER_MODULE_ID && row.version === file.version)) {
|
|
148
|
-
const failure = await applyFile(session, { unit: ledger, file, method: "applied" });
|
|
149
|
-
if (failure !== null) return failure;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
await session.exec("BEGIN");
|
|
154
|
-
for (const file of unit.files) {
|
|
155
|
-
await recordMigration(session, {
|
|
156
|
-
module: unit.module,
|
|
157
|
-
version: file.version,
|
|
158
|
-
name: file.name,
|
|
159
|
-
checksum: file.checksum,
|
|
160
|
-
moduleVersion: unit.moduleVersion,
|
|
161
|
-
method: "adopted",
|
|
162
|
-
});
|
|
120
|
+
/** Applies the steps in order, each file in its own transaction; stops at the first failure. */
|
|
121
|
+
async function applyPending(
|
|
122
|
+
session: MigrationSession,
|
|
123
|
+
units: readonly MigrationUnit[],
|
|
124
|
+
steps: readonly MigrationStep[],
|
|
125
|
+
): Promise<{ problem: MigrationProblem; step: MigrationStep } | null> {
|
|
126
|
+
for (const step of steps) {
|
|
127
|
+
const unit = units.find((candidate) => candidate.module === step.module);
|
|
128
|
+
const file = unit?.files.find((candidate) => candidate.version === step.version);
|
|
129
|
+
if (unit === undefined || file === undefined) {
|
|
130
|
+
throw new Error(`adoptModule: step ${step.module} ${step.version} has no file; compareJournal is broken`);
|
|
163
131
|
}
|
|
164
|
-
await session
|
|
165
|
-
return
|
|
166
|
-
} catch (error) {
|
|
167
|
-
const reason = await rollBack(session, error);
|
|
168
|
-
const first = unit.files[0];
|
|
169
|
-
return { code: "db.migration_failed", module: unit.module, version: first?.version ?? 0, name: first?.name ?? "", reason };
|
|
132
|
+
const failure = await applyFile(session, { unit, file, method: "applied" });
|
|
133
|
+
if (failure !== null) return { problem: failure, step };
|
|
170
134
|
}
|
|
135
|
+
return null;
|
|
171
136
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// The primitives every migration path shares (migrate, adoption, the reference schema): one file in
|
|
2
|
+
// one transaction with its ledger row, adoption rows in one transaction, the advisory lock.
|
|
3
|
+
import type { MigrationFile } from "./files.js";
|
|
4
|
+
import { recordMigration, type MigrationMethod } from "./ledger.js";
|
|
5
|
+
import type { MigrationProblem } from "./problems.js";
|
|
6
|
+
import type { MigrationSession } from "./session.js";
|
|
7
|
+
|
|
8
|
+
/** One migration owner: the ledger itself or an enabled module with a schema. */
|
|
9
|
+
export interface MigrationUnit {
|
|
10
|
+
readonly module: string;
|
|
11
|
+
readonly schema: string;
|
|
12
|
+
readonly moduleVersion: string;
|
|
13
|
+
readonly files: readonly MigrationFile[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Any constant works; it only has to be the same for every runner of every app.
|
|
17
|
+
const MIGRATION_LOCK_KEY = "73012026";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Runs one file and its ledger row in one transaction, inside the unit's schema. Returns the
|
|
21
|
+
* problem when the SQL fails; the transaction is rolled back first.
|
|
22
|
+
*/
|
|
23
|
+
export async function applyFile(
|
|
24
|
+
session: MigrationSession,
|
|
25
|
+
input: { unit: MigrationUnit; file: MigrationFile; method: MigrationMethod },
|
|
26
|
+
): Promise<MigrationProblem | null> {
|
|
27
|
+
const { unit, file, method } = input;
|
|
28
|
+
// The schema passed the manifest's lower_snake_case rule and checkModule; quoting is a second line.
|
|
29
|
+
const schema = quoteIdentifier(unit.schema);
|
|
30
|
+
try {
|
|
31
|
+
await session.exec(`BEGIN; CREATE SCHEMA IF NOT EXISTS ${schema}; SET LOCAL search_path TO ${schema}, public;`);
|
|
32
|
+
const transactionId = await readTransactionId(session);
|
|
33
|
+
if (method === "applied") {
|
|
34
|
+
await session.exec(file.sql);
|
|
35
|
+
}
|
|
36
|
+
// Second line behind findTransactionControl: a file that still ended the transaction left
|
|
37
|
+
// part of itself committed, so it must not be reported as cleanly rolled back.
|
|
38
|
+
if ((await readTransactionId(session)) !== transactionId) {
|
|
39
|
+
throw new TransactionEndedError();
|
|
40
|
+
}
|
|
41
|
+
await recordMigration(session, {
|
|
42
|
+
module: unit.module,
|
|
43
|
+
version: file.version,
|
|
44
|
+
name: file.name,
|
|
45
|
+
checksum: file.checksum,
|
|
46
|
+
moduleVersion: unit.moduleVersion,
|
|
47
|
+
method,
|
|
48
|
+
});
|
|
49
|
+
await session.exec("COMMIT");
|
|
50
|
+
return null;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const reason = await rollBack(session, error);
|
|
53
|
+
return { code: "db.migration_failed", module: unit.module, version: file.version, name: file.name, reason };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class TransactionEndedError extends Error {
|
|
58
|
+
constructor() {
|
|
59
|
+
super("the file ended the migrator's transaction, so the statements before that point may be committed; check the schema by hand");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The transaction id, not now(): two transactions can share a start time (PGlite's clock ticks in
|
|
64
|
+
// milliseconds), but never an id.
|
|
65
|
+
async function readTransactionId(session: MigrationSession): Promise<string> {
|
|
66
|
+
const [row] = await session.query<{ id: string }>("SELECT pg_current_xact_id()::text AS id");
|
|
67
|
+
return row?.id ?? "";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Rolls the open transaction back and returns the reason to report. A failing ROLLBACK (a lost
|
|
72
|
+
* connection) is added to the reason instead of hiding the original error.
|
|
73
|
+
*/
|
|
74
|
+
export async function rollBack(session: MigrationSession, error: unknown): Promise<string> {
|
|
75
|
+
const reason = describeError(error);
|
|
76
|
+
try {
|
|
77
|
+
await session.exec("ROLLBACK");
|
|
78
|
+
return reason;
|
|
79
|
+
} catch (rollbackError) {
|
|
80
|
+
return `${reason} (and ROLLBACK failed: ${describeError(rollbackError)})`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Holds the session-level advisory lock while `run` runs; a second runner waits for it. */
|
|
85
|
+
export async function withMigrationLock<T>(session: MigrationSession, run: () => Promise<T>): Promise<T> {
|
|
86
|
+
await session.query("SELECT pg_advisory_lock($1)", [MIGRATION_LOCK_KEY]);
|
|
87
|
+
let result: T;
|
|
88
|
+
try {
|
|
89
|
+
result = await run();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// The run's own error wins; an unlock failure here is secondary (the session is discarded).
|
|
92
|
+
await session.query("SELECT pg_advisory_unlock($1)", [MIGRATION_LOCK_KEY]).catch((unlockError: unknown) => {
|
|
93
|
+
throw new Error(`migration run failed (${describeError(error)}) and releasing the lock failed: ${describeError(unlockError)}`, {
|
|
94
|
+
cause: error,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
await session.query("SELECT pg_advisory_unlock($1)", [MIGRATION_LOCK_KEY]);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Records `files` of the unit as adopted, all in one transaction: either every row is written or
|
|
105
|
+
* none. Returns the problem when a write fails.
|
|
106
|
+
*/
|
|
107
|
+
export async function recordAdoption(session: MigrationSession, unit: MigrationUnit, files: readonly MigrationFile[]): Promise<MigrationProblem | null> {
|
|
108
|
+
try {
|
|
109
|
+
await session.exec("BEGIN");
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
await recordMigration(session, {
|
|
112
|
+
module: unit.module,
|
|
113
|
+
version: file.version,
|
|
114
|
+
name: file.name,
|
|
115
|
+
checksum: file.checksum,
|
|
116
|
+
moduleVersion: unit.moduleVersion,
|
|
117
|
+
method: "adopted",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
await session.exec("COMMIT");
|
|
121
|
+
return null;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const reason = await rollBack(session, error);
|
|
124
|
+
const first = files[0];
|
|
125
|
+
return { code: "db.migration_failed", module: unit.module, version: first?.version ?? 0, name: first?.name ?? "", reason };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function quoteIdentifier(name: string): string {
|
|
130
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function describeError(error: unknown): string {
|
|
134
|
+
return error instanceof Error ? error.message : String(error);
|
|
135
|
+
}
|