@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/src/query.ts
CHANGED
|
@@ -135,6 +135,35 @@ export interface QueryColumn {
|
|
|
135
135
|
/** `exact` = resolved to an IR column (plain ref); `fallback` = a computed
|
|
136
136
|
* expression typed by the documented honest fallback. */
|
|
137
137
|
readonly fidelity: 'exact' | 'fallback';
|
|
138
|
+
/** Proven physical origin for reactive row identity and diagnostics. */
|
|
139
|
+
readonly origin?: { readonly table: string; readonly column: string };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface QueryScopeBinding {
|
|
143
|
+
readonly table: string;
|
|
144
|
+
readonly variable: string;
|
|
145
|
+
readonly pattern: string;
|
|
146
|
+
readonly params: readonly string[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface QueryCoverageBinding {
|
|
150
|
+
readonly table: string;
|
|
151
|
+
readonly variable: string;
|
|
152
|
+
readonly units: readonly string[];
|
|
153
|
+
readonly fixedScopes: readonly {
|
|
154
|
+
readonly variable: string;
|
|
155
|
+
readonly params: readonly string[];
|
|
156
|
+
}[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface QueryReactiveMetadata {
|
|
160
|
+
readonly dependencies: readonly {
|
|
161
|
+
readonly table: string;
|
|
162
|
+
readonly scopes: readonly QueryScopeBinding[];
|
|
163
|
+
}[];
|
|
164
|
+
readonly coverage: readonly QueryCoverageBinding[];
|
|
165
|
+
/** Language-facing projection fields forming a proven unique row key. */
|
|
166
|
+
readonly rowKey?: readonly string[];
|
|
138
167
|
}
|
|
139
168
|
|
|
140
169
|
/** §7/§8 backend selection: neutralization (default), always-variants, or
|
|
@@ -174,6 +203,7 @@ export interface AnalyzedQuery {
|
|
|
174
203
|
readonly columns: readonly QueryColumn[];
|
|
175
204
|
/** IR tables this query reads (the useRawSql `{tables}` set), sorted. */
|
|
176
205
|
readonly tables: readonly string[];
|
|
206
|
+
readonly reactive: QueryReactiveMetadata;
|
|
177
207
|
/** §6 orderBy knob (`.syql` tier). When present, `sql`/`positionalSql`
|
|
178
208
|
* carry the DEFAULT order-by tail and `positionalSqlBase` is the static
|
|
179
209
|
* prefix emitters compose the selected column onto. */
|
|
@@ -480,15 +510,29 @@ function stripCommentsAndStrings(sql: string): string {
|
|
|
480
510
|
const next = sql[i + 1];
|
|
481
511
|
if (ch === '-' && next === '-') {
|
|
482
512
|
const end = sql.indexOf('\n', i);
|
|
483
|
-
|
|
513
|
+
const stop = end === -1 ? sql.length : end;
|
|
514
|
+
out += ' '.repeat(stop - i);
|
|
515
|
+
i = stop;
|
|
484
516
|
} else if (ch === '/' && next === '*') {
|
|
485
517
|
const end = sql.indexOf('*/', i + 2);
|
|
486
|
-
|
|
487
|
-
out += ' ';
|
|
518
|
+
const stop = end === -1 ? sql.length : end + 2;
|
|
519
|
+
out += sql.slice(i, stop).replace(/[^\n]/g, ' ');
|
|
520
|
+
i = stop;
|
|
488
521
|
} else if (ch === "'") {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
522
|
+
let stop = i + 1;
|
|
523
|
+
while (stop < sql.length) {
|
|
524
|
+
if (sql[stop] === "'" && sql[stop + 1] === "'") {
|
|
525
|
+
stop += 2;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (sql[stop] === "'") {
|
|
529
|
+
stop += 1;
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
stop += 1;
|
|
533
|
+
}
|
|
534
|
+
out += sql.slice(i, stop).replace(/[^\n]/g, ' ');
|
|
535
|
+
i = stop;
|
|
492
536
|
} else {
|
|
493
537
|
out += ch;
|
|
494
538
|
i += 1;
|
|
@@ -691,6 +735,7 @@ function splitSelectList(sql: string): SelectItem[] | null {
|
|
|
691
735
|
const PLAIN_REF_RE = new RegExp(`^(?:(${IDENT})\\.)?(${IDENT})$`);
|
|
692
736
|
|
|
693
737
|
interface ResolvedSource {
|
|
738
|
+
readonly table: string;
|
|
694
739
|
readonly column: IrTable['columns'][number];
|
|
695
740
|
}
|
|
696
741
|
|
|
@@ -710,14 +755,14 @@ function resolveSource(
|
|
|
710
755
|
if (ref === undefined) return null;
|
|
711
756
|
const table = byName.get(ref.table);
|
|
712
757
|
const col = table?.columns.find((c) => c.name === columnName);
|
|
713
|
-
return col === undefined ? null : { column: col };
|
|
758
|
+
return col === undefined ? null : { table: ref.table, column: col };
|
|
714
759
|
}
|
|
715
760
|
// Unqualified: search every FROM/JOIN table (SQLite already resolved
|
|
716
761
|
// ambiguity; a single-table query is the common case).
|
|
717
762
|
for (const ref of refs) {
|
|
718
763
|
const table = byName.get(ref.table);
|
|
719
764
|
const col = table?.columns.find((c) => c.name === columnName);
|
|
720
|
-
if (col !== undefined) return { column: col };
|
|
765
|
+
if (col !== undefined) return { table: ref.table, column: col };
|
|
721
766
|
}
|
|
722
767
|
return null;
|
|
723
768
|
}
|
|
@@ -821,6 +866,156 @@ function fallbackColumnType(expr: string): IrColumnType {
|
|
|
821
866
|
return 'string';
|
|
822
867
|
}
|
|
823
868
|
|
|
869
|
+
function inferReactiveMetadata(
|
|
870
|
+
sql: string,
|
|
871
|
+
refs: readonly TableRef[],
|
|
872
|
+
ir: IrDocument,
|
|
873
|
+
params: readonly QueryParam[],
|
|
874
|
+
columns: readonly QueryColumn[],
|
|
875
|
+
): QueryReactiveMetadata {
|
|
876
|
+
const conservative = /\b(?:UNION|EXCEPT|INTERSECT)\b/i.test(
|
|
877
|
+
stripCommentsAndStrings(sql),
|
|
878
|
+
);
|
|
879
|
+
const requiredAt = (index: number): boolean => {
|
|
880
|
+
const cleaned = stripCommentsAndStrings(sql);
|
|
881
|
+
const where = /\bWHERE\b/i.exec(cleaned);
|
|
882
|
+
if (where === null || index < where.index + where[0].length) return false;
|
|
883
|
+
const matchStack: number[] = [];
|
|
884
|
+
for (
|
|
885
|
+
let cursor = where.index + where[0].length;
|
|
886
|
+
cursor < index;
|
|
887
|
+
cursor += 1
|
|
888
|
+
) {
|
|
889
|
+
if (cleaned[cursor] === '(') matchStack.push(cursor);
|
|
890
|
+
else if (cleaned[cursor] === ')') matchStack.pop();
|
|
891
|
+
}
|
|
892
|
+
const stack: number[] = [];
|
|
893
|
+
for (
|
|
894
|
+
let cursor = where.index + where[0].length;
|
|
895
|
+
cursor < cleaned.length;
|
|
896
|
+
cursor += 1
|
|
897
|
+
) {
|
|
898
|
+
const char = cleaned[cursor];
|
|
899
|
+
if (char === '(') stack.push(cursor);
|
|
900
|
+
else if (char === ')') stack.pop();
|
|
901
|
+
if (
|
|
902
|
+
cursor > index &&
|
|
903
|
+
/^(?:GROUP\s+BY|ORDER\s+BY|LIMIT|HAVING)\b/i.test(cleaned.slice(cursor))
|
|
904
|
+
) {
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
if (
|
|
908
|
+
/^OR\b/i.test(cleaned.slice(cursor)) &&
|
|
909
|
+
stack.every((open, stackIndex) => matchStack[stackIndex] === open)
|
|
910
|
+
) {
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return true;
|
|
915
|
+
};
|
|
916
|
+
const byParam = new Map(params.map((param) => [param.name, param] as const));
|
|
917
|
+
const dependencies: Array<{
|
|
918
|
+
table: string;
|
|
919
|
+
scopes: QueryScopeBinding[];
|
|
920
|
+
}> = [];
|
|
921
|
+
const coverage: QueryCoverageBinding[] = [];
|
|
922
|
+
|
|
923
|
+
for (const tableName of [...new Set(refs.map((ref) => ref.table))].sort()) {
|
|
924
|
+
const table = ir.tables.find((candidate) => candidate.name === tableName);
|
|
925
|
+
const tableRefs = refs.filter((ref) => ref.table === tableName);
|
|
926
|
+
const scopes: QueryScopeBinding[] = [];
|
|
927
|
+
if (table !== undefined && tableRefs.length === 1 && !conservative) {
|
|
928
|
+
const qualifier = tableRefs[0]?.alias;
|
|
929
|
+
for (const scope of table.scopes) {
|
|
930
|
+
const column = scope.column.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
931
|
+
const qualified =
|
|
932
|
+
qualifier === undefined
|
|
933
|
+
? `(?:${IDENT}\\.)?${column}`
|
|
934
|
+
: `(?:${qualifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.)?${column}`;
|
|
935
|
+
const found: string[] = [];
|
|
936
|
+
const equality = new RegExp(
|
|
937
|
+
`${qualified}\\s*(?:=|==|\\bIS\\b)\\s*:(${IDENT})\\b|:(${IDENT})\\b\\s*(?:=|==)\\s*${qualified}`,
|
|
938
|
+
'gi',
|
|
939
|
+
);
|
|
940
|
+
for (const match of sql.matchAll(equality)) {
|
|
941
|
+
if (!requiredAt(match.index)) continue;
|
|
942
|
+
const name = match[1] ?? match[2];
|
|
943
|
+
const param = name === undefined ? undefined : byParam.get(name);
|
|
944
|
+
if (
|
|
945
|
+
param !== undefined &&
|
|
946
|
+
param.optional !== true &&
|
|
947
|
+
param.flag !== true
|
|
948
|
+
) {
|
|
949
|
+
found.push(param.name);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const inList = new RegExp(`${qualified}\\s+IN\\s*\\(([^)]*)\\)`, 'gi');
|
|
953
|
+
for (const match of sql.matchAll(inList)) {
|
|
954
|
+
if (!requiredAt(match.index)) continue;
|
|
955
|
+
for (const paramMatch of (match[1] ?? '').matchAll(
|
|
956
|
+
/:([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
957
|
+
)) {
|
|
958
|
+
const param = byParam.get(paramMatch[1] as string);
|
|
959
|
+
if (
|
|
960
|
+
param !== undefined &&
|
|
961
|
+
param.optional !== true &&
|
|
962
|
+
param.flag !== true
|
|
963
|
+
) {
|
|
964
|
+
found.push(param.name);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const unique = [...new Set(found)];
|
|
969
|
+
if (unique.length > 0) {
|
|
970
|
+
scopes.push({
|
|
971
|
+
table: tableName,
|
|
972
|
+
variable: scope.variable,
|
|
973
|
+
pattern: scope.pattern,
|
|
974
|
+
params: unique,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (table.scopes.length > 0 && scopes.length === table.scopes.length) {
|
|
979
|
+
const variable = scopes[0] as QueryScopeBinding;
|
|
980
|
+
coverage.push({
|
|
981
|
+
table: tableName,
|
|
982
|
+
variable: variable.variable,
|
|
983
|
+
units: variable.params,
|
|
984
|
+
fixedScopes: scopes.slice(1).map((scope) => ({
|
|
985
|
+
variable: scope.variable,
|
|
986
|
+
params: scope.params,
|
|
987
|
+
})),
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
dependencies.push({ table: tableName, scopes });
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
let rowKey: readonly string[] | undefined;
|
|
995
|
+
const simpleSingleTable =
|
|
996
|
+
refs.length === 1 &&
|
|
997
|
+
!/\b(?:DISTINCT|GROUP\s+BY|UNION|EXCEPT|INTERSECT)\b/i.test(
|
|
998
|
+
stripCommentsAndStrings(sql),
|
|
999
|
+
);
|
|
1000
|
+
if (simpleSingleTable) {
|
|
1001
|
+
const table = ir.tables.find(
|
|
1002
|
+
(candidate) => candidate.name === refs[0]?.table,
|
|
1003
|
+
);
|
|
1004
|
+
const primary = columns.find(
|
|
1005
|
+
(column) =>
|
|
1006
|
+
table !== undefined &&
|
|
1007
|
+
column.origin?.table === table.name &&
|
|
1008
|
+
column.origin?.column === table.primaryKey,
|
|
1009
|
+
);
|
|
1010
|
+
if (primary !== undefined) rowKey = [primary.langName];
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
dependencies,
|
|
1014
|
+
coverage,
|
|
1015
|
+
...(rowKey !== undefined ? { rowKey } : {}),
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
824
1019
|
// -- public API ---------------------------------------------------------------
|
|
825
1020
|
|
|
826
1021
|
export interface QueryDb {
|
|
@@ -989,6 +1184,7 @@ export function analyzeStatement(
|
|
|
989
1184
|
type: source.column.type,
|
|
990
1185
|
nullable: source.column.nullable,
|
|
991
1186
|
fidelity: 'exact',
|
|
1187
|
+
origin: { table: source.table, column: source.column.name },
|
|
992
1188
|
};
|
|
993
1189
|
}
|
|
994
1190
|
// Fallback: decltype affinity if present, else the expr-shape fallback.
|
|
@@ -1051,6 +1247,8 @@ export function analyzeStatement(
|
|
|
1051
1247
|
}
|
|
1052
1248
|
}
|
|
1053
1249
|
|
|
1250
|
+
const reactive = inferReactiveMetadata(sql, refs, ir, params, columns);
|
|
1251
|
+
|
|
1054
1252
|
return {
|
|
1055
1253
|
name,
|
|
1056
1254
|
file,
|
|
@@ -1060,6 +1258,7 @@ export function analyzeStatement(
|
|
|
1060
1258
|
params,
|
|
1061
1259
|
columns,
|
|
1062
1260
|
tables,
|
|
1261
|
+
reactive,
|
|
1063
1262
|
};
|
|
1064
1263
|
}
|
|
1065
1264
|
|
package/src/syql-ast.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/** Stable, span-free semantic AST used by revision-1 conformance fixtures. */
|
|
2
|
+
import { isSyqlTrivia, type SyqlToken } from './syql-lexer';
|
|
3
|
+
import type {
|
|
4
|
+
SyqlDeclaration,
|
|
5
|
+
SyqlGroupMember,
|
|
6
|
+
SyqlQueryParameter,
|
|
7
|
+
SyqlSyntaxFile,
|
|
8
|
+
SyqlTemplate,
|
|
9
|
+
SyqlValueType,
|
|
10
|
+
} from './syql-parser';
|
|
11
|
+
import type { SyqlEmbeddedNode } from './syql-template-parser';
|
|
12
|
+
|
|
13
|
+
export interface SyqlSemanticToken {
|
|
14
|
+
readonly kind: SyqlToken['kind'];
|
|
15
|
+
readonly text: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SyqlSemanticType {
|
|
19
|
+
readonly base: SyqlValueType['base'];
|
|
20
|
+
readonly nullable: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SyqlSemanticTemplateNode =
|
|
24
|
+
| {
|
|
25
|
+
readonly kind: 'sql';
|
|
26
|
+
readonly tokens: readonly SyqlSemanticToken[];
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
readonly kind: 'predicate-call';
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly arguments: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
readonly kind: 'when';
|
|
35
|
+
readonly controls: readonly string[];
|
|
36
|
+
readonly body: readonly SyqlSemanticTemplateNode[];
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
readonly kind: 'scope' | 'cover';
|
|
40
|
+
readonly bindings: readonly {
|
|
41
|
+
readonly qualifier: string;
|
|
42
|
+
readonly column: string;
|
|
43
|
+
readonly operator: 'equal' | 'in';
|
|
44
|
+
readonly values: readonly string[];
|
|
45
|
+
}[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface SyqlSemanticTemplate {
|
|
49
|
+
readonly mode: SyqlTemplate['tree']['mode'];
|
|
50
|
+
readonly nodes: readonly SyqlSemanticTemplateNode[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type SyqlSemanticQueryParameter =
|
|
54
|
+
| {
|
|
55
|
+
readonly kind: 'value';
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly optional: boolean;
|
|
58
|
+
readonly type?: SyqlSemanticType;
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
readonly kind: 'switch';
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly optional: true;
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
readonly kind: 'group';
|
|
67
|
+
readonly name: string;
|
|
68
|
+
readonly optional: true;
|
|
69
|
+
readonly members: readonly {
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly type?: SyqlSemanticType;
|
|
72
|
+
}[];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type SyqlSemanticDeclaration =
|
|
76
|
+
| {
|
|
77
|
+
readonly kind: 'predicate';
|
|
78
|
+
readonly name: string;
|
|
79
|
+
readonly parameters: readonly {
|
|
80
|
+
readonly name: string;
|
|
81
|
+
readonly type?: SyqlSemanticType;
|
|
82
|
+
}[];
|
|
83
|
+
readonly body: SyqlSemanticTemplate;
|
|
84
|
+
}
|
|
85
|
+
| {
|
|
86
|
+
readonly kind: 'query';
|
|
87
|
+
readonly name: string;
|
|
88
|
+
readonly parameters: readonly SyqlSemanticQueryParameter[];
|
|
89
|
+
readonly sql: SyqlSemanticTemplate;
|
|
90
|
+
readonly sort?: {
|
|
91
|
+
readonly control: string;
|
|
92
|
+
readonly defaultProfile: string;
|
|
93
|
+
readonly profiles: readonly {
|
|
94
|
+
readonly name: string;
|
|
95
|
+
readonly order: SyqlSemanticTemplate;
|
|
96
|
+
}[];
|
|
97
|
+
};
|
|
98
|
+
readonly page?: {
|
|
99
|
+
readonly control: string;
|
|
100
|
+
readonly defaultSize: number;
|
|
101
|
+
readonly maxSize: number;
|
|
102
|
+
};
|
|
103
|
+
readonly identity?: readonly string[];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export interface SyqlSemanticFile {
|
|
107
|
+
readonly kind: 'syql-file';
|
|
108
|
+
readonly revision: 1;
|
|
109
|
+
readonly imports: readonly {
|
|
110
|
+
readonly path: string;
|
|
111
|
+
readonly items: readonly {
|
|
112
|
+
readonly imported: string;
|
|
113
|
+
readonly local: string;
|
|
114
|
+
}[];
|
|
115
|
+
}[];
|
|
116
|
+
readonly declarations: readonly SyqlSemanticDeclaration[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function semanticType(
|
|
120
|
+
type: SyqlValueType | undefined,
|
|
121
|
+
): SyqlSemanticType | undefined {
|
|
122
|
+
return type === undefined
|
|
123
|
+
? undefined
|
|
124
|
+
: { base: type.base, nullable: type.nullable };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function semanticMember(member: SyqlGroupMember): {
|
|
128
|
+
readonly name: string;
|
|
129
|
+
readonly type?: SyqlSemanticType;
|
|
130
|
+
} {
|
|
131
|
+
const type = semanticType(member.type);
|
|
132
|
+
return { name: member.name, ...(type === undefined ? {} : { type }) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function semanticParameter(
|
|
136
|
+
parameter: SyqlQueryParameter,
|
|
137
|
+
): SyqlSemanticQueryParameter {
|
|
138
|
+
if (parameter.kind === 'switch') {
|
|
139
|
+
return { kind: 'switch', name: parameter.name, optional: true };
|
|
140
|
+
}
|
|
141
|
+
if (parameter.kind === 'group') {
|
|
142
|
+
return {
|
|
143
|
+
kind: 'group',
|
|
144
|
+
name: parameter.name,
|
|
145
|
+
optional: true,
|
|
146
|
+
members: parameter.members.map(semanticMember),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const type = semanticType(parameter.type);
|
|
150
|
+
return {
|
|
151
|
+
kind: 'value',
|
|
152
|
+
name: parameter.name,
|
|
153
|
+
optional: parameter.optional,
|
|
154
|
+
...(type === undefined ? {} : { type }),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function semanticRawTokens(
|
|
159
|
+
tokens: readonly SyqlToken[],
|
|
160
|
+
): readonly SyqlSemanticToken[] {
|
|
161
|
+
return tokens
|
|
162
|
+
.filter((token) => !isSyqlTrivia(token))
|
|
163
|
+
.map((token) => ({ kind: token.kind, text: token.text }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function semanticNode(
|
|
167
|
+
node: SyqlEmbeddedNode,
|
|
168
|
+
): SyqlSemanticTemplateNode | undefined {
|
|
169
|
+
if (node.kind === 'raw') {
|
|
170
|
+
const tokens = semanticRawTokens(node.tokens);
|
|
171
|
+
return tokens.length === 0 ? undefined : { kind: 'sql', tokens };
|
|
172
|
+
}
|
|
173
|
+
if (node.kind === 'predicate-call') {
|
|
174
|
+
return {
|
|
175
|
+
kind: 'predicate-call',
|
|
176
|
+
name: node.name,
|
|
177
|
+
arguments: node.arguments.map((argument) => argument.name),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (node.kind === 'when') {
|
|
181
|
+
return {
|
|
182
|
+
kind: 'when',
|
|
183
|
+
controls: node.controls,
|
|
184
|
+
body: semanticNodes(node.body.nodes),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
kind: node.kind,
|
|
189
|
+
bindings: node.bindings.map((binding) => ({
|
|
190
|
+
qualifier: binding.column.qualifier,
|
|
191
|
+
column: binding.column.name,
|
|
192
|
+
operator: binding.operator,
|
|
193
|
+
values: binding.values.map((value) => value.name),
|
|
194
|
+
})),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function semanticNodes(
|
|
199
|
+
nodes: readonly SyqlEmbeddedNode[],
|
|
200
|
+
): readonly SyqlSemanticTemplateNode[] {
|
|
201
|
+
return nodes.flatMap((node) => {
|
|
202
|
+
const semantic = semanticNode(node);
|
|
203
|
+
return semantic === undefined ? [] : [semantic];
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function semanticTemplate(template: SyqlTemplate): SyqlSemanticTemplate {
|
|
208
|
+
return {
|
|
209
|
+
mode: template.tree.mode,
|
|
210
|
+
nodes: semanticNodes(template.tree.nodes),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function semanticDeclaration(
|
|
215
|
+
declaration: SyqlDeclaration,
|
|
216
|
+
): SyqlSemanticDeclaration {
|
|
217
|
+
if (declaration.kind === 'predicate') {
|
|
218
|
+
return {
|
|
219
|
+
kind: 'predicate',
|
|
220
|
+
name: declaration.name,
|
|
221
|
+
parameters: declaration.parameters.map((parameter) => {
|
|
222
|
+
const type = semanticType(parameter.type);
|
|
223
|
+
return {
|
|
224
|
+
name: parameter.name,
|
|
225
|
+
...(type === undefined ? {} : { type }),
|
|
226
|
+
};
|
|
227
|
+
}),
|
|
228
|
+
body: semanticTemplate(declaration.body),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
kind: 'query',
|
|
233
|
+
name: declaration.name,
|
|
234
|
+
parameters: declaration.parameters.map(semanticParameter),
|
|
235
|
+
sql: semanticTemplate(declaration.sql.body),
|
|
236
|
+
...(declaration.sort === undefined
|
|
237
|
+
? {}
|
|
238
|
+
: {
|
|
239
|
+
sort: {
|
|
240
|
+
control: declaration.sort.control,
|
|
241
|
+
defaultProfile: declaration.sort.defaultProfile,
|
|
242
|
+
profiles: declaration.sort.profiles.map((profile) => ({
|
|
243
|
+
name: profile.name,
|
|
244
|
+
order: semanticTemplate(profile.order),
|
|
245
|
+
})),
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
...(declaration.page === undefined
|
|
249
|
+
? {}
|
|
250
|
+
: {
|
|
251
|
+
page: {
|
|
252
|
+
control: declaration.page.control,
|
|
253
|
+
defaultSize: declaration.page.defaultSize,
|
|
254
|
+
maxSize: declaration.page.maxSize,
|
|
255
|
+
},
|
|
256
|
+
}),
|
|
257
|
+
...(declaration.identity === undefined
|
|
258
|
+
? {}
|
|
259
|
+
: { identity: declaration.identity.fields }),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Return the stable syntax/semantic AST pinned by `spec/syql` fixtures. */
|
|
264
|
+
export function toSyqlSemanticAst(file: SyqlSyntaxFile): SyqlSemanticFile {
|
|
265
|
+
return {
|
|
266
|
+
kind: 'syql-file',
|
|
267
|
+
revision: 1,
|
|
268
|
+
imports: file.imports.map((declaration) => ({
|
|
269
|
+
path: declaration.path,
|
|
270
|
+
items: declaration.items.map((item) => ({
|
|
271
|
+
imported: item.imported,
|
|
272
|
+
local: item.local,
|
|
273
|
+
})),
|
|
274
|
+
})),
|
|
275
|
+
declarations: file.declarations.map(semanticDeclaration),
|
|
276
|
+
};
|
|
277
|
+
}
|