@syncular/typegen 0.6.0 → 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 +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/src/syql-parser.ts
CHANGED
|
@@ -33,14 +33,18 @@ export interface SyqlValueParameter {
|
|
|
33
33
|
readonly name: string;
|
|
34
34
|
readonly optional: boolean;
|
|
35
35
|
readonly type?: SyqlValueType;
|
|
36
|
+
/** A source-level default. Revision 1 currently permits only `bool = false`. */
|
|
37
|
+
readonly default?: false;
|
|
36
38
|
readonly span: SyqlSourceSpan;
|
|
37
39
|
readonly nameSpan: SyqlSourceSpan;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
export interface
|
|
41
|
-
readonly kind: '
|
|
42
|
+
export interface SyqlRangeParameter {
|
|
43
|
+
readonly kind: 'range';
|
|
42
44
|
readonly name: string;
|
|
43
|
-
readonly optional:
|
|
45
|
+
readonly optional: boolean;
|
|
46
|
+
/** Element type, inferred from the left side of BETWEEN when omitted. */
|
|
47
|
+
readonly type?: SyqlValueType;
|
|
44
48
|
readonly span: SyqlSourceSpan;
|
|
45
49
|
readonly nameSpan: SyqlSourceSpan;
|
|
46
50
|
}
|
|
@@ -63,7 +67,7 @@ export interface SyqlGroupParameter {
|
|
|
63
67
|
|
|
64
68
|
export type SyqlQueryParameter =
|
|
65
69
|
| SyqlValueParameter
|
|
66
|
-
|
|
|
70
|
+
| SyqlRangeParameter
|
|
67
71
|
| SyqlGroupParameter;
|
|
68
72
|
|
|
69
73
|
export interface SyqlPredicateParameter {
|
|
@@ -106,12 +110,6 @@ export interface SyqlPredicateDeclaration {
|
|
|
106
110
|
readonly nameSpan: SyqlSourceSpan;
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
export interface SyqlSqlSection {
|
|
110
|
-
readonly kind: 'sql';
|
|
111
|
-
readonly body: SyqlTemplate;
|
|
112
|
-
readonly span: SyqlSourceSpan;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
113
|
export interface SyqlSortProfile {
|
|
116
114
|
readonly name: string;
|
|
117
115
|
readonly order: SyqlTemplate;
|
|
@@ -128,8 +126,8 @@ export interface SyqlSortSection {
|
|
|
128
126
|
readonly controlSpan: SyqlSourceSpan;
|
|
129
127
|
}
|
|
130
128
|
|
|
131
|
-
export interface
|
|
132
|
-
readonly kind: '
|
|
129
|
+
export interface SyqlLimitDeclaration {
|
|
130
|
+
readonly kind: 'limit';
|
|
133
131
|
readonly control: string;
|
|
134
132
|
readonly defaultSize: number;
|
|
135
133
|
readonly maxSize: number;
|
|
@@ -137,20 +135,22 @@ export interface SyqlPageDeclaration {
|
|
|
137
135
|
readonly controlSpan: SyqlSourceSpan;
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
export interface
|
|
141
|
-
readonly
|
|
142
|
-
readonly
|
|
138
|
+
export interface SyqlSyncDimension {
|
|
139
|
+
readonly qualifier: string;
|
|
140
|
+
readonly column: string;
|
|
143
141
|
readonly span: SyqlSourceSpan;
|
|
144
142
|
}
|
|
145
143
|
|
|
146
144
|
export interface SyqlQueryDeclaration {
|
|
147
145
|
readonly kind: 'query';
|
|
148
146
|
readonly name: string;
|
|
147
|
+
readonly sync: boolean;
|
|
148
|
+
readonly syncBy?: SyqlSyncDimension;
|
|
149
149
|
readonly parameters: readonly SyqlQueryParameter[];
|
|
150
|
-
|
|
150
|
+
/** The SQL-shaped statement body, excluding its terminating semicolon. */
|
|
151
|
+
readonly statement: SyqlTemplate;
|
|
151
152
|
readonly sort?: SyqlSortSection;
|
|
152
|
-
readonly
|
|
153
|
-
readonly identity?: SyqlIdentityDeclaration;
|
|
153
|
+
readonly limit?: SyqlLimitDeclaration;
|
|
154
154
|
readonly span: SyqlSourceSpan;
|
|
155
155
|
readonly nameSpan: SyqlSourceSpan;
|
|
156
156
|
}
|
|
@@ -180,10 +180,10 @@ export type SyqlParseErrorCode =
|
|
|
180
180
|
| 'SYQL2008_INVALID_MEMBER'
|
|
181
181
|
| 'SYQL2009_INVALID_INTEGER'
|
|
182
182
|
| 'SYQL2010_INVALID_PAGE_RANGE'
|
|
183
|
-
| 'SYQL2011_INVALID_PARAMETER'
|
|
183
|
+
| 'SYQL2011_INVALID_PARAMETER'
|
|
184
|
+
| 'SYQL2012_INVALID_QUERY_BODY';
|
|
184
185
|
|
|
185
186
|
const CAMEL_IDENT_RE = /^[a-z][A-Za-z0-9]*$/;
|
|
186
|
-
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
187
187
|
const INTEGER_LITERAL_RE = /^(?:0|[1-9][0-9]*)$/;
|
|
188
188
|
const BASE_TYPES = new Set<SyqlBaseType>([
|
|
189
189
|
'string',
|
|
@@ -198,29 +198,29 @@ const BASE_TYPES = new Set<SyqlBaseType>([
|
|
|
198
198
|
const RESERVED_NAMES = new Set([
|
|
199
199
|
'as',
|
|
200
200
|
'blob_ref',
|
|
201
|
+
'bool',
|
|
201
202
|
'boolean',
|
|
202
203
|
'by',
|
|
203
204
|
'bytes',
|
|
204
|
-
'cover',
|
|
205
205
|
'crdt',
|
|
206
206
|
'default',
|
|
207
207
|
'float',
|
|
208
|
+
'false',
|
|
208
209
|
'from',
|
|
209
|
-
'identity',
|
|
210
210
|
'import',
|
|
211
211
|
'in',
|
|
212
212
|
'integer',
|
|
213
213
|
'json',
|
|
214
214
|
'max',
|
|
215
215
|
'null',
|
|
216
|
-
'
|
|
216
|
+
'limit',
|
|
217
|
+
'present',
|
|
217
218
|
'predicate',
|
|
218
219
|
'query',
|
|
219
|
-
'scope',
|
|
220
220
|
'sort',
|
|
221
|
-
'sql',
|
|
222
221
|
'string',
|
|
223
|
-
'
|
|
222
|
+
'sync',
|
|
223
|
+
'true',
|
|
224
224
|
'when',
|
|
225
225
|
]);
|
|
226
226
|
|
|
@@ -236,6 +236,13 @@ interface ParsedQueryParameters {
|
|
|
236
236
|
readonly bindNames: Set<string>;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
interface ParsedQueryBody {
|
|
240
|
+
readonly statement: SyqlTemplate;
|
|
241
|
+
readonly sort?: SyqlSortSection;
|
|
242
|
+
readonly limit?: SyqlLimitDeclaration;
|
|
243
|
+
readonly close: SyqlToken;
|
|
244
|
+
}
|
|
245
|
+
|
|
239
246
|
function spanBetween(start: SyqlToken, end: SyqlToken): SyqlSourceSpan {
|
|
240
247
|
return {
|
|
241
248
|
file: start.span.file,
|
|
@@ -301,8 +308,8 @@ class Parser {
|
|
|
301
308
|
);
|
|
302
309
|
declarations.push(declaration);
|
|
303
310
|
predicates.push(declaration);
|
|
304
|
-
} else if (keyword.text === 'query') {
|
|
305
|
-
const declaration = this.#parseQuery();
|
|
311
|
+
} else if (keyword.text === 'query' || keyword.text === 'sync') {
|
|
312
|
+
const declaration = this.#parseQuery(keyword.text === 'sync');
|
|
306
313
|
this.#claimName(
|
|
307
314
|
fileNames,
|
|
308
315
|
declaration.name,
|
|
@@ -315,7 +322,7 @@ class Parser {
|
|
|
315
322
|
this.#fail(
|
|
316
323
|
'SYQL2008_INVALID_MEMBER',
|
|
317
324
|
keyword,
|
|
318
|
-
`expected import, predicate, or query; found ${JSON.stringify(keyword.text)}`,
|
|
325
|
+
`expected import, predicate, query, or sync query; found ${JSON.stringify(keyword.text)}`,
|
|
319
326
|
);
|
|
320
327
|
}
|
|
321
328
|
}
|
|
@@ -462,54 +469,89 @@ class Parser {
|
|
|
462
469
|
};
|
|
463
470
|
}
|
|
464
471
|
|
|
465
|
-
#parseQuery(): SyqlQueryDeclaration {
|
|
466
|
-
const start = this.#expectText('
|
|
472
|
+
#parseQuery(sync: boolean): SyqlQueryDeclaration {
|
|
473
|
+
const start = sync ? this.#expectText('sync') : this.#peek();
|
|
474
|
+
this.#expectText('query');
|
|
467
475
|
const name = this.#parseCamelName('query name');
|
|
468
476
|
const parsedParameters = this.#parseQueryParameters();
|
|
469
|
-
|
|
470
|
-
this.#
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
if (this.#peek().text === 'page') {
|
|
489
|
-
page = this.#parsePageDeclaration(publicNames);
|
|
490
|
-
}
|
|
491
|
-
if (this.#peek().text === 'identity') {
|
|
492
|
-
identity = this.#parseIdentityDeclaration();
|
|
477
|
+
let syncBy: SyqlSyncDimension | undefined;
|
|
478
|
+
if (this.#peek().text === 'by') {
|
|
479
|
+
const by = this.#take();
|
|
480
|
+
if (!sync) {
|
|
481
|
+
this.#fail(
|
|
482
|
+
'SYQL2012_INVALID_QUERY_BODY',
|
|
483
|
+
by,
|
|
484
|
+
'`by table.scope` is valid only on a sync query',
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
const qualifier = this.#expectKind('identifier', 'sync table or alias');
|
|
488
|
+
this.#expectText('.');
|
|
489
|
+
const column = this.#expectKind('identifier', 'sync scope column');
|
|
490
|
+
syncBy = {
|
|
491
|
+
qualifier: qualifier.text,
|
|
492
|
+
column: column.text,
|
|
493
|
+
span: spanBetween(qualifier, column),
|
|
494
|
+
};
|
|
493
495
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
496
|
+
const declaredRanges = new Set(
|
|
497
|
+
parsedParameters.parameters.flatMap((parameter) =>
|
|
498
|
+
parameter.kind === 'range' ? [parameter.name] : [],
|
|
499
|
+
),
|
|
500
|
+
);
|
|
501
|
+
const body = this.#parseQueryBody(
|
|
502
|
+
parsedParameters.publicNames,
|
|
503
|
+
declaredRanges,
|
|
504
|
+
);
|
|
505
|
+
const implicitRanges = this.#rangeShorthandNames(
|
|
506
|
+
body.statement.tokens,
|
|
507
|
+
declaredRanges,
|
|
508
|
+
);
|
|
509
|
+
const parameters = parsedParameters.parameters.map((parameter) => {
|
|
510
|
+
if (!implicitRanges.has(parameter.name)) return parameter;
|
|
511
|
+
if (parameter.kind === 'group') {
|
|
512
|
+
this.#fail(
|
|
513
|
+
'SYQL2011_INVALID_PARAMETER',
|
|
514
|
+
this.#tokenAt(parameter.nameSpan),
|
|
515
|
+
`range shorthand :${parameter.name} must name a scalar query input`,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
if (parameter.kind === 'range') return parameter;
|
|
519
|
+
if (parameter.default !== undefined) {
|
|
520
|
+
this.#fail(
|
|
521
|
+
'SYQL2011_INVALID_PARAMETER',
|
|
522
|
+
this.#tokenAt(parameter.nameSpan),
|
|
523
|
+
`range input ${parameter.name} cannot have a boolean default`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
kind: 'range' as const,
|
|
528
|
+
name: parameter.name,
|
|
529
|
+
optional: parameter.optional,
|
|
530
|
+
...(parameter.type === undefined ? {} : { type: parameter.type }),
|
|
531
|
+
span: parameter.span,
|
|
532
|
+
nameSpan: parameter.nameSpan,
|
|
533
|
+
};
|
|
534
|
+
});
|
|
535
|
+
for (const parameter of parameters) {
|
|
536
|
+
if (parameter.kind === 'range' && !implicitRanges.has(parameter.name)) {
|
|
537
|
+
this.#fail(
|
|
538
|
+
'SYQL2011_INVALID_PARAMETER',
|
|
539
|
+
this.#tokenAt(parameter.nameSpan),
|
|
540
|
+
`range input ${parameter.name} must be used as BETWEEN :${parameter.name}`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
501
543
|
}
|
|
502
|
-
const close = this.#expectText('}');
|
|
503
544
|
|
|
504
545
|
return {
|
|
505
546
|
kind: 'query',
|
|
506
547
|
name: name.text,
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
...(
|
|
512
|
-
|
|
548
|
+
sync,
|
|
549
|
+
...(syncBy === undefined ? {} : { syncBy }),
|
|
550
|
+
parameters,
|
|
551
|
+
statement: body.statement,
|
|
552
|
+
...(body.sort === undefined ? {} : { sort: body.sort }),
|
|
553
|
+
...(body.limit === undefined ? {} : { limit: body.limit }),
|
|
554
|
+
span: spanBetween(start, body.close),
|
|
513
555
|
nameSpan: name.span,
|
|
514
556
|
};
|
|
515
557
|
}
|
|
@@ -526,7 +568,19 @@ class Parser {
|
|
|
526
568
|
const optional = this.#peek().text === '?';
|
|
527
569
|
if (optional) this.#take();
|
|
528
570
|
|
|
529
|
-
if (
|
|
571
|
+
if (
|
|
572
|
+
optional &&
|
|
573
|
+
this.#peek().text === ':' &&
|
|
574
|
+
this.#peekSignificantAfter(1)?.text === '{'
|
|
575
|
+
) {
|
|
576
|
+
this.#take();
|
|
577
|
+
if (this.#peek().text !== '{') {
|
|
578
|
+
this.#fail(
|
|
579
|
+
'SYQL2011_INVALID_PARAMETER',
|
|
580
|
+
this.#peek(),
|
|
581
|
+
'an optional structured input must use `name?: { ... }`',
|
|
582
|
+
);
|
|
583
|
+
}
|
|
530
584
|
if (publicNames.has(name.text)) {
|
|
531
585
|
this.#duplicate(name, 'public query input');
|
|
532
586
|
}
|
|
@@ -562,9 +616,9 @@ class Parser {
|
|
|
562
616
|
});
|
|
563
617
|
if (this.#peek().text !== ',') break;
|
|
564
618
|
this.#take();
|
|
565
|
-
if (this.#peek().text === '
|
|
619
|
+
if (this.#peek().text === '}') break;
|
|
566
620
|
}
|
|
567
|
-
const groupClose = this.#expectText('
|
|
621
|
+
const groupClose = this.#expectText('}');
|
|
568
622
|
if (members.length < 2) {
|
|
569
623
|
this.#fail(
|
|
570
624
|
'SYQL2011_INVALID_PARAMETER',
|
|
@@ -593,19 +647,24 @@ class Parser {
|
|
|
593
647
|
if (this.#peek().text === ':') {
|
|
594
648
|
const colon = this.#take();
|
|
595
649
|
const typeName = this.#expectKind('identifier', 'parameter type');
|
|
596
|
-
if (typeName.text === '
|
|
597
|
-
if (
|
|
598
|
-
this.#
|
|
599
|
-
'SYQL2011_INVALID_PARAMETER',
|
|
600
|
-
typeName,
|
|
601
|
-
`switch ${name.text} must use the exact optional form ${name.text}?: switch`,
|
|
602
|
-
);
|
|
650
|
+
if (typeName.text === 'range') {
|
|
651
|
+
if (bindNames.has(name.text)) {
|
|
652
|
+
this.#duplicate(name, 'query bind');
|
|
603
653
|
}
|
|
654
|
+
bindNames.add(name.text);
|
|
655
|
+
this.#expectText('<');
|
|
656
|
+
const elementName = this.#expectKind(
|
|
657
|
+
'identifier',
|
|
658
|
+
'range element type',
|
|
659
|
+
);
|
|
660
|
+
const type = this.#parseValueTypeAfterName(colon, elementName);
|
|
661
|
+
const close = this.#expectText('>');
|
|
604
662
|
parameters.push({
|
|
605
|
-
kind: '
|
|
663
|
+
kind: 'range',
|
|
606
664
|
name: name.text,
|
|
607
|
-
optional
|
|
608
|
-
|
|
665
|
+
optional,
|
|
666
|
+
type,
|
|
667
|
+
span: spanBetween(name, close),
|
|
609
668
|
nameSpan: name.span,
|
|
610
669
|
});
|
|
611
670
|
} else {
|
|
@@ -614,16 +673,30 @@ class Parser {
|
|
|
614
673
|
this.#duplicate(name, 'query bind');
|
|
615
674
|
}
|
|
616
675
|
bindNames.add(name.text);
|
|
676
|
+
let end = type.span.end;
|
|
677
|
+
let defaultValue: false | undefined;
|
|
678
|
+
if (this.#peek().text === '=') {
|
|
679
|
+
this.#take();
|
|
680
|
+
const value = this.#expectText('false');
|
|
681
|
+
if (type.base !== 'boolean' || type.nullable || optional) {
|
|
682
|
+
this.#fail(
|
|
683
|
+
'SYQL2011_INVALID_PARAMETER',
|
|
684
|
+
value,
|
|
685
|
+
'only a non-optional bool parameter may declare `= false`',
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
defaultValue = false;
|
|
689
|
+
end = value.span.end;
|
|
690
|
+
}
|
|
617
691
|
parameters.push({
|
|
618
692
|
kind: 'value',
|
|
619
693
|
name: name.text,
|
|
620
694
|
optional,
|
|
621
695
|
type,
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
),
|
|
696
|
+
...(defaultValue === undefined
|
|
697
|
+
? {}
|
|
698
|
+
: { default: defaultValue }),
|
|
699
|
+
span: spanFromPositions(this.#file, name.span.start, end),
|
|
627
700
|
nameSpan: name.span,
|
|
628
701
|
});
|
|
629
702
|
}
|
|
@@ -657,18 +730,265 @@ class Parser {
|
|
|
657
730
|
return { parameters, publicNames, bindNames };
|
|
658
731
|
}
|
|
659
732
|
|
|
660
|
-
#
|
|
661
|
-
|
|
662
|
-
|
|
733
|
+
#parseQueryBody(
|
|
734
|
+
publicNames: Set<string>,
|
|
735
|
+
declaredRanges: ReadonlySet<string>,
|
|
736
|
+
): ParsedQueryBody {
|
|
737
|
+
const open = this.#expectText('{');
|
|
738
|
+
const bodyStart = this.#index;
|
|
739
|
+
let cursor = bodyStart;
|
|
740
|
+
let braceDepth = 1;
|
|
741
|
+
let parenDepth = 0;
|
|
742
|
+
let terminatorIndex: number | undefined;
|
|
743
|
+
while (cursor < this.#tokens.length) {
|
|
744
|
+
const token = this.#tokens[cursor] as SyqlToken;
|
|
745
|
+
if (token.kind === 'eof') {
|
|
746
|
+
this.#fail(
|
|
747
|
+
'SYQL2001_EXPECTED_TOKEN',
|
|
748
|
+
token,
|
|
749
|
+
'expected ; and } to close query body',
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
if (token.text === '(') parenDepth += 1;
|
|
753
|
+
else if (token.text === ')') parenDepth -= 1;
|
|
754
|
+
else if (token.text === '{') braceDepth += 1;
|
|
755
|
+
else if (token.text === '}') {
|
|
756
|
+
if (braceDepth === 1) {
|
|
757
|
+
this.#fail(
|
|
758
|
+
'SYQL2012_INVALID_QUERY_BODY',
|
|
759
|
+
token,
|
|
760
|
+
'a query statement must end with a semicolon',
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
braceDepth -= 1;
|
|
764
|
+
} else if (token.text === ';' && braceDepth === 1 && parenDepth === 0) {
|
|
765
|
+
terminatorIndex = cursor;
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
cursor += 1;
|
|
769
|
+
}
|
|
770
|
+
const terminator = this.#tokens[terminatorIndex as number] as SyqlToken;
|
|
771
|
+
|
|
772
|
+
let dynamicSortIndex: number | undefined;
|
|
773
|
+
let dynamicLimitIndex: number | undefined;
|
|
774
|
+
braceDepth = 1;
|
|
775
|
+
parenDepth = 0;
|
|
776
|
+
for (
|
|
777
|
+
cursor = bodyStart;
|
|
778
|
+
cursor < (terminatorIndex as number);
|
|
779
|
+
cursor += 1
|
|
780
|
+
) {
|
|
781
|
+
const token = this.#tokens[cursor] as SyqlToken;
|
|
782
|
+
if (isSyqlTrivia(token)) continue;
|
|
783
|
+
if (token.text === '(') parenDepth += 1;
|
|
784
|
+
else if (token.text === ')') parenDepth -= 1;
|
|
785
|
+
if (braceDepth === 1 && parenDepth === 0) {
|
|
786
|
+
const lower = token.text.toLowerCase();
|
|
787
|
+
if (
|
|
788
|
+
lower === 'order' &&
|
|
789
|
+
this.#significantTextAfter(cursor, 1) === 'by' &&
|
|
790
|
+
this.#significantTextAfter(cursor, 3) === 'default' &&
|
|
791
|
+
this.#significantTextAfter(cursor, 5) === '{'
|
|
792
|
+
) {
|
|
793
|
+
dynamicSortIndex ??= cursor;
|
|
794
|
+
} else if (
|
|
795
|
+
lower === 'limit' &&
|
|
796
|
+
this.#significantTextAfter(cursor, 2) === 'default' &&
|
|
797
|
+
this.#significantTextAfter(cursor, 4) === 'max'
|
|
798
|
+
) {
|
|
799
|
+
dynamicLimitIndex ??= cursor;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (token.text === '{') braceDepth += 1;
|
|
803
|
+
else if (token.text === '}') braceDepth -= 1;
|
|
804
|
+
}
|
|
805
|
+
if (
|
|
806
|
+
dynamicSortIndex !== undefined &&
|
|
807
|
+
dynamicLimitIndex !== undefined &&
|
|
808
|
+
dynamicLimitIndex < dynamicSortIndex
|
|
809
|
+
) {
|
|
810
|
+
this.#fail(
|
|
811
|
+
'SYQL2012_INVALID_QUERY_BODY',
|
|
812
|
+
this.#tokens[dynamicLimitIndex] as SyqlToken,
|
|
813
|
+
'dynamic ORDER BY must precede dynamic LIMIT',
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const sqlEnd = Math.min(
|
|
818
|
+
dynamicSortIndex ?? Number.MAX_SAFE_INTEGER,
|
|
819
|
+
dynamicLimitIndex ?? Number.MAX_SAFE_INTEGER,
|
|
820
|
+
terminatorIndex as number,
|
|
821
|
+
);
|
|
822
|
+
const statementTokens = this.#tokens.slice(bodyStart, sqlEnd);
|
|
823
|
+
const rangeNames = this.#rangeShorthandNames(
|
|
824
|
+
statementTokens,
|
|
825
|
+
declaredRanges,
|
|
826
|
+
);
|
|
827
|
+
const statement = this.#makeTemplate(
|
|
828
|
+
statementTokens,
|
|
829
|
+
'query statement',
|
|
830
|
+
'statement',
|
|
831
|
+
rangeNames,
|
|
832
|
+
open.span.end,
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
let sort: SyqlSortSection | undefined;
|
|
836
|
+
let limit: SyqlLimitDeclaration | undefined;
|
|
837
|
+
this.#index = sqlEnd;
|
|
838
|
+
if (dynamicSortIndex !== undefined) {
|
|
839
|
+
sort = this.#parseInlineSort(publicNames);
|
|
840
|
+
}
|
|
841
|
+
if (dynamicLimitIndex !== undefined) {
|
|
842
|
+
limit = this.#parseLimitDeclaration(publicNames);
|
|
843
|
+
}
|
|
844
|
+
const end = this.#expectText(';');
|
|
845
|
+
if (end !== terminator) {
|
|
846
|
+
this.#fail(
|
|
847
|
+
'SYQL2012_INVALID_QUERY_BODY',
|
|
848
|
+
end,
|
|
849
|
+
'unexpected query member before the statement terminator',
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
const close = this.#expectText('}');
|
|
853
|
+
return {
|
|
854
|
+
statement,
|
|
855
|
+
...(sort === undefined ? {} : { sort }),
|
|
856
|
+
...(limit === undefined ? {} : { limit }),
|
|
857
|
+
close,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
#rangeShorthandNames(
|
|
862
|
+
tokens: readonly SyqlToken[],
|
|
863
|
+
declaredRanges: ReadonlySet<string> = new Set(),
|
|
864
|
+
): ReadonlySet<string> {
|
|
865
|
+
const significant = tokens.filter((token) => !isSyqlTrivia(token));
|
|
866
|
+
const names = new Set<string>();
|
|
867
|
+
const clauseEnd = new Set([
|
|
868
|
+
'group',
|
|
869
|
+
'having',
|
|
870
|
+
'order',
|
|
871
|
+
'limit',
|
|
872
|
+
'offset',
|
|
873
|
+
'window',
|
|
874
|
+
'union',
|
|
875
|
+
'intersect',
|
|
876
|
+
'except',
|
|
877
|
+
]);
|
|
878
|
+
for (let index = 0; index < significant.length; index += 1) {
|
|
879
|
+
const token = significant[index] as SyqlToken;
|
|
880
|
+
if (token.kind !== 'identifier' || token.text.toLowerCase() !== 'between')
|
|
881
|
+
continue;
|
|
882
|
+
const bind = significant[index + 1];
|
|
883
|
+
if (bind?.kind !== 'bind') continue;
|
|
884
|
+
const name = bind.text.slice(1);
|
|
885
|
+
const next = significant[index + 2];
|
|
886
|
+
const afterNext = significant[index + 3];
|
|
887
|
+
let conjunctStart = index - 1;
|
|
888
|
+
while (
|
|
889
|
+
conjunctStart >= 0 &&
|
|
890
|
+
!(
|
|
891
|
+
significant[conjunctStart]?.kind === 'identifier' &&
|
|
892
|
+
significant[conjunctStart]?.text.toLowerCase() === 'and'
|
|
893
|
+
)
|
|
894
|
+
) {
|
|
895
|
+
conjunctStart -= 1;
|
|
896
|
+
}
|
|
897
|
+
const controlledByRange = significant
|
|
898
|
+
.slice(conjunctStart + 1, index)
|
|
899
|
+
.some((candidate, candidateIndex, conjunct) => {
|
|
900
|
+
if (
|
|
901
|
+
candidate.kind !== 'identifier' ||
|
|
902
|
+
candidate.text.toLowerCase() !== 'when' ||
|
|
903
|
+
conjunct[candidateIndex + 1]?.text !== '('
|
|
904
|
+
) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
let depth = 0;
|
|
908
|
+
for (
|
|
909
|
+
let controlIndex = candidateIndex + 1;
|
|
910
|
+
controlIndex < conjunct.length;
|
|
911
|
+
controlIndex += 1
|
|
912
|
+
) {
|
|
913
|
+
const control = conjunct[controlIndex] as SyqlToken;
|
|
914
|
+
if (control.text === '(') depth += 1;
|
|
915
|
+
else if (control.text === ')') {
|
|
916
|
+
depth -= 1;
|
|
917
|
+
if (depth === 0) return false;
|
|
918
|
+
} else if (
|
|
919
|
+
depth > 0 &&
|
|
920
|
+
control.kind === 'identifier' &&
|
|
921
|
+
control.text === name
|
|
922
|
+
) {
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return false;
|
|
927
|
+
});
|
|
928
|
+
const endsRange =
|
|
929
|
+
declaredRanges.has(name) ||
|
|
930
|
+
controlledByRange ||
|
|
931
|
+
next === undefined ||
|
|
932
|
+
next.text === '}' ||
|
|
933
|
+
(next.kind === 'identifier' &&
|
|
934
|
+
clauseEnd.has(next.text.toLowerCase())) ||
|
|
935
|
+
(next.kind === 'identifier' &&
|
|
936
|
+
next.text.toLowerCase() === 'and' &&
|
|
937
|
+
afterNext?.kind === 'identifier' &&
|
|
938
|
+
afterNext.text === 'when');
|
|
939
|
+
if (endsRange) names.add(name);
|
|
940
|
+
}
|
|
941
|
+
return names;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
#makeTemplate(
|
|
945
|
+
tokens: readonly SyqlToken[],
|
|
946
|
+
label: string,
|
|
947
|
+
mode: SyqlTemplateMode,
|
|
948
|
+
rangeNames: ReadonlySet<string> = new Set(),
|
|
949
|
+
fallbackStart?: SyqlSourceSpan['end'],
|
|
950
|
+
): SyqlTemplate {
|
|
951
|
+
const semantic = tokens.filter((token) => !isSyqlTrivia(token));
|
|
952
|
+
if (semantic.length === 0) {
|
|
953
|
+
this.#fail(
|
|
954
|
+
'SYQL2006_EMPTY_TEMPLATE',
|
|
955
|
+
semantic[0] ?? this.#peek(),
|
|
956
|
+
`${label} must not be empty`,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
const first = tokens[0] ?? semantic[0];
|
|
960
|
+
const last = tokens[tokens.length - 1] ?? semantic[semantic.length - 1];
|
|
961
|
+
const start = first?.span.start ?? fallbackStart ?? this.#peek().span.start;
|
|
962
|
+
const end = last?.span.end ?? start;
|
|
963
|
+
const span = spanFromPositions(this.#file, start, end);
|
|
663
964
|
return {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
965
|
+
text: this.#source.slice(start.offset, end.offset),
|
|
966
|
+
tokens,
|
|
967
|
+
tree: parseSyqlEmbeddedTemplate(
|
|
968
|
+
this.#file,
|
|
969
|
+
tokens,
|
|
970
|
+
span,
|
|
971
|
+
mode,
|
|
972
|
+
rangeNames,
|
|
973
|
+
),
|
|
974
|
+
span,
|
|
667
975
|
};
|
|
668
976
|
}
|
|
669
977
|
|
|
670
|
-
#
|
|
671
|
-
|
|
978
|
+
#significantTextAfter(index: number, count: number): string | undefined {
|
|
979
|
+
let remaining = count;
|
|
980
|
+
for (let cursor = index + 1; cursor < this.#tokens.length; cursor += 1) {
|
|
981
|
+
const token = this.#tokens[cursor] as SyqlToken;
|
|
982
|
+
if (isSyqlTrivia(token)) continue;
|
|
983
|
+
remaining -= 1;
|
|
984
|
+
if (remaining === 0) return token.text.toLowerCase();
|
|
985
|
+
}
|
|
986
|
+
return undefined;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
#parseInlineSort(publicNames: Set<string>): SyqlSortSection {
|
|
990
|
+
const start = this.#expectText('order');
|
|
991
|
+
this.#expectText('by');
|
|
672
992
|
const control = this.#parseCamelName('sort control');
|
|
673
993
|
if (publicNames.has(control.text)) {
|
|
674
994
|
this.#duplicate(control, 'public query input');
|
|
@@ -693,11 +1013,32 @@ class Parser {
|
|
|
693
1013
|
this.#duplicate(profileName, 'sort profile');
|
|
694
1014
|
}
|
|
695
1015
|
profileNames.add(profileName.text);
|
|
696
|
-
|
|
1016
|
+
this.#expectText(':');
|
|
1017
|
+
const contentStart = this.#index;
|
|
1018
|
+
let cursor = contentStart;
|
|
1019
|
+
let depth = 0;
|
|
1020
|
+
while (cursor < this.#tokens.length) {
|
|
1021
|
+
const token = this.#tokens[cursor] as SyqlToken;
|
|
1022
|
+
if (token.kind === 'eof' || token.text === '}') {
|
|
1023
|
+
this.#fail(
|
|
1024
|
+
'SYQL2001_EXPECTED_TOKEN',
|
|
1025
|
+
token,
|
|
1026
|
+
'expected ; after sort profile',
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
if (token.text === '(') depth += 1;
|
|
1030
|
+
else if (token.text === ')') depth -= 1;
|
|
1031
|
+
else if (token.text === ';' && depth === 0) break;
|
|
1032
|
+
cursor += 1;
|
|
1033
|
+
}
|
|
1034
|
+
const end = this.#tokens[cursor] as SyqlToken;
|
|
1035
|
+
const orderTokens = this.#tokens.slice(contentStart, cursor);
|
|
1036
|
+
const order = this.#makeTemplate(orderTokens, 'sort profile', 'order');
|
|
1037
|
+
this.#index = cursor + 1;
|
|
697
1038
|
profiles.push({
|
|
698
1039
|
name: profileName.text,
|
|
699
|
-
order
|
|
700
|
-
span: spanBetween(profileName,
|
|
1040
|
+
order,
|
|
1041
|
+
span: spanBetween(profileName, end),
|
|
701
1042
|
nameSpan: profileName.span,
|
|
702
1043
|
});
|
|
703
1044
|
}
|
|
@@ -706,7 +1047,7 @@ class Parser {
|
|
|
706
1047
|
this.#fail(
|
|
707
1048
|
'SYQL2008_INVALID_MEMBER',
|
|
708
1049
|
close,
|
|
709
|
-
'a
|
|
1050
|
+
'a dynamic ORDER BY must contain at least one profile',
|
|
710
1051
|
);
|
|
711
1052
|
}
|
|
712
1053
|
if (!profileNames.has(defaultProfile.text)) {
|
|
@@ -726,61 +1067,36 @@ class Parser {
|
|
|
726
1067
|
};
|
|
727
1068
|
}
|
|
728
1069
|
|
|
729
|
-
#
|
|
730
|
-
const start = this.#expectText('
|
|
731
|
-
const control = this.#parseCamelName('
|
|
1070
|
+
#parseLimitDeclaration(publicNames: Set<string>): SyqlLimitDeclaration {
|
|
1071
|
+
const start = this.#expectText('limit');
|
|
1072
|
+
const control = this.#parseCamelName('limit control');
|
|
732
1073
|
if (publicNames.has(control.text)) {
|
|
733
1074
|
this.#duplicate(control, 'public query input');
|
|
734
1075
|
}
|
|
735
1076
|
publicNames.add(control.text);
|
|
736
1077
|
this.#expectText('default');
|
|
737
|
-
const defaultToken = this.#parseInteger('
|
|
1078
|
+
const defaultToken = this.#parseInteger('limit default');
|
|
738
1079
|
this.#expectText('max');
|
|
739
|
-
const maxToken = this.#parseInteger('
|
|
740
|
-
const end = this.#expectText(';');
|
|
1080
|
+
const maxToken = this.#parseInteger('limit maximum');
|
|
741
1081
|
const defaultSize = Number(defaultToken.text);
|
|
742
1082
|
const maxSize = Number(maxToken.text);
|
|
743
1083
|
if (defaultSize < 1 || defaultSize > maxSize || maxSize > 2_147_483_647) {
|
|
744
1084
|
this.#fail(
|
|
745
1085
|
'SYQL2010_INVALID_PAGE_RANGE',
|
|
746
1086
|
defaultToken,
|
|
747
|
-
`
|
|
1087
|
+
`limit bounds must satisfy 1 <= default <= max <= 2147483647; found default ${defaultSize}, max ${maxSize}`,
|
|
748
1088
|
);
|
|
749
1089
|
}
|
|
750
1090
|
return {
|
|
751
|
-
kind: '
|
|
1091
|
+
kind: 'limit',
|
|
752
1092
|
control: control.text,
|
|
753
1093
|
defaultSize,
|
|
754
1094
|
maxSize,
|
|
755
|
-
span: spanBetween(start,
|
|
1095
|
+
span: spanBetween(start, maxToken),
|
|
756
1096
|
controlSpan: control.span,
|
|
757
1097
|
};
|
|
758
1098
|
}
|
|
759
1099
|
|
|
760
|
-
#parseIdentityDeclaration(): SyqlIdentityDeclaration {
|
|
761
|
-
const start = this.#expectText('identity');
|
|
762
|
-
this.#expectText('by');
|
|
763
|
-
const fields: string[] = [];
|
|
764
|
-
const names = new Set<string>();
|
|
765
|
-
for (;;) {
|
|
766
|
-
const field = this.#expectKind('identifier', 'identity result name');
|
|
767
|
-
if (!IDENT_RE.test(field.text)) {
|
|
768
|
-
this.#fail(
|
|
769
|
-
'SYQL2002_INVALID_NAME',
|
|
770
|
-
field,
|
|
771
|
-
`invalid identity result name ${JSON.stringify(field.text)}`,
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
if (names.has(field.text)) this.#duplicate(field, 'identity field');
|
|
775
|
-
names.add(field.text);
|
|
776
|
-
fields.push(field.text);
|
|
777
|
-
if (this.#peek().text !== ',') break;
|
|
778
|
-
this.#take();
|
|
779
|
-
}
|
|
780
|
-
const end = this.#expectText(';');
|
|
781
|
-
return { kind: 'identity', fields, span: spanBetween(start, end) };
|
|
782
|
-
}
|
|
783
|
-
|
|
784
1100
|
#parseTypeAnnotation(): SyqlValueType {
|
|
785
1101
|
const colon = this.#expectText(':');
|
|
786
1102
|
const name = this.#expectKind('identifier', 'value type');
|
|
@@ -788,7 +1104,8 @@ class Parser {
|
|
|
788
1104
|
}
|
|
789
1105
|
|
|
790
1106
|
#parseValueTypeAfterName(colon: SyqlToken, name: SyqlToken): SyqlValueType {
|
|
791
|
-
|
|
1107
|
+
const base = name.text === 'bool' ? 'boolean' : name.text;
|
|
1108
|
+
if (name.text === 'boolean' || !BASE_TYPES.has(base as SyqlBaseType)) {
|
|
792
1109
|
this.#fail(
|
|
793
1110
|
'SYQL2011_INVALID_PARAMETER',
|
|
794
1111
|
name,
|
|
@@ -803,7 +1120,7 @@ class Parser {
|
|
|
803
1120
|
nullable = true;
|
|
804
1121
|
}
|
|
805
1122
|
return {
|
|
806
|
-
base:
|
|
1123
|
+
base: base as SyqlBaseType,
|
|
807
1124
|
nullable,
|
|
808
1125
|
span: spanBetween(colon, end),
|
|
809
1126
|
};
|
|
@@ -961,6 +1278,19 @@ class Parser {
|
|
|
961
1278
|
return this.#tokens[index] as SyqlToken;
|
|
962
1279
|
}
|
|
963
1280
|
|
|
1281
|
+
#peekSignificantAfter(count: number): SyqlToken | undefined {
|
|
1282
|
+
let index = this.#index;
|
|
1283
|
+
let remaining = count;
|
|
1284
|
+
while (index < this.#tokens.length) {
|
|
1285
|
+
const token = this.#tokens[index] as SyqlToken;
|
|
1286
|
+
index += 1;
|
|
1287
|
+
if (isSyqlTrivia(token)) continue;
|
|
1288
|
+
if (remaining === 0) return token;
|
|
1289
|
+
remaining -= 1;
|
|
1290
|
+
}
|
|
1291
|
+
return undefined;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
964
1294
|
#take(): SyqlToken {
|
|
965
1295
|
while (
|
|
966
1296
|
this.#index < this.#tokens.length &&
|
|
@@ -983,6 +1313,14 @@ class Parser {
|
|
|
983
1313
|
return undefined;
|
|
984
1314
|
}
|
|
985
1315
|
|
|
1316
|
+
#tokenAt(span: SyqlSourceSpan): SyqlToken {
|
|
1317
|
+
return (
|
|
1318
|
+
this.#tokens.find(
|
|
1319
|
+
(token) => token.span.start.offset === span.start.offset,
|
|
1320
|
+
) ?? this.#peek()
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
986
1324
|
#expectText(text: string): SyqlToken {
|
|
987
1325
|
const token = this.#take();
|
|
988
1326
|
if (token.text !== text) {
|