@syncular/typegen 0.6.0 → 0.8.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 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
- constructive `@scope`/`@cover` facts, complete sort profiles, bounded pages,
334
- and proven identity. Its complete normative definition is
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?(start: integer, end: integer),
351
+ range?,
352
352
  q?: string,
353
- unassigned?: switch,
353
+ unassigned: bool = false,
354
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
- }
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
- 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;
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 switch defaults false. Sort is a
384
- generated enum/union of complete checked profiles; page is validated as a
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. A `.syql` query constructs exact reactive facts in the same
506
- node that restricts the SQL:
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
- sql {
511
- select id, title from tasks
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
- `@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
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}: page 1..${input.maxSize} (default ${input.defaultSize})`);
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 === 'switch')
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 === 'page')
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 === 'page');
185
- if (page?.kind === 'page') {
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('SYQL_RUNTIME_INVALID_PAGE', ${quote(`${query.name}: invalid page size`)});`, ' }');
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 === 'switch')
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 === 'page')
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 === 'page');
190
- if (page?.kind === 'page') {
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("SYQL_RUNTIME_INVALID_PAGE", ${quote(`${query.name}: invalid page size`)})`, ' }');
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 === 'switch')
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 === 'page')
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 === 'page');
203
- if (page?.kind === 'page') {
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: "SYQL_RUNTIME_INVALID_PAGE", message: ${quote(`${query.name}: invalid page size`)})`, ' }');
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');
@@ -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
- return `String(params.${propertyKey(input.langName)})`;
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 === 'switch')
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 === 'page') {
91
- if (input.kind !== 'page')
92
- throw new Error('page bind/input mismatch');
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('SYQL_RUNTIME_INVALID_PAGE', ${quote(`${query.name}: page size must be an integer from 1 through ${input.maxSize}`)});`);
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
- " | 'SYQL_RUNTIME_INVALID_PAGE';",
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.sql.body,
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' || lower === 'predicate' || lower === 'query');
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 (importList && braceDepth === 0) {
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 === ',' && declarationParameters?.depth === parenDepth) {
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 === 'switch') {
85
- return `switch \`${parameter.name}\` — false/absent or true`;
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
- if (found.word === '@scope' || found.word === '@cover') {
337
- return {
338
- contents: {
339
- kind: 'markdown',
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.page?.control === found.word) {
373
+ if (query.limit?.control === found.word) {
386
374
  return {
387
375
  contents: {
388
376
  kind: 'markdown',
389
- value: `page control \`${found.word}\`: default ${query.page.defaultSize}, maximum ${query.page.maxSize}`,
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 || !request.found.word.startsWith('@'))
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.slice(1));
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 || !request.found.word.startsWith('@'))
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.slice(1));
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 !== 'at-identifier')
435
+ if (token.kind !== 'identifier')
448
436
  continue;
449
- const resolved = scope?.get(token.text.slice(1));
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
- /** Revision-1 SYQL public inputs. These are deliberately separate from SQL
71
- * binds: groups and switches are public values without corresponding SQLite
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: 'page';
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: 'page';
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 optional scalar/group/switch, in declaration order. */
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[];