@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
package/dist/syql-lexer.js
CHANGED
|
@@ -92,9 +92,11 @@ class Lexer {
|
|
|
92
92
|
#column = 1;
|
|
93
93
|
#braceDepth = 0;
|
|
94
94
|
#expectImportPath = false;
|
|
95
|
-
|
|
95
|
+
#recognizeImportPaths;
|
|
96
|
+
constructor(file, source, recognizeImportPaths = true) {
|
|
96
97
|
this.#file = file;
|
|
97
98
|
this.#source = source;
|
|
99
|
+
this.#recognizeImportPaths = recognizeImportPaths;
|
|
98
100
|
}
|
|
99
101
|
lex() {
|
|
100
102
|
while (this.#offset < this.#source.length)
|
|
@@ -161,7 +163,8 @@ class Lexer {
|
|
|
161
163
|
else if (token.text === '}')
|
|
162
164
|
this.#braceDepth = Math.max(0, this.#braceDepth - 1);
|
|
163
165
|
}
|
|
164
|
-
if (
|
|
166
|
+
if (this.#recognizeImportPaths &&
|
|
167
|
+
token.kind === 'identifier' &&
|
|
165
168
|
token.text === 'from' &&
|
|
166
169
|
this.#braceDepth === 0) {
|
|
167
170
|
this.#expectImportPath = true;
|
|
@@ -365,3 +368,7 @@ class Lexer {
|
|
|
365
368
|
export function lexSyqlSource(file, source) {
|
|
366
369
|
return new Lexer(file, source).lex();
|
|
367
370
|
}
|
|
371
|
+
/** Lex an isolated SQL/template string without import-path contextual rules. */
|
|
372
|
+
export function lexSyqlSqlSource(file, source) {
|
|
373
|
+
return new Lexer(file, source, false).lex();
|
|
374
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IrDocument } from './ir.js';
|
|
2
|
+
import { type AnalyzedQuery, type QueryBackend, type QueryDb, type QueryNamingOptions, type QuerySyqlExecutionPlan } from './query.js';
|
|
3
|
+
import type { SyqlValidatedQuery } from './syql-validator.js';
|
|
4
|
+
export type SyqlLoweringErrorCode = 'SYQL7001_ENUMERATION_LIMIT' | 'SYQL7002_INTERNAL_LOWERING';
|
|
5
|
+
export interface SyqlLoweringOptions {
|
|
6
|
+
readonly backend?: QueryBackend;
|
|
7
|
+
/** Maximum physical activation variants generated for diagnostics and a
|
|
8
|
+
* forced variants backend. Sort profiles multiply this count. */
|
|
9
|
+
readonly maxEnumeratedStatements?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SyqlLoweredQuery {
|
|
12
|
+
readonly validated: SyqlValidatedQuery;
|
|
13
|
+
readonly analysis: AnalyzedQuery;
|
|
14
|
+
readonly selected: QuerySyqlExecutionPlan;
|
|
15
|
+
readonly neutralized: QuerySyqlExecutionPlan;
|
|
16
|
+
/** Omitted only when enumeration exceeds compiler resource policy. */
|
|
17
|
+
readonly enumerated?: QuerySyqlExecutionPlan;
|
|
18
|
+
}
|
|
19
|
+
/** Lower one validated revision-1 query and prepare every generated physical
|
|
20
|
+
* statement. The selected plan is embedded into the returned shared QueryIR
|
|
21
|
+
* query; the second plan is retained for equivalence conformance tests. */
|
|
22
|
+
export declare function lowerSyqlQuery(validated: SyqlValidatedQuery, ir: IrDocument, db: QueryDb, naming?: QueryNamingOptions, options?: SyqlLoweringOptions): SyqlLoweredQuery;
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/** Revision-1 SYQL logical-to-physical lowering (§§15–17). */
|
|
2
|
+
import { TypegenError } from './errors.js';
|
|
3
|
+
import { buildNamingMap } from './naming.js';
|
|
4
|
+
import { analyzeStatement, } from './query.js';
|
|
5
|
+
import { lexSyqlSqlSource } from './syql-lexer.js';
|
|
6
|
+
import { syqlRangeEndBind, syqlRangeStartBind } from './syql-semantics.js';
|
|
7
|
+
const DEFAULT_NAMING = {
|
|
8
|
+
naming: 'camel',
|
|
9
|
+
targets: ['ts'],
|
|
10
|
+
backend: 'auto',
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_ENUMERATION_LIMIT = 256;
|
|
13
|
+
const LIMIT_BIND = '__syqlLimit';
|
|
14
|
+
function fail(code, query, message) {
|
|
15
|
+
throw new TypegenError(`${query.module.file} (query ${query.declaration.name})`, `${code}: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
function renderTemplate(nodes, conditions, mode) {
|
|
18
|
+
return nodes
|
|
19
|
+
.map((node) => {
|
|
20
|
+
if (node.kind === 'sql') {
|
|
21
|
+
return node.parts
|
|
22
|
+
.map((part) => (part.kind === 'text' ? part.text : `:${part.name}`))
|
|
23
|
+
.join('');
|
|
24
|
+
}
|
|
25
|
+
if (node.kind === 'predicate') {
|
|
26
|
+
return `(${renderTemplate(node.body, conditions, mode)})`;
|
|
27
|
+
}
|
|
28
|
+
if (node.kind !== 'when') {
|
|
29
|
+
throw new Error('revision-1 lowering found an unknown logical node');
|
|
30
|
+
}
|
|
31
|
+
const condition = conditions.get(node);
|
|
32
|
+
if (condition === undefined) {
|
|
33
|
+
throw new Error('revision-1 lowering lost a logical condition');
|
|
34
|
+
}
|
|
35
|
+
if (mode.kind === 'neutralized') {
|
|
36
|
+
const predicate = renderTemplate(node.body, conditions, mode);
|
|
37
|
+
return `case when :__syqlActive${condition} = 0 then 1 else (${predicate}) end`;
|
|
38
|
+
}
|
|
39
|
+
if (node.controls.every((control) => mode.active.has(control))) {
|
|
40
|
+
return `(${renderTemplate(node.body, conditions, mode)})`;
|
|
41
|
+
}
|
|
42
|
+
return '1';
|
|
43
|
+
})
|
|
44
|
+
.join('');
|
|
45
|
+
}
|
|
46
|
+
function outerLimitOffset(sql, file) {
|
|
47
|
+
const tokens = lexSyqlSqlSource(file, sql).filter((token) => token.kind !== 'whitespace' &&
|
|
48
|
+
token.kind !== 'line-comment' &&
|
|
49
|
+
token.kind !== 'block-comment' &&
|
|
50
|
+
token.kind !== 'eof');
|
|
51
|
+
let depth = 0;
|
|
52
|
+
for (const token of tokens) {
|
|
53
|
+
if (token.text === ')')
|
|
54
|
+
depth -= 1;
|
|
55
|
+
if (depth === 0 &&
|
|
56
|
+
token.kind === 'identifier' &&
|
|
57
|
+
(token.text.toLowerCase() === 'limit' ||
|
|
58
|
+
token.text.toLowerCase() === 'offset')) {
|
|
59
|
+
return token.span.start.offset;
|
|
60
|
+
}
|
|
61
|
+
if (token.text === '(')
|
|
62
|
+
depth += 1;
|
|
63
|
+
}
|
|
64
|
+
return sql.length;
|
|
65
|
+
}
|
|
66
|
+
function composeControls(sql, file, sortSql, limit) {
|
|
67
|
+
let out = sql.trim();
|
|
68
|
+
if (sortSql !== undefined) {
|
|
69
|
+
const insertion = outerLimitOffset(out, file);
|
|
70
|
+
out = `${out.slice(0, insertion).trimEnd()} order by ${sortSql} ${out
|
|
71
|
+
.slice(insertion)
|
|
72
|
+
.trimStart()}`.trim();
|
|
73
|
+
}
|
|
74
|
+
if (limit !== undefined) {
|
|
75
|
+
// `coalesce` gives the generator's metadata-only execution a valid value;
|
|
76
|
+
// runtimes still validate and bind the effective size before execution.
|
|
77
|
+
out = `${out.trimEnd()} limit min(coalesce(:${LIMIT_BIND}, ${limit.defaultSize}), ${limit.maxSize})`;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
function distinctBindNames(sql, file) {
|
|
82
|
+
const names = [];
|
|
83
|
+
for (const token of lexSyqlSqlSource(file, sql)) {
|
|
84
|
+
if (token.kind !== 'bind')
|
|
85
|
+
continue;
|
|
86
|
+
const name = token.text.slice(1);
|
|
87
|
+
if (!names.includes(name))
|
|
88
|
+
names.push(name);
|
|
89
|
+
}
|
|
90
|
+
return names;
|
|
91
|
+
}
|
|
92
|
+
function headersFor(names, valueOwners, conditionBinds, limit, query) {
|
|
93
|
+
return names
|
|
94
|
+
.map((name) => {
|
|
95
|
+
const value = valueOwners.get(name);
|
|
96
|
+
if (value !== undefined)
|
|
97
|
+
return `-- param :${name} ${value.type}`;
|
|
98
|
+
if (conditionBinds.has(name))
|
|
99
|
+
return `-- param :${name} boolean`;
|
|
100
|
+
if (name === LIMIT_BIND && limit !== undefined)
|
|
101
|
+
return `-- param :${name} integer`;
|
|
102
|
+
return fail('SYQL7002_INTERNAL_LOWERING', query, `generated SQL contains unresolved bind :${name}`);
|
|
103
|
+
})
|
|
104
|
+
.join('\n');
|
|
105
|
+
}
|
|
106
|
+
function planBind(name, valueOwners, conditionBinds, conditions, limit, query) {
|
|
107
|
+
const value = valueOwners.get(name);
|
|
108
|
+
if (value?.kind === 'value') {
|
|
109
|
+
return { kind: 'value', name, type: value.type, input: value.input };
|
|
110
|
+
}
|
|
111
|
+
if (value?.kind === 'group-member' && value.member !== undefined) {
|
|
112
|
+
return {
|
|
113
|
+
kind: 'group-member',
|
|
114
|
+
name,
|
|
115
|
+
type: value.type,
|
|
116
|
+
input: value.input,
|
|
117
|
+
member: value.member,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const condition = conditionBinds.get(name);
|
|
121
|
+
if (condition !== undefined) {
|
|
122
|
+
return {
|
|
123
|
+
kind: 'condition-active',
|
|
124
|
+
name,
|
|
125
|
+
type: 'boolean',
|
|
126
|
+
condition,
|
|
127
|
+
controls: conditions[condition].controls,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (name === LIMIT_BIND && limit !== undefined) {
|
|
131
|
+
return {
|
|
132
|
+
kind: 'limit',
|
|
133
|
+
name,
|
|
134
|
+
type: 'integer',
|
|
135
|
+
input: limit.control,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return fail('SYQL7002_INTERNAL_LOWERING', query, `cannot describe generated bind :${name}`);
|
|
139
|
+
}
|
|
140
|
+
function publicInputs(validated, naming) {
|
|
141
|
+
const query = validated.logical;
|
|
142
|
+
const declaration = query.declaration;
|
|
143
|
+
const publicNames = [
|
|
144
|
+
...declaration.parameters.map((parameter) => parameter.name),
|
|
145
|
+
...(declaration.sort === undefined ? [] : [declaration.sort.control]),
|
|
146
|
+
...(declaration.limit === undefined ? [] : [declaration.limit.control]),
|
|
147
|
+
];
|
|
148
|
+
const mapped = new Map(buildNamingMap(publicNames, naming.naming, query.module.file, `query ${declaration.name} public inputs`, naming.targets).map((entry) => [entry.sqlName, entry.langName]));
|
|
149
|
+
const result = declaration.parameters.map((parameter) => {
|
|
150
|
+
const langName = mapped.get(parameter.name);
|
|
151
|
+
if (parameter.kind === 'range') {
|
|
152
|
+
const type = validated.bindTypes.get(syqlRangeStartBind(parameter.name));
|
|
153
|
+
if (type === undefined) {
|
|
154
|
+
return fail('SYQL7002_INTERNAL_LOWERING', query, `range ${parameter.name} has no resolved element type`);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
kind: 'group',
|
|
158
|
+
name: parameter.name,
|
|
159
|
+
langName,
|
|
160
|
+
members: [
|
|
161
|
+
{
|
|
162
|
+
name: 'start',
|
|
163
|
+
langName: 'start',
|
|
164
|
+
type: type.base,
|
|
165
|
+
nullable: type.nullable,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'end',
|
|
169
|
+
langName: 'end',
|
|
170
|
+
type: type.base,
|
|
171
|
+
nullable: type.nullable,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (parameter.kind === 'group') {
|
|
177
|
+
const members = buildNamingMap(parameter.members.map((member) => member.name), naming.naming, query.module.file, `query ${declaration.name} group ${parameter.name}`, naming.targets);
|
|
178
|
+
return {
|
|
179
|
+
kind: 'group',
|
|
180
|
+
name: parameter.name,
|
|
181
|
+
langName,
|
|
182
|
+
members: parameter.members.map((member, index) => {
|
|
183
|
+
const type = validated.bindTypes.get(member.name);
|
|
184
|
+
if (type === undefined) {
|
|
185
|
+
return fail('SYQL7002_INTERNAL_LOWERING', query, `group member ${parameter.name}.${member.name} has no resolved type`);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
name: member.name,
|
|
189
|
+
langName: members[index]
|
|
190
|
+
.langName,
|
|
191
|
+
type: type.base,
|
|
192
|
+
nullable: type.nullable,
|
|
193
|
+
};
|
|
194
|
+
}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const type = validated.bindTypes.get(parameter.name);
|
|
198
|
+
if (type === undefined) {
|
|
199
|
+
return fail('SYQL7002_INTERNAL_LOWERING', query, `input ${parameter.name} has no resolved type`);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
kind: 'value',
|
|
203
|
+
name: parameter.name,
|
|
204
|
+
langName,
|
|
205
|
+
type: type.base,
|
|
206
|
+
nullable: type.nullable,
|
|
207
|
+
required: !parameter.optional && parameter.default === undefined,
|
|
208
|
+
...(parameter.default === undefined
|
|
209
|
+
? {}
|
|
210
|
+
: { default: parameter.default }),
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
if (validated.sort !== undefined) {
|
|
214
|
+
const profiles = buildNamingMap(validated.sort.profiles.map((profile) => profile.name), naming.naming, query.module.file, `query ${declaration.name} sort profiles`, naming.targets);
|
|
215
|
+
result.push({
|
|
216
|
+
kind: 'sort',
|
|
217
|
+
name: validated.sort.control,
|
|
218
|
+
langName: mapped.get(validated.sort.control),
|
|
219
|
+
defaultProfile: validated.sort.defaultProfile,
|
|
220
|
+
profiles: validated.sort.profiles.map((profile, index) => ({
|
|
221
|
+
name: profile.name,
|
|
222
|
+
langName: profiles[index].langName,
|
|
223
|
+
})),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (validated.limit !== undefined) {
|
|
227
|
+
result.push({
|
|
228
|
+
kind: 'limit',
|
|
229
|
+
name: validated.limit.control,
|
|
230
|
+
langName: mapped.get(validated.limit.control),
|
|
231
|
+
defaultSize: validated.limit.defaultSize,
|
|
232
|
+
maxSize: validated.limit.maxSize,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
function valueOwners(validated) {
|
|
238
|
+
const owners = new Map();
|
|
239
|
+
for (const parameter of validated.logical.declaration.parameters) {
|
|
240
|
+
if (parameter.kind === 'range') {
|
|
241
|
+
for (const [name, member] of [
|
|
242
|
+
[syqlRangeStartBind(parameter.name), 'start'],
|
|
243
|
+
[syqlRangeEndBind(parameter.name), 'end'],
|
|
244
|
+
]) {
|
|
245
|
+
const type = validated.bindTypes.get(name);
|
|
246
|
+
if (type === undefined)
|
|
247
|
+
continue;
|
|
248
|
+
owners.set(name, {
|
|
249
|
+
kind: 'group-member',
|
|
250
|
+
input: parameter.name,
|
|
251
|
+
member,
|
|
252
|
+
type: type.base,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (parameter.kind === 'group') {
|
|
257
|
+
for (const member of parameter.members) {
|
|
258
|
+
const type = validated.bindTypes.get(member.name);
|
|
259
|
+
if (type === undefined)
|
|
260
|
+
continue;
|
|
261
|
+
owners.set(member.name, {
|
|
262
|
+
kind: 'group-member',
|
|
263
|
+
input: parameter.name,
|
|
264
|
+
member: member.name,
|
|
265
|
+
type: type.base,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const type = validated.bindTypes.get(parameter.name);
|
|
271
|
+
if (type === undefined)
|
|
272
|
+
continue;
|
|
273
|
+
owners.set(parameter.name, {
|
|
274
|
+
kind: 'value',
|
|
275
|
+
input: parameter.name,
|
|
276
|
+
type: type.base,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return owners;
|
|
281
|
+
}
|
|
282
|
+
function activationControls(query) {
|
|
283
|
+
const used = new Set(query.conditions.flatMap((condition) => condition.controls));
|
|
284
|
+
return query.declaration.parameters
|
|
285
|
+
.map((parameter) => parameter.name)
|
|
286
|
+
.filter((name) => used.has(name));
|
|
287
|
+
}
|
|
288
|
+
function lowerStatement(validated, ir, db, naming, sql, sortProfile, activationMask, owners, conditionBinds) {
|
|
289
|
+
const query = validated.logical;
|
|
290
|
+
const location = `${query.module.file} (query ${query.declaration.name}, generated)`;
|
|
291
|
+
const names = distinctBindNames(sql, location);
|
|
292
|
+
const headers = headersFor(names, owners, conditionBinds, validated.limit, query);
|
|
293
|
+
const candidate = headers.length === 0 ? sql : `${headers}\n${sql}`;
|
|
294
|
+
const analyzed = analyzeStatement(query.declaration.name, location, candidate, ir, db, {
|
|
295
|
+
...naming,
|
|
296
|
+
internalParams: [
|
|
297
|
+
...conditionBinds.keys(),
|
|
298
|
+
...(validated.limit === undefined ? [] : [LIMIT_BIND]),
|
|
299
|
+
...[...owners.keys()].filter((name) => name.startsWith('__syqlRange')),
|
|
300
|
+
],
|
|
301
|
+
});
|
|
302
|
+
const namedSql = headers.length === 0
|
|
303
|
+
? analyzed.sql.trim()
|
|
304
|
+
: analyzed.sql.slice(headers.length).trimStart();
|
|
305
|
+
const bindNames = distinctBindNames(namedSql, location);
|
|
306
|
+
return {
|
|
307
|
+
statement: {
|
|
308
|
+
...(sortProfile === undefined ? {} : { sortProfile }),
|
|
309
|
+
...(activationMask === undefined ? {} : { activationMask }),
|
|
310
|
+
sql: namedSql,
|
|
311
|
+
positionalSql: analyzed.positionalSql,
|
|
312
|
+
binds: bindNames.map((name) => planBind(name, owners, conditionBinds, query.conditions, validated.limit, query)),
|
|
313
|
+
},
|
|
314
|
+
analysis: { ...analyzed, sourceSql: namedSql, sql: namedSql },
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function sortProfiles(validated) {
|
|
318
|
+
return validated.sort === undefined
|
|
319
|
+
? [{}]
|
|
320
|
+
: validated.sort.profiles.map((profile) => ({
|
|
321
|
+
name: profile.name,
|
|
322
|
+
sql: profile.sql,
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
/** Lower one validated revision-1 query and prepare every generated physical
|
|
326
|
+
* statement. The selected plan is embedded into the returned shared QueryIR
|
|
327
|
+
* query; the second plan is retained for equivalence conformance tests. */
|
|
328
|
+
export function lowerSyqlQuery(validated, ir, db, naming = DEFAULT_NAMING, options = {}) {
|
|
329
|
+
const logical = validated.logical;
|
|
330
|
+
const conditions = new Map(logical.conditions.map((condition, index) => [condition, index]));
|
|
331
|
+
const controls = activationControls(logical);
|
|
332
|
+
const owners = valueOwners(validated);
|
|
333
|
+
const conditionBinds = new Map(logical.conditions.map((_, index) => [`__syqlActive${index}`, index]));
|
|
334
|
+
const profiles = sortProfiles(validated);
|
|
335
|
+
const neutralStatements = [];
|
|
336
|
+
let neutralDefault;
|
|
337
|
+
for (const profile of profiles) {
|
|
338
|
+
const body = renderTemplate(logical.template, conditions, {
|
|
339
|
+
kind: 'neutralized',
|
|
340
|
+
});
|
|
341
|
+
const sql = composeControls(body, logical.module.file, profile.sql, validated.limit);
|
|
342
|
+
const lowered = lowerStatement(validated, ir, db, naming, sql, profile.name, undefined, owners, conditionBinds);
|
|
343
|
+
neutralStatements.push(lowered.statement);
|
|
344
|
+
if (neutralDefault === undefined ||
|
|
345
|
+
profile.name === validated.sort?.defaultProfile) {
|
|
346
|
+
neutralDefault = lowered.analysis;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const neutralized = {
|
|
350
|
+
backend: 'neutralize',
|
|
351
|
+
activationControls: controls,
|
|
352
|
+
conditions: logical.conditions.map((condition, index) => ({
|
|
353
|
+
controls: condition.controls,
|
|
354
|
+
bind: `__syqlActive${index}`,
|
|
355
|
+
})),
|
|
356
|
+
statements: neutralStatements,
|
|
357
|
+
};
|
|
358
|
+
const maxStatements = options.maxEnumeratedStatements ?? DEFAULT_ENUMERATION_LIMIT;
|
|
359
|
+
const activationCount = 2 ** controls.length;
|
|
360
|
+
const enumeratedCount = activationCount * profiles.length;
|
|
361
|
+
let enumerated;
|
|
362
|
+
let enumeratedDefault;
|
|
363
|
+
if (Number.isSafeInteger(activationCount) &&
|
|
364
|
+
enumeratedCount <= maxStatements) {
|
|
365
|
+
const statements = [];
|
|
366
|
+
for (let mask = 0; mask < activationCount; mask += 1) {
|
|
367
|
+
const active = new Set(controls.filter((_, index) => (mask & (2 ** index)) !== 0));
|
|
368
|
+
for (const profile of profiles) {
|
|
369
|
+
const body = renderTemplate(logical.template, conditions, {
|
|
370
|
+
kind: 'enumerated',
|
|
371
|
+
active,
|
|
372
|
+
});
|
|
373
|
+
const sql = composeControls(body, logical.module.file, profile.sql, validated.limit);
|
|
374
|
+
const lowered = lowerStatement(validated, ir, db, naming, sql, profile.name, mask, owners, new Map());
|
|
375
|
+
statements.push(lowered.statement);
|
|
376
|
+
if (mask === 0 &&
|
|
377
|
+
(enumeratedDefault === undefined ||
|
|
378
|
+
profile.name === validated.sort?.defaultProfile)) {
|
|
379
|
+
enumeratedDefault = lowered.analysis;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
enumerated = {
|
|
384
|
+
backend: 'variants',
|
|
385
|
+
activationControls: controls,
|
|
386
|
+
conditions: logical.conditions.map((condition) => ({
|
|
387
|
+
controls: condition.controls,
|
|
388
|
+
})),
|
|
389
|
+
statements,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const policy = options.backend ?? naming.backend ?? 'auto';
|
|
393
|
+
let selected;
|
|
394
|
+
let canonical;
|
|
395
|
+
if (policy === 'variants') {
|
|
396
|
+
if (enumerated === undefined || enumeratedDefault === undefined) {
|
|
397
|
+
return fail('SYQL7001_ENUMERATION_LIMIT', logical, `${enumeratedCount} generated statements exceed the compiler limit ${maxStatements}; use auto/neutralize or raise the diagnostic limit`);
|
|
398
|
+
}
|
|
399
|
+
selected = enumerated;
|
|
400
|
+
canonical = enumeratedDefault;
|
|
401
|
+
}
|
|
402
|
+
else if (policy === 'auto' &&
|
|
403
|
+
controls.length <= 2 &&
|
|
404
|
+
enumerated !== undefined &&
|
|
405
|
+
enumeratedDefault !== undefined) {
|
|
406
|
+
selected = enumerated;
|
|
407
|
+
canonical = enumeratedDefault;
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
selected = neutralized;
|
|
411
|
+
canonical = neutralDefault;
|
|
412
|
+
}
|
|
413
|
+
const metadata = {
|
|
414
|
+
revision: 1,
|
|
415
|
+
inputs: publicInputs(validated, naming),
|
|
416
|
+
plan: selected,
|
|
417
|
+
...(validated.identity === undefined
|
|
418
|
+
? {}
|
|
419
|
+
: { identity: validated.identity }),
|
|
420
|
+
};
|
|
421
|
+
const analysis = {
|
|
422
|
+
...canonical,
|
|
423
|
+
file: logical.module.file,
|
|
424
|
+
reactive: validated.reactive,
|
|
425
|
+
syql: metadata,
|
|
426
|
+
};
|
|
427
|
+
return {
|
|
428
|
+
validated,
|
|
429
|
+
analysis,
|
|
430
|
+
selected,
|
|
431
|
+
neutralized,
|
|
432
|
+
...(enumerated === undefined ? {} : { enumerated }),
|
|
433
|
+
};
|
|
434
|
+
}
|
package/dist/syql-parser.d.ts
CHANGED
|
@@ -12,13 +12,17 @@ export interface SyqlValueParameter {
|
|
|
12
12
|
readonly name: string;
|
|
13
13
|
readonly optional: boolean;
|
|
14
14
|
readonly type?: SyqlValueType;
|
|
15
|
+
/** A source-level default. Revision 1 currently permits only `bool = false`. */
|
|
16
|
+
readonly default?: false;
|
|
15
17
|
readonly span: SyqlSourceSpan;
|
|
16
18
|
readonly nameSpan: SyqlSourceSpan;
|
|
17
19
|
}
|
|
18
|
-
export interface
|
|
19
|
-
readonly kind: '
|
|
20
|
+
export interface SyqlRangeParameter {
|
|
21
|
+
readonly kind: 'range';
|
|
20
22
|
readonly name: string;
|
|
21
|
-
readonly optional:
|
|
23
|
+
readonly optional: boolean;
|
|
24
|
+
/** Element type, inferred from the left side of BETWEEN when omitted. */
|
|
25
|
+
readonly type?: SyqlValueType;
|
|
22
26
|
readonly span: SyqlSourceSpan;
|
|
23
27
|
readonly nameSpan: SyqlSourceSpan;
|
|
24
28
|
}
|
|
@@ -36,7 +40,7 @@ export interface SyqlGroupParameter {
|
|
|
36
40
|
readonly span: SyqlSourceSpan;
|
|
37
41
|
readonly nameSpan: SyqlSourceSpan;
|
|
38
42
|
}
|
|
39
|
-
export type SyqlQueryParameter = SyqlValueParameter |
|
|
43
|
+
export type SyqlQueryParameter = SyqlValueParameter | SyqlRangeParameter | SyqlGroupParameter;
|
|
40
44
|
export interface SyqlPredicateParameter {
|
|
41
45
|
readonly name: string;
|
|
42
46
|
readonly type?: SyqlValueType;
|
|
@@ -72,11 +76,6 @@ export interface SyqlPredicateDeclaration {
|
|
|
72
76
|
readonly span: SyqlSourceSpan;
|
|
73
77
|
readonly nameSpan: SyqlSourceSpan;
|
|
74
78
|
}
|
|
75
|
-
export interface SyqlSqlSection {
|
|
76
|
-
readonly kind: 'sql';
|
|
77
|
-
readonly body: SyqlTemplate;
|
|
78
|
-
readonly span: SyqlSourceSpan;
|
|
79
|
-
}
|
|
80
79
|
export interface SyqlSortProfile {
|
|
81
80
|
readonly name: string;
|
|
82
81
|
readonly order: SyqlTemplate;
|
|
@@ -91,27 +90,29 @@ export interface SyqlSortSection {
|
|
|
91
90
|
readonly span: SyqlSourceSpan;
|
|
92
91
|
readonly controlSpan: SyqlSourceSpan;
|
|
93
92
|
}
|
|
94
|
-
export interface
|
|
95
|
-
readonly kind: '
|
|
93
|
+
export interface SyqlLimitDeclaration {
|
|
94
|
+
readonly kind: 'limit';
|
|
96
95
|
readonly control: string;
|
|
97
96
|
readonly defaultSize: number;
|
|
98
97
|
readonly maxSize: number;
|
|
99
98
|
readonly span: SyqlSourceSpan;
|
|
100
99
|
readonly controlSpan: SyqlSourceSpan;
|
|
101
100
|
}
|
|
102
|
-
export interface
|
|
103
|
-
readonly
|
|
104
|
-
readonly
|
|
101
|
+
export interface SyqlSyncDimension {
|
|
102
|
+
readonly qualifier: string;
|
|
103
|
+
readonly column: string;
|
|
105
104
|
readonly span: SyqlSourceSpan;
|
|
106
105
|
}
|
|
107
106
|
export interface SyqlQueryDeclaration {
|
|
108
107
|
readonly kind: 'query';
|
|
109
108
|
readonly name: string;
|
|
109
|
+
readonly sync: boolean;
|
|
110
|
+
readonly syncBy?: SyqlSyncDimension;
|
|
110
111
|
readonly parameters: readonly SyqlQueryParameter[];
|
|
111
|
-
|
|
112
|
+
/** The SQL-shaped statement body, excluding its terminating semicolon. */
|
|
113
|
+
readonly statement: SyqlTemplate;
|
|
112
114
|
readonly sort?: SyqlSortSection;
|
|
113
|
-
readonly
|
|
114
|
-
readonly identity?: SyqlIdentityDeclaration;
|
|
115
|
+
readonly limit?: SyqlLimitDeclaration;
|
|
115
116
|
readonly span: SyqlSourceSpan;
|
|
116
117
|
readonly nameSpan: SyqlSourceSpan;
|
|
117
118
|
}
|
|
@@ -127,6 +128,6 @@ export interface SyqlSyntaxFile {
|
|
|
127
128
|
readonly queries: readonly SyqlQueryDeclaration[];
|
|
128
129
|
readonly span: SyqlSourceSpan;
|
|
129
130
|
}
|
|
130
|
-
export type SyqlParseErrorCode = 'SYQL2001_EXPECTED_TOKEN' | 'SYQL2002_INVALID_NAME' | 'SYQL2003_RESERVED_NAME' | 'SYQL2004_DUPLICATE_NAME' | 'SYQL2005_INVALID_IMPORT' | 'SYQL2006_EMPTY_TEMPLATE' | 'SYQL2007_FORBIDDEN_SEMICOLON' | 'SYQL2008_INVALID_MEMBER' | 'SYQL2009_INVALID_INTEGER' | 'SYQL2010_INVALID_PAGE_RANGE' | 'SYQL2011_INVALID_PARAMETER';
|
|
131
|
+
export type SyqlParseErrorCode = 'SYQL2001_EXPECTED_TOKEN' | 'SYQL2002_INVALID_NAME' | 'SYQL2003_RESERVED_NAME' | 'SYQL2004_DUPLICATE_NAME' | 'SYQL2005_INVALID_IMPORT' | 'SYQL2006_EMPTY_TEMPLATE' | 'SYQL2007_FORBIDDEN_SEMICOLON' | 'SYQL2008_INVALID_MEMBER' | 'SYQL2009_INVALID_INTEGER' | 'SYQL2010_INVALID_PAGE_RANGE' | 'SYQL2011_INVALID_PARAMETER' | 'SYQL2012_INVALID_QUERY_BODY';
|
|
131
132
|
/** Parse one `.syql` file using the destructive revision-1 grammar. */
|
|
132
133
|
export declare function parseSyqlSyntaxFile(file: string, source: string): SyqlSyntaxFile;
|