@syncular/typegen 0.4.1 → 0.5.1
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/syql.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
type QueryDb,
|
|
49
49
|
type QueryNamingOptions,
|
|
50
50
|
type QueryParam,
|
|
51
|
+
type QueryReactiveMetadata,
|
|
51
52
|
toPositionalSql,
|
|
52
53
|
validateOverrideName,
|
|
53
54
|
} from './query';
|
|
@@ -76,6 +77,22 @@ export interface SyqlLimit {
|
|
|
76
77
|
readonly default?: number;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
export interface SyqlDepends {
|
|
81
|
+
readonly table: string;
|
|
82
|
+
readonly variable: string;
|
|
83
|
+
readonly params: readonly string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface SyqlWindow {
|
|
87
|
+
readonly table: string;
|
|
88
|
+
readonly variable: string;
|
|
89
|
+
readonly units: readonly string[];
|
|
90
|
+
readonly fixedScopes: readonly {
|
|
91
|
+
readonly variable: string;
|
|
92
|
+
readonly param: string;
|
|
93
|
+
}[];
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
export interface SyqlFragmentDecl {
|
|
80
97
|
readonly name: string;
|
|
81
98
|
readonly params: readonly SyqlParamDecl[];
|
|
@@ -93,6 +110,11 @@ export interface SyqlQueryDecl {
|
|
|
93
110
|
/** §7 opt-in: lower to 2^N enumerated statements (perfect index use)
|
|
94
111
|
* instead of one neutralized statement. API-invisible. */
|
|
95
112
|
readonly variants?: boolean;
|
|
113
|
+
/** Checked escape hatches for SQL shapes the conservative analyzer cannot
|
|
114
|
+
* prove. They live with the query, never in React application code. */
|
|
115
|
+
readonly depends?: readonly SyqlDepends[];
|
|
116
|
+
readonly windows?: readonly SyqlWindow[];
|
|
117
|
+
readonly keyBy?: readonly string[];
|
|
96
118
|
/** The SQL-shaped body (may contain @fragment refs and if-guards). */
|
|
97
119
|
readonly body: string;
|
|
98
120
|
/** Source offset of the `query` keyword (editor tooling). */
|
|
@@ -296,16 +318,77 @@ function parseSignature(cur: Cursor, owner: string): SyqlParamDecl[] {
|
|
|
296
318
|
function parseKnobs(
|
|
297
319
|
cur: Cursor,
|
|
298
320
|
owner: string,
|
|
299
|
-
): {
|
|
321
|
+
): {
|
|
322
|
+
orderBy?: SyqlOrderBy;
|
|
323
|
+
limit?: SyqlLimit;
|
|
324
|
+
variants?: boolean;
|
|
325
|
+
depends?: readonly SyqlDepends[];
|
|
326
|
+
windows?: readonly SyqlWindow[];
|
|
327
|
+
keyBy?: readonly string[];
|
|
328
|
+
} {
|
|
300
329
|
let orderBy: SyqlOrderBy | undefined;
|
|
301
330
|
let limit: SyqlLimit | undefined;
|
|
302
331
|
let variants: boolean | undefined;
|
|
332
|
+
const depends: SyqlDepends[] = [];
|
|
333
|
+
const windows: SyqlWindow[] = [];
|
|
334
|
+
let keyBy: string[] | undefined;
|
|
335
|
+
const paramList = (label: string): string[] => {
|
|
336
|
+
const out = [cur.word(`${owner}: ${label} param`)];
|
|
337
|
+
while (cur.peekChar() === '|') {
|
|
338
|
+
cur.expect('|');
|
|
339
|
+
out.push(cur.word(`${owner}: ${label} param`));
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
};
|
|
303
343
|
for (;;) {
|
|
304
344
|
const word = cur.peekWord();
|
|
305
345
|
if (word === 'variants') {
|
|
306
346
|
if (variants === true) cur.fail(`${owner}: duplicate variants knob`);
|
|
307
347
|
cur.word('variants');
|
|
308
348
|
variants = true;
|
|
349
|
+
} else if (word === 'depends') {
|
|
350
|
+
cur.word('depends');
|
|
351
|
+
const table = cur.word(`${owner}: dependency table`);
|
|
352
|
+
const on = cur.word(`${owner}: dependency \`on\``);
|
|
353
|
+
if (on !== 'on')
|
|
354
|
+
cur.fail(
|
|
355
|
+
`${owner}: depends syntax is \`depends <table> on <scope> = <param>\``,
|
|
356
|
+
);
|
|
357
|
+
const variable = cur.word(`${owner}: dependency scope variable`);
|
|
358
|
+
cur.expect('=');
|
|
359
|
+
depends.push({ table, variable, params: paramList('dependency') });
|
|
360
|
+
} else if (word === 'window') {
|
|
361
|
+
cur.word('window');
|
|
362
|
+
const table = cur.word(`${owner}: window table`);
|
|
363
|
+
const by = cur.word(`${owner}: window \`by\``);
|
|
364
|
+
if (by !== 'by')
|
|
365
|
+
cur.fail(
|
|
366
|
+
`${owner}: window syntax is \`window <table> by <scope> = <param>\``,
|
|
367
|
+
);
|
|
368
|
+
const variable = cur.word(`${owner}: window scope variable`);
|
|
369
|
+
cur.expect('=');
|
|
370
|
+
const units = paramList('window unit');
|
|
371
|
+
const fixedScopes: Array<{ variable: string; param: string }> = [];
|
|
372
|
+
if (cur.peekWord() === 'fixed') {
|
|
373
|
+
cur.word('fixed');
|
|
374
|
+
for (;;) {
|
|
375
|
+
const fixedVariable = cur.word(`${owner}: fixed scope variable`);
|
|
376
|
+
cur.expect('=');
|
|
377
|
+
const param = cur.word(`${owner}: fixed scope param`);
|
|
378
|
+
fixedScopes.push({ variable: fixedVariable, param });
|
|
379
|
+
if (cur.peekChar() !== ',') break;
|
|
380
|
+
cur.expect(',');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
windows.push({ table, variable, units, fixedScopes });
|
|
384
|
+
} else if (word === 'key') {
|
|
385
|
+
if (keyBy !== undefined)
|
|
386
|
+
cur.fail(`${owner}: duplicate key by declaration`);
|
|
387
|
+
cur.word('key');
|
|
388
|
+
const by = cur.word(`${owner}: key \`by\``);
|
|
389
|
+
if (by !== 'by')
|
|
390
|
+
cur.fail(`${owner}: key syntax is \`key by <result-column>\``);
|
|
391
|
+
keyBy = paramList('key column');
|
|
309
392
|
} else if (word === 'orderBy') {
|
|
310
393
|
if (orderBy !== undefined) cur.fail(`${owner}: duplicate orderBy knob`);
|
|
311
394
|
cur.word('orderBy');
|
|
@@ -375,6 +458,9 @@ function parseKnobs(
|
|
|
375
458
|
...(orderBy !== undefined ? { orderBy } : {}),
|
|
376
459
|
...(limit !== undefined ? { limit } : {}),
|
|
377
460
|
...(variants !== undefined ? { variants } : {}),
|
|
461
|
+
...(depends.length > 0 ? { depends } : {}),
|
|
462
|
+
...(windows.length > 0 ? { windows } : {}),
|
|
463
|
+
...(keyBy !== undefined ? { keyBy } : {}),
|
|
378
464
|
};
|
|
379
465
|
}
|
|
380
466
|
}
|
|
@@ -876,6 +962,216 @@ export function lowerSyqlBody(
|
|
|
876
962
|
// Analysis (parse → lower → the shared SQLite-check pipeline)
|
|
877
963
|
// ---------------------------------------------------------------------------
|
|
878
964
|
|
|
965
|
+
function checkedReactiveMetadata(
|
|
966
|
+
decl: SyqlQueryDecl,
|
|
967
|
+
base: AnalyzedQuery,
|
|
968
|
+
params: readonly QueryParam[],
|
|
969
|
+
ir: IrDocument,
|
|
970
|
+
location: string,
|
|
971
|
+
): QueryReactiveMetadata {
|
|
972
|
+
if (
|
|
973
|
+
decl.depends === undefined &&
|
|
974
|
+
decl.windows === undefined &&
|
|
975
|
+
decl.keyBy === undefined
|
|
976
|
+
) {
|
|
977
|
+
return base.reactive;
|
|
978
|
+
}
|
|
979
|
+
const paramByName = new Map(params.map((param) => [param.name, param]));
|
|
980
|
+
const explicitScopes = new Map<
|
|
981
|
+
string,
|
|
982
|
+
Map<string, { pattern: string; params: Set<string> }>
|
|
983
|
+
>();
|
|
984
|
+
const explicitTables = new Set<string>();
|
|
985
|
+
|
|
986
|
+
const tableAndScope = (tableName: string, variable: string) => {
|
|
987
|
+
if (!base.tables.includes(tableName)) {
|
|
988
|
+
throw new TypegenError(
|
|
989
|
+
location,
|
|
990
|
+
`reactive declaration names table ${JSON.stringify(tableName)}, but the query does not read it`,
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
const table = ir.tables.find((candidate) => candidate.name === tableName);
|
|
994
|
+
if (table === undefined) {
|
|
995
|
+
throw new TypegenError(
|
|
996
|
+
location,
|
|
997
|
+
`unknown reactive table ${JSON.stringify(tableName)}`,
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
const scope = table.scopes.find(
|
|
1001
|
+
(candidate) => candidate.variable === variable,
|
|
1002
|
+
);
|
|
1003
|
+
if (scope === undefined) {
|
|
1004
|
+
throw new TypegenError(
|
|
1005
|
+
location,
|
|
1006
|
+
`table ${tableName} has no scope variable ${JSON.stringify(variable)}`,
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
return { table, scope };
|
|
1010
|
+
};
|
|
1011
|
+
const checkedParam = (
|
|
1012
|
+
tableName: string,
|
|
1013
|
+
variable: string,
|
|
1014
|
+
name: string,
|
|
1015
|
+
): QueryParam => {
|
|
1016
|
+
const param = paramByName.get(name);
|
|
1017
|
+
if (param === undefined) {
|
|
1018
|
+
throw new TypegenError(
|
|
1019
|
+
location,
|
|
1020
|
+
`reactive declaration references undeclared param :${name}`,
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
if (param.optional === true) {
|
|
1024
|
+
throw new TypegenError(
|
|
1025
|
+
location,
|
|
1026
|
+
`reactive scope ${tableName}.${variable} cannot use optional param :${name}; an absent value is not a window unit`,
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
const { table, scope } = tableAndScope(tableName, variable);
|
|
1030
|
+
const column = table.columns.find(
|
|
1031
|
+
(candidate) => candidate.name === scope.column,
|
|
1032
|
+
);
|
|
1033
|
+
if (column === undefined || column.type !== param.type) {
|
|
1034
|
+
throw new TypegenError(
|
|
1035
|
+
location,
|
|
1036
|
+
`reactive param :${name} has type ${param.type}, incompatible with ${tableName}.${scope.column} (${column?.type ?? 'unknown'})`,
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
return param;
|
|
1040
|
+
};
|
|
1041
|
+
const addScope = (
|
|
1042
|
+
table: string,
|
|
1043
|
+
variable: string,
|
|
1044
|
+
names: readonly string[],
|
|
1045
|
+
) => {
|
|
1046
|
+
const { scope } = tableAndScope(table, variable);
|
|
1047
|
+
explicitTables.add(table);
|
|
1048
|
+
let byVariable = explicitScopes.get(table);
|
|
1049
|
+
if (byVariable === undefined) {
|
|
1050
|
+
byVariable = new Map();
|
|
1051
|
+
explicitScopes.set(table, byVariable);
|
|
1052
|
+
}
|
|
1053
|
+
let binding = byVariable.get(variable);
|
|
1054
|
+
if (binding === undefined) {
|
|
1055
|
+
binding = { pattern: scope.pattern, params: new Set() };
|
|
1056
|
+
byVariable.set(variable, binding);
|
|
1057
|
+
}
|
|
1058
|
+
for (const name of names) {
|
|
1059
|
+
checkedParam(table, variable, name);
|
|
1060
|
+
binding.params.add(name);
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
for (const dependency of decl.depends ?? []) {
|
|
1065
|
+
addScope(dependency.table, dependency.variable, dependency.params);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const coverage: Array<QueryReactiveMetadata['coverage'][number]> = [];
|
|
1069
|
+
const seenWindows = new Set<string>();
|
|
1070
|
+
for (const window of decl.windows ?? []) {
|
|
1071
|
+
const key = `${window.table}\0${window.variable}`;
|
|
1072
|
+
if (seenWindows.has(key)) {
|
|
1073
|
+
throw new TypegenError(
|
|
1074
|
+
location,
|
|
1075
|
+
`duplicate window declaration for ${window.table}.${window.variable}`,
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
seenWindows.add(key);
|
|
1079
|
+
const { table } = tableAndScope(window.table, window.variable);
|
|
1080
|
+
addScope(window.table, window.variable, window.units);
|
|
1081
|
+
const fixedByVariable = new Map<string, string>();
|
|
1082
|
+
for (const fixed of window.fixedScopes) {
|
|
1083
|
+
if (fixed.variable === window.variable) {
|
|
1084
|
+
throw new TypegenError(
|
|
1085
|
+
location,
|
|
1086
|
+
`window ${window.table}.${window.variable} repeats its unit scope as fixed`,
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
if (fixedByVariable.has(fixed.variable)) {
|
|
1090
|
+
throw new TypegenError(
|
|
1091
|
+
location,
|
|
1092
|
+
`window ${window.table} repeats fixed scope ${fixed.variable}`,
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
checkedParam(window.table, fixed.variable, fixed.param);
|
|
1096
|
+
fixedByVariable.set(fixed.variable, fixed.param);
|
|
1097
|
+
addScope(window.table, fixed.variable, [fixed.param]);
|
|
1098
|
+
}
|
|
1099
|
+
const missing = table.scopes
|
|
1100
|
+
.map((scope) => scope.variable)
|
|
1101
|
+
.filter(
|
|
1102
|
+
(variable) =>
|
|
1103
|
+
variable !== window.variable && !fixedByVariable.has(variable),
|
|
1104
|
+
);
|
|
1105
|
+
if (missing.length > 0) {
|
|
1106
|
+
throw new TypegenError(
|
|
1107
|
+
location,
|
|
1108
|
+
`window ${window.table}.${window.variable} must fix the remaining scope variables: ${missing.join(', ')}`,
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
coverage.push({
|
|
1112
|
+
table: window.table,
|
|
1113
|
+
variable: window.variable,
|
|
1114
|
+
units: window.units,
|
|
1115
|
+
fixedScopes: table.scopes
|
|
1116
|
+
.filter((scope) => scope.variable !== window.variable)
|
|
1117
|
+
.map((scope) => ({
|
|
1118
|
+
variable: scope.variable,
|
|
1119
|
+
params: [fixedByVariable.get(scope.variable) as string],
|
|
1120
|
+
})),
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const dependencies = base.reactive.dependencies.map((dependency) => {
|
|
1125
|
+
if (!explicitTables.has(dependency.table)) return dependency;
|
|
1126
|
+
const scopes = [...(explicitScopes.get(dependency.table)?.entries() ?? [])]
|
|
1127
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
1128
|
+
.map(([variable, binding]) => ({
|
|
1129
|
+
table: dependency.table,
|
|
1130
|
+
variable,
|
|
1131
|
+
pattern: binding.pattern,
|
|
1132
|
+
params: [...binding.params],
|
|
1133
|
+
}));
|
|
1134
|
+
return { table: dependency.table, scopes };
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
let rowKey = base.reactive.rowKey;
|
|
1138
|
+
if (decl.keyBy !== undefined) {
|
|
1139
|
+
const fields = decl.keyBy.map((name) => {
|
|
1140
|
+
const column = base.columns.find(
|
|
1141
|
+
(candidate) => candidate.name === name || candidate.langName === name,
|
|
1142
|
+
);
|
|
1143
|
+
if (column === undefined) {
|
|
1144
|
+
throw new TypegenError(
|
|
1145
|
+
location,
|
|
1146
|
+
`key by column ${JSON.stringify(name)} is not projected by the query`,
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
return column.langName;
|
|
1150
|
+
});
|
|
1151
|
+
if (new Set(fields).size !== fields.length) {
|
|
1152
|
+
throw new TypegenError(
|
|
1153
|
+
location,
|
|
1154
|
+
'key by contains a duplicate result column',
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
rowKey = fields;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return {
|
|
1161
|
+
dependencies,
|
|
1162
|
+
coverage:
|
|
1163
|
+
decl.windows === undefined
|
|
1164
|
+
? base.reactive.coverage
|
|
1165
|
+
: [
|
|
1166
|
+
...base.reactive.coverage.filter(
|
|
1167
|
+
(item) => !seenWindows.has(`${item.table}\0${item.variable}`),
|
|
1168
|
+
),
|
|
1169
|
+
...coverage,
|
|
1170
|
+
],
|
|
1171
|
+
...(rowKey !== undefined ? { rowKey } : {}),
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
879
1175
|
/** Analyze every query of one `.syql` file into `AnalyzedQuery` units —
|
|
880
1176
|
* byte-compatible with the `.sql` frontend's output (§1: nothing below the
|
|
881
1177
|
* IR knows the frontend). */
|
|
@@ -1001,6 +1297,8 @@ export function analyzeSyqlFile(
|
|
|
1001
1297
|
const mapCol = (name: string): string =>
|
|
1002
1298
|
mode === 'preserve' ? name : snakeToCamel(name);
|
|
1003
1299
|
|
|
1300
|
+
const reactive = checkedReactiveMetadata(decl, base, params, ir, location);
|
|
1301
|
+
|
|
1004
1302
|
const orderBy =
|
|
1005
1303
|
decl.orderBy === undefined
|
|
1006
1304
|
? undefined
|
|
@@ -1158,6 +1456,7 @@ export function analyzeSyqlFile(
|
|
|
1158
1456
|
sourceSql: decl.body.trim(),
|
|
1159
1457
|
sql: exposedSql,
|
|
1160
1458
|
params,
|
|
1459
|
+
reactive,
|
|
1161
1460
|
...(orderBy !== undefined ? { orderBy } : {}),
|
|
1162
1461
|
...(decl.limit !== undefined ? { limit: decl.limit } : {}),
|
|
1163
1462
|
...(positionalSqlBase !== undefined ? { positionalSqlBase } : {}),
|