@syncular/typegen 0.2.1 → 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 +2 -2
  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
@@ -0,0 +1,54 @@
1
+ import type { IrDocument } from './ir.js';
2
+ /** One top-level SELECT-list item, verbatim. */
3
+ interface ProjectionItem {
4
+ /** The item's exact source text, trimmed. */
5
+ readonly text: string;
6
+ }
7
+ interface ProjectionSpan {
8
+ /** Offset of the first list character (after SELECT [DISTINCT|ALL]). */
9
+ readonly start: number;
10
+ /** Offset just past the list (at the `FROM` keyword). */
11
+ readonly end: number;
12
+ readonly items: readonly ProjectionItem[];
13
+ }
14
+ /**
15
+ * Scan `sql` for the outermost SELECT list: the first paren-depth-0 `SELECT`
16
+ * keyword up to the first depth-0 `FROM` after it. String literals, quoted
17
+ * identifiers, and comments are skipped; a CTE's inner selects sit at depth
18
+ * ≥ 1 so `WITH … SELECT a FROM x` resolves to the outer list. Returns null
19
+ * when there is no depth-0 SELECT…FROM shape (e.g. `SELECT 1`).
20
+ */
21
+ export declare function findProjection(sql: string): ProjectionSpan | null;
22
+ /**
23
+ * The main verb of a `WITH …` statement (uppercased): SQLite allows a
24
+ * with-clause before SELECT and before INSERT/UPDATE/DELETE. CTE bodies live
25
+ * inside parentheses and a bare keyword cannot be a CTE name, so the first
26
+ * paren-depth-0 keyword after `WITH` is the main verb.
27
+ */
28
+ export declare function mainVerbAfterWith(sql: string): string | undefined;
29
+ /** A `FROM`/`JOIN` base-table reference the caller already resolved. */
30
+ export interface LowerTableRef {
31
+ readonly table: string;
32
+ readonly alias: string;
33
+ }
34
+ export interface LoweredProjection {
35
+ /** The rewritten SQL (identical to the input when nothing needed
36
+ * aliasing). */
37
+ readonly sql: string;
38
+ /** Whether any item was rewritten or expanded. */
39
+ readonly changed: boolean;
40
+ /** Pre-rewrite result names — the SQL-truth names the IR records. */
41
+ readonly sqlNames: readonly string[];
42
+ /** Post-rewrite result names — index-aligned with `sqlNames`. */
43
+ readonly langNames: readonly string[];
44
+ }
45
+ /**
46
+ * Rewrite the top-level projection of `sql` so every result column's runtime
47
+ * key equals its language-facing name. `analyze` prepares a candidate SQL
48
+ * and returns its result column names; `buildMap` maps the star-expanded
49
+ * projection's SQL-truth result names to language names (owning the §12
50
+ * collision/keyword checks). Re-checking the returned SQL is the caller's
51
+ * job.
52
+ */
53
+ export declare function lowerProjection(sql: string, refs: readonly LowerTableRef[], ir: IrDocument, location: string, analyze: (sql: string) => readonly string[], buildMap: (sqlNames: readonly string[]) => readonly string[]): LoweredProjection;
54
+ export {};
package/dist/lower.js ADDED
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Projection lowering (DESIGN-queries.md §5): rewrite a query's top-level
3
+ * SELECT list so the runtime result keys ARE the language-facing names —
4
+ * `select created_at from todos` lowers to
5
+ * `select created_at as createdAt from todos` under camelCase naming, so
6
+ * drivers return camel keys with no runtime mapping loop. Author-written
7
+ * aliases are the SQL-truth result name and convention-map like any column.
8
+ *
9
+ * The rewrite is text-surgical and verbatim-preserving: we locate the
10
+ * outermost (paren-depth-0) `SELECT … FROM` span with a string/comment-aware
11
+ * scan, split the list on top-level commas keeping each item's original
12
+ * text, and only touch the alias tail of items whose result name changes.
13
+ * `*` / `t.*` items are EXPANDED from the schema IR (single-table `*` only)
14
+ * — which also pins the projection against hidden local columns. After the
15
+ * rewrite the query is re-checked by SQLite; a projection this pass cannot
16
+ * rewrite (and that needs rewriting) is a generate-time error pointing at
17
+ * the manual-alias escape hatch.
18
+ */
19
+ import { TypegenError } from './errors.js';
20
+ const WORD_RE = /[A-Za-z_]/;
21
+ /**
22
+ * Scan `sql` for the outermost SELECT list: the first paren-depth-0 `SELECT`
23
+ * keyword up to the first depth-0 `FROM` after it. String literals, quoted
24
+ * identifiers, and comments are skipped; a CTE's inner selects sit at depth
25
+ * ≥ 1 so `WITH … SELECT a FROM x` resolves to the outer list. Returns null
26
+ * when there is no depth-0 SELECT…FROM shape (e.g. `SELECT 1`).
27
+ */
28
+ export function findProjection(sql) {
29
+ let depth = 0;
30
+ let i = 0;
31
+ let listStart = -1;
32
+ let seenSelect = false;
33
+ const itemBounds = []; // top-level comma offsets within the list
34
+ while (i < sql.length) {
35
+ const ch = sql[i];
36
+ const next = sql[i + 1];
37
+ if (ch === '-' && next === '-') {
38
+ const end = sql.indexOf('\n', i);
39
+ i = end === -1 ? sql.length : end + 1;
40
+ continue;
41
+ }
42
+ if (ch === '/' && next === '*') {
43
+ const end = sql.indexOf('*/', i + 2);
44
+ i = end === -1 ? sql.length : end + 2;
45
+ continue;
46
+ }
47
+ if (ch === "'" || ch === '"') {
48
+ const quote = ch;
49
+ let j = i + 1;
50
+ for (;;) {
51
+ if (j >= sql.length)
52
+ break;
53
+ if (sql[j] === quote) {
54
+ if (sql[j + 1] === quote)
55
+ j += 2;
56
+ else {
57
+ j += 1;
58
+ break;
59
+ }
60
+ }
61
+ else
62
+ j += 1;
63
+ }
64
+ i = j;
65
+ continue;
66
+ }
67
+ if (ch === '(') {
68
+ depth += 1;
69
+ i += 1;
70
+ continue;
71
+ }
72
+ if (ch === ')') {
73
+ depth -= 1;
74
+ i += 1;
75
+ continue;
76
+ }
77
+ if (depth === 0 && ch === ',' && seenSelect && listStart !== -1) {
78
+ itemBounds.push(i);
79
+ i += 1;
80
+ continue;
81
+ }
82
+ if (WORD_RE.test(ch)) {
83
+ let j = i + 1;
84
+ while (j < sql.length && /[A-Za-z0-9_]/.test(sql[j]))
85
+ j += 1;
86
+ const word = sql.slice(i, j).toUpperCase();
87
+ if (depth === 0 && !seenSelect && word === 'SELECT') {
88
+ seenSelect = true;
89
+ // Skip an immediate DISTINCT / ALL quantifier.
90
+ let k = j;
91
+ while (k < sql.length && /\s/.test(sql[k]))
92
+ k += 1;
93
+ let m = k;
94
+ while (m < sql.length && /[A-Za-z]/.test(sql[m]))
95
+ m += 1;
96
+ const quant = sql.slice(k, m).toUpperCase();
97
+ listStart = quant === 'DISTINCT' || quant === 'ALL' ? m : j;
98
+ i = listStart;
99
+ continue;
100
+ }
101
+ if (depth === 0 && seenSelect && listStart !== -1 && word === 'FROM') {
102
+ const items = [];
103
+ let prev = listStart;
104
+ for (const comma of itemBounds) {
105
+ items.push({ text: sql.slice(prev, comma).trim() });
106
+ prev = comma + 1;
107
+ }
108
+ items.push({ text: sql.slice(prev, i).trim() });
109
+ return { start: listStart, end: i, items };
110
+ }
111
+ i = j;
112
+ continue;
113
+ }
114
+ i += 1;
115
+ }
116
+ return null;
117
+ }
118
+ /**
119
+ * The main verb of a `WITH …` statement (uppercased): SQLite allows a
120
+ * with-clause before SELECT and before INSERT/UPDATE/DELETE. CTE bodies live
121
+ * inside parentheses and a bare keyword cannot be a CTE name, so the first
122
+ * paren-depth-0 keyword after `WITH` is the main verb.
123
+ */
124
+ export function mainVerbAfterWith(sql) {
125
+ const MAIN_VERBS = new Set([
126
+ 'SELECT',
127
+ 'VALUES',
128
+ 'INSERT',
129
+ 'UPDATE',
130
+ 'DELETE',
131
+ 'REPLACE',
132
+ ]);
133
+ let depth = 0;
134
+ let i = 0;
135
+ let sawWith = false;
136
+ while (i < sql.length) {
137
+ const ch = sql[i];
138
+ const next = sql[i + 1];
139
+ if (ch === '-' && next === '-') {
140
+ const end = sql.indexOf('\n', i);
141
+ i = end === -1 ? sql.length : end + 1;
142
+ }
143
+ else if (ch === '/' && next === '*') {
144
+ const end = sql.indexOf('*/', i + 2);
145
+ i = end === -1 ? sql.length : end + 2;
146
+ }
147
+ else if (ch === "'" || ch === '"' || ch === '`') {
148
+ let j = i + 1;
149
+ while (j < sql.length && sql[j] !== ch)
150
+ j += 1;
151
+ i = j + 1;
152
+ }
153
+ else if (ch === '(') {
154
+ depth += 1;
155
+ i += 1;
156
+ }
157
+ else if (ch === ')') {
158
+ depth -= 1;
159
+ i += 1;
160
+ }
161
+ else if (WORD_RE.test(ch)) {
162
+ let j = i + 1;
163
+ while (j < sql.length && /[A-Za-z0-9_]/.test(sql[j]))
164
+ j += 1;
165
+ const word = sql.slice(i, j).toUpperCase();
166
+ if (depth === 0) {
167
+ if (!sawWith && word === 'WITH')
168
+ sawWith = true;
169
+ else if (sawWith && MAIN_VERBS.has(word))
170
+ return word;
171
+ }
172
+ i = j;
173
+ }
174
+ else {
175
+ i += 1;
176
+ }
177
+ }
178
+ return undefined;
179
+ }
180
+ const IDENT = '[A-Za-z_][A-Za-z0-9_]*';
181
+ const EXPLICIT_ALIAS_RE = new RegExp(`^([\\s\\S]*?)\\s+AS\\s+(${IDENT})$`, 'i');
182
+ const TRAILING_IDENT_RE = new RegExp(`^([\\s\\S]*?)\\s+(${IDENT})$`);
183
+ const STAR_RE = new RegExp(`^(?:(${IDENT})\\.)?\\*$`);
184
+ /**
185
+ * Expand `*` / `t.*` items from the IR: `t.*` uses the alias's table; a bare
186
+ * `*` requires exactly one referenced table (multi-table `*` is a
187
+ * generate-time error under camel naming — write the projection out).
188
+ */
189
+ function expandStar(item, qualifier, refs, ir, location) {
190
+ let tableName;
191
+ if (qualifier !== undefined) {
192
+ const ref = refs.find((r) => r.alias === qualifier);
193
+ if (ref === undefined) {
194
+ throw new TypegenError(location, `cannot expand ${JSON.stringify(item.text)} — unknown table alias ${JSON.stringify(qualifier)}`);
195
+ }
196
+ tableName = ref.table;
197
+ }
198
+ else {
199
+ if (refs.length !== 1) {
200
+ throw new TypegenError(location, `camelCase naming cannot expand a bare \`*\` over ${refs.length} tables — list the columns explicitly (or set "naming": "preserve")`);
201
+ }
202
+ tableName = refs[0].table;
203
+ }
204
+ const table = ir.tables.find((t) => t.name === tableName);
205
+ if (table === undefined) {
206
+ throw new TypegenError(location, `cannot expand ${JSON.stringify(item.text)} — ${JSON.stringify(tableName)} is not a synced table`);
207
+ }
208
+ const prefix = qualifier !== undefined ? `${qualifier}.` : '';
209
+ return table.columns.map((c) => `${prefix}${c.name}`);
210
+ }
211
+ /**
212
+ * Rewrite the top-level projection of `sql` so every result column's runtime
213
+ * key equals its language-facing name. `analyze` prepares a candidate SQL
214
+ * and returns its result column names; `buildMap` maps the star-expanded
215
+ * projection's SQL-truth result names to language names (owning the §12
216
+ * collision/keyword checks). Re-checking the returned SQL is the caller's
217
+ * job.
218
+ */
219
+ export function lowerProjection(sql, refs, ir, location, analyze, buildMap) {
220
+ const span = findProjection(sql);
221
+ if (span === null) {
222
+ // No rewritable SELECT list (`SELECT 1`-shaped). If nothing needs
223
+ // renaming the query passes through; the caller's no-tables check
224
+ // rejects the degenerate shapes first.
225
+ const names = analyze(sql);
226
+ const langNames = buildMap(names);
227
+ if (langNames.every((langName, i) => langName === names[i])) {
228
+ return { sql, changed: false, sqlNames: names, langNames };
229
+ }
230
+ throw new TypegenError(location, 'camelCase naming could not locate this query\'s SELECT list to alias it — alias the snake_case result columns explicitly (e.g. `created_at AS createdAt`) or set "naming": "preserve"');
231
+ }
232
+ // Star expansion first, so items align 1:1 with SQLite's result columns.
233
+ let expanded = false;
234
+ const flatItems = [];
235
+ for (const item of span.items) {
236
+ const star = STAR_RE.exec(item.text);
237
+ if (star !== null) {
238
+ flatItems.push(...expandStar(item, star[1], refs, ir, location));
239
+ expanded = true;
240
+ }
241
+ else {
242
+ flatItems.push(item.text);
243
+ }
244
+ }
245
+ const expandedSql = expanded
246
+ ? `${sql.slice(0, span.start)} ${flatItems.join(', ')} ${sql.slice(span.end)}`
247
+ : sql;
248
+ const names = analyze(expandedSql);
249
+ if (names.length !== flatItems.length) {
250
+ throw new TypegenError(location, `camelCase naming could not align this query's SELECT list (${flatItems.length} items) with its ${names.length} result columns — alias the snake_case result columns explicitly or set "naming": "preserve"`);
251
+ }
252
+ const langNames = buildMap(names);
253
+ let changed = expanded;
254
+ const rewritten = flatItems.map((text, index) => {
255
+ const resultName = names[index];
256
+ const langName = langNames[index];
257
+ if (langName === resultName)
258
+ return text;
259
+ changed = true;
260
+ const explicit = EXPLICIT_ALIAS_RE.exec(text);
261
+ if (explicit !== null && explicit[2] === resultName) {
262
+ return `${explicit[1].trim()} AS ${langName}`;
263
+ }
264
+ const implicit = TRAILING_IDENT_RE.exec(text);
265
+ if (implicit !== null && implicit[2] === resultName) {
266
+ // Implicit alias (`expr name`) — swap the trailing identifier.
267
+ return `${implicit[1].trim()} AS ${langName}`;
268
+ }
269
+ return `${text} AS ${langName}`;
270
+ });
271
+ if (!changed) {
272
+ return { sql, changed: false, sqlNames: names, langNames };
273
+ }
274
+ const span2 = expanded ? findProjection(expandedSql) : span;
275
+ if (span2 === null)
276
+ throw new Error('unreachable: projection vanished');
277
+ return {
278
+ sql: `${expandedSql.slice(0, span2.start)} ${rewritten.join(', ')} ${expandedSql.slice(span2.end)}`,
279
+ changed: true,
280
+ sqlNames: names,
281
+ langNames,
282
+ };
283
+ }
package/dist/lsp.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export interface RpcMessage {
2
+ jsonrpc?: '2.0';
3
+ id?: number | string | undefined;
4
+ method?: string;
5
+ params?: unknown;
6
+ result?: unknown;
7
+ error?: {
8
+ code: number;
9
+ message: string;
10
+ };
11
+ }
12
+ export declare class SyqlLanguageServer {
13
+ #private;
14
+ /** Handle one incoming message; returns the outgoing messages. */
15
+ handle(message: RpcMessage): RpcMessage[];
16
+ /** Reset cached manifest contexts (e.g. after schema edits). */
17
+ invalidateProjects(): void;
18
+ }
19
+ /** Run the language server over stdio until `exit`. */
20
+ export declare function runLspStdio(): Promise<void>;