@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.
- package/README.md +79 -32
- package/dist/cli.js +40 -17
- package/dist/emit-queries-dart.js +168 -55
- package/dist/emit-queries-kotlin.js +167 -55
- package/dist/emit-queries-swift.js +183 -57
- package/dist/emit-queries.js +286 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +348 -316
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +28 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lsp.d.ts +1 -3
- package/dist/lsp.js +349 -187
- package/dist/manifest.d.ts +1 -3
- package/dist/manifest.js +1 -1
- package/dist/query-ir.js +53 -42
- package/dist/query.d.ts +110 -63
- package/dist/query.js +89 -11
- package/dist/syql-ast.d.ts +12 -13
- package/dist/syql-ast.js +26 -20
- package/dist/syql-lexer.d.ts +2 -0
- package/dist/syql-lexer.js +9 -2
- package/dist/syql-lowering.d.ts +22 -0
- package/dist/syql-lowering.js +434 -0
- package/dist/syql-parser.d.ts +19 -18
- package/dist/syql-parser.js +341 -93
- package/dist/syql-semantics.d.ts +73 -0
- package/dist/syql-semantics.js +607 -0
- package/dist/syql-template-parser.d.ts +5 -20
- package/dist/syql-template-parser.js +116 -109
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +993 -0
- package/package.json +3 -3
- package/src/cli.ts +49 -21
- package/src/emit-queries-dart.ts +195 -69
- package/src/emit-queries-kotlin.ts +197 -69
- package/src/emit-queries-swift.ts +224 -68
- package/src/emit-queries.ts +410 -165
- package/src/fmt.ts +405 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +414 -216
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +229 -79
- package/src/syql-ast.ts +38 -34
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +664 -0
- package/src/syql-parser.ts +472 -134
- package/src/syql-semantics.ts +930 -0
- package/src/syql-template-parser.ts +119 -163
- package/src/syql-validator.ts +1486 -0
- package/dist/syql.d.ts +0 -102
- package/dist/syql.js +0 -1141
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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:
|
|
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
|
-
#
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
+
if (token.text === '}' || clauseEnders.has(lower))
|
|
230
|
+
return this.#leadingTriviaIndex(cursor);
|
|
223
231
|
}
|
|
224
|
-
return
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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;
|