@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/query.ts
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
* error listing the fix. bun:sqlite can't name params, so we parse `:name`
|
|
49
49
|
* tokens from the SQL ourselves (first-occurrence order = positional order).
|
|
50
50
|
*
|
|
51
|
-
* ## The tables set (for
|
|
51
|
+
* ## The tables set (for useRawSql `{tables}` — exact invalidation)
|
|
52
52
|
*
|
|
53
53
|
* bun:sqlite exposes no authorizer and no statement table-list; EXPLAIN
|
|
54
54
|
* opcodes are fragile. The honest mechanism: the FROM/JOIN table set we
|
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
*/
|
|
63
63
|
import { TypegenError } from './errors';
|
|
64
64
|
import type { IrColumnType, IrDocument, IrTable } from './ir';
|
|
65
|
+
import { lowerProjection, mainVerbAfterWith } from './lower';
|
|
66
|
+
import { buildNamingMap, type NamingMode, type NamingTarget } from './naming';
|
|
65
67
|
|
|
66
68
|
/** The SQL decltype keyword → §2.4 type map (mirrors sql.ts TYPE_MAP so a
|
|
67
69
|
* plain column ref's decltype resolves to the exact IR type). */
|
|
@@ -89,14 +91,45 @@ const DECLTYPE_MAP: Readonly<Record<string, IrColumnType>> = {
|
|
|
89
91
|
export type QueryParamType = IrColumnType;
|
|
90
92
|
|
|
91
93
|
export interface QueryParam {
|
|
94
|
+
/** SQL-truth `:name` (as authored). */
|
|
92
95
|
readonly name: string;
|
|
96
|
+
/** Language-facing name (§5 naming map; equals `name` under "preserve"). */
|
|
97
|
+
readonly langName: string;
|
|
93
98
|
readonly type: QueryParamType;
|
|
94
99
|
/** How the type was resolved — for the docs/tests, not emitted. */
|
|
95
100
|
readonly source: 'inferred' | 'comment';
|
|
101
|
+
/** §4 (DESIGN-queries.md): an optional param — its auto-guarded conjunct
|
|
102
|
+
* applies only when it is provided. Always false for the `.sql` tier. */
|
|
103
|
+
readonly optional?: boolean;
|
|
104
|
+
/** §4: the `from+to` pairing — params sharing a group apply together. */
|
|
105
|
+
readonly group?: string;
|
|
106
|
+
/** §3: a `: flag` boolean guard param (never in a predicate as written;
|
|
107
|
+
* the lowering binds it). Implies optional. */
|
|
108
|
+
readonly flag?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** §6 orderBy knob: a generate-time allowlist (identifiers cannot bind). */
|
|
112
|
+
export interface QueryOrderBy {
|
|
113
|
+
/** Allowed columns: authored SQL name + the language-facing key the
|
|
114
|
+
* generated signature accepts. Each is SQLite-checked at generate time. */
|
|
115
|
+
readonly allowed: readonly { name: string; langName: string }[];
|
|
116
|
+
/** Default column (authored SQL name). */
|
|
117
|
+
readonly defaultColumn: string;
|
|
118
|
+
readonly defaultDir: 'asc' | 'desc';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** §6 limit knob: a bound value with a codegen clamp + default. */
|
|
122
|
+
export interface QueryLimit {
|
|
123
|
+
readonly max?: number;
|
|
124
|
+
readonly default?: number;
|
|
96
125
|
}
|
|
97
126
|
|
|
98
127
|
export interface QueryColumn {
|
|
128
|
+
/** SQL-truth result name (pre-lowering, as authored). */
|
|
99
129
|
readonly name: string;
|
|
130
|
+
/** Language-facing name — the RUNTIME result key after §5 projection
|
|
131
|
+
* lowering (equals `name` under "preserve"). */
|
|
132
|
+
readonly langName: string;
|
|
100
133
|
readonly type: IrColumnType;
|
|
101
134
|
readonly nullable: boolean;
|
|
102
135
|
/** `exact` = resolved to an IR column (plain ref); `fallback` = a computed
|
|
@@ -104,22 +137,78 @@ export interface QueryColumn {
|
|
|
104
137
|
readonly fidelity: 'exact' | 'fallback';
|
|
105
138
|
}
|
|
106
139
|
|
|
140
|
+
/** §7/§8 backend selection: neutralization (default), always-variants, or
|
|
141
|
+
* the small-N heuristic (`auto`: enumerate at ≤ 2 optional groups). */
|
|
142
|
+
export type QueryBackend = 'neutralize' | 'variants' | 'auto';
|
|
143
|
+
|
|
144
|
+
/** Naming/backend options threaded from the manifest into query analysis. */
|
|
145
|
+
export interface QueryNamingOptions {
|
|
146
|
+
readonly naming: NamingMode;
|
|
147
|
+
/** Emitter targets this run generates — keyword hazards are only real on
|
|
148
|
+
* targets that exist. */
|
|
149
|
+
readonly targets: readonly NamingTarget[];
|
|
150
|
+
/** `.syql` conditional-lowering backend (default `neutralize`); the
|
|
151
|
+
* per-query `variants` knob always forces enumeration. */
|
|
152
|
+
readonly backend?: QueryBackend;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const DEFAULT_NAMING: QueryNamingOptions = { naming: 'camel', targets: ['ts'] };
|
|
156
|
+
|
|
107
157
|
export interface AnalyzedQuery {
|
|
108
158
|
/** camelCase function name (path-derived, or a `-- name:` override). */
|
|
109
159
|
readonly name: string;
|
|
110
160
|
/** Source location for errors, e.g. `billing/list.sql` (or `list.sql#2`
|
|
111
161
|
* for the 2nd statement of a multi-statement file). */
|
|
112
162
|
readonly file: string;
|
|
113
|
-
/** The SQL as
|
|
163
|
+
/** The SQL as authored (named `:params`), trimmed — pre-lowering. */
|
|
164
|
+
readonly sourceSql: string;
|
|
165
|
+
/** The LOWERED SQL (named `:params`): §5 projection aliasing applied, so
|
|
166
|
+
* runtime result keys are the language-facing names. Equals `sourceSql`
|
|
167
|
+
* when nothing needed rewriting. */
|
|
114
168
|
readonly sql: string;
|
|
115
|
-
/** The SQL with `:name` rewritten to positional
|
|
169
|
+
/** The lowered SQL with `:name` rewritten to positional `?`. */
|
|
116
170
|
readonly positionalSql: string;
|
|
117
171
|
/** Params in first-occurrence (positional) order. */
|
|
118
172
|
readonly params: readonly QueryParam[];
|
|
119
173
|
/** Result columns in SELECT order. */
|
|
120
174
|
readonly columns: readonly QueryColumn[];
|
|
121
|
-
/** IR tables this query reads (the
|
|
175
|
+
/** IR tables this query reads (the useRawSql `{tables}` set), sorted. */
|
|
122
176
|
readonly tables: readonly string[];
|
|
177
|
+
/** §6 orderBy knob (`.syql` tier). When present, `sql`/`positionalSql`
|
|
178
|
+
* carry the DEFAULT order-by tail and `positionalSqlBase` is the static
|
|
179
|
+
* prefix emitters compose the selected column onto. */
|
|
180
|
+
readonly orderBy?: QueryOrderBy;
|
|
181
|
+
/** §6 limit knob (`.syql` tier): the trailing `LIMIT ?` bind's clamp. */
|
|
182
|
+
readonly limit?: QueryLimit;
|
|
183
|
+
/** positional SQL WITHOUT the dynamic ORDER BY/LIMIT tail; present only
|
|
184
|
+
* when `orderBy` is (emitters build `base + ORDER BY <baked> + limit
|
|
185
|
+
* tail`). */
|
|
186
|
+
readonly positionalSqlBase?: string;
|
|
187
|
+
/** The positional limit tail (e.g. ` limit min(coalesce(?, 50), 100)` —
|
|
188
|
+
* the default+clamp live IN the SQL, so runtimes bind `limit ?? null`).
|
|
189
|
+
* Present only when BOTH knobs are declared (composers need it verbatim). */
|
|
190
|
+
readonly positionalLimitTail?: string;
|
|
191
|
+
/** §7 variant-enumeration backend (opt-in `variants` knob): one checked
|
|
192
|
+
* statement per combination of provided optional groups. Semantically
|
|
193
|
+
* identical to the neutralization `sql` by construction; the TS emitter
|
|
194
|
+
* dispatches on provided-ness for perfect index use. `when` lists the
|
|
195
|
+
* provided group keys; `params` are the DISTINCT param names this variant
|
|
196
|
+
* binds, positional order. */
|
|
197
|
+
readonly variants?: readonly {
|
|
198
|
+
readonly when: readonly string[];
|
|
199
|
+
readonly sql: string;
|
|
200
|
+
readonly positionalSql: string;
|
|
201
|
+
readonly params: readonly string[];
|
|
202
|
+
}[];
|
|
203
|
+
/** The optional-group keys, in dispatch-bit order (bit i of the variant
|
|
204
|
+
* index = group i provided). Present iff `variants` is. */
|
|
205
|
+
readonly variantGroups?: readonly {
|
|
206
|
+
readonly key: string;
|
|
207
|
+
/** Params whose provision defines the group (all must be non-null; a
|
|
208
|
+
* flag group is "on" when its single param is TRUE). */
|
|
209
|
+
readonly params: readonly string[];
|
|
210
|
+
readonly flag: boolean;
|
|
211
|
+
}[];
|
|
123
212
|
}
|
|
124
213
|
|
|
125
214
|
// -- path → name --------------------------------------------------------------
|
|
@@ -422,12 +511,21 @@ function scanParamNames(sql: string): string[] {
|
|
|
422
511
|
return seen;
|
|
423
512
|
}
|
|
424
513
|
|
|
425
|
-
/** Rewrite `:name` → positional
|
|
426
|
-
*
|
|
514
|
+
/** Rewrite `:name` → positional placeholders AND strip comments + collapse
|
|
515
|
+
* whitespace, producing a clean single-line SQL string suitable for
|
|
427
516
|
* embedding in a generated string literal. String literals are preserved
|
|
428
|
-
* verbatim (their inner whitespace is not collapsed).
|
|
429
|
-
|
|
430
|
-
|
|
517
|
+
* verbatim (their inner whitespace is not collapsed).
|
|
518
|
+
*
|
|
519
|
+
* When every param occurs exactly once, plain `?` is emitted. When ANY
|
|
520
|
+
* param repeats (the §7 neutralization guards mention a param in the guard
|
|
521
|
+
* AND the predicate), the WHOLE statement uses SQLite's numbered `?N`
|
|
522
|
+
* form — one bound value per DISTINCT param, reuse resolved by SQLite —
|
|
523
|
+
* so the generated bind array stays one-entry-per-param. */
|
|
524
|
+
export function toPositionalSql(sql: string): string {
|
|
525
|
+
interface ParamToken {
|
|
526
|
+
readonly param: string;
|
|
527
|
+
}
|
|
528
|
+
const tokens: (string | ParamToken)[] = [];
|
|
431
529
|
let i = 0;
|
|
432
530
|
while (i < sql.length) {
|
|
433
531
|
const ch = sql[i] as string;
|
|
@@ -467,7 +565,7 @@ function toPositionalSql(sql: string): string {
|
|
|
467
565
|
while (end < sql.length && /[A-Za-z0-9_]/.test(sql[end] as string)) {
|
|
468
566
|
end += 1;
|
|
469
567
|
}
|
|
470
|
-
tokens.push(
|
|
568
|
+
tokens.push({ param: sql.slice(i + 1, end) });
|
|
471
569
|
i = end;
|
|
472
570
|
} else if (/\s/.test(ch)) {
|
|
473
571
|
tokens.push(' ');
|
|
@@ -477,8 +575,21 @@ function toPositionalSql(sql: string): string {
|
|
|
477
575
|
i += 1;
|
|
478
576
|
}
|
|
479
577
|
}
|
|
578
|
+
const order: string[] = [];
|
|
579
|
+
let repeats = false;
|
|
580
|
+
for (const token of tokens) {
|
|
581
|
+
if (typeof token === 'string') continue;
|
|
582
|
+
if (order.includes(token.param)) repeats = true;
|
|
583
|
+
else order.push(token.param);
|
|
584
|
+
}
|
|
585
|
+
const rendered = tokens
|
|
586
|
+
.map((token) => {
|
|
587
|
+
if (typeof token === 'string') return token;
|
|
588
|
+
return repeats ? `?${order.indexOf(token.param) + 1}` : '?';
|
|
589
|
+
})
|
|
590
|
+
.join('');
|
|
480
591
|
// Collapse runs of the whitespace placeholders; trim.
|
|
481
|
-
return
|
|
592
|
+
return rendered.replace(/\s+/g, ' ').trim();
|
|
482
593
|
}
|
|
483
594
|
|
|
484
595
|
// -- FROM/JOIN table resolution ----------------------------------------------
|
|
@@ -671,6 +782,31 @@ function inferParamType(
|
|
|
671
782
|
const t = lookup(inM[1], inM[2] as string);
|
|
672
783
|
if (t !== null) return t;
|
|
673
784
|
}
|
|
785
|
+
// `col BETWEEN :name AND …` / `col BETWEEN … AND :name` — both endpoints
|
|
786
|
+
// take the column's type (the §4 from+to group shape).
|
|
787
|
+
const btwLeft = new RegExp(
|
|
788
|
+
`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+:${paramName}\\b`,
|
|
789
|
+
'i',
|
|
790
|
+
);
|
|
791
|
+
const btwLeftM = btwLeft.exec(cleaned);
|
|
792
|
+
if (btwLeftM !== null) {
|
|
793
|
+
const t = lookup(btwLeftM[1], btwLeftM[2] as string);
|
|
794
|
+
if (t !== null) return t;
|
|
795
|
+
}
|
|
796
|
+
const btwRight = new RegExp(
|
|
797
|
+
`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+\\S+\\s+AND\\s+:${paramName}\\b`,
|
|
798
|
+
'i',
|
|
799
|
+
);
|
|
800
|
+
const btwRightM = btwRight.exec(cleaned);
|
|
801
|
+
if (btwRightM !== null) {
|
|
802
|
+
const t = lookup(btwRightM[1], btwRightM[2] as string);
|
|
803
|
+
if (t !== null) return t;
|
|
804
|
+
}
|
|
805
|
+
// `… LIKE <expr mentioning :name>` — a LIKE operand is TEXT, even inside
|
|
806
|
+
// a concatenation (`title like '%' || :q || '%'`, the search-fragment
|
|
807
|
+
// shape).
|
|
808
|
+
const like = new RegExp(`\\bLIKE\\b[^()]*:${paramName}\\b`, 'i');
|
|
809
|
+
if (like.test(cleaned)) return 'string';
|
|
674
810
|
return null;
|
|
675
811
|
}
|
|
676
812
|
|
|
@@ -747,36 +883,49 @@ export function analyzeStatement(
|
|
|
747
883
|
statementText: string,
|
|
748
884
|
ir: IrDocument,
|
|
749
885
|
db: QueryDb,
|
|
886
|
+
naming: QueryNamingOptions = DEFAULT_NAMING,
|
|
750
887
|
): AnalyzedQuery {
|
|
751
888
|
const file = location;
|
|
752
889
|
const commentParams = parseCommentParams(file, statementText);
|
|
753
|
-
const
|
|
754
|
-
if (
|
|
890
|
+
const sourceSql = statementText.trim();
|
|
891
|
+
if (sourceSql.length === 0) {
|
|
755
892
|
throw new TypegenError(file, 'query file is empty');
|
|
756
893
|
}
|
|
757
894
|
|
|
758
|
-
// SELECT-only (the read tier).
|
|
895
|
+
// SELECT-only (the read tier). A `WITH` is allowed when its main statement
|
|
896
|
+
// is a SELECT (SQLite also allows WITH … INSERT/UPDATE/DELETE — writes).
|
|
759
897
|
const firstKeyword = /^\s*([A-Za-z]+)/.exec(
|
|
760
898
|
stripCommentsAndStrings(statementText).trimStart(),
|
|
761
899
|
)?.[1];
|
|
762
|
-
|
|
900
|
+
const upperKeyword = firstKeyword?.toUpperCase();
|
|
901
|
+
if (upperKeyword === 'WITH') {
|
|
902
|
+
const main = mainVerbAfterWith(stripCommentsAndStrings(statementText));
|
|
903
|
+
if (main !== 'SELECT') {
|
|
904
|
+
throw new TypegenError(
|
|
905
|
+
file,
|
|
906
|
+
`named queries are SELECT-only (the read tier); this WITH statement's main verb is ${JSON.stringify(main ?? '')}. Writes go through mutate() (SPEC §7.1).`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
} else if (upperKeyword !== 'SELECT') {
|
|
763
910
|
throw new TypegenError(
|
|
764
911
|
file,
|
|
765
912
|
`named queries are SELECT-only (the read tier); found ${JSON.stringify(firstKeyword ?? '')}. Writes go through mutate() (SPEC §7.1).`,
|
|
766
913
|
);
|
|
767
914
|
}
|
|
768
915
|
|
|
769
|
-
// Let SQLite validate + describe the query. Any bad
|
|
770
|
-
// with SQLite's own message
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
916
|
+
// Let SQLite validate + describe the AUTHORED query first. Any bad
|
|
917
|
+
// reference throws here with SQLite's own message.
|
|
918
|
+
const analyze = (candidate: string): ReturnType<QueryDb['analyze']> => {
|
|
919
|
+
try {
|
|
920
|
+
return db.analyze(candidate);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
923
|
+
throw new TypegenError(file, `SQL rejected by SQLite: ${message}`);
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
let described = analyze(sourceSql);
|
|
778
927
|
|
|
779
|
-
const refs = scanTableRefs(
|
|
928
|
+
const refs = scanTableRefs(sourceSql, ir);
|
|
780
929
|
const tableSet = new Set(refs.map((r) => r.table));
|
|
781
930
|
const tables = [...tableSet].sort();
|
|
782
931
|
if (tables.length === 0) {
|
|
@@ -786,17 +935,57 @@ export function analyzeStatement(
|
|
|
786
935
|
);
|
|
787
936
|
}
|
|
788
937
|
|
|
789
|
-
//
|
|
938
|
+
// §5 lowering: rewrite the projection so runtime result keys are the
|
|
939
|
+
// language-facing names ("camel"); a no-op pass-through under "preserve".
|
|
940
|
+
let sql = sourceSql;
|
|
941
|
+
let sqlNames: readonly string[] = described.columnNames;
|
|
942
|
+
let langNames: readonly string[] = described.columnNames;
|
|
943
|
+
if (naming.naming === 'camel') {
|
|
944
|
+
const lowered = lowerProjection(
|
|
945
|
+
sourceSql,
|
|
946
|
+
refs,
|
|
947
|
+
ir,
|
|
948
|
+
file,
|
|
949
|
+
(candidate) => analyze(candidate).columnNames,
|
|
950
|
+
(names) =>
|
|
951
|
+
buildNamingMap(names, 'camel', file, 'projection', naming.targets).map(
|
|
952
|
+
(m) => m.langName,
|
|
953
|
+
),
|
|
954
|
+
);
|
|
955
|
+
sqlNames = lowered.sqlNames;
|
|
956
|
+
langNames = lowered.langNames;
|
|
957
|
+
if (lowered.changed) {
|
|
958
|
+
sql = lowered.sql;
|
|
959
|
+
described = analyze(sql);
|
|
960
|
+
// Defensive: the rewritten projection's runtime keys must BE the
|
|
961
|
+
// language names — this is the whole point of the lowering.
|
|
962
|
+
if (
|
|
963
|
+
described.columnNames.length !== langNames.length ||
|
|
964
|
+
described.columnNames.some((n, i) => n !== langNames[i])
|
|
965
|
+
) {
|
|
966
|
+
throw new TypegenError(
|
|
967
|
+
file,
|
|
968
|
+
`internal: lowered projection keys [${described.columnNames.join(', ')}] do not match the naming map [${langNames.join(', ')}]`,
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Columns: pair bun's column names + decltypes with our source resolution
|
|
975
|
+
// (against the LOWERED sql — aliased plain refs still resolve exactly).
|
|
790
976
|
const items = splitSelectList(sql);
|
|
791
977
|
const columns: QueryColumn[] = described.columnNames.map((colName, index) => {
|
|
792
978
|
const decl = described.declaredTypes[index];
|
|
793
979
|
const item = items?.[index];
|
|
794
980
|
const source = item !== undefined ? resolveSource(item, refs, ir) : null;
|
|
981
|
+
const sqlName = sqlNames[index] ?? colName;
|
|
982
|
+
const langName = langNames[index] ?? colName;
|
|
795
983
|
if (source !== null) {
|
|
796
984
|
// Exact: IR column type + IR nullability. (decltype agrees; we prefer
|
|
797
985
|
// the IR type so blob_ref/crdt/json semantic types survive.)
|
|
798
986
|
return {
|
|
799
|
-
name:
|
|
987
|
+
name: sqlName,
|
|
988
|
+
langName,
|
|
800
989
|
type: source.column.type,
|
|
801
990
|
nullable: source.column.nullable,
|
|
802
991
|
fidelity: 'exact',
|
|
@@ -809,11 +998,18 @@ export function analyzeStatement(
|
|
|
809
998
|
: undefined;
|
|
810
999
|
const type =
|
|
811
1000
|
mapped ?? (item !== undefined ? fallbackColumnType(item.expr) : 'string');
|
|
812
|
-
return {
|
|
1001
|
+
return {
|
|
1002
|
+
name: sqlName,
|
|
1003
|
+
langName,
|
|
1004
|
+
type,
|
|
1005
|
+
nullable: true,
|
|
1006
|
+
fidelity: 'fallback',
|
|
1007
|
+
};
|
|
813
1008
|
});
|
|
814
1009
|
|
|
815
1010
|
// Params: names from the text (positional order), types from inference or
|
|
816
|
-
// the `-- param` comment; every param must resolve.
|
|
1011
|
+
// the `-- param` comment; every param must resolve. Param langNames go
|
|
1012
|
+
// through the same §12 map (authored camelCase params are identity).
|
|
817
1013
|
const paramNames = scanParamNames(sql);
|
|
818
1014
|
if (paramNames.length !== described.paramsCount) {
|
|
819
1015
|
// Defensive: our scan and SQLite disagree (shouldn't happen for the subset).
|
|
@@ -822,15 +1018,23 @@ export function analyzeStatement(
|
|
|
822
1018
|
`internal: scanned ${paramNames.length} params (${paramNames.join(', ')}) but SQLite reports ${described.paramsCount}`,
|
|
823
1019
|
);
|
|
824
1020
|
}
|
|
1021
|
+
const paramLangNames = buildNamingMap(
|
|
1022
|
+
paramNames,
|
|
1023
|
+
naming.naming,
|
|
1024
|
+
file,
|
|
1025
|
+
'params',
|
|
1026
|
+
naming.targets,
|
|
1027
|
+
);
|
|
825
1028
|
const commentByName = new Map(commentParams.map((p) => [p.name, p.type]));
|
|
826
|
-
const params: QueryParam[] = paramNames.map((paramName) => {
|
|
1029
|
+
const params: QueryParam[] = paramNames.map((paramName, index) => {
|
|
1030
|
+
const langName = paramLangNames[index]?.langName ?? paramName;
|
|
827
1031
|
const commented = commentByName.get(paramName);
|
|
828
1032
|
if (commented !== undefined) {
|
|
829
|
-
return { name: paramName, type: commented, source: 'comment' };
|
|
1033
|
+
return { name: paramName, langName, type: commented, source: 'comment' };
|
|
830
1034
|
}
|
|
831
1035
|
const inferred = inferParamType(paramName, sql, refs, ir);
|
|
832
1036
|
if (inferred !== null) {
|
|
833
|
-
return { name: paramName, type: inferred, source: 'inferred' };
|
|
1037
|
+
return { name: paramName, langName, type: inferred, source: 'inferred' };
|
|
834
1038
|
}
|
|
835
1039
|
throw new TypegenError(
|
|
836
1040
|
file,
|
|
@@ -850,6 +1054,7 @@ export function analyzeStatement(
|
|
|
850
1054
|
return {
|
|
851
1055
|
name,
|
|
852
1056
|
file,
|
|
1057
|
+
sourceSql,
|
|
853
1058
|
sql,
|
|
854
1059
|
positionalSql: toPositionalSql(sql),
|
|
855
1060
|
params,
|
|
@@ -874,6 +1079,7 @@ export function analyzeQueryFile(
|
|
|
874
1079
|
content: string,
|
|
875
1080
|
ir: IrDocument,
|
|
876
1081
|
db: QueryDb,
|
|
1082
|
+
naming: QueryNamingOptions = DEFAULT_NAMING,
|
|
877
1083
|
): AnalyzedQuery[] {
|
|
878
1084
|
const statements = splitStatements(content);
|
|
879
1085
|
if (statements.length === 0) {
|
|
@@ -893,7 +1099,7 @@ export function analyzeQueryFile(
|
|
|
893
1099
|
);
|
|
894
1100
|
}
|
|
895
1101
|
const name = override ?? defaultName;
|
|
896
|
-
return analyzeStatement(name, location, stmt.text, ir, db);
|
|
1102
|
+
return analyzeStatement(name, location, stmt.text, ir, db, naming);
|
|
897
1103
|
});
|
|
898
1104
|
}
|
|
899
1105
|
|
|
@@ -905,6 +1111,7 @@ export function analyzeQuery(
|
|
|
905
1111
|
raw: string,
|
|
906
1112
|
ir: IrDocument,
|
|
907
1113
|
db: QueryDb,
|
|
1114
|
+
naming: QueryNamingOptions = DEFAULT_NAMING,
|
|
908
1115
|
): AnalyzedQuery {
|
|
909
1116
|
const statements = splitStatements(raw);
|
|
910
1117
|
if (statements.length > 1) {
|
|
@@ -913,6 +1120,6 @@ export function analyzeQuery(
|
|
|
913
1120
|
'a query file holds exactly one SELECT statement here (found a `;` separating statements) — use analyzeQueryFile for multi-statement files',
|
|
914
1121
|
);
|
|
915
1122
|
}
|
|
916
|
-
const results = analyzeQueryFile(file, raw, ir, db);
|
|
1123
|
+
const results = analyzeQueryFile(file, raw, ir, db, naming);
|
|
917
1124
|
return results[0] as AnalyzedQuery;
|
|
918
1125
|
}
|