@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
|
@@ -37,38 +37,19 @@ export interface SyqlPredicateCall {
|
|
|
37
37
|
export interface SyqlWhenExpression {
|
|
38
38
|
readonly kind: 'when';
|
|
39
39
|
readonly controls: readonly string[];
|
|
40
|
+
/** True when the author wrote present(control); bare optional controls have
|
|
41
|
+
* the same semantics and therefore normally keep this false. */
|
|
42
|
+
readonly explicitPresence: readonly boolean[];
|
|
40
43
|
readonly controlSpans: readonly SyqlSourceSpan[];
|
|
41
44
|
readonly body: SyqlEmbeddedTemplate;
|
|
42
45
|
readonly tokens: readonly SyqlToken[];
|
|
43
46
|
readonly span: SyqlSourceSpan;
|
|
44
47
|
}
|
|
45
48
|
|
|
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
49
|
export type SyqlEmbeddedNode =
|
|
68
50
|
| SyqlRawTemplateNode
|
|
69
51
|
| SyqlPredicateCall
|
|
70
|
-
| SyqlWhenExpression
|
|
71
|
-
| SyqlReactiveDirective;
|
|
52
|
+
| SyqlWhenExpression;
|
|
72
53
|
|
|
73
54
|
export interface SyqlEmbeddedTemplate {
|
|
74
55
|
readonly kind: 'template';
|
|
@@ -88,7 +69,6 @@ export type SyqlTemplateParseErrorCode =
|
|
|
88
69
|
| 'SYQL3008_FORBIDDEN_PARAMETER_FORM';
|
|
89
70
|
|
|
90
71
|
const CAMEL_IDENT_RE = /^[a-z][A-Za-z0-9]*$/;
|
|
91
|
-
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
92
72
|
|
|
93
73
|
interface IndexedToken {
|
|
94
74
|
readonly index: number;
|
|
@@ -112,6 +92,7 @@ class EmbeddedParser {
|
|
|
112
92
|
readonly #tokens: readonly SyqlToken[];
|
|
113
93
|
readonly #span: SyqlSourceSpan;
|
|
114
94
|
readonly #mode: SyqlTemplateMode;
|
|
95
|
+
readonly #rangeNames: ReadonlySet<string>;
|
|
115
96
|
#index = 0;
|
|
116
97
|
|
|
117
98
|
constructor(
|
|
@@ -119,11 +100,13 @@ class EmbeddedParser {
|
|
|
119
100
|
tokens: readonly SyqlToken[],
|
|
120
101
|
span: SyqlSourceSpan,
|
|
121
102
|
mode: SyqlTemplateMode,
|
|
103
|
+
rangeNames: ReadonlySet<string>,
|
|
122
104
|
) {
|
|
123
105
|
this.#file = file;
|
|
124
106
|
this.#tokens = tokens;
|
|
125
107
|
this.#span = span;
|
|
126
108
|
this.#mode = mode;
|
|
109
|
+
this.#rangeNames = rangeNames;
|
|
127
110
|
}
|
|
128
111
|
|
|
129
112
|
parse(): SyqlEmbeddedTemplate {
|
|
@@ -139,9 +122,7 @@ class EmbeddedParser {
|
|
|
139
122
|
if (embeddedKind !== undefined) {
|
|
140
123
|
this.#pushRaw(nodes, rawStart, next.index);
|
|
141
124
|
if (embeddedKind === 'when') nodes.push(this.#parseWhen());
|
|
142
|
-
else
|
|
143
|
-
nodes.push(this.#parseDirective(embeddedKind));
|
|
144
|
-
} else {
|
|
125
|
+
else {
|
|
145
126
|
nodes.push(this.#parsePredicateCall());
|
|
146
127
|
}
|
|
147
128
|
rawStart = this.#index;
|
|
@@ -156,14 +137,7 @@ class EmbeddedParser {
|
|
|
156
137
|
return { kind: 'template', mode: this.#mode, nodes, span: this.#span };
|
|
157
138
|
}
|
|
158
139
|
|
|
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
|
-
}
|
|
140
|
+
#embeddedKind(token: SyqlToken): 'when' | 'predicate-call' | undefined {
|
|
167
141
|
if (
|
|
168
142
|
token.kind === 'identifier' &&
|
|
169
143
|
token.text === 'when' &&
|
|
@@ -171,27 +145,31 @@ class EmbeddedParser {
|
|
|
171
145
|
) {
|
|
172
146
|
return 'when';
|
|
173
147
|
}
|
|
148
|
+
if (token.kind === 'identifier' && this.#isCallCandidate(token)) {
|
|
149
|
+
return 'predicate-call';
|
|
150
|
+
}
|
|
174
151
|
return undefined;
|
|
175
152
|
}
|
|
176
153
|
|
|
154
|
+
#isCallCandidate(token: SyqlToken): boolean {
|
|
155
|
+
let next = this.#peekAfter(token);
|
|
156
|
+
if (next?.token.text !== '(') return false;
|
|
157
|
+
next = this.#peek(next.index + 1);
|
|
158
|
+
if (next?.token.text === ')') return true;
|
|
159
|
+
for (;;) {
|
|
160
|
+
if (next?.token.kind !== 'bind') return false;
|
|
161
|
+
next = this.#peek(next.index + 1);
|
|
162
|
+
if (next?.token.text === ')') return true;
|
|
163
|
+
if (next?.token.text !== ',') return false;
|
|
164
|
+
next = this.#peek(next.index + 1);
|
|
165
|
+
if (next?.token.text === ')') return true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
177
169
|
#parsePredicateCall(): SyqlPredicateCall {
|
|
178
170
|
const startIndex = this.#index;
|
|
179
171
|
const nameToken = this.#take() as SyqlToken;
|
|
180
|
-
|
|
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
|
-
}
|
|
172
|
+
const name = nameToken.text;
|
|
195
173
|
this.#expectText('(', 'after predicate name');
|
|
196
174
|
const args: SyqlBindReference[] = [];
|
|
197
175
|
if (this.#peek()?.token.text !== ')') {
|
|
@@ -224,6 +202,7 @@ class EmbeddedParser {
|
|
|
224
202
|
}
|
|
225
203
|
this.#expectText('(', 'after when');
|
|
226
204
|
const controls: string[] = [];
|
|
205
|
+
const explicitPresenceFlags: boolean[] = [];
|
|
227
206
|
const controlSpans: SyqlSourceSpan[] = [];
|
|
228
207
|
const seen = new Set<string>();
|
|
229
208
|
if (this.#peek()?.token.text === ')') {
|
|
@@ -234,7 +213,22 @@ class EmbeddedParser {
|
|
|
234
213
|
);
|
|
235
214
|
}
|
|
236
215
|
for (;;) {
|
|
237
|
-
|
|
216
|
+
let explicitPresence = false;
|
|
217
|
+
let control: SyqlToken;
|
|
218
|
+
const candidate = this.#peek()?.token;
|
|
219
|
+
if (
|
|
220
|
+
candidate?.kind === 'identifier' &&
|
|
221
|
+
candidate.text === 'present' &&
|
|
222
|
+
this.#peekAfter(candidate)?.token.text === '('
|
|
223
|
+
) {
|
|
224
|
+
this.#take();
|
|
225
|
+
this.#expectText('(', 'after present');
|
|
226
|
+
control = this.#expectIdentifier('present control');
|
|
227
|
+
this.#expectText(')', 'after present control');
|
|
228
|
+
explicitPresence = true;
|
|
229
|
+
} else {
|
|
230
|
+
control = this.#expectIdentifier('when control');
|
|
231
|
+
}
|
|
238
232
|
this.#validateCamel(control, 'when control', 'SYQL3004_INVALID_WHEN');
|
|
239
233
|
if (seen.has(control.text)) {
|
|
240
234
|
this.#fail(
|
|
@@ -245,34 +239,47 @@ class EmbeddedParser {
|
|
|
245
239
|
}
|
|
246
240
|
seen.add(control.text);
|
|
247
241
|
controls.push(control.text);
|
|
242
|
+
explicitPresenceFlags.push(explicitPresence);
|
|
248
243
|
controlSpans.push(control.span);
|
|
249
244
|
if (this.#peek()?.token.text !== ',') break;
|
|
250
245
|
this.#take();
|
|
251
246
|
if (this.#peek()?.token.text === ')') break;
|
|
252
247
|
}
|
|
253
248
|
this.#expectText(')', 'after when controls');
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
249
|
+
let close: SyqlToken;
|
|
250
|
+
let bodyTokens: readonly SyqlToken[];
|
|
251
|
+
if (this.#peek()?.token.text === '{') {
|
|
252
|
+
const open = this.#expectText('{', 'to open when body');
|
|
253
|
+
const closeIndex = this.#matchingBraceIndex(open);
|
|
254
|
+
close = this.#tokens[closeIndex] as SyqlToken;
|
|
255
|
+
bodyTokens = this.#tokens.slice(this.#index, closeIndex);
|
|
256
|
+
this.#index = closeIndex + 1;
|
|
257
|
+
} else {
|
|
258
|
+
const closeIndex = this.#singleWhenBodyEndIndex();
|
|
259
|
+
bodyTokens = this.#tokens.slice(this.#index, closeIndex);
|
|
260
|
+
const significant = bodyTokens.filter((token) => !isSyqlTrivia(token));
|
|
261
|
+
close = significant[significant.length - 1] ?? start;
|
|
262
|
+
this.#index = closeIndex;
|
|
263
|
+
}
|
|
258
264
|
if (bodyTokens.every(isSyqlTrivia)) {
|
|
259
265
|
this.#fail('SYQL3004_INVALID_WHEN', close, 'when body must not be empty');
|
|
260
266
|
}
|
|
261
267
|
const bodySpan: SyqlSourceSpan = {
|
|
262
268
|
file: this.#file,
|
|
263
|
-
start:
|
|
264
|
-
end: close.span.start,
|
|
269
|
+
start: bodyTokens[0]?.span.start ?? close.span.start,
|
|
270
|
+
end: bodyTokens[bodyTokens.length - 1]?.span.end ?? close.span.start,
|
|
265
271
|
};
|
|
266
272
|
const body = parseSyqlEmbeddedTemplate(
|
|
267
273
|
this.#file,
|
|
268
274
|
bodyTokens,
|
|
269
275
|
bodySpan,
|
|
270
276
|
'condition',
|
|
277
|
+
this.#rangeNames,
|
|
271
278
|
);
|
|
272
|
-
this.#index = closeIndex + 1;
|
|
273
279
|
return {
|
|
274
280
|
kind: 'when',
|
|
275
281
|
controls,
|
|
282
|
+
explicitPresence: explicitPresenceFlags,
|
|
276
283
|
controlSpans,
|
|
277
284
|
body,
|
|
278
285
|
tokens: this.#tokens.slice(startIndex, this.#index),
|
|
@@ -280,105 +287,56 @@ class EmbeddedParser {
|
|
|
280
287
|
};
|
|
281
288
|
}
|
|
282
289
|
|
|
283
|
-
#
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
);
|
|
290
|
+
#singleWhenBodyEndIndex(): number {
|
|
291
|
+
let depth = 0;
|
|
292
|
+
let pendingBetween = false;
|
|
293
|
+
let rangeBetween = false;
|
|
294
|
+
const clauseEnders = new Set([
|
|
295
|
+
'group',
|
|
296
|
+
'having',
|
|
297
|
+
'order',
|
|
298
|
+
'limit',
|
|
299
|
+
'offset',
|
|
300
|
+
'window',
|
|
301
|
+
'union',
|
|
302
|
+
'intersect',
|
|
303
|
+
'except',
|
|
304
|
+
]);
|
|
305
|
+
for (let cursor = this.#index; cursor < this.#tokens.length; cursor += 1) {
|
|
306
|
+
const token = this.#tokens[cursor] as SyqlToken;
|
|
307
|
+
if (isSyqlTrivia(token)) continue;
|
|
308
|
+
if (token.text === '(') depth += 1;
|
|
309
|
+
else if (token.text === ')') depth -= 1;
|
|
310
|
+
if (depth !== 0) continue;
|
|
311
|
+
const lower = token.text.toLowerCase();
|
|
312
|
+
if (token.kind === 'identifier' && lower === 'between') {
|
|
313
|
+
const next = this.#peek(cursor + 1)?.token;
|
|
314
|
+
pendingBetween = true;
|
|
315
|
+
rangeBetween =
|
|
316
|
+
next?.kind === 'bind' && this.#rangeNames.has(next.text.slice(1));
|
|
317
|
+
continue;
|
|
355
318
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
this.#fail(
|
|
361
|
-
'SYQL3005_INVALID_REACTIVE_DIRECTIVE',
|
|
362
|
-
value.token,
|
|
363
|
-
`duplicate scope bind :${value.name}`,
|
|
364
|
-
);
|
|
319
|
+
if (token.kind === 'identifier' && lower === 'and') {
|
|
320
|
+
if (pendingBetween && !rangeBetween) {
|
|
321
|
+
pendingBetween = false;
|
|
322
|
+
continue;
|
|
365
323
|
}
|
|
366
|
-
|
|
367
|
-
values.push(value);
|
|
368
|
-
if (this.#peek()?.token.text !== ',') break;
|
|
369
|
-
this.#take();
|
|
370
|
-
if (this.#peek()?.token.text === ')') break;
|
|
324
|
+
return this.#leadingTriviaIndex(cursor);
|
|
371
325
|
}
|
|
372
|
-
|
|
326
|
+
if (token.text === '}' || clauseEnders.has(lower))
|
|
327
|
+
return this.#leadingTriviaIndex(cursor);
|
|
373
328
|
}
|
|
329
|
+
return this.#tokens.length;
|
|
330
|
+
}
|
|
374
331
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
332
|
+
#leadingTriviaIndex(index: number): number {
|
|
333
|
+
let cursor = index;
|
|
334
|
+
while (
|
|
335
|
+
cursor > this.#index &&
|
|
336
|
+
isSyqlTrivia(this.#tokens[cursor - 1] as SyqlToken)
|
|
337
|
+
)
|
|
338
|
+
cursor -= 1;
|
|
339
|
+
return cursor;
|
|
382
340
|
}
|
|
383
341
|
|
|
384
342
|
#parseBindReference(): SyqlBindReference {
|
|
@@ -402,6 +360,13 @@ class EmbeddedParser {
|
|
|
402
360
|
}
|
|
403
361
|
|
|
404
362
|
#validateRawToken(token: SyqlToken): void {
|
|
363
|
+
if (token.kind === 'at-identifier') {
|
|
364
|
+
this.#fail(
|
|
365
|
+
'SYQL3006_FORBIDDEN_TEMPLATE_NODE',
|
|
366
|
+
token,
|
|
367
|
+
`${token.text} uses removed directive/call syntax; use ordinary SQL predicates or a normal predicate call`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
405
370
|
if (token.kind === 'bind') {
|
|
406
371
|
if (this.#mode === 'order') {
|
|
407
372
|
this.#fail(
|
|
@@ -468,16 +433,6 @@ class EmbeddedParser {
|
|
|
468
433
|
}
|
|
469
434
|
}
|
|
470
435
|
|
|
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
436
|
#matchingBraceIndex(open: SyqlToken): number {
|
|
482
437
|
let depth = 1;
|
|
483
438
|
for (let cursor = this.#index; cursor < this.#tokens.length; cursor += 1) {
|
|
@@ -577,6 +532,7 @@ export function parseSyqlEmbeddedTemplate(
|
|
|
577
532
|
tokens: readonly SyqlToken[],
|
|
578
533
|
span: SyqlSourceSpan,
|
|
579
534
|
mode: SyqlTemplateMode,
|
|
535
|
+
rangeNames: ReadonlySet<string> = new Set(),
|
|
580
536
|
): SyqlEmbeddedTemplate {
|
|
581
|
-
return new EmbeddedParser(file, tokens, span, mode).parse();
|
|
537
|
+
return new EmbeddedParser(file, tokens, span, mode, rangeNames).parse();
|
|
582
538
|
}
|