@onreza/sqlx-js 0.4.0 → 0.6.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 +139 -48
- package/ROADMAP.md +1 -3
- package/dist/bin/sqlx-js-diagnostics.d.ts +2 -0
- package/dist/bin/sqlx-js-diagnostics.js +96 -0
- package/dist/bin/sqlx-js.js +337 -89
- package/dist/src/artifacts.d.ts +9 -0
- package/dist/src/artifacts.js +26 -0
- package/dist/src/cache.d.ts +17 -0
- package/dist/src/cache.js +83 -1
- package/dist/src/codegen.js +14 -2
- package/dist/src/commands/doctor.d.ts +16 -0
- package/dist/src/commands/doctor.js +196 -0
- package/dist/src/commands/init.js +93 -13
- package/dist/src/commands/migrate.d.ts +2 -101
- package/dist/src/commands/migrate.js +11 -566
- package/dist/src/commands/pgschema.d.ts +6 -0
- package/dist/src/commands/pgschema.js +30 -0
- package/dist/src/commands/prepare.d.ts +41 -6
- package/dist/src/commands/prepare.js +440 -63
- package/dist/src/commands/schema.js +1 -1
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +23 -9
- package/dist/src/config.d.ts +22 -0
- package/dist/src/config.js +149 -11
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- package/dist/src/migration-core.d.ts +157 -0
- package/dist/src/migration-core.js +578 -0
- package/dist/src/postgres-runtime.d.ts +3 -0
- package/dist/src/postgres-runtime.js +66 -29
- package/dist/src/runtime.d.ts +36 -3
- package/dist/src/runtime.js +92 -23
- package/dist/src/scan/scanner.d.ts +10 -3
- package/dist/src/scan/scanner.js +83 -32
- package/dist/src/type-inspection.d.ts +1 -0
- package/dist/src/type-inspection.js +20 -0
- package/dist/src/typed.d.ts +10 -0
- package/package.json +11 -6
|
@@ -4,79 +4,12 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { PgClient, parseDatabaseUrl, decodeText } from "../pg/wire.js";
|
|
7
|
-
import {
|
|
7
|
+
import { DEFAULT_MIGRATE_LOCK_KEY, SQUASH_PREFIX, acquireMigrateLock, applyPending, buildMigrationPlan, effectiveSquashReplacements, ensureTable, inspectMigrationPlan, inspectMigrations, listApplied, parseSquashMetadata, planPending, readMigrations, releaseMigrateLock, resetMigrationSession, } from "../migration-core.js";
|
|
8
8
|
import { introspectConnected, schemaSnapshotEqual, } from "../schema-snapshot.js";
|
|
9
|
-
|
|
10
|
-
export const DEFAULT_MIGRATE_LOCK_KEY = 18750938867203960n;
|
|
9
|
+
export * from "../migration-core.js";
|
|
11
10
|
const FILE_RE = /^(\d+)_(.+)\.up\.sql$/;
|
|
12
11
|
const DOWN_FILE_RE = /^(\d+)_(.+)\.down\.sql$/;
|
|
13
12
|
const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
14
|
-
const MIGRATIONS_TABLE = "_sqlx_js_migrations";
|
|
15
|
-
function readMigrations(dir) {
|
|
16
|
-
if (!existsSync(dir))
|
|
17
|
-
return [];
|
|
18
|
-
const out = [];
|
|
19
|
-
for (const f of readdirSync(dir).sort()) {
|
|
20
|
-
const m = FILE_RE.exec(f);
|
|
21
|
-
if (!m)
|
|
22
|
-
continue;
|
|
23
|
-
const version = parseInt(m[1], 10);
|
|
24
|
-
const name = m[2];
|
|
25
|
-
if (!SAFE_NAME_RE.test(name)) {
|
|
26
|
-
throw new Error(`sqlx-js.migrate: unsafe migration filename ${f} — name must match /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/`);
|
|
27
|
-
}
|
|
28
|
-
const upPath = join(dir, f);
|
|
29
|
-
const downPath = join(dir, `${m[1]}_${name}.down.sql`);
|
|
30
|
-
const upSql = readFileSync(upPath, "utf8");
|
|
31
|
-
out.push({
|
|
32
|
-
version,
|
|
33
|
-
name,
|
|
34
|
-
upPath,
|
|
35
|
-
downPath: existsSync(downPath) ? downPath : null,
|
|
36
|
-
upSql,
|
|
37
|
-
upHash: createHash("sha256").update(upSql).digest("hex"),
|
|
38
|
-
squash: parseSquashMetadata(upSql),
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
return out;
|
|
42
|
-
}
|
|
43
|
-
function parseSquashMetadata(sql) {
|
|
44
|
-
const line = sql.split(/\r?\n/).find((l) => l.startsWith(SQUASH_PREFIX));
|
|
45
|
-
if (!line)
|
|
46
|
-
return null;
|
|
47
|
-
const raw = line.slice(SQUASH_PREFIX.length).trim();
|
|
48
|
-
let parsed;
|
|
49
|
-
try {
|
|
50
|
-
parsed = JSON.parse(raw);
|
|
51
|
-
}
|
|
52
|
-
catch (e) {
|
|
53
|
-
throw new Error(`sqlx-js.migrate: invalid squash metadata JSON: ${e.message}`);
|
|
54
|
-
}
|
|
55
|
-
if (!parsed || typeof parsed !== "object") {
|
|
56
|
-
throw new Error("sqlx-js.migrate: invalid squash metadata");
|
|
57
|
-
}
|
|
58
|
-
const obj = parsed;
|
|
59
|
-
if (obj.format !== 1 || !Array.isArray(obj.replaces) || obj.replaces.length === 0) {
|
|
60
|
-
throw new Error("sqlx-js.migrate: invalid squash metadata");
|
|
61
|
-
}
|
|
62
|
-
const replaces = [];
|
|
63
|
-
for (const r of obj.replaces) {
|
|
64
|
-
if (!r || typeof r !== "object")
|
|
65
|
-
throw new Error("sqlx-js.migrate: invalid squash replacement metadata");
|
|
66
|
-
const item = r;
|
|
67
|
-
if (typeof item.version !== "number" || !Number.isSafeInteger(item.version) || item.version <= 0) {
|
|
68
|
-
throw new Error("sqlx-js.migrate: invalid squash replacement version");
|
|
69
|
-
}
|
|
70
|
-
if (typeof item.name !== "string" || !SAFE_NAME_RE.test(item.name)) {
|
|
71
|
-
throw new Error("sqlx-js.migrate: invalid squash replacement name");
|
|
72
|
-
}
|
|
73
|
-
if (typeof item.upHash !== "string" || !/^[a-f0-9]{64}$/.test(item.upHash)) {
|
|
74
|
-
throw new Error("sqlx-js.migrate: invalid squash replacement hash");
|
|
75
|
-
}
|
|
76
|
-
replaces.push({ version: item.version, name: item.name, upHash: item.upHash });
|
|
77
|
-
}
|
|
78
|
-
return { format: 1, replaces };
|
|
79
|
-
}
|
|
80
13
|
function quoteIdent(ident) {
|
|
81
14
|
return `"${ident.replace(/"/g, '""')}"`;
|
|
82
15
|
}
|
|
@@ -91,104 +24,6 @@ function maintenanceDatabaseUrl(databaseUrl) {
|
|
|
91
24
|
function generatedShadowDatabaseName() {
|
|
92
25
|
return `sqlx_js_shadow_${process.pid}_${Date.now().toString(36)}_${randomBytes(4).toString("hex")}`;
|
|
93
26
|
}
|
|
94
|
-
async function findMigrationStore(c) {
|
|
95
|
-
const r = await c.simpleQuery(`
|
|
96
|
-
SELECT n.nspname, cls.relname
|
|
97
|
-
FROM pg_class cls
|
|
98
|
-
JOIN pg_namespace n ON n.oid = cls.relnamespace
|
|
99
|
-
WHERE cls.oid = to_regclass('${MIGRATIONS_TABLE}')
|
|
100
|
-
`);
|
|
101
|
-
const row = r.rows[0];
|
|
102
|
-
if (!row)
|
|
103
|
-
return null;
|
|
104
|
-
const schema = decodeText(row[0]);
|
|
105
|
-
const table = decodeText(row[1]);
|
|
106
|
-
if (!schema || !table) {
|
|
107
|
-
throw new Error(`sqlx-js.migrate: failed to resolve ${MIGRATIONS_TABLE} identifier`);
|
|
108
|
-
}
|
|
109
|
-
return { table: `${quoteIdent(schema)}.${quoteIdent(table)}` };
|
|
110
|
-
}
|
|
111
|
-
async function resolveMigrationStore(c) {
|
|
112
|
-
const store = await findMigrationStore(c);
|
|
113
|
-
if (!store) {
|
|
114
|
-
throw new Error(`sqlx-js.migrate: failed to resolve ${MIGRATIONS_TABLE} in current search_path`);
|
|
115
|
-
}
|
|
116
|
-
return store;
|
|
117
|
-
}
|
|
118
|
-
export async function ensureTable(c) {
|
|
119
|
-
const existing = await findMigrationStore(c);
|
|
120
|
-
if (existing)
|
|
121
|
-
return existing;
|
|
122
|
-
await c.simpleQuery(`
|
|
123
|
-
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
124
|
-
version BIGINT PRIMARY KEY,
|
|
125
|
-
name TEXT NOT NULL,
|
|
126
|
-
up_hash TEXT NOT NULL,
|
|
127
|
-
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
128
|
-
)
|
|
129
|
-
`);
|
|
130
|
-
return resolveMigrationStore(c);
|
|
131
|
-
}
|
|
132
|
-
export async function listApplied(c, store) {
|
|
133
|
-
const s = store ?? await resolveMigrationStore(c);
|
|
134
|
-
const r = await c.simpleQuery(`SELECT version, name, up_hash FROM ${s.table} ORDER BY version`);
|
|
135
|
-
const out = new Map();
|
|
136
|
-
for (const row of r.rows) {
|
|
137
|
-
out.set(Number(decodeText(row[0])), { name: decodeText(row[1]), hash: decodeText(row[2]) });
|
|
138
|
-
}
|
|
139
|
-
return out;
|
|
140
|
-
}
|
|
141
|
-
function appliedSquashSupersededVersions(all, applied, onEvent) {
|
|
142
|
-
const superseded = new Set();
|
|
143
|
-
const byVersion = new Map(all.map((m) => [m.version, m]));
|
|
144
|
-
const visitedSquashes = new Set();
|
|
145
|
-
const visitReplacements = (m) => {
|
|
146
|
-
if (!m.squash || visitedSquashes.has(m.version))
|
|
147
|
-
return true;
|
|
148
|
-
visitedSquashes.add(m.version);
|
|
149
|
-
for (const r of m.squash.replaces) {
|
|
150
|
-
superseded.add(r.version);
|
|
151
|
-
const current = byVersion.get(r.version);
|
|
152
|
-
if (!current?.squash || current.version >= m.version)
|
|
153
|
-
continue;
|
|
154
|
-
if (current.name !== r.name || current.upHash !== r.upHash) {
|
|
155
|
-
onEvent?.({ kind: "tampered", version: r.version, name: r.name, applied: r.upHash, current: current.upHash });
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
if (!visitReplacements(current))
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
return true;
|
|
162
|
-
};
|
|
163
|
-
for (const m of all) {
|
|
164
|
-
if (!m.squash)
|
|
165
|
-
continue;
|
|
166
|
-
const a = applied.get(m.version);
|
|
167
|
-
if (!a)
|
|
168
|
-
continue;
|
|
169
|
-
if (a.hash !== m.upHash) {
|
|
170
|
-
onEvent?.({ kind: "tampered", version: m.version, name: m.name, applied: a.hash, current: m.upHash });
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
if (!visitReplacements(m))
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
return superseded;
|
|
177
|
-
}
|
|
178
|
-
function squashCoveredVersions(all) {
|
|
179
|
-
const covered = new Set();
|
|
180
|
-
for (const m of all) {
|
|
181
|
-
if (!m.squash)
|
|
182
|
-
continue;
|
|
183
|
-
for (const r of m.squash.replaces) {
|
|
184
|
-
if (r.version >= m.version) {
|
|
185
|
-
throw new Error(`sqlx-js.migrate: squash replacement ${r.version}_${r.name} must be older than ${m.version}_${m.name}`);
|
|
186
|
-
}
|
|
187
|
-
covered.add(r.version);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return covered;
|
|
191
|
-
}
|
|
192
27
|
function migrationCheckIssue(code, message, details = {}) {
|
|
193
28
|
return { severity: "error", code, message, ...details };
|
|
194
29
|
}
|
|
@@ -489,6 +324,7 @@ async function validateLatestDownForWorkflow(databaseUrl, migrationsDir) {
|
|
|
489
324
|
}
|
|
490
325
|
}
|
|
491
326
|
async function prepareWorkflowArtifacts(opts, databaseUrl) {
|
|
327
|
+
const { openSession, prepareOnce } = await import("./prepare.js");
|
|
492
328
|
const prepareOpts = {
|
|
493
329
|
root: opts.root,
|
|
494
330
|
databaseUrl,
|
|
@@ -496,6 +332,7 @@ async function prepareWorkflowArtifacts(opts, databaseUrl) {
|
|
|
496
332
|
dtsPath: opts.dtsPath,
|
|
497
333
|
check: false,
|
|
498
334
|
prune: opts.prune,
|
|
335
|
+
strictInference: opts.strictInference,
|
|
499
336
|
};
|
|
500
337
|
const session = await openSession(prepareOpts);
|
|
501
338
|
try {
|
|
@@ -512,410 +349,18 @@ async function prepareWorkflowArtifacts(opts, databaseUrl) {
|
|
|
512
349
|
}
|
|
513
350
|
}
|
|
514
351
|
async function prepareInTemporaryArtifacts(opts, databaseUrl) {
|
|
515
|
-
const
|
|
516
|
-
const
|
|
517
|
-
const dtsPath = join(tmp, "sqlx-js-env.d.ts");
|
|
518
|
-
const prepareOpts = {
|
|
352
|
+
const { verifyPrepareArtifacts } = await import("./prepare.js");
|
|
353
|
+
const verification = await verifyPrepareArtifacts({
|
|
519
354
|
root: opts.root,
|
|
520
355
|
databaseUrl,
|
|
521
|
-
cacheDir,
|
|
522
|
-
dtsPath,
|
|
356
|
+
cacheDir: opts.cacheDir,
|
|
357
|
+
dtsPath: opts.dtsPath,
|
|
523
358
|
check: false,
|
|
359
|
+
verify: true,
|
|
524
360
|
prune: true,
|
|
525
|
-
|
|
526
|
-
const session = await openSession(prepareOpts);
|
|
527
|
-
try {
|
|
528
|
-
const r = await prepareOnce(prepareOpts, session);
|
|
529
|
-
if (r.failures > 0) {
|
|
530
|
-
console.error(`\n${r.failures} query/queries failed to prepare`);
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
console.log(`shadow: prepared ${r.entries} unique query/queries`);
|
|
534
|
-
return true;
|
|
535
|
-
}
|
|
536
|
-
finally {
|
|
537
|
-
await session.client.end();
|
|
538
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
function effectiveSquashReplacements(all) {
|
|
542
|
-
const covered = squashCoveredVersions(all);
|
|
543
|
-
return all
|
|
544
|
-
.filter((m) => !covered.has(m.version))
|
|
545
|
-
.map((m) => ({ version: m.version, name: m.name, upHash: m.upHash }));
|
|
546
|
-
}
|
|
547
|
-
function preflightSquashMigrations(all, applied, superseded, onEvent) {
|
|
548
|
-
const byVersion = new Map(all.map((m) => [m.version, m]));
|
|
549
|
-
for (const m of all) {
|
|
550
|
-
if (!m.squash)
|
|
551
|
-
continue;
|
|
552
|
-
let present = 0;
|
|
553
|
-
for (const r of m.squash.replaces) {
|
|
554
|
-
if (r.version >= m.version) {
|
|
555
|
-
onEvent?.({
|
|
556
|
-
kind: "failed",
|
|
557
|
-
version: m.version,
|
|
558
|
-
name: m.name,
|
|
559
|
-
error: `squash replacement ${r.version}_${r.name} must be older than ${m.version}_${m.name}`,
|
|
560
|
-
});
|
|
561
|
-
return "failed";
|
|
562
|
-
}
|
|
563
|
-
const current = byVersion.get(r.version);
|
|
564
|
-
if (current && !superseded.has(r.version) && (current.name !== r.name || current.upHash !== r.upHash)) {
|
|
565
|
-
onEvent?.({ kind: "tampered", version: r.version, name: r.name, applied: r.upHash, current: current.upHash });
|
|
566
|
-
return "tampered";
|
|
567
|
-
}
|
|
568
|
-
const a = applied.get(r.version);
|
|
569
|
-
if (!a)
|
|
570
|
-
continue;
|
|
571
|
-
present++;
|
|
572
|
-
if (a.hash !== r.upHash || a.name !== r.name) {
|
|
573
|
-
onEvent?.({ kind: "tampered", version: r.version, name: r.name, applied: a.hash, current: r.upHash });
|
|
574
|
-
return "tampered";
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
if (present > 0 && present !== m.squash.replaces.length) {
|
|
578
|
-
onEvent?.({
|
|
579
|
-
kind: "failed",
|
|
580
|
-
version: m.version,
|
|
581
|
-
name: m.name,
|
|
582
|
-
error: `squash migration replaces ${m.squash.replaces.length} migration(s), but only ${present} matching row(s) are applied`,
|
|
583
|
-
});
|
|
584
|
-
return "failed";
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
return "ok";
|
|
588
|
-
}
|
|
589
|
-
function planSquashAdoption(m, applied, onEvent) {
|
|
590
|
-
if (!m.squash)
|
|
591
|
-
return { kind: "none" };
|
|
592
|
-
let present = 0;
|
|
593
|
-
for (const r of m.squash.replaces) {
|
|
594
|
-
if (r.version >= m.version) {
|
|
595
|
-
onEvent?.({
|
|
596
|
-
kind: "failed",
|
|
597
|
-
version: m.version,
|
|
598
|
-
name: m.name,
|
|
599
|
-
error: `squash replacement ${r.version}_${r.name} must be older than ${m.version}_${m.name}`,
|
|
600
|
-
});
|
|
601
|
-
return { kind: "failed" };
|
|
602
|
-
}
|
|
603
|
-
const a = applied.get(r.version);
|
|
604
|
-
if (!a)
|
|
605
|
-
continue;
|
|
606
|
-
present++;
|
|
607
|
-
if (a.hash !== r.upHash || a.name !== r.name) {
|
|
608
|
-
onEvent?.({ kind: "tampered", version: r.version, name: r.name, applied: a.hash, current: r.upHash });
|
|
609
|
-
return { kind: "tampered" };
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
if (present === 0)
|
|
613
|
-
return { kind: "none" };
|
|
614
|
-
if (present !== m.squash.replaces.length) {
|
|
615
|
-
onEvent?.({
|
|
616
|
-
kind: "failed",
|
|
617
|
-
version: m.version,
|
|
618
|
-
name: m.name,
|
|
619
|
-
error: `squash migration replaces ${m.squash.replaces.length} migration(s), but only ${present} matching row(s) are applied`,
|
|
620
|
-
});
|
|
621
|
-
return { kind: "failed" };
|
|
622
|
-
}
|
|
623
|
-
return { kind: "adopt", replaced: m.squash.replaces.length };
|
|
624
|
-
}
|
|
625
|
-
function buildMigrationPlan(all, applied, onEvent) {
|
|
626
|
-
const plannedApplied = new Map(applied);
|
|
627
|
-
const superseded = appliedSquashSupersededVersions(all, plannedApplied, onEvent);
|
|
628
|
-
if (!superseded)
|
|
629
|
-
return { kind: "tampered" };
|
|
630
|
-
const preflight = preflightSquashMigrations(all, plannedApplied, superseded, onEvent);
|
|
631
|
-
if (preflight !== "ok")
|
|
632
|
-
return { kind: preflight };
|
|
633
|
-
const steps = [];
|
|
634
|
-
for (const m of all) {
|
|
635
|
-
if (superseded.has(m.version))
|
|
636
|
-
continue;
|
|
637
|
-
const a = plannedApplied.get(m.version);
|
|
638
|
-
if (a) {
|
|
639
|
-
if (a.hash !== m.upHash) {
|
|
640
|
-
onEvent?.({ kind: "tampered", version: m.version, name: m.name, applied: a.hash, current: m.upHash });
|
|
641
|
-
return { kind: "tampered" };
|
|
642
|
-
}
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
645
|
-
const adoption = planSquashAdoption(m, plannedApplied, onEvent);
|
|
646
|
-
if (adoption.kind === "adopt") {
|
|
647
|
-
steps.push({ kind: "adopt", migration: m, replaced: adoption.replaced });
|
|
648
|
-
for (const r of m.squash.replaces)
|
|
649
|
-
plannedApplied.delete(r.version);
|
|
650
|
-
plannedApplied.set(m.version, { name: m.name, hash: m.upHash });
|
|
651
|
-
continue;
|
|
652
|
-
}
|
|
653
|
-
if (adoption.kind === "tampered" || adoption.kind === "failed")
|
|
654
|
-
return { kind: adoption.kind };
|
|
655
|
-
steps.push({ kind: "apply", migration: m });
|
|
656
|
-
plannedApplied.set(m.version, { name: m.name, hash: m.upHash });
|
|
657
|
-
}
|
|
658
|
-
return { kind: "ok", steps };
|
|
659
|
-
}
|
|
660
|
-
function publicPlanItem(step) {
|
|
661
|
-
if (step.kind === "apply") {
|
|
662
|
-
return { kind: "apply", version: step.migration.version, name: step.migration.name };
|
|
663
|
-
}
|
|
664
|
-
return {
|
|
665
|
-
kind: "adopt",
|
|
666
|
-
version: step.migration.version,
|
|
667
|
-
name: step.migration.name,
|
|
668
|
-
replaced: step.replaced,
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
async function resetMigrationSession(c) {
|
|
672
|
-
await c.simpleQuery("RESET ALL");
|
|
673
|
-
}
|
|
674
|
-
async function executeSquashAdoption(c, store, m, applied, replaced, onEvent) {
|
|
675
|
-
if (!m.squash)
|
|
676
|
-
return "failed";
|
|
677
|
-
let committed = false;
|
|
678
|
-
await c.simpleQuery("BEGIN");
|
|
679
|
-
try {
|
|
680
|
-
for (const r of m.squash.replaces) {
|
|
681
|
-
await c.execParamsText(`DELETE FROM ${store.table} WHERE version = $1`, [String(r.version)]);
|
|
682
|
-
}
|
|
683
|
-
await c.execParamsText(`INSERT INTO ${store.table} (version, name, up_hash) VALUES ($1, $2, $3)`, [String(m.version), m.name, m.upHash]);
|
|
684
|
-
await c.simpleQuery("COMMIT");
|
|
685
|
-
committed = true;
|
|
686
|
-
}
|
|
687
|
-
catch (err) {
|
|
688
|
-
let rollbackErr;
|
|
689
|
-
if (!committed) {
|
|
690
|
-
try {
|
|
691
|
-
await c.simpleQuery("ROLLBACK");
|
|
692
|
-
}
|
|
693
|
-
catch (rb) {
|
|
694
|
-
rollbackErr = rb.message;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
const message = rollbackErr
|
|
698
|
-
? `${err.message} (rollback also failed: ${rollbackErr})`
|
|
699
|
-
: err.message;
|
|
700
|
-
onEvent?.({ kind: "failed", version: m.version, name: m.name, error: message });
|
|
701
|
-
return "failed";
|
|
702
|
-
}
|
|
703
|
-
try {
|
|
704
|
-
await resetMigrationSession(c);
|
|
705
|
-
}
|
|
706
|
-
catch (err) {
|
|
707
|
-
onEvent?.({ kind: "failed", version: m.version, name: m.name, error: `failed to reset migration session: ${err.message}` });
|
|
708
|
-
return "failed";
|
|
709
|
-
}
|
|
710
|
-
for (const r of m.squash.replaces)
|
|
711
|
-
applied.delete(r.version);
|
|
712
|
-
applied.set(m.version, { name: m.name, hash: m.upHash });
|
|
713
|
-
onEvent?.({ kind: "adopted", version: m.version, name: m.name, replaced });
|
|
714
|
-
return "done";
|
|
715
|
-
}
|
|
716
|
-
export async function planPending(c, migrationsDir, onEvent) {
|
|
717
|
-
const store = await findMigrationStore(c);
|
|
718
|
-
const applied = store ? await listApplied(c, store) : new Map();
|
|
719
|
-
const all = readMigrations(migrationsDir);
|
|
720
|
-
const counts = { pending: 0, adoptable: 0, tampered: 0, failed: 0 };
|
|
721
|
-
const plan = buildMigrationPlan(all, applied, onEvent);
|
|
722
|
-
if (plan.kind === "tampered") {
|
|
723
|
-
counts.tampered++;
|
|
724
|
-
return { ...counts, steps: [] };
|
|
725
|
-
}
|
|
726
|
-
if (plan.kind === "failed") {
|
|
727
|
-
counts.failed++;
|
|
728
|
-
return { ...counts, steps: [] };
|
|
729
|
-
}
|
|
730
|
-
const steps = plan.steps.map(publicPlanItem);
|
|
731
|
-
for (const step of steps) {
|
|
732
|
-
if (step.kind === "apply") {
|
|
733
|
-
counts.pending++;
|
|
734
|
-
onEvent?.({ kind: "pending", version: step.version, name: step.name });
|
|
735
|
-
}
|
|
736
|
-
else {
|
|
737
|
-
counts.adoptable++;
|
|
738
|
-
onEvent?.({ kind: "adoptable", version: step.version, name: step.name, replaced: step.replaced });
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
return { ...counts, steps };
|
|
742
|
-
}
|
|
743
|
-
export async function inspectMigrationPlan(c, migrationsDir) {
|
|
744
|
-
const diagnostics = [];
|
|
745
|
-
const result = await planPending(c, migrationsDir, (e) => {
|
|
746
|
-
if (e.kind === "tampered" || e.kind === "failed")
|
|
747
|
-
diagnostics.push(e);
|
|
361
|
+
strictInference: opts.strictInference,
|
|
748
362
|
});
|
|
749
|
-
return
|
|
750
|
-
ok: result.tampered === 0 && result.failed === 0,
|
|
751
|
-
...result,
|
|
752
|
-
diagnostics,
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
export async function inspectMigrations(c, migrationsDir) {
|
|
756
|
-
const store = await findMigrationStore(c);
|
|
757
|
-
const applied = store ? await listApplied(c, store) : new Map();
|
|
758
|
-
const all = readMigrations(migrationsDir);
|
|
759
|
-
const validation = [];
|
|
760
|
-
const plan = buildMigrationPlan(all, applied, (e) => validation.push(e));
|
|
761
|
-
const planned = new Map();
|
|
762
|
-
if (plan.kind === "ok") {
|
|
763
|
-
for (const step of plan.steps)
|
|
764
|
-
planned.set(step.migration.version, step);
|
|
765
|
-
}
|
|
766
|
-
const validationByVersion = new Map(validation.map((e) => [e.version, e]));
|
|
767
|
-
const superseded = appliedSquashSupersededVersions(all, applied) ?? new Set();
|
|
768
|
-
const summary = {
|
|
769
|
-
applied: 0,
|
|
770
|
-
pending: 0,
|
|
771
|
-
adoptable: 0,
|
|
772
|
-
superseded: 0,
|
|
773
|
-
tampered: 0,
|
|
774
|
-
failed: 0,
|
|
775
|
-
};
|
|
776
|
-
const items = all.map((m) => {
|
|
777
|
-
const validationEvent = validationByVersion.get(m.version);
|
|
778
|
-
if (validationEvent?.kind === "tampered") {
|
|
779
|
-
summary.tampered++;
|
|
780
|
-
return {
|
|
781
|
-
version: m.version,
|
|
782
|
-
name: m.name,
|
|
783
|
-
status: "tampered",
|
|
784
|
-
detail: `hash mismatch: applied ${validationEvent.applied.slice(0, 16)} vs current ${validationEvent.current.slice(0, 16)}`,
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
if (validationEvent?.kind === "failed") {
|
|
788
|
-
summary.failed++;
|
|
789
|
-
return { version: m.version, name: m.name, status: "failed", detail: validationEvent.error };
|
|
790
|
-
}
|
|
791
|
-
if (superseded.has(m.version)) {
|
|
792
|
-
summary.superseded++;
|
|
793
|
-
return { version: m.version, name: m.name, status: "superseded" };
|
|
794
|
-
}
|
|
795
|
-
const a = applied.get(m.version);
|
|
796
|
-
if (a) {
|
|
797
|
-
if (a.hash !== m.upHash) {
|
|
798
|
-
summary.tampered++;
|
|
799
|
-
return {
|
|
800
|
-
version: m.version,
|
|
801
|
-
name: m.name,
|
|
802
|
-
status: "tampered",
|
|
803
|
-
detail: `hash mismatch: applied ${a.hash.slice(0, 16)} vs current ${m.upHash.slice(0, 16)}`,
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
summary.applied++;
|
|
807
|
-
return { version: m.version, name: m.name, status: "applied" };
|
|
808
|
-
}
|
|
809
|
-
const plannedStep = planned.get(m.version);
|
|
810
|
-
if (plannedStep?.kind === "adopt") {
|
|
811
|
-
summary.adoptable++;
|
|
812
|
-
return { version: m.version, name: m.name, status: "adoptable", detail: `${plannedStep.replaced} replaced` };
|
|
813
|
-
}
|
|
814
|
-
summary.pending++;
|
|
815
|
-
return { version: m.version, name: m.name, status: "pending" };
|
|
816
|
-
});
|
|
817
|
-
return { historyTable: store?.table ?? null, summary, items };
|
|
818
|
-
}
|
|
819
|
-
export async function applyPending(c, migrationsDir, onEvent) {
|
|
820
|
-
const store = await ensureTable(c);
|
|
821
|
-
const applied = await listApplied(c, store);
|
|
822
|
-
const all = readMigrations(migrationsDir);
|
|
823
|
-
const counts = { applied: 0, tampered: 0, failed: 0 };
|
|
824
|
-
const plan = buildMigrationPlan(all, applied, onEvent);
|
|
825
|
-
if (plan.kind === "tampered") {
|
|
826
|
-
counts.tampered++;
|
|
827
|
-
return counts;
|
|
828
|
-
}
|
|
829
|
-
if (plan.kind === "failed") {
|
|
830
|
-
counts.failed++;
|
|
831
|
-
return counts;
|
|
832
|
-
}
|
|
833
|
-
for (const step of plan.steps) {
|
|
834
|
-
const m = step.migration;
|
|
835
|
-
if (step.kind === "adopt") {
|
|
836
|
-
const adoption = await executeSquashAdoption(c, store, m, applied, step.replaced, onEvent);
|
|
837
|
-
if (adoption === "failed") {
|
|
838
|
-
counts.failed++;
|
|
839
|
-
return counts;
|
|
840
|
-
}
|
|
841
|
-
counts.applied++;
|
|
842
|
-
continue;
|
|
843
|
-
}
|
|
844
|
-
let committed = false;
|
|
845
|
-
await c.simpleQuery("BEGIN");
|
|
846
|
-
try {
|
|
847
|
-
await c.simpleQuery(m.upSql);
|
|
848
|
-
await c.execParamsText(`INSERT INTO ${store.table} (version, name, up_hash) VALUES ($1, $2, $3)`, [String(m.version), m.name, m.upHash]);
|
|
849
|
-
await c.simpleQuery("COMMIT");
|
|
850
|
-
committed = true;
|
|
851
|
-
}
|
|
852
|
-
catch (err) {
|
|
853
|
-
let rollbackErr;
|
|
854
|
-
if (!committed) {
|
|
855
|
-
try {
|
|
856
|
-
await c.simpleQuery("ROLLBACK");
|
|
857
|
-
}
|
|
858
|
-
catch (rb) {
|
|
859
|
-
rollbackErr = rb.message;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
counts.failed++;
|
|
863
|
-
const message = rollbackErr
|
|
864
|
-
? `${err.message} (rollback also failed: ${rollbackErr})`
|
|
865
|
-
: err.message;
|
|
866
|
-
onEvent?.({ kind: "failed", version: m.version, name: m.name, error: message });
|
|
867
|
-
return counts;
|
|
868
|
-
}
|
|
869
|
-
try {
|
|
870
|
-
await resetMigrationSession(c);
|
|
871
|
-
}
|
|
872
|
-
catch (err) {
|
|
873
|
-
counts.failed++;
|
|
874
|
-
onEvent?.({ kind: "failed", version: m.version, name: m.name, error: `failed to reset migration session: ${err.message}` });
|
|
875
|
-
return counts;
|
|
876
|
-
}
|
|
877
|
-
counts.applied++;
|
|
878
|
-
applied.set(m.version, { name: m.name, hash: m.upHash });
|
|
879
|
-
onEvent?.({ kind: "applied", version: m.version, name: m.name });
|
|
880
|
-
}
|
|
881
|
-
return counts;
|
|
882
|
-
}
|
|
883
|
-
function lockKeyToString(lockKey) {
|
|
884
|
-
if (typeof lockKey === "bigint")
|
|
885
|
-
return lockKey.toString();
|
|
886
|
-
if (!Number.isSafeInteger(lockKey)) {
|
|
887
|
-
throw new Error(`sqlx-js.migrate: lockKey must be a safe integer or bigint, got ${lockKey}`);
|
|
888
|
-
}
|
|
889
|
-
return BigInt(lockKey).toString();
|
|
890
|
-
}
|
|
891
|
-
export async function acquireMigrateLock(c, lockKey = DEFAULT_MIGRATE_LOCK_KEY, timeoutMs) {
|
|
892
|
-
if (timeoutMs !== undefined && !Number.isFinite(timeoutMs)) {
|
|
893
|
-
throw new Error(`sqlx-js.migrate: lockTimeoutMs must be a finite number, got ${timeoutMs}`);
|
|
894
|
-
}
|
|
895
|
-
const key = lockKeyToString(lockKey);
|
|
896
|
-
if (timeoutMs === undefined || timeoutMs <= 0) {
|
|
897
|
-
await c.simpleQuery(`SELECT pg_advisory_lock(${key})`);
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
const start = Date.now();
|
|
901
|
-
let delay = 50;
|
|
902
|
-
while (true) {
|
|
903
|
-
const r = await c.simpleQuery(`SELECT pg_try_advisory_lock(${key})`);
|
|
904
|
-
const got = decodeText(r.rows[0]?.[0] ?? null) === "t";
|
|
905
|
-
if (got)
|
|
906
|
-
return;
|
|
907
|
-
const elapsed = Date.now() - start;
|
|
908
|
-
if (elapsed >= timeoutMs) {
|
|
909
|
-
throw new Error(`sqlx-js.migrate: failed to acquire advisory lock ${key} within ${timeoutMs}ms`);
|
|
910
|
-
}
|
|
911
|
-
const remaining = timeoutMs - elapsed;
|
|
912
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(delay, remaining)));
|
|
913
|
-
delay = Math.min(delay * 2, 2000);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
export async function releaseMigrateLock(c, lockKey = DEFAULT_MIGRATE_LOCK_KEY) {
|
|
917
|
-
const key = lockKeyToString(lockKey);
|
|
918
|
-
await c.simpleQuery(`SELECT pg_advisory_unlock(${key})`);
|
|
363
|
+
return verification.ok;
|
|
919
364
|
}
|
|
920
365
|
function safeMigrationName(name) {
|
|
921
366
|
const safe = name.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^[^a-zA-Z0-9]+/, "");
|
|
@@ -19,7 +19,13 @@ export type PgschemaInstallOptions = {
|
|
|
19
19
|
baseUrl?: string;
|
|
20
20
|
log?: (msg: string) => void;
|
|
21
21
|
};
|
|
22
|
+
export type PgschemaProbe = {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
command?: string;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
22
27
|
export declare function resolvePgschemaAsset(platform?: NodeJS.Platform, arch?: NodeJS.Architecture): PgschemaAsset;
|
|
23
28
|
export declare function managedPgschemaPath(root: string, asset?: PgschemaAsset): string;
|
|
29
|
+
export declare function probePgschema(root: string, config: SqlxJsConfig): PgschemaProbe;
|
|
24
30
|
export declare function runPgschemaInstall(opts: PgschemaInstallOptions): Promise<void>;
|
|
25
31
|
export declare function runPgschemaCommand(opts: PgschemaCommandOptions): void;
|
|
@@ -73,6 +73,36 @@ function commandName(root, config) {
|
|
|
73
73
|
return managed;
|
|
74
74
|
return "pgschema";
|
|
75
75
|
}
|
|
76
|
+
export function probePgschema(root, config) {
|
|
77
|
+
try {
|
|
78
|
+
const schema = pgschemaConfig(config);
|
|
79
|
+
const command = commandName(root, schema);
|
|
80
|
+
const child = spawnSync(command, ["--help"], {
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
env: process.env,
|
|
83
|
+
timeout: 5_000,
|
|
84
|
+
});
|
|
85
|
+
if (child.error) {
|
|
86
|
+
const code = child.error.code;
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
command,
|
|
90
|
+
message: code === "ENOENT"
|
|
91
|
+
? installHint(command)
|
|
92
|
+
: code === "ETIMEDOUT"
|
|
93
|
+
? `${command} --help timed out after 5000ms`
|
|
94
|
+
: child.error.message,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (child.status !== 0) {
|
|
98
|
+
return { ok: false, command, message: `${command} --help exited with ${child.status}` };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, command, message: `pgschema is available: ${command}` };
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
return { ok: false, message: e.message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
76
106
|
function schemaFile(root, config) {
|
|
77
107
|
return resolve(root, config.file ?? "schema.sql");
|
|
78
108
|
}
|