@syncular/typegen 0.5.1 → 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 +79 -32
- package/dist/cli.js +40 -17
- package/dist/emit-queries-dart.js +168 -55
- package/dist/emit-queries-kotlin.js +167 -55
- package/dist/emit-queries-swift.js +183 -57
- package/dist/emit-queries.js +286 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +348 -316
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +28 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lsp.d.ts +1 -3
- package/dist/lsp.js +349 -187
- package/dist/manifest.d.ts +1 -3
- package/dist/manifest.js +1 -1
- package/dist/query-ir.js +53 -42
- package/dist/query.d.ts +110 -63
- package/dist/query.js +89 -11
- package/dist/syql-ast.d.ts +12 -13
- package/dist/syql-ast.js +26 -20
- package/dist/syql-lexer.d.ts +2 -0
- package/dist/syql-lexer.js +9 -2
- package/dist/syql-lowering.d.ts +22 -0
- package/dist/syql-lowering.js +434 -0
- package/dist/syql-parser.d.ts +19 -18
- package/dist/syql-parser.js +341 -93
- package/dist/syql-semantics.d.ts +73 -0
- package/dist/syql-semantics.js +607 -0
- package/dist/syql-template-parser.d.ts +5 -20
- package/dist/syql-template-parser.js +116 -109
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +993 -0
- package/package.json +3 -3
- package/src/cli.ts +49 -21
- package/src/emit-queries-dart.ts +195 -69
- package/src/emit-queries-kotlin.ts +197 -69
- package/src/emit-queries-swift.ts +224 -68
- package/src/emit-queries.ts +410 -165
- package/src/fmt.ts +405 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +414 -216
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +229 -79
- package/src/syql-ast.ts +38 -34
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +664 -0
- package/src/syql-parser.ts +472 -134
- package/src/syql-semantics.ts +930 -0
- package/src/syql-template-parser.ts +119 -163
- package/src/syql-validator.ts +1486 -0
- package/dist/syql.d.ts +0 -102
- package/dist/syql.js +0 -1141
- package/src/syql.ts +0 -1470
package/README.md
CHANGED
|
@@ -68,8 +68,8 @@ schema guide and suggests `syncular init`.
|
|
|
68
68
|
| `manifestVersion` | yes | Must be `1`. Format growth happens by bumping this, not by tolerating unknown keys. |
|
|
69
69
|
| `migrations` | no (default `./migrations`) | Directory of `NNNN_name/up.sql` migrations, relative to the manifest. |
|
|
70
70
|
| `queries` | no (default `./queries`) | Directory of `.sql` / `.syql` named-query files (see §6). Only read when some output requests a queries file. |
|
|
71
|
-
| `naming` | no (default `"camel"`) |
|
|
72
|
-
| `queryBackend` | no (default `"
|
|
71
|
+
| `naming` | no (default `"camel"`) | `"camel"` maps snake_case SQL names to camelCase in generated row types/params (projections lower with `AS` aliases so runtime keys match); `"preserve"` keeps SQL-truth names. Collisions/keyword hazards are generate-time errors. |
|
|
72
|
+
| `queryBackend` | no (default `"auto"`) | Advanced revision-1 SYQL lowering override: `"neutralize"` emits guarded statements, `"variants"` enumerates activation states, and `"auto"` deterministically enumerates at ≤ 2 activation controls. This never changes source meaning or the public API. |
|
|
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
75
|
| `output.queryIr` | no | Deterministic analyzed QueryIR JSON. Its hash keys generated reactive query descriptors, so SQL-only changes invalidate caches. |
|
|
@@ -326,18 +326,54 @@ function on every platform — killing query↔type drift *by construction*. It
|
|
|
326
326
|
the type-safe **read** tier; raw `query(sql, params)` — guarded read-only —
|
|
327
327
|
stays the escape hatch for queries built at runtime.
|
|
328
328
|
|
|
329
|
-
Two frontends feed one pipeline
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
`
|
|
339
|
-
|
|
340
|
-
|
|
329
|
+
Two frontends feed one QueryIR pipeline. **`.sql`** is plain SQL plus
|
|
330
|
+
`:params`, the compatibility floor documented below. **`.syql`** is the
|
|
331
|
+
revision-1 structured frontend: authoritative typed inputs, explicit
|
|
332
|
+
`when(...)` conjuncts, atomic optional groups, imported hygienic predicates,
|
|
333
|
+
inferred scope dependencies, explicit sync coverage, complete sort profiles,
|
|
334
|
+
bounded limits, and inferred identity. Its complete normative definition is
|
|
335
|
+
[`../../docs/SYQL.md`](../../docs/SYQL.md); the rationale and implementation
|
|
336
|
+
record are [`../../docs/rfcs/0004-syql-language.md`](../../docs/rfcs/0004-syql-language.md).
|
|
337
|
+
|
|
338
|
+
Tooling: `syncular generate --print <name>` prints every selected checked
|
|
339
|
+
statement and bind, `syncular fmt` is the semantic-preserving canonical
|
|
340
|
+
formatter, and `syncular lsp` provides project-aware diagnostics, hover,
|
|
341
|
+
definitions, references, symbols, and formatting. Casing follows the manifest
|
|
342
|
+
`naming` mode (§1): generated rows/inputs are camelCase and projections are
|
|
343
|
+
`AS`-aliased so runtime keys match.
|
|
344
|
+
|
|
345
|
+
```syql
|
|
346
|
+
import { matchesTitle } from "./predicates.syql";
|
|
347
|
+
|
|
348
|
+
sync query listTodos(
|
|
349
|
+
listId,
|
|
350
|
+
status?: string | null,
|
|
351
|
+
range?,
|
|
352
|
+
q?: string,
|
|
353
|
+
unassigned: bool = false,
|
|
354
|
+
) {
|
|
355
|
+
select id, title, status, created_at
|
|
356
|
+
from todos
|
|
357
|
+
where todos.list_id = :listId
|
|
358
|
+
and when(status) status is :status
|
|
359
|
+
and when(range) created_at between :range
|
|
360
|
+
and when(unassigned) assignee_id is null
|
|
361
|
+
and when(q) matchesTitle(:q)
|
|
362
|
+
order by sortBy default newest {
|
|
363
|
+
newest: created_at desc, id desc;
|
|
364
|
+
oldest: created_at asc, id asc;
|
|
365
|
+
}
|
|
366
|
+
limit pageSize default 50 max 200;
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Optional nullable scalars use a generated presence wrapper so absent,
|
|
371
|
+
present-null, and present-value remain distinct. A group is one optional host
|
|
372
|
+
object whose members are all required. A `bool = false` flag activates on
|
|
373
|
+
true. Sort is a generated enum/union of complete checked profiles; limit is validated as a
|
|
374
|
+
positive bounded integer before execution. `integer` inputs are exact signed
|
|
375
|
+
64-bit values in the SYQL API (`bigint` in TypeScript, `Int64`/`Long`/`int` on
|
|
376
|
+
native targets).
|
|
341
377
|
|
|
342
378
|
**File & naming convention.** Named queries live in a `queries/` directory
|
|
343
379
|
next to `migrations/` (override with the top-level `"queries"` manifest key).
|
|
@@ -412,6 +448,27 @@ boundary):
|
|
|
412
448
|
| **plain column ref** (`title`, `t.title`, `title AS x`) | resolved to the IR column (decltype confirms) — exact IR type, incl. `json`/`blob_ref`/`crdt` | the IR column's `NOT NULL` | **exact** |
|
|
413
449
|
| **computed expression** (`count(*)`, `done + 1`, `:p AS l`) | decltype is null → documented fallback: aggregate/arithmetic → number, else string | always nullable (an expression's nullability is not knowable from decltype) | **fallback** |
|
|
414
450
|
|
|
451
|
+
Every local synced table also has the protocol-owned `_sync_version` column.
|
|
452
|
+
Named queries may project it with an explicit public alias when an application
|
|
453
|
+
needs the observed server version for optimistic concurrency:
|
|
454
|
+
|
|
455
|
+
```syql
|
|
456
|
+
query getTodo(listId, todoId) {
|
|
457
|
+
select id, title, _sync_version as server_version
|
|
458
|
+
from todos
|
|
459
|
+
where todos.list_id = :listId and todos.id = :todoId;
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
The generated `serverVersion` field is an exact, non-null `integer`. The
|
|
464
|
+
physical name is intentionally not a public result name: project it with an
|
|
465
|
+
alias such as `server_version`. `_sync_version` is query-only, is excluded from
|
|
466
|
+
schema and mutation types, and is not added by `select *`; this prevents client
|
|
467
|
+
code from writing engine-owned concurrency state. Pass a positive observed
|
|
468
|
+
value as a mutation `baseVersion`. Locally-created or still-unacknowledged rows
|
|
469
|
+
can carry a non-positive sentinel and should omit the base version until the
|
|
470
|
+
server assigns one.
|
|
471
|
+
|
|
415
472
|
`bun:sqlite` exposes `columnNames`, `declaredTypes` (once executed once — we
|
|
416
473
|
run the statement against the empty DB), and `paramsCount`. It does **not**
|
|
417
474
|
expose column origin (table/column), param **names**, or `NOT NULL` flags — so
|
|
@@ -455,30 +512,20 @@ identifier and keeping those that name an IR table. This captures subquery /
|
|
|
455
512
|
prepare() still guarantees the SQL itself is correct.
|
|
456
513
|
|
|
457
514
|
Inference is deliberately conservative around `OR`, joins, grouping, and
|
|
458
|
-
computed identity.
|
|
515
|
+
computed identity. Ordinary scope predicates construct exact reactive facts;
|
|
516
|
+
`sync query` additionally requests checked coverage:
|
|
459
517
|
|
|
460
518
|
```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
|
-
{
|
|
519
|
+
sync query compareLists(left, right) {
|
|
466
520
|
select id, title from tasks
|
|
467
|
-
where project_id
|
|
521
|
+
where tasks.project_id in (:left, :right);
|
|
468
522
|
}
|
|
469
523
|
```
|
|
470
524
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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.
|
|
525
|
+
Coverage must resolve one table instance and bind every declared scope. Only
|
|
526
|
+
required, non-null, exactly typed binds are allowed. Result identity is inferred
|
|
527
|
+
from schema keys and the projection. Without constructive proof the compiler
|
|
528
|
+
falls back to table-wide dependency, no coverage, and/or unkeyed reconciliation.
|
|
482
529
|
|
|
483
530
|
**Emitted shape, per language** (one query, five outputs — abbreviated):
|
|
484
531
|
|
package/dist/cli.js
CHANGED
|
@@ -109,29 +109,52 @@ function runGeneratePrint(args, name) {
|
|
|
109
109
|
}
|
|
110
110
|
console.log(`-- ${query.name} (${query.file})`);
|
|
111
111
|
console.log(`-- tables: ${query.tables.join(', ')}`);
|
|
112
|
+
if (query.syql !== undefined) {
|
|
113
|
+
console.log('-- revision: SYQL 1');
|
|
114
|
+
console.log(`-- backend: ${query.syql.plan.backend}`);
|
|
115
|
+
if (query.syql.inputs.length > 0) {
|
|
116
|
+
console.log('-- public inputs:');
|
|
117
|
+
for (const input of query.syql.inputs) {
|
|
118
|
+
if (input.kind === 'value') {
|
|
119
|
+
console.log(`-- ${input.name}: ${input.type}${input.nullable ? ' | null' : ''} (${input.required ? 'required' : 'optional'})`);
|
|
120
|
+
}
|
|
121
|
+
else if (input.kind === 'group') {
|
|
122
|
+
console.log(`-- ${input.name}: optional group (${input.members.map((member) => `${member.name}: ${member.type}${member.nullable ? ' | null' : ''}`).join(', ')})`);
|
|
123
|
+
}
|
|
124
|
+
else if (input.kind === 'sort') {
|
|
125
|
+
console.log(`-- ${input.name}: sort [${input.profiles.map((profile) => profile.name).join(', ')}] (default ${input.defaultProfile})`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log(`-- ${input.name}: limit 1..${input.maxSize} (default ${input.defaultSize})`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (query.syql.identity !== undefined) {
|
|
133
|
+
console.log(`-- identity: ${query.syql.identity.join(', ')}`);
|
|
134
|
+
}
|
|
135
|
+
console.log(`-- checked statements: ${query.syql.plan.statements.length} (activation controls: ${query.syql.plan.activationControls.join(', ') || 'none'})`);
|
|
136
|
+
for (const statement of query.syql.plan.statements) {
|
|
137
|
+
const selectors = [
|
|
138
|
+
...(statement.activationMask === undefined
|
|
139
|
+
? []
|
|
140
|
+
: [`mask=${statement.activationMask}`]),
|
|
141
|
+
...(statement.sortProfile === undefined
|
|
142
|
+
? []
|
|
143
|
+
: [`sort=${statement.sortProfile}`]),
|
|
144
|
+
];
|
|
145
|
+
console.log(`-- [${selectors.join(', ') || 'default'}]`);
|
|
146
|
+
console.log(statement.sql);
|
|
147
|
+
console.log(`-- binds: ${statement.binds.map((bind) => `:${bind.name}`).join(', ') || '(none)'}`);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
112
151
|
if (query.params.length > 0) {
|
|
113
152
|
console.log('-- params:');
|
|
114
153
|
for (const param of query.params) {
|
|
115
|
-
|
|
116
|
-
param.optional === true ? 'optional' : 'required',
|
|
117
|
-
...(param.flag === true ? ['flag'] : []),
|
|
118
|
-
...(param.group !== undefined ? [`group ${param.group}`] : []),
|
|
119
|
-
].join(', ');
|
|
120
|
-
console.log(`-- :${param.name} ${param.type} (${marks})`);
|
|
154
|
+
console.log(`-- :${param.name} ${param.type} (required)`);
|
|
121
155
|
}
|
|
122
156
|
}
|
|
123
157
|
console.log(query.sql);
|
|
124
|
-
if (query.orderBy !== undefined) {
|
|
125
|
-
console.log('-- orderBy variants (each checked against the schema):');
|
|
126
|
-
const base = query.positionalSqlBase ?? '';
|
|
127
|
-
for (const col of query.orderBy.allowed) {
|
|
128
|
-
const isDefault = col.name === query.orderBy.defaultColumn;
|
|
129
|
-
console.log(`-- ${col.langName}: ${base} order by ${col.name} asc|desc${query.positionalLimitTail ?? ''}${isDefault ? ' (default)' : ''}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (query.limit !== undefined) {
|
|
133
|
-
console.log(`-- limit: bound value, default ${query.limit.default ?? query.limit.max}${query.limit.max !== undefined ? `, clamped to ${query.limit.max}` : ''}`);
|
|
134
|
-
}
|
|
135
158
|
}
|
|
136
159
|
function parseManifestDir(argv) {
|
|
137
160
|
let manifestDir = '.';
|
|
@@ -65,25 +65,148 @@ function optionalParamValue(type, name) {
|
|
|
65
65
|
return name;
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
function syqlInput(query, name) {
|
|
69
|
+
const input = query.syql?.inputs.find((candidate) => candidate.name === name);
|
|
70
|
+
if (input === undefined)
|
|
71
|
+
throw new Error(`unknown SYQL input ${name}`);
|
|
72
|
+
return input;
|
|
73
|
+
}
|
|
74
|
+
function syqlDartType(type, nullable) {
|
|
75
|
+
return `${DART_TYPE[type]}${nullable ? '?' : ''}`;
|
|
76
|
+
}
|
|
77
|
+
function syqlControlActive(query, name) {
|
|
78
|
+
const input = syqlInput(query, name);
|
|
79
|
+
const access = camelCase(input.langName);
|
|
80
|
+
if (input.kind === 'value' && input.default === false)
|
|
81
|
+
return access;
|
|
82
|
+
if (input.kind === 'value' && input.nullable)
|
|
83
|
+
return `${access}.isPresent`;
|
|
84
|
+
if (input.kind === 'value' || input.kind === 'group') {
|
|
85
|
+
return `${access} != null`;
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`${name} is not an activation control`);
|
|
88
|
+
}
|
|
89
|
+
function syqlBindExpr(query, bind) {
|
|
90
|
+
if (bind.kind === 'condition-active') {
|
|
91
|
+
return bind.controls
|
|
92
|
+
.map((control) => syqlControlActive(query, control))
|
|
93
|
+
.join(' && ');
|
|
94
|
+
}
|
|
95
|
+
const input = syqlInput(query, bind.input);
|
|
96
|
+
const access = camelCase(input.langName);
|
|
97
|
+
if (bind.kind === 'limit')
|
|
98
|
+
return `effective${typeName(access)}`;
|
|
99
|
+
if (bind.kind === 'group-member') {
|
|
100
|
+
if (input.kind !== 'group')
|
|
101
|
+
throw new Error('group bind/input mismatch');
|
|
102
|
+
const member = input.members.find((candidate) => candidate.name === bind.member);
|
|
103
|
+
if (member === undefined)
|
|
104
|
+
throw new Error(`unknown group member ${bind.member}`);
|
|
105
|
+
const memberAccess = `${access}.${camelCase(member.langName)}`;
|
|
106
|
+
const value = member.nullable
|
|
107
|
+
? optionalParamValue(member.type, memberAccess)
|
|
108
|
+
: paramValue(member.type, memberAccess);
|
|
109
|
+
return `${access} == null ? null : ${value}`;
|
|
110
|
+
}
|
|
111
|
+
if (input.kind !== 'value')
|
|
112
|
+
throw new Error('value bind/input mismatch');
|
|
113
|
+
if (input.default === false)
|
|
114
|
+
return paramValue(input.type, access);
|
|
115
|
+
if (input.required) {
|
|
116
|
+
return input.nullable
|
|
117
|
+
? optionalParamValue(input.type, access)
|
|
118
|
+
: paramValue(input.type, access);
|
|
119
|
+
}
|
|
120
|
+
return input.nullable
|
|
121
|
+
? `${access}.isPresent ? ${optionalParamValue(input.type, `${access}.value`)} : null`
|
|
122
|
+
: optionalParamValue(input.type, access);
|
|
123
|
+
}
|
|
124
|
+
function emitSyqlDartTypes(query) {
|
|
77
125
|
const lines = [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
126
|
+
for (const input of query.syql?.inputs ?? []) {
|
|
127
|
+
if (input.kind === 'group') {
|
|
128
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
129
|
+
lines.push(`class ${name} {`);
|
|
130
|
+
for (const member of input.members) {
|
|
131
|
+
lines.push(` final ${syqlDartType(member.type, member.nullable)} ${camelCase(member.langName)};`);
|
|
132
|
+
}
|
|
133
|
+
const args = input.members
|
|
134
|
+
.map((member) => `required this.${camelCase(member.langName)}`)
|
|
135
|
+
.join(', ');
|
|
136
|
+
lines.push(` const ${name}({${args}});`, '}', '');
|
|
137
|
+
}
|
|
138
|
+
else if (input.kind === 'sort') {
|
|
139
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
140
|
+
lines.push(`enum ${name} { ${input.profiles.map((profile) => camelCase(profile.langName)).join(', ')} }`, '');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (lines[lines.length - 1] === '')
|
|
144
|
+
lines.pop();
|
|
145
|
+
return lines;
|
|
146
|
+
}
|
|
147
|
+
function emitSyqlDartRunner(query) {
|
|
148
|
+
const metadata = query.syql;
|
|
149
|
+
if (metadata === undefined)
|
|
150
|
+
throw new Error('missing SYQL metadata');
|
|
151
|
+
const Row = `${typeName(query.name)}Row`;
|
|
152
|
+
const Pascal = pascalCase(query.name);
|
|
153
|
+
const lines = [];
|
|
154
|
+
lines.push(`/// Tables the ${query.name} query reads (exact invalidation set).`, `const List<String> syncular${Pascal}QueryTables = [${query.tables.map(quote).join(', ')}];`, '', `/// Run the ${query.name} revision-1 SYQL query.`);
|
|
155
|
+
const args = [];
|
|
156
|
+
for (const input of metadata.inputs) {
|
|
157
|
+
const name = camelCase(input.langName);
|
|
158
|
+
if (input.kind === 'value') {
|
|
159
|
+
const type = syqlDartType(input.type, input.nullable);
|
|
160
|
+
if (input.required)
|
|
161
|
+
args.push(`required ${type} ${name}`);
|
|
162
|
+
else if (input.default === false)
|
|
163
|
+
args.push(`bool ${name} = false`);
|
|
164
|
+
else if (input.nullable) {
|
|
165
|
+
args.push(`SyqlQueryPresence<${type}> ${name} = const SyqlQueryPresence.absent()`);
|
|
166
|
+
}
|
|
167
|
+
else
|
|
168
|
+
args.push(`${type}? ${name}`);
|
|
169
|
+
}
|
|
170
|
+
else if (input.kind === 'group') {
|
|
171
|
+
args.push(`${typeName(query.name)}${typeName(input.langName)}? ${name}`);
|
|
172
|
+
}
|
|
173
|
+
else if (input.kind === 'sort') {
|
|
174
|
+
const defaultCase = input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
175
|
+
?.langName ?? input.defaultProfile;
|
|
176
|
+
const type = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
177
|
+
args.push(`${type} ${name} = ${type}.${camelCase(defaultCase)}`);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
args.push(`int? ${name}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const argsClause = args.length > 0 ? `, {${args.join(', ')}}` : '';
|
|
184
|
+
lines.push(`List<${Row}> syncular${Pascal}Query(SyncularClient client${argsClause}) {`);
|
|
185
|
+
const page = metadata.inputs.find((input) => input.kind === 'limit');
|
|
186
|
+
if (page?.kind === 'limit') {
|
|
187
|
+
const name = camelCase(page.langName);
|
|
188
|
+
lines.push(` final effective${typeName(name)} = ${name} ?? ${page.defaultSize};`, ` if (effective${typeName(name)} < 1 || effective${typeName(name)} > ${page.maxSize}) {`, ` throw SyqlQueryInputException('SYQL_RUNTIME_INVALID_LIMIT', ${quote(`${query.name}: invalid limit`)});`, ' }');
|
|
189
|
+
}
|
|
190
|
+
if (metadata.plan.backend === 'variants') {
|
|
191
|
+
lines.push(' var activationMask = 0;');
|
|
192
|
+
metadata.plan.activationControls.forEach((control, index) => {
|
|
193
|
+
lines.push(` if (${syqlControlActive(query, control)}) activationMask |= ${2 ** index};`);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const sort = metadata.inputs.find((input) => input.kind === 'sort');
|
|
197
|
+
const profileCount = sort?.kind === 'sort' ? sort.profiles.length : 1;
|
|
198
|
+
const sortIndex = sort?.kind === 'sort' ? `${camelCase(sort.langName)}.index` : '0';
|
|
199
|
+
const index = metadata.plan.backend === 'variants'
|
|
200
|
+
? `activationMask * ${profileCount} + ${sortIndex}`
|
|
201
|
+
: sortIndex;
|
|
202
|
+
lines.push(` final statementIndex = ${index};`, ' late final String sql;', ' late final List<Object?> params;', ' switch (statementIndex) {');
|
|
203
|
+
metadata.plan.statements.forEach((statement, statementIndex) => {
|
|
204
|
+
const binds = statement.binds
|
|
205
|
+
.map((bind) => syqlBindExpr(query, bind))
|
|
206
|
+
.join(', ');
|
|
207
|
+
lines.push(` case ${statementIndex}: sql = ${quote(statement.positionalSql)}; params = <Object?>[${binds}]; break;`);
|
|
208
|
+
});
|
|
209
|
+
lines.push(" default: throw StateError('invalid generated SYQL statement index');", ' }', ` return client.query(sql, params: params).map(${Row}.fromRow).whereType<${Row}>().toList();`, '}');
|
|
87
210
|
return lines;
|
|
88
211
|
}
|
|
89
212
|
function emitClass(query) {
|
|
@@ -121,52 +244,30 @@ function emitClass(query) {
|
|
|
121
244
|
return lines;
|
|
122
245
|
}
|
|
123
246
|
function emitRunner(query) {
|
|
247
|
+
if (query.syql !== undefined)
|
|
248
|
+
return emitSyqlDartRunner(query);
|
|
124
249
|
const Row = `${typeName(query.name)}Row`;
|
|
125
250
|
const Pascal = pascalCase(query.name);
|
|
126
251
|
const lines = [];
|
|
127
252
|
lines.push(`/// Tables the ${query.name} query reads (exact invalidation set).`);
|
|
128
253
|
lines.push(`const List<String> syncular${Pascal}QueryTables = [${query.tables.map(quote).join(', ')}];`);
|
|
129
254
|
lines.push('');
|
|
130
|
-
|
|
131
|
-
lines.push(`const String _${query.name}SqlBase = ${quote(query.positionalSqlBase ?? '')};`);
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
lines.push(`const String _${query.name}Sql = ${quote(query.positionalSql)};`);
|
|
135
|
-
}
|
|
255
|
+
lines.push(`const String _${query.name}Sql = ${quote(query.positionalSql)};`);
|
|
136
256
|
lines.push('');
|
|
137
257
|
lines.push(`/// Run the ${query.name} named query (SELECT-only).`);
|
|
138
258
|
const args = [];
|
|
139
259
|
for (const p of query.params) {
|
|
140
260
|
const name = camelCase(p.langName);
|
|
141
|
-
|
|
142
|
-
args.push(`${DART_TYPE[p.type]}? ${name}`);
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
args.push(`required ${DART_TYPE[p.type]} ${name}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (query.orderBy !== undefined) {
|
|
149
|
-
const defaultCase = camelCase(query.orderBy.allowed.find((c) => c.name === query.orderBy?.defaultColumn)
|
|
150
|
-
?.langName ?? query.orderBy.defaultColumn);
|
|
151
|
-
args.push(`${typeName(query.name)}OrderBy orderBy = ${typeName(query.name)}OrderBy.${defaultCase}`);
|
|
152
|
-
args.push(`SyncularQueryDir dir = SyncularQueryDir.${query.orderBy.defaultDir}`);
|
|
261
|
+
args.push(`required ${DART_TYPE[p.type]} ${name}`);
|
|
153
262
|
}
|
|
154
263
|
const argsClause = args.length > 0 ? `, {${args.join(', ')}}` : '';
|
|
155
264
|
lines.push(`List<${Row}> syncular${Pascal}Query(SyncularClient client${argsClause}) {`);
|
|
156
|
-
|
|
157
|
-
const limitTail = query.positionalLimitTail !== undefined
|
|
158
|
-
? ` ${quote(query.positionalLimitTail.trim())}`
|
|
159
|
-
: '';
|
|
160
|
-
lines.push(` final sql = '$_${query.name}SqlBase order by \${orderBy.column} \${dir.name}'${limitTail === '' ? '' : `\n ' ' ${limitTail.trim()}`};`);
|
|
161
|
-
}
|
|
162
|
-
const sqlRef = query.orderBy !== undefined ? 'sql' : `_${query.name}Sql`;
|
|
265
|
+
const sqlRef = `_${query.name}Sql`;
|
|
163
266
|
if (query.params.length > 0) {
|
|
164
267
|
const binds = query.params
|
|
165
268
|
.map((p) => {
|
|
166
269
|
const name = camelCase(p.langName);
|
|
167
|
-
return
|
|
168
|
-
? optionalParamValue(p.type, name)
|
|
169
|
-
: paramValue(p.type, name);
|
|
270
|
+
return paramValue(p.type, name);
|
|
170
271
|
})
|
|
171
272
|
.join(', ');
|
|
172
273
|
lines.push(` final params = <Object?>[${binds}];`);
|
|
@@ -188,6 +289,24 @@ export function emitQueriesDartModule(queries, hash, irVersion) {
|
|
|
188
289
|
'',
|
|
189
290
|
"import 'package:syncular/syncular.dart';",
|
|
190
291
|
].join('\n'));
|
|
292
|
+
if (queries.some((query) => query.syql !== undefined)) {
|
|
293
|
+
parts.push([
|
|
294
|
+
'class SyqlQueryPresence<T> {',
|
|
295
|
+
' final bool isPresent;',
|
|
296
|
+
' final T? value;',
|
|
297
|
+
' const SyqlQueryPresence.absent() : isPresent = false, value = null;',
|
|
298
|
+
' const SyqlQueryPresence.present(this.value) : isPresent = true;',
|
|
299
|
+
'}',
|
|
300
|
+
'',
|
|
301
|
+
'class SyqlQueryInputException implements Exception {',
|
|
302
|
+
' final String code;',
|
|
303
|
+
' final String message;',
|
|
304
|
+
' const SyqlQueryInputException(this.code, this.message);',
|
|
305
|
+
' @override',
|
|
306
|
+
" String toString() => '$code: $message';",
|
|
307
|
+
'}',
|
|
308
|
+
].join('\n'));
|
|
309
|
+
}
|
|
191
310
|
parts.push([
|
|
192
311
|
'/// Lift a SQLite boolean: a real bool, or 0/1 as a number.',
|
|
193
312
|
'bool? _queryRowBool(Object? value) {',
|
|
@@ -213,17 +332,11 @@ export function emitQueriesDartModule(queries, hash, irVersion) {
|
|
|
213
332
|
" return {r'$bytes': hex};",
|
|
214
333
|
'}',
|
|
215
334
|
].join('\n'));
|
|
216
|
-
if (queries.some((q) => q.orderBy !== undefined)) {
|
|
217
|
-
parts.push([
|
|
218
|
-
'/// §6 orderBy direction (shared by every orderBy-knob query).',
|
|
219
|
-
'enum SyncularQueryDir { asc, desc }',
|
|
220
|
-
].join('\n'));
|
|
221
|
-
}
|
|
222
335
|
for (const query of queries) {
|
|
336
|
+
const syqlTypes = emitSyqlDartTypes(query);
|
|
337
|
+
if (syqlTypes.length > 0)
|
|
338
|
+
parts.push(syqlTypes.join('\n'));
|
|
223
339
|
parts.push(emitClass(query).join('\n'));
|
|
224
|
-
const orderByEnum = emitOrderByEnum(query);
|
|
225
|
-
if (orderByEnum.length > 0)
|
|
226
|
-
parts.push(orderByEnum.join('\n'));
|
|
227
340
|
}
|
|
228
341
|
for (const query of queries) {
|
|
229
342
|
parts.push(emitRunner(query).join('\n'));
|