@syncular/typegen 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +79 -32
  2. package/dist/cli.js +40 -17
  3. package/dist/emit-queries-dart.js +168 -55
  4. package/dist/emit-queries-kotlin.js +167 -55
  5. package/dist/emit-queries-swift.js +183 -57
  6. package/dist/emit-queries.js +286 -123
  7. package/dist/fmt.d.ts +2 -2
  8. package/dist/fmt.js +348 -316
  9. package/dist/generate.d.ts +1 -1
  10. package/dist/generate.js +28 -9
  11. package/dist/index.d.ts +3 -1
  12. package/dist/index.js +3 -1
  13. package/dist/lsp.d.ts +1 -3
  14. package/dist/lsp.js +349 -187
  15. package/dist/manifest.d.ts +1 -3
  16. package/dist/manifest.js +1 -1
  17. package/dist/query-ir.js +53 -42
  18. package/dist/query.d.ts +110 -63
  19. package/dist/query.js +89 -11
  20. package/dist/syql-ast.d.ts +12 -13
  21. package/dist/syql-ast.js +26 -20
  22. package/dist/syql-lexer.d.ts +2 -0
  23. package/dist/syql-lexer.js +9 -2
  24. package/dist/syql-lowering.d.ts +22 -0
  25. package/dist/syql-lowering.js +434 -0
  26. package/dist/syql-parser.d.ts +19 -18
  27. package/dist/syql-parser.js +341 -93
  28. package/dist/syql-semantics.d.ts +73 -0
  29. package/dist/syql-semantics.js +607 -0
  30. package/dist/syql-template-parser.d.ts +5 -20
  31. package/dist/syql-template-parser.js +116 -109
  32. package/dist/syql-validator.d.ts +35 -0
  33. package/dist/syql-validator.js +993 -0
  34. package/package.json +3 -3
  35. package/src/cli.ts +49 -21
  36. package/src/emit-queries-dart.ts +195 -69
  37. package/src/emit-queries-kotlin.ts +197 -69
  38. package/src/emit-queries-swift.ts +224 -68
  39. package/src/emit-queries.ts +410 -165
  40. package/src/fmt.ts +405 -303
  41. package/src/generate.ts +43 -9
  42. package/src/index.ts +3 -1
  43. package/src/lsp.ts +414 -216
  44. package/src/manifest.ts +2 -4
  45. package/src/query-ir.ts +52 -42
  46. package/src/query.ts +229 -79
  47. package/src/syql-ast.ts +38 -34
  48. package/src/syql-lexer.ts +12 -1
  49. package/src/syql-lowering.ts +664 -0
  50. package/src/syql-parser.ts +472 -134
  51. package/src/syql-semantics.ts +930 -0
  52. package/src/syql-template-parser.ts +119 -163
  53. package/src/syql-validator.ts +1486 -0
  54. package/dist/syql.d.ts +0 -102
  55. package/dist/syql.js +0 -1141
  56. package/src/syql.ts +0 -1470
@@ -77,9 +77,7 @@ export interface Manifest {
77
77
  * generated row types/params (queries lower with `AS` aliases); "preserve"
78
78
  * keeps SQL-truth names everywhere. */
79
79
  readonly naming: NamingMode;
80
- /** §7 `.syql` backend: "neutralize" (default), "variants" (enumerate
81
- * whenever eligible), or "auto" (enumerate at ≤ 2 optional groups). The
82
- * per-query `variants` knob always forces enumeration. */
80
+ /** §7 `.syql` backend: "auto" (default), "neutralize", or "variants". */
83
81
  readonly queryBackend: ManifestQueryBackend;
84
82
  readonly output: ManifestOutput;
85
83
  readonly schemaVersions: readonly ManifestSchemaVersion[];
package/dist/manifest.js CHANGED
@@ -232,7 +232,7 @@ export function parseManifest(raw) {
232
232
  }
233
233
  naming = obj.naming;
234
234
  }
235
- let queryBackend = 'neutralize';
235
+ let queryBackend = 'auto';
236
236
  if (obj.queryBackend !== undefined) {
237
237
  if (obj.queryBackend !== 'neutralize' &&
238
238
  obj.queryBackend !== 'variants' &&
package/dist/query-ir.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Serialize analyzed queries as the deterministic QueryIR JSON document. */
2
2
  export function serializeQueryIr(queries) {
3
3
  const doc = {
4
- queryIrVersion: 2,
4
+ queryIrVersion: 3,
5
5
  queries: queries.map((query) => ({
6
6
  name: query.name,
7
7
  file: query.file,
@@ -12,11 +12,6 @@ export function serializeQueryIr(queries) {
12
12
  name: param.name,
13
13
  langName: param.langName,
14
14
  type: param.type,
15
- // §4 metadata — emitted only when set, so `.sql`-tier IR bytes are
16
- // unchanged from before the DSL existed.
17
- ...(param.optional === true ? { optional: true } : {}),
18
- ...(param.group !== undefined ? { group: param.group } : {}),
19
- ...(param.flag === true ? { flag: true } : {}),
20
15
  })),
21
16
  columns: query.columns.map((column) => ({
22
17
  name: column.name,
@@ -49,43 +44,59 @@ export function serializeQueryIr(queries) {
49
44
  ? { rowKey: query.reactive.rowKey }
50
45
  : {}),
51
46
  },
52
- // §6 knob metadata — emitted only when declared.
53
- ...(query.orderBy !== undefined
54
- ? {
55
- orderBy: {
56
- allowed: query.orderBy.allowed.map((c) => ({
57
- name: c.name,
58
- langName: c.langName,
59
- })),
60
- defaultColumn: query.orderBy.defaultColumn,
61
- defaultDir: query.orderBy.defaultDir,
47
+ ...(query.syql === undefined
48
+ ? {}
49
+ : {
50
+ syql: {
51
+ revision: query.syql.revision,
52
+ inputs: query.syql.inputs.map((input) => {
53
+ if (input.kind === 'value')
54
+ return { ...input };
55
+ if (input.kind === 'group') {
56
+ return {
57
+ kind: input.kind,
58
+ name: input.name,
59
+ langName: input.langName,
60
+ members: input.members.map((member) => ({ ...member })),
61
+ };
62
+ }
63
+ if (input.kind === 'sort') {
64
+ return {
65
+ kind: input.kind,
66
+ name: input.name,
67
+ langName: input.langName,
68
+ defaultProfile: input.defaultProfile,
69
+ profiles: input.profiles.map((profile) => ({ ...profile })),
70
+ };
71
+ }
72
+ return { ...input };
73
+ }),
74
+ plan: {
75
+ backend: query.syql.plan.backend,
76
+ activationControls: query.syql.plan.activationControls,
77
+ conditions: query.syql.plan.conditions.map((condition) => ({
78
+ controls: condition.controls,
79
+ ...(condition.bind === undefined
80
+ ? {}
81
+ : { bind: condition.bind }),
82
+ })),
83
+ statements: query.syql.plan.statements.map((statement) => ({
84
+ ...(statement.sortProfile === undefined
85
+ ? {}
86
+ : { sortProfile: statement.sortProfile }),
87
+ ...(statement.activationMask === undefined
88
+ ? {}
89
+ : { activationMask: statement.activationMask }),
90
+ sql: statement.sql,
91
+ positionalSql: statement.positionalSql,
92
+ binds: statement.binds.map((bind) => ({ ...bind })),
93
+ })),
94
+ },
95
+ ...(query.syql.identity === undefined
96
+ ? {}
97
+ : { identity: query.syql.identity }),
62
98
  },
63
- }
64
- : {}),
65
- ...(query.limit !== undefined ? { limit: query.limit } : {}),
66
- ...(query.positionalSqlBase !== undefined
67
- ? { positionalSqlBase: query.positionalSqlBase }
68
- : {}),
69
- // §7 variant backend — emitted only when the query opts in.
70
- ...(query.variantGroups !== undefined
71
- ? {
72
- variantGroups: query.variantGroups.map((g) => ({
73
- key: g.key,
74
- params: g.params,
75
- flag: g.flag,
76
- })),
77
- }
78
- : {}),
79
- ...(query.variants !== undefined
80
- ? {
81
- variants: query.variants.map((v) => ({
82
- when: v.when,
83
- sql: v.sql,
84
- positionalSql: v.positionalSql,
85
- params: v.params,
86
- })),
87
- }
88
- : {}),
99
+ }),
89
100
  })),
90
101
  };
91
102
  return `${JSON.stringify(doc, null, 2)}\n`;
package/dist/query.d.ts CHANGED
@@ -10,31 +10,6 @@ export interface QueryParam {
10
10
  readonly type: QueryParamType;
11
11
  /** How the type was resolved — for the docs/tests, not emitted. */
12
12
  readonly source: 'inferred' | 'comment';
13
- /** §4 (DESIGN-queries.md): an optional param — its auto-guarded conjunct
14
- * applies only when it is provided. Always false for the `.sql` tier. */
15
- readonly optional?: boolean;
16
- /** §4: the `from+to` pairing — params sharing a group apply together. */
17
- readonly group?: string;
18
- /** §3: a `: flag` boolean guard param (never in a predicate as written;
19
- * the lowering binds it). Implies optional. */
20
- readonly flag?: boolean;
21
- }
22
- /** §6 orderBy knob: a generate-time allowlist (identifiers cannot bind). */
23
- export interface QueryOrderBy {
24
- /** Allowed columns: authored SQL name + the language-facing key the
25
- * generated signature accepts. Each is SQLite-checked at generate time. */
26
- readonly allowed: readonly {
27
- name: string;
28
- langName: string;
29
- }[];
30
- /** Default column (authored SQL name). */
31
- readonly defaultColumn: string;
32
- readonly defaultDir: 'asc' | 'desc';
33
- }
34
- /** §6 limit knob: a bound value with a codegen clamp + default. */
35
- export interface QueryLimit {
36
- readonly max?: number;
37
- readonly default?: number;
38
13
  }
39
14
  export interface QueryColumn {
40
15
  /** SQL-truth result name (pre-lowering, as authored). */
@@ -77,7 +52,7 @@ export interface QueryReactiveMetadata {
77
52
  /** Language-facing projection fields forming a proven unique row key. */
78
53
  readonly rowKey?: readonly string[];
79
54
  }
80
- /** §7/§8 backend selection: neutralization (default), always-variants, or
55
+ /** §7/§8 backend selection: neutralization, always-variants, or
81
56
  * the small-N heuristic (`auto`: enumerate at ≤ 2 optional groups). */
82
57
  export type QueryBackend = 'neutralize' | 'variants' | 'auto';
83
58
  /** Naming/backend options threaded from the manifest into query analysis. */
@@ -86,9 +61,98 @@ export interface QueryNamingOptions {
86
61
  /** Emitter targets this run generates — keyword hazards are only real on
87
62
  * targets that exist. */
88
63
  readonly targets: readonly NamingTarget[];
89
- /** `.syql` conditional-lowering backend (default `neutralize`); the
90
- * per-query `variants` knob always forces enumeration. */
64
+ /** `.syql` conditional-lowering policy (default `auto`). */
91
65
  readonly backend?: QueryBackend;
66
+ /** Compiler-reserved physical binds. They retain their exact names and do
67
+ * not participate in public target-language keyword/private-name checks. */
68
+ readonly internalParams?: readonly string[];
69
+ }
70
+ /** SYQL public inputs. These are deliberately separate from SQL binds: groups
71
+ * and compiler-generated parameters have distinct public/runtime shapes. */
72
+ export type QuerySyqlPublicInput = {
73
+ readonly kind: 'value';
74
+ readonly name: string;
75
+ readonly langName: string;
76
+ readonly type: QueryParamType;
77
+ readonly nullable: boolean;
78
+ readonly required: boolean;
79
+ readonly default?: false;
80
+ } | {
81
+ readonly kind: 'group';
82
+ readonly name: string;
83
+ readonly langName: string;
84
+ readonly members: readonly {
85
+ readonly name: string;
86
+ readonly langName: string;
87
+ readonly type: QueryParamType;
88
+ readonly nullable: boolean;
89
+ }[];
90
+ } | {
91
+ readonly kind: 'sort';
92
+ readonly name: string;
93
+ readonly langName: string;
94
+ readonly defaultProfile: string;
95
+ readonly profiles: readonly {
96
+ readonly name: string;
97
+ readonly langName: string;
98
+ }[];
99
+ } | {
100
+ readonly kind: 'limit';
101
+ readonly name: string;
102
+ readonly langName: string;
103
+ readonly defaultSize: number;
104
+ readonly maxSize: number;
105
+ };
106
+ /** One distinct SQLite bind in a physical revision-1 statement. */
107
+ export type QuerySyqlPlanBind = {
108
+ readonly kind: 'value';
109
+ readonly name: string;
110
+ readonly type: QueryParamType;
111
+ readonly input: string;
112
+ } | {
113
+ readonly kind: 'group-member';
114
+ readonly name: string;
115
+ readonly type: QueryParamType;
116
+ readonly input: string;
117
+ readonly member: string;
118
+ } | {
119
+ readonly kind: 'condition-active';
120
+ readonly name: string;
121
+ readonly type: 'boolean';
122
+ readonly condition: number;
123
+ readonly controls: readonly string[];
124
+ } | {
125
+ readonly kind: 'limit';
126
+ readonly name: string;
127
+ readonly type: 'integer';
128
+ readonly input: string;
129
+ };
130
+ /** One SQLite-checked statement selected by sort profile and, for the
131
+ * enumerated backend, the activation bitmask. */
132
+ export interface QuerySyqlStatement {
133
+ readonly sortProfile?: string;
134
+ readonly activationMask?: number;
135
+ readonly sql: string;
136
+ readonly positionalSql: string;
137
+ readonly binds: readonly QuerySyqlPlanBind[];
138
+ }
139
+ /** The target-neutral physical plan every emitter must implement exactly. */
140
+ export interface QuerySyqlExecutionPlan {
141
+ readonly backend: Exclude<QueryBackend, 'auto'>;
142
+ /** One bit per conditional activation control, in declaration order. */
143
+ readonly activationControls: readonly string[];
144
+ readonly conditions: readonly {
145
+ readonly controls: readonly string[];
146
+ /** Present only for neutralized plans. */
147
+ readonly bind?: string;
148
+ }[];
149
+ readonly statements: readonly QuerySyqlStatement[];
150
+ }
151
+ export interface QuerySyqlMetadata {
152
+ readonly revision: 1;
153
+ readonly inputs: readonly QuerySyqlPublicInput[];
154
+ readonly plan: QuerySyqlExecutionPlan;
155
+ readonly identity?: readonly string[];
92
156
  }
93
157
  export interface AnalyzedQuery {
94
158
  /** camelCase function name (path-derived, or a `-- name:` override). */
@@ -111,41 +175,9 @@ export interface AnalyzedQuery {
111
175
  /** IR tables this query reads (the useRawSql `{tables}` set), sorted. */
112
176
  readonly tables: readonly string[];
113
177
  readonly reactive: QueryReactiveMetadata;
114
- /** §6 orderBy knob (`.syql` tier). When present, `sql`/`positionalSql`
115
- * carry the DEFAULT order-by tail and `positionalSqlBase` is the static
116
- * prefix emitters compose the selected column onto. */
117
- readonly orderBy?: QueryOrderBy;
118
- /** §6 limit knob (`.syql` tier): the trailing `LIMIT ?` bind's clamp. */
119
- readonly limit?: QueryLimit;
120
- /** positional SQL WITHOUT the dynamic ORDER BY/LIMIT tail; present only
121
- * when `orderBy` is (emitters build `base + ORDER BY <baked> + limit
122
- * tail`). */
123
- readonly positionalSqlBase?: string;
124
- /** The positional limit tail (e.g. ` limit min(coalesce(?, 50), 100)` —
125
- * the default+clamp live IN the SQL, so runtimes bind `limit ?? null`).
126
- * Present only when BOTH knobs are declared (composers need it verbatim). */
127
- readonly positionalLimitTail?: string;
128
- /** §7 variant-enumeration backend (opt-in `variants` knob): one checked
129
- * statement per combination of provided optional groups. Semantically
130
- * identical to the neutralization `sql` by construction; the TS emitter
131
- * dispatches on provided-ness for perfect index use. `when` lists the
132
- * provided group keys; `params` are the DISTINCT param names this variant
133
- * binds, positional order. */
134
- readonly variants?: readonly {
135
- readonly when: readonly string[];
136
- readonly sql: string;
137
- readonly positionalSql: string;
138
- readonly params: readonly string[];
139
- }[];
140
- /** The optional-group keys, in dispatch-bit order (bit i of the variant
141
- * index = group i provided). Present iff `variants` is. */
142
- readonly variantGroups?: readonly {
143
- readonly key: string;
144
- /** Params whose provision defines the group (all must be non-null; a
145
- * flag group is "on" when its single param is TRUE). */
146
- readonly params: readonly string[];
147
- readonly flag: boolean;
148
- }[];
178
+ /** Revision-1 frontend/public-input and physical execution contract. When
179
+ * present, emitters use this instead of treating `params` as the API. */
180
+ readonly syql?: QuerySyqlMetadata;
149
181
  }
150
182
  /**
151
183
  * Path-relative default name. `billing/invoices/list.sql` → `billingInvoicesList`;
@@ -171,6 +203,9 @@ export interface SplitStatement {
171
203
  export declare function splitStatements(content: string): SplitStatement[];
172
204
  /** The `-- name:` override in a statement's leading comment block, or null. */
173
205
  export declare function parseStatementName(statementText: string, location: string): string | null;
206
+ /** Strip `--` and block comments so identifier scans don't hit commented SQL,
207
+ * and string literals so `':x'` inside a string isn't read as a param. */
208
+ export declare function stripCommentsAndStrings(sql: string): string;
174
209
  /** Rewrite `:name` → positional placeholders AND strip comments + collapse
175
210
  * whitespace, producing a clean single-line SQL string suitable for
176
211
  * embedding in a generated string literal. String literals are preserved
@@ -182,6 +217,18 @@ export declare function parseStatementName(statementText: string, location: stri
182
217
  * form — one bound value per DISTINCT param, reuse resolved by SQLite —
183
218
  * so the generated bind array stays one-entry-per-param. */
184
219
  export declare function toPositionalSql(sql: string): string;
220
+ export interface TableRef {
221
+ readonly table: string;
222
+ /** Alias (or the table name when un-aliased). */
223
+ readonly alias: string;
224
+ }
225
+ export declare function scanTableRefs(sql: string, ir: IrDocument): TableRef[];
226
+ /**
227
+ * Return every revision-1 column/LIKE type constraint for one bind. Unlike
228
+ * `inferParamType` (the legacy first-match helper), SYQL uses the complete set
229
+ * so conflicting checked uses cannot be silently accepted.
230
+ */
231
+ export declare function inferParamTypeEvidence(paramName: string, sql: string, refs: readonly TableRef[], ir: IrDocument): readonly IrColumnType[];
185
232
  export interface QueryDb {
186
233
  /** Prepare + return the result columns / declared types / param count.
187
234
  * Throws (with SQLite's message) when the SQL is invalid against the DDL. */
package/dist/query.js CHANGED
@@ -84,6 +84,8 @@ const DECLTYPE_MAP = {
84
84
  BLOBREF: 'blob_ref',
85
85
  CRDT: 'crdt',
86
86
  };
87
+ /** Local client protocol column; query-only and never part of schema IR. */
88
+ const SYNC_VERSION_COLUMN = '_sync_version';
87
89
  const DEFAULT_NAMING = { naming: 'camel', targets: ['ts'] };
88
90
  // -- path → name --------------------------------------------------------------
89
91
  /** A single kebab-case path segment (folder) or filename stem. */
@@ -290,7 +292,7 @@ function parseCommentParams(file, raw) {
290
292
  }
291
293
  /** Strip `--` and block comments so identifier scans don't hit commented SQL,
292
294
  * and string literals so `':x'` inside a string isn't read as a param. */
293
- function stripCommentsAndStrings(sql) {
295
+ export function stripCommentsAndStrings(sql) {
294
296
  let out = '';
295
297
  let i = 0;
296
298
  while (i < sql.length) {
@@ -453,7 +455,7 @@ const RESERVED_ALIAS = new Set([
453
455
  'limit',
454
456
  'having',
455
457
  ]);
456
- function scanTableRefs(sql, ir) {
458
+ export function scanTableRefs(sql, ir) {
457
459
  const cleaned = stripCommentsAndStrings(sql);
458
460
  const known = new Map(ir.tables.map((t) => [t.name, t]));
459
461
  const refs = [];
@@ -533,6 +535,17 @@ function resolveSource(item, refs, ir) {
533
535
  }
534
536
  return null;
535
537
  }
538
+ /** Resolve the one supported query-only protocol column. SQLite still proves
539
+ * qualifier/ambiguity against the synthesized local tables. */
540
+ function isSyncVersionSource(item, refs) {
541
+ const match = PLAIN_REF_RE.exec(item.expr);
542
+ if (match === null || match[2] !== SYNC_VERSION_COLUMN)
543
+ return false;
544
+ const qualifier = match[1];
545
+ return qualifier === undefined
546
+ ? refs.length === 1
547
+ : refs.some((ref) => ref.alias === qualifier);
548
+ }
536
549
  // -- param type inference -----------------------------------------------------
537
550
  //
538
551
  // Infer a param's type from `col <op> :name` or `col IN (:name, …)` where `col`
@@ -603,6 +616,57 @@ function inferParamType(paramName, sql, refs, ir) {
603
616
  return 'string';
604
617
  return null;
605
618
  }
619
+ /**
620
+ * Return every revision-1 column/LIKE type constraint for one bind. Unlike
621
+ * `inferParamType` (the legacy first-match helper), SYQL uses the complete set
622
+ * so conflicting checked uses cannot be silently accepted.
623
+ */
624
+ export function inferParamTypeEvidence(paramName, sql, refs, ir) {
625
+ const cleaned = stripCommentsAndStrings(sql);
626
+ const byName = new Map(ir.tables.map((table) => [table.name, table]));
627
+ const evidence = [];
628
+ const add = (qualifier, column) => {
629
+ if (qualifier !== undefined) {
630
+ const ref = refs.find((candidate) => candidate.alias === qualifier);
631
+ const table = ref === undefined ? undefined : byName.get(ref.table);
632
+ const type = table?.columns.find((item) => item.name === column)?.type;
633
+ if (type !== undefined)
634
+ evidence.push(type);
635
+ return;
636
+ }
637
+ const matches = refs.flatMap((ref) => {
638
+ const table = byName.get(ref.table);
639
+ const type = table?.columns.find((item) => item.name === column)?.type;
640
+ return type === undefined ? [] : [type];
641
+ });
642
+ if (matches.length === 1)
643
+ evidence.push(matches[0]);
644
+ };
645
+ const comparison = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s*(?:=|==|!=|<>|<=|>=|<|>|\\bLIKE\\b|\\bIS\\b)\\s*:${paramName}\\b`, 'gi');
646
+ for (const match of cleaned.matchAll(comparison)) {
647
+ add(match[1], match[2]);
648
+ }
649
+ const reversed = new RegExp(`:${paramName}\\b\\s*(?:=|==|!=|<>|<=|>=|<|>)\\s*(?:(${IDENT})\\.)?(${IDENT})`, 'gi');
650
+ for (const match of cleaned.matchAll(reversed)) {
651
+ add(match[1], match[2]);
652
+ }
653
+ const inList = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+IN\\s*\\(([^)]*:${paramName}\\b[^)]*)\\)`, 'gi');
654
+ for (const match of cleaned.matchAll(inList)) {
655
+ add(match[1], match[2]);
656
+ }
657
+ const betweenLeft = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+:${paramName}\\b`, 'gi');
658
+ for (const match of cleaned.matchAll(betweenLeft)) {
659
+ add(match[1], match[2]);
660
+ }
661
+ const betweenRight = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+\\S+\\s+AND\\s+:${paramName}\\b`, 'gi');
662
+ for (const match of cleaned.matchAll(betweenRight)) {
663
+ add(match[1], match[2]);
664
+ }
665
+ const like = new RegExp(`\\bLIKE\\b[^()]*:${paramName}\\b`, 'gi');
666
+ if (like.test(cleaned))
667
+ evidence.push('string');
668
+ return evidence;
669
+ }
606
670
  // -- fallback column typing (computed expressions) ----------------------------
607
671
  const AGG_RE = /\b(count|sum|total|avg|min|max)\s*\(/i;
608
672
  /** decltype-null column: the documented honest fallback. Aggregates and
@@ -665,9 +729,7 @@ function inferReactiveMetadata(sql, refs, ir, params, columns) {
665
729
  continue;
666
730
  const name = match[1] ?? match[2];
667
731
  const param = name === undefined ? undefined : byParam.get(name);
668
- if (param !== undefined &&
669
- param.optional !== true &&
670
- param.flag !== true) {
732
+ if (param !== undefined) {
671
733
  found.push(param.name);
672
734
  }
673
735
  }
@@ -677,9 +739,7 @@ function inferReactiveMetadata(sql, refs, ir, params, columns) {
677
739
  continue;
678
740
  for (const paramMatch of (match[1] ?? '').matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g)) {
679
741
  const param = byParam.get(paramMatch[1]);
680
- if (param !== undefined &&
681
- param.optional !== true &&
682
- param.flag !== true) {
742
+ if (param !== undefined) {
683
743
  found.push(param.name);
684
744
  }
685
745
  }
@@ -750,6 +810,10 @@ export function synthesizeDdl(ir) {
750
810
  const pk = c.name === table.primaryKey ? ' PRIMARY KEY' : '';
751
811
  return ` ${c.name} ${SQL_TYPE[c.type]}${pk}${notNull}`;
752
812
  });
813
+ // The materialized local client table always carries this hidden protocol
814
+ // token. It is absent from IR mutation types but may be explicitly aliased
815
+ // by a named read query for optimistic concurrency.
816
+ cols.push(` ${SYNC_VERSION_COLUMN} INTEGER NOT NULL DEFAULT -1`);
753
817
  lines.push(`CREATE TABLE ${table.name} (\n${cols.join(',\n')}\n);`);
754
818
  // Create the declared indexes too — harmless for prepare()/decltype, and
755
819
  // keeps the type-check DB's shape honest with what the client materializes.
@@ -834,6 +898,18 @@ export function analyzeStatement(name, location, statementText, ir, db, naming =
834
898
  const source = item !== undefined ? resolveSource(item, refs, ir) : null;
835
899
  const sqlName = sqlNames[index] ?? colName;
836
900
  const langName = langNames[index] ?? colName;
901
+ if (item !== undefined && isSyncVersionSource(item, refs)) {
902
+ if (sqlName === SYNC_VERSION_COLUMN) {
903
+ throw new TypegenError(file, `${SYNC_VERSION_COLUMN} is stripped from app-facing query rows unless explicitly aliased (for example \`${SYNC_VERSION_COLUMN} AS server_version\`)`);
904
+ }
905
+ return {
906
+ name: sqlName,
907
+ langName,
908
+ type: 'integer',
909
+ nullable: false,
910
+ fidelity: 'exact',
911
+ };
912
+ }
837
913
  if (source !== null) {
838
914
  // Exact: IR column type + IR nullability. (decltype agrees; we prefer
839
915
  // the IR type so blob_ref/crdt/json semantic types survive.)
@@ -867,10 +943,12 @@ export function analyzeStatement(name, location, statementText, ir, db, naming =
867
943
  // Defensive: our scan and SQLite disagree (shouldn't happen for the subset).
868
944
  throw new TypegenError(file, `internal: scanned ${paramNames.length} params (${paramNames.join(', ')}) but SQLite reports ${described.paramsCount}`);
869
945
  }
870
- const paramLangNames = buildNamingMap(paramNames, naming.naming, file, 'params', naming.targets);
946
+ const internalParams = new Set(naming.internalParams ?? []);
947
+ const publicParamLangNames = buildNamingMap(paramNames.filter((name) => !internalParams.has(name)), naming.naming, file, 'params', naming.targets);
948
+ const langNameByParam = new Map(publicParamLangNames.map((mapping) => [mapping.sqlName, mapping.langName]));
871
949
  const commentByName = new Map(commentParams.map((p) => [p.name, p.type]));
872
- const params = paramNames.map((paramName, index) => {
873
- const langName = paramLangNames[index]?.langName ?? paramName;
950
+ const params = paramNames.map((paramName) => {
951
+ const langName = langNameByParam.get(paramName) ?? paramName;
874
952
  const commented = commentByName.get(paramName);
875
953
  if (commented !== undefined) {
876
954
  return { name: paramName, langName, type: commented, source: 'comment' };
@@ -19,15 +19,8 @@ export type SyqlSemanticTemplateNode = {
19
19
  } | {
20
20
  readonly kind: 'when';
21
21
  readonly controls: readonly string[];
22
+ readonly explicitPresence: readonly boolean[];
22
23
  readonly body: readonly SyqlSemanticTemplateNode[];
23
- } | {
24
- readonly kind: 'scope' | 'cover';
25
- readonly bindings: readonly {
26
- readonly qualifier: string;
27
- readonly column: string;
28
- readonly operator: 'equal' | 'in';
29
- readonly values: readonly string[];
30
- }[];
31
24
  };
32
25
  export interface SyqlSemanticTemplate {
33
26
  readonly mode: SyqlTemplate['tree']['mode'];
@@ -38,10 +31,12 @@ export type SyqlSemanticQueryParameter = {
38
31
  readonly name: string;
39
32
  readonly optional: boolean;
40
33
  readonly type?: SyqlSemanticType;
34
+ readonly default?: false;
41
35
  } | {
42
- readonly kind: 'switch';
36
+ readonly kind: 'range';
43
37
  readonly name: string;
44
- readonly optional: true;
38
+ readonly optional: boolean;
39
+ readonly type?: SyqlSemanticType;
45
40
  } | {
46
41
  readonly kind: 'group';
47
42
  readonly name: string;
@@ -62,8 +57,13 @@ export type SyqlSemanticDeclaration = {
62
57
  } | {
63
58
  readonly kind: 'query';
64
59
  readonly name: string;
60
+ readonly sync: boolean;
61
+ readonly syncBy?: {
62
+ readonly qualifier: string;
63
+ readonly column: string;
64
+ };
65
65
  readonly parameters: readonly SyqlSemanticQueryParameter[];
66
- readonly sql: SyqlSemanticTemplate;
66
+ readonly statement: SyqlSemanticTemplate;
67
67
  readonly sort?: {
68
68
  readonly control: string;
69
69
  readonly defaultProfile: string;
@@ -72,12 +72,11 @@ export type SyqlSemanticDeclaration = {
72
72
  readonly order: SyqlSemanticTemplate;
73
73
  }[];
74
74
  };
75
- readonly page?: {
75
+ readonly limit?: {
76
76
  readonly control: string;
77
77
  readonly defaultSize: number;
78
78
  readonly maxSize: number;
79
79
  };
80
- readonly identity?: readonly string[];
81
80
  };
82
81
  export interface SyqlSemanticFile {
83
82
  readonly kind: 'syql-file';
package/dist/syql-ast.js CHANGED
@@ -10,8 +10,14 @@ function semanticMember(member) {
10
10
  return { name: member.name, ...(type === undefined ? {} : { type }) };
11
11
  }
12
12
  function semanticParameter(parameter) {
13
- if (parameter.kind === 'switch') {
14
- return { kind: 'switch', name: parameter.name, optional: true };
13
+ if (parameter.kind === 'range') {
14
+ const type = semanticType(parameter.type);
15
+ return {
16
+ kind: 'range',
17
+ name: parameter.name,
18
+ optional: parameter.optional,
19
+ ...(type === undefined ? {} : { type }),
20
+ };
15
21
  }
16
22
  if (parameter.kind === 'group') {
17
23
  return {
@@ -27,6 +33,7 @@ function semanticParameter(parameter) {
27
33
  name: parameter.name,
28
34
  optional: parameter.optional,
29
35
  ...(type === undefined ? {} : { type }),
36
+ ...(parameter.default === undefined ? {} : { default: parameter.default }),
30
37
  };
31
38
  }
32
39
  function semanticRawTokens(tokens) {
@@ -50,18 +57,11 @@ function semanticNode(node) {
50
57
  return {
51
58
  kind: 'when',
52
59
  controls: node.controls,
60
+ explicitPresence: node.explicitPresence,
53
61
  body: semanticNodes(node.body.nodes),
54
62
  };
55
63
  }
56
- return {
57
- kind: node.kind,
58
- bindings: node.bindings.map((binding) => ({
59
- qualifier: binding.column.qualifier,
60
- column: binding.column.name,
61
- operator: binding.operator,
62
- values: binding.values.map((value) => value.name),
63
- })),
64
- };
64
+ return undefined;
65
65
  }
66
66
  function semanticNodes(nodes) {
67
67
  return nodes.flatMap((node) => {
@@ -93,8 +93,17 @@ function semanticDeclaration(declaration) {
93
93
  return {
94
94
  kind: 'query',
95
95
  name: declaration.name,
96
+ sync: declaration.sync,
97
+ ...(declaration.syncBy === undefined
98
+ ? {}
99
+ : {
100
+ syncBy: {
101
+ qualifier: declaration.syncBy.qualifier,
102
+ column: declaration.syncBy.column,
103
+ },
104
+ }),
96
105
  parameters: declaration.parameters.map(semanticParameter),
97
- sql: semanticTemplate(declaration.sql.body),
106
+ statement: semanticTemplate(declaration.statement),
98
107
  ...(declaration.sort === undefined
99
108
  ? {}
100
109
  : {
@@ -107,18 +116,15 @@ function semanticDeclaration(declaration) {
107
116
  })),
108
117
  },
109
118
  }),
110
- ...(declaration.page === undefined
119
+ ...(declaration.limit === undefined
111
120
  ? {}
112
121
  : {
113
- page: {
114
- control: declaration.page.control,
115
- defaultSize: declaration.page.defaultSize,
116
- maxSize: declaration.page.maxSize,
122
+ limit: {
123
+ control: declaration.limit.control,
124
+ defaultSize: declaration.limit.defaultSize,
125
+ maxSize: declaration.limit.maxSize,
117
126
  },
118
127
  }),
119
- ...(declaration.identity === undefined
120
- ? {}
121
- : { identity: declaration.identity.fields }),
122
128
  };
123
129
  }
124
130
  /** Return the stable syntax/semantic AST pinned by `spec/syql` fixtures. */
@@ -38,3 +38,5 @@ export declare class SyqlFrontendError extends TypegenError {
38
38
  export declare function isSyqlTrivia(token: SyqlToken): boolean;
39
39
  /** Lex one complete `.syql` source file, including trivia and an EOF token. */
40
40
  export declare function lexSyqlSource(file: string, source: string): readonly SyqlToken[];
41
+ /** Lex an isolated SQL/template string without import-path contextual rules. */
42
+ export declare function lexSyqlSqlSource(file: string, source: string): readonly SyqlToken[];