@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.
- package/README.md +42 -13
- package/ROADMAP.md +2 -2
- package/dist/bin/sqlx-js.js +38 -16
- package/dist/src/cache.d.ts +1 -1
- package/dist/src/cache.js +1 -1
- package/dist/src/codegen.js +27 -14
- package/dist/src/commands/init.js +13 -3
- package/dist/src/commands/prepare.d.ts +8 -1
- package/dist/src/commands/prepare.js +99 -30
- package/dist/src/commands/watch.d.ts +14 -6
- package/dist/src/commands/watch.js +184 -21
- package/dist/src/function-cache.d.ts +2 -0
- package/dist/src/function-cache.js +6 -3
- package/dist/src/index.d.ts +17 -1
- package/dist/src/index.js +3 -0
- package/dist/src/pg/schema.d.ts +1 -0
- package/dist/src/pg/schema.js +42 -31
- package/dist/src/pg/wire.d.ts +1 -0
- package/dist/src/pg/wire.js +6 -0
- package/dist/src/postgres-runtime.d.ts +11 -1
- package/dist/src/postgres-runtime.js +22 -5
- package/dist/src/runtime.d.ts +5 -2
- package/dist/src/runtime.js +33 -12
- package/dist/src/scan/scanner.js +80 -13
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
-
|
|
307
|
-
sites =
|
|
312
|
+
if (input.sites) {
|
|
313
|
+
sites = input.sites;
|
|
308
314
|
}
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
337
|
+
const reusedEntries = [];
|
|
338
|
+
const reusedGenerated = [];
|
|
326
339
|
const { client, schema, userCfg } = session;
|
|
327
|
-
const
|
|
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
|
|
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
|
|
459
|
-
dmlTablesToLoad.set(
|
|
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
|
-
|
|
548
|
-
functions =
|
|
578
|
+
if (input.reuseFunctions && functionCacheExists(opts.cacheDir)) {
|
|
579
|
+
functions = readFunctionCache(opts.cacheDir);
|
|
549
580
|
}
|
|
550
|
-
|
|
551
|
-
|
|
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
|
|
677
|
+
mode,
|
|
641
678
|
sites: sites.length,
|
|
642
|
-
entries: unique.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
45
|
-
|
|
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
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
if (opts.jsonl)
|
|
201
|
+
event("error", watchErrorData(e));
|
|
202
|
+
else
|
|
203
|
+
err(`watch: initial prepare failed — ${e.message}`);
|
|
59
204
|
}
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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")}`;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/src/pg/schema.d.ts
CHANGED
|
@@ -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;
|