@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.
- 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/dist/naming.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
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.js';
|
|
21
|
+
/** A name the mapper touches: a plain (optionally underscore-framed)
|
|
22
|
+
* snake_case identifier. Expression-shaped result names (`count(*)`) pass
|
|
23
|
+
* through unmapped. */
|
|
24
|
+
const MAPPABLE_RE = /^_*[A-Za-z][A-Za-z0-9_]*$/;
|
|
25
|
+
/** The pinned §12 snake→camel conversion. Non-identifier names (computed
|
|
26
|
+
* result columns like `count(*)`) are returned unchanged. */
|
|
27
|
+
export function snakeToCamel(name) {
|
|
28
|
+
if (!MAPPABLE_RE.test(name))
|
|
29
|
+
return name;
|
|
30
|
+
const lead = /^_*/.exec(name)?.[0] ?? '';
|
|
31
|
+
const bare = name.slice(lead.length);
|
|
32
|
+
const trail = /_*$/.exec(bare)?.[0] ?? '';
|
|
33
|
+
const middle = bare.slice(0, bare.length - trail.length);
|
|
34
|
+
const segments = middle.split('_').filter((s) => s.length > 0);
|
|
35
|
+
if (segments.length === 0)
|
|
36
|
+
return name;
|
|
37
|
+
const first = segments[0];
|
|
38
|
+
const rest = segments
|
|
39
|
+
.slice(1)
|
|
40
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1));
|
|
41
|
+
return lead + first + rest.join('') + trail;
|
|
42
|
+
}
|
|
43
|
+
/** Reserved words that cannot be a generated field/property name on each
|
|
44
|
+
* target. Deliberately the core keyword lists — mechanical and predictable
|
|
45
|
+
* beats exhaustive; an escape hatch (`AS` alias / "preserve") always exists. */
|
|
46
|
+
const TARGET_KEYWORDS = {
|
|
47
|
+
// TS interface properties/object keys may be quoted, so only names that
|
|
48
|
+
// would break generated code paths matter; keep the ES reserved core.
|
|
49
|
+
ts: new Set([
|
|
50
|
+
'break',
|
|
51
|
+
'case',
|
|
52
|
+
'catch',
|
|
53
|
+
'class',
|
|
54
|
+
'const',
|
|
55
|
+
'continue',
|
|
56
|
+
'debugger',
|
|
57
|
+
'default',
|
|
58
|
+
'delete',
|
|
59
|
+
'do',
|
|
60
|
+
'else',
|
|
61
|
+
'enum',
|
|
62
|
+
'export',
|
|
63
|
+
'extends',
|
|
64
|
+
'false',
|
|
65
|
+
'finally',
|
|
66
|
+
'for',
|
|
67
|
+
'function',
|
|
68
|
+
'if',
|
|
69
|
+
'import',
|
|
70
|
+
'in',
|
|
71
|
+
'instanceof',
|
|
72
|
+
'new',
|
|
73
|
+
'null',
|
|
74
|
+
'return',
|
|
75
|
+
'super',
|
|
76
|
+
'switch',
|
|
77
|
+
'this',
|
|
78
|
+
'throw',
|
|
79
|
+
'true',
|
|
80
|
+
'try',
|
|
81
|
+
'typeof',
|
|
82
|
+
'var',
|
|
83
|
+
'void',
|
|
84
|
+
'while',
|
|
85
|
+
'with',
|
|
86
|
+
]),
|
|
87
|
+
swift: new Set([
|
|
88
|
+
'associatedtype',
|
|
89
|
+
'class',
|
|
90
|
+
'deinit',
|
|
91
|
+
'enum',
|
|
92
|
+
'extension',
|
|
93
|
+
'fileprivate',
|
|
94
|
+
'func',
|
|
95
|
+
'import',
|
|
96
|
+
'init',
|
|
97
|
+
'inout',
|
|
98
|
+
'internal',
|
|
99
|
+
'let',
|
|
100
|
+
'open',
|
|
101
|
+
'operator',
|
|
102
|
+
'private',
|
|
103
|
+
'protocol',
|
|
104
|
+
'public',
|
|
105
|
+
'rethrows',
|
|
106
|
+
'static',
|
|
107
|
+
'struct',
|
|
108
|
+
'subscript',
|
|
109
|
+
'typealias',
|
|
110
|
+
'var',
|
|
111
|
+
'break',
|
|
112
|
+
'case',
|
|
113
|
+
'continue',
|
|
114
|
+
'default',
|
|
115
|
+
'defer',
|
|
116
|
+
'do',
|
|
117
|
+
'else',
|
|
118
|
+
'fallthrough',
|
|
119
|
+
'for',
|
|
120
|
+
'guard',
|
|
121
|
+
'if',
|
|
122
|
+
'in',
|
|
123
|
+
'repeat',
|
|
124
|
+
'return',
|
|
125
|
+
'switch',
|
|
126
|
+
'where',
|
|
127
|
+
'while',
|
|
128
|
+
'as',
|
|
129
|
+
'catch',
|
|
130
|
+
'false',
|
|
131
|
+
'is',
|
|
132
|
+
'nil',
|
|
133
|
+
'super',
|
|
134
|
+
'self',
|
|
135
|
+
'throw',
|
|
136
|
+
'throws',
|
|
137
|
+
'true',
|
|
138
|
+
'try',
|
|
139
|
+
]),
|
|
140
|
+
kotlin: new Set([
|
|
141
|
+
'as',
|
|
142
|
+
'break',
|
|
143
|
+
'class',
|
|
144
|
+
'continue',
|
|
145
|
+
'do',
|
|
146
|
+
'else',
|
|
147
|
+
'false',
|
|
148
|
+
'for',
|
|
149
|
+
'fun',
|
|
150
|
+
'if',
|
|
151
|
+
'in',
|
|
152
|
+
'interface',
|
|
153
|
+
'is',
|
|
154
|
+
'null',
|
|
155
|
+
'object',
|
|
156
|
+
'package',
|
|
157
|
+
'return',
|
|
158
|
+
'super',
|
|
159
|
+
'this',
|
|
160
|
+
'throw',
|
|
161
|
+
'true',
|
|
162
|
+
'try',
|
|
163
|
+
'typealias',
|
|
164
|
+
'typeof',
|
|
165
|
+
'val',
|
|
166
|
+
'var',
|
|
167
|
+
'when',
|
|
168
|
+
'while',
|
|
169
|
+
]),
|
|
170
|
+
dart: new Set([
|
|
171
|
+
'assert',
|
|
172
|
+
'break',
|
|
173
|
+
'case',
|
|
174
|
+
'catch',
|
|
175
|
+
'class',
|
|
176
|
+
'const',
|
|
177
|
+
'continue',
|
|
178
|
+
'default',
|
|
179
|
+
'do',
|
|
180
|
+
'else',
|
|
181
|
+
'enum',
|
|
182
|
+
'extends',
|
|
183
|
+
'false',
|
|
184
|
+
'final',
|
|
185
|
+
'finally',
|
|
186
|
+
'for',
|
|
187
|
+
'if',
|
|
188
|
+
'in',
|
|
189
|
+
'is',
|
|
190
|
+
'new',
|
|
191
|
+
'null',
|
|
192
|
+
'rethrow',
|
|
193
|
+
'return',
|
|
194
|
+
'super',
|
|
195
|
+
'switch',
|
|
196
|
+
'this',
|
|
197
|
+
'throw',
|
|
198
|
+
'true',
|
|
199
|
+
'try',
|
|
200
|
+
'var',
|
|
201
|
+
'void',
|
|
202
|
+
'while',
|
|
203
|
+
'with',
|
|
204
|
+
]),
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Map a set of SQL names to language names under `mode`, enforcing the §12
|
|
208
|
+
* hard errors within the scope (one table's columns / one query's projection
|
|
209
|
+
* / one query's params). `context` and `scope` name the error location
|
|
210
|
+
* (e.g. `queries/list.sql`, `table todos`); `targets` are the emitters this
|
|
211
|
+
* run generates — keyword hazards are only real on targets that exist.
|
|
212
|
+
*/
|
|
213
|
+
export function buildNamingMap(sqlNames, mode, context, scope, targets) {
|
|
214
|
+
if (mode === 'preserve') {
|
|
215
|
+
return sqlNames.map((sqlName) => ({ sqlName, langName: sqlName }));
|
|
216
|
+
}
|
|
217
|
+
const bySqlName = new Map();
|
|
218
|
+
const byLangName = new Map();
|
|
219
|
+
for (const sqlName of sqlNames) {
|
|
220
|
+
const langName = snakeToCamel(sqlName);
|
|
221
|
+
const clash = byLangName.get(langName);
|
|
222
|
+
if (clash !== undefined && clash !== sqlName) {
|
|
223
|
+
throw new TypegenError(context, `${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`);
|
|
224
|
+
}
|
|
225
|
+
for (const target of targets) {
|
|
226
|
+
if (TARGET_KEYWORDS[target].has(langName)) {
|
|
227
|
+
throw new TypegenError(context, `${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`);
|
|
228
|
+
}
|
|
229
|
+
if (target === 'dart' && langName.startsWith('_')) {
|
|
230
|
+
throw new TypegenError(context, `${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`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
bySqlName.set(sqlName, langName);
|
|
234
|
+
byLangName.set(langName, sqlName);
|
|
235
|
+
}
|
|
236
|
+
return sqlNames.map((sqlName) => ({
|
|
237
|
+
sqlName,
|
|
238
|
+
langName: bySqlName.get(sqlName),
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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.js';
|
|
13
|
+
/** Serialize analyzed queries as the deterministic QueryIR JSON document. */
|
|
14
|
+
export declare function serializeQueryIr(queries: readonly AnalyzedQuery[]): string;
|
package/dist/query-ir.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Serialize analyzed queries as the deterministic QueryIR JSON document. */
|
|
2
|
+
export function serializeQueryIr(queries) {
|
|
3
|
+
const doc = {
|
|
4
|
+
queryIrVersion: 1,
|
|
5
|
+
queries: queries.map((query) => ({
|
|
6
|
+
name: query.name,
|
|
7
|
+
file: query.file,
|
|
8
|
+
sourceSql: query.sourceSql,
|
|
9
|
+
sql: query.sql,
|
|
10
|
+
positionalSql: query.positionalSql,
|
|
11
|
+
params: query.params.map((param) => ({
|
|
12
|
+
name: param.name,
|
|
13
|
+
langName: param.langName,
|
|
14
|
+
type: param.type,
|
|
15
|
+
// §4 metadata — emitted only when set, so `.sql`-tier IR bytes are
|
|
16
|
+
// unchanged from before the DSL existed.
|
|
17
|
+
...(param.optional === true ? { optional: true } : {}),
|
|
18
|
+
...(param.group !== undefined ? { group: param.group } : {}),
|
|
19
|
+
...(param.flag === true ? { flag: true } : {}),
|
|
20
|
+
})),
|
|
21
|
+
columns: query.columns.map((column) => ({
|
|
22
|
+
name: column.name,
|
|
23
|
+
langName: column.langName,
|
|
24
|
+
type: column.type,
|
|
25
|
+
nullable: column.nullable,
|
|
26
|
+
fidelity: column.fidelity,
|
|
27
|
+
})),
|
|
28
|
+
tables: query.tables,
|
|
29
|
+
// §6 knob metadata — emitted only when declared.
|
|
30
|
+
...(query.orderBy !== undefined
|
|
31
|
+
? {
|
|
32
|
+
orderBy: {
|
|
33
|
+
allowed: query.orderBy.allowed.map((c) => ({
|
|
34
|
+
name: c.name,
|
|
35
|
+
langName: c.langName,
|
|
36
|
+
})),
|
|
37
|
+
defaultColumn: query.orderBy.defaultColumn,
|
|
38
|
+
defaultDir: query.orderBy.defaultDir,
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
: {}),
|
|
42
|
+
...(query.limit !== undefined ? { limit: query.limit } : {}),
|
|
43
|
+
...(query.positionalSqlBase !== undefined
|
|
44
|
+
? { positionalSqlBase: query.positionalSqlBase }
|
|
45
|
+
: {}),
|
|
46
|
+
// §7 variant backend — emitted only when the query opts in.
|
|
47
|
+
...(query.variantGroups !== undefined
|
|
48
|
+
? {
|
|
49
|
+
variantGroups: query.variantGroups.map((g) => ({
|
|
50
|
+
key: g.key,
|
|
51
|
+
params: g.params,
|
|
52
|
+
flag: g.flag,
|
|
53
|
+
})),
|
|
54
|
+
}
|
|
55
|
+
: {}),
|
|
56
|
+
...(query.variants !== undefined
|
|
57
|
+
? {
|
|
58
|
+
variants: query.variants.map((v) => ({
|
|
59
|
+
when: v.when,
|
|
60
|
+
sql: v.sql,
|
|
61
|
+
positionalSql: v.positionalSql,
|
|
62
|
+
params: v.params,
|
|
63
|
+
})),
|
|
64
|
+
}
|
|
65
|
+
: {}),
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
69
|
+
}
|
package/dist/query.d.ts
CHANGED
|
@@ -1,36 +1,121 @@
|
|
|
1
1
|
import type { IrColumnType, IrDocument } from './ir.js';
|
|
2
|
+
import { type NamingMode, type NamingTarget } from './naming.js';
|
|
2
3
|
/** A param type is one of the §2.4 types (the columns params compare to). */
|
|
3
4
|
export type QueryParamType = IrColumnType;
|
|
4
5
|
export interface QueryParam {
|
|
6
|
+
/** SQL-truth `:name` (as authored). */
|
|
5
7
|
readonly name: string;
|
|
8
|
+
/** Language-facing name (§5 naming map; equals `name` under "preserve"). */
|
|
9
|
+
readonly langName: string;
|
|
6
10
|
readonly type: QueryParamType;
|
|
7
11
|
/** How the type was resolved — for the docs/tests, not emitted. */
|
|
8
12
|
readonly source: 'inferred' | 'comment';
|
|
13
|
+
/** §4 (DESIGN-queries.md): an optional param — its auto-guarded conjunct
|
|
14
|
+
* applies only when it is provided. Always false for the `.sql` tier. */
|
|
15
|
+
readonly optional?: boolean;
|
|
16
|
+
/** §4: the `from+to` pairing — params sharing a group apply together. */
|
|
17
|
+
readonly group?: string;
|
|
18
|
+
/** §3: a `: flag` boolean guard param (never in a predicate as written;
|
|
19
|
+
* the lowering binds it). Implies optional. */
|
|
20
|
+
readonly flag?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** §6 orderBy knob: a generate-time allowlist (identifiers cannot bind). */
|
|
23
|
+
export interface QueryOrderBy {
|
|
24
|
+
/** Allowed columns: authored SQL name + the language-facing key the
|
|
25
|
+
* generated signature accepts. Each is SQLite-checked at generate time. */
|
|
26
|
+
readonly allowed: readonly {
|
|
27
|
+
name: string;
|
|
28
|
+
langName: string;
|
|
29
|
+
}[];
|
|
30
|
+
/** Default column (authored SQL name). */
|
|
31
|
+
readonly defaultColumn: string;
|
|
32
|
+
readonly defaultDir: 'asc' | 'desc';
|
|
33
|
+
}
|
|
34
|
+
/** §6 limit knob: a bound value with a codegen clamp + default. */
|
|
35
|
+
export interface QueryLimit {
|
|
36
|
+
readonly max?: number;
|
|
37
|
+
readonly default?: number;
|
|
9
38
|
}
|
|
10
39
|
export interface QueryColumn {
|
|
40
|
+
/** SQL-truth result name (pre-lowering, as authored). */
|
|
11
41
|
readonly name: string;
|
|
42
|
+
/** Language-facing name — the RUNTIME result key after §5 projection
|
|
43
|
+
* lowering (equals `name` under "preserve"). */
|
|
44
|
+
readonly langName: string;
|
|
12
45
|
readonly type: IrColumnType;
|
|
13
46
|
readonly nullable: boolean;
|
|
14
47
|
/** `exact` = resolved to an IR column (plain ref); `fallback` = a computed
|
|
15
48
|
* expression typed by the documented honest fallback. */
|
|
16
49
|
readonly fidelity: 'exact' | 'fallback';
|
|
17
50
|
}
|
|
51
|
+
/** §7/§8 backend selection: neutralization (default), always-variants, or
|
|
52
|
+
* the small-N heuristic (`auto`: enumerate at ≤ 2 optional groups). */
|
|
53
|
+
export type QueryBackend = 'neutralize' | 'variants' | 'auto';
|
|
54
|
+
/** Naming/backend options threaded from the manifest into query analysis. */
|
|
55
|
+
export interface QueryNamingOptions {
|
|
56
|
+
readonly naming: NamingMode;
|
|
57
|
+
/** Emitter targets this run generates — keyword hazards are only real on
|
|
58
|
+
* targets that exist. */
|
|
59
|
+
readonly targets: readonly NamingTarget[];
|
|
60
|
+
/** `.syql` conditional-lowering backend (default `neutralize`); the
|
|
61
|
+
* per-query `variants` knob always forces enumeration. */
|
|
62
|
+
readonly backend?: QueryBackend;
|
|
63
|
+
}
|
|
18
64
|
export interface AnalyzedQuery {
|
|
19
65
|
/** camelCase function name (path-derived, or a `-- name:` override). */
|
|
20
66
|
readonly name: string;
|
|
21
67
|
/** Source location for errors, e.g. `billing/list.sql` (or `list.sql#2`
|
|
22
68
|
* for the 2nd statement of a multi-statement file). */
|
|
23
69
|
readonly file: string;
|
|
24
|
-
/** The SQL as
|
|
70
|
+
/** The SQL as authored (named `:params`), trimmed — pre-lowering. */
|
|
71
|
+
readonly sourceSql: string;
|
|
72
|
+
/** The LOWERED SQL (named `:params`): §5 projection aliasing applied, so
|
|
73
|
+
* runtime result keys are the language-facing names. Equals `sourceSql`
|
|
74
|
+
* when nothing needed rewriting. */
|
|
25
75
|
readonly sql: string;
|
|
26
|
-
/** The SQL with `:name` rewritten to positional
|
|
76
|
+
/** The lowered SQL with `:name` rewritten to positional `?`. */
|
|
27
77
|
readonly positionalSql: string;
|
|
28
78
|
/** Params in first-occurrence (positional) order. */
|
|
29
79
|
readonly params: readonly QueryParam[];
|
|
30
80
|
/** Result columns in SELECT order. */
|
|
31
81
|
readonly columns: readonly QueryColumn[];
|
|
32
|
-
/** IR tables this query reads (the
|
|
82
|
+
/** IR tables this query reads (the useRawSql `{tables}` set), sorted. */
|
|
33
83
|
readonly tables: readonly string[];
|
|
84
|
+
/** §6 orderBy knob (`.syql` tier). When present, `sql`/`positionalSql`
|
|
85
|
+
* carry the DEFAULT order-by tail and `positionalSqlBase` is the static
|
|
86
|
+
* prefix emitters compose the selected column onto. */
|
|
87
|
+
readonly orderBy?: QueryOrderBy;
|
|
88
|
+
/** §6 limit knob (`.syql` tier): the trailing `LIMIT ?` bind's clamp. */
|
|
89
|
+
readonly limit?: QueryLimit;
|
|
90
|
+
/** positional SQL WITHOUT the dynamic ORDER BY/LIMIT tail; present only
|
|
91
|
+
* when `orderBy` is (emitters build `base + ORDER BY <baked> + limit
|
|
92
|
+
* tail`). */
|
|
93
|
+
readonly positionalSqlBase?: string;
|
|
94
|
+
/** The positional limit tail (e.g. ` limit min(coalesce(?, 50), 100)` —
|
|
95
|
+
* the default+clamp live IN the SQL, so runtimes bind `limit ?? null`).
|
|
96
|
+
* Present only when BOTH knobs are declared (composers need it verbatim). */
|
|
97
|
+
readonly positionalLimitTail?: string;
|
|
98
|
+
/** §7 variant-enumeration backend (opt-in `variants` knob): one checked
|
|
99
|
+
* statement per combination of provided optional groups. Semantically
|
|
100
|
+
* identical to the neutralization `sql` by construction; the TS emitter
|
|
101
|
+
* dispatches on provided-ness for perfect index use. `when` lists the
|
|
102
|
+
* provided group keys; `params` are the DISTINCT param names this variant
|
|
103
|
+
* binds, positional order. */
|
|
104
|
+
readonly variants?: readonly {
|
|
105
|
+
readonly when: readonly string[];
|
|
106
|
+
readonly sql: string;
|
|
107
|
+
readonly positionalSql: string;
|
|
108
|
+
readonly params: readonly string[];
|
|
109
|
+
}[];
|
|
110
|
+
/** The optional-group keys, in dispatch-bit order (bit i of the variant
|
|
111
|
+
* index = group i provided). Present iff `variants` is. */
|
|
112
|
+
readonly variantGroups?: readonly {
|
|
113
|
+
readonly key: string;
|
|
114
|
+
/** Params whose provision defines the group (all must be non-null; a
|
|
115
|
+
* flag group is "on" when its single param is TRUE). */
|
|
116
|
+
readonly params: readonly string[];
|
|
117
|
+
readonly flag: boolean;
|
|
118
|
+
}[];
|
|
34
119
|
}
|
|
35
120
|
/**
|
|
36
121
|
* Path-relative default name. `billing/invoices/list.sql` → `billingInvoicesList`;
|
|
@@ -56,6 +141,17 @@ export interface SplitStatement {
|
|
|
56
141
|
export declare function splitStatements(content: string): SplitStatement[];
|
|
57
142
|
/** The `-- name:` override in a statement's leading comment block, or null. */
|
|
58
143
|
export declare function parseStatementName(statementText: string, location: string): string | null;
|
|
144
|
+
/** Rewrite `:name` → positional placeholders AND strip comments + collapse
|
|
145
|
+
* whitespace, producing a clean single-line SQL string suitable for
|
|
146
|
+
* embedding in a generated string literal. String literals are preserved
|
|
147
|
+
* verbatim (their inner whitespace is not collapsed).
|
|
148
|
+
*
|
|
149
|
+
* When every param occurs exactly once, plain `?` is emitted. When ANY
|
|
150
|
+
* param repeats (the §7 neutralization guards mention a param in the guard
|
|
151
|
+
* AND the predicate), the WHOLE statement uses SQLite's numbered `?N`
|
|
152
|
+
* form — one bound value per DISTINCT param, reuse resolved by SQLite —
|
|
153
|
+
* so the generated bind array stays one-entry-per-param. */
|
|
154
|
+
export declare function toPositionalSql(sql: string): string;
|
|
59
155
|
export interface QueryDb {
|
|
60
156
|
/** Prepare + return the result columns / declared types / param count.
|
|
61
157
|
* Throws (with SQLite's message) when the SQL is invalid against the DDL. */
|
|
@@ -77,7 +173,7 @@ export declare function synthesizeDdl(ir: IrDocument): string;
|
|
|
77
173
|
* top-level `;`). This is the per-statement core; {@link analyzeQueryFile}
|
|
78
174
|
* splits a file and drives it.
|
|
79
175
|
*/
|
|
80
|
-
export declare function analyzeStatement(name: string, location: string, statementText: string, ir: IrDocument, db: QueryDb): AnalyzedQuery;
|
|
176
|
+
export declare function analyzeStatement(name: string, location: string, statementText: string, ir: IrDocument, db: QueryDb, naming?: QueryNamingOptions): AnalyzedQuery;
|
|
81
177
|
/**
|
|
82
178
|
* Analyze a whole query FILE: split into statements, resolve each statement's
|
|
83
179
|
* name (path-derived default, or a `-- name:` override), enforce the
|
|
@@ -89,8 +185,8 @@ export declare function analyzeStatement(name: string, location: string, stateme
|
|
|
89
185
|
* - A file with MULTIPLE statements requires `-- name:` on EVERY statement; a
|
|
90
186
|
* missing one errors, naming the file + the statement's position/first line.
|
|
91
187
|
*/
|
|
92
|
-
export declare function analyzeQueryFile(relPath: string, content: string, ir: IrDocument, db: QueryDb): AnalyzedQuery[];
|
|
188
|
+
export declare function analyzeQueryFile(relPath: string, content: string, ir: IrDocument, db: QueryDb, naming?: QueryNamingOptions): AnalyzedQuery[];
|
|
93
189
|
/** Back-compat single-statement analyzer: resolve the name from the file path
|
|
94
190
|
* (or a `-- name:` override) and analyze. Rejects a `;`-separated file loudly
|
|
95
191
|
* — use {@link analyzeQueryFile} for the multi-statement contract. */
|
|
96
|
-
export declare function analyzeQuery(file: string, raw: string, ir: IrDocument, db: QueryDb): AnalyzedQuery;
|
|
192
|
+
export declare function analyzeQuery(file: string, raw: string, ir: IrDocument, db: QueryDb, naming?: QueryNamingOptions): AnalyzedQuery;
|