@syncular/typegen 0.5.1 → 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.
Files changed (56) hide show
  1. package/README.md +79 -32
  2. package/dist/cli.js +40 -17
  3. package/dist/emit-queries-dart.js +168 -55
  4. package/dist/emit-queries-kotlin.js +167 -55
  5. package/dist/emit-queries-swift.js +183 -57
  6. package/dist/emit-queries.js +286 -123
  7. package/dist/fmt.d.ts +2 -2
  8. package/dist/fmt.js +348 -316
  9. package/dist/generate.d.ts +1 -1
  10. package/dist/generate.js +28 -9
  11. package/dist/index.d.ts +3 -1
  12. package/dist/index.js +3 -1
  13. package/dist/lsp.d.ts +1 -3
  14. package/dist/lsp.js +349 -187
  15. package/dist/manifest.d.ts +1 -3
  16. package/dist/manifest.js +1 -1
  17. package/dist/query-ir.js +53 -42
  18. package/dist/query.d.ts +110 -63
  19. package/dist/query.js +89 -11
  20. package/dist/syql-ast.d.ts +12 -13
  21. package/dist/syql-ast.js +26 -20
  22. package/dist/syql-lexer.d.ts +2 -0
  23. package/dist/syql-lexer.js +9 -2
  24. package/dist/syql-lowering.d.ts +22 -0
  25. package/dist/syql-lowering.js +434 -0
  26. package/dist/syql-parser.d.ts +19 -18
  27. package/dist/syql-parser.js +341 -93
  28. package/dist/syql-semantics.d.ts +73 -0
  29. package/dist/syql-semantics.js +607 -0
  30. package/dist/syql-template-parser.d.ts +5 -20
  31. package/dist/syql-template-parser.js +116 -109
  32. package/dist/syql-validator.d.ts +35 -0
  33. package/dist/syql-validator.js +993 -0
  34. package/package.json +3 -3
  35. package/src/cli.ts +49 -21
  36. package/src/emit-queries-dart.ts +195 -69
  37. package/src/emit-queries-kotlin.ts +197 -69
  38. package/src/emit-queries-swift.ts +224 -68
  39. package/src/emit-queries.ts +410 -165
  40. package/src/fmt.ts +405 -303
  41. package/src/generate.ts +43 -9
  42. package/src/index.ts +3 -1
  43. package/src/lsp.ts +414 -216
  44. package/src/manifest.ts +2 -4
  45. package/src/query-ir.ts +52 -42
  46. package/src/query.ts +229 -79
  47. package/src/syql-ast.ts +38 -34
  48. package/src/syql-lexer.ts +12 -1
  49. package/src/syql-lowering.ts +664 -0
  50. package/src/syql-parser.ts +472 -134
  51. package/src/syql-semantics.ts +930 -0
  52. package/src/syql-template-parser.ts +119 -163
  53. package/src/syql-validator.ts +1486 -0
  54. package/dist/syql.d.ts +0 -102
  55. package/dist/syql.js +0 -1141
  56. package/src/syql.ts +0 -1470
@@ -1,7 +1,6 @@
1
1
  /** Embedded SQL-template node parser for revision-1 SYQL (§§6, 8, and 10). */
2
2
  import { isSyqlTrivia, SyqlFrontendError, } from './syql-lexer.js';
3
3
  const CAMEL_IDENT_RE = /^[a-z][A-Za-z0-9]*$/;
4
- const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
5
4
  function spanBetween(start, end) {
6
5
  return {
7
6
  file: start.span.file,
@@ -17,12 +16,14 @@ class EmbeddedParser {
17
16
  #tokens;
18
17
  #span;
19
18
  #mode;
19
+ #rangeNames;
20
20
  #index = 0;
21
- constructor(file, tokens, span, mode) {
21
+ constructor(file, tokens, span, mode, rangeNames) {
22
22
  this.#file = file;
23
23
  this.#tokens = tokens;
24
24
  this.#span = span;
25
25
  this.#mode = mode;
26
+ this.#rangeNames = rangeNames;
26
27
  }
27
28
  parse() {
28
29
  const nodes = [];
@@ -37,9 +38,6 @@ class EmbeddedParser {
37
38
  this.#pushRaw(nodes, rawStart, next.index);
38
39
  if (embeddedKind === 'when')
39
40
  nodes.push(this.#parseWhen());
40
- else if (embeddedKind === 'scope' || embeddedKind === 'cover') {
41
- nodes.push(this.#parseDirective(embeddedKind));
42
- }
43
41
  else {
44
42
  nodes.push(this.#parsePredicateCall());
45
43
  }
@@ -53,30 +51,40 @@ class EmbeddedParser {
53
51
  return { kind: 'template', mode: this.#mode, nodes, span: this.#span };
54
52
  }
55
53
  #embeddedKind(token) {
56
- if (token.kind === 'at-identifier') {
57
- if (token.text === '@scope')
58
- return 'scope';
59
- if (token.text === '@cover')
60
- return 'cover';
61
- return 'predicate-call';
62
- }
63
54
  if (token.kind === 'identifier' &&
64
55
  token.text === 'when' &&
65
56
  this.#peekAfter(token)?.token.text === '(') {
66
57
  return 'when';
67
58
  }
59
+ if (token.kind === 'identifier' && this.#isCallCandidate(token)) {
60
+ return 'predicate-call';
61
+ }
68
62
  return undefined;
69
63
  }
64
+ #isCallCandidate(token) {
65
+ let next = this.#peekAfter(token);
66
+ if (next?.token.text !== '(')
67
+ return false;
68
+ next = this.#peek(next.index + 1);
69
+ if (next?.token.text === ')')
70
+ return true;
71
+ for (;;) {
72
+ if (next?.token.kind !== 'bind')
73
+ return false;
74
+ next = this.#peek(next.index + 1);
75
+ if (next?.token.text === ')')
76
+ return true;
77
+ if (next?.token.text !== ',')
78
+ return false;
79
+ next = this.#peek(next.index + 1);
80
+ if (next?.token.text === ')')
81
+ return true;
82
+ }
83
+ }
70
84
  #parsePredicateCall() {
71
85
  const startIndex = this.#index;
72
86
  const nameToken = this.#take();
73
- if (this.#mode === 'order') {
74
- this.#fail('SYQL3006_FORBIDDEN_TEMPLATE_NODE', nameToken, 'predicate calls are not allowed in sort profiles');
75
- }
76
- const name = nameToken.text.slice(1);
77
- if (!CAMEL_IDENT_RE.test(name) || name.toLowerCase().startsWith('__syql')) {
78
- this.#fail('SYQL3003_INVALID_PREDICATE_CALL', nameToken, `predicate call name ${JSON.stringify(name)} must match [a-z][A-Za-z0-9]* and not use __syql`);
79
- }
87
+ const name = nameToken.text;
80
88
  this.#expectText('(', 'after predicate name');
81
89
  const args = [];
82
90
  if (this.#peek()?.token.text !== ')') {
@@ -106,19 +114,35 @@ class EmbeddedParser {
106
114
  }
107
115
  this.#expectText('(', 'after when');
108
116
  const controls = [];
117
+ const explicitPresenceFlags = [];
109
118
  const controlSpans = [];
110
119
  const seen = new Set();
111
120
  if (this.#peek()?.token.text === ')') {
112
121
  this.#fail('SYQL3004_INVALID_WHEN', this.#peek()?.token ?? start, 'when requires at least one control');
113
122
  }
114
123
  for (;;) {
115
- const control = this.#expectIdentifier('when control');
124
+ let explicitPresence = false;
125
+ let control;
126
+ const candidate = this.#peek()?.token;
127
+ if (candidate?.kind === 'identifier' &&
128
+ candidate.text === 'present' &&
129
+ this.#peekAfter(candidate)?.token.text === '(') {
130
+ this.#take();
131
+ this.#expectText('(', 'after present');
132
+ control = this.#expectIdentifier('present control');
133
+ this.#expectText(')', 'after present control');
134
+ explicitPresence = true;
135
+ }
136
+ else {
137
+ control = this.#expectIdentifier('when control');
138
+ }
116
139
  this.#validateCamel(control, 'when control', 'SYQL3004_INVALID_WHEN');
117
140
  if (seen.has(control.text)) {
118
141
  this.#fail('SYQL3004_INVALID_WHEN', control, `duplicate when control ${JSON.stringify(control.text)}`);
119
142
  }
120
143
  seen.add(control.text);
121
144
  controls.push(control.text);
145
+ explicitPresenceFlags.push(explicitPresence);
122
146
  controlSpans.push(control.span);
123
147
  if (this.#peek()?.token.text !== ',')
124
148
  break;
@@ -127,107 +151,92 @@ class EmbeddedParser {
127
151
  break;
128
152
  }
129
153
  this.#expectText(')', 'after when controls');
130
- const open = this.#expectText('{', 'to open when body');
131
- const closeIndex = this.#matchingBraceIndex(open);
132
- const close = this.#tokens[closeIndex];
133
- const bodyTokens = this.#tokens.slice(this.#index, closeIndex);
154
+ let close;
155
+ let bodyTokens;
156
+ if (this.#peek()?.token.text === '{') {
157
+ const open = this.#expectText('{', 'to open when body');
158
+ const closeIndex = this.#matchingBraceIndex(open);
159
+ close = this.#tokens[closeIndex];
160
+ bodyTokens = this.#tokens.slice(this.#index, closeIndex);
161
+ this.#index = closeIndex + 1;
162
+ }
163
+ else {
164
+ const closeIndex = this.#singleWhenBodyEndIndex();
165
+ bodyTokens = this.#tokens.slice(this.#index, closeIndex);
166
+ const significant = bodyTokens.filter((token) => !isSyqlTrivia(token));
167
+ close = significant[significant.length - 1] ?? start;
168
+ this.#index = closeIndex;
169
+ }
134
170
  if (bodyTokens.every(isSyqlTrivia)) {
135
171
  this.#fail('SYQL3004_INVALID_WHEN', close, 'when body must not be empty');
136
172
  }
137
173
  const bodySpan = {
138
174
  file: this.#file,
139
- start: open.span.end,
140
- end: close.span.start,
175
+ start: bodyTokens[0]?.span.start ?? close.span.start,
176
+ end: bodyTokens[bodyTokens.length - 1]?.span.end ?? close.span.start,
141
177
  };
142
- const body = parseSyqlEmbeddedTemplate(this.#file, bodyTokens, bodySpan, 'condition');
143
- this.#index = closeIndex + 1;
178
+ const body = parseSyqlEmbeddedTemplate(this.#file, bodyTokens, bodySpan, 'condition', this.#rangeNames);
144
179
  return {
145
180
  kind: 'when',
146
181
  controls,
182
+ explicitPresence: explicitPresenceFlags,
147
183
  controlSpans,
148
184
  body,
149
185
  tokens: this.#tokens.slice(startIndex, this.#index),
150
186
  span: spanBetween(start, close),
151
187
  };
152
188
  }
153
- #parseDirective(kind) {
154
- const startIndex = this.#index;
155
- const start = this.#take();
156
- if (this.#mode !== 'statement') {
157
- this.#fail('SYQL3006_FORBIDDEN_TEMPLATE_NODE', start, `@${kind} is not allowed in a ${this.#mode} template`);
158
- }
159
- this.#expectText('(', `after @${kind}`);
160
- const bindings = [];
161
- if (this.#peek()?.token.text === ')') {
162
- this.#fail('SYQL3005_INVALID_REACTIVE_DIRECTIVE', this.#peek()?.token ?? start, `@${kind} requires at least one scope binding`);
163
- }
164
- for (;;) {
165
- bindings.push(this.#parseScopeBinding());
166
- if (this.#peek()?.token.text !== ',')
167
- break;
168
- this.#take();
169
- if (this.#peek()?.token.text === ')')
170
- break;
171
- }
172
- const close = this.#expectText(')', `to close @${kind}`);
173
- return {
174
- kind,
175
- bindings,
176
- tokens: this.#tokens.slice(startIndex, this.#index),
177
- span: spanBetween(start, close),
178
- };
179
- }
180
- #parseScopeBinding() {
181
- const qualifier = this.#expectIdentifier('scope table or alias');
182
- this.#validateSqlIdent(qualifier, 'scope table or alias');
183
- this.#expectText('.', 'in qualified scope column');
184
- const columnName = this.#expectIdentifier('scope column');
185
- this.#validateSqlIdent(columnName, 'scope column');
186
- const column = {
187
- qualifier: qualifier.text,
188
- name: columnName.text,
189
- span: spanBetween(qualifier, columnName),
190
- };
191
- const operator = this.#take();
192
- if (operator === undefined ||
193
- (operator.text !== '=' && operator.text !== 'in')) {
194
- this.#fail('SYQL3005_INVALID_REACTIVE_DIRECTIVE', operator ?? columnName, 'scope binding must use = or lowercase in');
195
- }
196
- const values = [];
197
- let end;
198
- if (operator.text === '=') {
199
- const value = this.#parseBindReference();
200
- values.push(value);
201
- end = value.token;
202
- }
203
- else {
204
- this.#expectText('(', 'after scope in');
205
- if (this.#peek()?.token.text === ')') {
206
- this.#fail('SYQL3005_INVALID_REACTIVE_DIRECTIVE', this.#peek()?.token ?? operator, 'scope in list must not be empty');
189
+ #singleWhenBodyEndIndex() {
190
+ let depth = 0;
191
+ let pendingBetween = false;
192
+ let rangeBetween = false;
193
+ const clauseEnders = new Set([
194
+ 'group',
195
+ 'having',
196
+ 'order',
197
+ 'limit',
198
+ 'offset',
199
+ 'window',
200
+ 'union',
201
+ 'intersect',
202
+ 'except',
203
+ ]);
204
+ for (let cursor = this.#index; cursor < this.#tokens.length; cursor += 1) {
205
+ const token = this.#tokens[cursor];
206
+ if (isSyqlTrivia(token))
207
+ continue;
208
+ if (token.text === '(')
209
+ depth += 1;
210
+ else if (token.text === ')')
211
+ depth -= 1;
212
+ if (depth !== 0)
213
+ continue;
214
+ const lower = token.text.toLowerCase();
215
+ if (token.kind === 'identifier' && lower === 'between') {
216
+ const next = this.#peek(cursor + 1)?.token;
217
+ pendingBetween = true;
218
+ rangeBetween =
219
+ next?.kind === 'bind' && this.#rangeNames.has(next.text.slice(1));
220
+ continue;
207
221
  }
208
- const names = new Set();
209
- for (;;) {
210
- const value = this.#parseBindReference();
211
- if (names.has(value.name)) {
212
- this.#fail('SYQL3005_INVALID_REACTIVE_DIRECTIVE', value.token, `duplicate scope bind :${value.name}`);
222
+ if (token.kind === 'identifier' && lower === 'and') {
223
+ if (pendingBetween && !rangeBetween) {
224
+ pendingBetween = false;
225
+ continue;
213
226
  }
214
- names.add(value.name);
215
- values.push(value);
216
- if (this.#peek()?.token.text !== ',')
217
- break;
218
- this.#take();
219
- if (this.#peek()?.token.text === ')')
220
- break;
227
+ return this.#leadingTriviaIndex(cursor);
221
228
  }
222
- end = this.#expectText(')', 'to close scope in list');
229
+ if (token.text === '}' || clauseEnders.has(lower))
230
+ return this.#leadingTriviaIndex(cursor);
223
231
  }
224
- return {
225
- kind: 'scope-binding',
226
- column,
227
- operator: operator.text === '=' ? 'equal' : 'in',
228
- values,
229
- span: spanBetween(qualifier, end),
230
- };
232
+ return this.#tokens.length;
233
+ }
234
+ #leadingTriviaIndex(index) {
235
+ let cursor = index;
236
+ while (cursor > this.#index &&
237
+ isSyqlTrivia(this.#tokens[cursor - 1]))
238
+ cursor -= 1;
239
+ return cursor;
231
240
  }
232
241
  #parseBindReference() {
233
242
  const token = this.#take();
@@ -241,6 +250,9 @@ class EmbeddedParser {
241
250
  return { kind: 'bind-reference', name, token, span: token.span };
242
251
  }
243
252
  #validateRawToken(token) {
253
+ if (token.kind === 'at-identifier') {
254
+ this.#fail('SYQL3006_FORBIDDEN_TEMPLATE_NODE', token, `${token.text} uses removed directive/call syntax; use ordinary SQL predicates or a normal predicate call`);
255
+ }
244
256
  if (token.kind === 'bind') {
245
257
  if (this.#mode === 'order') {
246
258
  this.#fail('SYQL3006_FORBIDDEN_TEMPLATE_NODE', token, 'binds are not allowed in sort profiles');
@@ -269,11 +281,6 @@ class EmbeddedParser {
269
281
  this.#fail(code, token, `${label} must match [a-z][A-Za-z0-9]* and not use __syql`);
270
282
  }
271
283
  }
272
- #validateSqlIdent(token, label) {
273
- if (!IDENT_RE.test(token.text)) {
274
- this.#fail('SYQL3005_INVALID_REACTIVE_DIRECTIVE', token, `${label} must be an unquoted SQLite identifier`);
275
- }
276
- }
277
284
  #matchingBraceIndex(open) {
278
285
  let depth = 1;
279
286
  for (let cursor = this.#index; cursor < this.#tokens.length; cursor += 1) {
@@ -345,6 +352,6 @@ class EmbeddedParser {
345
352
  }
346
353
  }
347
354
  /** Parse embedded nodes from one already-delimited lossless template. */
348
- export function parseSyqlEmbeddedTemplate(file, tokens, span, mode) {
349
- return new EmbeddedParser(file, tokens, span, mode).parse();
355
+ export function parseSyqlEmbeddedTemplate(file, tokens, span, mode, rangeNames = new Set()) {
356
+ return new EmbeddedParser(file, tokens, span, mode, rangeNames).parse();
350
357
  }
@@ -0,0 +1,35 @@
1
+ /** Schema-aware revision-1 SYQL validation (§§5, 8–13). */
2
+ import type { IrDocument } from './ir.js';
3
+ import { type AnalyzedQuery, type QueryDb, type QueryNamingOptions, type QueryReactiveMetadata } from './query.js';
4
+ import type { SyqlValueType } from './syql-parser.js';
5
+ import type { SyqlLogicalQuery, SyqlSemanticProgram } from './syql-semantics.js';
6
+ export type SyqlValidationErrorCode = 'SYQL6001_INVALID_PLACEMENT' | 'SYQL6002_INVALID_SQL' | 'SYQL6003_NONDETERMINISTIC_SQL' | 'SYQL6004_TYPE_CONFLICT' | 'SYQL6005_INVALID_SYNC_QUERY' | 'SYQL6006_INVALID_SORT' | 'SYQL6007_INVALID_LIMIT' | 'SYQL6008_INVALID_IDENTITY';
7
+ export interface SyqlValidatedSort {
8
+ readonly control: string;
9
+ readonly defaultProfile: string;
10
+ readonly profiles: readonly {
11
+ readonly name: string;
12
+ readonly sql: string;
13
+ }[];
14
+ }
15
+ export interface SyqlValidatedQuery {
16
+ readonly logical: SyqlLogicalQuery;
17
+ /** Reference realization: every conditional active, default controls. */
18
+ readonly referenceSql: string;
19
+ readonly analysis: AnalyzedQuery;
20
+ readonly bindTypes: ReadonlyMap<string, SyqlValueType>;
21
+ readonly reactive: QueryReactiveMetadata;
22
+ readonly identity?: readonly string[];
23
+ readonly sort?: SyqlValidatedSort;
24
+ readonly limit?: {
25
+ readonly control: string;
26
+ readonly defaultSize: number;
27
+ readonly maxSize: number;
28
+ };
29
+ }
30
+ export interface SyqlValidatedProgram {
31
+ readonly semantic: SyqlSemanticProgram;
32
+ readonly queries: readonly SyqlValidatedQuery[];
33
+ }
34
+ /** Validate a semantic SYQL program against schema IR and SQLite. */
35
+ export declare function validateSyqlProgram(semantic: SyqlSemanticProgram, ir: IrDocument, db: QueryDb, naming?: QueryNamingOptions): SyqlValidatedProgram;