@onreza/sqlx-js 0.6.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,7 +9,7 @@ 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";
@@ -71,7 +71,8 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
71
71
  const target = paramMap.get(paramIndex);
72
72
  if (target) {
73
73
  const column = resolveTargetColumn(target, schema);
74
- 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;
75
76
  if (decl)
76
77
  return jsonParameter(decl);
77
78
  }
@@ -81,7 +82,8 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
81
82
  const target = paramMap.get(paramIndex);
82
83
  if (target) {
83
84
  const column = resolveTargetColumn(target, schema);
84
- 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;
85
87
  if (decl)
86
88
  return arrayParameter(jsonParameter(decl));
87
89
  }
@@ -103,6 +105,10 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
103
105
  }
104
106
  return resolveTs(paramOid, (oid) => schema.customType(oid));
105
107
  }
108
+ function resolvedTargetTable(target, schema) {
109
+ const oid = schema.resolveTable(target.schema, target.table);
110
+ return oid === undefined ? undefined : schema.tableNameByOid(oid);
111
+ }
106
112
  function resolveTargetColumn(target, schema) {
107
113
  if (target.column)
108
114
  return target.column;
@@ -301,17 +307,23 @@ export async function describeAll(cfg, sessionClient, queries, concurrency) {
301
307
  }
302
308
  return results;
303
309
  }
304
- 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 = {}) {
305
311
  let sites;
306
- try {
307
- sites = scanProject(opts.root, session.userCfg.scan);
312
+ if (input.sites) {
313
+ sites = input.sites;
308
314
  }
309
- catch (error) {
310
- 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
+ }
311
322
  }
312
323
  log(`scanned: found ${sites.length} sql() call site(s)`);
313
324
  const diagnostics = [];
314
325
  const cache = new Cache(opts.cacheDir);
326
+ let failures = 0;
315
327
  const unique = new Map();
316
328
  for (const s of sites) {
317
329
  const fp = fingerprint(s.query);
@@ -322,11 +334,30 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
322
334
  unique.set(fp, { fp, query: s.query, sites: [s] });
323
335
  }
324
336
  const raw = [];
325
- let failures = 0;
337
+ const reusedEntries = [];
338
+ const reusedGenerated = [];
326
339
  const { client, schema, userCfg } = session;
327
- 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 }));
328
359
  const describeResults = await describeAll(parseDatabaseUrl(opts.databaseUrl), client, describeList, concurrency);
329
- for (const { fp, query, sites: ss } of unique.values()) {
360
+ for (const { fp, query, sites: ss } of toPrepare.values()) {
330
361
  const site = ss[0];
331
362
  const outcome = describeResults.get(fp);
332
363
  if (outcome.ok) {
@@ -455,8 +486,8 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
455
486
  for (const idx of pm.dmlBound) {
456
487
  const t = pm.targets.get(idx);
457
488
  if (t) {
458
- const schema = t.schema ?? "public";
459
- 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 });
460
491
  }
461
492
  }
462
493
  }
@@ -491,8 +522,8 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
491
522
  catch (error) {
492
523
  throw fatal("introspect", error);
493
524
  }
494
- const entries = [];
495
- const generated = [];
525
+ const entries = [...reusedEntries];
526
+ const generated = [...reusedGenerated];
496
527
  for (const r of raw) {
497
528
  if (failedFps.has(r.fp))
498
529
  continue;
@@ -544,11 +575,16 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
544
575
  return { sites: sites.length, entries: entries.length, failures, pruned: 0, functions: 0, diagnostics };
545
576
  }
546
577
  let functions;
547
- try {
548
- functions = await introspectFunctions(client, schema);
578
+ if (input.reuseFunctions && functionCacheExists(opts.cacheDir)) {
579
+ functions = readFunctionCache(opts.cacheDir);
549
580
  }
550
- catch (error) {
551
- throw fatal("introspect", error);
581
+ else {
582
+ try {
583
+ functions = await introspectFunctions(client, schema);
584
+ }
585
+ catch (error) {
586
+ throw fatal("introspect", error);
587
+ }
552
588
  }
553
589
  let pruned;
554
590
  try {
@@ -580,7 +616,8 @@ export async function runPrepare(opts) {
580
616
  process.exitCode = 1;
581
617
  return;
582
618
  }
583
- if (opts.check) {
619
+ if (opts.check || opts.offline) {
620
+ const mode = opts.offline ? "offline" : "check";
584
621
  let userCfg;
585
622
  try {
586
623
  userCfg = await loadConfig(opts.root);
@@ -637,9 +674,9 @@ export async function runPrepare(opts) {
637
674
  console.log(JSON.stringify({
638
675
  formatVersion: 1,
639
676
  ok: false,
640
- mode: "check",
677
+ mode,
641
678
  sites: sites.length,
642
- entries: unique.size - diagnostics.length,
679
+ entries: [...unique.keys()].filter((fp) => cache.has(fp)).length,
643
680
  failures: diagnostics.length,
644
681
  pruned: 0,
645
682
  functions: 0,
@@ -647,7 +684,7 @@ export async function runPrepare(opts) {
647
684
  }, null, 2));
648
685
  }
649
686
  else {
650
- 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.`);
651
688
  }
652
689
  process.exitCode = 1;
653
690
  return;
@@ -670,12 +707,39 @@ export async function runPrepare(opts) {
670
707
  entries.push(current);
671
708
  }
672
709
  functions = readFunctionCache(opts.cacheDir);
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
+ }
673
737
  if (inferenceFailures > 0) {
674
738
  if (opts.json) {
675
739
  console.log(JSON.stringify({
676
740
  formatVersion: 1,
677
741
  ok: false,
678
- mode: "check",
742
+ mode,
679
743
  sites: sites.length,
680
744
  entries: entries.length,
681
745
  failures: inferenceFailures,
@@ -686,13 +750,17 @@ export async function runPrepare(opts) {
686
750
  }
687
751
  else {
688
752
  for (const diagnostic of diagnostics) {
689
- console.error(`inference failed: ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — ${diagnostic.message}`);
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}`);
690
757
  }
691
758
  }
692
759
  process.exitCode = 1;
693
760
  return;
694
761
  }
695
- emitDts(opts.dtsPath, entries, functions);
762
+ if (opts.offline)
763
+ emitDts(opts.dtsPath, entries, functions);
696
764
  }
697
765
  catch (error) {
698
766
  throw fatal("cache", error);
@@ -701,7 +769,7 @@ export async function runPrepare(opts) {
701
769
  console.log(JSON.stringify({
702
770
  formatVersion: 1,
703
771
  ok: true,
704
- mode: "check",
772
+ mode,
705
773
  sites: sites.length,
706
774
  entries: entries.length,
707
775
  failures: 0,
@@ -714,7 +782,8 @@ export async function runPrepare(opts) {
714
782
  for (const diagnostic of diagnostics) {
715
783
  console.error(`inference warning: ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — ${diagnostic.message}`);
716
784
  }
717
- console.log(`ok ${entries.length} unique queries, ${functions.length} function(s), types regenerated`);
785
+ const suffix = opts.offline ? ", types regenerated" : ", generated artifacts are current";
786
+ console.log(`ok — ${entries.length} unique queries, ${functions.length} function(s)${suffix}`);
718
787
  }
719
788
  return;
720
789
  }
@@ -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 {};
@@ -1,7 +1,9 @@
1
- import { watch as fsWatch } from "node:fs";
2
- import { basename } from "node:path";
3
- import { openSession, prepareOnce } from "./prepare.js";
1
+ import { existsSync, watch as fsWatch } from "node:fs";
2
+ import { basename, relative, resolve } from "node:path";
3
+ import { openSession, prepareOnce, PrepareFatalError, } from "./prepare.js";
4
4
  import { configHash, loadConfig } from "../config.js";
5
+ import { fingerprint } from "../cache.js";
6
+ import { findSourceFiles, scanFile, scanProject } from "../scan/scanner.js";
5
7
  const EXT_RE = /\.(ts|tsx|mts|cts|sql)$/;
6
8
  const SKIP_DIRS = ["node_modules", ".git", ".sqlx-js", "dist", "build", ".next"];
7
9
  const DEBOUNCE_MS = 150;
@@ -23,7 +25,25 @@ export function shouldWatchFile(filename) {
23
25
  return true;
24
26
  return EXT_RE.test(file);
25
27
  }
26
- const DEFAULT_DEPS = { openSession, prepareOnce };
28
+ export function formatWatchEvent(name, data = {}, timestamp = new Date().toISOString()) {
29
+ return JSON.stringify({ formatVersion: 1, event: name, timestamp, ...data });
30
+ }
31
+ export function watchErrorData(error) {
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ if (!(error instanceof PrepareFatalError))
34
+ return { message };
35
+ return {
36
+ diagnostic: {
37
+ severity: "error",
38
+ phase: error.phase,
39
+ message,
40
+ ...(error.file === undefined ? {} : { file: error.file }),
41
+ ...(error.line === undefined ? {} : { line: error.line }),
42
+ ...(error.column === undefined ? {} : { column: error.column }),
43
+ },
44
+ };
45
+ }
46
+ const DEFAULT_DEPS = { openSession, prepareOnce, loadConfig, scanProject, scanFile, findSourceFiles };
27
47
  async function closeSession(session) {
28
48
  if (!session)
29
49
  return;
@@ -32,35 +52,164 @@ async function closeSession(session) {
32
52
  }
33
53
  catch { }
34
54
  }
35
- export async function prepareWatchedOnce(opts, state, log, err, deps = DEFAULT_DEPS) {
55
+ export async function prepareWatchedOnce(opts, state, log, err, deps = {}, changedFiles = []) {
56
+ const active = { ...DEFAULT_DEPS, ...deps };
36
57
  const hookResult = await opts.beforePrepare?.();
37
- const currentConfig = await loadConfig(opts.root);
38
- if (hookResult?.resetSession === true ||
39
- (state.session !== null && configHash(state.session.userCfg) !== configHash(currentConfig))) {
58
+ const currentConfig = await active.loadConfig(opts.root);
59
+ const configChanged = state.session !== null && configHash(state.session.userCfg) !== configHash(currentConfig);
60
+ const resetSession = hookResult?.resetSession === true || configChanged;
61
+ if (resetSession) {
40
62
  await closeSession(state.session);
41
63
  state.session = null;
42
64
  }
43
65
  if (!state.session)
44
- state.session = await deps.openSession(opts);
45
- return await deps.prepareOnce(opts, state.session, log, err, 1);
66
+ state.session = await active.openSession(opts);
67
+ const full = resetSession || !state.sitesByFile || changedFiles.some(isProjectGraphFile);
68
+ let nextSites;
69
+ let changedFps = new Set();
70
+ if (full) {
71
+ const sites = active.scanProject(opts.root, currentConfig.scan);
72
+ nextSites = groupSites(sites);
73
+ changedFps = new Set(sites.map((site) => fingerprint(site.query)));
74
+ }
75
+ else {
76
+ const previousSites = state.sitesByFile;
77
+ const dirtyFiles = state.dirtyFiles ?? new Set();
78
+ const requested = new Set([...changedFiles, ...dirtyFiles].map((file) => projectPath(opts.root, file)));
79
+ const affectedSources = new Set();
80
+ for (const changed of requested) {
81
+ if (/\.(ts|tsx|mts|cts)$/.test(changed))
82
+ affectedSources.add(changed);
83
+ if (changed.endsWith(".sql")) {
84
+ for (const [source, sites] of previousSites) {
85
+ if (sites.some((site) => site.sqlFilePath && projectPath(opts.root, site.sqlFilePath) === changed)) {
86
+ affectedSources.add(source);
87
+ }
88
+ }
89
+ }
90
+ }
91
+ nextSites = new Map(previousSites);
92
+ const allowed = new Set(active.findSourceFiles(opts.root, currentConfig.scan).map((file) => projectPath(opts.root, file)));
93
+ for (const source of nextSites.keys()) {
94
+ if (!allowed.has(source))
95
+ affectedSources.add(source);
96
+ }
97
+ try {
98
+ for (const source of affectedSources) {
99
+ const previous = nextSites.get(source) ?? [];
100
+ for (const site of previous)
101
+ changedFps.add(fingerprint(site.query));
102
+ const absolute = resolve(opts.root, source);
103
+ if (!existsSync(absolute) || !allowed.has(source)) {
104
+ nextSites.delete(source);
105
+ continue;
106
+ }
107
+ const scanned = active.scanFile(absolute, opts.root, currentConfig.scan?.modules);
108
+ if (scanned.length > 0)
109
+ nextSites.set(source, scanned);
110
+ else
111
+ nextSites.delete(source);
112
+ for (const site of scanned)
113
+ changedFps.add(fingerprint(site.query));
114
+ }
115
+ for (const source of affectedSources)
116
+ dirtyFiles.delete(source);
117
+ }
118
+ catch (error) {
119
+ for (const source of affectedSources)
120
+ dirtyFiles.add(source);
121
+ state.dirtyFiles = dirtyFiles;
122
+ throw error;
123
+ }
124
+ state.dirtyFiles = dirtyFiles;
125
+ }
126
+ const sites = [...nextSites.values()].flat();
127
+ const dirtyFps = state.dirtyFps ?? new Set();
128
+ const reuseCacheFps = new Set(sites
129
+ .map((site) => fingerprint(site.query))
130
+ .filter((fp) => !changedFps.has(fp) && !dirtyFps.has(fp)));
131
+ const result = await active.prepareOnce(opts, state.session, log, err, 1, {
132
+ sites,
133
+ reuseCacheFps,
134
+ reuseFunctions: !full,
135
+ });
136
+ state.sitesByFile = nextSites;
137
+ if (result.failures === 0) {
138
+ state.dirtyFps = new Set();
139
+ }
140
+ else {
141
+ state.dirtyFps = new Set([...dirtyFps, ...changedFps]);
142
+ }
143
+ return result;
144
+ }
145
+ function normalizePath(path) {
146
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
147
+ }
148
+ function projectPath(root, path) {
149
+ return normalizePath(relative(resolve(root), resolve(root, path)));
150
+ }
151
+ function isProjectGraphFile(path) {
152
+ const base = basename(path);
153
+ return CONFIG_FILES.has(base) || /^tsconfig(?:\.[^.]+)*\.json$/.test(base);
154
+ }
155
+ function groupSites(sites) {
156
+ const grouped = new Map();
157
+ for (const site of sites) {
158
+ const file = normalizePath(site.file);
159
+ const current = grouped.get(file) ?? [];
160
+ current.push(site);
161
+ grouped.set(file, current);
162
+ }
163
+ return grouped;
46
164
  }
47
165
  export async function runWatch(opts) {
48
166
  const stamp = () => new Date().toTimeString().slice(0, 8);
49
- const log = (m) => console.log(`[${stamp()}] ${m}`);
50
- const err = (m) => console.error(`[${stamp()}] ${m}`);
167
+ const event = (name, data = {}) => {
168
+ console.log(formatWatchEvent(name, data));
169
+ };
170
+ const log = opts.jsonl ? () => { } : (m) => console.log(`[${stamp()}] ${m}`);
171
+ const err = opts.jsonl ? () => { } : (m) => console.error(`[${stamp()}] ${m}`);
51
172
  const state = { session: null };
52
- log("watch: initial prepare");
173
+ const report = (result, durationMs) => {
174
+ if (!opts.jsonl)
175
+ return;
176
+ for (const diagnostic of result.diagnostics)
177
+ event("diagnostic", { diagnostic });
178
+ event("prepared", {
179
+ ok: result.failures === 0,
180
+ sites: result.sites,
181
+ entries: result.entries,
182
+ failures: result.failures,
183
+ pruned: result.pruned,
184
+ functions: result.functions,
185
+ ...(durationMs === undefined ? {} : { durationMs }),
186
+ });
187
+ };
188
+ if (opts.jsonl)
189
+ event("start", { root: opts.root });
190
+ else
191
+ log("watch: initial prepare");
53
192
  try {
54
193
  const r = await prepareWatchedOnce(opts, state, log, err);
55
- log(`watch: ready — ${r.entries} queries, ${r.failures} failures`);
194
+ if (opts.jsonl)
195
+ report(r);
196
+ else
197
+ log(`watch: ready — ${r.entries} queries, ${r.failures} failures`);
56
198
  }
57
199
  catch (e) {
58
- err(`watch: initial prepare failed — ${e.message}`);
200
+ if (opts.jsonl)
201
+ event("error", watchErrorData(e));
202
+ else
203
+ err(`watch: initial prepare failed — ${e.message}`);
59
204
  }
60
- log(`watch: monitoring ${opts.root}`);
205
+ if (opts.jsonl)
206
+ event("watching", { root: opts.root });
207
+ else
208
+ log(`watch: monitoring ${opts.root}`);
61
209
  let pending = false;
62
210
  let running = null;
63
211
  let timer = null;
212
+ const changedFiles = new Set();
64
213
  const trigger = () => {
65
214
  if (timer)
66
215
  clearTimeout(timer);
@@ -71,13 +220,22 @@ export async function runWatch(opts) {
71
220
  return;
72
221
  }
73
222
  const start = Date.now();
223
+ const batch = [...changedFiles];
224
+ changedFiles.clear();
74
225
  running = (async () => {
75
226
  try {
76
- const r = await prepareWatchedOnce(opts, state, log, err);
77
- log(`watch: re-prepared in ${Date.now() - start}ms (${r.entries} queries, ${r.failures} failures)`);
227
+ const r = await prepareWatchedOnce(opts, state, log, err, {}, batch);
228
+ const durationMs = Date.now() - start;
229
+ if (opts.jsonl)
230
+ report(r, durationMs);
231
+ else
232
+ log(`watch: re-prepared in ${durationMs}ms (${r.entries} queries, ${r.failures} failures)`);
78
233
  }
79
234
  catch (e) {
80
- err(`watch: prepare error — ${e.message}`);
235
+ if (opts.jsonl)
236
+ event("error", watchErrorData(e));
237
+ else
238
+ err(`watch: prepare error — ${e.message}`);
81
239
  }
82
240
  })();
83
241
  await running;
@@ -93,11 +251,16 @@ export async function runWatch(opts) {
93
251
  return;
94
252
  if (!shouldWatchFile(filename.toString()))
95
253
  return;
254
+ changedFiles.add(normalizePath(filename.toString()));
96
255
  trigger();
97
256
  });
98
257
  const shutdown = async () => {
99
- console.log();
100
- log("watch: stopping");
258
+ if (opts.jsonl)
259
+ event("stopping");
260
+ else {
261
+ console.log();
262
+ log("watch: stopping");
263
+ }
101
264
  watcher.close();
102
265
  await closeSession(state.session);
103
266
  process.exit(0);
@@ -14,5 +14,7 @@ export type FunctionEntry = {
14
14
  returns: string;
15
15
  returnsSet: boolean;
16
16
  };
17
+ export declare function functionCachePath(cacheDir: string): string;
18
+ export declare function functionCacheExists(cacheDir: string): boolean;
17
19
  export declare function readFunctionCache(cacheDir: string): FunctionEntry[];
18
20
  export declare function writeFunctionCache(cacheDir: string, functions: FunctionEntry[]): void;
@@ -1,9 +1,12 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { randomBytes } from "node:crypto";
4
- function cachePath(cacheDir) {
4
+ export function functionCachePath(cacheDir) {
5
5
  return join(cacheDir, "functions", "functions.json");
6
6
  }
7
+ export function functionCacheExists(cacheDir) {
8
+ return existsSync(functionCachePath(cacheDir));
9
+ }
7
10
  function parseFunctionCache(raw) {
8
11
  if (!raw || typeof raw !== "object")
9
12
  return [];
@@ -13,14 +16,14 @@ function parseFunctionCache(raw) {
13
16
  return obj.functions;
14
17
  }
15
18
  export function readFunctionCache(cacheDir) {
16
- const path = cachePath(cacheDir);
19
+ const path = functionCachePath(cacheDir);
17
20
  if (!existsSync(path))
18
21
  return [];
19
22
  const text = readFileSync(path, "utf8");
20
23
  return parseFunctionCache(JSON.parse(text));
21
24
  }
22
25
  export function writeFunctionCache(cacheDir, functions) {
23
- const path = cachePath(cacheDir);
26
+ const path = functionCachePath(cacheDir);
24
27
  mkdirSync(dirname(path), { recursive: true });
25
28
  const payload = { version: 1, functions };
26
29
  const tmp = `${path}.tmp-${randomBytes(4).toString("hex")}`;
@@ -23,17 +23,33 @@ export type { ScanConfig, SqlxJsConfig } from "./config.js";
23
23
  export type { SslMode, ConnConfig } from "./pg/wire.js";
24
24
  export { PgError, ConnectionLostError } from "./pg/wire.js";
25
25
  export { NoRowsError, TooManyRowsError } from "./runtime.js";
26
- export type { TransactionOptions, MigrateOptions, OnQueryEvent, OnQueryHook } from "./runtime.js";
26
+ export type { TransactionOptions, MigrateOptions, OnQueryEvent, OnQueryHook, OnQueryHookError } from "./runtime.js";
27
27
  export type { ExecuteResult, JsonParameter, PgArrayParameter } from "./runtime.js";
28
28
  export type { PostgresClient, PostgresOptions, CreateClientOptions } from "./postgres-runtime.js";
29
29
  export type TypedFile = TypedFileFor<KnownFileQueries>;
30
30
  export type TypedSql = TypedSqlFor<KnownQueries, KnownFileQueries>;
31
31
  export type Typed = TypedFor<KnownQueries, KnownFileQueries, import("./runtime.js").TransactionOptions>;
32
+ export type QueryRegistry = {
33
+ queries: object;
34
+ fileQueries: object;
35
+ };
36
+ export interface DefaultQueryRegistry {
37
+ queries: KnownQueries;
38
+ fileQueries: KnownFileQueries;
39
+ functions: KnownFunctions;
40
+ }
41
+ export type SqlClient<Registry extends QueryRegistry = DefaultQueryRegistry> = {
42
+ sql: TypedFor<Registry["queries"], Registry["fileQueries"], import("./runtime.js").TransactionOptions>;
43
+ unsafe: Unsafe;
44
+ client: import("./postgres-runtime.js").PostgresClient;
45
+ close: () => Promise<void>;
46
+ };
32
47
  export declare const sql: Typed;
33
48
  export type Unsafe = (query: string, ...params: unknown[]) => Promise<Record<string, unknown>[]>;
34
49
  export declare const unsafe: Unsafe;
35
50
  export declare const getClient: typeof rt.getClient;
36
51
  export declare const setClient: typeof rt.setClient;
37
52
  export declare const createClient: typeof rt.createClient;
53
+ export declare function createSqlClient<Registry extends QueryRegistry = DefaultQueryRegistry>(url?: string, options?: import("./postgres-runtime.js").CreateClientOptions): SqlClient<Registry>;
38
54
  export declare const close: typeof rt.close;
39
55
  export { migrate, clearSqlFileCache, encodePgArrayLiteral, id, json, array } from "./runtime.js";
package/dist/src/index.js CHANGED
@@ -7,5 +7,8 @@ export const unsafe = rt.unsafe;
7
7
  export const getClient = rt.getClient;
8
8
  export const setClient = rt.setClient;
9
9
  export const createClient = rt.createClient;
10
+ export function createSqlClient(url, options) {
11
+ return rt.createSqlClient(url, options);
12
+ }
10
13
  export const close = rt.close;
11
14
  export { migrate, clearSqlFileCache, encodePgArrayLiteral, id, json, array } from "./runtime.js";
@@ -68,6 +68,7 @@ export declare class SchemaCache {
68
68
  name: string;
69
69
  } | undefined;
70
70
  loadTableNamesByOid(oids: number[]): Promise<void>;
71
+ private recordTable;
71
72
  columnNameByAttno(tableOid: number, attno: number): string | undefined;
72
73
  loadColumnsForTables(tableOids: number[]): Promise<void>;
73
74
  columnsOf(tableOid: number): Map<string, ColumnInfo> | undefined;