@syncular/typegen 0.6.0 → 0.8.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 +47 -42
- package/dist/cli.js +1 -4
- package/dist/emit-queries-dart.js +9 -8
- package/dist/emit-queries-kotlin.js +9 -8
- package/dist/emit-queries-swift.js +9 -8
- package/dist/emit-queries.js +10 -13
- package/dist/fmt.js +29 -4
- package/dist/lsp.js +16 -28
- package/dist/query.d.ts +6 -11
- package/dist/query.js +29 -0
- package/dist/syql-ast.d.ts +12 -13
- package/dist/syql-ast.js +26 -20
- package/dist/syql-lowering.js +65 -42
- package/dist/syql-parser.d.ts +19 -18
- package/dist/syql-parser.js +341 -93
- package/dist/syql-semantics.d.ts +4 -7
- package/dist/syql-semantics.js +121 -65
- package/dist/syql-template-parser.d.ts +5 -20
- package/dist/syql-template-parser.js +116 -109
- package/dist/syql-validator.d.ts +2 -2
- package/dist/syql-validator.js +251 -224
- package/package.json +3 -3
- package/src/cli.ts +1 -3
- package/src/emit-queries-dart.ts +7 -7
- package/src/emit-queries-kotlin.ts +7 -7
- package/src/emit-queries-swift.ts +7 -7
- package/src/emit-queries.ts +9 -12
- package/src/fmt.ts +32 -4
- package/src/lsp.ts +16 -28
- package/src/query.ts +42 -12
- package/src/syql-ast.ts +38 -34
- package/src/syql-lowering.ts +69 -43
- package/src/syql-parser.ts +472 -134
- package/src/syql-semantics.ts +158 -87
- package/src/syql-template-parser.ts +119 -163
- package/src/syql-validator.ts +330 -336
package/dist/syql-parser.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { isSyqlTrivia, lexSyqlSource, SyqlFrontendError, } from './syql-lexer.js';
|
|
3
3
|
import { parseSyqlEmbeddedTemplate, } from './syql-template-parser.js';
|
|
4
4
|
const CAMEL_IDENT_RE = /^[a-z][A-Za-z0-9]*$/;
|
|
5
|
-
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6
5
|
const INTEGER_LITERAL_RE = /^(?:0|[1-9][0-9]*)$/;
|
|
7
6
|
const BASE_TYPES = new Set([
|
|
8
7
|
'string',
|
|
@@ -17,29 +16,29 @@ const BASE_TYPES = new Set([
|
|
|
17
16
|
const RESERVED_NAMES = new Set([
|
|
18
17
|
'as',
|
|
19
18
|
'blob_ref',
|
|
19
|
+
'bool',
|
|
20
20
|
'boolean',
|
|
21
21
|
'by',
|
|
22
22
|
'bytes',
|
|
23
|
-
'cover',
|
|
24
23
|
'crdt',
|
|
25
24
|
'default',
|
|
26
25
|
'float',
|
|
26
|
+
'false',
|
|
27
27
|
'from',
|
|
28
|
-
'identity',
|
|
29
28
|
'import',
|
|
30
29
|
'in',
|
|
31
30
|
'integer',
|
|
32
31
|
'json',
|
|
33
32
|
'max',
|
|
34
33
|
'null',
|
|
35
|
-
'
|
|
34
|
+
'limit',
|
|
35
|
+
'present',
|
|
36
36
|
'predicate',
|
|
37
37
|
'query',
|
|
38
|
-
'scope',
|
|
39
38
|
'sort',
|
|
40
|
-
'sql',
|
|
41
39
|
'string',
|
|
42
|
-
'
|
|
40
|
+
'sync',
|
|
41
|
+
'true',
|
|
43
42
|
'when',
|
|
44
43
|
]);
|
|
45
44
|
function spanBetween(start, end) {
|
|
@@ -89,14 +88,14 @@ class Parser {
|
|
|
89
88
|
declarations.push(declaration);
|
|
90
89
|
predicates.push(declaration);
|
|
91
90
|
}
|
|
92
|
-
else if (keyword.text === 'query') {
|
|
93
|
-
const declaration = this.#parseQuery();
|
|
91
|
+
else if (keyword.text === 'query' || keyword.text === 'sync') {
|
|
92
|
+
const declaration = this.#parseQuery(keyword.text === 'sync');
|
|
94
93
|
this.#claimName(fileNames, declaration.name, declaration.nameSpan, 'declaration');
|
|
95
94
|
declarations.push(declaration);
|
|
96
95
|
queries.push(declaration);
|
|
97
96
|
}
|
|
98
97
|
else {
|
|
99
|
-
this.#fail('SYQL2008_INVALID_MEMBER', keyword, `expected import, predicate, or query; found ${JSON.stringify(keyword.text)}`);
|
|
98
|
+
this.#fail('SYQL2008_INVALID_MEMBER', keyword, `expected import, predicate, query, or sync query; found ${JSON.stringify(keyword.text)}`);
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
101
|
const first = this.#tokens[0];
|
|
@@ -201,41 +200,64 @@ class Parser {
|
|
|
201
200
|
nameSpan: name.span,
|
|
202
201
|
};
|
|
203
202
|
}
|
|
204
|
-
#parseQuery() {
|
|
205
|
-
const start = this.#expectText('
|
|
203
|
+
#parseQuery(sync) {
|
|
204
|
+
const start = sync ? this.#expectText('sync') : this.#peek();
|
|
205
|
+
this.#expectText('query');
|
|
206
206
|
const name = this.#parseCamelName('query name');
|
|
207
207
|
const parsedParameters = this.#parseQueryParameters();
|
|
208
|
-
|
|
209
|
-
this.#
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
208
|
+
let syncBy;
|
|
209
|
+
if (this.#peek().text === 'by') {
|
|
210
|
+
const by = this.#take();
|
|
211
|
+
if (!sync) {
|
|
212
|
+
this.#fail('SYQL2012_INVALID_QUERY_BODY', by, '`by table.scope` is valid only on a sync query');
|
|
213
|
+
}
|
|
214
|
+
const qualifier = this.#expectKind('identifier', 'sync table or alias');
|
|
215
|
+
this.#expectText('.');
|
|
216
|
+
const column = this.#expectKind('identifier', 'sync scope column');
|
|
217
|
+
syncBy = {
|
|
218
|
+
qualifier: qualifier.text,
|
|
219
|
+
column: column.text,
|
|
220
|
+
span: spanBetween(qualifier, column),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const declaredRanges = new Set(parsedParameters.parameters.flatMap((parameter) => parameter.kind === 'range' ? [parameter.name] : []));
|
|
224
|
+
const body = this.#parseQueryBody(parsedParameters.publicNames, declaredRanges);
|
|
225
|
+
const implicitRanges = this.#rangeShorthandNames(body.statement.tokens, declaredRanges);
|
|
226
|
+
const parameters = parsedParameters.parameters.map((parameter) => {
|
|
227
|
+
if (!implicitRanges.has(parameter.name))
|
|
228
|
+
return parameter;
|
|
229
|
+
if (parameter.kind === 'group') {
|
|
230
|
+
this.#fail('SYQL2011_INVALID_PARAMETER', this.#tokenAt(parameter.nameSpan), `range shorthand :${parameter.name} must name a scalar query input`);
|
|
231
|
+
}
|
|
232
|
+
if (parameter.kind === 'range')
|
|
233
|
+
return parameter;
|
|
234
|
+
if (parameter.default !== undefined) {
|
|
235
|
+
this.#fail('SYQL2011_INVALID_PARAMETER', this.#tokenAt(parameter.nameSpan), `range input ${parameter.name} cannot have a boolean default`);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
kind: 'range',
|
|
239
|
+
name: parameter.name,
|
|
240
|
+
optional: parameter.optional,
|
|
241
|
+
...(parameter.type === undefined ? {} : { type: parameter.type }),
|
|
242
|
+
span: parameter.span,
|
|
243
|
+
nameSpan: parameter.nameSpan,
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
for (const parameter of parameters) {
|
|
247
|
+
if (parameter.kind === 'range' && !implicitRanges.has(parameter.name)) {
|
|
248
|
+
this.#fail('SYQL2011_INVALID_PARAMETER', this.#tokenAt(parameter.nameSpan), `range input ${parameter.name} must be used as BETWEEN :${parameter.name}`);
|
|
249
|
+
}
|
|
228
250
|
}
|
|
229
|
-
const close = this.#expectText('}');
|
|
230
251
|
return {
|
|
231
252
|
kind: 'query',
|
|
232
253
|
name: name.text,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
...(
|
|
238
|
-
|
|
254
|
+
sync,
|
|
255
|
+
...(syncBy === undefined ? {} : { syncBy }),
|
|
256
|
+
parameters,
|
|
257
|
+
statement: body.statement,
|
|
258
|
+
...(body.sort === undefined ? {} : { sort: body.sort }),
|
|
259
|
+
...(body.limit === undefined ? {} : { limit: body.limit }),
|
|
260
|
+
span: spanBetween(start, body.close),
|
|
239
261
|
nameSpan: name.span,
|
|
240
262
|
};
|
|
241
263
|
}
|
|
@@ -250,7 +272,13 @@ class Parser {
|
|
|
250
272
|
const optional = this.#peek().text === '?';
|
|
251
273
|
if (optional)
|
|
252
274
|
this.#take();
|
|
253
|
-
if (optional &&
|
|
275
|
+
if (optional &&
|
|
276
|
+
this.#peek().text === ':' &&
|
|
277
|
+
this.#peekSignificantAfter(1)?.text === '{') {
|
|
278
|
+
this.#take();
|
|
279
|
+
if (this.#peek().text !== '{') {
|
|
280
|
+
this.#fail('SYQL2011_INVALID_PARAMETER', this.#peek(), 'an optional structured input must use `name?: { ... }`');
|
|
281
|
+
}
|
|
254
282
|
if (publicNames.has(name.text)) {
|
|
255
283
|
this.#duplicate(name, 'public query input');
|
|
256
284
|
}
|
|
@@ -278,10 +306,10 @@ class Parser {
|
|
|
278
306
|
if (this.#peek().text !== ',')
|
|
279
307
|
break;
|
|
280
308
|
this.#take();
|
|
281
|
-
if (this.#peek().text === '
|
|
309
|
+
if (this.#peek().text === '}')
|
|
282
310
|
break;
|
|
283
311
|
}
|
|
284
|
-
const groupClose = this.#expectText('
|
|
312
|
+
const groupClose = this.#expectText('}');
|
|
285
313
|
if (members.length < 2) {
|
|
286
314
|
this.#fail('SYQL2011_INVALID_PARAMETER', name, `group ${name.text} must contain at least two members`);
|
|
287
315
|
}
|
|
@@ -302,15 +330,21 @@ class Parser {
|
|
|
302
330
|
if (this.#peek().text === ':') {
|
|
303
331
|
const colon = this.#take();
|
|
304
332
|
const typeName = this.#expectKind('identifier', 'parameter type');
|
|
305
|
-
if (typeName.text === '
|
|
306
|
-
if (
|
|
307
|
-
this.#
|
|
333
|
+
if (typeName.text === 'range') {
|
|
334
|
+
if (bindNames.has(name.text)) {
|
|
335
|
+
this.#duplicate(name, 'query bind');
|
|
308
336
|
}
|
|
337
|
+
bindNames.add(name.text);
|
|
338
|
+
this.#expectText('<');
|
|
339
|
+
const elementName = this.#expectKind('identifier', 'range element type');
|
|
340
|
+
const type = this.#parseValueTypeAfterName(colon, elementName);
|
|
341
|
+
const close = this.#expectText('>');
|
|
309
342
|
parameters.push({
|
|
310
|
-
kind: '
|
|
343
|
+
kind: 'range',
|
|
311
344
|
name: name.text,
|
|
312
|
-
optional
|
|
313
|
-
|
|
345
|
+
optional,
|
|
346
|
+
type,
|
|
347
|
+
span: spanBetween(name, close),
|
|
314
348
|
nameSpan: name.span,
|
|
315
349
|
});
|
|
316
350
|
}
|
|
@@ -320,12 +354,26 @@ class Parser {
|
|
|
320
354
|
this.#duplicate(name, 'query bind');
|
|
321
355
|
}
|
|
322
356
|
bindNames.add(name.text);
|
|
357
|
+
let end = type.span.end;
|
|
358
|
+
let defaultValue;
|
|
359
|
+
if (this.#peek().text === '=') {
|
|
360
|
+
this.#take();
|
|
361
|
+
const value = this.#expectText('false');
|
|
362
|
+
if (type.base !== 'boolean' || type.nullable || optional) {
|
|
363
|
+
this.#fail('SYQL2011_INVALID_PARAMETER', value, 'only a non-optional bool parameter may declare `= false`');
|
|
364
|
+
}
|
|
365
|
+
defaultValue = false;
|
|
366
|
+
end = value.span.end;
|
|
367
|
+
}
|
|
323
368
|
parameters.push({
|
|
324
369
|
kind: 'value',
|
|
325
370
|
name: name.text,
|
|
326
371
|
optional,
|
|
327
372
|
type,
|
|
328
|
-
|
|
373
|
+
...(defaultValue === undefined
|
|
374
|
+
? {}
|
|
375
|
+
: { default: defaultValue }),
|
|
376
|
+
span: spanFromPositions(this.#file, name.span.start, end),
|
|
329
377
|
nameSpan: name.span,
|
|
330
378
|
});
|
|
331
379
|
}
|
|
@@ -356,17 +404,201 @@ class Parser {
|
|
|
356
404
|
this.#expectText(')');
|
|
357
405
|
return { parameters, publicNames, bindNames };
|
|
358
406
|
}
|
|
359
|
-
#
|
|
360
|
-
const
|
|
361
|
-
const
|
|
407
|
+
#parseQueryBody(publicNames, declaredRanges) {
|
|
408
|
+
const open = this.#expectText('{');
|
|
409
|
+
const bodyStart = this.#index;
|
|
410
|
+
let cursor = bodyStart;
|
|
411
|
+
let braceDepth = 1;
|
|
412
|
+
let parenDepth = 0;
|
|
413
|
+
let terminatorIndex;
|
|
414
|
+
while (cursor < this.#tokens.length) {
|
|
415
|
+
const token = this.#tokens[cursor];
|
|
416
|
+
if (token.kind === 'eof') {
|
|
417
|
+
this.#fail('SYQL2001_EXPECTED_TOKEN', token, 'expected ; and } to close query body');
|
|
418
|
+
}
|
|
419
|
+
if (token.text === '(')
|
|
420
|
+
parenDepth += 1;
|
|
421
|
+
else if (token.text === ')')
|
|
422
|
+
parenDepth -= 1;
|
|
423
|
+
else if (token.text === '{')
|
|
424
|
+
braceDepth += 1;
|
|
425
|
+
else if (token.text === '}') {
|
|
426
|
+
if (braceDepth === 1) {
|
|
427
|
+
this.#fail('SYQL2012_INVALID_QUERY_BODY', token, 'a query statement must end with a semicolon');
|
|
428
|
+
}
|
|
429
|
+
braceDepth -= 1;
|
|
430
|
+
}
|
|
431
|
+
else if (token.text === ';' && braceDepth === 1 && parenDepth === 0) {
|
|
432
|
+
terminatorIndex = cursor;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
cursor += 1;
|
|
436
|
+
}
|
|
437
|
+
const terminator = this.#tokens[terminatorIndex];
|
|
438
|
+
let dynamicSortIndex;
|
|
439
|
+
let dynamicLimitIndex;
|
|
440
|
+
braceDepth = 1;
|
|
441
|
+
parenDepth = 0;
|
|
442
|
+
for (cursor = bodyStart; cursor < terminatorIndex; cursor += 1) {
|
|
443
|
+
const token = this.#tokens[cursor];
|
|
444
|
+
if (isSyqlTrivia(token))
|
|
445
|
+
continue;
|
|
446
|
+
if (token.text === '(')
|
|
447
|
+
parenDepth += 1;
|
|
448
|
+
else if (token.text === ')')
|
|
449
|
+
parenDepth -= 1;
|
|
450
|
+
if (braceDepth === 1 && parenDepth === 0) {
|
|
451
|
+
const lower = token.text.toLowerCase();
|
|
452
|
+
if (lower === 'order' &&
|
|
453
|
+
this.#significantTextAfter(cursor, 1) === 'by' &&
|
|
454
|
+
this.#significantTextAfter(cursor, 3) === 'default' &&
|
|
455
|
+
this.#significantTextAfter(cursor, 5) === '{') {
|
|
456
|
+
dynamicSortIndex ??= cursor;
|
|
457
|
+
}
|
|
458
|
+
else if (lower === 'limit' &&
|
|
459
|
+
this.#significantTextAfter(cursor, 2) === 'default' &&
|
|
460
|
+
this.#significantTextAfter(cursor, 4) === 'max') {
|
|
461
|
+
dynamicLimitIndex ??= cursor;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (token.text === '{')
|
|
465
|
+
braceDepth += 1;
|
|
466
|
+
else if (token.text === '}')
|
|
467
|
+
braceDepth -= 1;
|
|
468
|
+
}
|
|
469
|
+
if (dynamicSortIndex !== undefined &&
|
|
470
|
+
dynamicLimitIndex !== undefined &&
|
|
471
|
+
dynamicLimitIndex < dynamicSortIndex) {
|
|
472
|
+
this.#fail('SYQL2012_INVALID_QUERY_BODY', this.#tokens[dynamicLimitIndex], 'dynamic ORDER BY must precede dynamic LIMIT');
|
|
473
|
+
}
|
|
474
|
+
const sqlEnd = Math.min(dynamicSortIndex ?? Number.MAX_SAFE_INTEGER, dynamicLimitIndex ?? Number.MAX_SAFE_INTEGER, terminatorIndex);
|
|
475
|
+
const statementTokens = this.#tokens.slice(bodyStart, sqlEnd);
|
|
476
|
+
const rangeNames = this.#rangeShorthandNames(statementTokens, declaredRanges);
|
|
477
|
+
const statement = this.#makeTemplate(statementTokens, 'query statement', 'statement', rangeNames, open.span.end);
|
|
478
|
+
let sort;
|
|
479
|
+
let limit;
|
|
480
|
+
this.#index = sqlEnd;
|
|
481
|
+
if (dynamicSortIndex !== undefined) {
|
|
482
|
+
sort = this.#parseInlineSort(publicNames);
|
|
483
|
+
}
|
|
484
|
+
if (dynamicLimitIndex !== undefined) {
|
|
485
|
+
limit = this.#parseLimitDeclaration(publicNames);
|
|
486
|
+
}
|
|
487
|
+
const end = this.#expectText(';');
|
|
488
|
+
if (end !== terminator) {
|
|
489
|
+
this.#fail('SYQL2012_INVALID_QUERY_BODY', end, 'unexpected query member before the statement terminator');
|
|
490
|
+
}
|
|
491
|
+
const close = this.#expectText('}');
|
|
492
|
+
return {
|
|
493
|
+
statement,
|
|
494
|
+
...(sort === undefined ? {} : { sort }),
|
|
495
|
+
...(limit === undefined ? {} : { limit }),
|
|
496
|
+
close,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
#rangeShorthandNames(tokens, declaredRanges = new Set()) {
|
|
500
|
+
const significant = tokens.filter((token) => !isSyqlTrivia(token));
|
|
501
|
+
const names = new Set();
|
|
502
|
+
const clauseEnd = new Set([
|
|
503
|
+
'group',
|
|
504
|
+
'having',
|
|
505
|
+
'order',
|
|
506
|
+
'limit',
|
|
507
|
+
'offset',
|
|
508
|
+
'window',
|
|
509
|
+
'union',
|
|
510
|
+
'intersect',
|
|
511
|
+
'except',
|
|
512
|
+
]);
|
|
513
|
+
for (let index = 0; index < significant.length; index += 1) {
|
|
514
|
+
const token = significant[index];
|
|
515
|
+
if (token.kind !== 'identifier' || token.text.toLowerCase() !== 'between')
|
|
516
|
+
continue;
|
|
517
|
+
const bind = significant[index + 1];
|
|
518
|
+
if (bind?.kind !== 'bind')
|
|
519
|
+
continue;
|
|
520
|
+
const name = bind.text.slice(1);
|
|
521
|
+
const next = significant[index + 2];
|
|
522
|
+
const afterNext = significant[index + 3];
|
|
523
|
+
let conjunctStart = index - 1;
|
|
524
|
+
while (conjunctStart >= 0 &&
|
|
525
|
+
!(significant[conjunctStart]?.kind === 'identifier' &&
|
|
526
|
+
significant[conjunctStart]?.text.toLowerCase() === 'and')) {
|
|
527
|
+
conjunctStart -= 1;
|
|
528
|
+
}
|
|
529
|
+
const controlledByRange = significant
|
|
530
|
+
.slice(conjunctStart + 1, index)
|
|
531
|
+
.some((candidate, candidateIndex, conjunct) => {
|
|
532
|
+
if (candidate.kind !== 'identifier' ||
|
|
533
|
+
candidate.text.toLowerCase() !== 'when' ||
|
|
534
|
+
conjunct[candidateIndex + 1]?.text !== '(') {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
let depth = 0;
|
|
538
|
+
for (let controlIndex = candidateIndex + 1; controlIndex < conjunct.length; controlIndex += 1) {
|
|
539
|
+
const control = conjunct[controlIndex];
|
|
540
|
+
if (control.text === '(')
|
|
541
|
+
depth += 1;
|
|
542
|
+
else if (control.text === ')') {
|
|
543
|
+
depth -= 1;
|
|
544
|
+
if (depth === 0)
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
else if (depth > 0 &&
|
|
548
|
+
control.kind === 'identifier' &&
|
|
549
|
+
control.text === name) {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return false;
|
|
554
|
+
});
|
|
555
|
+
const endsRange = declaredRanges.has(name) ||
|
|
556
|
+
controlledByRange ||
|
|
557
|
+
next === undefined ||
|
|
558
|
+
next.text === '}' ||
|
|
559
|
+
(next.kind === 'identifier' &&
|
|
560
|
+
clauseEnd.has(next.text.toLowerCase())) ||
|
|
561
|
+
(next.kind === 'identifier' &&
|
|
562
|
+
next.text.toLowerCase() === 'and' &&
|
|
563
|
+
afterNext?.kind === 'identifier' &&
|
|
564
|
+
afterNext.text === 'when');
|
|
565
|
+
if (endsRange)
|
|
566
|
+
names.add(name);
|
|
567
|
+
}
|
|
568
|
+
return names;
|
|
569
|
+
}
|
|
570
|
+
#makeTemplate(tokens, label, mode, rangeNames = new Set(), fallbackStart) {
|
|
571
|
+
const semantic = tokens.filter((token) => !isSyqlTrivia(token));
|
|
572
|
+
if (semantic.length === 0) {
|
|
573
|
+
this.#fail('SYQL2006_EMPTY_TEMPLATE', semantic[0] ?? this.#peek(), `${label} must not be empty`);
|
|
574
|
+
}
|
|
575
|
+
const first = tokens[0] ?? semantic[0];
|
|
576
|
+
const last = tokens[tokens.length - 1] ?? semantic[semantic.length - 1];
|
|
577
|
+
const start = first?.span.start ?? fallbackStart ?? this.#peek().span.start;
|
|
578
|
+
const end = last?.span.end ?? start;
|
|
579
|
+
const span = spanFromPositions(this.#file, start, end);
|
|
362
580
|
return {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
581
|
+
text: this.#source.slice(start.offset, end.offset),
|
|
582
|
+
tokens,
|
|
583
|
+
tree: parseSyqlEmbeddedTemplate(this.#file, tokens, span, mode, rangeNames),
|
|
584
|
+
span,
|
|
366
585
|
};
|
|
367
586
|
}
|
|
368
|
-
#
|
|
369
|
-
|
|
587
|
+
#significantTextAfter(index, count) {
|
|
588
|
+
let remaining = count;
|
|
589
|
+
for (let cursor = index + 1; cursor < this.#tokens.length; cursor += 1) {
|
|
590
|
+
const token = this.#tokens[cursor];
|
|
591
|
+
if (isSyqlTrivia(token))
|
|
592
|
+
continue;
|
|
593
|
+
remaining -= 1;
|
|
594
|
+
if (remaining === 0)
|
|
595
|
+
return token.text.toLowerCase();
|
|
596
|
+
}
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
#parseInlineSort(publicNames) {
|
|
600
|
+
const start = this.#expectText('order');
|
|
601
|
+
this.#expectText('by');
|
|
370
602
|
const control = this.#parseCamelName('sort control');
|
|
371
603
|
if (publicNames.has(control.text)) {
|
|
372
604
|
this.#duplicate(control, 'public query input');
|
|
@@ -386,17 +618,37 @@ class Parser {
|
|
|
386
618
|
this.#duplicate(profileName, 'sort profile');
|
|
387
619
|
}
|
|
388
620
|
profileNames.add(profileName.text);
|
|
389
|
-
|
|
621
|
+
this.#expectText(':');
|
|
622
|
+
const contentStart = this.#index;
|
|
623
|
+
let cursor = contentStart;
|
|
624
|
+
let depth = 0;
|
|
625
|
+
while (cursor < this.#tokens.length) {
|
|
626
|
+
const token = this.#tokens[cursor];
|
|
627
|
+
if (token.kind === 'eof' || token.text === '}') {
|
|
628
|
+
this.#fail('SYQL2001_EXPECTED_TOKEN', token, 'expected ; after sort profile');
|
|
629
|
+
}
|
|
630
|
+
if (token.text === '(')
|
|
631
|
+
depth += 1;
|
|
632
|
+
else if (token.text === ')')
|
|
633
|
+
depth -= 1;
|
|
634
|
+
else if (token.text === ';' && depth === 0)
|
|
635
|
+
break;
|
|
636
|
+
cursor += 1;
|
|
637
|
+
}
|
|
638
|
+
const end = this.#tokens[cursor];
|
|
639
|
+
const orderTokens = this.#tokens.slice(contentStart, cursor);
|
|
640
|
+
const order = this.#makeTemplate(orderTokens, 'sort profile', 'order');
|
|
641
|
+
this.#index = cursor + 1;
|
|
390
642
|
profiles.push({
|
|
391
643
|
name: profileName.text,
|
|
392
|
-
order
|
|
393
|
-
span: spanBetween(profileName,
|
|
644
|
+
order,
|
|
645
|
+
span: spanBetween(profileName, end),
|
|
394
646
|
nameSpan: profileName.span,
|
|
395
647
|
});
|
|
396
648
|
}
|
|
397
649
|
const close = this.#expectText('}');
|
|
398
650
|
if (profiles.length === 0) {
|
|
399
|
-
this.#fail('SYQL2008_INVALID_MEMBER', close, 'a
|
|
651
|
+
this.#fail('SYQL2008_INVALID_MEMBER', close, 'a dynamic ORDER BY must contain at least one profile');
|
|
400
652
|
}
|
|
401
653
|
if (!profileNames.has(defaultProfile.text)) {
|
|
402
654
|
this.#fail('SYQL2008_INVALID_MEMBER', defaultProfile, `default sort profile ${JSON.stringify(defaultProfile.text)} is not declared`);
|
|
@@ -410,60 +662,39 @@ class Parser {
|
|
|
410
662
|
controlSpan: control.span,
|
|
411
663
|
};
|
|
412
664
|
}
|
|
413
|
-
#
|
|
414
|
-
const start = this.#expectText('
|
|
415
|
-
const control = this.#parseCamelName('
|
|
665
|
+
#parseLimitDeclaration(publicNames) {
|
|
666
|
+
const start = this.#expectText('limit');
|
|
667
|
+
const control = this.#parseCamelName('limit control');
|
|
416
668
|
if (publicNames.has(control.text)) {
|
|
417
669
|
this.#duplicate(control, 'public query input');
|
|
418
670
|
}
|
|
419
671
|
publicNames.add(control.text);
|
|
420
672
|
this.#expectText('default');
|
|
421
|
-
const defaultToken = this.#parseInteger('
|
|
673
|
+
const defaultToken = this.#parseInteger('limit default');
|
|
422
674
|
this.#expectText('max');
|
|
423
|
-
const maxToken = this.#parseInteger('
|
|
424
|
-
const end = this.#expectText(';');
|
|
675
|
+
const maxToken = this.#parseInteger('limit maximum');
|
|
425
676
|
const defaultSize = Number(defaultToken.text);
|
|
426
677
|
const maxSize = Number(maxToken.text);
|
|
427
678
|
if (defaultSize < 1 || defaultSize > maxSize || maxSize > 2_147_483_647) {
|
|
428
|
-
this.#fail('SYQL2010_INVALID_PAGE_RANGE', defaultToken, `
|
|
679
|
+
this.#fail('SYQL2010_INVALID_PAGE_RANGE', defaultToken, `limit bounds must satisfy 1 <= default <= max <= 2147483647; found default ${defaultSize}, max ${maxSize}`);
|
|
429
680
|
}
|
|
430
681
|
return {
|
|
431
|
-
kind: '
|
|
682
|
+
kind: 'limit',
|
|
432
683
|
control: control.text,
|
|
433
684
|
defaultSize,
|
|
434
685
|
maxSize,
|
|
435
|
-
span: spanBetween(start,
|
|
686
|
+
span: spanBetween(start, maxToken),
|
|
436
687
|
controlSpan: control.span,
|
|
437
688
|
};
|
|
438
689
|
}
|
|
439
|
-
#parseIdentityDeclaration() {
|
|
440
|
-
const start = this.#expectText('identity');
|
|
441
|
-
this.#expectText('by');
|
|
442
|
-
const fields = [];
|
|
443
|
-
const names = new Set();
|
|
444
|
-
for (;;) {
|
|
445
|
-
const field = this.#expectKind('identifier', 'identity result name');
|
|
446
|
-
if (!IDENT_RE.test(field.text)) {
|
|
447
|
-
this.#fail('SYQL2002_INVALID_NAME', field, `invalid identity result name ${JSON.stringify(field.text)}`);
|
|
448
|
-
}
|
|
449
|
-
if (names.has(field.text))
|
|
450
|
-
this.#duplicate(field, 'identity field');
|
|
451
|
-
names.add(field.text);
|
|
452
|
-
fields.push(field.text);
|
|
453
|
-
if (this.#peek().text !== ',')
|
|
454
|
-
break;
|
|
455
|
-
this.#take();
|
|
456
|
-
}
|
|
457
|
-
const end = this.#expectText(';');
|
|
458
|
-
return { kind: 'identity', fields, span: spanBetween(start, end) };
|
|
459
|
-
}
|
|
460
690
|
#parseTypeAnnotation() {
|
|
461
691
|
const colon = this.#expectText(':');
|
|
462
692
|
const name = this.#expectKind('identifier', 'value type');
|
|
463
693
|
return this.#parseValueTypeAfterName(colon, name);
|
|
464
694
|
}
|
|
465
695
|
#parseValueTypeAfterName(colon, name) {
|
|
466
|
-
|
|
696
|
+
const base = name.text === 'bool' ? 'boolean' : name.text;
|
|
697
|
+
if (name.text === 'boolean' || !BASE_TYPES.has(base)) {
|
|
467
698
|
this.#fail('SYQL2011_INVALID_PARAMETER', name, `unknown value type ${JSON.stringify(name.text)}`);
|
|
468
699
|
}
|
|
469
700
|
let end = name;
|
|
@@ -474,7 +705,7 @@ class Parser {
|
|
|
474
705
|
nullable = true;
|
|
475
706
|
}
|
|
476
707
|
return {
|
|
477
|
-
base:
|
|
708
|
+
base: base,
|
|
478
709
|
nullable,
|
|
479
710
|
span: spanBetween(colon, end),
|
|
480
711
|
};
|
|
@@ -561,6 +792,20 @@ class Parser {
|
|
|
561
792
|
}
|
|
562
793
|
return this.#tokens[index];
|
|
563
794
|
}
|
|
795
|
+
#peekSignificantAfter(count) {
|
|
796
|
+
let index = this.#index;
|
|
797
|
+
let remaining = count;
|
|
798
|
+
while (index < this.#tokens.length) {
|
|
799
|
+
const token = this.#tokens[index];
|
|
800
|
+
index += 1;
|
|
801
|
+
if (isSyqlTrivia(token))
|
|
802
|
+
continue;
|
|
803
|
+
if (remaining === 0)
|
|
804
|
+
return token;
|
|
805
|
+
remaining -= 1;
|
|
806
|
+
}
|
|
807
|
+
return undefined;
|
|
808
|
+
}
|
|
564
809
|
#take() {
|
|
565
810
|
while (this.#index < this.#tokens.length &&
|
|
566
811
|
isSyqlTrivia(this.#tokens[this.#index])) {
|
|
@@ -580,6 +825,9 @@ class Parser {
|
|
|
580
825
|
}
|
|
581
826
|
return undefined;
|
|
582
827
|
}
|
|
828
|
+
#tokenAt(span) {
|
|
829
|
+
return (this.#tokens.find((token) => token.span.start.offset === span.start.offset) ?? this.#peek());
|
|
830
|
+
}
|
|
583
831
|
#expectText(text) {
|
|
584
832
|
const token = this.#take();
|
|
585
833
|
if (token.text !== text) {
|
package/dist/syql-semantics.d.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import { type SyqlSourceSpan } from './syql-lexer.js';
|
|
9
9
|
import type { SyqlModuleGraph } from './syql-modules.js';
|
|
10
10
|
import type { SyqlPredicateDeclaration, SyqlQueryDeclaration, SyqlQueryParameter, SyqlSyntaxFile, SyqlValueType } from './syql-parser.js';
|
|
11
|
-
import type { SyqlReactiveDirective } from './syql-template-parser.js';
|
|
12
11
|
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
12
|
export interface SyqlResolvedPredicate {
|
|
14
13
|
readonly id: string;
|
|
@@ -40,16 +39,12 @@ export interface SyqlLogicalPredicateNode {
|
|
|
40
39
|
export interface SyqlLogicalWhenNode {
|
|
41
40
|
readonly kind: 'when';
|
|
42
41
|
readonly controls: readonly string[];
|
|
42
|
+
readonly explicitPresence: readonly boolean[];
|
|
43
43
|
readonly controlSpans: readonly SyqlSourceSpan[];
|
|
44
44
|
readonly body: readonly SyqlLogicalTemplateNode[];
|
|
45
45
|
readonly span: SyqlSourceSpan;
|
|
46
46
|
}
|
|
47
|
-
export
|
|
48
|
-
readonly kind: 'scope' | 'cover';
|
|
49
|
-
readonly directive: SyqlReactiveDirective;
|
|
50
|
-
readonly span: SyqlSourceSpan;
|
|
51
|
-
}
|
|
52
|
-
export type SyqlLogicalTemplateNode = SyqlLogicalSqlNode | SyqlLogicalPredicateNode | SyqlLogicalWhenNode | SyqlLogicalReactiveNode;
|
|
47
|
+
export type SyqlLogicalTemplateNode = SyqlLogicalSqlNode | SyqlLogicalPredicateNode | SyqlLogicalWhenNode;
|
|
53
48
|
export interface SyqlLogicalInput {
|
|
54
49
|
readonly parameter: SyqlQueryParameter;
|
|
55
50
|
/** Type declared in source or constrained by annotated predicate formals. */
|
|
@@ -70,6 +65,8 @@ export interface SyqlSemanticProgram {
|
|
|
70
65
|
readonly predicates: readonly SyqlResolvedPredicate[];
|
|
71
66
|
readonly queries: readonly SyqlLogicalQuery[];
|
|
72
67
|
}
|
|
68
|
+
export declare function syqlRangeStartBind(name: string): string;
|
|
69
|
+
export declare function syqlRangeEndBind(name: string): string;
|
|
73
70
|
/** Resolve and statically validate a complete reachable SYQL module graph. */
|
|
74
71
|
export declare function analyzeSyqlSemantics(graph: SyqlModuleGraph): SyqlSemanticProgram;
|
|
75
72
|
/** Losslessly render expanded SQL/predicate nodes, retaining `when` markers. */
|