@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.
@@ -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
- export declare function prepareOnce(opts: PrepareOptions, session: PrepareSession, log?: (msg: string) => void, err?: (msg: string) => void): Promise<{
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 decl = lookupJsonbType(cfg, target.schema ?? "public", target.table, target.column);
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 col = schema.columnsOf(oid)?.get(t.column);
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 async function prepareOnce(opts, session, log = console.log, err = console.error) {
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
- try {
127
- const d = await client.describe(query);
128
- raw.push({ fp, query, sites: ss, paramOids: d.paramOids, fields: d.fields });
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
- catch (e) {
131
- failures++;
132
- if (e instanceof PgError) {
133
- const extras = [];
134
- if (e.position)
135
- extras.push(`pos ${e.position}`);
136
- if (e.code)
137
- extras.push(`code ${e.code}`);
138
- const tail = extras.length > 0 ? ` (${extras.join(", ")})` : "";
139
- err(` ✗ ${formatSite(site)} — describe failed: ${e.message}${tail}`);
140
- if (e.hint)
141
- err(` hint: ${e.hint}`);
142
- err(` query: ${snippet(query)}`);
143
- }
144
- else {
145
- err(` ✗ ${formatSite(site)} — describe failed: ${e.message}`);
146
- err(` query: ${snippet(query)}`);
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) => cache.read(u.fp)).filter(Boolean);
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);
@@ -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>;
@@ -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) {
@@ -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: "unknown" },
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: "unknown" },
48
+ 3802: { ts: JSON_VALUE },
48
49
  3904: { ts: "string" },
49
50
  3906: { ts: "string" },
50
51
  3908: { ts: "string" },
@@ -1,7 +1,8 @@
1
1
  export type ParamTarget = {
2
2
  schema?: string;
3
3
  table: string;
4
- column: string;
4
+ column?: string;
5
+ columnIndex?: number;
5
6
  };
6
7
  export type ParamMap = Map<number, ParamTarget>;
7
8
  export type ParamMapResult = {
@@ -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
- walkWhere(stmt.SelectStmt.whereClause, defaultRel(stmt.SelectStmt), targets);
15
+ walkSelect(stmt.SelectStmt, targets, dmlBound);
16
16
  else if (stmt.DeleteStmt)
17
- walkWhere(stmt.DeleteStmt.whereClause, relOf(stmt.DeleteStmt.relation), targets);
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.set(pn, { ...rel, column: colName });
38
- dmlBound.add(pn);
35
+ bindParam(map, dmlBound, pn, insertTarget(rel, cols, i), true);
39
36
  }
40
37
  }
41
38
  }
42
- if (ins.returningList) {
43
- for (const rt of ins.returningList) {
44
- collectFromExpr(rt?.ResTarget?.val, rel, map);
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
- walkWhere(ins.whereClause, rel, map);
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.set(pn, { ...rel, column: colName });
60
- dmlBound.add(pn);
80
+ bindParam(map, dmlBound, pn, { ...rel, column: colName }, true);
61
81
  }
62
82
  }
63
- walkWhere(upd.whereClause, rel, map);
83
+ walkExpr(upd.whereClause, scope, map, dmlBound);
64
84
  }
65
- function walkWhere(node, defaultRel, map) {
66
- if (!node || !defaultRel)
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
- walkWhere(a, defaultRel, map);
108
+ walkExpr(a, scope, map, dmlBound);
71
109
  return;
72
110
  }
73
111
  if (node.A_Expr) {
74
- collectFromExpr(node, defaultRel, map);
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, defaultRel, map) {
79
- if (!node || !defaultRel)
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, defaultRel, map);
86
- tryBind(e.rexpr, e.lexpr, defaultRel, map);
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 colName = colNameOf(e.lexpr);
90
- if (colName) {
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.set(pn, { ...defaultRel, column: colName });
169
+ bindParam(map, dmlBound, pn, target, false);
96
170
  }
97
171
  }
98
172
  }
99
173
  }
100
174
  }
101
- function tryBind(colSide, valSide, defaultRel, map) {
102
- const colName = colNameOf(colSide);
175
+ function tryBind(colSide, valSide, scope, map, dmlBound) {
176
+ const target = targetOfColumnRef(colSide, scope);
103
177
  const pn = paramNumber(valSide);
104
- if (colName !== null && pn !== null)
105
- map.set(pn, { ...defaultRel, column: colName });
178
+ if (target && pn !== null)
179
+ bindParam(map, dmlBound, pn, target, false);
106
180
  }
107
- function colNameOf(node) {
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
- return fields[fields.length - 1]?.String?.sval ?? null;
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 defaultRel(select) {
133
- const from = select?.fromClause;
134
- if (!Array.isArray(from) || from.length !== 1)
135
- return null;
136
- const node = from[0];
137
- if (node?.RangeVar)
138
- return relOf(node.RangeVar);
139
- return null;
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)