@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.
Files changed (208) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/dist/builder/assemble.d.ts +13 -0
  4. package/dist/builder/assemble.d.ts.map +1 -0
  5. package/dist/builder/assemble.js +86 -0
  6. package/dist/builder/assemble.js.map +1 -0
  7. package/dist/builder/condition-tree.d.ts +27 -0
  8. package/dist/builder/condition-tree.d.ts.map +1 -0
  9. package/dist/builder/condition-tree.js +91 -0
  10. package/dist/builder/condition-tree.js.map +1 -0
  11. package/dist/builder/conditional-sql.d.ts +80 -0
  12. package/dist/builder/conditional-sql.d.ts.map +1 -0
  13. package/dist/builder/conditional-sql.js +88 -0
  14. package/dist/builder/conditional-sql.js.map +1 -0
  15. package/dist/builder/db.d.ts +76 -0
  16. package/dist/builder/db.d.ts.map +1 -0
  17. package/dist/builder/db.js +12 -0
  18. package/dist/builder/db.js.map +1 -0
  19. package/dist/builder/delete.d.ts +39 -0
  20. package/dist/builder/delete.d.ts.map +1 -0
  21. package/dist/builder/delete.js +33 -0
  22. package/dist/builder/delete.js.map +1 -0
  23. package/dist/builder/extract-params.d.ts +97 -0
  24. package/dist/builder/extract-params.d.ts.map +1 -0
  25. package/dist/builder/extract-params.js +2 -0
  26. package/dist/builder/extract-params.js.map +1 -0
  27. package/dist/builder/index.d.ts +23 -0
  28. package/dist/builder/index.d.ts.map +1 -0
  29. package/dist/builder/index.js +14 -0
  30. package/dist/builder/index.js.map +1 -0
  31. package/dist/builder/insert.d.ts +51 -0
  32. package/dist/builder/insert.d.ts.map +1 -0
  33. package/dist/builder/insert.js +39 -0
  34. package/dist/builder/insert.js.map +1 -0
  35. package/dist/builder/mutate.d.ts +28 -0
  36. package/dist/builder/mutate.d.ts.map +1 -0
  37. package/dist/builder/mutate.js +17 -0
  38. package/dist/builder/mutate.js.map +1 -0
  39. package/dist/builder/params.d.ts +22 -0
  40. package/dist/builder/params.d.ts.map +1 -0
  41. package/dist/builder/params.js +65 -0
  42. package/dist/builder/params.js.map +1 -0
  43. package/dist/builder/return-type.d.ts +39 -0
  44. package/dist/builder/return-type.d.ts.map +1 -0
  45. package/dist/builder/return-type.js +2 -0
  46. package/dist/builder/return-type.js.map +1 -0
  47. package/dist/builder/scanner.d.ts +49 -0
  48. package/dist/builder/scanner.d.ts.map +1 -0
  49. package/dist/builder/scanner.js +240 -0
  50. package/dist/builder/scanner.js.map +1 -0
  51. package/dist/builder/select.d.ts +76 -0
  52. package/dist/builder/select.d.ts.map +1 -0
  53. package/dist/builder/select.js +240 -0
  54. package/dist/builder/select.js.map +1 -0
  55. package/dist/builder/sql-tag.d.ts +319 -0
  56. package/dist/builder/sql-tag.d.ts.map +1 -0
  57. package/dist/builder/sql-tag.js +3 -0
  58. package/dist/builder/sql-tag.js.map +1 -0
  59. package/dist/builder/sql.d.ts +17 -0
  60. package/dist/builder/sql.d.ts.map +1 -0
  61. package/dist/builder/sql.js +36 -0
  62. package/dist/builder/sql.js.map +1 -0
  63. package/dist/builder/state.d.ts +53 -0
  64. package/dist/builder/state.d.ts.map +1 -0
  65. package/dist/builder/state.js +18 -0
  66. package/dist/builder/state.js.map +1 -0
  67. package/dist/builder/update.d.ts +60 -0
  68. package/dist/builder/update.d.ts.map +1 -0
  69. package/dist/builder/update.js +40 -0
  70. package/dist/builder/update.js.map +1 -0
  71. package/dist/builder/write-assemble.d.ts +5 -0
  72. package/dist/builder/write-assemble.d.ts.map +1 -0
  73. package/dist/builder/write-assemble.js +57 -0
  74. package/dist/builder/write-assemble.js.map +1 -0
  75. package/dist/builder/write-state.d.ts +39 -0
  76. package/dist/builder/write-state.d.ts.map +1 -0
  77. package/dist/builder/write-state.js +6 -0
  78. package/dist/builder/write-state.js.map +1 -0
  79. package/dist/builder/write-tag.d.ts +91 -0
  80. package/dist/builder/write-tag.d.ts.map +1 -0
  81. package/dist/builder/write-tag.js +2 -0
  82. package/dist/builder/write-tag.js.map +1 -0
  83. package/dist/columns.d.ts +33 -0
  84. package/dist/columns.d.ts.map +1 -0
  85. package/dist/columns.js +2 -0
  86. package/dist/columns.js.map +1 -0
  87. package/dist/expressions.d.ts +71 -0
  88. package/dist/expressions.d.ts.map +1 -0
  89. package/dist/expressions.js +2 -0
  90. package/dist/expressions.js.map +1 -0
  91. package/dist/index.d.ts +22 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +5 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/parsing/extract.d.ts +47 -0
  96. package/dist/parsing/extract.d.ts.map +1 -0
  97. package/dist/parsing/extract.js +2 -0
  98. package/dist/parsing/extract.js.map +1 -0
  99. package/dist/parsing/normalize.d.ts +44 -0
  100. package/dist/parsing/normalize.d.ts.map +1 -0
  101. package/dist/parsing/normalize.js +2 -0
  102. package/dist/parsing/normalize.js.map +1 -0
  103. package/dist/parsing/pg-literals.d.ts +37 -0
  104. package/dist/parsing/pg-literals.d.ts.map +1 -0
  105. package/dist/parsing/pg-literals.js +2 -0
  106. package/dist/parsing/pg-literals.js.map +1 -0
  107. package/dist/parsing/split.d.ts +100 -0
  108. package/dist/parsing/split.d.ts.map +1 -0
  109. package/dist/parsing/split.js +2 -0
  110. package/dist/parsing/split.js.map +1 -0
  111. package/dist/parsing/string-utils.d.ts +29 -0
  112. package/dist/parsing/string-utils.d.ts.map +1 -0
  113. package/dist/parsing/string-utils.js +2 -0
  114. package/dist/parsing/string-utils.js.map +1 -0
  115. package/dist/parsing/tokenize.d.ts +27 -0
  116. package/dist/parsing/tokenize.d.ts.map +1 -0
  117. package/dist/parsing/tokenize.js +2 -0
  118. package/dist/parsing/tokenize.js.map +1 -0
  119. package/dist/parsing.d.ts +7 -0
  120. package/dist/parsing.d.ts.map +1 -0
  121. package/dist/parsing.js +9 -0
  122. package/dist/parsing.js.map +1 -0
  123. package/dist/partial.d.ts +30 -0
  124. package/dist/partial.d.ts.map +1 -0
  125. package/dist/partial.js +10 -0
  126. package/dist/partial.js.map +1 -0
  127. package/dist/schema.d.ts +28 -0
  128. package/dist/schema.d.ts.map +1 -0
  129. package/dist/schema.js +2 -0
  130. package/dist/schema.js.map +1 -0
  131. package/dist/tables.d.ts +34 -0
  132. package/dist/tables.d.ts.map +1 -0
  133. package/dist/tables.js +2 -0
  134. package/dist/tables.js.map +1 -0
  135. package/dist/utils.d.ts +13 -0
  136. package/dist/utils.d.ts.map +1 -0
  137. package/dist/utils.js +3 -0
  138. package/dist/utils.js.map +1 -0
  139. package/dist/validation/cte.d.ts +54 -0
  140. package/dist/validation/cte.d.ts.map +1 -0
  141. package/dist/validation/cte.js +2 -0
  142. package/dist/validation/cte.js.map +1 -0
  143. package/dist/validation/dispatch.d.ts +31 -0
  144. package/dist/validation/dispatch.d.ts.map +1 -0
  145. package/dist/validation/dispatch.js +2 -0
  146. package/dist/validation/dispatch.js.map +1 -0
  147. package/dist/validation/joins.d.ts +16 -0
  148. package/dist/validation/joins.d.ts.map +1 -0
  149. package/dist/validation/joins.js +2 -0
  150. package/dist/validation/joins.js.map +1 -0
  151. package/dist/validation/return-derived.d.ts +67 -0
  152. package/dist/validation/return-derived.d.ts.map +1 -0
  153. package/dist/validation/return-derived.js +5 -0
  154. package/dist/validation/return-derived.js.map +1 -0
  155. package/dist/validation/return-types.d.ts +41 -0
  156. package/dist/validation/return-types.d.ts.map +1 -0
  157. package/dist/validation/return-types.js +2 -0
  158. package/dist/validation/return-types.js.map +1 -0
  159. package/dist/validation/validate-columns.d.ts +63 -0
  160. package/dist/validation/validate-columns.d.ts.map +1 -0
  161. package/dist/validation/validate-columns.js +2 -0
  162. package/dist/validation/validate-columns.js.map +1 -0
  163. package/dist/validation.d.ts +7 -0
  164. package/dist/validation.d.ts.map +1 -0
  165. package/dist/validation.js +9 -0
  166. package/dist/validation.js.map +1 -0
  167. package/package.json +64 -0
  168. package/src/builder/assemble.ts +100 -0
  169. package/src/builder/condition-tree.ts +162 -0
  170. package/src/builder/conditional-sql.ts +325 -0
  171. package/src/builder/db.ts +281 -0
  172. package/src/builder/delete.ts +57 -0
  173. package/src/builder/extract-params.ts +507 -0
  174. package/src/builder/index.ts +58 -0
  175. package/src/builder/insert.ts +75 -0
  176. package/src/builder/mutate.ts +55 -0
  177. package/src/builder/params.ts +95 -0
  178. package/src/builder/return-type.ts +66 -0
  179. package/src/builder/scanner.ts +254 -0
  180. package/src/builder/select.ts +470 -0
  181. package/src/builder/sql-tag.ts +422 -0
  182. package/src/builder/sql.ts +51 -0
  183. package/src/builder/state.ts +55 -0
  184. package/src/builder/update.ts +77 -0
  185. package/src/builder/write-assemble.ts +52 -0
  186. package/src/builder/write-state.ts +43 -0
  187. package/src/builder/write-tag.ts +119 -0
  188. package/src/columns.ts +336 -0
  189. package/src/expressions.ts +745 -0
  190. package/src/index.ts +81 -0
  191. package/src/parsing/extract.ts +260 -0
  192. package/src/parsing/normalize.ts +243 -0
  193. package/src/parsing/pg-literals.ts +289 -0
  194. package/src/parsing/split.ts +288 -0
  195. package/src/parsing/string-utils.ts +172 -0
  196. package/src/parsing/tokenize.ts +321 -0
  197. package/src/parsing.ts +8 -0
  198. package/src/partial.ts +241 -0
  199. package/src/schema.ts +130 -0
  200. package/src/tables.ts +278 -0
  201. package/src/utils.ts +43 -0
  202. package/src/validation/cte.ts +198 -0
  203. package/src/validation/dispatch.ts +312 -0
  204. package/src/validation/joins.ts +198 -0
  205. package/src/validation/return-derived.ts +253 -0
  206. package/src/validation/return-types.ts +271 -0
  207. package/src/validation/validate-columns.ts +489 -0
  208. package/src/validation.ts +8 -0
package/src/partial.ts ADDED
@@ -0,0 +1,241 @@
1
+ // Partial (fragment) query validation for the query builder. Each clause of a
2
+ // query gets its own validation entry point. Clause/SELECT validation is now
3
+ // scope-aware: the builder derives an alias→table map from the FROM+JOIN
4
+ // fragments once and passes it in (Tables/Aliases) so a qualified reference is
5
+ // checked against the whole query's scope, not just one fragment in isolation.
6
+ // Refs whose alias/table is still out of that scope, or that aren't plain
7
+ // `alias.col` identifiers, are SKIPPED rather than failed. FROM/JOIN fragments
8
+ // remain self-contained (validated against the schema directly).
9
+
10
+ import type { DatabaseSchema, ColumnExists, TableExists } from "./schema.js";
11
+ import type {
12
+ CleanExpr,
13
+ CleanIdent,
14
+ ExtractBefore,
15
+ HasSpecial,
16
+ NormalizeQuery,
17
+ SplitOnDotClean,
18
+ SplitTopLevel,
19
+ TokenizeLoose,
20
+ Trim
21
+ } from "./parsing.js";
22
+ import type {
23
+ QualifiedColumnRefs,
24
+ ResolveAlias,
25
+ StripDoubleQuotes,
26
+ TableKeysByName
27
+ } from "./columns.js";
28
+ import type { AliasesInQuery, TableKeyValid, TablesInQuery } from "./tables.js";
29
+ import type { AllTrue } from "./utils.js";
30
+
31
+ // Resolve a qualified-ref prefix to a known table key WITHIN the fragment, or
32
+ // `never` when it cannot be resolved (an out-of-scope alias/table -> skip):
33
+ // 1. an alias defined in this part
34
+ // 2. a table named in this part
35
+ // 3. a real table in the default schema with this name
36
+ // Unlike the full-query `ResolveTableKey`, there is NO phantom
37
+ // `${defaultSchema}.${Name}` fallback for names that are not real tables.
38
+ export type PartialResolvePrefix<
39
+ Name extends string,
40
+ Tables extends string,
41
+ Aliases extends string,
42
+ S extends DatabaseSchema
43
+ > =
44
+ [ResolveAlias<CleanIdent<Name>, Aliases>] extends [never]
45
+ ? [TableKeysByName<CleanIdent<Name>, Tables>] extends [never]
46
+ ? TableExists<S, S["defaultSchema"], CleanIdent<Name>> extends true
47
+ ? `${S["defaultSchema"]}.${CleanIdent<Name>}`
48
+ : never
49
+ : TableKeysByName<CleanIdent<Name>, Tables>
50
+ : ResolveAlias<CleanIdent<Name>, Aliases>;
51
+
52
+ // Validate a single column-ref string in fragment mode:
53
+ // - `prefix.*` -> skip (no column to validate)
54
+ // - schema.table.col -> strict if schema.table is real, else skip
55
+ // - prefix.col -> strict if prefix resolves in-fragment, else skip
56
+ // - bare col -> skip (may belong to an out-of-fragment table)
57
+ export type ColumnRefValidPartialWith<
58
+ ColRef extends string,
59
+ Tables extends string,
60
+ Aliases extends string,
61
+ S extends DatabaseSchema
62
+ > =
63
+ ColRef extends `${string}.*`
64
+ ? true
65
+ : SplitOnDotClean<StripDoubleQuotes<CleanExpr<ColRef>>> extends [infer A extends string, infer B extends string, infer C extends string]
66
+ ? TableExists<S, A, B> extends true
67
+ ? ColumnExists<`${A}.${B}`, C, S>
68
+ : true
69
+ : SplitOnDotClean<StripDoubleQuotes<CleanExpr<ColRef>>> extends [infer A extends string, infer B extends string]
70
+ ? PartialResolvePrefix<A, Tables, Aliases, S> extends infer TK
71
+ ? [TK] extends [never]
72
+ ? true
73
+ : TK extends string
74
+ ? ColumnExists<TK, B, S>
75
+ : true
76
+ : true
77
+ : true;
78
+
79
+ // Validate every qualified column ref in a token list, partial-mode.
80
+ export type QualifiedColumnRefsValidPartialFor<
81
+ S extends DatabaseSchema,
82
+ Tables extends string,
83
+ Aliases extends string,
84
+ LooseTokens extends string[]
85
+ > = QualifiedColumnRefs<LooseTokens, S, Tables, Aliases> extends infer Cols
86
+ ? AllTrue<Cols extends string ? ColumnRefValidPartialWith<Cols, Tables, Aliases, S> : true>
87
+ : true;
88
+
89
+ // Strict table-existence over every table named in the fragment. Vacuously
90
+ // `true` when the fragment names no table (`Tables` is `never`).
91
+ export type AllPartTablesValid<Tables extends string, S extends DatabaseSchema> =
92
+ AllTrue<Tables extends string ? TableKeyValid<Tables, S> : true>;
93
+
94
+ // Shared validator for table-source fragments (FROM / JOIN): every named table
95
+ // must exist (strict), and every resolvable qualified ref must check out.
96
+ export type ValidateTableSourcePart<N extends string, S extends DatabaseSchema> =
97
+ TablesInQuery<N, S> extends infer Tables extends string
98
+ ? AliasesInQuery<N, S> extends infer Aliases extends string
99
+ ? AllPartTablesValid<Tables, S> extends true
100
+ ? TokenizeLoose<N> extends infer Toks extends string[]
101
+ ? QualifiedColumnRefsValidPartialFor<S, Tables, Aliases, Toks>
102
+ : true
103
+ : false
104
+ : true
105
+ : true;
106
+
107
+ // A FROM fragment may arrive bare (`users u`) or led (`from users u`). The
108
+ // table/alias collectors key off the `from` keyword, so ensure it is present.
109
+ export type EnsureFromLed<N extends string> =
110
+ N extends `from ${string}` ? N : `from ${N}`;
111
+
112
+ export type ValidateFromPart<Part extends string, S extends DatabaseSchema> =
113
+ string extends Part
114
+ ? false
115
+ : NormalizeQuery<Part> extends infer N extends string
116
+ ? ValidateTableSourcePart<EnsureFromLed<N>, S>
117
+ : false;
118
+
119
+ export type ValidateJoinPart<Part extends string, S extends DatabaseSchema> =
120
+ string extends Part
121
+ ? false
122
+ : NormalizeQuery<Part> extends infer N extends string
123
+ ? ValidateTableSourcePart<N, S>
124
+ : false;
125
+
126
+ // Clause fragments (SELECT list, WHERE, HAVING, GROUP BY, ORDER BY) carry no
127
+ // table source in isolation. Validate only refs resolvable without one
128
+ // (`schema.table.col` and real `table.col`); skip alias-qualified and bare cols.
129
+ export type ValidateClausePart<Part extends string, S extends DatabaseSchema> =
130
+ string extends Part
131
+ ? false
132
+ : NormalizeQuery<Part> extends infer N extends string
133
+ ? TokenizeLoose<N> extends infer Toks extends string[]
134
+ ? QualifiedColumnRefsValidPartialFor<S, never, never, Toks>
135
+ : true
136
+ : false;
137
+
138
+ // Scope-aware clause validation: identical to ValidateClausePart, but the
139
+ // alias->table map (built from FROM+JOIN by the builder) is threaded in so that
140
+ // alias-qualified refs (`u.col`) resolve and typos are caught.
141
+ export type ValidateClausePartScoped<
142
+ Part extends string,
143
+ Tables extends string,
144
+ Aliases extends string,
145
+ S extends DatabaseSchema
146
+ > =
147
+ string extends Part
148
+ ? false
149
+ : NormalizeQuery<Part> extends infer N extends string
150
+ ? TokenizeLoose<N> extends infer Toks extends string[]
151
+ ? QualifiedColumnRefsValidPartialFor<S, Tables, Aliases, Toks>
152
+ : true
153
+ : false;
154
+
155
+ // Expression-detector for a single SELECT-item token. HasSpecial covers space,
156
+ // parens, arithmetic/comparison operators, comma, `::`, `||`. We additionally
157
+ // reject `[ ] " ' :` so array-indexing, quoted-with-space idents, json arrows,
158
+ // and param/cast colons are treated as expressions (skipped, never falsely
159
+ // rejected). A token clearing this guard is a plain identifier piece.
160
+ type RefHasSpecial<S extends string> =
161
+ HasSpecial<S> extends true ? true :
162
+ S extends `${string}[${string}` ? true :
163
+ S extends `${string}]${string}` ? true :
164
+ S extends `${string}"${string}` ? true :
165
+ S extends `${string}'${string}` ? true :
166
+ S extends `${string}:${string}` ? true :
167
+ false;
168
+
169
+ // True iff S is a plain two-part `alias.col` ref (no expression syntax).
170
+ // `${infer A}.${infer B}` binds A to the shortest pre-first-dot match; a 3-part
171
+ // `schema.table.col` leaves a dot in B and is rejected (skipped).
172
+ type IsPlainQualifiedRef<S extends string> =
173
+ S extends `${infer A}.${infer B}`
174
+ ? RefHasSpecial<A> extends true
175
+ ? false
176
+ : RefHasSpecial<B> extends true
177
+ ? false
178
+ : B extends `${string}.${string}`
179
+ ? false
180
+ : true
181
+ : false;
182
+
183
+ // The leading token of a SELECT item with any trailing alias dropped
184
+ // (`o.id as foo` / `o.id foo` -> `o.id`). ExtractBefore returns the whole string
185
+ // when there is no space.
186
+ type SelectItemRef<Item extends string> = ExtractBefore<Trim<Item>, " ">;
187
+
188
+ // Validate ONE select item: resolve only plain `alias.col` refs; skip everything
189
+ // else (functions, CASE, casts, literals, `*`, quoted-space idents) -> true.
190
+ type SelectItemValid<
191
+ Item extends string,
192
+ Tables extends string,
193
+ Aliases extends string,
194
+ S extends DatabaseSchema
195
+ > = SelectItemRef<Item> extends infer Ref extends string
196
+ ? IsPlainQualifiedRef<Ref> extends true
197
+ ? ColumnRefValidPartialWith<Ref, Tables, Aliases, S>
198
+ : true
199
+ : true;
200
+
201
+ // Validate every top-level SELECT item. Early-exit on first false. Step-capped.
202
+ type SelectListValidScoped<
203
+ List extends readonly string[],
204
+ Tables extends string,
205
+ Aliases extends string,
206
+ S extends DatabaseSchema,
207
+ Steps extends any[] = []
208
+ > = Steps["length"] extends 200
209
+ ? true
210
+ : List extends readonly [infer H extends string, ...infer R extends readonly string[]]
211
+ ? SelectItemValid<H, Tables, Aliases, S> extends false
212
+ ? false
213
+ : SelectListValidScoped<R, Tables, Aliases, S, [any, ...Steps]>
214
+ : true;
215
+
216
+ // Identifiers-only SELECT validation: split the list at top-level commas
217
+ // (depth-safe, never normalizes expression bodies) and check only plain refs.
218
+ export type ValidateSelectIdentifiersScoped<
219
+ Part extends string,
220
+ Tables extends string,
221
+ Aliases extends string,
222
+ S extends DatabaseSchema
223
+ > = string extends Part
224
+ ? false
225
+ : SplitTopLevel<Part> extends infer Items extends readonly string[]
226
+ ? SelectListValidScoped<Items, Tables, Aliases, S>
227
+ : true;
228
+
229
+ // Distinct public entry points, identical in isolation; they will diverge once
230
+ // clause-specific context is threaded in (HAVING aggregates, GROUP BY/ORDER BY
231
+ // ordinals, SELECT `*`), so keep them as separate names rather than collapsing.
232
+ export type ValidateSelectPart<Part extends string, S extends DatabaseSchema> =
233
+ ValidateClausePart<Part, S>;
234
+ export type ValidateWherePart<Part extends string, S extends DatabaseSchema> =
235
+ ValidateClausePart<Part, S>;
236
+ export type ValidateHavingPart<Part extends string, S extends DatabaseSchema> =
237
+ ValidateClausePart<Part, S>;
238
+ export type ValidateGroupByPart<Part extends string, S extends DatabaseSchema> =
239
+ ValidateClausePart<Part, S>;
240
+ export type ValidateOrderByPart<Part extends string, S extends DatabaseSchema> =
241
+ ValidateClausePart<Part, S>;
package/src/schema.ts ADDED
@@ -0,0 +1,130 @@
1
+ import type { AnyTrue, Simplify, UnionToIntersection } from "./utils.js";
2
+
3
+ export type DatabaseSchema = {
4
+ defaultSchema: string;
5
+ schemas: Record<string, Record<string, Record<string, any>>>;
6
+ };
7
+
8
+ export type StringKeys<T> = Extract<keyof T, string>;
9
+
10
+ export type MatchKeyCaseInsensitive<Obj, Name extends string> =
11
+ string extends Name
12
+ ? never
13
+ : StringKeys<Obj> extends infer K extends string
14
+ ? K extends any
15
+ ? Lowercase<K> extends Lowercase<Name>
16
+ ? K
17
+ : never
18
+ : never
19
+ : never;
20
+
21
+ export type ResolveSchemaName<S extends DatabaseSchema, Schema extends string> =
22
+ MatchKeyCaseInsensitive<S["schemas"], Schema>;
23
+
24
+ export type ResolveTableName<
25
+ S extends DatabaseSchema,
26
+ Schema extends string,
27
+ Table extends string
28
+ > =
29
+ Schema extends keyof S["schemas"]
30
+ ? MatchKeyCaseInsensitive<S["schemas"][Schema], Table>
31
+ : never;
32
+
33
+ export type NormalizeTableKey<TableKey extends string, S extends DatabaseSchema> =
34
+ TableKey extends `${infer Schema}.${infer Table}`
35
+ ? ResolveSchemaName<S, Schema> extends infer RS extends string
36
+ ? ResolveTableName<S, RS, Table> extends infer RT extends string
37
+ ? `${RS}.${RT}`
38
+ : never
39
+ : never
40
+ : never;
41
+
42
+ export type RowTypeFromNormalizedTableKey<
43
+ TableKey extends string,
44
+ S extends DatabaseSchema
45
+ > =
46
+ TableKey extends `${infer Schema}.${infer Table}`
47
+ ? Schema extends keyof S["schemas"]
48
+ ? Table extends keyof S["schemas"][Schema]
49
+ ? S["schemas"][Schema][Table]
50
+ : never
51
+ : never
52
+ : never;
53
+
54
+ export type RowTypeForResolvedTableKey<TableKey extends string, S extends DatabaseSchema> =
55
+ NormalizeTableKey<TableKey, S> extends infer Normalized extends string
56
+ ? RowTypeFromNormalizedTableKey<Normalized, S>
57
+ : never;
58
+
59
+ export type ResolveColumnName<
60
+ TableKey extends string,
61
+ Column extends string,
62
+ S extends DatabaseSchema
63
+ > =
64
+ RowTypeForResolvedTableKey<TableKey, S> extends infer Row extends Record<string, any>
65
+ ? MatchKeyCaseInsensitive<Row, Column>
66
+ : never;
67
+
68
+ export type TableExists<S extends DatabaseSchema, Schema extends string, Table extends string> =
69
+ NormalizeTableKey<`${Schema}.${Table}`, S> extends never ? false : true;
70
+
71
+ export type ColumnExists<TableKey extends string, Column extends string, S extends DatabaseSchema> =
72
+ ColumnTypeFromTableKey<TableKey, Column, S> extends never ? false : true;
73
+
74
+ export type ColumnTypeFromTableKey<TableKey extends string, Column extends string, S extends DatabaseSchema> =
75
+ ResolveColumnName<TableKey, Column, S> extends infer ResolvedColumn extends string
76
+ ? RowTypeForResolvedTableKey<TableKey, S> extends infer Row extends Record<string, any>
77
+ ? ResolvedColumn extends keyof Row
78
+ ? Row[ResolvedColumn]
79
+ : never
80
+ : never
81
+ : never;
82
+
83
+ export type ColumnTypeFromSchemaTable<
84
+ Schema extends string,
85
+ Table extends string,
86
+ Column extends string,
87
+ S extends DatabaseSchema
88
+ > =
89
+ ColumnTypeFromTableKey<`${Schema}.${Table}`, Column, S>;
90
+
91
+ export type RowTypeForTables<Tables extends string, S extends DatabaseSchema> =
92
+ MergeRowUnion<
93
+ Tables extends string
94
+ ? RowTypeForResolvedTableKey<Tables, S>
95
+ : never
96
+ >;
97
+
98
+ // Merge a union of row types into one row. Unlike UnionToIntersection, a column
99
+ // present in several joined tables keeps the UNION of its types (not their
100
+ // intersection, which collapses differing same-named columns to `never`).
101
+ export type MergeRowUnion<Rows> =
102
+ [Rows] extends [never]
103
+ ? {}
104
+ : Simplify<{
105
+ [K in (Rows extends any ? keyof Rows : never) & PropertyKey]:
106
+ Rows extends any ? (K extends keyof Rows ? Rows[K] : never) : never;
107
+ }>;
108
+
109
+ export type RowTypeForTable<TableKey extends string, S extends DatabaseSchema> =
110
+ RowTypeForResolvedTableKey<TableKey, S>;
111
+
112
+ export type AllTableKeys<S extends DatabaseSchema> =
113
+ keyof S["schemas"] extends infer Schema
114
+ ? Schema extends string
115
+ ? keyof S["schemas"][Schema] extends infer Table
116
+ ? Table extends string
117
+ ? `${Schema}.${Table}`
118
+ : never
119
+ : never
120
+ : never
121
+ : never;
122
+
123
+ export type ColumnExistsInAnyTable<Column extends string, S extends DatabaseSchema> =
124
+ AnyTrue<ColumnExistsInTableUnion<AllTableKeys<S>, Column, S>>;
125
+
126
+ export type ColumnExistsInTableUnion<
127
+ Tables extends string,
128
+ Column extends string,
129
+ S extends DatabaseSchema
130
+ > = Tables extends any ? ColumnExists<Tables, Column, S> : false;
package/src/tables.ts ADDED
@@ -0,0 +1,278 @@
1
+ import type { DatabaseSchema, NormalizeTableKey, TableExists } from "./schema.js";
2
+ import type { CleanIdent, CommaSep, SplitOnDotClean, SqlKeyword, Tokenize, TokenizeTables } from "./parsing.js";
3
+
4
+ // Table and alias extraction
5
+
6
+ export type TablesInQuery<N extends string, S extends DatabaseSchema> =
7
+ CollectTables<TokenizeTables<N>, S, never, false, N extends `delete ${string}` ? true : false>;
8
+
9
+ export type AliasesInQuery<N extends string, S extends DatabaseSchema> =
10
+ CollectAliases<TokenizeTables<N>, S, never, false, N extends `delete ${string}` ? true : false>;
11
+
12
+ export type TableKeyValid<Key extends string, S extends DatabaseSchema> =
13
+ Key extends `${infer Schema}.${infer Table}`
14
+ ? TableExists<S, Schema, Table>
15
+ : false;
16
+
17
+ export type PreferNormalizedTableKey<Raw extends string, Normalized> =
18
+ [Normalized] extends [never] ? Raw : Extract<Normalized, string>;
19
+
20
+ export type TableKeyFromToken<Token extends string, S extends DatabaseSchema> =
21
+ ParseTableToken<Token, S> extends infer Parsed
22
+ ? Parsed extends { schema: infer Schema extends string; table: infer Table extends string }
23
+ ? PreferNormalizedTableKey<`${Schema}.${Table}`, NormalizeTableKey<`${Schema}.${Table}`, S>>
24
+ : never
25
+ : never;
26
+
27
+ export type ParseTableToken<Token extends string, S extends DatabaseSchema> =
28
+ SplitOnDotClean<Token> extends [infer A extends string, infer B extends string]
29
+ ? { schema: A; table: B }
30
+ : SplitOnDotClean<Token> extends [infer A extends string]
31
+ ? { schema: S["defaultSchema"]; table: A }
32
+ : never;
33
+
34
+ // Insert/update/delete target table
35
+
36
+ export type InsertTargetTable<N extends string, S extends DatabaseSchema> =
37
+ TableAfter<Tokenize<N>, "into", S>;
38
+
39
+ export type UpdateTargetTable<N extends string, S extends DatabaseSchema> =
40
+ TableAfter<Tokenize<N>, "update", S>;
41
+
42
+ export type DeleteTargetTable<N extends string, S extends DatabaseSchema> =
43
+ TableAfter<Tokenize<N>, "from", S>;
44
+
45
+ // Collect tables by keyword
46
+
47
+ // `InList` tracks whether we are inside a FROM-source list, so a TOP-LEVEL comma
48
+ // (preserved as a `,` token by `TokenizeTables`) introduces ANOTHER table source
49
+ // — the ANSI comma cross-join `from a, b`. The flag is turned on after a
50
+ // `from`/`join`/`into`/`update` source and off at the next clause keyword, so
51
+ // commas in the SELECT list / GROUP BY / ORDER BY / value tuples are ignored.
52
+ //
53
+ // `InDelete` marks that we are inside a DELETE statement, where `USING` is a
54
+ // table-source clause (`DELETE FROM a USING b, c`) — collected like FROM/JOIN.
55
+ // `USING` in a SELECT (the JOIN ... USING (cols) join condition) is NOT a table
56
+ // source, so the branch is gated: outside a DELETE, `using` is skipped as before.
57
+ export type CollectTables<
58
+ Tokens extends string[],
59
+ S extends DatabaseSchema,
60
+ Acc extends string = never,
61
+ InList extends boolean = false,
62
+ InDelete extends boolean = false
63
+ > =
64
+ Tokens extends [infer T extends string, infer Next extends string, ...infer Rest extends string[]]
65
+ ? T extends "from" | "join" | "into"
66
+ // `JOIN LATERAL (subquery|func(...)) alias` — `LATERAL` is a source
67
+ // modifier, not a relation. Skip it: the subquery body's own `from`
68
+ // re-establishes collection of its real tables, and a function-call
69
+ // source token is never reached by a from/join/comma branch, so neither
70
+ // the bare `lateral` nor the function name is mistaken for a table.
71
+ ? Next extends "lateral"
72
+ ? CollectTables<Rest, S, Acc, true, InDelete>
73
+ : CollectTables<Rest, S, Acc | TableKeyFromToken<Next, S>, true, InDelete>
74
+ : T extends "update"
75
+ ? Next extends "set"
76
+ ? CollectTables<Rest, S, Acc, false, InDelete>
77
+ : CollectTables<Rest, S, Acc | TableKeyFromToken<Next, S>, true, InDelete>
78
+ : T extends "delete"
79
+ ? Next extends "from"
80
+ ? Rest extends [infer DelTable extends string, ...infer Rest2 extends string[]]
81
+ ? CollectTables<Rest2, S, Acc | TableKeyFromToken<DelTable, S>, false, true>
82
+ : Acc
83
+ : CollectTables<[Next, ...Rest], S, Acc, false, true>
84
+ : T extends "using"
85
+ ? InDelete extends true
86
+ ? CollectTables<Rest, S, Acc | TableKeyFromToken<Next, S>, true, InDelete>
87
+ : CollectTables<[Next, ...Rest], S, Acc, InList, InDelete>
88
+ : T extends CommaSep
89
+ ? InList extends true
90
+ ? CollectTables<Rest, S, Acc | TableKeyFromToken<Next, S>, true, InDelete>
91
+ : CollectTables<[Next, ...Rest], S, Acc, false, InDelete>
92
+ : T extends "as"
93
+ ? CollectTables<[Next, ...Rest], S, Acc, InList, InDelete>
94
+ // `IS [NOT] DISTINCT FROM` is a comparison
95
+ // operator, not a FROM clause: the `from` after
96
+ // `distinct` must NOT be collected as a table
97
+ // source. Drop the operator `from` (process
98
+ // `Rest`) so its RHS isn't mistaken for a table.
99
+ : T extends "distinct"
100
+ ? Next extends "from"
101
+ ? CollectTables<Rest, S, Acc, false, InDelete>
102
+ : CollectTables<[Next, ...Rest], S, Acc, false, InDelete>
103
+ : T extends SqlKeyword
104
+ ? CollectTables<[Next, ...Rest], S, Acc, false, InDelete>
105
+ : CollectTables<[Next, ...Rest], S, Acc, InList, InDelete>
106
+ : Acc;
107
+
108
+ // Collect aliases for tables in FROM/JOIN/UPDATE
109
+
110
+ // `InList` mirrors `CollectTables`: after a `from`/`join`/`update` source, a
111
+ // top-level `,` introduces another aliased source (`from users u, orders o`).
112
+ // `InDelete` likewise mirrors `CollectTables`: inside a DELETE, `USING` opens an
113
+ // aliased table source (`DELETE FROM a USING users u`); outside one it is left
114
+ // alone (the JOIN ... USING (cols) join condition is not a source).
115
+ export type CollectAliases<
116
+ Tokens extends string[],
117
+ S extends DatabaseSchema,
118
+ Acc extends string = never,
119
+ InList extends boolean = false,
120
+ InDelete extends boolean = false
121
+ > =
122
+ Tokens extends [infer T extends string, infer Next extends string, ...infer Rest extends string[]]
123
+ ? T extends "from" | "join" | "update"
124
+ // `JOIN LATERAL (...)` — mirror `CollectTables`: skip the `lateral`
125
+ // modifier so it is never parsed as an aliased table source.
126
+ ? Next extends "lateral"
127
+ ? CollectAliases<Rest, S, Acc, true, InDelete>
128
+ : ParseAliasSource<Next, Rest, S, Acc, InDelete>
129
+ : T extends "using"
130
+ ? InDelete extends true
131
+ ? ParseAliasSource<Next, Rest, S, Acc, InDelete>
132
+ : CollectAliases<[Next, ...Rest], S, Acc, InList, InDelete>
133
+ : T extends CommaSep
134
+ ? InList extends true
135
+ ? ParseAliasSource<Next, Rest, S, Acc, InDelete>
136
+ : CollectAliases<[Next, ...Rest], S, Acc, false, InDelete>
137
+ : T extends "as"
138
+ ? CollectAliases<[Next, ...Rest], S, Acc, InList, InDelete>
139
+ // `IS [NOT] DISTINCT FROM`: the operator `from` is not a
140
+ // table source, so it must not open an aliased source.
141
+ : T extends "distinct"
142
+ ? Next extends "from"
143
+ ? CollectAliases<Rest, S, Acc, false, InDelete>
144
+ : CollectAliases<[Next, ...Rest], S, Acc, false, InDelete>
145
+ : T extends SqlKeyword
146
+ ? CollectAliases<[Next, ...Rest], S, Acc, false, InDelete>
147
+ : CollectAliases<[Next, ...Rest], S, Acc, InList, InDelete>
148
+ : Acc;
149
+
150
+ // Parse a single table source (`Next`) plus its optional alias from the tokens
151
+ // that follow it (`Rest`), record the alias, then continue collecting with
152
+ // `InList=true` so a subsequent top-level comma is recognized as another source.
153
+ // An immediate `,` after the table (no alias) is handed back to `CollectAliases`
154
+ // rather than mistaken for an alias.
155
+ export type ParseAliasSource<
156
+ Next extends string,
157
+ Rest extends string[],
158
+ S extends DatabaseSchema,
159
+ Acc extends string,
160
+ InDelete extends boolean = false
161
+ > =
162
+ TableKeyFromToken<Next, S> extends infer TableKey extends string
163
+ ? Rest extends [infer MaybeAlias extends string, ...infer Rest2 extends string[]]
164
+ ? MaybeAlias extends "as"
165
+ ? Rest2 extends [infer Alias extends string, ...infer Rest3 extends string[]]
166
+ ? CollectAliases<Rest3, S, Acc | AliasEntry<Alias, TableKey>, true, InDelete>
167
+ : Acc
168
+ : MaybeAlias extends CommaSep
169
+ ? CollectAliases<Rest, S, Acc, true, InDelete>
170
+ // Inside a DELETE, a following `using` is not this source's
171
+ // alias — it opens the next table source. Hand it back so
172
+ // `CollectAliases` processes the USING clause.
173
+ : InDelete extends true
174
+ ? MaybeAlias extends "using"
175
+ ? CollectAliases<Rest, S, Acc, true, InDelete>
176
+ : IsAliasCandidate<MaybeAlias> extends true
177
+ ? CollectAliases<Rest2, S, Acc | AliasEntry<MaybeAlias, TableKey>, true, InDelete>
178
+ : CollectAliases<Rest, S, Acc, true, InDelete>
179
+ : IsAliasCandidate<MaybeAlias> extends true
180
+ ? CollectAliases<Rest2, S, Acc | AliasEntry<MaybeAlias, TableKey>, true, InDelete>
181
+ : CollectAliases<Rest, S, Acc, true, InDelete>
182
+ : Acc
183
+ : CollectAliases<Rest, S, Acc, true, InDelete>;
184
+
185
+ // Outer-join nullability
186
+ //
187
+ // A column drawn from a relation on the OUTER side of a join can be NULL even
188
+ // when its declared schema type is non-nullable: a LEFT JOIN's right table may
189
+ // have no matching row, a RIGHT JOIN's left tables may have none, and a FULL
190
+ // JOIN's rows may be missing on either side. `NullableRelations` returns the set
191
+ // of *reference qualifiers* (the alias each relation is referenced by, or its
192
+ // table name when unaliased) that are nullable for this reason; the projection
193
+ // path unions `| null` onto any directly-projected column qualified by one of
194
+ // them. Keying by qualifier (not table key) is what lets a self-join
195
+ // (`users u ... left join users m`) nullablize only the outer alias `m`, not the
196
+ // base `u`. INNER/CROSS joins and the leading FROM source contribute nothing.
197
+ // (Limitation: an UNqualified projected column from an outer-joined relation is
198
+ // not nullablized, since it carries no qualifier to match.)
199
+ export type NullableRelations<N extends string, S extends DatabaseSchema> =
200
+ CollectNullable<TokenizeTables<N>, "none", never, never>;
201
+
202
+ // `Mod` is the pending join modifier ("left"/"right"/"full"/"none"); `Left` is
203
+ // the set of qualifiers accumulated so far (the left side of any later join);
204
+ // `Acc` is the nullable-qualifier accumulator. `outer` is noise (keeps `Mod`);
205
+ // `inner`/`cross` reset `Mod` to "none". On a `join <table>`: LEFT adds the
206
+ // joined relation; RIGHT adds the accumulated left side; FULL adds both.
207
+ export type CollectNullable<
208
+ Tokens extends string[],
209
+ Mod extends string,
210
+ Left extends string,
211
+ Acc extends string
212
+ > =
213
+ Tokens extends [infer T extends string, ...infer Rest extends string[]]
214
+ ? T extends "left"
215
+ ? CollectNullable<Rest, "left", Left, Acc>
216
+ : T extends "right"
217
+ ? CollectNullable<Rest, "right", Left, Acc>
218
+ : T extends "full"
219
+ ? CollectNullable<Rest, "full", Left, Acc>
220
+ : T extends "inner" | "cross"
221
+ ? CollectNullable<Rest, "none", Left, Acc>
222
+ : T extends "outer"
223
+ ? CollectNullable<Rest, Mod, Left, Acc>
224
+ : T extends "from" | "into"
225
+ ? Rest extends [infer Tbl extends string, ...infer R2 extends string[]]
226
+ ? CollectNullable<R2, "none", Left | SourceQualifier<Tbl, R2>, Acc>
227
+ : Acc
228
+ : T extends "join"
229
+ ? Rest extends [infer Tbl extends string, ...infer R2 extends string[]]
230
+ ? SourceQualifier<Tbl, R2> extends infer Q extends string
231
+ ? Mod extends "left"
232
+ ? CollectNullable<R2, "none", Left | Q, Acc | Q>
233
+ : Mod extends "right"
234
+ ? CollectNullable<R2, "none", Left | Q, Acc | Left>
235
+ : Mod extends "full"
236
+ ? CollectNullable<R2, "none", Left | Q, Acc | Left | Q>
237
+ : CollectNullable<R2, "none", Left | Q, Acc>
238
+ : Acc
239
+ : Acc
240
+ : CollectNullable<Rest, Mod, Left, Acc>
241
+ : Acc;
242
+
243
+ // The qualifier a relation is referenced by in projections: its alias when one
244
+ // is present (`... t`/`... as t`), else the (cleaned) table name. `Rest` are the
245
+ // tokens following the table token. A following keyword (`on`, `where`, another
246
+ // `join`, ...) is not an alias, so the table name is used.
247
+ export type SourceQualifier<Tbl extends string, Rest extends string[]> =
248
+ Rest extends ["as", infer A extends string, ...string[]]
249
+ ? CleanIdent<A>
250
+ : Rest extends [infer Maybe extends string, ...string[]]
251
+ ? IsAliasCandidate<Maybe> extends true
252
+ ? CleanIdent<Maybe>
253
+ : CleanIdent<Tbl>
254
+ : CleanIdent<Tbl>;
255
+
256
+ // Table lookup after a keyword
257
+
258
+ export type TableAfter<Tokens extends string[], Keyword extends string, S extends DatabaseSchema> =
259
+ Tokens extends [infer T extends string, infer Next extends string, ...infer Rest extends string[]]
260
+ ? T extends Keyword
261
+ ? TableKeyFromToken<Next, S>
262
+ : TableAfter<[Next, ...Rest], Keyword, S>
263
+ : never;
264
+
265
+ // Aliases
266
+
267
+ export type AliasEntry<Alias extends string, TableKey extends string> = `${CleanIdent<Alias>}=>${TableKey}`;
268
+
269
+ export type IsAliasCandidate<Token extends string> =
270
+ Token extends "" ? false :
271
+ Token extends SqlKeyword ? false :
272
+ true;
273
+
274
+ export type AliasNames<Aliases extends string> =
275
+ Aliases extends `${infer A}=>${string}` ? A : never;
276
+
277
+ export type IsAliasName<Token extends string, Aliases extends string> =
278
+ Token extends AliasNames<Aliases> ? true : false;