@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
@@ -0,0 +1,422 @@
1
+ // src/builder/sql-tag.ts
2
+
3
+ /** A non-select clause fragment (where/join/group/having/order/cte). */
4
+ export interface Frag {
5
+ readonly id: string;
6
+ readonly text: string;
7
+ }
8
+
9
+ /** A select fragment: raw rendered column-list text + conditional flag. */
10
+ export interface SelFrag {
11
+ readonly id: string;
12
+ readonly text: string; // e.g. "u.id, u.name" (cols already joined with ", ")
13
+ readonly cond: boolean; // true = selectIf / applyIf-introduced
14
+ }
15
+
16
+ /** Lean type-level fragment tag: ordered fragment lists per clause. */
17
+ export interface SqlTag {
18
+ readonly ctes: readonly Frag[];
19
+ readonly selects: readonly SelFrag[];
20
+ readonly from: string | null; // null = no FROM; `string` (non-literal) widens BuildSQL
21
+ readonly joins: readonly Frag[];
22
+ readonly wheres: readonly Frag[];
23
+ readonly groupBys: readonly Frag[];
24
+ readonly havings: readonly Frag[];
25
+ readonly orderBys: readonly Frag[];
26
+ readonly limit: number | null;
27
+ readonly offset: number | null;
28
+ readonly union: string | null;
29
+ }
30
+
31
+ /** Upper bound for "any builder" (replaces old AnyBuilderSqlTag/AnyBuilderStateTag). */
32
+ export type AnySqlTag = SqlTag;
33
+
34
+ export type EmptySqlTag = {
35
+ readonly ctes: readonly [];
36
+ readonly selects: readonly [];
37
+ readonly from: null;
38
+ readonly joins: readonly [];
39
+ readonly wheres: readonly [];
40
+ readonly groupBys: readonly [];
41
+ readonly havings: readonly [];
42
+ readonly orderBys: readonly [];
43
+ readonly limit: null;
44
+ readonly offset: null;
45
+ readonly union: null;
46
+ };
47
+
48
+ // --- list mutation helpers (replace-by-id-or-push; matches runtime) ---
49
+ //
50
+ // GENERIC-BASE NOTE: the working element bound is `{ id: string; text: string }`,
51
+ // NOT just `{ id: string }`. When a builder is used generically
52
+ // (`fn<Schema, Sql extends SqlTag>(b)`), `Sql["wheres"]` is the symbolic
53
+ // `readonly Frag[]` and the auto id becomes a `where_${number}` pattern; the
54
+ // tuple recursion then matches a variadic `[...Frag[], X]` and TS widens the
55
+ // inferred head `H` to the declared bound. With the looser `{ id: string }` bound
56
+ // that widened element drops `text`, so the accumulated list is no longer
57
+ // assignable to `readonly Frag[]` and any downstream `Sql2 extends SqlTag` check
58
+ // (e.g. `setPeriod(b)`) fails. Both `Frag` AND `SelFrag` carry `id` + `text`, so
59
+ // the tighter bound keeps the widened element `Frag`-assignable while leaving
60
+ // concrete-tuple inference (every literal `EmptySqlTag` chain) unchanged — the
61
+ // bound is only an upper limit; exact element types are still inferred for real
62
+ // tuples.
63
+ type HasId<List extends readonly { id: string }[], Id extends string> =
64
+ List extends readonly [infer H extends { id: string; text: string }, ...infer R extends readonly { id: string; text: string }[]]
65
+ ? H["id"] extends Id ? true : HasId<R, Id>
66
+ : false;
67
+
68
+ type ReplaceById<
69
+ List extends readonly { id: string }[],
70
+ Id extends string,
71
+ Item,
72
+ > = List extends readonly [infer H extends { id: string; text: string }, ...infer R extends readonly { id: string; text: string }[]]
73
+ ? H["id"] extends Id
74
+ ? readonly [Item, ...R]
75
+ : readonly [H, ...ReplaceById<R, Id, Item>]
76
+ : readonly [];
77
+
78
+ type UpsertById<
79
+ List extends readonly { id: string }[],
80
+ Id extends string,
81
+ Item extends { id: string },
82
+ > = HasId<List, Id> extends true
83
+ ? ReplaceById<List, Id, Item>
84
+ : readonly [...List, Item];
85
+
86
+ type FilterOutId<
87
+ List extends readonly { id: string }[],
88
+ Id extends string,
89
+ > = List extends readonly [infer H extends { id: string; text: string }, ...infer R extends readonly { id: string; text: string }[]]
90
+ ? H["id"] extends Id
91
+ ? FilterOutId<R, Id>
92
+ : readonly [H, ...FilterOutId<R, Id>]
93
+ : readonly [];
94
+
95
+ // --- type-level auto-id (mirrors runtime `select_${count}`, `join_${count}`, …) ---
96
+ // The next fragment id is the current fragment count, exactly as the runtime
97
+ // derives `select_${Object.keys(selectSql).length}`. O(1) — `length` on a
98
+ // readonly tuple is the literal element count.
99
+ export type AutoId<Prefix extends string, List extends readonly unknown[]> =
100
+ `${Prefix}_${List["length"] & number}`;
101
+
102
+ // An explicit caller id wins; `undefined` → the clause's auto id.
103
+ export type ResolveId<
104
+ Provided extends string | undefined,
105
+ Prefix extends string,
106
+ List extends readonly unknown[],
107
+ > = Provided extends string ? Provided : AutoId<Prefix, List>;
108
+
109
+ // --- per-clause `With*` helpers used by select.ts ---
110
+ //
111
+ // DEPTH NOTE: these MUST NOT be written as `Omit<Sql, K> & { K: New }`.
112
+ // `Omit<X,K>` is `Pick<X, Exclude<keyof X, K>>`, so a chain of N builder calls
113
+ // nests N `Omit`s and reading ANY field forces `keyof` over the whole
114
+ // `Omit`-of-`Omit`-of-… intersection — recomputed at every step. A ~17-call
115
+ // `.whereIf()` chain crosses TS's depth-100 guard (TS2589/TS2590), and the deep
116
+ // nest can also stop being recognised as a `SqlTag`. Instead each helper rebuilds
117
+ // a FLAT 11-field object, overriding only its one field and copying the other ten
118
+ // via direct indexed access (`Sql["<field>"]`) — O(1) depth per call. The
119
+ // produced type is structurally the same `SqlTag` (same fields + `readonly`).
120
+ //
121
+ // Each helper writes a FLAT 11-field literal directly: override the one field it
122
+ // owns, copy the other ten via direct indexed access. No generic patch-merge
123
+ // helper — a plain literal is the cheapest possible (no extra conditionals).
124
+
125
+ export type WithSelect<
126
+ Sql extends SqlTag,
127
+ Text extends string,
128
+ Id extends string,
129
+ Cond extends boolean,
130
+ > = {
131
+ readonly ctes: Sql["ctes"];
132
+ readonly selects: UpsertById<Sql["selects"], Id, { id: Id; text: Text; cond: Cond }>;
133
+ readonly from: Sql["from"];
134
+ readonly joins: Sql["joins"];
135
+ readonly wheres: Sql["wheres"];
136
+ readonly groupBys: Sql["groupBys"];
137
+ readonly havings: Sql["havings"];
138
+ readonly orderBys: Sql["orderBys"];
139
+ readonly limit: Sql["limit"];
140
+ readonly offset: Sql["offset"];
141
+ readonly union: Sql["union"];
142
+ };
143
+
144
+ export type WithoutSelect<Sql extends SqlTag, Id extends string> = {
145
+ readonly ctes: Sql["ctes"];
146
+ readonly selects: FilterOutId<Sql["selects"], Id>;
147
+ readonly from: Sql["from"];
148
+ readonly joins: Sql["joins"];
149
+ readonly wheres: Sql["wheres"];
150
+ readonly groupBys: Sql["groupBys"];
151
+ readonly havings: Sql["havings"];
152
+ readonly orderBys: Sql["orderBys"];
153
+ readonly limit: Sql["limit"];
154
+ readonly offset: Sql["offset"];
155
+ readonly union: Sql["union"];
156
+ };
157
+
158
+ export type WithFrom<Sql extends SqlTag, Text extends string> = {
159
+ readonly ctes: Sql["ctes"];
160
+ readonly selects: Sql["selects"];
161
+ readonly from: Text;
162
+ readonly joins: Sql["joins"];
163
+ readonly wheres: Sql["wheres"];
164
+ readonly groupBys: Sql["groupBys"];
165
+ readonly havings: Sql["havings"];
166
+ readonly orderBys: Sql["orderBys"];
167
+ readonly limit: Sql["limit"];
168
+ readonly offset: Sql["offset"];
169
+ readonly union: Sql["union"];
170
+ };
171
+
172
+ export type WithJoin<Sql extends SqlTag, Text extends string, Id extends string> = {
173
+ readonly ctes: Sql["ctes"];
174
+ readonly selects: Sql["selects"];
175
+ readonly from: Sql["from"];
176
+ readonly joins: UpsertById<Sql["joins"], Id, { id: Id; text: Text }>;
177
+ readonly wheres: Sql["wheres"];
178
+ readonly groupBys: Sql["groupBys"];
179
+ readonly havings: Sql["havings"];
180
+ readonly orderBys: Sql["orderBys"];
181
+ readonly limit: Sql["limit"];
182
+ readonly offset: Sql["offset"];
183
+ readonly union: Sql["union"];
184
+ };
185
+
186
+ export type WithoutJoin<Sql extends SqlTag, Id extends string> = {
187
+ readonly ctes: Sql["ctes"];
188
+ readonly selects: Sql["selects"];
189
+ readonly from: Sql["from"];
190
+ readonly joins: FilterOutId<Sql["joins"], Id>;
191
+ readonly wheres: Sql["wheres"];
192
+ readonly groupBys: Sql["groupBys"];
193
+ readonly havings: Sql["havings"];
194
+ readonly orderBys: Sql["orderBys"];
195
+ readonly limit: Sql["limit"];
196
+ readonly offset: Sql["offset"];
197
+ readonly union: Sql["union"];
198
+ };
199
+
200
+ export type WithoutWhere<Sql extends SqlTag, Id extends string> = {
201
+ readonly ctes: Sql["ctes"];
202
+ readonly selects: Sql["selects"];
203
+ readonly from: Sql["from"];
204
+ readonly joins: Sql["joins"];
205
+ readonly wheres: FilterOutId<Sql["wheres"], Id>;
206
+ readonly groupBys: Sql["groupBys"];
207
+ readonly havings: Sql["havings"];
208
+ readonly orderBys: Sql["orderBys"];
209
+ readonly limit: Sql["limit"];
210
+ readonly offset: Sql["offset"];
211
+ readonly union: Sql["union"];
212
+ };
213
+
214
+ export type WithWhere<Sql extends SqlTag, Text extends string, Id extends string> = {
215
+ readonly ctes: Sql["ctes"];
216
+ readonly selects: Sql["selects"];
217
+ readonly from: Sql["from"];
218
+ readonly joins: Sql["joins"];
219
+ readonly wheres: UpsertById<Sql["wheres"], Id, { id: Id; text: Text }>;
220
+ readonly groupBys: Sql["groupBys"];
221
+ readonly havings: Sql["havings"];
222
+ readonly orderBys: Sql["orderBys"];
223
+ readonly limit: Sql["limit"];
224
+ readonly offset: Sql["offset"];
225
+ readonly union: Sql["union"];
226
+ };
227
+
228
+ export type WithGroupBy<Sql extends SqlTag, Text extends string, Id extends string> = {
229
+ readonly ctes: Sql["ctes"];
230
+ readonly selects: Sql["selects"];
231
+ readonly from: Sql["from"];
232
+ readonly joins: Sql["joins"];
233
+ readonly wheres: Sql["wheres"];
234
+ readonly groupBys: UpsertById<Sql["groupBys"], Id, { id: Id; text: Text }>;
235
+ readonly havings: Sql["havings"];
236
+ readonly orderBys: Sql["orderBys"];
237
+ readonly limit: Sql["limit"];
238
+ readonly offset: Sql["offset"];
239
+ readonly union: Sql["union"];
240
+ };
241
+
242
+ export type WithoutGroupBy<Sql extends SqlTag, Id extends string> = {
243
+ readonly ctes: Sql["ctes"];
244
+ readonly selects: Sql["selects"];
245
+ readonly from: Sql["from"];
246
+ readonly joins: Sql["joins"];
247
+ readonly wheres: Sql["wheres"];
248
+ readonly groupBys: FilterOutId<Sql["groupBys"], Id>;
249
+ readonly havings: Sql["havings"];
250
+ readonly orderBys: Sql["orderBys"];
251
+ readonly limit: Sql["limit"];
252
+ readonly offset: Sql["offset"];
253
+ readonly union: Sql["union"];
254
+ };
255
+
256
+ export type WithHaving<Sql extends SqlTag, Text extends string, Id extends string> = {
257
+ readonly ctes: Sql["ctes"];
258
+ readonly selects: Sql["selects"];
259
+ readonly from: Sql["from"];
260
+ readonly joins: Sql["joins"];
261
+ readonly wheres: Sql["wheres"];
262
+ readonly groupBys: Sql["groupBys"];
263
+ readonly havings: UpsertById<Sql["havings"], Id, { id: Id; text: Text }>;
264
+ readonly orderBys: Sql["orderBys"];
265
+ readonly limit: Sql["limit"];
266
+ readonly offset: Sql["offset"];
267
+ readonly union: Sql["union"];
268
+ };
269
+
270
+ export type WithoutHaving<Sql extends SqlTag, Id extends string> = {
271
+ readonly ctes: Sql["ctes"];
272
+ readonly selects: Sql["selects"];
273
+ readonly from: Sql["from"];
274
+ readonly joins: Sql["joins"];
275
+ readonly wheres: Sql["wheres"];
276
+ readonly groupBys: Sql["groupBys"];
277
+ readonly havings: FilterOutId<Sql["havings"], Id>;
278
+ readonly orderBys: Sql["orderBys"];
279
+ readonly limit: Sql["limit"];
280
+ readonly offset: Sql["offset"];
281
+ readonly union: Sql["union"];
282
+ };
283
+
284
+ export type WithOrderBy<Sql extends SqlTag, Text extends string, Id extends string> = {
285
+ readonly ctes: Sql["ctes"];
286
+ readonly selects: Sql["selects"];
287
+ readonly from: Sql["from"];
288
+ readonly joins: Sql["joins"];
289
+ readonly wheres: Sql["wheres"];
290
+ readonly groupBys: Sql["groupBys"];
291
+ readonly havings: Sql["havings"];
292
+ readonly orderBys: UpsertById<Sql["orderBys"], Id, { id: Id; text: Text }>;
293
+ readonly limit: Sql["limit"];
294
+ readonly offset: Sql["offset"];
295
+ readonly union: Sql["union"];
296
+ };
297
+
298
+ export type WithoutOrderBy<Sql extends SqlTag, Id extends string> = {
299
+ readonly ctes: Sql["ctes"];
300
+ readonly selects: Sql["selects"];
301
+ readonly from: Sql["from"];
302
+ readonly joins: Sql["joins"];
303
+ readonly wheres: Sql["wheres"];
304
+ readonly groupBys: Sql["groupBys"];
305
+ readonly havings: Sql["havings"];
306
+ readonly orderBys: FilterOutId<Sql["orderBys"], Id>;
307
+ readonly limit: Sql["limit"];
308
+ readonly offset: Sql["offset"];
309
+ readonly union: Sql["union"];
310
+ };
311
+
312
+ export type WithLimit<Sql extends SqlTag, L extends number> = {
313
+ readonly ctes: Sql["ctes"];
314
+ readonly selects: Sql["selects"];
315
+ readonly from: Sql["from"];
316
+ readonly joins: Sql["joins"];
317
+ readonly wheres: Sql["wheres"];
318
+ readonly groupBys: Sql["groupBys"];
319
+ readonly havings: Sql["havings"];
320
+ readonly orderBys: Sql["orderBys"];
321
+ readonly limit: L;
322
+ readonly offset: Sql["offset"];
323
+ readonly union: Sql["union"];
324
+ };
325
+
326
+ export type WithOffset<Sql extends SqlTag, O extends number> = {
327
+ readonly ctes: Sql["ctes"];
328
+ readonly selects: Sql["selects"];
329
+ readonly from: Sql["from"];
330
+ readonly joins: Sql["joins"];
331
+ readonly wheres: Sql["wheres"];
332
+ readonly groupBys: Sql["groupBys"];
333
+ readonly havings: Sql["havings"];
334
+ readonly orderBys: Sql["orderBys"];
335
+ readonly limit: Sql["limit"];
336
+ readonly offset: O;
337
+ readonly union: Sql["union"];
338
+ };
339
+
340
+ // --- BuildSQL: assemble a literal mirroring assembleSelectSQL's ordering ---
341
+
342
+ export type BuildMode = "max" | "req" | "scope";
343
+
344
+ // Join a list of fragment texts with a separator (drops empties).
345
+ type JoinTexts<
346
+ List extends readonly { text: string }[],
347
+ Sep extends string,
348
+ Acc extends string = "",
349
+ > = List extends readonly [infer H extends { text: string }, ...infer R extends readonly { text: string }[]]
350
+ ? JoinTexts<R, Sep, Acc extends "" ? H["text"] : `${Acc}${Sep}${H["text"]}`>
351
+ : Acc;
352
+
353
+ // Select fragments for a given mode: "req" keeps only cond=false.
354
+ type SelectsForMode<List extends readonly SelFrag[], Mode extends BuildMode> =
355
+ Mode extends "req"
356
+ ? FilterUncond<List>
357
+ : List;
358
+
359
+ type FilterUncond<List extends readonly SelFrag[]> =
360
+ List extends readonly [infer H extends SelFrag, ...infer R extends readonly SelFrag[]]
361
+ ? H["cond"] extends false
362
+ ? readonly [H, ...FilterUncond<R>]
363
+ : FilterUncond<R>
364
+ : readonly [];
365
+
366
+ // SELECT clause text for a mode.
367
+ type SelectClause<Sql extends SqlTag, Mode extends BuildMode> =
368
+ Mode extends "scope"
369
+ ? "SELECT *"
370
+ : SelectsForMode<Sql["selects"], Mode> extends infer Sel extends readonly SelFrag[]
371
+ ? Sel extends readonly []
372
+ ? "SELECT *"
373
+ : `SELECT ${JoinTexts<Sel, ", ">}`
374
+ : "SELECT *";
375
+
376
+ // Optional prefixed clause: "" when the list is empty.
377
+ type Clause<Kw extends string, List extends readonly { text: string }[], Sep extends string> =
378
+ List extends readonly [] ? "" : ` ${Kw} ${JoinTexts<List, Sep>}`;
379
+
380
+ type FromClause<From extends string | null> =
381
+ From extends null ? "" : ` FROM ${From & string}`;
382
+
383
+ type JoinClause<List extends readonly Frag[]> =
384
+ List extends readonly [] ? "" : ` ${JoinTexts<List, " ">}`;
385
+
386
+ type WithClause<List extends readonly Frag[]> =
387
+ List extends readonly [] ? "" : `WITH ${JoinTexts<List, ", ">} `;
388
+
389
+ type LimitClause<L extends number | null> =
390
+ L extends number ? ` LIMIT ${L}` : "";
391
+
392
+ type OffsetClause<O extends number | null> =
393
+ O extends number ? ` OFFSET ${O}` : "";
394
+
395
+ type UnionClause<U extends string | null> =
396
+ U extends null ? "" : ` ${U & string}`;
397
+
398
+ // Raw literal assembly — mirrors assembleSelectSQL's clause ordering exactly.
399
+ type BuildSQLRaw<Sql extends SqlTag, Mode extends BuildMode> =
400
+ `${WithClause<Sql["ctes"]>}${SelectClause<Sql, Mode>}${FromClause<Sql["from"]>}${JoinClause<Sql["joins"]>}${Clause<"WHERE", Sql["wheres"], " AND ">}${Clause<"GROUP BY", Sql["groupBys"], ", ">}${Clause<"HAVING", Sql["havings"], " AND ">}${Clause<"ORDER BY", Sql["orderBys"], ", ">}${LimitClause<Sql["limit"]>}${OffsetClause<Sql["offset"]>}${UnionClause<Sql["union"]>}`;
401
+
402
+ // Union of every participating fragment text (+ from / union clauses). If any
403
+ // member is the unconstrained `string`, the union contains `string`.
404
+ type AllTexts<Sql extends SqlTag> =
405
+ | (Sql["from"] extends null ? never : Sql["from"])
406
+ | Sql["selects"][number]["text"]
407
+ | Sql["joins"][number]["text"]
408
+ | Sql["wheres"][number]["text"]
409
+ | Sql["groupBys"][number]["text"]
410
+ | Sql["havings"][number]["text"]
411
+ | Sql["orderBys"][number]["text"]
412
+ | Sql["ctes"][number]["text"]
413
+ | (Sql["union"] extends null ? never : Sql["union"]);
414
+
415
+ /**
416
+ * Assemble the tag into a literal SQL string. Widens to `string` if any
417
+ * participating fragment text is non-literal (e.g. from(dynamic)) — a template
418
+ * with a `string` placeholder (`\`SELECT * FROM ${string}\``) is NOT itself
419
+ * `string`, so we detect non-literal inputs up front and short-circuit.
420
+ */
421
+ export type BuildSQL<Sql extends SqlTag, Mode extends BuildMode> =
422
+ string extends AllTexts<Sql> ? string : BuildSQLRaw<Sql, Mode>;
@@ -0,0 +1,51 @@
1
+ // src/builder/sql.ts
2
+ import type { DatabaseSchema } from "../schema.js";
3
+ import type { ExtractParams, ExtractReturning } from "./extract-params.js";
4
+ import {
5
+ assertAllProvided, collectScanned, expandScanned, type DriverParamValue,
6
+ } from "./scanner.js";
7
+
8
+ /** A reusable, typed raw-SQL query object. */
9
+ export interface TypedSql<Q extends string, S extends DatabaseSchema> {
10
+ withParams(params: ExtractParams<Q, S>): BoundSql<Q, S>;
11
+ toString(): string;
12
+ }
13
+
14
+ export interface BoundSql<Q extends string, S extends DatabaseSchema> {
15
+ toString(): string;
16
+ getParams(): ReadonlyArray<DriverParamValue>;
17
+ /** Phantom carrier for the RETURNING row type (read by createMutateFn). */
18
+ readonly __returning?: ExtractReturning<Q, S>;
19
+ }
20
+
21
+ class BoundSqlImpl<Q extends string, S extends DatabaseSchema> {
22
+ constructor(
23
+ private readonly raw: string,
24
+ private readonly params: Record<string, DriverParamValue>,
25
+ ) {}
26
+ toString(): string {
27
+ assertAllProvided(this.raw, this.params);
28
+ return expandScanned(this.raw, this.params);
29
+ }
30
+ getParams(): ReadonlyArray<DriverParamValue> {
31
+ assertAllProvided(this.raw, this.params);
32
+ return collectScanned(this.raw, this.params);
33
+ }
34
+ }
35
+
36
+ class TypedSqlImpl<Q extends string, S extends DatabaseSchema> {
37
+ constructor(private readonly raw: string) {}
38
+ withParams(params: Record<string, DriverParamValue>): any {
39
+ return new BoundSqlImpl<Q, S>(this.raw, params);
40
+ }
41
+ toString(): string {
42
+ return this.raw;
43
+ }
44
+ }
45
+
46
+ /** Factory binding the schema once; covers INSERT/UPDATE/DELETE in Phase 1. */
47
+ export function createSql<S extends DatabaseSchema>() {
48
+ return function sql<Q extends string>(query: Q): TypedSql<Q, S> {
49
+ return new TypedSqlImpl<Q, S>(query) as unknown as TypedSql<Q, S>;
50
+ };
51
+ }
@@ -0,0 +1,55 @@
1
+ // src/builder/state.ts
2
+ import type { QueryParamInput, QueryParamValue } from "./params.js";
3
+
4
+ /**
5
+ * Immutable runtime state for the SELECT builder. Standalone — carries only
6
+ * the fields consumed by assembleSelectSQL / getParams. Fragments are keyed
7
+ * by id; join ORDER is preserved by the `joins` array.
8
+ */
9
+ export interface RuntimeSelectState {
10
+ /** Raw SELECT fragments by id; each is the array of column strings. */
11
+ readonly selectSql: { readonly [id: string]: string[] };
12
+ /** Raw FROM fragment (if present). */
13
+ readonly fromSql?: string;
14
+ /** Raw JOIN fragments by id. */
15
+ readonly joinSql: { readonly [id: string]: string };
16
+ /** Join ordering — only the id is needed for assembly. */
17
+ readonly joins: ReadonlyArray<{ readonly id: string }>;
18
+ /** Raw WHERE fragments by id (joined with AND). */
19
+ readonly whereSql: { readonly [id: string]: string };
20
+ /** Raw GROUP BY fragments by id (joined with ", "). */
21
+ readonly groupBySql: { readonly [id: string]: string };
22
+ /** Raw HAVING fragments by id (joined with AND). */
23
+ readonly havingSql: { readonly [id: string]: string };
24
+ /** Raw ORDER BY fragments by id (joined with ", "). */
25
+ readonly orderBySql: { readonly [id: string]: string };
26
+ /** Raw CTE fragments by id. */
27
+ readonly cteSql: { readonly [id: string]: string };
28
+ /** Raw UNION fragment (if any). */
29
+ readonly unionSql?: string;
30
+ readonly distinct: boolean;
31
+ readonly limit?: number;
32
+ readonly offset?: number;
33
+ /** Legacy positional params (kept for getParams() fallback). */
34
+ readonly params: ReadonlyArray<QueryParamValue>;
35
+ /** Named params; :name placeholders resolve from here. */
36
+ readonly namedParams: Record<string, QueryParamInput>;
37
+ }
38
+
39
+ export const EMPTY_RUNTIME_STATE: RuntimeSelectState = {
40
+ selectSql: {},
41
+ fromSql: undefined,
42
+ joinSql: {},
43
+ joins: [],
44
+ whereSql: {},
45
+ groupBySql: {},
46
+ havingSql: {},
47
+ orderBySql: {},
48
+ cteSql: {},
49
+ unionSql: undefined,
50
+ distinct: false,
51
+ limit: undefined,
52
+ offset: undefined,
53
+ params: [],
54
+ namedParams: {},
55
+ };
@@ -0,0 +1,77 @@
1
+ // src/builder/update.ts
2
+ import type { DatabaseSchema } from "../schema.js";
3
+ import { assembleUpdateSQL } from "./write-assemble.js";
4
+ import { EMPTY_UPDATE_STATE, type RuntimeUpdateState } from "./write-state.js";
5
+ import {
6
+ assertAllProvided, collectScanned, expandScanned, type DriverParamValue,
7
+ } from "./scanner.js";
8
+ import type { UpdateTag, WriteParamsFor } from "./write-tag.js";
9
+ import type { BoundWrite } from "./insert.js";
10
+
11
+ type PushSet<T extends UpdateTag, Text extends string, Cond extends boolean> =
12
+ Omit<T, "sets"> & { readonly sets: readonly [...T["sets"], { text: Text; cond: Cond }] };
13
+ type PushFrom<T extends UpdateTag, Text extends string, Cond extends boolean> =
14
+ Omit<T, "from"> & { readonly from: readonly [...T["from"], { text: Text; cond: Cond }] };
15
+ type PushWhere<T extends UpdateTag, Text extends string, Cond extends boolean> =
16
+ Omit<T, "wheres"> & { readonly wheres: readonly [...T["wheres"], { text: Text; cond: Cond }] };
17
+
18
+ export interface UpdateQueryBuilder<S extends DatabaseSchema, T extends UpdateTag> {
19
+ // Prepend a CTE: `with <name> as [materialized ](<body>)`. The body text is
20
+ // captured at the type level so its `:params` are extracted (and typed) in
21
+ // the assembled SQL, ahead of the SET/WHERE params.
22
+ // `M` captures the `materialized` literal (default `false`) so the type-level
23
+ // RenderCte can emit `materialized ` only when the call passed `true` — keeping
24
+ // the type-computed SQL string in lock-step with the runtime string.
25
+ with<N extends string, B extends string, M extends boolean = false>(name: N, body: B, materialized?: M):
26
+ UpdateQueryBuilder<S, Omit<T, "ctes"> & { ctes: readonly [...(T extends { ctes: infer C extends readonly unknown[] } ? C : []), { name: N; body: B; materialized: M }] }>;
27
+ table<Tbl extends string, Al extends string = "">(table: Tbl, alias?: Al): UpdateQueryBuilder<S, Omit<T, "table" | "alias"> & { table: Tbl; alias: Al }>;
28
+ set<Text extends string>(assignment: Text): UpdateQueryBuilder<S, PushSet<T, Text, false>>;
29
+ setIf<Text extends string>(cond: boolean, assignment: Text): UpdateQueryBuilder<S, PushSet<T, Text, true>>;
30
+ from<Text extends string>(source: Text): UpdateQueryBuilder<S, PushFrom<T, Text, false>>;
31
+ fromIf<Text extends string>(cond: boolean, source: Text): UpdateQueryBuilder<S, PushFrom<T, Text, true>>;
32
+ where<Text extends string>(cond: Text): UpdateQueryBuilder<S, PushWhere<T, Text, false>>;
33
+ whereIf<Text extends string>(cond: boolean, clause: Text): UpdateQueryBuilder<S, PushWhere<T, Text, true>>;
34
+ returning<R extends string>(cols: R): UpdateQueryBuilder<S, Omit<T, "returning"> & { returning: R }>;
35
+ withParams(params: WriteParamsFor<T, S>): BoundWrite<S, T>;
36
+ toString(): string;
37
+ }
38
+
39
+ class UpdateImpl<S extends DatabaseSchema, T extends UpdateTag> {
40
+ constructor(private readonly st: RuntimeUpdateState) {}
41
+ private next(st: RuntimeUpdateState): any { return new UpdateImpl<S, any>(st); }
42
+ // Append a CTE; it is rendered as a leading `with ...` clause by assemble.
43
+ with(name: string, body: string, materialized = false): any {
44
+ return this.next({ ...this.st, ctes: [...(this.st.ctes ?? []), { name, body, materialized }] });
45
+ }
46
+ // Store the table and optional alias; alias flows into assembleUpdateSQL.
47
+ table(table: string, alias?: string): any { return this.next({ ...this.st, table, alias }); }
48
+ set(a: string): any { return this.next({ ...this.st, sets: [...this.st.sets, a] }); }
49
+ setIf(c: boolean, a: string): any { return c ? this.set(a) : this.next(this.st); }
50
+ from(src: string): any { return this.next({ ...this.st, froms: [...this.st.froms, src] }); }
51
+ fromIf(c: boolean, src: string): any { return c ? this.from(src) : this.next(this.st); }
52
+ where(cond: string): any { return this.next({ ...this.st, wheres: [...this.st.wheres, cond] }); }
53
+ whereIf(c: boolean, cond: string): any { return c ? this.where(cond) : this.next(this.st); }
54
+ returning(cols: string): any { return this.next({ ...this.st, returning: cols }); }
55
+ withParams(params: Record<string, DriverParamValue>): any {
56
+ return this.next({ ...this.st, namedParams: { ...this.st.namedParams, ...params } });
57
+ }
58
+ toString(): string {
59
+ const sql = assembleUpdateSQL(this.st);
60
+ assertAllProvided(sql, this.st.namedParams);
61
+ return expandScanned(sql, this.st.namedParams);
62
+ }
63
+ getParams(): ReadonlyArray<DriverParamValue> {
64
+ const sql = assembleUpdateSQL(this.st);
65
+ assertAllProvided(sql, this.st.namedParams);
66
+ return collectScanned(sql, this.st.namedParams);
67
+ }
68
+ }
69
+
70
+ export type EmptyUpdateTag = {
71
+ kind: "update"; table: ""; alias: ""; ctes: readonly []; sets: readonly []; from: readonly [];
72
+ wheres: readonly []; returning: null;
73
+ };
74
+
75
+ export function createUpdateQuery<S extends DatabaseSchema>(): UpdateQueryBuilder<S, EmptyUpdateTag> {
76
+ return new UpdateImpl<S, EmptyUpdateTag>(EMPTY_UPDATE_STATE) as unknown as UpdateQueryBuilder<S, EmptyUpdateTag>;
77
+ }
@@ -0,0 +1,52 @@
1
+ // src/builder/write-assemble.ts
2
+ import type { RuntimeInsertState, RuntimeUpdateState, RuntimeDeleteState } from "./write-state.js";
3
+
4
+ export function assembleInsertSQL(s: RuntimeInsertState): string {
5
+ // INSERT...SELECT form: emit `insert into T (cols) <select body>` instead of a
6
+ // VALUES list. Any `:params` in the SELECT body are scanned positionally by the
7
+ // shared scanner just like the rest of the statement.
8
+ if (s.fromSelect) {
9
+ let sql = `insert into ${s.table} (${s.columns}) ${s.fromSelect}`;
10
+ if (s.conflict) sql += ` on conflict ${s.conflict}`;
11
+ if (s.returning) sql += ` returning ${s.returning}`;
12
+ return sql;
13
+ }
14
+ if (s.values.length === 0) {
15
+ throw new Error("INSERT has no columns — all value fragments were conditional and excluded");
16
+ }
17
+ const cols = s.values.map(v => v.col).join(", ");
18
+ const vals = s.values.map(v => v.text).join(", ");
19
+ let sql = `insert into ${s.table} (${cols}) values (${vals})`;
20
+ if (s.conflict) sql += ` on conflict ${s.conflict}`;
21
+ if (s.returning) sql += ` returning ${s.returning}`;
22
+ return sql;
23
+ }
24
+
25
+ export function assembleUpdateSQL(s: RuntimeUpdateState): string {
26
+ if (s.sets.length === 0) {
27
+ throw new Error("UPDATE has no assignments — all SET fragments were conditional and excluded");
28
+ }
29
+ // Build a leading `with ...` clause when CTEs were supplied. It precedes the
30
+ // UPDATE so any `:params` in the CTE bodies are scanned first and get the
31
+ // lowest `$n` positions.
32
+ let prefix = "";
33
+ if (s.ctes?.length) {
34
+ prefix = "with " + s.ctes.map(c =>
35
+ `${c.name} as ${c.materialized ? "materialized " : ""}(${c.body})`).join(", ") + " ";
36
+ }
37
+ // Prepend the alias to the table name when one was supplied.
38
+ const head = s.alias ? `${s.table} ${s.alias}` : s.table;
39
+ let sql = `${prefix}update ${head} set ${s.sets.join(", ")}`;
40
+ if (s.froms.length) sql += ` from ${s.froms.join(", ")}`;
41
+ if (s.wheres.length) sql += ` where ${s.wheres.join(" and ")}`;
42
+ if (s.returning) sql += ` returning ${s.returning}`;
43
+ return sql;
44
+ }
45
+
46
+ export function assembleDeleteSQL(s: RuntimeDeleteState): string {
47
+ let sql = `delete from ${s.table}`;
48
+ if (s.usings.length) sql += ` using ${s.usings.join(", ")}`;
49
+ if (s.wheres.length) sql += ` where ${s.wheres.join(" and ")}`;
50
+ if (s.returning) sql += ` returning ${s.returning}`;
51
+ return sql;
52
+ }