@syncular/typegen 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +75 -33
  2. package/dist/cli.js +43 -17
  3. package/dist/emit-queries-dart.js +167 -55
  4. package/dist/emit-queries-kotlin.js +166 -55
  5. package/dist/emit-queries-swift.js +182 -57
  6. package/dist/emit-queries.js +289 -123
  7. package/dist/fmt.d.ts +2 -2
  8. package/dist/fmt.js +323 -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 +359 -185
  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 +115 -63
  19. package/dist/query.js +60 -11
  20. package/dist/syql-lexer.d.ts +2 -0
  21. package/dist/syql-lexer.js +9 -2
  22. package/dist/syql-lowering.d.ts +22 -0
  23. package/dist/syql-lowering.js +411 -0
  24. package/dist/syql-semantics.d.ts +76 -0
  25. package/dist/syql-semantics.js +551 -0
  26. package/dist/syql-validator.d.ts +35 -0
  27. package/dist/syql-validator.js +966 -0
  28. package/package.json +3 -3
  29. package/src/cli.ts +51 -21
  30. package/src/emit-queries-dart.ts +195 -69
  31. package/src/emit-queries-kotlin.ts +197 -69
  32. package/src/emit-queries-swift.ts +224 -68
  33. package/src/emit-queries.ts +413 -165
  34. package/src/fmt.ts +377 -303
  35. package/src/generate.ts +43 -9
  36. package/src/index.ts +3 -1
  37. package/src/lsp.ts +425 -215
  38. package/src/manifest.ts +2 -4
  39. package/src/query-ir.ts +52 -42
  40. package/src/query.ts +199 -79
  41. package/src/syql-lexer.ts +12 -1
  42. package/src/syql-lowering.ts +638 -0
  43. package/src/syql-semantics.ts +859 -0
  44. package/src/syql-validator.ts +1492 -0
  45. package/dist/syql.d.ts +0 -102
  46. package/dist/syql.js +0 -1141
  47. 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,103 @@ 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
+ /** 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. */
73
+ export type QuerySyqlPublicInput = {
74
+ readonly kind: 'value';
75
+ readonly name: string;
76
+ readonly langName: string;
77
+ readonly type: QueryParamType;
78
+ readonly nullable: boolean;
79
+ readonly required: boolean;
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: 'switch';
92
+ readonly name: string;
93
+ readonly langName: string;
94
+ readonly default: false;
95
+ } | {
96
+ readonly kind: 'sort';
97
+ readonly name: string;
98
+ readonly langName: string;
99
+ readonly defaultProfile: string;
100
+ readonly profiles: readonly {
101
+ readonly name: string;
102
+ readonly langName: string;
103
+ }[];
104
+ } | {
105
+ readonly kind: 'page';
106
+ readonly name: string;
107
+ readonly langName: string;
108
+ readonly defaultSize: number;
109
+ readonly maxSize: number;
110
+ };
111
+ /** One distinct SQLite bind in a physical revision-1 statement. */
112
+ export type QuerySyqlPlanBind = {
113
+ readonly kind: 'value';
114
+ readonly name: string;
115
+ readonly type: QueryParamType;
116
+ readonly input: string;
117
+ } | {
118
+ readonly kind: 'group-member';
119
+ readonly name: string;
120
+ readonly type: QueryParamType;
121
+ readonly input: string;
122
+ readonly member: string;
123
+ } | {
124
+ readonly kind: 'condition-active';
125
+ readonly name: string;
126
+ readonly type: 'boolean';
127
+ readonly condition: number;
128
+ readonly controls: readonly string[];
129
+ } | {
130
+ readonly kind: 'page';
131
+ readonly name: string;
132
+ readonly type: 'integer';
133
+ readonly input: string;
134
+ };
135
+ /** One SQLite-checked statement selected by sort profile and, for the
136
+ * enumerated backend, the activation bitmask. */
137
+ export interface QuerySyqlStatement {
138
+ readonly sortProfile?: string;
139
+ readonly activationMask?: number;
140
+ readonly sql: string;
141
+ readonly positionalSql: string;
142
+ readonly binds: readonly QuerySyqlPlanBind[];
143
+ }
144
+ /** The target-neutral physical plan every emitter must implement exactly. */
145
+ export interface QuerySyqlExecutionPlan {
146
+ readonly backend: Exclude<QueryBackend, 'auto'>;
147
+ /** One bit per optional scalar/group/switch, in declaration order. */
148
+ readonly activationControls: readonly string[];
149
+ readonly conditions: readonly {
150
+ readonly controls: readonly string[];
151
+ /** Present only for neutralized plans. */
152
+ readonly bind?: string;
153
+ }[];
154
+ readonly statements: readonly QuerySyqlStatement[];
155
+ }
156
+ export interface QuerySyqlMetadata {
157
+ readonly revision: 1;
158
+ readonly inputs: readonly QuerySyqlPublicInput[];
159
+ readonly plan: QuerySyqlExecutionPlan;
160
+ readonly identity?: readonly string[];
92
161
  }
93
162
  export interface AnalyzedQuery {
94
163
  /** camelCase function name (path-derived, or a `-- name:` override). */
@@ -111,41 +180,9 @@ export interface AnalyzedQuery {
111
180
  /** IR tables this query reads (the useRawSql `{tables}` set), sorted. */
112
181
  readonly tables: readonly string[];
113
182
  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
- }[];
183
+ /** Revision-1 frontend/public-input and physical execution contract. When
184
+ * present, emitters use this instead of treating `params` as the API. */
185
+ readonly syql?: QuerySyqlMetadata;
149
186
  }
150
187
  /**
151
188
  * Path-relative default name. `billing/invoices/list.sql` → `billingInvoicesList`;
@@ -171,6 +208,9 @@ export interface SplitStatement {
171
208
  export declare function splitStatements(content: string): SplitStatement[];
172
209
  /** The `-- name:` override in a statement's leading comment block, or null. */
173
210
  export declare function parseStatementName(statementText: string, location: string): string | null;
211
+ /** Strip `--` and block comments so identifier scans don't hit commented SQL,
212
+ * and string literals so `':x'` inside a string isn't read as a param. */
213
+ export declare function stripCommentsAndStrings(sql: string): string;
174
214
  /** Rewrite `:name` → positional placeholders AND strip comments + collapse
175
215
  * whitespace, producing a clean single-line SQL string suitable for
176
216
  * embedding in a generated string literal. String literals are preserved
@@ -182,6 +222,18 @@ export declare function parseStatementName(statementText: string, location: stri
182
222
  * form — one bound value per DISTINCT param, reuse resolved by SQLite —
183
223
  * so the generated bind array stays one-entry-per-param. */
184
224
  export declare function toPositionalSql(sql: string): string;
225
+ export interface TableRef {
226
+ readonly table: string;
227
+ /** Alias (or the table name when un-aliased). */
228
+ readonly alias: string;
229
+ }
230
+ export declare function scanTableRefs(sql: string, ir: IrDocument): TableRef[];
231
+ /**
232
+ * Return every revision-1 column/LIKE type constraint for one bind. Unlike
233
+ * `inferParamType` (the legacy first-match helper), SYQL uses the complete set
234
+ * so conflicting checked uses cannot be silently accepted.
235
+ */
236
+ export declare function inferParamTypeEvidence(paramName: string, sql: string, refs: readonly TableRef[], ir: IrDocument): readonly IrColumnType[];
185
237
  export interface QueryDb {
186
238
  /** Prepare + return the result columns / declared types / param count.
187
239
  * Throws (with SQLite's message) when the SQL is invalid against the DDL. */
package/dist/query.js CHANGED
@@ -290,7 +290,7 @@ function parseCommentParams(file, raw) {
290
290
  }
291
291
  /** Strip `--` and block comments so identifier scans don't hit commented SQL,
292
292
  * and string literals so `':x'` inside a string isn't read as a param. */
293
- function stripCommentsAndStrings(sql) {
293
+ export function stripCommentsAndStrings(sql) {
294
294
  let out = '';
295
295
  let i = 0;
296
296
  while (i < sql.length) {
@@ -453,7 +453,7 @@ const RESERVED_ALIAS = new Set([
453
453
  'limit',
454
454
  'having',
455
455
  ]);
456
- function scanTableRefs(sql, ir) {
456
+ export function scanTableRefs(sql, ir) {
457
457
  const cleaned = stripCommentsAndStrings(sql);
458
458
  const known = new Map(ir.tables.map((t) => [t.name, t]));
459
459
  const refs = [];
@@ -603,6 +603,57 @@ function inferParamType(paramName, sql, refs, ir) {
603
603
  return 'string';
604
604
  return null;
605
605
  }
606
+ /**
607
+ * Return every revision-1 column/LIKE type constraint for one bind. Unlike
608
+ * `inferParamType` (the legacy first-match helper), SYQL uses the complete set
609
+ * so conflicting checked uses cannot be silently accepted.
610
+ */
611
+ export function inferParamTypeEvidence(paramName, sql, refs, ir) {
612
+ const cleaned = stripCommentsAndStrings(sql);
613
+ const byName = new Map(ir.tables.map((table) => [table.name, table]));
614
+ const evidence = [];
615
+ const add = (qualifier, column) => {
616
+ if (qualifier !== undefined) {
617
+ const ref = refs.find((candidate) => candidate.alias === qualifier);
618
+ const table = ref === undefined ? undefined : byName.get(ref.table);
619
+ const type = table?.columns.find((item) => item.name === column)?.type;
620
+ if (type !== undefined)
621
+ evidence.push(type);
622
+ return;
623
+ }
624
+ const matches = refs.flatMap((ref) => {
625
+ const table = byName.get(ref.table);
626
+ const type = table?.columns.find((item) => item.name === column)?.type;
627
+ return type === undefined ? [] : [type];
628
+ });
629
+ if (matches.length === 1)
630
+ evidence.push(matches[0]);
631
+ };
632
+ const comparison = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s*(?:=|==|!=|<>|<=|>=|<|>|\\bLIKE\\b|\\bIS\\b)\\s*:${paramName}\\b`, 'gi');
633
+ for (const match of cleaned.matchAll(comparison)) {
634
+ add(match[1], match[2]);
635
+ }
636
+ const reversed = new RegExp(`:${paramName}\\b\\s*(?:=|==|!=|<>|<=|>=|<|>)\\s*(?:(${IDENT})\\.)?(${IDENT})`, 'gi');
637
+ for (const match of cleaned.matchAll(reversed)) {
638
+ add(match[1], match[2]);
639
+ }
640
+ const inList = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+IN\\s*\\(([^)]*:${paramName}\\b[^)]*)\\)`, 'gi');
641
+ for (const match of cleaned.matchAll(inList)) {
642
+ add(match[1], match[2]);
643
+ }
644
+ const betweenLeft = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+:${paramName}\\b`, 'gi');
645
+ for (const match of cleaned.matchAll(betweenLeft)) {
646
+ add(match[1], match[2]);
647
+ }
648
+ const betweenRight = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+\\S+\\s+AND\\s+:${paramName}\\b`, 'gi');
649
+ for (const match of cleaned.matchAll(betweenRight)) {
650
+ add(match[1], match[2]);
651
+ }
652
+ const like = new RegExp(`\\bLIKE\\b[^()]*:${paramName}\\b`, 'gi');
653
+ if (like.test(cleaned))
654
+ evidence.push('string');
655
+ return evidence;
656
+ }
606
657
  // -- fallback column typing (computed expressions) ----------------------------
607
658
  const AGG_RE = /\b(count|sum|total|avg|min|max)\s*\(/i;
608
659
  /** decltype-null column: the documented honest fallback. Aggregates and
@@ -665,9 +716,7 @@ function inferReactiveMetadata(sql, refs, ir, params, columns) {
665
716
  continue;
666
717
  const name = match[1] ?? match[2];
667
718
  const param = name === undefined ? undefined : byParam.get(name);
668
- if (param !== undefined &&
669
- param.optional !== true &&
670
- param.flag !== true) {
719
+ if (param !== undefined) {
671
720
  found.push(param.name);
672
721
  }
673
722
  }
@@ -677,9 +726,7 @@ function inferReactiveMetadata(sql, refs, ir, params, columns) {
677
726
  continue;
678
727
  for (const paramMatch of (match[1] ?? '').matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g)) {
679
728
  const param = byParam.get(paramMatch[1]);
680
- if (param !== undefined &&
681
- param.optional !== true &&
682
- param.flag !== true) {
729
+ if (param !== undefined) {
683
730
  found.push(param.name);
684
731
  }
685
732
  }
@@ -867,10 +914,12 @@ export function analyzeStatement(name, location, statementText, ir, db, naming =
867
914
  // Defensive: our scan and SQLite disagree (shouldn't happen for the subset).
868
915
  throw new TypegenError(file, `internal: scanned ${paramNames.length} params (${paramNames.join(', ')}) but SQLite reports ${described.paramsCount}`);
869
916
  }
870
- const paramLangNames = buildNamingMap(paramNames, naming.naming, file, 'params', naming.targets);
917
+ const internalParams = new Set(naming.internalParams ?? []);
918
+ const publicParamLangNames = buildNamingMap(paramNames.filter((name) => !internalParams.has(name)), naming.naming, file, 'params', naming.targets);
919
+ const langNameByParam = new Map(publicParamLangNames.map((mapping) => [mapping.sqlName, mapping.langName]));
871
920
  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;
921
+ const params = paramNames.map((paramName) => {
922
+ const langName = langNameByParam.get(paramName) ?? paramName;
874
923
  const commented = commentByName.get(paramName);
875
924
  if (commented !== undefined) {
876
925
  return { name: paramName, langName, type: commented, source: 'comment' };
@@ -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[];
@@ -92,9 +92,11 @@ class Lexer {
92
92
  #column = 1;
93
93
  #braceDepth = 0;
94
94
  #expectImportPath = false;
95
- constructor(file, source) {
95
+ #recognizeImportPaths;
96
+ constructor(file, source, recognizeImportPaths = true) {
96
97
  this.#file = file;
97
98
  this.#source = source;
99
+ this.#recognizeImportPaths = recognizeImportPaths;
98
100
  }
99
101
  lex() {
100
102
  while (this.#offset < this.#source.length)
@@ -161,7 +163,8 @@ class Lexer {
161
163
  else if (token.text === '}')
162
164
  this.#braceDepth = Math.max(0, this.#braceDepth - 1);
163
165
  }
164
- if (token.kind === 'identifier' &&
166
+ if (this.#recognizeImportPaths &&
167
+ token.kind === 'identifier' &&
165
168
  token.text === 'from' &&
166
169
  this.#braceDepth === 0) {
167
170
  this.#expectImportPath = true;
@@ -365,3 +368,7 @@ class Lexer {
365
368
  export function lexSyqlSource(file, source) {
366
369
  return new Lexer(file, source).lex();
367
370
  }
371
+ /** Lex an isolated SQL/template string without import-path contextual rules. */
372
+ export function lexSyqlSqlSource(file, source) {
373
+ return new Lexer(file, source, false).lex();
374
+ }
@@ -0,0 +1,22 @@
1
+ import type { IrDocument } from './ir.js';
2
+ import { type AnalyzedQuery, type QueryBackend, type QueryDb, type QueryNamingOptions, type QuerySyqlExecutionPlan } from './query.js';
3
+ import type { SyqlValidatedQuery } from './syql-validator.js';
4
+ export type SyqlLoweringErrorCode = 'SYQL7001_ENUMERATION_LIMIT' | 'SYQL7002_INTERNAL_LOWERING';
5
+ export interface SyqlLoweringOptions {
6
+ readonly backend?: QueryBackend;
7
+ /** Maximum physical activation variants generated for diagnostics and a
8
+ * forced variants backend. Sort profiles multiply this count. */
9
+ readonly maxEnumeratedStatements?: number;
10
+ }
11
+ export interface SyqlLoweredQuery {
12
+ readonly validated: SyqlValidatedQuery;
13
+ readonly analysis: AnalyzedQuery;
14
+ readonly selected: QuerySyqlExecutionPlan;
15
+ readonly neutralized: QuerySyqlExecutionPlan;
16
+ /** Omitted only when enumeration exceeds compiler resource policy. */
17
+ readonly enumerated?: QuerySyqlExecutionPlan;
18
+ }
19
+ /** Lower one validated revision-1 query and prepare every generated physical
20
+ * statement. The selected plan is embedded into the returned shared QueryIR
21
+ * query; the second plan is retained for equivalence conformance tests. */
22
+ export declare function lowerSyqlQuery(validated: SyqlValidatedQuery, ir: IrDocument, db: QueryDb, naming?: QueryNamingOptions, options?: SyqlLoweringOptions): SyqlLoweredQuery;