@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.
- package/README.md +25 -10
- package/dist/cli.js +155 -4
- package/dist/emit-dart.js +3 -2
- package/dist/emit-kotlin.js +3 -2
- package/dist/emit-queries-dart.js +87 -15
- package/dist/emit-queries-kotlin.js +89 -15
- package/dist/emit-queries-swift.js +92 -14
- package/dist/emit-queries.js +131 -22
- package/dist/emit-swift.js +3 -2
- package/dist/emit.d.ts +2 -1
- package/dist/emit.js +13 -27
- package/dist/fmt.d.ts +3 -0
- package/dist/fmt.js +356 -0
- package/dist/generate.d.ts +16 -6
- package/dist/generate.js +34 -9
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lower.d.ts +54 -0
- package/dist/lower.js +283 -0
- package/dist/lsp.d.ts +20 -0
- package/dist/lsp.js +376 -0
- package/dist/manifest.d.ts +11 -0
- package/dist/manifest.js +20 -0
- package/dist/naming.d.ts +22 -0
- package/dist/naming.js +240 -0
- package/dist/query-ir.d.ts +14 -0
- package/dist/query-ir.js +69 -0
- package/dist/query.d.ts +102 -6
- package/dist/query.js +126 -34
- package/dist/syql.d.ts +83 -0
- package/dist/syql.js +945 -0
- package/package.json +4 -4
- package/src/cli.ts +155 -4
- package/src/emit-dart.ts +3 -2
- package/src/emit-kotlin.ts +3 -2
- package/src/emit-queries-dart.ts +109 -16
- package/src/emit-queries-kotlin.ts +111 -18
- package/src/emit-queries-swift.ts +106 -17
- package/src/emit-queries.ts +179 -28
- package/src/emit-swift.ts +3 -2
- package/src/emit.ts +18 -28
- package/src/fmt.ts +351 -0
- package/src/generate.ts +54 -11
- package/src/index.ts +6 -0
- package/src/lower.ts +331 -0
- package/src/lsp.ts +445 -0
- package/src/manifest.ts +38 -0
- package/src/naming.ts +271 -0
- package/src/query-ir.ts +82 -0
- package/src/query.ts +241 -34
- package/src/syql.ts +1171 -0
package/src/fmt.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
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';
|
|
18
|
+
import { parseSyqlFile, type SyqlQueryDecl } from './syql';
|
|
19
|
+
|
|
20
|
+
/** SQL keywords the canon lowercases (a pragmatic core set — identifiers
|
|
21
|
+
* that collide with these would need quoting anyway). */
|
|
22
|
+
const SQL_KEYWORDS = new Set(
|
|
23
|
+
(
|
|
24
|
+
'select from where group by having order limit offset window and or not ' +
|
|
25
|
+
'in is null like glob between exists case when then else end as on using ' +
|
|
26
|
+
'join left right full outer inner cross natural union all except ' +
|
|
27
|
+
'intersect distinct values with recursive asc desc collate cast'
|
|
28
|
+
).split(' '),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
interface Token {
|
|
32
|
+
readonly kind: 'word' | 'string' | 'comment' | 'punct' | 'param' | 'fragment';
|
|
33
|
+
readonly text: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Tokenize a body: strings/comments verbatim, words, `:params`,
|
|
37
|
+
* `@fragment` refs, single punctuation chars. */
|
|
38
|
+
function tokenize(body: string): Token[] {
|
|
39
|
+
const tokens: Token[] = [];
|
|
40
|
+
let i = 0;
|
|
41
|
+
while (i < body.length) {
|
|
42
|
+
const ch = body[i] as string;
|
|
43
|
+
if (/\s/.test(ch)) {
|
|
44
|
+
i += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (body.startsWith('--', i)) {
|
|
48
|
+
const nl = body.indexOf('\n', i);
|
|
49
|
+
const end = nl === -1 ? body.length : nl;
|
|
50
|
+
tokens.push({ kind: 'comment', text: body.slice(i, end).trimEnd() });
|
|
51
|
+
i = end;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (body.startsWith('/*', i)) {
|
|
55
|
+
const close = body.indexOf('*/', i + 2);
|
|
56
|
+
const end = close === -1 ? body.length : close + 2;
|
|
57
|
+
tokens.push({ kind: 'comment', text: body.slice(i, end) });
|
|
58
|
+
i = end;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === "'") {
|
|
62
|
+
let j = i + 1;
|
|
63
|
+
for (;;) {
|
|
64
|
+
if (j >= body.length) break;
|
|
65
|
+
if (body[j] === "'") {
|
|
66
|
+
if (body[j + 1] === "'") j += 2;
|
|
67
|
+
else {
|
|
68
|
+
j += 1;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
} else j += 1;
|
|
72
|
+
}
|
|
73
|
+
tokens.push({ kind: 'string', text: body.slice(i, j) });
|
|
74
|
+
i = j;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (ch === ':' && /[A-Za-z_]/.test(body[i + 1] ?? '')) {
|
|
78
|
+
let j = i + 1;
|
|
79
|
+
while (j < body.length && /[A-Za-z0-9_]/.test(body[j] as string)) j += 1;
|
|
80
|
+
tokens.push({ kind: 'param', text: body.slice(i, j) });
|
|
81
|
+
i = j;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === '@' && /[A-Za-z_]/.test(body[i + 1] ?? '')) {
|
|
85
|
+
let j = i + 1;
|
|
86
|
+
while (j < body.length && /[A-Za-z0-9_]/.test(body[j] as string)) j += 1;
|
|
87
|
+
tokens.push({ kind: 'fragment', text: body.slice(i, j) });
|
|
88
|
+
i = j;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (/[A-Za-z_]/.test(ch)) {
|
|
92
|
+
let j = i + 1;
|
|
93
|
+
while (j < body.length && /[A-Za-z0-9_]/.test(body[j] as string)) j += 1;
|
|
94
|
+
tokens.push({ kind: 'word', text: body.slice(i, j) });
|
|
95
|
+
i = j;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Multi-char operators stay one token (`||`, `>=`, `<=`, `<>`, `!=`, `==`).
|
|
99
|
+
const two = body.slice(i, i + 2);
|
|
100
|
+
if (['||', '>=', '<=', '<>', '!=', '=='].includes(two)) {
|
|
101
|
+
tokens.push({ kind: 'punct', text: two });
|
|
102
|
+
i += 2;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
tokens.push({ kind: 'punct', text: ch });
|
|
106
|
+
i += 1;
|
|
107
|
+
}
|
|
108
|
+
return tokens;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Render tokens back to one line with canonical spacing + keyword case. */
|
|
112
|
+
function render(tokens: readonly Token[]): string {
|
|
113
|
+
let out = '';
|
|
114
|
+
let prev: Token | undefined;
|
|
115
|
+
for (const token of tokens) {
|
|
116
|
+
let text = token.text;
|
|
117
|
+
if (token.kind === 'word' && SQL_KEYWORDS.has(text.toLowerCase())) {
|
|
118
|
+
text = text.toLowerCase();
|
|
119
|
+
}
|
|
120
|
+
const glueLeft =
|
|
121
|
+
token.kind === 'punct' &&
|
|
122
|
+
(text === ',' || text === ')' || text === ';' || text === '.');
|
|
123
|
+
const glueRight =
|
|
124
|
+
prev !== undefined &&
|
|
125
|
+
((prev.kind === 'punct' && (prev.text === '(' || prev.text === '.')) ||
|
|
126
|
+
prev.kind === 'fragment');
|
|
127
|
+
if (out.length > 0 && !glueLeft && !glueRight) out += ' ';
|
|
128
|
+
out += text;
|
|
129
|
+
prev = token;
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Clause keywords that start a new line at paren depth 0. */
|
|
135
|
+
const CLAUSE_STARTERS = new Set([
|
|
136
|
+
'select',
|
|
137
|
+
'from',
|
|
138
|
+
'where',
|
|
139
|
+
'group',
|
|
140
|
+
'having',
|
|
141
|
+
'order',
|
|
142
|
+
'limit',
|
|
143
|
+
'window',
|
|
144
|
+
'union',
|
|
145
|
+
'except',
|
|
146
|
+
'intersect',
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
/** Format a body: one clause per line; WHERE conjuncts one per line. */
|
|
150
|
+
function formatBody(body: string): string[] {
|
|
151
|
+
const tokens = tokenize(body);
|
|
152
|
+
const lines: string[] = [];
|
|
153
|
+
let current: Token[] = [];
|
|
154
|
+
let depth = 0;
|
|
155
|
+
let braceDepth = 0;
|
|
156
|
+
let inWhere = false;
|
|
157
|
+
let pendingBetween = 0;
|
|
158
|
+
const flush = (): void => {
|
|
159
|
+
if (current.length > 0) lines.push(render(current));
|
|
160
|
+
current = [];
|
|
161
|
+
};
|
|
162
|
+
for (const token of tokens) {
|
|
163
|
+
if (token.kind === 'comment') {
|
|
164
|
+
flush();
|
|
165
|
+
lines.push(token.text);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (token.kind === 'punct') {
|
|
169
|
+
if (token.text === '(') depth += 1;
|
|
170
|
+
else if (token.text === ')') depth -= 1;
|
|
171
|
+
else if (token.text === '{') braceDepth += 1;
|
|
172
|
+
else if (token.text === '}') braceDepth -= 1;
|
|
173
|
+
}
|
|
174
|
+
if (token.kind === 'word' && depth === 0 && braceDepth === 0) {
|
|
175
|
+
const word = token.text.toLowerCase();
|
|
176
|
+
if (CLAUSE_STARTERS.has(word)) {
|
|
177
|
+
flush();
|
|
178
|
+
inWhere = word === 'where';
|
|
179
|
+
pendingBetween = 0;
|
|
180
|
+
} else if (inWhere) {
|
|
181
|
+
if (word === 'between') pendingBetween += 1;
|
|
182
|
+
else if (word === 'and') {
|
|
183
|
+
if (pendingBetween > 0) pendingBetween -= 1;
|
|
184
|
+
else flush();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
current.push(token);
|
|
189
|
+
}
|
|
190
|
+
flush();
|
|
191
|
+
// Indent: clauses flush-left; WHERE continuation (`and …`) indented 2.
|
|
192
|
+
return lines.map((line) =>
|
|
193
|
+
/^(and|or)\b/.test(line) || line.startsWith('--') ? ` ${line}` : line,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatSignature(decl: {
|
|
198
|
+
readonly params: SyqlQueryDecl['params'];
|
|
199
|
+
}): string {
|
|
200
|
+
const parts: string[] = [];
|
|
201
|
+
const seenGroups = new Set<string>();
|
|
202
|
+
for (const param of decl.params) {
|
|
203
|
+
if (param.group !== undefined) {
|
|
204
|
+
if (seenGroups.has(param.group)) continue;
|
|
205
|
+
seenGroups.add(param.group);
|
|
206
|
+
const members = decl.params.filter((p) => p.group === param.group);
|
|
207
|
+
parts.push(`${members.map((p) => p.name).join('+')}?`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (param.flag) parts.push(`${param.name}?: flag`);
|
|
211
|
+
else parts.push(`${param.name}${param.optional ? '?' : ''}`);
|
|
212
|
+
}
|
|
213
|
+
return parts.join(', ');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The leading comment block (verbatim lines) before each declaration. */
|
|
217
|
+
function leadingComments(source: string): Map<number, string[]> {
|
|
218
|
+
// Map from declaration index (0-based, in order of appearance) to its
|
|
219
|
+
// preceding comment lines.
|
|
220
|
+
const out = new Map<number, string[]>();
|
|
221
|
+
let pending: string[] = [];
|
|
222
|
+
let declIndex = 0;
|
|
223
|
+
let i = 0;
|
|
224
|
+
while (i < source.length) {
|
|
225
|
+
const ch = source[i] as string;
|
|
226
|
+
if (/\s/.test(ch)) {
|
|
227
|
+
i += 1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (source.startsWith('--', i)) {
|
|
231
|
+
const nl = source.indexOf('\n', i);
|
|
232
|
+
const end = nl === -1 ? source.length : nl;
|
|
233
|
+
pending.push(source.slice(i, end).trimEnd());
|
|
234
|
+
i = end;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (source.startsWith('/*', i)) {
|
|
238
|
+
const close = source.indexOf('*/', i + 2);
|
|
239
|
+
const end = close === -1 ? source.length : close + 2;
|
|
240
|
+
pending.push(source.slice(i, end));
|
|
241
|
+
i = end;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
// A declaration keyword: attach pending comments, then skip through its
|
|
245
|
+
// brace block.
|
|
246
|
+
if (pending.length > 0) {
|
|
247
|
+
out.set(declIndex, pending);
|
|
248
|
+
pending = [];
|
|
249
|
+
}
|
|
250
|
+
declIndex += 1;
|
|
251
|
+
const brace = source.indexOf('{', i);
|
|
252
|
+
if (brace === -1) break;
|
|
253
|
+
let depth = 0;
|
|
254
|
+
let j = brace;
|
|
255
|
+
while (j < source.length) {
|
|
256
|
+
const c = source[j] as string;
|
|
257
|
+
if (c === "'") {
|
|
258
|
+
j += 1;
|
|
259
|
+
while (j < source.length && source[j] !== "'") j += 1;
|
|
260
|
+
} else if (source.startsWith('--', j)) {
|
|
261
|
+
const nl = source.indexOf('\n', j);
|
|
262
|
+
j = nl === -1 ? source.length : nl;
|
|
263
|
+
} else if (c === '{') depth += 1;
|
|
264
|
+
else if (c === '}') {
|
|
265
|
+
depth -= 1;
|
|
266
|
+
if (depth === 0) {
|
|
267
|
+
j += 1;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
j += 1;
|
|
272
|
+
}
|
|
273
|
+
i = j;
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Format one `.syql` source file into its canonical form. Throws
|
|
279
|
+
* {@link TypegenError} when the source does not parse. */
|
|
280
|
+
export function formatSyql(file: string, source: string): string {
|
|
281
|
+
const parsed = parseSyqlFile(file, source);
|
|
282
|
+
const comments = leadingComments(source);
|
|
283
|
+
const decls: { order: number; text: string }[] = [];
|
|
284
|
+
|
|
285
|
+
// Re-derive declaration order (fragments/queries interleave in-source);
|
|
286
|
+
// parseSyqlFile keeps per-kind order, so re-scan the source for kind
|
|
287
|
+
// keywords to interleave faithfully.
|
|
288
|
+
const orderRe = /\b(query|fragment)\s+([A-Za-z][A-Za-z0-9]*)/g;
|
|
289
|
+
const blanked = source
|
|
290
|
+
.replace(/--[^\n]*/g, (m) => ' '.repeat(m.length))
|
|
291
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
|
|
292
|
+
.replace(/'(?:[^']|'')*'/g, (m) => ' '.repeat(m.length));
|
|
293
|
+
const sequence: { kind: string; name: string }[] = [];
|
|
294
|
+
let braceDepth = 0;
|
|
295
|
+
let lastIndex = 0;
|
|
296
|
+
for (let i = 0; i < blanked.length; i++) {
|
|
297
|
+
const c = blanked[i];
|
|
298
|
+
if (c === '{') braceDepth += 1;
|
|
299
|
+
else if (c === '}') braceDepth -= 1;
|
|
300
|
+
else if (braceDepth === 0) {
|
|
301
|
+
orderRe.lastIndex = i;
|
|
302
|
+
const m = orderRe.exec(blanked);
|
|
303
|
+
if (m !== null && m.index === i) {
|
|
304
|
+
sequence.push({ kind: m[1] as string, name: m[2] as string });
|
|
305
|
+
i = orderRe.lastIndex - 1;
|
|
306
|
+
lastIndex = orderRe.lastIndex;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
void lastIndex;
|
|
311
|
+
|
|
312
|
+
sequence.forEach((entry, index) => {
|
|
313
|
+
const lines: string[] = [];
|
|
314
|
+
const leading = comments.get(index);
|
|
315
|
+
if (leading !== undefined) lines.push(...leading);
|
|
316
|
+
if (entry.kind === 'fragment') {
|
|
317
|
+
const decl = parsed.fragments.find((f) => f.name === entry.name);
|
|
318
|
+
if (decl === undefined) return;
|
|
319
|
+
lines.push(`fragment ${decl.name}(${formatSignature(decl)}) {`);
|
|
320
|
+
for (const line of formatBody(decl.body)) lines.push(` ${line}`);
|
|
321
|
+
lines.push('}');
|
|
322
|
+
} else {
|
|
323
|
+
const decl = parsed.queries.find((q) => q.name === entry.name);
|
|
324
|
+
if (decl === undefined) return;
|
|
325
|
+
const knobs: string[] = [];
|
|
326
|
+
if (decl.orderBy !== undefined) {
|
|
327
|
+
const dir = decl.orderBy.defaultDir === 'desc' ? ' desc' : '';
|
|
328
|
+
knobs.push(
|
|
329
|
+
` orderBy ${decl.orderBy.allowed.join(' | ')} default ${decl.orderBy.defaultColumn}${dir}`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (decl.limit !== undefined) {
|
|
333
|
+
const parts: string[] = [];
|
|
334
|
+
if (decl.limit.max !== undefined) parts.push(`max ${decl.limit.max}`);
|
|
335
|
+
if (decl.limit.default !== undefined) {
|
|
336
|
+
parts.push(`default ${decl.limit.default}`);
|
|
337
|
+
}
|
|
338
|
+
knobs.push(` limit ${parts.join(' ')}`);
|
|
339
|
+
}
|
|
340
|
+
if (decl.variants === true) knobs.push(' variants');
|
|
341
|
+
lines.push(
|
|
342
|
+
`query ${decl.name}(${formatSignature(decl)})${knobs.length > 0 ? `\n${knobs.join('\n')}\n{` : ' {'}`,
|
|
343
|
+
);
|
|
344
|
+
for (const line of formatBody(decl.body)) lines.push(` ${line}`);
|
|
345
|
+
lines.push('}');
|
|
346
|
+
}
|
|
347
|
+
decls.push({ order: index, text: lines.join('\n') });
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return `${decls.map((d) => d.text).join('\n\n')}\n`;
|
|
351
|
+
}
|
package/src/generate.ts
CHANGED
|
@@ -40,13 +40,16 @@ import {
|
|
|
40
40
|
type ManifestScopeSpec,
|
|
41
41
|
parseManifest,
|
|
42
42
|
} from './manifest';
|
|
43
|
+
import { buildNamingMap, type NamingTarget } from './naming';
|
|
43
44
|
import {
|
|
44
45
|
type AnalyzedQuery,
|
|
45
46
|
analyzeQueryFile,
|
|
46
47
|
type QueryDb,
|
|
48
|
+
type QueryNamingOptions,
|
|
47
49
|
synthesizeDdl,
|
|
48
50
|
} from './query';
|
|
49
51
|
import { applyMigrationSql, type ParsedTable } from './sql';
|
|
52
|
+
import { analyzeSyqlFile } from './syql';
|
|
50
53
|
|
|
51
54
|
export interface MigrationInput {
|
|
52
55
|
/** Directory name, e.g. `0001_initial`. */
|
|
@@ -340,31 +343,36 @@ export function loadMigrations(migrationsDir: string): MigrationInput[] {
|
|
|
340
343
|
});
|
|
341
344
|
}
|
|
342
345
|
|
|
343
|
-
/** One
|
|
344
|
-
*
|
|
346
|
+
/** One named-query file (`.sql` — the compatibility floor — or `.syql`, the
|
|
347
|
+
* DSL): path relative to the queries root (forward slashes) + raw contents.
|
|
348
|
+
* A file may hold multiple statements/declarations. */
|
|
345
349
|
export interface QueryInput {
|
|
346
350
|
/** Path relative to the queries root, forward-slashed (e.g.
|
|
347
|
-
* `billing/invoices/list.sql`). Drives the path-derived default name
|
|
351
|
+
* `billing/invoices/list.sql`). Drives the path-derived default name for
|
|
352
|
+
* `.sql`; `.syql` queries carry their own names. */
|
|
348
353
|
readonly file: string;
|
|
349
354
|
readonly sql: string;
|
|
350
355
|
}
|
|
351
356
|
|
|
352
|
-
/** Recursively collect `*.sql` paths under `dir`, relative to
|
|
353
|
-
* (forward-slashed), sorted for deterministic emission. */
|
|
357
|
+
/** Recursively collect `*.sql` + `*.syql` paths under `dir`, relative to
|
|
358
|
+
* `root` (forward-slashed), sorted for deterministic emission. */
|
|
354
359
|
function collectSqlFiles(root: string, dir: string): string[] {
|
|
355
360
|
const out: string[] = [];
|
|
356
361
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
357
362
|
const abs = join(dir, entry.name);
|
|
358
363
|
if (entry.isDirectory()) {
|
|
359
364
|
out.push(...collectSqlFiles(root, abs));
|
|
360
|
-
} else if (
|
|
365
|
+
} else if (
|
|
366
|
+
entry.isFile() &&
|
|
367
|
+
(entry.name.endsWith('.sql') || entry.name.endsWith('.syql'))
|
|
368
|
+
) {
|
|
361
369
|
out.push(relative(root, abs).split(sep).join('/'));
|
|
362
370
|
}
|
|
363
371
|
}
|
|
364
372
|
return out;
|
|
365
373
|
}
|
|
366
374
|
|
|
367
|
-
/** Load `queries/**\/*.sql` recursively, ordered by relative path
|
|
375
|
+
/** Load `queries/**\/*.{sql,syql}` recursively, ordered by relative path
|
|
368
376
|
* (deterministic emission). Folders are pure organization. */
|
|
369
377
|
export function loadQueries(queriesDir: string): QueryInput[] {
|
|
370
378
|
if (!existsSync(queriesDir)) return [];
|
|
@@ -380,7 +388,10 @@ export function loadQueries(queriesDir: string): QueryInput[] {
|
|
|
380
388
|
* `analyze` prepares the statement (validating references) and reads its
|
|
381
389
|
* column names + declared types; it executes once against the empty DB so
|
|
382
390
|
* `declaredTypes` (decltype) is populated. */
|
|
383
|
-
function makeQueryDb(ir: IrDocument): {
|
|
391
|
+
export function makeQueryDb(ir: IrDocument): {
|
|
392
|
+
db: QueryDb;
|
|
393
|
+
close: () => void;
|
|
394
|
+
} {
|
|
384
395
|
const sqlite = new Database(':memory:');
|
|
385
396
|
sqlite.run(synthesizeDdl(ir));
|
|
386
397
|
const db: QueryDb = {
|
|
@@ -408,13 +419,18 @@ function makeQueryDb(ir: IrDocument): { db: QueryDb; close: () => void } {
|
|
|
408
419
|
export function analyzeQueries(
|
|
409
420
|
ir: IrDocument,
|
|
410
421
|
queries: readonly QueryInput[],
|
|
422
|
+
naming?: QueryNamingOptions,
|
|
411
423
|
): AnalyzedQuery[] {
|
|
412
424
|
if (queries.length === 0) return [];
|
|
413
425
|
const { db, close } = makeQueryDb(ir);
|
|
414
426
|
try {
|
|
415
427
|
// Each file may hold multiple statements; flatten in path order (stable).
|
|
428
|
+
// Frontends dispatch on extension; both produce the same AnalyzedQuery
|
|
429
|
+
// shape (§1 — nothing downstream knows the frontend).
|
|
416
430
|
const analyzed = queries.flatMap((q) =>
|
|
417
|
-
|
|
431
|
+
q.file.endsWith('.syql')
|
|
432
|
+
? analyzeSyqlFile(q.file, q.sql, ir, db, naming)
|
|
433
|
+
: analyzeQueryFile(q.file, q.sql, ir, db, naming),
|
|
418
434
|
);
|
|
419
435
|
// Names must be unique across the whole manifest — the filesystem no longer
|
|
420
436
|
// guarantees uniqueness once `-- name:` overrides exist. Report BOTH source
|
|
@@ -476,9 +492,32 @@ export function generate(manifestDir: string): GenerateResult {
|
|
|
476
492
|
const manifest = parseManifest(raw);
|
|
477
493
|
const migrations = loadMigrations(resolve(manifestDir, manifest.migrations));
|
|
478
494
|
const ir = buildIr(manifest, migrations);
|
|
495
|
+
|
|
496
|
+
// §5/§12 naming: the emitter targets this run generates (keyword hazards
|
|
497
|
+
// are only real on targets that exist), and the per-table collision check
|
|
498
|
+
// for the generated row types.
|
|
499
|
+
const targets: NamingTarget[] = ['ts'];
|
|
500
|
+
if (manifest.output.swift !== undefined) targets.push('swift');
|
|
501
|
+
if (manifest.output.kotlin !== undefined) targets.push('kotlin');
|
|
502
|
+
if (manifest.output.dart !== undefined) targets.push('dart');
|
|
503
|
+
const naming: QueryNamingOptions = {
|
|
504
|
+
naming: manifest.naming,
|
|
505
|
+
targets,
|
|
506
|
+
backend: manifest.queryBackend,
|
|
507
|
+
};
|
|
508
|
+
for (const table of ir.tables) {
|
|
509
|
+
buildNamingMap(
|
|
510
|
+
table.columns.map((c) => c.name),
|
|
511
|
+
manifest.naming,
|
|
512
|
+
MANIFEST_FILENAME,
|
|
513
|
+
`table ${table.name}`,
|
|
514
|
+
targets,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
479
518
|
const irJson = serializeIr(ir);
|
|
480
519
|
const hash = irHash(irJson);
|
|
481
|
-
const module = emitModule(ir, hash);
|
|
520
|
+
const module = emitModule(ir, hash, manifest.naming);
|
|
482
521
|
const irPath = resolve(manifestDir, manifest.output.ir);
|
|
483
522
|
const modulePath = resolve(manifestDir, manifest.output.module);
|
|
484
523
|
const outputs: GeneratedOutput[] = [
|
|
@@ -497,7 +536,11 @@ export function generate(manifestDir: string): GenerateResult {
|
|
|
497
536
|
kotlin?.queriesPath !== undefined ||
|
|
498
537
|
dart?.queriesPath !== undefined;
|
|
499
538
|
const analyzedQueries = wantsQueries
|
|
500
|
-
? analyzeQueries(
|
|
539
|
+
? analyzeQueries(
|
|
540
|
+
ir,
|
|
541
|
+
loadQueries(resolve(manifestDir, manifest.queries)),
|
|
542
|
+
naming,
|
|
543
|
+
)
|
|
501
544
|
: [];
|
|
502
545
|
if (wantsQueries && analyzedQueries.length === 0) {
|
|
503
546
|
throw new TypegenError(
|
package/src/index.ts
CHANGED
|
@@ -12,8 +12,14 @@ export * from './emit-queries-kotlin';
|
|
|
12
12
|
export * from './emit-queries-swift';
|
|
13
13
|
export * from './emit-swift';
|
|
14
14
|
export * from './errors';
|
|
15
|
+
export * from './fmt';
|
|
15
16
|
export * from './generate';
|
|
16
17
|
export * from './ir';
|
|
18
|
+
export * from './lower';
|
|
19
|
+
export * from './lsp';
|
|
17
20
|
export * from './manifest';
|
|
21
|
+
export * from './naming';
|
|
18
22
|
export * from './query';
|
|
23
|
+
export * from './query-ir';
|
|
19
24
|
export * from './sql';
|
|
25
|
+
export * from './syql';
|