@syncular/typegen 0.5.1 → 0.6.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 +75 -33
- package/dist/cli.js +43 -17
- package/dist/emit-queries-dart.js +167 -55
- package/dist/emit-queries-kotlin.js +166 -55
- package/dist/emit-queries-swift.js +182 -57
- package/dist/emit-queries.js +289 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +323 -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 +359 -185
- 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 +115 -63
- package/dist/query.js +60 -11
- 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 +411 -0
- package/dist/syql-semantics.d.ts +76 -0
- package/dist/syql-semantics.js +551 -0
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +966 -0
- package/package.json +3 -3
- package/src/cli.ts +51 -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 +413 -165
- package/src/fmt.ts +377 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +425 -215
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +199 -79
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +638 -0
- package/src/syql-semantics.ts +859 -0
- package/src/syql-validator.ts +1492 -0
- package/dist/syql.d.ts +0 -102
- package/dist/syql.js +0 -1141
- package/src/syql.ts +0 -1470
|
@@ -0,0 +1,411 @@
|
|
|
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
|
+
const DEFAULT_NAMING = {
|
|
7
|
+
naming: 'camel',
|
|
8
|
+
targets: ['ts'],
|
|
9
|
+
backend: 'auto',
|
|
10
|
+
};
|
|
11
|
+
const DEFAULT_ENUMERATION_LIMIT = 256;
|
|
12
|
+
const PAGE_BIND = '__syqlPageSize';
|
|
13
|
+
function fail(code, query, message) {
|
|
14
|
+
throw new TypegenError(`${query.module.file} (query ${query.declaration.name})`, `${code}: ${message}`);
|
|
15
|
+
}
|
|
16
|
+
function renderDirective(node) {
|
|
17
|
+
const predicates = node.directive.bindings.map((binding) => {
|
|
18
|
+
const column = `${binding.column.qualifier}.${binding.column.name}`;
|
|
19
|
+
const values = binding.values.map((value) => `:${value.name}`);
|
|
20
|
+
return binding.operator === 'equal'
|
|
21
|
+
? `${column} = ${values[0]}`
|
|
22
|
+
: `${column} in (${values.join(', ')})`;
|
|
23
|
+
});
|
|
24
|
+
return `(${predicates.join(' and ')})`;
|
|
25
|
+
}
|
|
26
|
+
function renderTemplate(nodes, conditions, mode) {
|
|
27
|
+
return nodes
|
|
28
|
+
.map((node) => {
|
|
29
|
+
if (node.kind === 'sql') {
|
|
30
|
+
return node.parts
|
|
31
|
+
.map((part) => (part.kind === 'text' ? part.text : `:${part.name}`))
|
|
32
|
+
.join('');
|
|
33
|
+
}
|
|
34
|
+
if (node.kind === 'predicate') {
|
|
35
|
+
return `(${renderTemplate(node.body, conditions, mode)})`;
|
|
36
|
+
}
|
|
37
|
+
if (node.kind === 'scope' || node.kind === 'cover') {
|
|
38
|
+
return renderDirective(node);
|
|
39
|
+
}
|
|
40
|
+
if (node.kind !== 'when') {
|
|
41
|
+
throw new Error('revision-1 lowering found an unknown logical node');
|
|
42
|
+
}
|
|
43
|
+
const condition = conditions.get(node);
|
|
44
|
+
if (condition === undefined) {
|
|
45
|
+
throw new Error('revision-1 lowering lost a logical condition');
|
|
46
|
+
}
|
|
47
|
+
if (mode.kind === 'neutralized') {
|
|
48
|
+
const predicate = renderTemplate(node.body, conditions, mode);
|
|
49
|
+
return `case when :__syqlActive${condition} = 0 then 1 else (${predicate}) end`;
|
|
50
|
+
}
|
|
51
|
+
if (node.controls.every((control) => mode.active.has(control))) {
|
|
52
|
+
return `(${renderTemplate(node.body, conditions, mode)})`;
|
|
53
|
+
}
|
|
54
|
+
return '1';
|
|
55
|
+
})
|
|
56
|
+
.join('');
|
|
57
|
+
}
|
|
58
|
+
function outerLimitOffset(sql, file) {
|
|
59
|
+
const tokens = lexSyqlSqlSource(file, sql).filter((token) => token.kind !== 'whitespace' &&
|
|
60
|
+
token.kind !== 'line-comment' &&
|
|
61
|
+
token.kind !== 'block-comment' &&
|
|
62
|
+
token.kind !== 'eof');
|
|
63
|
+
let depth = 0;
|
|
64
|
+
for (const token of tokens) {
|
|
65
|
+
if (token.text === ')')
|
|
66
|
+
depth -= 1;
|
|
67
|
+
if (depth === 0 &&
|
|
68
|
+
token.kind === 'identifier' &&
|
|
69
|
+
(token.text.toLowerCase() === 'limit' ||
|
|
70
|
+
token.text.toLowerCase() === 'offset')) {
|
|
71
|
+
return token.span.start.offset;
|
|
72
|
+
}
|
|
73
|
+
if (token.text === '(')
|
|
74
|
+
depth += 1;
|
|
75
|
+
}
|
|
76
|
+
return sql.length;
|
|
77
|
+
}
|
|
78
|
+
function composeControls(sql, file, sortSql, page) {
|
|
79
|
+
let out = sql.trim();
|
|
80
|
+
if (sortSql !== undefined) {
|
|
81
|
+
const insertion = outerLimitOffset(out, file);
|
|
82
|
+
out = `${out.slice(0, insertion).trimEnd()} order by ${sortSql} ${out
|
|
83
|
+
.slice(insertion)
|
|
84
|
+
.trimStart()}`.trim();
|
|
85
|
+
}
|
|
86
|
+
if (page !== undefined) {
|
|
87
|
+
// `coalesce` gives the generator's metadata-only execution a valid value;
|
|
88
|
+
// runtimes still validate and bind the effective size before execution.
|
|
89
|
+
out = `${out.trimEnd()} limit min(coalesce(:${PAGE_BIND}, ${page.defaultSize}), ${page.maxSize})`;
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function distinctBindNames(sql, file) {
|
|
94
|
+
const names = [];
|
|
95
|
+
for (const token of lexSyqlSqlSource(file, sql)) {
|
|
96
|
+
if (token.kind !== 'bind')
|
|
97
|
+
continue;
|
|
98
|
+
const name = token.text.slice(1);
|
|
99
|
+
if (!names.includes(name))
|
|
100
|
+
names.push(name);
|
|
101
|
+
}
|
|
102
|
+
return names;
|
|
103
|
+
}
|
|
104
|
+
function headersFor(names, valueOwners, conditionBinds, page, query) {
|
|
105
|
+
return names
|
|
106
|
+
.map((name) => {
|
|
107
|
+
const value = valueOwners.get(name);
|
|
108
|
+
if (value !== undefined)
|
|
109
|
+
return `-- param :${name} ${value.type}`;
|
|
110
|
+
if (conditionBinds.has(name))
|
|
111
|
+
return `-- param :${name} boolean`;
|
|
112
|
+
if (name === PAGE_BIND && page !== undefined)
|
|
113
|
+
return `-- param :${name} integer`;
|
|
114
|
+
return fail('SYQL7002_INTERNAL_LOWERING', query, `generated SQL contains unresolved bind :${name}`);
|
|
115
|
+
})
|
|
116
|
+
.join('\n');
|
|
117
|
+
}
|
|
118
|
+
function planBind(name, valueOwners, conditionBinds, conditions, page, query) {
|
|
119
|
+
const value = valueOwners.get(name);
|
|
120
|
+
if (value?.kind === 'value') {
|
|
121
|
+
return { kind: 'value', name, type: value.type, input: value.input };
|
|
122
|
+
}
|
|
123
|
+
if (value?.kind === 'group-member' && value.member !== undefined) {
|
|
124
|
+
return {
|
|
125
|
+
kind: 'group-member',
|
|
126
|
+
name,
|
|
127
|
+
type: value.type,
|
|
128
|
+
input: value.input,
|
|
129
|
+
member: value.member,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const condition = conditionBinds.get(name);
|
|
133
|
+
if (condition !== undefined) {
|
|
134
|
+
return {
|
|
135
|
+
kind: 'condition-active',
|
|
136
|
+
name,
|
|
137
|
+
type: 'boolean',
|
|
138
|
+
condition,
|
|
139
|
+
controls: conditions[condition].controls,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (name === PAGE_BIND && page !== undefined) {
|
|
143
|
+
return {
|
|
144
|
+
kind: 'page',
|
|
145
|
+
name,
|
|
146
|
+
type: 'integer',
|
|
147
|
+
input: page.control,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return fail('SYQL7002_INTERNAL_LOWERING', query, `cannot describe generated bind :${name}`);
|
|
151
|
+
}
|
|
152
|
+
function publicInputs(validated, naming) {
|
|
153
|
+
const query = validated.logical;
|
|
154
|
+
const declaration = query.declaration;
|
|
155
|
+
const publicNames = [
|
|
156
|
+
...declaration.parameters.map((parameter) => parameter.name),
|
|
157
|
+
...(declaration.sort === undefined ? [] : [declaration.sort.control]),
|
|
158
|
+
...(declaration.page === undefined ? [] : [declaration.page.control]),
|
|
159
|
+
];
|
|
160
|
+
const mapped = new Map(buildNamingMap(publicNames, naming.naming, query.module.file, `query ${declaration.name} public inputs`, naming.targets).map((entry) => [entry.sqlName, entry.langName]));
|
|
161
|
+
const result = declaration.parameters.map((parameter) => {
|
|
162
|
+
const langName = mapped.get(parameter.name);
|
|
163
|
+
if (parameter.kind === 'switch') {
|
|
164
|
+
return {
|
|
165
|
+
kind: 'switch',
|
|
166
|
+
name: parameter.name,
|
|
167
|
+
langName,
|
|
168
|
+
default: false,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (parameter.kind === 'group') {
|
|
172
|
+
const members = buildNamingMap(parameter.members.map((member) => member.name), naming.naming, query.module.file, `query ${declaration.name} group ${parameter.name}`, naming.targets);
|
|
173
|
+
return {
|
|
174
|
+
kind: 'group',
|
|
175
|
+
name: parameter.name,
|
|
176
|
+
langName,
|
|
177
|
+
members: parameter.members.map((member, index) => {
|
|
178
|
+
const type = validated.bindTypes.get(member.name);
|
|
179
|
+
if (type === undefined) {
|
|
180
|
+
return fail('SYQL7002_INTERNAL_LOWERING', query, `group member ${parameter.name}.${member.name} has no resolved type`);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
name: member.name,
|
|
184
|
+
langName: members[index]
|
|
185
|
+
.langName,
|
|
186
|
+
type: type.base,
|
|
187
|
+
nullable: type.nullable,
|
|
188
|
+
};
|
|
189
|
+
}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const type = validated.bindTypes.get(parameter.name);
|
|
193
|
+
if (type === undefined) {
|
|
194
|
+
return fail('SYQL7002_INTERNAL_LOWERING', query, `input ${parameter.name} has no resolved type`);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
kind: 'value',
|
|
198
|
+
name: parameter.name,
|
|
199
|
+
langName,
|
|
200
|
+
type: type.base,
|
|
201
|
+
nullable: type.nullable,
|
|
202
|
+
required: !parameter.optional,
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
if (validated.sort !== undefined) {
|
|
206
|
+
const profiles = buildNamingMap(validated.sort.profiles.map((profile) => profile.name), naming.naming, query.module.file, `query ${declaration.name} sort profiles`, naming.targets);
|
|
207
|
+
result.push({
|
|
208
|
+
kind: 'sort',
|
|
209
|
+
name: validated.sort.control,
|
|
210
|
+
langName: mapped.get(validated.sort.control),
|
|
211
|
+
defaultProfile: validated.sort.defaultProfile,
|
|
212
|
+
profiles: validated.sort.profiles.map((profile, index) => ({
|
|
213
|
+
name: profile.name,
|
|
214
|
+
langName: profiles[index].langName,
|
|
215
|
+
})),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (validated.page !== undefined) {
|
|
219
|
+
result.push({
|
|
220
|
+
kind: 'page',
|
|
221
|
+
name: validated.page.control,
|
|
222
|
+
langName: mapped.get(validated.page.control),
|
|
223
|
+
defaultSize: validated.page.defaultSize,
|
|
224
|
+
maxSize: validated.page.maxSize,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
function valueOwners(validated) {
|
|
230
|
+
const owners = new Map();
|
|
231
|
+
for (const parameter of validated.logical.declaration.parameters) {
|
|
232
|
+
if (parameter.kind === 'switch')
|
|
233
|
+
continue;
|
|
234
|
+
if (parameter.kind === 'group') {
|
|
235
|
+
for (const member of parameter.members) {
|
|
236
|
+
const type = validated.bindTypes.get(member.name);
|
|
237
|
+
if (type === undefined)
|
|
238
|
+
continue;
|
|
239
|
+
owners.set(member.name, {
|
|
240
|
+
kind: 'group-member',
|
|
241
|
+
input: parameter.name,
|
|
242
|
+
member: member.name,
|
|
243
|
+
type: type.base,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const type = validated.bindTypes.get(parameter.name);
|
|
249
|
+
if (type === undefined)
|
|
250
|
+
continue;
|
|
251
|
+
owners.set(parameter.name, {
|
|
252
|
+
kind: 'value',
|
|
253
|
+
input: parameter.name,
|
|
254
|
+
type: type.base,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return owners;
|
|
259
|
+
}
|
|
260
|
+
function activationControls(query) {
|
|
261
|
+
const used = new Set(query.conditions.flatMap((condition) => condition.controls));
|
|
262
|
+
return query.declaration.parameters
|
|
263
|
+
.map((parameter) => parameter.name)
|
|
264
|
+
.filter((name) => used.has(name));
|
|
265
|
+
}
|
|
266
|
+
function lowerStatement(validated, ir, db, naming, sql, sortProfile, activationMask, owners, conditionBinds) {
|
|
267
|
+
const query = validated.logical;
|
|
268
|
+
const location = `${query.module.file} (query ${query.declaration.name}, generated)`;
|
|
269
|
+
const names = distinctBindNames(sql, location);
|
|
270
|
+
const headers = headersFor(names, owners, conditionBinds, validated.page, query);
|
|
271
|
+
const candidate = headers.length === 0 ? sql : `${headers}\n${sql}`;
|
|
272
|
+
const analyzed = analyzeStatement(query.declaration.name, location, candidate, ir, db, {
|
|
273
|
+
...naming,
|
|
274
|
+
internalParams: [
|
|
275
|
+
...conditionBinds.keys(),
|
|
276
|
+
...(validated.page === undefined ? [] : [PAGE_BIND]),
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
const namedSql = headers.length === 0
|
|
280
|
+
? analyzed.sql.trim()
|
|
281
|
+
: analyzed.sql.slice(headers.length).trimStart();
|
|
282
|
+
const bindNames = distinctBindNames(namedSql, location);
|
|
283
|
+
return {
|
|
284
|
+
statement: {
|
|
285
|
+
...(sortProfile === undefined ? {} : { sortProfile }),
|
|
286
|
+
...(activationMask === undefined ? {} : { activationMask }),
|
|
287
|
+
sql: namedSql,
|
|
288
|
+
positionalSql: analyzed.positionalSql,
|
|
289
|
+
binds: bindNames.map((name) => planBind(name, owners, conditionBinds, query.conditions, validated.page, query)),
|
|
290
|
+
},
|
|
291
|
+
analysis: { ...analyzed, sourceSql: namedSql, sql: namedSql },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function sortProfiles(validated) {
|
|
295
|
+
return validated.sort === undefined
|
|
296
|
+
? [{}]
|
|
297
|
+
: validated.sort.profiles.map((profile) => ({
|
|
298
|
+
name: profile.name,
|
|
299
|
+
sql: profile.sql,
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
/** Lower one validated revision-1 query and prepare every generated physical
|
|
303
|
+
* statement. The selected plan is embedded into the returned shared QueryIR
|
|
304
|
+
* query; the second plan is retained for equivalence conformance tests. */
|
|
305
|
+
export function lowerSyqlQuery(validated, ir, db, naming = DEFAULT_NAMING, options = {}) {
|
|
306
|
+
const logical = validated.logical;
|
|
307
|
+
const conditions = new Map(logical.conditions.map((condition, index) => [condition, index]));
|
|
308
|
+
const controls = activationControls(logical);
|
|
309
|
+
const owners = valueOwners(validated);
|
|
310
|
+
const conditionBinds = new Map(logical.conditions.map((_, index) => [`__syqlActive${index}`, index]));
|
|
311
|
+
const profiles = sortProfiles(validated);
|
|
312
|
+
const neutralStatements = [];
|
|
313
|
+
let neutralDefault;
|
|
314
|
+
for (const profile of profiles) {
|
|
315
|
+
const body = renderTemplate(logical.template, conditions, {
|
|
316
|
+
kind: 'neutralized',
|
|
317
|
+
});
|
|
318
|
+
const sql = composeControls(body, logical.module.file, profile.sql, validated.page);
|
|
319
|
+
const lowered = lowerStatement(validated, ir, db, naming, sql, profile.name, undefined, owners, conditionBinds);
|
|
320
|
+
neutralStatements.push(lowered.statement);
|
|
321
|
+
if (neutralDefault === undefined ||
|
|
322
|
+
profile.name === validated.sort?.defaultProfile) {
|
|
323
|
+
neutralDefault = lowered.analysis;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const neutralized = {
|
|
327
|
+
backend: 'neutralize',
|
|
328
|
+
activationControls: controls,
|
|
329
|
+
conditions: logical.conditions.map((condition, index) => ({
|
|
330
|
+
controls: condition.controls,
|
|
331
|
+
bind: `__syqlActive${index}`,
|
|
332
|
+
})),
|
|
333
|
+
statements: neutralStatements,
|
|
334
|
+
};
|
|
335
|
+
const maxStatements = options.maxEnumeratedStatements ?? DEFAULT_ENUMERATION_LIMIT;
|
|
336
|
+
const activationCount = 2 ** controls.length;
|
|
337
|
+
const enumeratedCount = activationCount * profiles.length;
|
|
338
|
+
let enumerated;
|
|
339
|
+
let enumeratedDefault;
|
|
340
|
+
if (Number.isSafeInteger(activationCount) &&
|
|
341
|
+
enumeratedCount <= maxStatements) {
|
|
342
|
+
const statements = [];
|
|
343
|
+
for (let mask = 0; mask < activationCount; mask += 1) {
|
|
344
|
+
const active = new Set(controls.filter((_, index) => (mask & (2 ** index)) !== 0));
|
|
345
|
+
for (const profile of profiles) {
|
|
346
|
+
const body = renderTemplate(logical.template, conditions, {
|
|
347
|
+
kind: 'enumerated',
|
|
348
|
+
active,
|
|
349
|
+
});
|
|
350
|
+
const sql = composeControls(body, logical.module.file, profile.sql, validated.page);
|
|
351
|
+
const lowered = lowerStatement(validated, ir, db, naming, sql, profile.name, mask, owners, new Map());
|
|
352
|
+
statements.push(lowered.statement);
|
|
353
|
+
if (mask === 0 &&
|
|
354
|
+
(enumeratedDefault === undefined ||
|
|
355
|
+
profile.name === validated.sort?.defaultProfile)) {
|
|
356
|
+
enumeratedDefault = lowered.analysis;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
enumerated = {
|
|
361
|
+
backend: 'variants',
|
|
362
|
+
activationControls: controls,
|
|
363
|
+
conditions: logical.conditions.map((condition) => ({
|
|
364
|
+
controls: condition.controls,
|
|
365
|
+
})),
|
|
366
|
+
statements,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const policy = options.backend ?? naming.backend ?? 'auto';
|
|
370
|
+
let selected;
|
|
371
|
+
let canonical;
|
|
372
|
+
if (policy === 'variants') {
|
|
373
|
+
if (enumerated === undefined || enumeratedDefault === undefined) {
|
|
374
|
+
return fail('SYQL7001_ENUMERATION_LIMIT', logical, `${enumeratedCount} generated statements exceed the compiler limit ${maxStatements}; use auto/neutralize or raise the diagnostic limit`);
|
|
375
|
+
}
|
|
376
|
+
selected = enumerated;
|
|
377
|
+
canonical = enumeratedDefault;
|
|
378
|
+
}
|
|
379
|
+
else if (policy === 'auto' &&
|
|
380
|
+
controls.length <= 2 &&
|
|
381
|
+
enumerated !== undefined &&
|
|
382
|
+
enumeratedDefault !== undefined) {
|
|
383
|
+
selected = enumerated;
|
|
384
|
+
canonical = enumeratedDefault;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
selected = neutralized;
|
|
388
|
+
canonical = neutralDefault;
|
|
389
|
+
}
|
|
390
|
+
const metadata = {
|
|
391
|
+
revision: 1,
|
|
392
|
+
inputs: publicInputs(validated, naming),
|
|
393
|
+
plan: selected,
|
|
394
|
+
...(validated.identity === undefined
|
|
395
|
+
? {}
|
|
396
|
+
: { identity: validated.identity }),
|
|
397
|
+
};
|
|
398
|
+
const analysis = {
|
|
399
|
+
...canonical,
|
|
400
|
+
file: logical.module.file,
|
|
401
|
+
reactive: validated.reactive,
|
|
402
|
+
syql: metadata,
|
|
403
|
+
};
|
|
404
|
+
return {
|
|
405
|
+
validated,
|
|
406
|
+
analysis,
|
|
407
|
+
selected,
|
|
408
|
+
neutralized,
|
|
409
|
+
...(enumerated === undefined ? {} : { enumerated }),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revision-1 SYQL module/name/signature semantics (§§4–8).
|
|
3
|
+
*
|
|
4
|
+
* This pass deliberately runs before SQLite/schema analysis. It resolves and
|
|
5
|
+
* expands predicates using token nodes, proves authoritative query inputs and
|
|
6
|
+
* `when` dominance, and produces a backend-neutral logical template.
|
|
7
|
+
*/
|
|
8
|
+
import { type SyqlSourceSpan } from './syql-lexer.js';
|
|
9
|
+
import type { SyqlModuleGraph } from './syql-modules.js';
|
|
10
|
+
import type { SyqlPredicateDeclaration, SyqlQueryDeclaration, SyqlQueryParameter, SyqlSyntaxFile, SyqlValueType } from './syql-parser.js';
|
|
11
|
+
import type { SyqlReactiveDirective } from './syql-template-parser.js';
|
|
12
|
+
export type SyqlSemanticErrorCode = 'SYQL5001_UNKNOWN_PREDICATE' | 'SYQL5002_PREDICATE_CYCLE' | 'SYQL5003_PREDICATE_ARITY' | 'SYQL5004_CLOSED_PREDICATE' | 'SYQL5005_UNUSED_PREDICATE_PARAMETER' | 'SYQL5006_UNDECLARED_BIND' | 'SYQL5007_UNUSED_INPUT' | 'SYQL5008_INVALID_CONTROL' | 'SYQL5009_MISSING_DOMINANCE' | 'SYQL5010_UNUSED_CONTROL' | 'SYQL5011_TYPE_CONFLICT';
|
|
13
|
+
export interface SyqlResolvedPredicate {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly module: SyqlSyntaxFile;
|
|
16
|
+
readonly declaration: SyqlPredicateDeclaration;
|
|
17
|
+
}
|
|
18
|
+
export interface SyqlLogicalBind {
|
|
19
|
+
readonly kind: 'bind';
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly span: SyqlSourceSpan;
|
|
22
|
+
/** Outermost call-site first, authored bind last. */
|
|
23
|
+
readonly origins: readonly SyqlSourceSpan[];
|
|
24
|
+
}
|
|
25
|
+
export type SyqlSqlPart = {
|
|
26
|
+
readonly kind: 'text';
|
|
27
|
+
readonly text: string;
|
|
28
|
+
} | SyqlLogicalBind;
|
|
29
|
+
export interface SyqlLogicalSqlNode {
|
|
30
|
+
readonly kind: 'sql';
|
|
31
|
+
readonly parts: readonly SyqlSqlPart[];
|
|
32
|
+
readonly span: SyqlSourceSpan;
|
|
33
|
+
}
|
|
34
|
+
export interface SyqlLogicalPredicateNode {
|
|
35
|
+
readonly kind: 'predicate';
|
|
36
|
+
readonly predicate: SyqlResolvedPredicate;
|
|
37
|
+
readonly body: readonly SyqlLogicalTemplateNode[];
|
|
38
|
+
readonly span: SyqlSourceSpan;
|
|
39
|
+
}
|
|
40
|
+
export interface SyqlLogicalWhenNode {
|
|
41
|
+
readonly kind: 'when';
|
|
42
|
+
readonly controls: readonly string[];
|
|
43
|
+
readonly controlSpans: readonly SyqlSourceSpan[];
|
|
44
|
+
readonly body: readonly SyqlLogicalTemplateNode[];
|
|
45
|
+
readonly span: SyqlSourceSpan;
|
|
46
|
+
}
|
|
47
|
+
export interface SyqlLogicalReactiveNode {
|
|
48
|
+
readonly kind: 'scope' | 'cover';
|
|
49
|
+
readonly directive: SyqlReactiveDirective;
|
|
50
|
+
readonly span: SyqlSourceSpan;
|
|
51
|
+
}
|
|
52
|
+
export type SyqlLogicalTemplateNode = SyqlLogicalSqlNode | SyqlLogicalPredicateNode | SyqlLogicalWhenNode | SyqlLogicalReactiveNode;
|
|
53
|
+
export interface SyqlLogicalInput {
|
|
54
|
+
readonly parameter: SyqlQueryParameter;
|
|
55
|
+
/** Type declared in source or constrained by annotated predicate formals. */
|
|
56
|
+
readonly type?: SyqlValueType;
|
|
57
|
+
}
|
|
58
|
+
export interface SyqlLogicalQuery {
|
|
59
|
+
readonly module: SyqlSyntaxFile;
|
|
60
|
+
readonly declaration: SyqlQueryDeclaration;
|
|
61
|
+
readonly inputs: readonly SyqlLogicalInput[];
|
|
62
|
+
readonly template: readonly SyqlLogicalTemplateNode[];
|
|
63
|
+
readonly conditions: readonly SyqlLogicalWhenNode[];
|
|
64
|
+
/** Resolved source/predicate types by scalar or group-member bind name. */
|
|
65
|
+
readonly bindTypes: ReadonlyMap<string, SyqlValueType>;
|
|
66
|
+
}
|
|
67
|
+
export interface SyqlSemanticProgram {
|
|
68
|
+
readonly graph: SyqlModuleGraph;
|
|
69
|
+
readonly predicateScopes: ReadonlyMap<string, ReadonlyMap<string, SyqlResolvedPredicate>>;
|
|
70
|
+
readonly predicates: readonly SyqlResolvedPredicate[];
|
|
71
|
+
readonly queries: readonly SyqlLogicalQuery[];
|
|
72
|
+
}
|
|
73
|
+
/** Resolve and statically validate a complete reachable SYQL module graph. */
|
|
74
|
+
export declare function analyzeSyqlSemantics(graph: SyqlModuleGraph): SyqlSemanticProgram;
|
|
75
|
+
/** Losslessly render expanded SQL/predicate nodes, retaining `when` markers. */
|
|
76
|
+
export declare function renderSyqlLogicalTemplate(nodes: readonly SyqlLogicalTemplateNode[]): string;
|