@syncular/typegen 0.5.1 → 0.7.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/README.md +79 -32
- package/dist/cli.js +40 -17
- package/dist/emit-queries-dart.js +168 -55
- package/dist/emit-queries-kotlin.js +167 -55
- package/dist/emit-queries-swift.js +183 -57
- package/dist/emit-queries.js +286 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +348 -316
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +28 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lsp.d.ts +1 -3
- package/dist/lsp.js +349 -187
- package/dist/manifest.d.ts +1 -3
- package/dist/manifest.js +1 -1
- package/dist/query-ir.js +53 -42
- package/dist/query.d.ts +110 -63
- package/dist/query.js +89 -11
- package/dist/syql-ast.d.ts +12 -13
- package/dist/syql-ast.js +26 -20
- package/dist/syql-lexer.d.ts +2 -0
- package/dist/syql-lexer.js +9 -2
- package/dist/syql-lowering.d.ts +22 -0
- package/dist/syql-lowering.js +434 -0
- package/dist/syql-parser.d.ts +19 -18
- package/dist/syql-parser.js +341 -93
- package/dist/syql-semantics.d.ts +73 -0
- package/dist/syql-semantics.js +607 -0
- package/dist/syql-template-parser.d.ts +5 -20
- package/dist/syql-template-parser.js +116 -109
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +993 -0
- package/package.json +3 -3
- package/src/cli.ts +49 -21
- package/src/emit-queries-dart.ts +195 -69
- package/src/emit-queries-kotlin.ts +197 -69
- package/src/emit-queries-swift.ts +224 -68
- package/src/emit-queries.ts +410 -165
- package/src/fmt.ts +405 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +414 -216
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +229 -79
- package/src/syql-ast.ts +38 -34
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +664 -0
- package/src/syql-parser.ts +472 -134
- package/src/syql-semantics.ts +930 -0
- package/src/syql-template-parser.ts +119 -163
- package/src/syql-validator.ts +1486 -0
- package/dist/syql.d.ts +0 -102
- package/dist/syql.js +0 -1141
- package/src/syql.ts +0 -1470
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
/** Revision-1 SYQL logical-to-physical lowering (§§15–17). */
|
|
2
|
+
import { TypegenError } from './errors';
|
|
3
|
+
import type { IrDocument } from './ir';
|
|
4
|
+
import { buildNamingMap } from './naming';
|
|
5
|
+
import {
|
|
6
|
+
type AnalyzedQuery,
|
|
7
|
+
analyzeStatement,
|
|
8
|
+
type QueryBackend,
|
|
9
|
+
type QueryDb,
|
|
10
|
+
type QueryNamingOptions,
|
|
11
|
+
type QueryParamType,
|
|
12
|
+
type QuerySyqlExecutionPlan,
|
|
13
|
+
type QuerySyqlMetadata,
|
|
14
|
+
type QuerySyqlPlanBind,
|
|
15
|
+
type QuerySyqlPublicInput,
|
|
16
|
+
type QuerySyqlStatement,
|
|
17
|
+
} from './query';
|
|
18
|
+
import { lexSyqlSqlSource } from './syql-lexer';
|
|
19
|
+
import type {
|
|
20
|
+
SyqlLogicalQuery,
|
|
21
|
+
SyqlLogicalTemplateNode,
|
|
22
|
+
SyqlLogicalWhenNode,
|
|
23
|
+
} from './syql-semantics';
|
|
24
|
+
import { syqlRangeEndBind, syqlRangeStartBind } from './syql-semantics';
|
|
25
|
+
import type { SyqlValidatedQuery } from './syql-validator';
|
|
26
|
+
|
|
27
|
+
export type SyqlLoweringErrorCode =
|
|
28
|
+
| 'SYQL7001_ENUMERATION_LIMIT'
|
|
29
|
+
| 'SYQL7002_INTERNAL_LOWERING';
|
|
30
|
+
|
|
31
|
+
export interface SyqlLoweringOptions {
|
|
32
|
+
readonly backend?: QueryBackend;
|
|
33
|
+
/** Maximum physical activation variants generated for diagnostics and a
|
|
34
|
+
* forced variants backend. Sort profiles multiply this count. */
|
|
35
|
+
readonly maxEnumeratedStatements?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SyqlLoweredQuery {
|
|
39
|
+
readonly validated: SyqlValidatedQuery;
|
|
40
|
+
readonly analysis: AnalyzedQuery;
|
|
41
|
+
readonly selected: QuerySyqlExecutionPlan;
|
|
42
|
+
readonly neutralized: QuerySyqlExecutionPlan;
|
|
43
|
+
/** Omitted only when enumeration exceeds compiler resource policy. */
|
|
44
|
+
readonly enumerated?: QuerySyqlExecutionPlan;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DEFAULT_NAMING: QueryNamingOptions = {
|
|
48
|
+
naming: 'camel',
|
|
49
|
+
targets: ['ts'],
|
|
50
|
+
backend: 'auto',
|
|
51
|
+
};
|
|
52
|
+
const DEFAULT_ENUMERATION_LIMIT = 256;
|
|
53
|
+
const LIMIT_BIND = '__syqlLimit';
|
|
54
|
+
|
|
55
|
+
interface ValueOwner {
|
|
56
|
+
readonly kind: 'value' | 'group-member';
|
|
57
|
+
readonly input: string;
|
|
58
|
+
readonly member?: string;
|
|
59
|
+
readonly type: QueryParamType;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fail(
|
|
63
|
+
code: SyqlLoweringErrorCode,
|
|
64
|
+
query: SyqlLogicalQuery,
|
|
65
|
+
message: string,
|
|
66
|
+
): never {
|
|
67
|
+
throw new TypegenError(
|
|
68
|
+
`${query.module.file} (query ${query.declaration.name})`,
|
|
69
|
+
`${code}: ${message}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderTemplate(
|
|
74
|
+
nodes: readonly SyqlLogicalTemplateNode[],
|
|
75
|
+
conditions: ReadonlyMap<SyqlLogicalWhenNode, number>,
|
|
76
|
+
mode:
|
|
77
|
+
| { readonly kind: 'neutralized' }
|
|
78
|
+
| { readonly kind: 'enumerated'; readonly active: ReadonlySet<string> },
|
|
79
|
+
): string {
|
|
80
|
+
return nodes
|
|
81
|
+
.map((node) => {
|
|
82
|
+
if (node.kind === 'sql') {
|
|
83
|
+
return node.parts
|
|
84
|
+
.map((part) => (part.kind === 'text' ? part.text : `:${part.name}`))
|
|
85
|
+
.join('');
|
|
86
|
+
}
|
|
87
|
+
if (node.kind === 'predicate') {
|
|
88
|
+
return `(${renderTemplate(node.body, conditions, mode)})`;
|
|
89
|
+
}
|
|
90
|
+
if (node.kind !== 'when') {
|
|
91
|
+
throw new Error('revision-1 lowering found an unknown logical node');
|
|
92
|
+
}
|
|
93
|
+
const condition = conditions.get(node);
|
|
94
|
+
if (condition === undefined) {
|
|
95
|
+
throw new Error('revision-1 lowering lost a logical condition');
|
|
96
|
+
}
|
|
97
|
+
if (mode.kind === 'neutralized') {
|
|
98
|
+
const predicate = renderTemplate(node.body, conditions, mode);
|
|
99
|
+
return `case when :__syqlActive${condition} = 0 then 1 else (${predicate}) end`;
|
|
100
|
+
}
|
|
101
|
+
if (node.controls.every((control) => mode.active.has(control))) {
|
|
102
|
+
return `(${renderTemplate(node.body, conditions, mode)})`;
|
|
103
|
+
}
|
|
104
|
+
return '1';
|
|
105
|
+
})
|
|
106
|
+
.join('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function outerLimitOffset(sql: string, file: string): number {
|
|
110
|
+
const tokens = lexSyqlSqlSource(file, sql).filter(
|
|
111
|
+
(token) =>
|
|
112
|
+
token.kind !== 'whitespace' &&
|
|
113
|
+
token.kind !== 'line-comment' &&
|
|
114
|
+
token.kind !== 'block-comment' &&
|
|
115
|
+
token.kind !== 'eof',
|
|
116
|
+
);
|
|
117
|
+
let depth = 0;
|
|
118
|
+
for (const token of tokens) {
|
|
119
|
+
if (token.text === ')') depth -= 1;
|
|
120
|
+
if (
|
|
121
|
+
depth === 0 &&
|
|
122
|
+
token.kind === 'identifier' &&
|
|
123
|
+
(token.text.toLowerCase() === 'limit' ||
|
|
124
|
+
token.text.toLowerCase() === 'offset')
|
|
125
|
+
) {
|
|
126
|
+
return token.span.start.offset;
|
|
127
|
+
}
|
|
128
|
+
if (token.text === '(') depth += 1;
|
|
129
|
+
}
|
|
130
|
+
return sql.length;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function composeControls(
|
|
134
|
+
sql: string,
|
|
135
|
+
file: string,
|
|
136
|
+
sortSql: string | undefined,
|
|
137
|
+
limit: SyqlValidatedQuery['limit'],
|
|
138
|
+
): string {
|
|
139
|
+
let out = sql.trim();
|
|
140
|
+
if (sortSql !== undefined) {
|
|
141
|
+
const insertion = outerLimitOffset(out, file);
|
|
142
|
+
out = `${out.slice(0, insertion).trimEnd()} order by ${sortSql} ${out
|
|
143
|
+
.slice(insertion)
|
|
144
|
+
.trimStart()}`.trim();
|
|
145
|
+
}
|
|
146
|
+
if (limit !== undefined) {
|
|
147
|
+
// `coalesce` gives the generator's metadata-only execution a valid value;
|
|
148
|
+
// runtimes still validate and bind the effective size before execution.
|
|
149
|
+
out = `${out.trimEnd()} limit min(coalesce(:${LIMIT_BIND}, ${limit.defaultSize}), ${limit.maxSize})`;
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function distinctBindNames(sql: string, file: string): readonly string[] {
|
|
155
|
+
const names: string[] = [];
|
|
156
|
+
for (const token of lexSyqlSqlSource(file, sql)) {
|
|
157
|
+
if (token.kind !== 'bind') continue;
|
|
158
|
+
const name = token.text.slice(1);
|
|
159
|
+
if (!names.includes(name)) names.push(name);
|
|
160
|
+
}
|
|
161
|
+
return names;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function headersFor(
|
|
165
|
+
names: readonly string[],
|
|
166
|
+
valueOwners: ReadonlyMap<string, ValueOwner>,
|
|
167
|
+
conditionBinds: ReadonlyMap<string, number>,
|
|
168
|
+
limit: SyqlValidatedQuery['limit'],
|
|
169
|
+
query: SyqlLogicalQuery,
|
|
170
|
+
): string {
|
|
171
|
+
return names
|
|
172
|
+
.map((name) => {
|
|
173
|
+
const value = valueOwners.get(name);
|
|
174
|
+
if (value !== undefined) return `-- param :${name} ${value.type}`;
|
|
175
|
+
if (conditionBinds.has(name)) return `-- param :${name} boolean`;
|
|
176
|
+
if (name === LIMIT_BIND && limit !== undefined)
|
|
177
|
+
return `-- param :${name} integer`;
|
|
178
|
+
return fail(
|
|
179
|
+
'SYQL7002_INTERNAL_LOWERING',
|
|
180
|
+
query,
|
|
181
|
+
`generated SQL contains unresolved bind :${name}`,
|
|
182
|
+
);
|
|
183
|
+
})
|
|
184
|
+
.join('\n');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function planBind(
|
|
188
|
+
name: string,
|
|
189
|
+
valueOwners: ReadonlyMap<string, ValueOwner>,
|
|
190
|
+
conditionBinds: ReadonlyMap<string, number>,
|
|
191
|
+
conditions: readonly SyqlLogicalWhenNode[],
|
|
192
|
+
limit: SyqlValidatedQuery['limit'],
|
|
193
|
+
query: SyqlLogicalQuery,
|
|
194
|
+
): QuerySyqlPlanBind {
|
|
195
|
+
const value = valueOwners.get(name);
|
|
196
|
+
if (value?.kind === 'value') {
|
|
197
|
+
return { kind: 'value', name, type: value.type, input: value.input };
|
|
198
|
+
}
|
|
199
|
+
if (value?.kind === 'group-member' && value.member !== undefined) {
|
|
200
|
+
return {
|
|
201
|
+
kind: 'group-member',
|
|
202
|
+
name,
|
|
203
|
+
type: value.type,
|
|
204
|
+
input: value.input,
|
|
205
|
+
member: value.member,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const condition = conditionBinds.get(name);
|
|
209
|
+
if (condition !== undefined) {
|
|
210
|
+
return {
|
|
211
|
+
kind: 'condition-active',
|
|
212
|
+
name,
|
|
213
|
+
type: 'boolean',
|
|
214
|
+
condition,
|
|
215
|
+
controls: (conditions[condition] as SyqlLogicalWhenNode).controls,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (name === LIMIT_BIND && limit !== undefined) {
|
|
219
|
+
return {
|
|
220
|
+
kind: 'limit',
|
|
221
|
+
name,
|
|
222
|
+
type: 'integer',
|
|
223
|
+
input: limit.control,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return fail(
|
|
227
|
+
'SYQL7002_INTERNAL_LOWERING',
|
|
228
|
+
query,
|
|
229
|
+
`cannot describe generated bind :${name}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function publicInputs(
|
|
234
|
+
validated: SyqlValidatedQuery,
|
|
235
|
+
naming: QueryNamingOptions,
|
|
236
|
+
): readonly QuerySyqlPublicInput[] {
|
|
237
|
+
const query = validated.logical;
|
|
238
|
+
const declaration = query.declaration;
|
|
239
|
+
const publicNames = [
|
|
240
|
+
...declaration.parameters.map((parameter) => parameter.name),
|
|
241
|
+
...(declaration.sort === undefined ? [] : [declaration.sort.control]),
|
|
242
|
+
...(declaration.limit === undefined ? [] : [declaration.limit.control]),
|
|
243
|
+
];
|
|
244
|
+
const mapped = new Map(
|
|
245
|
+
buildNamingMap(
|
|
246
|
+
publicNames,
|
|
247
|
+
naming.naming,
|
|
248
|
+
query.module.file,
|
|
249
|
+
`query ${declaration.name} public inputs`,
|
|
250
|
+
naming.targets,
|
|
251
|
+
).map((entry) => [entry.sqlName, entry.langName]),
|
|
252
|
+
);
|
|
253
|
+
const result: QuerySyqlPublicInput[] = declaration.parameters.map(
|
|
254
|
+
(parameter) => {
|
|
255
|
+
const langName = mapped.get(parameter.name) as string;
|
|
256
|
+
if (parameter.kind === 'range') {
|
|
257
|
+
const type = validated.bindTypes.get(
|
|
258
|
+
syqlRangeStartBind(parameter.name),
|
|
259
|
+
);
|
|
260
|
+
if (type === undefined) {
|
|
261
|
+
return fail(
|
|
262
|
+
'SYQL7002_INTERNAL_LOWERING',
|
|
263
|
+
query,
|
|
264
|
+
`range ${parameter.name} has no resolved element type`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
kind: 'group',
|
|
269
|
+
name: parameter.name,
|
|
270
|
+
langName,
|
|
271
|
+
members: [
|
|
272
|
+
{
|
|
273
|
+
name: 'start',
|
|
274
|
+
langName: 'start',
|
|
275
|
+
type: type.base,
|
|
276
|
+
nullable: type.nullable,
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'end',
|
|
280
|
+
langName: 'end',
|
|
281
|
+
type: type.base,
|
|
282
|
+
nullable: type.nullable,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (parameter.kind === 'group') {
|
|
288
|
+
const members = buildNamingMap(
|
|
289
|
+
parameter.members.map((member) => member.name),
|
|
290
|
+
naming.naming,
|
|
291
|
+
query.module.file,
|
|
292
|
+
`query ${declaration.name} group ${parameter.name}`,
|
|
293
|
+
naming.targets,
|
|
294
|
+
);
|
|
295
|
+
return {
|
|
296
|
+
kind: 'group',
|
|
297
|
+
name: parameter.name,
|
|
298
|
+
langName,
|
|
299
|
+
members: parameter.members.map((member, index) => {
|
|
300
|
+
const type = validated.bindTypes.get(member.name);
|
|
301
|
+
if (type === undefined) {
|
|
302
|
+
return fail(
|
|
303
|
+
'SYQL7002_INTERNAL_LOWERING',
|
|
304
|
+
query,
|
|
305
|
+
`group member ${parameter.name}.${member.name} has no resolved type`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
name: member.name,
|
|
310
|
+
langName: (members[index] as { readonly langName: string })
|
|
311
|
+
.langName,
|
|
312
|
+
type: type.base,
|
|
313
|
+
nullable: type.nullable,
|
|
314
|
+
};
|
|
315
|
+
}),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const type = validated.bindTypes.get(parameter.name);
|
|
319
|
+
if (type === undefined) {
|
|
320
|
+
return fail(
|
|
321
|
+
'SYQL7002_INTERNAL_LOWERING',
|
|
322
|
+
query,
|
|
323
|
+
`input ${parameter.name} has no resolved type`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
kind: 'value',
|
|
328
|
+
name: parameter.name,
|
|
329
|
+
langName,
|
|
330
|
+
type: type.base,
|
|
331
|
+
nullable: type.nullable,
|
|
332
|
+
required: !parameter.optional && parameter.default === undefined,
|
|
333
|
+
...(parameter.default === undefined
|
|
334
|
+
? {}
|
|
335
|
+
: { default: parameter.default }),
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
);
|
|
339
|
+
if (validated.sort !== undefined) {
|
|
340
|
+
const profiles = buildNamingMap(
|
|
341
|
+
validated.sort.profiles.map((profile) => profile.name),
|
|
342
|
+
naming.naming,
|
|
343
|
+
query.module.file,
|
|
344
|
+
`query ${declaration.name} sort profiles`,
|
|
345
|
+
naming.targets,
|
|
346
|
+
);
|
|
347
|
+
result.push({
|
|
348
|
+
kind: 'sort',
|
|
349
|
+
name: validated.sort.control,
|
|
350
|
+
langName: mapped.get(validated.sort.control) as string,
|
|
351
|
+
defaultProfile: validated.sort.defaultProfile,
|
|
352
|
+
profiles: validated.sort.profiles.map((profile, index) => ({
|
|
353
|
+
name: profile.name,
|
|
354
|
+
langName: (profiles[index] as { readonly langName: string }).langName,
|
|
355
|
+
})),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if (validated.limit !== undefined) {
|
|
359
|
+
result.push({
|
|
360
|
+
kind: 'limit',
|
|
361
|
+
name: validated.limit.control,
|
|
362
|
+
langName: mapped.get(validated.limit.control) as string,
|
|
363
|
+
defaultSize: validated.limit.defaultSize,
|
|
364
|
+
maxSize: validated.limit.maxSize,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function valueOwners(
|
|
371
|
+
validated: SyqlValidatedQuery,
|
|
372
|
+
): ReadonlyMap<string, ValueOwner> {
|
|
373
|
+
const owners = new Map<string, ValueOwner>();
|
|
374
|
+
for (const parameter of validated.logical.declaration.parameters) {
|
|
375
|
+
if (parameter.kind === 'range') {
|
|
376
|
+
for (const [name, member] of [
|
|
377
|
+
[syqlRangeStartBind(parameter.name), 'start'],
|
|
378
|
+
[syqlRangeEndBind(parameter.name), 'end'],
|
|
379
|
+
] as const) {
|
|
380
|
+
const type = validated.bindTypes.get(name);
|
|
381
|
+
if (type === undefined) continue;
|
|
382
|
+
owners.set(name, {
|
|
383
|
+
kind: 'group-member',
|
|
384
|
+
input: parameter.name,
|
|
385
|
+
member,
|
|
386
|
+
type: type.base,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
} else if (parameter.kind === 'group') {
|
|
390
|
+
for (const member of parameter.members) {
|
|
391
|
+
const type = validated.bindTypes.get(member.name);
|
|
392
|
+
if (type === undefined) continue;
|
|
393
|
+
owners.set(member.name, {
|
|
394
|
+
kind: 'group-member',
|
|
395
|
+
input: parameter.name,
|
|
396
|
+
member: member.name,
|
|
397
|
+
type: type.base,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
const type = validated.bindTypes.get(parameter.name);
|
|
402
|
+
if (type === undefined) continue;
|
|
403
|
+
owners.set(parameter.name, {
|
|
404
|
+
kind: 'value',
|
|
405
|
+
input: parameter.name,
|
|
406
|
+
type: type.base,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return owners;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function activationControls(query: SyqlLogicalQuery): readonly string[] {
|
|
414
|
+
const used = new Set(
|
|
415
|
+
query.conditions.flatMap((condition) => condition.controls),
|
|
416
|
+
);
|
|
417
|
+
return query.declaration.parameters
|
|
418
|
+
.map((parameter) => parameter.name)
|
|
419
|
+
.filter((name) => used.has(name));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function lowerStatement(
|
|
423
|
+
validated: SyqlValidatedQuery,
|
|
424
|
+
ir: IrDocument,
|
|
425
|
+
db: QueryDb,
|
|
426
|
+
naming: QueryNamingOptions,
|
|
427
|
+
sql: string,
|
|
428
|
+
sortProfile: string | undefined,
|
|
429
|
+
activationMask: number | undefined,
|
|
430
|
+
owners: ReadonlyMap<string, ValueOwner>,
|
|
431
|
+
conditionBinds: ReadonlyMap<string, number>,
|
|
432
|
+
): {
|
|
433
|
+
readonly statement: QuerySyqlStatement;
|
|
434
|
+
readonly analysis: AnalyzedQuery;
|
|
435
|
+
} {
|
|
436
|
+
const query = validated.logical;
|
|
437
|
+
const location = `${query.module.file} (query ${query.declaration.name}, generated)`;
|
|
438
|
+
const names = distinctBindNames(sql, location);
|
|
439
|
+
const headers = headersFor(
|
|
440
|
+
names,
|
|
441
|
+
owners,
|
|
442
|
+
conditionBinds,
|
|
443
|
+
validated.limit,
|
|
444
|
+
query,
|
|
445
|
+
);
|
|
446
|
+
const candidate = headers.length === 0 ? sql : `${headers}\n${sql}`;
|
|
447
|
+
const analyzed = analyzeStatement(
|
|
448
|
+
query.declaration.name,
|
|
449
|
+
location,
|
|
450
|
+
candidate,
|
|
451
|
+
ir,
|
|
452
|
+
db,
|
|
453
|
+
{
|
|
454
|
+
...naming,
|
|
455
|
+
internalParams: [
|
|
456
|
+
...conditionBinds.keys(),
|
|
457
|
+
...(validated.limit === undefined ? [] : [LIMIT_BIND]),
|
|
458
|
+
...[...owners.keys()].filter((name) => name.startsWith('__syqlRange')),
|
|
459
|
+
],
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
const namedSql =
|
|
463
|
+
headers.length === 0
|
|
464
|
+
? analyzed.sql.trim()
|
|
465
|
+
: analyzed.sql.slice(headers.length).trimStart();
|
|
466
|
+
const bindNames = distinctBindNames(namedSql, location);
|
|
467
|
+
return {
|
|
468
|
+
statement: {
|
|
469
|
+
...(sortProfile === undefined ? {} : { sortProfile }),
|
|
470
|
+
...(activationMask === undefined ? {} : { activationMask }),
|
|
471
|
+
sql: namedSql,
|
|
472
|
+
positionalSql: analyzed.positionalSql,
|
|
473
|
+
binds: bindNames.map((name) =>
|
|
474
|
+
planBind(
|
|
475
|
+
name,
|
|
476
|
+
owners,
|
|
477
|
+
conditionBinds,
|
|
478
|
+
query.conditions,
|
|
479
|
+
validated.limit,
|
|
480
|
+
query,
|
|
481
|
+
),
|
|
482
|
+
),
|
|
483
|
+
},
|
|
484
|
+
analysis: { ...analyzed, sourceSql: namedSql, sql: namedSql },
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function sortProfiles(
|
|
489
|
+
validated: SyqlValidatedQuery,
|
|
490
|
+
): readonly { readonly name?: string; readonly sql?: string }[] {
|
|
491
|
+
return validated.sort === undefined
|
|
492
|
+
? [{}]
|
|
493
|
+
: validated.sort.profiles.map((profile) => ({
|
|
494
|
+
name: profile.name,
|
|
495
|
+
sql: profile.sql,
|
|
496
|
+
}));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Lower one validated revision-1 query and prepare every generated physical
|
|
500
|
+
* statement. The selected plan is embedded into the returned shared QueryIR
|
|
501
|
+
* query; the second plan is retained for equivalence conformance tests. */
|
|
502
|
+
export function lowerSyqlQuery(
|
|
503
|
+
validated: SyqlValidatedQuery,
|
|
504
|
+
ir: IrDocument,
|
|
505
|
+
db: QueryDb,
|
|
506
|
+
naming: QueryNamingOptions = DEFAULT_NAMING,
|
|
507
|
+
options: SyqlLoweringOptions = {},
|
|
508
|
+
): SyqlLoweredQuery {
|
|
509
|
+
const logical = validated.logical;
|
|
510
|
+
const conditions = new Map(
|
|
511
|
+
logical.conditions.map((condition, index) => [condition, index]),
|
|
512
|
+
);
|
|
513
|
+
const controls = activationControls(logical);
|
|
514
|
+
const owners = valueOwners(validated);
|
|
515
|
+
const conditionBinds = new Map(
|
|
516
|
+
logical.conditions.map((_, index) => [`__syqlActive${index}`, index]),
|
|
517
|
+
);
|
|
518
|
+
const profiles = sortProfiles(validated);
|
|
519
|
+
const neutralStatements: QuerySyqlStatement[] = [];
|
|
520
|
+
let neutralDefault: AnalyzedQuery | undefined;
|
|
521
|
+
for (const profile of profiles) {
|
|
522
|
+
const body = renderTemplate(logical.template, conditions, {
|
|
523
|
+
kind: 'neutralized',
|
|
524
|
+
});
|
|
525
|
+
const sql = composeControls(
|
|
526
|
+
body,
|
|
527
|
+
logical.module.file,
|
|
528
|
+
profile.sql,
|
|
529
|
+
validated.limit,
|
|
530
|
+
);
|
|
531
|
+
const lowered = lowerStatement(
|
|
532
|
+
validated,
|
|
533
|
+
ir,
|
|
534
|
+
db,
|
|
535
|
+
naming,
|
|
536
|
+
sql,
|
|
537
|
+
profile.name,
|
|
538
|
+
undefined,
|
|
539
|
+
owners,
|
|
540
|
+
conditionBinds,
|
|
541
|
+
);
|
|
542
|
+
neutralStatements.push(lowered.statement);
|
|
543
|
+
if (
|
|
544
|
+
neutralDefault === undefined ||
|
|
545
|
+
profile.name === validated.sort?.defaultProfile
|
|
546
|
+
) {
|
|
547
|
+
neutralDefault = lowered.analysis;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const neutralized: QuerySyqlExecutionPlan = {
|
|
551
|
+
backend: 'neutralize',
|
|
552
|
+
activationControls: controls,
|
|
553
|
+
conditions: logical.conditions.map((condition, index) => ({
|
|
554
|
+
controls: condition.controls,
|
|
555
|
+
bind: `__syqlActive${index}`,
|
|
556
|
+
})),
|
|
557
|
+
statements: neutralStatements,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const maxStatements =
|
|
561
|
+
options.maxEnumeratedStatements ?? DEFAULT_ENUMERATION_LIMIT;
|
|
562
|
+
const activationCount = 2 ** controls.length;
|
|
563
|
+
const enumeratedCount = activationCount * profiles.length;
|
|
564
|
+
let enumerated: QuerySyqlExecutionPlan | undefined;
|
|
565
|
+
let enumeratedDefault: AnalyzedQuery | undefined;
|
|
566
|
+
if (
|
|
567
|
+
Number.isSafeInteger(activationCount) &&
|
|
568
|
+
enumeratedCount <= maxStatements
|
|
569
|
+
) {
|
|
570
|
+
const statements: QuerySyqlStatement[] = [];
|
|
571
|
+
for (let mask = 0; mask < activationCount; mask += 1) {
|
|
572
|
+
const active = new Set(
|
|
573
|
+
controls.filter((_, index) => (mask & (2 ** index)) !== 0),
|
|
574
|
+
);
|
|
575
|
+
for (const profile of profiles) {
|
|
576
|
+
const body = renderTemplate(logical.template, conditions, {
|
|
577
|
+
kind: 'enumerated',
|
|
578
|
+
active,
|
|
579
|
+
});
|
|
580
|
+
const sql = composeControls(
|
|
581
|
+
body,
|
|
582
|
+
logical.module.file,
|
|
583
|
+
profile.sql,
|
|
584
|
+
validated.limit,
|
|
585
|
+
);
|
|
586
|
+
const lowered = lowerStatement(
|
|
587
|
+
validated,
|
|
588
|
+
ir,
|
|
589
|
+
db,
|
|
590
|
+
naming,
|
|
591
|
+
sql,
|
|
592
|
+
profile.name,
|
|
593
|
+
mask,
|
|
594
|
+
owners,
|
|
595
|
+
new Map(),
|
|
596
|
+
);
|
|
597
|
+
statements.push(lowered.statement);
|
|
598
|
+
if (
|
|
599
|
+
mask === 0 &&
|
|
600
|
+
(enumeratedDefault === undefined ||
|
|
601
|
+
profile.name === validated.sort?.defaultProfile)
|
|
602
|
+
) {
|
|
603
|
+
enumeratedDefault = lowered.analysis;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
enumerated = {
|
|
608
|
+
backend: 'variants',
|
|
609
|
+
activationControls: controls,
|
|
610
|
+
conditions: logical.conditions.map((condition) => ({
|
|
611
|
+
controls: condition.controls,
|
|
612
|
+
})),
|
|
613
|
+
statements,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const policy = options.backend ?? naming.backend ?? 'auto';
|
|
618
|
+
let selected: QuerySyqlExecutionPlan;
|
|
619
|
+
let canonical: AnalyzedQuery;
|
|
620
|
+
if (policy === 'variants') {
|
|
621
|
+
if (enumerated === undefined || enumeratedDefault === undefined) {
|
|
622
|
+
return fail(
|
|
623
|
+
'SYQL7001_ENUMERATION_LIMIT',
|
|
624
|
+
logical,
|
|
625
|
+
`${enumeratedCount} generated statements exceed the compiler limit ${maxStatements}; use auto/neutralize or raise the diagnostic limit`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
selected = enumerated;
|
|
629
|
+
canonical = enumeratedDefault;
|
|
630
|
+
} else if (
|
|
631
|
+
policy === 'auto' &&
|
|
632
|
+
controls.length <= 2 &&
|
|
633
|
+
enumerated !== undefined &&
|
|
634
|
+
enumeratedDefault !== undefined
|
|
635
|
+
) {
|
|
636
|
+
selected = enumerated;
|
|
637
|
+
canonical = enumeratedDefault;
|
|
638
|
+
} else {
|
|
639
|
+
selected = neutralized;
|
|
640
|
+
canonical = neutralDefault as AnalyzedQuery;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const metadata: QuerySyqlMetadata = {
|
|
644
|
+
revision: 1,
|
|
645
|
+
inputs: publicInputs(validated, naming),
|
|
646
|
+
plan: selected,
|
|
647
|
+
...(validated.identity === undefined
|
|
648
|
+
? {}
|
|
649
|
+
: { identity: validated.identity }),
|
|
650
|
+
};
|
|
651
|
+
const analysis: AnalyzedQuery = {
|
|
652
|
+
...canonical,
|
|
653
|
+
file: logical.module.file,
|
|
654
|
+
reactive: validated.reactive,
|
|
655
|
+
syql: metadata,
|
|
656
|
+
};
|
|
657
|
+
return {
|
|
658
|
+
validated,
|
|
659
|
+
analysis,
|
|
660
|
+
selected,
|
|
661
|
+
neutralized,
|
|
662
|
+
...(enumerated === undefined ? {} : { enumerated }),
|
|
663
|
+
};
|
|
664
|
+
}
|