@syncular/typegen 0.4.0 → 0.5.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 +38 -9
- package/dist/emit-queries.js +63 -5
- package/dist/emit.js +32 -0
- package/dist/generate.d.ts +4 -0
- package/dist/generate.js +18 -6
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +8 -2
- package/dist/query-ir.js +24 -1
- package/dist/query.d.ts +30 -0
- package/dist/query.js +139 -8
- package/dist/syql-ast.d.ts +95 -0
- package/dist/syql-ast.js +138 -0
- package/dist/syql-lexer.d.ts +40 -0
- package/dist/syql-lexer.js +367 -0
- package/dist/syql-modules.d.ts +21 -0
- package/dist/syql-modules.js +129 -0
- package/dist/syql-parser.d.ts +132 -0
- package/dist/syql-parser.js +604 -0
- package/dist/syql-template-parser.d.ts +58 -0
- package/dist/syql-template-parser.js +350 -0
- package/dist/syql.d.ts +19 -0
- package/dist/syql.js +196 -0
- package/package.json +3 -3
- package/src/emit-queries.ts +86 -5
- package/src/emit.ts +38 -0
- package/src/generate.ts +32 -5
- package/src/index.ts +5 -0
- package/src/manifest.ts +11 -2
- package/src/query-ir.ts +24 -1
- package/src/query.ts +207 -8
- package/src/syql-ast.ts +277 -0
- package/src/syql-lexer.ts +514 -0
- package/src/syql-modules.ts +217 -0
- package/src/syql-parser.ts +1021 -0
- package/src/syql-template-parser.ts +582 -0
- package/src/syql.ts +300 -1
package/dist/query.js
CHANGED
|
@@ -298,17 +298,31 @@ function stripCommentsAndStrings(sql) {
|
|
|
298
298
|
const next = sql[i + 1];
|
|
299
299
|
if (ch === '-' && next === '-') {
|
|
300
300
|
const end = sql.indexOf('\n', i);
|
|
301
|
-
|
|
301
|
+
const stop = end === -1 ? sql.length : end;
|
|
302
|
+
out += ' '.repeat(stop - i);
|
|
303
|
+
i = stop;
|
|
302
304
|
}
|
|
303
305
|
else if (ch === '/' && next === '*') {
|
|
304
306
|
const end = sql.indexOf('*/', i + 2);
|
|
305
|
-
|
|
306
|
-
out += ' ';
|
|
307
|
+
const stop = end === -1 ? sql.length : end + 2;
|
|
308
|
+
out += sql.slice(i, stop).replace(/[^\n]/g, ' ');
|
|
309
|
+
i = stop;
|
|
307
310
|
}
|
|
308
311
|
else if (ch === "'") {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
+
let stop = i + 1;
|
|
313
|
+
while (stop < sql.length) {
|
|
314
|
+
if (sql[stop] === "'" && sql[stop + 1] === "'") {
|
|
315
|
+
stop += 2;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (sql[stop] === "'") {
|
|
319
|
+
stop += 1;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
stop += 1;
|
|
323
|
+
}
|
|
324
|
+
out += sql.slice(i, stop).replace(/[^\n]/g, ' ');
|
|
325
|
+
i = stop;
|
|
312
326
|
}
|
|
313
327
|
else {
|
|
314
328
|
out += ch;
|
|
@@ -507,7 +521,7 @@ function resolveSource(item, refs, ir) {
|
|
|
507
521
|
return null;
|
|
508
522
|
const table = byName.get(ref.table);
|
|
509
523
|
const col = table?.columns.find((c) => c.name === columnName);
|
|
510
|
-
return col === undefined ? null : { column: col };
|
|
524
|
+
return col === undefined ? null : { table: ref.table, column: col };
|
|
511
525
|
}
|
|
512
526
|
// Unqualified: search every FROM/JOIN table (SQLite already resolved
|
|
513
527
|
// ambiguity; a single-table query is the common case).
|
|
@@ -515,7 +529,7 @@ function resolveSource(item, refs, ir) {
|
|
|
515
529
|
const table = byName.get(ref.table);
|
|
516
530
|
const col = table?.columns.find((c) => c.name === columnName);
|
|
517
531
|
if (col !== undefined)
|
|
518
|
-
return { column: col };
|
|
532
|
+
return { table: ref.table, column: col };
|
|
519
533
|
}
|
|
520
534
|
return null;
|
|
521
535
|
}
|
|
@@ -598,6 +612,120 @@ function fallbackColumnType(expr) {
|
|
|
598
612
|
return 'float';
|
|
599
613
|
return 'string';
|
|
600
614
|
}
|
|
615
|
+
function inferReactiveMetadata(sql, refs, ir, params, columns) {
|
|
616
|
+
const conservative = /\b(?:UNION|EXCEPT|INTERSECT)\b/i.test(stripCommentsAndStrings(sql));
|
|
617
|
+
const requiredAt = (index) => {
|
|
618
|
+
const cleaned = stripCommentsAndStrings(sql);
|
|
619
|
+
const where = /\bWHERE\b/i.exec(cleaned);
|
|
620
|
+
if (where === null || index < where.index + where[0].length)
|
|
621
|
+
return false;
|
|
622
|
+
const matchStack = [];
|
|
623
|
+
for (let cursor = where.index + where[0].length; cursor < index; cursor += 1) {
|
|
624
|
+
if (cleaned[cursor] === '(')
|
|
625
|
+
matchStack.push(cursor);
|
|
626
|
+
else if (cleaned[cursor] === ')')
|
|
627
|
+
matchStack.pop();
|
|
628
|
+
}
|
|
629
|
+
const stack = [];
|
|
630
|
+
for (let cursor = where.index + where[0].length; cursor < cleaned.length; cursor += 1) {
|
|
631
|
+
const char = cleaned[cursor];
|
|
632
|
+
if (char === '(')
|
|
633
|
+
stack.push(cursor);
|
|
634
|
+
else if (char === ')')
|
|
635
|
+
stack.pop();
|
|
636
|
+
if (cursor > index &&
|
|
637
|
+
/^(?:GROUP\s+BY|ORDER\s+BY|LIMIT|HAVING)\b/i.test(cleaned.slice(cursor))) {
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
if (/^OR\b/i.test(cleaned.slice(cursor)) &&
|
|
641
|
+
stack.every((open, stackIndex) => matchStack[stackIndex] === open)) {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return true;
|
|
646
|
+
};
|
|
647
|
+
const byParam = new Map(params.map((param) => [param.name, param]));
|
|
648
|
+
const dependencies = [];
|
|
649
|
+
const coverage = [];
|
|
650
|
+
for (const tableName of [...new Set(refs.map((ref) => ref.table))].sort()) {
|
|
651
|
+
const table = ir.tables.find((candidate) => candidate.name === tableName);
|
|
652
|
+
const tableRefs = refs.filter((ref) => ref.table === tableName);
|
|
653
|
+
const scopes = [];
|
|
654
|
+
if (table !== undefined && tableRefs.length === 1 && !conservative) {
|
|
655
|
+
const qualifier = tableRefs[0]?.alias;
|
|
656
|
+
for (const scope of table.scopes) {
|
|
657
|
+
const column = scope.column.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
658
|
+
const qualified = qualifier === undefined
|
|
659
|
+
? `(?:${IDENT}\\.)?${column}`
|
|
660
|
+
: `(?:${qualifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.)?${column}`;
|
|
661
|
+
const found = [];
|
|
662
|
+
const equality = new RegExp(`${qualified}\\s*(?:=|==|\\bIS\\b)\\s*:(${IDENT})\\b|:(${IDENT})\\b\\s*(?:=|==)\\s*${qualified}`, 'gi');
|
|
663
|
+
for (const match of sql.matchAll(equality)) {
|
|
664
|
+
if (!requiredAt(match.index))
|
|
665
|
+
continue;
|
|
666
|
+
const name = match[1] ?? match[2];
|
|
667
|
+
const param = name === undefined ? undefined : byParam.get(name);
|
|
668
|
+
if (param !== undefined &&
|
|
669
|
+
param.optional !== true &&
|
|
670
|
+
param.flag !== true) {
|
|
671
|
+
found.push(param.name);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const inList = new RegExp(`${qualified}\\s+IN\\s*\\(([^)]*)\\)`, 'gi');
|
|
675
|
+
for (const match of sql.matchAll(inList)) {
|
|
676
|
+
if (!requiredAt(match.index))
|
|
677
|
+
continue;
|
|
678
|
+
for (const paramMatch of (match[1] ?? '').matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
679
|
+
const param = byParam.get(paramMatch[1]);
|
|
680
|
+
if (param !== undefined &&
|
|
681
|
+
param.optional !== true &&
|
|
682
|
+
param.flag !== true) {
|
|
683
|
+
found.push(param.name);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const unique = [...new Set(found)];
|
|
688
|
+
if (unique.length > 0) {
|
|
689
|
+
scopes.push({
|
|
690
|
+
table: tableName,
|
|
691
|
+
variable: scope.variable,
|
|
692
|
+
pattern: scope.pattern,
|
|
693
|
+
params: unique,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (table.scopes.length > 0 && scopes.length === table.scopes.length) {
|
|
698
|
+
const variable = scopes[0];
|
|
699
|
+
coverage.push({
|
|
700
|
+
table: tableName,
|
|
701
|
+
variable: variable.variable,
|
|
702
|
+
units: variable.params,
|
|
703
|
+
fixedScopes: scopes.slice(1).map((scope) => ({
|
|
704
|
+
variable: scope.variable,
|
|
705
|
+
params: scope.params,
|
|
706
|
+
})),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
dependencies.push({ table: tableName, scopes });
|
|
711
|
+
}
|
|
712
|
+
let rowKey;
|
|
713
|
+
const simpleSingleTable = refs.length === 1 &&
|
|
714
|
+
!/\b(?:DISTINCT|GROUP\s+BY|UNION|EXCEPT|INTERSECT)\b/i.test(stripCommentsAndStrings(sql));
|
|
715
|
+
if (simpleSingleTable) {
|
|
716
|
+
const table = ir.tables.find((candidate) => candidate.name === refs[0]?.table);
|
|
717
|
+
const primary = columns.find((column) => table !== undefined &&
|
|
718
|
+
column.origin?.table === table.name &&
|
|
719
|
+
column.origin?.column === table.primaryKey);
|
|
720
|
+
if (primary !== undefined)
|
|
721
|
+
rowKey = [primary.langName];
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
dependencies,
|
|
725
|
+
coverage,
|
|
726
|
+
...(rowKey !== undefined ? { rowKey } : {}),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
601
729
|
/** Synthesize the schema DDL from the IR (the reverse of the migration
|
|
602
730
|
* parser): a `CREATE TABLE` per IR table with each column's declared SQLite
|
|
603
731
|
* type + NOT NULL, so `prepare()` validates references and decltype resolves.
|
|
@@ -715,6 +843,7 @@ export function analyzeStatement(name, location, statementText, ir, db, naming =
|
|
|
715
843
|
type: source.column.type,
|
|
716
844
|
nullable: source.column.nullable,
|
|
717
845
|
fidelity: 'exact',
|
|
846
|
+
origin: { table: source.table, column: source.column.name },
|
|
718
847
|
};
|
|
719
848
|
}
|
|
720
849
|
// Fallback: decltype affinity if present, else the expr-shape fallback.
|
|
@@ -758,6 +887,7 @@ export function analyzeStatement(name, location, statementText, ir, db, naming =
|
|
|
758
887
|
throw new TypegenError(file, `-- param :${cp.name} names a parameter the query does not use`);
|
|
759
888
|
}
|
|
760
889
|
}
|
|
890
|
+
const reactive = inferReactiveMetadata(sql, refs, ir, params, columns);
|
|
761
891
|
return {
|
|
762
892
|
name,
|
|
763
893
|
file,
|
|
@@ -767,6 +897,7 @@ export function analyzeStatement(name, location, statementText, ir, db, naming =
|
|
|
767
897
|
params,
|
|
768
898
|
columns,
|
|
769
899
|
tables,
|
|
900
|
+
reactive,
|
|
770
901
|
};
|
|
771
902
|
}
|
|
772
903
|
/**
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** Stable, span-free semantic AST used by revision-1 conformance fixtures. */
|
|
2
|
+
import { type SyqlToken } from './syql-lexer.js';
|
|
3
|
+
import type { SyqlSyntaxFile, SyqlTemplate, SyqlValueType } from './syql-parser.js';
|
|
4
|
+
export interface SyqlSemanticToken {
|
|
5
|
+
readonly kind: SyqlToken['kind'];
|
|
6
|
+
readonly text: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SyqlSemanticType {
|
|
9
|
+
readonly base: SyqlValueType['base'];
|
|
10
|
+
readonly nullable: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type SyqlSemanticTemplateNode = {
|
|
13
|
+
readonly kind: 'sql';
|
|
14
|
+
readonly tokens: readonly SyqlSemanticToken[];
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'predicate-call';
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly arguments: readonly string[];
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'when';
|
|
21
|
+
readonly controls: readonly string[];
|
|
22
|
+
readonly body: readonly SyqlSemanticTemplateNode[];
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: 'scope' | 'cover';
|
|
25
|
+
readonly bindings: readonly {
|
|
26
|
+
readonly qualifier: string;
|
|
27
|
+
readonly column: string;
|
|
28
|
+
readonly operator: 'equal' | 'in';
|
|
29
|
+
readonly values: readonly string[];
|
|
30
|
+
}[];
|
|
31
|
+
};
|
|
32
|
+
export interface SyqlSemanticTemplate {
|
|
33
|
+
readonly mode: SyqlTemplate['tree']['mode'];
|
|
34
|
+
readonly nodes: readonly SyqlSemanticTemplateNode[];
|
|
35
|
+
}
|
|
36
|
+
export type SyqlSemanticQueryParameter = {
|
|
37
|
+
readonly kind: 'value';
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly optional: boolean;
|
|
40
|
+
readonly type?: SyqlSemanticType;
|
|
41
|
+
} | {
|
|
42
|
+
readonly kind: 'switch';
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly optional: true;
|
|
45
|
+
} | {
|
|
46
|
+
readonly kind: 'group';
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly optional: true;
|
|
49
|
+
readonly members: readonly {
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly type?: SyqlSemanticType;
|
|
52
|
+
}[];
|
|
53
|
+
};
|
|
54
|
+
export type SyqlSemanticDeclaration = {
|
|
55
|
+
readonly kind: 'predicate';
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly parameters: readonly {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly type?: SyqlSemanticType;
|
|
60
|
+
}[];
|
|
61
|
+
readonly body: SyqlSemanticTemplate;
|
|
62
|
+
} | {
|
|
63
|
+
readonly kind: 'query';
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly parameters: readonly SyqlSemanticQueryParameter[];
|
|
66
|
+
readonly sql: SyqlSemanticTemplate;
|
|
67
|
+
readonly sort?: {
|
|
68
|
+
readonly control: string;
|
|
69
|
+
readonly defaultProfile: string;
|
|
70
|
+
readonly profiles: readonly {
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly order: SyqlSemanticTemplate;
|
|
73
|
+
}[];
|
|
74
|
+
};
|
|
75
|
+
readonly page?: {
|
|
76
|
+
readonly control: string;
|
|
77
|
+
readonly defaultSize: number;
|
|
78
|
+
readonly maxSize: number;
|
|
79
|
+
};
|
|
80
|
+
readonly identity?: readonly string[];
|
|
81
|
+
};
|
|
82
|
+
export interface SyqlSemanticFile {
|
|
83
|
+
readonly kind: 'syql-file';
|
|
84
|
+
readonly revision: 1;
|
|
85
|
+
readonly imports: readonly {
|
|
86
|
+
readonly path: string;
|
|
87
|
+
readonly items: readonly {
|
|
88
|
+
readonly imported: string;
|
|
89
|
+
readonly local: string;
|
|
90
|
+
}[];
|
|
91
|
+
}[];
|
|
92
|
+
readonly declarations: readonly SyqlSemanticDeclaration[];
|
|
93
|
+
}
|
|
94
|
+
/** Return the stable syntax/semantic AST pinned by `spec/syql` fixtures. */
|
|
95
|
+
export declare function toSyqlSemanticAst(file: SyqlSyntaxFile): SyqlSemanticFile;
|
package/dist/syql-ast.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** Stable, span-free semantic AST used by revision-1 conformance fixtures. */
|
|
2
|
+
import { isSyqlTrivia } from './syql-lexer.js';
|
|
3
|
+
function semanticType(type) {
|
|
4
|
+
return type === undefined
|
|
5
|
+
? undefined
|
|
6
|
+
: { base: type.base, nullable: type.nullable };
|
|
7
|
+
}
|
|
8
|
+
function semanticMember(member) {
|
|
9
|
+
const type = semanticType(member.type);
|
|
10
|
+
return { name: member.name, ...(type === undefined ? {} : { type }) };
|
|
11
|
+
}
|
|
12
|
+
function semanticParameter(parameter) {
|
|
13
|
+
if (parameter.kind === 'switch') {
|
|
14
|
+
return { kind: 'switch', name: parameter.name, optional: true };
|
|
15
|
+
}
|
|
16
|
+
if (parameter.kind === 'group') {
|
|
17
|
+
return {
|
|
18
|
+
kind: 'group',
|
|
19
|
+
name: parameter.name,
|
|
20
|
+
optional: true,
|
|
21
|
+
members: parameter.members.map(semanticMember),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const type = semanticType(parameter.type);
|
|
25
|
+
return {
|
|
26
|
+
kind: 'value',
|
|
27
|
+
name: parameter.name,
|
|
28
|
+
optional: parameter.optional,
|
|
29
|
+
...(type === undefined ? {} : { type }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function semanticRawTokens(tokens) {
|
|
33
|
+
return tokens
|
|
34
|
+
.filter((token) => !isSyqlTrivia(token))
|
|
35
|
+
.map((token) => ({ kind: token.kind, text: token.text }));
|
|
36
|
+
}
|
|
37
|
+
function semanticNode(node) {
|
|
38
|
+
if (node.kind === 'raw') {
|
|
39
|
+
const tokens = semanticRawTokens(node.tokens);
|
|
40
|
+
return tokens.length === 0 ? undefined : { kind: 'sql', tokens };
|
|
41
|
+
}
|
|
42
|
+
if (node.kind === 'predicate-call') {
|
|
43
|
+
return {
|
|
44
|
+
kind: 'predicate-call',
|
|
45
|
+
name: node.name,
|
|
46
|
+
arguments: node.arguments.map((argument) => argument.name),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (node.kind === 'when') {
|
|
50
|
+
return {
|
|
51
|
+
kind: 'when',
|
|
52
|
+
controls: node.controls,
|
|
53
|
+
body: semanticNodes(node.body.nodes),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
kind: node.kind,
|
|
58
|
+
bindings: node.bindings.map((binding) => ({
|
|
59
|
+
qualifier: binding.column.qualifier,
|
|
60
|
+
column: binding.column.name,
|
|
61
|
+
operator: binding.operator,
|
|
62
|
+
values: binding.values.map((value) => value.name),
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function semanticNodes(nodes) {
|
|
67
|
+
return nodes.flatMap((node) => {
|
|
68
|
+
const semantic = semanticNode(node);
|
|
69
|
+
return semantic === undefined ? [] : [semantic];
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function semanticTemplate(template) {
|
|
73
|
+
return {
|
|
74
|
+
mode: template.tree.mode,
|
|
75
|
+
nodes: semanticNodes(template.tree.nodes),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function semanticDeclaration(declaration) {
|
|
79
|
+
if (declaration.kind === 'predicate') {
|
|
80
|
+
return {
|
|
81
|
+
kind: 'predicate',
|
|
82
|
+
name: declaration.name,
|
|
83
|
+
parameters: declaration.parameters.map((parameter) => {
|
|
84
|
+
const type = semanticType(parameter.type);
|
|
85
|
+
return {
|
|
86
|
+
name: parameter.name,
|
|
87
|
+
...(type === undefined ? {} : { type }),
|
|
88
|
+
};
|
|
89
|
+
}),
|
|
90
|
+
body: semanticTemplate(declaration.body),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
kind: 'query',
|
|
95
|
+
name: declaration.name,
|
|
96
|
+
parameters: declaration.parameters.map(semanticParameter),
|
|
97
|
+
sql: semanticTemplate(declaration.sql.body),
|
|
98
|
+
...(declaration.sort === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: {
|
|
101
|
+
sort: {
|
|
102
|
+
control: declaration.sort.control,
|
|
103
|
+
defaultProfile: declaration.sort.defaultProfile,
|
|
104
|
+
profiles: declaration.sort.profiles.map((profile) => ({
|
|
105
|
+
name: profile.name,
|
|
106
|
+
order: semanticTemplate(profile.order),
|
|
107
|
+
})),
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
...(declaration.page === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: {
|
|
113
|
+
page: {
|
|
114
|
+
control: declaration.page.control,
|
|
115
|
+
defaultSize: declaration.page.defaultSize,
|
|
116
|
+
maxSize: declaration.page.maxSize,
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
...(declaration.identity === undefined
|
|
120
|
+
? {}
|
|
121
|
+
: { identity: declaration.identity.fields }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Return the stable syntax/semantic AST pinned by `spec/syql` fixtures. */
|
|
125
|
+
export function toSyqlSemanticAst(file) {
|
|
126
|
+
return {
|
|
127
|
+
kind: 'syql-file',
|
|
128
|
+
revision: 1,
|
|
129
|
+
imports: file.imports.map((declaration) => ({
|
|
130
|
+
path: declaration.path,
|
|
131
|
+
items: declaration.items.map((item) => ({
|
|
132
|
+
imported: item.imported,
|
|
133
|
+
local: item.local,
|
|
134
|
+
})),
|
|
135
|
+
})),
|
|
136
|
+
declarations: file.declarations.map(semanticDeclaration),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lossless lexer for the revision-1 SYQL frontend (`docs/SYQL.md` §2).
|
|
3
|
+
*
|
|
4
|
+
* The lexer owns the SQLite atomic-token boundary used by parsing, formatting,
|
|
5
|
+
* bind discovery, predicate expansion, and editor tooling. It never rewrites
|
|
6
|
+
* source text: concatenating every non-EOF token reproduces the input exactly.
|
|
7
|
+
*/
|
|
8
|
+
import { TypegenError } from './errors.js';
|
|
9
|
+
export interface SyqlSourcePosition {
|
|
10
|
+
/** UTF-16 source offset, suitable for slicing the JavaScript source string. */
|
|
11
|
+
readonly offset: number;
|
|
12
|
+
/** One-based source line. */
|
|
13
|
+
readonly line: number;
|
|
14
|
+
/** One-based Unicode-scalar column. */
|
|
15
|
+
readonly column: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SyqlSourceSpan {
|
|
18
|
+
readonly file: string;
|
|
19
|
+
readonly start: SyqlSourcePosition;
|
|
20
|
+
/** Exclusive end position. */
|
|
21
|
+
readonly end: SyqlSourcePosition;
|
|
22
|
+
}
|
|
23
|
+
export type SyqlTokenKind = 'whitespace' | 'line-comment' | 'block-comment' | 'identifier' | 'number' | 'string' | 'quoted-identifier' | 'import-path' | 'blob' | 'bind' | 'at-identifier' | 'punctuation' | 'operator' | 'eof';
|
|
24
|
+
export interface SyqlToken {
|
|
25
|
+
readonly kind: SyqlTokenKind;
|
|
26
|
+
/** Exact source spelling. */
|
|
27
|
+
readonly text: string;
|
|
28
|
+
readonly span: SyqlSourceSpan;
|
|
29
|
+
}
|
|
30
|
+
export type SyqlLexErrorCode = 'SYQL1001_UNTERMINATED_STRING' | 'SYQL1002_UNTERMINATED_IDENTIFIER' | 'SYQL1003_UNTERMINATED_COMMENT' | 'SYQL1004_UNTERMINATED_IMPORT_PATH';
|
|
31
|
+
/** A source-spanned, stable-code error shared by the lexer and parser. */
|
|
32
|
+
export declare class SyqlFrontendError extends TypegenError {
|
|
33
|
+
readonly code: string;
|
|
34
|
+
readonly span: SyqlSourceSpan;
|
|
35
|
+
readonly sourceFile: string;
|
|
36
|
+
constructor(code: string, span: SyqlSourceSpan, message: string);
|
|
37
|
+
}
|
|
38
|
+
export declare function isSyqlTrivia(token: SyqlToken): boolean;
|
|
39
|
+
/** Lex one complete `.syql` source file, including trivia and an EOF token. */
|
|
40
|
+
export declare function lexSyqlSource(file: string, source: string): readonly SyqlToken[];
|