@onreza/sqlx-js 0.1.0 → 0.3.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 +128 -37
- package/ROADMAP.md +2 -3
- package/dist/bin/sqlx-js.js +97 -14
- package/dist/src/cache.d.ts +1 -0
- package/dist/src/cache.js +128 -1
- package/dist/src/codegen.js +16 -16
- package/dist/src/commands/init.d.ts +5 -0
- package/dist/src/commands/init.js +57 -0
- package/dist/src/commands/migrate.d.ts +190 -3
- package/dist/src/commands/migrate.js +1383 -46
- package/dist/src/commands/prepare.d.ts +16 -2
- package/dist/src/commands/prepare.js +116 -33
- package/dist/src/commands/schema.js +4 -0
- package/dist/src/commands/watch.js +1 -1
- package/dist/src/index.d.ts +14 -2
- package/dist/src/pg/narrow.js +8 -0
- package/dist/src/pg/oids.js +3 -2
- package/dist/src/pg/param-map.d.ts +2 -1
- package/dist/src/pg/param-map.js +181 -40
- package/dist/src/pg/schema.d.ts +17 -1
- package/dist/src/pg/schema.js +48 -2
- package/dist/src/pg/wire.d.ts +14 -1
- package/dist/src/pg/wire.js +81 -3
- package/dist/src/postgres-runtime.d.ts +9 -2
- package/dist/src/postgres-runtime.js +20 -5
- package/dist/src/runtime.d.ts +12 -0
- package/dist/src/runtime.js +83 -3
- package/dist/src/scan/scanner.js +1 -1
- package/package.json +5 -8
- package/dist/src/bun-runtime.d.ts +0 -7
- package/dist/src/bun-runtime.js +0 -45
- package/dist/src/bun.d.ts +0 -22
- package/dist/src/bun.js +0 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PgClient } from "../pg/wire.js";
|
|
1
|
+
import { PgClient, type ConnConfig, type FieldDescription } from "../pg/wire.js";
|
|
2
2
|
import { SchemaCache } from "../pg/schema.js";
|
|
3
3
|
import { type SqlxJsConfig } from "../config.js";
|
|
4
4
|
export type PrepareOptions = {
|
|
@@ -15,9 +15,23 @@ export type PrepareSession = {
|
|
|
15
15
|
userCfg: SqlxJsConfig;
|
|
16
16
|
};
|
|
17
17
|
export declare function openSession(opts: PrepareOptions): Promise<PrepareSession>;
|
|
18
|
-
|
|
18
|
+
type DescribeOutcome = {
|
|
19
|
+
ok: true;
|
|
20
|
+
paramOids: number[];
|
|
21
|
+
fields: FieldDescription[];
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
error: unknown;
|
|
25
|
+
};
|
|
26
|
+
export declare function defaultPrepareConcurrency(): number;
|
|
27
|
+
export declare function describeAll(cfg: ConnConfig, sessionClient: PgClient, queries: {
|
|
28
|
+
fp: string;
|
|
29
|
+
query: string;
|
|
30
|
+
}[], concurrency: number): Promise<Map<string, DescribeOutcome>>;
|
|
31
|
+
export declare function prepareOnce(opts: PrepareOptions, session: PrepareSession, log?: (msg: string) => void, err?: (msg: string) => void, concurrency?: number): Promise<{
|
|
19
32
|
entries: number;
|
|
20
33
|
failures: number;
|
|
21
34
|
pruned: number;
|
|
22
35
|
}>;
|
|
23
36
|
export declare function runPrepare(opts: PrepareOptions): Promise<void>;
|
|
37
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PgClient, parseDatabaseUrl, PgError } from "../pg/wire.js";
|
|
2
|
-
import { SchemaCache } from "../pg/schema.js";
|
|
2
|
+
import { SchemaCache, compositeLiteral } from "../pg/schema.js";
|
|
3
3
|
import { analyzeQuery } from "../pg/analyze.js";
|
|
4
4
|
import { isBuiltinOid, oidToTs } from "../pg/oids.js";
|
|
5
5
|
import { scanProject } from "../scan/scanner.js";
|
|
@@ -10,6 +10,7 @@ import { buildParamMap } from "../pg/param-map.js";
|
|
|
10
10
|
import { mergeExtensionTypes } from "../pg/extensions.js";
|
|
11
11
|
const JSON_OIDS = new Set([114, 3802]);
|
|
12
12
|
const JSON_ARRAY_OIDS = new Set([199, 3807]);
|
|
13
|
+
const JSON_INPUT = 'import("@onreza/sqlx-js").JsonInput';
|
|
13
14
|
function enumUnion(values) {
|
|
14
15
|
if (values.length === 0)
|
|
15
16
|
return "never";
|
|
@@ -26,6 +27,10 @@ function resolveTs(oid, customLookup) {
|
|
|
26
27
|
return c.tsType;
|
|
27
28
|
if (c.kind === "scalarArray")
|
|
28
29
|
return `(${c.element.tsType})[]`;
|
|
30
|
+
if (c.kind === "composite")
|
|
31
|
+
return compositeLiteral(c);
|
|
32
|
+
if (c.kind === "compositeArray")
|
|
33
|
+
return `(${compositeLiteral(c.element)})[]`;
|
|
29
34
|
}
|
|
30
35
|
return oidToTs(oid).ts;
|
|
31
36
|
}
|
|
@@ -52,13 +57,28 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
|
|
|
52
57
|
if (JSON_OIDS.has(paramOid) || JSON_ARRAY_OIDS.has(paramOid)) {
|
|
53
58
|
const target = paramMap.get(paramIndex);
|
|
54
59
|
if (target) {
|
|
55
|
-
const
|
|
60
|
+
const column = resolveTargetColumn(target, schema);
|
|
61
|
+
const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
|
|
56
62
|
if (decl)
|
|
57
63
|
return JSON_ARRAY_OIDS.has(paramOid) ? `(${decl})[]` : decl;
|
|
58
64
|
}
|
|
65
|
+
return JSON_ARRAY_OIDS.has(paramOid) ? `(${JSON_INPUT})[]` : JSON_INPUT;
|
|
59
66
|
}
|
|
60
67
|
return resolveTs(paramOid, (oid) => schema.customType(oid));
|
|
61
68
|
}
|
|
69
|
+
function resolveTargetColumn(target, schema) {
|
|
70
|
+
if (target.column)
|
|
71
|
+
return target.column;
|
|
72
|
+
if (target.columnIndex === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
const oid = schema.resolveTable(target.schema, target.table);
|
|
75
|
+
if (oid === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const cols = schema.columnsOf(oid);
|
|
78
|
+
if (!cols)
|
|
79
|
+
return undefined;
|
|
80
|
+
return [...cols.values()].sort((a, b) => a.attnum - b.attnum)[target.columnIndex - 1]?.name;
|
|
81
|
+
}
|
|
62
82
|
function resolveParamNullable(paramIndex, pm, schema) {
|
|
63
83
|
if (pm.forceNullable.has(paramIndex))
|
|
64
84
|
return true;
|
|
@@ -69,7 +89,10 @@ function resolveParamNullable(paramIndex, pm, schema) {
|
|
|
69
89
|
const oid = schema.resolveTable(t.schema, t.table);
|
|
70
90
|
if (oid === undefined)
|
|
71
91
|
return false;
|
|
72
|
-
const
|
|
92
|
+
const column = resolveTargetColumn(t, schema);
|
|
93
|
+
if (!column)
|
|
94
|
+
return false;
|
|
95
|
+
const col = schema.columnsOf(oid)?.get(column);
|
|
73
96
|
if (!col)
|
|
74
97
|
return false;
|
|
75
98
|
return !col.notNull;
|
|
@@ -96,6 +119,15 @@ function snippet(query, max = 80) {
|
|
|
96
119
|
const oneLine = query.replace(/\s+/g, " ").trim();
|
|
97
120
|
return oneLine.length > max ? oneLine.slice(0, max) + "…" : oneLine;
|
|
98
121
|
}
|
|
122
|
+
function siteUsage(sites) {
|
|
123
|
+
const inlineQueries = Array.from(new Set(sites.filter((s) => s.kind !== "file").map((s) => s.query))).sort();
|
|
124
|
+
const filePaths = Array.from(new Set(sites.filter((s) => s.kind === "file").map((s) => s.sqlFilePath).filter(Boolean))).sort();
|
|
125
|
+
return {
|
|
126
|
+
hasInline: inlineQueries.length > 0,
|
|
127
|
+
...(inlineQueries.length > 0 ? { inlineQueries } : {}),
|
|
128
|
+
...(filePaths.length > 0 ? { filePaths } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
99
131
|
export async function openSession(opts) {
|
|
100
132
|
const userCfg = await loadConfig(opts.root);
|
|
101
133
|
const cfg = parseDatabaseUrl(opts.databaseUrl);
|
|
@@ -105,7 +137,59 @@ export async function openSession(opts) {
|
|
|
105
137
|
schema.setTypeRegistry(mergeExtensionTypes(userCfg.customTypes));
|
|
106
138
|
return { client, schema, userCfg };
|
|
107
139
|
}
|
|
108
|
-
export
|
|
140
|
+
export function defaultPrepareConcurrency() {
|
|
141
|
+
const raw = process.env.SQLX_JS_PREPARE_CONCURRENCY;
|
|
142
|
+
const n = raw ? Number(raw) : NaN;
|
|
143
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 8;
|
|
144
|
+
}
|
|
145
|
+
// describe() is sequential per PgClient (see wire.ts), so concurrency comes from
|
|
146
|
+
// running several short-lived connections in parallel, each draining a shared
|
|
147
|
+
// cursor. The session connection is reused as one worker; extras are closed after.
|
|
148
|
+
export async function describeAll(cfg, sessionClient, queries, concurrency) {
|
|
149
|
+
const results = new Map();
|
|
150
|
+
if (queries.length === 0)
|
|
151
|
+
return results;
|
|
152
|
+
const workerCount = Math.max(1, Math.min(concurrency, queries.length));
|
|
153
|
+
let cursor = 0;
|
|
154
|
+
const drain = async (client) => {
|
|
155
|
+
while (true) {
|
|
156
|
+
const i = cursor++;
|
|
157
|
+
if (i >= queries.length)
|
|
158
|
+
return;
|
|
159
|
+
const { fp, query } = queries[i];
|
|
160
|
+
try {
|
|
161
|
+
const d = await client.describe(query);
|
|
162
|
+
results.set(fp, { ok: true, paramOids: d.paramOids, fields: d.fields });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
results.set(fp, { ok: false, error });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const extras = [];
|
|
170
|
+
try {
|
|
171
|
+
// Open extra connections best-effort. The session connection alone is enough
|
|
172
|
+
// to drain the queue, so a connection-limited server (low max_connections,
|
|
173
|
+
// PgBouncer) degrades to fewer workers instead of failing the whole prepare.
|
|
174
|
+
for (let i = 1; i < workerCount; i++) {
|
|
175
|
+
const c = new PgClient(cfg);
|
|
176
|
+
try {
|
|
177
|
+
await c.connect();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
await c.end().catch(() => { });
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
extras.push(c);
|
|
184
|
+
}
|
|
185
|
+
await Promise.all([sessionClient, ...extras].map((c) => drain(c)));
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
await Promise.all(extras.map((c) => c.end().catch(() => { })));
|
|
189
|
+
}
|
|
190
|
+
return results;
|
|
191
|
+
}
|
|
192
|
+
export async function prepareOnce(opts, session, log = console.log, err = console.error, concurrency = defaultPrepareConcurrency()) {
|
|
109
193
|
const sites = scanProject(opts.root);
|
|
110
194
|
log(`scanned: found ${sites.length} sql() call site(s)`);
|
|
111
195
|
const cache = new Cache(opts.cacheDir);
|
|
@@ -121,30 +205,32 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
121
205
|
const raw = [];
|
|
122
206
|
let failures = 0;
|
|
123
207
|
const { client, schema, userCfg } = session;
|
|
208
|
+
const describeList = [...unique.values()].map((u) => ({ fp: u.fp, query: u.query }));
|
|
209
|
+
const describeResults = await describeAll(parseDatabaseUrl(opts.databaseUrl), client, describeList, concurrency);
|
|
124
210
|
for (const { fp, query, sites: ss } of unique.values()) {
|
|
125
211
|
const site = ss[0];
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
raw.push({ fp, query, sites: ss, paramOids:
|
|
212
|
+
const outcome = describeResults.get(fp);
|
|
213
|
+
if (outcome.ok) {
|
|
214
|
+
raw.push({ fp, query, sites: ss, paramOids: outcome.paramOids, fields: outcome.fields });
|
|
215
|
+
continue;
|
|
129
216
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
217
|
+
failures++;
|
|
218
|
+
const e = outcome.error;
|
|
219
|
+
if (e instanceof PgError) {
|
|
220
|
+
const extras = [];
|
|
221
|
+
if (e.position)
|
|
222
|
+
extras.push(`pos ${e.position}`);
|
|
223
|
+
if (e.code)
|
|
224
|
+
extras.push(`code ${e.code}`);
|
|
225
|
+
const tail = extras.length > 0 ? ` (${extras.join(", ")})` : "";
|
|
226
|
+
err(` ✗ ${formatSite(site)} — describe failed: ${e.message}${tail}`);
|
|
227
|
+
if (e.hint)
|
|
228
|
+
err(` hint: ${e.hint}`);
|
|
229
|
+
err(` query: ${snippet(query)}`);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
err(` ✗ ${formatSite(site)} — describe failed: ${e.message}`);
|
|
233
|
+
err(` query: ${snippet(query)}`);
|
|
148
234
|
}
|
|
149
235
|
}
|
|
150
236
|
const allAttrRefs = [];
|
|
@@ -226,13 +312,9 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
226
312
|
forceNullable: new Set(),
|
|
227
313
|
dmlBound: new Set(),
|
|
228
314
|
};
|
|
229
|
-
const fileSites = r.sites.filter((s) => s.kind === "file");
|
|
230
|
-
const hasInline = r.sites.some((s) => s.kind !== "file");
|
|
231
|
-
const filePaths = fileSites.length > 0
|
|
232
|
-
? Array.from(new Set(fileSites.map((s) => s.sqlFilePath))).sort()
|
|
233
|
-
: undefined;
|
|
234
315
|
const entry = {
|
|
235
316
|
query: r.query,
|
|
317
|
+
...siteUsage(r.sites),
|
|
236
318
|
paramOids: r.paramOids,
|
|
237
319
|
paramTsTypes: r.paramOids.map((o, idx) => resolveParamTs(idx + 1, o, pm.targets, schema, userCfg)),
|
|
238
320
|
paramNullable: r.paramOids.map((_o, idx) => resolveParamNullable(idx + 1, pm, schema)),
|
|
@@ -248,8 +330,6 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
248
330
|
};
|
|
249
331
|
}),
|
|
250
332
|
hasResultSet: r.fields.length > 0,
|
|
251
|
-
hasInline,
|
|
252
|
-
...(filePaths ? { filePaths } : {}),
|
|
253
333
|
...(analysis.degraded ? { degraded: analysis.degraded } : {}),
|
|
254
334
|
};
|
|
255
335
|
cache.write(r.fp, entry);
|
|
@@ -293,7 +373,10 @@ export async function runPrepare(opts) {
|
|
|
293
373
|
console.error(`\nsqlx-js prepare --check: ${stale} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
|
|
294
374
|
process.exit(1);
|
|
295
375
|
}
|
|
296
|
-
const entries = [...unique.values()].map((u) =>
|
|
376
|
+
const entries = [...unique.values()].map((u) => {
|
|
377
|
+
const entry = cache.read(u.fp);
|
|
378
|
+
return entry ? { ...entry, ...siteUsage(u.sites) } : null;
|
|
379
|
+
}).filter((e) => e !== null);
|
|
297
380
|
emitDts(opts.dtsPath, entries);
|
|
298
381
|
console.log(`ok — ${entries.length} unique queries, types regenerated`);
|
|
299
382
|
return;
|
|
@@ -14,6 +14,10 @@ export async function applyShadowMigrations(databaseUrl, migrationsDir, log = co
|
|
|
14
14
|
applied++;
|
|
15
15
|
log(`shadow: applied ${String(e.version).padStart(4, "0")}_${e.name}`);
|
|
16
16
|
}
|
|
17
|
+
else if (e.kind === "adopted") {
|
|
18
|
+
applied++;
|
|
19
|
+
log(`shadow: adopted ${String(e.version).padStart(4, "0")}_${e.name} (${e.replaced} replaced)`);
|
|
20
|
+
}
|
|
17
21
|
else if (e.kind === "tampered") {
|
|
18
22
|
throw new Error(`sqlx-js shadow: ${e.version}_${e.name} hash mismatch (applied ${e.applied.slice(0, 16)} vs current ${e.current.slice(0, 16)})`);
|
|
19
23
|
}
|
|
@@ -21,7 +21,7 @@ export async function prepareWatchedOnce(opts, state, log, err, deps = DEFAULT_D
|
|
|
21
21
|
}
|
|
22
22
|
if (!state.session)
|
|
23
23
|
state.session = await deps.openSession(opts);
|
|
24
|
-
return await deps.prepareOnce(opts, state.session, log, err);
|
|
24
|
+
return await deps.prepareOnce(opts, state.session, log, err, 1);
|
|
25
25
|
}
|
|
26
26
|
export async function runWatch(opts) {
|
|
27
27
|
const stamp = () => new Date().toTimeString().slice(0, 8);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -4,12 +4,24 @@ export interface KnownQueries {
|
|
|
4
4
|
}
|
|
5
5
|
export interface KnownFileQueries {
|
|
6
6
|
}
|
|
7
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
8
|
+
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
|
9
|
+
export type JsonObject = {
|
|
10
|
+
readonly [key: string]: JsonValue;
|
|
11
|
+
};
|
|
12
|
+
export type JsonArray = readonly JsonValue[];
|
|
13
|
+
export type JsonInput = string | number | boolean | JsonInputObject | JsonInputArray;
|
|
14
|
+
export type JsonInputValue = JsonPrimitive | JsonInputObject | JsonInputArray;
|
|
15
|
+
export type JsonInputObject = {
|
|
16
|
+
readonly [key: string]: JsonInputValue | undefined;
|
|
17
|
+
};
|
|
18
|
+
export type JsonInputArray = readonly JsonInputValue[];
|
|
7
19
|
export type { SqlxJsConfig } from "./config.js";
|
|
8
20
|
export type { SslMode, ConnConfig } from "./pg/wire.js";
|
|
9
21
|
export { PgError, ConnectionLostError } from "./pg/wire.js";
|
|
10
22
|
export { NoRowsError, TooManyRowsError } from "./runtime.js";
|
|
11
|
-
export type { TransactionOptions, MigrateOptions } from "./runtime.js";
|
|
12
|
-
export type { PostgresClient, PostgresOptions } from "./postgres-runtime.js";
|
|
23
|
+
export type { TransactionOptions, MigrateOptions, OnQueryEvent, OnQueryHook } from "./runtime.js";
|
|
24
|
+
export type { PostgresClient, PostgresOptions, CreateClientOptions } from "./postgres-runtime.js";
|
|
13
25
|
export type TypedFile = TypedFileFor<KnownFileQueries>;
|
|
14
26
|
export type TypedSql = TypedSqlFor<KnownQueries, KnownFileQueries>;
|
|
15
27
|
export type Typed = TypedFor<KnownQueries, KnownFileQueries, import("./runtime.js").TransactionOptions>;
|
package/dist/src/pg/narrow.js
CHANGED
|
@@ -78,6 +78,14 @@ function walk(node) {
|
|
|
78
78
|
}
|
|
79
79
|
return forcedInfo(acc ?? []);
|
|
80
80
|
}
|
|
81
|
+
if (op === "NOT_EXPR") {
|
|
82
|
+
const arg = args[0];
|
|
83
|
+
if (arg?.NullTest?.nulltesttype === "IS_NULL") {
|
|
84
|
+
const k = keyOfColumnRef(arg.NullTest.arg);
|
|
85
|
+
return k ? forcedInfo([k]) : emptyInfo();
|
|
86
|
+
}
|
|
87
|
+
return emptyInfo();
|
|
88
|
+
}
|
|
81
89
|
return emptyInfo();
|
|
82
90
|
}
|
|
83
91
|
if (node.A_Expr) {
|
package/dist/src/pg/oids.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const JSON_VALUE = 'import("@onreza/sqlx-js").JsonValue';
|
|
1
2
|
const SCALAR = {
|
|
2
3
|
16: { ts: "boolean" },
|
|
3
4
|
17: { ts: "Uint8Array" },
|
|
@@ -11,7 +12,7 @@ const SCALAR = {
|
|
|
11
12
|
27: { ts: "string" },
|
|
12
13
|
28: { ts: "string" },
|
|
13
14
|
29: { ts: "string" },
|
|
14
|
-
114: { ts:
|
|
15
|
+
114: { ts: JSON_VALUE },
|
|
15
16
|
142: { ts: "string" },
|
|
16
17
|
600: { ts: "string" },
|
|
17
18
|
601: { ts: "string" },
|
|
@@ -44,7 +45,7 @@ const SCALAR = {
|
|
|
44
45
|
3220: { ts: "string" },
|
|
45
46
|
3614: { ts: "string" },
|
|
46
47
|
3615: { ts: "string" },
|
|
47
|
-
3802: { ts:
|
|
48
|
+
3802: { ts: JSON_VALUE },
|
|
48
49
|
3904: { ts: "string" },
|
|
49
50
|
3906: { ts: "string" },
|
|
50
51
|
3908: { ts: "string" },
|
package/dist/src/pg/param-map.js
CHANGED
|
@@ -12,9 +12,9 @@ export async function buildParamMap(sql) {
|
|
|
12
12
|
else if (stmt.UpdateStmt)
|
|
13
13
|
walkUpdate(stmt.UpdateStmt, targets, dmlBound);
|
|
14
14
|
else if (stmt.SelectStmt)
|
|
15
|
-
|
|
15
|
+
walkSelect(stmt.SelectStmt, targets, dmlBound);
|
|
16
16
|
else if (stmt.DeleteStmt)
|
|
17
|
-
|
|
17
|
+
walkDelete(stmt.DeleteStmt, targets, dmlBound);
|
|
18
18
|
walkForceNullable(stmt, false, forceNullable);
|
|
19
19
|
return { targets, forceNullable, dmlBound };
|
|
20
20
|
}
|
|
@@ -22,6 +22,7 @@ function walkInsert(ins, map, dmlBound) {
|
|
|
22
22
|
const rel = relOf(ins.relation);
|
|
23
23
|
if (!rel)
|
|
24
24
|
return;
|
|
25
|
+
const scope = scopeFromRelationNode(ins.relation, rel);
|
|
25
26
|
const cols = (ins.cols ?? [])
|
|
26
27
|
.map((c) => c?.ResTarget?.name)
|
|
27
28
|
.filter((n) => typeof n === "string");
|
|
@@ -29,82 +30,162 @@ function walkInsert(ins, map, dmlBound) {
|
|
|
29
30
|
for (const row of valuesLists) {
|
|
30
31
|
const items = row?.List?.items ?? [];
|
|
31
32
|
for (let i = 0; i < items.length; i++) {
|
|
32
|
-
const colName = cols[i];
|
|
33
|
-
if (!colName)
|
|
34
|
-
continue;
|
|
35
33
|
const pn = paramNumber(items[i]);
|
|
36
34
|
if (pn !== null) {
|
|
37
|
-
map
|
|
38
|
-
dmlBound.add(pn);
|
|
35
|
+
bindParam(map, dmlBound, pn, insertTarget(rel, cols, i), true);
|
|
39
36
|
}
|
|
40
37
|
}
|
|
41
38
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
const select = ins.selectStmt?.SelectStmt;
|
|
40
|
+
if (select && !select.valuesLists && Array.isArray(select.targetList)) {
|
|
41
|
+
for (let i = 0; i < select.targetList.length; i++) {
|
|
42
|
+
const pn = paramNumber(select.targetList[i]?.ResTarget?.val);
|
|
43
|
+
if (pn !== null) {
|
|
44
|
+
bindParam(map, dmlBound, pn, insertTarget(rel, cols, i), true);
|
|
45
|
+
}
|
|
45
46
|
}
|
|
47
|
+
walkSelect(select, map, dmlBound);
|
|
46
48
|
}
|
|
47
|
-
|
|
49
|
+
if (ins.returningList)
|
|
50
|
+
walkExpr(ins.returningList, scope, map, dmlBound);
|
|
51
|
+
walkOnConflict(ins.onConflictClause, rel, scope, map, dmlBound);
|
|
52
|
+
walkExpr(ins.whereClause, scope, map, dmlBound);
|
|
53
|
+
}
|
|
54
|
+
function walkOnConflict(conflict, rel, scope, map, dmlBound) {
|
|
55
|
+
if (!conflict || conflict.action !== "ONCONFLICT_UPDATE")
|
|
56
|
+
return;
|
|
57
|
+
for (const rt of conflict.targetList ?? []) {
|
|
58
|
+
const colName = rt?.ResTarget?.name;
|
|
59
|
+
if (typeof colName !== "string")
|
|
60
|
+
continue;
|
|
61
|
+
const pn = paramNumber(rt.ResTarget.val);
|
|
62
|
+
if (pn !== null) {
|
|
63
|
+
bindParam(map, dmlBound, pn, { ...rel, column: colName }, true);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
walkExpr(conflict.whereClause, scope, map, dmlBound);
|
|
48
67
|
}
|
|
49
68
|
function walkUpdate(upd, map, dmlBound) {
|
|
50
69
|
const rel = relOf(upd.relation);
|
|
51
70
|
if (!rel)
|
|
52
71
|
return;
|
|
72
|
+
const scope = scopeFromRelationNode(upd.relation, rel);
|
|
73
|
+
addRangeVars(upd.fromClause ?? [], scope);
|
|
53
74
|
for (const rt of upd.targetList ?? []) {
|
|
54
75
|
const colName = rt?.ResTarget?.name;
|
|
55
76
|
if (typeof colName !== "string")
|
|
56
77
|
continue;
|
|
57
78
|
const pn = paramNumber(rt.ResTarget.val);
|
|
58
79
|
if (pn !== null) {
|
|
59
|
-
map
|
|
60
|
-
dmlBound.add(pn);
|
|
80
|
+
bindParam(map, dmlBound, pn, { ...rel, column: colName }, true);
|
|
61
81
|
}
|
|
62
82
|
}
|
|
63
|
-
|
|
83
|
+
walkExpr(upd.whereClause, scope, map, dmlBound);
|
|
64
84
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
85
|
+
function walkDelete(del, map, dmlBound) {
|
|
86
|
+
const rel = relOf(del.relation);
|
|
87
|
+
if (!rel)
|
|
67
88
|
return;
|
|
89
|
+
const scope = scopeFromRelationNode(del.relation, rel);
|
|
90
|
+
addRangeVars(del.usingClause ?? [], scope);
|
|
91
|
+
walkExpr(del.whereClause, scope, map, dmlBound);
|
|
92
|
+
}
|
|
93
|
+
function walkSelect(select, map, dmlBound) {
|
|
94
|
+
const scope = scopeFromSelect(select);
|
|
95
|
+
walkJoinQuals(select.fromClause ?? [], scope, map, dmlBound);
|
|
96
|
+
walkExpr(select.whereClause, scope, map, dmlBound);
|
|
97
|
+
}
|
|
98
|
+
function walkExpr(node, scope, map, dmlBound) {
|
|
99
|
+
if (!node)
|
|
100
|
+
return;
|
|
101
|
+
if (Array.isArray(node)) {
|
|
102
|
+
for (const item of node)
|
|
103
|
+
walkExpr(item, scope, map, dmlBound);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
68
106
|
if (node.BoolExpr) {
|
|
69
107
|
for (const a of node.BoolExpr.args ?? [])
|
|
70
|
-
|
|
108
|
+
walkExpr(a, scope, map, dmlBound);
|
|
71
109
|
return;
|
|
72
110
|
}
|
|
73
111
|
if (node.A_Expr) {
|
|
74
|
-
collectFromExpr(node,
|
|
112
|
+
collectFromExpr(node, scope, map, dmlBound);
|
|
113
|
+
walkExpr(node.A_Expr.lexpr, scope, map, dmlBound);
|
|
114
|
+
walkExpr(node.A_Expr.rexpr, scope, map, dmlBound);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (node.TypeCast) {
|
|
118
|
+
walkExpr(node.TypeCast.arg, scope, map, dmlBound);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (node.NullTest) {
|
|
122
|
+
walkExpr(node.NullTest.arg, scope, map, dmlBound);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (node.CoalesceExpr) {
|
|
126
|
+
walkExpr(node.CoalesceExpr.args ?? [], scope, map, dmlBound);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (node.FuncCall) {
|
|
130
|
+
walkExpr(node.FuncCall.args ?? [], scope, map, dmlBound);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (node.CaseExpr) {
|
|
134
|
+
walkExpr(node.CaseExpr.arg, scope, map, dmlBound);
|
|
135
|
+
walkExpr(node.CaseExpr.args ?? [], scope, map, dmlBound);
|
|
136
|
+
walkExpr(node.CaseExpr.defresult, scope, map, dmlBound);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (node.CaseWhen) {
|
|
140
|
+
walkExpr(node.CaseWhen.expr, scope, map, dmlBound);
|
|
141
|
+
walkExpr(node.CaseWhen.result, scope, map, dmlBound);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (node.SubLink) {
|
|
145
|
+
const sub = node.SubLink.subselect?.SelectStmt;
|
|
146
|
+
if (sub)
|
|
147
|
+
walkSelect(sub, map, dmlBound);
|
|
148
|
+
walkExpr(node.SubLink.testexpr, scope, map, dmlBound);
|
|
75
149
|
return;
|
|
76
150
|
}
|
|
77
151
|
}
|
|
78
|
-
function collectFromExpr(node,
|
|
79
|
-
if (!node
|
|
152
|
+
function collectFromExpr(node, scope, map, dmlBound) {
|
|
153
|
+
if (!node)
|
|
80
154
|
return;
|
|
81
155
|
if (node.A_Expr) {
|
|
82
156
|
const e = node.A_Expr;
|
|
83
157
|
const opName = e.name?.[0]?.String?.sval;
|
|
84
158
|
if (e.kind === "AEXPR_OP" && opName === "=") {
|
|
85
|
-
tryBind(e.lexpr, e.rexpr,
|
|
86
|
-
tryBind(e.rexpr, e.lexpr,
|
|
159
|
+
tryBind(e.lexpr, e.rexpr, scope, map, dmlBound);
|
|
160
|
+
tryBind(e.rexpr, e.lexpr, scope, map, dmlBound);
|
|
87
161
|
}
|
|
88
162
|
if (e.kind === "AEXPR_IN") {
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
const list = Array.isArray(e.rexpr) ? e.rexpr : [];
|
|
163
|
+
const target = targetOfColumnRef(e.lexpr, scope);
|
|
164
|
+
if (target) {
|
|
165
|
+
const list = Array.isArray(e.rexpr) ? e.rexpr : e.rexpr?.List?.items ?? [];
|
|
92
166
|
for (const item of list) {
|
|
93
167
|
const pn = paramNumber(item);
|
|
94
168
|
if (pn !== null)
|
|
95
|
-
map
|
|
169
|
+
bindParam(map, dmlBound, pn, target, false);
|
|
96
170
|
}
|
|
97
171
|
}
|
|
98
172
|
}
|
|
99
173
|
}
|
|
100
174
|
}
|
|
101
|
-
function tryBind(colSide, valSide,
|
|
102
|
-
const
|
|
175
|
+
function tryBind(colSide, valSide, scope, map, dmlBound) {
|
|
176
|
+
const target = targetOfColumnRef(colSide, scope);
|
|
103
177
|
const pn = paramNumber(valSide);
|
|
104
|
-
if (
|
|
105
|
-
map
|
|
178
|
+
if (target && pn !== null)
|
|
179
|
+
bindParam(map, dmlBound, pn, target, false);
|
|
106
180
|
}
|
|
107
|
-
function
|
|
181
|
+
function bindParam(map, dmlBound, pn, target, dml) {
|
|
182
|
+
if (!dml && dmlBound.has(pn))
|
|
183
|
+
return;
|
|
184
|
+
map.set(pn, target);
|
|
185
|
+
if (dml)
|
|
186
|
+
dmlBound.add(pn);
|
|
187
|
+
}
|
|
188
|
+
function stringFields(node) {
|
|
108
189
|
if (!node?.ColumnRef)
|
|
109
190
|
return null;
|
|
110
191
|
const fields = node.ColumnRef.fields;
|
|
@@ -112,7 +193,25 @@ function colNameOf(node) {
|
|
|
112
193
|
return null;
|
|
113
194
|
if (fields.some((f) => f.A_Star !== undefined))
|
|
114
195
|
return null;
|
|
115
|
-
|
|
196
|
+
const out = fields.map((f) => f.String?.sval);
|
|
197
|
+
return out.every((s) => typeof s === "string") ? out : null;
|
|
198
|
+
}
|
|
199
|
+
function targetOfColumnRef(node, scope) {
|
|
200
|
+
const fields = stringFields(node);
|
|
201
|
+
if (!fields)
|
|
202
|
+
return null;
|
|
203
|
+
const column = fields[fields.length - 1];
|
|
204
|
+
if (fields.length === 1) {
|
|
205
|
+
return scope.defaultRel ? { ...scope.defaultRel, column } : null;
|
|
206
|
+
}
|
|
207
|
+
if (fields.length === 2) {
|
|
208
|
+
const qualifier = fields[0];
|
|
209
|
+
const rel = scope.aliases.get(qualifier) ?? scope.relations.find((r) => r.table === qualifier);
|
|
210
|
+
return rel ? { ...rel, column } : { table: qualifier, column };
|
|
211
|
+
}
|
|
212
|
+
const table = fields[fields.length - 2];
|
|
213
|
+
const schema = fields[fields.length - 3];
|
|
214
|
+
return { schema, table, column };
|
|
116
215
|
}
|
|
117
216
|
function paramNumber(node) {
|
|
118
217
|
if (node?.ParamRef && typeof node.ParamRef.number === "number")
|
|
@@ -129,14 +228,56 @@ function relOf(relation) {
|
|
|
129
228
|
return null;
|
|
130
229
|
return { schema: relation.schemaname || undefined, table: name };
|
|
131
230
|
}
|
|
132
|
-
function
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
231
|
+
function insertTarget(rel, cols, index) {
|
|
232
|
+
const column = cols[index];
|
|
233
|
+
return column ? { ...rel, column } : { ...rel, columnIndex: index + 1 };
|
|
234
|
+
}
|
|
235
|
+
function scopeFromSelect(select) {
|
|
236
|
+
const scope = scopeFromRelations([], null);
|
|
237
|
+
addRangeVars(select?.fromClause ?? [], scope);
|
|
238
|
+
if (scope.relations.length === 1)
|
|
239
|
+
scope.defaultRel = scope.relations[0];
|
|
240
|
+
return scope;
|
|
241
|
+
}
|
|
242
|
+
function scopeFromRelations(relations, defaultRel) {
|
|
243
|
+
const scope = { aliases: new Map(), relations: [], defaultRel };
|
|
244
|
+
for (const rel of relations)
|
|
245
|
+
addRelation(scope, rel, rel.table);
|
|
246
|
+
return scope;
|
|
247
|
+
}
|
|
248
|
+
function scopeFromRelationNode(relation, rel) {
|
|
249
|
+
const scope = scopeFromRelations([rel], rel);
|
|
250
|
+
const alias = relation?.alias?.aliasname;
|
|
251
|
+
if (typeof alias === "string")
|
|
252
|
+
scope.aliases.set(alias, rel);
|
|
253
|
+
return scope;
|
|
254
|
+
}
|
|
255
|
+
function addRelation(scope, rel, alias) {
|
|
256
|
+
scope.relations.push(rel);
|
|
257
|
+
scope.aliases.set(alias, rel);
|
|
258
|
+
}
|
|
259
|
+
function addRangeVars(nodes, scope) {
|
|
260
|
+
for (const node of nodes) {
|
|
261
|
+
if (node?.RangeVar) {
|
|
262
|
+
const rel = relOf(node.RangeVar);
|
|
263
|
+
if (!rel)
|
|
264
|
+
continue;
|
|
265
|
+
const alias = node.RangeVar.alias?.aliasname ?? rel.table;
|
|
266
|
+
addRelation(scope, rel, alias);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (node?.JoinExpr) {
|
|
270
|
+
addRangeVars([node.JoinExpr.larg, node.JoinExpr.rarg], scope);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function walkJoinQuals(nodes, scope, map, dmlBound) {
|
|
275
|
+
for (const node of nodes) {
|
|
276
|
+
if (!node?.JoinExpr)
|
|
277
|
+
continue;
|
|
278
|
+
walkExpr(node.JoinExpr.quals, scope, map, dmlBound);
|
|
279
|
+
walkJoinQuals([node.JoinExpr.larg, node.JoinExpr.rarg], scope, map, dmlBound);
|
|
280
|
+
}
|
|
140
281
|
}
|
|
141
282
|
function walkForceNullable(node, forceContext, out) {
|
|
142
283
|
if (node === null || node === undefined)
|