@syncular/typegen 0.2.1 → 0.3.1
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 +2 -2
- 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/naming.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pinned naming-map algorithm (DESIGN-queries.md §5, §12): SQL stays
|
|
3
|
+
* snake_case; emitters render their language's convention. The IR carries
|
|
4
|
+
* SQL-truth names plus a derived, collision-checked map — this module is
|
|
5
|
+
* that derivation.
|
|
6
|
+
*
|
|
7
|
+
* `snake_case → camelCase`, pinned:
|
|
8
|
+
* 1. A leading run of `_` is preserved as a prefix, a trailing run as a
|
|
9
|
+
* suffix (they carry intent — privacy markers, disambiguation).
|
|
10
|
+
* 2. The middle splits on `_`; empty segments from doubled underscores drop.
|
|
11
|
+
* 3. First segment verbatim; each later segment upper-cases its first
|
|
12
|
+
* character, rest verbatim. No acronym awareness (`id_url` → `idUrl`).
|
|
13
|
+
* 4. Prefix/suffix re-attach. `created_at`→`createdAt`, `col_2`→`col2`,
|
|
14
|
+
* `_internal`→`_internal`, `__foo_bar`→`__fooBar`, `row_`→`row_`.
|
|
15
|
+
*
|
|
16
|
+
* Collisions and hazards are generate-time ERRORS, not warnings: two SQL
|
|
17
|
+
* names mapping to one language name, a mapped name hitting a target-language
|
|
18
|
+
* keyword, or a leading underscore on the Dart target (library-private).
|
|
19
|
+
*/
|
|
20
|
+
import { TypegenError } from './errors';
|
|
21
|
+
|
|
22
|
+
/** Manifest `"naming"`: camelCase emission (default) or SQL-truth names. */
|
|
23
|
+
export type NamingMode = 'camel' | 'preserve';
|
|
24
|
+
|
|
25
|
+
/** A name the mapper touches: a plain (optionally underscore-framed)
|
|
26
|
+
* snake_case identifier. Expression-shaped result names (`count(*)`) pass
|
|
27
|
+
* through unmapped. */
|
|
28
|
+
const MAPPABLE_RE = /^_*[A-Za-z][A-Za-z0-9_]*$/;
|
|
29
|
+
|
|
30
|
+
/** The pinned §12 snake→camel conversion. Non-identifier names (computed
|
|
31
|
+
* result columns like `count(*)`) are returned unchanged. */
|
|
32
|
+
export function snakeToCamel(name: string): string {
|
|
33
|
+
if (!MAPPABLE_RE.test(name)) return name;
|
|
34
|
+
const lead = /^_*/.exec(name)?.[0] ?? '';
|
|
35
|
+
const bare = name.slice(lead.length);
|
|
36
|
+
const trail = /_*$/.exec(bare)?.[0] ?? '';
|
|
37
|
+
const middle = bare.slice(0, bare.length - trail.length);
|
|
38
|
+
const segments = middle.split('_').filter((s) => s.length > 0);
|
|
39
|
+
if (segments.length === 0) return name;
|
|
40
|
+
const first = segments[0] as string;
|
|
41
|
+
const rest = segments
|
|
42
|
+
.slice(1)
|
|
43
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1));
|
|
44
|
+
return lead + first + rest.join('') + trail;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The emitter targets whose keyword sets we police. `ts` is included for
|
|
48
|
+
* completeness (its emitters can quote any property, so its list is the
|
|
49
|
+
* small set that breaks generated FUNCTION/const identifiers). */
|
|
50
|
+
export type NamingTarget = 'ts' | 'swift' | 'kotlin' | 'dart';
|
|
51
|
+
|
|
52
|
+
/** Reserved words that cannot be a generated field/property name on each
|
|
53
|
+
* target. Deliberately the core keyword lists — mechanical and predictable
|
|
54
|
+
* beats exhaustive; an escape hatch (`AS` alias / "preserve") always exists. */
|
|
55
|
+
const TARGET_KEYWORDS: Readonly<Record<NamingTarget, ReadonlySet<string>>> = {
|
|
56
|
+
// TS interface properties/object keys may be quoted, so only names that
|
|
57
|
+
// would break generated code paths matter; keep the ES reserved core.
|
|
58
|
+
ts: new Set([
|
|
59
|
+
'break',
|
|
60
|
+
'case',
|
|
61
|
+
'catch',
|
|
62
|
+
'class',
|
|
63
|
+
'const',
|
|
64
|
+
'continue',
|
|
65
|
+
'debugger',
|
|
66
|
+
'default',
|
|
67
|
+
'delete',
|
|
68
|
+
'do',
|
|
69
|
+
'else',
|
|
70
|
+
'enum',
|
|
71
|
+
'export',
|
|
72
|
+
'extends',
|
|
73
|
+
'false',
|
|
74
|
+
'finally',
|
|
75
|
+
'for',
|
|
76
|
+
'function',
|
|
77
|
+
'if',
|
|
78
|
+
'import',
|
|
79
|
+
'in',
|
|
80
|
+
'instanceof',
|
|
81
|
+
'new',
|
|
82
|
+
'null',
|
|
83
|
+
'return',
|
|
84
|
+
'super',
|
|
85
|
+
'switch',
|
|
86
|
+
'this',
|
|
87
|
+
'throw',
|
|
88
|
+
'true',
|
|
89
|
+
'try',
|
|
90
|
+
'typeof',
|
|
91
|
+
'var',
|
|
92
|
+
'void',
|
|
93
|
+
'while',
|
|
94
|
+
'with',
|
|
95
|
+
]),
|
|
96
|
+
swift: new Set([
|
|
97
|
+
'associatedtype',
|
|
98
|
+
'class',
|
|
99
|
+
'deinit',
|
|
100
|
+
'enum',
|
|
101
|
+
'extension',
|
|
102
|
+
'fileprivate',
|
|
103
|
+
'func',
|
|
104
|
+
'import',
|
|
105
|
+
'init',
|
|
106
|
+
'inout',
|
|
107
|
+
'internal',
|
|
108
|
+
'let',
|
|
109
|
+
'open',
|
|
110
|
+
'operator',
|
|
111
|
+
'private',
|
|
112
|
+
'protocol',
|
|
113
|
+
'public',
|
|
114
|
+
'rethrows',
|
|
115
|
+
'static',
|
|
116
|
+
'struct',
|
|
117
|
+
'subscript',
|
|
118
|
+
'typealias',
|
|
119
|
+
'var',
|
|
120
|
+
'break',
|
|
121
|
+
'case',
|
|
122
|
+
'continue',
|
|
123
|
+
'default',
|
|
124
|
+
'defer',
|
|
125
|
+
'do',
|
|
126
|
+
'else',
|
|
127
|
+
'fallthrough',
|
|
128
|
+
'for',
|
|
129
|
+
'guard',
|
|
130
|
+
'if',
|
|
131
|
+
'in',
|
|
132
|
+
'repeat',
|
|
133
|
+
'return',
|
|
134
|
+
'switch',
|
|
135
|
+
'where',
|
|
136
|
+
'while',
|
|
137
|
+
'as',
|
|
138
|
+
'catch',
|
|
139
|
+
'false',
|
|
140
|
+
'is',
|
|
141
|
+
'nil',
|
|
142
|
+
'super',
|
|
143
|
+
'self',
|
|
144
|
+
'throw',
|
|
145
|
+
'throws',
|
|
146
|
+
'true',
|
|
147
|
+
'try',
|
|
148
|
+
]),
|
|
149
|
+
kotlin: new Set([
|
|
150
|
+
'as',
|
|
151
|
+
'break',
|
|
152
|
+
'class',
|
|
153
|
+
'continue',
|
|
154
|
+
'do',
|
|
155
|
+
'else',
|
|
156
|
+
'false',
|
|
157
|
+
'for',
|
|
158
|
+
'fun',
|
|
159
|
+
'if',
|
|
160
|
+
'in',
|
|
161
|
+
'interface',
|
|
162
|
+
'is',
|
|
163
|
+
'null',
|
|
164
|
+
'object',
|
|
165
|
+
'package',
|
|
166
|
+
'return',
|
|
167
|
+
'super',
|
|
168
|
+
'this',
|
|
169
|
+
'throw',
|
|
170
|
+
'true',
|
|
171
|
+
'try',
|
|
172
|
+
'typealias',
|
|
173
|
+
'typeof',
|
|
174
|
+
'val',
|
|
175
|
+
'var',
|
|
176
|
+
'when',
|
|
177
|
+
'while',
|
|
178
|
+
]),
|
|
179
|
+
dart: new Set([
|
|
180
|
+
'assert',
|
|
181
|
+
'break',
|
|
182
|
+
'case',
|
|
183
|
+
'catch',
|
|
184
|
+
'class',
|
|
185
|
+
'const',
|
|
186
|
+
'continue',
|
|
187
|
+
'default',
|
|
188
|
+
'do',
|
|
189
|
+
'else',
|
|
190
|
+
'enum',
|
|
191
|
+
'extends',
|
|
192
|
+
'false',
|
|
193
|
+
'final',
|
|
194
|
+
'finally',
|
|
195
|
+
'for',
|
|
196
|
+
'if',
|
|
197
|
+
'in',
|
|
198
|
+
'is',
|
|
199
|
+
'new',
|
|
200
|
+
'null',
|
|
201
|
+
'rethrow',
|
|
202
|
+
'return',
|
|
203
|
+
'super',
|
|
204
|
+
'switch',
|
|
205
|
+
'this',
|
|
206
|
+
'throw',
|
|
207
|
+
'true',
|
|
208
|
+
'try',
|
|
209
|
+
'var',
|
|
210
|
+
'void',
|
|
211
|
+
'while',
|
|
212
|
+
'with',
|
|
213
|
+
]),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/** One naming-map entry: the SQL-truth name and its language-facing name. */
|
|
217
|
+
export interface NameMapping {
|
|
218
|
+
readonly sqlName: string;
|
|
219
|
+
readonly langName: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Map a set of SQL names to language names under `mode`, enforcing the §12
|
|
224
|
+
* hard errors within the scope (one table's columns / one query's projection
|
|
225
|
+
* / one query's params). `context` and `scope` name the error location
|
|
226
|
+
* (e.g. `queries/list.sql`, `table todos`); `targets` are the emitters this
|
|
227
|
+
* run generates — keyword hazards are only real on targets that exist.
|
|
228
|
+
*/
|
|
229
|
+
export function buildNamingMap(
|
|
230
|
+
sqlNames: readonly string[],
|
|
231
|
+
mode: NamingMode,
|
|
232
|
+
context: string,
|
|
233
|
+
scope: string,
|
|
234
|
+
targets: readonly NamingTarget[],
|
|
235
|
+
): NameMapping[] {
|
|
236
|
+
if (mode === 'preserve') {
|
|
237
|
+
return sqlNames.map((sqlName) => ({ sqlName, langName: sqlName }));
|
|
238
|
+
}
|
|
239
|
+
const bySqlName = new Map<string, string>();
|
|
240
|
+
const byLangName = new Map<string, string>();
|
|
241
|
+
for (const sqlName of sqlNames) {
|
|
242
|
+
const langName = snakeToCamel(sqlName);
|
|
243
|
+
const clash = byLangName.get(langName);
|
|
244
|
+
if (clash !== undefined && clash !== sqlName) {
|
|
245
|
+
throw new TypegenError(
|
|
246
|
+
context,
|
|
247
|
+
`${scope}: ${JSON.stringify(clash)} and ${JSON.stringify(sqlName)} both map to ${JSON.stringify(langName)} under camelCase naming — rename one, alias it in SQL (AS), or set "naming": "preserve" in syncular.json`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
for (const target of targets) {
|
|
251
|
+
if (TARGET_KEYWORDS[target].has(langName)) {
|
|
252
|
+
throw new TypegenError(
|
|
253
|
+
context,
|
|
254
|
+
`${scope}: ${JSON.stringify(sqlName)} maps to ${JSON.stringify(langName)}, a reserved word on the ${target} target — rename it, alias it in SQL (AS), or set "naming": "preserve" in syncular.json`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (target === 'dart' && langName.startsWith('_')) {
|
|
258
|
+
throw new TypegenError(
|
|
259
|
+
context,
|
|
260
|
+
`${scope}: ${JSON.stringify(sqlName)} maps to ${JSON.stringify(langName)} — a leading underscore is library-private on the dart target. Alias it in SQL (AS) or set "naming": "preserve" in syncular.json`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
bySqlName.set(sqlName, langName);
|
|
265
|
+
byLangName.set(langName, sqlName);
|
|
266
|
+
}
|
|
267
|
+
return sqlNames.map((sqlName) => ({
|
|
268
|
+
sqlName,
|
|
269
|
+
langName: bySqlName.get(sqlName) as string,
|
|
270
|
+
}));
|
|
271
|
+
}
|
package/src/query-ir.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueryIR serialization (DESIGN-queries.md §1): the frontend-agnostic,
|
|
3
|
+
* deterministic JSON form of analyzed queries. This is the golden-fixture
|
|
4
|
+
* format — equivalent inputs in any frontend (`.sql` today, `.syql` later)
|
|
5
|
+
* must produce byte-identical IR JSON, the same trick the wire protocol
|
|
6
|
+
* uses for the two cores. Fixed key order, 2-space indent, trailing
|
|
7
|
+
* newline.
|
|
8
|
+
*
|
|
9
|
+
* Names are SQL-truth (`name`) plus the collision-checked language name
|
|
10
|
+
* (`langName`, §5); `sql` is the LOWERED statement (what actually runs).
|
|
11
|
+
*/
|
|
12
|
+
import type { AnalyzedQuery } from './query';
|
|
13
|
+
|
|
14
|
+
/** Serialize analyzed queries as the deterministic QueryIR JSON document. */
|
|
15
|
+
export function serializeQueryIr(queries: readonly AnalyzedQuery[]): string {
|
|
16
|
+
const doc = {
|
|
17
|
+
queryIrVersion: 1,
|
|
18
|
+
queries: queries.map((query) => ({
|
|
19
|
+
name: query.name,
|
|
20
|
+
file: query.file,
|
|
21
|
+
sourceSql: query.sourceSql,
|
|
22
|
+
sql: query.sql,
|
|
23
|
+
positionalSql: query.positionalSql,
|
|
24
|
+
params: query.params.map((param) => ({
|
|
25
|
+
name: param.name,
|
|
26
|
+
langName: param.langName,
|
|
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
|
+
})),
|
|
34
|
+
columns: query.columns.map((column) => ({
|
|
35
|
+
name: column.name,
|
|
36
|
+
langName: column.langName,
|
|
37
|
+
type: column.type,
|
|
38
|
+
nullable: column.nullable,
|
|
39
|
+
fidelity: column.fidelity,
|
|
40
|
+
})),
|
|
41
|
+
tables: query.tables,
|
|
42
|
+
// §6 knob metadata — emitted only when declared.
|
|
43
|
+
...(query.orderBy !== undefined
|
|
44
|
+
? {
|
|
45
|
+
orderBy: {
|
|
46
|
+
allowed: query.orderBy.allowed.map((c) => ({
|
|
47
|
+
name: c.name,
|
|
48
|
+
langName: c.langName,
|
|
49
|
+
})),
|
|
50
|
+
defaultColumn: query.orderBy.defaultColumn,
|
|
51
|
+
defaultDir: query.orderBy.defaultDir,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
: {}),
|
|
55
|
+
...(query.limit !== undefined ? { limit: query.limit } : {}),
|
|
56
|
+
...(query.positionalSqlBase !== undefined
|
|
57
|
+
? { positionalSqlBase: query.positionalSqlBase }
|
|
58
|
+
: {}),
|
|
59
|
+
// §7 variant backend — emitted only when the query opts in.
|
|
60
|
+
...(query.variantGroups !== undefined
|
|
61
|
+
? {
|
|
62
|
+
variantGroups: query.variantGroups.map((g) => ({
|
|
63
|
+
key: g.key,
|
|
64
|
+
params: g.params,
|
|
65
|
+
flag: g.flag,
|
|
66
|
+
})),
|
|
67
|
+
}
|
|
68
|
+
: {}),
|
|
69
|
+
...(query.variants !== undefined
|
|
70
|
+
? {
|
|
71
|
+
variants: query.variants.map((v) => ({
|
|
72
|
+
when: v.when,
|
|
73
|
+
sql: v.sql,
|
|
74
|
+
positionalSql: v.positionalSql,
|
|
75
|
+
params: v.params,
|
|
76
|
+
})),
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
})),
|
|
80
|
+
};
|
|
81
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
82
|
+
}
|