@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
|
@@ -1,18 +1,29 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { PgClient, parseDatabaseUrl, PgError } from "../pg/wire.js";
|
|
2
5
|
import { SchemaCache, compositeLiteral } from "../pg/schema.js";
|
|
3
6
|
import { analyzeQuery } from "../pg/analyze.js";
|
|
4
|
-
import { isBuiltinOid, oidToTs } from "../pg/oids.js";
|
|
5
|
-
import { scanProject } from "../scan/scanner.js";
|
|
6
|
-
import { Cache, fingerprint, effectiveNullable } from "../cache.js";
|
|
7
|
+
import { arrayElementOid, isBuiltinOid, oidToTs } from "../pg/oids.js";
|
|
8
|
+
import { ScanError, scanProject } from "../scan/scanner.js";
|
|
9
|
+
import { assertCacheManifest, Cache, fingerprint, effectiveNullable, portableCacheOid, writeCacheManifest, } from "../cache.js";
|
|
7
10
|
import { emitDts } from "../codegen.js";
|
|
8
|
-
import { loadConfig, lookupJsonbType } from "../config.js";
|
|
11
|
+
import { loadConfig, lookupJsonbType, prepareConfigHash } from "../config.js";
|
|
9
12
|
import { readFunctionCache, writeFunctionCache } from "../function-cache.js";
|
|
10
13
|
import { introspectFunctions } from "../pg/functions.js";
|
|
11
14
|
import { buildParamMap } from "../pg/param-map.js";
|
|
12
15
|
import { mergeExtensionTypes } from "../pg/extensions.js";
|
|
16
|
+
import { compareArtifacts } from "../artifacts.js";
|
|
17
|
+
import { containsUnknownType } from "../type-inspection.js";
|
|
13
18
|
const JSON_OIDS = new Set([114, 3802]);
|
|
14
19
|
const JSON_ARRAY_OIDS = new Set([199, 3807]);
|
|
15
|
-
const
|
|
20
|
+
const JSON_INPUT_VALUE = 'import("@onreza/sqlx-js").JsonInputValue';
|
|
21
|
+
function jsonParameter(type) {
|
|
22
|
+
return `import("@onreza/sqlx-js").JsonParameter<${type}>`;
|
|
23
|
+
}
|
|
24
|
+
function arrayParameter(type) {
|
|
25
|
+
return `import("@onreza/sqlx-js").PgArrayParameter<${type}>`;
|
|
26
|
+
}
|
|
16
27
|
function enumUnion(values) {
|
|
17
28
|
if (values.length === 0)
|
|
18
29
|
return "never";
|
|
@@ -56,15 +67,39 @@ function resolveColumnTs(f, schema, cfg) {
|
|
|
56
67
|
return resolveTs(f.typeOid, (oid) => schema.customType(oid));
|
|
57
68
|
}
|
|
58
69
|
function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
|
|
59
|
-
if (JSON_OIDS.has(paramOid)
|
|
70
|
+
if (JSON_OIDS.has(paramOid)) {
|
|
71
|
+
const target = paramMap.get(paramIndex);
|
|
72
|
+
if (target) {
|
|
73
|
+
const column = resolveTargetColumn(target, schema);
|
|
74
|
+
const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
|
|
75
|
+
if (decl)
|
|
76
|
+
return jsonParameter(decl);
|
|
77
|
+
}
|
|
78
|
+
return jsonParameter(JSON_INPUT_VALUE);
|
|
79
|
+
}
|
|
80
|
+
if (JSON_ARRAY_OIDS.has(paramOid)) {
|
|
60
81
|
const target = paramMap.get(paramIndex);
|
|
61
82
|
if (target) {
|
|
62
83
|
const column = resolveTargetColumn(target, schema);
|
|
63
84
|
const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
|
|
64
85
|
if (decl)
|
|
65
|
-
return
|
|
86
|
+
return arrayParameter(jsonParameter(decl));
|
|
66
87
|
}
|
|
67
|
-
return
|
|
88
|
+
return arrayParameter(jsonParameter(JSON_INPUT_VALUE));
|
|
89
|
+
}
|
|
90
|
+
const builtinElementOid = arrayElementOid(paramOid);
|
|
91
|
+
if (builtinElementOid !== undefined) {
|
|
92
|
+
return arrayParameter(resolveTs(builtinElementOid, (oid) => schema.customType(oid)));
|
|
93
|
+
}
|
|
94
|
+
const custom = schema.customType(paramOid);
|
|
95
|
+
if (custom?.kind === "enumArray")
|
|
96
|
+
return arrayParameter(enumUnion(custom.element.values));
|
|
97
|
+
if (custom?.kind === "scalarArray")
|
|
98
|
+
return arrayParameter(custom.element.tsType);
|
|
99
|
+
if (custom?.kind === "compositeArray")
|
|
100
|
+
return arrayParameter(compositeLiteral(custom.element));
|
|
101
|
+
if (custom) {
|
|
102
|
+
return resolveTs(paramOid, () => custom);
|
|
68
103
|
}
|
|
69
104
|
return resolveTs(paramOid, (oid) => schema.customType(oid));
|
|
70
105
|
}
|
|
@@ -114,6 +149,37 @@ function isAliasOrExpression(f, schema) {
|
|
|
114
149
|
const real = schema.columnNameByAttno(f.tableOid, f.columnAttr);
|
|
115
150
|
return real !== undefined && real !== f.name;
|
|
116
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
|
+
}
|
|
160
|
+
export class PrepareFatalError extends Error {
|
|
161
|
+
phase;
|
|
162
|
+
file;
|
|
163
|
+
line;
|
|
164
|
+
column;
|
|
165
|
+
constructor(phase, message, location = {}, options) {
|
|
166
|
+
super(message, options);
|
|
167
|
+
this.phase = phase;
|
|
168
|
+
this.name = "PrepareFatalError";
|
|
169
|
+
this.file = location.file;
|
|
170
|
+
this.line = location.line;
|
|
171
|
+
this.column = location.column;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function fatal(phase, error) {
|
|
175
|
+
if (error instanceof PrepareFatalError)
|
|
176
|
+
return error;
|
|
177
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
178
|
+
const location = error instanceof ScanError
|
|
179
|
+
? { file: error.file, line: error.line, column: error.column }
|
|
180
|
+
: {};
|
|
181
|
+
return new PrepareFatalError(phase, message, location, { cause: error });
|
|
182
|
+
}
|
|
117
183
|
function formatSite(s) {
|
|
118
184
|
return `${s.file}:${s.line}:${s.column}`;
|
|
119
185
|
}
|
|
@@ -130,11 +196,55 @@ function siteUsage(sites) {
|
|
|
130
196
|
...(filePaths.length > 0 ? { filePaths } : {}),
|
|
131
197
|
};
|
|
132
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
|
+
}
|
|
133
225
|
export async function openSession(opts) {
|
|
134
|
-
|
|
135
|
-
|
|
226
|
+
let userCfg;
|
|
227
|
+
try {
|
|
228
|
+
userCfg = await loadConfig(opts.root);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
throw fatal("config", error);
|
|
232
|
+
}
|
|
233
|
+
let cfg;
|
|
234
|
+
try {
|
|
235
|
+
cfg = parseDatabaseUrl(opts.databaseUrl);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
throw fatal("connect", error);
|
|
239
|
+
}
|
|
136
240
|
const client = new PgClient(cfg);
|
|
137
|
-
|
|
241
|
+
try {
|
|
242
|
+
await client.connect();
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
await client.end().catch(() => { });
|
|
246
|
+
throw fatal("connect", error);
|
|
247
|
+
}
|
|
138
248
|
const schema = new SchemaCache(client);
|
|
139
249
|
schema.setTypeRegistry(mergeExtensionTypes(userCfg.customTypes));
|
|
140
250
|
return { client, schema, userCfg };
|
|
@@ -192,8 +302,15 @@ export async function describeAll(cfg, sessionClient, queries, concurrency) {
|
|
|
192
302
|
return results;
|
|
193
303
|
}
|
|
194
304
|
export async function prepareOnce(opts, session, log = console.log, err = console.error, concurrency = defaultPrepareConcurrency()) {
|
|
195
|
-
|
|
305
|
+
let sites;
|
|
306
|
+
try {
|
|
307
|
+
sites = scanProject(opts.root, session.userCfg.scan);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
throw fatal("scan", error);
|
|
311
|
+
}
|
|
196
312
|
log(`scanned: found ${sites.length} sql() call site(s)`);
|
|
313
|
+
const diagnostics = [];
|
|
197
314
|
const cache = new Cache(opts.cacheDir);
|
|
198
315
|
const unique = new Map();
|
|
199
316
|
for (const s of sites) {
|
|
@@ -213,12 +330,41 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
213
330
|
const site = ss[0];
|
|
214
331
|
const outcome = describeResults.get(fp);
|
|
215
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
|
+
}
|
|
216
350
|
raw.push({ fp, query, sites: ss, paramOids: outcome.paramOids, fields: outcome.fields });
|
|
217
351
|
continue;
|
|
218
352
|
}
|
|
219
353
|
failures++;
|
|
220
354
|
const e = outcome.error;
|
|
221
355
|
if (e instanceof PgError) {
|
|
356
|
+
diagnostics.push({
|
|
357
|
+
severity: "error",
|
|
358
|
+
phase: "describe",
|
|
359
|
+
message: e.message,
|
|
360
|
+
file: site.file,
|
|
361
|
+
line: site.line,
|
|
362
|
+
column: site.column,
|
|
363
|
+
query,
|
|
364
|
+
...(e.code ? { code: e.code } : {}),
|
|
365
|
+
...(e.position ? { position: e.position } : {}),
|
|
366
|
+
...(e.hint ? { hint: e.hint } : {}),
|
|
367
|
+
});
|
|
222
368
|
const extras = [];
|
|
223
369
|
if (e.position)
|
|
224
370
|
extras.push(`pos ${e.position}`);
|
|
@@ -231,6 +377,15 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
231
377
|
err(` query: ${snippet(query)}`);
|
|
232
378
|
}
|
|
233
379
|
else {
|
|
380
|
+
diagnostics.push({
|
|
381
|
+
severity: "error",
|
|
382
|
+
phase: "describe",
|
|
383
|
+
message: e.message,
|
|
384
|
+
file: site.file,
|
|
385
|
+
line: site.line,
|
|
386
|
+
column: site.column,
|
|
387
|
+
query,
|
|
388
|
+
});
|
|
234
389
|
err(` ✗ ${formatSite(site)} — describe failed: ${e.message}`);
|
|
235
390
|
err(` query: ${snippet(query)}`);
|
|
236
391
|
}
|
|
@@ -245,8 +400,13 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
245
400
|
}
|
|
246
401
|
}
|
|
247
402
|
}
|
|
248
|
-
|
|
249
|
-
|
|
403
|
+
try {
|
|
404
|
+
await schema.loadAttributes(allAttrRefs);
|
|
405
|
+
await schema.loadTableNamesByOid(allTableOids);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
throw fatal("introspect", error);
|
|
409
|
+
}
|
|
250
410
|
const analyses = new Map();
|
|
251
411
|
const paramMaps = new Map();
|
|
252
412
|
const failedFps = new Set();
|
|
@@ -258,6 +418,15 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
258
418
|
catch (e) {
|
|
259
419
|
failures++;
|
|
260
420
|
failedFps.add(r.fp);
|
|
421
|
+
diagnostics.push({
|
|
422
|
+
severity: "error",
|
|
423
|
+
phase: "analyze",
|
|
424
|
+
message: e.message,
|
|
425
|
+
file: site.file,
|
|
426
|
+
line: site.line,
|
|
427
|
+
column: site.column,
|
|
428
|
+
query: r.query,
|
|
429
|
+
});
|
|
261
430
|
err(` ✗ ${formatSite(site)} — analyze failed: ${e.message}`);
|
|
262
431
|
err(` query: ${snippet(r.query)}`);
|
|
263
432
|
continue;
|
|
@@ -268,31 +437,44 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
268
437
|
catch (e) {
|
|
269
438
|
failures++;
|
|
270
439
|
failedFps.add(r.fp);
|
|
440
|
+
diagnostics.push({
|
|
441
|
+
severity: "error",
|
|
442
|
+
phase: "param-map",
|
|
443
|
+
message: e.message,
|
|
444
|
+
file: site.file,
|
|
445
|
+
line: site.line,
|
|
446
|
+
column: site.column,
|
|
447
|
+
query: r.query,
|
|
448
|
+
});
|
|
271
449
|
err(` ✗ ${formatSite(site)} — paramMap failed: ${e.message}`);
|
|
272
450
|
err(` query: ${snippet(r.query)}`);
|
|
273
451
|
}
|
|
274
452
|
}
|
|
275
|
-
const dmlTablesToLoad = new
|
|
453
|
+
const dmlTablesToLoad = new Map();
|
|
276
454
|
for (const pm of paramMaps.values()) {
|
|
277
455
|
for (const idx of pm.dmlBound) {
|
|
278
456
|
const t = pm.targets.get(idx);
|
|
279
|
-
if (t)
|
|
280
|
-
|
|
457
|
+
if (t) {
|
|
458
|
+
const schema = t.schema ?? "public";
|
|
459
|
+
dmlTablesToLoad.set(JSON.stringify([schema, t.table]), { schema, name: t.table });
|
|
460
|
+
}
|
|
281
461
|
}
|
|
282
462
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
463
|
+
try {
|
|
464
|
+
if (dmlTablesToLoad.size > 0) {
|
|
465
|
+
const names = [...dmlTablesToLoad.values()];
|
|
466
|
+
await schema.loadTableNames(names);
|
|
467
|
+
const oids = [];
|
|
468
|
+
for (const n of names) {
|
|
469
|
+
const oid = schema.resolveTable(n.schema, n.name);
|
|
470
|
+
if (oid !== undefined)
|
|
471
|
+
oids.push(oid);
|
|
472
|
+
}
|
|
473
|
+
await schema.loadColumnsForTables(oids);
|
|
294
474
|
}
|
|
295
|
-
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
throw fatal("introspect", error);
|
|
296
478
|
}
|
|
297
479
|
const unknownOids = new Set();
|
|
298
480
|
for (const r of raw) {
|
|
@@ -303,8 +485,14 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
303
485
|
if (!isBuiltinOid(f.typeOid))
|
|
304
486
|
unknownOids.add(f.typeOid);
|
|
305
487
|
}
|
|
306
|
-
|
|
488
|
+
try {
|
|
489
|
+
await schema.loadCustomTypes([...unknownOids]);
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
throw fatal("introspect", error);
|
|
493
|
+
}
|
|
307
494
|
const entries = [];
|
|
495
|
+
const generated = [];
|
|
308
496
|
for (const r of raw) {
|
|
309
497
|
if (failedFps.has(r.fp))
|
|
310
498
|
continue;
|
|
@@ -317,7 +505,7 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
317
505
|
const entry = {
|
|
318
506
|
query: r.query,
|
|
319
507
|
...siteUsage(r.sites),
|
|
320
|
-
paramOids: r.paramOids,
|
|
508
|
+
paramOids: r.paramOids.map(portableCacheOid),
|
|
321
509
|
paramTsTypes: r.paramOids.map((o, idx) => resolveParamTs(idx + 1, o, pm.targets, schema, userCfg)),
|
|
322
510
|
paramNullable: r.paramOids.map((_o, idx) => resolveParamNullable(idx + 1, pm, schema)),
|
|
323
511
|
columns: r.fields.map((f, i) => {
|
|
@@ -325,7 +513,7 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
325
513
|
const treatAsOverride = parsed.override !== undefined && isAliasOrExpression(f, schema);
|
|
326
514
|
return {
|
|
327
515
|
name: parsed.name,
|
|
328
|
-
typeOid: f.typeOid,
|
|
516
|
+
typeOid: portableCacheOid(f.typeOid),
|
|
329
517
|
tsType: resolveColumnTs(f, schema, userCfg),
|
|
330
518
|
nullable: analysis.perColumnNullable[i] ?? true,
|
|
331
519
|
...(treatAsOverride ? { override: parsed.override } : {}),
|
|
@@ -334,31 +522,88 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
334
522
|
hasResultSet: r.fields.length > 0,
|
|
335
523
|
...(analysis.degraded ? { degraded: analysis.degraded } : {}),
|
|
336
524
|
};
|
|
337
|
-
|
|
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
|
+
}
|
|
338
537
|
entries.push(entry);
|
|
538
|
+
generated.push({ fp: r.fp, entry });
|
|
339
539
|
const nn = entry.columns.filter((c) => !effectiveNullable(c)).length;
|
|
340
540
|
const tag = entry.degraded ? ` [degraded: ${entry.degraded.reason}]` : "";
|
|
341
541
|
log(` ✓ ${formatSite(r.sites[0])} → ${r.paramOids.length} param(s), ${r.fields.length} col(s) [${nn} non-null]${tag}`);
|
|
342
542
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
pruned = cache.prune(unique.keys()).length;
|
|
346
|
-
if (pruned > 0)
|
|
347
|
-
log(`pruned ${pruned} orphaned cache entry/entries`);
|
|
543
|
+
if (failures > 0) {
|
|
544
|
+
return { sites: sites.length, entries: entries.length, failures, pruned: 0, functions: 0, diagnostics };
|
|
348
545
|
}
|
|
349
|
-
let functions
|
|
350
|
-
|
|
546
|
+
let functions;
|
|
547
|
+
try {
|
|
351
548
|
functions = await introspectFunctions(client, schema);
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
throw fatal("introspect", error);
|
|
552
|
+
}
|
|
553
|
+
let pruned;
|
|
554
|
+
try {
|
|
555
|
+
pruned = cache.replaceAll(generated, opts.prune !== false).length;
|
|
556
|
+
if (pruned > 0)
|
|
557
|
+
log(`pruned ${pruned} orphaned cache entry/entries`);
|
|
352
558
|
writeFunctionCache(opts.cacheDir, functions);
|
|
559
|
+
writeCacheManifest(opts.cacheDir, prepareConfigHash(userCfg));
|
|
560
|
+
emitDts(opts.dtsPath, entries, functions);
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
throw fatal("cache", error);
|
|
353
564
|
}
|
|
354
|
-
|
|
355
|
-
return { entries: entries.length, failures, pruned, functions: functions.length };
|
|
565
|
+
return { sites: sites.length, entries: entries.length, failures, pruned, functions: functions.length, diagnostics };
|
|
356
566
|
}
|
|
357
567
|
export async function runPrepare(opts) {
|
|
568
|
+
if (opts.verify) {
|
|
569
|
+
const verification = await verifyPrepareArtifacts(opts, opts.json ? () => { } : console.log, opts.json ? () => { } : console.error);
|
|
570
|
+
if (opts.json) {
|
|
571
|
+
console.log(JSON.stringify({
|
|
572
|
+
formatVersion: 1,
|
|
573
|
+
ok: verification.ok,
|
|
574
|
+
mode: "verify",
|
|
575
|
+
...verification.result,
|
|
576
|
+
changed: verification.changed,
|
|
577
|
+
}, null, 2));
|
|
578
|
+
}
|
|
579
|
+
if (!verification.ok)
|
|
580
|
+
process.exitCode = 1;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
358
583
|
if (opts.check) {
|
|
359
|
-
|
|
360
|
-
|
|
584
|
+
let userCfg;
|
|
585
|
+
try {
|
|
586
|
+
userCfg = await loadConfig(opts.root);
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
throw fatal("config", error);
|
|
590
|
+
}
|
|
591
|
+
let sites;
|
|
592
|
+
try {
|
|
593
|
+
sites = scanProject(opts.root, userCfg.scan);
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
throw fatal("scan", error);
|
|
597
|
+
}
|
|
598
|
+
if (!opts.json)
|
|
599
|
+
console.log(`scanned: found ${sites.length} sql() call site(s)`);
|
|
361
600
|
const cache = new Cache(opts.cacheDir);
|
|
601
|
+
try {
|
|
602
|
+
assertCacheManifest(opts.cacheDir, prepareConfigHash(userCfg));
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
throw fatal("cache", error);
|
|
606
|
+
}
|
|
362
607
|
const unique = new Map();
|
|
363
608
|
for (const s of sites) {
|
|
364
609
|
const fp = fingerprint(s.query);
|
|
@@ -368,38 +613,170 @@ export async function runPrepare(opts) {
|
|
|
368
613
|
else
|
|
369
614
|
unique.set(fp, { fp, query: s.query, sites: [s] });
|
|
370
615
|
}
|
|
371
|
-
|
|
616
|
+
const diagnostics = [];
|
|
372
617
|
for (const { fp, query, sites: ss } of unique.values()) {
|
|
373
618
|
if (!cache.has(fp)) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
619
|
+
const site = ss[0];
|
|
620
|
+
diagnostics.push({
|
|
621
|
+
severity: "error",
|
|
622
|
+
phase: "cache",
|
|
623
|
+
message: "query is not in the offline cache",
|
|
624
|
+
file: site.file,
|
|
625
|
+
line: site.line,
|
|
626
|
+
column: site.column,
|
|
627
|
+
query,
|
|
628
|
+
});
|
|
629
|
+
if (!opts.json) {
|
|
630
|
+
console.error(`stale: ${formatSite(site)} — query not in cache`);
|
|
631
|
+
console.error(` query: ${snippet(query)}`);
|
|
632
|
+
}
|
|
377
633
|
}
|
|
378
634
|
}
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
|
|
635
|
+
if (diagnostics.length > 0) {
|
|
636
|
+
if (opts.json) {
|
|
637
|
+
console.log(JSON.stringify({
|
|
638
|
+
formatVersion: 1,
|
|
639
|
+
ok: false,
|
|
640
|
+
mode: "check",
|
|
641
|
+
sites: sites.length,
|
|
642
|
+
entries: unique.size - diagnostics.length,
|
|
643
|
+
failures: diagnostics.length,
|
|
644
|
+
pruned: 0,
|
|
645
|
+
functions: 0,
|
|
646
|
+
diagnostics,
|
|
647
|
+
}, null, 2));
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
console.error(`\nsqlx-js prepare --check: ${diagnostics.length} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
|
|
651
|
+
}
|
|
652
|
+
process.exitCode = 1;
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const entries = [];
|
|
656
|
+
let inferenceFailures = 0;
|
|
657
|
+
let functions;
|
|
658
|
+
try {
|
|
659
|
+
for (const u of unique.values()) {
|
|
660
|
+
const entry = cache.read(u.fp);
|
|
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
|
+
}
|
|
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
|
+
}
|
|
695
|
+
emitDts(opts.dtsPath, entries, functions);
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
throw fatal("cache", error);
|
|
699
|
+
}
|
|
700
|
+
if (opts.json) {
|
|
701
|
+
console.log(JSON.stringify({
|
|
702
|
+
formatVersion: 1,
|
|
703
|
+
ok: true,
|
|
704
|
+
mode: "check",
|
|
705
|
+
sites: sites.length,
|
|
706
|
+
entries: entries.length,
|
|
707
|
+
failures: 0,
|
|
708
|
+
pruned: 0,
|
|
709
|
+
functions: functions.length,
|
|
710
|
+
diagnostics,
|
|
711
|
+
}, null, 2));
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
for (const diagnostic of diagnostics) {
|
|
715
|
+
console.error(`inference warning: ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — ${diagnostic.message}`);
|
|
716
|
+
}
|
|
717
|
+
console.log(`ok — ${entries.length} unique queries, ${functions.length} function(s), types regenerated`);
|
|
382
718
|
}
|
|
383
|
-
const entries = [...unique.values()].map((u) => {
|
|
384
|
-
const entry = cache.read(u.fp);
|
|
385
|
-
return entry ? { ...entry, ...siteUsage(u.sites) } : null;
|
|
386
|
-
}).filter((e) => e !== null);
|
|
387
|
-
const functions = readFunctionCache(opts.cacheDir);
|
|
388
|
-
emitDts(opts.dtsPath, entries, functions);
|
|
389
|
-
console.log(`ok — ${entries.length} unique queries, ${functions.length} function(s), types regenerated`);
|
|
390
719
|
return;
|
|
391
720
|
}
|
|
392
721
|
const session = await openSession(opts);
|
|
393
722
|
try {
|
|
394
|
-
const r = await prepareOnce(opts, session);
|
|
723
|
+
const r = await prepareOnce(opts, session, opts.json ? () => { } : console.log, opts.json ? () => { } : console.error);
|
|
724
|
+
if (opts.json) {
|
|
725
|
+
console.log(JSON.stringify({ formatVersion: 1, ok: r.failures === 0, mode: "prepare", ...r }, null, 2));
|
|
726
|
+
}
|
|
395
727
|
if (r.failures > 0) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
process.
|
|
728
|
+
if (!opts.json)
|
|
729
|
+
console.error(`\n${r.failures} query/queries failed to prepare`);
|
|
730
|
+
process.exitCode = 1;
|
|
731
|
+
return;
|
|
399
732
|
}
|
|
400
|
-
|
|
733
|
+
if (!opts.json)
|
|
734
|
+
console.log(`\nprepared ${r.entries} unique query/queries, ${r.functions} function(s) → ${opts.dtsPath}`);
|
|
401
735
|
}
|
|
402
736
|
finally {
|
|
403
737
|
await session.client.end();
|
|
404
738
|
}
|
|
405
739
|
}
|
|
740
|
+
export async function verifyPrepareArtifacts(opts, log = console.log, err = console.error) {
|
|
741
|
+
const tmp = mkdtempSync(join(tmpdir(), "sqlx-js-verify-"));
|
|
742
|
+
const cacheDir = join(tmp, "cache");
|
|
743
|
+
const dtsPath = join(tmp, "sqlx-js-env.d.ts");
|
|
744
|
+
const verifyOpts = {
|
|
745
|
+
...opts,
|
|
746
|
+
cacheDir,
|
|
747
|
+
dtsPath,
|
|
748
|
+
check: false,
|
|
749
|
+
verify: false,
|
|
750
|
+
prune: true,
|
|
751
|
+
};
|
|
752
|
+
let session;
|
|
753
|
+
try {
|
|
754
|
+
session = await openSession(verifyOpts);
|
|
755
|
+
const result = await prepareOnce(verifyOpts, session, log, err);
|
|
756
|
+
if (result.failures > 0) {
|
|
757
|
+
err(`\n${result.failures} query/queries failed to prepare`);
|
|
758
|
+
return { ok: false, result, changed: [] };
|
|
759
|
+
}
|
|
760
|
+
let comparison;
|
|
761
|
+
try {
|
|
762
|
+
comparison = compareArtifacts({ cacheDir: opts.cacheDir, dtsPath: opts.dtsPath }, { cacheDir, dtsPath });
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
throw fatal("verify", error);
|
|
766
|
+
}
|
|
767
|
+
if (!comparison.ok) {
|
|
768
|
+
err("sqlx-js prepare --verify: generated artifacts are stale:");
|
|
769
|
+
for (const file of comparison.changed)
|
|
770
|
+
err(` ${file}`);
|
|
771
|
+
err("Run `sqlx-js prepare` and commit the regenerated artifacts.");
|
|
772
|
+
return { ok: false, result, changed: comparison.changed };
|
|
773
|
+
}
|
|
774
|
+
log(`verified ${result.entries} query/queries and ${result.functions} function(s); generated artifacts are current`);
|
|
775
|
+
return { ok: true, result, changed: [] };
|
|
776
|
+
}
|
|
777
|
+
finally {
|
|
778
|
+
if (session)
|
|
779
|
+
await session.client.end();
|
|
780
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
781
|
+
}
|
|
782
|
+
}
|
|
@@ -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();
|