@syncular/typegen 0.6.0 → 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 +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
package/dist/syql-validator.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { analyzeStatement, inferParamTypeEvidence, scanTableRefs, stripCommentsAndStrings, } from './query.js';
|
|
2
2
|
import { isSyqlTrivia, lexSyqlSqlSource, SyqlFrontendError, } from './syql-lexer.js';
|
|
3
|
+
import { syqlRangeEndBind, syqlRangeStartBind } from './syql-semantics.js';
|
|
3
4
|
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4
5
|
const NONDETERMINISTIC_FUNCTIONS = new Set([
|
|
5
6
|
'random',
|
|
@@ -246,44 +247,56 @@ class Validator {
|
|
|
246
247
|
const activeSql = this.#render(logical.template, 'active', []);
|
|
247
248
|
const activeStructure = this.#inspect(activeSql, location);
|
|
248
249
|
this.#validateStatementShape(activeSql, activeStructure, logical.declaration, location);
|
|
249
|
-
this.#validateDeterminism(activeSql, logical.declaration.
|
|
250
|
-
this.#validatePortableProfile(activeSql, logical.declaration.
|
|
250
|
+
this.#validateDeterminism(activeSql, logical.declaration.statement.span);
|
|
251
|
+
this.#validatePortableProfile(activeSql, logical.declaration.statement.span);
|
|
251
252
|
const refs = scanTableRefs(activeSql, this.#ir);
|
|
252
253
|
const bindSymbols = this.#bindSymbols(logical.declaration);
|
|
253
|
-
const
|
|
254
|
-
const bindTypes = this.#resolveBindTypes(logical, activeSql, refs, bindSymbols, resolvedDirectives);
|
|
254
|
+
const bindTypes = this.#resolveBindTypes(logical, activeSql, refs, bindSymbols);
|
|
255
255
|
const sort = this.#validateSort(logical.declaration, activeStructure, location);
|
|
256
256
|
let referenceSql = this.#composeSort(activeSql, activeStructure, sort?.profiles.find((profile) => profile.name === sort.defaultProfile)
|
|
257
257
|
?.sql);
|
|
258
|
-
if (logical.declaration.
|
|
259
|
-
referenceSql = `${referenceSql.trimEnd()} limit ${logical.declaration.
|
|
258
|
+
if (logical.declaration.limit !== undefined) {
|
|
259
|
+
referenceSql = `${referenceSql.trimEnd()} limit ${logical.declaration.limit.defaultSize}`;
|
|
260
260
|
}
|
|
261
|
+
const usedBindNames = new Set(lexSyqlSqlSource(location, activeSql)
|
|
262
|
+
.filter((token) => token.kind === 'bind')
|
|
263
|
+
.map((token) => token.text.slice(1)));
|
|
261
264
|
const headers = [...bindTypes]
|
|
265
|
+
.filter(([name]) => usedBindNames.has(name))
|
|
262
266
|
.map(([name, type]) => `-- param :${name} ${type.base}`)
|
|
263
267
|
.join('\n');
|
|
264
268
|
const statement = headers.length === 0 ? referenceSql : `${headers}\n${referenceSql}`;
|
|
265
269
|
let analysis;
|
|
266
270
|
try {
|
|
267
|
-
|
|
271
|
+
const analysisNaming = this.#naming === undefined
|
|
272
|
+
? undefined
|
|
273
|
+
: {
|
|
274
|
+
...this.#naming,
|
|
275
|
+
internalParams: [
|
|
276
|
+
...(this.#naming.internalParams ?? []),
|
|
277
|
+
...[...usedBindNames].filter((name) => name.startsWith('__syql')),
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
analysis = analyzeStatement(logical.declaration.name, location, statement, this.#ir, this.#db, analysisNaming);
|
|
268
281
|
}
|
|
269
282
|
catch (error) {
|
|
270
283
|
const message = error instanceof Error ? error.message : String(error);
|
|
271
|
-
this.#fail('SYQL6002_INVALID_SQL', logical.declaration.
|
|
284
|
+
this.#fail('SYQL6002_INVALID_SQL', logical.declaration.statement.span, `reference SQL rejected: ${message}`);
|
|
272
285
|
}
|
|
273
286
|
for (const profile of sort?.profiles ?? []) {
|
|
274
287
|
const candidate = this.#composeSort(activeSql, activeStructure, profile.sql);
|
|
275
|
-
const paged = logical.declaration.
|
|
288
|
+
const paged = logical.declaration.limit === undefined
|
|
276
289
|
? candidate
|
|
277
|
-
: `${candidate.trimEnd()} limit ${logical.declaration.
|
|
290
|
+
: `${candidate.trimEnd()} limit ${logical.declaration.limit.defaultSize}`;
|
|
278
291
|
try {
|
|
279
292
|
this.#db.analyze(paged);
|
|
280
293
|
}
|
|
281
294
|
catch (error) {
|
|
282
295
|
const message = error instanceof Error ? error.message : String(error);
|
|
283
|
-
this.#fail('SYQL6006_INVALID_SORT', logical.declaration.sort?.span ?? logical.declaration.
|
|
296
|
+
this.#fail('SYQL6006_INVALID_SORT', logical.declaration.sort?.span ?? logical.declaration.statement.span, `sort profile ${profile.name} rejected by SQLite: ${message}`);
|
|
284
297
|
}
|
|
285
298
|
}
|
|
286
|
-
const reactive = this.#
|
|
299
|
+
const reactive = this.#inferReactive(logical, markerSql, refs, bindSymbols);
|
|
287
300
|
const identity = this.#proveIdentity(logical.declaration, activeSql, refs, analysis);
|
|
288
301
|
this.#validateStableOrder(logical.declaration, activeSql, activeStructure, sort, identity, analysis, refs);
|
|
289
302
|
const reactiveWithIdentity = {
|
|
@@ -298,13 +311,13 @@ class Validator {
|
|
|
298
311
|
reactive: reactiveWithIdentity,
|
|
299
312
|
...(identity === undefined ? {} : { identity }),
|
|
300
313
|
...(sort === undefined ? {} : { sort }),
|
|
301
|
-
...(logical.declaration.
|
|
314
|
+
...(logical.declaration.limit === undefined
|
|
302
315
|
? {}
|
|
303
316
|
: {
|
|
304
|
-
|
|
305
|
-
control: logical.declaration.
|
|
306
|
-
defaultSize: logical.declaration.
|
|
307
|
-
maxSize: logical.declaration.
|
|
317
|
+
limit: {
|
|
318
|
+
control: logical.declaration.limit.control,
|
|
319
|
+
defaultSize: logical.declaration.limit.defaultSize,
|
|
320
|
+
maxSize: logical.declaration.limit.maxSize,
|
|
308
321
|
},
|
|
309
322
|
}),
|
|
310
323
|
};
|
|
@@ -328,20 +341,10 @@ class Validator {
|
|
|
328
341
|
if (node.kind === 'when') {
|
|
329
342
|
return `(${this.#render(node.body, mode, markers)})`;
|
|
330
343
|
}
|
|
331
|
-
|
|
344
|
+
throw new Error('unknown SYQL logical template node');
|
|
332
345
|
})
|
|
333
346
|
.join('');
|
|
334
347
|
}
|
|
335
|
-
#renderDirective(node) {
|
|
336
|
-
const predicates = node.directive.bindings.map((binding) => {
|
|
337
|
-
const column = `${binding.column.qualifier}.${binding.column.name}`;
|
|
338
|
-
const values = binding.values.map((value) => `:${value.name}`);
|
|
339
|
-
return binding.operator === 'equal'
|
|
340
|
-
? `${column} = ${values[0]}`
|
|
341
|
-
: `${column} in (${values.join(', ')})`;
|
|
342
|
-
});
|
|
343
|
-
return `(${predicates.join(' and ')})`;
|
|
344
|
-
}
|
|
345
348
|
#inspect(sql, file) {
|
|
346
349
|
const raw = significant(lexSyqlSqlSource(file, sql));
|
|
347
350
|
const tokens = [];
|
|
@@ -435,42 +438,37 @@ class Validator {
|
|
|
435
438
|
const boundaryBefore = previous === 'where' || previous === 'having' || previous === 'and';
|
|
436
439
|
const boundaryAfter = next === undefined || next === 'and' || OUTER_CLAUSE_ENDERS.has(next);
|
|
437
440
|
if (!boundaryBefore || !boundaryAfter) {
|
|
438
|
-
this.#fail('SYQL6001_INVALID_PLACEMENT', marker.node.span,
|
|
439
|
-
}
|
|
440
|
-
if (marker.node.kind === 'when') {
|
|
441
|
-
if (clause !== 'where' && clause !== 'having') {
|
|
442
|
-
this.#fail('SYQL6001_INVALID_PLACEMENT', marker.node.span, 'when is allowed only in the outer WHERE or HAVING clause');
|
|
443
|
-
}
|
|
441
|
+
this.#fail('SYQL6001_INVALID_PLACEMENT', marker.node.span, 'when must be an entire outer conjunct');
|
|
444
442
|
}
|
|
445
|
-
|
|
446
|
-
this.#fail('SYQL6001_INVALID_PLACEMENT', marker.node.span,
|
|
443
|
+
if (clause !== 'where' && clause !== 'having') {
|
|
444
|
+
this.#fail('SYQL6001_INVALID_PLACEMENT', marker.node.span, 'when is allowed only in the outer WHERE or HAVING clause');
|
|
447
445
|
}
|
|
448
446
|
}
|
|
449
447
|
for (const marker of markers) {
|
|
450
448
|
if (!structure.outer.some((item) => item.token.text === marker.name)) {
|
|
451
|
-
this.#fail('SYQL6001_INVALID_PLACEMENT', marker.node.span,
|
|
449
|
+
this.#fail('SYQL6001_INVALID_PLACEMENT', marker.node.span, 'when cannot appear in a nested SQL expression or statement');
|
|
452
450
|
}
|
|
453
451
|
}
|
|
454
452
|
if (structure.hasCompound && markers.length > 0) {
|
|
455
|
-
this.#fail('SYQL6001_INVALID_PLACEMENT', query.
|
|
453
|
+
this.#fail('SYQL6001_INVALID_PLACEMENT', query.statement.span, 'embedded SYQL conjuncts are ambiguous in a compound outer statement');
|
|
456
454
|
}
|
|
457
455
|
}
|
|
458
456
|
#validateStatementShape(sql, structure, query, location) {
|
|
459
457
|
if (query.sort !== undefined && structure.hasOuterOrder) {
|
|
460
458
|
this.#fail('SYQL6006_INVALID_SORT', query.sort.span, 'sort section conflicts with an authored outer ORDER BY');
|
|
461
459
|
}
|
|
462
|
-
if (query.
|
|
460
|
+
if (query.limit !== undefined &&
|
|
463
461
|
(structure.hasOuterLimit || structure.hasOuterOffset)) {
|
|
464
|
-
this.#fail('
|
|
462
|
+
this.#fail('SYQL6007_INVALID_LIMIT', query.limit.span, 'dynamic LIMIT conflicts with an authored outer LIMIT or OFFSET');
|
|
465
463
|
}
|
|
466
464
|
const first = structure.outer
|
|
467
465
|
.find((item) => item.token.kind === 'identifier')
|
|
468
466
|
?.token.text.toLowerCase();
|
|
469
467
|
if (first !== 'select' && first !== 'with') {
|
|
470
|
-
this.#fail('SYQL6002_INVALID_SQL', query.
|
|
468
|
+
this.#fail('SYQL6002_INVALID_SQL', query.statement.span, `${location} must contain one SELECT or WITH ... SELECT statement`);
|
|
471
469
|
}
|
|
472
470
|
if (sql.trim().length === 0) {
|
|
473
|
-
this.#fail('SYQL6002_INVALID_SQL', query.
|
|
471
|
+
this.#fail('SYQL6002_INVALID_SQL', query.statement.span, 'empty SQL statement');
|
|
474
472
|
}
|
|
475
473
|
}
|
|
476
474
|
#validateDeterminism(sql, span) {
|
|
@@ -562,9 +560,22 @@ class Validator {
|
|
|
562
560
|
#bindSymbols(query) {
|
|
563
561
|
const symbols = new Map();
|
|
564
562
|
for (const parameter of query.parameters) {
|
|
565
|
-
if (parameter.kind === '
|
|
566
|
-
|
|
567
|
-
|
|
563
|
+
if (parameter.kind === 'range') {
|
|
564
|
+
for (const name of [
|
|
565
|
+
syqlRangeStartBind(parameter.name),
|
|
566
|
+
syqlRangeEndBind(parameter.name),
|
|
567
|
+
]) {
|
|
568
|
+
symbols.set(name, {
|
|
569
|
+
name,
|
|
570
|
+
parameter,
|
|
571
|
+
span: parameter.nameSpan,
|
|
572
|
+
...(parameter.type === undefined
|
|
573
|
+
? {}
|
|
574
|
+
: { authoredType: parameter.type }),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
else if (parameter.kind === 'group') {
|
|
568
579
|
for (const member of parameter.members) {
|
|
569
580
|
symbols.set(member.name, {
|
|
570
581
|
name: member.name,
|
|
@@ -587,26 +598,10 @@ class Validator {
|
|
|
587
598
|
}
|
|
588
599
|
return symbols;
|
|
589
600
|
}
|
|
590
|
-
#resolveBindTypes(logical, sql, refs, symbols
|
|
601
|
+
#resolveBindTypes(logical, sql, refs, symbols) {
|
|
591
602
|
const resolved = new Map(logical.bindTypes);
|
|
592
|
-
const directiveTypes = new Map();
|
|
593
|
-
for (const directive of directives) {
|
|
594
|
-
for (const binding of directive.node.directive.bindings) {
|
|
595
|
-
const column = directive.table.columns.find((item) => item.name === binding.column.name);
|
|
596
|
-
if (column === undefined)
|
|
597
|
-
continue;
|
|
598
|
-
for (const bind of binding.values) {
|
|
599
|
-
const list = directiveTypes.get(bind.name) ?? [];
|
|
600
|
-
list.push(column.type);
|
|
601
|
-
directiveTypes.set(bind.name, list);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
603
|
for (const symbol of symbols.values()) {
|
|
606
|
-
const evidence =
|
|
607
|
-
...inferParamTypeEvidence(symbol.name, sql, refs, this.#ir),
|
|
608
|
-
...(directiveTypes.get(symbol.name) ?? []),
|
|
609
|
-
];
|
|
604
|
+
const evidence = inferParamTypeEvidence(symbol.name, sql, refs, this.#ir);
|
|
610
605
|
const unique = [...new Set(evidence)];
|
|
611
606
|
if (unique.length > 1) {
|
|
612
607
|
this.#fail('SYQL6004_TYPE_CONFLICT', symbol.span, `bind :${symbol.name} has conflicting checked types: ${unique.join(', ')}`);
|
|
@@ -627,148 +622,206 @@ class Validator {
|
|
|
627
622
|
this.#fail('SYQL6004_TYPE_CONFLICT', symbol.span, `cannot infer a revision-1 type for bind :${symbol.name}; add an annotation`);
|
|
628
623
|
}
|
|
629
624
|
}
|
|
630
|
-
for (const directive of directives) {
|
|
631
|
-
for (const binding of directive.node.directive.bindings) {
|
|
632
|
-
const column = directive.table.columns.find((item) => item.name === binding.column.name);
|
|
633
|
-
if (column === undefined)
|
|
634
|
-
continue;
|
|
635
|
-
for (const bind of binding.values) {
|
|
636
|
-
const type = resolved.get(bind.name);
|
|
637
|
-
if (type === undefined ||
|
|
638
|
-
type.base !== column.type ||
|
|
639
|
-
type.nullable !== column.nullable) {
|
|
640
|
-
this.#fail('SYQL6005_INVALID_REACTIVE_DIRECTIVE', bind.span, `scope bind :${bind.name} must exactly match ${column.type}${column.nullable ? ' | null' : ''}`);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
625
|
return resolved;
|
|
646
626
|
}
|
|
647
|
-
#
|
|
648
|
-
const
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
627
|
+
#inferReactive(logical, markerSql, refs, symbols) {
|
|
628
|
+
const cleaned = stripCommentsAndStrings(markerSql);
|
|
629
|
+
const structure = this.#inspect(cleaned, logical.declaration.statement.span.file);
|
|
630
|
+
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
631
|
+
const requiredAt = (index) => {
|
|
632
|
+
let whereStart;
|
|
633
|
+
for (const item of structure.outer) {
|
|
634
|
+
if (item.token.span.start.offset >= index)
|
|
635
|
+
break;
|
|
636
|
+
const lower = tokenLower(item.token);
|
|
637
|
+
if (lower === 'where')
|
|
638
|
+
whereStart = item.token.span.end.offset;
|
|
639
|
+
else if (OUTER_CLAUSE_ENDERS.has(lower ?? ''))
|
|
640
|
+
whereStart = undefined;
|
|
641
|
+
}
|
|
642
|
+
if (whereStart === undefined)
|
|
643
|
+
return false;
|
|
644
|
+
const clauseEnd = structure.outer.find((item) => item.token.span.start.offset > index &&
|
|
645
|
+
OUTER_CLAUSE_ENDERS.has(tokenLower(item.token) ?? ''))?.token.span.start.offset ?? cleaned.length;
|
|
646
|
+
const matchStack = [];
|
|
647
|
+
for (let cursor = whereStart; cursor < index; cursor += 1) {
|
|
648
|
+
if (cleaned[cursor] === '(')
|
|
649
|
+
matchStack.push(cursor);
|
|
650
|
+
else if (cleaned[cursor] === ')')
|
|
651
|
+
matchStack.pop();
|
|
652
|
+
}
|
|
653
|
+
// Parenthesized boolean expressions are valid, but predicates inside a
|
|
654
|
+
// CTE or subquery are not outer proof obligations for sync coverage.
|
|
655
|
+
if (matchStack.some((open) => /\b(?:SELECT|WITH)\b/i.test(cleaned.slice(open + 1, index)))) {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
// Negated equality does not constrain the result to that scope.
|
|
659
|
+
const prefix = cleaned.slice(whereStart, index);
|
|
660
|
+
const boundaries = [...prefix.matchAll(/\b(?:AND|OR)\b/gi)].map((match) => (match.index ?? -1) + match[0].length);
|
|
661
|
+
const lastBoundary = Math.max(prefix.lastIndexOf('('), ...boundaries);
|
|
662
|
+
if (/\bNOT\b/i.test(prefix.slice(lastBoundary + 1)))
|
|
663
|
+
return false;
|
|
664
|
+
if (matchStack.some((open) => /\bNOT\s*$/i.test(cleaned.slice(whereStart, open)))) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
const stack = [];
|
|
668
|
+
for (let cursor = whereStart; cursor < clauseEnd; cursor += 1) {
|
|
669
|
+
const char = cleaned[cursor];
|
|
670
|
+
if (char === '(')
|
|
671
|
+
stack.push(cursor);
|
|
672
|
+
else if (char === ')')
|
|
673
|
+
stack.pop();
|
|
674
|
+
if (/^OR\b/i.test(cleaned.slice(cursor)) &&
|
|
675
|
+
stack.length <= matchStack.length &&
|
|
676
|
+
stack.every((open, stackIndex) => matchStack[stackIndex] === open)) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
655
679
|
}
|
|
680
|
+
return true;
|
|
656
681
|
};
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const matchingRefs = refs.filter((ref) => ref.alias === qualifier);
|
|
665
|
-
if (matchingRefs.length !== 1) {
|
|
666
|
-
this.#fail('SYQL6005_INVALID_REACTIVE_DIRECTIVE', first.column.span, `scope qualifier ${qualifier} must resolve to exactly one read table instance`);
|
|
667
|
-
}
|
|
668
|
-
const ref = matchingRefs[0];
|
|
682
|
+
const required = new Set([...symbols.values()].flatMap((symbol) => symbol.parameter.kind === 'value' &&
|
|
683
|
+
!symbol.parameter.optional &&
|
|
684
|
+
symbol.authoredType?.nullable !== true
|
|
685
|
+
? [symbol.name]
|
|
686
|
+
: []));
|
|
687
|
+
const candidates = [];
|
|
688
|
+
for (const ref of refs) {
|
|
669
689
|
const table = this.#ir.tables.find((item) => item.name === ref.table);
|
|
670
|
-
if (table === undefined)
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
this.#
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
690
|
+
if (table === undefined)
|
|
691
|
+
continue;
|
|
692
|
+
const inferred = [];
|
|
693
|
+
for (const scope of table.scopes) {
|
|
694
|
+
const alias = escapeRegex(ref.alias);
|
|
695
|
+
const column = escapeRegex(scope.column);
|
|
696
|
+
const unqualifiedMatches = refs.filter((candidate) => {
|
|
697
|
+
const other = this.#ir.tables.find((item) => item.name === candidate.table);
|
|
698
|
+
return other?.columns.some((item) => item.name === scope.column);
|
|
699
|
+
}).length;
|
|
700
|
+
const subject = unqualifiedMatches === 1
|
|
701
|
+
? `(?:${alias}\\.)?${column}`
|
|
702
|
+
: `${alias}\\.${column}`;
|
|
703
|
+
const found = [];
|
|
704
|
+
const equality = new RegExp(`${subject}\\s*(?:=|==|\\bIS\\b)\\s*:([A-Za-z_][A-Za-z0-9_]*)\\b|:([A-Za-z_][A-Za-z0-9_]*)\\b\\s*(?:=|==)\\s*${subject}`, 'gi');
|
|
705
|
+
for (const match of cleaned.matchAll(equality)) {
|
|
706
|
+
if (!requiredAt(match.index))
|
|
707
|
+
continue;
|
|
708
|
+
const name = match[1] ?? match[2];
|
|
709
|
+
if (name !== undefined && required.has(name)) {
|
|
710
|
+
found.push({ name, operator: 'equal' });
|
|
711
|
+
}
|
|
686
712
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
713
|
+
const inList = new RegExp(`${subject}\\s+IN\\s*\\(([^)]*)\\)`, 'gi');
|
|
714
|
+
for (const match of cleaned.matchAll(inList)) {
|
|
715
|
+
if (!requiredAt(match.index))
|
|
716
|
+
continue;
|
|
717
|
+
const body = match[1] ?? '';
|
|
718
|
+
const bodyStart = match.index + match[0].length - body.length - 1;
|
|
719
|
+
const bodyTokens = significant(lexSyqlSqlSource(logical.declaration.statement.span.file, markerSql.slice(bodyStart, bodyStart + body.length)));
|
|
720
|
+
const validBindList = bodyTokens.every((token, index) => index % 2 === 0
|
|
721
|
+
? token.kind === 'bind'
|
|
722
|
+
: token.kind === 'punctuation' && token.text === ',');
|
|
723
|
+
const params = bodyTokens
|
|
724
|
+
.filter((_, index) => index % 2 === 0)
|
|
725
|
+
.map((token) => token.text.slice(1));
|
|
726
|
+
if (validBindList &&
|
|
727
|
+
params.length > 0 &&
|
|
728
|
+
params.every((name) => required.has(name))) {
|
|
729
|
+
for (const name of params)
|
|
730
|
+
found.push({ name, operator: 'in' });
|
|
693
731
|
}
|
|
694
732
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
733
|
+
const params = [...new Set(found.map((item) => item.name))];
|
|
734
|
+
if (params.length > 0) {
|
|
735
|
+
inferred.push({
|
|
736
|
+
binding: {
|
|
737
|
+
table: table.name,
|
|
738
|
+
variable: scope.variable,
|
|
739
|
+
pattern: scope.pattern,
|
|
740
|
+
params,
|
|
741
|
+
},
|
|
742
|
+
operator: found.some((item) => item.operator === 'in')
|
|
743
|
+
? 'in'
|
|
744
|
+
: 'equal',
|
|
745
|
+
});
|
|
708
746
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
747
|
+
}
|
|
748
|
+
candidates.push({ ref, scopes: inferred });
|
|
749
|
+
}
|
|
750
|
+
const dependencies = [...new Set(refs.map((ref) => ref.table))]
|
|
751
|
+
.sort()
|
|
752
|
+
.map((table) => {
|
|
753
|
+
const instances = candidates.filter((item) => item.ref.table === table);
|
|
754
|
+
if (instances.some((item) => item.scopes.length === 0)) {
|
|
755
|
+
return { table, scopes: [] };
|
|
756
|
+
}
|
|
757
|
+
const scopes = instances
|
|
758
|
+
.flatMap((item) => item.scopes.map((scope) => scope.binding))
|
|
759
|
+
.reduce((out, scope) => {
|
|
760
|
+
const existing = out.find((item) => item.variable === scope.variable);
|
|
761
|
+
if (existing === undefined)
|
|
762
|
+
out.push(scope);
|
|
763
|
+
else {
|
|
764
|
+
out[out.indexOf(existing)] = {
|
|
765
|
+
...existing,
|
|
766
|
+
params: [...new Set([...existing.params, ...scope.params])],
|
|
767
|
+
};
|
|
715
768
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
769
|
+
return out;
|
|
770
|
+
}, []);
|
|
771
|
+
return { table, scopes };
|
|
772
|
+
});
|
|
773
|
+
if (!logical.declaration.sync)
|
|
774
|
+
return { dependencies, coverage: [] };
|
|
775
|
+
let eligible = candidates.filter((candidate) => {
|
|
776
|
+
const table = this.#ir.tables.find((item) => item.name === candidate.ref.table);
|
|
777
|
+
return (table !== undefined &&
|
|
778
|
+
candidates.filter((item) => item.ref.table === candidate.ref.table)
|
|
779
|
+
.length === 1 &&
|
|
780
|
+
table.scopes.length > 0 &&
|
|
781
|
+
candidate.scopes.length === table.scopes.length);
|
|
782
|
+
});
|
|
783
|
+
const syncBy = logical.declaration.syncBy;
|
|
784
|
+
if (syncBy !== undefined) {
|
|
785
|
+
eligible = eligible.filter((candidate) => candidate.ref.alias === syncBy.qualifier);
|
|
786
|
+
}
|
|
787
|
+
if (eligible.length !== 1) {
|
|
788
|
+
this.#fail('SYQL6005_INVALID_SYNC_QUERY', logical.declaration.syncBy?.span ?? logical.declaration.nameSpan, eligible.length === 0
|
|
789
|
+
? 'sync query coverage cannot be proven from required equality/IN predicates over every declared scope'
|
|
790
|
+
: 'sync query resolves to multiple coverable table instances; split the query or select one with `by alias.scope`');
|
|
791
|
+
}
|
|
792
|
+
const candidate = eligible[0];
|
|
793
|
+
const table = this.#ir.tables.find((item) => item.name === candidate.ref.table);
|
|
794
|
+
if (table === undefined)
|
|
795
|
+
throw new Error('eligible table disappeared');
|
|
796
|
+
if (table.scopes.length > 1 && syncBy === undefined) {
|
|
797
|
+
this.#fail('SYQL6005_INVALID_SYNC_QUERY', logical.declaration.nameSpan, `sync query for multi-scope table ${table.name} must select its unit dimension with \`by ${candidate.ref.alias}.scope_column\``);
|
|
798
|
+
}
|
|
799
|
+
const dimensionColumn = syncBy?.column ?? table.scopes[0]?.column;
|
|
800
|
+
const dimension = table.scopes.find((scope) => scope.column === dimensionColumn);
|
|
801
|
+
if (dimension === undefined) {
|
|
802
|
+
this.#fail('SYQL6005_INVALID_SYNC_QUERY', syncBy?.span ?? logical.declaration.nameSpan, `${candidate.ref.alias}.${dimensionColumn ?? ''} is not a declared scope`);
|
|
803
|
+
}
|
|
804
|
+
const byVariable = new Map(candidate.scopes.map((scope) => [scope.binding.variable, scope]));
|
|
805
|
+
const unit = byVariable.get(dimension.variable);
|
|
806
|
+
const fixedScopes = table.scopes
|
|
807
|
+
.filter((scope) => scope.variable !== dimension.variable)
|
|
808
|
+
.map((scope) => {
|
|
809
|
+
const fixed = byVariable.get(scope.variable);
|
|
810
|
+
if (fixed.operator !== 'equal' || fixed.binding.params.length !== 1) {
|
|
811
|
+
this.#fail('SYQL6005_INVALID_SYNC_QUERY', syncBy?.span ?? logical.declaration.nameSpan, `fixed sync scope ${table.name}.${scope.column} must use one required equality bind`);
|
|
728
812
|
}
|
|
729
813
|
return {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
table,
|
|
733
|
-
scopes,
|
|
734
|
-
...(coverage === undefined ? {} : { coverage }),
|
|
814
|
+
variable: fixed.binding.variable,
|
|
815
|
+
params: fixed.binding.params,
|
|
735
816
|
};
|
|
736
817
|
});
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
for (const directive of directives.filter((item) => item.table.name === tableName)) {
|
|
745
|
-
const list = byAlias.get(directive.ref.alias) ?? [];
|
|
746
|
-
list.push(directive);
|
|
747
|
-
byAlias.set(directive.ref.alias, list);
|
|
748
|
-
}
|
|
749
|
-
const fallback = instances.some((instance) => !byAlias.has(instance.alias));
|
|
750
|
-
const scopes = fallback
|
|
751
|
-
? []
|
|
752
|
-
: [...byAlias.values()]
|
|
753
|
-
.flatMap((items) => items.flatMap((item) => item.scopes))
|
|
754
|
-
.reduce((out, scope) => {
|
|
755
|
-
const existing = out.find((item) => item.variable === scope.variable);
|
|
756
|
-
if (existing === undefined)
|
|
757
|
-
out.push(scope);
|
|
758
|
-
else {
|
|
759
|
-
const merged = [
|
|
760
|
-
...new Set([...existing.params, ...scope.params]),
|
|
761
|
-
];
|
|
762
|
-
out[out.indexOf(existing)] = { ...existing, params: merged };
|
|
763
|
-
}
|
|
764
|
-
return out;
|
|
765
|
-
}, []);
|
|
766
|
-
dependencies.push({ table: tableName, scopes });
|
|
767
|
-
if (!fallback) {
|
|
768
|
-
coverage.push(...[...byAlias.values()].flatMap((items) => items.flatMap((item) => item.coverage === undefined ? [] : [item.coverage])));
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
return { dependencies, coverage };
|
|
818
|
+
const coverage = {
|
|
819
|
+
table: table.name,
|
|
820
|
+
variable: unit.binding.variable,
|
|
821
|
+
units: unit.binding.params,
|
|
822
|
+
fixedScopes,
|
|
823
|
+
};
|
|
824
|
+
return { dependencies, coverage: [coverage] };
|
|
772
825
|
}
|
|
773
826
|
#validateSort(query, structure, location) {
|
|
774
827
|
if (query.sort === undefined)
|
|
@@ -827,39 +880,13 @@ class Validator {
|
|
|
827
880
|
const resultNames = new Set();
|
|
828
881
|
for (const column of analysis.columns) {
|
|
829
882
|
if (!IDENT_RE.test(column.name)) {
|
|
830
|
-
this.#fail('SYQL6008_INVALID_IDENTITY', query.
|
|
883
|
+
this.#fail('SYQL6008_INVALID_IDENTITY', query.statement.span, `result name ${JSON.stringify(column.name)} must be an unquoted IDENT value`);
|
|
831
884
|
}
|
|
832
885
|
if (resultNames.has(column.name)) {
|
|
833
|
-
this.#fail('SYQL6008_INVALID_IDENTITY', query.
|
|
886
|
+
this.#fail('SYQL6008_INVALID_IDENTITY', query.statement.span, `result name ${JSON.stringify(column.name)} is duplicated`);
|
|
834
887
|
}
|
|
835
888
|
resultNames.add(column.name);
|
|
836
889
|
}
|
|
837
|
-
if (query.identity !== undefined) {
|
|
838
|
-
if (nonSimple) {
|
|
839
|
-
this.#fail('SYQL6008_INVALID_IDENTITY', query.identity.span, 'identity cannot be proven for this grouped, compound, nested, outer-join, aggregate, or self-join shape');
|
|
840
|
-
}
|
|
841
|
-
const selected = query.identity.fields.map((field) => {
|
|
842
|
-
const matches = analysis.columns.filter((column) => column.name === field);
|
|
843
|
-
const column = matches[0];
|
|
844
|
-
if (matches.length !== 1 ||
|
|
845
|
-
column === undefined ||
|
|
846
|
-
column.fidelity !== 'exact' ||
|
|
847
|
-
column.nullable ||
|
|
848
|
-
column.origin === undefined) {
|
|
849
|
-
this.#fail('SYQL6008_INVALID_IDENTITY', query.identity?.span ?? query.sql.span, `identity field ${field} must be one exact, non-null physical result column`);
|
|
850
|
-
}
|
|
851
|
-
return column;
|
|
852
|
-
});
|
|
853
|
-
for (const ref of refs) {
|
|
854
|
-
const table = this.#ir.tables.find((item) => item.name === ref.table);
|
|
855
|
-
if (table !== undefined &&
|
|
856
|
-
!selected.some((column) => column.origin?.table === table.name &&
|
|
857
|
-
column.origin.column === table.primaryKey)) {
|
|
858
|
-
this.#fail('SYQL6008_INVALID_IDENTITY', query.identity.span, `identity must include projected primary key ${table.name}.${table.primaryKey}`);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
return selected.map((column) => column.langName);
|
|
862
|
-
}
|
|
863
890
|
if (nonSimple || refs.length === 0)
|
|
864
891
|
return undefined;
|
|
865
892
|
const inferred = [];
|
|
@@ -876,18 +903,18 @@ class Validator {
|
|
|
876
903
|
return inferred;
|
|
877
904
|
}
|
|
878
905
|
#validateStableOrder(query, sql, structure, sort, identity, analysis, refs) {
|
|
879
|
-
const bounded = query.
|
|
906
|
+
const bounded = query.limit !== undefined ||
|
|
880
907
|
structure.hasOuterLimit ||
|
|
881
908
|
structure.hasOuterOffset;
|
|
882
909
|
if (!bounded)
|
|
883
910
|
return;
|
|
884
911
|
if (identity === undefined || identity.length === 0) {
|
|
885
|
-
this.#fail('SYQL6006_INVALID_SORT', query.sort?.span ?? query.
|
|
912
|
+
this.#fail('SYQL6006_INVALID_SORT', query.sort?.span ?? query.limit?.span ?? query.statement.span, 'a bounded query requires a proven identity and total outer order');
|
|
886
913
|
}
|
|
887
914
|
const orders = sort?.profiles.map((profile) => ({
|
|
888
915
|
name: profile.name,
|
|
889
916
|
sql: profile.sql,
|
|
890
|
-
span: query.sort?.span ?? query.
|
|
917
|
+
span: query.sort?.span ?? query.statement.span,
|
|
891
918
|
})) ??
|
|
892
919
|
(structure.orderBodyStart !== undefined &&
|
|
893
920
|
structure.orderEnd !== undefined
|
|
@@ -897,12 +924,12 @@ class Validator {
|
|
|
897
924
|
sql: sql
|
|
898
925
|
.slice(structure.orderBodyStart, structure.orderEnd)
|
|
899
926
|
.trim(),
|
|
900
|
-
span: query.
|
|
927
|
+
span: query.statement.span,
|
|
901
928
|
},
|
|
902
929
|
]
|
|
903
930
|
: []);
|
|
904
931
|
if (orders.length === 0) {
|
|
905
|
-
this.#fail('SYQL6006_INVALID_SORT', query.
|
|
932
|
+
this.#fail('SYQL6006_INVALID_SORT', query.limit?.span ?? query.statement.span, 'a bounded query requires an outer ORDER BY or sort section');
|
|
906
933
|
}
|
|
907
934
|
for (const order of orders) {
|
|
908
935
|
const terms = this.#splitOrderTerms(order.sql, order.span);
|