@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.
@@ -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 if (embeddedKind === 'scope' || embeddedKind === 'cover') {
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
- 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
- }
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
- const control = this.#expectIdentifier('when control');
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
- 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);
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: open.span.end,
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
- #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
- );
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
- 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
- );
319
+ if (token.kind === 'identifier' && lower === 'and') {
320
+ if (pendingBetween && !rangeBetween) {
321
+ pendingBetween = false;
322
+ continue;
365
323
  }
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;
324
+ return this.#leadingTriviaIndex(cursor);
371
325
  }
372
- end = this.#expectText(')', 'to close scope in list');
326
+ if (token.text === '}' || clauseEnders.has(lower))
327
+ return this.#leadingTriviaIndex(cursor);
373
328
  }
329
+ return this.#tokens.length;
330
+ }
374
331
 
375
- return {
376
- kind: 'scope-binding',
377
- column,
378
- operator: operator.text === '=' ? 'equal' : 'in',
379
- values,
380
- span: spanBetween(qualifier, end),
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
  }