@typed-sql/postgres 1.0.0-beta.0 → 1.0.0-beta.2
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/CHANGELOG.md +44 -0
- package/README.md +22 -2
- package/dist/packages/postgres/src/index.d.ts +4 -4
- package/dist/packages/postgres/src/index.d.ts.map +1 -1
- package/dist/packages/postgres/src/index.js +21 -4
- package/dist/packages/postgres/src/index.js.map +1 -1
- package/dist/packages/postgres/src/pg.d.ts +1 -1
- package/dist/packages/postgres/src/pg.d.ts.map +1 -1
- package/dist/packages/postgres/src/pg.js +25 -8
- package/dist/packages/postgres/src/pg.js.map +1 -1
- package/dist/packages/postgres/src/provider.d.ts.map +1 -1
- package/dist/packages/postgres/src/provider.js +12 -4
- package/dist/packages/postgres/src/provider.js.map +1 -1
- package/dist/packages/postgres/src/resolver.d.ts +3 -2
- package/dist/packages/postgres/src/resolver.d.ts.map +1 -1
- package/dist/packages/postgres/src/resolver.js +237 -105
- package/dist/packages/postgres/src/resolver.js.map +1 -1
- package/dist/packages/postgres/src/runtime.d.ts +3 -1
- package/dist/packages/postgres/src/runtime.d.ts.map +1 -1
- package/dist/packages/postgres/src/runtime.js +18 -13
- package/dist/packages/postgres/src/runtime.js.map +1 -1
- package/dist/packages/postgres/src/type-policy.d.ts.map +1 -1
- package/dist/packages/postgres/src/type-policy.js +28 -4
- package/dist/packages/postgres/src/type-policy.js.map +1 -1
- package/package.json +8 -4
|
@@ -1,48 +1,77 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { closestName, ParameterCollector, ResolverSchemaIndex, unionTypeLiterals, } from "@typed-sql/core";
|
|
2
|
+
import { defaultPostgresTypePolicy, isKnownPostgresType, mapPostgresType, } from "./type-policy.js";
|
|
2
3
|
const booleanOperators = new Set([
|
|
3
|
-
"=",
|
|
4
|
-
"
|
|
5
|
-
"
|
|
4
|
+
"=",
|
|
5
|
+
"!=",
|
|
6
|
+
"<>",
|
|
7
|
+
"<",
|
|
8
|
+
"<=",
|
|
9
|
+
">",
|
|
10
|
+
">=",
|
|
11
|
+
"IS",
|
|
12
|
+
"IS NOT",
|
|
13
|
+
"IS DISTINCT FROM",
|
|
14
|
+
"IS NOT DISTINCT FROM",
|
|
15
|
+
"LIKE",
|
|
16
|
+
"NOT LIKE",
|
|
17
|
+
"ILIKE",
|
|
18
|
+
"NOT ILIKE",
|
|
19
|
+
"SIMILAR TO",
|
|
20
|
+
"NOT SIMILAR",
|
|
21
|
+
"~",
|
|
22
|
+
"~*",
|
|
23
|
+
"!~",
|
|
24
|
+
"!~*",
|
|
25
|
+
"AND",
|
|
26
|
+
"OR",
|
|
27
|
+
"@>",
|
|
28
|
+
"<@",
|
|
29
|
+
"?",
|
|
30
|
+
"?|",
|
|
31
|
+
"?&",
|
|
32
|
+
"&&",
|
|
33
|
+
]);
|
|
34
|
+
const numericTypes = new Set([
|
|
35
|
+
"smallint",
|
|
36
|
+
"int2",
|
|
37
|
+
"integer",
|
|
38
|
+
"int",
|
|
39
|
+
"int4",
|
|
40
|
+
"bigint",
|
|
41
|
+
"int8",
|
|
42
|
+
"numeric",
|
|
43
|
+
"decimal",
|
|
44
|
+
"real",
|
|
45
|
+
"float4",
|
|
46
|
+
"double precision",
|
|
47
|
+
"float8",
|
|
6
48
|
]);
|
|
7
|
-
const numericTypes = new Set(["smallint", "int2", "integer", "int", "int4", "bigint", "int8", "numeric", "decimal", "real", "float4", "double precision", "float8"]);
|
|
8
49
|
function sqlName(identifier) {
|
|
9
50
|
return identifier.quoted ? identifier.name : identifier.name.toLowerCase();
|
|
10
51
|
}
|
|
11
52
|
function normalizeDatabaseType(value) {
|
|
12
|
-
return value
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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];
|
|
53
|
+
return value
|
|
54
|
+
.trim()
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.replace(/\(\d+(?:,\s*\d+)?\)/gu, "")
|
|
57
|
+
.replace(/\s+/gu, " ");
|
|
26
58
|
}
|
|
27
59
|
function suggestion(name, candidates) {
|
|
28
|
-
const candidate =
|
|
29
|
-
if (candidate === undefined
|
|
60
|
+
const candidate = closestName(name, candidates);
|
|
61
|
+
if (candidate === undefined)
|
|
30
62
|
return undefined;
|
|
31
63
|
return `Did you mean ${candidate}?`;
|
|
32
64
|
}
|
|
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
65
|
class Resolver {
|
|
40
66
|
#schema;
|
|
41
67
|
#policy;
|
|
42
68
|
#strictExpressions;
|
|
43
69
|
#diagnostics = [];
|
|
70
|
+
#parameters = new ParameterCollector();
|
|
71
|
+
#index;
|
|
44
72
|
constructor(schema, options) {
|
|
45
73
|
this.#schema = schema;
|
|
74
|
+
this.#index = new ResolverSchemaIndex(schema);
|
|
46
75
|
this.#policy = options.typePolicy ?? defaultPostgresTypePolicy;
|
|
47
76
|
this.#strictExpressions = options.strictExpressions ?? true;
|
|
48
77
|
}
|
|
@@ -51,15 +80,23 @@ class Resolver {
|
|
|
51
80
|
this.#diagnostic("TSQ007", `PostgreSQL resolver cannot analyze ${this.#schema.dialect}`, statement.range);
|
|
52
81
|
}
|
|
53
82
|
const result = this.#resolveStatement(statement, undefined, new Map());
|
|
54
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
...result,
|
|
85
|
+
parameters: this.#parameters.values(),
|
|
86
|
+
diagnostics: this.#diagnostics,
|
|
87
|
+
};
|
|
55
88
|
}
|
|
56
89
|
#resolveStatement(statement, outer, inheritedCtes) {
|
|
57
90
|
const ctes = this.#resolveWith(statement.with, outer, inheritedCtes);
|
|
58
91
|
switch (statement.kind) {
|
|
59
|
-
case "select":
|
|
60
|
-
|
|
61
|
-
case "
|
|
62
|
-
|
|
92
|
+
case "select":
|
|
93
|
+
return { columns: this.#resolveSelect(statement, outer, ctes), resultKind: "rows" };
|
|
94
|
+
case "insert":
|
|
95
|
+
return this.#resolveInsert(statement, outer, ctes);
|
|
96
|
+
case "update":
|
|
97
|
+
return this.#resolveUpdate(statement, outer, ctes);
|
|
98
|
+
case "delete":
|
|
99
|
+
return this.#resolveDelete(statement, outer, ctes);
|
|
63
100
|
}
|
|
64
101
|
}
|
|
65
102
|
#resolveWith(withClause, outer, inherited) {
|
|
@@ -76,7 +113,12 @@ class Resolver {
|
|
|
76
113
|
if (withClause.recursive) {
|
|
77
114
|
const placeholderColumns = {};
|
|
78
115
|
for (const column of query.columns) {
|
|
79
|
-
placeholderColumns[sqlName(column)] = {
|
|
116
|
+
placeholderColumns[sqlName(column)] = {
|
|
117
|
+
name: sqlName(column),
|
|
118
|
+
databaseType: "unknown",
|
|
119
|
+
tsType: "unknown",
|
|
120
|
+
nullable: true,
|
|
121
|
+
};
|
|
80
122
|
}
|
|
81
123
|
ctes.set(key, { name: key, columns: placeholderColumns });
|
|
82
124
|
}
|
|
@@ -109,11 +151,11 @@ class Resolver {
|
|
|
109
151
|
for (const join of statement.joins)
|
|
110
152
|
this.#addJoin(join, scope, ctes);
|
|
111
153
|
if (statement.where !== undefined)
|
|
112
|
-
this.#resolveExpression(statement.where, scope, ctes);
|
|
154
|
+
this.#resolveExpression(statement.where, scope, ctes, this.#databaseType("boolean", false));
|
|
113
155
|
for (const expression of statement.groupBy)
|
|
114
156
|
this.#resolveExpression(expression, scope, ctes);
|
|
115
157
|
if (statement.having !== undefined)
|
|
116
|
-
this.#resolveExpression(statement.having, scope, ctes);
|
|
158
|
+
this.#resolveExpression(statement.having, scope, ctes, this.#databaseType("boolean", false));
|
|
117
159
|
for (const expression of statement.distinctOn)
|
|
118
160
|
this.#resolveExpression(expression, scope, ctes);
|
|
119
161
|
for (const window of statement.windows) {
|
|
@@ -125,9 +167,9 @@ class Resolver {
|
|
|
125
167
|
for (const order of statement.orderBy)
|
|
126
168
|
this.#resolveExpression(order.expression, scope, ctes);
|
|
127
169
|
if (statement.limit !== undefined)
|
|
128
|
-
this.#resolveExpression(statement.limit, scope, ctes);
|
|
170
|
+
this.#resolveExpression(statement.limit, scope, ctes, this.#databaseType("integer", false));
|
|
129
171
|
if (statement.offset !== undefined)
|
|
130
|
-
this.#resolveExpression(statement.offset, scope, ctes);
|
|
172
|
+
this.#resolveExpression(statement.offset, scope, ctes, this.#databaseType("integer", false));
|
|
131
173
|
return this.#resolveItems(statement.columns, scope, ctes);
|
|
132
174
|
}
|
|
133
175
|
#resolveInsert(statement, outer, ctes) {
|
|
@@ -141,8 +183,9 @@ class Resolver {
|
|
|
141
183
|
if (row.length !== targetColumns.length) {
|
|
142
184
|
this.#diagnostic("TSQ214", `INSERT has ${targetColumns.length} target columns but ${row.length} values`, statement.source.range);
|
|
143
185
|
}
|
|
144
|
-
|
|
145
|
-
this.#resolveExpression(value, scope, ctes);
|
|
186
|
+
row.forEach((value, index) => {
|
|
187
|
+
this.#resolveExpression(value, scope, ctes, this.#snapshotType(targetColumns[index]));
|
|
188
|
+
});
|
|
146
189
|
}
|
|
147
190
|
}
|
|
148
191
|
else if (statement.source.kind === "select") {
|
|
@@ -158,15 +201,15 @@ class Resolver {
|
|
|
158
201
|
const scope = { relations: [], usingColumns: new Map(), ...(outer === undefined ? {} : { outer }) };
|
|
159
202
|
const target = this.#addRelation(statement.table, false, scope, ctes);
|
|
160
203
|
for (const assignment of statement.assignments) {
|
|
161
|
-
this.#findColumn(target?.table, assignment.column);
|
|
162
|
-
this.#resolveExpression(assignment.value, scope, ctes);
|
|
204
|
+
const column = this.#findColumn(target?.table, assignment.column);
|
|
205
|
+
this.#resolveExpression(assignment.value, scope, ctes, this.#snapshotType(column));
|
|
163
206
|
}
|
|
164
207
|
if (statement.from !== undefined)
|
|
165
208
|
this.#addRelation(statement.from, false, scope, ctes);
|
|
166
209
|
for (const join of statement.joins)
|
|
167
210
|
this.#addJoin(join, scope, ctes);
|
|
168
211
|
if (statement.where !== undefined)
|
|
169
|
-
this.#resolveExpression(statement.where, scope, ctes);
|
|
212
|
+
this.#resolveExpression(statement.where, scope, ctes, this.#databaseType("boolean", false));
|
|
170
213
|
const columns = this.#resolveItems(statement.returning, scope, ctes);
|
|
171
214
|
return { columns, resultKind: statement.returning.length === 0 ? "command" : "rows" };
|
|
172
215
|
}
|
|
@@ -176,7 +219,7 @@ class Resolver {
|
|
|
176
219
|
for (const reference of statement.using)
|
|
177
220
|
this.#addRelation(reference, false, scope, ctes);
|
|
178
221
|
if (statement.where !== undefined)
|
|
179
|
-
this.#resolveExpression(statement.where, scope, ctes);
|
|
222
|
+
this.#resolveExpression(statement.where, scope, ctes, this.#databaseType("boolean", false));
|
|
180
223
|
const columns = this.#resolveItems(statement.returning, scope, ctes);
|
|
181
224
|
return { columns, resultKind: statement.returning.length === 0 ? "command" : "rows" };
|
|
182
225
|
}
|
|
@@ -187,7 +230,7 @@ class Resolver {
|
|
|
187
230
|
relation.nullable = true;
|
|
188
231
|
const relation = this.#addRelation(join.table, join.kind === "left" || join.kind === "full", scope, ctes);
|
|
189
232
|
if (join.on !== undefined)
|
|
190
|
-
this.#resolveExpression(join.on, scope, ctes);
|
|
233
|
+
this.#resolveExpression(join.on, scope, ctes, this.#databaseType("boolean", false));
|
|
191
234
|
if (join.using !== undefined && relation !== undefined) {
|
|
192
235
|
for (const identifier of join.using) {
|
|
193
236
|
const name = sqlName(identifier);
|
|
@@ -200,9 +243,15 @@ class Resolver {
|
|
|
200
243
|
const left = this.#columnType(leftMatches[0], this.#column(leftMatches[0], name, identifier.quoted));
|
|
201
244
|
const rightType = this.#columnType(relation, right);
|
|
202
245
|
scope.usingColumns.set(name, {
|
|
203
|
-
tsType:
|
|
204
|
-
nullable: join.kind === "left"
|
|
205
|
-
|
|
246
|
+
tsType: unionTypeLiterals([left.tsType, rightType.tsType]),
|
|
247
|
+
nullable: join.kind === "left"
|
|
248
|
+
? left.nullable
|
|
249
|
+
: join.kind === "right"
|
|
250
|
+
? rightType.nullable
|
|
251
|
+
: left.nullable || rightType.nullable,
|
|
252
|
+
...(left.databaseType === rightType.databaseType && left.databaseType !== undefined
|
|
253
|
+
? { databaseType: left.databaseType }
|
|
254
|
+
: {}),
|
|
206
255
|
});
|
|
207
256
|
}
|
|
208
257
|
}
|
|
@@ -231,15 +280,7 @@ class Resolver {
|
|
|
231
280
|
if (requestedSchema === undefined)
|
|
232
281
|
table = ctes.get(requested);
|
|
233
282
|
if (table === undefined) {
|
|
234
|
-
const entries =
|
|
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
|
-
});
|
|
283
|
+
const entries = this.#index.tables(requested, requestedSchema, reference.name.quoted || reference.schema?.quoted === true);
|
|
243
284
|
if (entries.length === 0) {
|
|
244
285
|
this.#diagnostic("TSQ100", `Unknown table ${reference.name.name}`, reference.name.range, suggestion(requested, [...ctes.keys(), ...Object.keys(this.#schema.tables)]));
|
|
245
286
|
return undefined;
|
|
@@ -248,7 +289,7 @@ class Resolver {
|
|
|
248
289
|
this.#diagnostic("TSQ107", `Ambiguous table ${reference.name.name}`, reference.name.range, "Qualify the table with a schema name.");
|
|
249
290
|
return undefined;
|
|
250
291
|
}
|
|
251
|
-
table = entries[0]
|
|
292
|
+
table = entries[0].table;
|
|
252
293
|
}
|
|
253
294
|
alias = reference.alias === undefined ? requested : sqlName(reference.alias);
|
|
254
295
|
}
|
|
@@ -278,7 +319,9 @@ class Resolver {
|
|
|
278
319
|
? scope.relations
|
|
279
320
|
: scope.relations.filter((relation) => relation.alias === sqlName(star.relation));
|
|
280
321
|
if (relations.length === 0) {
|
|
281
|
-
this.#diagnostic("TSQ103", star.relation === undefined
|
|
322
|
+
this.#diagnostic("TSQ103", star.relation === undefined
|
|
323
|
+
? "SELECT * requires a FROM relation"
|
|
324
|
+
: `Unknown relation alias ${star.relation.name}`, star.range);
|
|
282
325
|
continue;
|
|
283
326
|
}
|
|
284
327
|
if (star.relation === undefined) {
|
|
@@ -287,7 +330,8 @@ class Resolver {
|
|
|
287
330
|
}
|
|
288
331
|
for (const relation of relations) {
|
|
289
332
|
for (const column of Object.values(relation.table.columns)) {
|
|
290
|
-
if (star.relation === undefined &&
|
|
333
|
+
if (star.relation === undefined &&
|
|
334
|
+
(scope.usingColumns.has(column.name) || scope.usingColumns.has(column.name.toLowerCase())))
|
|
291
335
|
continue;
|
|
292
336
|
add({ name: column.name, ...this.#columnType(relation, column), range: item.range });
|
|
293
337
|
}
|
|
@@ -317,7 +361,7 @@ class Resolver {
|
|
|
317
361
|
return "case";
|
|
318
362
|
return undefined;
|
|
319
363
|
}
|
|
320
|
-
#resolveExpression(expression, scope, ctes) {
|
|
364
|
+
#resolveExpression(expression, scope, ctes, expected) {
|
|
321
365
|
switch (expression.kind) {
|
|
322
366
|
case "column": {
|
|
323
367
|
if (expression.relation === undefined && expression.column.name === "DEFAULT")
|
|
@@ -330,15 +374,23 @@ class Resolver {
|
|
|
330
374
|
if (typeof expression.value === "boolean")
|
|
331
375
|
return { tsType: "boolean", nullable: false, databaseType: "boolean" };
|
|
332
376
|
if (typeof expression.value === "number")
|
|
333
|
-
return {
|
|
377
|
+
return {
|
|
378
|
+
tsType: "number",
|
|
379
|
+
nullable: false,
|
|
380
|
+
databaseType: Number.isInteger(expression.value) ? "integer" : "numeric",
|
|
381
|
+
};
|
|
334
382
|
return { tsType: "string", nullable: false, databaseType: "text" };
|
|
335
383
|
}
|
|
336
|
-
case "parameter":
|
|
337
|
-
|
|
384
|
+
case "parameter":
|
|
385
|
+
return this.#recordParameter(expression.index, expected);
|
|
386
|
+
case "star":
|
|
387
|
+
return { tsType: "unknown", nullable: false };
|
|
338
388
|
case "array": {
|
|
339
389
|
const elements = expression.elements.map((element) => this.#resolveExpression(element, scope, ctes));
|
|
340
|
-
const elementType =
|
|
341
|
-
const databaseTypes = [
|
|
390
|
+
const elementType = unionTypeLiterals(elements.map((element) => element.tsType));
|
|
391
|
+
const databaseTypes = [
|
|
392
|
+
...new Set(elements.map((element) => element.databaseType).filter((value) => value !== undefined)),
|
|
393
|
+
];
|
|
342
394
|
return {
|
|
343
395
|
tsType: `readonly (${elementType})[]`,
|
|
344
396
|
nullable: false,
|
|
@@ -347,21 +399,33 @@ class Resolver {
|
|
|
347
399
|
}
|
|
348
400
|
case "row": {
|
|
349
401
|
const elements = expression.elements.map((element) => this.#resolveExpression(element, scope, ctes));
|
|
350
|
-
return {
|
|
402
|
+
return {
|
|
403
|
+
tsType: `readonly [${elements.map((element) => element.tsType + (element.nullable ? " | null" : "")).join(", ")}]`,
|
|
404
|
+
nullable: false,
|
|
405
|
+
};
|
|
351
406
|
}
|
|
352
407
|
case "cast": {
|
|
353
|
-
const
|
|
408
|
+
const castType = this.#databaseType(expression.databaseType.name, true);
|
|
409
|
+
const source = this.#resolveExpression(expression.expression, scope, ctes, castType);
|
|
354
410
|
if (!isKnownPostgresType(expression.databaseType.name, this.#schema)) {
|
|
355
411
|
this.#diagnostic("TSQ106", `Invalid or unknown PostgreSQL cast type ${expression.databaseType.name}`, expression.databaseType.range);
|
|
356
412
|
}
|
|
357
|
-
return {
|
|
413
|
+
return {
|
|
414
|
+
tsType: mapPostgresType(expression.databaseType.name, this.#policy, this.#schema),
|
|
415
|
+
nullable: source.nullable,
|
|
416
|
+
databaseType: normalizeDatabaseType(expression.databaseType.name),
|
|
417
|
+
};
|
|
358
418
|
}
|
|
359
419
|
case "unary": {
|
|
360
420
|
const operand = this.#resolveExpression(expression.expression, scope, ctes);
|
|
361
|
-
return expression.operator === "NOT"
|
|
421
|
+
return expression.operator === "NOT"
|
|
422
|
+
? { tsType: "boolean", nullable: operand.nullable, databaseType: "boolean" }
|
|
423
|
+
: operand;
|
|
362
424
|
}
|
|
363
|
-
case "binary":
|
|
364
|
-
|
|
425
|
+
case "binary":
|
|
426
|
+
return this.#resolveBinary(expression, scope, ctes);
|
|
427
|
+
case "call":
|
|
428
|
+
return this.#resolveCall(expression, scope, ctes);
|
|
365
429
|
case "case": {
|
|
366
430
|
if (expression.operand !== undefined)
|
|
367
431
|
this.#resolveExpression(expression.operand, scope, ctes);
|
|
@@ -370,9 +434,11 @@ class Resolver {
|
|
|
370
434
|
const results = expression.branches.map((branch) => this.#resolveExpression(branch.then, scope, ctes));
|
|
371
435
|
if (expression.elseExpression !== undefined)
|
|
372
436
|
results.push(this.#resolveExpression(expression.elseExpression, scope, ctes));
|
|
373
|
-
const databaseTypes = [
|
|
437
|
+
const databaseTypes = [
|
|
438
|
+
...new Set(results.map((result) => result.databaseType).filter((value) => value !== undefined)),
|
|
439
|
+
];
|
|
374
440
|
return {
|
|
375
|
-
tsType:
|
|
441
|
+
tsType: unionTypeLiterals(results.map((result) => result.tsType)),
|
|
376
442
|
nullable: expression.elseExpression === undefined || results.some((result) => result.nullable),
|
|
377
443
|
...(databaseTypes.length === 1 ? { databaseType: databaseTypes[0] } : {}),
|
|
378
444
|
};
|
|
@@ -384,7 +450,11 @@ class Resolver {
|
|
|
384
450
|
return { tsType: "unknown", nullable: true };
|
|
385
451
|
}
|
|
386
452
|
const column = resolved.columns[0];
|
|
387
|
-
return {
|
|
453
|
+
return {
|
|
454
|
+
tsType: column.tsType,
|
|
455
|
+
nullable: true,
|
|
456
|
+
...(column.databaseType === undefined ? {} : { databaseType: column.databaseType }),
|
|
457
|
+
};
|
|
388
458
|
}
|
|
389
459
|
case "exists": {
|
|
390
460
|
this.#resolveStatement(expression.query, scope, ctes);
|
|
@@ -394,7 +464,7 @@ class Resolver {
|
|
|
394
464
|
const subject = this.#resolveExpression(expression.expression, scope, ctes);
|
|
395
465
|
let nullable = subject.nullable;
|
|
396
466
|
if (Array.isArray(expression.values)) {
|
|
397
|
-
const values = expression.values.map((value) => this.#resolveExpression(value, scope, ctes));
|
|
467
|
+
const values = expression.values.map((value) => this.#resolveExpression(value, scope, ctes, subject));
|
|
398
468
|
nullable ||= values.some((value) => value.nullable);
|
|
399
469
|
}
|
|
400
470
|
else {
|
|
@@ -407,18 +477,34 @@ class Resolver {
|
|
|
407
477
|
}
|
|
408
478
|
case "between": {
|
|
409
479
|
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 {
|
|
480
|
+
const lower = this.#resolveExpression(expression.lower, scope, ctes, subject);
|
|
481
|
+
const upper = this.#resolveExpression(expression.upper, scope, ctes, subject);
|
|
482
|
+
return {
|
|
483
|
+
tsType: "boolean",
|
|
484
|
+
nullable: subject.nullable || lower.nullable || upper.nullable,
|
|
485
|
+
databaseType: "boolean",
|
|
486
|
+
};
|
|
413
487
|
}
|
|
414
488
|
}
|
|
415
489
|
}
|
|
416
490
|
#resolveBinary(expression, scope, ctes) {
|
|
417
|
-
|
|
418
|
-
|
|
491
|
+
let left;
|
|
492
|
+
let right;
|
|
493
|
+
if (expression.left.kind === "parameter" && expression.right.kind !== "parameter") {
|
|
494
|
+
right = this.#resolveExpression(expression.right, scope, ctes);
|
|
495
|
+
left = this.#resolveExpression(expression.left, scope, ctes, right);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
left = this.#resolveExpression(expression.left, scope, ctes);
|
|
499
|
+
right = this.#resolveExpression(expression.right, scope, ctes, expression.right.kind === "parameter" ? left : undefined);
|
|
500
|
+
}
|
|
419
501
|
if (booleanOperators.has(expression.operator)) {
|
|
420
502
|
const neverNull = expression.operator.startsWith("IS");
|
|
421
|
-
return {
|
|
503
|
+
return {
|
|
504
|
+
tsType: "boolean",
|
|
505
|
+
nullable: neverNull ? false : left.nullable || right.nullable,
|
|
506
|
+
databaseType: "boolean",
|
|
507
|
+
};
|
|
422
508
|
}
|
|
423
509
|
if (["->", "#>"].includes(expression.operator))
|
|
424
510
|
return { tsType: this.#policy.json, nullable: true, databaseType: "jsonb" };
|
|
@@ -427,15 +513,28 @@ class Resolver {
|
|
|
427
513
|
if (expression.operator === "||") {
|
|
428
514
|
if (left.tsType.startsWith("readonly (") && right.tsType.startsWith("readonly (")) {
|
|
429
515
|
const databaseType = left.databaseType ?? right.databaseType;
|
|
430
|
-
return {
|
|
516
|
+
return {
|
|
517
|
+
tsType: unionTypeLiterals([left.tsType, right.tsType]),
|
|
518
|
+
nullable: left.nullable || right.nullable,
|
|
519
|
+
...(databaseType === undefined ? {} : { databaseType }),
|
|
520
|
+
};
|
|
431
521
|
}
|
|
432
522
|
return { tsType: "string", nullable: left.nullable || right.nullable, databaseType: "text" };
|
|
433
523
|
}
|
|
434
524
|
const leftDatabase = left.databaseType === undefined ? undefined : normalizeDatabaseType(left.databaseType);
|
|
435
525
|
const rightDatabase = right.databaseType === undefined ? undefined : normalizeDatabaseType(right.databaseType);
|
|
436
|
-
if (leftDatabase !== undefined &&
|
|
437
|
-
|
|
438
|
-
|
|
526
|
+
if (leftDatabase !== undefined &&
|
|
527
|
+
rightDatabase !== undefined &&
|
|
528
|
+
numericTypes.has(leftDatabase) &&
|
|
529
|
+
numericTypes.has(rightDatabase)) {
|
|
530
|
+
const databaseType = [leftDatabase, rightDatabase].some((type) => type === "numeric" || type === "decimal")
|
|
531
|
+
? "numeric"
|
|
532
|
+
: "double precision";
|
|
533
|
+
return {
|
|
534
|
+
tsType: databaseType === "numeric" ? this.#policy.numeric : "number",
|
|
535
|
+
nullable: left.nullable || right.nullable,
|
|
536
|
+
databaseType,
|
|
537
|
+
};
|
|
439
538
|
}
|
|
440
539
|
if (left.tsType === "unknown" || right.tsType === "unknown")
|
|
441
540
|
return { tsType: "unknown", nullable: left.nullable || right.nullable };
|
|
@@ -487,15 +586,11 @@ class Resolver {
|
|
|
487
586
|
return scope.relations.find((candidate) => candidate.alias === alias);
|
|
488
587
|
}
|
|
489
588
|
#column(relation, name, quoted = false) {
|
|
490
|
-
return
|
|
491
|
-
? key === name || column.name === name
|
|
492
|
-
: key.toLowerCase() === name || column.name.toLowerCase() === name)?.[1];
|
|
589
|
+
return this.#index.column(relation.table, name, quoted);
|
|
493
590
|
}
|
|
494
591
|
#findColumn(table, identifier) {
|
|
495
592
|
const name = sqlName(identifier);
|
|
496
|
-
const column = table === undefined ? undefined :
|
|
497
|
-
? key === name || candidate.name === name
|
|
498
|
-
: key.toLowerCase() === name || candidate.name.toLowerCase() === name)?.[1];
|
|
593
|
+
const column = table === undefined ? undefined : this.#index.column(table, name, identifier.quoted);
|
|
499
594
|
if (column === undefined)
|
|
500
595
|
this.#diagnostic("TSQ101", `Unknown column ${name}`, identifier.range, suggestion(name, Object.keys(table?.columns ?? {})));
|
|
501
596
|
return column;
|
|
@@ -522,17 +617,28 @@ class Resolver {
|
|
|
522
617
|
return argument?.kind !== "literal" || argument.value !== null;
|
|
523
618
|
});
|
|
524
619
|
return {
|
|
525
|
-
tsType:
|
|
620
|
+
tsType: unionTypeLiterals(known.map((result) => result.tsType)),
|
|
526
621
|
nullable: resolved.every((result) => result.nullable),
|
|
527
|
-
...(known.length > 0 &&
|
|
622
|
+
...(known.length > 0 &&
|
|
623
|
+
known.every((result) => result.databaseType === known[0]?.databaseType) &&
|
|
624
|
+
known[0]?.databaseType !== undefined
|
|
625
|
+
? { databaseType: known[0].databaseType }
|
|
626
|
+
: {}),
|
|
528
627
|
};
|
|
529
628
|
}
|
|
530
629
|
if (name === "NULLIF")
|
|
531
630
|
return { ...(resolved[0] ?? { tsType: "unknown" }), nullable: true };
|
|
532
631
|
if (["MIN", "MAX", "GREATEST", "LEAST"].includes(name))
|
|
533
|
-
return {
|
|
632
|
+
return {
|
|
633
|
+
...(resolved[0] ?? { tsType: "unknown", nullable: true }),
|
|
634
|
+
nullable: name === "MIN" || name === "MAX" ? true : resolved.some((result) => result.nullable),
|
|
635
|
+
};
|
|
534
636
|
if (name === "SUM" || name === "AVG")
|
|
535
|
-
return {
|
|
637
|
+
return {
|
|
638
|
+
tsType: resolved[0]?.tsType ?? this.#policy.numeric,
|
|
639
|
+
nullable: true,
|
|
640
|
+
databaseType: resolved[0]?.databaseType ?? "numeric",
|
|
641
|
+
};
|
|
536
642
|
if (["BOOL_AND", "BOOL_OR", "EVERY"].includes(name))
|
|
537
643
|
return { tsType: "boolean", nullable: true, databaseType: "boolean" };
|
|
538
644
|
if (name === "STRING_AGG")
|
|
@@ -541,20 +647,28 @@ class Resolver {
|
|
|
541
647
|
return { tsType: this.#policy.json, nullable: true, databaseType: name.startsWith("JSONB") ? "jsonb" : "json" };
|
|
542
648
|
if (name === "ARRAY_AGG") {
|
|
543
649
|
const item = resolved[0] ?? { tsType: "unknown", nullable: true };
|
|
544
|
-
return {
|
|
650
|
+
return {
|
|
651
|
+
tsType: `readonly (${item.tsType}${item.nullable ? " | null" : ""})[]`,
|
|
652
|
+
nullable: true,
|
|
653
|
+
...(item.databaseType === undefined ? {} : { databaseType: `${item.databaseType}[]` }),
|
|
654
|
+
};
|
|
545
655
|
}
|
|
546
656
|
const functionName = sqlName(expression.name);
|
|
547
657
|
const schemaName = expression.schema === undefined ? undefined : sqlName(expression.schema);
|
|
548
|
-
const candidates =
|
|
549
|
-
&& candidate.argumentTypes.length === expression.arguments.length
|
|
550
|
-
&& (schemaName === undefined || candidate.schema?.toLowerCase() === schemaName));
|
|
658
|
+
const candidates = this.#index.functions(functionName, expression.arguments.length, schemaName);
|
|
551
659
|
const exact = candidates.filter((candidate) => candidate.argumentTypes.every((type, index) => {
|
|
552
660
|
const actual = resolved[index]?.databaseType;
|
|
553
661
|
return actual === undefined || this.#typesCompatible(type, actual);
|
|
554
662
|
}));
|
|
555
663
|
const selected = exact.length === 1 ? exact[0] : candidates.length === 1 ? candidates[0] : undefined;
|
|
556
|
-
if (selected !== undefined)
|
|
664
|
+
if (selected !== undefined) {
|
|
665
|
+
expression.arguments.forEach((argument, index) => {
|
|
666
|
+
if (argument.kind === "parameter") {
|
|
667
|
+
this.#resolveExpression(argument, scope, ctes, this.#databaseType(selected.argumentTypes[index], true));
|
|
668
|
+
}
|
|
669
|
+
});
|
|
557
670
|
return this.#functionType(selected);
|
|
671
|
+
}
|
|
558
672
|
if (exact.length > 1 || candidates.length > 1) {
|
|
559
673
|
this.#diagnostic("TSQ204", `Ambiguous overloaded function ${expression.name.name}`, expression.range, "Cast arguments to select a specific overload.");
|
|
560
674
|
return { tsType: "unknown", nullable: true };
|
|
@@ -578,8 +692,29 @@ class Resolver {
|
|
|
578
692
|
...(value.databaseReturnType === undefined ? {} : { databaseType: value.databaseReturnType }),
|
|
579
693
|
};
|
|
580
694
|
}
|
|
695
|
+
#snapshotType(column) {
|
|
696
|
+
if (column === undefined)
|
|
697
|
+
return undefined;
|
|
698
|
+
return { tsType: column.tsType, nullable: column.nullable, databaseType: column.databaseType };
|
|
699
|
+
}
|
|
700
|
+
#databaseType(databaseType, nullable) {
|
|
701
|
+
return {
|
|
702
|
+
tsType: mapPostgresType(databaseType, this.#policy, this.#schema),
|
|
703
|
+
nullable,
|
|
704
|
+
databaseType: normalizeDatabaseType(databaseType),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
#recordParameter(index, expected) {
|
|
708
|
+
return this.#parameters.record(index, expected);
|
|
709
|
+
}
|
|
581
710
|
#diagnostic(code, message, range, suggestionText, severity = "error") {
|
|
582
|
-
this.#diagnostics.push({
|
|
711
|
+
this.#diagnostics.push({
|
|
712
|
+
code,
|
|
713
|
+
message,
|
|
714
|
+
range,
|
|
715
|
+
severity,
|
|
716
|
+
...(suggestionText === undefined ? {} : { suggestion: suggestionText }),
|
|
717
|
+
});
|
|
583
718
|
}
|
|
584
719
|
}
|
|
585
720
|
export function resolveStatement(statement, schema, options = {}) {
|
|
@@ -588,8 +723,5 @@ export function resolveStatement(statement, schema, options = {}) {
|
|
|
588
723
|
export function resolveSelect(statement, schema, options = {}) {
|
|
589
724
|
return resolveStatement(statement, schema, options);
|
|
590
725
|
}
|
|
591
|
-
export
|
|
592
|
-
const properties = columns.map((column) => `${JSON.stringify(column.name)}: ${column.tsType}${column.nullable ? " | null" : ""};`);
|
|
593
|
-
return `{ ${properties.join(" ")} }`;
|
|
594
|
-
}
|
|
726
|
+
export { rowTypeLiteral } from "@typed-sql/core";
|
|
595
727
|
//# sourceMappingURL=resolver.js.map
|