@syncular/typegen 0.4.1 → 0.5.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 +38 -9
- package/dist/emit-queries.js +63 -5
- package/dist/emit.js +32 -0
- package/dist/generate.d.ts +4 -0
- package/dist/generate.js +18 -6
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +8 -2
- package/dist/query-ir.js +24 -1
- package/dist/query.d.ts +30 -0
- package/dist/query.js +139 -8
- package/dist/syql-ast.d.ts +95 -0
- package/dist/syql-ast.js +138 -0
- package/dist/syql-lexer.d.ts +40 -0
- package/dist/syql-lexer.js +367 -0
- package/dist/syql-modules.d.ts +21 -0
- package/dist/syql-modules.js +129 -0
- package/dist/syql-parser.d.ts +132 -0
- package/dist/syql-parser.js +604 -0
- package/dist/syql-template-parser.d.ts +58 -0
- package/dist/syql-template-parser.js +350 -0
- package/dist/syql.d.ts +19 -0
- package/dist/syql.js +196 -0
- package/package.json +3 -3
- package/src/emit-queries.ts +86 -5
- package/src/emit.ts +38 -0
- package/src/generate.ts +32 -5
- package/src/index.ts +5 -0
- package/src/manifest.ts +11 -2
- package/src/query-ir.ts +24 -1
- package/src/query.ts +207 -8
- package/src/syql-ast.ts +277 -0
- package/src/syql-lexer.ts +514 -0
- package/src/syql-modules.ts +217 -0
- package/src/syql-parser.ts +1021 -0
- package/src/syql-template-parser.ts +582 -0
- package/src/syql.ts +300 -1
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/** Embedded SQL-template node parser for revision-1 SYQL (§§6, 8, and 10). */
|
|
2
|
+
import {
|
|
3
|
+
isSyqlTrivia,
|
|
4
|
+
SyqlFrontendError,
|
|
5
|
+
type SyqlSourceSpan,
|
|
6
|
+
type SyqlToken,
|
|
7
|
+
} from './syql-lexer';
|
|
8
|
+
|
|
9
|
+
export type SyqlTemplateMode =
|
|
10
|
+
| 'statement'
|
|
11
|
+
| 'predicate'
|
|
12
|
+
| 'condition'
|
|
13
|
+
| 'order';
|
|
14
|
+
|
|
15
|
+
export interface SyqlRawTemplateNode {
|
|
16
|
+
readonly kind: 'raw';
|
|
17
|
+
readonly text: string;
|
|
18
|
+
readonly tokens: readonly SyqlToken[];
|
|
19
|
+
readonly span: SyqlSourceSpan;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SyqlBindReference {
|
|
23
|
+
readonly kind: 'bind-reference';
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly token: SyqlToken;
|
|
26
|
+
readonly span: SyqlSourceSpan;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SyqlPredicateCall {
|
|
30
|
+
readonly kind: 'predicate-call';
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly arguments: readonly SyqlBindReference[];
|
|
33
|
+
readonly tokens: readonly SyqlToken[];
|
|
34
|
+
readonly span: SyqlSourceSpan;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SyqlWhenExpression {
|
|
38
|
+
readonly kind: 'when';
|
|
39
|
+
readonly controls: readonly string[];
|
|
40
|
+
readonly controlSpans: readonly SyqlSourceSpan[];
|
|
41
|
+
readonly body: SyqlEmbeddedTemplate;
|
|
42
|
+
readonly tokens: readonly SyqlToken[];
|
|
43
|
+
readonly span: SyqlSourceSpan;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SyqlQualifiedColumn {
|
|
47
|
+
readonly qualifier: string;
|
|
48
|
+
readonly name: string;
|
|
49
|
+
readonly span: SyqlSourceSpan;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SyqlScopeBinding {
|
|
53
|
+
readonly kind: 'scope-binding';
|
|
54
|
+
readonly column: SyqlQualifiedColumn;
|
|
55
|
+
readonly operator: 'equal' | 'in';
|
|
56
|
+
readonly values: readonly SyqlBindReference[];
|
|
57
|
+
readonly span: SyqlSourceSpan;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SyqlReactiveDirective {
|
|
61
|
+
readonly kind: 'scope' | 'cover';
|
|
62
|
+
readonly bindings: readonly SyqlScopeBinding[];
|
|
63
|
+
readonly tokens: readonly SyqlToken[];
|
|
64
|
+
readonly span: SyqlSourceSpan;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type SyqlEmbeddedNode =
|
|
68
|
+
| SyqlRawTemplateNode
|
|
69
|
+
| SyqlPredicateCall
|
|
70
|
+
| SyqlWhenExpression
|
|
71
|
+
| SyqlReactiveDirective;
|
|
72
|
+
|
|
73
|
+
export interface SyqlEmbeddedTemplate {
|
|
74
|
+
readonly kind: 'template';
|
|
75
|
+
readonly mode: SyqlTemplateMode;
|
|
76
|
+
readonly nodes: readonly SyqlEmbeddedNode[];
|
|
77
|
+
readonly span: SyqlSourceSpan;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type SyqlTemplateParseErrorCode =
|
|
81
|
+
| 'SYQL3001_EXPECTED_EMBEDDED_TOKEN'
|
|
82
|
+
| 'SYQL3002_INVALID_BIND'
|
|
83
|
+
| 'SYQL3003_INVALID_PREDICATE_CALL'
|
|
84
|
+
| 'SYQL3004_INVALID_WHEN'
|
|
85
|
+
| 'SYQL3005_INVALID_REACTIVE_DIRECTIVE'
|
|
86
|
+
| 'SYQL3006_FORBIDDEN_TEMPLATE_NODE'
|
|
87
|
+
| 'SYQL3007_UNEXPECTED_BRACE'
|
|
88
|
+
| 'SYQL3008_FORBIDDEN_PARAMETER_FORM';
|
|
89
|
+
|
|
90
|
+
const CAMEL_IDENT_RE = /^[a-z][A-Za-z0-9]*$/;
|
|
91
|
+
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
92
|
+
|
|
93
|
+
interface IndexedToken {
|
|
94
|
+
readonly index: number;
|
|
95
|
+
readonly token: SyqlToken;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function spanBetween(start: SyqlToken, end: SyqlToken): SyqlSourceSpan {
|
|
99
|
+
return {
|
|
100
|
+
file: start.span.file,
|
|
101
|
+
start: start.span.start,
|
|
102
|
+
end: end.span.end,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function emptySpanAtStart(span: SyqlSourceSpan): SyqlSourceSpan {
|
|
107
|
+
return { file: span.file, start: span.start, end: span.start };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class EmbeddedParser {
|
|
111
|
+
readonly #file: string;
|
|
112
|
+
readonly #tokens: readonly SyqlToken[];
|
|
113
|
+
readonly #span: SyqlSourceSpan;
|
|
114
|
+
readonly #mode: SyqlTemplateMode;
|
|
115
|
+
#index = 0;
|
|
116
|
+
|
|
117
|
+
constructor(
|
|
118
|
+
file: string,
|
|
119
|
+
tokens: readonly SyqlToken[],
|
|
120
|
+
span: SyqlSourceSpan,
|
|
121
|
+
mode: SyqlTemplateMode,
|
|
122
|
+
) {
|
|
123
|
+
this.#file = file;
|
|
124
|
+
this.#tokens = tokens;
|
|
125
|
+
this.#span = span;
|
|
126
|
+
this.#mode = mode;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
parse(): SyqlEmbeddedTemplate {
|
|
130
|
+
const nodes: SyqlEmbeddedNode[] = [];
|
|
131
|
+
let rawStart = 0;
|
|
132
|
+
|
|
133
|
+
while (this.#index < this.#tokens.length) {
|
|
134
|
+
const next = this.#peek();
|
|
135
|
+
if (next === undefined) break;
|
|
136
|
+
this.#index = next.index;
|
|
137
|
+
|
|
138
|
+
const embeddedKind = this.#embeddedKind(next.token);
|
|
139
|
+
if (embeddedKind !== undefined) {
|
|
140
|
+
this.#pushRaw(nodes, rawStart, next.index);
|
|
141
|
+
if (embeddedKind === 'when') nodes.push(this.#parseWhen());
|
|
142
|
+
else if (embeddedKind === 'scope' || embeddedKind === 'cover') {
|
|
143
|
+
nodes.push(this.#parseDirective(embeddedKind));
|
|
144
|
+
} else {
|
|
145
|
+
nodes.push(this.#parsePredicateCall());
|
|
146
|
+
}
|
|
147
|
+
rawStart = this.#index;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.#validateRawToken(next.token);
|
|
152
|
+
this.#index = next.index + 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.#pushRaw(nodes, rawStart, this.#tokens.length);
|
|
156
|
+
return { kind: 'template', mode: this.#mode, nodes, span: this.#span };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#embeddedKind(
|
|
160
|
+
token: SyqlToken,
|
|
161
|
+
): 'when' | 'scope' | 'cover' | 'predicate-call' | undefined {
|
|
162
|
+
if (token.kind === 'at-identifier') {
|
|
163
|
+
if (token.text === '@scope') return 'scope';
|
|
164
|
+
if (token.text === '@cover') return 'cover';
|
|
165
|
+
return 'predicate-call';
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
token.kind === 'identifier' &&
|
|
169
|
+
token.text === 'when' &&
|
|
170
|
+
this.#peekAfter(token)?.token.text === '('
|
|
171
|
+
) {
|
|
172
|
+
return 'when';
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#parsePredicateCall(): SyqlPredicateCall {
|
|
178
|
+
const startIndex = this.#index;
|
|
179
|
+
const nameToken = this.#take() as SyqlToken;
|
|
180
|
+
if (this.#mode === 'order') {
|
|
181
|
+
this.#fail(
|
|
182
|
+
'SYQL3006_FORBIDDEN_TEMPLATE_NODE',
|
|
183
|
+
nameToken,
|
|
184
|
+
'predicate calls are not allowed in sort profiles',
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const name = nameToken.text.slice(1);
|
|
188
|
+
if (!CAMEL_IDENT_RE.test(name) || name.toLowerCase().startsWith('__syql')) {
|
|
189
|
+
this.#fail(
|
|
190
|
+
'SYQL3003_INVALID_PREDICATE_CALL',
|
|
191
|
+
nameToken,
|
|
192
|
+
`predicate call name ${JSON.stringify(name)} must match [a-z][A-Za-z0-9]* and not use __syql`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
this.#expectText('(', 'after predicate name');
|
|
196
|
+
const args: SyqlBindReference[] = [];
|
|
197
|
+
if (this.#peek()?.token.text !== ')') {
|
|
198
|
+
for (;;) {
|
|
199
|
+
args.push(this.#parseBindReference());
|
|
200
|
+
if (this.#peek()?.token.text !== ',') break;
|
|
201
|
+
this.#take();
|
|
202
|
+
if (this.#peek()?.token.text === ')') break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const close = this.#expectText(')', 'to close predicate call');
|
|
206
|
+
return {
|
|
207
|
+
kind: 'predicate-call',
|
|
208
|
+
name,
|
|
209
|
+
arguments: args,
|
|
210
|
+
tokens: this.#tokens.slice(startIndex, this.#index),
|
|
211
|
+
span: spanBetween(nameToken, close),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#parseWhen(): SyqlWhenExpression {
|
|
216
|
+
const startIndex = this.#index;
|
|
217
|
+
const start = this.#take() as SyqlToken;
|
|
218
|
+
if (this.#mode !== 'statement') {
|
|
219
|
+
this.#fail(
|
|
220
|
+
'SYQL3006_FORBIDDEN_TEMPLATE_NODE',
|
|
221
|
+
start,
|
|
222
|
+
`when is not allowed in a ${this.#mode} template`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
this.#expectText('(', 'after when');
|
|
226
|
+
const controls: string[] = [];
|
|
227
|
+
const controlSpans: SyqlSourceSpan[] = [];
|
|
228
|
+
const seen = new Set<string>();
|
|
229
|
+
if (this.#peek()?.token.text === ')') {
|
|
230
|
+
this.#fail(
|
|
231
|
+
'SYQL3004_INVALID_WHEN',
|
|
232
|
+
this.#peek()?.token ?? start,
|
|
233
|
+
'when requires at least one control',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
for (;;) {
|
|
237
|
+
const control = this.#expectIdentifier('when control');
|
|
238
|
+
this.#validateCamel(control, 'when control', 'SYQL3004_INVALID_WHEN');
|
|
239
|
+
if (seen.has(control.text)) {
|
|
240
|
+
this.#fail(
|
|
241
|
+
'SYQL3004_INVALID_WHEN',
|
|
242
|
+
control,
|
|
243
|
+
`duplicate when control ${JSON.stringify(control.text)}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
seen.add(control.text);
|
|
247
|
+
controls.push(control.text);
|
|
248
|
+
controlSpans.push(control.span);
|
|
249
|
+
if (this.#peek()?.token.text !== ',') break;
|
|
250
|
+
this.#take();
|
|
251
|
+
if (this.#peek()?.token.text === ')') break;
|
|
252
|
+
}
|
|
253
|
+
this.#expectText(')', 'after when controls');
|
|
254
|
+
const open = this.#expectText('{', 'to open when body');
|
|
255
|
+
const closeIndex = this.#matchingBraceIndex(open);
|
|
256
|
+
const close = this.#tokens[closeIndex] as SyqlToken;
|
|
257
|
+
const bodyTokens = this.#tokens.slice(this.#index, closeIndex);
|
|
258
|
+
if (bodyTokens.every(isSyqlTrivia)) {
|
|
259
|
+
this.#fail('SYQL3004_INVALID_WHEN', close, 'when body must not be empty');
|
|
260
|
+
}
|
|
261
|
+
const bodySpan: SyqlSourceSpan = {
|
|
262
|
+
file: this.#file,
|
|
263
|
+
start: open.span.end,
|
|
264
|
+
end: close.span.start,
|
|
265
|
+
};
|
|
266
|
+
const body = parseSyqlEmbeddedTemplate(
|
|
267
|
+
this.#file,
|
|
268
|
+
bodyTokens,
|
|
269
|
+
bodySpan,
|
|
270
|
+
'condition',
|
|
271
|
+
);
|
|
272
|
+
this.#index = closeIndex + 1;
|
|
273
|
+
return {
|
|
274
|
+
kind: 'when',
|
|
275
|
+
controls,
|
|
276
|
+
controlSpans,
|
|
277
|
+
body,
|
|
278
|
+
tokens: this.#tokens.slice(startIndex, this.#index),
|
|
279
|
+
span: spanBetween(start, close),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#parseDirective(kind: 'scope' | 'cover'): SyqlReactiveDirective {
|
|
284
|
+
const startIndex = this.#index;
|
|
285
|
+
const start = this.#take() as SyqlToken;
|
|
286
|
+
if (this.#mode !== 'statement') {
|
|
287
|
+
this.#fail(
|
|
288
|
+
'SYQL3006_FORBIDDEN_TEMPLATE_NODE',
|
|
289
|
+
start,
|
|
290
|
+
`@${kind} is not allowed in a ${this.#mode} template`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
this.#expectText('(', `after @${kind}`);
|
|
294
|
+
const bindings: SyqlScopeBinding[] = [];
|
|
295
|
+
if (this.#peek()?.token.text === ')') {
|
|
296
|
+
this.#fail(
|
|
297
|
+
'SYQL3005_INVALID_REACTIVE_DIRECTIVE',
|
|
298
|
+
this.#peek()?.token ?? start,
|
|
299
|
+
`@${kind} requires at least one scope binding`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
for (;;) {
|
|
303
|
+
bindings.push(this.#parseScopeBinding());
|
|
304
|
+
if (this.#peek()?.token.text !== ',') break;
|
|
305
|
+
this.#take();
|
|
306
|
+
if (this.#peek()?.token.text === ')') break;
|
|
307
|
+
}
|
|
308
|
+
const close = this.#expectText(')', `to close @${kind}`);
|
|
309
|
+
return {
|
|
310
|
+
kind,
|
|
311
|
+
bindings,
|
|
312
|
+
tokens: this.#tokens.slice(startIndex, this.#index),
|
|
313
|
+
span: spanBetween(start, close),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#parseScopeBinding(): SyqlScopeBinding {
|
|
318
|
+
const qualifier = this.#expectIdentifier('scope table or alias');
|
|
319
|
+
this.#validateSqlIdent(qualifier, 'scope table or alias');
|
|
320
|
+
this.#expectText('.', 'in qualified scope column');
|
|
321
|
+
const columnName = this.#expectIdentifier('scope column');
|
|
322
|
+
this.#validateSqlIdent(columnName, 'scope column');
|
|
323
|
+
const column: SyqlQualifiedColumn = {
|
|
324
|
+
qualifier: qualifier.text,
|
|
325
|
+
name: columnName.text,
|
|
326
|
+
span: spanBetween(qualifier, columnName),
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const operator = this.#take();
|
|
330
|
+
if (
|
|
331
|
+
operator === undefined ||
|
|
332
|
+
(operator.text !== '=' && operator.text !== 'in')
|
|
333
|
+
) {
|
|
334
|
+
this.#fail(
|
|
335
|
+
'SYQL3005_INVALID_REACTIVE_DIRECTIVE',
|
|
336
|
+
operator ?? columnName,
|
|
337
|
+
'scope binding must use = or lowercase in',
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const values: SyqlBindReference[] = [];
|
|
342
|
+
let end: SyqlToken;
|
|
343
|
+
if (operator.text === '=') {
|
|
344
|
+
const value = this.#parseBindReference();
|
|
345
|
+
values.push(value);
|
|
346
|
+
end = value.token;
|
|
347
|
+
} else {
|
|
348
|
+
this.#expectText('(', 'after scope in');
|
|
349
|
+
if (this.#peek()?.token.text === ')') {
|
|
350
|
+
this.#fail(
|
|
351
|
+
'SYQL3005_INVALID_REACTIVE_DIRECTIVE',
|
|
352
|
+
this.#peek()?.token ?? operator,
|
|
353
|
+
'scope in list must not be empty',
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
const names = new Set<string>();
|
|
357
|
+
for (;;) {
|
|
358
|
+
const value = this.#parseBindReference();
|
|
359
|
+
if (names.has(value.name)) {
|
|
360
|
+
this.#fail(
|
|
361
|
+
'SYQL3005_INVALID_REACTIVE_DIRECTIVE',
|
|
362
|
+
value.token,
|
|
363
|
+
`duplicate scope bind :${value.name}`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
names.add(value.name);
|
|
367
|
+
values.push(value);
|
|
368
|
+
if (this.#peek()?.token.text !== ',') break;
|
|
369
|
+
this.#take();
|
|
370
|
+
if (this.#peek()?.token.text === ')') break;
|
|
371
|
+
}
|
|
372
|
+
end = this.#expectText(')', 'to close scope in list');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
kind: 'scope-binding',
|
|
377
|
+
column,
|
|
378
|
+
operator: operator.text === '=' ? 'equal' : 'in',
|
|
379
|
+
values,
|
|
380
|
+
span: spanBetween(qualifier, end),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#parseBindReference(): SyqlBindReference {
|
|
385
|
+
const token = this.#take();
|
|
386
|
+
if (token?.kind !== 'bind') {
|
|
387
|
+
this.#fail(
|
|
388
|
+
'SYQL3002_INVALID_BIND',
|
|
389
|
+
token ?? this.#tokens[this.#tokens.length - 1],
|
|
390
|
+
'expected a :camelCase bind reference',
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const name = token.text.slice(1);
|
|
394
|
+
if (!CAMEL_IDENT_RE.test(name) || name.toLowerCase().startsWith('__syql')) {
|
|
395
|
+
this.#fail(
|
|
396
|
+
'SYQL3002_INVALID_BIND',
|
|
397
|
+
token,
|
|
398
|
+
`bind ${JSON.stringify(token.text)} must match :[a-z][A-Za-z0-9]* and not use __syql`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return { kind: 'bind-reference', name, token, span: token.span };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#validateRawToken(token: SyqlToken): void {
|
|
405
|
+
if (token.kind === 'bind') {
|
|
406
|
+
if (this.#mode === 'order') {
|
|
407
|
+
this.#fail(
|
|
408
|
+
'SYQL3006_FORBIDDEN_TEMPLATE_NODE',
|
|
409
|
+
token,
|
|
410
|
+
'binds are not allowed in sort profiles',
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const name = token.text.slice(1);
|
|
414
|
+
if (
|
|
415
|
+
!CAMEL_IDENT_RE.test(name) ||
|
|
416
|
+
name.toLowerCase().startsWith('__syql')
|
|
417
|
+
) {
|
|
418
|
+
this.#fail(
|
|
419
|
+
'SYQL3002_INVALID_BIND',
|
|
420
|
+
token,
|
|
421
|
+
`bind ${JSON.stringify(token.text)} must match :[a-z][A-Za-z0-9]* and not use __syql`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (token.kind === 'punctuation' && token.text === '?') {
|
|
426
|
+
this.#fail(
|
|
427
|
+
'SYQL3008_FORBIDDEN_PARAMETER_FORM',
|
|
428
|
+
token,
|
|
429
|
+
'SQLite ? parameters are forbidden; declare and use a :camelCase bind',
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (
|
|
433
|
+
token.kind === 'operator' &&
|
|
434
|
+
(token.text === '$' || token.text === '@')
|
|
435
|
+
) {
|
|
436
|
+
this.#fail(
|
|
437
|
+
'SYQL3008_FORBIDDEN_PARAMETER_FORM',
|
|
438
|
+
token,
|
|
439
|
+
`SQLite ${token.text} parameters are forbidden in SYQL templates`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
if (
|
|
443
|
+
token.kind === 'punctuation' &&
|
|
444
|
+
(token.text === '{' || token.text === '}')
|
|
445
|
+
) {
|
|
446
|
+
this.#fail(
|
|
447
|
+
'SYQL3007_UNEXPECTED_BRACE',
|
|
448
|
+
token,
|
|
449
|
+
'a template brace must belong to a when(...) { ... } node',
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
#validateCamel(
|
|
455
|
+
token: SyqlToken,
|
|
456
|
+
label: string,
|
|
457
|
+
code: SyqlTemplateParseErrorCode,
|
|
458
|
+
): void {
|
|
459
|
+
if (
|
|
460
|
+
!CAMEL_IDENT_RE.test(token.text) ||
|
|
461
|
+
token.text.toLowerCase().startsWith('__syql')
|
|
462
|
+
) {
|
|
463
|
+
this.#fail(
|
|
464
|
+
code,
|
|
465
|
+
token,
|
|
466
|
+
`${label} must match [a-z][A-Za-z0-9]* and not use __syql`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#validateSqlIdent(token: SyqlToken, label: string): void {
|
|
472
|
+
if (!IDENT_RE.test(token.text)) {
|
|
473
|
+
this.#fail(
|
|
474
|
+
'SYQL3005_INVALID_REACTIVE_DIRECTIVE',
|
|
475
|
+
token,
|
|
476
|
+
`${label} must be an unquoted SQLite identifier`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
#matchingBraceIndex(open: SyqlToken): number {
|
|
482
|
+
let depth = 1;
|
|
483
|
+
for (let cursor = this.#index; cursor < this.#tokens.length; cursor += 1) {
|
|
484
|
+
const token = this.#tokens[cursor] as SyqlToken;
|
|
485
|
+
if (token.kind !== 'punctuation') continue;
|
|
486
|
+
if (token.text === '{') depth += 1;
|
|
487
|
+
else if (token.text === '}') {
|
|
488
|
+
depth -= 1;
|
|
489
|
+
if (depth === 0) return cursor;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
this.#fail(
|
|
493
|
+
'SYQL3001_EXPECTED_EMBEDDED_TOKEN',
|
|
494
|
+
open,
|
|
495
|
+
'expected } to close when body',
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
#pushRaw(nodes: SyqlEmbeddedNode[], start: number, end: number): void {
|
|
500
|
+
if (start >= end) return;
|
|
501
|
+
const tokens = this.#tokens.slice(start, end);
|
|
502
|
+
const first = tokens[0] as SyqlToken;
|
|
503
|
+
const last = tokens[tokens.length - 1] as SyqlToken;
|
|
504
|
+
nodes.push({
|
|
505
|
+
kind: 'raw',
|
|
506
|
+
text: tokens.map((token) => token.text).join(''),
|
|
507
|
+
tokens,
|
|
508
|
+
span: spanBetween(first, last),
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
#peek(from = this.#index): IndexedToken | undefined {
|
|
513
|
+
let index = from;
|
|
514
|
+
while (index < this.#tokens.length) {
|
|
515
|
+
const token = this.#tokens[index] as SyqlToken;
|
|
516
|
+
if (!isSyqlTrivia(token)) return { index, token };
|
|
517
|
+
index += 1;
|
|
518
|
+
}
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
#peekAfter(token: SyqlToken): IndexedToken | undefined {
|
|
523
|
+
const index = this.#tokens.indexOf(token, this.#index);
|
|
524
|
+
return index < 0 ? undefined : this.#peek(index + 1);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
#take(): SyqlToken | undefined {
|
|
528
|
+
const next = this.#peek();
|
|
529
|
+
if (next === undefined) {
|
|
530
|
+
this.#index = this.#tokens.length;
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
this.#index = next.index + 1;
|
|
534
|
+
return next.token;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
#expectText(text: string, context: string): SyqlToken {
|
|
538
|
+
const token = this.#take();
|
|
539
|
+
if (token?.text !== text) {
|
|
540
|
+
this.#fail(
|
|
541
|
+
'SYQL3001_EXPECTED_EMBEDDED_TOKEN',
|
|
542
|
+
token ?? this.#tokens[this.#tokens.length - 1],
|
|
543
|
+
`expected ${JSON.stringify(text)} ${context}`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
return token;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#expectIdentifier(label: string): SyqlToken {
|
|
550
|
+
const token = this.#take();
|
|
551
|
+
if (token?.kind !== 'identifier') {
|
|
552
|
+
this.#fail(
|
|
553
|
+
'SYQL3001_EXPECTED_EMBEDDED_TOKEN',
|
|
554
|
+
token ?? this.#tokens[this.#tokens.length - 1],
|
|
555
|
+
`expected ${label}`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
return token;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
#fail(
|
|
562
|
+
code: SyqlTemplateParseErrorCode,
|
|
563
|
+
token: SyqlToken | undefined,
|
|
564
|
+
message: string,
|
|
565
|
+
): never {
|
|
566
|
+
throw new SyqlFrontendError(
|
|
567
|
+
code,
|
|
568
|
+
token?.span ?? emptySpanAtStart(this.#span),
|
|
569
|
+
message,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Parse embedded nodes from one already-delimited lossless template. */
|
|
575
|
+
export function parseSyqlEmbeddedTemplate(
|
|
576
|
+
file: string,
|
|
577
|
+
tokens: readonly SyqlToken[],
|
|
578
|
+
span: SyqlSourceSpan,
|
|
579
|
+
mode: SyqlTemplateMode,
|
|
580
|
+
): SyqlEmbeddedTemplate {
|
|
581
|
+
return new EmbeddedParser(file, tokens, span, mode).parse();
|
|
582
|
+
}
|