@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/README.md +124 -15
- package/ROADMAP.md +4 -3
- package/dist/bin/sqlx-js.js +117 -7
- package/dist/src/cache.d.ts +6 -2
- package/dist/src/cache.js +35 -134
- package/dist/src/codegen.js +3 -1
- package/dist/src/commands/ci.d.ts +28 -0
- package/dist/src/commands/ci.js +103 -0
- package/dist/src/commands/init.js +9 -0
- package/dist/src/commands/prepare.d.ts +2 -0
- package/dist/src/commands/prepare.js +63 -47
- package/dist/src/commands/queries.d.ts +38 -0
- package/dist/src/commands/queries.js +143 -0
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.js +29 -0
- package/dist/src/index.d.ts +10 -2
- package/dist/src/index.js +3 -1
- package/dist/src/pg/functions.d.ts +3 -1
- package/dist/src/pg/functions.js +11 -3
- package/dist/src/postgres-runtime.d.ts +2 -0
- package/dist/src/postgres-runtime.js +9 -6
- package/dist/src/query-id.d.ts +1 -0
- package/dist/src/query-id.js +61 -0
- package/dist/src/query.d.ts +53 -0
- package/dist/src/query.js +38 -0
- package/dist/src/runtime.d.ts +27 -3
- package/dist/src/runtime.js +75 -23
- package/dist/src/scan/scanner.d.ts +2 -0
- package/dist/src/scan/scanner.js +123 -17
- package/dist/src/sql-lex.d.ts +7 -0
- package/dist/src/sql-lex.js +76 -0
- package/dist/src/sql-params.d.ts +11 -0
- package/dist/src/sql-params.js +101 -0
- package/dist/src/typed.d.ts +6 -4
- package/package.json +6 -1
package/dist/src/cache.js
CHANGED
|
@@ -1,143 +1,23 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/dist/src/codegen.js
CHANGED
|
@@ -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 =
|
|
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)) {
|
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
line: site.line,
|
|
393
|
-
column: site.column,
|
|
394
|
-
query,
|
|
416
|
+
...siteDiagnostic(site),
|
|
395
417
|
...(e.code ? { code: e.code } : {}),
|
|
396
|
-
...(
|
|
418
|
+
...(position ? { position } : {}),
|
|
397
419
|
...(e.hint ? { hint: e.hint } : {}),
|
|
398
420
|
});
|
|
399
421
|
const extras = [];
|
|
400
|
-
if (
|
|
401
|
-
extras.push(`pos ${
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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`);
|