@onreza/sqlx-js 0.2.0 → 0.4.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.
@@ -16,36 +16,38 @@ function entrySignature(e) {
16
16
  });
17
17
  return { params, row: `{ ${cols.join("; ")} }` };
18
18
  }
19
- export function emitDts(outPath, entries) {
19
+ export function emitDts(outPath, entries, functions = []) {
20
20
  mkdirSync(dirname(outPath), { recursive: true });
21
21
  const lines = [];
22
22
  lines.push("// AUTO-GENERATED by sqlx-js. Do not edit.");
23
23
  lines.push("// Run `sqlx-js prepare` to regenerate.");
24
24
  lines.push("");
25
- emitModule(lines, "@onreza/sqlx-js", entries);
25
+ emitModule(lines, "@onreza/sqlx-js", entries, functions);
26
26
  lines.push("");
27
27
  lines.push("export {};");
28
28
  writeFileSync(outPath, lines.join("\n") + "\n");
29
29
  }
30
- function emitModule(lines, moduleName, entries) {
30
+ function emitModule(lines, moduleName, entries, functions) {
31
31
  lines.push(`declare module ${JSON.stringify(moduleName)} {`);
32
32
  lines.push(" interface KnownQueries {");
33
33
  const inlineSeen = new Set();
34
- const inlineEntries = entries.filter((e) => {
35
- if (e.hasInline === true)
36
- return true;
37
- if (e.hasInline === false)
38
- return false;
39
- return !e.filePaths || e.filePaths.length === 0;
40
- });
41
- for (const e of inlineEntries.slice().sort((a, b) => a.query.localeCompare(b.query))) {
42
- if (inlineSeen.has(e.query))
34
+ const inlinePairs = [];
35
+ for (const e of entries) {
36
+ const emitInline = e.hasInline === true || (e.hasInline !== false && (!e.filePaths || e.filePaths.length === 0));
37
+ if (!emitInline)
38
+ continue;
39
+ const queries = e.inlineQueries && e.inlineQueries.length > 0 ? e.inlineQueries : [e.query];
40
+ for (const query of queries)
41
+ inlinePairs.push({ query, entry: e });
42
+ }
43
+ for (const { query, entry } of inlinePairs.slice().sort((a, b) => a.query.localeCompare(b.query))) {
44
+ if (inlineSeen.has(query))
43
45
  continue;
44
- inlineSeen.add(e.query);
45
- const { params, row } = entrySignature(e);
46
- if (e.degraded)
47
- lines.push(` /** Nullability inference degraded: ${e.degraded.reason}. All result columns conservatively typed as nullable. */`);
48
- lines.push(` ${JSON.stringify(e.query)}: { params: ${params}; row: ${row} };`);
46
+ inlineSeen.add(query);
47
+ const { params, row } = entrySignature(entry);
48
+ if (entry.degraded)
49
+ lines.push(` /** Nullability inference degraded: ${entry.degraded.reason}. All result columns conservatively typed as nullable. */`);
50
+ lines.push(` ${JSON.stringify(query)}: { params: ${params}; row: ${row} };`);
49
51
  }
50
52
  lines.push(" }");
51
53
  lines.push("");
@@ -68,5 +70,22 @@ function emitModule(lines, moduleName, entries) {
68
70
  lines.push(` ${JSON.stringify(path)}: { params: ${params}; row: ${row} };`);
69
71
  }
70
72
  lines.push(" }");
73
+ lines.push("");
74
+ lines.push(" interface KnownFunctions {");
75
+ const functionSeen = new Set();
76
+ for (const fn of functions.slice().sort((a, b) => a.signature.localeCompare(b.signature))) {
77
+ if (functionSeen.has(fn.signature))
78
+ continue;
79
+ functionSeen.add(fn.signature);
80
+ const params = inputParams(fn);
81
+ lines.push(` ${JSON.stringify(fn.signature)}: { kind: ${JSON.stringify(fn.kind)}; params: ${params}; returns: ${fn.returns}; returnsSet: ${fn.returnsSet} };`);
82
+ }
83
+ lines.push(" }");
71
84
  lines.push("}");
72
85
  }
86
+ function inputParams(fn) {
87
+ const params = fn.params
88
+ .filter((p) => p.mode === "in" || p.mode === "inout" || p.mode === "variadic")
89
+ .map((p) => p.tsType);
90
+ return params.length === 0 ? "[]" : `[${params.join(", ")}]`;
91
+ }
@@ -1,5 +1,6 @@
1
1
  export type InitOptions = {
2
2
  root: string;
3
+ schemaProvider?: "builtin" | "pgschema";
3
4
  log?: (msg: string) => void;
4
5
  };
5
6
  export declare function runInit(opts: InitOptions): void;
@@ -11,6 +11,20 @@ const config: SqlxJsConfig = {
11
11
  customTypes: {},
12
12
  };
13
13
 
14
+ export default config;
15
+ `;
16
+ const PGSCHEMA_CONFIG_TEMPLATE = `import type { SqlxJsConfig } from "@onreza/sqlx-js";
17
+
18
+ const config: SqlxJsConfig = {
19
+ schema: {
20
+ provider: "pgschema",
21
+ file: "schema.sql",
22
+ schemas: ["public"],
23
+ },
24
+ jsonbTypes: {},
25
+ customTypes: {},
26
+ };
27
+
14
28
  export default config;
15
29
  `;
16
30
  const ENV_TEMPLATE = `# Connection string used by sqlx-js prepare/migrate and the runtime.
@@ -18,6 +32,11 @@ DATABASE_URL=postgres://user:password@localhost:5432/your_db
18
32
  # Managed Postgres with TLS:
19
33
  # DATABASE_URL=postgres://user:password@db.example.com:5432/your_db?sslmode=verify-full
20
34
  `;
35
+ const PGSCHEMA_TEMPLATE = `-- Desired PostgreSQL schema managed by pgschema.
36
+ -- Edit this file, then run:
37
+ -- sqlx-js db plan -- --output-json plan.json
38
+ -- sqlx-js db apply -- --auto-approve
39
+ `;
21
40
  export function runInit(opts) {
22
41
  const log = opts.log ?? console.log;
23
42
  const created = [];
@@ -41,8 +60,12 @@ export function runInit(opts) {
41
60
  mkdirSync(full, { recursive: true });
42
61
  created.push(`${rel}/`);
43
62
  };
44
- ensureFile("sqlx-js.config.ts", CONFIG_TEMPLATE);
45
- ensureDir("migrations");
63
+ const schemaProvider = opts.schemaProvider ?? "builtin";
64
+ ensureFile("sqlx-js.config.ts", schemaProvider === "pgschema" ? PGSCHEMA_CONFIG_TEMPLATE : CONFIG_TEMPLATE);
65
+ if (schemaProvider === "pgschema")
66
+ ensureFile("schema.sql", PGSCHEMA_TEMPLATE);
67
+ else
68
+ ensureDir("migrations");
46
69
  ensureFile(".env.example", ENV_TEMPLATE);
47
70
  for (const f of created)
48
71
  log(`created ${f}`);
@@ -51,7 +74,15 @@ export function runInit(opts) {
51
74
  log("");
52
75
  log("Next steps:");
53
76
  log(" 1. Set DATABASE_URL (see .env.example).");
54
- log(" 2. Add a migration: sqlx-js migrate add init");
55
- log(" 3. Make sure tsconfig.json \"include\" covers sqlx-js-env.d.ts.");
56
- log(" 4. Develop locally: sqlx-js migrate dev");
77
+ if (schemaProvider === "pgschema") {
78
+ log(" 2. Install managed pgschema: sqlx-js db install");
79
+ log(" 3. Verify it: sqlx-js db check");
80
+ log(" 4. Edit schema.sql, then review: sqlx-js db plan");
81
+ log(" 5. After applying schema changes, run: sqlx-js prepare");
82
+ }
83
+ else {
84
+ log(" 2. Add a migration: sqlx-js migrate add init");
85
+ log(" 3. Make sure tsconfig.json \"include\" covers sqlx-js-env.d.ts.");
86
+ log(" 4. Develop locally: sqlx-js migrate dev");
87
+ }
57
88
  }
@@ -0,0 +1,25 @@
1
+ import type { SqlxJsConfig } from "../config.js";
2
+ export type PgschemaSubcommand = "check" | "plan" | "apply";
3
+ export declare const PGSCHEMA_VERSION = "1.12.0";
4
+ export type PgschemaAsset = {
5
+ key: string;
6
+ name: string;
7
+ sha256: string;
8
+ };
9
+ export type PgschemaCommandOptions = {
10
+ root: string;
11
+ databaseUrl: string;
12
+ config: SqlxJsConfig;
13
+ subcommand: PgschemaSubcommand;
14
+ passthrough?: string[];
15
+ };
16
+ export type PgschemaInstallOptions = {
17
+ root: string;
18
+ asset?: PgschemaAsset;
19
+ baseUrl?: string;
20
+ log?: (msg: string) => void;
21
+ };
22
+ export declare function resolvePgschemaAsset(platform?: NodeJS.Platform, arch?: NodeJS.Architecture): PgschemaAsset;
23
+ export declare function managedPgschemaPath(root: string, asset?: PgschemaAsset): string;
24
+ export declare function runPgschemaInstall(opts: PgschemaInstallOptions): Promise<void>;
25
+ export declare function runPgschemaCommand(opts: PgschemaCommandOptions): void;
@@ -0,0 +1,178 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { parseDatabaseUrl } from "../pg/wire.js";
6
+ export const PGSCHEMA_VERSION = "1.12.0";
7
+ const PGSCHEMA_BASE_URL = `https://github.com/pgplex/pgschema/releases/download/v${PGSCHEMA_VERSION}`;
8
+ const WINDOWS_UNSUPPORTED = "sqlx-js db: pgschema is not supported on Windows. Run sqlx-js under WSL/Linux/macOS or use the built-in sqlx-js migrate workflow.";
9
+ const PGSCHEMA_ASSETS = {
10
+ "darwin:x64": {
11
+ key: "darwin-amd64",
12
+ name: `pgschema-${PGSCHEMA_VERSION}-darwin-amd64`,
13
+ sha256: "c64b2ac24c4246344908910e892c4123be282bbb449f0b535079ff41d0f47c8f",
14
+ },
15
+ "darwin:arm64": {
16
+ key: "darwin-arm64",
17
+ name: `pgschema-${PGSCHEMA_VERSION}-darwin-arm64`,
18
+ sha256: "f01ea488f21700752d5747bc013c406daa583a68b631739f33af430d5d3ec449",
19
+ },
20
+ "linux:x64": {
21
+ key: "linux-amd64",
22
+ name: `pgschema-${PGSCHEMA_VERSION}-linux-amd64`,
23
+ sha256: "12610adf748b0dafe4e488ee7e9e68e6ffbef1f4e0f038dda36cf0138eede598",
24
+ },
25
+ "linux:arm64": {
26
+ key: "linux-arm64",
27
+ name: `pgschema-${PGSCHEMA_VERSION}-linux-arm64`,
28
+ sha256: "58ec57023954a0239cf9d607c4e5432da6dd0b279399d1c318204120619a221d",
29
+ },
30
+ };
31
+ export function resolvePgschemaAsset(platform = process.platform, arch = process.arch) {
32
+ if (platform === "win32")
33
+ throw new Error(WINDOWS_UNSUPPORTED);
34
+ const asset = PGSCHEMA_ASSETS[`${platform}:${arch}`];
35
+ if (!asset)
36
+ throw new Error(`sqlx-js db install: unsupported platform ${platform}/${arch}`);
37
+ return asset;
38
+ }
39
+ function pgschemaConfig(config) {
40
+ if (config.schema?.provider !== "pgschema") {
41
+ throw new Error("sqlx-js db: set schema.provider = \"pgschema\" in sqlx-js.config.ts");
42
+ }
43
+ return config.schema;
44
+ }
45
+ export function managedPgschemaPath(root, asset = resolvePgschemaAsset()) {
46
+ return join(root, "node_modules/.cache/sqlx-js/pgschema", `v${PGSCHEMA_VERSION}`, asset.key, "pgschema");
47
+ }
48
+ function maybeManagedPgschemaPath(root) {
49
+ try {
50
+ const asset = resolvePgschemaAsset();
51
+ const managed = managedPgschemaPath(root, asset);
52
+ if (!existsSync(managed))
53
+ return undefined;
54
+ if (sha256(readFileSync(managed)) !== asset.sha256) {
55
+ throw new Error(`sqlx-js db: managed pgschema checksum mismatch at ${managed}. Run sqlx-js db install.`);
56
+ }
57
+ chmodSync(managed, 0o755);
58
+ return managed;
59
+ }
60
+ catch (e) {
61
+ if (e.message.includes("checksum mismatch"))
62
+ throw e;
63
+ return undefined;
64
+ }
65
+ }
66
+ function commandName(root, config) {
67
+ if (process.platform === "win32")
68
+ throw new Error(WINDOWS_UNSUPPORTED);
69
+ if (config.command)
70
+ return config.command;
71
+ const managed = maybeManagedPgschemaPath(root);
72
+ if (managed && existsSync(managed))
73
+ return managed;
74
+ return "pgschema";
75
+ }
76
+ function schemaFile(root, config) {
77
+ return resolve(root, config.file ?? "schema.sql");
78
+ }
79
+ function appliesPlan(subcommand, passthrough) {
80
+ return subcommand === "apply" && (passthrough ?? []).some((arg) => arg === "--plan" || arg.startsWith("--plan="));
81
+ }
82
+ function installHint(command) {
83
+ return `sqlx-js db: ${command} was not found. Run sqlx-js db install or set schema.command in sqlx-js.config.ts.`;
84
+ }
85
+ function run(command, args, env) {
86
+ const child = spawnSync(command, args, { encoding: "utf8", env, stdio: "inherit" });
87
+ if (child.error) {
88
+ const code = child.error.code;
89
+ if (code === "ENOENT")
90
+ throw new Error(installHint(command));
91
+ throw child.error;
92
+ }
93
+ if (child.signal)
94
+ throw new Error(`sqlx-js db: ${command} terminated by signal ${child.signal}`);
95
+ if (child.status && child.status !== 0)
96
+ process.exit(child.status);
97
+ }
98
+ function pgschemaEnv(db) {
99
+ return {
100
+ ...process.env,
101
+ ...(db.password ? { PGPASSWORD: db.password } : {}),
102
+ ...(db.sslmode ? { PGSSLMODE: db.sslmode } : {}),
103
+ ...(db.sslRootCert ? { PGSSLROOTCERT: db.sslRootCert } : {}),
104
+ ...(db.sslCert ? { PGSSLCERT: db.sslCert } : {}),
105
+ ...(db.sslKey ? { PGSSLKEY: db.sslKey } : {}),
106
+ };
107
+ }
108
+ function sha256(data) {
109
+ return createHash("sha256").update(data).digest("hex");
110
+ }
111
+ export async function runPgschemaInstall(opts) {
112
+ const asset = opts.asset ?? resolvePgschemaAsset();
113
+ const baseUrl = opts.baseUrl ?? PGSCHEMA_BASE_URL;
114
+ const log = opts.log ?? console.log;
115
+ const target = managedPgschemaPath(opts.root, asset);
116
+ if (existsSync(target) && sha256(readFileSync(target)) === asset.sha256) {
117
+ chmodSync(target, 0o755);
118
+ log(`pgschema v${PGSCHEMA_VERSION} already installed at ${target}`);
119
+ return;
120
+ }
121
+ const url = `${baseUrl.replace(/\/$/, "")}/${asset.name}`;
122
+ const response = await fetch(url);
123
+ if (!response.ok) {
124
+ throw new Error(`sqlx-js db install: failed to download pgschema v${PGSCHEMA_VERSION}: HTTP ${response.status}`);
125
+ }
126
+ const bytes = new Uint8Array(await response.arrayBuffer());
127
+ const actual = sha256(bytes);
128
+ if (actual !== asset.sha256) {
129
+ throw new Error(`sqlx-js db install: checksum mismatch for ${asset.name}`);
130
+ }
131
+ mkdirSync(dirname(target), { recursive: true });
132
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
133
+ try {
134
+ writeFileSync(tmp, bytes, { mode: 0o755 });
135
+ chmodSync(tmp, 0o755);
136
+ renameSync(tmp, target);
137
+ }
138
+ catch (e) {
139
+ rmSync(tmp, { force: true });
140
+ throw e;
141
+ }
142
+ writeFileSync(`${target}.json`, JSON.stringify({ version: PGSCHEMA_VERSION, asset: asset.name, sha256: asset.sha256 }, null, 2) + "\n");
143
+ log(`installed pgschema v${PGSCHEMA_VERSION} to ${target}`);
144
+ }
145
+ export function runPgschemaCommand(opts) {
146
+ const config = pgschemaConfig(opts.config);
147
+ const command = commandName(opts.root, config);
148
+ if (opts.subcommand === "check") {
149
+ run(command, ["--help"], process.env);
150
+ return;
151
+ }
152
+ if (!opts.databaseUrl)
153
+ throw new Error("DATABASE_URL is required for sqlx-js db commands");
154
+ const file = appliesPlan(opts.subcommand, opts.passthrough) ? undefined : schemaFile(opts.root, config);
155
+ if (file && !existsSync(file))
156
+ throw new Error(`sqlx-js db: schema file not found: ${file}`);
157
+ const db = parseDatabaseUrl(opts.databaseUrl);
158
+ const schemas = pgschemaSchemas(config);
159
+ const args = [
160
+ opts.subcommand,
161
+ "--host", db.host,
162
+ "--port", String(db.port),
163
+ "--db", db.database,
164
+ "--user", db.user,
165
+ ];
166
+ if (file)
167
+ args.push("--file", file);
168
+ args.push("--schema", schemas[0]);
169
+ args.push(...opts.passthrough ?? []);
170
+ run(command, args, pgschemaEnv(db));
171
+ }
172
+ function pgschemaSchemas(config) {
173
+ const schemas = config.schemas?.length ? config.schemas : ["public"];
174
+ if (schemas.length > 1) {
175
+ throw new Error("sqlx-js db: pgschema 1.12.0 supports exactly one --schema value; split plan/apply per schema or use a single schema in sqlx-js.config.ts");
176
+ }
177
+ return schemas;
178
+ }
@@ -32,6 +32,7 @@ export declare function prepareOnce(opts: PrepareOptions, session: PrepareSessio
32
32
  entries: number;
33
33
  failures: number;
34
34
  pruned: number;
35
+ functions: number;
35
36
  }>;
36
37
  export declare function runPrepare(opts: PrepareOptions): Promise<void>;
37
38
  export {};
@@ -6,10 +6,13 @@ import { scanProject } from "../scan/scanner.js";
6
6
  import { Cache, fingerprint, effectiveNullable } from "../cache.js";
7
7
  import { emitDts } from "../codegen.js";
8
8
  import { loadConfig, lookupJsonbType } from "../config.js";
9
+ import { readFunctionCache, writeFunctionCache } from "../function-cache.js";
10
+ import { introspectFunctions } from "../pg/functions.js";
9
11
  import { buildParamMap } from "../pg/param-map.js";
10
12
  import { mergeExtensionTypes } from "../pg/extensions.js";
11
13
  const JSON_OIDS = new Set([114, 3802]);
12
14
  const JSON_ARRAY_OIDS = new Set([199, 3807]);
15
+ const JSON_INPUT = 'import("@onreza/sqlx-js").JsonInput';
13
16
  function enumUnion(values) {
14
17
  if (values.length === 0)
15
18
  return "never";
@@ -56,13 +59,28 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
56
59
  if (JSON_OIDS.has(paramOid) || JSON_ARRAY_OIDS.has(paramOid)) {
57
60
  const target = paramMap.get(paramIndex);
58
61
  if (target) {
59
- const decl = lookupJsonbType(cfg, target.schema ?? "public", target.table, target.column);
62
+ const column = resolveTargetColumn(target, schema);
63
+ const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
60
64
  if (decl)
61
65
  return JSON_ARRAY_OIDS.has(paramOid) ? `(${decl})[]` : decl;
62
66
  }
67
+ return JSON_ARRAY_OIDS.has(paramOid) ? `(${JSON_INPUT})[]` : JSON_INPUT;
63
68
  }
64
69
  return resolveTs(paramOid, (oid) => schema.customType(oid));
65
70
  }
71
+ function resolveTargetColumn(target, schema) {
72
+ if (target.column)
73
+ return target.column;
74
+ if (target.columnIndex === undefined)
75
+ return undefined;
76
+ const oid = schema.resolveTable(target.schema, target.table);
77
+ if (oid === undefined)
78
+ return undefined;
79
+ const cols = schema.columnsOf(oid);
80
+ if (!cols)
81
+ return undefined;
82
+ return [...cols.values()].sort((a, b) => a.attnum - b.attnum)[target.columnIndex - 1]?.name;
83
+ }
66
84
  function resolveParamNullable(paramIndex, pm, schema) {
67
85
  if (pm.forceNullable.has(paramIndex))
68
86
  return true;
@@ -73,7 +91,10 @@ function resolveParamNullable(paramIndex, pm, schema) {
73
91
  const oid = schema.resolveTable(t.schema, t.table);
74
92
  if (oid === undefined)
75
93
  return false;
76
- const col = schema.columnsOf(oid)?.get(t.column);
94
+ const column = resolveTargetColumn(t, schema);
95
+ if (!column)
96
+ return false;
97
+ const col = schema.columnsOf(oid)?.get(column);
77
98
  if (!col)
78
99
  return false;
79
100
  return !col.notNull;
@@ -100,6 +121,15 @@ function snippet(query, max = 80) {
100
121
  const oneLine = query.replace(/\s+/g, " ").trim();
101
122
  return oneLine.length > max ? oneLine.slice(0, max) + "…" : oneLine;
102
123
  }
124
+ function siteUsage(sites) {
125
+ const inlineQueries = Array.from(new Set(sites.filter((s) => s.kind !== "file").map((s) => s.query))).sort();
126
+ const filePaths = Array.from(new Set(sites.filter((s) => s.kind === "file").map((s) => s.sqlFilePath).filter(Boolean))).sort();
127
+ return {
128
+ hasInline: inlineQueries.length > 0,
129
+ ...(inlineQueries.length > 0 ? { inlineQueries } : {}),
130
+ ...(filePaths.length > 0 ? { filePaths } : {}),
131
+ };
132
+ }
103
133
  export async function openSession(opts) {
104
134
  const userCfg = await loadConfig(opts.root);
105
135
  const cfg = parseDatabaseUrl(opts.databaseUrl);
@@ -284,13 +314,9 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
284
314
  forceNullable: new Set(),
285
315
  dmlBound: new Set(),
286
316
  };
287
- const fileSites = r.sites.filter((s) => s.kind === "file");
288
- const hasInline = r.sites.some((s) => s.kind !== "file");
289
- const filePaths = fileSites.length > 0
290
- ? Array.from(new Set(fileSites.map((s) => s.sqlFilePath))).sort()
291
- : undefined;
292
317
  const entry = {
293
318
  query: r.query,
319
+ ...siteUsage(r.sites),
294
320
  paramOids: r.paramOids,
295
321
  paramTsTypes: r.paramOids.map((o, idx) => resolveParamTs(idx + 1, o, pm.targets, schema, userCfg)),
296
322
  paramNullable: r.paramOids.map((_o, idx) => resolveParamNullable(idx + 1, pm, schema)),
@@ -306,8 +332,6 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
306
332
  };
307
333
  }),
308
334
  hasResultSet: r.fields.length > 0,
309
- hasInline,
310
- ...(filePaths ? { filePaths } : {}),
311
335
  ...(analysis.degraded ? { degraded: analysis.degraded } : {}),
312
336
  };
313
337
  cache.write(r.fp, entry);
@@ -322,8 +346,13 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
322
346
  if (pruned > 0)
323
347
  log(`pruned ${pruned} orphaned cache entry/entries`);
324
348
  }
325
- emitDts(opts.dtsPath, entries);
326
- return { entries: entries.length, failures, pruned };
349
+ let functions = [];
350
+ if (failures === 0) {
351
+ functions = await introspectFunctions(client, schema);
352
+ writeFunctionCache(opts.cacheDir, functions);
353
+ }
354
+ emitDts(opts.dtsPath, entries, functions);
355
+ return { entries: entries.length, failures, pruned, functions: functions.length };
327
356
  }
328
357
  export async function runPrepare(opts) {
329
358
  if (opts.check) {
@@ -351,9 +380,13 @@ export async function runPrepare(opts) {
351
380
  console.error(`\nsqlx-js prepare --check: ${stale} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
352
381
  process.exit(1);
353
382
  }
354
- const entries = [...unique.values()].map((u) => cache.read(u.fp)).filter(Boolean);
355
- emitDts(opts.dtsPath, entries);
356
- console.log(`ok ${entries.length} unique queries, types regenerated`);
383
+ const entries = [...unique.values()].map((u) => {
384
+ const entry = cache.read(u.fp);
385
+ return entry ? { ...entry, ...siteUsage(u.sites) } : null;
386
+ }).filter((e) => e !== null);
387
+ const functions = readFunctionCache(opts.cacheDir);
388
+ emitDts(opts.dtsPath, entries, functions);
389
+ console.log(`ok — ${entries.length} unique queries, ${functions.length} function(s), types regenerated`);
357
390
  return;
358
391
  }
359
392
  const session = await openSession(opts);
@@ -364,7 +397,7 @@ export async function runPrepare(opts) {
364
397
  await session.client.end();
365
398
  process.exit(1);
366
399
  }
367
- console.log(`\nprepared ${r.entries} unique query/queries → ${opts.dtsPath}`);
400
+ console.log(`\nprepared ${r.entries} unique query/queries, ${r.functions} function(s) → ${opts.dtsPath}`);
368
401
  }
369
402
  finally {
370
403
  await session.client.end();
@@ -1,6 +1,12 @@
1
1
  export type SqlxJsConfig = {
2
2
  jsonbTypes?: Record<string, string>;
3
3
  customTypes?: Record<string, string>;
4
+ schema?: {
5
+ provider?: "builtin" | "pgschema";
6
+ file?: string;
7
+ schemas?: string[];
8
+ command?: string;
9
+ };
4
10
  };
5
11
  export declare function loadConfig(root: string): Promise<SqlxJsConfig>;
6
12
  export declare function lookupJsonbType(cfg: SqlxJsConfig, schema: string, table: string, column: string): string | undefined;
@@ -0,0 +1,18 @@
1
+ export type FunctionKind = "function" | "procedure" | "aggregate" | "window";
2
+ export type FunctionParamMode = "in" | "out" | "inout" | "variadic" | "table";
3
+ export type FunctionParamEntry = {
4
+ mode: FunctionParamMode;
5
+ tsType: string;
6
+ name?: string;
7
+ };
8
+ export type FunctionEntry = {
9
+ schema: string;
10
+ name: string;
11
+ signature: string;
12
+ kind: FunctionKind;
13
+ params: FunctionParamEntry[];
14
+ returns: string;
15
+ returnsSet: boolean;
16
+ };
17
+ export declare function readFunctionCache(cacheDir: string): FunctionEntry[];
18
+ export declare function writeFunctionCache(cacheDir: string, functions: FunctionEntry[]): void;
@@ -0,0 +1,38 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ function cachePath(cacheDir) {
5
+ return join(cacheDir, "functions", "functions.json");
6
+ }
7
+ function parseFunctionCache(raw) {
8
+ if (!raw || typeof raw !== "object")
9
+ return [];
10
+ const obj = raw;
11
+ if (obj.version !== 1 || !Array.isArray(obj.functions))
12
+ return [];
13
+ return obj.functions;
14
+ }
15
+ export function readFunctionCache(cacheDir) {
16
+ const path = cachePath(cacheDir);
17
+ if (!existsSync(path))
18
+ return [];
19
+ const text = readFileSync(path, "utf8");
20
+ return parseFunctionCache(JSON.parse(text));
21
+ }
22
+ export function writeFunctionCache(cacheDir, functions) {
23
+ const path = cachePath(cacheDir);
24
+ mkdirSync(dirname(path), { recursive: true });
25
+ const payload = { version: 1, functions };
26
+ const tmp = `${path}.tmp-${randomBytes(4).toString("hex")}`;
27
+ writeFileSync(tmp, JSON.stringify(payload, null, 2));
28
+ try {
29
+ renameSync(tmp, path);
30
+ }
31
+ catch (err) {
32
+ try {
33
+ unlinkSync(tmp);
34
+ }
35
+ catch { }
36
+ throw err;
37
+ }
38
+ }
@@ -4,6 +4,20 @@ export interface KnownQueries {
4
4
  }
5
5
  export interface KnownFileQueries {
6
6
  }
7
+ export interface KnownFunctions {
8
+ }
9
+ export type JsonPrimitive = string | number | boolean | null;
10
+ export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
11
+ export type JsonObject = {
12
+ readonly [key: string]: JsonValue;
13
+ };
14
+ export type JsonArray = readonly JsonValue[];
15
+ export type JsonInput = string | number | boolean | JsonInputObject | JsonInputArray;
16
+ export type JsonInputValue = JsonPrimitive | JsonInputObject | JsonInputArray;
17
+ export type JsonInputObject = {
18
+ readonly [key: string]: JsonInputValue | undefined;
19
+ };
20
+ export type JsonInputArray = readonly JsonInputValue[];
7
21
  export type { SqlxJsConfig } from "./config.js";
8
22
  export type { SslMode, ConnConfig } from "./pg/wire.js";
9
23
  export { PgError, ConnectionLostError } from "./pg/wire.js";
@@ -0,0 +1,4 @@
1
+ import type { FunctionEntry } from "../function-cache.js";
2
+ import { type PgClient } from "./wire.js";
3
+ import { SchemaCache } from "./schema.js";
4
+ export declare function introspectFunctions(client: PgClient, schema: SchemaCache): Promise<FunctionEntry[]>;