@typed-sql/postgres 1.0.0-beta.1 → 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.
@@ -1,48 +1,77 @@
1
- import { defaultPostgresTypePolicy, isKnownPostgresType, mapPostgresType } from "./type-policy.js";
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
- "=", "!=", "<>", "<", "<=", ">", ">=", "IS", "IS NOT", "IS DISTINCT FROM",
4
- "IS NOT DISTINCT FROM", "LIKE", "NOT LIKE", "ILIKE", "NOT ILIKE", "SIMILAR TO",
5
- "NOT SIMILAR", "~", "~*", "!~", "!~*", "AND", "OR", "@>", "<@", "?", "?|", "?&", "&&",
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.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];
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 = [...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)))
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 { ...result, diagnostics: this.#diagnostics };
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": 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);
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)] = { name: sqlName(column), databaseType: "unknown", tsType: "unknown", nullable: true };
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
- for (const value of row)
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: 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 } : {}),
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 = 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
- });
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][1];
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 ? "SELECT * requires a FROM relation" : `Unknown relation alias ${star.relation.name}`, star.range);
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 && (scope.usingColumns.has(column.name) || scope.usingColumns.has(column.name.toLowerCase())))
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 { tsType: "number", nullable: false, databaseType: Number.isInteger(expression.value) ? "integer" : "numeric" };
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": return { tsType: "unknown", nullable: true };
337
- case "star": return { tsType: "unknown", nullable: false };
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 = unionTypes(elements.map((element) => element.tsType));
341
- const databaseTypes = [...new Set(elements.map((element) => element.databaseType).filter((value) => value !== undefined))];
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 { tsType: `readonly [${elements.map((element) => element.tsType + (element.nullable ? " | null" : "")).join(", ")}]`, nullable: false };
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 source = this.#resolveExpression(expression.expression, scope, ctes);
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 { tsType: mapPostgresType(expression.databaseType.name, this.#policy, this.#schema), nullable: source.nullable, databaseType: normalizeDatabaseType(expression.databaseType.name) };
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" ? { tsType: "boolean", nullable: operand.nullable, databaseType: "boolean" } : operand;
421
+ return expression.operator === "NOT"
422
+ ? { tsType: "boolean", nullable: operand.nullable, databaseType: "boolean" }
423
+ : operand;
362
424
  }
363
- case "binary": return this.#resolveBinary(expression, scope, ctes);
364
- case "call": return this.#resolveCall(expression, scope, ctes);
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 = [...new Set(results.map((result) => result.databaseType).filter((value) => value !== undefined))];
437
+ const databaseTypes = [
438
+ ...new Set(results.map((result) => result.databaseType).filter((value) => value !== undefined)),
439
+ ];
374
440
  return {
375
- tsType: unionTypes(results.map((result) => result.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 { tsType: column.tsType, nullable: true, ...(column.databaseType === undefined ? {} : { databaseType: column.databaseType }) };
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 { tsType: "boolean", nullable: subject.nullable || lower.nullable || upper.nullable, databaseType: "boolean" };
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
- const left = this.#resolveExpression(expression.left, scope, ctes);
418
- const right = this.#resolveExpression(expression.right, scope, ctes);
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 { tsType: "boolean", nullable: neverNull ? false : left.nullable || right.nullable, databaseType: "boolean" };
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 { tsType: unionTypes([left.tsType, right.tsType]), nullable: left.nullable || right.nullable, ...(databaseType === undefined ? {} : { databaseType }) };
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 && 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 };
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 Object.entries(relation.table.columns).find(([key, column]) => quoted
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 : Object.entries(table.columns).find(([key, candidate]) => identifier.quoted
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: unionTypes(known.map((result) => result.tsType)),
620
+ tsType: unionTypeLiterals(known.map((result) => result.tsType)),
526
621
  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 } : {}),
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 { ...(resolved[0] ?? { tsType: "unknown", nullable: true }), nullable: name === "MIN" || name === "MAX" ? true : resolved.some((result) => result.nullable) };
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 { tsType: resolved[0]?.tsType ?? this.#policy.numeric, nullable: true, databaseType: resolved[0]?.databaseType ?? "numeric" };
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 { tsType: `readonly (${item.tsType}${item.nullable ? " | null" : ""})[]`, nullable: true, ...(item.databaseType === undefined ? {} : { databaseType: `${item.databaseType}[]` }) };
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 = 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));
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({ code, message, range, severity, ...(suggestionText === undefined ? {} : { suggestion: suggestionText }) });
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 function rowTypeLiteral(columns) {
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