@kuindji/typed-sql 0.1.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/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/builder/assemble.d.ts +13 -0
- package/dist/builder/assemble.d.ts.map +1 -0
- package/dist/builder/assemble.js +86 -0
- package/dist/builder/assemble.js.map +1 -0
- package/dist/builder/condition-tree.d.ts +27 -0
- package/dist/builder/condition-tree.d.ts.map +1 -0
- package/dist/builder/condition-tree.js +91 -0
- package/dist/builder/condition-tree.js.map +1 -0
- package/dist/builder/conditional-sql.d.ts +80 -0
- package/dist/builder/conditional-sql.d.ts.map +1 -0
- package/dist/builder/conditional-sql.js +88 -0
- package/dist/builder/conditional-sql.js.map +1 -0
- package/dist/builder/db.d.ts +76 -0
- package/dist/builder/db.d.ts.map +1 -0
- package/dist/builder/db.js +12 -0
- package/dist/builder/db.js.map +1 -0
- package/dist/builder/delete.d.ts +39 -0
- package/dist/builder/delete.d.ts.map +1 -0
- package/dist/builder/delete.js +33 -0
- package/dist/builder/delete.js.map +1 -0
- package/dist/builder/extract-params.d.ts +97 -0
- package/dist/builder/extract-params.d.ts.map +1 -0
- package/dist/builder/extract-params.js +2 -0
- package/dist/builder/extract-params.js.map +1 -0
- package/dist/builder/index.d.ts +23 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +14 -0
- package/dist/builder/index.js.map +1 -0
- package/dist/builder/insert.d.ts +51 -0
- package/dist/builder/insert.d.ts.map +1 -0
- package/dist/builder/insert.js +39 -0
- package/dist/builder/insert.js.map +1 -0
- package/dist/builder/mutate.d.ts +28 -0
- package/dist/builder/mutate.d.ts.map +1 -0
- package/dist/builder/mutate.js +17 -0
- package/dist/builder/mutate.js.map +1 -0
- package/dist/builder/params.d.ts +22 -0
- package/dist/builder/params.d.ts.map +1 -0
- package/dist/builder/params.js +65 -0
- package/dist/builder/params.js.map +1 -0
- package/dist/builder/return-type.d.ts +39 -0
- package/dist/builder/return-type.d.ts.map +1 -0
- package/dist/builder/return-type.js +2 -0
- package/dist/builder/return-type.js.map +1 -0
- package/dist/builder/scanner.d.ts +49 -0
- package/dist/builder/scanner.d.ts.map +1 -0
- package/dist/builder/scanner.js +240 -0
- package/dist/builder/scanner.js.map +1 -0
- package/dist/builder/select.d.ts +76 -0
- package/dist/builder/select.d.ts.map +1 -0
- package/dist/builder/select.js +240 -0
- package/dist/builder/select.js.map +1 -0
- package/dist/builder/sql-tag.d.ts +319 -0
- package/dist/builder/sql-tag.d.ts.map +1 -0
- package/dist/builder/sql-tag.js +3 -0
- package/dist/builder/sql-tag.js.map +1 -0
- package/dist/builder/sql.d.ts +17 -0
- package/dist/builder/sql.d.ts.map +1 -0
- package/dist/builder/sql.js +36 -0
- package/dist/builder/sql.js.map +1 -0
- package/dist/builder/state.d.ts +53 -0
- package/dist/builder/state.d.ts.map +1 -0
- package/dist/builder/state.js +18 -0
- package/dist/builder/state.js.map +1 -0
- package/dist/builder/update.d.ts +60 -0
- package/dist/builder/update.d.ts.map +1 -0
- package/dist/builder/update.js +40 -0
- package/dist/builder/update.js.map +1 -0
- package/dist/builder/write-assemble.d.ts +5 -0
- package/dist/builder/write-assemble.d.ts.map +1 -0
- package/dist/builder/write-assemble.js +57 -0
- package/dist/builder/write-assemble.js.map +1 -0
- package/dist/builder/write-state.d.ts +39 -0
- package/dist/builder/write-state.d.ts.map +1 -0
- package/dist/builder/write-state.js +6 -0
- package/dist/builder/write-state.js.map +1 -0
- package/dist/builder/write-tag.d.ts +91 -0
- package/dist/builder/write-tag.d.ts.map +1 -0
- package/dist/builder/write-tag.js +2 -0
- package/dist/builder/write-tag.js.map +1 -0
- package/dist/columns.d.ts +33 -0
- package/dist/columns.d.ts.map +1 -0
- package/dist/columns.js +2 -0
- package/dist/columns.js.map +1 -0
- package/dist/expressions.d.ts +71 -0
- package/dist/expressions.d.ts.map +1 -0
- package/dist/expressions.js +2 -0
- package/dist/expressions.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/parsing/extract.d.ts +47 -0
- package/dist/parsing/extract.d.ts.map +1 -0
- package/dist/parsing/extract.js +2 -0
- package/dist/parsing/extract.js.map +1 -0
- package/dist/parsing/normalize.d.ts +44 -0
- package/dist/parsing/normalize.d.ts.map +1 -0
- package/dist/parsing/normalize.js +2 -0
- package/dist/parsing/normalize.js.map +1 -0
- package/dist/parsing/pg-literals.d.ts +37 -0
- package/dist/parsing/pg-literals.d.ts.map +1 -0
- package/dist/parsing/pg-literals.js +2 -0
- package/dist/parsing/pg-literals.js.map +1 -0
- package/dist/parsing/split.d.ts +100 -0
- package/dist/parsing/split.d.ts.map +1 -0
- package/dist/parsing/split.js +2 -0
- package/dist/parsing/split.js.map +1 -0
- package/dist/parsing/string-utils.d.ts +29 -0
- package/dist/parsing/string-utils.d.ts.map +1 -0
- package/dist/parsing/string-utils.js +2 -0
- package/dist/parsing/string-utils.js.map +1 -0
- package/dist/parsing/tokenize.d.ts +27 -0
- package/dist/parsing/tokenize.d.ts.map +1 -0
- package/dist/parsing/tokenize.js +2 -0
- package/dist/parsing/tokenize.js.map +1 -0
- package/dist/parsing.d.ts +7 -0
- package/dist/parsing.d.ts.map +1 -0
- package/dist/parsing.js +9 -0
- package/dist/parsing.js.map +1 -0
- package/dist/partial.d.ts +30 -0
- package/dist/partial.d.ts.map +1 -0
- package/dist/partial.js +10 -0
- package/dist/partial.js.map +1 -0
- package/dist/schema.d.ts +28 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +2 -0
- package/dist/schema.js.map +1 -0
- package/dist/tables.d.ts +34 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +2 -0
- package/dist/tables.js.map +1 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +3 -0
- package/dist/utils.js.map +1 -0
- package/dist/validation/cte.d.ts +54 -0
- package/dist/validation/cte.d.ts.map +1 -0
- package/dist/validation/cte.js +2 -0
- package/dist/validation/cte.js.map +1 -0
- package/dist/validation/dispatch.d.ts +31 -0
- package/dist/validation/dispatch.d.ts.map +1 -0
- package/dist/validation/dispatch.js +2 -0
- package/dist/validation/dispatch.js.map +1 -0
- package/dist/validation/joins.d.ts +16 -0
- package/dist/validation/joins.d.ts.map +1 -0
- package/dist/validation/joins.js +2 -0
- package/dist/validation/joins.js.map +1 -0
- package/dist/validation/return-derived.d.ts +67 -0
- package/dist/validation/return-derived.d.ts.map +1 -0
- package/dist/validation/return-derived.js +5 -0
- package/dist/validation/return-derived.js.map +1 -0
- package/dist/validation/return-types.d.ts +41 -0
- package/dist/validation/return-types.d.ts.map +1 -0
- package/dist/validation/return-types.js +2 -0
- package/dist/validation/return-types.js.map +1 -0
- package/dist/validation/validate-columns.d.ts +63 -0
- package/dist/validation/validate-columns.d.ts.map +1 -0
- package/dist/validation/validate-columns.js +2 -0
- package/dist/validation/validate-columns.js.map +1 -0
- package/dist/validation.d.ts +7 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +9 -0
- package/dist/validation.js.map +1 -0
- package/package.json +64 -0
- package/src/builder/assemble.ts +100 -0
- package/src/builder/condition-tree.ts +162 -0
- package/src/builder/conditional-sql.ts +325 -0
- package/src/builder/db.ts +281 -0
- package/src/builder/delete.ts +57 -0
- package/src/builder/extract-params.ts +507 -0
- package/src/builder/index.ts +58 -0
- package/src/builder/insert.ts +75 -0
- package/src/builder/mutate.ts +55 -0
- package/src/builder/params.ts +95 -0
- package/src/builder/return-type.ts +66 -0
- package/src/builder/scanner.ts +254 -0
- package/src/builder/select.ts +470 -0
- package/src/builder/sql-tag.ts +422 -0
- package/src/builder/sql.ts +51 -0
- package/src/builder/state.ts +55 -0
- package/src/builder/update.ts +77 -0
- package/src/builder/write-assemble.ts +52 -0
- package/src/builder/write-state.ts +43 -0
- package/src/builder/write-tag.ts +119 -0
- package/src/columns.ts +336 -0
- package/src/expressions.ts +745 -0
- package/src/index.ts +81 -0
- package/src/parsing/extract.ts +260 -0
- package/src/parsing/normalize.ts +243 -0
- package/src/parsing/pg-literals.ts +289 -0
- package/src/parsing/split.ts +288 -0
- package/src/parsing/string-utils.ts +172 -0
- package/src/parsing/tokenize.ts +321 -0
- package/src/parsing.ts +8 -0
- package/src/partial.ts +241 -0
- package/src/schema.ts +130 -0
- package/src/tables.ts +278 -0
- package/src/utils.ts +43 -0
- package/src/validation/cte.ts +198 -0
- package/src/validation/dispatch.ts +312 -0
- package/src/validation/joins.ts +198 -0
- package/src/validation/return-derived.ts +253 -0
- package/src/validation/return-types.ts +271 -0
- package/src/validation/validate-columns.ts +489 -0
- package/src/validation.ts +8 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
// src/builder/extract-params.ts
|
|
2
|
+
import type { DatabaseSchema } from "../schema.js";
|
|
3
|
+
import type { ColumnTypeFromTableKey, RowTypeForTable } from "../schema.js";
|
|
4
|
+
import type { NormalizeQuery, NormalizeQueryKeepParams } from "../parsing.js";
|
|
5
|
+
import type {
|
|
6
|
+
ExtractInsertColumns, ExtractReturningList, ExtractLastWhere,
|
|
7
|
+
ExtractBefore, SplitCommaSimple, SplitTopLevel, Trim, CleanIdent,
|
|
8
|
+
ExceedsLengthBudget,
|
|
9
|
+
} from "../parsing.js";
|
|
10
|
+
import type {
|
|
11
|
+
InsertTargetTable, UpdateTargetTable, DeleteTargetTable,
|
|
12
|
+
} from "../tables.js";
|
|
13
|
+
import type { Simplify } from "../utils.js";
|
|
14
|
+
import type { GetReturnType } from "../index.js";
|
|
15
|
+
import type { DriverParamValue } from "./scanner.js";
|
|
16
|
+
|
|
17
|
+
// ---- :name detection ----
|
|
18
|
+
export type ParamName<Token extends string> =
|
|
19
|
+
Trim<Token> extends `:${infer Name}` ? CleanParamIdent<Name> : never;
|
|
20
|
+
|
|
21
|
+
// A `:name` identifier ends at the first terminator char. Use the deterministic
|
|
22
|
+
// left-to-right `ReadName` walk (NameStop set) rather than a single template with
|
|
23
|
+
// a *union* of separators: `${infer Head}${")" | "," | " "}${string}` infers a
|
|
24
|
+
// DIFFERENT Head per union member and yields a *union* result — so a param like
|
|
25
|
+
// `:userId` trailed by `\n order by "pseName", …` (an un-stripped ORDER BY leaking
|
|
26
|
+
// into the WHERE block) produced `"userId\n" | "userId\n order by \"pseName\""`
|
|
27
|
+
// instead of `"userId"`. ReadName stops at the first NameStop (incl. `\n`,`\t`,`:`
|
|
28
|
+
// so `::cast` suffixes drop too), so it returns a single clean name.
|
|
29
|
+
type CleanParamIdent<S extends string> = ReadName<S>;
|
|
30
|
+
|
|
31
|
+
// Column name for a (possibly qualified, possibly quoted) ref. Quotes are
|
|
32
|
+
// stripped via CleanIdent so the case-insensitive schema lookup matches; a raw
|
|
33
|
+
// `"shopperId"` qualifier would otherwise miss the column and bind `never`.
|
|
34
|
+
type ColOf<S extends string> =
|
|
35
|
+
FirstToken<Trim<S>> extends infer T extends string
|
|
36
|
+
? T extends `${infer _A}.${infer C}` ? CleanIdent<C> : CleanIdent<T> : never;
|
|
37
|
+
type FirstToken<S extends string> = S extends `${infer A} ${infer _}` ? A : S;
|
|
38
|
+
|
|
39
|
+
// ---- INSERT ----
|
|
40
|
+
export type ExtractInsertValues<N extends string> =
|
|
41
|
+
N extends `${string} values (${infer V})${string}` ? SplitCommaSimple<V>
|
|
42
|
+
: N extends `${string} values(${infer V2})${string}` ? SplitCommaSimple<V2>
|
|
43
|
+
: [];
|
|
44
|
+
|
|
45
|
+
type ZipInsert<
|
|
46
|
+
Cols extends readonly string[], Vals extends readonly string[],
|
|
47
|
+
Table extends string, S extends DatabaseSchema, Acc = {},
|
|
48
|
+
> = Cols extends readonly [infer C extends string, ...infer CR extends string[]]
|
|
49
|
+
? Vals extends readonly [infer V extends string, ...infer VR extends string[]]
|
|
50
|
+
? ParamName<V> extends infer P
|
|
51
|
+
// not exactly `:name` → extract any inner placeholders loose (spec §6)
|
|
52
|
+
? [P] extends [never] ? ZipInsert<CR, VR, Table, S, Acc & LooseParams<V>>
|
|
53
|
+
: P extends string
|
|
54
|
+
? ZipInsert<CR, VR, Table, S, Acc & { [K in P]: ColumnTypeFromTableKey<Table, CleanIdent<C>, S> }>
|
|
55
|
+
: ZipInsert<CR, VR, Table, S, Acc>
|
|
56
|
+
: Acc
|
|
57
|
+
: Acc
|
|
58
|
+
: Acc;
|
|
59
|
+
|
|
60
|
+
type ConflictSetBlock<N extends string> =
|
|
61
|
+
N extends `${string} do update set ${infer Rest}`
|
|
62
|
+
? ExtractBefore<ExtractBefore<Rest, " where ">, " returning "> : "";
|
|
63
|
+
|
|
64
|
+
// ---- multi-row VALUES detection (spec §3) ----
|
|
65
|
+
type IsMultiRowInsert<N extends string> =
|
|
66
|
+
N extends `${string} values ${infer After}` ? HasTopLevelTupleSep<After> : false;
|
|
67
|
+
|
|
68
|
+
// Walk After: skip single-quoted literals AND dollar-quoted strings; track paren
|
|
69
|
+
// depth. When the FIRST top-level tuple closes (depth returns to 0), it is a
|
|
70
|
+
// multi-row INSERT iff the next non-whitespace char is a comma (another tuple
|
|
71
|
+
// follows). Any other trailing clause — `on conflict (id)`, `returning …` —
|
|
72
|
+
// has no top-level `),` so it stays single-row. Step-capped; widens to false on
|
|
73
|
+
// overrun. Comments are already stripped by NormalizeQuery, so no comment arm.
|
|
74
|
+
type HasTopLevelTupleSep<
|
|
75
|
+
S extends string, Depth extends any[] = [], Steps extends any[] = [],
|
|
76
|
+
> = Steps["length"] extends 400 ? false
|
|
77
|
+
// single-quoted literal: `''` escape first, then a whole literal
|
|
78
|
+
: S extends `''${infer R}` ? HasTopLevelTupleSep<R, Depth, [any, ...Steps]>
|
|
79
|
+
: S extends `'${infer _Q}'${infer R}` ? HasTopLevelTupleSep<R, Depth, [any, ...Steps]>
|
|
80
|
+
// dollar-quoted string: `$tag$ … $tag$` (tag may be empty → `$$ … $$`).
|
|
81
|
+
: S extends `$${infer Tag}$${infer Rest}`
|
|
82
|
+
? Rest extends `${infer _Body}$${Tag}$${infer After}`
|
|
83
|
+
? HasTopLevelTupleSep<After, Depth, [any, ...Steps]>
|
|
84
|
+
: false // unterminated dollar-quote → stop (not multi-row)
|
|
85
|
+
: S extends `(${infer R}` ? HasTopLevelTupleSep<R, [any, ...Depth], [any, ...Steps]>
|
|
86
|
+
: S extends `)${infer R}`
|
|
87
|
+
? Depth extends [any, ...infer Rest extends any[]]
|
|
88
|
+
// top-level tuple just closed → another tuple iff next non-space is ","
|
|
89
|
+
? Rest extends [] ? AfterTupleIsComma<R>
|
|
90
|
+
: HasTopLevelTupleSep<R, Rest, [any, ...Steps]>
|
|
91
|
+
: HasTopLevelTupleSep<R, [], [any, ...Steps]> // unbalanced ")" — ignore
|
|
92
|
+
: S extends `${infer _C}${infer R}` ? HasTopLevelTupleSep<R, Depth, [any, ...Steps]>
|
|
93
|
+
: false;
|
|
94
|
+
|
|
95
|
+
// True iff the next non-whitespace char begins another VALUES tuple separator.
|
|
96
|
+
type AfterTupleIsComma<S extends string> =
|
|
97
|
+
S extends `${" " | "\t" | "\n"}${infer R}` ? AfterTupleIsComma<R>
|
|
98
|
+
: S extends `,${string}` ? true
|
|
99
|
+
: false;
|
|
100
|
+
|
|
101
|
+
type InsertParams<N extends string, S extends DatabaseSchema> =
|
|
102
|
+
IsMultiRowInsert<N> extends true
|
|
103
|
+
? { __error: true; message: "[SQL Error] multi-row VALUES not supported in the typed path; use the untyped driver call" }
|
|
104
|
+
: InsertTargetTable<N, S> extends infer Table extends string
|
|
105
|
+
? ZipInsert<ExtractInsertColumns<N>, ExtractInsertValues<N>, Table, S>
|
|
106
|
+
& SetParams<SplitTopLevel<ConflictSetBlock<N>>, Table, S>
|
|
107
|
+
& WhereParamsFor<N, Table, S>
|
|
108
|
+
: {};
|
|
109
|
+
|
|
110
|
+
// ---- UPDATE SET ----
|
|
111
|
+
type ExtractSetBlock<N extends string> =
|
|
112
|
+
N extends `${string} set ${infer Rest}`
|
|
113
|
+
? ExtractBefore<ExtractBefore<Rest, " where ">, " returning "> : "";
|
|
114
|
+
|
|
115
|
+
type SetParams<
|
|
116
|
+
Pairs extends readonly string[], Table extends string,
|
|
117
|
+
S extends DatabaseSchema, Acc = {},
|
|
118
|
+
> = Pairs extends readonly [infer P extends string, ...infer R extends string[]]
|
|
119
|
+
? P extends `${infer Left}=${infer Right}`
|
|
120
|
+
? ParamName<Right> extends infer Name
|
|
121
|
+
// not exactly `:name` → extract any inner placeholders loose (spec §6)
|
|
122
|
+
? [Name] extends [never] ? SetParams<R, Table, S, Acc & LooseParams<Right>>
|
|
123
|
+
: Name extends string
|
|
124
|
+
? SetParams<R, Table, S, Acc & { [K in Name]: ColumnTypeFromTableKey<Table, CleanIdent<Left>, S> }>
|
|
125
|
+
: SetParams<R, Table, S, Acc>
|
|
126
|
+
: Acc
|
|
127
|
+
: SetParams<R, Table, S, Acc>
|
|
128
|
+
: Acc;
|
|
129
|
+
|
|
130
|
+
// ---- WHERE / USING ----
|
|
131
|
+
type WhereBlock<N extends string> =
|
|
132
|
+
N extends `${string} where ${string}` ? ExtractLastWhere<N> : "";
|
|
133
|
+
|
|
134
|
+
// Split on top-level " and "/" or ", but keep a `between X and Y` range intact:
|
|
135
|
+
// when a part ends with a dangling `between ... ` (no closing operand yet),
|
|
136
|
+
// re-glue it with the following part.
|
|
137
|
+
type SplitConds<S extends string> =
|
|
138
|
+
Reglue<SplitOn<S, " and ">> extends infer A extends string[]
|
|
139
|
+
? FlatSplit<A, " or "> : [];
|
|
140
|
+
type SplitOn<S extends string, D extends string> =
|
|
141
|
+
S extends `${infer H}${D}${infer T}` ? [H, ...SplitOn<T, D>] : [S];
|
|
142
|
+
type FlatSplit<Parts extends readonly string[], D extends string, Acc extends string[] = []> =
|
|
143
|
+
Parts extends readonly [infer H extends string, ...infer R extends string[]]
|
|
144
|
+
? FlatSplit<R, D, [...Acc, ...SplitOn<H, D>]> : Acc;
|
|
145
|
+
|
|
146
|
+
type Reglue<Parts extends readonly string[], Acc extends string[] = []> =
|
|
147
|
+
Parts extends readonly [infer H extends string, infer N extends string, ...infer R extends string[]]
|
|
148
|
+
? EndsWithBetween<H> extends true
|
|
149
|
+
? Reglue<[`${H} and ${N}`, ...R], Acc>
|
|
150
|
+
: Reglue<[N, ...R], [...Acc, H]>
|
|
151
|
+
: Parts extends readonly [infer L extends string]
|
|
152
|
+
? [...Acc, L]
|
|
153
|
+
: Acc;
|
|
154
|
+
|
|
155
|
+
type EndsWithBetween<S extends string> =
|
|
156
|
+
Lowercase<Trim<S>> extends `${string} between ${infer Rest}`
|
|
157
|
+
? Rest extends `${string} and ${string}` ? false : true
|
|
158
|
+
: false;
|
|
159
|
+
|
|
160
|
+
// Extract EVERY placeholder name in a fragment and type each DriverParamValue.
|
|
161
|
+
// Used as the loose fallback (spec §6.5) — present, not dropped, not column-typed.
|
|
162
|
+
// Delegates to the colon-jumping `AllLooseParams`: `${infer _Pre}:${infer Rest}`
|
|
163
|
+
// matches the LEFTMOST colon (probe-confirmed — the old "greedy → LAST colon"
|
|
164
|
+
// rationale for the per-char walk was a misconception), so this scans colon-to-colon
|
|
165
|
+
// (O(colons), NOT O(chars)) yet yields the identical param set. Between colons the
|
|
166
|
+
// old per-char walk only consumed chars without touching Acc, so jumping straight to
|
|
167
|
+
// the next colon is equivalent; `::cast` skip / empty-name skip / colons inside an
|
|
168
|
+
// un-stripped single-quoted literal (`'a:b'` → `:b`) all match exactly. The 64
|
|
169
|
+
// colon-event cap is lenient and strictly safer than the implicitly TS2589-bounded
|
|
170
|
+
// per-char walk it replaces (design contract: widen/lenient on overrun).
|
|
171
|
+
type LooseParams<S extends string, Acc = {}> = AllLooseParams<S, Acc>;
|
|
172
|
+
|
|
173
|
+
// Whole-query placeholder sweep for the select/with path (step-capped). The select
|
|
174
|
+
// path types params precisely from the *last* WHERE only, so placeholders elsewhere
|
|
175
|
+
// — in the SELECT projection (`:currency::text` inside a function call) or in an
|
|
176
|
+
// *earlier* WHERE of a UNION — are otherwise dropped, and withParams then rejects
|
|
177
|
+
// those keys. This sweep captures EVERY `:name` as DriverParamValue (= unknown);
|
|
178
|
+
// intersecting it with the precise WHERE bindings is a no-op on shared keys
|
|
179
|
+
// (`unknown & T = T`), so precise column types still win.
|
|
180
|
+
//
|
|
181
|
+
// Single-quoted string literals must be skipped, or a literal like `'draft:team:'`
|
|
182
|
+
// or `'team:' || …` would have its inner colons misread as `:team` / `:user`. Rather
|
|
183
|
+
// than pre-build a literal-stripped copy of the whole (~7.6k-char) query and re-walk
|
|
184
|
+
// it — the old `StripSingleQuoted` rebuilt the full string via `\`${Pre} ${After}\``
|
|
185
|
+
// once PER literal (~30 big-string interns on the hot SELECTs) — this FUSES the two
|
|
186
|
+
// passes: jump to the next quote in one instantiation, colon-scan the quote-free
|
|
187
|
+
// prefix (`AllLooseParams<Pre>` — colons there are provably not inside a literal),
|
|
188
|
+
// skip the literal body, then recurse on the suffix, never interning the full string.
|
|
189
|
+
// Behaviour matches the old strip→scan exactly: an unterminated quote drops its
|
|
190
|
+
// dangling tail (lenient), and escaped `''` degrades identically (treated as two
|
|
191
|
+
// literal boundaries, never yielding a param).
|
|
192
|
+
type LooseParamsSkipLit<S extends string, Acc = {}, Steps extends any[] = []> =
|
|
193
|
+
Steps["length"] extends 64 ? Acc
|
|
194
|
+
: S extends `${infer Pre}'${infer Rest}`
|
|
195
|
+
? Rest extends `${infer _Lit}'${infer After}`
|
|
196
|
+
? LooseParamsSkipLit<After, AllLooseParams<Pre, Acc>, [any, ...Steps]>
|
|
197
|
+
: AllLooseParams<Pre, Acc>
|
|
198
|
+
: AllLooseParams<S, Acc>;
|
|
199
|
+
|
|
200
|
+
// Jumps colon-to-colon via template inference (`${infer _Pre}:${infer Rest}`
|
|
201
|
+
// matches up to the LEFTMOST colon in ONE instantiation), so recursion depth is
|
|
202
|
+
// the number of colons in the query — a handful — NOT its character length. A
|
|
203
|
+
// per-char walk capped near ~1000 instead *causes* TS2589 (design contract), so
|
|
204
|
+
// this scans cheaply and the step cap is a small colon count, not a length cap.
|
|
205
|
+
// `::cast` is detected by `Rest` starting with a second colon and skipped.
|
|
206
|
+
// Caller passes a quote-free segment (see LooseParamsSkipLit), so a colon here is
|
|
207
|
+
// never inside a single-quoted string literal.
|
|
208
|
+
type AllLooseParams<S extends string, Acc = {}, Steps extends any[] = []> =
|
|
209
|
+
Steps["length"] extends 64 ? Acc
|
|
210
|
+
: S extends `${infer _Pre}:${infer Rest}`
|
|
211
|
+
? Rest extends `:${infer R2}` ? AllLooseParams<R2, Acc, [any, ...Steps]>
|
|
212
|
+
: ReadName<Rest> extends infer Nm extends string
|
|
213
|
+
? Nm extends "" ? AllLooseParams<Rest, Acc, [any, ...Steps]>
|
|
214
|
+
: AllLooseParams<DropName<Rest>, Acc & { [K in Nm]: DriverParamValue }, [any, ...Steps]>
|
|
215
|
+
: Acc
|
|
216
|
+
: Acc;
|
|
217
|
+
|
|
218
|
+
// Chars that terminate a `:name` identifier in a SQL fragment.
|
|
219
|
+
type NameStop =
|
|
220
|
+
| " " | "\t" | "\n" | "," | ";" | ")" | "(" | "'" | '"' | ":" | "."
|
|
221
|
+
| "=" | "+" | "-" | "*" | "/" | "|" | "%" | ">" | "<" | "!" | "~"
|
|
222
|
+
| "@" | "#" | "&" | "^" | "[" | "]" | "{" | "}";
|
|
223
|
+
type ReadName<S extends string, Acc extends string = ""> =
|
|
224
|
+
S extends `${infer C}${infer R}`
|
|
225
|
+
? C extends NameStop ? Acc : ReadName<R, `${Acc}${C}`>
|
|
226
|
+
: Acc;
|
|
227
|
+
type DropName<S extends string> =
|
|
228
|
+
S extends `${infer C}${infer R}` ? C extends NameStop ? S : DropName<R> : S;
|
|
229
|
+
|
|
230
|
+
type WhereParam<Cond extends string, Alias extends string, Table extends string, S extends DatabaseSchema> =
|
|
231
|
+
// col between :lo and :hi (keywords lowercased post-normalize)
|
|
232
|
+
Trim<Cond> extends `${infer Lhs} between ${infer Lo} and ${infer Hi}`
|
|
233
|
+
? BetweenParams<Lhs, Lo, Hi, Alias, Table, S>
|
|
234
|
+
// col is [not] distinct from :p
|
|
235
|
+
: Trim<Cond> extends `${infer Lhs} is not distinct from ${infer Rhs}`
|
|
236
|
+
? DistinctParam<Lhs, Rhs, Alias, Table, S>
|
|
237
|
+
: Trim<Cond> extends `${infer Lhs} is distinct from ${infer Rhs}`
|
|
238
|
+
? DistinctParam<Lhs, Rhs, Alias, Table, S>
|
|
239
|
+
: Trim<Cond> extends `${infer Lhs} in (${infer Inner})`
|
|
240
|
+
? ParamName<Inner> extends infer P
|
|
241
|
+
? [P] extends [never] ? LooseParams<Inner>
|
|
242
|
+
: P extends string
|
|
243
|
+
? IsBareColumnRef<Lhs> extends true
|
|
244
|
+
? ScopedBindArray<Lhs, P, Alias, Table, S>
|
|
245
|
+
: LooseParams<Inner>
|
|
246
|
+
: LooseParams<Inner>
|
|
247
|
+
: LooseParams<Inner>
|
|
248
|
+
: Trim<Cond> extends `${infer Lhs}:${infer Tail}`
|
|
249
|
+
? CleanParamIdent<Tail> extends infer P
|
|
250
|
+
? [P] extends [never] ? LooseParams<Cond>
|
|
251
|
+
: P extends "" ? LooseParams<Cond>
|
|
252
|
+
: P extends string
|
|
253
|
+
// Recognized `col <op> :p` ONLY when, after removing the trailing
|
|
254
|
+
// comparison operator, the left side is a bare (optionally
|
|
255
|
+
// alias-qualified) identifier — no arithmetic, function call, or
|
|
256
|
+
// second placeholder. Anything else widens to loose (spec §6.4).
|
|
257
|
+
? StripTrailingCmpOp<Lhs> extends infer Col extends string
|
|
258
|
+
? IsBareColumnRef<Col> extends true
|
|
259
|
+
? ScopedBind<Col, P, Alias, Table, S>
|
|
260
|
+
: LooseParams<Cond>
|
|
261
|
+
: LooseParams<Cond>
|
|
262
|
+
: LooseParams<Cond>
|
|
263
|
+
: LooseParams<Cond>
|
|
264
|
+
: LooseParams<Cond>;
|
|
265
|
+
|
|
266
|
+
// Honor a qualified ref only when its qualifier is the target's own alias or
|
|
267
|
+
// the target base-table name (spec §6.1); a foreign qualifier (e.g. a FROM-clause
|
|
268
|
+
// alias) widens to DriverParamValue. An unqualified ref binds to the target.
|
|
269
|
+
type ScopedBind<Col extends string, P extends string, Alias extends string,
|
|
270
|
+
Table extends string, S extends DatabaseSchema> =
|
|
271
|
+
Trim<Col> extends `${infer Qual}.${infer _C}`
|
|
272
|
+
? Qual extends Alias ? { [K in P]: ColumnTypeFromTableKey<Table, ColOf<Col>, S> }
|
|
273
|
+
: LowerEq<Qual, BaseName<Table>> extends true ? { [K in P]: ColumnTypeFromTableKey<Table, ColOf<Col>, S> }
|
|
274
|
+
: { [K in P]: DriverParamValue }
|
|
275
|
+
: { [K in P]: ColumnTypeFromTableKey<Table, ColOf<Col>, S> };
|
|
276
|
+
|
|
277
|
+
// Same scoping, but the bound type is the column type as an array (IN-list).
|
|
278
|
+
type ScopedBindArray<Col extends string, P extends string, Alias extends string,
|
|
279
|
+
Table extends string, S extends DatabaseSchema> =
|
|
280
|
+
Trim<Col> extends `${infer Qual}.${infer _C}`
|
|
281
|
+
? Qual extends Alias ? { [K in P]: ColumnTypeFromTableKey<Table, ColOf<Col>, S>[] }
|
|
282
|
+
: LowerEq<Qual, BaseName<Table>> extends true ? { [K in P]: ColumnTypeFromTableKey<Table, ColOf<Col>, S>[] }
|
|
283
|
+
: { [K in P]: DriverParamValue }
|
|
284
|
+
: { [K in P]: ColumnTypeFromTableKey<Table, ColOf<Col>, S>[] };
|
|
285
|
+
|
|
286
|
+
type BaseName<TableKey extends string> =
|
|
287
|
+
TableKey extends `${string}.${infer T}` ? T : TableKey;
|
|
288
|
+
type LowerEq<A extends string, B extends string> =
|
|
289
|
+
Lowercase<A> extends Lowercase<B> ? true : false;
|
|
290
|
+
|
|
291
|
+
// Target's own alias: the token after the target table in `update <t> <alias>`,
|
|
292
|
+
// `delete from <t> <alias>`, or `insert into <t> as <alias>` (spec §6.1).
|
|
293
|
+
type TargetAlias<N extends string> =
|
|
294
|
+
N extends `update ${infer Rest}` ? AliasAfterTable<Rest>
|
|
295
|
+
: N extends `delete from ${infer Rest}` ? AliasAfterTable<Rest>
|
|
296
|
+
: N extends `insert into ${infer Rest}`
|
|
297
|
+
? Rest extends `${infer _T} as ${infer A} ${string}` ? FirstToken<Trim<A>>
|
|
298
|
+
: Rest extends `${infer _T} as ${infer A}` ? FirstToken<Trim<A>> : ""
|
|
299
|
+
: "";
|
|
300
|
+
// `<table> <alias> set|from|where|using|...` — alias is the 2nd token unless it
|
|
301
|
+
// is itself a clause keyword or a parenthesised list.
|
|
302
|
+
type AliasAfterTable<Rest extends string> =
|
|
303
|
+
Rest extends `${infer _Table} ${infer After}`
|
|
304
|
+
? FirstToken<Trim<After>> extends infer A extends string
|
|
305
|
+
? A extends "set" | "where" | "from" | "using" | "(" | "as" ? ""
|
|
306
|
+
: A extends `(${string}` ? "" : A
|
|
307
|
+
: ""
|
|
308
|
+
: "";
|
|
309
|
+
|
|
310
|
+
// Strip a trailing recognized comparison operator (and surrounding spaces) from
|
|
311
|
+
// the left side of a `col <op> :p` split. COMPOUND/symbol ops first (so `!=`,
|
|
312
|
+
// `<=`, `>=`, `<>` are not mis-split by the `=`/`<`/`>` arms), then the word ops
|
|
313
|
+
// `like`/`ilike` (case-insensitive). If nothing recognized trails, returns the
|
|
314
|
+
// input unchanged so the bare-ref check below fails → loose.
|
|
315
|
+
type StripTrailingCmpOp<S extends string> =
|
|
316
|
+
Trim<S> extends `${infer P}!=` ? Trim<P>
|
|
317
|
+
: Trim<S> extends `${infer P}<>` ? Trim<P>
|
|
318
|
+
: Trim<S> extends `${infer P}<=` ? Trim<P>
|
|
319
|
+
: Trim<S> extends `${infer P}>=` ? Trim<P>
|
|
320
|
+
: Trim<S> extends `${infer P}=` ? Trim<P>
|
|
321
|
+
: Trim<S> extends `${infer P}<` ? Trim<P>
|
|
322
|
+
: Trim<S> extends `${infer P}>` ? Trim<P>
|
|
323
|
+
: Trim<S> extends `${infer P} ${infer Op}`
|
|
324
|
+
? Lowercase<Op> extends "like" | "ilike" ? Trim<P> : Trim<S>
|
|
325
|
+
: Trim<S>;
|
|
326
|
+
|
|
327
|
+
// True iff `S` (trimmed) is a single column ref: an identifier, optionally
|
|
328
|
+
// alias/schema-qualified with dots, and NOTHING else — no space, arithmetic,
|
|
329
|
+
// parenthesis, pipe, percent, or extra colon. This is what makes `amount + `,
|
|
330
|
+
// `lower(x)`, and an empty Lhs (reversed `:p = col`) fail recognition → loose.
|
|
331
|
+
type IsBareColumnRef<S extends string> =
|
|
332
|
+
Trim<S> extends "" ? false
|
|
333
|
+
: Trim<S> extends `${string}${" " | "+" | "-" | "*" | "/" | "(" | ")" | ":" | "|" | "%"}${string}` ? false
|
|
334
|
+
: true;
|
|
335
|
+
|
|
336
|
+
type BetweenParams<Lhs extends string, Lo extends string, Hi extends string,
|
|
337
|
+
Alias extends string, Table extends string, S extends DatabaseSchema> =
|
|
338
|
+
ScopedColType<Lhs, Alias, Table, S> extends infer CT
|
|
339
|
+
? IsBareColumnRef<Lhs> extends true
|
|
340
|
+
? MergeName<ParamName<Lo>, CT> & MergeName<ParamName<Hi>, CT>
|
|
341
|
+
& LooseLeftover<Lo, Hi>
|
|
342
|
+
: LooseParams<`${Lo} ${Hi}`>
|
|
343
|
+
: {};
|
|
344
|
+
|
|
345
|
+
type DistinctParam<Lhs extends string, Rhs extends string,
|
|
346
|
+
Alias extends string, Table extends string, S extends DatabaseSchema> =
|
|
347
|
+
IsBareColumnRef<Lhs> extends true
|
|
348
|
+
? MergeName<ParamName<Rhs>, ScopedColType<Lhs, Alias, Table, S>>
|
|
349
|
+
: LooseParams<Rhs>;
|
|
350
|
+
|
|
351
|
+
// Column type for a (possibly qualified) bare ref under target-alias scoping:
|
|
352
|
+
// the column type when the qualifier is the target alias / base name or absent,
|
|
353
|
+
// else DriverParamValue (foreign qualifier).
|
|
354
|
+
type ScopedColType<Col extends string, Alias extends string,
|
|
355
|
+
Table extends string, S extends DatabaseSchema> =
|
|
356
|
+
Trim<Col> extends `${infer Qual}.${infer _C}`
|
|
357
|
+
? Qual extends Alias ? ColumnTypeFromTableKey<Table, ColOf<Col>, S>
|
|
358
|
+
: LowerEq<Qual, BaseName<Table>> extends true ? ColumnTypeFromTableKey<Table, ColOf<Col>, S>
|
|
359
|
+
: DriverParamValue
|
|
360
|
+
: ColumnTypeFromTableKey<Table, ColOf<Col>, S>;
|
|
361
|
+
|
|
362
|
+
// { name: T } when name is a real param, else {} (a literal operand contributes none).
|
|
363
|
+
type MergeName<P, T> = [P] extends [never] ? {} : P extends string ? { [K in P]: T } : {};
|
|
364
|
+
// If an operand is not a placeholder it contributes nothing; this no-op keeps
|
|
365
|
+
// the between arm total.
|
|
366
|
+
type LooseLeftover<_Lo extends string, _Hi extends string> = {};
|
|
367
|
+
|
|
368
|
+
type WhereParams<
|
|
369
|
+
Conds extends readonly string[], Alias extends string, Table extends string,
|
|
370
|
+
S extends DatabaseSchema, Acc = {},
|
|
371
|
+
> = Conds extends readonly [infer C extends string, ...infer R extends string[]]
|
|
372
|
+
? WhereParams<R, Alias, Table, S, Acc & WhereParam<C, Alias, Table, S>> : Acc;
|
|
373
|
+
|
|
374
|
+
// Colon pre-gate: a WHERE block with no `:` at all binds no params (every WhereParam
|
|
375
|
+
// arm on a colon-free cond falls through to the loose path → {}), so skip the
|
|
376
|
+
// SplitConds + WhereParams fold (and TargetAlias) entirely. Behavior-preserving — the
|
|
377
|
+
// gated-out case produced {} anyway. Helps the hot param-free outer WHEREs (e.g.
|
|
378
|
+
// `("groups" like '%…%' …)` on the big SELECTs). `::casts`/literal colons keep the
|
|
379
|
+
// full path (gate passes), exactly as before.
|
|
380
|
+
type WhereParamsFor<N extends string, Table extends string, S extends DatabaseSchema> =
|
|
381
|
+
WhereBlock<N> extends infer W extends string
|
|
382
|
+
? W extends `${string}:${string}`
|
|
383
|
+
? WhereParams<SplitConds<W>, TargetAlias<N>, Table, S>
|
|
384
|
+
: {}
|
|
385
|
+
: {};
|
|
386
|
+
|
|
387
|
+
// ---- leading-WITH (CTE) split ----------------------------------------------
|
|
388
|
+
// A write builder may prepend `with <name> as [materialized ](<body>) update …`
|
|
389
|
+
// (Task 1.2). The normalized string then starts with `with `, so the dispatch
|
|
390
|
+
// below would route it down the loose select/with arm and bind the UPDATE's own
|
|
391
|
+
// WHERE/SET params imprecisely (`oid: never`, since no real update-target table
|
|
392
|
+
// is resolved). To keep precise UPDATE/INSERT/DELETE typing while still capturing
|
|
393
|
+
// the CTE body's `:params`, peel a leading `with`-clause off: split at the FIRST
|
|
394
|
+
// paren-depth-0 main-statement keyword (`update `/`insert into `/`delete from `).
|
|
395
|
+
// The peeled head (CTE bodies) contributes loose `:params`; the tail dispatches
|
|
396
|
+
// as the real write statement. SELECT/with-SELECT (no DML keyword at depth 0) is
|
|
397
|
+
// left untouched and flows through the existing select arm unchanged.
|
|
398
|
+
//
|
|
399
|
+
// `SplitLeadingWith<N>` walks char-by-char tracking paren depth; when at depth 0
|
|
400
|
+
// it matches a DML keyword boundary, returning `{ head; tail }`. Step-capped; on
|
|
401
|
+
// overrun or no match it yields `never` (caller falls back to the plain dispatch).
|
|
402
|
+
type WithSplit = { head: string; tail: string };
|
|
403
|
+
// Step cap ≈ char cap: this walk consumes one step per char of the FULL
|
|
404
|
+
// `with … <dml>` prefix — the entire CTE head PLUS the depth-0 keyword boundary,
|
|
405
|
+
// not just a clause tail. So the 600-step cap means leading-CTE text up to ~600
|
|
406
|
+
// chars is split precisely; beyond that (or when no depth-0 DML keyword exists,
|
|
407
|
+
// e.g. a leading-`with` SELECT) this yields `never` and the caller DEGRADES TO
|
|
408
|
+
// LOOSE param typing — a safe fallback, NOT a hard error.
|
|
409
|
+
//
|
|
410
|
+
// The cap is intentionally NOT raised: large raw `with …` statements (e.g. the
|
|
411
|
+
// multi-CTE / `with recursive` fixtures fed to the raw sql tag) push tsc past its
|
|
412
|
+
// instantiation-depth limit (TS2589) at 1200. 600 keeps every real builder CTE
|
|
413
|
+
// prefix precise while staying within budget for the raw-string passthrough.
|
|
414
|
+
type SplitLeadingWith<S extends string, Acc extends string = "", Depth extends any[] = [], Steps extends any[] = []> =
|
|
415
|
+
Steps["length"] extends 600 ? never
|
|
416
|
+
: Depth extends []
|
|
417
|
+
// At top level, a DML keyword starts the real statement — split here.
|
|
418
|
+
? S extends `update ${infer _R}` ? { head: Acc; tail: S }
|
|
419
|
+
: S extends `insert into ${infer _R}` ? { head: Acc; tail: S }
|
|
420
|
+
: S extends `delete from ${infer _R}` ? { head: Acc; tail: S }
|
|
421
|
+
: S extends `(${infer R}` ? SplitLeadingWith<R, `${Acc}(`, [any, ...Depth], [any, ...Steps]>
|
|
422
|
+
: S extends `${infer C}${infer R}` ? SplitLeadingWith<R, `${Acc}${C}`, Depth, [any, ...Steps]>
|
|
423
|
+
: never
|
|
424
|
+
// Inside parens — only track depth; never split.
|
|
425
|
+
: S extends `(${infer R}` ? SplitLeadingWith<R, `${Acc}(`, [any, ...Depth], [any, ...Steps]>
|
|
426
|
+
: S extends `)${infer R}`
|
|
427
|
+
? Depth extends [any, ...infer D extends any[]] ? SplitLeadingWith<R, `${Acc})`, D, [any, ...Steps]> : never
|
|
428
|
+
: S extends `${infer C}${infer R}` ? SplitLeadingWith<R, `${Acc}${C}`, Depth, [any, ...Steps]>
|
|
429
|
+
: never;
|
|
430
|
+
|
|
431
|
+
// ---- dispatch ----
|
|
432
|
+
type ParamsForKind<N extends string, S extends DatabaseSchema> =
|
|
433
|
+
N extends `insert into ${string}` ? InsertParams<N, S>
|
|
434
|
+
: N extends `update ${string}`
|
|
435
|
+
? UpdateTargetTable<N, S> extends infer T extends string
|
|
436
|
+
? SetParams<SplitTopLevel<ExtractSetBlock<N>>, T, S> & WhereParamsFor<N, T, S> : {}
|
|
437
|
+
: N extends `delete from ${string}`
|
|
438
|
+
? DeleteTargetTable<N, S> extends infer T extends string ? WhereParamsFor<N, T, S> : {}
|
|
439
|
+
// Leading `with`-clause wrapping a DML statement (builder CTE prefix): peel
|
|
440
|
+
// the CTE head (loose params) and dispatch the inner write statement, so its
|
|
441
|
+
// own params stay precisely typed.
|
|
442
|
+
: N extends `with ${string}`
|
|
443
|
+
// `[never] extends [never]` guards the no-DML-keyword case (with-SELECT):
|
|
444
|
+
// SplitLeadingWith yields `never`, and we keep the loose select/with arm.
|
|
445
|
+
? [SplitLeadingWith<N>] extends [never]
|
|
446
|
+
? DeleteTargetTable<N, S> extends infer T extends string
|
|
447
|
+
? LooseParamsSkipLit<N> & WhereParamsFor<N, T, S>
|
|
448
|
+
: LooseParamsSkipLit<N>
|
|
449
|
+
: SplitLeadingWith<N> extends infer W extends WithSplit
|
|
450
|
+
? LooseParamsSkipLit<W["head"]> & ParamsForKind<W["tail"], S>
|
|
451
|
+
: LooseParamsSkipLit<N>
|
|
452
|
+
: N extends `select ${string}`
|
|
453
|
+
? DeleteTargetTable<N, S> extends infer T extends string
|
|
454
|
+
? LooseParamsSkipLit<N> & WhereParamsFor<N, T, S>
|
|
455
|
+
: LooseParamsSkipLit<N>
|
|
456
|
+
: {};
|
|
457
|
+
|
|
458
|
+
export type ExtractParams<Query extends string, S extends DatabaseSchema> =
|
|
459
|
+
NormalizeQueryKeepParams<Query> extends infer N extends string ? Simplify<ParamsForKind<N, S>> : {};
|
|
460
|
+
|
|
461
|
+
// ---- RETURNING ----
|
|
462
|
+
type TargetForReturning<N extends string, S extends DatabaseSchema> =
|
|
463
|
+
N extends `insert into ${string}` ? InsertTargetTable<N, S>
|
|
464
|
+
: N extends `update ${string}` ? UpdateTargetTable<N, S>
|
|
465
|
+
: N extends `delete from ${string}` ? DeleteTargetTable<N, S>
|
|
466
|
+
: never;
|
|
467
|
+
|
|
468
|
+
// Reuse the existing GetReturnType inferrer (aliases, `as`, casts, functions,
|
|
469
|
+
// expressions, `*`) by synthesizing `select <returning-list> from <target>` and
|
|
470
|
+
// running the full machinery over it (spec §6/§7 — "reuse GetReturnType for
|
|
471
|
+
// aliases/expressions where applicable"). A bare `*` short-circuits to the full
|
|
472
|
+
// row. `T` is the normalized target key (e.g. "public.orders"), which the
|
|
473
|
+
// validator resolves as a schema-qualified FROM source.
|
|
474
|
+
//
|
|
475
|
+
// Cheap pre-filter: a query with no `returning` keyword at all can have no
|
|
476
|
+
// RETURNING row, so skip the (expensive) `NormalizeQuery` char-walk entirely and
|
|
477
|
+
// return `{}` after a single `Lowercase` intrinsic. This matters when many
|
|
478
|
+
// `BoundSql<Q, S>` types are unioned (e.g. a big `createSql` smoke-test array):
|
|
479
|
+
// each element's phantom `__returning` member is resolved during the structural
|
|
480
|
+
// union comparison, and running a full normalize per element exhausts TS's
|
|
481
|
+
// cumulative instantiation budget (TS2589). The word-only test is safe — a false
|
|
482
|
+
// positive (the literal `returning` inside a string/identifier) just falls through
|
|
483
|
+
// to the accurate path below, so correctness is unchanged.
|
|
484
|
+
export type ExtractReturning<Query extends string, S extends DatabaseSchema> =
|
|
485
|
+
Lowercase<Query> extends `${string}returning${string}`
|
|
486
|
+
// A very long RETURNING query (e.g. a writable-CTE INSERT … RETURNING) runs
|
|
487
|
+
// the full inferrer below, which is a deep chain; when many `BoundSql<Q,S>`
|
|
488
|
+
// are unioned (a big `createSql` smoke-test array) the union comparison
|
|
489
|
+
// resolves each element's phantom `__returning`, and that one deep inner
|
|
490
|
+
// chain tips TS past its instantiation-depth limit (TS2589). Degrade the
|
|
491
|
+
// RETURNING row to `{}` on over-budget queries — the documented "precision
|
|
492
|
+
// traded away on very wide/long queries" path. Ordinary-size RETURNING
|
|
493
|
+
// queries keep precise row inference.
|
|
494
|
+
? ExceedsLengthBudget<Query> extends true ? {} : ExtractReturningInner<Query, S>
|
|
495
|
+
: {};
|
|
496
|
+
|
|
497
|
+
type ExtractReturningInner<Query extends string, S extends DatabaseSchema> =
|
|
498
|
+
NormalizeQuery<Query> extends infer N extends string
|
|
499
|
+
? ExtractReturningList<N> extends infer L extends string
|
|
500
|
+
? L extends "" ? {}
|
|
501
|
+
: TargetForReturning<N, S> extends infer T extends string
|
|
502
|
+
? Trim<L> extends "*"
|
|
503
|
+
? RowTypeForTable<T, S>
|
|
504
|
+
: Simplify<GetReturnType<`select ${L} from ${T}`, S>>
|
|
505
|
+
: {}
|
|
506
|
+
: {}
|
|
507
|
+
: {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/builder/index.ts
|
|
2
|
+
|
|
3
|
+
// Values
|
|
4
|
+
export { createSelectQuery, type SelectQueryBuilder } from "./select.js";
|
|
5
|
+
export { createSelectFn } from "./db.js";
|
|
6
|
+
export { createConditionTree, ConditionTreeBuilder } from "./condition-tree.js";
|
|
7
|
+
export {
|
|
8
|
+
createConditionalQuery,
|
|
9
|
+
withConditions,
|
|
10
|
+
conditionalSQL,
|
|
11
|
+
processConditionalSQL,
|
|
12
|
+
processParams,
|
|
13
|
+
normalizeWhitespace,
|
|
14
|
+
} from "./conditional-sql.js";
|
|
15
|
+
export { assembleSelectSQL } from "./assemble.js";
|
|
16
|
+
export { createInsertQuery, type InsertQueryBuilder } from "./insert.js";
|
|
17
|
+
export { createUpdateQuery, type UpdateQueryBuilder } from "./update.js";
|
|
18
|
+
export { createDeleteQuery, type DeleteQueryBuilder } from "./delete.js";
|
|
19
|
+
export { createSql } from "./sql.js";
|
|
20
|
+
export { createMutateFn, type MutationHandler, type MutationReturnType } from "./mutate.js";
|
|
21
|
+
export {
|
|
22
|
+
scanPlaceholders, expandScanned, collectScanned, assertAllProvided, prepareScanned,
|
|
23
|
+
} from "./scanner.js";
|
|
24
|
+
|
|
25
|
+
// Types — only those needed to use the runtime API (spec scope).
|
|
26
|
+
export type { QueryParamValue, QueryParamInput } from "./params.js";
|
|
27
|
+
export type { RuntimeSelectState } from "./state.js";
|
|
28
|
+
export type { AnySqlTag, SqlTag } from "./sql-tag.js";
|
|
29
|
+
export type {
|
|
30
|
+
BuilderSQL,
|
|
31
|
+
BuilderReturnType,
|
|
32
|
+
BuilderResultBrand,
|
|
33
|
+
} from "./return-type.js";
|
|
34
|
+
export type {
|
|
35
|
+
ValidQuery,
|
|
36
|
+
ValidQueryBuilder,
|
|
37
|
+
FragmentErrors,
|
|
38
|
+
SelectResult,
|
|
39
|
+
SelectResultArray,
|
|
40
|
+
SelectBuilderResult,
|
|
41
|
+
SelectBuilderResultArray,
|
|
42
|
+
MergeOverrides,
|
|
43
|
+
IsValidSelect,
|
|
44
|
+
QueryHandler,
|
|
45
|
+
} from "./db.js";
|
|
46
|
+
export type {
|
|
47
|
+
ConditionalQueryResult,
|
|
48
|
+
ProcessedSQL,
|
|
49
|
+
ValidateConditionalSQL,
|
|
50
|
+
ConditionalSQLOutput,
|
|
51
|
+
ConditionalSQLOptions,
|
|
52
|
+
TypedConditionalSQLOutput,
|
|
53
|
+
} from "./conditional-sql.js";
|
|
54
|
+
export type { DriverParamValue, PlaceholderOccurrence } from "./scanner.js";
|
|
55
|
+
export type { ExtractParams, ExtractReturning } from "./extract-params.js";
|
|
56
|
+
export type { TypedSql, BoundSql } from "./sql.js";
|
|
57
|
+
export type { BoundWrite } from "./insert.js";
|
|
58
|
+
export type { WriteParamsFor, WriteReturnFor } from "./write-tag.js";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/builder/insert.ts
|
|
2
|
+
import type { DatabaseSchema } from "../schema.js";
|
|
3
|
+
import { assembleInsertSQL } from "./write-assemble.js";
|
|
4
|
+
import { EMPTY_INSERT_STATE, type RuntimeInsertState } from "./write-state.js";
|
|
5
|
+
import {
|
|
6
|
+
assertAllProvided, collectScanned, expandScanned, type DriverParamValue,
|
|
7
|
+
} from "./scanner.js";
|
|
8
|
+
import type { InsertTag, WriteParamsFor, WriteReturnFor } from "./write-tag.js";
|
|
9
|
+
|
|
10
|
+
type PushVal<T extends InsertTag, Col extends string, Text extends string, Cond extends boolean> =
|
|
11
|
+
Omit<T, "values"> & { readonly values: readonly [...T["values"], { col: Col; text: Text; cond: Cond }] };
|
|
12
|
+
|
|
13
|
+
export interface InsertQueryBuilder<S extends DatabaseSchema, T extends InsertTag> {
|
|
14
|
+
into<Tbl extends string>(table: Tbl): InsertQueryBuilder<S, Omit<T, "table"> & { table: Tbl }>;
|
|
15
|
+
// INSERT...SELECT: explicit column list, then a free-text SELECT body. When
|
|
16
|
+
// `fromSelect` is set the VALUES path is bypassed and the SELECT form is built.
|
|
17
|
+
columns<C extends string>(cols: C): InsertQueryBuilder<S, Omit<T, "columns"> & { columns: C }>;
|
|
18
|
+
fromSelect<Q extends string>(q: Q): InsertQueryBuilder<S, Omit<T, "fromSelect"> & { fromSelect: Q }>;
|
|
19
|
+
value<Col extends string, Text extends string>(col: Col, text: Text):
|
|
20
|
+
InsertQueryBuilder<S, PushVal<T, Col, Text, false>>;
|
|
21
|
+
valueIf<Col extends string, Text extends string>(cond: boolean, col: Col, text: Text):
|
|
22
|
+
InsertQueryBuilder<S, PushVal<T, Col, Text, true>>;
|
|
23
|
+
onConflict<C extends string>(clause: C):
|
|
24
|
+
InsertQueryBuilder<S, Omit<T, "conflict"> & { conflict: C }>;
|
|
25
|
+
returning<R extends string>(cols: R):
|
|
26
|
+
InsertQueryBuilder<S, Omit<T, "returning"> & { returning: R }>;
|
|
27
|
+
withParams(params: WriteParamsFor<T, S>): BoundWrite<S, T>;
|
|
28
|
+
toString(): string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BoundWrite<S extends DatabaseSchema, T> {
|
|
32
|
+
toString(): string;
|
|
33
|
+
getParams(): ReadonlyArray<DriverParamValue>;
|
|
34
|
+
readonly __returning?: WriteReturnFor<T, S>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class InsertImpl<S extends DatabaseSchema, T extends InsertTag> {
|
|
38
|
+
constructor(private readonly st: RuntimeInsertState) {}
|
|
39
|
+
private next(st: RuntimeInsertState): any { return new InsertImpl<S, any>(st); }
|
|
40
|
+
into(table: string): any { return this.next({ ...this.st, table }); }
|
|
41
|
+
// Store the explicit column list / SELECT body for the INSERT...SELECT form.
|
|
42
|
+
columns(cols: string): any { return this.next({ ...this.st, columns: cols }); }
|
|
43
|
+
fromSelect(q: string): any { return this.next({ ...this.st, fromSelect: q }); }
|
|
44
|
+
value(col: string, text: string): any {
|
|
45
|
+
return this.next({ ...this.st, values: [...this.st.values, { col, text }] });
|
|
46
|
+
}
|
|
47
|
+
valueIf(cond: boolean, col: string, text: string): any {
|
|
48
|
+
return cond ? this.value(col, text) : this.next(this.st);
|
|
49
|
+
}
|
|
50
|
+
onConflict(clause: string): any { return this.next({ ...this.st, conflict: clause }); }
|
|
51
|
+
returning(cols: string): any { return this.next({ ...this.st, returning: cols }); }
|
|
52
|
+
withParams(params: Record<string, DriverParamValue>): any {
|
|
53
|
+
return this.next({ ...this.st, namedParams: { ...this.st.namedParams, ...params } });
|
|
54
|
+
}
|
|
55
|
+
toString(): string {
|
|
56
|
+
const sql = assembleInsertSQL(this.st);
|
|
57
|
+
assertAllProvided(sql, this.st.namedParams);
|
|
58
|
+
return expandScanned(sql, this.st.namedParams);
|
|
59
|
+
}
|
|
60
|
+
getParams(): ReadonlyArray<DriverParamValue> {
|
|
61
|
+
const sql = assembleInsertSQL(this.st);
|
|
62
|
+
assertAllProvided(sql, this.st.namedParams);
|
|
63
|
+
return collectScanned(sql, this.st.namedParams);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type EmptyInsertTag = {
|
|
68
|
+
kind: "insert"; table: ""; values: readonly []; conflict: null;
|
|
69
|
+
columns: ""; fromSelect: "";
|
|
70
|
+
wheres: readonly []; using: readonly []; from: readonly []; returning: null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export function createInsertQuery<S extends DatabaseSchema>(): InsertQueryBuilder<S, EmptyInsertTag> {
|
|
74
|
+
return new InsertImpl<S, EmptyInsertTag>(EMPTY_INSERT_STATE) as unknown as InsertQueryBuilder<S, EmptyInsertTag>;
|
|
75
|
+
}
|