@syncular/typegen 0.2.0 → 0.3.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 (51) hide show
  1. package/README.md +25 -10
  2. package/dist/cli.js +155 -4
  3. package/dist/emit-dart.js +3 -2
  4. package/dist/emit-kotlin.js +3 -2
  5. package/dist/emit-queries-dart.js +87 -15
  6. package/dist/emit-queries-kotlin.js +89 -15
  7. package/dist/emit-queries-swift.js +92 -14
  8. package/dist/emit-queries.js +131 -22
  9. package/dist/emit-swift.js +3 -2
  10. package/dist/emit.d.ts +2 -1
  11. package/dist/emit.js +13 -27
  12. package/dist/fmt.d.ts +3 -0
  13. package/dist/fmt.js +356 -0
  14. package/dist/generate.d.ts +16 -6
  15. package/dist/generate.js +34 -9
  16. package/dist/index.d.ts +6 -0
  17. package/dist/index.js +6 -0
  18. package/dist/lower.d.ts +54 -0
  19. package/dist/lower.js +283 -0
  20. package/dist/lsp.d.ts +20 -0
  21. package/dist/lsp.js +376 -0
  22. package/dist/manifest.d.ts +11 -0
  23. package/dist/manifest.js +20 -0
  24. package/dist/naming.d.ts +22 -0
  25. package/dist/naming.js +240 -0
  26. package/dist/query-ir.d.ts +14 -0
  27. package/dist/query-ir.js +69 -0
  28. package/dist/query.d.ts +102 -6
  29. package/dist/query.js +126 -34
  30. package/dist/syql.d.ts +83 -0
  31. package/dist/syql.js +945 -0
  32. package/package.json +4 -4
  33. package/src/cli.ts +155 -4
  34. package/src/emit-dart.ts +3 -2
  35. package/src/emit-kotlin.ts +3 -2
  36. package/src/emit-queries-dart.ts +109 -16
  37. package/src/emit-queries-kotlin.ts +111 -18
  38. package/src/emit-queries-swift.ts +106 -17
  39. package/src/emit-queries.ts +179 -28
  40. package/src/emit-swift.ts +3 -2
  41. package/src/emit.ts +18 -28
  42. package/src/fmt.ts +351 -0
  43. package/src/generate.ts +54 -11
  44. package/src/index.ts +6 -0
  45. package/src/lower.ts +331 -0
  46. package/src/lsp.ts +445 -0
  47. package/src/manifest.ts +38 -0
  48. package/src/naming.ts +271 -0
  49. package/src/query-ir.ts +82 -0
  50. package/src/query.ts +241 -34
  51. package/src/syql.ts +1171 -0
package/dist/emit.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { snakeToCamel } from './naming.js';
1
2
  const TS_TYPE = {
2
3
  string: 'string',
3
4
  integer: 'number',
@@ -82,14 +83,18 @@ function emitSchema(ir) {
82
83
  lines.push('} as const;');
83
84
  return lines.join('\n');
84
85
  }
85
- function emitRowInterfaces(table) {
86
+ function emitRowInterfaces(table, naming) {
86
87
  const type = pascalCase(table.name);
88
+ // §5: mutation-input/row keys are the language-facing names; `mutate`
89
+ // normalizes them back through the schema naming map (camel or snake both
90
+ // bind — one map lookup per key).
91
+ const key = (name) => propertyKey(naming === 'camel' ? snakeToCamel(name) : name);
87
92
  const lines = [];
88
93
  lines.push(`/** One ${table.name} row (§2.4 column order). */`);
89
94
  lines.push(`export interface ${type}Row {`);
90
95
  for (const column of table.columns) {
91
96
  const ts = appTsType(column);
92
- lines.push(` ${propertyKey(column.name)}: ${ts}${column.nullable ? ' | null' : ''};`);
97
+ lines.push(` ${key(column.name)}: ${ts}${column.nullable ? ' | null' : ''};`);
93
98
  }
94
99
  lines.push('}');
95
100
  lines.push('');
@@ -98,8 +103,8 @@ function emitRowInterfaces(table) {
98
103
  for (const column of table.columns) {
99
104
  const ts = appTsType(column);
100
105
  lines.push(column.nullable
101
- ? ` ${propertyKey(column.name)}?: ${ts} | null;`
102
- : ` ${propertyKey(column.name)}: ${ts};`);
106
+ ? ` ${key(column.name)}?: ${ts} | null;`
107
+ : ` ${key(column.name)}: ${ts};`);
103
108
  }
104
109
  lines.push('}');
105
110
  lines.push('');
@@ -108,33 +113,15 @@ function emitRowInterfaces(table) {
108
113
  for (const column of table.columns) {
109
114
  const ts = appTsType(column);
110
115
  if (column.name === table.primaryKey) {
111
- lines.push(` ${propertyKey(column.name)}: ${ts};`);
116
+ lines.push(` ${key(column.name)}: ${ts};`);
112
117
  }
113
118
  else {
114
- lines.push(` ${propertyKey(column.name)}?: ${ts}${column.nullable ? ' | null' : ''};`);
119
+ lines.push(` ${key(column.name)}?: ${ts}${column.nullable ? ' | null' : ''};`);
115
120
  }
116
121
  }
117
122
  lines.push('}');
118
123
  return lines.join('\n');
119
124
  }
120
- /**
121
- * A Kysely-compatible `Database` interface: table name → Row type. This is
122
- * the generic `@syncular/kysely`'s `SyncularDialect` is parameterized by
123
- * (`new Kysely<Database>({ dialect })`). Additive — the Row interfaces it
124
- * references are already emitted above. Property keys use the raw table name
125
- * (quoted when not an identifier) so `db.selectFrom('table-name')` types.
126
- */
127
- function emitDatabaseInterface(ir) {
128
- const lines = [];
129
- lines.push('/** Kysely `Database` interface (table → Row); the generic for');
130
- lines.push(" * @syncular/kysely's SyncularDialect. */");
131
- lines.push('export interface Database {');
132
- for (const table of ir.tables) {
133
- lines.push(` ${propertyKey(table.name)}: ${pascalCase(table.name)}Row;`);
134
- }
135
- lines.push('}');
136
- return lines.join('\n');
137
- }
138
125
  function emitSubscription(sub) {
139
126
  const params = [];
140
127
  for (const scope of sub.scopes) {
@@ -172,7 +159,7 @@ function emitSubscription(sub) {
172
159
  lines.push('} as const;');
173
160
  return lines.join('\n');
174
161
  }
175
- export function emitModule(ir, hash) {
162
+ export function emitModule(ir, hash, naming = 'camel') {
176
163
  const parts = [];
177
164
  parts.push([
178
165
  '// Generated by @syncular/typegen — DO NOT EDIT.',
@@ -181,9 +168,8 @@ export function emitModule(ir, hash) {
181
168
  ].join('\n'));
182
169
  parts.push(emitSchema(ir));
183
170
  for (const table of ir.tables) {
184
- parts.push(emitRowInterfaces(table));
171
+ parts.push(emitRowInterfaces(table, naming));
185
172
  }
186
- parts.push(emitDatabaseInterface(ir));
187
173
  for (const sub of ir.subscriptions) {
188
174
  parts.push(emitSubscription(sub));
189
175
  }
package/dist/fmt.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** Format one `.syql` source file into its canonical form. Throws
2
+ * {@link TypegenError} when the source does not parse. */
3
+ export declare function formatSyql(file: string, source: string): string;
package/dist/fmt.js ADDED
@@ -0,0 +1,356 @@
1
+ /**
2
+ * `syncular fmt` — the canonical `.syql` formatter (DESIGN-queries.md §10,
3
+ * §12). One style, no options:
4
+ *
5
+ * - SQL keywords lowercase; identifier casing preserved (SQL-truth).
6
+ * - one space after commas; collapsed whitespace elsewhere.
7
+ * - one clause per line in the body; WHERE conjuncts one per line,
8
+ * `and`-prefixed and indented under the `where`.
9
+ * - declarations separated by one blank line; knobs on their own lines.
10
+ * - comments are preserved: a declaration's leading comment block stays
11
+ * above it; comments inside bodies stay on their own lines.
12
+ *
13
+ * The formatter is built on the same parser as the generator (`.syql` that
14
+ * does not parse does not format), and formatting is semantics-preserving
15
+ * by construction — `fmt` output re-parses to the same declarations.
16
+ */
17
+ import { TypegenError } from './errors.js';
18
+ import { parseSyqlFile } from './syql.js';
19
+ /** SQL keywords the canon lowercases (a pragmatic core set — identifiers
20
+ * that collide with these would need quoting anyway). */
21
+ const SQL_KEYWORDS = new Set(('select from where group by having order limit offset window and or not ' +
22
+ 'in is null like glob between exists case when then else end as on using ' +
23
+ 'join left right full outer inner cross natural union all except ' +
24
+ 'intersect distinct values with recursive asc desc collate cast').split(' '));
25
+ /** Tokenize a body: strings/comments verbatim, words, `:params`,
26
+ * `@fragment` refs, single punctuation chars. */
27
+ function tokenize(body) {
28
+ const tokens = [];
29
+ let i = 0;
30
+ while (i < body.length) {
31
+ const ch = body[i];
32
+ if (/\s/.test(ch)) {
33
+ i += 1;
34
+ continue;
35
+ }
36
+ if (body.startsWith('--', i)) {
37
+ const nl = body.indexOf('\n', i);
38
+ const end = nl === -1 ? body.length : nl;
39
+ tokens.push({ kind: 'comment', text: body.slice(i, end).trimEnd() });
40
+ i = end;
41
+ continue;
42
+ }
43
+ if (body.startsWith('/*', i)) {
44
+ const close = body.indexOf('*/', i + 2);
45
+ const end = close === -1 ? body.length : close + 2;
46
+ tokens.push({ kind: 'comment', text: body.slice(i, end) });
47
+ i = end;
48
+ continue;
49
+ }
50
+ if (ch === "'") {
51
+ let j = i + 1;
52
+ for (;;) {
53
+ if (j >= body.length)
54
+ break;
55
+ if (body[j] === "'") {
56
+ if (body[j + 1] === "'")
57
+ j += 2;
58
+ else {
59
+ j += 1;
60
+ break;
61
+ }
62
+ }
63
+ else
64
+ j += 1;
65
+ }
66
+ tokens.push({ kind: 'string', text: body.slice(i, j) });
67
+ i = j;
68
+ continue;
69
+ }
70
+ if (ch === ':' && /[A-Za-z_]/.test(body[i + 1] ?? '')) {
71
+ let j = i + 1;
72
+ while (j < body.length && /[A-Za-z0-9_]/.test(body[j]))
73
+ j += 1;
74
+ tokens.push({ kind: 'param', text: body.slice(i, j) });
75
+ i = j;
76
+ continue;
77
+ }
78
+ if (ch === '@' && /[A-Za-z_]/.test(body[i + 1] ?? '')) {
79
+ let j = i + 1;
80
+ while (j < body.length && /[A-Za-z0-9_]/.test(body[j]))
81
+ j += 1;
82
+ tokens.push({ kind: 'fragment', text: body.slice(i, j) });
83
+ i = j;
84
+ continue;
85
+ }
86
+ if (/[A-Za-z_]/.test(ch)) {
87
+ let j = i + 1;
88
+ while (j < body.length && /[A-Za-z0-9_]/.test(body[j]))
89
+ j += 1;
90
+ tokens.push({ kind: 'word', text: body.slice(i, j) });
91
+ i = j;
92
+ continue;
93
+ }
94
+ // Multi-char operators stay one token (`||`, `>=`, `<=`, `<>`, `!=`, `==`).
95
+ const two = body.slice(i, i + 2);
96
+ if (['||', '>=', '<=', '<>', '!=', '=='].includes(two)) {
97
+ tokens.push({ kind: 'punct', text: two });
98
+ i += 2;
99
+ continue;
100
+ }
101
+ tokens.push({ kind: 'punct', text: ch });
102
+ i += 1;
103
+ }
104
+ return tokens;
105
+ }
106
+ /** Render tokens back to one line with canonical spacing + keyword case. */
107
+ function render(tokens) {
108
+ let out = '';
109
+ let prev;
110
+ for (const token of tokens) {
111
+ let text = token.text;
112
+ if (token.kind === 'word' && SQL_KEYWORDS.has(text.toLowerCase())) {
113
+ text = text.toLowerCase();
114
+ }
115
+ const glueLeft = token.kind === 'punct' &&
116
+ (text === ',' || text === ')' || text === ';' || text === '.');
117
+ const glueRight = prev !== undefined &&
118
+ ((prev.kind === 'punct' && (prev.text === '(' || prev.text === '.')) ||
119
+ prev.kind === 'fragment');
120
+ if (out.length > 0 && !glueLeft && !glueRight)
121
+ out += ' ';
122
+ out += text;
123
+ prev = token;
124
+ }
125
+ return out;
126
+ }
127
+ /** Clause keywords that start a new line at paren depth 0. */
128
+ const CLAUSE_STARTERS = new Set([
129
+ 'select',
130
+ 'from',
131
+ 'where',
132
+ 'group',
133
+ 'having',
134
+ 'order',
135
+ 'limit',
136
+ 'window',
137
+ 'union',
138
+ 'except',
139
+ 'intersect',
140
+ ]);
141
+ /** Format a body: one clause per line; WHERE conjuncts one per line. */
142
+ function formatBody(body) {
143
+ const tokens = tokenize(body);
144
+ const lines = [];
145
+ let current = [];
146
+ let depth = 0;
147
+ let braceDepth = 0;
148
+ let inWhere = false;
149
+ let pendingBetween = 0;
150
+ const flush = () => {
151
+ if (current.length > 0)
152
+ lines.push(render(current));
153
+ current = [];
154
+ };
155
+ for (const token of tokens) {
156
+ if (token.kind === 'comment') {
157
+ flush();
158
+ lines.push(token.text);
159
+ continue;
160
+ }
161
+ if (token.kind === 'punct') {
162
+ if (token.text === '(')
163
+ depth += 1;
164
+ else if (token.text === ')')
165
+ depth -= 1;
166
+ else if (token.text === '{')
167
+ braceDepth += 1;
168
+ else if (token.text === '}')
169
+ braceDepth -= 1;
170
+ }
171
+ if (token.kind === 'word' && depth === 0 && braceDepth === 0) {
172
+ const word = token.text.toLowerCase();
173
+ if (CLAUSE_STARTERS.has(word)) {
174
+ flush();
175
+ inWhere = word === 'where';
176
+ pendingBetween = 0;
177
+ }
178
+ else if (inWhere) {
179
+ if (word === 'between')
180
+ pendingBetween += 1;
181
+ else if (word === 'and') {
182
+ if (pendingBetween > 0)
183
+ pendingBetween -= 1;
184
+ else
185
+ flush();
186
+ }
187
+ }
188
+ }
189
+ current.push(token);
190
+ }
191
+ flush();
192
+ // Indent: clauses flush-left; WHERE continuation (`and …`) indented 2.
193
+ return lines.map((line) => /^(and|or)\b/.test(line) || line.startsWith('--') ? ` ${line}` : line);
194
+ }
195
+ function formatSignature(decl) {
196
+ const parts = [];
197
+ const seenGroups = new Set();
198
+ for (const param of decl.params) {
199
+ if (param.group !== undefined) {
200
+ if (seenGroups.has(param.group))
201
+ continue;
202
+ seenGroups.add(param.group);
203
+ const members = decl.params.filter((p) => p.group === param.group);
204
+ parts.push(`${members.map((p) => p.name).join('+')}?`);
205
+ continue;
206
+ }
207
+ if (param.flag)
208
+ parts.push(`${param.name}?: flag`);
209
+ else
210
+ parts.push(`${param.name}${param.optional ? '?' : ''}`);
211
+ }
212
+ return parts.join(', ');
213
+ }
214
+ /** The leading comment block (verbatim lines) before each declaration. */
215
+ function leadingComments(source) {
216
+ // Map from declaration index (0-based, in order of appearance) to its
217
+ // preceding comment lines.
218
+ const out = new Map();
219
+ let pending = [];
220
+ let declIndex = 0;
221
+ let i = 0;
222
+ while (i < source.length) {
223
+ const ch = source[i];
224
+ if (/\s/.test(ch)) {
225
+ i += 1;
226
+ continue;
227
+ }
228
+ if (source.startsWith('--', i)) {
229
+ const nl = source.indexOf('\n', i);
230
+ const end = nl === -1 ? source.length : nl;
231
+ pending.push(source.slice(i, end).trimEnd());
232
+ i = end;
233
+ continue;
234
+ }
235
+ if (source.startsWith('/*', i)) {
236
+ const close = source.indexOf('*/', i + 2);
237
+ const end = close === -1 ? source.length : close + 2;
238
+ pending.push(source.slice(i, end));
239
+ i = end;
240
+ continue;
241
+ }
242
+ // A declaration keyword: attach pending comments, then skip through its
243
+ // brace block.
244
+ if (pending.length > 0) {
245
+ out.set(declIndex, pending);
246
+ pending = [];
247
+ }
248
+ declIndex += 1;
249
+ const brace = source.indexOf('{', i);
250
+ if (brace === -1)
251
+ break;
252
+ let depth = 0;
253
+ let j = brace;
254
+ while (j < source.length) {
255
+ const c = source[j];
256
+ if (c === "'") {
257
+ j += 1;
258
+ while (j < source.length && source[j] !== "'")
259
+ j += 1;
260
+ }
261
+ else if (source.startsWith('--', j)) {
262
+ const nl = source.indexOf('\n', j);
263
+ j = nl === -1 ? source.length : nl;
264
+ }
265
+ else if (c === '{')
266
+ depth += 1;
267
+ else if (c === '}') {
268
+ depth -= 1;
269
+ if (depth === 0) {
270
+ j += 1;
271
+ break;
272
+ }
273
+ }
274
+ j += 1;
275
+ }
276
+ i = j;
277
+ }
278
+ return out;
279
+ }
280
+ /** Format one `.syql` source file into its canonical form. Throws
281
+ * {@link TypegenError} when the source does not parse. */
282
+ export function formatSyql(file, source) {
283
+ const parsed = parseSyqlFile(file, source);
284
+ const comments = leadingComments(source);
285
+ const decls = [];
286
+ // Re-derive declaration order (fragments/queries interleave in-source);
287
+ // parseSyqlFile keeps per-kind order, so re-scan the source for kind
288
+ // keywords to interleave faithfully.
289
+ const orderRe = /\b(query|fragment)\s+([A-Za-z][A-Za-z0-9]*)/g;
290
+ const blanked = source
291
+ .replace(/--[^\n]*/g, (m) => ' '.repeat(m.length))
292
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
293
+ .replace(/'(?:[^']|'')*'/g, (m) => ' '.repeat(m.length));
294
+ const sequence = [];
295
+ let braceDepth = 0;
296
+ let lastIndex = 0;
297
+ for (let i = 0; i < blanked.length; i++) {
298
+ const c = blanked[i];
299
+ if (c === '{')
300
+ braceDepth += 1;
301
+ else if (c === '}')
302
+ braceDepth -= 1;
303
+ else if (braceDepth === 0) {
304
+ orderRe.lastIndex = i;
305
+ const m = orderRe.exec(blanked);
306
+ if (m !== null && m.index === i) {
307
+ sequence.push({ kind: m[1], name: m[2] });
308
+ i = orderRe.lastIndex - 1;
309
+ lastIndex = orderRe.lastIndex;
310
+ }
311
+ }
312
+ }
313
+ void lastIndex;
314
+ sequence.forEach((entry, index) => {
315
+ const lines = [];
316
+ const leading = comments.get(index);
317
+ if (leading !== undefined)
318
+ lines.push(...leading);
319
+ if (entry.kind === 'fragment') {
320
+ const decl = parsed.fragments.find((f) => f.name === entry.name);
321
+ if (decl === undefined)
322
+ return;
323
+ lines.push(`fragment ${decl.name}(${formatSignature(decl)}) {`);
324
+ for (const line of formatBody(decl.body))
325
+ lines.push(` ${line}`);
326
+ lines.push('}');
327
+ }
328
+ else {
329
+ const decl = parsed.queries.find((q) => q.name === entry.name);
330
+ if (decl === undefined)
331
+ return;
332
+ const knobs = [];
333
+ if (decl.orderBy !== undefined) {
334
+ const dir = decl.orderBy.defaultDir === 'desc' ? ' desc' : '';
335
+ knobs.push(` orderBy ${decl.orderBy.allowed.join(' | ')} default ${decl.orderBy.defaultColumn}${dir}`);
336
+ }
337
+ if (decl.limit !== undefined) {
338
+ const parts = [];
339
+ if (decl.limit.max !== undefined)
340
+ parts.push(`max ${decl.limit.max}`);
341
+ if (decl.limit.default !== undefined) {
342
+ parts.push(`default ${decl.limit.default}`);
343
+ }
344
+ knobs.push(` limit ${parts.join(' ')}`);
345
+ }
346
+ if (decl.variants === true)
347
+ knobs.push(' variants');
348
+ lines.push(`query ${decl.name}(${formatSignature(decl)})${knobs.length > 0 ? `\n${knobs.join('\n')}\n{` : ' {'}`);
349
+ for (const line of formatBody(decl.body))
350
+ lines.push(` ${line}`);
351
+ lines.push('}');
352
+ }
353
+ decls.push({ order: index, text: lines.join('\n') });
354
+ });
355
+ return `${decls.map((d) => d.text).join('\n\n')}\n`;
356
+ }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { type IrDocument } from './ir.js';
6
6
  import { type Manifest } from './manifest.js';
7
- import { type AnalyzedQuery } from './query.js';
7
+ import { type AnalyzedQuery, type QueryDb, type QueryNamingOptions } from './query.js';
8
8
  export interface MigrationInput {
9
9
  /** Directory name, e.g. `0001_initial`. */
10
10
  readonly name: string;
@@ -15,19 +15,29 @@ export interface MigrationInput {
15
15
  export declare function buildIr(manifest: Manifest, migrations: readonly MigrationInput[]): IrDocument;
16
16
  /** Load `NNNN_name/up.sql` migrations, ordered by numeric prefix. */
17
17
  export declare function loadMigrations(migrationsDir: string): MigrationInput[];
18
- /** One `.sql` named-query file: path (relative to the queries root, forward
19
- * slashes) + raw contents. A file may hold multiple statements. */
18
+ /** One named-query file (`.sql` the compatibility floor — or `.syql`, the
19
+ * DSL): path relative to the queries root (forward slashes) + raw contents.
20
+ * A file may hold multiple statements/declarations. */
20
21
  export interface QueryInput {
21
22
  /** Path relative to the queries root, forward-slashed (e.g.
22
- * `billing/invoices/list.sql`). Drives the path-derived default name. */
23
+ * `billing/invoices/list.sql`). Drives the path-derived default name for
24
+ * `.sql`; `.syql` queries carry their own names. */
23
25
  readonly file: string;
24
26
  readonly sql: string;
25
27
  }
26
- /** Load `queries/**\/*.sql` recursively, ordered by relative path
28
+ /** Load `queries/**\/*.{sql,syql}` recursively, ordered by relative path
27
29
  * (deterministic emission). Folders are pure organization. */
28
30
  export declare function loadQueries(queriesDir: string): QueryInput[];
31
+ /** A {@link QueryDb} backed by an in-memory bun:sqlite built from the IR DDL.
32
+ * `analyze` prepares the statement (validating references) and reads its
33
+ * column names + declared types; it executes once against the empty DB so
34
+ * `declaredTypes` (decltype) is populated. */
35
+ export declare function makeQueryDb(ir: IrDocument): {
36
+ db: QueryDb;
37
+ close: () => void;
38
+ };
29
39
  /** Analyze every query file against the IR (SELECT-only, typed, deps). */
30
- export declare function analyzeQueries(ir: IrDocument, queries: readonly QueryInput[]): AnalyzedQuery[];
40
+ export declare function analyzeQueries(ir: IrDocument, queries: readonly QueryInput[], naming?: QueryNamingOptions): AnalyzedQuery[];
31
41
  /** One generated artifact: an absolute output path and its exact bytes. */
32
42
  export interface GeneratedOutput {
33
43
  readonly path: string;
package/dist/generate.js CHANGED
@@ -16,8 +16,10 @@ import { emitSwiftModule } from './emit-swift.js';
16
16
  import { TypegenError } from './errors.js';
17
17
  import { canonicalizeExtensions, IR_VERSION, irHash, serializeIr, } from './ir.js';
18
18
  import { MANIFEST_FILENAME, parseManifest, } from './manifest.js';
19
+ import { buildNamingMap } from './naming.js';
19
20
  import { analyzeQueryFile, synthesizeDdl, } from './query.js';
20
21
  import { applyMigrationSql } from './sql.js';
22
+ import { analyzeSyqlFile } from './syql.js';
21
23
  /** Same grammar the server compiles (§3.1): `prefix:{variable}`. */
22
24
  const PATTERN_RE = /^([^{}]+):\{([^{}:]+)\}$/;
23
25
  /** Whole-value placeholder: `{paramName}`. */
@@ -208,8 +210,8 @@ export function loadMigrations(migrationsDir) {
208
210
  return { name, sql: readFileSync(upPath, 'utf8') };
209
211
  });
210
212
  }
211
- /** Recursively collect `*.sql` paths under `dir`, relative to `root`
212
- * (forward-slashed), sorted for deterministic emission. */
213
+ /** Recursively collect `*.sql` + `*.syql` paths under `dir`, relative to
214
+ * `root` (forward-slashed), sorted for deterministic emission. */
213
215
  function collectSqlFiles(root, dir) {
214
216
  const out = [];
215
217
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -217,13 +219,14 @@ function collectSqlFiles(root, dir) {
217
219
  if (entry.isDirectory()) {
218
220
  out.push(...collectSqlFiles(root, abs));
219
221
  }
220
- else if (entry.isFile() && entry.name.endsWith('.sql')) {
222
+ else if (entry.isFile() &&
223
+ (entry.name.endsWith('.sql') || entry.name.endsWith('.syql'))) {
221
224
  out.push(relative(root, abs).split(sep).join('/'));
222
225
  }
223
226
  }
224
227
  return out;
225
228
  }
226
- /** Load `queries/**\/*.sql` recursively, ordered by relative path
229
+ /** Load `queries/**\/*.{sql,syql}` recursively, ordered by relative path
227
230
  * (deterministic emission). Folders are pure organization. */
228
231
  export function loadQueries(queriesDir) {
229
232
  if (!existsSync(queriesDir))
@@ -239,7 +242,7 @@ export function loadQueries(queriesDir) {
239
242
  * `analyze` prepares the statement (validating references) and reads its
240
243
  * column names + declared types; it executes once against the empty DB so
241
244
  * `declaredTypes` (decltype) is populated. */
242
- function makeQueryDb(ir) {
245
+ export function makeQueryDb(ir) {
243
246
  const sqlite = new Database(':memory:');
244
247
  sqlite.run(synthesizeDdl(ir));
245
248
  const db = {
@@ -262,13 +265,17 @@ function makeQueryDb(ir) {
262
265
  return { db, close: () => sqlite.close() };
263
266
  }
264
267
  /** Analyze every query file against the IR (SELECT-only, typed, deps). */
265
- export function analyzeQueries(ir, queries) {
268
+ export function analyzeQueries(ir, queries, naming) {
266
269
  if (queries.length === 0)
267
270
  return [];
268
271
  const { db, close } = makeQueryDb(ir);
269
272
  try {
270
273
  // Each file may hold multiple statements; flatten in path order (stable).
271
- const analyzed = queries.flatMap((q) => analyzeQueryFile(q.file, q.sql, ir, db));
274
+ // Frontends dispatch on extension; both produce the same AnalyzedQuery
275
+ // shape (§1 — nothing downstream knows the frontend).
276
+ const analyzed = queries.flatMap((q) => q.file.endsWith('.syql')
277
+ ? analyzeSyqlFile(q.file, q.sql, ir, db, naming)
278
+ : analyzeQueryFile(q.file, q.sql, ir, db, naming));
272
279
  // Names must be unique across the whole manifest — the filesystem no longer
273
280
  // guarantees uniqueness once `-- name:` overrides exist. Report BOTH source
274
281
  // locations (file + statement position) on a collision.
@@ -302,9 +309,27 @@ export function generate(manifestDir) {
302
309
  const manifest = parseManifest(raw);
303
310
  const migrations = loadMigrations(resolve(manifestDir, manifest.migrations));
304
311
  const ir = buildIr(manifest, migrations);
312
+ // §5/§12 naming: the emitter targets this run generates (keyword hazards
313
+ // are only real on targets that exist), and the per-table collision check
314
+ // for the generated row types.
315
+ const targets = ['ts'];
316
+ if (manifest.output.swift !== undefined)
317
+ targets.push('swift');
318
+ if (manifest.output.kotlin !== undefined)
319
+ targets.push('kotlin');
320
+ if (manifest.output.dart !== undefined)
321
+ targets.push('dart');
322
+ const naming = {
323
+ naming: manifest.naming,
324
+ targets,
325
+ backend: manifest.queryBackend,
326
+ };
327
+ for (const table of ir.tables) {
328
+ buildNamingMap(table.columns.map((c) => c.name), manifest.naming, MANIFEST_FILENAME, `table ${table.name}`, targets);
329
+ }
305
330
  const irJson = serializeIr(ir);
306
331
  const hash = irHash(irJson);
307
- const module = emitModule(ir, hash);
332
+ const module = emitModule(ir, hash, manifest.naming);
308
333
  const irPath = resolve(manifestDir, manifest.output.ir);
309
334
  const modulePath = resolve(manifestDir, manifest.output.module);
310
335
  const outputs = [
@@ -321,7 +346,7 @@ export function generate(manifestDir) {
321
346
  kotlin?.queriesPath !== undefined ||
322
347
  dart?.queriesPath !== undefined;
323
348
  const analyzedQueries = wantsQueries
324
- ? analyzeQueries(ir, loadQueries(resolve(manifestDir, manifest.queries)))
349
+ ? analyzeQueries(ir, loadQueries(resolve(manifestDir, manifest.queries)), naming)
325
350
  : [];
326
351
  if (wantsQueries && analyzedQueries.length === 0) {
327
352
  throw new TypegenError(resolve(manifestDir, manifest.queries), `an output requests named queries but no .sql files were found in ${manifest.queries} — add a query file or drop the queries output`);
package/dist/index.d.ts CHANGED
@@ -12,8 +12,14 @@ export * from './emit-queries-kotlin.js';
12
12
  export * from './emit-queries-swift.js';
13
13
  export * from './emit-swift.js';
14
14
  export * from './errors.js';
15
+ export * from './fmt.js';
15
16
  export * from './generate.js';
16
17
  export * from './ir.js';
18
+ export * from './lower.js';
19
+ export * from './lsp.js';
17
20
  export * from './manifest.js';
21
+ export * from './naming.js';
18
22
  export * from './query.js';
23
+ export * from './query-ir.js';
19
24
  export * from './sql.js';
25
+ export * from './syql.js';
package/dist/index.js CHANGED
@@ -12,8 +12,14 @@ export * from './emit-queries-kotlin.js';
12
12
  export * from './emit-queries-swift.js';
13
13
  export * from './emit-swift.js';
14
14
  export * from './errors.js';
15
+ export * from './fmt.js';
15
16
  export * from './generate.js';
16
17
  export * from './ir.js';
18
+ export * from './lower.js';
19
+ export * from './lsp.js';
17
20
  export * from './manifest.js';
21
+ export * from './naming.js';
18
22
  export * from './query.js';
23
+ export * from './query-ir.js';
19
24
  export * from './sql.js';
25
+ export * from './syql.js';