@typed-sql/postgres 1.0.0-beta.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 @@
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../../../src/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAMV,eAAe,EACf,WAAW,EACX,aAAa,EACb,SAAS,EAIV,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAoC,cAAc,EAAiB,MAAM,mBAAmB,CAAC;AACzG,OAAO,EAAmE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAc5H,UAAU,YAAY;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,cAAe,SAAQ,YAAY;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IAC5C,QAAQ,CAAC,WAAW,EAAE,SAAS,aAAa,EAAE,CAAC;IAC/C,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;IACzC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACtC;AAikBD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,cAAmB,GAAG,aAAa,CAE1H;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,cAAmB,GAAG,aAAa,CAE7H;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAGzE"}
@@ -0,0 +1,595 @@
1
+ import { defaultPostgresTypePolicy, isKnownPostgresType, mapPostgresType } from "./type-policy.js";
2
+ const booleanOperators = new Set([
3
+ "=", "!=", "<>", "<", "<=", ">", ">=", "IS", "IS NOT", "IS DISTINCT FROM",
4
+ "IS NOT DISTINCT FROM", "LIKE", "NOT LIKE", "ILIKE", "NOT ILIKE", "SIMILAR TO",
5
+ "NOT SIMILAR", "~", "~*", "!~", "!~*", "AND", "OR", "@>", "<@", "?", "?|", "?&", "&&",
6
+ ]);
7
+ const numericTypes = new Set(["smallint", "int2", "integer", "int", "int4", "bigint", "int8", "numeric", "decimal", "real", "float4", "double precision", "float8"]);
8
+ function sqlName(identifier) {
9
+ return identifier.quoted ? identifier.name : identifier.name.toLowerCase();
10
+ }
11
+ function normalizeDatabaseType(value) {
12
+ return value.trim().toLowerCase().replace(/\(\d+(?:,\s*\d+)?\)/gu, "").replace(/\s+/gu, " ");
13
+ }
14
+ function distance(a, b) {
15
+ const rows = Array.from({ length: a.length + 1 }, (_, index) => index);
16
+ for (let column = 1; column <= b.length; column += 1) {
17
+ let diagonal = rows[0];
18
+ rows[0] = column;
19
+ for (let row = 1; row <= a.length; row += 1) {
20
+ const above = rows[row];
21
+ rows[row] = Math.min(rows[row] + 1, rows[row - 1] + 1, diagonal + (a[row - 1] === b[column - 1] ? 0 : 1));
22
+ diagonal = above;
23
+ }
24
+ }
25
+ return rows[a.length];
26
+ }
27
+ function suggestion(name, candidates) {
28
+ const candidate = [...candidates].sort((a, b) => distance(name, a) - distance(name, b))[0];
29
+ if (candidate === undefined || distance(name, candidate) > Math.max(2, Math.floor(name.length / 2)))
30
+ return undefined;
31
+ return `Did you mean ${candidate}?`;
32
+ }
33
+ function unionTypes(types) {
34
+ const distinct = [...new Set(types)];
35
+ if (distinct.includes("unknown"))
36
+ return "unknown";
37
+ return distinct.join(" | ") || "unknown";
38
+ }
39
+ class Resolver {
40
+ #schema;
41
+ #policy;
42
+ #strictExpressions;
43
+ #diagnostics = [];
44
+ constructor(schema, options) {
45
+ this.#schema = schema;
46
+ this.#policy = options.typePolicy ?? defaultPostgresTypePolicy;
47
+ this.#strictExpressions = options.strictExpressions ?? true;
48
+ }
49
+ resolve(statement) {
50
+ if (this.#schema.dialect !== "postgres") {
51
+ this.#diagnostic("TSQ007", `PostgreSQL resolver cannot analyze ${this.#schema.dialect}`, statement.range);
52
+ }
53
+ const result = this.#resolveStatement(statement, undefined, new Map());
54
+ return { ...result, diagnostics: this.#diagnostics };
55
+ }
56
+ #resolveStatement(statement, outer, inheritedCtes) {
57
+ const ctes = this.#resolveWith(statement.with, outer, inheritedCtes);
58
+ switch (statement.kind) {
59
+ case "select": return { columns: this.#resolveSelect(statement, outer, ctes), resultKind: "rows" };
60
+ case "insert": return this.#resolveInsert(statement, outer, ctes);
61
+ case "update": return this.#resolveUpdate(statement, outer, ctes);
62
+ case "delete": return this.#resolveDelete(statement, outer, ctes);
63
+ }
64
+ }
65
+ #resolveWith(withClause, outer, inherited) {
66
+ const ctes = new Map(inherited);
67
+ if (withClause === undefined)
68
+ return ctes;
69
+ if (withClause.recursive) {
70
+ this.#diagnostic("TSQ210", "Recursive CTE inference is not supported safely", withClause.range, "Use a non-recursive CTE or annotate the query explicitly.");
71
+ }
72
+ for (const query of withClause.queries) {
73
+ const key = sqlName(query.name);
74
+ if (ctes.has(key))
75
+ this.#diagnostic("TSQ211", `Duplicate CTE ${query.name.name}`, query.name.range);
76
+ if (withClause.recursive) {
77
+ const placeholderColumns = {};
78
+ for (const column of query.columns) {
79
+ placeholderColumns[sqlName(column)] = { name: sqlName(column), databaseType: "unknown", tsType: "unknown", nullable: true };
80
+ }
81
+ ctes.set(key, { name: key, columns: placeholderColumns });
82
+ }
83
+ const resolved = this.#resolveStatement(query.statement, outer, ctes);
84
+ if (resolved.resultKind === "command") {
85
+ this.#diagnostic("TSQ212", `CTE ${query.name.name} does not return rows`, query.range, "Add RETURNING to the data-changing CTE.");
86
+ }
87
+ if (query.columns.length > 0 && query.columns.length !== resolved.columns.length) {
88
+ this.#diagnostic("TSQ213", `CTE ${query.name.name} declares ${query.columns.length} columns but returns ${resolved.columns.length}`, query.range);
89
+ }
90
+ const columns = {};
91
+ resolved.columns.forEach((column, index) => {
92
+ const declared = query.columns[index];
93
+ const name = declared === undefined ? column.name : sqlName(declared);
94
+ columns[name] = {
95
+ name,
96
+ databaseType: column.databaseType ?? "unknown",
97
+ tsType: column.tsType,
98
+ nullable: column.nullable,
99
+ };
100
+ });
101
+ ctes.set(key, { name: key, columns });
102
+ }
103
+ return ctes;
104
+ }
105
+ #resolveSelect(statement, outer, ctes) {
106
+ const scope = { relations: [], usingColumns: new Map(), ...(outer === undefined ? {} : { outer }) };
107
+ if (statement.from !== undefined)
108
+ this.#addRelation(statement.from, false, scope, ctes);
109
+ for (const join of statement.joins)
110
+ this.#addJoin(join, scope, ctes);
111
+ if (statement.where !== undefined)
112
+ this.#resolveExpression(statement.where, scope, ctes);
113
+ for (const expression of statement.groupBy)
114
+ this.#resolveExpression(expression, scope, ctes);
115
+ if (statement.having !== undefined)
116
+ this.#resolveExpression(statement.having, scope, ctes);
117
+ for (const expression of statement.distinctOn)
118
+ this.#resolveExpression(expression, scope, ctes);
119
+ for (const window of statement.windows) {
120
+ for (const expression of window.specification.partitionBy)
121
+ this.#resolveExpression(expression, scope, ctes);
122
+ for (const order of window.specification.orderBy)
123
+ this.#resolveExpression(order.expression, scope, ctes);
124
+ }
125
+ for (const order of statement.orderBy)
126
+ this.#resolveExpression(order.expression, scope, ctes);
127
+ if (statement.limit !== undefined)
128
+ this.#resolveExpression(statement.limit, scope, ctes);
129
+ if (statement.offset !== undefined)
130
+ this.#resolveExpression(statement.offset, scope, ctes);
131
+ return this.#resolveItems(statement.columns, scope, ctes);
132
+ }
133
+ #resolveInsert(statement, outer, ctes) {
134
+ const scope = { relations: [], usingColumns: new Map(), ...(outer === undefined ? {} : { outer }) };
135
+ const target = this.#addRelation(statement.table, false, scope, ctes);
136
+ const targetColumns = statement.columns.length === 0
137
+ ? Object.values(target?.table.columns ?? {})
138
+ : statement.columns.map((column) => this.#findColumn(target?.table, column));
139
+ if (statement.source.kind === "values") {
140
+ for (const row of statement.source.rows) {
141
+ if (row.length !== targetColumns.length) {
142
+ this.#diagnostic("TSQ214", `INSERT has ${targetColumns.length} target columns but ${row.length} values`, statement.source.range);
143
+ }
144
+ for (const value of row)
145
+ this.#resolveExpression(value, scope, ctes);
146
+ }
147
+ }
148
+ else if (statement.source.kind === "select") {
149
+ const source = this.#resolveStatement(statement.source, outer, ctes);
150
+ if (source.columns.length !== targetColumns.length) {
151
+ this.#diagnostic("TSQ214", `INSERT has ${targetColumns.length} target columns but SELECT returns ${source.columns.length}`, statement.source.range);
152
+ }
153
+ }
154
+ const columns = this.#resolveItems(statement.returning, scope, ctes);
155
+ return { columns, resultKind: statement.returning.length === 0 ? "command" : "rows" };
156
+ }
157
+ #resolveUpdate(statement, outer, ctes) {
158
+ const scope = { relations: [], usingColumns: new Map(), ...(outer === undefined ? {} : { outer }) };
159
+ const target = this.#addRelation(statement.table, false, scope, ctes);
160
+ for (const assignment of statement.assignments) {
161
+ this.#findColumn(target?.table, assignment.column);
162
+ this.#resolveExpression(assignment.value, scope, ctes);
163
+ }
164
+ if (statement.from !== undefined)
165
+ this.#addRelation(statement.from, false, scope, ctes);
166
+ for (const join of statement.joins)
167
+ this.#addJoin(join, scope, ctes);
168
+ if (statement.where !== undefined)
169
+ this.#resolveExpression(statement.where, scope, ctes);
170
+ const columns = this.#resolveItems(statement.returning, scope, ctes);
171
+ return { columns, resultKind: statement.returning.length === 0 ? "command" : "rows" };
172
+ }
173
+ #resolveDelete(statement, outer, ctes) {
174
+ const scope = { relations: [], usingColumns: new Map(), ...(outer === undefined ? {} : { outer }) };
175
+ this.#addRelation(statement.table, false, scope, ctes);
176
+ for (const reference of statement.using)
177
+ this.#addRelation(reference, false, scope, ctes);
178
+ if (statement.where !== undefined)
179
+ this.#resolveExpression(statement.where, scope, ctes);
180
+ const columns = this.#resolveItems(statement.returning, scope, ctes);
181
+ return { columns, resultKind: statement.returning.length === 0 ? "command" : "rows" };
182
+ }
183
+ #addJoin(join, scope, ctes) {
184
+ const previous = [...scope.relations];
185
+ if (join.kind === "right" || join.kind === "full")
186
+ for (const relation of previous)
187
+ relation.nullable = true;
188
+ const relation = this.#addRelation(join.table, join.kind === "left" || join.kind === "full", scope, ctes);
189
+ if (join.on !== undefined)
190
+ this.#resolveExpression(join.on, scope, ctes);
191
+ if (join.using !== undefined && relation !== undefined) {
192
+ for (const identifier of join.using) {
193
+ const name = sqlName(identifier);
194
+ const leftMatches = previous.filter((candidate) => this.#column(candidate, name, identifier.quoted) !== undefined);
195
+ const right = this.#column(relation, name, identifier.quoted);
196
+ if (leftMatches.length !== 1 || right === undefined) {
197
+ this.#diagnostic("TSQ215", `JOIN USING column ${identifier.name} must exist once on both sides`, identifier.range);
198
+ continue;
199
+ }
200
+ const left = this.#columnType(leftMatches[0], this.#column(leftMatches[0], name, identifier.quoted));
201
+ const rightType = this.#columnType(relation, right);
202
+ scope.usingColumns.set(name, {
203
+ tsType: unionTypes([left.tsType, rightType.tsType]),
204
+ nullable: join.kind === "left" ? left.nullable : join.kind === "right" ? rightType.nullable : left.nullable || rightType.nullable,
205
+ ...(left.databaseType === rightType.databaseType && left.databaseType !== undefined ? { databaseType: left.databaseType } : {}),
206
+ });
207
+ }
208
+ }
209
+ }
210
+ #addRelation(reference, nullable, scope, ctes) {
211
+ let table;
212
+ let alias;
213
+ if (reference.kind === "subquery") {
214
+ const outer = reference.lateral ? scope : scope.outer;
215
+ const resolved = this.#resolveStatement(reference.query, outer, ctes);
216
+ const columns = {};
217
+ for (const column of resolved.columns) {
218
+ columns[column.name] = {
219
+ name: column.name,
220
+ databaseType: column.databaseType ?? "unknown",
221
+ tsType: column.tsType,
222
+ nullable: column.nullable,
223
+ };
224
+ }
225
+ table = { name: sqlName(reference.alias), columns };
226
+ alias = sqlName(reference.alias);
227
+ }
228
+ else {
229
+ const requested = sqlName(reference.name);
230
+ const requestedSchema = reference.schema === undefined ? undefined : sqlName(reference.schema);
231
+ if (requestedSchema === undefined)
232
+ table = ctes.get(requested);
233
+ if (table === undefined) {
234
+ const entries = Object.entries(this.#schema.tables).filter(([key, candidate]) => {
235
+ const nameMatches = reference.name.quoted
236
+ ? key === requested || key.endsWith(`.${requested}`) || candidate.name === requested
237
+ : key.toLowerCase() === requested || key.toLowerCase().endsWith(`.${requested}`) || candidate.name.toLowerCase() === requested;
238
+ const schemaMatches = requestedSchema === undefined || (reference.schema?.quoted
239
+ ? candidate.schema === requestedSchema || key === `${requestedSchema}.${requested}`
240
+ : candidate.schema?.toLowerCase() === requestedSchema || key.toLowerCase() === `${requestedSchema}.${requested}`);
241
+ return nameMatches && schemaMatches;
242
+ });
243
+ if (entries.length === 0) {
244
+ this.#diagnostic("TSQ100", `Unknown table ${reference.name.name}`, reference.name.range, suggestion(requested, [...ctes.keys(), ...Object.keys(this.#schema.tables)]));
245
+ return undefined;
246
+ }
247
+ if (requestedSchema === undefined && entries.length > 1) {
248
+ this.#diagnostic("TSQ107", `Ambiguous table ${reference.name.name}`, reference.name.range, "Qualify the table with a schema name.");
249
+ return undefined;
250
+ }
251
+ table = entries[0][1];
252
+ }
253
+ alias = reference.alias === undefined ? requested : sqlName(reference.alias);
254
+ }
255
+ if (scope.relations.some((candidate) => candidate.alias === alias)) {
256
+ this.#diagnostic("TSQ108", `Duplicate relation alias ${alias}`, reference.range, "Use a unique table alias.");
257
+ return undefined;
258
+ }
259
+ const relation = { alias, table, nullable };
260
+ scope.relations.push(relation);
261
+ return relation;
262
+ }
263
+ #resolveItems(items, scope, ctes) {
264
+ const columns = [];
265
+ const names = new Set();
266
+ const add = (column) => {
267
+ if (names.has(column.name)) {
268
+ this.#diagnostic("TSQ105", `Duplicate output property ${column.name}`, column.range, "Give one expression a unique alias.");
269
+ return;
270
+ }
271
+ names.add(column.name);
272
+ columns.push(column);
273
+ };
274
+ for (const item of items) {
275
+ if (item.expression.kind === "star") {
276
+ const star = item.expression;
277
+ const relations = star.relation === undefined
278
+ ? scope.relations
279
+ : scope.relations.filter((relation) => relation.alias === sqlName(star.relation));
280
+ if (relations.length === 0) {
281
+ this.#diagnostic("TSQ103", star.relation === undefined ? "SELECT * requires a FROM relation" : `Unknown relation alias ${star.relation.name}`, star.range);
282
+ continue;
283
+ }
284
+ if (star.relation === undefined) {
285
+ for (const [name, type] of scope.usingColumns)
286
+ add({ name, ...type, range: item.range });
287
+ }
288
+ for (const relation of relations) {
289
+ for (const column of Object.values(relation.table.columns)) {
290
+ if (star.relation === undefined && (scope.usingColumns.has(column.name) || scope.usingColumns.has(column.name.toLowerCase())))
291
+ continue;
292
+ add({ name: column.name, ...this.#columnType(relation, column), range: item.range });
293
+ }
294
+ }
295
+ continue;
296
+ }
297
+ const type = this.#resolveExpression(item.expression, scope, ctes);
298
+ const inferredName = this.#outputName(item.expression);
299
+ const name = item.alias === undefined ? inferredName : sqlName(item.alias);
300
+ if (name === undefined) {
301
+ if (this.#strictExpressions)
302
+ this.#diagnostic("TSQ104", "Expressions in SELECT or RETURNING require an explicit alias", item.range, "Add AS <name>.");
303
+ continue;
304
+ }
305
+ add({ name, ...type, range: item.range });
306
+ }
307
+ return columns;
308
+ }
309
+ #outputName(expression) {
310
+ if (expression.kind === "column")
311
+ return sqlName(expression.column);
312
+ if (expression.kind === "cast")
313
+ return this.#outputName(expression.expression);
314
+ if (expression.kind === "call")
315
+ return sqlName(expression.name);
316
+ if (expression.kind === "case")
317
+ return "case";
318
+ return undefined;
319
+ }
320
+ #resolveExpression(expression, scope, ctes) {
321
+ switch (expression.kind) {
322
+ case "column": {
323
+ if (expression.relation === undefined && expression.column.name === "DEFAULT")
324
+ return { tsType: "unknown", nullable: true };
325
+ return this.#resolveColumn(expression.relation, expression.column, scope);
326
+ }
327
+ case "literal": {
328
+ if (expression.value === null)
329
+ return { tsType: "unknown", nullable: true };
330
+ if (typeof expression.value === "boolean")
331
+ return { tsType: "boolean", nullable: false, databaseType: "boolean" };
332
+ if (typeof expression.value === "number")
333
+ return { tsType: "number", nullable: false, databaseType: Number.isInteger(expression.value) ? "integer" : "numeric" };
334
+ return { tsType: "string", nullable: false, databaseType: "text" };
335
+ }
336
+ case "parameter": return { tsType: "unknown", nullable: true };
337
+ case "star": return { tsType: "unknown", nullable: false };
338
+ case "array": {
339
+ const elements = expression.elements.map((element) => this.#resolveExpression(element, scope, ctes));
340
+ const elementType = unionTypes(elements.map((element) => element.tsType));
341
+ const databaseTypes = [...new Set(elements.map((element) => element.databaseType).filter((value) => value !== undefined))];
342
+ return {
343
+ tsType: `readonly (${elementType})[]`,
344
+ nullable: false,
345
+ ...(databaseTypes.length === 1 ? { databaseType: `${databaseTypes[0]}[]` } : {}),
346
+ };
347
+ }
348
+ case "row": {
349
+ const elements = expression.elements.map((element) => this.#resolveExpression(element, scope, ctes));
350
+ return { tsType: `readonly [${elements.map((element) => element.tsType + (element.nullable ? " | null" : "")).join(", ")}]`, nullable: false };
351
+ }
352
+ case "cast": {
353
+ const source = this.#resolveExpression(expression.expression, scope, ctes);
354
+ if (!isKnownPostgresType(expression.databaseType.name, this.#schema)) {
355
+ this.#diagnostic("TSQ106", `Invalid or unknown PostgreSQL cast type ${expression.databaseType.name}`, expression.databaseType.range);
356
+ }
357
+ return { tsType: mapPostgresType(expression.databaseType.name, this.#policy, this.#schema), nullable: source.nullable, databaseType: normalizeDatabaseType(expression.databaseType.name) };
358
+ }
359
+ case "unary": {
360
+ const operand = this.#resolveExpression(expression.expression, scope, ctes);
361
+ return expression.operator === "NOT" ? { tsType: "boolean", nullable: operand.nullable, databaseType: "boolean" } : operand;
362
+ }
363
+ case "binary": return this.#resolveBinary(expression, scope, ctes);
364
+ case "call": return this.#resolveCall(expression, scope, ctes);
365
+ case "case": {
366
+ if (expression.operand !== undefined)
367
+ this.#resolveExpression(expression.operand, scope, ctes);
368
+ for (const branch of expression.branches)
369
+ this.#resolveExpression(branch.when, scope, ctes);
370
+ const results = expression.branches.map((branch) => this.#resolveExpression(branch.then, scope, ctes));
371
+ if (expression.elseExpression !== undefined)
372
+ results.push(this.#resolveExpression(expression.elseExpression, scope, ctes));
373
+ const databaseTypes = [...new Set(results.map((result) => result.databaseType).filter((value) => value !== undefined))];
374
+ return {
375
+ tsType: unionTypes(results.map((result) => result.tsType)),
376
+ nullable: expression.elseExpression === undefined || results.some((result) => result.nullable),
377
+ ...(databaseTypes.length === 1 ? { databaseType: databaseTypes[0] } : {}),
378
+ };
379
+ }
380
+ case "subquery": {
381
+ const resolved = this.#resolveStatement(expression.query, scope, ctes);
382
+ if (resolved.columns.length !== 1) {
383
+ this.#diagnostic("TSQ216", `Scalar subquery returns ${resolved.columns.length} columns instead of one`, expression.range);
384
+ return { tsType: "unknown", nullable: true };
385
+ }
386
+ const column = resolved.columns[0];
387
+ return { tsType: column.tsType, nullable: true, ...(column.databaseType === undefined ? {} : { databaseType: column.databaseType }) };
388
+ }
389
+ case "exists": {
390
+ this.#resolveStatement(expression.query, scope, ctes);
391
+ return { tsType: "boolean", nullable: false, databaseType: "boolean" };
392
+ }
393
+ case "in": {
394
+ const subject = this.#resolveExpression(expression.expression, scope, ctes);
395
+ let nullable = subject.nullable;
396
+ if (Array.isArray(expression.values)) {
397
+ const values = expression.values.map((value) => this.#resolveExpression(value, scope, ctes));
398
+ nullable ||= values.some((value) => value.nullable);
399
+ }
400
+ else {
401
+ const resolved = this.#resolveStatement(expression.values, scope, ctes);
402
+ if (resolved.columns.length !== 1)
403
+ this.#diagnostic("TSQ217", `IN subquery returns ${resolved.columns.length} columns instead of one`, expression.range);
404
+ nullable ||= resolved.columns[0]?.nullable ?? true;
405
+ }
406
+ return { tsType: "boolean", nullable, databaseType: "boolean" };
407
+ }
408
+ case "between": {
409
+ const subject = this.#resolveExpression(expression.expression, scope, ctes);
410
+ const lower = this.#resolveExpression(expression.lower, scope, ctes);
411
+ const upper = this.#resolveExpression(expression.upper, scope, ctes);
412
+ return { tsType: "boolean", nullable: subject.nullable || lower.nullable || upper.nullable, databaseType: "boolean" };
413
+ }
414
+ }
415
+ }
416
+ #resolveBinary(expression, scope, ctes) {
417
+ const left = this.#resolveExpression(expression.left, scope, ctes);
418
+ const right = this.#resolveExpression(expression.right, scope, ctes);
419
+ if (booleanOperators.has(expression.operator)) {
420
+ const neverNull = expression.operator.startsWith("IS");
421
+ return { tsType: "boolean", nullable: neverNull ? false : left.nullable || right.nullable, databaseType: "boolean" };
422
+ }
423
+ if (["->", "#>"].includes(expression.operator))
424
+ return { tsType: this.#policy.json, nullable: true, databaseType: "jsonb" };
425
+ if (["->>", "#>>"].includes(expression.operator))
426
+ return { tsType: "string", nullable: true, databaseType: "text" };
427
+ if (expression.operator === "||") {
428
+ if (left.tsType.startsWith("readonly (") && right.tsType.startsWith("readonly (")) {
429
+ const databaseType = left.databaseType ?? right.databaseType;
430
+ return { tsType: unionTypes([left.tsType, right.tsType]), nullable: left.nullable || right.nullable, ...(databaseType === undefined ? {} : { databaseType }) };
431
+ }
432
+ return { tsType: "string", nullable: left.nullable || right.nullable, databaseType: "text" };
433
+ }
434
+ const leftDatabase = left.databaseType === undefined ? undefined : normalizeDatabaseType(left.databaseType);
435
+ const rightDatabase = right.databaseType === undefined ? undefined : normalizeDatabaseType(right.databaseType);
436
+ if (leftDatabase !== undefined && rightDatabase !== undefined && numericTypes.has(leftDatabase) && numericTypes.has(rightDatabase)) {
437
+ const databaseType = [leftDatabase, rightDatabase].some((type) => type === "numeric" || type === "decimal") ? "numeric" : "double precision";
438
+ return { tsType: databaseType === "numeric" ? this.#policy.numeric : "number", nullable: left.nullable || right.nullable, databaseType };
439
+ }
440
+ if (left.tsType === "unknown" || right.tsType === "unknown")
441
+ return { tsType: "unknown", nullable: left.nullable || right.nullable };
442
+ this.#diagnostic("TSQ203", `Cannot safely infer operator ${expression.operator} for ${leftDatabase ?? left.tsType} and ${rightDatabase ?? right.tsType}`, expression.range);
443
+ return { tsType: "unknown", nullable: true };
444
+ }
445
+ #resolveColumn(relationIdentifier, columnIdentifier, scope) {
446
+ const name = sqlName(columnIdentifier);
447
+ if (scope.relations.length === 0 && this.#diagnostics.some((diagnostic) => diagnostic.code === "TSQ100")) {
448
+ return { tsType: "unknown", nullable: true };
449
+ }
450
+ if (relationIdentifier === undefined) {
451
+ const using = scope.usingColumns.get(name);
452
+ if (using !== undefined)
453
+ return using;
454
+ }
455
+ const matches = this.#columnMatches(relationIdentifier, name, scope, columnIdentifier.quoted);
456
+ if (matches.length > 1) {
457
+ this.#diagnostic("TSQ102", `Ambiguous column ${columnIdentifier.name}`, columnIdentifier.range, "Qualify the column with a table alias.");
458
+ return { tsType: "unknown", nullable: true };
459
+ }
460
+ if (matches.length === 0) {
461
+ if (scope.outer !== undefined)
462
+ return this.#resolveColumn(relationIdentifier, columnIdentifier, scope.outer);
463
+ if (relationIdentifier !== undefined && !this.#relation(scope, sqlName(relationIdentifier))) {
464
+ this.#diagnostic("TSQ103", `Unknown relation alias ${relationIdentifier.name}`, relationIdentifier.range, suggestion(sqlName(relationIdentifier), scope.relations.map((item) => item.alias)));
465
+ }
466
+ else {
467
+ const candidates = scope.relations.flatMap((relation) => Object.keys(relation.table.columns));
468
+ this.#diagnostic("TSQ101", `Unknown column ${columnIdentifier.name}`, columnIdentifier.range, suggestion(name, candidates));
469
+ }
470
+ return { tsType: "unknown", nullable: true };
471
+ }
472
+ const [relation, column] = matches[0];
473
+ return this.#columnType(relation, column);
474
+ }
475
+ #columnMatches(relationIdentifier, name, scope, quoted) {
476
+ if (relationIdentifier !== undefined) {
477
+ const relation = this.#relation(scope, sqlName(relationIdentifier));
478
+ const column = relation === undefined ? undefined : this.#column(relation, name, quoted);
479
+ return relation === undefined || column === undefined ? [] : [[relation, column]];
480
+ }
481
+ return scope.relations.flatMap((relation) => {
482
+ const column = this.#column(relation, name, quoted);
483
+ return column === undefined ? [] : [[relation, column]];
484
+ });
485
+ }
486
+ #relation(scope, alias) {
487
+ return scope.relations.find((candidate) => candidate.alias === alias);
488
+ }
489
+ #column(relation, name, quoted = false) {
490
+ return Object.entries(relation.table.columns).find(([key, column]) => quoted
491
+ ? key === name || column.name === name
492
+ : key.toLowerCase() === name || column.name.toLowerCase() === name)?.[1];
493
+ }
494
+ #findColumn(table, identifier) {
495
+ const name = sqlName(identifier);
496
+ const column = table === undefined ? undefined : Object.entries(table.columns).find(([key, candidate]) => identifier.quoted
497
+ ? key === name || candidate.name === name
498
+ : key.toLowerCase() === name || candidate.name.toLowerCase() === name)?.[1];
499
+ if (column === undefined)
500
+ this.#diagnostic("TSQ101", `Unknown column ${name}`, identifier.range, suggestion(name, Object.keys(table?.columns ?? {})));
501
+ return column;
502
+ }
503
+ #columnType(relation, column) {
504
+ return { tsType: column.tsType, nullable: column.nullable || relation.nullable, databaseType: column.databaseType };
505
+ }
506
+ #resolveCall(expression, scope, ctes) {
507
+ const resolved = expression.arguments.map((argument) => this.#resolveExpression(argument, scope, ctes));
508
+ if (expression.filter !== undefined)
509
+ this.#resolveExpression(expression.filter, scope, ctes);
510
+ if (expression.over !== undefined && "partitionBy" in expression.over) {
511
+ for (const item of expression.over.partitionBy)
512
+ this.#resolveExpression(item, scope, ctes);
513
+ for (const item of expression.over.orderBy)
514
+ this.#resolveExpression(item.expression, scope, ctes);
515
+ }
516
+ const name = expression.name.name.toUpperCase();
517
+ if (name === "COUNT")
518
+ return { tsType: this.#policy.bigint, nullable: false, databaseType: "bigint" };
519
+ if (name === "COALESCE") {
520
+ const known = resolved.filter((_, index) => {
521
+ const argument = expression.arguments[index];
522
+ return argument?.kind !== "literal" || argument.value !== null;
523
+ });
524
+ return {
525
+ tsType: unionTypes(known.map((result) => result.tsType)),
526
+ nullable: resolved.every((result) => result.nullable),
527
+ ...(known.length > 0 && known.every((result) => result.databaseType === known[0]?.databaseType) && known[0]?.databaseType !== undefined ? { databaseType: known[0].databaseType } : {}),
528
+ };
529
+ }
530
+ if (name === "NULLIF")
531
+ return { ...(resolved[0] ?? { tsType: "unknown" }), nullable: true };
532
+ if (["MIN", "MAX", "GREATEST", "LEAST"].includes(name))
533
+ return { ...(resolved[0] ?? { tsType: "unknown", nullable: true }), nullable: name === "MIN" || name === "MAX" ? true : resolved.some((result) => result.nullable) };
534
+ if (name === "SUM" || name === "AVG")
535
+ return { tsType: resolved[0]?.tsType ?? this.#policy.numeric, nullable: true, databaseType: resolved[0]?.databaseType ?? "numeric" };
536
+ if (["BOOL_AND", "BOOL_OR", "EVERY"].includes(name))
537
+ return { tsType: "boolean", nullable: true, databaseType: "boolean" };
538
+ if (name === "STRING_AGG")
539
+ return { tsType: "string", nullable: true, databaseType: "text" };
540
+ if (["JSON_AGG", "JSONB_AGG", "JSON_OBJECT_AGG", "JSONB_OBJECT_AGG"].includes(name))
541
+ return { tsType: this.#policy.json, nullable: true, databaseType: name.startsWith("JSONB") ? "jsonb" : "json" };
542
+ if (name === "ARRAY_AGG") {
543
+ const item = resolved[0] ?? { tsType: "unknown", nullable: true };
544
+ return { tsType: `readonly (${item.tsType}${item.nullable ? " | null" : ""})[]`, nullable: true, ...(item.databaseType === undefined ? {} : { databaseType: `${item.databaseType}[]` }) };
545
+ }
546
+ const functionName = sqlName(expression.name);
547
+ const schemaName = expression.schema === undefined ? undefined : sqlName(expression.schema);
548
+ const candidates = Object.values(this.#schema.functions ?? {}).filter((candidate) => candidate.name.toLowerCase() === functionName
549
+ && candidate.argumentTypes.length === expression.arguments.length
550
+ && (schemaName === undefined || candidate.schema?.toLowerCase() === schemaName));
551
+ const exact = candidates.filter((candidate) => candidate.argumentTypes.every((type, index) => {
552
+ const actual = resolved[index]?.databaseType;
553
+ return actual === undefined || this.#typesCompatible(type, actual);
554
+ }));
555
+ const selected = exact.length === 1 ? exact[0] : candidates.length === 1 ? candidates[0] : undefined;
556
+ if (selected !== undefined)
557
+ return this.#functionType(selected);
558
+ if (exact.length > 1 || candidates.length > 1) {
559
+ this.#diagnostic("TSQ204", `Ambiguous overloaded function ${expression.name.name}`, expression.range, "Cast arguments to select a specific overload.");
560
+ return { tsType: "unknown", nullable: true };
561
+ }
562
+ this.#diagnostic("TSQ202", `Unknown function ${expression.name.name}`, expression.range, undefined, "warning");
563
+ return { tsType: "unknown", nullable: true };
564
+ }
565
+ #typesCompatible(expected, actual) {
566
+ const left = normalizeDatabaseType(expected);
567
+ const right = normalizeDatabaseType(actual);
568
+ if (left === right)
569
+ return true;
570
+ if (numericTypes.has(left) && numericTypes.has(right))
571
+ return true;
572
+ return false;
573
+ }
574
+ #functionType(value) {
575
+ return {
576
+ tsType: value.returnType,
577
+ nullable: value.nullable,
578
+ ...(value.databaseReturnType === undefined ? {} : { databaseType: value.databaseReturnType }),
579
+ };
580
+ }
581
+ #diagnostic(code, message, range, suggestionText, severity = "error") {
582
+ this.#diagnostics.push({ code, message, range, severity, ...(suggestionText === undefined ? {} : { suggestion: suggestionText }) });
583
+ }
584
+ }
585
+ export function resolveStatement(statement, schema, options = {}) {
586
+ return new Resolver(schema, options).resolve(statement);
587
+ }
588
+ export function resolveSelect(statement, schema, options = {}) {
589
+ return resolveStatement(statement, schema, options);
590
+ }
591
+ export function rowTypeLiteral(columns) {
592
+ const properties = columns.map((column) => `${JSON.stringify(column.name)}: ${column.tsType}${column.nullable ? " | null" : ""};`);
593
+ return `{ ${properties.join(" ")} }`;
594
+ }
595
+ //# sourceMappingURL=resolver.js.map