@onreza/sqlx-js 0.5.0 → 0.7.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,11 +9,12 @@ 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
+ 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';
@@ -70,7 +71,8 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
70
71
  const target = paramMap.get(paramIndex);
71
72
  if (target) {
72
73
  const column = resolveTargetColumn(target, schema);
73
- const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
74
+ const table = resolvedTargetTable(target, schema);
75
+ const decl = column && table ? lookupJsonbType(cfg, table.schema, table.name, column) : undefined;
74
76
  if (decl)
75
77
  return jsonParameter(decl);
76
78
  }
@@ -80,7 +82,8 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
80
82
  const target = paramMap.get(paramIndex);
81
83
  if (target) {
82
84
  const column = resolveTargetColumn(target, schema);
83
- const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
85
+ const table = resolvedTargetTable(target, schema);
86
+ const decl = column && table ? lookupJsonbType(cfg, table.schema, table.name, column) : undefined;
84
87
  if (decl)
85
88
  return arrayParameter(jsonParameter(decl));
86
89
  }
@@ -102,6 +105,10 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
102
105
  }
103
106
  return resolveTs(paramOid, (oid) => schema.customType(oid));
104
107
  }
108
+ function resolvedTargetTable(target, schema) {
109
+ const oid = schema.resolveTable(target.schema, target.table);
110
+ return oid === undefined ? undefined : schema.tableNameByOid(oid);
111
+ }
105
112
  function resolveTargetColumn(target, schema) {
106
113
  if (target.column)
107
114
  return target.column;
@@ -148,6 +155,14 @@ function isAliasOrExpression(f, schema) {
148
155
  const real = schema.columnNameByAttno(f.tableOid, f.columnAttr);
149
156
  return real !== undefined && real !== f.name;
150
157
  }
158
+ function duplicateOutputColumns(fields) {
159
+ const counts = new Map();
160
+ for (const field of fields) {
161
+ const name = parseColumnOverride(field.name).name;
162
+ counts.set(name, (counts.get(name) ?? 0) + 1);
163
+ }
164
+ return [...counts].filter(([, count]) => count > 1).map(([name]) => name).sort();
165
+ }
151
166
  export class PrepareFatalError extends Error {
152
167
  phase;
153
168
  file;
@@ -187,6 +202,32 @@ function siteUsage(sites) {
187
202
  ...(filePaths.length > 0 ? { filePaths } : {}),
188
203
  };
189
204
  }
205
+ function inferenceIssues(entry) {
206
+ const issues = [];
207
+ if (entry.degraded)
208
+ issues.push(`nullability inference degraded: ${entry.degraded.reason}`);
209
+ entry.paramTsTypes.forEach((type, index) => {
210
+ if (containsUnknownType(type))
211
+ issues.push(`parameter $${index + 1} resolved to ${type}`);
212
+ });
213
+ for (const column of entry.columns) {
214
+ if (containsUnknownType(column.tsType)) {
215
+ issues.push(`result column ${JSON.stringify(column.name)} resolved to ${column.tsType}`);
216
+ }
217
+ }
218
+ return issues;
219
+ }
220
+ function inferenceDiagnostics(entry, site, strict) {
221
+ return inferenceIssues(entry).map((message) => ({
222
+ severity: strict ? "error" : "warning",
223
+ phase: "inference",
224
+ message,
225
+ file: site.file,
226
+ line: site.line,
227
+ column: site.column,
228
+ query: entry.query,
229
+ }));
230
+ }
190
231
  export async function openSession(opts) {
191
232
  let userCfg;
192
233
  try {
@@ -266,17 +307,23 @@ export async function describeAll(cfg, sessionClient, queries, concurrency) {
266
307
  }
267
308
  return results;
268
309
  }
269
- export async function prepareOnce(opts, session, log = console.log, err = console.error, concurrency = defaultPrepareConcurrency()) {
310
+ export async function prepareOnce(opts, session, log = console.log, err = console.error, concurrency = defaultPrepareConcurrency(), input = {}) {
270
311
  let sites;
271
- try {
272
- sites = scanProject(opts.root, session.userCfg.scan);
312
+ if (input.sites) {
313
+ sites = input.sites;
273
314
  }
274
- catch (error) {
275
- throw fatal("scan", error);
315
+ else {
316
+ try {
317
+ sites = scanProject(opts.root, session.userCfg.scan);
318
+ }
319
+ catch (error) {
320
+ throw fatal("scan", error);
321
+ }
276
322
  }
277
323
  log(`scanned: found ${sites.length} sql() call site(s)`);
278
324
  const diagnostics = [];
279
325
  const cache = new Cache(opts.cacheDir);
326
+ let failures = 0;
280
327
  const unique = new Map();
281
328
  for (const s of sites) {
282
329
  const fp = fingerprint(s.query);
@@ -287,14 +334,50 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
287
334
  unique.set(fp, { fp, query: s.query, sites: [s] });
288
335
  }
289
336
  const raw = [];
290
- let failures = 0;
337
+ const reusedEntries = [];
338
+ const reusedGenerated = [];
291
339
  const { client, schema, userCfg } = session;
292
- const describeList = [...unique.values()].map((u) => ({ fp: u.fp, query: u.query }));
340
+ const toPrepare = new Map();
341
+ for (const [fp, item] of unique) {
342
+ const cached = input.reuseCacheFps?.has(fp) ? cache.read(fp) : null;
343
+ if (!cached) {
344
+ toPrepare.set(fp, item);
345
+ continue;
346
+ }
347
+ const entry = { ...cached, ...siteUsage(item.sites) };
348
+ const entryDiagnostics = inferenceDiagnostics(entry, item.sites[0], opts.strictInference === true);
349
+ diagnostics.push(...entryDiagnostics);
350
+ if (opts.strictInference && entryDiagnostics.length > 0) {
351
+ failures++;
352
+ continue;
353
+ }
354
+ reusedEntries.push(entry);
355
+ reusedGenerated.push({ fp, entry });
356
+ log(` ↺ ${formatSite(item.sites[0])} → reused ${entry.paramOids.length} param(s), ${entry.columns.length} col(s)`);
357
+ }
358
+ const describeList = [...toPrepare.values()].map((u) => ({ fp: u.fp, query: u.query }));
293
359
  const describeResults = await describeAll(parseDatabaseUrl(opts.databaseUrl), client, describeList, concurrency);
294
- for (const { fp, query, sites: ss } of unique.values()) {
360
+ for (const { fp, query, sites: ss } of toPrepare.values()) {
295
361
  const site = ss[0];
296
362
  const outcome = describeResults.get(fp);
297
363
  if (outcome.ok) {
364
+ const duplicates = duplicateOutputColumns(outcome.fields);
365
+ if (duplicates.length > 0) {
366
+ failures++;
367
+ const message = `duplicate output column name(s): ${duplicates.join(", ")}. Alias each result column to a unique name`;
368
+ diagnostics.push({
369
+ severity: "error",
370
+ phase: "result-shape",
371
+ message,
372
+ file: site.file,
373
+ line: site.line,
374
+ column: site.column,
375
+ query,
376
+ });
377
+ err(` ✗ ${formatSite(site)} — ${message}`);
378
+ err(` query: ${snippet(query)}`);
379
+ continue;
380
+ }
298
381
  raw.push({ fp, query, sites: ss, paramOids: outcome.paramOids, fields: outcome.fields });
299
382
  continue;
300
383
  }
@@ -403,8 +486,8 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
403
486
  for (const idx of pm.dmlBound) {
404
487
  const t = pm.targets.get(idx);
405
488
  if (t) {
406
- const schema = t.schema ?? "public";
407
- dmlTablesToLoad.set(JSON.stringify([schema, t.table]), { schema, name: t.table });
489
+ const key = JSON.stringify([t.schema ?? null, t.table]);
490
+ dmlTablesToLoad.set(key, t.schema ? { schema: t.schema, name: t.table } : { name: t.table });
408
491
  }
409
492
  }
410
493
  }
@@ -439,8 +522,8 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
439
522
  catch (error) {
440
523
  throw fatal("introspect", error);
441
524
  }
442
- const entries = [];
443
- const generated = [];
525
+ const entries = [...reusedEntries];
526
+ const generated = [...reusedGenerated];
444
527
  for (const r of raw) {
445
528
  if (failedFps.has(r.fp))
446
529
  continue;
@@ -470,6 +553,18 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
470
553
  hasResultSet: r.fields.length > 0,
471
554
  ...(analysis.degraded ? { degraded: analysis.degraded } : {}),
472
555
  };
556
+ const entryDiagnostics = inferenceDiagnostics(entry, r.sites[0], opts.strictInference === true);
557
+ diagnostics.push(...entryDiagnostics);
558
+ if (entryDiagnostics.length > 0) {
559
+ for (const diagnostic of entryDiagnostics) {
560
+ const label = diagnostic.severity === "error" ? "inference failed" : "inference warning";
561
+ err(` ${label}: ${formatSite(r.sites[0])} — ${diagnostic.message}`);
562
+ }
563
+ if (opts.strictInference) {
564
+ failures++;
565
+ continue;
566
+ }
567
+ }
473
568
  entries.push(entry);
474
569
  generated.push({ fp: r.fp, entry });
475
570
  const nn = entry.columns.filter((c) => !effectiveNullable(c)).length;
@@ -480,11 +575,16 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
480
575
  return { sites: sites.length, entries: entries.length, failures, pruned: 0, functions: 0, diagnostics };
481
576
  }
482
577
  let functions;
483
- try {
484
- functions = await introspectFunctions(client, schema);
578
+ if (input.reuseFunctions && functionCacheExists(opts.cacheDir)) {
579
+ functions = readFunctionCache(opts.cacheDir);
485
580
  }
486
- catch (error) {
487
- throw fatal("introspect", error);
581
+ else {
582
+ try {
583
+ functions = await introspectFunctions(client, schema);
584
+ }
585
+ catch (error) {
586
+ throw fatal("introspect", error);
587
+ }
488
588
  }
489
589
  let pruned;
490
590
  try {
@@ -516,7 +616,8 @@ export async function runPrepare(opts) {
516
616
  process.exitCode = 1;
517
617
  return;
518
618
  }
519
- if (opts.check) {
619
+ if (opts.check || opts.offline) {
620
+ const mode = opts.offline ? "offline" : "check";
520
621
  let userCfg;
521
622
  try {
522
623
  userCfg = await loadConfig(opts.root);
@@ -573,9 +674,9 @@ export async function runPrepare(opts) {
573
674
  console.log(JSON.stringify({
574
675
  formatVersion: 1,
575
676
  ok: false,
576
- mode: "check",
677
+ mode,
577
678
  sites: sites.length,
578
- entries: unique.size - diagnostics.length,
679
+ entries: [...unique.keys()].filter((fp) => cache.has(fp)).length,
579
680
  failures: diagnostics.length,
580
681
  pruned: 0,
581
682
  functions: 0,
@@ -583,20 +684,83 @@ export async function runPrepare(opts) {
583
684
  }, null, 2));
584
685
  }
585
686
  else {
586
- console.error(`\nsqlx-js prepare --check: ${diagnostics.length} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
687
+ console.error(`\nsqlx-js prepare --${mode}: ${diagnostics.length} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
587
688
  }
588
689
  process.exitCode = 1;
589
690
  return;
590
691
  }
591
- let entries;
692
+ const entries = [];
693
+ let inferenceFailures = 0;
592
694
  let functions;
593
695
  try {
594
- entries = [...unique.values()].map((u) => {
696
+ for (const u of unique.values()) {
595
697
  const entry = cache.read(u.fp);
596
- return entry ? { ...entry, ...siteUsage(u.sites) } : null;
597
- }).filter((e) => e !== null);
698
+ if (!entry)
699
+ continue;
700
+ const current = { ...entry, ...siteUsage(u.sites) };
701
+ const entryDiagnostics = inferenceDiagnostics(current, u.sites[0], opts.strictInference === true);
702
+ diagnostics.push(...entryDiagnostics);
703
+ if (opts.strictInference && entryDiagnostics.length > 0) {
704
+ inferenceFailures++;
705
+ continue;
706
+ }
707
+ entries.push(current);
708
+ }
598
709
  functions = readFunctionCache(opts.cacheDir);
599
- emitDts(opts.dtsPath, entries, functions);
710
+ if (!functionCacheExists(opts.cacheDir)) {
711
+ diagnostics.push({
712
+ severity: "error",
713
+ phase: "cache",
714
+ message: "function cache is missing",
715
+ });
716
+ inferenceFailures++;
717
+ }
718
+ if (opts.check && inferenceFailures === 0) {
719
+ const tmp = mkdtempSync(join(tmpdir(), "sqlx-js-check-"));
720
+ const generatedDts = join(tmp, "sqlx-js-env.d.ts");
721
+ try {
722
+ emitDts(generatedDts, entries, functions);
723
+ if (!existsSync(opts.dtsPath) || readFileSync(opts.dtsPath, "utf8") !== readFileSync(generatedDts, "utf8")) {
724
+ diagnostics.push({
725
+ severity: "error",
726
+ phase: "cache",
727
+ message: "generated declaration is stale or missing",
728
+ file: relative(opts.root, opts.dtsPath).replace(/\\/g, "/"),
729
+ });
730
+ inferenceFailures++;
731
+ }
732
+ }
733
+ finally {
734
+ rmSync(tmp, { recursive: true, force: true });
735
+ }
736
+ }
737
+ if (inferenceFailures > 0) {
738
+ if (opts.json) {
739
+ console.log(JSON.stringify({
740
+ formatVersion: 1,
741
+ ok: false,
742
+ mode,
743
+ sites: sites.length,
744
+ entries: entries.length,
745
+ failures: inferenceFailures,
746
+ pruned: 0,
747
+ functions: functions.length,
748
+ diagnostics,
749
+ }, null, 2));
750
+ }
751
+ else {
752
+ for (const diagnostic of diagnostics) {
753
+ const location = diagnostic.file
754
+ ? `${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}:${diagnostic.column ?? 1}` : ""} — `
755
+ : "";
756
+ console.error(`${diagnostic.phase} failed: ${location}${diagnostic.message}`);
757
+ }
758
+ }
759
+ process.exitCode = 1;
760
+ return;
761
+ }
762
+ if (opts.offline)
763
+ emitDts(opts.dtsPath, entries, functions);
600
764
  }
601
765
  catch (error) {
602
766
  throw fatal("cache", error);
@@ -605,17 +769,21 @@ export async function runPrepare(opts) {
605
769
  console.log(JSON.stringify({
606
770
  formatVersion: 1,
607
771
  ok: true,
608
- mode: "check",
772
+ mode,
609
773
  sites: sites.length,
610
774
  entries: entries.length,
611
775
  failures: 0,
612
776
  pruned: 0,
613
777
  functions: functions.length,
614
- diagnostics: [],
778
+ diagnostics,
615
779
  }, null, 2));
616
780
  }
617
781
  else {
618
- console.log(`ok ${entries.length} unique queries, ${functions.length} function(s), types regenerated`);
782
+ for (const diagnostic of diagnostics) {
783
+ console.error(`inference warning: ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — ${diagnostic.message}`);
784
+ }
785
+ const suffix = opts.offline ? ", types regenerated" : ", generated artifacts are current";
786
+ console.log(`ok — ${entries.length} unique queries, ${functions.length} function(s)${suffix}`);
619
787
  }
620
788
  return;
621
789
  }
@@ -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,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 {};