@onreza/sqlx-js 0.5.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 +59 -19
- package/ROADMAP.md +1 -1
- 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 +258 -81
- package/dist/src/commands/init.js +87 -3
- package/dist/src/commands/migrate.d.ts +2 -101
- package/dist/src/commands/migrate.js +6 -546
- package/dist/src/commands/prepare.d.ts +2 -1
- package/dist/src/commands/prepare.js +104 -5
- package/dist/src/commands/schema.js +1 -1
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +8 -0
- package/dist/src/migration-core.d.ts +157 -0
- package/dist/src/migration-core.js +578 -0
- package/dist/src/runtime.js +1 -1
- package/dist/src/scan/scanner.d.ts +1 -1
- package/dist/src/scan/scanner.js +4 -4
- package/dist/src/type-inspection.d.ts +1 -0
- package/dist/src/type-inspection.js +20 -0
- package/package.json +6 -2
|
@@ -14,6 +14,7 @@ import { introspectFunctions } from "../pg/functions.js";
|
|
|
14
14
|
import { buildParamMap } from "../pg/param-map.js";
|
|
15
15
|
import { mergeExtensionTypes } from "../pg/extensions.js";
|
|
16
16
|
import { compareArtifacts } from "../artifacts.js";
|
|
17
|
+
import { containsUnknownType } from "../type-inspection.js";
|
|
17
18
|
const JSON_OIDS = new Set([114, 3802]);
|
|
18
19
|
const JSON_ARRAY_OIDS = new Set([199, 3807]);
|
|
19
20
|
const JSON_INPUT_VALUE = 'import("@onreza/sqlx-js").JsonInputValue';
|
|
@@ -148,6 +149,14 @@ function isAliasOrExpression(f, schema) {
|
|
|
148
149
|
const real = schema.columnNameByAttno(f.tableOid, f.columnAttr);
|
|
149
150
|
return real !== undefined && real !== f.name;
|
|
150
151
|
}
|
|
152
|
+
function duplicateOutputColumns(fields) {
|
|
153
|
+
const counts = new Map();
|
|
154
|
+
for (const field of fields) {
|
|
155
|
+
const name = parseColumnOverride(field.name).name;
|
|
156
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
157
|
+
}
|
|
158
|
+
return [...counts].filter(([, count]) => count > 1).map(([name]) => name).sort();
|
|
159
|
+
}
|
|
151
160
|
export class PrepareFatalError extends Error {
|
|
152
161
|
phase;
|
|
153
162
|
file;
|
|
@@ -187,6 +196,32 @@ function siteUsage(sites) {
|
|
|
187
196
|
...(filePaths.length > 0 ? { filePaths } : {}),
|
|
188
197
|
};
|
|
189
198
|
}
|
|
199
|
+
function inferenceIssues(entry) {
|
|
200
|
+
const issues = [];
|
|
201
|
+
if (entry.degraded)
|
|
202
|
+
issues.push(`nullability inference degraded: ${entry.degraded.reason}`);
|
|
203
|
+
entry.paramTsTypes.forEach((type, index) => {
|
|
204
|
+
if (containsUnknownType(type))
|
|
205
|
+
issues.push(`parameter $${index + 1} resolved to ${type}`);
|
|
206
|
+
});
|
|
207
|
+
for (const column of entry.columns) {
|
|
208
|
+
if (containsUnknownType(column.tsType)) {
|
|
209
|
+
issues.push(`result column ${JSON.stringify(column.name)} resolved to ${column.tsType}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return issues;
|
|
213
|
+
}
|
|
214
|
+
function inferenceDiagnostics(entry, site, strict) {
|
|
215
|
+
return inferenceIssues(entry).map((message) => ({
|
|
216
|
+
severity: strict ? "error" : "warning",
|
|
217
|
+
phase: "inference",
|
|
218
|
+
message,
|
|
219
|
+
file: site.file,
|
|
220
|
+
line: site.line,
|
|
221
|
+
column: site.column,
|
|
222
|
+
query: entry.query,
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
190
225
|
export async function openSession(opts) {
|
|
191
226
|
let userCfg;
|
|
192
227
|
try {
|
|
@@ -295,6 +330,23 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
295
330
|
const site = ss[0];
|
|
296
331
|
const outcome = describeResults.get(fp);
|
|
297
332
|
if (outcome.ok) {
|
|
333
|
+
const duplicates = duplicateOutputColumns(outcome.fields);
|
|
334
|
+
if (duplicates.length > 0) {
|
|
335
|
+
failures++;
|
|
336
|
+
const message = `duplicate output column name(s): ${duplicates.join(", ")}. Alias each result column to a unique name`;
|
|
337
|
+
diagnostics.push({
|
|
338
|
+
severity: "error",
|
|
339
|
+
phase: "result-shape",
|
|
340
|
+
message,
|
|
341
|
+
file: site.file,
|
|
342
|
+
line: site.line,
|
|
343
|
+
column: site.column,
|
|
344
|
+
query,
|
|
345
|
+
});
|
|
346
|
+
err(` ✗ ${formatSite(site)} — ${message}`);
|
|
347
|
+
err(` query: ${snippet(query)}`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
298
350
|
raw.push({ fp, query, sites: ss, paramOids: outcome.paramOids, fields: outcome.fields });
|
|
299
351
|
continue;
|
|
300
352
|
}
|
|
@@ -470,6 +522,18 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
470
522
|
hasResultSet: r.fields.length > 0,
|
|
471
523
|
...(analysis.degraded ? { degraded: analysis.degraded } : {}),
|
|
472
524
|
};
|
|
525
|
+
const entryDiagnostics = inferenceDiagnostics(entry, r.sites[0], opts.strictInference === true);
|
|
526
|
+
diagnostics.push(...entryDiagnostics);
|
|
527
|
+
if (entryDiagnostics.length > 0) {
|
|
528
|
+
for (const diagnostic of entryDiagnostics) {
|
|
529
|
+
const label = diagnostic.severity === "error" ? "inference failed" : "inference warning";
|
|
530
|
+
err(` ${label}: ${formatSite(r.sites[0])} — ${diagnostic.message}`);
|
|
531
|
+
}
|
|
532
|
+
if (opts.strictInference) {
|
|
533
|
+
failures++;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
473
537
|
entries.push(entry);
|
|
474
538
|
generated.push({ fp: r.fp, entry });
|
|
475
539
|
const nn = entry.columns.filter((c) => !effectiveNullable(c)).length;
|
|
@@ -588,14 +652,46 @@ export async function runPrepare(opts) {
|
|
|
588
652
|
process.exitCode = 1;
|
|
589
653
|
return;
|
|
590
654
|
}
|
|
591
|
-
|
|
655
|
+
const entries = [];
|
|
656
|
+
let inferenceFailures = 0;
|
|
592
657
|
let functions;
|
|
593
658
|
try {
|
|
594
|
-
|
|
659
|
+
for (const u of unique.values()) {
|
|
595
660
|
const entry = cache.read(u.fp);
|
|
596
|
-
|
|
597
|
-
|
|
661
|
+
if (!entry)
|
|
662
|
+
continue;
|
|
663
|
+
const current = { ...entry, ...siteUsage(u.sites) };
|
|
664
|
+
const entryDiagnostics = inferenceDiagnostics(current, u.sites[0], opts.strictInference === true);
|
|
665
|
+
diagnostics.push(...entryDiagnostics);
|
|
666
|
+
if (opts.strictInference && entryDiagnostics.length > 0) {
|
|
667
|
+
inferenceFailures++;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
entries.push(current);
|
|
671
|
+
}
|
|
598
672
|
functions = readFunctionCache(opts.cacheDir);
|
|
673
|
+
if (inferenceFailures > 0) {
|
|
674
|
+
if (opts.json) {
|
|
675
|
+
console.log(JSON.stringify({
|
|
676
|
+
formatVersion: 1,
|
|
677
|
+
ok: false,
|
|
678
|
+
mode: "check",
|
|
679
|
+
sites: sites.length,
|
|
680
|
+
entries: entries.length,
|
|
681
|
+
failures: inferenceFailures,
|
|
682
|
+
pruned: 0,
|
|
683
|
+
functions: functions.length,
|
|
684
|
+
diagnostics,
|
|
685
|
+
}, null, 2));
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
for (const diagnostic of diagnostics) {
|
|
689
|
+
console.error(`inference failed: ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — ${diagnostic.message}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
process.exitCode = 1;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
599
695
|
emitDts(opts.dtsPath, entries, functions);
|
|
600
696
|
}
|
|
601
697
|
catch (error) {
|
|
@@ -611,10 +707,13 @@ export async function runPrepare(opts) {
|
|
|
611
707
|
failures: 0,
|
|
612
708
|
pruned: 0,
|
|
613
709
|
functions: functions.length,
|
|
614
|
-
diagnostics
|
|
710
|
+
diagnostics,
|
|
615
711
|
}, null, 2));
|
|
616
712
|
}
|
|
617
713
|
else {
|
|
714
|
+
for (const diagnostic of diagnostics) {
|
|
715
|
+
console.error(`inference warning: ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — ${diagnostic.message}`);
|
|
716
|
+
}
|
|
618
717
|
console.log(`ok — ${entries.length} unique queries, ${functions.length} function(s), types regenerated`);
|
|
619
718
|
}
|
|
620
719
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { introspectDatabase, readSchemaSnapshot, schemaSnapshotEqual, schemaSnapshotExists, writeSchemaManifest, writeSchemaSnapshot } from "../schema-snapshot.js";
|
|
2
2
|
import { PgClient, parseDatabaseUrl } from "../pg/wire.js";
|
|
3
|
-
import { acquireMigrateLock, applyPending, DEFAULT_MIGRATE_LOCK_KEY, releaseMigrateLock } from "
|
|
3
|
+
import { acquireMigrateLock, applyPending, DEFAULT_MIGRATE_LOCK_KEY, releaseMigrateLock } from "../migration-core.js";
|
|
4
4
|
export async function applyShadowMigrations(databaseUrl, migrationsDir, log = console.log) {
|
|
5
5
|
const client = new PgClient(parseDatabaseUrl(databaseUrl));
|
|
6
6
|
await client.connect();
|
package/dist/src/config.d.ts
CHANGED
package/dist/src/config.js
CHANGED
|
@@ -52,6 +52,12 @@ function validateStringArray(value, name, path) {
|
|
|
52
52
|
throw new Error(`sqlx-js: ${path} ${name} must be an array of strings`);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
function validateModuleArray(value, path) {
|
|
56
|
+
validateStringArray(value, "scan.modules", path);
|
|
57
|
+
if (value.length === 0 || value.some((item) => item.trim() === "")) {
|
|
58
|
+
throw new Error(`sqlx-js: ${path} scan.modules must contain at least one non-empty module name`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
55
61
|
function validateConfig(value, path) {
|
|
56
62
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
57
63
|
throw new Error(`sqlx-js: ${path} must default-export a config object`);
|
|
@@ -70,6 +76,8 @@ function validateConfig(value, path) {
|
|
|
70
76
|
validateStringArray(scan.include, "scan.include", path);
|
|
71
77
|
if (scan.exclude !== undefined)
|
|
72
78
|
validateStringArray(scan.exclude, "scan.exclude", path);
|
|
79
|
+
if (scan.modules !== undefined)
|
|
80
|
+
validateModuleArray(scan.modules, path);
|
|
73
81
|
}
|
|
74
82
|
if (config.schema !== undefined) {
|
|
75
83
|
if (!config.schema || typeof config.schema !== "object" || Array.isArray(config.schema)) {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { PgClient } from "./pg/wire.js";
|
|
2
|
+
export type MigrationFile = {
|
|
3
|
+
version: number;
|
|
4
|
+
name: string;
|
|
5
|
+
upPath: string;
|
|
6
|
+
downPath: string | null;
|
|
7
|
+
upSql: string;
|
|
8
|
+
upHash: string;
|
|
9
|
+
squash: SquashMetadata | null;
|
|
10
|
+
};
|
|
11
|
+
export type SquashReplacement = {
|
|
12
|
+
version: number;
|
|
13
|
+
name: string;
|
|
14
|
+
upHash: string;
|
|
15
|
+
};
|
|
16
|
+
export type SquashMetadata = {
|
|
17
|
+
format: 1;
|
|
18
|
+
replaces: SquashReplacement[];
|
|
19
|
+
};
|
|
20
|
+
export type MigrationStore = {
|
|
21
|
+
table: string;
|
|
22
|
+
};
|
|
23
|
+
export declare const SQUASH_PREFIX = "-- sqlx-js-squash:";
|
|
24
|
+
export declare const DEFAULT_MIGRATE_LOCK_KEY = 18750938867203960n;
|
|
25
|
+
export declare function readMigrations(dir: string): MigrationFile[];
|
|
26
|
+
export declare function parseSquashMetadata(sql: string): SquashMetadata | null;
|
|
27
|
+
export declare function ensureTable(client: PgClient): Promise<MigrationStore>;
|
|
28
|
+
export declare function listApplied(client: PgClient, store?: MigrationStore): Promise<Map<number, {
|
|
29
|
+
name: string;
|
|
30
|
+
hash: string;
|
|
31
|
+
}>>;
|
|
32
|
+
export type ApplyOutcome = {
|
|
33
|
+
kind: "applied";
|
|
34
|
+
version: number;
|
|
35
|
+
name: string;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "adopted";
|
|
38
|
+
version: number;
|
|
39
|
+
name: string;
|
|
40
|
+
replaced: number;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "tampered";
|
|
43
|
+
version: number;
|
|
44
|
+
name: string;
|
|
45
|
+
applied: string;
|
|
46
|
+
current: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "failed";
|
|
49
|
+
version: number;
|
|
50
|
+
name: string;
|
|
51
|
+
error: string;
|
|
52
|
+
};
|
|
53
|
+
export type PlanOutcome = {
|
|
54
|
+
kind: "pending";
|
|
55
|
+
version: number;
|
|
56
|
+
name: string;
|
|
57
|
+
} | {
|
|
58
|
+
kind: "adoptable";
|
|
59
|
+
version: number;
|
|
60
|
+
name: string;
|
|
61
|
+
replaced: number;
|
|
62
|
+
} | {
|
|
63
|
+
kind: "tampered";
|
|
64
|
+
version: number;
|
|
65
|
+
name: string;
|
|
66
|
+
applied: string;
|
|
67
|
+
current: string;
|
|
68
|
+
} | {
|
|
69
|
+
kind: "failed";
|
|
70
|
+
version: number;
|
|
71
|
+
name: string;
|
|
72
|
+
error: string;
|
|
73
|
+
};
|
|
74
|
+
export type MigrationPlanItem = {
|
|
75
|
+
kind: "apply";
|
|
76
|
+
version: number;
|
|
77
|
+
name: string;
|
|
78
|
+
} | {
|
|
79
|
+
kind: "adopt";
|
|
80
|
+
version: number;
|
|
81
|
+
name: string;
|
|
82
|
+
replaced: number;
|
|
83
|
+
};
|
|
84
|
+
export type MigrationPlanDiagnostic = Extract<PlanOutcome, {
|
|
85
|
+
kind: "tampered" | "failed";
|
|
86
|
+
}>;
|
|
87
|
+
export type MigrationPlanSnapshot = {
|
|
88
|
+
ok: boolean;
|
|
89
|
+
pending: number;
|
|
90
|
+
adoptable: number;
|
|
91
|
+
tampered: number;
|
|
92
|
+
failed: number;
|
|
93
|
+
steps: MigrationPlanItem[];
|
|
94
|
+
diagnostics: MigrationPlanDiagnostic[];
|
|
95
|
+
};
|
|
96
|
+
export type MigrationInfoStatus = "applied" | "pending" | "adoptable" | "superseded" | "tampered" | "failed";
|
|
97
|
+
export type MigrationInfoItem = {
|
|
98
|
+
version: number;
|
|
99
|
+
name: string;
|
|
100
|
+
status: MigrationInfoStatus;
|
|
101
|
+
detail?: string;
|
|
102
|
+
};
|
|
103
|
+
export type MigrationInfoSnapshot = {
|
|
104
|
+
historyTable: string | null;
|
|
105
|
+
summary: Record<MigrationInfoStatus, number>;
|
|
106
|
+
items: MigrationInfoItem[];
|
|
107
|
+
};
|
|
108
|
+
type InternalMigrationPlanItem = {
|
|
109
|
+
kind: "apply";
|
|
110
|
+
migration: MigrationFile;
|
|
111
|
+
} | {
|
|
112
|
+
kind: "adopt";
|
|
113
|
+
migration: MigrationFile;
|
|
114
|
+
replaced: number;
|
|
115
|
+
};
|
|
116
|
+
export type MigrationValidationOutcome = {
|
|
117
|
+
kind: "tampered";
|
|
118
|
+
version: number;
|
|
119
|
+
name: string;
|
|
120
|
+
applied: string;
|
|
121
|
+
current: string;
|
|
122
|
+
} | {
|
|
123
|
+
kind: "failed";
|
|
124
|
+
version: number;
|
|
125
|
+
name: string;
|
|
126
|
+
error: string;
|
|
127
|
+
};
|
|
128
|
+
export declare function effectiveSquashReplacements(all: MigrationFile[]): SquashReplacement[];
|
|
129
|
+
export declare function buildMigrationPlan(all: MigrationFile[], applied: Map<number, {
|
|
130
|
+
name: string;
|
|
131
|
+
hash: string;
|
|
132
|
+
}>, onEvent?: (event: MigrationValidationOutcome) => void): {
|
|
133
|
+
kind: "ok";
|
|
134
|
+
steps: InternalMigrationPlanItem[];
|
|
135
|
+
} | {
|
|
136
|
+
kind: "failed";
|
|
137
|
+
} | {
|
|
138
|
+
kind: "tampered";
|
|
139
|
+
};
|
|
140
|
+
export declare function resetMigrationSession(client: PgClient): Promise<void>;
|
|
141
|
+
export declare function planPending(client: PgClient, migrationsDir: string, onEvent?: (event: PlanOutcome) => void): Promise<{
|
|
142
|
+
pending: number;
|
|
143
|
+
adoptable: number;
|
|
144
|
+
tampered: number;
|
|
145
|
+
failed: number;
|
|
146
|
+
steps: MigrationPlanItem[];
|
|
147
|
+
}>;
|
|
148
|
+
export declare function inspectMigrationPlan(client: PgClient, migrationsDir: string): Promise<MigrationPlanSnapshot>;
|
|
149
|
+
export declare function inspectMigrations(client: PgClient, migrationsDir: string): Promise<MigrationInfoSnapshot>;
|
|
150
|
+
export declare function applyPending(client: PgClient, migrationsDir: string, onEvent?: (event: ApplyOutcome) => void): Promise<{
|
|
151
|
+
applied: number;
|
|
152
|
+
tampered: number;
|
|
153
|
+
failed: number;
|
|
154
|
+
}>;
|
|
155
|
+
export declare function acquireMigrateLock(client: PgClient, lockKey?: number | bigint, timeoutMs?: number): Promise<void>;
|
|
156
|
+
export declare function releaseMigrateLock(client: PgClient, lockKey?: number | bigint): Promise<void>;
|
|
157
|
+
export {};
|