@onreza/sqlx-js 0.7.0 → 0.9.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/dist/src/cache.js CHANGED
@@ -1,143 +1,23 @@
1
- import { createHash, randomBytes } from "node:crypto";
1
+ import { randomBytes } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { isBuiltinOid } from "./pg/oids.js";
5
- export const CACHE_FORMAT_VERSION = 2;
6
- export const GENERATOR_REVISION = 4;
5
+ import { queryId } from "./query-id.js";
6
+ import { rewriteNamedParameters } from "./sql-params.js";
7
+ export const CACHE_FORMAT_VERSION = 3;
8
+ export const GENERATOR_REVISION = 6;
7
9
  export const CACHE_MANIFEST_FILE = "cache-manifest.json";
10
+ export class CacheManifestStaleError extends Error {
11
+ constructor(path) {
12
+ super(`sqlx-js: cache manifest is stale: ${path}. Run \`sqlx-js prepare\`.`);
13
+ this.name = "CacheManifestStaleError";
14
+ }
15
+ }
8
16
  export function portableCacheOid(oid) {
9
17
  return isBuiltinOid(oid) ? oid : 0;
10
18
  }
11
19
  export function fingerprint(query) {
12
- const norm = normalizeForFingerprint(query);
13
- return createHash("sha256").update(norm).digest("hex").slice(0, 16);
14
- }
15
- function normalizeForFingerprint(query) {
16
- let out = "";
17
- let pendingSpace = false;
18
- let i = 0;
19
- const emit = (text) => {
20
- if (pendingSpace && out.length > 0)
21
- out += " ";
22
- out += text;
23
- pendingSpace = false;
24
- };
25
- const markSpace = () => {
26
- if (out.length > 0)
27
- pendingSpace = true;
28
- };
29
- while (i < query.length) {
30
- const ch = query[i];
31
- if (/\s/.test(ch)) {
32
- markSpace();
33
- i++;
34
- continue;
35
- }
36
- if (ch === "-" && query[i + 1] === "-") {
37
- i = readLineComment(query, i);
38
- markSpace();
39
- continue;
40
- }
41
- if (ch === "/" && query[i + 1] === "*") {
42
- i = readBlockComment(query, i);
43
- markSpace();
44
- continue;
45
- }
46
- if (ch === "'") {
47
- const next = readSingleQuoted(query, i, isEscapeStringPrefix(query, i));
48
- emit(query.slice(i, next));
49
- i = next;
50
- continue;
51
- }
52
- if (ch === "\"") {
53
- const next = readQuotedIdentifier(query, i);
54
- emit(query.slice(i, next));
55
- i = next;
56
- continue;
57
- }
58
- if (ch === "$") {
59
- const next = readDollarQuoted(query, i);
60
- if (next !== null) {
61
- emit(query.slice(i, next));
62
- i = next;
63
- continue;
64
- }
65
- }
66
- emit(ch);
67
- i++;
68
- }
69
- return out;
70
- }
71
- function readSingleQuoted(query, start, escapeBackslash) {
72
- let i = start + 1;
73
- while (i < query.length) {
74
- const ch = query[i];
75
- if (escapeBackslash && ch === "\\") {
76
- i += 2;
77
- continue;
78
- }
79
- if (ch === "'") {
80
- if (query[i + 1] === "'") {
81
- i += 2;
82
- continue;
83
- }
84
- return i + 1;
85
- }
86
- i++;
87
- }
88
- return query.length;
89
- }
90
- function readQuotedIdentifier(query, start) {
91
- let i = start + 1;
92
- while (i < query.length) {
93
- if (query[i] === "\"") {
94
- if (query[i + 1] === "\"") {
95
- i += 2;
96
- continue;
97
- }
98
- return i + 1;
99
- }
100
- i++;
101
- }
102
- return query.length;
103
- }
104
- function readDollarQuoted(query, start) {
105
- let tagEnd = start + 1;
106
- while (tagEnd < query.length && /[A-Za-z0-9_]/.test(query[tagEnd]))
107
- tagEnd++;
108
- if (query[tagEnd] !== "$")
109
- return null;
110
- const tag = query.slice(start, tagEnd + 1);
111
- const end = query.indexOf(tag, tagEnd + 1);
112
- return end === -1 ? query.length : end + tag.length;
113
- }
114
- function readLineComment(query, start) {
115
- const end = query.indexOf("\n", start + 2);
116
- return end === -1 ? query.length : end + 1;
117
- }
118
- function readBlockComment(query, start) {
119
- let depth = 1;
120
- let i = start + 2;
121
- while (i < query.length && depth > 0) {
122
- if (query[i] === "/" && query[i + 1] === "*") {
123
- depth++;
124
- i += 2;
125
- continue;
126
- }
127
- if (query[i] === "*" && query[i + 1] === "/") {
128
- depth--;
129
- i += 2;
130
- continue;
131
- }
132
- i++;
133
- }
134
- return i;
135
- }
136
- function isEscapeStringPrefix(query, quoteIndex) {
137
- if (quoteIndex === 0 || query[quoteIndex - 1]?.toLowerCase() !== "e")
138
- return false;
139
- const beforePrefix = query[quoteIndex - 2];
140
- return beforePrefix === undefined || !/[A-Za-z0-9_$]/.test(beforePrefix);
20
+ return queryId(query);
141
21
  }
142
22
  export function effectiveNullable(c) {
143
23
  if (c.override === "non-null")
@@ -166,6 +46,27 @@ function assertEntryShape(fp, raw) {
166
46
  throw new Error(`sqlx-js: cache entry ${fp}.json is malformed`);
167
47
  }
168
48
  const cols = raw.columns;
49
+ const entry = raw;
50
+ let expectedNames;
51
+ try {
52
+ if (typeof entry.query !== "string")
53
+ throw new Error("query must be a string");
54
+ expectedNames = rewriteNamedParameters(entry.query).names;
55
+ }
56
+ catch {
57
+ throw new Error(`sqlx-js: cache entry ${fp}.json has malformed named parameter metadata. Run \`sqlx-js prepare\`.`);
58
+ }
59
+ if (entry.paramNames !== undefined || expectedNames.length > 0) {
60
+ if (!Array.isArray(entry.paramNames) ||
61
+ !entry.paramNames.every((name) => typeof name === "string") ||
62
+ !Array.isArray(entry.paramTsTypes) ||
63
+ entry.paramNames.length !== entry.paramTsTypes.length ||
64
+ new Set(entry.paramNames).size !== entry.paramNames.length ||
65
+ entry.paramNames.length !== expectedNames.length ||
66
+ entry.paramNames.some((name, index) => name !== expectedNames[index])) {
67
+ throw new Error(`sqlx-js: cache entry ${fp}.json has malformed named parameter metadata. Run \`sqlx-js prepare\`.`);
68
+ }
69
+ }
169
70
  if (cols.length > 0) {
170
71
  const c = cols[0];
171
72
  if ("forceNonNull" in c || "forceNullable" in c) {
@@ -297,7 +198,7 @@ export function readCacheManifest(cacheDir) {
297
198
  if (value.cacheFormat !== CACHE_FORMAT_VERSION ||
298
199
  value.generatorRevision !== GENERATOR_REVISION ||
299
200
  typeof value.configHash !== "string") {
300
- throw new Error(`sqlx-js: cache manifest is stale: ${path}. Run \`sqlx-js prepare\`.`);
201
+ throw new CacheManifestStaleError(path);
301
202
  }
302
203
  return value;
303
204
  }
@@ -307,7 +208,7 @@ export function assertCacheManifest(cacheDir, configHash) {
307
208
  throw new Error(`sqlx-js: cache manifest is missing. Run \`sqlx-js prepare\` to regenerate the cache.`);
308
209
  }
309
210
  if (manifest.configHash !== configHash) {
310
- throw new Error("sqlx-js: cache was generated with a different jsonbTypes/customTypes config. Run `sqlx-js prepare`.");
211
+ throw new Error("sqlx-js: cache was generated with a different jsonbTypes/customTypes config or other type/function catalog settings. Run `sqlx-js prepare`.");
311
212
  }
312
213
  return manifest;
313
214
  }
@@ -7,7 +7,9 @@ function entrySignature(e) {
7
7
  const nullable = e.paramNullable?.[i] === true;
8
8
  return nullable ? `${t} | null` : t;
9
9
  });
10
- const params = paramTypes.length === 0 ? "[]" : `[${paramTypes.join(", ")}]`;
10
+ const params = e.paramNames && e.paramNames.length > 0
11
+ ? `{ ${e.paramNames.map((name, index) => `${JSON.stringify(name)}: ${paramTypes[index]}`).join("; ")} }`
12
+ : paramTypes.length === 0 ? "[]" : `[${paramTypes.join(", ")}]`;
11
13
  if (!e.hasResultSet)
12
14
  return { params, row: "never" };
13
15
  const cols = e.columns.map((c) => {
@@ -0,0 +1,28 @@
1
+ import type { SqlxJsConfig } from "../config.js";
2
+ export type CiStep = {
3
+ name: string;
4
+ args: string[];
5
+ check?: "pgschema-plan";
6
+ };
7
+ export type CiStepResult = {
8
+ name: string;
9
+ ok: boolean;
10
+ durationMs: number;
11
+ exitCode: number;
12
+ stderr?: string;
13
+ };
14
+ export type CiOptions = {
15
+ executable: string;
16
+ cliPath: string;
17
+ root: string;
18
+ config: SqlxJsConfig;
19
+ schemaPath: string;
20
+ json?: boolean;
21
+ shadowUrl?: string;
22
+ shadowAdminUrl?: string;
23
+ migrationsDir?: string;
24
+ dtsPath?: string;
25
+ };
26
+ export declare function buildCiSteps(opts: Pick<CiOptions, "config" | "root" | "schemaPath" | "shadowUrl" | "shadowAdminUrl" | "migrationsDir" | "dtsPath">): CiStep[];
27
+ export declare function assertPgschemaPlanClean(path: string): void;
28
+ export declare function runCi(opts: CiOptions): void;
@@ -0,0 +1,103 @@
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ const PLAN_PATH = "{sqlx-js-ci-plan}";
6
+ export function buildCiSteps(opts) {
7
+ const root = ["--root", opts.root];
8
+ const shadow = opts.shadowUrl ? ["--shadow-url", opts.shadowUrl] : [];
9
+ const shadowAdmin = opts.shadowAdminUrl ? ["--shadow-admin-url", opts.shadowAdminUrl] : [];
10
+ const migrations = opts.migrationsDir ? ["--migrations", opts.migrationsDir] : [];
11
+ const dts = opts.dtsPath ? ["--dts", opts.dtsPath] : [];
12
+ const steps = opts.config.schema?.provider === "pgschema"
13
+ ? [
14
+ { name: "pgschema", args: ["db", "check", ...root] },
15
+ {
16
+ name: "pgschema-plan",
17
+ args: ["db", "plan", ...root, "--", "--output-json", PLAN_PATH, "--no-color"],
18
+ check: "pgschema-plan",
19
+ },
20
+ { name: "prepare-live", args: ["prepare", "--verify", "--strict-inference", ...shadow, ...dts, ...root] },
21
+ ]
22
+ : [{
23
+ name: "migrations",
24
+ args: ["migrate", "verify", "--strict-inference", ...shadow, ...shadowAdmin, ...migrations, ...dts, ...root],
25
+ }];
26
+ steps.push({ name: "prepare-offline", args: ["prepare", "--check", "--strict-inference", ...dts, ...root] });
27
+ if (existsSync(opts.schemaPath)) {
28
+ steps.push({ name: "schema", args: ["schema", "check", ...shadow, "--schema", opts.schemaPath, ...migrations, ...root] });
29
+ }
30
+ return steps;
31
+ }
32
+ export function assertPgschemaPlanClean(path) {
33
+ const plan = JSON.parse(readFileSync(path, "utf8"));
34
+ if (plan.groups === null)
35
+ return;
36
+ if (!Array.isArray(plan.groups))
37
+ throw new Error("plan JSON does not contain groups as an array or null");
38
+ if (plan.groups.length > 0)
39
+ throw new Error(`pgschema plan contains ${plan.groups.length} unapplied change group(s)`);
40
+ }
41
+ export function runCi(opts) {
42
+ const results = [];
43
+ const temp = mkdtempSync(join(tmpdir(), "sqlx-js-ci-"));
44
+ try {
45
+ for (const step of buildCiSteps(opts)) {
46
+ if (!opts.json)
47
+ console.log(`ci: ${step.name}`);
48
+ const started = performance.now();
49
+ const planPath = join(temp, "pgschema-plan.json");
50
+ const args = step.args.map((arg) => arg === PLAN_PATH ? planPath : arg);
51
+ const result = spawnSync(opts.executable, [opts.cliPath, ...args], {
52
+ encoding: "utf8",
53
+ env: process.env,
54
+ stdio: opts.json ? ["ignore", "ignore", "pipe"] : "inherit",
55
+ maxBuffer: 4 * 1024 * 1024,
56
+ });
57
+ let exitCode = result.status ?? 1;
58
+ let checkError;
59
+ if (exitCode === 0 && step.check === "pgschema-plan") {
60
+ try {
61
+ assertPgschemaPlanClean(planPath);
62
+ }
63
+ catch (error) {
64
+ checkError = error.message.startsWith("pgschema plan contains")
65
+ ? error.message
66
+ : `cannot verify pgschema plan: ${error.message}`;
67
+ exitCode = 1;
68
+ }
69
+ }
70
+ if (result.error) {
71
+ checkError = result.error.message;
72
+ exitCode = 1;
73
+ }
74
+ else if (result.signal) {
75
+ checkError = `terminated by signal ${result.signal}`;
76
+ exitCode = 1;
77
+ }
78
+ if (checkError && !opts.json)
79
+ console.error(`ci: ${step.name}: ${checkError}`);
80
+ results.push({
81
+ name: step.name,
82
+ ok: exitCode === 0,
83
+ durationMs: Math.round(performance.now() - started),
84
+ exitCode,
85
+ ...(opts.json && exitCode !== 0 && (checkError || result.stderr)
86
+ ? { stderr: [result.stderr?.trim(), checkError].filter(Boolean).join("\n") }
87
+ : {}),
88
+ });
89
+ if (exitCode !== 0)
90
+ break;
91
+ }
92
+ }
93
+ finally {
94
+ rmSync(temp, { recursive: true, force: true });
95
+ }
96
+ const ok = results.length > 0 && results.every((result) => result.ok);
97
+ if (opts.json)
98
+ console.log(JSON.stringify({ formatVersion: 1, ok, results }, null, 2));
99
+ else if (ok)
100
+ console.log(`ci: ok — ${results.length} check(s) passed`);
101
+ if (!ok)
102
+ process.exitCode = 1;
103
+ }
@@ -6,9 +6,14 @@ export default defineConfig({
6
6
  // Map jsonb columns/params to TypeScript types declared in a .d.ts, e.g.
7
7
  // "users.settings": "SqlxJsJson.UserSettings",
8
8
  jsonbTypes: {},
9
+ // Assert narrower TypeScript types for direct scalar columns.
10
+ columnTypes: {},
9
11
  // Map PostgreSQL type names to TypeScript types, e.g.
10
12
  // geometry: "GeoJSON.Geometry",
11
13
  customTypes: {},
14
+ // Extension-owned functions are excluded by default. Set false to disable
15
+ // the generated function catalog entirely.
16
+ functionCatalog: {},
12
17
  });
13
18
  `;
14
19
  const PGSCHEMA_CONFIG_TEMPLATE = `import { defineConfig } from "@onreza/sqlx-js";
@@ -20,7 +25,9 @@ export default defineConfig({
20
25
  schemas: ["public"],
21
26
  },
22
27
  jsonbTypes: {},
28
+ columnTypes: {},
23
29
  customTypes: {},
30
+ functionCatalog: {},
24
31
  });
25
32
  `;
26
33
  const ENV_TEMPLATE = `# Connection string used by sqlx-js prepare/migrate and the runtime.
@@ -143,6 +150,8 @@ export function runInit(opts) {
143
150
  "sqlx:check": "sqlx-js prepare --check",
144
151
  "sqlx:offline": "sqlx-js prepare --offline",
145
152
  "sqlx:verify": "sqlx-js prepare --verify --strict-inference",
153
+ "sqlx:ci": "sqlx-js ci",
154
+ "sqlx:queries": "sqlx-js queries --json",
146
155
  };
147
156
  let changed = false;
148
157
  for (const [name, command] of Object.entries(defaults)) {
@@ -34,6 +34,8 @@ export type PrepareDiagnostic = {
34
34
  line?: number;
35
35
  column?: number;
36
36
  query?: string;
37
+ queryId?: string;
38
+ queryName?: string;
37
39
  code?: string;
38
40
  position?: number;
39
41
  hint?: string;
@@ -8,16 +8,17 @@ import { arrayElementOid, isBuiltinOid, oidToTs } from "../pg/oids.js";
8
8
  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
- import { loadConfig, lookupJsonbType, prepareConfigHash } from "../config.js";
11
+ import { loadConfig, lookupColumnType, lookupJsonbType, prepareConfigHash } from "../config.js";
12
12
  import { functionCacheExists, readFunctionCache, writeFunctionCache } from "../function-cache.js";
13
13
  import { introspectFunctions } from "../pg/functions.js";
14
14
  import { buildParamMap } from "../pg/param-map.js";
15
15
  import { mergeExtensionTypes } from "../pg/extensions.js";
16
16
  import { compareArtifacts } from "../artifacts.js";
17
17
  import { containsUnknownType } from "../type-inspection.js";
18
+ import { originalPosition, rewriteNamedParameters } from "../sql-params.js";
18
19
  const JSON_OIDS = new Set([114, 3802]);
19
20
  const JSON_ARRAY_OIDS = new Set([199, 3807]);
20
- const JSON_INPUT_VALUE = 'import("@onreza/sqlx-js").JsonInputValue';
21
+ const JSON_INPUT_VALUE = "unknown";
21
22
  function jsonParameter(type) {
22
23
  return `import("@onreza/sqlx-js").JsonParameter<${type}>`;
23
24
  }
@@ -47,6 +48,12 @@ function resolveTs(oid, customLookup) {
47
48
  }
48
49
  return oidToTs(oid).ts;
49
50
  }
51
+ function isScalarColumnType(oid, schema) {
52
+ if (JSON_OIDS.has(oid) || JSON_ARRAY_OIDS.has(oid) || arrayElementOid(oid) !== undefined)
53
+ return false;
54
+ const custom = schema.customType(oid);
55
+ return custom?.kind !== "enumArray" && custom?.kind !== "scalarArray" && custom?.kind !== "compositeArray";
56
+ }
50
57
  function resolveColumnTs(f, schema, cfg) {
51
58
  if (f.tableOid !== 0 && f.columnAttr !== 0) {
52
59
  const tbl = schema.tableNameByOid(f.tableOid);
@@ -62,13 +69,25 @@ function resolveColumnTs(f, schema, cfg) {
62
69
  if (decl)
63
70
  return `(${decl})[]`;
64
71
  }
72
+ if (isScalarColumnType(f.typeOid, schema)) {
73
+ const decl = lookupColumnType(cfg, tbl.schema, tbl.name, colName);
74
+ if (decl)
75
+ return decl;
76
+ }
65
77
  }
66
78
  }
67
79
  return resolveTs(f.typeOid, (oid) => schema.customType(oid));
68
80
  }
69
81
  function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
82
+ const target = paramMap.get(paramIndex);
83
+ if (isScalarColumnType(paramOid, schema) && target) {
84
+ const column = resolveTargetColumn(target, schema);
85
+ const table = resolvedTargetTable(target, schema);
86
+ const decl = column && table ? lookupColumnType(cfg, table.schema, table.name, column) : undefined;
87
+ if (decl)
88
+ return decl;
89
+ }
70
90
  if (JSON_OIDS.has(paramOid)) {
71
- const target = paramMap.get(paramIndex);
72
91
  if (target) {
73
92
  const column = resolveTargetColumn(target, schema);
74
93
  const table = resolvedTargetTable(target, schema);
@@ -79,7 +98,6 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
79
98
  return jsonParameter(JSON_INPUT_VALUE);
80
99
  }
81
100
  if (JSON_ARRAY_OIDS.has(paramOid)) {
82
- const target = paramMap.get(paramIndex);
83
101
  if (target) {
84
102
  const column = resolveTargetColumn(target, schema);
85
103
  const table = resolvedTargetTable(target, schema);
@@ -187,7 +205,17 @@ function fatal(phase, error) {
187
205
  return new PrepareFatalError(phase, message, location, { cause: error });
188
206
  }
189
207
  function formatSite(s) {
190
- return `${s.file}:${s.line}:${s.column}`;
208
+ return `${s.file}:${s.line}:${s.column}${s.queryName ? ` [${s.queryName}]` : ""}`;
209
+ }
210
+ function siteDiagnostic(site) {
211
+ return {
212
+ file: site.file,
213
+ line: site.line,
214
+ column: site.column,
215
+ query: site.query,
216
+ queryId: fingerprint(site.query),
217
+ ...(site.queryName ? { queryName: site.queryName } : {}),
218
+ };
191
219
  }
192
220
  function snippet(query, max = 80) {
193
221
  const oneLine = query.replace(/\s+/g, " ").trim();
@@ -207,8 +235,9 @@ function inferenceIssues(entry) {
207
235
  if (entry.degraded)
208
236
  issues.push(`nullability inference degraded: ${entry.degraded.reason}`);
209
237
  entry.paramTsTypes.forEach((type, index) => {
238
+ const parameter = entry.paramNames?.[index] ? `$${entry.paramNames[index]}` : `$${index + 1}`;
210
239
  if (containsUnknownType(type))
211
- issues.push(`parameter $${index + 1} resolved to ${type}`);
240
+ issues.push(`parameter ${parameter} resolved to ${type}`);
212
241
  });
213
242
  for (const column of entry.columns) {
214
243
  if (containsUnknownType(column.tsType)) {
@@ -222,10 +251,7 @@ function inferenceDiagnostics(entry, site, strict) {
222
251
  severity: strict ? "error" : "warning",
223
252
  phase: "inference",
224
253
  message,
225
- file: site.file,
226
- line: site.line,
227
- column: site.column,
228
- query: entry.query,
254
+ ...siteDiagnostic(site),
229
255
  }));
230
256
  }
231
257
  export async function openSession(opts) {
@@ -326,12 +352,13 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
326
352
  let failures = 0;
327
353
  const unique = new Map();
328
354
  for (const s of sites) {
355
+ const rewritten = rewriteNamedParameters(s.query);
329
356
  const fp = fingerprint(s.query);
330
357
  const existing = unique.get(fp);
331
358
  if (existing)
332
359
  existing.sites.push(s);
333
360
  else
334
- unique.set(fp, { fp, query: s.query, sites: [s] });
361
+ unique.set(fp, { fp, query: rewritten.query, paramNames: rewritten.names, sites: [s] });
335
362
  }
336
363
  const raw = [];
337
364
  const reusedEntries = [];
@@ -369,56 +396,48 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
369
396
  severity: "error",
370
397
  phase: "result-shape",
371
398
  message,
372
- file: site.file,
373
- line: site.line,
374
- column: site.column,
375
- query,
399
+ ...siteDiagnostic(site),
376
400
  });
377
401
  err(` ✗ ${formatSite(site)} — ${message}`);
378
- err(` query: ${snippet(query)}`);
402
+ err(` query: ${snippet(site.query)}`);
379
403
  continue;
380
404
  }
381
- raw.push({ fp, query, sites: ss, paramOids: outcome.paramOids, fields: outcome.fields });
405
+ raw.push({ fp, query, sites: ss, paramOids: outcome.paramOids, fields: outcome.fields, paramNames: toPrepare.get(fp).paramNames });
382
406
  continue;
383
407
  }
384
408
  failures++;
385
409
  const e = outcome.error;
386
410
  if (e instanceof PgError) {
411
+ const position = e.position ? originalPosition(rewriteNamedParameters(site.query), e.position) : undefined;
387
412
  diagnostics.push({
388
413
  severity: "error",
389
414
  phase: "describe",
390
415
  message: e.message,
391
- file: site.file,
392
- line: site.line,
393
- column: site.column,
394
- query,
416
+ ...siteDiagnostic(site),
395
417
  ...(e.code ? { code: e.code } : {}),
396
- ...(e.position ? { position: e.position } : {}),
418
+ ...(position ? { position } : {}),
397
419
  ...(e.hint ? { hint: e.hint } : {}),
398
420
  });
399
421
  const extras = [];
400
- if (e.position)
401
- extras.push(`pos ${e.position}`);
422
+ if (position)
423
+ extras.push(`pos ${position}`);
402
424
  if (e.code)
403
425
  extras.push(`code ${e.code}`);
404
426
  const tail = extras.length > 0 ? ` (${extras.join(", ")})` : "";
405
427
  err(` ✗ ${formatSite(site)} — describe failed: ${e.message}${tail}`);
406
428
  if (e.hint)
407
429
  err(` hint: ${e.hint}`);
408
- err(` query: ${snippet(query)}`);
430
+ err(` query: ${snippet(site.query)}`);
409
431
  }
410
432
  else {
411
433
  diagnostics.push({
412
434
  severity: "error",
413
435
  phase: "describe",
414
436
  message: e.message,
415
- file: site.file,
416
- line: site.line,
417
- column: site.column,
418
- query,
437
+ ...siteDiagnostic(site),
419
438
  });
420
439
  err(` ✗ ${formatSite(site)} — describe failed: ${e.message}`);
421
- err(` query: ${snippet(query)}`);
440
+ err(` query: ${snippet(site.query)}`);
422
441
  }
423
442
  }
424
443
  const allAttrRefs = [];
@@ -453,13 +472,10 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
453
472
  severity: "error",
454
473
  phase: "analyze",
455
474
  message: e.message,
456
- file: site.file,
457
- line: site.line,
458
- column: site.column,
459
- query: r.query,
475
+ ...siteDiagnostic(site),
460
476
  });
461
477
  err(` ✗ ${formatSite(site)} — analyze failed: ${e.message}`);
462
- err(` query: ${snippet(r.query)}`);
478
+ err(` query: ${snippet(site.query)}`);
463
479
  continue;
464
480
  }
465
481
  try {
@@ -472,13 +488,10 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
472
488
  severity: "error",
473
489
  phase: "param-map",
474
490
  message: e.message,
475
- file: site.file,
476
- line: site.line,
477
- column: site.column,
478
- query: r.query,
491
+ ...siteDiagnostic(site),
479
492
  });
480
493
  err(` ✗ ${formatSite(site)} — paramMap failed: ${e.message}`);
481
- err(` query: ${snippet(r.query)}`);
494
+ err(` query: ${snippet(site.query)}`);
482
495
  }
483
496
  }
484
497
  const dmlTablesToLoad = new Map();
@@ -534,11 +547,12 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
534
547
  dmlBound: new Set(),
535
548
  };
536
549
  const entry = {
537
- query: r.query,
550
+ query: r.sites[0].query,
538
551
  ...siteUsage(r.sites),
539
552
  paramOids: r.paramOids.map(portableCacheOid),
540
553
  paramTsTypes: r.paramOids.map((o, idx) => resolveParamTs(idx + 1, o, pm.targets, schema, userCfg)),
541
554
  paramNullable: r.paramOids.map((_o, idx) => resolveParamNullable(idx + 1, pm, schema)),
555
+ ...(r.paramNames.length > 0 ? { paramNames: r.paramNames } : {}),
542
556
  columns: r.fields.map((f, i) => {
543
557
  const parsed = parseColumnOverride(f.name);
544
558
  const treatAsOverride = parsed.override !== undefined && isAliasOrExpression(f, schema);
@@ -575,12 +589,17 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
575
589
  return { sites: sites.length, entries: entries.length, failures, pruned: 0, functions: 0, diagnostics };
576
590
  }
577
591
  let functions;
578
- if (input.reuseFunctions && functionCacheExists(opts.cacheDir)) {
592
+ if (userCfg.functionCatalog === false) {
593
+ functions = [];
594
+ }
595
+ else if (input.reuseFunctions && functionCacheExists(opts.cacheDir)) {
579
596
  functions = readFunctionCache(opts.cacheDir);
580
597
  }
581
598
  else {
582
599
  try {
583
- functions = await introspectFunctions(client, schema);
600
+ functions = await introspectFunctions(client, schema, {
601
+ includeExtensionOwned: userCfg.functionCatalog?.includeExtensionOwned === true,
602
+ });
584
603
  }
585
604
  catch (error) {
586
605
  throw fatal("introspect", error);
@@ -658,10 +677,7 @@ export async function runPrepare(opts) {
658
677
  severity: "error",
659
678
  phase: "cache",
660
679
  message: "query is not in the offline cache",
661
- file: site.file,
662
- line: site.line,
663
- column: site.column,
664
- query,
680
+ ...siteDiagnostic(site),
665
681
  });
666
682
  if (!opts.json) {
667
683
  console.error(`stale: ${formatSite(site)} — query not in cache`);