@syncular/typegen 0.5.1 → 0.6.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 +75 -33
- package/dist/cli.js +43 -17
- package/dist/emit-queries-dart.js +167 -55
- package/dist/emit-queries-kotlin.js +166 -55
- package/dist/emit-queries-swift.js +182 -57
- package/dist/emit-queries.js +289 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +323 -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 +359 -185
- 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 +115 -63
- package/dist/query.js +60 -11
- 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 +411 -0
- package/dist/syql-semantics.d.ts +76 -0
- package/dist/syql-semantics.js +551 -0
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +966 -0
- package/package.json +3 -3
- package/src/cli.ts +51 -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 +413 -165
- package/src/fmt.ts +377 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +425 -215
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +199 -79
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +638 -0
- package/src/syql-semantics.ts +859 -0
- package/src/syql-validator.ts +1492 -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,65 @@ 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
|
+
constructive `@scope`/`@cover` facts, complete sort profiles, bounded pages,
|
|
334
|
+
and proven 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
|
+
query listTodos(
|
|
349
|
+
listId,
|
|
350
|
+
status?: string | null,
|
|
351
|
+
range?(start: integer, end: integer),
|
|
352
|
+
q?: string,
|
|
353
|
+
unassigned?: switch,
|
|
354
|
+
) {
|
|
355
|
+
sql {
|
|
356
|
+
select id, title, status, created_at
|
|
357
|
+
from todos
|
|
358
|
+
where @cover(todos.list_id = :listId)
|
|
359
|
+
and when(status) {
|
|
360
|
+
status is :status
|
|
361
|
+
}
|
|
362
|
+
and when(range) {
|
|
363
|
+
created_at between :start and :end
|
|
364
|
+
}
|
|
365
|
+
and when(unassigned) {
|
|
366
|
+
assignee_id is null
|
|
367
|
+
}
|
|
368
|
+
and when(q) {
|
|
369
|
+
@matchesTitle(:q)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
sort sortBy default newest {
|
|
373
|
+
newest { created_at desc, id desc }
|
|
374
|
+
oldest { created_at asc, id asc }
|
|
375
|
+
}
|
|
376
|
+
page pageSize default 50 max 200;
|
|
377
|
+
identity by id;
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Optional nullable scalars use a generated presence wrapper so absent,
|
|
382
|
+
present-null, and present-value remain distinct. A group is one optional host
|
|
383
|
+
object whose members are all required. A switch defaults false. Sort is a
|
|
384
|
+
generated enum/union of complete checked profiles; page is validated as a
|
|
385
|
+
positive bounded integer before execution. `integer` inputs are exact signed
|
|
386
|
+
64-bit values in the SYQL API (`bigint` in TypeScript, `Int64`/`Long`/`int` on
|
|
387
|
+
native targets).
|
|
341
388
|
|
|
342
389
|
**File & naming convention.** Named queries live in a `queries/` directory
|
|
343
390
|
next to `migrations/` (override with the top-level `"queries"` manifest key).
|
|
@@ -455,30 +502,25 @@ identifier and keeping those that name an IR table. This captures subquery /
|
|
|
455
502
|
prepare() still guarantees the SQL itself is correct.
|
|
456
503
|
|
|
457
504
|
Inference is deliberately conservative around `OR`, joins, grouping, and
|
|
458
|
-
computed identity. A `.syql` query
|
|
505
|
+
computed identity. A `.syql` query constructs exact reactive facts in the same
|
|
506
|
+
node that restricts the SQL:
|
|
459
507
|
|
|
460
508
|
```syql
|
|
461
|
-
query compareLists(left, right)
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
where project_id = :left or project_id = :right
|
|
509
|
+
query compareLists(left, right) {
|
|
510
|
+
sql {
|
|
511
|
+
select id, title from tasks
|
|
512
|
+
where @cover(tasks.project_id in (:left, :right))
|
|
513
|
+
}
|
|
514
|
+
identity by id;
|
|
468
515
|
}
|
|
469
516
|
```
|
|
470
517
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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.
|
|
518
|
+
`@scope` emits a real predicate plus exact dependency keys. `@cover` emits the
|
|
519
|
+
same dependency facts plus proven window coverage and must bind every declared
|
|
520
|
+
scope of that table instance. Only required, exactly typed binds are allowed.
|
|
521
|
+
`identity by ...` is accepted only when the projected fields are proven unique;
|
|
522
|
+
it is not an unchecked assertion. Without constructive proof the compiler
|
|
523
|
+
falls back to table-wide dependency, no coverage, and/or unkeyed reconciliation.
|
|
482
524
|
|
|
483
525
|
**Emitted shape, per language** (one query, five outputs — abbreviated):
|
|
484
526
|
|
package/dist/cli.js
CHANGED
|
@@ -109,29 +109,55 @@ 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 === 'switch') {
|
|
125
|
+
console.log(`-- ${input.name}: switch (default false)`);
|
|
126
|
+
}
|
|
127
|
+
else if (input.kind === 'sort') {
|
|
128
|
+
console.log(`-- ${input.name}: sort [${input.profiles.map((profile) => profile.name).join(', ')}] (default ${input.defaultProfile})`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(`-- ${input.name}: page 1..${input.maxSize} (default ${input.defaultSize})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (query.syql.identity !== undefined) {
|
|
136
|
+
console.log(`-- identity: ${query.syql.identity.join(', ')}`);
|
|
137
|
+
}
|
|
138
|
+
console.log(`-- checked statements: ${query.syql.plan.statements.length} (activation controls: ${query.syql.plan.activationControls.join(', ') || 'none'})`);
|
|
139
|
+
for (const statement of query.syql.plan.statements) {
|
|
140
|
+
const selectors = [
|
|
141
|
+
...(statement.activationMask === undefined
|
|
142
|
+
? []
|
|
143
|
+
: [`mask=${statement.activationMask}`]),
|
|
144
|
+
...(statement.sortProfile === undefined
|
|
145
|
+
? []
|
|
146
|
+
: [`sort=${statement.sortProfile}`]),
|
|
147
|
+
];
|
|
148
|
+
console.log(`-- [${selectors.join(', ') || 'default'}]`);
|
|
149
|
+
console.log(statement.sql);
|
|
150
|
+
console.log(`-- binds: ${statement.binds.map((bind) => `:${bind.name}`).join(', ') || '(none)'}`);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
112
154
|
if (query.params.length > 0) {
|
|
113
155
|
console.log('-- params:');
|
|
114
156
|
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})`);
|
|
157
|
+
console.log(`-- :${param.name} ${param.type} (required)`);
|
|
121
158
|
}
|
|
122
159
|
}
|
|
123
160
|
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
161
|
}
|
|
136
162
|
function parseManifestDir(argv) {
|
|
137
163
|
let manifestDir = '.';
|
|
@@ -65,25 +65,147 @@ 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 === 'switch')
|
|
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 === 'page')
|
|
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.required) {
|
|
114
|
+
return input.nullable
|
|
115
|
+
? optionalParamValue(input.type, access)
|
|
116
|
+
: paramValue(input.type, access);
|
|
117
|
+
}
|
|
118
|
+
return input.nullable
|
|
119
|
+
? `${access}.isPresent ? ${optionalParamValue(input.type, `${access}.value`)} : null`
|
|
120
|
+
: optionalParamValue(input.type, access);
|
|
121
|
+
}
|
|
122
|
+
function emitSyqlDartTypes(query) {
|
|
77
123
|
const lines = [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
124
|
+
for (const input of query.syql?.inputs ?? []) {
|
|
125
|
+
if (input.kind === 'group') {
|
|
126
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
127
|
+
lines.push(`class ${name} {`);
|
|
128
|
+
for (const member of input.members) {
|
|
129
|
+
lines.push(` final ${syqlDartType(member.type, member.nullable)} ${camelCase(member.langName)};`);
|
|
130
|
+
}
|
|
131
|
+
const args = input.members
|
|
132
|
+
.map((member) => `required this.${camelCase(member.langName)}`)
|
|
133
|
+
.join(', ');
|
|
134
|
+
lines.push(` const ${name}({${args}});`, '}', '');
|
|
135
|
+
}
|
|
136
|
+
else if (input.kind === 'sort') {
|
|
137
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
138
|
+
lines.push(`enum ${name} { ${input.profiles.map((profile) => camelCase(profile.langName)).join(', ')} }`, '');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (lines[lines.length - 1] === '')
|
|
142
|
+
lines.pop();
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
145
|
+
function emitSyqlDartRunner(query) {
|
|
146
|
+
const metadata = query.syql;
|
|
147
|
+
if (metadata === undefined)
|
|
148
|
+
throw new Error('missing SYQL metadata');
|
|
149
|
+
const Row = `${typeName(query.name)}Row`;
|
|
150
|
+
const Pascal = pascalCase(query.name);
|
|
151
|
+
const lines = [];
|
|
152
|
+
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.`);
|
|
153
|
+
const args = [];
|
|
154
|
+
for (const input of metadata.inputs) {
|
|
155
|
+
const name = camelCase(input.langName);
|
|
156
|
+
if (input.kind === 'value') {
|
|
157
|
+
const type = syqlDartType(input.type, input.nullable);
|
|
158
|
+
if (input.required)
|
|
159
|
+
args.push(`required ${type} ${name}`);
|
|
160
|
+
else if (input.nullable) {
|
|
161
|
+
args.push(`SyqlQueryPresence<${type}> ${name} = const SyqlQueryPresence.absent()`);
|
|
162
|
+
}
|
|
163
|
+
else
|
|
164
|
+
args.push(`${type}? ${name}`);
|
|
165
|
+
}
|
|
166
|
+
else if (input.kind === 'group') {
|
|
167
|
+
args.push(`${typeName(query.name)}${typeName(input.langName)}? ${name}`);
|
|
168
|
+
}
|
|
169
|
+
else if (input.kind === 'switch') {
|
|
170
|
+
args.push(`bool ${name} = false`);
|
|
171
|
+
}
|
|
172
|
+
else if (input.kind === 'sort') {
|
|
173
|
+
const defaultCase = input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
174
|
+
?.langName ?? input.defaultProfile;
|
|
175
|
+
const type = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
176
|
+
args.push(`${type} ${name} = ${type}.${camelCase(defaultCase)}`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
args.push(`int? ${name}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const argsClause = args.length > 0 ? `, {${args.join(', ')}}` : '';
|
|
183
|
+
lines.push(`List<${Row}> syncular${Pascal}Query(SyncularClient client${argsClause}) {`);
|
|
184
|
+
const page = metadata.inputs.find((input) => input.kind === 'page');
|
|
185
|
+
if (page?.kind === 'page') {
|
|
186
|
+
const name = camelCase(page.langName);
|
|
187
|
+
lines.push(` final effective${typeName(name)} = ${name} ?? ${page.defaultSize};`, ` if (effective${typeName(name)} < 1 || effective${typeName(name)} > ${page.maxSize}) {`, ` throw SyqlQueryInputException('SYQL_RUNTIME_INVALID_PAGE', ${quote(`${query.name}: invalid page size`)});`, ' }');
|
|
188
|
+
}
|
|
189
|
+
if (metadata.plan.backend === 'variants') {
|
|
190
|
+
lines.push(' var activationMask = 0;');
|
|
191
|
+
metadata.plan.activationControls.forEach((control, index) => {
|
|
192
|
+
lines.push(` if (${syqlControlActive(query, control)}) activationMask |= ${2 ** index};`);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const sort = metadata.inputs.find((input) => input.kind === 'sort');
|
|
196
|
+
const profileCount = sort?.kind === 'sort' ? sort.profiles.length : 1;
|
|
197
|
+
const sortIndex = sort?.kind === 'sort' ? `${camelCase(sort.langName)}.index` : '0';
|
|
198
|
+
const index = metadata.plan.backend === 'variants'
|
|
199
|
+
? `activationMask * ${profileCount} + ${sortIndex}`
|
|
200
|
+
: sortIndex;
|
|
201
|
+
lines.push(` final statementIndex = ${index};`, ' late final String sql;', ' late final List<Object?> params;', ' switch (statementIndex) {');
|
|
202
|
+
metadata.plan.statements.forEach((statement, statementIndex) => {
|
|
203
|
+
const binds = statement.binds
|
|
204
|
+
.map((bind) => syqlBindExpr(query, bind))
|
|
205
|
+
.join(', ');
|
|
206
|
+
lines.push(` case ${statementIndex}: sql = ${quote(statement.positionalSql)}; params = <Object?>[${binds}]; break;`);
|
|
207
|
+
});
|
|
208
|
+
lines.push(" default: throw StateError('invalid generated SYQL statement index');", ' }', ` return client.query(sql, params: params).map(${Row}.fromRow).whereType<${Row}>().toList();`, '}');
|
|
87
209
|
return lines;
|
|
88
210
|
}
|
|
89
211
|
function emitClass(query) {
|
|
@@ -121,52 +243,30 @@ function emitClass(query) {
|
|
|
121
243
|
return lines;
|
|
122
244
|
}
|
|
123
245
|
function emitRunner(query) {
|
|
246
|
+
if (query.syql !== undefined)
|
|
247
|
+
return emitSyqlDartRunner(query);
|
|
124
248
|
const Row = `${typeName(query.name)}Row`;
|
|
125
249
|
const Pascal = pascalCase(query.name);
|
|
126
250
|
const lines = [];
|
|
127
251
|
lines.push(`/// Tables the ${query.name} query reads (exact invalidation set).`);
|
|
128
252
|
lines.push(`const List<String> syncular${Pascal}QueryTables = [${query.tables.map(quote).join(', ')}];`);
|
|
129
253
|
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
|
-
}
|
|
254
|
+
lines.push(`const String _${query.name}Sql = ${quote(query.positionalSql)};`);
|
|
136
255
|
lines.push('');
|
|
137
256
|
lines.push(`/// Run the ${query.name} named query (SELECT-only).`);
|
|
138
257
|
const args = [];
|
|
139
258
|
for (const p of query.params) {
|
|
140
259
|
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}`);
|
|
260
|
+
args.push(`required ${DART_TYPE[p.type]} ${name}`);
|
|
153
261
|
}
|
|
154
262
|
const argsClause = args.length > 0 ? `, {${args.join(', ')}}` : '';
|
|
155
263
|
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`;
|
|
264
|
+
const sqlRef = `_${query.name}Sql`;
|
|
163
265
|
if (query.params.length > 0) {
|
|
164
266
|
const binds = query.params
|
|
165
267
|
.map((p) => {
|
|
166
268
|
const name = camelCase(p.langName);
|
|
167
|
-
return
|
|
168
|
-
? optionalParamValue(p.type, name)
|
|
169
|
-
: paramValue(p.type, name);
|
|
269
|
+
return paramValue(p.type, name);
|
|
170
270
|
})
|
|
171
271
|
.join(', ');
|
|
172
272
|
lines.push(` final params = <Object?>[${binds}];`);
|
|
@@ -188,6 +288,24 @@ export function emitQueriesDartModule(queries, hash, irVersion) {
|
|
|
188
288
|
'',
|
|
189
289
|
"import 'package:syncular/syncular.dart';",
|
|
190
290
|
].join('\n'));
|
|
291
|
+
if (queries.some((query) => query.syql !== undefined)) {
|
|
292
|
+
parts.push([
|
|
293
|
+
'class SyqlQueryPresence<T> {',
|
|
294
|
+
' final bool isPresent;',
|
|
295
|
+
' final T? value;',
|
|
296
|
+
' const SyqlQueryPresence.absent() : isPresent = false, value = null;',
|
|
297
|
+
' const SyqlQueryPresence.present(this.value) : isPresent = true;',
|
|
298
|
+
'}',
|
|
299
|
+
'',
|
|
300
|
+
'class SyqlQueryInputException implements Exception {',
|
|
301
|
+
' final String code;',
|
|
302
|
+
' final String message;',
|
|
303
|
+
' const SyqlQueryInputException(this.code, this.message);',
|
|
304
|
+
' @override',
|
|
305
|
+
" String toString() => '$code: $message';",
|
|
306
|
+
'}',
|
|
307
|
+
].join('\n'));
|
|
308
|
+
}
|
|
191
309
|
parts.push([
|
|
192
310
|
'/// Lift a SQLite boolean: a real bool, or 0/1 as a number.',
|
|
193
311
|
'bool? _queryRowBool(Object? value) {',
|
|
@@ -213,17 +331,11 @@ export function emitQueriesDartModule(queries, hash, irVersion) {
|
|
|
213
331
|
" return {r'$bytes': hex};",
|
|
214
332
|
'}',
|
|
215
333
|
].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
334
|
for (const query of queries) {
|
|
335
|
+
const syqlTypes = emitSyqlDartTypes(query);
|
|
336
|
+
if (syqlTypes.length > 0)
|
|
337
|
+
parts.push(syqlTypes.join('\n'));
|
|
223
338
|
parts.push(emitClass(query).join('\n'));
|
|
224
|
-
const orderByEnum = emitOrderByEnum(query);
|
|
225
|
-
if (orderByEnum.length > 0)
|
|
226
|
-
parts.push(orderByEnum.join('\n'));
|
|
227
339
|
}
|
|
228
340
|
for (const query of queries) {
|
|
229
341
|
parts.push(emitRunner(query).join('\n'));
|