@syncular/typegen 0.6.0 → 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 +47 -42
- package/dist/cli.js +1 -4
- package/dist/emit-queries-dart.js +9 -8
- package/dist/emit-queries-kotlin.js +9 -8
- package/dist/emit-queries-swift.js +9 -8
- package/dist/emit-queries.js +10 -13
- package/dist/fmt.js +29 -4
- package/dist/lsp.js +16 -28
- package/dist/query.d.ts +6 -11
- package/dist/query.js +29 -0
- package/dist/syql-ast.d.ts +12 -13
- package/dist/syql-ast.js +26 -20
- package/dist/syql-lowering.js +65 -42
- package/dist/syql-parser.d.ts +19 -18
- package/dist/syql-parser.js +341 -93
- package/dist/syql-semantics.d.ts +4 -7
- package/dist/syql-semantics.js +121 -65
- package/dist/syql-template-parser.d.ts +5 -20
- package/dist/syql-template-parser.js +116 -109
- package/dist/syql-validator.d.ts +2 -2
- package/dist/syql-validator.js +251 -224
- package/package.json +3 -3
- package/src/cli.ts +1 -3
- package/src/emit-queries-dart.ts +7 -7
- package/src/emit-queries-kotlin.ts +7 -7
- package/src/emit-queries-swift.ts +7 -7
- package/src/emit-queries.ts +9 -12
- package/src/fmt.ts +32 -4
- package/src/lsp.ts +16 -28
- package/src/query.ts +42 -12
- package/src/syql-ast.ts +38 -34
- package/src/syql-lowering.ts +69 -43
- package/src/syql-parser.ts +472 -134
- package/src/syql-semantics.ts +158 -87
- package/src/syql-template-parser.ts +119 -163
- package/src/syql-validator.ts +330 -336
package/README.md
CHANGED
|
@@ -330,8 +330,8 @@ Two frontends feed one QueryIR pipeline. **`.sql`** is plain SQL plus
|
|
|
330
330
|
`:params`, the compatibility floor documented below. **`.syql`** is the
|
|
331
331
|
revision-1 structured frontend: authoritative typed inputs, explicit
|
|
332
332
|
`when(...)` conjuncts, atomic optional groups, imported hygienic predicates,
|
|
333
|
-
|
|
334
|
-
and
|
|
333
|
+
inferred scope dependencies, explicit sync coverage, complete sort profiles,
|
|
334
|
+
bounded limits, and inferred identity. Its complete normative definition is
|
|
335
335
|
[`../../docs/SYQL.md`](../../docs/SYQL.md); the rationale and implementation
|
|
336
336
|
record are [`../../docs/rfcs/0004-syql-language.md`](../../docs/rfcs/0004-syql-language.md).
|
|
337
337
|
|
|
@@ -345,43 +345,32 @@ definitions, references, symbols, and formatting. Casing follows the manifest
|
|
|
345
345
|
```syql
|
|
346
346
|
import { matchesTitle } from "./predicates.syql";
|
|
347
347
|
|
|
348
|
-
query listTodos(
|
|
348
|
+
sync query listTodos(
|
|
349
349
|
listId,
|
|
350
350
|
status?: string | null,
|
|
351
|
-
range
|
|
351
|
+
range?,
|
|
352
352
|
q?: string,
|
|
353
|
-
unassigned
|
|
353
|
+
unassigned: bool = false,
|
|
354
354
|
) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
and when(unassigned) {
|
|
366
|
-
assignee_id is null
|
|
367
|
-
}
|
|
368
|
-
and when(q) {
|
|
369
|
-
@matchesTitle(:q)
|
|
370
|
-
}
|
|
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;
|
|
371
365
|
}
|
|
372
|
-
|
|
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;
|
|
366
|
+
limit pageSize default 50 max 200;
|
|
378
367
|
}
|
|
379
368
|
```
|
|
380
369
|
|
|
381
370
|
Optional nullable scalars use a generated presence wrapper so absent,
|
|
382
371
|
present-null, and present-value remain distinct. A group is one optional host
|
|
383
|
-
object whose members are all required. A
|
|
384
|
-
generated enum/union of complete checked profiles;
|
|
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
|
|
385
374
|
positive bounded integer before execution. `integer` inputs are exact signed
|
|
386
375
|
64-bit values in the SYQL API (`bigint` in TypeScript, `Int64`/`Long`/`int` on
|
|
387
376
|
native targets).
|
|
@@ -459,6 +448,27 @@ boundary):
|
|
|
459
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** |
|
|
460
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** |
|
|
461
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
|
+
|
|
462
472
|
`bun:sqlite` exposes `columnNames`, `declaredTypes` (once executed once — we
|
|
463
473
|
run the statement against the empty DB), and `paramsCount`. It does **not**
|
|
464
474
|
expose column origin (table/column), param **names**, or `NOT NULL` flags — so
|
|
@@ -502,24 +512,19 @@ identifier and keeping those that name an IR table. This captures subquery /
|
|
|
502
512
|
prepare() still guarantees the SQL itself is correct.
|
|
503
513
|
|
|
504
514
|
Inference is deliberately conservative around `OR`, joins, grouping, and
|
|
505
|
-
computed identity.
|
|
506
|
-
|
|
515
|
+
computed identity. Ordinary scope predicates construct exact reactive facts;
|
|
516
|
+
`sync query` additionally requests checked coverage:
|
|
507
517
|
|
|
508
518
|
```syql
|
|
509
|
-
query compareLists(left, right) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
where @cover(tasks.project_id in (:left, :right))
|
|
513
|
-
}
|
|
514
|
-
identity by id;
|
|
519
|
+
sync query compareLists(left, right) {
|
|
520
|
+
select id, title from tasks
|
|
521
|
+
where tasks.project_id in (:left, :right);
|
|
515
522
|
}
|
|
516
523
|
```
|
|
517
524
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
|
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
|
|
523
528
|
falls back to table-wide dependency, no coverage, and/or unkeyed reconciliation.
|
|
524
529
|
|
|
525
530
|
**Emitted shape, per language** (one query, five outputs — abbreviated):
|
package/dist/cli.js
CHANGED
|
@@ -121,14 +121,11 @@ function runGeneratePrint(args, name) {
|
|
|
121
121
|
else if (input.kind === 'group') {
|
|
122
122
|
console.log(`-- ${input.name}: optional group (${input.members.map((member) => `${member.name}: ${member.type}${member.nullable ? ' | null' : ''}`).join(', ')})`);
|
|
123
123
|
}
|
|
124
|
-
else if (input.kind === 'switch') {
|
|
125
|
-
console.log(`-- ${input.name}: switch (default false)`);
|
|
126
|
-
}
|
|
127
124
|
else if (input.kind === 'sort') {
|
|
128
125
|
console.log(`-- ${input.name}: sort [${input.profiles.map((profile) => profile.name).join(', ')}] (default ${input.defaultProfile})`);
|
|
129
126
|
}
|
|
130
127
|
else {
|
|
131
|
-
console.log(`-- ${input.name}:
|
|
128
|
+
console.log(`-- ${input.name}: limit 1..${input.maxSize} (default ${input.defaultSize})`);
|
|
132
129
|
}
|
|
133
130
|
}
|
|
134
131
|
}
|
|
@@ -77,7 +77,7 @@ function syqlDartType(type, nullable) {
|
|
|
77
77
|
function syqlControlActive(query, name) {
|
|
78
78
|
const input = syqlInput(query, name);
|
|
79
79
|
const access = camelCase(input.langName);
|
|
80
|
-
if (input.kind === '
|
|
80
|
+
if (input.kind === 'value' && input.default === false)
|
|
81
81
|
return access;
|
|
82
82
|
if (input.kind === 'value' && input.nullable)
|
|
83
83
|
return `${access}.isPresent`;
|
|
@@ -94,7 +94,7 @@ function syqlBindExpr(query, bind) {
|
|
|
94
94
|
}
|
|
95
95
|
const input = syqlInput(query, bind.input);
|
|
96
96
|
const access = camelCase(input.langName);
|
|
97
|
-
if (bind.kind === '
|
|
97
|
+
if (bind.kind === 'limit')
|
|
98
98
|
return `effective${typeName(access)}`;
|
|
99
99
|
if (bind.kind === 'group-member') {
|
|
100
100
|
if (input.kind !== 'group')
|
|
@@ -110,6 +110,8 @@ function syqlBindExpr(query, bind) {
|
|
|
110
110
|
}
|
|
111
111
|
if (input.kind !== 'value')
|
|
112
112
|
throw new Error('value bind/input mismatch');
|
|
113
|
+
if (input.default === false)
|
|
114
|
+
return paramValue(input.type, access);
|
|
113
115
|
if (input.required) {
|
|
114
116
|
return input.nullable
|
|
115
117
|
? optionalParamValue(input.type, access)
|
|
@@ -157,6 +159,8 @@ function emitSyqlDartRunner(query) {
|
|
|
157
159
|
const type = syqlDartType(input.type, input.nullable);
|
|
158
160
|
if (input.required)
|
|
159
161
|
args.push(`required ${type} ${name}`);
|
|
162
|
+
else if (input.default === false)
|
|
163
|
+
args.push(`bool ${name} = false`);
|
|
160
164
|
else if (input.nullable) {
|
|
161
165
|
args.push(`SyqlQueryPresence<${type}> ${name} = const SyqlQueryPresence.absent()`);
|
|
162
166
|
}
|
|
@@ -166,9 +170,6 @@ function emitSyqlDartRunner(query) {
|
|
|
166
170
|
else if (input.kind === 'group') {
|
|
167
171
|
args.push(`${typeName(query.name)}${typeName(input.langName)}? ${name}`);
|
|
168
172
|
}
|
|
169
|
-
else if (input.kind === 'switch') {
|
|
170
|
-
args.push(`bool ${name} = false`);
|
|
171
|
-
}
|
|
172
173
|
else if (input.kind === 'sort') {
|
|
173
174
|
const defaultCase = input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
174
175
|
?.langName ?? input.defaultProfile;
|
|
@@ -181,10 +182,10 @@ function emitSyqlDartRunner(query) {
|
|
|
181
182
|
}
|
|
182
183
|
const argsClause = args.length > 0 ? `, {${args.join(', ')}}` : '';
|
|
183
184
|
lines.push(`List<${Row}> syncular${Pascal}Query(SyncularClient client${argsClause}) {`);
|
|
184
|
-
const page = metadata.inputs.find((input) => input.kind === '
|
|
185
|
-
if (page?.kind === '
|
|
185
|
+
const page = metadata.inputs.find((input) => input.kind === 'limit');
|
|
186
|
+
if (page?.kind === 'limit') {
|
|
186
187
|
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('
|
|
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`)});`, ' }');
|
|
188
189
|
}
|
|
189
190
|
if (metadata.plan.backend === 'variants') {
|
|
190
191
|
lines.push(' var activationMask = 0;');
|
|
@@ -78,7 +78,7 @@ function syqlKotlinType(type, nullable) {
|
|
|
78
78
|
function syqlControlActive(query, name) {
|
|
79
79
|
const input = syqlInput(query, name);
|
|
80
80
|
const access = camelCase(input.langName);
|
|
81
|
-
if (input.kind === '
|
|
81
|
+
if (input.kind === 'value' && input.default === false)
|
|
82
82
|
return access;
|
|
83
83
|
if (input.kind === 'value' && input.nullable) {
|
|
84
84
|
return `${access} is SyncularQueryPresence.Present`;
|
|
@@ -100,7 +100,7 @@ function syqlBindExpr(query, bind) {
|
|
|
100
100
|
}
|
|
101
101
|
const input = syqlInput(query, bind.input);
|
|
102
102
|
const access = camelCase(input.langName);
|
|
103
|
-
if (bind.kind === '
|
|
103
|
+
if (bind.kind === 'limit')
|
|
104
104
|
return `JsonValue.of(effective${typeName(access)}.toDouble())`;
|
|
105
105
|
if (bind.kind === 'group-member') {
|
|
106
106
|
if (input.kind !== 'group')
|
|
@@ -116,6 +116,8 @@ function syqlBindExpr(query, bind) {
|
|
|
116
116
|
}
|
|
117
117
|
if (input.kind !== 'value')
|
|
118
118
|
throw new Error('value bind/input mismatch');
|
|
119
|
+
if (input.default === false)
|
|
120
|
+
return paramValue(input.type, access);
|
|
119
121
|
if (input.required) {
|
|
120
122
|
return input.nullable
|
|
121
123
|
? optionalParamValue(input.type, access)
|
|
@@ -163,6 +165,8 @@ function emitSyqlKotlinRunner(query) {
|
|
|
163
165
|
const type = syqlKotlinType(input.type, input.nullable);
|
|
164
166
|
if (input.required)
|
|
165
167
|
args.push(`${name}: ${type}`);
|
|
168
|
+
else if (input.default === false)
|
|
169
|
+
args.push(`${name}: Boolean = false`);
|
|
166
170
|
else if (input.nullable) {
|
|
167
171
|
args.push(`${name}: SyncularQueryPresence<${type}> = SyncularQueryPresence.Absent`);
|
|
168
172
|
}
|
|
@@ -172,9 +176,6 @@ function emitSyqlKotlinRunner(query) {
|
|
|
172
176
|
else if (input.kind === 'group') {
|
|
173
177
|
args.push(`${name}: ${typeName(query.name)}${typeName(input.langName)}? = null`);
|
|
174
178
|
}
|
|
175
|
-
else if (input.kind === 'switch') {
|
|
176
|
-
args.push(`${name}: Boolean = false`);
|
|
177
|
-
}
|
|
178
179
|
else if (input.kind === 'sort') {
|
|
179
180
|
const defaultCase = input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
180
181
|
?.langName ?? input.defaultProfile;
|
|
@@ -186,10 +187,10 @@ function emitSyqlKotlinRunner(query) {
|
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
lines.push(` fun ${query.name}(${args.join(', ')}): List<${Row}> {`);
|
|
189
|
-
const page = metadata.inputs.find((input) => input.kind === '
|
|
190
|
-
if (page?.kind === '
|
|
190
|
+
const page = metadata.inputs.find((input) => input.kind === 'limit');
|
|
191
|
+
if (page?.kind === 'limit') {
|
|
191
192
|
const name = camelCase(page.langName);
|
|
192
|
-
lines.push(` val effective${typeName(name)} = ${name} ?: ${page.defaultSize}L`, ` if (effective${typeName(name)} < 1L || effective${typeName(name)} > ${page.maxSize}L) {`, ` throw SyncularQueryInputException("
|
|
193
|
+
lines.push(` val effective${typeName(name)} = ${name} ?: ${page.defaultSize}L`, ` if (effective${typeName(name)} < 1L || effective${typeName(name)} > ${page.maxSize}L) {`, ` throw SyncularQueryInputException("SYQL_RUNTIME_INVALID_LIMIT", ${quote(`${query.name}: invalid limit`)})`, ' }');
|
|
193
194
|
}
|
|
194
195
|
if (metadata.plan.backend === 'variants') {
|
|
195
196
|
lines.push(' var activationMask = 0');
|
|
@@ -85,7 +85,7 @@ function syqlSwiftType(type, nullable) {
|
|
|
85
85
|
function syqlControlActive(query, name) {
|
|
86
86
|
const input = syqlInput(query, name);
|
|
87
87
|
const access = camelCase(input.langName);
|
|
88
|
-
if (input.kind === '
|
|
88
|
+
if (input.kind === 'value' && input.default === false)
|
|
89
89
|
return access;
|
|
90
90
|
if (input.kind === 'value' && input.nullable) {
|
|
91
91
|
return `{ if case .present = ${access} { return true }; return false }()`;
|
|
@@ -107,7 +107,7 @@ function syqlBindExpr(query, bind) {
|
|
|
107
107
|
}
|
|
108
108
|
const input = syqlInput(query, bind.input);
|
|
109
109
|
const access = camelCase(input.langName);
|
|
110
|
-
if (bind.kind === '
|
|
110
|
+
if (bind.kind === 'limit')
|
|
111
111
|
return `.number(Double(effective${typeName(access)}))`;
|
|
112
112
|
if (bind.kind === 'group-member') {
|
|
113
113
|
if (input.kind !== 'group')
|
|
@@ -123,6 +123,8 @@ function syqlBindExpr(query, bind) {
|
|
|
123
123
|
}
|
|
124
124
|
if (input.kind !== 'value')
|
|
125
125
|
throw new Error('value bind/input mismatch');
|
|
126
|
+
if (input.default === false)
|
|
127
|
+
return paramValue(input.type, access);
|
|
126
128
|
if (input.required) {
|
|
127
129
|
return input.nullable
|
|
128
130
|
? optionalParamValue(input.type, access)
|
|
@@ -178,6 +180,8 @@ function emitSyqlSwiftRunner(query) {
|
|
|
178
180
|
const type = syqlSwiftType(input.type, input.nullable);
|
|
179
181
|
if (input.required)
|
|
180
182
|
args.push(`${name}: ${type}`);
|
|
183
|
+
else if (input.default === false)
|
|
184
|
+
args.push(`${name}: Bool = false`);
|
|
181
185
|
else if (input.nullable)
|
|
182
186
|
args.push(`${name}: SyncularQueryPresence<${type}> = .absent`);
|
|
183
187
|
else
|
|
@@ -186,9 +190,6 @@ function emitSyqlSwiftRunner(query) {
|
|
|
186
190
|
else if (input.kind === 'group') {
|
|
187
191
|
args.push(`${name}: ${typeName(query.name)}${typeName(input.langName)}? = nil`);
|
|
188
192
|
}
|
|
189
|
-
else if (input.kind === 'switch') {
|
|
190
|
-
args.push(`${name}: Bool = false`);
|
|
191
|
-
}
|
|
192
193
|
else if (input.kind === 'sort') {
|
|
193
194
|
const defaultCase = input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
194
195
|
?.langName ?? input.defaultProfile;
|
|
@@ -199,10 +200,10 @@ function emitSyqlSwiftRunner(query) {
|
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
lines.push(` public static func ${query.name}(${args.join(', ')}) throws -> [${Row}] {`);
|
|
202
|
-
const page = metadata.inputs.find((input) => input.kind === '
|
|
203
|
-
if (page?.kind === '
|
|
203
|
+
const page = metadata.inputs.find((input) => input.kind === 'limit');
|
|
204
|
+
if (page?.kind === 'limit') {
|
|
204
205
|
const name = camelCase(page.langName);
|
|
205
|
-
lines.push(` let effective${typeName(name)} = ${name} ?? ${page.defaultSize}`, ` guard effective${typeName(name)} >= 1 && effective${typeName(name)} <= ${page.maxSize} else {`, ` throw SyncularQueryInputError(code: "
|
|
206
|
+
lines.push(` let effective${typeName(name)} = ${name} ?? ${page.defaultSize}`, ` guard effective${typeName(name)} >= 1 && effective${typeName(name)} <= ${page.maxSize} else {`, ` throw SyncularQueryInputError(code: "SYQL_RUNTIME_INVALID_LIMIT", message: ${quote(`${query.name}: invalid limit`)})`, ' }');
|
|
206
207
|
}
|
|
207
208
|
if (metadata.plan.backend === 'variants') {
|
|
208
209
|
lines.push(' var activationMask = 0');
|
package/dist/emit-queries.js
CHANGED
|
@@ -32,7 +32,8 @@ function reactiveParam(query, name) {
|
|
|
32
32
|
const input = query.syql.inputs.find((candidate) => candidate.kind === 'value' && candidate.name === name);
|
|
33
33
|
if (input?.kind !== 'value')
|
|
34
34
|
throw new Error(`unknown reactive input ${name}`);
|
|
35
|
-
|
|
35
|
+
const access = `params.${propertyKey(input.langName)}`;
|
|
36
|
+
return `String(${input.default === false ? `${access} ?? false` : access})`;
|
|
36
37
|
}
|
|
37
38
|
const param = query.params.find((candidate) => candidate.name === name);
|
|
38
39
|
if (param === undefined)
|
|
@@ -72,7 +73,7 @@ function syqlTsTypeCheck(expression, type, nullable) {
|
|
|
72
73
|
function syqlControlActive(query, control, params) {
|
|
73
74
|
const input = syqlInput(query, control);
|
|
74
75
|
const access = `${params}.${propertyKey(input.langName)}`;
|
|
75
|
-
if (input.kind === '
|
|
76
|
+
if (input.kind === 'value' && input.default === false)
|
|
76
77
|
return `${access} === true`;
|
|
77
78
|
if (input.kind === 'value' || input.kind === 'group') {
|
|
78
79
|
return `${access} !== undefined`;
|
|
@@ -87,9 +88,9 @@ function syqlBindExpr(query, bind, params) {
|
|
|
87
88
|
}
|
|
88
89
|
const input = syqlInput(query, bind.input);
|
|
89
90
|
const access = `${params}.${propertyKey(input.langName)}`;
|
|
90
|
-
if (bind.kind === '
|
|
91
|
-
if (input.kind !== '
|
|
92
|
-
throw new Error('
|
|
91
|
+
if (bind.kind === 'limit') {
|
|
92
|
+
if (input.kind !== 'limit')
|
|
93
|
+
throw new Error('limit bind/input mismatch');
|
|
93
94
|
return `${access} ?? ${input.defaultSize}`;
|
|
94
95
|
}
|
|
95
96
|
if (bind.kind === 'group-member') {
|
|
@@ -102,6 +103,8 @@ function syqlBindExpr(query, bind, params) {
|
|
|
102
103
|
}
|
|
103
104
|
if (input.kind !== 'value')
|
|
104
105
|
throw new Error('value bind/input mismatch');
|
|
106
|
+
if (input.default === false)
|
|
107
|
+
return `${access} ?? false`;
|
|
105
108
|
if (input.required)
|
|
106
109
|
return access;
|
|
107
110
|
return input.nullable ? `${access}?.value ?? null` : `${access} ?? null`;
|
|
@@ -132,14 +135,11 @@ function emitSyqlValidation(query, Params) {
|
|
|
132
135
|
}
|
|
133
136
|
lines.push(' }');
|
|
134
137
|
}
|
|
135
|
-
else if (input.kind === 'switch') {
|
|
136
|
-
lines.push(` if (${access} !== undefined && typeof ${access} !== 'boolean') throw new SyqlInputError('SYQL_RUNTIME_INVALID_INPUT', ${quote(`${query.name}: invalid switch ${input.name}`)});`);
|
|
137
|
-
}
|
|
138
138
|
else if (input.kind === 'sort') {
|
|
139
139
|
lines.push(` if (${access} !== undefined && ![${input.profiles.map((profile) => quote(profile.langName)).join(', ')}].includes(${access})) throw new SyqlInputError('SYQL_RUNTIME_INVALID_SORT', ${quote(`${query.name}: invalid sort profile`)});`);
|
|
140
140
|
}
|
|
141
141
|
else {
|
|
142
|
-
lines.push(` if (${access} !== undefined && (!Number.isSafeInteger(${access}) || ${access} < 1 || ${access} > ${input.maxSize})) throw new SyqlInputError('
|
|
142
|
+
lines.push(` if (${access} !== undefined && (!Number.isSafeInteger(${access}) || ${access} < 1 || ${access} > ${input.maxSize})) throw new SyqlInputError('SYQL_RUNTIME_INVALID_LIMIT', ${quote(`${query.name}: limit must be an integer from 1 through ${input.maxSize}`)});`);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
lines.push(' return params;', '}');
|
|
@@ -183,9 +183,6 @@ function emitSyqlQuery(query, hash) {
|
|
|
183
183
|
else if (input.kind === 'group') {
|
|
184
184
|
lines.push(` ${key}?: ${pascalCase(query.name)}${pascalCase(input.langName)};`);
|
|
185
185
|
}
|
|
186
|
-
else if (input.kind === 'switch') {
|
|
187
|
-
lines.push(` ${key}?: boolean;`);
|
|
188
|
-
}
|
|
189
186
|
else if (input.kind === 'sort') {
|
|
190
187
|
lines.push(` ${key}?: ${input.profiles.map((profile) => quote(profile.langName)).join(' | ')};`);
|
|
191
188
|
}
|
|
@@ -382,7 +379,7 @@ export function emitQueriesModule(queries, hash, irVersion) {
|
|
|
382
379
|
" | 'SYQL_RUNTIME_INVALID_INPUT'",
|
|
383
380
|
" | 'SYQL_RUNTIME_INVALID_GROUP'",
|
|
384
381
|
" | 'SYQL_RUNTIME_INVALID_SORT'",
|
|
385
|
-
" | '
|
|
382
|
+
" | 'SYQL_RUNTIME_INVALID_LIMIT';",
|
|
386
383
|
'',
|
|
387
384
|
'export class SyqlInputError extends Error {',
|
|
388
385
|
" readonly name = 'SyqlInputError';",
|
package/dist/fmt.js
CHANGED
|
@@ -42,7 +42,7 @@ function templates(file) {
|
|
|
42
42
|
if (declaration.kind === 'predicate')
|
|
43
43
|
return [declaration.body];
|
|
44
44
|
return [
|
|
45
|
-
declaration.
|
|
45
|
+
declaration.statement,
|
|
46
46
|
...(declaration.sort?.profiles.map((profile) => profile.order) ?? []),
|
|
47
47
|
];
|
|
48
48
|
});
|
|
@@ -218,6 +218,8 @@ function formatTokens(parsed) {
|
|
|
218
218
|
let justClosedImportList = false;
|
|
219
219
|
let seenTopLevel = false;
|
|
220
220
|
let pendingBetween = 0;
|
|
221
|
+
let inlineParameterRecord = false;
|
|
222
|
+
let justClosedInlineParameterRecord = false;
|
|
221
223
|
let declarationParameters;
|
|
222
224
|
let awaitsDeclarationParameters = false;
|
|
223
225
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
@@ -226,7 +228,11 @@ function formatTokens(parsed) {
|
|
|
226
228
|
const template = inTemplate(ranges, token);
|
|
227
229
|
const topLevelDeclaration = braceDepth === 0 &&
|
|
228
230
|
token.kind === 'identifier' &&
|
|
229
|
-
(lower === 'import' ||
|
|
231
|
+
(lower === 'import' ||
|
|
232
|
+
lower === 'predicate' ||
|
|
233
|
+
lower === 'query' ||
|
|
234
|
+
lower === 'sync') &&
|
|
235
|
+
!(lower === 'query' && previous?.text === 'sync');
|
|
230
236
|
if (topLevelDeclaration) {
|
|
231
237
|
if (seenTopLevel)
|
|
232
238
|
writer.blankline();
|
|
@@ -234,8 +240,14 @@ function formatTokens(parsed) {
|
|
|
234
240
|
previous = undefined;
|
|
235
241
|
awaitsDeclarationParameters = lower === 'predicate' || lower === 'query';
|
|
236
242
|
}
|
|
243
|
+
else if (braceDepth === 0 &&
|
|
244
|
+
lower === 'query' &&
|
|
245
|
+
previous?.text === 'sync') {
|
|
246
|
+
awaitsDeclarationParameters = true;
|
|
247
|
+
}
|
|
237
248
|
else if (previous?.text === '}' &&
|
|
238
249
|
!justClosedImportList &&
|
|
250
|
+
!justClosedInlineParameterRecord &&
|
|
239
251
|
token.text !== ';' &&
|
|
240
252
|
token.text !== ',') {
|
|
241
253
|
writer.newline();
|
|
@@ -271,6 +283,11 @@ function formatTokens(parsed) {
|
|
|
271
283
|
writer.write('{', true);
|
|
272
284
|
importList = true;
|
|
273
285
|
}
|
|
286
|
+
else if (declarationParameters !== undefined &&
|
|
287
|
+
previous?.text === ':') {
|
|
288
|
+
writer.write('{', true);
|
|
289
|
+
inlineParameterRecord = true;
|
|
290
|
+
}
|
|
274
291
|
else {
|
|
275
292
|
writer.openBlock();
|
|
276
293
|
braceDepth += 1;
|
|
@@ -279,7 +296,12 @@ function formatTokens(parsed) {
|
|
|
279
296
|
continue;
|
|
280
297
|
}
|
|
281
298
|
if (token.text === '}') {
|
|
282
|
-
if (
|
|
299
|
+
if (inlineParameterRecord) {
|
|
300
|
+
writer.write('}', true);
|
|
301
|
+
inlineParameterRecord = false;
|
|
302
|
+
justClosedInlineParameterRecord = true;
|
|
303
|
+
}
|
|
304
|
+
else if (importList && braceDepth === 0) {
|
|
283
305
|
writer.write('}', true);
|
|
284
306
|
importList = false;
|
|
285
307
|
justClosedImportList = true;
|
|
@@ -291,6 +313,7 @@ function formatTokens(parsed) {
|
|
|
291
313
|
previous = token;
|
|
292
314
|
continue;
|
|
293
315
|
}
|
|
316
|
+
justClosedInlineParameterRecord = false;
|
|
294
317
|
justClosedImportList = false;
|
|
295
318
|
if (token.text === '(') {
|
|
296
319
|
parenDepth += 1;
|
|
@@ -308,7 +331,9 @@ function formatTokens(parsed) {
|
|
|
308
331
|
previous = token;
|
|
309
332
|
continue;
|
|
310
333
|
}
|
|
311
|
-
if (token.text === ',' &&
|
|
334
|
+
if (token.text === ',' &&
|
|
335
|
+
declarationParameters?.depth === parenDepth &&
|
|
336
|
+
!inlineParameterRecord) {
|
|
312
337
|
const next = tokens[index + 1];
|
|
313
338
|
if (!(declarationParameters.multiline === false && next?.text === ')')) {
|
|
314
339
|
writer.write(',', false);
|
package/dist/lsp.js
CHANGED
|
@@ -78,18 +78,18 @@ function spanRange(text, span) {
|
|
|
78
78
|
function typeText(type) {
|
|
79
79
|
return type === undefined
|
|
80
80
|
? 'inferred'
|
|
81
|
-
: `${type.base}${type.nullable ? ' | null' : ''}`;
|
|
81
|
+
: `${type.base === 'boolean' ? 'bool' : type.base}${type.nullable ? ' | null' : ''}`;
|
|
82
82
|
}
|
|
83
83
|
function parameterHover(parameter) {
|
|
84
|
-
if (parameter.kind === '
|
|
85
|
-
return
|
|
84
|
+
if (parameter.kind === 'range') {
|
|
85
|
+
return `${parameter.optional ? 'optional' : 'required'} inclusive range \`${parameter.name}: range<${typeText(parameter.type)}>\``;
|
|
86
86
|
}
|
|
87
87
|
if (parameter.kind === 'group') {
|
|
88
88
|
return `optional group \`${parameter.name}\`\n\n${parameter.members
|
|
89
89
|
.map((member) => `- \`${member.name}: ${typeText(member.type)}\``)
|
|
90
90
|
.join('\n')}`;
|
|
91
91
|
}
|
|
92
|
-
return `${parameter.optional ? 'optional' : 'required'} input \`${parameter.name}: ${typeText(parameter.type)}\``;
|
|
92
|
+
return `${parameter.optional ? 'optional' : parameter.default === false ? 'default-false' : 'required'} input \`${parameter.name}: ${typeText(parameter.type)}\``;
|
|
93
93
|
}
|
|
94
94
|
export class SyqlLanguageServer {
|
|
95
95
|
#documents = new Map();
|
|
@@ -333,22 +333,10 @@ export class SyqlLanguageServer {
|
|
|
333
333
|
if (request === null)
|
|
334
334
|
return null;
|
|
335
335
|
const { found, view, offset } = request;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
value: found.word === '@scope'
|
|
341
|
-
? '`@scope` constructs exact reactive dependency keys and the same SQL predicate.'
|
|
342
|
-
: '`@cover` constructs exact dependencies plus checked window coverage.',
|
|
343
|
-
},
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
if (found.word.startsWith('@')) {
|
|
347
|
-
const predicate = view.semantic.predicateScopes
|
|
348
|
-
.get(view.file)
|
|
349
|
-
?.get(found.word.slice(1));
|
|
350
|
-
if (predicate === undefined)
|
|
351
|
-
return null;
|
|
336
|
+
const predicate = view.semantic.predicateScopes
|
|
337
|
+
.get(view.file)
|
|
338
|
+
?.get(found.word);
|
|
339
|
+
if (predicate !== undefined) {
|
|
352
340
|
return {
|
|
353
341
|
contents: {
|
|
354
342
|
kind: 'markdown',
|
|
@@ -382,11 +370,11 @@ export class SyqlLanguageServer {
|
|
|
382
370
|
},
|
|
383
371
|
};
|
|
384
372
|
}
|
|
385
|
-
if (query.
|
|
373
|
+
if (query.limit?.control === found.word) {
|
|
386
374
|
return {
|
|
387
375
|
contents: {
|
|
388
376
|
kind: 'markdown',
|
|
389
|
-
value: `
|
|
377
|
+
value: `limit control \`${found.word}\`: default ${query.limit.defaultSize}, maximum ${query.limit.maxSize}`,
|
|
390
378
|
},
|
|
391
379
|
};
|
|
392
380
|
}
|
|
@@ -418,11 +406,11 @@ export class SyqlLanguageServer {
|
|
|
418
406
|
}
|
|
419
407
|
#definition(params) {
|
|
420
408
|
const request = this.#request(params);
|
|
421
|
-
if (request === null
|
|
409
|
+
if (request === null)
|
|
422
410
|
return null;
|
|
423
411
|
const predicate = request.view.semantic.predicateScopes
|
|
424
412
|
.get(request.view.file)
|
|
425
|
-
?.get(request.found.word
|
|
413
|
+
?.get(request.found.word);
|
|
426
414
|
if (predicate === undefined)
|
|
427
415
|
return null;
|
|
428
416
|
const text = this.#openText(predicate.module.file) ?? predicate.module.source;
|
|
@@ -433,20 +421,20 @@ export class SyqlLanguageServer {
|
|
|
433
421
|
}
|
|
434
422
|
#references(params) {
|
|
435
423
|
const request = this.#request(params);
|
|
436
|
-
if (request === null
|
|
424
|
+
if (request === null)
|
|
437
425
|
return [];
|
|
438
426
|
const target = request.view.semantic.predicateScopes
|
|
439
427
|
.get(request.view.file)
|
|
440
|
-
?.get(request.found.word
|
|
428
|
+
?.get(request.found.word);
|
|
441
429
|
if (target === undefined)
|
|
442
430
|
return [];
|
|
443
431
|
const locations = [];
|
|
444
432
|
for (const module of request.view.semantic.graph.modules) {
|
|
445
433
|
const scope = request.view.semantic.predicateScopes.get(module.file);
|
|
446
434
|
for (const token of module.tokens) {
|
|
447
|
-
if (token.kind !== '
|
|
435
|
+
if (token.kind !== 'identifier')
|
|
448
436
|
continue;
|
|
449
|
-
const resolved = scope?.get(token.text
|
|
437
|
+
const resolved = scope?.get(token.text);
|
|
450
438
|
if (resolved?.id !== target.id)
|
|
451
439
|
continue;
|
|
452
440
|
locations.push({
|
package/dist/query.d.ts
CHANGED
|
@@ -67,9 +67,8 @@ export interface QueryNamingOptions {
|
|
|
67
67
|
* not participate in public target-language keyword/private-name checks. */
|
|
68
68
|
readonly internalParams?: readonly string[];
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
71
|
-
*
|
|
72
|
-
* parameters, while compiler-generated parameters are never public. */
|
|
70
|
+
/** SYQL public inputs. These are deliberately separate from SQL binds: groups
|
|
71
|
+
* and compiler-generated parameters have distinct public/runtime shapes. */
|
|
73
72
|
export type QuerySyqlPublicInput = {
|
|
74
73
|
readonly kind: 'value';
|
|
75
74
|
readonly name: string;
|
|
@@ -77,6 +76,7 @@ export type QuerySyqlPublicInput = {
|
|
|
77
76
|
readonly type: QueryParamType;
|
|
78
77
|
readonly nullable: boolean;
|
|
79
78
|
readonly required: boolean;
|
|
79
|
+
readonly default?: false;
|
|
80
80
|
} | {
|
|
81
81
|
readonly kind: 'group';
|
|
82
82
|
readonly name: string;
|
|
@@ -87,11 +87,6 @@ export type QuerySyqlPublicInput = {
|
|
|
87
87
|
readonly type: QueryParamType;
|
|
88
88
|
readonly nullable: boolean;
|
|
89
89
|
}[];
|
|
90
|
-
} | {
|
|
91
|
-
readonly kind: 'switch';
|
|
92
|
-
readonly name: string;
|
|
93
|
-
readonly langName: string;
|
|
94
|
-
readonly default: false;
|
|
95
90
|
} | {
|
|
96
91
|
readonly kind: 'sort';
|
|
97
92
|
readonly name: string;
|
|
@@ -102,7 +97,7 @@ export type QuerySyqlPublicInput = {
|
|
|
102
97
|
readonly langName: string;
|
|
103
98
|
}[];
|
|
104
99
|
} | {
|
|
105
|
-
readonly kind: '
|
|
100
|
+
readonly kind: 'limit';
|
|
106
101
|
readonly name: string;
|
|
107
102
|
readonly langName: string;
|
|
108
103
|
readonly defaultSize: number;
|
|
@@ -127,7 +122,7 @@ export type QuerySyqlPlanBind = {
|
|
|
127
122
|
readonly condition: number;
|
|
128
123
|
readonly controls: readonly string[];
|
|
129
124
|
} | {
|
|
130
|
-
readonly kind: '
|
|
125
|
+
readonly kind: 'limit';
|
|
131
126
|
readonly name: string;
|
|
132
127
|
readonly type: 'integer';
|
|
133
128
|
readonly input: string;
|
|
@@ -144,7 +139,7 @@ export interface QuerySyqlStatement {
|
|
|
144
139
|
/** The target-neutral physical plan every emitter must implement exactly. */
|
|
145
140
|
export interface QuerySyqlExecutionPlan {
|
|
146
141
|
readonly backend: Exclude<QueryBackend, 'auto'>;
|
|
147
|
-
/** One bit per
|
|
142
|
+
/** One bit per conditional activation control, in declaration order. */
|
|
148
143
|
readonly activationControls: readonly string[];
|
|
149
144
|
readonly conditions: readonly {
|
|
150
145
|
readonly controls: readonly string[];
|