@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
package/src/manifest.ts CHANGED
@@ -128,9 +128,7 @@ export interface Manifest {
128
128
  * generated row types/params (queries lower with `AS` aliases); "preserve"
129
129
  * keeps SQL-truth names everywhere. */
130
130
  readonly naming: NamingMode;
131
- /** §7 `.syql` backend: "neutralize" (default), "variants" (enumerate
132
- * whenever eligible), or "auto" (enumerate at ≤ 2 optional groups). The
133
- * per-query `variants` knob always forces enumeration. */
131
+ /** §7 `.syql` backend: "auto" (default), "neutralize", or "variants". */
134
132
  readonly queryBackend: ManifestQueryBackend;
135
133
  readonly output: ManifestOutput;
136
134
  readonly schemaVersions: readonly ManifestSchemaVersion[];
@@ -389,7 +387,7 @@ export function parseManifest(raw: unknown): Manifest {
389
387
  }
390
388
  naming = obj.naming;
391
389
  }
392
- let queryBackend: ManifestQueryBackend = 'neutralize';
390
+ let queryBackend: ManifestQueryBackend = 'auto';
393
391
  if (obj.queryBackend !== undefined) {
394
392
  if (
395
393
  obj.queryBackend !== 'neutralize' &&
package/src/query-ir.ts CHANGED
@@ -14,7 +14,7 @@ import type { AnalyzedQuery } from './query';
14
14
  /** Serialize analyzed queries as the deterministic QueryIR JSON document. */
15
15
  export function serializeQueryIr(queries: readonly AnalyzedQuery[]): string {
16
16
  const doc = {
17
- queryIrVersion: 2,
17
+ queryIrVersion: 3,
18
18
  queries: queries.map((query) => ({
19
19
  name: query.name,
20
20
  file: query.file,
@@ -25,11 +25,6 @@ export function serializeQueryIr(queries: readonly AnalyzedQuery[]): string {
25
25
  name: param.name,
26
26
  langName: param.langName,
27
27
  type: param.type,
28
- // §4 metadata — emitted only when set, so `.sql`-tier IR bytes are
29
- // unchanged from before the DSL existed.
30
- ...(param.optional === true ? { optional: true } : {}),
31
- ...(param.group !== undefined ? { group: param.group } : {}),
32
- ...(param.flag === true ? { flag: true } : {}),
33
28
  })),
34
29
  columns: query.columns.map((column) => ({
35
30
  name: column.name,
@@ -62,43 +57,58 @@ export function serializeQueryIr(queries: readonly AnalyzedQuery[]): string {
62
57
  ? { rowKey: query.reactive.rowKey }
63
58
  : {}),
64
59
  },
65
- // §6 knob metadata — emitted only when declared.
66
- ...(query.orderBy !== undefined
67
- ? {
68
- orderBy: {
69
- allowed: query.orderBy.allowed.map((c) => ({
70
- name: c.name,
71
- langName: c.langName,
72
- })),
73
- defaultColumn: query.orderBy.defaultColumn,
74
- defaultDir: query.orderBy.defaultDir,
60
+ ...(query.syql === undefined
61
+ ? {}
62
+ : {
63
+ syql: {
64
+ revision: query.syql.revision,
65
+ inputs: query.syql.inputs.map((input) => {
66
+ if (input.kind === 'value') return { ...input };
67
+ if (input.kind === 'group') {
68
+ return {
69
+ kind: input.kind,
70
+ name: input.name,
71
+ langName: input.langName,
72
+ members: input.members.map((member) => ({ ...member })),
73
+ };
74
+ }
75
+ if (input.kind === 'sort') {
76
+ return {
77
+ kind: input.kind,
78
+ name: input.name,
79
+ langName: input.langName,
80
+ defaultProfile: input.defaultProfile,
81
+ profiles: input.profiles.map((profile) => ({ ...profile })),
82
+ };
83
+ }
84
+ return { ...input };
85
+ }),
86
+ plan: {
87
+ backend: query.syql.plan.backend,
88
+ activationControls: query.syql.plan.activationControls,
89
+ conditions: query.syql.plan.conditions.map((condition) => ({
90
+ controls: condition.controls,
91
+ ...(condition.bind === undefined
92
+ ? {}
93
+ : { bind: condition.bind }),
94
+ })),
95
+ statements: query.syql.plan.statements.map((statement) => ({
96
+ ...(statement.sortProfile === undefined
97
+ ? {}
98
+ : { sortProfile: statement.sortProfile }),
99
+ ...(statement.activationMask === undefined
100
+ ? {}
101
+ : { activationMask: statement.activationMask }),
102
+ sql: statement.sql,
103
+ positionalSql: statement.positionalSql,
104
+ binds: statement.binds.map((bind) => ({ ...bind })),
105
+ })),
106
+ },
107
+ ...(query.syql.identity === undefined
108
+ ? {}
109
+ : { identity: query.syql.identity }),
75
110
  },
76
- }
77
- : {}),
78
- ...(query.limit !== undefined ? { limit: query.limit } : {}),
79
- ...(query.positionalSqlBase !== undefined
80
- ? { positionalSqlBase: query.positionalSqlBase }
81
- : {}),
82
- // §7 variant backend — emitted only when the query opts in.
83
- ...(query.variantGroups !== undefined
84
- ? {
85
- variantGroups: query.variantGroups.map((g) => ({
86
- key: g.key,
87
- params: g.params,
88
- flag: g.flag,
89
- })),
90
- }
91
- : {}),
92
- ...(query.variants !== undefined
93
- ? {
94
- variants: query.variants.map((v) => ({
95
- when: v.when,
96
- sql: v.sql,
97
- positionalSql: v.positionalSql,
98
- params: v.params,
99
- })),
100
- }
101
- : {}),
111
+ }),
102
112
  })),
103
113
  };
104
114
  return `${JSON.stringify(doc, null, 2)}\n`;
package/src/query.ts CHANGED
@@ -98,30 +98,6 @@ export interface QueryParam {
98
98
  readonly type: QueryParamType;
99
99
  /** How the type was resolved — for the docs/tests, not emitted. */
100
100
  readonly source: 'inferred' | 'comment';
101
- /** §4 (DESIGN-queries.md): an optional param — its auto-guarded conjunct
102
- * applies only when it is provided. Always false for the `.sql` tier. */
103
- readonly optional?: boolean;
104
- /** §4: the `from+to` pairing — params sharing a group apply together. */
105
- readonly group?: string;
106
- /** §3: a `: flag` boolean guard param (never in a predicate as written;
107
- * the lowering binds it). Implies optional. */
108
- readonly flag?: boolean;
109
- }
110
-
111
- /** §6 orderBy knob: a generate-time allowlist (identifiers cannot bind). */
112
- export interface QueryOrderBy {
113
- /** Allowed columns: authored SQL name + the language-facing key the
114
- * generated signature accepts. Each is SQLite-checked at generate time. */
115
- readonly allowed: readonly { name: string; langName: string }[];
116
- /** Default column (authored SQL name). */
117
- readonly defaultColumn: string;
118
- readonly defaultDir: 'asc' | 'desc';
119
- }
120
-
121
- /** §6 limit knob: a bound value with a codegen clamp + default. */
122
- export interface QueryLimit {
123
- readonly max?: number;
124
- readonly default?: number;
125
101
  }
126
102
 
127
103
  export interface QueryColumn {
@@ -166,7 +142,7 @@ export interface QueryReactiveMetadata {
166
142
  readonly rowKey?: readonly string[];
167
143
  }
168
144
 
169
- /** §7/§8 backend selection: neutralization (default), always-variants, or
145
+ /** §7/§8 backend selection: neutralization, always-variants, or
170
146
  * the small-N heuristic (`auto`: enumerate at ≤ 2 optional groups). */
171
147
  export type QueryBackend = 'neutralize' | 'variants' | 'auto';
172
148
 
@@ -176,13 +152,121 @@ export interface QueryNamingOptions {
176
152
  /** Emitter targets this run generates — keyword hazards are only real on
177
153
  * targets that exist. */
178
154
  readonly targets: readonly NamingTarget[];
179
- /** `.syql` conditional-lowering backend (default `neutralize`); the
180
- * per-query `variants` knob always forces enumeration. */
155
+ /** `.syql` conditional-lowering policy (default `auto`). */
181
156
  readonly backend?: QueryBackend;
157
+ /** Compiler-reserved physical binds. They retain their exact names and do
158
+ * not participate in public target-language keyword/private-name checks. */
159
+ readonly internalParams?: readonly string[];
182
160
  }
183
161
 
184
162
  const DEFAULT_NAMING: QueryNamingOptions = { naming: 'camel', targets: ['ts'] };
185
163
 
164
+ /** Revision-1 SYQL public inputs. These are deliberately separate from SQL
165
+ * binds: groups and switches are public values without corresponding SQLite
166
+ * parameters, while compiler-generated parameters are never public. */
167
+ export type QuerySyqlPublicInput =
168
+ | {
169
+ readonly kind: 'value';
170
+ readonly name: string;
171
+ readonly langName: string;
172
+ readonly type: QueryParamType;
173
+ readonly nullable: boolean;
174
+ readonly required: boolean;
175
+ }
176
+ | {
177
+ readonly kind: 'group';
178
+ readonly name: string;
179
+ readonly langName: string;
180
+ readonly members: readonly {
181
+ readonly name: string;
182
+ readonly langName: string;
183
+ readonly type: QueryParamType;
184
+ readonly nullable: boolean;
185
+ }[];
186
+ }
187
+ | {
188
+ readonly kind: 'switch';
189
+ readonly name: string;
190
+ readonly langName: string;
191
+ readonly default: false;
192
+ }
193
+ | {
194
+ readonly kind: 'sort';
195
+ readonly name: string;
196
+ readonly langName: string;
197
+ readonly defaultProfile: string;
198
+ readonly profiles: readonly {
199
+ readonly name: string;
200
+ readonly langName: string;
201
+ }[];
202
+ }
203
+ | {
204
+ readonly kind: 'page';
205
+ readonly name: string;
206
+ readonly langName: string;
207
+ readonly defaultSize: number;
208
+ readonly maxSize: number;
209
+ };
210
+
211
+ /** One distinct SQLite bind in a physical revision-1 statement. */
212
+ export type QuerySyqlPlanBind =
213
+ | {
214
+ readonly kind: 'value';
215
+ readonly name: string;
216
+ readonly type: QueryParamType;
217
+ readonly input: string;
218
+ }
219
+ | {
220
+ readonly kind: 'group-member';
221
+ readonly name: string;
222
+ readonly type: QueryParamType;
223
+ readonly input: string;
224
+ readonly member: string;
225
+ }
226
+ | {
227
+ readonly kind: 'condition-active';
228
+ readonly name: string;
229
+ readonly type: 'boolean';
230
+ readonly condition: number;
231
+ readonly controls: readonly string[];
232
+ }
233
+ | {
234
+ readonly kind: 'page';
235
+ readonly name: string;
236
+ readonly type: 'integer';
237
+ readonly input: string;
238
+ };
239
+
240
+ /** One SQLite-checked statement selected by sort profile and, for the
241
+ * enumerated backend, the activation bitmask. */
242
+ export interface QuerySyqlStatement {
243
+ readonly sortProfile?: string;
244
+ readonly activationMask?: number;
245
+ readonly sql: string;
246
+ readonly positionalSql: string;
247
+ readonly binds: readonly QuerySyqlPlanBind[];
248
+ }
249
+
250
+ /** The target-neutral physical plan every emitter must implement exactly. */
251
+ export interface QuerySyqlExecutionPlan {
252
+ readonly backend: Exclude<QueryBackend, 'auto'>;
253
+ /** One bit per optional scalar/group/switch, in declaration order. */
254
+ readonly activationControls: readonly string[];
255
+ readonly conditions: readonly {
256
+ readonly controls: readonly string[];
257
+ /** Present only for neutralized plans. */
258
+ readonly bind?: string;
259
+ }[];
260
+ readonly statements: readonly QuerySyqlStatement[];
261
+ }
262
+
263
+ export interface QuerySyqlMetadata {
264
+ readonly revision: 1;
265
+ readonly inputs: readonly QuerySyqlPublicInput[];
266
+ readonly plan: QuerySyqlExecutionPlan;
267
+ readonly identity?: readonly string[];
268
+ }
269
+
186
270
  export interface AnalyzedQuery {
187
271
  /** camelCase function name (path-derived, or a `-- name:` override). */
188
272
  readonly name: string;
@@ -204,41 +288,9 @@ export interface AnalyzedQuery {
204
288
  /** IR tables this query reads (the useRawSql `{tables}` set), sorted. */
205
289
  readonly tables: readonly string[];
206
290
  readonly reactive: QueryReactiveMetadata;
207
- /** §6 orderBy knob (`.syql` tier). When present, `sql`/`positionalSql`
208
- * carry the DEFAULT order-by tail and `positionalSqlBase` is the static
209
- * prefix emitters compose the selected column onto. */
210
- readonly orderBy?: QueryOrderBy;
211
- /** §6 limit knob (`.syql` tier): the trailing `LIMIT ?` bind's clamp. */
212
- readonly limit?: QueryLimit;
213
- /** positional SQL WITHOUT the dynamic ORDER BY/LIMIT tail; present only
214
- * when `orderBy` is (emitters build `base + ORDER BY <baked> + limit
215
- * tail`). */
216
- readonly positionalSqlBase?: string;
217
- /** The positional limit tail (e.g. ` limit min(coalesce(?, 50), 100)` —
218
- * the default+clamp live IN the SQL, so runtimes bind `limit ?? null`).
219
- * Present only when BOTH knobs are declared (composers need it verbatim). */
220
- readonly positionalLimitTail?: string;
221
- /** §7 variant-enumeration backend (opt-in `variants` knob): one checked
222
- * statement per combination of provided optional groups. Semantically
223
- * identical to the neutralization `sql` by construction; the TS emitter
224
- * dispatches on provided-ness for perfect index use. `when` lists the
225
- * provided group keys; `params` are the DISTINCT param names this variant
226
- * binds, positional order. */
227
- readonly variants?: readonly {
228
- readonly when: readonly string[];
229
- readonly sql: string;
230
- readonly positionalSql: string;
231
- readonly params: readonly string[];
232
- }[];
233
- /** The optional-group keys, in dispatch-bit order (bit i of the variant
234
- * index = group i provided). Present iff `variants` is. */
235
- readonly variantGroups?: readonly {
236
- readonly key: string;
237
- /** Params whose provision defines the group (all must be non-null; a
238
- * flag group is "on" when its single param is TRUE). */
239
- readonly params: readonly string[];
240
- readonly flag: boolean;
241
- }[];
291
+ /** Revision-1 frontend/public-input and physical execution contract. When
292
+ * present, emitters use this instead of treating `params` as the API. */
293
+ readonly syql?: QuerySyqlMetadata;
242
294
  }
243
295
 
244
296
  // -- path → name --------------------------------------------------------------
@@ -502,7 +554,7 @@ function parseCommentParams(file: string, raw: string): CommentParam[] {
502
554
 
503
555
  /** Strip `--` and block comments so identifier scans don't hit commented SQL,
504
556
  * and string literals so `':x'` inside a string isn't read as a param. */
505
- function stripCommentsAndStrings(sql: string): string {
557
+ export function stripCommentsAndStrings(sql: string): string {
506
558
  let out = '';
507
559
  let i = 0;
508
560
  while (i < sql.length) {
@@ -638,7 +690,7 @@ export function toPositionalSql(sql: string): string {
638
690
 
639
691
  // -- FROM/JOIN table resolution ----------------------------------------------
640
692
 
641
- interface TableRef {
693
+ export interface TableRef {
642
694
  readonly table: string;
643
695
  /** Alias (or the table name when un-aliased). */
644
696
  readonly alias: string;
@@ -668,7 +720,7 @@ const RESERVED_ALIAS = new Set([
668
720
  'having',
669
721
  ]);
670
722
 
671
- function scanTableRefs(sql: string, ir: IrDocument): TableRef[] {
723
+ export function scanTableRefs(sql: string, ir: IrDocument): TableRef[] {
672
724
  const cleaned = stripCommentsAndStrings(sql);
673
725
  const known = new Map(ir.tables.map((t) => [t.name, t] as const));
674
726
  const refs: TableRef[] = [];
@@ -855,6 +907,78 @@ function inferParamType(
855
907
  return null;
856
908
  }
857
909
 
910
+ /**
911
+ * Return every revision-1 column/LIKE type constraint for one bind. Unlike
912
+ * `inferParamType` (the legacy first-match helper), SYQL uses the complete set
913
+ * so conflicting checked uses cannot be silently accepted.
914
+ */
915
+ export function inferParamTypeEvidence(
916
+ paramName: string,
917
+ sql: string,
918
+ refs: readonly TableRef[],
919
+ ir: IrDocument,
920
+ ): readonly IrColumnType[] {
921
+ const cleaned = stripCommentsAndStrings(sql);
922
+ const byName = new Map(
923
+ ir.tables.map((table) => [table.name, table] as const),
924
+ );
925
+ const evidence: IrColumnType[] = [];
926
+ const add = (qualifier: string | undefined, column: string): void => {
927
+ if (qualifier !== undefined) {
928
+ const ref = refs.find((candidate) => candidate.alias === qualifier);
929
+ const table = ref === undefined ? undefined : byName.get(ref.table);
930
+ const type = table?.columns.find((item) => item.name === column)?.type;
931
+ if (type !== undefined) evidence.push(type);
932
+ return;
933
+ }
934
+ const matches = refs.flatMap((ref) => {
935
+ const table = byName.get(ref.table);
936
+ const type = table?.columns.find((item) => item.name === column)?.type;
937
+ return type === undefined ? [] : [type];
938
+ });
939
+ if (matches.length === 1) evidence.push(matches[0] as IrColumnType);
940
+ };
941
+
942
+ const comparison = new RegExp(
943
+ `(?:(${IDENT})\\.)?(${IDENT})\\s*(?:=|==|!=|<>|<=|>=|<|>|\\bLIKE\\b|\\bIS\\b)\\s*:${paramName}\\b`,
944
+ 'gi',
945
+ );
946
+ for (const match of cleaned.matchAll(comparison)) {
947
+ add(match[1], match[2] as string);
948
+ }
949
+ const reversed = new RegExp(
950
+ `:${paramName}\\b\\s*(?:=|==|!=|<>|<=|>=|<|>)\\s*(?:(${IDENT})\\.)?(${IDENT})`,
951
+ 'gi',
952
+ );
953
+ for (const match of cleaned.matchAll(reversed)) {
954
+ add(match[1], match[2] as string);
955
+ }
956
+ const inList = new RegExp(
957
+ `(?:(${IDENT})\\.)?(${IDENT})\\s+IN\\s*\\(([^)]*:${paramName}\\b[^)]*)\\)`,
958
+ 'gi',
959
+ );
960
+ for (const match of cleaned.matchAll(inList)) {
961
+ add(match[1], match[2] as string);
962
+ }
963
+ const betweenLeft = new RegExp(
964
+ `(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+:${paramName}\\b`,
965
+ 'gi',
966
+ );
967
+ for (const match of cleaned.matchAll(betweenLeft)) {
968
+ add(match[1], match[2] as string);
969
+ }
970
+ const betweenRight = new RegExp(
971
+ `(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+\\S+\\s+AND\\s+:${paramName}\\b`,
972
+ 'gi',
973
+ );
974
+ for (const match of cleaned.matchAll(betweenRight)) {
975
+ add(match[1], match[2] as string);
976
+ }
977
+ const like = new RegExp(`\\bLIKE\\b[^()]*:${paramName}\\b`, 'gi');
978
+ if (like.test(cleaned)) evidence.push('string');
979
+ return evidence;
980
+ }
981
+
858
982
  // -- fallback column typing (computed expressions) ----------------------------
859
983
 
860
984
  const AGG_RE = /\b(count|sum|total|avg|min|max)\s*\(/i;
@@ -941,11 +1065,7 @@ function inferReactiveMetadata(
941
1065
  if (!requiredAt(match.index)) continue;
942
1066
  const name = match[1] ?? match[2];
943
1067
  const param = name === undefined ? undefined : byParam.get(name);
944
- if (
945
- param !== undefined &&
946
- param.optional !== true &&
947
- param.flag !== true
948
- ) {
1068
+ if (param !== undefined) {
949
1069
  found.push(param.name);
950
1070
  }
951
1071
  }
@@ -956,11 +1076,7 @@ function inferReactiveMetadata(
956
1076
  /:([A-Za-z_][A-Za-z0-9_]*)/g,
957
1077
  )) {
958
1078
  const param = byParam.get(paramMatch[1] as string);
959
- if (
960
- param !== undefined &&
961
- param.optional !== true &&
962
- param.flag !== true
963
- ) {
1079
+ if (param !== undefined) {
964
1080
  found.push(param.name);
965
1081
  }
966
1082
  }
@@ -1214,16 +1330,20 @@ export function analyzeStatement(
1214
1330
  `internal: scanned ${paramNames.length} params (${paramNames.join(', ')}) but SQLite reports ${described.paramsCount}`,
1215
1331
  );
1216
1332
  }
1217
- const paramLangNames = buildNamingMap(
1218
- paramNames,
1333
+ const internalParams = new Set(naming.internalParams ?? []);
1334
+ const publicParamLangNames = buildNamingMap(
1335
+ paramNames.filter((name) => !internalParams.has(name)),
1219
1336
  naming.naming,
1220
1337
  file,
1221
1338
  'params',
1222
1339
  naming.targets,
1223
1340
  );
1341
+ const langNameByParam = new Map(
1342
+ publicParamLangNames.map((mapping) => [mapping.sqlName, mapping.langName]),
1343
+ );
1224
1344
  const commentByName = new Map(commentParams.map((p) => [p.name, p.type]));
1225
- const params: QueryParam[] = paramNames.map((paramName, index) => {
1226
- const langName = paramLangNames[index]?.langName ?? paramName;
1345
+ const params: QueryParam[] = paramNames.map((paramName) => {
1346
+ const langName = langNameByParam.get(paramName) ?? paramName;
1227
1347
  const commented = commentByName.get(paramName);
1228
1348
  if (commented !== undefined) {
1229
1349
  return { name: paramName, langName, type: commented, source: 'comment' };
package/src/syql-lexer.ts CHANGED
@@ -163,10 +163,12 @@ class Lexer {
163
163
  #column = 1;
164
164
  #braceDepth = 0;
165
165
  #expectImportPath = false;
166
+ readonly #recognizeImportPaths: boolean;
166
167
 
167
- constructor(file: string, source: string) {
168
+ constructor(file: string, source: string, recognizeImportPaths = true) {
168
169
  this.#file = file;
169
170
  this.#source = source;
171
+ this.#recognizeImportPaths = recognizeImportPaths;
170
172
  }
171
173
 
172
174
  lex(): readonly SyqlToken[] {
@@ -237,6 +239,7 @@ class Lexer {
237
239
  }
238
240
 
239
241
  if (
242
+ this.#recognizeImportPaths &&
240
243
  token.kind === 'identifier' &&
241
244
  token.text === 'from' &&
242
245
  this.#braceDepth === 0
@@ -512,3 +515,11 @@ export function lexSyqlSource(
512
515
  ): readonly SyqlToken[] {
513
516
  return new Lexer(file, source).lex();
514
517
  }
518
+
519
+ /** Lex an isolated SQL/template string without import-path contextual rules. */
520
+ export function lexSyqlSqlSource(
521
+ file: string,
522
+ source: string,
523
+ ): readonly SyqlToken[] {
524
+ return new Lexer(file, source, false).lex();
525
+ }