@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
@@ -92,9 +92,11 @@ class Lexer {
92
92
  #column = 1;
93
93
  #braceDepth = 0;
94
94
  #expectImportPath = false;
95
- constructor(file, source) {
95
+ #recognizeImportPaths;
96
+ constructor(file, source, recognizeImportPaths = true) {
96
97
  this.#file = file;
97
98
  this.#source = source;
99
+ this.#recognizeImportPaths = recognizeImportPaths;
98
100
  }
99
101
  lex() {
100
102
  while (this.#offset < this.#source.length)
@@ -161,7 +163,8 @@ class Lexer {
161
163
  else if (token.text === '}')
162
164
  this.#braceDepth = Math.max(0, this.#braceDepth - 1);
163
165
  }
164
- if (token.kind === 'identifier' &&
166
+ if (this.#recognizeImportPaths &&
167
+ token.kind === 'identifier' &&
165
168
  token.text === 'from' &&
166
169
  this.#braceDepth === 0) {
167
170
  this.#expectImportPath = true;
@@ -365,3 +368,7 @@ class Lexer {
365
368
  export function lexSyqlSource(file, source) {
366
369
  return new Lexer(file, source).lex();
367
370
  }
371
+ /** Lex an isolated SQL/template string without import-path contextual rules. */
372
+ export function lexSyqlSqlSource(file, source) {
373
+ return new Lexer(file, source, false).lex();
374
+ }
@@ -0,0 +1,22 @@
1
+ import type { IrDocument } from './ir.js';
2
+ import { type AnalyzedQuery, type QueryBackend, type QueryDb, type QueryNamingOptions, type QuerySyqlExecutionPlan } from './query.js';
3
+ import type { SyqlValidatedQuery } from './syql-validator.js';
4
+ export type SyqlLoweringErrorCode = 'SYQL7001_ENUMERATION_LIMIT' | 'SYQL7002_INTERNAL_LOWERING';
5
+ export interface SyqlLoweringOptions {
6
+ readonly backend?: QueryBackend;
7
+ /** Maximum physical activation variants generated for diagnostics and a
8
+ * forced variants backend. Sort profiles multiply this count. */
9
+ readonly maxEnumeratedStatements?: number;
10
+ }
11
+ export interface SyqlLoweredQuery {
12
+ readonly validated: SyqlValidatedQuery;
13
+ readonly analysis: AnalyzedQuery;
14
+ readonly selected: QuerySyqlExecutionPlan;
15
+ readonly neutralized: QuerySyqlExecutionPlan;
16
+ /** Omitted only when enumeration exceeds compiler resource policy. */
17
+ readonly enumerated?: QuerySyqlExecutionPlan;
18
+ }
19
+ /** Lower one validated revision-1 query and prepare every generated physical
20
+ * statement. The selected plan is embedded into the returned shared QueryIR
21
+ * query; the second plan is retained for equivalence conformance tests. */
22
+ export declare function lowerSyqlQuery(validated: SyqlValidatedQuery, ir: IrDocument, db: QueryDb, naming?: QueryNamingOptions, options?: SyqlLoweringOptions): SyqlLoweredQuery;
@@ -0,0 +1,434 @@
1
+ /** Revision-1 SYQL logical-to-physical lowering (§§15–17). */
2
+ import { TypegenError } from './errors.js';
3
+ import { buildNamingMap } from './naming.js';
4
+ import { analyzeStatement, } from './query.js';
5
+ import { lexSyqlSqlSource } from './syql-lexer.js';
6
+ import { syqlRangeEndBind, syqlRangeStartBind } from './syql-semantics.js';
7
+ const DEFAULT_NAMING = {
8
+ naming: 'camel',
9
+ targets: ['ts'],
10
+ backend: 'auto',
11
+ };
12
+ const DEFAULT_ENUMERATION_LIMIT = 256;
13
+ const LIMIT_BIND = '__syqlLimit';
14
+ function fail(code, query, message) {
15
+ throw new TypegenError(`${query.module.file} (query ${query.declaration.name})`, `${code}: ${message}`);
16
+ }
17
+ function renderTemplate(nodes, conditions, mode) {
18
+ return nodes
19
+ .map((node) => {
20
+ if (node.kind === 'sql') {
21
+ return node.parts
22
+ .map((part) => (part.kind === 'text' ? part.text : `:${part.name}`))
23
+ .join('');
24
+ }
25
+ if (node.kind === 'predicate') {
26
+ return `(${renderTemplate(node.body, conditions, mode)})`;
27
+ }
28
+ if (node.kind !== 'when') {
29
+ throw new Error('revision-1 lowering found an unknown logical node');
30
+ }
31
+ const condition = conditions.get(node);
32
+ if (condition === undefined) {
33
+ throw new Error('revision-1 lowering lost a logical condition');
34
+ }
35
+ if (mode.kind === 'neutralized') {
36
+ const predicate = renderTemplate(node.body, conditions, mode);
37
+ return `case when :__syqlActive${condition} = 0 then 1 else (${predicate}) end`;
38
+ }
39
+ if (node.controls.every((control) => mode.active.has(control))) {
40
+ return `(${renderTemplate(node.body, conditions, mode)})`;
41
+ }
42
+ return '1';
43
+ })
44
+ .join('');
45
+ }
46
+ function outerLimitOffset(sql, file) {
47
+ const tokens = lexSyqlSqlSource(file, sql).filter((token) => token.kind !== 'whitespace' &&
48
+ token.kind !== 'line-comment' &&
49
+ token.kind !== 'block-comment' &&
50
+ token.kind !== 'eof');
51
+ let depth = 0;
52
+ for (const token of tokens) {
53
+ if (token.text === ')')
54
+ depth -= 1;
55
+ if (depth === 0 &&
56
+ token.kind === 'identifier' &&
57
+ (token.text.toLowerCase() === 'limit' ||
58
+ token.text.toLowerCase() === 'offset')) {
59
+ return token.span.start.offset;
60
+ }
61
+ if (token.text === '(')
62
+ depth += 1;
63
+ }
64
+ return sql.length;
65
+ }
66
+ function composeControls(sql, file, sortSql, limit) {
67
+ let out = sql.trim();
68
+ if (sortSql !== undefined) {
69
+ const insertion = outerLimitOffset(out, file);
70
+ out = `${out.slice(0, insertion).trimEnd()} order by ${sortSql} ${out
71
+ .slice(insertion)
72
+ .trimStart()}`.trim();
73
+ }
74
+ if (limit !== undefined) {
75
+ // `coalesce` gives the generator's metadata-only execution a valid value;
76
+ // runtimes still validate and bind the effective size before execution.
77
+ out = `${out.trimEnd()} limit min(coalesce(:${LIMIT_BIND}, ${limit.defaultSize}), ${limit.maxSize})`;
78
+ }
79
+ return out;
80
+ }
81
+ function distinctBindNames(sql, file) {
82
+ const names = [];
83
+ for (const token of lexSyqlSqlSource(file, sql)) {
84
+ if (token.kind !== 'bind')
85
+ continue;
86
+ const name = token.text.slice(1);
87
+ if (!names.includes(name))
88
+ names.push(name);
89
+ }
90
+ return names;
91
+ }
92
+ function headersFor(names, valueOwners, conditionBinds, limit, query) {
93
+ return names
94
+ .map((name) => {
95
+ const value = valueOwners.get(name);
96
+ if (value !== undefined)
97
+ return `-- param :${name} ${value.type}`;
98
+ if (conditionBinds.has(name))
99
+ return `-- param :${name} boolean`;
100
+ if (name === LIMIT_BIND && limit !== undefined)
101
+ return `-- param :${name} integer`;
102
+ return fail('SYQL7002_INTERNAL_LOWERING', query, `generated SQL contains unresolved bind :${name}`);
103
+ })
104
+ .join('\n');
105
+ }
106
+ function planBind(name, valueOwners, conditionBinds, conditions, limit, query) {
107
+ const value = valueOwners.get(name);
108
+ if (value?.kind === 'value') {
109
+ return { kind: 'value', name, type: value.type, input: value.input };
110
+ }
111
+ if (value?.kind === 'group-member' && value.member !== undefined) {
112
+ return {
113
+ kind: 'group-member',
114
+ name,
115
+ type: value.type,
116
+ input: value.input,
117
+ member: value.member,
118
+ };
119
+ }
120
+ const condition = conditionBinds.get(name);
121
+ if (condition !== undefined) {
122
+ return {
123
+ kind: 'condition-active',
124
+ name,
125
+ type: 'boolean',
126
+ condition,
127
+ controls: conditions[condition].controls,
128
+ };
129
+ }
130
+ if (name === LIMIT_BIND && limit !== undefined) {
131
+ return {
132
+ kind: 'limit',
133
+ name,
134
+ type: 'integer',
135
+ input: limit.control,
136
+ };
137
+ }
138
+ return fail('SYQL7002_INTERNAL_LOWERING', query, `cannot describe generated bind :${name}`);
139
+ }
140
+ function publicInputs(validated, naming) {
141
+ const query = validated.logical;
142
+ const declaration = query.declaration;
143
+ const publicNames = [
144
+ ...declaration.parameters.map((parameter) => parameter.name),
145
+ ...(declaration.sort === undefined ? [] : [declaration.sort.control]),
146
+ ...(declaration.limit === undefined ? [] : [declaration.limit.control]),
147
+ ];
148
+ const mapped = new Map(buildNamingMap(publicNames, naming.naming, query.module.file, `query ${declaration.name} public inputs`, naming.targets).map((entry) => [entry.sqlName, entry.langName]));
149
+ const result = declaration.parameters.map((parameter) => {
150
+ const langName = mapped.get(parameter.name);
151
+ if (parameter.kind === 'range') {
152
+ const type = validated.bindTypes.get(syqlRangeStartBind(parameter.name));
153
+ if (type === undefined) {
154
+ return fail('SYQL7002_INTERNAL_LOWERING', query, `range ${parameter.name} has no resolved element type`);
155
+ }
156
+ return {
157
+ kind: 'group',
158
+ name: parameter.name,
159
+ langName,
160
+ members: [
161
+ {
162
+ name: 'start',
163
+ langName: 'start',
164
+ type: type.base,
165
+ nullable: type.nullable,
166
+ },
167
+ {
168
+ name: 'end',
169
+ langName: 'end',
170
+ type: type.base,
171
+ nullable: type.nullable,
172
+ },
173
+ ],
174
+ };
175
+ }
176
+ if (parameter.kind === 'group') {
177
+ const members = buildNamingMap(parameter.members.map((member) => member.name), naming.naming, query.module.file, `query ${declaration.name} group ${parameter.name}`, naming.targets);
178
+ return {
179
+ kind: 'group',
180
+ name: parameter.name,
181
+ langName,
182
+ members: parameter.members.map((member, index) => {
183
+ const type = validated.bindTypes.get(member.name);
184
+ if (type === undefined) {
185
+ return fail('SYQL7002_INTERNAL_LOWERING', query, `group member ${parameter.name}.${member.name} has no resolved type`);
186
+ }
187
+ return {
188
+ name: member.name,
189
+ langName: members[index]
190
+ .langName,
191
+ type: type.base,
192
+ nullable: type.nullable,
193
+ };
194
+ }),
195
+ };
196
+ }
197
+ const type = validated.bindTypes.get(parameter.name);
198
+ if (type === undefined) {
199
+ return fail('SYQL7002_INTERNAL_LOWERING', query, `input ${parameter.name} has no resolved type`);
200
+ }
201
+ return {
202
+ kind: 'value',
203
+ name: parameter.name,
204
+ langName,
205
+ type: type.base,
206
+ nullable: type.nullable,
207
+ required: !parameter.optional && parameter.default === undefined,
208
+ ...(parameter.default === undefined
209
+ ? {}
210
+ : { default: parameter.default }),
211
+ };
212
+ });
213
+ if (validated.sort !== undefined) {
214
+ const profiles = buildNamingMap(validated.sort.profiles.map((profile) => profile.name), naming.naming, query.module.file, `query ${declaration.name} sort profiles`, naming.targets);
215
+ result.push({
216
+ kind: 'sort',
217
+ name: validated.sort.control,
218
+ langName: mapped.get(validated.sort.control),
219
+ defaultProfile: validated.sort.defaultProfile,
220
+ profiles: validated.sort.profiles.map((profile, index) => ({
221
+ name: profile.name,
222
+ langName: profiles[index].langName,
223
+ })),
224
+ });
225
+ }
226
+ if (validated.limit !== undefined) {
227
+ result.push({
228
+ kind: 'limit',
229
+ name: validated.limit.control,
230
+ langName: mapped.get(validated.limit.control),
231
+ defaultSize: validated.limit.defaultSize,
232
+ maxSize: validated.limit.maxSize,
233
+ });
234
+ }
235
+ return result;
236
+ }
237
+ function valueOwners(validated) {
238
+ const owners = new Map();
239
+ for (const parameter of validated.logical.declaration.parameters) {
240
+ if (parameter.kind === 'range') {
241
+ for (const [name, member] of [
242
+ [syqlRangeStartBind(parameter.name), 'start'],
243
+ [syqlRangeEndBind(parameter.name), 'end'],
244
+ ]) {
245
+ const type = validated.bindTypes.get(name);
246
+ if (type === undefined)
247
+ continue;
248
+ owners.set(name, {
249
+ kind: 'group-member',
250
+ input: parameter.name,
251
+ member,
252
+ type: type.base,
253
+ });
254
+ }
255
+ }
256
+ else if (parameter.kind === 'group') {
257
+ for (const member of parameter.members) {
258
+ const type = validated.bindTypes.get(member.name);
259
+ if (type === undefined)
260
+ continue;
261
+ owners.set(member.name, {
262
+ kind: 'group-member',
263
+ input: parameter.name,
264
+ member: member.name,
265
+ type: type.base,
266
+ });
267
+ }
268
+ }
269
+ else {
270
+ const type = validated.bindTypes.get(parameter.name);
271
+ if (type === undefined)
272
+ continue;
273
+ owners.set(parameter.name, {
274
+ kind: 'value',
275
+ input: parameter.name,
276
+ type: type.base,
277
+ });
278
+ }
279
+ }
280
+ return owners;
281
+ }
282
+ function activationControls(query) {
283
+ const used = new Set(query.conditions.flatMap((condition) => condition.controls));
284
+ return query.declaration.parameters
285
+ .map((parameter) => parameter.name)
286
+ .filter((name) => used.has(name));
287
+ }
288
+ function lowerStatement(validated, ir, db, naming, sql, sortProfile, activationMask, owners, conditionBinds) {
289
+ const query = validated.logical;
290
+ const location = `${query.module.file} (query ${query.declaration.name}, generated)`;
291
+ const names = distinctBindNames(sql, location);
292
+ const headers = headersFor(names, owners, conditionBinds, validated.limit, query);
293
+ const candidate = headers.length === 0 ? sql : `${headers}\n${sql}`;
294
+ const analyzed = analyzeStatement(query.declaration.name, location, candidate, ir, db, {
295
+ ...naming,
296
+ internalParams: [
297
+ ...conditionBinds.keys(),
298
+ ...(validated.limit === undefined ? [] : [LIMIT_BIND]),
299
+ ...[...owners.keys()].filter((name) => name.startsWith('__syqlRange')),
300
+ ],
301
+ });
302
+ const namedSql = headers.length === 0
303
+ ? analyzed.sql.trim()
304
+ : analyzed.sql.slice(headers.length).trimStart();
305
+ const bindNames = distinctBindNames(namedSql, location);
306
+ return {
307
+ statement: {
308
+ ...(sortProfile === undefined ? {} : { sortProfile }),
309
+ ...(activationMask === undefined ? {} : { activationMask }),
310
+ sql: namedSql,
311
+ positionalSql: analyzed.positionalSql,
312
+ binds: bindNames.map((name) => planBind(name, owners, conditionBinds, query.conditions, validated.limit, query)),
313
+ },
314
+ analysis: { ...analyzed, sourceSql: namedSql, sql: namedSql },
315
+ };
316
+ }
317
+ function sortProfiles(validated) {
318
+ return validated.sort === undefined
319
+ ? [{}]
320
+ : validated.sort.profiles.map((profile) => ({
321
+ name: profile.name,
322
+ sql: profile.sql,
323
+ }));
324
+ }
325
+ /** Lower one validated revision-1 query and prepare every generated physical
326
+ * statement. The selected plan is embedded into the returned shared QueryIR
327
+ * query; the second plan is retained for equivalence conformance tests. */
328
+ export function lowerSyqlQuery(validated, ir, db, naming = DEFAULT_NAMING, options = {}) {
329
+ const logical = validated.logical;
330
+ const conditions = new Map(logical.conditions.map((condition, index) => [condition, index]));
331
+ const controls = activationControls(logical);
332
+ const owners = valueOwners(validated);
333
+ const conditionBinds = new Map(logical.conditions.map((_, index) => [`__syqlActive${index}`, index]));
334
+ const profiles = sortProfiles(validated);
335
+ const neutralStatements = [];
336
+ let neutralDefault;
337
+ for (const profile of profiles) {
338
+ const body = renderTemplate(logical.template, conditions, {
339
+ kind: 'neutralized',
340
+ });
341
+ const sql = composeControls(body, logical.module.file, profile.sql, validated.limit);
342
+ const lowered = lowerStatement(validated, ir, db, naming, sql, profile.name, undefined, owners, conditionBinds);
343
+ neutralStatements.push(lowered.statement);
344
+ if (neutralDefault === undefined ||
345
+ profile.name === validated.sort?.defaultProfile) {
346
+ neutralDefault = lowered.analysis;
347
+ }
348
+ }
349
+ const neutralized = {
350
+ backend: 'neutralize',
351
+ activationControls: controls,
352
+ conditions: logical.conditions.map((condition, index) => ({
353
+ controls: condition.controls,
354
+ bind: `__syqlActive${index}`,
355
+ })),
356
+ statements: neutralStatements,
357
+ };
358
+ const maxStatements = options.maxEnumeratedStatements ?? DEFAULT_ENUMERATION_LIMIT;
359
+ const activationCount = 2 ** controls.length;
360
+ const enumeratedCount = activationCount * profiles.length;
361
+ let enumerated;
362
+ let enumeratedDefault;
363
+ if (Number.isSafeInteger(activationCount) &&
364
+ enumeratedCount <= maxStatements) {
365
+ const statements = [];
366
+ for (let mask = 0; mask < activationCount; mask += 1) {
367
+ const active = new Set(controls.filter((_, index) => (mask & (2 ** index)) !== 0));
368
+ for (const profile of profiles) {
369
+ const body = renderTemplate(logical.template, conditions, {
370
+ kind: 'enumerated',
371
+ active,
372
+ });
373
+ const sql = composeControls(body, logical.module.file, profile.sql, validated.limit);
374
+ const lowered = lowerStatement(validated, ir, db, naming, sql, profile.name, mask, owners, new Map());
375
+ statements.push(lowered.statement);
376
+ if (mask === 0 &&
377
+ (enumeratedDefault === undefined ||
378
+ profile.name === validated.sort?.defaultProfile)) {
379
+ enumeratedDefault = lowered.analysis;
380
+ }
381
+ }
382
+ }
383
+ enumerated = {
384
+ backend: 'variants',
385
+ activationControls: controls,
386
+ conditions: logical.conditions.map((condition) => ({
387
+ controls: condition.controls,
388
+ })),
389
+ statements,
390
+ };
391
+ }
392
+ const policy = options.backend ?? naming.backend ?? 'auto';
393
+ let selected;
394
+ let canonical;
395
+ if (policy === 'variants') {
396
+ if (enumerated === undefined || enumeratedDefault === undefined) {
397
+ return fail('SYQL7001_ENUMERATION_LIMIT', logical, `${enumeratedCount} generated statements exceed the compiler limit ${maxStatements}; use auto/neutralize or raise the diagnostic limit`);
398
+ }
399
+ selected = enumerated;
400
+ canonical = enumeratedDefault;
401
+ }
402
+ else if (policy === 'auto' &&
403
+ controls.length <= 2 &&
404
+ enumerated !== undefined &&
405
+ enumeratedDefault !== undefined) {
406
+ selected = enumerated;
407
+ canonical = enumeratedDefault;
408
+ }
409
+ else {
410
+ selected = neutralized;
411
+ canonical = neutralDefault;
412
+ }
413
+ const metadata = {
414
+ revision: 1,
415
+ inputs: publicInputs(validated, naming),
416
+ plan: selected,
417
+ ...(validated.identity === undefined
418
+ ? {}
419
+ : { identity: validated.identity }),
420
+ };
421
+ const analysis = {
422
+ ...canonical,
423
+ file: logical.module.file,
424
+ reactive: validated.reactive,
425
+ syql: metadata,
426
+ };
427
+ return {
428
+ validated,
429
+ analysis,
430
+ selected,
431
+ neutralized,
432
+ ...(enumerated === undefined ? {} : { enumerated }),
433
+ };
434
+ }
@@ -12,13 +12,17 @@ export interface SyqlValueParameter {
12
12
  readonly name: string;
13
13
  readonly optional: boolean;
14
14
  readonly type?: SyqlValueType;
15
+ /** A source-level default. Revision 1 currently permits only `bool = false`. */
16
+ readonly default?: false;
15
17
  readonly span: SyqlSourceSpan;
16
18
  readonly nameSpan: SyqlSourceSpan;
17
19
  }
18
- export interface SyqlSwitchParameter {
19
- readonly kind: 'switch';
20
+ export interface SyqlRangeParameter {
21
+ readonly kind: 'range';
20
22
  readonly name: string;
21
- readonly optional: true;
23
+ readonly optional: boolean;
24
+ /** Element type, inferred from the left side of BETWEEN when omitted. */
25
+ readonly type?: SyqlValueType;
22
26
  readonly span: SyqlSourceSpan;
23
27
  readonly nameSpan: SyqlSourceSpan;
24
28
  }
@@ -36,7 +40,7 @@ export interface SyqlGroupParameter {
36
40
  readonly span: SyqlSourceSpan;
37
41
  readonly nameSpan: SyqlSourceSpan;
38
42
  }
39
- export type SyqlQueryParameter = SyqlValueParameter | SyqlSwitchParameter | SyqlGroupParameter;
43
+ export type SyqlQueryParameter = SyqlValueParameter | SyqlRangeParameter | SyqlGroupParameter;
40
44
  export interface SyqlPredicateParameter {
41
45
  readonly name: string;
42
46
  readonly type?: SyqlValueType;
@@ -72,11 +76,6 @@ export interface SyqlPredicateDeclaration {
72
76
  readonly span: SyqlSourceSpan;
73
77
  readonly nameSpan: SyqlSourceSpan;
74
78
  }
75
- export interface SyqlSqlSection {
76
- readonly kind: 'sql';
77
- readonly body: SyqlTemplate;
78
- readonly span: SyqlSourceSpan;
79
- }
80
79
  export interface SyqlSortProfile {
81
80
  readonly name: string;
82
81
  readonly order: SyqlTemplate;
@@ -91,27 +90,29 @@ export interface SyqlSortSection {
91
90
  readonly span: SyqlSourceSpan;
92
91
  readonly controlSpan: SyqlSourceSpan;
93
92
  }
94
- export interface SyqlPageDeclaration {
95
- readonly kind: 'page';
93
+ export interface SyqlLimitDeclaration {
94
+ readonly kind: 'limit';
96
95
  readonly control: string;
97
96
  readonly defaultSize: number;
98
97
  readonly maxSize: number;
99
98
  readonly span: SyqlSourceSpan;
100
99
  readonly controlSpan: SyqlSourceSpan;
101
100
  }
102
- export interface SyqlIdentityDeclaration {
103
- readonly kind: 'identity';
104
- readonly fields: readonly string[];
101
+ export interface SyqlSyncDimension {
102
+ readonly qualifier: string;
103
+ readonly column: string;
105
104
  readonly span: SyqlSourceSpan;
106
105
  }
107
106
  export interface SyqlQueryDeclaration {
108
107
  readonly kind: 'query';
109
108
  readonly name: string;
109
+ readonly sync: boolean;
110
+ readonly syncBy?: SyqlSyncDimension;
110
111
  readonly parameters: readonly SyqlQueryParameter[];
111
- readonly sql: SyqlSqlSection;
112
+ /** The SQL-shaped statement body, excluding its terminating semicolon. */
113
+ readonly statement: SyqlTemplate;
112
114
  readonly sort?: SyqlSortSection;
113
- readonly page?: SyqlPageDeclaration;
114
- readonly identity?: SyqlIdentityDeclaration;
115
+ readonly limit?: SyqlLimitDeclaration;
115
116
  readonly span: SyqlSourceSpan;
116
117
  readonly nameSpan: SyqlSourceSpan;
117
118
  }
@@ -127,6 +128,6 @@ export interface SyqlSyntaxFile {
127
128
  readonly queries: readonly SyqlQueryDeclaration[];
128
129
  readonly span: SyqlSourceSpan;
129
130
  }
130
- export type SyqlParseErrorCode = 'SYQL2001_EXPECTED_TOKEN' | 'SYQL2002_INVALID_NAME' | 'SYQL2003_RESERVED_NAME' | 'SYQL2004_DUPLICATE_NAME' | 'SYQL2005_INVALID_IMPORT' | 'SYQL2006_EMPTY_TEMPLATE' | 'SYQL2007_FORBIDDEN_SEMICOLON' | 'SYQL2008_INVALID_MEMBER' | 'SYQL2009_INVALID_INTEGER' | 'SYQL2010_INVALID_PAGE_RANGE' | 'SYQL2011_INVALID_PARAMETER';
131
+ export type SyqlParseErrorCode = 'SYQL2001_EXPECTED_TOKEN' | 'SYQL2002_INVALID_NAME' | 'SYQL2003_RESERVED_NAME' | 'SYQL2004_DUPLICATE_NAME' | 'SYQL2005_INVALID_IMPORT' | 'SYQL2006_EMPTY_TEMPLATE' | 'SYQL2007_FORBIDDEN_SEMICOLON' | 'SYQL2008_INVALID_MEMBER' | 'SYQL2009_INVALID_INTEGER' | 'SYQL2010_INVALID_PAGE_RANGE' | 'SYQL2011_INVALID_PARAMETER' | 'SYQL2012_INVALID_QUERY_BODY';
131
132
  /** Parse one `.syql` file using the destructive revision-1 grammar. */
132
133
  export declare function parseSyqlSyntaxFile(file: string, source: string): SyqlSyntaxFile;