@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.
Files changed (38) hide show
  1. package/README.md +139 -48
  2. package/ROADMAP.md +1 -3
  3. package/dist/bin/sqlx-js-diagnostics.d.ts +2 -0
  4. package/dist/bin/sqlx-js-diagnostics.js +96 -0
  5. package/dist/bin/sqlx-js.js +337 -89
  6. package/dist/src/artifacts.d.ts +9 -0
  7. package/dist/src/artifacts.js +26 -0
  8. package/dist/src/cache.d.ts +17 -0
  9. package/dist/src/cache.js +83 -1
  10. package/dist/src/codegen.js +14 -2
  11. package/dist/src/commands/doctor.d.ts +16 -0
  12. package/dist/src/commands/doctor.js +196 -0
  13. package/dist/src/commands/init.js +93 -13
  14. package/dist/src/commands/migrate.d.ts +2 -101
  15. package/dist/src/commands/migrate.js +11 -566
  16. package/dist/src/commands/pgschema.d.ts +6 -0
  17. package/dist/src/commands/pgschema.js +30 -0
  18. package/dist/src/commands/prepare.d.ts +41 -6
  19. package/dist/src/commands/prepare.js +440 -63
  20. package/dist/src/commands/schema.js +1 -1
  21. package/dist/src/commands/watch.d.ts +1 -0
  22. package/dist/src/commands/watch.js +23 -9
  23. package/dist/src/config.d.ts +22 -0
  24. package/dist/src/config.js +149 -11
  25. package/dist/src/index.d.ts +4 -2
  26. package/dist/src/index.js +2 -1
  27. package/dist/src/migration-core.d.ts +157 -0
  28. package/dist/src/migration-core.js +578 -0
  29. package/dist/src/postgres-runtime.d.ts +3 -0
  30. package/dist/src/postgres-runtime.js +66 -29
  31. package/dist/src/runtime.d.ts +36 -3
  32. package/dist/src/runtime.js +92 -23
  33. package/dist/src/scan/scanner.d.ts +10 -3
  34. package/dist/src/scan/scanner.js +83 -32
  35. package/dist/src/type-inspection.d.ts +1 -0
  36. package/dist/src/type-inspection.js +20 -0
  37. package/dist/src/typed.d.ts +10 -0
  38. 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 JSON_INPUT = 'import("@onreza/sqlx-js").JsonInput';
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) || JSON_ARRAY_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 JSON_ARRAY_OIDS.has(paramOid) ? `(${decl})[]` : decl;
86
+ return arrayParameter(jsonParameter(decl));
66
87
  }
67
- return JSON_ARRAY_OIDS.has(paramOid) ? `(${JSON_INPUT})[]` : JSON_INPUT;
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
- const userCfg = await loadConfig(opts.root);
135
- const cfg = parseDatabaseUrl(opts.databaseUrl);
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
- await client.connect();
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
- const sites = scanProject(opts.root);
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
- await schema.loadAttributes(allAttrRefs);
249
- await schema.loadTableNamesByOid(allTableOids);
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 Set();
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
- dmlTablesToLoad.add(`${t.schema ?? "public"}.${t.table}`);
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
- if (dmlTablesToLoad.size > 0) {
284
- const names = [...dmlTablesToLoad].map((k) => {
285
- const [schema, name] = k.split(".");
286
- return { schema, name: name };
287
- });
288
- await schema.loadTableNames(names);
289
- const oids = [];
290
- for (const n of names) {
291
- const oid = schema.resolveTable(n.schema, n.name);
292
- if (oid !== undefined)
293
- oids.push(oid);
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
- await schema.loadColumnsForTables(oids);
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
- await schema.loadCustomTypes([...unknownOids]);
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
- cache.write(r.fp, entry);
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
- let pruned = 0;
344
- if (opts.prune !== false) {
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
- if (failures === 0) {
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
- emitDts(opts.dtsPath, entries, functions);
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
- const sites = scanProject(opts.root);
360
- console.log(`scanned: found ${sites.length} sql() call site(s)`);
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
- let stale = 0;
616
+ const diagnostics = [];
372
617
  for (const { fp, query, sites: ss } of unique.values()) {
373
618
  if (!cache.has(fp)) {
374
- stale++;
375
- console.error(`stale: ${formatSite(ss[0])} — query not in cache`);
376
- console.error(` query: ${snippet(query)}`);
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 (stale > 0) {
380
- console.error(`\nsqlx-js prepare --check: ${stale} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
381
- process.exit(1);
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
- console.error(`\n${r.failures} query/queries failed to prepare`);
397
- await session.client.end();
398
- process.exit(1);
728
+ if (!opts.json)
729
+ console.error(`\n${r.failures} query/queries failed to prepare`);
730
+ process.exitCode = 1;
731
+ return;
399
732
  }
400
- console.log(`\nprepared ${r.entries} unique query/queries, ${r.functions} function(s) → ${opts.dtsPath}`);
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 "./migrate.js";
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();
@@ -1,4 +1,5 @@
1
1
  import { openSession, prepareOnce, type PrepareOptions, type PrepareSession } from "./prepare.js";
2
+ export declare function shouldWatchFile(filename: string): boolean;
2
3
  export type WatchPrepareHookResult = {
3
4
  resetSession?: boolean;
4
5
  };