@typed-sql/postgres 2.0.0 → 2.1.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.
- package/CHANGELOG.md +289 -0
- package/README.md +17 -0
- package/dist/packages/postgres/src/bulk.d.ts.map +1 -1
- package/dist/packages/postgres/src/bulk.js +1 -1
- package/dist/packages/postgres/src/bulk.js.map +1 -1
- package/dist/packages/postgres/src/capabilities.d.ts +19 -0
- package/dist/packages/postgres/src/capabilities.d.ts.map +1 -0
- package/dist/packages/postgres/src/capabilities.js +120 -0
- package/dist/packages/postgres/src/capabilities.js.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/14.d.ts +4 -0
- package/dist/packages/postgres/src/catalog/generated/14.d.ts.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/14.js +2018 -0
- package/dist/packages/postgres/src/catalog/generated/14.js.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/15.d.ts +4 -0
- package/dist/packages/postgres/src/catalog/generated/15.d.ts.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/15.js +2012 -0
- package/dist/packages/postgres/src/catalog/generated/15.js.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/16.d.ts +4 -0
- package/dist/packages/postgres/src/catalog/generated/16.d.ts.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/16.js +2012 -0
- package/dist/packages/postgres/src/catalog/generated/16.js.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/17.d.ts +4 -0
- package/dist/packages/postgres/src/catalog/generated/17.d.ts.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/17.js +2012 -0
- package/dist/packages/postgres/src/catalog/generated/17.js.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/18.d.ts +4 -0
- package/dist/packages/postgres/src/catalog/generated/18.d.ts.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/18.js +2048 -0
- package/dist/packages/postgres/src/catalog/generated/18.js.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/19.d.ts +4 -0
- package/dist/packages/postgres/src/catalog/generated/19.d.ts.map +1 -0
- package/dist/packages/postgres/src/catalog/generated/19.js +2048 -0
- package/dist/packages/postgres/src/catalog/generated/19.js.map +1 -0
- package/dist/packages/postgres/src/catalog/index.d.ts +14 -0
- package/dist/packages/postgres/src/catalog/index.d.ts.map +1 -0
- package/dist/packages/postgres/src/catalog/index.js +86 -0
- package/dist/packages/postgres/src/catalog/index.js.map +1 -0
- package/dist/packages/postgres/src/catalog/types.d.ts +46 -0
- package/dist/packages/postgres/src/catalog/types.d.ts.map +1 -0
- package/dist/packages/postgres/src/catalog/types.js +2 -0
- package/dist/packages/postgres/src/catalog/types.js.map +1 -0
- package/dist/packages/postgres/src/expression-evidence.d.ts +11 -0
- package/dist/packages/postgres/src/expression-evidence.d.ts.map +1 -0
- package/dist/packages/postgres/src/expression-evidence.js +33 -0
- package/dist/packages/postgres/src/expression-evidence.js.map +1 -0
- package/dist/packages/postgres/src/extensions.d.ts +61 -0
- package/dist/packages/postgres/src/extensions.d.ts.map +1 -0
- package/dist/packages/postgres/src/extensions.js +311 -0
- package/dist/packages/postgres/src/extensions.js.map +1 -0
- package/dist/packages/postgres/src/index.d.ts +13 -0
- package/dist/packages/postgres/src/index.d.ts.map +1 -1
- package/dist/packages/postgres/src/index.js +51 -17
- package/dist/packages/postgres/src/index.js.map +1 -1
- package/dist/packages/postgres/src/parser/index.d.ts +8 -0
- package/dist/packages/postgres/src/parser/index.d.ts.map +1 -0
- package/dist/packages/postgres/src/parser/index.js +4 -0
- package/dist/packages/postgres/src/parser/index.js.map +1 -0
- package/dist/packages/postgres/src/parser/parser.d.ts +14 -0
- package/dist/packages/postgres/src/parser/parser.d.ts.map +1 -0
- package/dist/packages/postgres/src/parser/parser.js +2683 -0
- package/dist/packages/postgres/src/parser/parser.js.map +1 -0
- package/dist/packages/postgres/src/parser/tokenizer.d.ts +15 -0
- package/dist/packages/postgres/src/parser/tokenizer.d.ts.map +1 -0
- package/dist/packages/postgres/src/parser/tokenizer.js +470 -0
- package/dist/packages/postgres/src/parser/tokenizer.js.map +1 -0
- package/dist/packages/postgres/src/parser/types.d.ts +652 -0
- package/dist/packages/postgres/src/parser/types.d.ts.map +1 -0
- package/dist/packages/postgres/src/parser/types.js +9 -0
- package/dist/packages/postgres/src/parser/types.js.map +1 -0
- package/dist/packages/postgres/src/parser/walk.d.ts +13 -0
- package/dist/packages/postgres/src/parser/walk.d.ts.map +1 -0
- package/dist/packages/postgres/src/parser/walk.js +374 -0
- package/dist/packages/postgres/src/parser/walk.js.map +1 -0
- package/dist/packages/postgres/src/pg.d.ts +35 -4
- package/dist/packages/postgres/src/pg.d.ts.map +1 -1
- package/dist/packages/postgres/src/pg.js +355 -40
- package/dist/packages/postgres/src/pg.js.map +1 -1
- package/dist/packages/postgres/src/prepared.d.ts +5 -3
- package/dist/packages/postgres/src/prepared.d.ts.map +1 -1
- package/dist/packages/postgres/src/prepared.js +23 -15
- package/dist/packages/postgres/src/prepared.js.map +1 -1
- package/dist/packages/postgres/src/provider.d.ts +149 -11
- package/dist/packages/postgres/src/provider.d.ts.map +1 -1
- package/dist/packages/postgres/src/provider.js +652 -42
- package/dist/packages/postgres/src/provider.js.map +1 -1
- package/dist/packages/postgres/src/resolver.d.ts +1 -1
- package/dist/packages/postgres/src/resolver.d.ts.map +1 -1
- package/dist/packages/postgres/src/resolver.js +2767 -203
- package/dist/packages/postgres/src/resolver.js.map +1 -1
- package/dist/packages/postgres/src/routing.d.ts.map +1 -1
- package/dist/packages/postgres/src/routing.js +1 -1
- package/dist/packages/postgres/src/routing.js.map +1 -1
- package/dist/packages/postgres/src/runtime-compatibility.d.ts +14 -0
- package/dist/packages/postgres/src/runtime-compatibility.d.ts.map +1 -0
- package/dist/packages/postgres/src/runtime-compatibility.js +63 -0
- package/dist/packages/postgres/src/runtime-compatibility.js.map +1 -0
- package/dist/packages/postgres/src/runtime.d.ts +10 -1
- package/dist/packages/postgres/src/runtime.d.ts.map +1 -1
- package/dist/packages/postgres/src/runtime.js +50 -2
- package/dist/packages/postgres/src/runtime.js.map +1 -1
- package/dist/packages/postgres/src/semantics.d.ts +1 -1
- package/dist/packages/postgres/src/semantics.d.ts.map +1 -1
- package/dist/packages/postgres/src/semantics.js +19 -6
- package/dist/packages/postgres/src/semantics.js.map +1 -1
- package/dist/packages/postgres/src/support.d.ts +29 -0
- package/dist/packages/postgres/src/support.d.ts.map +1 -0
- package/dist/packages/postgres/src/support.js +45 -0
- package/dist/packages/postgres/src/support.js.map +1 -0
- package/dist/packages/postgres/src/type-policy.d.ts.map +1 -1
- package/dist/packages/postgres/src/type-policy.js +34 -43
- package/dist/packages/postgres/src/type-policy.js.map +1 -1
- package/dist/packages/postgres/src/type-resolution.d.ts +33 -0
- package/dist/packages/postgres/src/type-resolution.d.ts.map +1 -0
- package/dist/packages/postgres/src/type-resolution.js +906 -0
- package/dist/packages/postgres/src/type-resolution.js.map +1 -0
- package/package.json +7 -4
|
@@ -1,54 +1,19 @@
|
|
|
1
1
|
import { closestName, ParameterCollector, ResolverSchemaIndex, unionTypeLiterals, } from "@typed-sql/core";
|
|
2
|
+
import { postgresCatalogRoutineRule, postgresCatalogTableRoutineRule, postgresCatalogTypeMapping, } from "./catalog/index.js";
|
|
3
|
+
import { fingerprintPostgresExpression, postgresExpressionIdentity } from "./expression-evidence.js";
|
|
4
|
+
import { walkStatement } from "./parser/index.js";
|
|
5
|
+
import { parsePostgresMajor } from "./support.js";
|
|
2
6
|
import { defaultPostgresTypePolicy, isKnownPostgresType, mapPostgresType, } from "./type-policy.js";
|
|
3
|
-
|
|
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",
|
|
48
|
-
]);
|
|
7
|
+
import { postgresCanCoerce, postgresCanonicalType, postgresCommonType, postgresElementType, postgresTypeCategory, resolvePostgresCandidates, resolvePostgresOperator, resolvePostgresUnaryOperator, } from "./type-resolution.js";
|
|
49
8
|
function sqlName(identifier) {
|
|
50
9
|
return identifier.quoted ? identifier.name : identifier.name.toLowerCase();
|
|
51
10
|
}
|
|
11
|
+
function qualifiedSqlName(identifier) {
|
|
12
|
+
return identifier.parts.map(sqlName).join(".");
|
|
13
|
+
}
|
|
14
|
+
function qualifiedNameIsCaseSensitive(identifier) {
|
|
15
|
+
return identifier.parts.some(({ quoted }) => quoted);
|
|
16
|
+
}
|
|
52
17
|
function normalizeDatabaseType(value) {
|
|
53
18
|
return value
|
|
54
19
|
.trim()
|
|
@@ -62,6 +27,31 @@ function suggestion(name, candidates) {
|
|
|
62
27
|
return undefined;
|
|
63
28
|
return `Did you mean ${candidate}?`;
|
|
64
29
|
}
|
|
30
|
+
function mergeTypeLiterals(left, right) {
|
|
31
|
+
if (left === right)
|
|
32
|
+
return left;
|
|
33
|
+
const leftMembers = left.split(" | ");
|
|
34
|
+
if (leftMembers.includes(right))
|
|
35
|
+
return left;
|
|
36
|
+
const rightMembers = right.split(" | ");
|
|
37
|
+
if (rightMembers.includes(left))
|
|
38
|
+
return right;
|
|
39
|
+
return unionTypeLiterals([left, right]);
|
|
40
|
+
}
|
|
41
|
+
const aggregateRoutineRules = new Set([
|
|
42
|
+
"array-aggregate",
|
|
43
|
+
"boolean-aggregate",
|
|
44
|
+
"count",
|
|
45
|
+
"json-aggregate",
|
|
46
|
+
"numeric-aggregate",
|
|
47
|
+
"ordered-set-value",
|
|
48
|
+
"string-aggregate",
|
|
49
|
+
]);
|
|
50
|
+
const aggregateExtrema = new Set(["MAX", "MIN"]);
|
|
51
|
+
const hypotheticalAggregates = new Set(["CUME_DIST", "DENSE_RANK", "PERCENT_RANK", "RANK"]);
|
|
52
|
+
function isSpecialGrouping(grouping) {
|
|
53
|
+
return ["empty-group", "grouping-set", "rollup", "cube", "grouping-sets"].includes(grouping.kind);
|
|
54
|
+
}
|
|
65
55
|
class Resolver {
|
|
66
56
|
#schema;
|
|
67
57
|
#policy;
|
|
@@ -69,11 +59,14 @@ class Resolver {
|
|
|
69
59
|
#diagnostics = [];
|
|
70
60
|
#parameters = new ParameterCollector();
|
|
71
61
|
#index;
|
|
62
|
+
#serverMajor;
|
|
63
|
+
#insideMergeReturning = false;
|
|
72
64
|
constructor(schema, options) {
|
|
73
65
|
this.#schema = schema;
|
|
74
66
|
this.#index = ResolverSchemaIndex.for(schema);
|
|
75
67
|
this.#policy = options.typePolicy ?? defaultPostgresTypePolicy;
|
|
76
68
|
this.#strictExpressions = options.strictExpressions ?? true;
|
|
69
|
+
this.#serverMajor = parsePostgresMajor(schema.server?.versionKey ?? ("version" in schema ? (schema.version ?? "") : ""));
|
|
77
70
|
}
|
|
78
71
|
resolve(statement) {
|
|
79
72
|
if (this.#schema.dialect !== "postgres") {
|
|
@@ -86,43 +79,40 @@ class Resolver {
|
|
|
86
79
|
diagnostics: this.#diagnostics,
|
|
87
80
|
};
|
|
88
81
|
}
|
|
89
|
-
#resolveStatement(statement, outer, inheritedCtes) {
|
|
82
|
+
#resolveStatement(statement, outer, inheritedCtes, expectedOutput) {
|
|
90
83
|
const ctes = this.#resolveWith(statement.with, outer, inheritedCtes);
|
|
91
84
|
switch (statement.kind) {
|
|
92
85
|
case "select":
|
|
93
|
-
return { columns: this.#resolveSelect(statement, outer, ctes), resultKind: "rows" };
|
|
86
|
+
return { columns: this.#resolveSelect(statement, outer, ctes, expectedOutput), resultKind: "rows" };
|
|
94
87
|
case "insert":
|
|
95
88
|
return this.#resolveInsert(statement, outer, ctes);
|
|
96
89
|
case "update":
|
|
97
90
|
return this.#resolveUpdate(statement, outer, ctes);
|
|
98
91
|
case "delete":
|
|
99
92
|
return this.#resolveDelete(statement, outer, ctes);
|
|
93
|
+
case "merge":
|
|
94
|
+
return this.#resolveMerge(statement, outer, ctes);
|
|
100
95
|
}
|
|
101
96
|
}
|
|
102
97
|
#resolveWith(withClause, outer, inherited) {
|
|
103
98
|
const ctes = new Map(inherited);
|
|
104
99
|
if (withClause === undefined)
|
|
105
100
|
return ctes;
|
|
106
|
-
if (withClause.recursive) {
|
|
107
|
-
this.#diagnostic("TSQ210", "Recursive CTE inference is not supported safely", withClause.range, "Use a non-recursive CTE or annotate the query explicitly.");
|
|
108
|
-
}
|
|
109
101
|
for (const query of withClause.queries) {
|
|
110
102
|
const key = sqlName(query.name);
|
|
111
103
|
if (ctes.has(key))
|
|
112
104
|
this.#diagnostic("TSQ211", `Duplicate CTE ${query.name.name}`, query.name.range);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
name: sqlName(column),
|
|
118
|
-
databaseType: "unknown",
|
|
119
|
-
tsType: "unknown",
|
|
120
|
-
nullable: true,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
ctes.set(key, { name: key, columns: placeholderColumns });
|
|
105
|
+
const selfReferences = query.statement.kind === "select" ? this.#selfReferences(query.statement, query.name) : 0;
|
|
106
|
+
if ((query.search !== undefined || query.cycle !== undefined) &&
|
|
107
|
+
(!withClause.recursive || selfReferences === 0)) {
|
|
108
|
+
this.#diagnostic("TSQ220", `SEARCH and CYCLE require a recursive CTE ${query.name.name}`, query.range);
|
|
124
109
|
}
|
|
125
|
-
|
|
110
|
+
if (selfReferences > 0 && !withClause.recursive) {
|
|
111
|
+
this.#diagnostic("TSQ220", `Recursive CTE ${query.name.name} requires WITH RECURSIVE`, query.range);
|
|
112
|
+
}
|
|
113
|
+
const resolved = selfReferences > 0 && query.statement.kind === "select"
|
|
114
|
+
? this.#resolveRecursiveCte(query, outer, ctes)
|
|
115
|
+
: this.#resolveStatement(query.statement, outer, ctes);
|
|
126
116
|
if (resolved.resultKind === "command") {
|
|
127
117
|
this.#diagnostic("TSQ212", `CTE ${query.name.name} does not return rows`, query.range, "Add RETURNING to the data-changing CTE.");
|
|
128
118
|
}
|
|
@@ -140,15 +130,151 @@ class Resolver {
|
|
|
140
130
|
nullable: column.nullable,
|
|
141
131
|
};
|
|
142
132
|
});
|
|
133
|
+
const addGenerated = (identifier, column) => {
|
|
134
|
+
const generatedName = sqlName(identifier);
|
|
135
|
+
if (columns[generatedName] !== undefined) {
|
|
136
|
+
this.#diagnostic("TSQ105", `CTE generated column ${identifier.name} conflicts with an output column`, identifier.range);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
columns[generatedName] = column;
|
|
140
|
+
};
|
|
141
|
+
if (query.search !== undefined) {
|
|
142
|
+
for (const identifier of query.search.by) {
|
|
143
|
+
if (columns[sqlName(identifier)] === undefined)
|
|
144
|
+
this.#diagnostic("TSQ101", `Unknown SEARCH column ${identifier.name}`, identifier.range);
|
|
145
|
+
}
|
|
146
|
+
addGenerated(query.search.set, {
|
|
147
|
+
name: sqlName(query.search.set),
|
|
148
|
+
databaseType: query.search.order === "depth" ? "record[]" : "record",
|
|
149
|
+
tsType: "unknown",
|
|
150
|
+
nullable: false,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (query.cycle !== undefined) {
|
|
154
|
+
for (const identifier of query.cycle.columns) {
|
|
155
|
+
if (columns[sqlName(identifier)] === undefined)
|
|
156
|
+
this.#diagnostic("TSQ101", `Unknown CYCLE column ${identifier.name}`, identifier.range);
|
|
157
|
+
}
|
|
158
|
+
const markScope = {
|
|
159
|
+
relations: [],
|
|
160
|
+
usingColumns: new Map(),
|
|
161
|
+
windows: new Map(),
|
|
162
|
+
...(outer === undefined ? {} : { outer }),
|
|
163
|
+
};
|
|
164
|
+
const markValue = query.cycle.markValue === undefined
|
|
165
|
+
? this.#databaseType("boolean", false)
|
|
166
|
+
: this.#resolveExpression(query.cycle.markValue, markScope, ctes);
|
|
167
|
+
const markDefault = query.cycle.markDefault === undefined
|
|
168
|
+
? this.#databaseType("boolean", false)
|
|
169
|
+
: this.#resolveExpression(query.cycle.markDefault, markScope, ctes, markValue);
|
|
170
|
+
for (const mark of [query.cycle.markValue, query.cycle.markDefault]) {
|
|
171
|
+
if (mark !== undefined &&
|
|
172
|
+
mark.kind !== "literal" &&
|
|
173
|
+
!(mark.kind === "cast" && mark.expression.kind === "literal")) {
|
|
174
|
+
this.#diagnostic("TSQ220", "CYCLE mark values must be constants", mark.range);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
addGenerated(query.cycle.set, {
|
|
178
|
+
name: sqlName(query.cycle.set),
|
|
179
|
+
databaseType: markValue.databaseType === markDefault.databaseType && markValue.databaseType !== undefined
|
|
180
|
+
? markValue.databaseType
|
|
181
|
+
: "unknown",
|
|
182
|
+
tsType: mergeTypeLiterals(markValue.tsType, markDefault.tsType),
|
|
183
|
+
nullable: markValue.nullable || markDefault.nullable,
|
|
184
|
+
});
|
|
185
|
+
addGenerated(query.cycle.using, {
|
|
186
|
+
name: sqlName(query.cycle.using),
|
|
187
|
+
databaseType: "record[]",
|
|
188
|
+
tsType: "unknown",
|
|
189
|
+
nullable: false,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
143
192
|
ctes.set(key, { name: key, columns });
|
|
144
193
|
}
|
|
145
194
|
return ctes;
|
|
146
195
|
}
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
196
|
+
#resolveRecursiveCte(query, outer, inherited) {
|
|
197
|
+
const arms = this.#selectArms(query.statement);
|
|
198
|
+
const references = arms.map(({ statement }) => ({
|
|
199
|
+
direct: this.#directSelfReferences(statement, query.name),
|
|
200
|
+
total: this.#selfReferences(statement, query.name),
|
|
201
|
+
}));
|
|
202
|
+
const firstRecursive = references.findIndex(({ total }) => total > 0);
|
|
203
|
+
if (firstRecursive <= 0) {
|
|
204
|
+
this.#diagnostic("TSQ220", `Recursive CTE ${query.name.name} requires a non-recursive SELECT before its recursive member`, query.range);
|
|
205
|
+
}
|
|
206
|
+
for (let index = Math.max(firstRecursive, 0); index < arms.length; index += 1) {
|
|
207
|
+
const arm = arms[index];
|
|
208
|
+
const reference = references[index];
|
|
209
|
+
if (reference.total === 0) {
|
|
210
|
+
this.#diagnostic("TSQ220", `Non-recursive members of ${query.name.name} must precede recursive members`, arm.range);
|
|
211
|
+
}
|
|
212
|
+
else if (reference.direct !== 1 || reference.total !== 1) {
|
|
213
|
+
this.#diagnostic("TSQ220", `Each recursive member of ${query.name.name} must reference itself exactly once in its top-level FROM`, arm.statement.range);
|
|
214
|
+
}
|
|
215
|
+
if (arm.operator !== "union") {
|
|
216
|
+
this.#diagnostic("TSQ220", `Recursive members of ${query.name.name} must use UNION or UNION ALL`, arm.range);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const seedStatements = arms.slice(0, Math.max(firstRecursive, 1)).map(({ statement }) => statement);
|
|
220
|
+
const seedColumns = seedStatements.map((statement) => this.#resolveSelect(statement, outer, inherited));
|
|
221
|
+
const mergedSeed = [...(seedColumns[0] ?? [])];
|
|
222
|
+
for (const columns of seedColumns.slice(1))
|
|
223
|
+
this.#mergeCompoundColumns(mergedSeed, columns, query.range);
|
|
224
|
+
const provisional = {};
|
|
225
|
+
const width = Math.max(query.columns.length, mergedSeed.length);
|
|
226
|
+
for (let index = 0; index < width; index += 1) {
|
|
227
|
+
const seed = mergedSeed[index];
|
|
228
|
+
const name = query.columns[index] === undefined ? seed?.name : sqlName(query.columns[index]);
|
|
229
|
+
if (name === undefined)
|
|
230
|
+
continue;
|
|
231
|
+
provisional[name] = {
|
|
232
|
+
name,
|
|
233
|
+
databaseType: seed?.databaseType ?? "unknown",
|
|
234
|
+
tsType: seed?.tsType ?? "unknown",
|
|
235
|
+
nullable: seed?.nullable ?? true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const recursiveScope = new Map(inherited);
|
|
239
|
+
recursiveScope.set(sqlName(query.name), { name: sqlName(query.name), columns: provisional });
|
|
240
|
+
return { columns: this.#resolveSelect(query.statement, outer, recursiveScope), resultKind: "rows" };
|
|
241
|
+
}
|
|
242
|
+
#selectArms(statement) {
|
|
243
|
+
const { compounds, ...simple } = statement;
|
|
244
|
+
const arms = [{ statement: { ...simple, compounds: [] }, range: statement.range }];
|
|
245
|
+
for (const compound of compounds) {
|
|
246
|
+
const [first, ...rest] = this.#selectArms(compound.statement);
|
|
247
|
+
if (first !== undefined)
|
|
248
|
+
arms.push({ ...first, operator: compound.operator, all: compound.all, range: compound.range });
|
|
249
|
+
arms.push(...rest);
|
|
150
250
|
}
|
|
151
|
-
|
|
251
|
+
return arms;
|
|
252
|
+
}
|
|
253
|
+
#directSelfReferences(statement, identifier) {
|
|
254
|
+
return [statement.from, ...statement.joins.map(({ table }) => table)].filter((reference) => reference?.kind === "table" &&
|
|
255
|
+
reference.schema === undefined &&
|
|
256
|
+
sqlName(reference.name) === sqlName(identifier)).length;
|
|
257
|
+
}
|
|
258
|
+
#selfReferences(statement, identifier) {
|
|
259
|
+
let references = 0;
|
|
260
|
+
walkStatement(statement, {
|
|
261
|
+
table(reference) {
|
|
262
|
+
if (reference.kind === "table" &&
|
|
263
|
+
reference.schema === undefined &&
|
|
264
|
+
sqlName(reference.name) === sqlName(identifier)) {
|
|
265
|
+
references += 1;
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return references;
|
|
270
|
+
}
|
|
271
|
+
#resolveSelect(statement, outer, ctes, expectedOutput) {
|
|
272
|
+
const scope = {
|
|
273
|
+
relations: [],
|
|
274
|
+
usingColumns: new Map(),
|
|
275
|
+
windows: new Map(),
|
|
276
|
+
...(outer === undefined ? {} : { outer }),
|
|
277
|
+
};
|
|
152
278
|
if (statement.from !== undefined)
|
|
153
279
|
this.#addRelation(statement.from, false, scope, ctes);
|
|
154
280
|
for (const join of statement.joins)
|
|
@@ -160,85 +286,896 @@ class Resolver {
|
|
|
160
286
|
}
|
|
161
287
|
}
|
|
162
288
|
}
|
|
289
|
+
for (const window of statement.windows) {
|
|
290
|
+
const name = sqlName(window.name);
|
|
291
|
+
if (scope.windows.has(name)) {
|
|
292
|
+
this.#diagnostic("TSQ222", `Duplicate window ${window.name.name}`, window.name.range);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const resolved = this.#resolveWindowSpecification(window.specification, scope, ctes);
|
|
296
|
+
scope.windows.set(name, resolved);
|
|
297
|
+
}
|
|
163
298
|
if (statement.where !== undefined)
|
|
164
299
|
this.#resolveExpression(statement.where, scope, ctes, this.#databaseType("boolean", false));
|
|
165
|
-
for (const
|
|
166
|
-
this.#
|
|
300
|
+
for (const grouping of statement.groupBy)
|
|
301
|
+
this.#resolveGrouping(grouping, statement.columns, scope, ctes);
|
|
167
302
|
if (statement.having !== undefined)
|
|
168
303
|
this.#resolveExpression(statement.having, scope, ctes, this.#databaseType("boolean", false));
|
|
169
304
|
for (const expression of statement.distinctOn)
|
|
170
305
|
this.#resolveExpression(expression, scope, ctes);
|
|
171
|
-
for (const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
306
|
+
for (const order of statement.orderBy) {
|
|
307
|
+
const output = this.#outputReference(order.expression, statement.columns, scope, true);
|
|
308
|
+
this.#resolveExpression(output ?? order.expression, scope, ctes);
|
|
309
|
+
}
|
|
310
|
+
if (statement.distinctOn.length > 0 && statement.orderBy.length > 0) {
|
|
311
|
+
const distinct = new Set(statement.distinctOn.map((expression) => this.#groupKey(expression, scope)));
|
|
312
|
+
const leading = statement.orderBy
|
|
313
|
+
.slice(0, statement.distinctOn.length)
|
|
314
|
+
.map(({ expression }) => this.#outputReference(expression, statement.columns, scope, true) ?? expression)
|
|
315
|
+
.map((expression) => this.#groupKey(expression, scope));
|
|
316
|
+
if (leading.length !== distinct.size || leading.some((key) => !distinct.has(key))) {
|
|
317
|
+
this.#diagnostic("TSQ228", "DISTINCT ON expressions must match the leftmost ORDER BY expressions", statement.orderBy[0].range);
|
|
318
|
+
}
|
|
176
319
|
}
|
|
177
|
-
for (const order of statement.orderBy)
|
|
178
|
-
this.#resolveExpression(order.expression, scope, ctes);
|
|
179
320
|
if (statement.limit !== undefined)
|
|
180
321
|
this.#resolveExpression(statement.limit, scope, ctes, this.#databaseType("integer", false));
|
|
181
322
|
if (statement.offset !== undefined)
|
|
182
323
|
this.#resolveExpression(statement.offset, scope, ctes, this.#databaseType("integer", false));
|
|
183
|
-
|
|
324
|
+
if (statement.fetch?.count !== undefined)
|
|
325
|
+
this.#resolveExpression(statement.fetch.count, scope, ctes, this.#databaseType("integer", false));
|
|
326
|
+
const columns = [...this.#resolveItems(statement.columns, scope, ctes, expectedOutput)];
|
|
327
|
+
this.#validateGroupedQuery(statement, columns, scope);
|
|
328
|
+
for (const compound of statement.compounds) {
|
|
329
|
+
const right = this.#resolveSelect(compound.statement, outer, ctes, expectedOutput);
|
|
330
|
+
this.#mergeCompoundColumns(columns, right, compound.range);
|
|
331
|
+
}
|
|
332
|
+
return columns;
|
|
333
|
+
}
|
|
334
|
+
#resolveGrouping(grouping, items, scope, ctes) {
|
|
335
|
+
if (!isSpecialGrouping(grouping)) {
|
|
336
|
+
const output = this.#outputReference(grouping, items, scope, false);
|
|
337
|
+
this.#resolveExpression(output ?? grouping, scope, ctes);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (grouping.kind !== "empty-group") {
|
|
341
|
+
for (const element of grouping.elements)
|
|
342
|
+
this.#resolveGrouping(element, items, scope, ctes);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
#outputReference(expression, items, scope, preferOutput) {
|
|
346
|
+
if (expression.kind === "literal" &&
|
|
347
|
+
typeof expression.value === "number" &&
|
|
348
|
+
Number.isSafeInteger(expression.value) &&
|
|
349
|
+
expression.value >= 1) {
|
|
350
|
+
return items[expression.value - 1]?.expression;
|
|
351
|
+
}
|
|
352
|
+
if (expression.kind !== "column" || expression.relation !== undefined)
|
|
353
|
+
return undefined;
|
|
354
|
+
const name = sqlName(expression.column);
|
|
355
|
+
if (!preferOutput && this.#columnMatches(undefined, name, scope, expression.column.quoted).length > 0)
|
|
356
|
+
return undefined;
|
|
357
|
+
const matches = items.filter((item) => item.alias !== undefined && sqlName(item.alias) === name);
|
|
358
|
+
if (matches.length > 1) {
|
|
359
|
+
this.#diagnostic("TSQ105", `Output reference ${expression.column.name} is ambiguous`, expression.range);
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
return matches[0]?.expression;
|
|
363
|
+
}
|
|
364
|
+
#validateGroupedQuery(statement, columns, scope) {
|
|
365
|
+
if (statement.where !== undefined)
|
|
366
|
+
this.#validateClauseFunctions(statement.where, false, false, "WHERE");
|
|
367
|
+
for (const grouping of statement.groupBy) {
|
|
368
|
+
this.#forEachGroupingExpression(grouping, (expression) => this.#validateClauseFunctions(expression, false, false, "GROUP BY"));
|
|
369
|
+
}
|
|
370
|
+
if (statement.having !== undefined)
|
|
371
|
+
this.#validateClauseFunctions(statement.having, true, false, "HAVING");
|
|
372
|
+
const aggregateQuery = statement.groupBy.length > 0 ||
|
|
373
|
+
statement.having !== undefined ||
|
|
374
|
+
statement.columns.some(({ expression }) => this.#containsAggregate(expression));
|
|
375
|
+
if (!aggregateQuery)
|
|
376
|
+
return;
|
|
377
|
+
if (statement.locking.length > 0) {
|
|
378
|
+
this.#diagnostic("TSQ228", "Locking clauses are not allowed on grouped queries", statement.locking[0].range);
|
|
379
|
+
}
|
|
380
|
+
const groupingSets = this.#expandGroupingList(statement.groupBy, statement.columns, scope);
|
|
381
|
+
const groupedKeys = new Set(groupingSets.flatMap((set) => [...set]));
|
|
382
|
+
for (const item of statement.columns)
|
|
383
|
+
this.#validateGroupedExpression(item.expression, groupedKeys, groupingSets, scope);
|
|
384
|
+
if (statement.having !== undefined) {
|
|
385
|
+
this.#validateGroupedExpression(statement.having, groupedKeys, groupingSets, scope);
|
|
386
|
+
}
|
|
387
|
+
statement.columns.forEach((item, index) => {
|
|
388
|
+
const column = columns[index];
|
|
389
|
+
if (column === undefined)
|
|
390
|
+
return;
|
|
391
|
+
const keys = this.#columnKeysOutsideAggregates(item.expression, scope);
|
|
392
|
+
if (keys.some((key) => groupingSets.some((set) => !set.has(key)))) {
|
|
393
|
+
columns[index] = { ...column, nullable: true };
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
#validateClauseFunctions(expression, allowAggregate, allowWindow, clause) {
|
|
398
|
+
if (expression.kind === "subquery" || expression.kind === "exists")
|
|
399
|
+
return;
|
|
400
|
+
if (expression.kind === "call") {
|
|
401
|
+
const window = expression.over !== undefined;
|
|
402
|
+
const aggregate = !window && this.#isAggregateCall(expression);
|
|
403
|
+
if (window && !allowWindow) {
|
|
404
|
+
this.#diagnostic("TSQ228", `Window functions are not allowed in ${clause}`, expression.range);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (aggregate && !allowAggregate) {
|
|
408
|
+
this.#diagnostic("TSQ228", `Aggregate functions are not allowed in ${clause}`, expression.range);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (expression.kind === "json-object-aggregate" || expression.kind === "json-array-aggregate") {
|
|
413
|
+
const window = expression.over !== undefined;
|
|
414
|
+
if (window && !allowWindow) {
|
|
415
|
+
this.#diagnostic("TSQ228", `Window functions are not allowed in ${clause}`, expression.range);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (!window && !allowAggregate) {
|
|
419
|
+
this.#diagnostic("TSQ228", `Aggregate functions are not allowed in ${clause}`, expression.range);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
for (const child of this.#expressionChildren(expression)) {
|
|
424
|
+
this.#validateClauseFunctions(child, allowAggregate, allowWindow, clause);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
#validateGroupedExpression(expression, groupedKeys, groupingSets, scope) {
|
|
428
|
+
if (groupedKeys.has(this.#groupKey(expression, scope)))
|
|
429
|
+
return;
|
|
430
|
+
if (expression.kind === "subquery" || expression.kind === "exists")
|
|
431
|
+
return;
|
|
432
|
+
if (expression.kind === "call" && expression.over === undefined && this.#isAggregateCall(expression))
|
|
433
|
+
return;
|
|
434
|
+
if ((expression.kind === "json-object-aggregate" || expression.kind === "json-array-aggregate") &&
|
|
435
|
+
expression.over === undefined) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (expression.kind === "column") {
|
|
439
|
+
const matches = this.#columnMatches(expression.relation, sqlName(expression.column), scope, expression.column.quoted);
|
|
440
|
+
const match = matches.length === 1 ? matches[0] : undefined;
|
|
441
|
+
if (match !== undefined && this.#functionallyDependent(match[0], groupingSets))
|
|
442
|
+
return;
|
|
443
|
+
this.#diagnostic("TSQ228", `Column ${expression.column.name} must appear in GROUP BY or be used in an aggregate`, expression.range);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
for (const child of this.#expressionChildren(expression)) {
|
|
447
|
+
this.#validateGroupedExpression(child, groupedKeys, groupingSets, scope);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
#functionallyDependent(relation, groupingSets) {
|
|
451
|
+
const primaryKey = this.#index
|
|
452
|
+
.relation(relation.table)
|
|
453
|
+
?.constraints.find(({ kind }) => kind === "primary-key")
|
|
454
|
+
?.columns.map((column) => `column:${relation.alias}.${column.toLowerCase()}`);
|
|
455
|
+
return (primaryKey !== undefined &&
|
|
456
|
+
primaryKey.length > 0 &&
|
|
457
|
+
groupingSets.every((set) => primaryKey.every((key) => set.has(key))));
|
|
458
|
+
}
|
|
459
|
+
#containsAggregate(expression) {
|
|
460
|
+
if (expression.kind === "subquery" || expression.kind === "exists")
|
|
461
|
+
return false;
|
|
462
|
+
if (expression.kind === "call" && expression.over === undefined && this.#isAggregateCall(expression))
|
|
463
|
+
return true;
|
|
464
|
+
if ((expression.kind === "json-object-aggregate" || expression.kind === "json-array-aggregate") &&
|
|
465
|
+
expression.over === undefined) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
return this.#expressionChildren(expression).some((child) => this.#containsAggregate(child));
|
|
469
|
+
}
|
|
470
|
+
#isAggregateCall(expression) {
|
|
471
|
+
const name = expression.name.name.toUpperCase();
|
|
472
|
+
const rule = postgresCatalogRoutineRule(name, this.#schema);
|
|
473
|
+
if (rule !== undefined && aggregateRoutineRules.has(rule))
|
|
474
|
+
return true;
|
|
475
|
+
if (aggregateExtrema.has(name))
|
|
476
|
+
return true;
|
|
477
|
+
if ((expression.withinGroup?.length ?? 0) > 0 && hypotheticalAggregates.has(name))
|
|
478
|
+
return true;
|
|
479
|
+
const schemaName = expression.schema === undefined ? undefined : sqlName(expression.schema);
|
|
480
|
+
return this.#index
|
|
481
|
+
.routineOverloads(sqlName(expression.name), expression.arguments.length, schemaName)
|
|
482
|
+
.some(({ kind }) => kind === "aggregate");
|
|
483
|
+
}
|
|
484
|
+
#expressionChildren(expression) {
|
|
485
|
+
switch (expression.kind) {
|
|
486
|
+
case "array":
|
|
487
|
+
case "row":
|
|
488
|
+
return expression.elements;
|
|
489
|
+
case "call": {
|
|
490
|
+
const window = expression.over !== undefined && "partitionBy" in expression.over
|
|
491
|
+
? [
|
|
492
|
+
...expression.over.partitionBy,
|
|
493
|
+
...expression.over.orderBy.map(({ expression: item }) => item),
|
|
494
|
+
...(expression.over.frame?.start.kind === "preceding" ||
|
|
495
|
+
expression.over.frame?.start.kind === "following"
|
|
496
|
+
? [expression.over.frame.start.expression]
|
|
497
|
+
: []),
|
|
498
|
+
...(expression.over.frame?.end?.kind === "preceding" || expression.over.frame?.end?.kind === "following"
|
|
499
|
+
? [expression.over.frame.end.expression]
|
|
500
|
+
: []),
|
|
501
|
+
]
|
|
502
|
+
: [];
|
|
503
|
+
return [
|
|
504
|
+
...expression.arguments,
|
|
505
|
+
...(expression.orderBy ?? []).map(({ expression: item }) => item),
|
|
506
|
+
...(expression.withinGroup ?? []).map(({ expression: item }) => item),
|
|
507
|
+
...(expression.filter === undefined ? [] : [expression.filter]),
|
|
508
|
+
...window,
|
|
509
|
+
];
|
|
510
|
+
}
|
|
511
|
+
case "cast":
|
|
512
|
+
case "unary":
|
|
513
|
+
return [expression.expression];
|
|
514
|
+
case "subscript":
|
|
515
|
+
return [
|
|
516
|
+
expression.expression,
|
|
517
|
+
...(expression.index === undefined ? [] : [expression.index]),
|
|
518
|
+
...(expression.lower === undefined ? [] : [expression.lower]),
|
|
519
|
+
...(expression.upper === undefined ? [] : [expression.upper]),
|
|
520
|
+
];
|
|
521
|
+
case "field-access":
|
|
522
|
+
return [expression.expression];
|
|
523
|
+
case "collate":
|
|
524
|
+
return [expression.expression];
|
|
525
|
+
case "at-time-zone":
|
|
526
|
+
return [expression.expression, ...(expression.zone === undefined ? [] : [expression.zone])];
|
|
527
|
+
case "json-object":
|
|
528
|
+
return expression.entries.flatMap(({ key, value }) => [key, value.expression]);
|
|
529
|
+
case "json-array":
|
|
530
|
+
return expression.values.map(({ expression: value }) => value);
|
|
531
|
+
case "json-object-aggregate":
|
|
532
|
+
case "json-array-aggregate": {
|
|
533
|
+
const window = expression.over !== undefined && "partitionBy" in expression.over
|
|
534
|
+
? [
|
|
535
|
+
...expression.over.partitionBy,
|
|
536
|
+
...expression.over.orderBy.map(({ expression: item }) => item),
|
|
537
|
+
...(expression.over.frame?.start.kind === "preceding" ||
|
|
538
|
+
expression.over.frame?.start.kind === "following"
|
|
539
|
+
? [expression.over.frame.start.expression]
|
|
540
|
+
: []),
|
|
541
|
+
...(expression.over.frame?.end?.kind === "preceding" || expression.over.frame?.end?.kind === "following"
|
|
542
|
+
? [expression.over.frame.end.expression]
|
|
543
|
+
: []),
|
|
544
|
+
]
|
|
545
|
+
: [];
|
|
546
|
+
return [
|
|
547
|
+
...(expression.kind === "json-object-aggregate" ? [expression.key] : []),
|
|
548
|
+
expression.value.expression,
|
|
549
|
+
...(expression.kind === "json-array-aggregate" ? expression.orderBy.map(({ expression: item }) => item) : []),
|
|
550
|
+
...(expression.filter === undefined ? [] : [expression.filter]),
|
|
551
|
+
...window,
|
|
552
|
+
];
|
|
553
|
+
}
|
|
554
|
+
case "json-parse":
|
|
555
|
+
return [expression.value.expression];
|
|
556
|
+
case "json-scalar":
|
|
557
|
+
return [expression.expression];
|
|
558
|
+
case "json-serialize":
|
|
559
|
+
return [expression.value.expression];
|
|
560
|
+
case "json-is":
|
|
561
|
+
return [expression.expression];
|
|
562
|
+
case "json-exists":
|
|
563
|
+
return [
|
|
564
|
+
expression.context.expression,
|
|
565
|
+
expression.path,
|
|
566
|
+
...expression.passing.map(({ value }) => value.expression),
|
|
567
|
+
];
|
|
568
|
+
case "json-query":
|
|
569
|
+
return [
|
|
570
|
+
expression.context.expression,
|
|
571
|
+
expression.path,
|
|
572
|
+
...expression.passing.map(({ value }) => value.expression),
|
|
573
|
+
...(expression.onEmpty?.expression === undefined ? [] : [expression.onEmpty.expression]),
|
|
574
|
+
...(expression.onError?.expression === undefined ? [] : [expression.onError.expression]),
|
|
575
|
+
];
|
|
576
|
+
case "json-value":
|
|
577
|
+
return [
|
|
578
|
+
expression.context.expression,
|
|
579
|
+
expression.path,
|
|
580
|
+
...expression.passing.map(({ value }) => value.expression),
|
|
581
|
+
...(expression.onEmpty?.expression === undefined ? [] : [expression.onEmpty.expression]),
|
|
582
|
+
...(expression.onError?.expression === undefined ? [] : [expression.onError.expression]),
|
|
583
|
+
];
|
|
584
|
+
case "binary":
|
|
585
|
+
return [expression.left, expression.right];
|
|
586
|
+
case "case":
|
|
587
|
+
return [
|
|
588
|
+
...(expression.operand === undefined ? [] : [expression.operand]),
|
|
589
|
+
...expression.branches.flatMap(({ when, then }) => [when, then]),
|
|
590
|
+
...(expression.elseExpression === undefined ? [] : [expression.elseExpression]),
|
|
591
|
+
];
|
|
592
|
+
case "in":
|
|
593
|
+
return [
|
|
594
|
+
expression.expression,
|
|
595
|
+
...(Array.isArray(expression.values) ? expression.values : []),
|
|
596
|
+
];
|
|
597
|
+
case "between":
|
|
598
|
+
return [expression.expression, expression.lower, expression.upper];
|
|
599
|
+
case "quantified-comparison":
|
|
600
|
+
return [expression.left, ...(expression.right.kind === "select" ? [] : [expression.right])];
|
|
601
|
+
case "column":
|
|
602
|
+
case "exists":
|
|
603
|
+
case "literal":
|
|
604
|
+
case "parameter":
|
|
605
|
+
case "star":
|
|
606
|
+
case "subquery":
|
|
607
|
+
return [];
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
#columnKeysOutsideAggregates(expression, scope) {
|
|
611
|
+
if (expression.kind === "subquery" || expression.kind === "exists")
|
|
612
|
+
return [];
|
|
613
|
+
if (expression.kind === "call" && expression.over === undefined && this.#isAggregateCall(expression))
|
|
614
|
+
return [];
|
|
615
|
+
if ((expression.kind === "json-object-aggregate" || expression.kind === "json-array-aggregate") &&
|
|
616
|
+
expression.over === undefined) {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
if (expression.kind === "call" && postgresCatalogRoutineRule(expression.name.name, this.#schema) === "grouping") {
|
|
620
|
+
return [];
|
|
621
|
+
}
|
|
622
|
+
if (expression.kind === "column")
|
|
623
|
+
return [this.#groupKey(expression, scope)];
|
|
624
|
+
return this.#expressionChildren(expression).flatMap((child) => this.#columnKeysOutsideAggregates(child, scope));
|
|
625
|
+
}
|
|
626
|
+
#groupKey(expression, scope) {
|
|
627
|
+
if (expression.kind === "column") {
|
|
628
|
+
const matches = this.#columnMatches(expression.relation, sqlName(expression.column), scope, expression.column.quoted);
|
|
629
|
+
if (matches.length === 1)
|
|
630
|
+
return `column:${matches[0][0].alias}.${matches[0][1].name.toLowerCase()}`;
|
|
631
|
+
}
|
|
632
|
+
return `expression:${postgresExpressionIdentity(expression)}`;
|
|
633
|
+
}
|
|
634
|
+
#forEachGroupingExpression(grouping, visit) {
|
|
635
|
+
if (!isSpecialGrouping(grouping))
|
|
636
|
+
visit(grouping);
|
|
637
|
+
else if (grouping.kind !== "empty-group")
|
|
638
|
+
for (const element of grouping.elements)
|
|
639
|
+
this.#forEachGroupingExpression(element, visit);
|
|
640
|
+
}
|
|
641
|
+
#expandGroupingList(groupings, items, scope) {
|
|
642
|
+
let result = [new Set()];
|
|
643
|
+
for (const grouping of groupings) {
|
|
644
|
+
const alternatives = this.#expandGrouping(grouping, items, scope);
|
|
645
|
+
result = result.flatMap((left) => alternatives.map((right) => new Set([...left, ...right])));
|
|
646
|
+
}
|
|
647
|
+
return result;
|
|
648
|
+
}
|
|
649
|
+
#expandGrouping(grouping, items, scope) {
|
|
650
|
+
if (grouping.kind === "empty-group")
|
|
651
|
+
return [new Set()];
|
|
652
|
+
if (!isSpecialGrouping(grouping)) {
|
|
653
|
+
const expression = this.#outputReference(grouping, items, scope, false) ?? grouping;
|
|
654
|
+
return [new Set([this.#groupKey(expression, scope)])];
|
|
655
|
+
}
|
|
656
|
+
if (grouping.kind === "grouping-sets") {
|
|
657
|
+
return grouping.elements.flatMap((element) => this.#expandGrouping(element, items, scope));
|
|
658
|
+
}
|
|
659
|
+
const units = grouping.elements.map((element) => {
|
|
660
|
+
const expanded = this.#expandGrouping(element, items, scope);
|
|
661
|
+
return new Set(expanded.flatMap((set) => [...set]));
|
|
662
|
+
});
|
|
663
|
+
if (grouping.kind === "grouping-set") {
|
|
664
|
+
return [new Set(units.flatMap((set) => [...set]))];
|
|
665
|
+
}
|
|
666
|
+
if (grouping.kind === "rollup") {
|
|
667
|
+
return Array.from({ length: units.length + 1 }, (_, omitted) => new Set(units.slice(0, units.length - omitted).flatMap((set) => [...set])));
|
|
668
|
+
}
|
|
669
|
+
return Array.from({ length: 2 ** units.length }, (_, mask) => new Set(units.flatMap((set, index) => ((mask & (1 << index)) === 0 ? [] : [...set]))));
|
|
670
|
+
}
|
|
671
|
+
#resolveWindowSpecification(specification, scope, ctes) {
|
|
672
|
+
const base = specification.base === undefined ? undefined : scope.windows.get(sqlName(specification.base));
|
|
673
|
+
if (specification.base !== undefined && base === undefined) {
|
|
674
|
+
this.#diagnostic("TSQ222", `Unknown window ${specification.base.name}`, specification.base.range);
|
|
675
|
+
}
|
|
676
|
+
if (base !== undefined && specification.partitionBy.length > 0) {
|
|
677
|
+
this.#diagnostic("TSQ222", "A derived window cannot override PARTITION BY", specification.range);
|
|
678
|
+
}
|
|
679
|
+
if (base !== undefined && base.orderBy.length > 0 && specification.orderBy.length > 0) {
|
|
680
|
+
this.#diagnostic("TSQ222", "A derived window cannot override ORDER BY", specification.range);
|
|
681
|
+
}
|
|
682
|
+
if (base?.frame !== undefined) {
|
|
683
|
+
this.#diagnostic("TSQ222", "A framed window cannot be inherited", specification.range);
|
|
684
|
+
}
|
|
685
|
+
const partitionBy = specification.partitionBy.length > 0 ? specification.partitionBy : (base?.partitionBy ?? []);
|
|
686
|
+
const orderBy = specification.orderBy.length > 0 ? specification.orderBy : (base?.orderBy ?? []);
|
|
687
|
+
for (const expression of specification.partitionBy)
|
|
688
|
+
this.#resolveExpression(expression, scope, ctes);
|
|
689
|
+
for (const order of specification.orderBy)
|
|
690
|
+
this.#resolveExpression(order.expression, scope, ctes);
|
|
691
|
+
if (specification.frame !== undefined) {
|
|
692
|
+
const { frame } = specification;
|
|
693
|
+
if (frame.start.kind === "unbounded-following") {
|
|
694
|
+
this.#diagnostic("TSQ222", "A window frame cannot start with UNBOUNDED FOLLOWING", frame.start.range);
|
|
695
|
+
}
|
|
696
|
+
if (frame.end?.kind === "unbounded-preceding") {
|
|
697
|
+
this.#diagnostic("TSQ222", "A window frame cannot end with UNBOUNDED PRECEDING", frame.end.range);
|
|
698
|
+
}
|
|
699
|
+
const boundRank = (bound) => {
|
|
700
|
+
if (bound.kind === "unbounded-preceding")
|
|
701
|
+
return 0;
|
|
702
|
+
if (bound.kind === "preceding")
|
|
703
|
+
return 1;
|
|
704
|
+
if (bound.kind === "current-row")
|
|
705
|
+
return 2;
|
|
706
|
+
if (bound.kind === "following")
|
|
707
|
+
return 3;
|
|
708
|
+
return 4;
|
|
709
|
+
};
|
|
710
|
+
if (frame.end !== undefined && boundRank(frame.start) > boundRank(frame.end)) {
|
|
711
|
+
this.#diagnostic("TSQ222", "A window frame end cannot precede its start", frame.range);
|
|
712
|
+
}
|
|
713
|
+
const offsetBounds = [frame.start, frame.end].filter((bound) => bound?.kind === "preceding" || bound?.kind === "following");
|
|
714
|
+
if (frame.unit === "range" && offsetBounds.length > 0 && orderBy.length !== 1) {
|
|
715
|
+
this.#diagnostic("TSQ222", "RANGE offset frames require exactly one ORDER BY expression", frame.range);
|
|
716
|
+
}
|
|
717
|
+
for (const bound of offsetBounds) {
|
|
718
|
+
const orderType = frame.unit === "range" && orderBy.length === 1
|
|
719
|
+
? this.#resolveExpression(orderBy[0].expression, scope, ctes)
|
|
720
|
+
: undefined;
|
|
721
|
+
const expected = frame.unit === "range"
|
|
722
|
+
? orderType?.databaseType !== undefined && this.#isNumericType(orderType.databaseType)
|
|
723
|
+
? orderType
|
|
724
|
+
: undefined
|
|
725
|
+
: this.#databaseType("integer", false);
|
|
726
|
+
this.#resolveExpression(bound.expression, scope, ctes, expected);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
...specification,
|
|
731
|
+
partitionBy,
|
|
732
|
+
orderBy,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
#mergeCompoundColumns(columns, right, range) {
|
|
736
|
+
if (right.length !== columns.length) {
|
|
737
|
+
this.#diagnostic("TSQ214", `Compound query has ${columns.length} columns on the left and ${right.length} on the right`, range);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
for (let index = 0; index < columns.length; index += 1) {
|
|
741
|
+
const left = columns[index];
|
|
742
|
+
const candidate = right[index];
|
|
743
|
+
const { databaseType: _databaseType, ...base } = left;
|
|
744
|
+
columns[index] = {
|
|
745
|
+
...base,
|
|
746
|
+
tsType: mergeTypeLiterals(left.tsType, candidate.tsType),
|
|
747
|
+
nullable: left.nullable || candidate.nullable,
|
|
748
|
+
...(left.databaseType === candidate.databaseType && left.databaseType !== undefined
|
|
749
|
+
? { databaseType: left.databaseType }
|
|
750
|
+
: {}),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
184
753
|
}
|
|
185
754
|
#resolveInsert(statement, outer, ctes) {
|
|
186
|
-
const scope = {
|
|
755
|
+
const scope = {
|
|
756
|
+
relations: [],
|
|
757
|
+
usingColumns: new Map(),
|
|
758
|
+
windows: new Map(),
|
|
759
|
+
...(outer === undefined ? {} : { outer }),
|
|
760
|
+
};
|
|
187
761
|
const target = this.#addRelation(statement.table, false, scope, ctes);
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
762
|
+
const allTargetColumns = Object.values(target?.table.columns ?? {});
|
|
763
|
+
let targetColumns = statement.columns.length > 0
|
|
764
|
+
? statement.columns.map((column) => this.#findColumn(target?.table, column))
|
|
765
|
+
: statement.source.kind === "values"
|
|
766
|
+
? allTargetColumns.slice(0, statement.source.rows[0]?.length ?? 0)
|
|
767
|
+
: statement.source.kind === "default-values"
|
|
768
|
+
? []
|
|
769
|
+
: allTargetColumns;
|
|
770
|
+
const insertNames = statement.columns.map(sqlName);
|
|
771
|
+
if (new Set(insertNames).size !== insertNames.length)
|
|
772
|
+
this.#diagnostic("TSQ227", "INSERT cannot target the same column more than once", statement.table.range);
|
|
191
773
|
if (statement.source.kind === "values") {
|
|
192
774
|
for (const row of statement.source.rows) {
|
|
193
775
|
if (row.length !== targetColumns.length) {
|
|
194
776
|
this.#diagnostic("TSQ214", `INSERT has ${targetColumns.length} target columns but ${row.length} values`, statement.source.range);
|
|
195
777
|
}
|
|
196
778
|
row.forEach((value, index) => {
|
|
197
|
-
this.#resolveExpression(value, scope, ctes, this.#snapshotType(targetColumns[index]));
|
|
779
|
+
const resolved = this.#resolveExpression(value, scope, ctes, this.#snapshotType(targetColumns[index]));
|
|
780
|
+
this.#validateWriteValue(targetColumns[index], resolved, value.range, "INSERT");
|
|
198
781
|
});
|
|
199
782
|
}
|
|
200
783
|
}
|
|
201
784
|
else if (statement.source.kind === "select") {
|
|
202
|
-
const source = this.#resolveStatement(statement.source, outer, ctes);
|
|
203
|
-
if (
|
|
785
|
+
const source = this.#resolveStatement(statement.source, outer, ctes, targetColumns.map((column) => this.#snapshotType(column)));
|
|
786
|
+
if (statement.columns.length === 0)
|
|
787
|
+
targetColumns = allTargetColumns.slice(0, source.columns.length);
|
|
788
|
+
if ((statement.columns.length > 0 && source.columns.length !== targetColumns.length) ||
|
|
789
|
+
source.columns.length > allTargetColumns.length) {
|
|
204
790
|
this.#diagnostic("TSQ214", `INSERT has ${targetColumns.length} target columns but SELECT returns ${source.columns.length}`, statement.source.range);
|
|
205
791
|
}
|
|
792
|
+
this.#validateWriteColumns(targetColumns, source.columns, statement.source.range, "INSERT");
|
|
793
|
+
}
|
|
794
|
+
if (target !== undefined) {
|
|
795
|
+
targetColumns.forEach((column, index) => {
|
|
796
|
+
if (column === undefined)
|
|
797
|
+
return;
|
|
798
|
+
const evidence = this.#index.columnEvidence(target.table, column);
|
|
799
|
+
const identityOverride = statement.overriding !== undefined && evidence?.identity !== "none";
|
|
800
|
+
const defaultOnly = statement.source.kind === "default-values" ||
|
|
801
|
+
(statement.source.kind === "values" &&
|
|
802
|
+
statement.source.rows.every((row) => this.#isDefaultValue(row[index])));
|
|
803
|
+
if (this.#index.columnEligibility(target.table, column, "insert") === false &&
|
|
804
|
+
!identityOverride &&
|
|
805
|
+
!defaultOnly) {
|
|
806
|
+
this.#diagnostic("TSQ218", `Cannot INSERT into non-insertable column ${column.name}`, statement.columns[index]?.range ?? statement.source.range);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
const supplied = new Set(targetColumns.flatMap((column, index) => {
|
|
810
|
+
if (column === undefined || statement.source.kind === "default-values")
|
|
811
|
+
return [];
|
|
812
|
+
if (statement.source.kind === "values" &&
|
|
813
|
+
statement.source.rows.every((row) => this.#isDefaultValue(row[index])))
|
|
814
|
+
return [];
|
|
815
|
+
return [column.name.toLowerCase()];
|
|
816
|
+
}));
|
|
817
|
+
const required = this.#index.requiredInsertColumns(target.table);
|
|
818
|
+
if (required !== "unknown") {
|
|
819
|
+
for (const column of required) {
|
|
820
|
+
if (!supplied.has(column.name.toLowerCase())) {
|
|
821
|
+
this.#diagnostic("TSQ219", `INSERT omits required column ${column.name}`, statement.table.range);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (statement.conflict !== undefined && target !== undefined) {
|
|
827
|
+
const conflict = statement.conflict;
|
|
828
|
+
if (conflict.action.kind === "update" && conflict.target === undefined) {
|
|
829
|
+
this.#diagnostic("TSQ227", "ON CONFLICT DO UPDATE requires a conflict target", conflict.range);
|
|
830
|
+
}
|
|
831
|
+
if (conflict.target?.kind === "constraint") {
|
|
832
|
+
const targetConstraint = conflict.target;
|
|
833
|
+
const requested = sqlName(targetConstraint.constraint);
|
|
834
|
+
const relation = this.#index.relation(target.table);
|
|
835
|
+
const match = relation?.constraints.find((constraint) => {
|
|
836
|
+
if (constraint.kind !== "primary-key" && constraint.kind !== "unique" && constraint.kind !== "exclusion")
|
|
837
|
+
return false;
|
|
838
|
+
const name = constraint.identity.slice(constraint.identity.lastIndexOf(".") + 1);
|
|
839
|
+
return targetConstraint.constraint.quoted ? name === requested : name.toLowerCase() === requested;
|
|
840
|
+
});
|
|
841
|
+
if (relation !== undefined && match === undefined) {
|
|
842
|
+
this.#diagnostic("TSQ226", `Unknown conflict constraint ${requested}`, conflict.target.range);
|
|
843
|
+
}
|
|
844
|
+
else if (relation === undefined) {
|
|
845
|
+
this.#diagnostic("TSQ402", `Conflict constraint ${requested} requires schema snapshot v2 evidence`, conflict.target.range, undefined, "warning");
|
|
846
|
+
}
|
|
847
|
+
else if (match?.deferrable === true) {
|
|
848
|
+
this.#diagnostic("TSQ224", `Deferrable constraint ${requested} cannot arbitrate ON CONFLICT`, conflict.target.range);
|
|
849
|
+
}
|
|
850
|
+
else if (match !== undefined && (match.deferrable === "unknown" || match.deferrable === undefined)) {
|
|
851
|
+
this.#diagnostic("TSQ402", `Conflict constraint ${requested} has unknown deferrability`, conflict.target.range, undefined, "warning");
|
|
852
|
+
}
|
|
853
|
+
else if (match?.kind === "exclusion" && conflict.action.kind === "update") {
|
|
854
|
+
this.#diagnostic("TSQ224", "Exclusion constraints cannot arbitrate ON CONFLICT DO UPDATE", conflict.target.range);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
else if (conflict.target?.kind === "inference") {
|
|
858
|
+
for (const element of conflict.target.elements) {
|
|
859
|
+
this.#resolveExpression(element.expression, scope, ctes);
|
|
860
|
+
}
|
|
861
|
+
if (conflict.target.predicate !== undefined)
|
|
862
|
+
this.#resolveExpression(conflict.target.predicate, scope, ctes, this.#databaseType("boolean", false));
|
|
863
|
+
const inference = this.#matchConflictInference(target.table, conflict.target);
|
|
864
|
+
if (inference === "unknown") {
|
|
865
|
+
this.#diagnostic("TSQ402", "Exact conflict-target inference requires canonical schema snapshot v2 evidence", conflict.target.range, undefined, "warning");
|
|
866
|
+
}
|
|
867
|
+
else if (!inference) {
|
|
868
|
+
this.#diagnostic("TSQ226", "Conflict target does not match a valid unique index or primary-key constraint", conflict.target.range);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (conflict.action.kind === "update") {
|
|
872
|
+
const excludedAlias = "excluded";
|
|
873
|
+
if (scope.relations.some(({ alias }) => alias === excludedAlias))
|
|
874
|
+
this.#diagnostic("TSQ108", "INSERT target alias excluded conflicts with the ON CONFLICT excluded row", statement.table.range);
|
|
875
|
+
const conflictScope = {
|
|
876
|
+
...scope,
|
|
877
|
+
relations: [
|
|
878
|
+
...scope.relations.filter(({ alias }) => alias !== excludedAlias),
|
|
879
|
+
{ alias: excludedAlias, table: target.table, nullable: false, qualifiedOnly: true },
|
|
880
|
+
],
|
|
881
|
+
};
|
|
882
|
+
this.#resolveAssignments(conflict.action.assignments, target.table, conflictScope, ctes);
|
|
883
|
+
if (conflict.action.where !== undefined)
|
|
884
|
+
this.#resolveExpression(conflict.action.where, conflictScope, ctes, this.#databaseType("boolean", false));
|
|
885
|
+
}
|
|
206
886
|
}
|
|
207
|
-
const
|
|
887
|
+
const returningScope = this.#returningScope(scope, target?.table, statement.returningAliases, statement.returning, "insert");
|
|
888
|
+
const columns = this.#resolveItems(statement.returning, returningScope, ctes);
|
|
208
889
|
return { columns, resultKind: statement.returning.length === 0 ? "command" : "rows" };
|
|
209
890
|
}
|
|
210
891
|
#resolveUpdate(statement, outer, ctes) {
|
|
211
|
-
const scope = {
|
|
892
|
+
const scope = {
|
|
893
|
+
relations: [],
|
|
894
|
+
usingColumns: new Map(),
|
|
895
|
+
windows: new Map(),
|
|
896
|
+
...(outer === undefined ? {} : { outer }),
|
|
897
|
+
};
|
|
212
898
|
const target = this.#addRelation(statement.table, false, scope, ctes);
|
|
213
|
-
for (const assignment of statement.assignments) {
|
|
214
|
-
const column = this.#findColumn(target?.table, assignment.column);
|
|
215
|
-
this.#resolveExpression(assignment.value, scope, ctes, this.#snapshotType(column));
|
|
216
|
-
}
|
|
217
899
|
if (statement.from !== undefined)
|
|
218
900
|
this.#addRelation(statement.from, false, scope, ctes);
|
|
219
901
|
for (const join of statement.joins)
|
|
220
902
|
this.#addJoin(join, scope, ctes);
|
|
903
|
+
this.#resolveAssignments(statement.assignments, target?.table, scope, ctes);
|
|
221
904
|
if (statement.where !== undefined)
|
|
222
905
|
this.#resolveExpression(statement.where, scope, ctes, this.#databaseType("boolean", false));
|
|
223
|
-
const columns = this.#resolveItems(statement.returning, scope, ctes);
|
|
906
|
+
const columns = this.#resolveItems(statement.returning, this.#returningScope(scope, target?.table, statement.returningAliases, statement.returning, "update"), ctes);
|
|
224
907
|
return { columns, resultKind: statement.returning.length === 0 ? "command" : "rows" };
|
|
225
908
|
}
|
|
226
909
|
#resolveDelete(statement, outer, ctes) {
|
|
227
|
-
const scope = {
|
|
228
|
-
|
|
910
|
+
const scope = {
|
|
911
|
+
relations: [],
|
|
912
|
+
usingColumns: new Map(),
|
|
913
|
+
windows: new Map(),
|
|
914
|
+
...(outer === undefined ? {} : { outer }),
|
|
915
|
+
};
|
|
916
|
+
const target = this.#addRelation(statement.table, false, scope, ctes);
|
|
229
917
|
for (const reference of statement.using)
|
|
230
918
|
this.#addRelation(reference, false, scope, ctes);
|
|
919
|
+
for (const join of statement.joins)
|
|
920
|
+
this.#addJoin(join, scope, ctes);
|
|
231
921
|
if (statement.where !== undefined)
|
|
232
922
|
this.#resolveExpression(statement.where, scope, ctes, this.#databaseType("boolean", false));
|
|
233
|
-
const columns = this.#resolveItems(statement.returning, scope, ctes);
|
|
923
|
+
const columns = this.#resolveItems(statement.returning, this.#returningScope(scope, target?.table, statement.returningAliases, statement.returning, "delete"), ctes);
|
|
924
|
+
return { columns, resultKind: statement.returning.length === 0 ? "command" : "rows" };
|
|
925
|
+
}
|
|
926
|
+
#resolveMerge(statement, outer, ctes) {
|
|
927
|
+
this.#requireServerMajor(15, "MERGE", statement.range);
|
|
928
|
+
if (statement.with?.recursive)
|
|
929
|
+
this.#diagnostic("TSQ227", "MERGE does not support WITH RECURSIVE", statement.with.range);
|
|
930
|
+
const scope = {
|
|
931
|
+
relations: [],
|
|
932
|
+
usingColumns: new Map(),
|
|
933
|
+
windows: new Map(),
|
|
934
|
+
...(outer === undefined ? {} : { outer }),
|
|
935
|
+
};
|
|
936
|
+
const target = this.#addRelation(statement.table, false, scope, ctes);
|
|
937
|
+
const targetAlias = target?.alias;
|
|
938
|
+
if (statement.source.kind === "values") {
|
|
939
|
+
const rows = statement.source.rows.map((row) => row.map((expression) => this.#resolveExpression(expression, { ...scope, relations: [] }, ctes)));
|
|
940
|
+
const width = Math.max(0, ...rows.map((row) => row.length));
|
|
941
|
+
if (rows.some((row) => row.length !== width))
|
|
942
|
+
this.#diagnostic("TSQ214", "MERGE VALUES source rows must have equal arity", statement.source.range);
|
|
943
|
+
if (statement.source.columns.length > 0 && statement.source.columns.length !== width)
|
|
944
|
+
this.#diagnostic("TSQ214", "MERGE VALUES source alias list does not match row arity", statement.source.range);
|
|
945
|
+
const columns = {};
|
|
946
|
+
for (let index = 0; index < width; index += 1) {
|
|
947
|
+
const candidates = rows.map((row) => row[index]).filter((value) => value !== undefined);
|
|
948
|
+
const name = statement.source.columns[index]?.name ?? `column${index + 1}`;
|
|
949
|
+
columns[name] = {
|
|
950
|
+
name,
|
|
951
|
+
databaseType: candidates[0]?.databaseType ?? "unknown",
|
|
952
|
+
tsType: unionTypeLiterals(candidates.map(({ tsType }) => tsType)),
|
|
953
|
+
nullable: candidates.some(({ nullable }) => nullable),
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
const sourceAlias = statement.source.alias === undefined
|
|
957
|
+
? `*merge-values*${statement.source.range.start}`
|
|
958
|
+
: sqlName(statement.source.alias);
|
|
959
|
+
if (sourceAlias === targetAlias)
|
|
960
|
+
this.#diagnostic("TSQ108", `Duplicate relation alias ${sourceAlias}`, statement.source.range);
|
|
961
|
+
scope.relations.push({
|
|
962
|
+
alias: sourceAlias,
|
|
963
|
+
table: { name: statement.source.alias?.name ?? sourceAlias, columns },
|
|
964
|
+
nullable: false,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
else
|
|
968
|
+
this.#addRelation(statement.source, false, scope, ctes);
|
|
969
|
+
this.#resolveExpression(statement.on, scope, ctes, this.#databaseType("boolean", false));
|
|
970
|
+
const terminal = new Set();
|
|
971
|
+
for (const clause of statement.clauses) {
|
|
972
|
+
if (clause.by !== undefined)
|
|
973
|
+
this.#requireServerMajor(17, `MERGE WHEN NOT MATCHED BY ${clause.by.toUpperCase()}`, clause.range);
|
|
974
|
+
if (terminal.has(clause.match))
|
|
975
|
+
this.#diagnostic("TSQ227", `Unreachable MERGE ${clause.match} clause`, clause.range);
|
|
976
|
+
if (clause.condition === undefined)
|
|
977
|
+
terminal.add(clause.match);
|
|
978
|
+
const clauseScope = {
|
|
979
|
+
...scope,
|
|
980
|
+
relations: scope.relations.filter(({ alias }) => {
|
|
981
|
+
if (clause.match === "matched")
|
|
982
|
+
return true;
|
|
983
|
+
return clause.match === "not-matched-source" ? alias === targetAlias : alias !== targetAlias;
|
|
984
|
+
}),
|
|
985
|
+
};
|
|
986
|
+
if (clause.condition !== undefined)
|
|
987
|
+
this.#resolveExpression(clause.condition, clauseScope, ctes, this.#databaseType("boolean", false));
|
|
988
|
+
const action = clause.action;
|
|
989
|
+
if (action.kind === "insert") {
|
|
990
|
+
if (clause.match !== "not-matched-target")
|
|
991
|
+
this.#diagnostic("TSQ227", "MERGE INSERT is only valid for NOT MATCHED BY TARGET", action.range);
|
|
992
|
+
const actionNames = action.columns.map(sqlName);
|
|
993
|
+
if (new Set(actionNames).size !== actionNames.length)
|
|
994
|
+
this.#diagnostic("TSQ227", "MERGE INSERT cannot target the same column more than once", action.range);
|
|
995
|
+
if (action.source.kind === "default-values" && action.overriding !== undefined)
|
|
996
|
+
this.#diagnostic("TSQ227", "MERGE DEFAULT VALUES cannot use OVERRIDING", action.range);
|
|
997
|
+
const targetColumns = action.columns.length === 0
|
|
998
|
+
? Object.values(target?.table.columns ?? {})
|
|
999
|
+
: action.columns.map((column) => this.#findColumn(target?.table, column));
|
|
1000
|
+
if (target !== undefined) {
|
|
1001
|
+
targetColumns.forEach((column, index) => {
|
|
1002
|
+
if (column === undefined)
|
|
1003
|
+
return;
|
|
1004
|
+
const evidence = this.#index.columnEvidence(target.table, column);
|
|
1005
|
+
const identityOverride = action.overriding !== undefined && evidence?.identity !== "none";
|
|
1006
|
+
const defaultOnly = action.source.kind === "default-values" ||
|
|
1007
|
+
action.source.rows.every((row) => this.#isDefaultValue(row[index]));
|
|
1008
|
+
if (this.#index.columnEligibility(target.table, column, "insert") === false &&
|
|
1009
|
+
!identityOverride &&
|
|
1010
|
+
!defaultOnly) {
|
|
1011
|
+
this.#diagnostic("TSQ218", `Cannot INSERT into non-insertable column ${column.name}`, action.columns[index]?.range ?? action.range);
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
const supplied = new Set();
|
|
1015
|
+
if (action.source.kind === "values") {
|
|
1016
|
+
const rows = action.source.rows;
|
|
1017
|
+
targetColumns.forEach((column, index) => {
|
|
1018
|
+
if (column !== undefined && !rows.every((row) => this.#isDefaultValue(row[index])))
|
|
1019
|
+
supplied.add(column.name.toLowerCase());
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
const required = this.#index.requiredInsertColumns(target.table);
|
|
1023
|
+
if (required !== "unknown") {
|
|
1024
|
+
for (const column of required) {
|
|
1025
|
+
if (!supplied.has(column.name.toLowerCase()))
|
|
1026
|
+
this.#diagnostic("TSQ219", `MERGE INSERT omits required column ${column.name}`, action.range);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (action.source.kind === "values") {
|
|
1031
|
+
const values = action.source.rows[0] ?? [];
|
|
1032
|
+
if (values.length !== targetColumns.length)
|
|
1033
|
+
this.#diagnostic("TSQ214", `MERGE INSERT has ${targetColumns.length} columns but ${values.length} values`, action.range);
|
|
1034
|
+
values.forEach((value, index) => {
|
|
1035
|
+
const resolved = this.#resolveExpression(value, clauseScope, ctes, this.#snapshotType(targetColumns[index]));
|
|
1036
|
+
this.#validateWriteValue(targetColumns[index], resolved, value.range, "MERGE INSERT");
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
else if (action.kind === "update") {
|
|
1041
|
+
if (clause.match === "not-matched-target")
|
|
1042
|
+
this.#diagnostic("TSQ227", "MERGE UPDATE cannot modify an unmatched target row", action.range);
|
|
1043
|
+
this.#resolveAssignments(action.assignments, target?.table, clauseScope, ctes);
|
|
1044
|
+
}
|
|
1045
|
+
else if (action.kind === "delete" && clause.match === "not-matched-target") {
|
|
1046
|
+
this.#diagnostic("TSQ227", "MERGE DELETE cannot delete an unmatched target row", action.range);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
if (statement.returning.length > 0)
|
|
1050
|
+
this.#requireServerMajor(17, "MERGE RETURNING", statement.range);
|
|
1051
|
+
const returningScope = this.#returningScope(scope, target?.table, statement.returningAliases, statement.returning, "merge");
|
|
1052
|
+
this.#insideMergeReturning = true;
|
|
1053
|
+
const columns = this.#resolveItems(statement.returning, returningScope, ctes);
|
|
1054
|
+
this.#insideMergeReturning = false;
|
|
234
1055
|
return { columns, resultKind: statement.returning.length === 0 ? "command" : "rows" };
|
|
235
1056
|
}
|
|
1057
|
+
#resolveAssignments(assignments, table, scope, ctes) {
|
|
1058
|
+
const assigned = new Set();
|
|
1059
|
+
for (const assignment of assignments) {
|
|
1060
|
+
const identifiers = "column" in assignment ? [assignment.column] : assignment.columns;
|
|
1061
|
+
const names = identifiers.map(sqlName);
|
|
1062
|
+
if (new Set(names).size !== names.length || names.some((name) => assigned.has(name))) {
|
|
1063
|
+
this.#diagnostic("TSQ227", "An assignment cannot target the same column more than once", assignment.range);
|
|
1064
|
+
}
|
|
1065
|
+
for (const name of names)
|
|
1066
|
+
assigned.add(name);
|
|
1067
|
+
const columns = identifiers.map((identifier) => this.#findColumn(table, identifier));
|
|
1068
|
+
for (let index = 0; index < columns.length; index += 1) {
|
|
1069
|
+
const column = columns[index];
|
|
1070
|
+
if (table !== undefined &&
|
|
1071
|
+
column !== undefined &&
|
|
1072
|
+
this.#index.columnEligibility(table, column, "update") === false) {
|
|
1073
|
+
this.#diagnostic("TSQ218", `Cannot UPDATE non-updatable column ${column.name}`, identifiers[index].range);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (identifiers.length === 1) {
|
|
1077
|
+
const resolved = this.#resolveExpression(assignment.value, scope, ctes, this.#snapshotType(columns[0]));
|
|
1078
|
+
this.#validateWriteValue(columns[0], resolved, assignment.value.range, "UPDATE");
|
|
1079
|
+
}
|
|
1080
|
+
else if (assignment.value.kind === "row") {
|
|
1081
|
+
assignment.value.elements.forEach((element, index) => {
|
|
1082
|
+
const resolved = this.#resolveExpression(element, scope, ctes, this.#snapshotType(columns[index]));
|
|
1083
|
+
this.#validateWriteValue(columns[index], resolved, element.range, "row assignment");
|
|
1084
|
+
});
|
|
1085
|
+
if (assignment.value.elements.length !== identifiers.length)
|
|
1086
|
+
this.#diagnostic("TSQ214", `Row assignment has ${identifiers.length} targets but ${assignment.value.elements.length} values`, assignment.range);
|
|
1087
|
+
}
|
|
1088
|
+
else if (assignment.value.kind === "subquery") {
|
|
1089
|
+
const resolved = this.#resolveStatement(assignment.value.query, scope, ctes, columns.map((column) => this.#snapshotType(column)));
|
|
1090
|
+
if (resolved.columns.length !== identifiers.length)
|
|
1091
|
+
this.#diagnostic("TSQ214", `Row assignment has ${identifiers.length} targets but subquery returns ${resolved.columns.length} columns`, assignment.range);
|
|
1092
|
+
this.#validateWriteColumns(columns, resolved.columns, assignment.range, "row assignment");
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
this.#resolveExpression(assignment.value, scope, ctes);
|
|
1096
|
+
this.#diagnostic("TSQ227", "Multi-column assignment requires a row or scalar subquery", assignment.range);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
#returningScope(scope, table, aliases, items, operation) {
|
|
1101
|
+
if (aliases !== undefined)
|
|
1102
|
+
this.#requireServerMajor(18, "RETURNING OLD/NEW aliases", aliases.range);
|
|
1103
|
+
if (aliases === undefined &&
|
|
1104
|
+
items.some(({ expression }) => this.#referencesDefaultReturningRow(expression, scope))) {
|
|
1105
|
+
this.#requireServerMajor(18, "RETURNING OLD/NEW rows", items[0].range);
|
|
1106
|
+
}
|
|
1107
|
+
if (items.length === 0)
|
|
1108
|
+
return scope;
|
|
1109
|
+
if (table === undefined || (aliases === undefined && (this.#serverMajor ?? 0) < 18))
|
|
1110
|
+
return scope;
|
|
1111
|
+
const oldAlias = aliases?.old === undefined ? "old" : sqlName(aliases.old);
|
|
1112
|
+
const newAlias = aliases?.new === undefined ? "new" : sqlName(aliases.new);
|
|
1113
|
+
const hiddenOld = aliases !== undefined && aliases.old !== undefined;
|
|
1114
|
+
const hiddenNew = aliases !== undefined && aliases.new !== undefined;
|
|
1115
|
+
if (oldAlias === newAlias)
|
|
1116
|
+
this.#diagnostic("TSQ108", `RETURNING OLD and NEW rows cannot share alias ${oldAlias}`, aliases?.range ?? items[0].range);
|
|
1117
|
+
for (const alias of [oldAlias, newAlias]) {
|
|
1118
|
+
if (scope.relations.some((relation) => relation.alias === alias))
|
|
1119
|
+
this.#diagnostic("TSQ108", `RETURNING row alias ${alias} conflicts with an existing relation`, aliases?.range ?? items[0].range);
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
...scope,
|
|
1123
|
+
relations: [
|
|
1124
|
+
...scope.relations,
|
|
1125
|
+
...(!hiddenOld || aliases?.old !== undefined
|
|
1126
|
+
? [{ alias: oldAlias, table, nullable: operation === "insert" || operation === "merge", qualifiedOnly: true }]
|
|
1127
|
+
: []),
|
|
1128
|
+
...(!hiddenNew || aliases?.new !== undefined
|
|
1129
|
+
? [{ alias: newAlias, table, nullable: operation === "delete" || operation === "merge", qualifiedOnly: true }]
|
|
1130
|
+
: []),
|
|
1131
|
+
],
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
#referencesDefaultReturningRow(expression, scope) {
|
|
1135
|
+
if (expression.kind === "column" &&
|
|
1136
|
+
expression.relation !== undefined &&
|
|
1137
|
+
(sqlName(expression.relation) === "old" || sqlName(expression.relation) === "new") &&
|
|
1138
|
+
!scope.relations.some(({ alias }) => alias === sqlName(expression.relation)))
|
|
1139
|
+
return true;
|
|
1140
|
+
if (expression.kind === "star" && expression.relation !== undefined) {
|
|
1141
|
+
const alias = sqlName(expression.relation);
|
|
1142
|
+
return (alias === "old" || alias === "new") && !scope.relations.some((relation) => relation.alias === alias);
|
|
1143
|
+
}
|
|
1144
|
+
return this.#expressionChildren(expression).some((child) => this.#referencesDefaultReturningRow(child, scope));
|
|
1145
|
+
}
|
|
1146
|
+
#requireServerMajor(minimum, feature, range) {
|
|
1147
|
+
if (this.#serverMajor === undefined) {
|
|
1148
|
+
this.#diagnostic("TSQ402", `${feature} requires PostgreSQL server-version evidence`, range);
|
|
1149
|
+
}
|
|
1150
|
+
else if (this.#serverMajor < minimum) {
|
|
1151
|
+
this.#diagnostic("TSQ403", `${feature} requires PostgreSQL ${minimum} or newer`, range);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
236
1154
|
#addJoin(join, scope, ctes) {
|
|
237
1155
|
const previous = [...scope.relations];
|
|
238
1156
|
if (join.kind === "right" || join.kind === "full")
|
|
239
1157
|
for (const relation of previous)
|
|
240
1158
|
relation.nullable = true;
|
|
241
|
-
const
|
|
1159
|
+
const restrictLateral = (join.kind === "right" || join.kind === "full") &&
|
|
1160
|
+
(join.table.kind === "function" ||
|
|
1161
|
+
join.table.kind === "json-table" ||
|
|
1162
|
+
(join.table.kind === "subquery" && join.table.lateral));
|
|
1163
|
+
const relationScope = restrictLateral
|
|
1164
|
+
? {
|
|
1165
|
+
relations: [],
|
|
1166
|
+
usingColumns: new Map(),
|
|
1167
|
+
windows: scope.windows,
|
|
1168
|
+
...(scope.outer === undefined ? {} : { outer: scope.outer }),
|
|
1169
|
+
}
|
|
1170
|
+
: scope;
|
|
1171
|
+
const relation = this.#addRelation(join.table, join.kind === "left" || join.kind === "full", relationScope, ctes);
|
|
1172
|
+
if (restrictLateral && relation !== undefined) {
|
|
1173
|
+
if (scope.relations.some(({ alias }) => alias === relation.alias)) {
|
|
1174
|
+
this.#diagnostic("TSQ108", `Duplicate relation alias ${relation.alias}`, join.table.range);
|
|
1175
|
+
}
|
|
1176
|
+
else
|
|
1177
|
+
scope.relations.push(relation);
|
|
1178
|
+
}
|
|
242
1179
|
if (join.on !== undefined)
|
|
243
1180
|
this.#resolveExpression(join.on, scope, ctes, this.#databaseType("boolean", false));
|
|
244
1181
|
if (join.using !== undefined && relation !== undefined) {
|
|
@@ -269,20 +1206,33 @@ class Resolver {
|
|
|
269
1206
|
#addRelation(reference, nullable, scope, ctes) {
|
|
270
1207
|
let table;
|
|
271
1208
|
let alias;
|
|
272
|
-
if (reference.kind === "
|
|
1209
|
+
if (reference.kind === "function") {
|
|
1210
|
+
table = this.#resolveFunctionTable(reference, scope, ctes);
|
|
1211
|
+
alias = reference.alias === undefined ? sqlName(reference.functions[0].call.name) : sqlName(reference.alias);
|
|
1212
|
+
}
|
|
1213
|
+
else if (reference.kind === "json-table") {
|
|
1214
|
+
table = this.#resolveJsonTable(reference, scope, ctes);
|
|
1215
|
+
alias = reference.alias === undefined ? "json_table" : sqlName(reference.alias);
|
|
1216
|
+
}
|
|
1217
|
+
else if (reference.kind === "subquery") {
|
|
1218
|
+
if (reference.alias === undefined)
|
|
1219
|
+
this.#requireServerMajor(16, "Unaliased derived tables", reference.range);
|
|
273
1220
|
const outer = reference.lateral ? scope : scope.outer;
|
|
274
1221
|
const resolved = this.#resolveStatement(reference.query, outer, ctes);
|
|
275
1222
|
const columns = {};
|
|
276
|
-
|
|
277
|
-
columns[column.name
|
|
278
|
-
|
|
1223
|
+
resolved.columns.forEach((column, index) => {
|
|
1224
|
+
const name = reference.columns[index] === undefined ? column.name : sqlName(reference.columns[index]);
|
|
1225
|
+
columns[name] = {
|
|
1226
|
+
name,
|
|
279
1227
|
databaseType: column.databaseType ?? "unknown",
|
|
280
1228
|
tsType: column.tsType,
|
|
281
1229
|
nullable: column.nullable,
|
|
282
1230
|
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
1231
|
+
});
|
|
1232
|
+
if (reference.columns.length > 0 && reference.columns.length !== resolved.columns.length)
|
|
1233
|
+
this.#diagnostic("TSQ213", "Derived-table column alias list has the wrong arity", reference.range);
|
|
1234
|
+
alias = reference.alias === undefined ? `*derived*${reference.range.start}` : sqlName(reference.alias);
|
|
1235
|
+
table = { name: alias, columns };
|
|
286
1236
|
}
|
|
287
1237
|
else {
|
|
288
1238
|
const requested = sqlName(reference.name);
|
|
@@ -309,26 +1259,398 @@ class Resolver {
|
|
|
309
1259
|
}
|
|
310
1260
|
const relation = { alias, table, nullable };
|
|
311
1261
|
scope.relations.push(relation);
|
|
1262
|
+
if (reference.kind === "table" && reference.sample !== undefined) {
|
|
1263
|
+
const sampleMethod = reference.sample.method.name.toUpperCase();
|
|
1264
|
+
if ((sampleMethod === "SYSTEM" || sampleMethod === "BERNOULLI") && reference.sample.arguments.length !== 1) {
|
|
1265
|
+
this.#diagnostic("TSQ227", `${sampleMethod} TABLESAMPLE requires one percentage argument`, reference.sample.range);
|
|
1266
|
+
}
|
|
1267
|
+
if (reference.sample.repeatable?.kind === "literal" && reference.sample.repeatable.value === null) {
|
|
1268
|
+
this.#diagnostic("TSQ227", "TABLESAMPLE REPEATABLE seed cannot be NULL", reference.sample.repeatable.range);
|
|
1269
|
+
}
|
|
1270
|
+
const sampleScope = {
|
|
1271
|
+
relations: [],
|
|
1272
|
+
usingColumns: new Map(),
|
|
1273
|
+
windows: scope.windows,
|
|
1274
|
+
...(scope.outer === undefined ? {} : { outer: scope.outer }),
|
|
1275
|
+
};
|
|
1276
|
+
for (const argument of reference.sample.arguments) {
|
|
1277
|
+
this.#resolveExpression(argument, sampleScope, ctes, this.#databaseType("numeric", false));
|
|
1278
|
+
}
|
|
1279
|
+
if (reference.sample.repeatable !== undefined) {
|
|
1280
|
+
this.#resolveExpression(reference.sample.repeatable, sampleScope, ctes, this.#databaseType("double precision", false));
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
312
1283
|
return relation;
|
|
313
1284
|
}
|
|
314
|
-
#
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
1285
|
+
#resolveJsonTable(reference, scope, ctes) {
|
|
1286
|
+
this.#requireServerMajor(17, "JSON_TABLE", reference.range);
|
|
1287
|
+
const context = this.#resolveSqlJsonValue(reference.context, scope, ctes, true);
|
|
1288
|
+
let pathValid = reference.path.kind === "literal" && typeof reference.path.value === "string";
|
|
1289
|
+
if (!pathValid) {
|
|
1290
|
+
this.#resolveExpression(reference.path, scope, ctes);
|
|
1291
|
+
this.#diagnostic("TSQ203", "PostgreSQL JSON_TABLE path must be a string constant", reference.path.range);
|
|
1292
|
+
}
|
|
1293
|
+
const passing = reference.passing.map((argument) => this.#resolveSqlJsonValue(argument.value, scope, ctes, false));
|
|
1294
|
+
const topOnErrorValid = reference.onError === undefined || reference.onError.kind === "error" || reference.onError.kind === "empty-array";
|
|
1295
|
+
if (!topOnErrorValid) {
|
|
1296
|
+
this.#diagnostic("TSQ203", "JSON_TABLE allows only ERROR or EMPTY ARRAY ON ERROR", reference.onError.range);
|
|
1297
|
+
this.#resolveJsonQueryBehavior(reference.onError, undefined, scope, ctes);
|
|
1298
|
+
}
|
|
1299
|
+
if (!context.valid || passing.some(({ valid }) => !valid))
|
|
1300
|
+
pathValid = false;
|
|
1301
|
+
const jsonNames = new Set();
|
|
1302
|
+
if (reference.pathName !== undefined)
|
|
1303
|
+
jsonNames.add(sqlName(reference.pathName));
|
|
1304
|
+
this.#validateJsonTableNames(reference.jsonColumns, jsonNames);
|
|
1305
|
+
const resolvedColumns = this.#resolveJsonTableColumns(reference.jsonColumns, scope, ctes, false);
|
|
1306
|
+
reference.columns.forEach((identifier, index) => {
|
|
1307
|
+
const column = resolvedColumns[index];
|
|
1308
|
+
if (column === undefined) {
|
|
1309
|
+
this.#diagnostic("TSQ213", "JSON_TABLE column alias list has more names than outputs", identifier.range);
|
|
321
1310
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
1311
|
+
else
|
|
1312
|
+
column.name = sqlName(identifier);
|
|
1313
|
+
});
|
|
1314
|
+
const columns = {};
|
|
1315
|
+
for (const column of resolvedColumns) {
|
|
1316
|
+
if (columns[column.name] !== undefined) {
|
|
1317
|
+
this.#diagnostic("TSQ105", `Duplicate JSON_TABLE column ${column.name}`, reference.range);
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
columns[column.name] =
|
|
1321
|
+
pathValid && topOnErrorValid
|
|
1322
|
+
? column
|
|
1323
|
+
: { ...column, databaseType: "unknown", tsType: "unknown", nullable: true };
|
|
1324
|
+
}
|
|
1325
|
+
return { name: reference.alias === undefined ? "json_table" : sqlName(reference.alias), columns };
|
|
1326
|
+
}
|
|
1327
|
+
#validateJsonTableNames(definitions, names) {
|
|
1328
|
+
for (const definition of definitions) {
|
|
1329
|
+
const identifier = definition.kind === "nested" ? definition.pathName : definition.name;
|
|
1330
|
+
if (identifier !== undefined) {
|
|
1331
|
+
const name = sqlName(identifier);
|
|
1332
|
+
if (names.has(name)) {
|
|
1333
|
+
this.#diagnostic("TSQ105", `Duplicate JSON_TABLE column or path name ${name}`, identifier.range);
|
|
1334
|
+
}
|
|
1335
|
+
names.add(name);
|
|
1336
|
+
}
|
|
1337
|
+
if (definition.kind === "nested")
|
|
1338
|
+
this.#validateJsonTableNames(definition.columns, names);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
#resolveJsonTableColumns(definitions, scope, ctes, nested) {
|
|
1342
|
+
return definitions.flatMap((definition) => {
|
|
1343
|
+
if (definition.kind === "nested") {
|
|
1344
|
+
return this.#resolveJsonTableColumns(definition.columns, scope, ctes, true).map((column) => ({
|
|
1345
|
+
...column,
|
|
1346
|
+
nullable: true,
|
|
1347
|
+
}));
|
|
1348
|
+
}
|
|
1349
|
+
if (definition.kind === "ordinality") {
|
|
1350
|
+
return [{ name: sqlName(definition.name), databaseType: "integer", tsType: "number", nullable: nested }];
|
|
1351
|
+
}
|
|
1352
|
+
const requestedType = definition.databaseType.name;
|
|
1353
|
+
const known = isKnownPostgresType(requestedType, this.#schema);
|
|
1354
|
+
if (!known) {
|
|
1355
|
+
this.#diagnostic("TSQ106", `Invalid or unknown PostgreSQL JSON_TABLE column type ${requestedType}`, definition.databaseType.range);
|
|
1356
|
+
}
|
|
1357
|
+
const canonical = known ? postgresCanonicalType(requestedType, this.#schema) : undefined;
|
|
1358
|
+
let valid = known;
|
|
1359
|
+
if (definition.kind === "exists") {
|
|
1360
|
+
if (canonical !== undefined && !postgresCanCoerce("boolean", canonical, "explicit", this.#schema)) {
|
|
1361
|
+
this.#diagnostic("TSQ203", `JSON_TABLE EXISTS result cannot be coerced to ${requestedType}`, definition.databaseType.range);
|
|
1362
|
+
valid = false;
|
|
1363
|
+
}
|
|
1364
|
+
if (definition.onError !== undefined &&
|
|
1365
|
+
!["error", "true", "false", "unknown"].includes(definition.onError.kind)) {
|
|
1366
|
+
this.#diagnostic("TSQ203", "JSON_TABLE EXISTS allows only ERROR, TRUE, FALSE, or UNKNOWN ON ERROR", definition.onError.range);
|
|
1367
|
+
this.#resolveJsonQueryBehavior(definition.onError, undefined, scope, ctes);
|
|
1368
|
+
valid = false;
|
|
1369
|
+
}
|
|
1370
|
+
return [
|
|
1371
|
+
{
|
|
1372
|
+
name: sqlName(definition.name),
|
|
1373
|
+
databaseType: valid ? canonical : "unknown",
|
|
1374
|
+
tsType: valid ? mapPostgresType(canonical, this.#policy, this.#schema) : "unknown",
|
|
1375
|
+
nullable: nested || definition.onError?.kind === "unknown",
|
|
1376
|
+
},
|
|
1377
|
+
];
|
|
1378
|
+
}
|
|
1379
|
+
if (definition.format !== undefined && canonical !== undefined) {
|
|
1380
|
+
const mapping = postgresCatalogTypeMapping(canonical, this.#schema);
|
|
1381
|
+
if (mapping !== "string" && mapping !== "bytes" && mapping !== "json") {
|
|
1382
|
+
this.#diagnostic("TSQ203", `JSON_TABLE FORMAT JSON requires a string, bytea, json, or jsonb column type, received ${requestedType}`, definition.format.range);
|
|
1383
|
+
valid = false;
|
|
1384
|
+
}
|
|
1385
|
+
if (definition.format.encoding !== undefined) {
|
|
1386
|
+
if (definition.format.encoding !== "UTF8") {
|
|
1387
|
+
this.#diagnostic("TSQ203", `PostgreSQL JSON ENCODING supports UTF8, received ${definition.format.encoding}`, definition.format.range);
|
|
1388
|
+
valid = false;
|
|
1389
|
+
}
|
|
1390
|
+
if (canonical !== "bytea") {
|
|
1391
|
+
this.#diagnostic("TSQ203", `JSON ENCODING requires bytea output, received ${requestedType}`, definition.format.range);
|
|
1392
|
+
valid = false;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (definition.quotes === "omit" && definition.wrapper !== undefined && definition.wrapper !== "without") {
|
|
1397
|
+
this.#diagnostic("TSQ203", "JSON_TABLE QUOTES behavior cannot be combined with WITH WRAPPER", definition.range);
|
|
1398
|
+
valid = false;
|
|
1399
|
+
}
|
|
1400
|
+
for (const behavior of [definition.onEmpty, definition.onError]) {
|
|
1401
|
+
if (behavior !== undefined && ["true", "false", "unknown"].includes(behavior.kind)) {
|
|
1402
|
+
this.#diagnostic("TSQ203", "JSON_TABLE value columns do not allow TRUE, FALSE, or UNKNOWN behavior", behavior.range);
|
|
1403
|
+
valid = false;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const expected = canonical === undefined ? undefined : this.#databaseType(canonical, true);
|
|
1407
|
+
const allowCollections = definition.format !== undefined || definition.wrapper !== undefined || definition.quotes !== undefined;
|
|
1408
|
+
const onEmpty = this.#resolveJsonQueryBehavior(definition.onEmpty, expected, scope, ctes, allowCollections, "JSON_TABLE scalar columns");
|
|
1409
|
+
const onError = this.#resolveJsonQueryBehavior(definition.onError, expected, scope, ctes, allowCollections, "JSON_TABLE scalar columns");
|
|
1410
|
+
valid &&= onEmpty.valid && onError.valid;
|
|
1411
|
+
return [
|
|
1412
|
+
{
|
|
1413
|
+
name: sqlName(definition.name),
|
|
1414
|
+
databaseType: valid ? canonical : "unknown",
|
|
1415
|
+
tsType: valid ? mapPostgresType(canonical, this.#policy, this.#schema) : "unknown",
|
|
1416
|
+
// Scalar columns convert JSON null to SQL NULL. Formatted columns
|
|
1417
|
+
// preserve JSON null and are nullable only through their behaviors.
|
|
1418
|
+
nullable: nested || !allowCollections || onEmpty.nullable || onError.nullable,
|
|
1419
|
+
},
|
|
1420
|
+
];
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
#resolveFunctionTable(reference, scope, ctes) {
|
|
1424
|
+
const outerDefinitions = reference.columns.some(({ databaseType }) => databaseType !== undefined);
|
|
1425
|
+
const directDefinitions = reference.functions.length === 1 &&
|
|
1426
|
+
reference.columns.length > 0 &&
|
|
1427
|
+
reference.columns.every(({ databaseType }) => databaseType !== undefined);
|
|
1428
|
+
if (outerDefinitions &&
|
|
1429
|
+
(reference.withOrdinality || (reference.rowsFrom === true && reference.functions.length > 1))) {
|
|
1430
|
+
this.#diagnostic("TSQ227", "An outer record definition list requires one function and cannot be combined with WITH ORDINALITY", reference.range);
|
|
1431
|
+
}
|
|
1432
|
+
const resolvedColumns = reference.functions.flatMap((item) => this.#resolveTableFunctionItem(directDefinitions ? { ...item, columns: reference.columns } : item, scope, ctes));
|
|
1433
|
+
if (reference.functions.length > 1) {
|
|
1434
|
+
for (const column of resolvedColumns)
|
|
1435
|
+
column.nullable = true;
|
|
1436
|
+
}
|
|
1437
|
+
if (reference.withOrdinality) {
|
|
1438
|
+
resolvedColumns.push({
|
|
1439
|
+
name: "ordinality",
|
|
1440
|
+
databaseType: "bigint",
|
|
1441
|
+
tsType: this.#policy.bigint,
|
|
1442
|
+
nullable: false,
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
(directDefinitions ? [] : reference.columns).forEach((definition, index) => {
|
|
1446
|
+
const column = resolvedColumns[index];
|
|
1447
|
+
if (column === undefined) {
|
|
1448
|
+
this.#diagnostic("TSQ213", "Table-function column alias list has more names than outputs", definition.range);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
column.name = sqlName(definition.name);
|
|
1452
|
+
if (definition.databaseType !== undefined) {
|
|
1453
|
+
column.databaseType = normalizeDatabaseType(definition.databaseType.name);
|
|
1454
|
+
column.tsType = mapPostgresType(definition.databaseType.name, this.#policy, this.#schema);
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1457
|
+
const columns = {};
|
|
1458
|
+
for (const column of resolvedColumns) {
|
|
1459
|
+
if (columns[column.name] !== undefined) {
|
|
1460
|
+
this.#diagnostic("TSQ105", `Duplicate table-function column ${column.name}`, reference.range);
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
columns[column.name] = column;
|
|
1464
|
+
}
|
|
1465
|
+
return {
|
|
1466
|
+
name: reference.alias === undefined ? sqlName(reference.functions[0].call.name) : sqlName(reference.alias),
|
|
1467
|
+
columns,
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
#resolveTableFunctionItem(item, scope, ctes) {
|
|
1471
|
+
const expression = item.call;
|
|
1472
|
+
const argumentsList = expression.arguments.map((argument) => this.#resolveExpression(argument, scope, ctes));
|
|
1473
|
+
if (item.columns.length > 0) {
|
|
1474
|
+
return item.columns.map((definition) => ({
|
|
1475
|
+
name: sqlName(definition.name),
|
|
1476
|
+
databaseType: definition.databaseType?.name ?? "unknown",
|
|
1477
|
+
tsType: definition.databaseType === undefined
|
|
1478
|
+
? "unknown"
|
|
1479
|
+
: mapPostgresType(definition.databaseType.name, this.#policy, this.#schema),
|
|
1480
|
+
nullable: true,
|
|
1481
|
+
}));
|
|
1482
|
+
}
|
|
1483
|
+
const functionName = sqlName(expression.name);
|
|
1484
|
+
const schemaName = expression.schema === undefined ? undefined : sqlName(expression.schema);
|
|
1485
|
+
const routines = this.#index.routineOverloads(functionName, expression.arguments.length, schemaName);
|
|
1486
|
+
const selectedRoutine = this.#selectTableRoutine(routines, argumentsList, expression);
|
|
1487
|
+
if (selectedRoutine !== undefined) {
|
|
1488
|
+
const { routine, argumentTypes, resultType } = selectedRoutine;
|
|
1489
|
+
expression.arguments.forEach((argument, index) => {
|
|
1490
|
+
if (this.#acceptsTypeContext(argument)) {
|
|
1491
|
+
this.#resolveExpression(argument, scope, ctes, this.#databaseType(argumentTypes[index], true));
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
const result = routine.result;
|
|
1495
|
+
if (result.kind === "record" || result.kind === "table") {
|
|
1496
|
+
return Object.values(result.columns)
|
|
1497
|
+
.sort((left, right) => left.position - right.position)
|
|
1498
|
+
.map((column) => ({
|
|
1499
|
+
name: column.name,
|
|
1500
|
+
databaseType: column.databaseType,
|
|
1501
|
+
tsType: column.tsType,
|
|
1502
|
+
nullable: column.nullable,
|
|
1503
|
+
}));
|
|
1504
|
+
}
|
|
1505
|
+
if (result.kind === "scalar" || result.kind === "set") {
|
|
1506
|
+
return [
|
|
1507
|
+
{
|
|
1508
|
+
name: functionName,
|
|
1509
|
+
databaseType: resultType,
|
|
1510
|
+
tsType: resultType === normalizeDatabaseType(result.databaseType)
|
|
1511
|
+
? result.tsType
|
|
1512
|
+
: mapPostgresType(resultType, this.#policy, this.#schema),
|
|
1513
|
+
nullable: result.nullable,
|
|
1514
|
+
},
|
|
1515
|
+
];
|
|
1516
|
+
}
|
|
1517
|
+
this.#diagnostic("TSQ212", `Routine ${expression.name.name} does not return rows`, expression.range);
|
|
1518
|
+
return [];
|
|
1519
|
+
}
|
|
1520
|
+
const rule = postgresCatalogTableRoutineRule(expression.name.name, this.#schema);
|
|
1521
|
+
if (rule === "array-elements") {
|
|
1522
|
+
return argumentsList.map((argument, index) => ({
|
|
1523
|
+
name: index === 0 ? functionName : `${functionName}_${index + 1}`,
|
|
1524
|
+
databaseType: argument.databaseType?.replace(/\[\]$/u, "") ?? "unknown",
|
|
1525
|
+
tsType: this.#arrayElementType(argument.tsType),
|
|
1526
|
+
nullable: true,
|
|
1527
|
+
}));
|
|
1528
|
+
}
|
|
1529
|
+
if (rule === "first-argument") {
|
|
1530
|
+
const value = argumentsList[0] ?? { tsType: "unknown", nullable: true };
|
|
1531
|
+
return [
|
|
1532
|
+
{ name: functionName, databaseType: value.databaseType ?? "unknown", tsType: value.tsType, nullable: false },
|
|
1533
|
+
];
|
|
1534
|
+
}
|
|
1535
|
+
if (rule === "integer") {
|
|
1536
|
+
return [{ name: functionName, databaseType: "integer", tsType: "number", nullable: false }];
|
|
1537
|
+
}
|
|
1538
|
+
if (rule === "json-each" || rule === "json-each-text") {
|
|
1539
|
+
const json = rule === "json-each";
|
|
1540
|
+
return [
|
|
1541
|
+
{ name: "key", databaseType: "text", tsType: "string", nullable: false },
|
|
1542
|
+
{
|
|
1543
|
+
name: "value",
|
|
1544
|
+
databaseType: json ? (expression.name.name.toUpperCase().startsWith("JSONB") ? "jsonb" : "json") : "text",
|
|
1545
|
+
tsType: json ? this.#policy.json : "string",
|
|
1546
|
+
nullable: false,
|
|
1547
|
+
},
|
|
1548
|
+
];
|
|
1549
|
+
}
|
|
1550
|
+
if (rule === "json-array-elements" || rule === "json-array-elements-text") {
|
|
1551
|
+
const json = rule === "json-array-elements";
|
|
1552
|
+
return [
|
|
1553
|
+
{
|
|
1554
|
+
name: "value",
|
|
1555
|
+
databaseType: json ? (expression.name.name.toUpperCase().startsWith("JSONB") ? "jsonb" : "json") : "text",
|
|
1556
|
+
tsType: json ? this.#policy.json : "string",
|
|
1557
|
+
nullable: false,
|
|
1558
|
+
},
|
|
1559
|
+
];
|
|
1560
|
+
}
|
|
1561
|
+
if (rule === "json-path-query") {
|
|
1562
|
+
const signature = this.#jsonPathCallTypes(expression);
|
|
1563
|
+
const selection = signature === undefined
|
|
1564
|
+
? { kind: "none" }
|
|
1565
|
+
: resolvePostgresCandidates([{ value: functionName, argumentTypes: signature, resultType: "jsonb" }], expression.arguments.map((argument, index) => this.#overloadInputType(argument, argumentsList[index])), this.#schema);
|
|
1566
|
+
if (signature === undefined || selection.kind !== "selected") {
|
|
1567
|
+
this.#diagnostic("TSQ202", `No overload of ${expression.name.name} accepts these argument types`, expression.range, "Use jsonb, jsonpath, optional jsonb variables, and an optional boolean silent flag.", "warning");
|
|
1568
|
+
return [];
|
|
1569
|
+
}
|
|
1570
|
+
expression.arguments.forEach((argument, index) => {
|
|
1571
|
+
if (this.#acceptsTypeContext(argument)) {
|
|
1572
|
+
this.#resolveExpression(argument, scope, ctes, this.#databaseType(signature[index], true));
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
return [
|
|
1576
|
+
{
|
|
1577
|
+
name: functionName,
|
|
1578
|
+
databaseType: "jsonb",
|
|
1579
|
+
tsType: this.#policy.json,
|
|
1580
|
+
nullable: false,
|
|
1581
|
+
},
|
|
1582
|
+
];
|
|
1583
|
+
}
|
|
1584
|
+
if (rule === "record") {
|
|
1585
|
+
this.#diagnostic("TSQ213", `Record-returning routine ${expression.name.name} requires a typed column definition list`, expression.range);
|
|
1586
|
+
return [];
|
|
1587
|
+
}
|
|
1588
|
+
const candidates = this.#index.functions(functionName, expression.arguments.length, schemaName);
|
|
1589
|
+
if (candidates.length === 1) {
|
|
1590
|
+
const type = this.#functionType(candidates[0]);
|
|
1591
|
+
return [
|
|
1592
|
+
{
|
|
1593
|
+
name: functionName,
|
|
1594
|
+
databaseType: type.databaseType ?? "unknown",
|
|
1595
|
+
tsType: type.tsType,
|
|
1596
|
+
nullable: type.nullable,
|
|
1597
|
+
},
|
|
1598
|
+
];
|
|
1599
|
+
}
|
|
1600
|
+
this.#diagnostic("TSQ202", `Unknown table function ${expression.name.name}`, expression.range, undefined, "warning");
|
|
1601
|
+
return [{ name: functionName, databaseType: "unknown", tsType: "unknown", nullable: true }];
|
|
1602
|
+
}
|
|
1603
|
+
#selectTableRoutine(candidates, argumentsList, expression) {
|
|
1604
|
+
const result = resolvePostgresCandidates(candidates.flatMap((candidate) => {
|
|
1605
|
+
const routineResult = candidate.result;
|
|
1606
|
+
if (routineResult.kind === "void" || routineResult.kind === "command")
|
|
1607
|
+
return [];
|
|
1608
|
+
const argumentTypes = this.#routineCallTypes(candidate, expression);
|
|
1609
|
+
if (argumentTypes === undefined)
|
|
1610
|
+
return [];
|
|
1611
|
+
return [
|
|
1612
|
+
{
|
|
1613
|
+
value: candidate,
|
|
1614
|
+
argumentTypes,
|
|
1615
|
+
resultType: routineResult.kind === "scalar" || routineResult.kind === "set"
|
|
1616
|
+
? routineResult.databaseType
|
|
1617
|
+
: routineResult.kind,
|
|
1618
|
+
},
|
|
1619
|
+
];
|
|
1620
|
+
}), expression.arguments.map((argument, index) => this.#overloadInputType(argument, argumentsList[index])), this.#schema);
|
|
1621
|
+
if (result.kind === "selected") {
|
|
1622
|
+
return { routine: result.candidate, argumentTypes: result.argumentTypes, resultType: result.resultType };
|
|
1623
|
+
}
|
|
1624
|
+
if (result.kind === "ambiguous") {
|
|
1625
|
+
this.#diagnostic("TSQ204", `Ambiguous overloaded table function ${expression.name.name}`, expression.range, "Cast arguments to select a specific overload.");
|
|
1626
|
+
}
|
|
1627
|
+
else if (candidates.length > 0) {
|
|
1628
|
+
this.#diagnostic("TSQ202", `No table-function overload of ${expression.name.name} accepts these argument types`, expression.range, "Cast arguments to a supported overload.", "warning");
|
|
1629
|
+
}
|
|
1630
|
+
return undefined;
|
|
1631
|
+
}
|
|
1632
|
+
#arrayElementType(type) {
|
|
1633
|
+
const match = /^readonly \((.*)\)\[\]$/u.exec(type);
|
|
1634
|
+
return match?.[1] ?? "unknown";
|
|
1635
|
+
}
|
|
1636
|
+
#resolveItems(items, scope, ctes, expectedOutput) {
|
|
1637
|
+
const columns = [];
|
|
1638
|
+
const names = new Set();
|
|
1639
|
+
const add = (column) => {
|
|
1640
|
+
if (names.has(column.name)) {
|
|
1641
|
+
this.#diagnostic("TSQ105", `Duplicate output property ${column.name}`, column.range, "Give one expression a unique alias.");
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
names.add(column.name);
|
|
1645
|
+
columns.push(column);
|
|
1646
|
+
};
|
|
1647
|
+
for (const item of items) {
|
|
1648
|
+
if (item.expression.kind === "star") {
|
|
1649
|
+
const star = item.expression;
|
|
1650
|
+
const relations = star.relation === undefined
|
|
1651
|
+
? scope.relations.filter((relation) => relation.qualifiedOnly !== true)
|
|
1652
|
+
: scope.relations.filter((relation) => relation.alias === sqlName(star.relation));
|
|
1653
|
+
if (relations.length === 0) {
|
|
332
1654
|
this.#diagnostic("TSQ103", star.relation === undefined
|
|
333
1655
|
? "SELECT * requires a FROM relation"
|
|
334
1656
|
: `Unknown relation alias ${star.relation.name}`, star.range);
|
|
@@ -348,7 +1670,7 @@ class Resolver {
|
|
|
348
1670
|
}
|
|
349
1671
|
continue;
|
|
350
1672
|
}
|
|
351
|
-
const type = this.#resolveExpression(item.expression, scope, ctes);
|
|
1673
|
+
const type = this.#resolveExpression(item.expression, scope, ctes, expectedOutput?.[columns.length]);
|
|
352
1674
|
const inferredName = this.#outputName(item.expression);
|
|
353
1675
|
const name = item.alias === undefined ? inferredName : sqlName(item.alias);
|
|
354
1676
|
if (name === undefined) {
|
|
@@ -365,6 +1687,34 @@ class Resolver {
|
|
|
365
1687
|
return sqlName(expression.column);
|
|
366
1688
|
if (expression.kind === "cast")
|
|
367
1689
|
return this.#outputName(expression.expression);
|
|
1690
|
+
if (expression.kind === "subscript")
|
|
1691
|
+
return this.#outputName(expression.expression);
|
|
1692
|
+
if (expression.kind === "field-access")
|
|
1693
|
+
return sqlName(expression.field);
|
|
1694
|
+
if (expression.kind === "collate")
|
|
1695
|
+
return this.#outputName(expression.expression);
|
|
1696
|
+
if (expression.kind === "at-time-zone")
|
|
1697
|
+
return "timezone";
|
|
1698
|
+
if (expression.kind === "json-object")
|
|
1699
|
+
return "json_object";
|
|
1700
|
+
if (expression.kind === "json-array")
|
|
1701
|
+
return "json_array";
|
|
1702
|
+
if (expression.kind === "json-object-aggregate")
|
|
1703
|
+
return "json_objectagg";
|
|
1704
|
+
if (expression.kind === "json-array-aggregate")
|
|
1705
|
+
return "json_arrayagg";
|
|
1706
|
+
if (expression.kind === "json-parse")
|
|
1707
|
+
return "json";
|
|
1708
|
+
if (expression.kind === "json-scalar")
|
|
1709
|
+
return "json_scalar";
|
|
1710
|
+
if (expression.kind === "json-serialize")
|
|
1711
|
+
return "json_serialize";
|
|
1712
|
+
if (expression.kind === "json-exists")
|
|
1713
|
+
return "json_exists";
|
|
1714
|
+
if (expression.kind === "json-query")
|
|
1715
|
+
return "json_query";
|
|
1716
|
+
if (expression.kind === "json-value")
|
|
1717
|
+
return "json_value";
|
|
368
1718
|
if (expression.kind === "call")
|
|
369
1719
|
return sqlName(expression.name);
|
|
370
1720
|
if (expression.kind === "case")
|
|
@@ -396,15 +1746,17 @@ class Resolver {
|
|
|
396
1746
|
case "star":
|
|
397
1747
|
return { tsType: "unknown", nullable: false };
|
|
398
1748
|
case "array": {
|
|
399
|
-
const
|
|
1749
|
+
const expectedElement = expected?.databaseType === undefined ? undefined : postgresElementType(expected.databaseType, this.#schema);
|
|
1750
|
+
const expectedElementType = expectedElement === undefined ? undefined : this.#databaseType(expectedElement, expected?.nullable ?? true);
|
|
1751
|
+
const elements = expression.elements.map((element) => this.#resolveExpression(element, scope, ctes, expectedElementType));
|
|
400
1752
|
const elementType = unionTypeLiterals(elements.map((element) => element.tsType));
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
1753
|
+
const databaseType = (elements.length === 0
|
|
1754
|
+
? undefined
|
|
1755
|
+
: postgresCommonType(elements.map((element) => element.databaseType), this.#schema)) ?? expectedElement;
|
|
404
1756
|
return {
|
|
405
1757
|
tsType: `readonly (${elementType})[]`,
|
|
406
1758
|
nullable: false,
|
|
407
|
-
...(
|
|
1759
|
+
...(databaseType === undefined ? {} : { databaseType: `${databaseType}[]` }),
|
|
408
1760
|
};
|
|
409
1761
|
}
|
|
410
1762
|
case "row": {
|
|
@@ -414,26 +1766,386 @@ class Resolver {
|
|
|
414
1766
|
nullable: false,
|
|
415
1767
|
};
|
|
416
1768
|
}
|
|
1769
|
+
case "subscript": {
|
|
1770
|
+
const source = this.#resolveExpression(expression.expression, scope, ctes);
|
|
1771
|
+
const expectedIndex = this.#databaseType("integer", false);
|
|
1772
|
+
const bounds = [expression.index, expression.lower, expression.upper]
|
|
1773
|
+
.filter((bound) => bound !== undefined)
|
|
1774
|
+
.map((bound) => ({
|
|
1775
|
+
expression: bound,
|
|
1776
|
+
resolved: this.#resolveExpression(bound, scope, ctes, expectedIndex),
|
|
1777
|
+
}));
|
|
1778
|
+
for (const bound of bounds) {
|
|
1779
|
+
if (bound.resolved.databaseType !== undefined &&
|
|
1780
|
+
!postgresCanCoerce(bound.resolved.databaseType, "integer", "implicit", this.#schema)) {
|
|
1781
|
+
this.#diagnostic("TSQ203", `Array subscript must resolve to integer, received ${bound.resolved.databaseType}`, bound.expression.range);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (expression.slice) {
|
|
1785
|
+
if (source.databaseType === undefined ||
|
|
1786
|
+
postgresElementType(source.databaseType, this.#schema) === undefined) {
|
|
1787
|
+
this.#diagnostic("TSQ203", `Array slicing requires an array value, received ${source.databaseType ?? source.tsType}`, expression.range);
|
|
1788
|
+
return { tsType: "unknown", nullable: true };
|
|
1789
|
+
}
|
|
1790
|
+
return {
|
|
1791
|
+
...source,
|
|
1792
|
+
nullable: source.nullable || bounds.some((bound) => bound.resolved.nullable),
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
const elementType = source.databaseType === "point"
|
|
1796
|
+
? "double precision"
|
|
1797
|
+
: source.databaseType === "box" || source.databaseType === "lseg"
|
|
1798
|
+
? "point"
|
|
1799
|
+
: source.databaseType === undefined
|
|
1800
|
+
? undefined
|
|
1801
|
+
: postgresElementType(source.databaseType, this.#schema);
|
|
1802
|
+
if (elementType === undefined) {
|
|
1803
|
+
this.#diagnostic("TSQ203", `Subscripting requires an array or subscriptable geometric value, received ${source.databaseType ?? source.tsType}`, expression.range);
|
|
1804
|
+
return { tsType: "unknown", nullable: true };
|
|
1805
|
+
}
|
|
1806
|
+
return {
|
|
1807
|
+
tsType: mapPostgresType(elementType, this.#policy, this.#schema),
|
|
1808
|
+
nullable: true,
|
|
1809
|
+
databaseType: elementType,
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
case "field-access": {
|
|
1813
|
+
const source = this.#resolveExpression(expression.expression, scope, ctes);
|
|
1814
|
+
const canonical = source.databaseType === undefined ? undefined : postgresCanonicalType(source.databaseType, this.#schema);
|
|
1815
|
+
const composite = canonical === undefined || this.#schema.formatVersion !== 2
|
|
1816
|
+
? undefined
|
|
1817
|
+
: Object.values(this.#schema.types).find((type) => {
|
|
1818
|
+
if (type.kind !== "composite")
|
|
1819
|
+
return false;
|
|
1820
|
+
const qualified = type.schema === undefined ? type.name : `${type.schema}.${type.name}`;
|
|
1821
|
+
return [type.databaseType, type.identity, qualified].some((identity) => normalizeDatabaseType(identity) === normalizeDatabaseType(canonical));
|
|
1822
|
+
});
|
|
1823
|
+
if (composite === undefined) {
|
|
1824
|
+
this.#diagnostic("TSQ203", `Field selection requires snapshot composite evidence for ${source.databaseType ?? source.tsType}`, expression.range);
|
|
1825
|
+
return { tsType: "unknown", nullable: true };
|
|
1826
|
+
}
|
|
1827
|
+
const fieldName = sqlName(expression.field);
|
|
1828
|
+
const field = composite.fields.find((candidate) => expression.field.quoted ? candidate.name === fieldName : candidate.name.toLowerCase() === fieldName);
|
|
1829
|
+
if (field === undefined) {
|
|
1830
|
+
this.#diagnostic("TSQ101", `Unknown field ${expression.field.name} on composite ${composite.databaseType}`, expression.field.range, suggestion(fieldName, composite.fields.map(({ name }) => name)));
|
|
1831
|
+
return { tsType: "unknown", nullable: true };
|
|
1832
|
+
}
|
|
1833
|
+
return {
|
|
1834
|
+
tsType: field.tsType,
|
|
1835
|
+
nullable: source.nullable || field.nullable,
|
|
1836
|
+
databaseType: field.databaseType,
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
case "collate": {
|
|
1840
|
+
const contextual = expected !== undefined && this.#acceptsTypeContext(expression.expression)
|
|
1841
|
+
? { ...expected, nullable: true }
|
|
1842
|
+
: expected;
|
|
1843
|
+
const source = this.#resolveExpression(expression.expression, scope, ctes, contextual);
|
|
1844
|
+
if (source.databaseType === undefined &&
|
|
1845
|
+
(this.#acceptsTypeContext(expression.expression) ||
|
|
1846
|
+
(expression.expression.kind === "literal" && expression.expression.value === null)))
|
|
1847
|
+
return source;
|
|
1848
|
+
if (source.databaseType === undefined || postgresTypeCategory(source.databaseType, this.#schema) !== "string") {
|
|
1849
|
+
this.#diagnostic("TSQ203", `COLLATE requires a collatable string value, received ${source.databaseType ?? source.tsType}`, expression.range);
|
|
1850
|
+
return { tsType: "unknown", nullable: true };
|
|
1851
|
+
}
|
|
1852
|
+
return source;
|
|
1853
|
+
}
|
|
1854
|
+
case "at-time-zone": {
|
|
1855
|
+
const conversion = expression.local ? "AT LOCAL" : "AT TIME ZONE";
|
|
1856
|
+
if (expression.local)
|
|
1857
|
+
this.#requireServerMajor(17, "AT LOCAL", expression.range);
|
|
1858
|
+
const source = this.#resolveExpression(expression.expression, scope, ctes);
|
|
1859
|
+
const zone = expression.zone === undefined
|
|
1860
|
+
? undefined
|
|
1861
|
+
: this.#resolveExpression(expression.zone, scope, ctes, this.#databaseType("text", true));
|
|
1862
|
+
const sourceType = source.databaseType === undefined ? undefined : postgresCanonicalType(source.databaseType, this.#schema);
|
|
1863
|
+
const zoneType = zone?.databaseType === undefined ? undefined : postgresCanonicalType(zone.databaseType, this.#schema);
|
|
1864
|
+
const validZone = expression.local ||
|
|
1865
|
+
(zoneType !== undefined &&
|
|
1866
|
+
(zoneType === "interval" || postgresCanCoerce(zoneType, "text", "implicit", this.#schema)));
|
|
1867
|
+
const resultType = sourceType === "timestamp"
|
|
1868
|
+
? "timestamptz"
|
|
1869
|
+
: sourceType === "timestamptz"
|
|
1870
|
+
? "timestamp"
|
|
1871
|
+
: sourceType === "timetz"
|
|
1872
|
+
? "timetz"
|
|
1873
|
+
: undefined;
|
|
1874
|
+
if (resultType === undefined) {
|
|
1875
|
+
this.#diagnostic("TSQ203", `${conversion} requires timestamp, timestamp with time zone, or time with time zone; received ${source.databaseType ?? source.tsType}`, expression.expression.range);
|
|
1876
|
+
}
|
|
1877
|
+
if (!validZone) {
|
|
1878
|
+
this.#diagnostic("TSQ203", `${conversion} requires a text or interval zone, received ${zone?.databaseType ?? zone?.tsType ?? "unknown"}`, expression.zone?.range ?? expression.range);
|
|
1879
|
+
}
|
|
1880
|
+
if (resultType === undefined || !validZone)
|
|
1881
|
+
return { tsType: "unknown", nullable: true };
|
|
1882
|
+
return {
|
|
1883
|
+
tsType: mapPostgresType(resultType, this.#policy, this.#schema),
|
|
1884
|
+
nullable: source.nullable || (zone?.nullable ?? false),
|
|
1885
|
+
databaseType: resultType,
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
417
1888
|
case "cast": {
|
|
418
1889
|
const castType = this.#databaseType(expression.databaseType.name, true);
|
|
419
1890
|
const source = this.#resolveExpression(expression.expression, scope, ctes, castType);
|
|
420
1891
|
if (!isKnownPostgresType(expression.databaseType.name, this.#schema)) {
|
|
421
1892
|
this.#diagnostic("TSQ106", `Invalid or unknown PostgreSQL cast type ${expression.databaseType.name}`, expression.databaseType.range);
|
|
422
1893
|
}
|
|
1894
|
+
else if (source.databaseType !== undefined &&
|
|
1895
|
+
!this.#isUnknownCastSource(expression.expression) &&
|
|
1896
|
+
!postgresCanCoerce(source.databaseType, expression.databaseType.name, "explicit", this.#schema)) {
|
|
1897
|
+
this.#diagnostic("TSQ230", `PostgreSQL has no recorded explicit cast from ${source.databaseType} to ${expression.databaseType.name}`, expression.range, "Use a supported intermediate type or add exact extension cast evidence.");
|
|
1898
|
+
}
|
|
423
1899
|
return {
|
|
424
1900
|
tsType: mapPostgresType(expression.databaseType.name, this.#policy, this.#schema),
|
|
425
1901
|
nullable: source.nullable,
|
|
426
|
-
databaseType:
|
|
1902
|
+
databaseType: postgresCanonicalType(expression.databaseType.name, this.#schema),
|
|
427
1903
|
};
|
|
428
1904
|
}
|
|
429
1905
|
case "unary": {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
: operand;
|
|
1906
|
+
let operand = this.#resolveExpression(expression.expression, scope, ctes, expression.operator === "NOT" ? this.#databaseType("boolean", false) : undefined);
|
|
1907
|
+
const operator = resolvePostgresUnaryOperator(expression.operator, this.#overloadInputType(expression.expression, operand), this.#schema);
|
|
1908
|
+
if (operator.kind !== "selected") {
|
|
1909
|
+
this.#diagnostic("TSQ203", `${operator.kind === "ambiguous" ? "Ambiguous" : "No matching"} unary operator ${expression.operator} for ${operand.databaseType ?? operand.tsType}`, expression.range, operator.kind === "ambiguous" ? "Cast the operand to select a specific overload." : undefined);
|
|
1910
|
+
return { tsType: "unknown", nullable: true };
|
|
1911
|
+
}
|
|
1912
|
+
if (this.#acceptsTypeContext(expression.expression)) {
|
|
1913
|
+
operand = this.#resolveExpression(expression.expression, scope, ctes, this.#databaseType(operator.argumentTypes[0], operand.nullable));
|
|
1914
|
+
}
|
|
1915
|
+
return {
|
|
1916
|
+
tsType: mapPostgresType(operator.resultType, this.#policy, this.#schema),
|
|
1917
|
+
nullable: operand.nullable,
|
|
1918
|
+
databaseType: operator.resultType,
|
|
1919
|
+
};
|
|
434
1920
|
}
|
|
435
1921
|
case "binary":
|
|
436
1922
|
return this.#resolveBinary(expression, scope, ctes);
|
|
1923
|
+
case "json-object": {
|
|
1924
|
+
this.#requireServerMajor(16, "JSON_OBJECT constructor", expression.range);
|
|
1925
|
+
let inputsValid = true;
|
|
1926
|
+
for (const entry of expression.entries) {
|
|
1927
|
+
const keyValid = this.#resolveJsonObjectKey(entry.key, scope, ctes, "JSON_OBJECT");
|
|
1928
|
+
inputsValid = keyValid && inputsValid;
|
|
1929
|
+
const valueValid = this.#resolveJsonConstructorValue(entry.value, scope, ctes);
|
|
1930
|
+
inputsValid = valueValid && inputsValid;
|
|
1931
|
+
}
|
|
1932
|
+
const output = this.#resolveJsonConstructorOutput(expression.returning, "JSON_OBJECT", expression.range);
|
|
1933
|
+
return inputsValid && output.valid
|
|
1934
|
+
? output.resolved
|
|
1935
|
+
: { tsType: "unknown", nullable: true, databaseType: "unknown" };
|
|
1936
|
+
}
|
|
1937
|
+
case "json-object-aggregate": {
|
|
1938
|
+
this.#requireServerMajor(16, "JSON_OBJECTAGG aggregate", expression.range);
|
|
1939
|
+
const keyValid = this.#resolveJsonObjectKey(expression.key, scope, ctes, "JSON_OBJECTAGG");
|
|
1940
|
+
const valueValid = this.#resolveJsonAggregateValue(expression.value, scope, ctes);
|
|
1941
|
+
this.#resolveJsonAggregateSuffix(expression, scope, ctes);
|
|
1942
|
+
const output = this.#resolveJsonConstructorOutput(expression.returning, "JSON_OBJECTAGG", expression.range, true);
|
|
1943
|
+
return keyValid && valueValid && output.valid
|
|
1944
|
+
? output.resolved
|
|
1945
|
+
: { tsType: "unknown", nullable: true, databaseType: "unknown" };
|
|
1946
|
+
}
|
|
1947
|
+
case "json-array-aggregate": {
|
|
1948
|
+
this.#requireServerMajor(16, "JSON_ARRAYAGG aggregate", expression.range);
|
|
1949
|
+
const valueValid = this.#resolveJsonAggregateValue(expression.value, scope, ctes);
|
|
1950
|
+
for (const item of expression.orderBy)
|
|
1951
|
+
this.#resolveExpression(item.expression, scope, ctes);
|
|
1952
|
+
this.#resolveJsonAggregateSuffix(expression, scope, ctes);
|
|
1953
|
+
const output = this.#resolveJsonConstructorOutput(expression.returning, "JSON_ARRAYAGG", expression.range, true);
|
|
1954
|
+
return valueValid && output.valid
|
|
1955
|
+
? output.resolved
|
|
1956
|
+
: { tsType: "unknown", nullable: true, databaseType: "unknown" };
|
|
1957
|
+
}
|
|
1958
|
+
case "json-array": {
|
|
1959
|
+
this.#requireServerMajor(16, "JSON_ARRAY constructor", expression.range);
|
|
1960
|
+
let inputsValid = true;
|
|
1961
|
+
for (const value of expression.values) {
|
|
1962
|
+
const valueValid = this.#resolveJsonConstructorValue(value, scope, ctes);
|
|
1963
|
+
inputsValid = valueValid && inputsValid;
|
|
1964
|
+
}
|
|
1965
|
+
if (expression.query !== undefined) {
|
|
1966
|
+
const query = this.#resolveStatement(expression.query, scope, ctes);
|
|
1967
|
+
if (query.columns.length !== 1) {
|
|
1968
|
+
this.#diagnostic("TSQ216", `JSON_ARRAY query returns ${query.columns.length} columns instead of one`, expression.query.range);
|
|
1969
|
+
inputsValid = false;
|
|
1970
|
+
}
|
|
1971
|
+
else if (expression.queryFormat !== undefined) {
|
|
1972
|
+
const formatValid = this.#validateSqlJsonFormat(expression.queryFormat, query.columns[0], "JSON_ARRAY query");
|
|
1973
|
+
inputsValid = formatValid && inputsValid;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
const output = this.#resolveJsonConstructorOutput(expression.returning, "JSON_ARRAY", expression.range);
|
|
1977
|
+
return inputsValid && output.valid
|
|
1978
|
+
? output.resolved
|
|
1979
|
+
: { tsType: "unknown", nullable: true, databaseType: "unknown" };
|
|
1980
|
+
}
|
|
1981
|
+
case "json-parse": {
|
|
1982
|
+
if (this.#serverMajor !== undefined && this.#serverMajor < 17) {
|
|
1983
|
+
if (expression.value.format !== undefined || expression.uniqueKeys) {
|
|
1984
|
+
this.#requireServerMajor(17, "JSON constructor", expression.range);
|
|
1985
|
+
this.#resolveExpression(expression.value.expression, scope, ctes);
|
|
1986
|
+
return { tsType: "unknown", nullable: true, databaseType: "unknown" };
|
|
1987
|
+
}
|
|
1988
|
+
const source = this.#resolveExpression(expression.value.expression, scope, ctes);
|
|
1989
|
+
const nullLiteral = expression.value.expression.kind === "literal" && expression.value.expression.value === null;
|
|
1990
|
+
if (source.databaseType === undefined && !nullLiteral) {
|
|
1991
|
+
this.#diagnostic("TSQ202", "PostgreSQL before 17 cannot resolve this functional json cast input", expression.value.expression.range);
|
|
1992
|
+
return { tsType: "unknown", nullable: true, databaseType: "unknown" };
|
|
1993
|
+
}
|
|
1994
|
+
if (source.databaseType !== undefined &&
|
|
1995
|
+
!this.#isUnknownCastSource(expression.value.expression) &&
|
|
1996
|
+
!postgresCanCoerce(source.databaseType, "json", "explicit", this.#schema)) {
|
|
1997
|
+
this.#diagnostic("TSQ230", `PostgreSQL has no recorded explicit cast from ${source.databaseType} to json`, expression.range);
|
|
1998
|
+
return { tsType: "unknown", nullable: true, databaseType: "unknown" };
|
|
1999
|
+
}
|
|
2000
|
+
return { tsType: this.#policy.json, nullable: source.nullable, databaseType: "json" };
|
|
2001
|
+
}
|
|
2002
|
+
this.#requireServerMajor(17, "JSON constructor", expression.range);
|
|
2003
|
+
const input = this.#resolveJsonStringInput(expression.value, scope, ctes, "JSON constructor");
|
|
2004
|
+
return input.valid
|
|
2005
|
+
? { tsType: this.#policy.json, nullable: input.resolved.nullable, databaseType: "json" }
|
|
2006
|
+
: { tsType: "unknown", nullable: true, databaseType: "unknown" };
|
|
2007
|
+
}
|
|
2008
|
+
case "json-scalar": {
|
|
2009
|
+
this.#requireServerMajor(17, "JSON_SCALAR", expression.range);
|
|
2010
|
+
const input = this.#resolveExpression(expression.expression, scope, ctes, this.#databaseType("text", true));
|
|
2011
|
+
return input.databaseType !== undefined || input.tsType !== "unknown"
|
|
2012
|
+
? { tsType: this.#policy.json, nullable: input.nullable, databaseType: "json" }
|
|
2013
|
+
: { tsType: "unknown", nullable: true, databaseType: "unknown" };
|
|
2014
|
+
}
|
|
2015
|
+
case "json-serialize": {
|
|
2016
|
+
this.#requireServerMajor(17, "JSON_SERIALIZE", expression.range);
|
|
2017
|
+
const input = this.#resolveJsonStringInput(expression.value, scope, ctes, "JSON_SERIALIZE");
|
|
2018
|
+
const output = this.#resolveJsonSerializeOutput(expression.returning, expression.range, input.resolved.nullable);
|
|
2019
|
+
return input.valid && output.valid
|
|
2020
|
+
? output.resolved
|
|
2021
|
+
: { tsType: "unknown", nullable: true, databaseType: "unknown" };
|
|
2022
|
+
}
|
|
2023
|
+
case "json-is": {
|
|
2024
|
+
this.#requireServerMajor(16, "IS JSON predicate", expression.range);
|
|
2025
|
+
const input = this.#resolveJsonStringInput({ expression: expression.expression, range: expression.expression.range }, scope, ctes, "IS JSON predicate");
|
|
2026
|
+
return input.valid
|
|
2027
|
+
? { tsType: "boolean", nullable: input.resolved.nullable, databaseType: "boolean" }
|
|
2028
|
+
: { tsType: "unknown", nullable: true, databaseType: "unknown" };
|
|
2029
|
+
}
|
|
2030
|
+
case "json-exists": {
|
|
2031
|
+
this.#requireServerMajor(17, "JSON_EXISTS", expression.range);
|
|
2032
|
+
const context = this.#resolveSqlJsonValue(expression.context, scope, ctes, true);
|
|
2033
|
+
const path = this.#resolveExpression(expression.path, scope, ctes, this.#databaseType("jsonpath", true));
|
|
2034
|
+
const pathType = path.databaseType === undefined ? undefined : postgresCanonicalType(path.databaseType, this.#schema);
|
|
2035
|
+
const pathValid = pathType === "jsonpath" ||
|
|
2036
|
+
(pathType !== undefined && postgresTypeCategory(pathType, this.#schema) === "string") ||
|
|
2037
|
+
(expression.path.kind === "literal" && expression.path.value === null);
|
|
2038
|
+
if (!pathValid) {
|
|
2039
|
+
this.#diagnostic("TSQ203", `JSON_EXISTS path must resolve to jsonpath or a character string, received ${path.databaseType ?? path.tsType}`, expression.path.range);
|
|
2040
|
+
}
|
|
2041
|
+
const passing = expression.passing.map(({ value }) => this.#resolveSqlJsonValue(value, scope, ctes, false));
|
|
2042
|
+
if (!context.valid || !pathValid || passing.some(({ valid }) => !valid)) {
|
|
2043
|
+
return { tsType: "unknown", nullable: true };
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
tsType: "boolean",
|
|
2047
|
+
nullable: context.resolved.nullable || path.nullable || expression.onError === "unknown",
|
|
2048
|
+
databaseType: "boolean",
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
case "json-query": {
|
|
2052
|
+
this.#requireServerMajor(17, "JSON_QUERY", expression.range);
|
|
2053
|
+
const context = this.#resolveSqlJsonValue(expression.context, scope, ctes, true);
|
|
2054
|
+
const path = this.#resolveExpression(expression.path, scope, ctes, this.#databaseType("jsonpath", true));
|
|
2055
|
+
const pathType = path.databaseType === undefined ? undefined : postgresCanonicalType(path.databaseType, this.#schema);
|
|
2056
|
+
const pathValid = pathType === "jsonpath" ||
|
|
2057
|
+
(pathType !== undefined && postgresTypeCategory(pathType, this.#schema) === "string") ||
|
|
2058
|
+
(expression.path.kind === "literal" && expression.path.value === null);
|
|
2059
|
+
if (!pathValid) {
|
|
2060
|
+
this.#diagnostic("TSQ203", `JSON_QUERY path must resolve to jsonpath or a character string, received ${path.databaseType ?? path.tsType}`, expression.path.range);
|
|
2061
|
+
}
|
|
2062
|
+
const passing = expression.passing.map(({ value }) => this.#resolveSqlJsonValue(value, scope, ctes, false));
|
|
2063
|
+
const outputType = expression.returning?.databaseType.name ?? "jsonb";
|
|
2064
|
+
const outputKnown = isKnownPostgresType(outputType, this.#schema);
|
|
2065
|
+
if (!outputKnown) {
|
|
2066
|
+
this.#diagnostic("TSQ106", `Invalid or unknown PostgreSQL JSON_QUERY return type ${outputType}`, expression.returning?.databaseType.range ?? expression.range);
|
|
2067
|
+
}
|
|
2068
|
+
const canonicalOutput = outputKnown ? postgresCanonicalType(outputType, this.#schema) : undefined;
|
|
2069
|
+
let outputValid = outputKnown;
|
|
2070
|
+
const outputFormat = expression.returning?.format;
|
|
2071
|
+
if (outputFormat !== undefined) {
|
|
2072
|
+
const mapping = canonicalOutput === undefined ? undefined : postgresCatalogTypeMapping(canonicalOutput, this.#schema);
|
|
2073
|
+
if (mapping !== "string" && mapping !== "bytes" && mapping !== "json") {
|
|
2074
|
+
this.#diagnostic("TSQ203", `JSON_QUERY FORMAT JSON requires a string, bytea, json, or jsonb return type, received ${outputType}`, outputFormat.range);
|
|
2075
|
+
outputValid = false;
|
|
2076
|
+
}
|
|
2077
|
+
if (outputFormat.encoding !== undefined) {
|
|
2078
|
+
if (outputFormat.encoding !== "UTF8") {
|
|
2079
|
+
this.#diagnostic("TSQ203", `PostgreSQL JSON ENCODING supports UTF8, received ${outputFormat.encoding}`, outputFormat.range);
|
|
2080
|
+
outputValid = false;
|
|
2081
|
+
}
|
|
2082
|
+
if (canonicalOutput !== "bytea") {
|
|
2083
|
+
this.#diagnostic("TSQ203", `JSON ENCODING requires bytea output, received ${outputType}`, outputFormat.range);
|
|
2084
|
+
outputValid = false;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
if (expression.quotes === "omit" && expression.wrapper !== undefined && expression.wrapper !== "without") {
|
|
2089
|
+
this.#diagnostic("TSQ203", "JSON_QUERY QUOTES behavior cannot be combined with WITH WRAPPER", expression.range);
|
|
2090
|
+
outputValid = false;
|
|
2091
|
+
}
|
|
2092
|
+
const expectedOutput = canonicalOutput === undefined ? undefined : this.#databaseType(canonicalOutput, true);
|
|
2093
|
+
const onEmpty = this.#resolveJsonQueryBehavior(expression.onEmpty, expectedOutput, scope, ctes);
|
|
2094
|
+
const onError = this.#resolveJsonQueryBehavior(expression.onError, expectedOutput, scope, ctes);
|
|
2095
|
+
if (!context.valid ||
|
|
2096
|
+
!pathValid ||
|
|
2097
|
+
passing.some(({ valid }) => !valid) ||
|
|
2098
|
+
!outputValid ||
|
|
2099
|
+
!onEmpty.valid ||
|
|
2100
|
+
!onError.valid) {
|
|
2101
|
+
return { tsType: "unknown", nullable: true };
|
|
2102
|
+
}
|
|
2103
|
+
return {
|
|
2104
|
+
tsType: mapPostgresType(canonicalOutput, this.#policy, this.#schema),
|
|
2105
|
+
nullable: context.resolved.nullable || path.nullable || onEmpty.nullable || onError.nullable,
|
|
2106
|
+
databaseType: canonicalOutput,
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
case "json-value": {
|
|
2110
|
+
this.#requireServerMajor(17, "JSON_VALUE", expression.range);
|
|
2111
|
+
const context = this.#resolveSqlJsonValue(expression.context, scope, ctes, true);
|
|
2112
|
+
const path = this.#resolveExpression(expression.path, scope, ctes, this.#databaseType("jsonpath", true));
|
|
2113
|
+
const pathType = path.databaseType === undefined ? undefined : postgresCanonicalType(path.databaseType, this.#schema);
|
|
2114
|
+
const pathValid = pathType === "jsonpath" ||
|
|
2115
|
+
(pathType !== undefined && postgresTypeCategory(pathType, this.#schema) === "string") ||
|
|
2116
|
+
(expression.path.kind === "literal" && expression.path.value === null);
|
|
2117
|
+
if (!pathValid) {
|
|
2118
|
+
this.#diagnostic("TSQ203", `JSON_VALUE path must resolve to jsonpath or a character string, received ${path.databaseType ?? path.tsType}`, expression.path.range);
|
|
2119
|
+
}
|
|
2120
|
+
const passing = expression.passing.map(({ value }) => this.#resolveSqlJsonValue(value, scope, ctes, false));
|
|
2121
|
+
const outputType = expression.returning?.databaseType.name ?? "text";
|
|
2122
|
+
const outputKnown = isKnownPostgresType(outputType, this.#schema);
|
|
2123
|
+
if (!outputKnown) {
|
|
2124
|
+
this.#diagnostic("TSQ106", `Invalid or unknown PostgreSQL JSON_VALUE return type ${outputType}`, expression.returning?.databaseType.range ?? expression.range);
|
|
2125
|
+
}
|
|
2126
|
+
const canonicalOutput = outputKnown ? postgresCanonicalType(outputType, this.#schema) : undefined;
|
|
2127
|
+
let outputValid = outputKnown;
|
|
2128
|
+
if (expression.returning?.format !== undefined) {
|
|
2129
|
+
this.#diagnostic("TSQ203", "JSON_VALUE does not allow FORMAT JSON in its RETURNING clause", expression.returning.format.range);
|
|
2130
|
+
outputValid = false;
|
|
2131
|
+
}
|
|
2132
|
+
const expectedOutput = canonicalOutput === undefined ? undefined : this.#databaseType(canonicalOutput, true);
|
|
2133
|
+
const onEmpty = this.#resolveJsonQueryBehavior(expression.onEmpty, expectedOutput, scope, ctes, false);
|
|
2134
|
+
const onError = this.#resolveJsonQueryBehavior(expression.onError, expectedOutput, scope, ctes, false);
|
|
2135
|
+
if (!context.valid ||
|
|
2136
|
+
!pathValid ||
|
|
2137
|
+
passing.some(({ valid }) => !valid) ||
|
|
2138
|
+
!outputValid ||
|
|
2139
|
+
!onEmpty.valid ||
|
|
2140
|
+
!onError.valid) {
|
|
2141
|
+
return { tsType: "unknown", nullable: true };
|
|
2142
|
+
}
|
|
2143
|
+
return {
|
|
2144
|
+
tsType: mapPostgresType(canonicalOutput, this.#policy, this.#schema),
|
|
2145
|
+
nullable: true,
|
|
2146
|
+
databaseType: canonicalOutput,
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
437
2149
|
case "call":
|
|
438
2150
|
return this.#resolveCall(expression, scope, ctes);
|
|
439
2151
|
case "case": {
|
|
@@ -454,7 +2166,7 @@ class Resolver {
|
|
|
454
2166
|
};
|
|
455
2167
|
}
|
|
456
2168
|
case "subquery": {
|
|
457
|
-
const resolved = this.#resolveStatement(expression.query, scope, ctes);
|
|
2169
|
+
const resolved = this.#resolveStatement(expression.query, scope, ctes, expected === undefined ? undefined : [expected]);
|
|
458
2170
|
if (resolved.columns.length !== 1) {
|
|
459
2171
|
this.#diagnostic("TSQ216", `Scalar subquery returns ${resolved.columns.length} columns instead of one`, expression.range);
|
|
460
2172
|
return { tsType: "unknown", nullable: true };
|
|
@@ -470,21 +2182,53 @@ class Resolver {
|
|
|
470
2182
|
this.#resolveStatement(expression.query, scope, ctes);
|
|
471
2183
|
return { tsType: "boolean", nullable: false, databaseType: "boolean" };
|
|
472
2184
|
}
|
|
473
|
-
case "
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (Array.isArray(expression.values)) {
|
|
477
|
-
const values = expression.values.map((value) => this.#resolveExpression(value, scope, ctes, subject));
|
|
478
|
-
nullable ||= values.some((value) => value.nullable);
|
|
2185
|
+
case "quantified-comparison": {
|
|
2186
|
+
if (expression.left.kind === "row" && expression.right.kind === "select") {
|
|
2187
|
+
return this.#resolveQuantifiedRowSubquery(expression, scope, ctes);
|
|
479
2188
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
2189
|
+
let left = this.#resolveExpression(expression.left, scope, ctes);
|
|
2190
|
+
let rightExpression;
|
|
2191
|
+
let rightType;
|
|
2192
|
+
let nullable = left.nullable;
|
|
2193
|
+
if (expression.right.kind === "select") {
|
|
2194
|
+
const resolved = this.#resolveStatement(expression.right, scope, ctes);
|
|
2195
|
+
if (resolved.columns.length !== 1) {
|
|
2196
|
+
this.#diagnostic("TSQ217", `${expression.quantifier.toUpperCase()} subquery returns ${resolved.columns.length} columns instead of one`, expression.range);
|
|
2197
|
+
}
|
|
2198
|
+
rightType = resolved.columns[0]?.databaseType;
|
|
484
2199
|
nullable ||= resolved.columns[0]?.nullable ?? true;
|
|
485
2200
|
}
|
|
2201
|
+
else {
|
|
2202
|
+
rightExpression = this.#resolveExpression(expression.right, scope, ctes);
|
|
2203
|
+
rightType =
|
|
2204
|
+
rightExpression.databaseType === undefined
|
|
2205
|
+
? undefined
|
|
2206
|
+
: postgresElementType(rightExpression.databaseType, this.#schema);
|
|
2207
|
+
if (rightExpression.databaseType !== undefined && rightType === undefined) {
|
|
2208
|
+
this.#diagnostic("TSQ203", `Quantified comparison requires an array, received ${rightExpression.databaseType}`, expression.right.range);
|
|
2209
|
+
return { tsType: "unknown", nullable: true };
|
|
2210
|
+
}
|
|
2211
|
+
nullable = true;
|
|
2212
|
+
}
|
|
2213
|
+
if (expression.right.kind === "select" && rightType === undefined) {
|
|
2214
|
+
this.#diagnostic("TSQ203", "Quantified subquery result type is unknown", expression.right.range);
|
|
2215
|
+
return { tsType: "unknown", nullable: true };
|
|
2216
|
+
}
|
|
2217
|
+
const operator = resolvePostgresOperator(expression.operator, this.#overloadInputType(expression.left, left), rightType, this.#schema);
|
|
2218
|
+
if (operator.kind !== "selected") {
|
|
2219
|
+
this.#diagnostic("TSQ203", `${operator.kind === "ambiguous" ? "Ambiguous" : "No matching"} quantified operator ${expression.operator} for ${left.databaseType ?? left.tsType} and ${rightType ?? rightExpression?.databaseType ?? "unknown collection"}`, expression.range, operator.kind === "ambiguous" ? "Cast the left operand or quantified input." : undefined);
|
|
2220
|
+
return { tsType: "unknown", nullable: true };
|
|
2221
|
+
}
|
|
2222
|
+
if (this.#acceptsTypeContext(expression.left)) {
|
|
2223
|
+
left = this.#resolveExpression(expression.left, scope, ctes, this.#databaseType(operator.argumentTypes[0], left.nullable));
|
|
2224
|
+
}
|
|
2225
|
+
if (expression.right.kind !== "select") {
|
|
2226
|
+
rightExpression = this.#resolveExpression(expression.right, scope, ctes, this.#databaseType(`${operator.argumentTypes[1]}[]`, rightExpression?.nullable ?? true));
|
|
2227
|
+
}
|
|
486
2228
|
return { tsType: "boolean", nullable, databaseType: "boolean" };
|
|
487
2229
|
}
|
|
2230
|
+
case "in":
|
|
2231
|
+
return this.#resolveIn(expression, scope, ctes);
|
|
488
2232
|
case "between": {
|
|
489
2233
|
const subject = this.#resolveExpression(expression.expression, scope, ctes);
|
|
490
2234
|
const lower = this.#resolveExpression(expression.lower, scope, ctes, subject);
|
|
@@ -498,59 +2242,282 @@ class Resolver {
|
|
|
498
2242
|
}
|
|
499
2243
|
}
|
|
500
2244
|
#resolveBinary(expression, scope, ctes) {
|
|
2245
|
+
if (expression.left.kind === "row" && expression.right.kind === "row") {
|
|
2246
|
+
return this.#resolveRowComparison(expression, scope, ctes);
|
|
2247
|
+
}
|
|
2248
|
+
if (expression.left.kind === "row" || expression.right.kind === "row") {
|
|
2249
|
+
this.#resolveExpression(expression.left, scope, ctes);
|
|
2250
|
+
this.#resolveExpression(expression.right, scope, ctes);
|
|
2251
|
+
this.#diagnostic("TSQ203", "Row comparison requires a row constructor on both sides", expression.range);
|
|
2252
|
+
return { tsType: "unknown", nullable: true };
|
|
2253
|
+
}
|
|
501
2254
|
let left;
|
|
502
2255
|
let right;
|
|
503
|
-
if (expression.left
|
|
2256
|
+
if (this.#acceptsTypeContext(expression.left) && !this.#acceptsTypeContext(expression.right)) {
|
|
504
2257
|
right = this.#resolveExpression(expression.right, scope, ctes);
|
|
505
|
-
left = this.#resolveExpression(expression.left, scope, ctes
|
|
2258
|
+
left = this.#resolveExpression(expression.left, scope, ctes);
|
|
506
2259
|
}
|
|
507
2260
|
else {
|
|
508
2261
|
left = this.#resolveExpression(expression.left, scope, ctes);
|
|
509
|
-
right = this.#resolveExpression(expression.right, scope, ctes
|
|
2262
|
+
right = this.#resolveExpression(expression.right, scope, ctes);
|
|
2263
|
+
}
|
|
2264
|
+
if (expression.operator === "IS" || expression.operator === "IS NOT") {
|
|
2265
|
+
return { tsType: "boolean", nullable: false, databaseType: "boolean" };
|
|
510
2266
|
}
|
|
511
|
-
|
|
512
|
-
|
|
2267
|
+
const operator = resolvePostgresOperator(expression.operator, this.#overloadInputType(expression.left, left), this.#overloadInputType(expression.right, right), this.#schema);
|
|
2268
|
+
if (operator.kind === "selected") {
|
|
2269
|
+
const invalidNumericLiteral = [expression.left, expression.right].some((operand, index) => {
|
|
2270
|
+
if (operand.kind !== "literal" || typeof operand.value !== "string")
|
|
2271
|
+
return false;
|
|
2272
|
+
const target = operator.argumentTypes[index];
|
|
2273
|
+
if (!this.#isNumericType(target))
|
|
2274
|
+
return false;
|
|
2275
|
+
const integer = ["smallint", "integer", "bigint"].includes(normalizeDatabaseType(target));
|
|
2276
|
+
return integer
|
|
2277
|
+
? !/^[+-]?\d+$/u.test(operand.value)
|
|
2278
|
+
: !/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/iu.test(operand.value);
|
|
2279
|
+
});
|
|
2280
|
+
if (invalidNumericLiteral) {
|
|
2281
|
+
this.#diagnostic("TSQ203", `A string literal is not valid input for the selected ${expression.operator} numeric overload`, expression.range);
|
|
2282
|
+
return { tsType: "unknown", nullable: true };
|
|
2283
|
+
}
|
|
2284
|
+
if (this.#acceptsTypeContext(expression.left)) {
|
|
2285
|
+
left = this.#resolveExpression(expression.left, scope, ctes, this.#databaseType(operator.argumentTypes[0], right.nullable));
|
|
2286
|
+
}
|
|
2287
|
+
if (this.#acceptsTypeContext(expression.right)) {
|
|
2288
|
+
right = this.#resolveExpression(expression.right, scope, ctes, this.#databaseType(operator.argumentTypes[1], left.nullable));
|
|
2289
|
+
}
|
|
513
2290
|
return {
|
|
514
|
-
tsType:
|
|
515
|
-
nullable:
|
|
516
|
-
|
|
2291
|
+
tsType: mapPostgresType(operator.resultType, this.#policy, this.#schema),
|
|
2292
|
+
nullable: expression.operator.startsWith("IS ")
|
|
2293
|
+
? false
|
|
2294
|
+
: ["->", "->>", "#>", "#>>"].includes(expression.operator) || left.nullable || right.nullable,
|
|
2295
|
+
databaseType: operator.resultType,
|
|
517
2296
|
};
|
|
518
2297
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
...(databaseType === undefined ? {} : { databaseType }),
|
|
530
|
-
};
|
|
2298
|
+
const compositeLeftType = left.databaseType ?? (this.#acceptsTypeContext(expression.left) ? right.databaseType : undefined);
|
|
2299
|
+
const compositeRightType = right.databaseType ?? (this.#acceptsTypeContext(expression.right) ? left.databaseType : undefined);
|
|
2300
|
+
if (compositeLeftType !== undefined &&
|
|
2301
|
+
compositeRightType !== undefined &&
|
|
2302
|
+
this.#compositeTypesComparable(compositeLeftType, compositeRightType, expression.operator)) {
|
|
2303
|
+
if (this.#acceptsTypeContext(expression.left)) {
|
|
2304
|
+
left = this.#resolveExpression(expression.left, scope, ctes, this.#databaseType(compositeLeftType, left.nullable));
|
|
2305
|
+
}
|
|
2306
|
+
if (this.#acceptsTypeContext(expression.right)) {
|
|
2307
|
+
right = this.#resolveExpression(expression.right, scope, ctes, this.#databaseType(compositeRightType, right.nullable));
|
|
531
2308
|
}
|
|
532
|
-
return { tsType: "string", nullable: left.nullable || right.nullable, databaseType: "text" };
|
|
533
|
-
}
|
|
534
|
-
const leftDatabase = left.databaseType === undefined ? undefined : normalizeDatabaseType(left.databaseType);
|
|
535
|
-
const rightDatabase = right.databaseType === undefined ? undefined : normalizeDatabaseType(right.databaseType);
|
|
536
|
-
if (leftDatabase !== undefined &&
|
|
537
|
-
rightDatabase !== undefined &&
|
|
538
|
-
numericTypes.has(leftDatabase) &&
|
|
539
|
-
numericTypes.has(rightDatabase)) {
|
|
540
|
-
const databaseType = [leftDatabase, rightDatabase].some((type) => type === "numeric" || type === "decimal")
|
|
541
|
-
? "numeric"
|
|
542
|
-
: "double precision";
|
|
543
2309
|
return {
|
|
544
|
-
tsType:
|
|
545
|
-
nullable: left.nullable || right.nullable,
|
|
546
|
-
databaseType,
|
|
2310
|
+
tsType: "boolean",
|
|
2311
|
+
nullable: expression.operator.includes("DISTINCT FROM") ? false : left.nullable || right.nullable,
|
|
2312
|
+
databaseType: "boolean",
|
|
547
2313
|
};
|
|
548
2314
|
}
|
|
549
|
-
|
|
550
|
-
return { tsType: "unknown", nullable: left.nullable || right.nullable };
|
|
551
|
-
this.#diagnostic("TSQ203", `Cannot safely infer operator ${expression.operator} for ${leftDatabase ?? left.tsType} and ${rightDatabase ?? right.tsType}`, expression.range);
|
|
2315
|
+
this.#diagnostic("TSQ203", `${operator.kind === "ambiguous" ? "Ambiguous" : "No matching"} operator ${expression.operator} for ${left.databaseType ?? left.tsType} and ${right.databaseType ?? right.tsType}`, expression.range, operator.kind === "ambiguous" ? "Cast an operand to select a specific overload." : undefined);
|
|
552
2316
|
return { tsType: "unknown", nullable: true };
|
|
553
2317
|
}
|
|
2318
|
+
#resolveIn(expression, scope, ctes) {
|
|
2319
|
+
if (expression.expression.kind === "row")
|
|
2320
|
+
return this.#resolveRowIn(expression, scope, ctes);
|
|
2321
|
+
let subject = this.#resolveExpression(expression.expression, scope, ctes);
|
|
2322
|
+
let nullable = subject.nullable;
|
|
2323
|
+
if (Array.isArray(expression.values)) {
|
|
2324
|
+
for (const valueExpression of expression.values) {
|
|
2325
|
+
let value = this.#resolveExpression(valueExpression, scope, ctes, subject);
|
|
2326
|
+
const compared = this.#resolveEqualityPair(expression.expression, subject, valueExpression, value, expression.range, "IN value", scope, ctes);
|
|
2327
|
+
subject = compared.left;
|
|
2328
|
+
value = compared.right;
|
|
2329
|
+
nullable ||= value.nullable;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
else {
|
|
2333
|
+
const resolved = this.#resolveStatement(expression.values, scope, ctes, [subject]);
|
|
2334
|
+
if (resolved.columns.length !== 1) {
|
|
2335
|
+
this.#diagnostic("TSQ217", `IN subquery returns ${resolved.columns.length} columns instead of one`, expression.range);
|
|
2336
|
+
}
|
|
2337
|
+
else {
|
|
2338
|
+
const compared = this.#resolveEqualityPair(expression.expression, subject, undefined, resolved.columns[0], expression.range, "IN subquery", scope, ctes);
|
|
2339
|
+
subject = compared.left;
|
|
2340
|
+
nullable ||= compared.right.nullable;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
return { tsType: "boolean", nullable, databaseType: "boolean" };
|
|
2344
|
+
}
|
|
2345
|
+
#resolveRowIn(expression, scope, ctes) {
|
|
2346
|
+
const row = expression.expression;
|
|
2347
|
+
const leftExpressions = row.elements;
|
|
2348
|
+
const left = leftExpressions.map((element) => this.#resolveExpression(element, scope, ctes));
|
|
2349
|
+
let nullable = left.some((element) => element.nullable);
|
|
2350
|
+
const compare = (rightExpressions, right, range) => {
|
|
2351
|
+
if (left.length !== right.length) {
|
|
2352
|
+
this.#diagnostic("TSQ217", `Row IN comparison requires ${left.length} columns, received ${right.length}`, range);
|
|
2353
|
+
nullable = true;
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
left.forEach((leftType, index) => {
|
|
2357
|
+
const compared = this.#resolveEqualityPair(leftExpressions[index], leftType, rightExpressions[index], right[index], range, `row IN position ${index + 1}`, scope, ctes);
|
|
2358
|
+
left[index] = compared.left;
|
|
2359
|
+
nullable ||= compared.left.nullable || compared.right.nullable;
|
|
2360
|
+
});
|
|
2361
|
+
};
|
|
2362
|
+
if (Array.isArray(expression.values)) {
|
|
2363
|
+
for (const valueExpression of expression.values) {
|
|
2364
|
+
if (valueExpression.kind !== "row") {
|
|
2365
|
+
const value = this.#resolveExpression(valueExpression, scope, ctes);
|
|
2366
|
+
this.#diagnostic("TSQ203", "A row-valued IN list requires row values", valueExpression.range);
|
|
2367
|
+
nullable ||= value.nullable;
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
const right = valueExpression.elements.map((element, index) => this.#resolveExpression(element, scope, ctes, left[index]));
|
|
2371
|
+
compare(valueExpression.elements, right, valueExpression.range);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
else {
|
|
2375
|
+
const resolved = this.#resolveStatement(expression.values, scope, ctes, left);
|
|
2376
|
+
compare(resolved.columns.map(() => undefined), resolved.columns, expression.range);
|
|
2377
|
+
}
|
|
2378
|
+
return { tsType: "boolean", nullable, databaseType: "boolean" };
|
|
2379
|
+
}
|
|
2380
|
+
#resolveEqualityPair(leftExpression, initialLeft, rightExpression, initialRight, range, label, scope, ctes) {
|
|
2381
|
+
let left = initialLeft;
|
|
2382
|
+
let right = initialRight;
|
|
2383
|
+
if ((left.databaseType === undefined && !this.#isUnknownOverloadExpression(leftExpression)) ||
|
|
2384
|
+
(right.databaseType === undefined &&
|
|
2385
|
+
(rightExpression === undefined || !this.#isUnknownOverloadExpression(rightExpression)))) {
|
|
2386
|
+
this.#diagnostic("TSQ203", `No recorded equality type for ${label}: ${left.databaseType ?? left.tsType} and ${right.databaseType ?? right.tsType}`, range);
|
|
2387
|
+
return { left, right };
|
|
2388
|
+
}
|
|
2389
|
+
const operator = resolvePostgresOperator("=", this.#overloadInputType(leftExpression, left), rightExpression === undefined ? right.databaseType : this.#overloadInputType(rightExpression, right), this.#schema);
|
|
2390
|
+
let argumentTypes = operator.kind === "selected" ? operator.argumentTypes : undefined;
|
|
2391
|
+
const compositeLeftType = left.databaseType ?? (this.#acceptsTypeContext(leftExpression) ? right.databaseType : undefined);
|
|
2392
|
+
const compositeRightType = right.databaseType ??
|
|
2393
|
+
(rightExpression !== undefined && this.#acceptsTypeContext(rightExpression) ? left.databaseType : undefined);
|
|
2394
|
+
if (argumentTypes === undefined &&
|
|
2395
|
+
compositeLeftType !== undefined &&
|
|
2396
|
+
compositeRightType !== undefined &&
|
|
2397
|
+
this.#compositeTypesComparable(compositeLeftType, compositeRightType, "=")) {
|
|
2398
|
+
argumentTypes = [compositeLeftType, compositeRightType];
|
|
2399
|
+
}
|
|
2400
|
+
if (argumentTypes === undefined) {
|
|
2401
|
+
this.#diagnostic("TSQ203", `${operator.kind === "ambiguous" ? "Ambiguous" : "No matching"} equality operator for ${label}: ${left.databaseType ?? left.tsType} and ${right.databaseType ?? right.tsType}`, range, operator.kind === "ambiguous" ? "Cast an operand to select a specific equality overload." : undefined);
|
|
2402
|
+
return { left, right };
|
|
2403
|
+
}
|
|
2404
|
+
const invalidNumericLiteral = (candidate, databaseType) => candidate?.kind === "literal" &&
|
|
2405
|
+
typeof candidate.value === "string" &&
|
|
2406
|
+
this.#isNumericType(databaseType) &&
|
|
2407
|
+
!this.#validNumericLiteral(candidate.value, databaseType);
|
|
2408
|
+
if (invalidNumericLiteral(leftExpression, argumentTypes[0]) ||
|
|
2409
|
+
invalidNumericLiteral(rightExpression, argumentTypes[1])) {
|
|
2410
|
+
this.#diagnostic("TSQ203", `A string literal is not valid input for ${label}'s numeric type`, range);
|
|
2411
|
+
return { left, right };
|
|
2412
|
+
}
|
|
2413
|
+
if (this.#acceptsTypeContext(leftExpression)) {
|
|
2414
|
+
left = this.#resolveExpression(leftExpression, scope, ctes, this.#databaseType(argumentTypes[0], left.nullable));
|
|
2415
|
+
}
|
|
2416
|
+
if (rightExpression !== undefined && this.#acceptsTypeContext(rightExpression)) {
|
|
2417
|
+
right = this.#resolveExpression(rightExpression, scope, ctes, this.#databaseType(argumentTypes[1], right.nullable));
|
|
2418
|
+
}
|
|
2419
|
+
return { left, right };
|
|
2420
|
+
}
|
|
2421
|
+
#compositeTypesComparable(leftType, rightType, operator) {
|
|
2422
|
+
const schema = this.#schema;
|
|
2423
|
+
if (schema.formatVersion !== 2)
|
|
2424
|
+
return false;
|
|
2425
|
+
const composite = (databaseType) => {
|
|
2426
|
+
const canonical = postgresCanonicalType(databaseType, schema);
|
|
2427
|
+
return Object.values(schema.types).find((type) => {
|
|
2428
|
+
if (type.kind !== "composite")
|
|
2429
|
+
return false;
|
|
2430
|
+
const qualified = type.schema === undefined ? type.name : `${type.schema}.${type.name}`;
|
|
2431
|
+
return [type.databaseType, type.identity, qualified].some((identity) => normalizeDatabaseType(identity) === normalizeDatabaseType(canonical));
|
|
2432
|
+
});
|
|
2433
|
+
};
|
|
2434
|
+
const left = composite(leftType);
|
|
2435
|
+
const right = composite(rightType);
|
|
2436
|
+
if (left === undefined || right === undefined || left.fields.length !== right.fields.length)
|
|
2437
|
+
return false;
|
|
2438
|
+
return left.fields.every((field, index) => {
|
|
2439
|
+
const candidate = resolvePostgresOperator(operator, field.databaseType, right.fields[index].databaseType, schema);
|
|
2440
|
+
return candidate.kind === "selected";
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
#resolveQuantifiedRowSubquery(expression, scope, ctes) {
|
|
2444
|
+
const leftRow = expression.left;
|
|
2445
|
+
const subquery = expression.right;
|
|
2446
|
+
const resolved = this.#resolveStatement(subquery, scope, ctes);
|
|
2447
|
+
if (leftRow.elements.length !== resolved.columns.length) {
|
|
2448
|
+
for (const element of leftRow.elements)
|
|
2449
|
+
this.#resolveExpression(element, scope, ctes);
|
|
2450
|
+
this.#diagnostic("TSQ217", `${expression.quantifier.toUpperCase()} row comparison requires ${leftRow.elements.length} subquery columns, received ${resolved.columns.length}`, expression.range);
|
|
2451
|
+
return { tsType: "unknown", nullable: true };
|
|
2452
|
+
}
|
|
2453
|
+
let nullable = false;
|
|
2454
|
+
leftRow.elements.forEach((leftExpression, index) => {
|
|
2455
|
+
let left = this.#resolveExpression(leftExpression, scope, ctes);
|
|
2456
|
+
const right = resolved.columns[index];
|
|
2457
|
+
const operator = resolvePostgresOperator(expression.operator, this.#overloadInputType(leftExpression, left), right.databaseType, this.#schema);
|
|
2458
|
+
let argumentTypes = operator.kind === "selected" ? operator.argumentTypes : undefined;
|
|
2459
|
+
const compositeLeftType = left.databaseType ?? (this.#acceptsTypeContext(leftExpression) ? right.databaseType : undefined);
|
|
2460
|
+
const compositeRightType = right.databaseType;
|
|
2461
|
+
if (argumentTypes === undefined &&
|
|
2462
|
+
compositeLeftType !== undefined &&
|
|
2463
|
+
compositeRightType !== undefined &&
|
|
2464
|
+
this.#compositeTypesComparable(compositeLeftType, compositeRightType, expression.operator)) {
|
|
2465
|
+
argumentTypes = [compositeLeftType, compositeRightType];
|
|
2466
|
+
}
|
|
2467
|
+
if (argumentTypes === undefined) {
|
|
2468
|
+
this.#diagnostic("TSQ203", `${operator.kind === "ambiguous" ? "Ambiguous" : "No matching"} quantified row operator ${expression.operator} for position ${index + 1}`, expression.range);
|
|
2469
|
+
}
|
|
2470
|
+
else if (this.#acceptsTypeContext(leftExpression)) {
|
|
2471
|
+
left = this.#resolveExpression(leftExpression, scope, ctes, this.#databaseType(argumentTypes[0], left.nullable));
|
|
2472
|
+
}
|
|
2473
|
+
nullable ||= left.nullable || right.nullable;
|
|
2474
|
+
});
|
|
2475
|
+
return { tsType: "boolean", nullable, databaseType: "boolean" };
|
|
2476
|
+
}
|
|
2477
|
+
#resolveRowComparison(expression, scope, ctes) {
|
|
2478
|
+
const leftRow = expression.left;
|
|
2479
|
+
const rightRow = expression.right;
|
|
2480
|
+
const supported = ["=", "!=", "<>", "<", "<=", ">", ">=", "IS DISTINCT FROM", "IS NOT DISTINCT FROM"].includes(expression.operator);
|
|
2481
|
+
if (!supported || leftRow.elements.length !== rightRow.elements.length) {
|
|
2482
|
+
for (const element of [...leftRow.elements, ...rightRow.elements])
|
|
2483
|
+
this.#resolveExpression(element, scope, ctes);
|
|
2484
|
+
this.#diagnostic("TSQ203", supported
|
|
2485
|
+
? `Row comparison requires equal arity, received ${leftRow.elements.length} and ${rightRow.elements.length}`
|
|
2486
|
+
: `PostgreSQL does not support row comparison operator ${expression.operator}`, expression.range);
|
|
2487
|
+
return { tsType: "unknown", nullable: true };
|
|
2488
|
+
}
|
|
2489
|
+
let nullable = false;
|
|
2490
|
+
leftRow.elements.forEach((leftExpression, index) => {
|
|
2491
|
+
const rightExpression = rightRow.elements[index];
|
|
2492
|
+
let left = this.#resolveExpression(leftExpression, scope, ctes);
|
|
2493
|
+
let right = this.#resolveExpression(rightExpression, scope, ctes);
|
|
2494
|
+
const operator = resolvePostgresOperator(expression.operator, this.#overloadInputType(leftExpression, left), this.#overloadInputType(rightExpression, right), this.#schema);
|
|
2495
|
+
let argumentTypes = operator.kind === "selected" ? operator.argumentTypes : undefined;
|
|
2496
|
+
const compositeLeftType = left.databaseType ?? (this.#acceptsTypeContext(leftExpression) ? right.databaseType : undefined);
|
|
2497
|
+
const compositeRightType = right.databaseType ?? (this.#acceptsTypeContext(rightExpression) ? left.databaseType : undefined);
|
|
2498
|
+
if (argumentTypes === undefined &&
|
|
2499
|
+
compositeLeftType !== undefined &&
|
|
2500
|
+
compositeRightType !== undefined &&
|
|
2501
|
+
this.#compositeTypesComparable(compositeLeftType, compositeRightType, expression.operator)) {
|
|
2502
|
+
argumentTypes = [compositeLeftType, compositeRightType];
|
|
2503
|
+
}
|
|
2504
|
+
if (argumentTypes === undefined) {
|
|
2505
|
+
this.#diagnostic("TSQ203", `${operator.kind === "ambiguous" ? "Ambiguous" : "No matching"} row operator ${expression.operator} for position ${index + 1}`, expression.range);
|
|
2506
|
+
}
|
|
2507
|
+
else {
|
|
2508
|
+
if (this.#acceptsTypeContext(leftExpression))
|
|
2509
|
+
left = this.#resolveExpression(leftExpression, scope, ctes, this.#databaseType(argumentTypes[0], left.nullable));
|
|
2510
|
+
if (this.#acceptsTypeContext(rightExpression))
|
|
2511
|
+
right = this.#resolveExpression(rightExpression, scope, ctes, this.#databaseType(argumentTypes[1], right.nullable));
|
|
2512
|
+
}
|
|
2513
|
+
nullable ||= left.nullable || right.nullable;
|
|
2514
|
+
});
|
|
2515
|
+
return {
|
|
2516
|
+
tsType: "boolean",
|
|
2517
|
+
nullable: expression.operator.includes("DISTINCT FROM") ? false : nullable,
|
|
2518
|
+
databaseType: "boolean",
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
554
2521
|
#resolveColumn(relationIdentifier, columnIdentifier, scope) {
|
|
555
2522
|
const name = sqlName(columnIdentifier);
|
|
556
2523
|
if (scope.relations.length === 0 && this.#diagnostics.some((diagnostic) => diagnostic.code === "TSQ100")) {
|
|
@@ -588,6 +2555,8 @@ class Resolver {
|
|
|
588
2555
|
return relation === undefined || column === undefined ? [] : [[relation, column]];
|
|
589
2556
|
}
|
|
590
2557
|
return scope.relations.flatMap((relation) => {
|
|
2558
|
+
if (relation.qualifiedOnly === true)
|
|
2559
|
+
return [];
|
|
591
2560
|
const column = this.#column(relation, name, quoted);
|
|
592
2561
|
return column === undefined ? [] : [[relation, column]];
|
|
593
2562
|
});
|
|
@@ -610,18 +2579,87 @@ class Resolver {
|
|
|
610
2579
|
}
|
|
611
2580
|
#resolveCall(expression, scope, ctes) {
|
|
612
2581
|
const resolved = expression.arguments.map((argument) => this.#resolveExpression(argument, scope, ctes));
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
if (expression.
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
2582
|
+
const aggregateOrder = (expression.orderBy ?? []).map((item) => this.#resolveExpression(item.expression, scope, ctes));
|
|
2583
|
+
const withinOrder = (expression.withinGroup ?? []).map((item) => this.#resolveExpression(item.expression, scope, ctes));
|
|
2584
|
+
if (expression.filter !== undefined) {
|
|
2585
|
+
this.#resolveExpression(expression.filter, scope, ctes, this.#databaseType("boolean", false));
|
|
2586
|
+
}
|
|
2587
|
+
if ((expression.orderBy?.length ?? 0) > 0 && (expression.withinGroup?.length ?? 0) > 0) {
|
|
2588
|
+
this.#diagnostic("TSQ223", "An aggregate cannot use both argument ORDER BY and WITHIN GROUP", expression.range);
|
|
2589
|
+
}
|
|
2590
|
+
if (expression.over !== undefined) {
|
|
2591
|
+
if ("partitionBy" in expression.over)
|
|
2592
|
+
this.#resolveWindowSpecification(expression.over, scope, ctes);
|
|
2593
|
+
else if (!scope.windows.has(sqlName(expression.over))) {
|
|
2594
|
+
this.#diagnostic("TSQ222", `Unknown window ${expression.over.name}`, expression.over.range);
|
|
2595
|
+
}
|
|
620
2596
|
}
|
|
621
2597
|
const name = expression.name.name.toUpperCase();
|
|
622
|
-
if (name === "
|
|
2598
|
+
if (name === "MERGE_ACTION" && expression.arguments.length === 0) {
|
|
2599
|
+
this.#requireServerMajor(17, "merge_action()", expression.range);
|
|
2600
|
+
if (!this.#insideMergeReturning)
|
|
2601
|
+
this.#diagnostic("TSQ227", "merge_action() is only valid in MERGE RETURNING", expression.range);
|
|
2602
|
+
return { tsType: "string", nullable: false, databaseType: "text" };
|
|
2603
|
+
}
|
|
2604
|
+
const routineRule = postgresCatalogRoutineRule(name, this.#schema);
|
|
2605
|
+
const aggregate = this.#isAggregateCall(expression);
|
|
2606
|
+
const windowOnly = routineRule === "bigint-window" ||
|
|
2607
|
+
routineRule === "double-window" ||
|
|
2608
|
+
routineRule === "integer-window" ||
|
|
2609
|
+
routineRule === "value-window";
|
|
2610
|
+
const hypothetical = (expression.withinGroup?.length ?? 0) > 0 && hypotheticalAggregates.has(name);
|
|
2611
|
+
if (expression.distinct &&
|
|
2612
|
+
(expression.orderBy ?? []).some(({ expression: ordered }) => !expression.arguments.some((argument) => postgresExpressionIdentity(argument) === postgresExpressionIdentity(ordered)))) {
|
|
2613
|
+
this.#diagnostic("TSQ227", "DISTINCT aggregate ORDER BY expressions must appear in its arguments", expression.range);
|
|
2614
|
+
}
|
|
2615
|
+
if (name === "MODE" && (expression.arguments.length !== 0 || withinOrder.length !== 1)) {
|
|
2616
|
+
this.#diagnostic("TSQ227", "MODE requires no direct arguments and one WITHIN GROUP key", expression.range);
|
|
2617
|
+
}
|
|
2618
|
+
if ((name === "PERCENTILE_CONT" || name === "PERCENTILE_DISC") &&
|
|
2619
|
+
(expression.arguments.length !== 1 || withinOrder.length !== 1)) {
|
|
2620
|
+
this.#diagnostic("TSQ227", `${expression.name.name} requires one fraction and one WITHIN GROUP key`, expression.range);
|
|
2621
|
+
}
|
|
2622
|
+
if (hypothetical && (expression.arguments.length === 0 || expression.arguments.length !== withinOrder.length)) {
|
|
2623
|
+
this.#diagnostic("TSQ227", `${expression.name.name} hypothetical arguments must match its WITHIN GROUP keys`, expression.range);
|
|
2624
|
+
}
|
|
2625
|
+
if (windowOnly && expression.over === undefined && !hypothetical) {
|
|
2626
|
+
this.#diagnostic("TSQ223", `${expression.name.name} requires an OVER clause`, expression.range);
|
|
2627
|
+
}
|
|
2628
|
+
if ((expression.withinGroup?.length ?? 0) > 0 && routineRule !== "ordered-set-value" && !hypothetical) {
|
|
2629
|
+
this.#diagnostic("TSQ227", `${expression.name.name} does not accept WITHIN GROUP`, expression.range);
|
|
2630
|
+
}
|
|
2631
|
+
if (expression.over !== undefined &&
|
|
2632
|
+
(expression.distinct || (expression.orderBy?.length ?? 0) > 0 || (expression.withinGroup?.length ?? 0) > 0)) {
|
|
2633
|
+
this.#diagnostic("TSQ223", "Window invocations cannot use DISTINCT, argument ORDER BY, or WITHIN GROUP", expression.range);
|
|
2634
|
+
}
|
|
2635
|
+
if (expression.filter !== undefined && !aggregate) {
|
|
2636
|
+
this.#diagnostic("TSQ223", "FILTER is only valid for aggregate functions", expression.filter.range);
|
|
2637
|
+
}
|
|
2638
|
+
if (expression.distinct && !aggregate) {
|
|
2639
|
+
this.#diagnostic("TSQ223", "DISTINCT is only valid for aggregate functions", expression.range);
|
|
2640
|
+
}
|
|
2641
|
+
if ((expression.orderBy?.length ?? 0) > 0 && !aggregate) {
|
|
2642
|
+
this.#diagnostic("TSQ223", "Argument ORDER BY is only valid for aggregate functions", expression.range);
|
|
2643
|
+
}
|
|
2644
|
+
if (routineRule === "count")
|
|
2645
|
+
return { tsType: this.#policy.bigint, nullable: false, databaseType: "bigint" };
|
|
2646
|
+
if (routineRule === "bigint-window")
|
|
623
2647
|
return { tsType: this.#policy.bigint, nullable: false, databaseType: "bigint" };
|
|
624
|
-
if (
|
|
2648
|
+
if (routineRule === "double-window")
|
|
2649
|
+
return { tsType: "number", nullable: false, databaseType: "double precision" };
|
|
2650
|
+
if (routineRule === "integer-window" || routineRule === "grouping")
|
|
2651
|
+
return { tsType: "number", nullable: false, databaseType: "integer" };
|
|
2652
|
+
if (routineRule === "value-window") {
|
|
2653
|
+
return { ...(resolved[0] ?? { tsType: "unknown" }), nullable: true };
|
|
2654
|
+
}
|
|
2655
|
+
if (routineRule === "ordered-set-value") {
|
|
2656
|
+
if (withinOrder.length === 0) {
|
|
2657
|
+
this.#diagnostic("TSQ227", `${expression.name.name} requires WITHIN GROUP`, expression.range);
|
|
2658
|
+
return { tsType: "unknown", nullable: true };
|
|
2659
|
+
}
|
|
2660
|
+
return { ...withinOrder[0], nullable: true };
|
|
2661
|
+
}
|
|
2662
|
+
if (routineRule === "coalesce") {
|
|
625
2663
|
const known = resolved.filter((_, index) => {
|
|
626
2664
|
const argument = expression.arguments[index];
|
|
627
2665
|
return argument?.kind !== "literal" || argument.value !== null;
|
|
@@ -636,27 +2674,60 @@ class Resolver {
|
|
|
636
2674
|
: {}),
|
|
637
2675
|
};
|
|
638
2676
|
}
|
|
639
|
-
if (
|
|
2677
|
+
if (routineRule === "nullif")
|
|
640
2678
|
return { ...(resolved[0] ?? { tsType: "unknown" }), nullable: true };
|
|
641
|
-
if (
|
|
2679
|
+
if (routineRule === "extrema")
|
|
642
2680
|
return {
|
|
643
2681
|
...(resolved[0] ?? { tsType: "unknown", nullable: true }),
|
|
644
2682
|
nullable: name === "MIN" || name === "MAX" ? true : resolved.some((result) => result.nullable),
|
|
645
2683
|
};
|
|
646
|
-
if (
|
|
2684
|
+
if (routineRule === "numeric-aggregate")
|
|
647
2685
|
return {
|
|
648
2686
|
tsType: resolved[0]?.tsType ?? this.#policy.numeric,
|
|
649
2687
|
nullable: true,
|
|
650
2688
|
databaseType: resolved[0]?.databaseType ?? "numeric",
|
|
651
2689
|
};
|
|
652
|
-
if (
|
|
2690
|
+
if (routineRule === "boolean-aggregate")
|
|
653
2691
|
return { tsType: "boolean", nullable: true, databaseType: "boolean" };
|
|
654
|
-
if (
|
|
2692
|
+
if (routineRule === "string-aggregate")
|
|
655
2693
|
return { tsType: "string", nullable: true, databaseType: "text" };
|
|
656
|
-
if (
|
|
2694
|
+
if (routineRule === "json-aggregate")
|
|
657
2695
|
return { tsType: this.#policy.json, nullable: true, databaseType: name.startsWith("JSONB") ? "jsonb" : "json" };
|
|
658
|
-
if (
|
|
659
|
-
const
|
|
2696
|
+
if (routineRule === "json-path-boolean" || routineRule === "json-path-json" || routineRule === "json-path-set") {
|
|
2697
|
+
const signature = this.#jsonPathCallTypes(expression);
|
|
2698
|
+
const selection = signature === undefined
|
|
2699
|
+
? { kind: "none" }
|
|
2700
|
+
: resolvePostgresCandidates([
|
|
2701
|
+
{
|
|
2702
|
+
value: name,
|
|
2703
|
+
argumentTypes: signature,
|
|
2704
|
+
resultType: routineRule === "json-path-boolean" ? "boolean" : "jsonb",
|
|
2705
|
+
},
|
|
2706
|
+
], expression.arguments.map((argument, index) => this.#overloadInputType(argument, resolved[index])), this.#schema);
|
|
2707
|
+
if (signature === undefined || selection.kind !== "selected") {
|
|
2708
|
+
this.#diagnostic("TSQ202", `No overload of ${expression.name.name} accepts these argument types`, expression.range, "Use jsonb, jsonpath, optional jsonb variables, and an optional boolean silent flag.", "warning");
|
|
2709
|
+
return { tsType: "unknown", nullable: true };
|
|
2710
|
+
}
|
|
2711
|
+
expression.arguments.forEach((argument, index) => {
|
|
2712
|
+
if (this.#acceptsTypeContext(argument)) {
|
|
2713
|
+
this.#resolveExpression(argument, scope, ctes, this.#databaseType(signature[index], true));
|
|
2714
|
+
}
|
|
2715
|
+
});
|
|
2716
|
+
if (routineRule === "json-path-boolean") {
|
|
2717
|
+
return {
|
|
2718
|
+
tsType: "boolean",
|
|
2719
|
+
nullable: name.includes("MATCH") || resolved.some(({ nullable }) => nullable),
|
|
2720
|
+
databaseType: "boolean",
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
return {
|
|
2724
|
+
tsType: this.#policy.json,
|
|
2725
|
+
nullable: routineRule === "json-path-json" && (name.includes("FIRST") || resolved.some(({ nullable }) => nullable)),
|
|
2726
|
+
databaseType: "jsonb",
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
if (routineRule === "array-aggregate") {
|
|
2730
|
+
const item = resolved[0] ?? aggregateOrder[0] ?? { tsType: "unknown", nullable: true };
|
|
660
2731
|
return {
|
|
661
2732
|
tsType: `readonly (${item.tsType}${item.nullable ? " | null" : ""})[]`,
|
|
662
2733
|
nullable: true,
|
|
@@ -665,6 +2736,48 @@ class Resolver {
|
|
|
665
2736
|
}
|
|
666
2737
|
const functionName = sqlName(expression.name);
|
|
667
2738
|
const schemaName = expression.schema === undefined ? undefined : sqlName(expression.schema);
|
|
2739
|
+
const routines = this.#index.routineOverloads(functionName, expression.arguments.length, schemaName);
|
|
2740
|
+
if (routines.length > 0) {
|
|
2741
|
+
const selection = resolvePostgresCandidates(routines.flatMap((routine) => {
|
|
2742
|
+
const result = routine.result;
|
|
2743
|
+
if (result.kind !== "scalar" && result.kind !== "set")
|
|
2744
|
+
return [];
|
|
2745
|
+
const argumentTypes = this.#routineCallTypes(routine, expression);
|
|
2746
|
+
if (argumentTypes === undefined)
|
|
2747
|
+
return [];
|
|
2748
|
+
return [
|
|
2749
|
+
{
|
|
2750
|
+
value: routine,
|
|
2751
|
+
argumentTypes,
|
|
2752
|
+
resultType: result.databaseType,
|
|
2753
|
+
},
|
|
2754
|
+
];
|
|
2755
|
+
}), expression.arguments.map((argument, index) => this.#overloadInputType(argument, resolved[index])), this.#schema);
|
|
2756
|
+
if (selection.kind === "selected") {
|
|
2757
|
+
expression.arguments.forEach((argument, index) => {
|
|
2758
|
+
if (this.#acceptsTypeContext(argument)) {
|
|
2759
|
+
this.#resolveExpression(argument, scope, ctes, this.#databaseType(selection.argumentTypes[index], true));
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2762
|
+
const result = selection.candidate.result;
|
|
2763
|
+
if (result.kind === "scalar" || result.kind === "set") {
|
|
2764
|
+
return {
|
|
2765
|
+
tsType: selection.resultType === normalizeDatabaseType(result.databaseType)
|
|
2766
|
+
? result.tsType
|
|
2767
|
+
: mapPostgresType(selection.resultType, this.#policy, this.#schema),
|
|
2768
|
+
nullable: result.nullable,
|
|
2769
|
+
databaseType: selection.resultType,
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
if (selection.kind === "ambiguous") {
|
|
2774
|
+
this.#diagnostic("TSQ204", `Ambiguous overloaded function ${expression.name.name}`, expression.range, "Cast arguments to select a specific overload.");
|
|
2775
|
+
}
|
|
2776
|
+
else {
|
|
2777
|
+
this.#diagnostic("TSQ202", `No overload of ${expression.name.name} accepts these argument types`, expression.range, "Cast arguments to a supported overload.", "warning");
|
|
2778
|
+
}
|
|
2779
|
+
return { tsType: "unknown", nullable: true };
|
|
2780
|
+
}
|
|
668
2781
|
const candidates = this.#index.functions(functionName, expression.arguments.length, schemaName);
|
|
669
2782
|
const exact = candidates.filter((candidate) => candidate.argumentTypes.every((type, index) => {
|
|
670
2783
|
const actual = resolved[index]?.databaseType;
|
|
@@ -673,7 +2786,7 @@ class Resolver {
|
|
|
673
2786
|
const selected = exact.length === 1 ? exact[0] : candidates.length === 1 ? candidates[0] : undefined;
|
|
674
2787
|
if (selected !== undefined) {
|
|
675
2788
|
expression.arguments.forEach((argument, index) => {
|
|
676
|
-
if (argument
|
|
2789
|
+
if (this.#acceptsTypeContext(argument)) {
|
|
677
2790
|
this.#resolveExpression(argument, scope, ctes, this.#databaseType(selected.argumentTypes[index], true));
|
|
678
2791
|
}
|
|
679
2792
|
});
|
|
@@ -686,14 +2799,465 @@ class Resolver {
|
|
|
686
2799
|
this.#diagnostic("TSQ202", `Unknown function ${expression.name.name}`, expression.range, undefined, "warning");
|
|
687
2800
|
return { tsType: "unknown", nullable: true };
|
|
688
2801
|
}
|
|
689
|
-
#typesCompatible(expected, actual) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
2802
|
+
#typesCompatible(expected, actual, context = "implicit") {
|
|
2803
|
+
return postgresCanCoerce(actual, expected, context, this.#schema);
|
|
2804
|
+
}
|
|
2805
|
+
#overloadInputType(expression, resolved) {
|
|
2806
|
+
if (this.#isUnknownOverloadExpression(expression))
|
|
2807
|
+
return undefined;
|
|
2808
|
+
return resolved?.databaseType;
|
|
2809
|
+
}
|
|
2810
|
+
#isUnknownOverloadExpression(expression) {
|
|
2811
|
+
return (this.#acceptsTypeContext(expression) ||
|
|
2812
|
+
(expression.kind === "literal" && (expression.value === null || typeof expression.value === "string")));
|
|
2813
|
+
}
|
|
2814
|
+
#acceptsTypeContext(expression) {
|
|
2815
|
+
return (expression.kind === "parameter" ||
|
|
2816
|
+
(expression.kind === "collate" && this.#acceptsTypeContext(expression.expression)));
|
|
2817
|
+
}
|
|
2818
|
+
#jsonPathCallTypes(expression) {
|
|
2819
|
+
const declaredNames = ["target", "path", "vars", "silent"];
|
|
2820
|
+
const declaredTypes = ["jsonb", "jsonpath", "jsonb", "boolean"];
|
|
2821
|
+
const assigned = new Set();
|
|
2822
|
+
const argumentTypes = expression.arguments.map((_, index) => {
|
|
2823
|
+
const name = expression.argumentNames?.[index];
|
|
2824
|
+
const position = name === undefined ? index : declaredNames.indexOf(sqlName(name));
|
|
2825
|
+
if (position < 0 || assigned.has(position))
|
|
2826
|
+
return undefined;
|
|
2827
|
+
assigned.add(position);
|
|
2828
|
+
return declaredTypes[position];
|
|
2829
|
+
});
|
|
2830
|
+
if (expression.arguments.length > declaredTypes.length ||
|
|
2831
|
+
argumentTypes.some((type) => type === undefined) ||
|
|
2832
|
+
!assigned.has(0) ||
|
|
2833
|
+
!assigned.has(1)) {
|
|
2834
|
+
return undefined;
|
|
2835
|
+
}
|
|
2836
|
+
return argumentTypes;
|
|
2837
|
+
}
|
|
2838
|
+
#isUnknownCastSource(expression) {
|
|
2839
|
+
return (expression.kind === "parameter" ||
|
|
2840
|
+
(expression.kind === "literal" && (expression.value === null || typeof expression.value === "string")));
|
|
2841
|
+
}
|
|
2842
|
+
#routineCallTypes(routine, expression) {
|
|
2843
|
+
const inputs = routine.arguments.filter(({ mode }) => mode !== "out");
|
|
2844
|
+
const names = expression.argumentNames;
|
|
2845
|
+
if (names !== undefined) {
|
|
2846
|
+
const used = new Set();
|
|
2847
|
+
let positional = 0;
|
|
2848
|
+
const mapped = names.map((name) => {
|
|
2849
|
+
let index;
|
|
2850
|
+
if (name === undefined) {
|
|
2851
|
+
while (used.has(positional))
|
|
2852
|
+
positional += 1;
|
|
2853
|
+
index = positional;
|
|
2854
|
+
positional += 1;
|
|
2855
|
+
}
|
|
2856
|
+
else {
|
|
2857
|
+
index = inputs.findIndex((argument) => argument.name !== undefined && argument.name.toLowerCase() === sqlName(name));
|
|
2858
|
+
}
|
|
2859
|
+
if (index < 0 || index >= inputs.length || used.has(index))
|
|
2860
|
+
return undefined;
|
|
2861
|
+
used.add(index);
|
|
2862
|
+
return inputs[index].databaseType;
|
|
2863
|
+
});
|
|
2864
|
+
if (mapped.some((type) => type === undefined))
|
|
2865
|
+
return undefined;
|
|
2866
|
+
if (inputs.some((argument, index) => !used.has(index) && argument.mode !== "variadic" && argument.default !== "present"))
|
|
2867
|
+
return undefined;
|
|
2868
|
+
return mapped;
|
|
2869
|
+
}
|
|
2870
|
+
const variadic = inputs.at(-1)?.mode === "variadic";
|
|
2871
|
+
if (!variadic) {
|
|
2872
|
+
if (expression.variadic === true)
|
|
2873
|
+
return undefined;
|
|
2874
|
+
if (expression.arguments.length > inputs.length)
|
|
2875
|
+
return undefined;
|
|
2876
|
+
if (inputs.slice(expression.arguments.length).some(({ default: defaultValue }) => defaultValue !== "present")) {
|
|
2877
|
+
return undefined;
|
|
2878
|
+
}
|
|
2879
|
+
return inputs.slice(0, expression.arguments.length).map(({ databaseType }) => databaseType);
|
|
2880
|
+
}
|
|
2881
|
+
const fixed = inputs.slice(0, -1);
|
|
2882
|
+
const variadicType = inputs.at(-1).databaseType;
|
|
2883
|
+
if (expression.variadic === true) {
|
|
2884
|
+
if (expression.arguments.length !== inputs.length)
|
|
2885
|
+
return undefined;
|
|
2886
|
+
return [...fixed.map(({ databaseType }) => databaseType), variadicType];
|
|
2887
|
+
}
|
|
2888
|
+
if (expression.arguments.length < fixed.length &&
|
|
2889
|
+
fixed.slice(expression.arguments.length).some(({ default: defaultValue }) => defaultValue !== "present")) {
|
|
2890
|
+
return undefined;
|
|
2891
|
+
}
|
|
2892
|
+
const elementType = normalizeDatabaseType(variadicType).replace(/\[\]$/u, "");
|
|
2893
|
+
if (elementType === normalizeDatabaseType(variadicType))
|
|
2894
|
+
return undefined;
|
|
2895
|
+
return expression.arguments.map((_, index) => (index < fixed.length ? fixed[index].databaseType : elementType));
|
|
2896
|
+
}
|
|
2897
|
+
#resolveSqlJsonValue(value, scope, ctes, requireJsonInput) {
|
|
2898
|
+
const expected = value.format?.encoding === undefined ? this.#databaseType("text", true) : undefined;
|
|
2899
|
+
const resolved = this.#resolveExpression(value.expression, scope, ctes, expected);
|
|
2900
|
+
const canonical = resolved.databaseType === undefined ? undefined : postgresCanonicalType(resolved.databaseType, this.#schema);
|
|
2901
|
+
let valid = true;
|
|
2902
|
+
if (value.format?.encoding !== undefined) {
|
|
2903
|
+
if (value.format.encoding !== "UTF8") {
|
|
2904
|
+
this.#diagnostic("TSQ203", `PostgreSQL JSON ENCODING supports UTF8, received ${value.format.encoding}`, value.format.range);
|
|
2905
|
+
valid = false;
|
|
2906
|
+
}
|
|
2907
|
+
if (canonical !== "bytea") {
|
|
2908
|
+
this.#diagnostic("TSQ203", `JSON ENCODING requires bytea input, received ${resolved.databaseType ?? resolved.tsType}`, value.expression.range);
|
|
2909
|
+
valid = false;
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
if (requireJsonInput || value.format !== undefined) {
|
|
2913
|
+
const nullLiteral = value.expression.kind === "literal" && value.expression.value === null;
|
|
2914
|
+
const jsonInput = canonical === "json" ||
|
|
2915
|
+
canonical === "jsonb" ||
|
|
2916
|
+
(canonical !== undefined && postgresTypeCategory(canonical, this.#schema) === "string") ||
|
|
2917
|
+
(value.format !== undefined && canonical === "bytea");
|
|
2918
|
+
if (!nullLiteral && !jsonInput) {
|
|
2919
|
+
this.#diagnostic("TSQ203", `SQL/JSON input must resolve to jsonb, a character string, or formatted bytea; received ${resolved.databaseType ?? resolved.tsType}`, value.expression.range);
|
|
2920
|
+
valid = false;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
return { resolved, valid };
|
|
2924
|
+
}
|
|
2925
|
+
#resolveJsonConstructorValue(value, scope, ctes) {
|
|
2926
|
+
if (value.format !== undefined)
|
|
2927
|
+
return this.#resolveSqlJsonValue(value, scope, ctes, false).valid;
|
|
2928
|
+
const resolved = this.#resolveExpression(value.expression, scope, ctes);
|
|
2929
|
+
return (resolved.databaseType !== undefined || (value.expression.kind === "literal" && value.expression.value === null));
|
|
2930
|
+
}
|
|
2931
|
+
#resolveJsonObjectKey(expression, scope, ctes, feature) {
|
|
2932
|
+
const key = this.#resolveExpression(expression, scope, ctes);
|
|
2933
|
+
const keyType = key.databaseType === undefined ? undefined : postgresCanonicalType(key.databaseType, this.#schema);
|
|
2934
|
+
const keyCategory = keyType === undefined ? undefined : postgresTypeCategory(keyType, this.#schema);
|
|
2935
|
+
if ((expression.kind === "literal" && expression.value === null) ||
|
|
2936
|
+
expression.kind === "row" ||
|
|
2937
|
+
keyType === "json" ||
|
|
2938
|
+
keyType === "jsonb" ||
|
|
2939
|
+
keyCategory === "array" ||
|
|
2940
|
+
keyCategory === "composite") {
|
|
2941
|
+
this.#diagnostic("TSQ203", `${feature} keys must be non-null scalar values and cannot be arrays, composites, json, or jsonb`, expression.range);
|
|
2942
|
+
return false;
|
|
2943
|
+
}
|
|
2944
|
+
return key.databaseType !== undefined;
|
|
2945
|
+
}
|
|
2946
|
+
#resolveJsonAggregateValue(value, scope, ctes) {
|
|
2947
|
+
if (value.format !== undefined)
|
|
2948
|
+
return this.#resolveSqlJsonValue(value, scope, ctes, false).valid;
|
|
2949
|
+
const resolved = this.#resolveExpression(value.expression, scope, ctes, this.#databaseType("text", true));
|
|
2950
|
+
return (resolved.databaseType !== undefined || (value.expression.kind === "literal" && value.expression.value === null));
|
|
2951
|
+
}
|
|
2952
|
+
#resolveJsonAggregateSuffix(expression, scope, ctes) {
|
|
2953
|
+
if (expression.filter !== undefined) {
|
|
2954
|
+
this.#resolveExpression(expression.filter, scope, ctes, this.#databaseType("boolean", false));
|
|
2955
|
+
}
|
|
2956
|
+
if (expression.over === undefined)
|
|
2957
|
+
return;
|
|
2958
|
+
if ("partitionBy" in expression.over)
|
|
2959
|
+
this.#resolveWindowSpecification(expression.over, scope, ctes);
|
|
2960
|
+
else if (!scope.windows.has(sqlName(expression.over))) {
|
|
2961
|
+
this.#diagnostic("TSQ222", `Unknown window ${expression.over.name}`, expression.over.range);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
#resolveJsonStringInput(value, scope, ctes, feature) {
|
|
2965
|
+
const expected = value.format?.encoding === undefined ? this.#databaseType("text", true) : undefined;
|
|
2966
|
+
const resolved = this.#resolveExpression(value.expression, scope, ctes, expected);
|
|
2967
|
+
const canonical = resolved.databaseType === undefined ? undefined : postgresCanonicalType(resolved.databaseType, this.#schema);
|
|
2968
|
+
const validInput = canonical === "json" ||
|
|
2969
|
+
canonical === "jsonb" ||
|
|
2970
|
+
canonical === "bytea" ||
|
|
2971
|
+
(canonical !== undefined && postgresTypeCategory(canonical, this.#schema) === "string");
|
|
2972
|
+
let valid = validInput;
|
|
2973
|
+
if (!validInput) {
|
|
2974
|
+
this.#diagnostic("TSQ203", `${feature} input must resolve to json, jsonb, a character string, or bytea; received ${resolved.databaseType ?? resolved.tsType}`, value.expression.range);
|
|
2975
|
+
}
|
|
2976
|
+
if (value.format?.encoding !== undefined) {
|
|
2977
|
+
if (value.format.encoding !== "UTF8") {
|
|
2978
|
+
this.#diagnostic("TSQ203", `PostgreSQL JSON ENCODING supports UTF8, received ${value.format.encoding}`, value.format.range);
|
|
2979
|
+
valid = false;
|
|
2980
|
+
}
|
|
2981
|
+
if (canonical !== "bytea") {
|
|
2982
|
+
this.#diagnostic("TSQ203", `JSON ENCODING requires bytea input, received ${resolved.databaseType ?? resolved.tsType}`, value.format.range);
|
|
2983
|
+
valid = false;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
return { resolved, valid };
|
|
2987
|
+
}
|
|
2988
|
+
#resolveJsonSerializeOutput(returning, range, nullable) {
|
|
2989
|
+
const requestedType = returning?.databaseType.name ?? "text";
|
|
2990
|
+
const known = isKnownPostgresType(requestedType, this.#schema);
|
|
2991
|
+
if (!known) {
|
|
2992
|
+
this.#diagnostic("TSQ106", `Invalid or unknown PostgreSQL JSON_SERIALIZE return type ${requestedType}`, returning?.databaseType.range ?? range);
|
|
2993
|
+
return { resolved: { tsType: "unknown", nullable: true, databaseType: "unknown" }, valid: false };
|
|
2994
|
+
}
|
|
2995
|
+
const canonical = postgresCanonicalType(requestedType, this.#schema);
|
|
2996
|
+
const mapping = postgresCatalogTypeMapping(canonical, this.#schema);
|
|
2997
|
+
let valid = mapping === "string" || mapping === "bytes";
|
|
2998
|
+
if (!valid) {
|
|
2999
|
+
this.#diagnostic("TSQ203", `JSON_SERIALIZE RETURNING requires a character string or bytea type; received ${requestedType}`, returning?.databaseType.range ?? range);
|
|
3000
|
+
}
|
|
3001
|
+
if (returning?.format?.encoding !== undefined) {
|
|
3002
|
+
if (returning.format.encoding !== "UTF8") {
|
|
3003
|
+
this.#diagnostic("TSQ203", `PostgreSQL JSON ENCODING supports UTF8, received ${returning.format.encoding}`, returning.format.range);
|
|
3004
|
+
valid = false;
|
|
3005
|
+
}
|
|
3006
|
+
if (canonical !== "bytea") {
|
|
3007
|
+
this.#diagnostic("TSQ203", `JSON ENCODING requires bytea output, received ${requestedType}`, returning.format.range);
|
|
3008
|
+
valid = false;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
return {
|
|
3012
|
+
resolved: valid
|
|
3013
|
+
? { tsType: mapPostgresType(canonical, this.#policy, this.#schema), nullable, databaseType: canonical }
|
|
3014
|
+
: { tsType: "unknown", nullable: true, databaseType: "unknown" },
|
|
3015
|
+
valid,
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
#validateSqlJsonFormat(format, resolved, feature) {
|
|
3019
|
+
const canonical = resolved.databaseType === undefined ? undefined : postgresCanonicalType(resolved.databaseType, this.#schema);
|
|
3020
|
+
const validInput = canonical === "json" ||
|
|
3021
|
+
canonical === "jsonb" ||
|
|
3022
|
+
(canonical !== undefined && postgresTypeCategory(canonical, this.#schema) === "string") ||
|
|
3023
|
+
canonical === "bytea";
|
|
3024
|
+
let valid = validInput;
|
|
3025
|
+
if (!validInput) {
|
|
3026
|
+
this.#diagnostic("TSQ203", `${feature} FORMAT JSON requires json, jsonb, a character string, or bytea input; received ${resolved.databaseType ?? resolved.tsType}`, format.range);
|
|
3027
|
+
}
|
|
3028
|
+
if (format.encoding !== undefined) {
|
|
3029
|
+
if (format.encoding !== "UTF8") {
|
|
3030
|
+
this.#diagnostic("TSQ203", `PostgreSQL JSON ENCODING supports UTF8, received ${format.encoding}`, format.range);
|
|
3031
|
+
valid = false;
|
|
3032
|
+
}
|
|
3033
|
+
if (canonical !== "bytea") {
|
|
3034
|
+
this.#diagnostic("TSQ203", `JSON ENCODING requires bytea input, received ${resolved.databaseType ?? resolved.tsType}`, format.range);
|
|
3035
|
+
valid = false;
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
return valid;
|
|
3039
|
+
}
|
|
3040
|
+
#resolveJsonConstructorOutput(returning, feature, range, nullable = false) {
|
|
3041
|
+
const requestedType = returning?.databaseType.name ?? "json";
|
|
3042
|
+
const known = isKnownPostgresType(requestedType, this.#schema);
|
|
3043
|
+
if (!known) {
|
|
3044
|
+
this.#diagnostic("TSQ106", `Invalid or unknown PostgreSQL ${feature} return type ${requestedType}`, returning?.databaseType.range ?? range);
|
|
3045
|
+
return { resolved: { tsType: "unknown", nullable: true, databaseType: "unknown" }, valid: false };
|
|
3046
|
+
}
|
|
3047
|
+
const canonical = postgresCanonicalType(requestedType, this.#schema);
|
|
3048
|
+
const mapping = postgresCatalogTypeMapping(canonical, this.#schema);
|
|
3049
|
+
let valid = mapping === "string" || mapping === "bytes" || mapping === "json";
|
|
3050
|
+
if (!valid) {
|
|
3051
|
+
this.#diagnostic("TSQ203", `${feature} RETURNING requires a string, bytea, json, or jsonb type; received ${requestedType}`, returning?.databaseType.range ?? range);
|
|
3052
|
+
}
|
|
3053
|
+
if (returning?.format?.encoding !== undefined) {
|
|
3054
|
+
if (returning.format.encoding !== "UTF8") {
|
|
3055
|
+
this.#diagnostic("TSQ203", `PostgreSQL JSON ENCODING supports UTF8, received ${returning.format.encoding}`, returning.format.range);
|
|
3056
|
+
valid = false;
|
|
3057
|
+
}
|
|
3058
|
+
if (canonical !== "bytea") {
|
|
3059
|
+
this.#diagnostic("TSQ203", `JSON ENCODING requires bytea output, received ${requestedType}`, returning.format.range);
|
|
3060
|
+
valid = false;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
return {
|
|
3064
|
+
resolved: valid
|
|
3065
|
+
? {
|
|
3066
|
+
tsType: mapPostgresType(canonical, this.#policy, this.#schema),
|
|
3067
|
+
nullable,
|
|
3068
|
+
databaseType: canonical,
|
|
3069
|
+
}
|
|
3070
|
+
: { tsType: "unknown", nullable: true, databaseType: "unknown" },
|
|
3071
|
+
valid,
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
#resolveJsonQueryBehavior(behavior, expected, scope, ctes, allowEmptyCollections = true, scalarFeature = "JSON_VALUE") {
|
|
3075
|
+
if (behavior === undefined || behavior.kind === "null")
|
|
3076
|
+
return { nullable: true, valid: true };
|
|
3077
|
+
if (behavior.kind !== "default") {
|
|
3078
|
+
if (!allowEmptyCollections && (behavior.kind === "empty-array" || behavior.kind === "empty-object")) {
|
|
3079
|
+
this.#diagnostic("TSQ203", `Only ERROR, NULL, or DEFAULT behavior is valid for ${scalarFeature}`, behavior.range);
|
|
3080
|
+
return { nullable: true, valid: false };
|
|
3081
|
+
}
|
|
3082
|
+
return { nullable: false, valid: true };
|
|
3083
|
+
}
|
|
3084
|
+
const expression = behavior.expression;
|
|
3085
|
+
const resolved = this.#resolveExpression(expression, scope, ctes, expected);
|
|
3086
|
+
let valid = true;
|
|
3087
|
+
if (!this.#isSqlJsonDefaultExpression(expression)) {
|
|
3088
|
+
this.#diagnostic("TSQ203", "SQL/JSON DEFAULT must be a constant expression without parameters, columns, subqueries, aggregates, or windows", expression.range);
|
|
3089
|
+
valid = false;
|
|
3090
|
+
}
|
|
3091
|
+
if (expected?.databaseType !== undefined &&
|
|
3092
|
+
resolved.databaseType !== undefined &&
|
|
3093
|
+
!this.#isUnknownCastSource(expression) &&
|
|
3094
|
+
!postgresCanCoerce(resolved.databaseType, expected.databaseType, "assignment", this.#schema)) {
|
|
3095
|
+
this.#diagnostic("TSQ203", `SQL/JSON DEFAULT of type ${resolved.databaseType} cannot be coerced to ${expected.databaseType}`, expression.range);
|
|
3096
|
+
valid = false;
|
|
3097
|
+
}
|
|
3098
|
+
return { nullable: resolved.nullable, valid };
|
|
3099
|
+
}
|
|
3100
|
+
#isSqlJsonDefaultExpression(expression) {
|
|
3101
|
+
if (expression.kind === "column" ||
|
|
3102
|
+
expression.kind === "parameter" ||
|
|
3103
|
+
expression.kind === "star" ||
|
|
3104
|
+
expression.kind === "subquery" ||
|
|
3105
|
+
expression.kind === "exists") {
|
|
3106
|
+
return false;
|
|
3107
|
+
}
|
|
3108
|
+
if (expression.kind === "call" && (expression.over !== undefined || this.#isAggregateCall(expression))) {
|
|
3109
|
+
return false;
|
|
3110
|
+
}
|
|
3111
|
+
if (expression.kind === "json-object-aggregate" || expression.kind === "json-array-aggregate")
|
|
3112
|
+
return false;
|
|
3113
|
+
return this.#expressionChildren(expression).every((child) => this.#isSqlJsonDefaultExpression(child));
|
|
3114
|
+
}
|
|
3115
|
+
#isDefaultValue(expression) {
|
|
3116
|
+
return (expression?.kind === "column" &&
|
|
3117
|
+
expression.relation === undefined &&
|
|
3118
|
+
expression.column.quoted === false &&
|
|
3119
|
+
expression.column.name === "DEFAULT");
|
|
3120
|
+
}
|
|
3121
|
+
#matchConflictInference(table, target) {
|
|
3122
|
+
const relation = this.#index.relation(table);
|
|
3123
|
+
if (relation === undefined)
|
|
3124
|
+
return "unknown";
|
|
3125
|
+
const requested = target.elements.map((element) => ({
|
|
3126
|
+
...(element.expression.kind === "column" && element.expression.relation === undefined
|
|
3127
|
+
? {
|
|
3128
|
+
column: sqlName(element.expression.column),
|
|
3129
|
+
columnCaseSensitive: element.expression.column.quoted,
|
|
3130
|
+
}
|
|
3131
|
+
: { expressionHash: fingerprintPostgresExpression(element.expression) }),
|
|
3132
|
+
...(element.operatorClass === undefined
|
|
3133
|
+
? {}
|
|
3134
|
+
: {
|
|
3135
|
+
operatorClass: qualifiedSqlName(element.operatorClass),
|
|
3136
|
+
operatorClassCaseSensitive: qualifiedNameIsCaseSensitive(element.operatorClass),
|
|
3137
|
+
}),
|
|
3138
|
+
...(element.collation === undefined
|
|
3139
|
+
? {}
|
|
3140
|
+
: {
|
|
3141
|
+
collation: qualifiedSqlName(element.collation),
|
|
3142
|
+
collationCaseSensitive: qualifiedNameIsCaseSensitive(element.collation),
|
|
3143
|
+
}),
|
|
3144
|
+
}));
|
|
3145
|
+
const predicateHash = target.predicate === undefined ? undefined : fingerprintPostgresExpression(target.predicate);
|
|
3146
|
+
let incomplete = false;
|
|
3147
|
+
const matchesElements = (candidates) => {
|
|
3148
|
+
if (candidates.length !== requested.length)
|
|
3149
|
+
return false;
|
|
3150
|
+
const remaining = [...candidates];
|
|
3151
|
+
const matchesNamedEvidence = (candidate, name, caseSensitive) => {
|
|
3152
|
+
if (candidate === undefined)
|
|
3153
|
+
return false;
|
|
3154
|
+
const candidateName = name.includes(".") ? candidate : candidate.slice(candidate.lastIndexOf(".") + 1);
|
|
3155
|
+
return caseSensitive ? candidateName === name : candidateName.toLowerCase() === name;
|
|
3156
|
+
};
|
|
3157
|
+
for (const element of requested) {
|
|
3158
|
+
const index = remaining.findIndex((candidate) => {
|
|
3159
|
+
const identityMatches = element.column === undefined
|
|
3160
|
+
? candidate.expressionHash === element.expressionHash
|
|
3161
|
+
: element.columnCaseSensitive
|
|
3162
|
+
? candidate.column === element.column
|
|
3163
|
+
: candidate.column?.toLowerCase() === element.column;
|
|
3164
|
+
return (identityMatches &&
|
|
3165
|
+
(element.operatorClass === undefined ||
|
|
3166
|
+
matchesNamedEvidence(candidate.operatorClass, element.operatorClass, element.operatorClassCaseSensitive ?? false)) &&
|
|
3167
|
+
(element.collation === undefined ||
|
|
3168
|
+
matchesNamedEvidence(candidate.collation, element.collation, element.collationCaseSensitive ?? false)));
|
|
3169
|
+
});
|
|
3170
|
+
if (index < 0)
|
|
3171
|
+
return false;
|
|
3172
|
+
remaining.splice(index, 1);
|
|
3173
|
+
}
|
|
695
3174
|
return true;
|
|
696
|
-
|
|
3175
|
+
};
|
|
3176
|
+
if (relation.indexes === undefined)
|
|
3177
|
+
incomplete = true;
|
|
3178
|
+
for (const index of relation.indexes ?? []) {
|
|
3179
|
+
if (!index.unique)
|
|
3180
|
+
continue;
|
|
3181
|
+
if (index.valid !== true) {
|
|
3182
|
+
if (index.valid === "unknown")
|
|
3183
|
+
incomplete = true;
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3186
|
+
if (target.predicate === undefined) {
|
|
3187
|
+
if (index.predicate === "unknown") {
|
|
3188
|
+
incomplete = true;
|
|
3189
|
+
continue;
|
|
3190
|
+
}
|
|
3191
|
+
if (index.predicate !== "none")
|
|
3192
|
+
continue;
|
|
3193
|
+
}
|
|
3194
|
+
else {
|
|
3195
|
+
if (index.predicate === "unknown") {
|
|
3196
|
+
incomplete = true;
|
|
3197
|
+
continue;
|
|
3198
|
+
}
|
|
3199
|
+
if (index.predicate === "present" && index.predicateHash === undefined) {
|
|
3200
|
+
incomplete = true;
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
if (index.predicate === "present" && index.predicateHash !== predicateHash && matchesElements(index.columns)) {
|
|
3204
|
+
incomplete = true;
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
if (matchesElements(index.columns))
|
|
3209
|
+
return true;
|
|
3210
|
+
if (requested.some((element) => (element.operatorClass?.includes(".") ?? false) || (element.collation?.includes(".") ?? false)) &&
|
|
3211
|
+
index.columns.some((column) => (column.operatorClass !== undefined && !column.operatorClass.includes(".")) ||
|
|
3212
|
+
(column.collation !== undefined && !column.collation.includes("."))))
|
|
3213
|
+
incomplete = true;
|
|
3214
|
+
if (index.columns.some((column) => column.column === undefined && column.expressionHash === undefined)) {
|
|
3215
|
+
incomplete = true;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
if (requested.every((element) => element.column !== undefined)) {
|
|
3219
|
+
const requestedColumns = requested;
|
|
3220
|
+
for (const constraint of relation.constraints) {
|
|
3221
|
+
if ((constraint.kind !== "primary-key" && constraint.kind !== "unique") ||
|
|
3222
|
+
constraint.partial !== false ||
|
|
3223
|
+
constraint.expressionBased !== false ||
|
|
3224
|
+
constraint.columns.length !== requestedColumns.length ||
|
|
3225
|
+
!constraint.columns.every((column) => requestedColumns.some((requestedColumn) => requestedColumn.columnCaseSensitive
|
|
3226
|
+
? column === requestedColumn.column
|
|
3227
|
+
: column.toLowerCase() === requestedColumn.column)))
|
|
3228
|
+
continue;
|
|
3229
|
+
if (constraint.deferrable === false)
|
|
3230
|
+
return true;
|
|
3231
|
+
if (constraint.deferrable === "unknown" || constraint.deferrable === undefined)
|
|
3232
|
+
incomplete = true;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
return incomplete ? "unknown" : false;
|
|
3236
|
+
}
|
|
3237
|
+
#validateWriteColumns(targets, sources, range, operation) {
|
|
3238
|
+
for (let index = 0; index < Math.min(targets.length, sources.length); index += 1) {
|
|
3239
|
+
this.#validateWriteValue(targets[index], sources[index], range, operation);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
#validateWriteValue(target, source, range, operation) {
|
|
3243
|
+
if (target === undefined ||
|
|
3244
|
+
source.databaseType === undefined ||
|
|
3245
|
+
source.tsType === "unknown" ||
|
|
3246
|
+
this.#typesCompatible(target.databaseType, source.databaseType, "assignment"))
|
|
3247
|
+
return;
|
|
3248
|
+
const expectedTsType = mapPostgresType(target.databaseType, this.#policy, this.#schema);
|
|
3249
|
+
const actualTsType = mapPostgresType(source.databaseType, this.#policy, this.#schema);
|
|
3250
|
+
if (expectedTsType === actualTsType)
|
|
3251
|
+
return;
|
|
3252
|
+
this.#diagnostic("TSQ229", `${operation} value of type ${source.databaseType} cannot be assigned to ${target.name} (${target.databaseType})`, range);
|
|
3253
|
+
}
|
|
3254
|
+
#isNumericType(databaseType) {
|
|
3255
|
+
const mapping = postgresCatalogTypeMapping(databaseType, this.#schema);
|
|
3256
|
+
return mapping === "bigint" || mapping === "number" || mapping === "numeric";
|
|
3257
|
+
}
|
|
3258
|
+
#validNumericLiteral(value, databaseType) {
|
|
3259
|
+
const integer = ["smallint", "integer", "bigint"].includes(normalizeDatabaseType(databaseType));
|
|
3260
|
+
return integer ? /^[+-]?\d+$/u.test(value) : /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/iu.test(value);
|
|
697
3261
|
}
|
|
698
3262
|
#functionType(value) {
|
|
699
3263
|
return {
|