@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/README.md
CHANGED
|
@@ -72,6 +72,7 @@ schema guide and suggests `syncular init`.
|
|
|
72
72
|
| `queryBackend` | no (default `"neutralize"`) | `.syql` conditional lowering: `"neutralize"` (one guarded statement), `"variants"` (enumerate per provided-combination whenever eligible), `"auto"` (enumerate at ≤ 2 optional groups). The per-query `variants` knob always forces. |
|
|
73
73
|
| `output.ir` | no (default `./syncular.ir.json`) | IR output path, relative to the manifest. |
|
|
74
74
|
| `output.module` | no (default `./syncular.generated.ts`) | Generated TS module path, relative to the manifest. |
|
|
75
|
+
| `output.queryIr` | no | Deterministic analyzed QueryIR JSON. Its hash keys generated reactive query descriptors, so SQL-only changes invalidate caches. |
|
|
75
76
|
| `output.queries` | no | Opt-in TS named-queries output path (see §6). A sibling `.ts` file. |
|
|
76
77
|
| `output.swift` | no | Opt-in Swift emitter (see §5). A path string, or `{ "path", "enumName", "queriesPath" }` (default `enumName` `SyncularSchema`; `queriesPath` opts into §6 Swift named queries). |
|
|
77
78
|
| `output.kotlin` | no | Opt-in Kotlin emitter (see §5). A path string, or `{ "path", "package", "objectName", "queriesPath" }` (defaults `syncular.generated` / `SyncularSchema`). |
|
|
@@ -440,17 +441,45 @@ so the emitted SQL rewrites `:name` → `?` (repeats included) and each runner
|
|
|
440
441
|
reorders the named params object into the positional array. Comments are
|
|
441
442
|
stripped and whitespace collapsed to a clean one-line string.
|
|
442
443
|
|
|
443
|
-
**
|
|
444
|
-
|
|
445
|
-
and
|
|
446
|
-
|
|
447
|
-
|
|
444
|
+
**Reactive metadata.** Each query emits a QueryIR-derived cache id, the
|
|
445
|
+
FROM/JOIN table set, table-associated scope dependencies, provable window
|
|
446
|
+
coverage, and a safe row identity when the primary key is projected by an
|
|
447
|
+
unambiguous single-table query. React uses these facts to share one query per
|
|
448
|
+
revision, route exact change batches, claim window coverage, and retain
|
|
449
|
+
unchanged row objects. The cache id is derived from the query IR rather than
|
|
450
|
+
the schema IR, so a SQL-only change cannot reuse old state. **Boundary**:
|
|
448
451
|
`bun:sqlite` exposes no authorizer and no statement table-list, and EXPLAIN
|
|
449
452
|
opcodes are fragile — so the set is derived by scanning every `FROM`/`JOIN`
|
|
450
453
|
identifier and keeping those that name an IR table. This captures subquery /
|
|
451
454
|
`WHERE EXISTS (…)` tables (they still appear under `FROM`/`JOIN`), and the
|
|
452
455
|
prepare() still guarantees the SQL itself is correct.
|
|
453
456
|
|
|
457
|
+
Inference is deliberately conservative around `OR`, joins, grouping, and
|
|
458
|
+
computed identity. A `.syql` query can state the missing fact explicitly:
|
|
459
|
+
|
|
460
|
+
```syql
|
|
461
|
+
query compareLists(left, right)
|
|
462
|
+
depends tasks on project_id = left | right
|
|
463
|
+
window tasks by project_id = left | right
|
|
464
|
+
key by id
|
|
465
|
+
{
|
|
466
|
+
select id, title from tasks
|
|
467
|
+
where project_id = :left or project_id = :right
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The declaration grammar is:
|
|
472
|
+
|
|
473
|
+
- `depends <table> on <scope> = <param> | <param>`;
|
|
474
|
+
- `window <table> by <scope> = <param> | <param>` with optional
|
|
475
|
+
`fixed <other_scope> = <param>, ...` for every other scope on that table;
|
|
476
|
+
- `key by <result-column> | <result-column>`.
|
|
477
|
+
|
|
478
|
+
These escape hatches are checked against the prepared query, schema, inferred
|
|
479
|
+
param types, and result projection. They reject unread tables, unknown scopes
|
|
480
|
+
or params, optional/incompatible coverage params, incomplete fixed scopes,
|
|
481
|
+
duplicate declarations, and unprojected key fields.
|
|
482
|
+
|
|
454
483
|
**Emitted shape, per language** (one query, five outputs — abbreviated):
|
|
455
484
|
|
|
456
485
|
| | Output |
|
|
@@ -467,11 +496,11 @@ the schema structs. `--check` gates every queries file byte-exactly, like the
|
|
|
467
496
|
schema files; each carries the DO-NOT-EDIT header + IR hash.
|
|
468
497
|
|
|
469
498
|
**React helper.** `@syncular/react` exports `useQuery(query, params?)`,
|
|
470
|
-
which takes the TS `NamedQuery` descriptor and
|
|
471
|
-
|
|
499
|
+
which takes the TS `NamedQuery` descriptor and observes the client-scoped
|
|
500
|
+
revisioned store:
|
|
472
501
|
|
|
473
502
|
```ts
|
|
474
503
|
import { listTodosQuery } from './syncular.queries';
|
|
475
|
-
const
|
|
476
|
-
//
|
|
504
|
+
const todos = useQuery(listTodosQuery, { listId });
|
|
505
|
+
// todos.rows: ListTodosRow[]; todos.phase: loading | partial | ready | error
|
|
477
506
|
```
|
package/dist/emit-queries.js
CHANGED
|
@@ -33,7 +33,22 @@ function bindExpr(query, param, access) {
|
|
|
33
33
|
const key = propertyKey(param.langName);
|
|
34
34
|
return isOptional(param) ? `${access}.${key} ?? null` : `params.${key}`;
|
|
35
35
|
}
|
|
36
|
-
function
|
|
36
|
+
function reactiveParam(query, name) {
|
|
37
|
+
const param = query.params.find((candidate) => candidate.name === name);
|
|
38
|
+
if (param === undefined)
|
|
39
|
+
throw new Error(`unknown reactive param ${name}`);
|
|
40
|
+
return `String(params.${propertyKey(param.langName)})`;
|
|
41
|
+
}
|
|
42
|
+
function scopeKeyExpression(query, pattern, variable, param) {
|
|
43
|
+
const marker = `{${variable}}`;
|
|
44
|
+
const index = pattern.indexOf(marker);
|
|
45
|
+
if (index < 0)
|
|
46
|
+
throw new Error(`scope pattern ${pattern} lacks ${marker}`);
|
|
47
|
+
const prefix = pattern.slice(0, index);
|
|
48
|
+
const suffix = pattern.slice(index + marker.length);
|
|
49
|
+
return `${quote(prefix)} + ${reactiveParam(query, param)} + ${quote(suffix)}`;
|
|
50
|
+
}
|
|
51
|
+
function emitQuery(query, hash) {
|
|
37
52
|
const Row = `${pascalCase(query.name)}Row`;
|
|
38
53
|
const Params = `${pascalCase(query.name)}Params`;
|
|
39
54
|
const lines = [];
|
|
@@ -75,7 +90,7 @@ function emitQuery(query) {
|
|
|
75
90
|
lines.push('');
|
|
76
91
|
}
|
|
77
92
|
// Tables dependency set (for useRawSql {tables} / useQuery).
|
|
78
|
-
lines.push(`/** Tables ${quote(query.name)} reads
|
|
93
|
+
lines.push(`/** Tables ${quote(query.name)} reads (compatibility/export surface). */`);
|
|
79
94
|
lines.push(`export const ${query.name}Tables = [${query.tables.map(quote).join(', ')}] as const;`);
|
|
80
95
|
lines.push('');
|
|
81
96
|
// SQL constants: the static default statement, plus (orderBy knob) the
|
|
@@ -166,9 +181,11 @@ function emitQuery(query) {
|
|
|
166
181
|
// the baked allowlist, or variant-selected), the exact table dependency
|
|
167
182
|
// set, and a `bind(params)` → positional array (always paired with the
|
|
168
183
|
// sqlFor selection). Typed by the query's own Row/Params.
|
|
169
|
-
lines.push(`/**
|
|
184
|
+
lines.push(`/** Revisioned reactive descriptor for \`useQuery(${query.name}Query${hasParams ? ', params' : ''})\`. */`);
|
|
170
185
|
const paramsTypeArg = hasParams ? Params : 'undefined';
|
|
171
186
|
lines.push(`export const ${query.name}Query: NamedQuery<${Row}, ${paramsTypeArg}> = {`);
|
|
187
|
+
lines.push(` id: ${quote(`${hash}/${query.name}`)},`);
|
|
188
|
+
lines.push(` hasParams: ${hasParams},`);
|
|
172
189
|
lines.push(` sql: ${sqlConst},`);
|
|
173
190
|
if (query.variants !== undefined) {
|
|
174
191
|
lines.push(` sqlFor: (params: ${Params}) => ${selectFn}(params).sql,`);
|
|
@@ -177,6 +194,24 @@ function emitQuery(query) {
|
|
|
177
194
|
lines.push(` sqlFor: (params: ${Params}) => ${composeFn}(params),`);
|
|
178
195
|
}
|
|
179
196
|
lines.push(` tables: ${query.name}Tables,`);
|
|
197
|
+
const reactiveUsesParams = query.reactive.dependencies.some((dependency) => dependency.scopes.some((scope) => scope.params.length > 0));
|
|
198
|
+
lines.push(` dependencies: (${reactiveUsesParams ? 'params' : ''}) => [`);
|
|
199
|
+
for (const dependency of query.reactive.dependencies) {
|
|
200
|
+
const keys = dependency.scopes.flatMap((scope) => scope.params.map((param) => scopeKeyExpression(query, scope.pattern, scope.variable, param)));
|
|
201
|
+
lines.push(` { table: ${quote(dependency.table)}${keys.length > 0 ? `, scopeKeys: [${keys.join(', ')}]` : ''} },`);
|
|
202
|
+
}
|
|
203
|
+
lines.push(' ],');
|
|
204
|
+
const coverageUsesParams = query.reactive.coverage.some((coverage) => coverage.units.length > 0 ||
|
|
205
|
+
coverage.fixedScopes.some((scope) => scope.params.length > 0));
|
|
206
|
+
lines.push(` coverage: (${coverageUsesParams ? 'params' : ''}) => [`);
|
|
207
|
+
for (const coverage of query.reactive.coverage) {
|
|
208
|
+
const fixed = coverage.fixedScopes
|
|
209
|
+
.map((scope) => `${propertyKey(scope.variable)}: [${scope.params.map((param) => reactiveParam(query, param)).join(', ')}]`)
|
|
210
|
+
.join(', ');
|
|
211
|
+
const fixedPart = fixed.length > 0 ? `, fixedScopes: { ${fixed} }` : '';
|
|
212
|
+
lines.push(` { base: { table: ${quote(coverage.table)}, variable: ${quote(coverage.variable)}${fixedPart} }, units: [${coverage.units.map((param) => reactiveParam(query, param)).join(', ')}] },`);
|
|
213
|
+
}
|
|
214
|
+
lines.push(' ],');
|
|
180
215
|
if (query.variants !== undefined) {
|
|
181
216
|
lines.push(` bind: (params: ${Params}) => ${selectFn}(params).bind,`);
|
|
182
217
|
}
|
|
@@ -186,6 +221,9 @@ function emitQuery(query) {
|
|
|
186
221
|
else {
|
|
187
222
|
lines.push(' bind: () => [],');
|
|
188
223
|
}
|
|
224
|
+
if (query.reactive.rowKey !== undefined) {
|
|
225
|
+
lines.push(` rowKey: (row) => [${query.reactive.rowKey.map((key) => `row.${propertyKey(key)}`).join(', ')}],`);
|
|
226
|
+
}
|
|
189
227
|
lines.push('};');
|
|
190
228
|
return lines.join('\n');
|
|
191
229
|
}
|
|
@@ -216,23 +254,43 @@ export function emitQueriesModule(queries, hash, irVersion) {
|
|
|
216
254
|
' ): unknown[] | Promise<unknown[]>;',
|
|
217
255
|
'}',
|
|
218
256
|
'',
|
|
219
|
-
'/** A named-query descriptor —
|
|
257
|
+
'/** A named-query descriptor — checked SQL plus revisioned reactive metadata and a',
|
|
220
258
|
' * `bind(params)` → positional args. Consumed by',
|
|
221
259
|
" * `@syncular/react`'s `useQuery`. `Row` is the projection row",
|
|
222
260
|
' * type; `Params` is `undefined` for a param-less query. `sqlFor`',
|
|
223
261
|
' * (present only with an orderBy knob) composes the statement from a',
|
|
224
262
|
' * generate-time-checked column allowlist. */',
|
|
225
263
|
'export interface NamedQuery<Row, Params = undefined> {',
|
|
264
|
+
' readonly id: string;',
|
|
265
|
+
' readonly hasParams: boolean;',
|
|
226
266
|
' readonly sql: string;',
|
|
227
267
|
' readonly tables: readonly string[];',
|
|
228
268
|
' readonly bind: (params: Params) => readonly QueryValue[];',
|
|
229
269
|
' readonly sqlFor?: (params: Params) => string;',
|
|
270
|
+
' readonly dependencies: (params: Params) => readonly QueryDependency[];',
|
|
271
|
+
' readonly coverage: (params: Params) => readonly WindowCoverage[];',
|
|
272
|
+
' readonly rowKey?: (row: Row) => readonly QueryValue[];',
|
|
230
273
|
' /** Phantom — carries the Row type for `useQuery` inference. */',
|
|
231
274
|
' readonly __row?: Row;',
|
|
232
275
|
'}',
|
|
276
|
+
'',
|
|
277
|
+
'export interface QueryDependency {',
|
|
278
|
+
' readonly table: string;',
|
|
279
|
+
' readonly scopeKeys?: readonly string[];',
|
|
280
|
+
'}',
|
|
281
|
+
'',
|
|
282
|
+
'export interface WindowCoverage {',
|
|
283
|
+
' readonly base: {',
|
|
284
|
+
' readonly table: string;',
|
|
285
|
+
' readonly variable: string;',
|
|
286
|
+
' readonly fixedScopes?: Readonly<Record<string, readonly string[]>>;',
|
|
287
|
+
' readonly params?: string;',
|
|
288
|
+
' };',
|
|
289
|
+
' readonly units: readonly string[];',
|
|
290
|
+
'}',
|
|
233
291
|
].join('\n'));
|
|
234
292
|
for (const query of queries) {
|
|
235
|
-
parts.push(emitQuery(query));
|
|
293
|
+
parts.push(emitQuery(query, hash));
|
|
236
294
|
}
|
|
237
295
|
return `${parts.join('\n\n')}\n`;
|
|
238
296
|
}
|
package/dist/emit.js
CHANGED
|
@@ -122,6 +122,22 @@ function emitRowInterfaces(table, naming) {
|
|
|
122
122
|
lines.push('}');
|
|
123
123
|
return lines.join('\n');
|
|
124
124
|
}
|
|
125
|
+
function emitTableDescriptor(table, naming) {
|
|
126
|
+
const type = pascalCase(table.name);
|
|
127
|
+
const primary = table.columns.find((column) => column.name === table.primaryKey);
|
|
128
|
+
if (primary === undefined)
|
|
129
|
+
throw new Error(`table ${table.name} lacks its primary key`);
|
|
130
|
+
const field = naming === 'camel' ? snakeToCamel(table.primaryKey) : table.primaryKey;
|
|
131
|
+
const constant = `${naming === 'camel' ? snakeToCamel(table.name) : table.name}Table`;
|
|
132
|
+
return [
|
|
133
|
+
`/** Typed mutation/resource descriptor for ${quote(table.name)}. */`,
|
|
134
|
+
`export const ${constant}: SyncTable<${type}Row, ${type}Insert, ${type}Update, ${appTsType(primary)}> = {`,
|
|
135
|
+
` name: ${quote(table.name)},`,
|
|
136
|
+
` primaryKey: ${quote(field)},`,
|
|
137
|
+
` physicalPrimaryKey: ${quote(table.primaryKey)},`,
|
|
138
|
+
'};',
|
|
139
|
+
].join('\n');
|
|
140
|
+
}
|
|
125
141
|
function emitSubscription(sub) {
|
|
126
142
|
const params = [];
|
|
127
143
|
for (const scope of sub.scopes) {
|
|
@@ -166,9 +182,25 @@ export function emitModule(ir, hash, naming = 'camel') {
|
|
|
166
182
|
`// irVersion: ${ir.irVersion}`,
|
|
167
183
|
`// irHash: ${hash}`,
|
|
168
184
|
].join('\n'));
|
|
185
|
+
parts.push([
|
|
186
|
+
'/** Structural descriptor consumed by renderer bindings; phantom type',
|
|
187
|
+
' * fields make row/insert/update/id inference available without imports. */',
|
|
188
|
+
'export interface SyncTable<Row, Insert, Update, Id> {',
|
|
189
|
+
' readonly name: string;',
|
|
190
|
+
' /** Language-facing key used in generated row and mutation types. */',
|
|
191
|
+
' readonly primaryKey: keyof Row & string;',
|
|
192
|
+
' /** Physical SQLite/wire primary-key column. */',
|
|
193
|
+
' readonly physicalPrimaryKey: string;',
|
|
194
|
+
' readonly __row?: Row;',
|
|
195
|
+
' readonly __insert?: Insert;',
|
|
196
|
+
' readonly __update?: Update;',
|
|
197
|
+
' readonly __id?: Id;',
|
|
198
|
+
'}',
|
|
199
|
+
].join('\n'));
|
|
169
200
|
parts.push(emitSchema(ir));
|
|
170
201
|
for (const table of ir.tables) {
|
|
171
202
|
parts.push(emitRowInterfaces(table, naming));
|
|
203
|
+
parts.push(emitTableDescriptor(table, naming));
|
|
172
204
|
}
|
|
173
205
|
for (const sub of ir.subscriptions) {
|
|
174
206
|
parts.push(emitSubscription(sub));
|
package/dist/generate.d.ts
CHANGED
|
@@ -48,6 +48,10 @@ export interface GenerateResult {
|
|
|
48
48
|
/** Serialized IR document (the exact bytes of the `.ir.json` output). */
|
|
49
49
|
readonly irJson: string;
|
|
50
50
|
readonly hash: string;
|
|
51
|
+
/** Serialized QueryIR and its content hash. Query descriptors use this hash
|
|
52
|
+
* so a SQL-only edit cannot reuse a stale reactive cache entry. */
|
|
53
|
+
readonly queryIrJson: string;
|
|
54
|
+
readonly queryHash: string;
|
|
51
55
|
/** Generated TS module source (the exact bytes of the `.ts` output). */
|
|
52
56
|
readonly module: string;
|
|
53
57
|
readonly irPath: string;
|
package/dist/generate.js
CHANGED
|
@@ -18,6 +18,7 @@ import { canonicalizeExtensions, IR_VERSION, irHash, serializeIr, } from './ir.j
|
|
|
18
18
|
import { MANIFEST_FILENAME, parseManifest, } from './manifest.js';
|
|
19
19
|
import { buildNamingMap } from './naming.js';
|
|
20
20
|
import { analyzeQueryFile, synthesizeDdl, } from './query.js';
|
|
21
|
+
import { serializeQueryIr } from './query-ir.js';
|
|
21
22
|
import { applyMigrationSql } from './sql.js';
|
|
22
23
|
import { analyzeSyqlFile } from './syql.js';
|
|
23
24
|
/** Same grammar the server compiles (§3.1): `prefix:{variable}`. */
|
|
@@ -337,11 +338,12 @@ export function generate(manifestDir) {
|
|
|
337
338
|
{ path: modulePath, content: module },
|
|
338
339
|
];
|
|
339
340
|
// Opt-in native emitters — each present only when the manifest requests it.
|
|
340
|
-
const { queries: tsQueriesPath, swift, kotlin, dart } = manifest.output;
|
|
341
|
+
const { queryIr: queryIrPath, queries: tsQueriesPath, swift, kotlin, dart, } = manifest.output;
|
|
341
342
|
// Named queries: analyzed once (SELECT-only, typed against the IR via
|
|
342
343
|
// SQLite), then emitted per-language into its OWN file so schema-only
|
|
343
344
|
// consumers never churn. Only analyzed when SOME query output is requested.
|
|
344
|
-
const wantsQueries =
|
|
345
|
+
const wantsQueries = queryIrPath !== undefined ||
|
|
346
|
+
tsQueriesPath !== undefined ||
|
|
345
347
|
swift?.queriesPath !== undefined ||
|
|
346
348
|
kotlin?.queriesPath !== undefined ||
|
|
347
349
|
dart?.queriesPath !== undefined;
|
|
@@ -351,10 +353,18 @@ export function generate(manifestDir) {
|
|
|
351
353
|
if (wantsQueries && analyzedQueries.length === 0) {
|
|
352
354
|
throw new TypegenError(resolve(manifestDir, manifest.queries), `an output requests named queries but no .sql files were found in ${manifest.queries} — add a query file or drop the queries output`);
|
|
353
355
|
}
|
|
356
|
+
const queryIrJson = serializeQueryIr(analyzedQueries);
|
|
357
|
+
const queryHash = irHash(queryIrJson);
|
|
358
|
+
if (queryIrPath !== undefined) {
|
|
359
|
+
outputs.push({
|
|
360
|
+
path: resolve(manifestDir, queryIrPath),
|
|
361
|
+
content: queryIrJson,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
354
364
|
if (tsQueriesPath !== undefined) {
|
|
355
365
|
outputs.push({
|
|
356
366
|
path: resolve(manifestDir, tsQueriesPath),
|
|
357
|
-
content: emitQueriesModule(analyzedQueries,
|
|
367
|
+
content: emitQueriesModule(analyzedQueries, queryHash, ir.irVersion),
|
|
358
368
|
});
|
|
359
369
|
}
|
|
360
370
|
if (swift !== undefined) {
|
|
@@ -365,7 +375,7 @@ export function generate(manifestDir) {
|
|
|
365
375
|
if (swift.queriesPath !== undefined) {
|
|
366
376
|
outputs.push({
|
|
367
377
|
path: resolve(manifestDir, swift.queriesPath),
|
|
368
|
-
content: emitQueriesSwiftModule(analyzedQueries,
|
|
378
|
+
content: emitQueriesSwiftModule(analyzedQueries, queryHash, ir.irVersion, swift.enumName),
|
|
369
379
|
});
|
|
370
380
|
}
|
|
371
381
|
}
|
|
@@ -377,7 +387,7 @@ export function generate(manifestDir) {
|
|
|
377
387
|
if (kotlin.queriesPath !== undefined) {
|
|
378
388
|
outputs.push({
|
|
379
389
|
path: resolve(manifestDir, kotlin.queriesPath),
|
|
380
|
-
content: emitQueriesKotlinModule(analyzedQueries,
|
|
390
|
+
content: emitQueriesKotlinModule(analyzedQueries, queryHash, ir.irVersion, kotlin.package, kotlin.objectName),
|
|
381
391
|
});
|
|
382
392
|
}
|
|
383
393
|
}
|
|
@@ -389,7 +399,7 @@ export function generate(manifestDir) {
|
|
|
389
399
|
if (dart.queriesPath !== undefined) {
|
|
390
400
|
outputs.push({
|
|
391
401
|
path: resolve(manifestDir, dart.queriesPath),
|
|
392
|
-
content: emitQueriesDartModule(analyzedQueries,
|
|
402
|
+
content: emitQueriesDartModule(analyzedQueries, queryHash, ir.irVersion),
|
|
393
403
|
});
|
|
394
404
|
}
|
|
395
405
|
}
|
|
@@ -397,6 +407,8 @@ export function generate(manifestDir) {
|
|
|
397
407
|
ir,
|
|
398
408
|
irJson,
|
|
399
409
|
hash,
|
|
410
|
+
queryIrJson,
|
|
411
|
+
queryHash,
|
|
400
412
|
module,
|
|
401
413
|
irPath,
|
|
402
414
|
modulePath,
|
package/dist/index.d.ts
CHANGED
|
@@ -23,3 +23,8 @@ export * from './query.js';
|
|
|
23
23
|
export * from './query-ir.js';
|
|
24
24
|
export * from './sql.js';
|
|
25
25
|
export * from './syql.js';
|
|
26
|
+
export * from './syql-ast.js';
|
|
27
|
+
export * from './syql-lexer.js';
|
|
28
|
+
export * from './syql-modules.js';
|
|
29
|
+
export * from './syql-parser.js';
|
|
30
|
+
export * from './syql-template-parser.js';
|
package/dist/index.js
CHANGED
|
@@ -23,3 +23,8 @@ export * from './query.js';
|
|
|
23
23
|
export * from './query-ir.js';
|
|
24
24
|
export * from './sql.js';
|
|
25
25
|
export * from './syql.js';
|
|
26
|
+
export * from './syql-ast.js';
|
|
27
|
+
export * from './syql-lexer.js';
|
|
28
|
+
export * from './syql-modules.js';
|
|
29
|
+
export * from './syql-parser.js';
|
|
30
|
+
export * from './syql-template-parser.js';
|
package/dist/manifest.d.ts
CHANGED
|
@@ -56,6 +56,9 @@ export interface DartOutput {
|
|
|
56
56
|
export interface ManifestOutput {
|
|
57
57
|
readonly ir: string;
|
|
58
58
|
readonly module: string;
|
|
59
|
+
/** Opt-in deterministic QueryIR JSON. This is also the source of the
|
|
60
|
+
* generated query-descriptor hash, so SQL-only changes rotate cache IDs. */
|
|
61
|
+
readonly queryIr?: string;
|
|
59
62
|
/** Opt-in TS named-queries output path (a sibling `.ts` file). Undefined →
|
|
60
63
|
* named queries are not generated for TS. */
|
|
61
64
|
readonly queries?: string;
|
package/dist/manifest.js
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
* final entry must cover the final migration.
|
|
27
27
|
* - Subscription scope values are literals or whole-value `{param}`
|
|
28
28
|
* placeholders (partial templates are unsupported).
|
|
29
|
-
* - `output.ir` / `output.module` are the always-emitted TS defaults
|
|
29
|
+
* - `output.ir` / `output.module` are the always-emitted TS defaults;
|
|
30
|
+
* `output.queryIr` opts into the deterministic analyzed-query document.
|
|
30
31
|
* `output.swift` / `output.kotlin` / `output.dart` are OPT-IN native
|
|
31
32
|
* emitters — a bare string is the output path; an object carries
|
|
32
33
|
* language-appropriate options (Kotlin `package`/`objectName`, Swift
|
|
@@ -242,18 +243,22 @@ export function parseManifest(raw) {
|
|
|
242
243
|
}
|
|
243
244
|
let ir = './syncular.ir.json';
|
|
244
245
|
let module = './syncular.generated.ts';
|
|
246
|
+
let queryIr;
|
|
245
247
|
let queriesOut;
|
|
246
248
|
let swift;
|
|
247
249
|
let kotlin;
|
|
248
250
|
let dart;
|
|
249
251
|
if (obj.output !== undefined) {
|
|
250
252
|
const output = asObject(obj.output, 'output');
|
|
251
|
-
rejectUnknownKeys(output, ['ir', 'module', 'queries', 'swift', 'kotlin', 'dart'], 'output');
|
|
253
|
+
rejectUnknownKeys(output, ['ir', 'module', 'queryIr', 'queries', 'swift', 'kotlin', 'dart'], 'output');
|
|
252
254
|
if (output.ir !== undefined)
|
|
253
255
|
ir = asString(output.ir, 'output.ir');
|
|
254
256
|
if (output.module !== undefined) {
|
|
255
257
|
module = asString(output.module, 'output.module');
|
|
256
258
|
}
|
|
259
|
+
if (output.queryIr !== undefined) {
|
|
260
|
+
queryIr = asString(output.queryIr, 'output.queryIr');
|
|
261
|
+
}
|
|
257
262
|
if (output.queries !== undefined) {
|
|
258
263
|
queriesOut = asString(output.queries, 'output.queries');
|
|
259
264
|
}
|
|
@@ -269,6 +274,7 @@ export function parseManifest(raw) {
|
|
|
269
274
|
const outputSpec = {
|
|
270
275
|
ir,
|
|
271
276
|
module,
|
|
277
|
+
...(queryIr !== undefined ? { queryIr } : {}),
|
|
272
278
|
...(queriesOut !== undefined ? { queries: queriesOut } : {}),
|
|
273
279
|
...(swift !== undefined ? { swift } : {}),
|
|
274
280
|
...(kotlin !== undefined ? { kotlin } : {}),
|
package/dist/query-ir.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Serialize analyzed queries as the deterministic QueryIR JSON document. */
|
|
2
2
|
export function serializeQueryIr(queries) {
|
|
3
3
|
const doc = {
|
|
4
|
-
queryIrVersion:
|
|
4
|
+
queryIrVersion: 2,
|
|
5
5
|
queries: queries.map((query) => ({
|
|
6
6
|
name: query.name,
|
|
7
7
|
file: query.file,
|
|
@@ -24,8 +24,31 @@ export function serializeQueryIr(queries) {
|
|
|
24
24
|
type: column.type,
|
|
25
25
|
nullable: column.nullable,
|
|
26
26
|
fidelity: column.fidelity,
|
|
27
|
+
...(column.origin !== undefined ? { origin: column.origin } : {}),
|
|
27
28
|
})),
|
|
28
29
|
tables: query.tables,
|
|
30
|
+
reactive: {
|
|
31
|
+
dependencies: query.reactive.dependencies.map((dependency) => ({
|
|
32
|
+
table: dependency.table,
|
|
33
|
+
scopes: dependency.scopes.map((scope) => ({
|
|
34
|
+
variable: scope.variable,
|
|
35
|
+
pattern: scope.pattern,
|
|
36
|
+
params: scope.params,
|
|
37
|
+
})),
|
|
38
|
+
})),
|
|
39
|
+
coverage: query.reactive.coverage.map((coverage) => ({
|
|
40
|
+
table: coverage.table,
|
|
41
|
+
variable: coverage.variable,
|
|
42
|
+
units: coverage.units,
|
|
43
|
+
fixedScopes: coverage.fixedScopes.map((scope) => ({
|
|
44
|
+
variable: scope.variable,
|
|
45
|
+
params: scope.params,
|
|
46
|
+
})),
|
|
47
|
+
})),
|
|
48
|
+
...(query.reactive.rowKey !== undefined
|
|
49
|
+
? { rowKey: query.reactive.rowKey }
|
|
50
|
+
: {}),
|
|
51
|
+
},
|
|
29
52
|
// §6 knob metadata — emitted only when declared.
|
|
30
53
|
...(query.orderBy !== undefined
|
|
31
54
|
? {
|
package/dist/query.d.ts
CHANGED
|
@@ -47,6 +47,35 @@ export interface QueryColumn {
|
|
|
47
47
|
/** `exact` = resolved to an IR column (plain ref); `fallback` = a computed
|
|
48
48
|
* expression typed by the documented honest fallback. */
|
|
49
49
|
readonly fidelity: 'exact' | 'fallback';
|
|
50
|
+
/** Proven physical origin for reactive row identity and diagnostics. */
|
|
51
|
+
readonly origin?: {
|
|
52
|
+
readonly table: string;
|
|
53
|
+
readonly column: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface QueryScopeBinding {
|
|
57
|
+
readonly table: string;
|
|
58
|
+
readonly variable: string;
|
|
59
|
+
readonly pattern: string;
|
|
60
|
+
readonly params: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
export interface QueryCoverageBinding {
|
|
63
|
+
readonly table: string;
|
|
64
|
+
readonly variable: string;
|
|
65
|
+
readonly units: readonly string[];
|
|
66
|
+
readonly fixedScopes: readonly {
|
|
67
|
+
readonly variable: string;
|
|
68
|
+
readonly params: readonly string[];
|
|
69
|
+
}[];
|
|
70
|
+
}
|
|
71
|
+
export interface QueryReactiveMetadata {
|
|
72
|
+
readonly dependencies: readonly {
|
|
73
|
+
readonly table: string;
|
|
74
|
+
readonly scopes: readonly QueryScopeBinding[];
|
|
75
|
+
}[];
|
|
76
|
+
readonly coverage: readonly QueryCoverageBinding[];
|
|
77
|
+
/** Language-facing projection fields forming a proven unique row key. */
|
|
78
|
+
readonly rowKey?: readonly string[];
|
|
50
79
|
}
|
|
51
80
|
/** §7/§8 backend selection: neutralization (default), always-variants, or
|
|
52
81
|
* the small-N heuristic (`auto`: enumerate at ≤ 2 optional groups). */
|
|
@@ -81,6 +110,7 @@ export interface AnalyzedQuery {
|
|
|
81
110
|
readonly columns: readonly QueryColumn[];
|
|
82
111
|
/** IR tables this query reads (the useRawSql `{tables}` set), sorted. */
|
|
83
112
|
readonly tables: readonly string[];
|
|
113
|
+
readonly reactive: QueryReactiveMetadata;
|
|
84
114
|
/** §6 orderBy knob (`.syql` tier). When present, `sql`/`positionalSql`
|
|
85
115
|
* carry the DEFAULT order-by tail and `positionalSqlBase` is the static
|
|
86
116
|
* prefix emitters compose the selected column onto. */
|