@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,198 @@
|
|
|
1
|
+
// JOIN USING / window-filter / DISTINCT ON column validation.
|
|
2
|
+
import type { AllTrue, And, IsUnion } from "../utils.js";
|
|
3
|
+
import type { CleanIdent, ExtractCallParenBodies, SplitCommaSimple, TokenizeLoose, Trim } from "../parsing.js";
|
|
4
|
+
import type { ColumnExists, DatabaseSchema } from "../schema.js";
|
|
5
|
+
import type { QualifiedColumnRefsValidFor, UnqualifiedColumnRefsValidFor } from "./validate-columns.js";
|
|
6
|
+
import type { QueryKind } from "./dispatch.js";
|
|
7
|
+
import type { TableKeyFromToken, TableKeyValid } from "../tables.js";
|
|
8
|
+
import type { TablesWithColumn } from "../columns.js";
|
|
9
|
+
// `JOIN ... USING (col)` requires `col` to exist on BOTH joined tables. The
|
|
10
|
+
// loose ref-scan only checks that an unqualified column resolves to SOME table,
|
|
11
|
+
// so a column present on just one side (e.g. `users JOIN orders USING (user_id)`
|
|
12
|
+
// where only `orders` has `user_id`) is wrongly accepted. Surface every
|
|
13
|
+
// ` using (cols)` body (spaced and no-space) and require each listed column to
|
|
14
|
+
// exist on at least TWO of the query's tables — a cheap proxy for "both sides of
|
|
15
|
+
// the join" that is correct for the common single-USING-join case and never
|
|
16
|
+
// false-rejects a column genuinely shared across the join. SELECT-only; a DELETE
|
|
17
|
+
// `... USING t` table source has no parenthesised column list so it never
|
|
18
|
+
// matches the ` using (` marker. A no-op (`true`) for queries without USING.
|
|
19
|
+
export type JoinUsingColsValid<
|
|
20
|
+
N extends string,
|
|
21
|
+
S extends DatabaseSchema,
|
|
22
|
+
Tables extends string
|
|
23
|
+
> =
|
|
24
|
+
QueryKind<N> extends "select"
|
|
25
|
+
// Cheap keyword pre-gate: skip the balanced-paren extraction walk entirely
|
|
26
|
+
// unless a ` using (` marker is actually present. `Lowercase<N>` only ever
|
|
27
|
+
// matches MORE strings than the lowercase-marker body, so a no-match here
|
|
28
|
+
// provably coincides with the `Trim<Seg> extends ""` -> `true` arm below.
|
|
29
|
+
? Lowercase<N> extends `${string} using (${string}` | `${string} using(${string}`
|
|
30
|
+
? `${ExtractCallParenBodies<N, " using (">} ${ExtractCallParenBodies<N, " using(">}` extends infer Seg extends string
|
|
31
|
+
? Trim<Seg> extends ""
|
|
32
|
+
? true
|
|
33
|
+
: And<
|
|
34
|
+
// Cheap "shared on >=2 tables" proxy (the left side approximation).
|
|
35
|
+
UsingColsInTwoTables<SplitCommaSimple<Seg>, Tables, S>,
|
|
36
|
+
// Precise right-side check: each USING column must exist on the
|
|
37
|
+
// specific table being joined, so an unrelated later table can no
|
|
38
|
+
// longer mask an invalid USING pair (adversarial round-9 J1).
|
|
39
|
+
JoinUsingRightSideValid<N, S>,
|
|
40
|
+
true,
|
|
41
|
+
true,
|
|
42
|
+
true
|
|
43
|
+
>
|
|
44
|
+
: true
|
|
45
|
+
: true
|
|
46
|
+
: true;
|
|
47
|
+
|
|
48
|
+
// Walk the query join-by-join, pairing each ` using (cols)` with the table named
|
|
49
|
+
// immediately before it (the join's RIGHT side) and requiring every listed column
|
|
50
|
+
// to exist on that table. When a ` join ` head is NOT immediately followed by its
|
|
51
|
+
// own ` using (` (the join source itself still contains a later ` join `), the
|
|
52
|
+
// USING belongs to a deeper join — skip this head and continue. A right side that
|
|
53
|
+
// resolves to something other than a real base table (alias / derived table) is
|
|
54
|
+
// left to the cheap shared-column proxy rather than risk a false reject.
|
|
55
|
+
export type JoinUsingRightSideValid<
|
|
56
|
+
S extends string,
|
|
57
|
+
Sch extends DatabaseSchema,
|
|
58
|
+
Steps extends any[] = []
|
|
59
|
+
> = Steps["length"] extends 40
|
|
60
|
+
? true
|
|
61
|
+
: S extends `${infer _Head} join ${infer AfterJoin}`
|
|
62
|
+
? AfterJoin extends `${infer Src} using (${infer Body})${infer Rest}`
|
|
63
|
+
? Src extends `${string} join ${string}`
|
|
64
|
+
? JoinUsingRightSideValid<AfterJoin, Sch, [any, ...Steps]>
|
|
65
|
+
: And<
|
|
66
|
+
UsingColsOnRightTable<SplitCommaSimple<Body>, JoinSrcFirstWord<Src>, Sch>,
|
|
67
|
+
JoinUsingRightSideValid<Rest, Sch, [any, ...Steps]>,
|
|
68
|
+
true, true, true
|
|
69
|
+
>
|
|
70
|
+
: AfterJoin extends `${infer Src2} using(${infer Body2})${infer Rest2}`
|
|
71
|
+
? Src2 extends `${string} join ${string}`
|
|
72
|
+
? JoinUsingRightSideValid<AfterJoin, Sch, [any, ...Steps]>
|
|
73
|
+
: And<
|
|
74
|
+
UsingColsOnRightTable<SplitCommaSimple<Body2>, JoinSrcFirstWord<Src2>, Sch>,
|
|
75
|
+
JoinUsingRightSideValid<Rest2, Sch, [any, ...Steps]>,
|
|
76
|
+
true, true, true
|
|
77
|
+
>
|
|
78
|
+
: JoinUsingRightSideValid<AfterJoin, Sch, [any, ...Steps]>
|
|
79
|
+
: true;
|
|
80
|
+
|
|
81
|
+
// The first whitespace-delimited token of a join source (`users`, `users u`,
|
|
82
|
+
// `public.users u` -> `users` / `public.users`). Left un-cleaned so
|
|
83
|
+
// `TableKeyFromToken` can parse a schema-qualified `schema.table` token.
|
|
84
|
+
export type JoinSrcFirstWord<Src extends string> =
|
|
85
|
+
Trim<Src> extends `${infer W} ${string}` ? W : Trim<Src>;
|
|
86
|
+
|
|
87
|
+
// Each USING column must exist on the joined (right-side) table. If the source
|
|
88
|
+
// token does not resolve to a real base table, defer to the shared-column proxy.
|
|
89
|
+
export type UsingColsOnRightTable<
|
|
90
|
+
Cols extends string[],
|
|
91
|
+
SrcWord extends string,
|
|
92
|
+
S extends DatabaseSchema
|
|
93
|
+
> = TableKeyFromToken<SrcWord, S> extends infer RK extends string
|
|
94
|
+
? TableKeyValid<RK, S> extends true
|
|
95
|
+
? AllTrue<
|
|
96
|
+
Cols[number] extends infer C extends string
|
|
97
|
+
? CleanIdent<C> extends ""
|
|
98
|
+
? true
|
|
99
|
+
: ColumnExists<RK, CleanIdent<C>, S>
|
|
100
|
+
: true
|
|
101
|
+
>
|
|
102
|
+
: true
|
|
103
|
+
: true;
|
|
104
|
+
|
|
105
|
+
export type UsingColsInTwoTables<
|
|
106
|
+
Cols extends string[],
|
|
107
|
+
Tables extends string,
|
|
108
|
+
S extends DatabaseSchema
|
|
109
|
+
> = AllTrue<
|
|
110
|
+
Cols[number] extends infer C extends string
|
|
111
|
+
? CleanIdent<C> extends ""
|
|
112
|
+
? true
|
|
113
|
+
: UsingColOnBothSides<CleanIdent<C>, Tables, S>
|
|
114
|
+
: true
|
|
115
|
+
>;
|
|
116
|
+
|
|
117
|
+
export type UsingColOnBothSides<
|
|
118
|
+
Col extends string,
|
|
119
|
+
Tables extends string,
|
|
120
|
+
S extends DatabaseSchema
|
|
121
|
+
> = TablesWithColumn<Tables, Col, S> extends infer Owners
|
|
122
|
+
? [Owners] extends [never]
|
|
123
|
+
? false
|
|
124
|
+
: IsUnion<Owners> extends true
|
|
125
|
+
? true
|
|
126
|
+
: false
|
|
127
|
+
: false;
|
|
128
|
+
|
|
129
|
+
// Columns inside `over (...)` / `filter (...)` / `within group (...)` clauses
|
|
130
|
+
// live in the SELECT list (before the top-level FROM), so the from-FROM-onward
|
|
131
|
+
// loose ref-scan never sees them and the select-list treats `fn() over (...)` /
|
|
132
|
+
// `fn() within group (...)` as a plain function call. We surface those clause
|
|
133
|
+
// bodies explicitly and validate their column refs the same way as the rest of
|
|
134
|
+
// the query. `WITHIN GROUP (ORDER BY <expr>)` is the ordered-set aggregate's sort
|
|
135
|
+
// body — its columns must be validated like a window's. A no-op (`true`) for
|
|
136
|
+
// queries without these clauses.
|
|
137
|
+
export type WindowFilterColsValid<
|
|
138
|
+
N extends string,
|
|
139
|
+
S extends DatabaseSchema,
|
|
140
|
+
Tables extends string,
|
|
141
|
+
Aliases extends string
|
|
142
|
+
> =
|
|
143
|
+
// Cheap keyword pre-gate: only run the (six) balanced-paren extraction walks
|
|
144
|
+
// when an ` over (` / ` filter (` / ` within group (` marker is actually
|
|
145
|
+
// present. `Lowercase<N>` matches a superset of the body's lowercase markers,
|
|
146
|
+
// so a no-match coincides with the `Trim<Seg> extends ""` -> `true` arm.
|
|
147
|
+
Lowercase<N> extends
|
|
148
|
+
| `${string} over (${string}` | `${string} over(${string}`
|
|
149
|
+
| `${string} filter (${string}` | `${string} filter(${string}`
|
|
150
|
+
| `${string} within group (${string}` | `${string} within group(${string}`
|
|
151
|
+
? `${ExtractCallParenBodies<N, " over (">} ${ExtractCallParenBodies<N, " over(">} ${ExtractCallParenBodies<N, " filter (">} ${ExtractCallParenBodies<N, " filter(">} ${ExtractCallParenBodies<N, " within group (">} ${ExtractCallParenBodies<N, " within group(">}` extends infer Seg extends string
|
|
152
|
+
? Trim<Seg> extends ""
|
|
153
|
+
? true
|
|
154
|
+
: TokenizeLoose<Seg> extends infer Toks extends string[]
|
|
155
|
+
? And<
|
|
156
|
+
QualifiedColumnRefsValidFor<N, S, Tables, Aliases, Toks>,
|
|
157
|
+
UnqualifiedColumnRefsValidFor<N, S, Tables, Aliases, Toks, never>,
|
|
158
|
+
true,
|
|
159
|
+
true,
|
|
160
|
+
true
|
|
161
|
+
>
|
|
162
|
+
: true
|
|
163
|
+
: true
|
|
164
|
+
: true;
|
|
165
|
+
|
|
166
|
+
// `DISTINCT ON (exprs)` columns are part of the SELECT scope but `StripDistinct`
|
|
167
|
+
// removes the whole ON-list before the projection / FROM-onward ref-scan runs, so
|
|
168
|
+
// `SELECT DISTINCT ON (bogus_col) id ...` escaped validation entirely. Surface the
|
|
169
|
+
// ON-list body explicitly (spaced and no-space variants) and validate its column
|
|
170
|
+
// refs against the query's tables/aliases exactly like `WindowFilterColsValid`.
|
|
171
|
+
// SELECT-only; a no-op (`true`) for queries without DISTINCT ON.
|
|
172
|
+
export type DistinctOnColsValid<
|
|
173
|
+
N extends string,
|
|
174
|
+
S extends DatabaseSchema,
|
|
175
|
+
Tables extends string,
|
|
176
|
+
Aliases extends string
|
|
177
|
+
> =
|
|
178
|
+
QueryKind<N> extends "select"
|
|
179
|
+
// Cheap keyword pre-gate: skip the extraction walk unless a ` distinct on (`
|
|
180
|
+
// marker is present. Superset match -> no-match coincides with the empty-Seg
|
|
181
|
+
// `true` arm.
|
|
182
|
+
? Lowercase<N> extends `${string} distinct on (${string}` | `${string} distinct on(${string}`
|
|
183
|
+
? `${ExtractCallParenBodies<N, " distinct on (">} ${ExtractCallParenBodies<N, " distinct on(">}` extends infer Seg extends string
|
|
184
|
+
? Trim<Seg> extends ""
|
|
185
|
+
? true
|
|
186
|
+
: TokenizeLoose<Seg> extends infer Toks extends string[]
|
|
187
|
+
? And<
|
|
188
|
+
QualifiedColumnRefsValidFor<N, S, Tables, Aliases, Toks>,
|
|
189
|
+
UnqualifiedColumnRefsValidFor<N, S, Tables, Aliases, Toks, never>,
|
|
190
|
+
true,
|
|
191
|
+
true,
|
|
192
|
+
true
|
|
193
|
+
>
|
|
194
|
+
: true
|
|
195
|
+
: true
|
|
196
|
+
: true
|
|
197
|
+
: true;
|
|
198
|
+
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Derived-table & JOIN LATERAL / joined-derived result inference.
|
|
2
|
+
import type { AliasesInQuery, TablesInQuery } from "../tables.js";
|
|
3
|
+
import type { ApplyJoinNull, OuterCastTs, RefQualifier } from "../expressions.js";
|
|
4
|
+
import type { CleanIdent, ExtractAliasResult, ExtractFromClause, ExtractSelectList, SplitBalancedParen, SplitCommaSimple, SplitSelectList, Trim, TrimLeft } from "../parsing.js";
|
|
5
|
+
import type { DatabaseSchema } from "../schema.js";
|
|
6
|
+
import type { MergeRow, SelectReturnWith } from "./return-types.js";
|
|
7
|
+
import type { Simplify, UnionToIntersection } from "../utils.js";
|
|
8
|
+
// The outer columns come from the subquery's projection rather than a real
|
|
9
|
+
// table, so the normal table/alias machinery yields `never`. Detect a single
|
|
10
|
+
// derived-table FROM, compute the subquery's row type, and resolve the outer
|
|
11
|
+
// select list against it.
|
|
12
|
+
|
|
13
|
+
export type DerivedTableMatch<N extends string> =
|
|
14
|
+
Trim<ExtractFromClause<N>> extends `(${string}`
|
|
15
|
+
? SplitBalancedParen<Trim<ExtractFromClause<N>>> extends { inner: infer Body extends string; rest: infer Rest extends string }
|
|
16
|
+
? Trim<Body> extends `select ${string}`
|
|
17
|
+
? { body: Trim<Body>; alias: DerivedAliasName<Trim<Rest>>; cols: DerivedColsList<Trim<Rest>> }
|
|
18
|
+
: never
|
|
19
|
+
: never
|
|
20
|
+
: never;
|
|
21
|
+
|
|
22
|
+
export type DerivedAliasName<S extends string> =
|
|
23
|
+
Trim<S> extends `as ${infer R}` ? DerivedFirstWord<Trim<R>> : DerivedFirstWord<Trim<S>>;
|
|
24
|
+
|
|
25
|
+
// First token after the subquery paren — the alias name. Breaks on a space OR on
|
|
26
|
+
// a `(` so a column-alias list (`p(c1, c2)`) doesn't bleed into the alias.
|
|
27
|
+
export type DerivedFirstWord<S extends string> =
|
|
28
|
+
S extends `${infer W} ${string}`
|
|
29
|
+
? DerivedFirstWord<W>
|
|
30
|
+
: S extends `${infer W}(${string}`
|
|
31
|
+
? CleanIdent<W>
|
|
32
|
+
: CleanIdent<S>;
|
|
33
|
+
|
|
34
|
+
// Column-alias list on a derived table: `(<subquery>) AS p(c1, c2)`. The list
|
|
35
|
+
// positionally renames the subquery's exposed columns. We only treat the FIRST
|
|
36
|
+
// `(...)` as the list when the text before it is a single identifier (the alias);
|
|
37
|
+
// otherwise the `(` belongs to a later clause (`p where f(x) = 1`) and there is
|
|
38
|
+
// no list.
|
|
39
|
+
export type DerivedColsList<S extends string> =
|
|
40
|
+
Trim<S> extends `as ${infer R}` ? DerivedColsAfterAlias<Trim<R>> : DerivedColsAfterAlias<Trim<S>>;
|
|
41
|
+
|
|
42
|
+
export type DerivedColsAfterAlias<S extends string> =
|
|
43
|
+
S extends `${infer A}(${infer Cols})${string}`
|
|
44
|
+
? Trim<A> extends "" ? [] :
|
|
45
|
+
AliasHasNoSpace<Trim<A>> extends true
|
|
46
|
+
? FilterDerivedCols<SplitCommaSimple<Cols>>
|
|
47
|
+
: []
|
|
48
|
+
: [];
|
|
49
|
+
|
|
50
|
+
export type AliasHasNoSpace<S extends string> =
|
|
51
|
+
S extends `${string} ${string}` ? false : true;
|
|
52
|
+
|
|
53
|
+
export type FilterDerivedCols<Cols extends string[], Acc extends string[] = []> =
|
|
54
|
+
Cols extends [infer C extends string, ...infer Rest extends string[]]
|
|
55
|
+
? CleanIdent<C> extends ""
|
|
56
|
+
? FilterDerivedCols<Rest, Acc>
|
|
57
|
+
: FilterDerivedCols<Rest, [...Acc, CleanIdent<C>]>
|
|
58
|
+
: Acc;
|
|
59
|
+
|
|
60
|
+
// The subquery's projected row.
|
|
61
|
+
export type DerivedSubRow<Body extends string, S extends DatabaseSchema> =
|
|
62
|
+
TablesInQuery<Body, S> extends infer SubTables extends string
|
|
63
|
+
? AliasesInQuery<Body, S> extends infer SubAliases extends string
|
|
64
|
+
? SelectReturnWith<ExtractSelectList<Body>, SubTables, SubAliases, S>
|
|
65
|
+
: {}
|
|
66
|
+
: {};
|
|
67
|
+
|
|
68
|
+
// The derived table's exposed row after applying an optional `p(c1, c2)`
|
|
69
|
+
// column-alias list. Without a list it is just the body's projection. With a
|
|
70
|
+
// list the body columns are renamed positionally; a *partial* list renames the
|
|
71
|
+
// leading columns and leaves the trailing ones under their original names
|
|
72
|
+
// (Postgres allows fewer aliases than columns).
|
|
73
|
+
export type DerivedRenamedRow<Body extends string, Cols extends string[], S extends DatabaseSchema> =
|
|
74
|
+
DerivedSubRow<Body, S> extends infer BaseRow
|
|
75
|
+
? Cols extends []
|
|
76
|
+
? BaseRow
|
|
77
|
+
: RenamePartialRow<SplitSelectList<ExtractSelectList<Body>>, Cols, BaseRow>
|
|
78
|
+
: {};
|
|
79
|
+
|
|
80
|
+
// Pair the body's i-th projected expression with `Cols[i]` when supplied, else
|
|
81
|
+
// keep its own name. Shallow mapped type over the body exprs (no recursive
|
|
82
|
+
// intersection accumulation) to stay within TS's instantiation budget.
|
|
83
|
+
export type RenamePartialRow<BodyExprs extends string[], Cols extends string[], BaseRow> =
|
|
84
|
+
Simplify<UnionToIntersection<
|
|
85
|
+
{
|
|
86
|
+
[I in keyof BodyExprs]: BodyExprs[I] extends string
|
|
87
|
+
? { [P in DerivedRenameKey<I, Cols, BodyExprs[I]> & string]: DerivedBodyColType<BodyExprs[I], BaseRow> }
|
|
88
|
+
: {}
|
|
89
|
+
}[number]
|
|
90
|
+
>>;
|
|
91
|
+
|
|
92
|
+
export type DerivedRenameKey<I, Cols extends string[], BodyExpr extends string> =
|
|
93
|
+
I extends keyof Cols
|
|
94
|
+
? Cols[I] extends string ? Cols[I] : DerivedBodyColKey<BodyExpr>
|
|
95
|
+
: DerivedBodyColKey<BodyExpr>;
|
|
96
|
+
|
|
97
|
+
export type DerivedBodyColKey<E extends string> =
|
|
98
|
+
ExtractAliasResult<E> extends { expr: infer Raw extends string; alias: infer A extends string }
|
|
99
|
+
? [A] extends [never] ? CleanIdent<Raw> : A
|
|
100
|
+
: CleanIdent<E>;
|
|
101
|
+
|
|
102
|
+
export type DerivedBodyColType<E extends string, BaseRow> =
|
|
103
|
+
DerivedBodyColKey<E> extends infer K extends string
|
|
104
|
+
? K extends keyof BaseRow ? BaseRow[K] : unknown
|
|
105
|
+
: unknown;
|
|
106
|
+
|
|
107
|
+
export type DerivedTableReturn<N extends string, S extends DatabaseSchema> =
|
|
108
|
+
DerivedTableMatch<N> extends { body: infer Body extends string; alias: infer DAlias extends string; cols: infer Cols extends string[] }
|
|
109
|
+
? DerivedRenamedRow<Body, Cols, S> extends infer SubRow
|
|
110
|
+
? BuildDerivedReturn<SplitSelectList<ExtractSelectList<N>>, DAlias, SubRow>
|
|
111
|
+
: {}
|
|
112
|
+
: {};
|
|
113
|
+
|
|
114
|
+
export type BuildDerivedReturn<
|
|
115
|
+
Exprs extends string[],
|
|
116
|
+
DAlias extends string,
|
|
117
|
+
SubRow,
|
|
118
|
+
Acc = {},
|
|
119
|
+
Steps extends any[] = []
|
|
120
|
+
> = Steps["length"] extends 100
|
|
121
|
+
? Simplify<Acc>
|
|
122
|
+
: Exprs extends [infer H extends string, ...infer Rest extends string[]]
|
|
123
|
+
? BuildDerivedReturn<Rest, DAlias, SubRow, MergeRow<Acc, DerivedExprToObject<H, DAlias, SubRow>>, [any, ...Steps]>
|
|
124
|
+
: Simplify<Acc>;
|
|
125
|
+
|
|
126
|
+
// `Nullable` carries the outer-join nullable-qualifier set. When the derived
|
|
127
|
+
// table is the nullable side of an outer join (`LEFT JOIN LATERAL (...) d`), its
|
|
128
|
+
// exposed columns must gain `| null` (Postgres: the whole derived row is NULL when
|
|
129
|
+
// the join doesn't match). Defaulted to `never` so the leading-derived callers that
|
|
130
|
+
// don't pass it are unaffected.
|
|
131
|
+
export type DerivedExprToObject<E extends string, DAlias extends string, SubRow, Nullable extends string = never> =
|
|
132
|
+
ExtractAliasResult<E> extends { expr: infer RawExpr extends string; alias: infer OutAlias }
|
|
133
|
+
? [OutAlias] extends [never]
|
|
134
|
+
? CleanIdent<RawExpr> extends "*"
|
|
135
|
+
? SubRow
|
|
136
|
+
: CleanIdent<RawExpr> extends `${DAlias}.*`
|
|
137
|
+
? SubRow
|
|
138
|
+
: DerivedColKey<RawExpr, DAlias> extends infer K extends string
|
|
139
|
+
? { [P in K]: ApplyJoinNull<DerivedProjType<RawExpr, K, SubRow>, RawExpr, Nullable> }
|
|
140
|
+
: Record<string, unknown>
|
|
141
|
+
: OutAlias extends string
|
|
142
|
+
? { [P in OutAlias]: ApplyJoinNull<DerivedProjType<RawExpr, DerivedColKey<RawExpr, DAlias>, SubRow>, RawExpr, Nullable> }
|
|
143
|
+
: Record<string, unknown>
|
|
144
|
+
: Record<string, unknown>;
|
|
145
|
+
|
|
146
|
+
export type DerivedColKey<RawExpr extends string, DAlias extends string> =
|
|
147
|
+
CleanIdent<RawExpr> extends `${DAlias}.${infer Col}` ? Col : CleanIdent<RawExpr>;
|
|
148
|
+
|
|
149
|
+
// Type of an outer projection over a derived subquery. A plain column ref takes
|
|
150
|
+
// its type from the subquery row; any other expression isn't a derived column,
|
|
151
|
+
// so we recover its type from an outer cast (`extract(...)::int`) when present,
|
|
152
|
+
// otherwise `unknown` (the conservative default for unmodeled expressions).
|
|
153
|
+
export type DerivedProjType<RawExpr extends string, Col extends string, SubRow> =
|
|
154
|
+
Col extends keyof SubRow ? SubRow[Col] : OuterCastTs<RawExpr>;
|
|
155
|
+
|
|
156
|
+
export type DerivedColType<Col extends string, SubRow> =
|
|
157
|
+
Col extends keyof SubRow ? SubRow[Col] : unknown;
|
|
158
|
+
|
|
159
|
+
// A derived subquery can also appear as a JOIN source — `... JOIN [LATERAL]
|
|
160
|
+
// (<subquery>) <alias> ON ...` — not only as a leading `FROM (...)`. In that case
|
|
161
|
+
// the normal table/alias machinery never registers `<alias>`, so a projected
|
|
162
|
+
// `<alias>.col` resolves to nothing and is dropped from the row. These helpers
|
|
163
|
+
// expose such a JOINed derived row under its alias and apply outer-join nullability.
|
|
164
|
+
|
|
165
|
+
// Drop a leading `LATERAL` modifier so the derived source `(...)` is reachable.
|
|
166
|
+
export type StripLeadingLateral<S extends string> =
|
|
167
|
+
TrimLeft<S> extends `lateral ${infer R}` ? R : TrimLeft<S>;
|
|
168
|
+
|
|
169
|
+
// Find the subquery body of a derived table JOINed under `Alias`, scanning each
|
|
170
|
+
// ` join ` head. Returns `never` when no JOINed derived source carries that alias.
|
|
171
|
+
export type JoinedDerivedBody<N extends string, Alias extends string, Steps extends any[] = []> =
|
|
172
|
+
Steps["length"] extends 30
|
|
173
|
+
? never
|
|
174
|
+
: N extends `${infer _Before} join ${infer After}`
|
|
175
|
+
? StripLeadingLateral<After> extends infer Src extends string
|
|
176
|
+
? Trim<Src> extends `(${string}`
|
|
177
|
+
? SplitBalancedParen<Trim<Src>> extends { inner: infer Body extends string; rest: infer Rest extends string }
|
|
178
|
+
? Trim<Body> extends `select ${string}`
|
|
179
|
+
? DerivedAliasName<Trim<Rest>> extends Alias
|
|
180
|
+
? Trim<Body>
|
|
181
|
+
: JoinedDerivedBody<After, Alias, [any, ...Steps]>
|
|
182
|
+
: JoinedDerivedBody<After, Alias, [any, ...Steps]>
|
|
183
|
+
: JoinedDerivedBody<After, Alias, [any, ...Steps]>
|
|
184
|
+
: JoinedDerivedBody<After, Alias, [any, ...Steps]>
|
|
185
|
+
: never
|
|
186
|
+
: never;
|
|
187
|
+
|
|
188
|
+
// Whether the JOIN introducing the derived `Alias` makes its row nullable — i.e.
|
|
189
|
+
// the source sits on the nullable side of a LEFT or FULL [OUTER] JOIN. Returns
|
|
190
|
+
// `Alias` (the nullable-qualifier set for `ApplyJoinNull`) when so, else `never`.
|
|
191
|
+
// The token-based `NullableRelations` can't be used here: in its inlined token
|
|
192
|
+
// view the derived alias is separated from its `join` by the whole subquery body,
|
|
193
|
+
// so the alias is mis-attributed to the `lateral` modifier.
|
|
194
|
+
export type JoinedDerivedNullable<N extends string, Alias extends string, Steps extends any[] = []> =
|
|
195
|
+
Steps["length"] extends 30
|
|
196
|
+
? never
|
|
197
|
+
: N extends `${infer Before} join ${infer After}`
|
|
198
|
+
? StripLeadingLateral<After> extends infer Src extends string
|
|
199
|
+
? Trim<Src> extends `(${string}`
|
|
200
|
+
? SplitBalancedParen<Trim<Src>> extends { inner: infer Body extends string; rest: infer Rest extends string }
|
|
201
|
+
? Trim<Body> extends `select ${string}`
|
|
202
|
+
? DerivedAliasName<Trim<Rest>> extends Alias
|
|
203
|
+
? JoinModNullable<Before> extends true ? Alias : never
|
|
204
|
+
: JoinedDerivedNullable<After, Alias, [any, ...Steps]>
|
|
205
|
+
: JoinedDerivedNullable<After, Alias, [any, ...Steps]>
|
|
206
|
+
: JoinedDerivedNullable<After, Alias, [any, ...Steps]>
|
|
207
|
+
: JoinedDerivedNullable<After, Alias, [any, ...Steps]>
|
|
208
|
+
: never
|
|
209
|
+
: never;
|
|
210
|
+
|
|
211
|
+
// The join-keyword chain immediately precedes the matched ` join `. A trailing
|
|
212
|
+
// `left` / `full` (optionally `... outer`) marks the right-hand source nullable.
|
|
213
|
+
export type JoinModNullable<Before extends string> =
|
|
214
|
+
StripTrailingOuter<Lowercase<Trim<Before>>> extends `${string} left` | "left" | `${string} full` | "full"
|
|
215
|
+
? true
|
|
216
|
+
: false;
|
|
217
|
+
|
|
218
|
+
export type StripTrailingOuter<S extends string> =
|
|
219
|
+
Trim<S> extends `${infer H} outer` ? Trim<H> : Trim<S>;
|
|
220
|
+
|
|
221
|
+
// Contribution of select-list exprs that reference a JOINed derived subquery,
|
|
222
|
+
// with outer-join nullability applied. Gated on the presence of a `(` after a
|
|
223
|
+
// ` join ` so ordinary queries pay nothing.
|
|
224
|
+
export type JoinedDerivedReturn<N extends string, S extends DatabaseSchema, _Nullable extends string> =
|
|
225
|
+
N extends `${string} join ${string}(${string}`
|
|
226
|
+
? BuildJoinedDerivedReturn<SplitSelectList<ExtractSelectList<N>>, N, S>
|
|
227
|
+
: {};
|
|
228
|
+
|
|
229
|
+
export type BuildJoinedDerivedReturn<
|
|
230
|
+
Exprs extends string[],
|
|
231
|
+
N extends string,
|
|
232
|
+
S extends DatabaseSchema,
|
|
233
|
+
Acc = {},
|
|
234
|
+
Steps extends any[] = []
|
|
235
|
+
> = Steps["length"] extends 50
|
|
236
|
+
? Simplify<Acc>
|
|
237
|
+
: Exprs extends [infer H extends string, ...infer Rest extends string[]]
|
|
238
|
+
? ExtractAliasResult<H> extends { expr: infer RawExpr extends string; alias: infer _OutAlias }
|
|
239
|
+
? RefQualifier<RawExpr> extends infer Q extends string
|
|
240
|
+
? [Q] extends [never]
|
|
241
|
+
? BuildJoinedDerivedReturn<Rest, N, S, Acc, [any, ...Steps]>
|
|
242
|
+
: [JoinedDerivedBody<N, Q>] extends [infer Body extends string]
|
|
243
|
+
? [Body] extends [never]
|
|
244
|
+
? BuildJoinedDerivedReturn<Rest, N, S, Acc, [any, ...Steps]>
|
|
245
|
+
: BuildJoinedDerivedReturn<Rest, N, S, MergeRow<Acc, DerivedExprToObject<H, Q, DerivedSubRow<Body, S>, JoinedDerivedNullable<N, Q>>>, [any, ...Steps]>
|
|
246
|
+
: BuildJoinedDerivedReturn<Rest, N, S, Acc, [any, ...Steps]>
|
|
247
|
+
: BuildJoinedDerivedReturn<Rest, N, S, Acc, [any, ...Steps]>
|
|
248
|
+
: BuildJoinedDerivedReturn<Rest, N, S, Acc, [any, ...Steps]>
|
|
249
|
+
: Simplify<Acc>;
|
|
250
|
+
|
|
251
|
+
// Common-table expression (single CTE): `WITH [RECURSIVE] <name>[(<cols>)] AS
|
|
252
|
+
// (<body>) SELECT <outer> FROM <name> ...`. The previous result path leaked the
|
|
253
|
+
// INNER CTE select list (greedy `with ${string} select` match), dropping the
|