@onreza/sqlx-js 0.6.0 → 0.8.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.
@@ -1,6 +1,6 @@
1
- import { mkdtempSync, rmSync } from "node:fs";
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
- import { join } from "node:path";
3
+ import { join, relative } from "node:path";
4
4
  import { PgClient, parseDatabaseUrl, PgError } from "../pg/wire.js";
5
5
  import { SchemaCache, compositeLiteral } from "../pg/schema.js";
6
6
  import { analyzeQuery } from "../pg/analyze.js";
@@ -9,12 +9,13 @@ import { ScanError, scanProject } from "../scan/scanner.js";
9
9
  import { assertCacheManifest, Cache, fingerprint, effectiveNullable, portableCacheOid, writeCacheManifest, } from "../cache.js";
10
10
  import { emitDts } from "../codegen.js";
11
11
  import { loadConfig, lookupJsonbType, prepareConfigHash } from "../config.js";
12
- import { readFunctionCache, writeFunctionCache } from "../function-cache.js";
12
+ import { functionCacheExists, readFunctionCache, writeFunctionCache } from "../function-cache.js";
13
13
  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
17
  import { containsUnknownType } from "../type-inspection.js";
18
+ import { originalPosition, rewriteNamedParameters } from "../sql-params.js";
18
19
  const JSON_OIDS = new Set([114, 3802]);
19
20
  const JSON_ARRAY_OIDS = new Set([199, 3807]);
20
21
  const JSON_INPUT_VALUE = 'import("@onreza/sqlx-js").JsonInputValue';
@@ -71,7 +72,8 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
71
72
  const target = paramMap.get(paramIndex);
72
73
  if (target) {
73
74
  const column = resolveTargetColumn(target, schema);
74
- const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
75
+ const table = resolvedTargetTable(target, schema);
76
+ const decl = column && table ? lookupJsonbType(cfg, table.schema, table.name, column) : undefined;
75
77
  if (decl)
76
78
  return jsonParameter(decl);
77
79
  }
@@ -81,7 +83,8 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
81
83
  const target = paramMap.get(paramIndex);
82
84
  if (target) {
83
85
  const column = resolveTargetColumn(target, schema);
84
- const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
86
+ const table = resolvedTargetTable(target, schema);
87
+ const decl = column && table ? lookupJsonbType(cfg, table.schema, table.name, column) : undefined;
85
88
  if (decl)
86
89
  return arrayParameter(jsonParameter(decl));
87
90
  }
@@ -103,6 +106,10 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
103
106
  }
104
107
  return resolveTs(paramOid, (oid) => schema.customType(oid));
105
108
  }
109
+ function resolvedTargetTable(target, schema) {
110
+ const oid = schema.resolveTable(target.schema, target.table);
111
+ return oid === undefined ? undefined : schema.tableNameByOid(oid);
112
+ }
106
113
  function resolveTargetColumn(target, schema) {
107
114
  if (target.column)
108
115
  return target.column;
@@ -201,8 +208,9 @@ function inferenceIssues(entry) {
201
208
  if (entry.degraded)
202
209
  issues.push(`nullability inference degraded: ${entry.degraded.reason}`);
203
210
  entry.paramTsTypes.forEach((type, index) => {
211
+ const parameter = entry.paramNames?.[index] ? `$${entry.paramNames[index]}` : `$${index + 1}`;
204
212
  if (containsUnknownType(type))
205
- issues.push(`parameter $${index + 1} resolved to ${type}`);
213
+ issues.push(`parameter ${parameter} resolved to ${type}`);
206
214
  });
207
215
  for (const column of entry.columns) {
208
216
  if (containsUnknownType(column.tsType)) {
@@ -219,7 +227,7 @@ function inferenceDiagnostics(entry, site, strict) {
219
227
  file: site.file,
220
228
  line: site.line,
221
229
  column: site.column,
222
- query: entry.query,
230
+ query: site.query,
223
231
  }));
224
232
  }
225
233
  export async function openSession(opts) {
@@ -301,32 +309,58 @@ export async function describeAll(cfg, sessionClient, queries, concurrency) {
301
309
  }
302
310
  return results;
303
311
  }
304
- export async function prepareOnce(opts, session, log = console.log, err = console.error, concurrency = defaultPrepareConcurrency()) {
312
+ export async function prepareOnce(opts, session, log = console.log, err = console.error, concurrency = defaultPrepareConcurrency(), input = {}) {
305
313
  let sites;
306
- try {
307
- sites = scanProject(opts.root, session.userCfg.scan);
314
+ if (input.sites) {
315
+ sites = input.sites;
308
316
  }
309
- catch (error) {
310
- throw fatal("scan", error);
317
+ else {
318
+ try {
319
+ sites = scanProject(opts.root, session.userCfg.scan);
320
+ }
321
+ catch (error) {
322
+ throw fatal("scan", error);
323
+ }
311
324
  }
312
325
  log(`scanned: found ${sites.length} sql() call site(s)`);
313
326
  const diagnostics = [];
314
327
  const cache = new Cache(opts.cacheDir);
328
+ let failures = 0;
315
329
  const unique = new Map();
316
330
  for (const s of sites) {
331
+ const rewritten = rewriteNamedParameters(s.query);
317
332
  const fp = fingerprint(s.query);
318
333
  const existing = unique.get(fp);
319
334
  if (existing)
320
335
  existing.sites.push(s);
321
336
  else
322
- unique.set(fp, { fp, query: s.query, sites: [s] });
337
+ unique.set(fp, { fp, query: rewritten.query, paramNames: rewritten.names, sites: [s] });
323
338
  }
324
339
  const raw = [];
325
- let failures = 0;
340
+ const reusedEntries = [];
341
+ const reusedGenerated = [];
326
342
  const { client, schema, userCfg } = session;
327
- const describeList = [...unique.values()].map((u) => ({ fp: u.fp, query: u.query }));
343
+ const toPrepare = new Map();
344
+ for (const [fp, item] of unique) {
345
+ const cached = input.reuseCacheFps?.has(fp) ? cache.read(fp) : null;
346
+ if (!cached) {
347
+ toPrepare.set(fp, item);
348
+ continue;
349
+ }
350
+ const entry = { ...cached, ...siteUsage(item.sites) };
351
+ const entryDiagnostics = inferenceDiagnostics(entry, item.sites[0], opts.strictInference === true);
352
+ diagnostics.push(...entryDiagnostics);
353
+ if (opts.strictInference && entryDiagnostics.length > 0) {
354
+ failures++;
355
+ continue;
356
+ }
357
+ reusedEntries.push(entry);
358
+ reusedGenerated.push({ fp, entry });
359
+ log(` ↺ ${formatSite(item.sites[0])} → reused ${entry.paramOids.length} param(s), ${entry.columns.length} col(s)`);
360
+ }
361
+ const describeList = [...toPrepare.values()].map((u) => ({ fp: u.fp, query: u.query }));
328
362
  const describeResults = await describeAll(parseDatabaseUrl(opts.databaseUrl), client, describeList, concurrency);
329
- for (const { fp, query, sites: ss } of unique.values()) {
363
+ for (const { fp, query, sites: ss } of toPrepare.values()) {
330
364
  const site = ss[0];
331
365
  const outcome = describeResults.get(fp);
332
366
  if (outcome.ok) {
@@ -341,18 +375,19 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
341
375
  file: site.file,
342
376
  line: site.line,
343
377
  column: site.column,
344
- query,
378
+ query: site.query,
345
379
  });
346
380
  err(` ✗ ${formatSite(site)} — ${message}`);
347
- err(` query: ${snippet(query)}`);
381
+ err(` query: ${snippet(site.query)}`);
348
382
  continue;
349
383
  }
350
- raw.push({ fp, query, sites: ss, paramOids: outcome.paramOids, fields: outcome.fields });
384
+ raw.push({ fp, query, sites: ss, paramOids: outcome.paramOids, fields: outcome.fields, paramNames: toPrepare.get(fp).paramNames });
351
385
  continue;
352
386
  }
353
387
  failures++;
354
388
  const e = outcome.error;
355
389
  if (e instanceof PgError) {
390
+ const position = e.position ? originalPosition(rewriteNamedParameters(site.query), e.position) : undefined;
356
391
  diagnostics.push({
357
392
  severity: "error",
358
393
  phase: "describe",
@@ -360,21 +395,21 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
360
395
  file: site.file,
361
396
  line: site.line,
362
397
  column: site.column,
363
- query,
398
+ query: site.query,
364
399
  ...(e.code ? { code: e.code } : {}),
365
- ...(e.position ? { position: e.position } : {}),
400
+ ...(position ? { position } : {}),
366
401
  ...(e.hint ? { hint: e.hint } : {}),
367
402
  });
368
403
  const extras = [];
369
- if (e.position)
370
- extras.push(`pos ${e.position}`);
404
+ if (position)
405
+ extras.push(`pos ${position}`);
371
406
  if (e.code)
372
407
  extras.push(`code ${e.code}`);
373
408
  const tail = extras.length > 0 ? ` (${extras.join(", ")})` : "";
374
409
  err(` ✗ ${formatSite(site)} — describe failed: ${e.message}${tail}`);
375
410
  if (e.hint)
376
411
  err(` hint: ${e.hint}`);
377
- err(` query: ${snippet(query)}`);
412
+ err(` query: ${snippet(site.query)}`);
378
413
  }
379
414
  else {
380
415
  diagnostics.push({
@@ -384,10 +419,10 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
384
419
  file: site.file,
385
420
  line: site.line,
386
421
  column: site.column,
387
- query,
422
+ query: site.query,
388
423
  });
389
424
  err(` ✗ ${formatSite(site)} — describe failed: ${e.message}`);
390
- err(` query: ${snippet(query)}`);
425
+ err(` query: ${snippet(site.query)}`);
391
426
  }
392
427
  }
393
428
  const allAttrRefs = [];
@@ -425,10 +460,10 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
425
460
  file: site.file,
426
461
  line: site.line,
427
462
  column: site.column,
428
- query: r.query,
463
+ query: site.query,
429
464
  });
430
465
  err(` ✗ ${formatSite(site)} — analyze failed: ${e.message}`);
431
- err(` query: ${snippet(r.query)}`);
466
+ err(` query: ${snippet(site.query)}`);
432
467
  continue;
433
468
  }
434
469
  try {
@@ -444,10 +479,10 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
444
479
  file: site.file,
445
480
  line: site.line,
446
481
  column: site.column,
447
- query: r.query,
482
+ query: site.query,
448
483
  });
449
484
  err(` ✗ ${formatSite(site)} — paramMap failed: ${e.message}`);
450
- err(` query: ${snippet(r.query)}`);
485
+ err(` query: ${snippet(site.query)}`);
451
486
  }
452
487
  }
453
488
  const dmlTablesToLoad = new Map();
@@ -455,8 +490,8 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
455
490
  for (const idx of pm.dmlBound) {
456
491
  const t = pm.targets.get(idx);
457
492
  if (t) {
458
- const schema = t.schema ?? "public";
459
- dmlTablesToLoad.set(JSON.stringify([schema, t.table]), { schema, name: t.table });
493
+ const key = JSON.stringify([t.schema ?? null, t.table]);
494
+ dmlTablesToLoad.set(key, t.schema ? { schema: t.schema, name: t.table } : { name: t.table });
460
495
  }
461
496
  }
462
497
  }
@@ -491,8 +526,8 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
491
526
  catch (error) {
492
527
  throw fatal("introspect", error);
493
528
  }
494
- const entries = [];
495
- const generated = [];
529
+ const entries = [...reusedEntries];
530
+ const generated = [...reusedGenerated];
496
531
  for (const r of raw) {
497
532
  if (failedFps.has(r.fp))
498
533
  continue;
@@ -503,11 +538,12 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
503
538
  dmlBound: new Set(),
504
539
  };
505
540
  const entry = {
506
- query: r.query,
541
+ query: r.sites[0].query,
507
542
  ...siteUsage(r.sites),
508
543
  paramOids: r.paramOids.map(portableCacheOid),
509
544
  paramTsTypes: r.paramOids.map((o, idx) => resolveParamTs(idx + 1, o, pm.targets, schema, userCfg)),
510
545
  paramNullable: r.paramOids.map((_o, idx) => resolveParamNullable(idx + 1, pm, schema)),
546
+ ...(r.paramNames.length > 0 ? { paramNames: r.paramNames } : {}),
511
547
  columns: r.fields.map((f, i) => {
512
548
  const parsed = parseColumnOverride(f.name);
513
549
  const treatAsOverride = parsed.override !== undefined && isAliasOrExpression(f, schema);
@@ -544,11 +580,16 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
544
580
  return { sites: sites.length, entries: entries.length, failures, pruned: 0, functions: 0, diagnostics };
545
581
  }
546
582
  let functions;
547
- try {
548
- functions = await introspectFunctions(client, schema);
583
+ if (input.reuseFunctions && functionCacheExists(opts.cacheDir)) {
584
+ functions = readFunctionCache(opts.cacheDir);
549
585
  }
550
- catch (error) {
551
- throw fatal("introspect", error);
586
+ else {
587
+ try {
588
+ functions = await introspectFunctions(client, schema);
589
+ }
590
+ catch (error) {
591
+ throw fatal("introspect", error);
592
+ }
552
593
  }
553
594
  let pruned;
554
595
  try {
@@ -580,7 +621,8 @@ export async function runPrepare(opts) {
580
621
  process.exitCode = 1;
581
622
  return;
582
623
  }
583
- if (opts.check) {
624
+ if (opts.check || opts.offline) {
625
+ const mode = opts.offline ? "offline" : "check";
584
626
  let userCfg;
585
627
  try {
586
628
  userCfg = await loadConfig(opts.root);
@@ -637,9 +679,9 @@ export async function runPrepare(opts) {
637
679
  console.log(JSON.stringify({
638
680
  formatVersion: 1,
639
681
  ok: false,
640
- mode: "check",
682
+ mode,
641
683
  sites: sites.length,
642
- entries: unique.size - diagnostics.length,
684
+ entries: [...unique.keys()].filter((fp) => cache.has(fp)).length,
643
685
  failures: diagnostics.length,
644
686
  pruned: 0,
645
687
  functions: 0,
@@ -647,7 +689,7 @@ export async function runPrepare(opts) {
647
689
  }, null, 2));
648
690
  }
649
691
  else {
650
- console.error(`\nsqlx-js prepare --check: ${diagnostics.length} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
692
+ console.error(`\nsqlx-js prepare --${mode}: ${diagnostics.length} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
651
693
  }
652
694
  process.exitCode = 1;
653
695
  return;
@@ -670,12 +712,39 @@ export async function runPrepare(opts) {
670
712
  entries.push(current);
671
713
  }
672
714
  functions = readFunctionCache(opts.cacheDir);
715
+ if (!functionCacheExists(opts.cacheDir)) {
716
+ diagnostics.push({
717
+ severity: "error",
718
+ phase: "cache",
719
+ message: "function cache is missing",
720
+ });
721
+ inferenceFailures++;
722
+ }
723
+ if (opts.check && inferenceFailures === 0) {
724
+ const tmp = mkdtempSync(join(tmpdir(), "sqlx-js-check-"));
725
+ const generatedDts = join(tmp, "sqlx-js-env.d.ts");
726
+ try {
727
+ emitDts(generatedDts, entries, functions);
728
+ if (!existsSync(opts.dtsPath) || readFileSync(opts.dtsPath, "utf8") !== readFileSync(generatedDts, "utf8")) {
729
+ diagnostics.push({
730
+ severity: "error",
731
+ phase: "cache",
732
+ message: "generated declaration is stale or missing",
733
+ file: relative(opts.root, opts.dtsPath).replace(/\\/g, "/"),
734
+ });
735
+ inferenceFailures++;
736
+ }
737
+ }
738
+ finally {
739
+ rmSync(tmp, { recursive: true, force: true });
740
+ }
741
+ }
673
742
  if (inferenceFailures > 0) {
674
743
  if (opts.json) {
675
744
  console.log(JSON.stringify({
676
745
  formatVersion: 1,
677
746
  ok: false,
678
- mode: "check",
747
+ mode,
679
748
  sites: sites.length,
680
749
  entries: entries.length,
681
750
  failures: inferenceFailures,
@@ -686,13 +755,17 @@ export async function runPrepare(opts) {
686
755
  }
687
756
  else {
688
757
  for (const diagnostic of diagnostics) {
689
- console.error(`inference failed: ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — ${diagnostic.message}`);
758
+ const location = diagnostic.file
759
+ ? `${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}:${diagnostic.column ?? 1}` : ""} — `
760
+ : "";
761
+ console.error(`${diagnostic.phase} failed: ${location}${diagnostic.message}`);
690
762
  }
691
763
  }
692
764
  process.exitCode = 1;
693
765
  return;
694
766
  }
695
- emitDts(opts.dtsPath, entries, functions);
767
+ if (opts.offline)
768
+ emitDts(opts.dtsPath, entries, functions);
696
769
  }
697
770
  catch (error) {
698
771
  throw fatal("cache", error);
@@ -701,7 +774,7 @@ export async function runPrepare(opts) {
701
774
  console.log(JSON.stringify({
702
775
  formatVersion: 1,
703
776
  ok: true,
704
- mode: "check",
777
+ mode,
705
778
  sites: sites.length,
706
779
  entries: entries.length,
707
780
  failures: 0,
@@ -714,7 +787,8 @@ export async function runPrepare(opts) {
714
787
  for (const diagnostic of diagnostics) {
715
788
  console.error(`inference warning: ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — ${diagnostic.message}`);
716
789
  }
717
- console.log(`ok ${entries.length} unique queries, ${functions.length} function(s), types regenerated`);
790
+ const suffix = opts.offline ? ", types regenerated" : ", generated artifacts are current";
791
+ console.log(`ok — ${entries.length} unique queries, ${functions.length} function(s)${suffix}`);
718
792
  }
719
793
  return;
720
794
  }
@@ -1,22 +1,30 @@
1
- import { openSession, prepareOnce, type PrepareOptions, type PrepareSession } from "./prepare.js";
1
+ import { openSession, prepareOnce, type PrepareOptions, type PrepareResult, type PrepareSession } from "./prepare.js";
2
+ import { loadConfig } from "../config.js";
3
+ import { findSourceFiles, scanFile, scanProject, type QueryCallSite } from "../scan/scanner.js";
2
4
  export declare function shouldWatchFile(filename: string): boolean;
3
5
  export type WatchPrepareHookResult = {
4
6
  resetSession?: boolean;
5
7
  };
6
8
  export type WatchOptions = PrepareOptions & {
7
9
  beforePrepare?: () => Promise<WatchPrepareHookResult | void>;
10
+ jsonl?: boolean;
8
11
  };
9
12
  export type WatchState = {
10
13
  session: PrepareSession | null;
14
+ sitesByFile?: Map<string, QueryCallSite[]>;
15
+ dirtyFiles?: Set<string>;
16
+ dirtyFps?: Set<string>;
11
17
  };
18
+ export declare function formatWatchEvent(name: string, data?: Record<string, unknown>, timestamp?: string): string;
19
+ export declare function watchErrorData(error: unknown): Record<string, unknown>;
12
20
  type WatchDeps = {
13
21
  openSession: typeof openSession;
14
22
  prepareOnce: typeof prepareOnce;
23
+ loadConfig: typeof loadConfig;
24
+ scanProject: typeof scanProject;
25
+ scanFile: typeof scanFile;
26
+ findSourceFiles: typeof findSourceFiles;
15
27
  };
16
- export declare function prepareWatchedOnce(opts: WatchOptions, state: WatchState, log: (msg: string) => void, err: (msg: string) => void, deps?: WatchDeps): Promise<{
17
- entries: number;
18
- failures: number;
19
- pruned: number;
20
- }>;
28
+ export declare function prepareWatchedOnce(opts: WatchOptions, state: WatchState, log: (msg: string) => void, err: (msg: string) => void, deps?: Partial<WatchDeps>, changedFiles?: readonly string[]): Promise<PrepareResult>;
21
29
  export declare function runWatch(opts: WatchOptions): Promise<void>;
22
30
  export {};