@onreza/sqlx-js 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ import { decodeText } from "./wire.js";
2
+ const JSON_OIDS = new Set([114, 3802]);
3
+ const JSON_ARRAY_OIDS = new Set([199, 3807]);
4
+ const JSON_INPUT = 'import("@onreza/sqlx-js").JsonInput';
5
+ export async function introspectFunctions(client, schema) {
6
+ const rows = await loadFunctionRows(client);
7
+ const typeOids = new Set();
8
+ for (const row of rows) {
9
+ typeOids.add(row.returnOid);
10
+ for (const oid of row.inputArgOids)
11
+ typeOids.add(oid);
12
+ for (const oid of row.allArgOids ?? [])
13
+ typeOids.add(oid);
14
+ }
15
+ await schema.loadCustomTypes([...typeOids]);
16
+ return rows.map((row) => toEntry(row, schema)).sort((a, b) => a.signature.localeCompare(b.signature));
17
+ }
18
+ async function loadFunctionRows(client) {
19
+ const result = await client.simpleQueryAll(`
20
+ SELECT
21
+ n.nspname,
22
+ p.proname,
23
+ p.prokind::text,
24
+ pg_get_function_identity_arguments(p.oid),
25
+ to_json(
26
+ CASE
27
+ WHEN p.proargtypes::text = '' THEN ARRAY[]::oid[]
28
+ ELSE string_to_array(p.proargtypes::text, ' ')::oid[]
29
+ END
30
+ )::text,
31
+ to_json(p.proallargtypes)::text,
32
+ to_json(p.proargmodes)::text,
33
+ to_json(p.proargnames)::text,
34
+ p.prorettype::int8,
35
+ p.proretset
36
+ FROM pg_proc p
37
+ JOIN pg_namespace n ON n.oid = p.pronamespace
38
+ WHERE ${userSchemaFilter("n")}
39
+ ORDER BY n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)
40
+ `);
41
+ return result.rows.map((row) => ({
42
+ schema: decodeText(row[0]),
43
+ name: decodeText(row[1]),
44
+ kind: functionKind(decodeText(row[2])),
45
+ identityArguments: decodeText(row[3]) ?? "",
46
+ inputArgOids: parseNumberJsonArray(decodeText(row[4])),
47
+ allArgOids: parseNullableNumberJsonArray(decodeText(row[5] ?? null)),
48
+ argModes: parseNullableStringJsonArray(decodeText(row[6] ?? null)),
49
+ argNames: parseNullableStringJsonArray(decodeText(row[7] ?? null)),
50
+ returnOid: Number(decodeText(row[8])),
51
+ returnsSet: decodeText(row[9]) === "t",
52
+ }));
53
+ }
54
+ function toEntry(row, schema) {
55
+ const allArgOids = row.allArgOids ?? row.inputArgOids;
56
+ const modes = row.argModes ?? allArgOids.map(() => "i");
57
+ const params = allArgOids.map((oid, i) => {
58
+ const mode = paramMode(modes[i]);
59
+ const resultTsType = outputTsType(oid, schema);
60
+ return {
61
+ mode,
62
+ tsType: mode === "out" || mode === "table" ? resultTsType : inputTsType(oid, schema),
63
+ ...(mode === "inout" ? { resultTsType } : {}),
64
+ ...(row.argNames?.[i] ? { name: row.argNames[i] } : {}),
65
+ };
66
+ });
67
+ const output = params.filter((p) => p.mode === "out" || p.mode === "inout" || p.mode === "table");
68
+ return {
69
+ schema: row.schema,
70
+ name: row.name,
71
+ signature: `${row.schema}.${row.name}(${row.identityArguments})`,
72
+ kind: row.kind,
73
+ params: params.map(persistedParam),
74
+ returns: returnTsType(row, output, schema),
75
+ returnsSet: row.returnsSet,
76
+ };
77
+ }
78
+ function persistedParam(param) {
79
+ return {
80
+ mode: param.mode,
81
+ tsType: param.tsType,
82
+ ...(param.name ? { name: param.name } : {}),
83
+ };
84
+ }
85
+ function returnTsType(row, output, schema) {
86
+ if (output.length > 0)
87
+ return outputObject(output);
88
+ if (row.kind === "procedure")
89
+ return "void";
90
+ return nullableReturn(schema.tsType(row.returnOid));
91
+ }
92
+ function outputObject(output) {
93
+ const fields = output.map((p, i) => {
94
+ const name = p.name && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(p.name) ? p.name : JSON.stringify(p.name ?? `column${i + 1}`);
95
+ return `${name}: ${nullableReturn(p.resultTsType ?? p.tsType)}`;
96
+ });
97
+ return `{ ${fields.join("; ")} }`;
98
+ }
99
+ function inputTsType(oid, schema) {
100
+ if (JSON_OIDS.has(oid))
101
+ return JSON_INPUT;
102
+ if (JSON_ARRAY_OIDS.has(oid))
103
+ return `(${JSON_INPUT})[]`;
104
+ return schema.tsType(oid);
105
+ }
106
+ function outputTsType(oid, schema) {
107
+ return schema.tsType(oid);
108
+ }
109
+ function nullableReturn(tsType) {
110
+ if (tsType === "void")
111
+ return tsType;
112
+ return `${tsType} | null`;
113
+ }
114
+ function parseNumberJsonArray(raw) {
115
+ if (!raw)
116
+ return [];
117
+ const parsed = JSON.parse(raw);
118
+ return Array.isArray(parsed) ? parsed.map(Number).filter((n) => Number.isFinite(n)) : [];
119
+ }
120
+ function parseNullableNumberJsonArray(raw) {
121
+ if (raw === null)
122
+ return null;
123
+ return parseNumberJsonArray(raw);
124
+ }
125
+ function parseNullableStringJsonArray(raw) {
126
+ if (raw === null)
127
+ return null;
128
+ const parsed = JSON.parse(raw);
129
+ return Array.isArray(parsed) ? parsed.map((v) => (typeof v === "string" ? v : "")) : [];
130
+ }
131
+ function paramMode(raw) {
132
+ switch (raw) {
133
+ case "o": return "out";
134
+ case "b": return "inout";
135
+ case "v": return "variadic";
136
+ case "t": return "table";
137
+ default: return "in";
138
+ }
139
+ }
140
+ function functionKind(raw) {
141
+ switch (raw) {
142
+ case "p": return "procedure";
143
+ case "a": return "aggregate";
144
+ case "w": return "window";
145
+ default: return "function";
146
+ }
147
+ }
148
+ function userSchemaFilter(alias) {
149
+ return `${alias}.nspname <> 'information_schema' AND ${alias}.nspname NOT LIKE 'pg\\_%' ESCAPE '\\'`;
150
+ }
@@ -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" },
@@ -41,10 +42,12 @@ const SCALAR = {
41
42
  2950: { ts: "string" },
42
43
  2205: { ts: "string" },
43
44
  2206: { ts: "string" },
45
+ 2249: { ts: "Record<string, unknown>" },
46
+ 2278: { ts: "void" },
44
47
  3220: { ts: "string" },
45
48
  3614: { ts: "string" },
46
49
  3615: { ts: "string" },
47
- 3802: { ts: "unknown" },
50
+ 3802: { ts: JSON_VALUE },
48
51
  3904: { ts: "string" },
49
52
  3906: { ts: "string" },
50
53
  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)
@@ -74,4 +74,5 @@ export declare class SchemaCache {
74
74
  loadCustomTypes(typeOids: number[]): Promise<void>;
75
75
  private resolveBaseTs;
76
76
  customType(oid: number): CustomTypeInfo | undefined;
77
+ tsType(oid: number): string;
77
78
  }
@@ -262,6 +262,9 @@ export class SchemaCache {
262
262
  customType(oid) {
263
263
  return this.customTypes.get(oid);
264
264
  }
265
+ tsType(oid) {
266
+ return this.resolveBaseTs(oid) ?? "unknown";
267
+ }
265
268
  }
266
269
  function key(oid, attno) {
267
270
  return `${oid}/${attno}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onreza/sqlx-js",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Compile-time-checked raw SQL for TypeScript + PostgreSQL. Inspired by sqlx.",
5
5
  "license": "MIT",
6
6
  "type": "module",