@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,325 @@
1
+ // src/builder/conditional-sql.ts
2
+ //
3
+ // Conditional SQL templates with `if:condition` / `endif` comment blocks and
4
+ // `:name` parameters. Runtime + type-level processing ported verbatim from the
5
+ // predecessor package; the result/validity matcher is rewired onto the new core
6
+ // (GetReturnType / ValidateSQL).
7
+ import type { DatabaseSchema } from "../schema.js";
8
+ import type { GetReturnType, ValidateSQL } from "../index.js";
9
+ import type { QueryParamValue } from "./params.js";
10
+
11
+ // ============================================================================
12
+ // Runtime (ported from OLD conditional/runtime.ts)
13
+ // ============================================================================
14
+
15
+ export interface ConditionalSQLOptions {
16
+ /** If true, preserves conditional comment markers in output (debugging). */
17
+ preserveMarkers?: boolean;
18
+ }
19
+
20
+ export interface ConditionalSQLOutput {
21
+ /** The processed SQL string with conditions applied. */
22
+ sql: string;
23
+ /** The parameter values in order of appearance. */
24
+ params: QueryParamValue[];
25
+ }
26
+
27
+ /** Get a nested value from an object using dot notation. */
28
+ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
29
+ const keys = path.split(".");
30
+ let current: unknown = obj;
31
+
32
+ for (const key of keys) {
33
+ if (current === null || current === undefined) {
34
+ return undefined;
35
+ }
36
+ if (typeof current !== "object") {
37
+ return undefined;
38
+ }
39
+ current = (current as Record<string, unknown>)[key];
40
+ }
41
+
42
+ return current;
43
+ }
44
+
45
+ /** Process conditional blocks in a SQL template (innermost-first, iterative). */
46
+ export function processConditionalSQL(
47
+ template: string,
48
+ conditions: Record<string, unknown>,
49
+ ): string {
50
+ // Matches an if-block with no nested if inside its content (innermost first).
51
+ const pattern =
52
+ /\/\*if:(!?[\w.]+)\*\/((?:(?!\/\*if:)[\s\S])*?)\/\*endif\*\//g;
53
+
54
+ let result = template;
55
+ let hasMatches = true;
56
+
57
+ // Process iteratively to handle nested conditions.
58
+ while (hasMatches) {
59
+ hasMatches = false;
60
+
61
+ result = result.replace(
62
+ pattern,
63
+ (_, condition: string, content: string) => {
64
+ hasMatches = true;
65
+
66
+ const isNegated = condition.startsWith("!");
67
+ const key = isNegated ? condition.slice(1) : condition;
68
+ const value = getNestedValue(conditions, key);
69
+ const isTruthy = Boolean(value);
70
+
71
+ return (isNegated ? !isTruthy : isTruthy) ? content : "";
72
+ },
73
+ );
74
+ }
75
+
76
+ return result;
77
+ }
78
+
79
+ /** Convert :name placeholders to positional $n; return processed SQL + values. */
80
+ export function processParams(
81
+ sql: string,
82
+ params: Record<string, QueryParamValue>,
83
+ ): ConditionalSQLOutput {
84
+ // Find all param references in order of first appearance.
85
+ const paramRegex = /:([a-zA-Z_][a-zA-Z0-9_]*)(?![a-zA-Z0-9_])/g;
86
+ const usedParams: string[] = [];
87
+ let match;
88
+
89
+ while ((match = paramRegex.exec(sql)) !== null) {
90
+ const name = match[1];
91
+ if (name in params && !usedParams.includes(name)) {
92
+ usedParams.push(name);
93
+ }
94
+ }
95
+
96
+ // Replace each param with positional placeholder.
97
+ let processedSql = sql;
98
+ for (let i = 0; i < usedParams.length; i++) {
99
+ const name = usedParams[i];
100
+ const regex = new RegExp(`:${name}(?![a-zA-Z0-9_])`, "g");
101
+ processedSql = processedSql.replace(regex, `$${i + 1}`);
102
+ }
103
+
104
+ // Extract param values in order.
105
+ const paramValues = usedParams.map(name => params[name]);
106
+
107
+ return {
108
+ sql: processedSql,
109
+ params: paramValues,
110
+ };
111
+ }
112
+
113
+ /** Process a template with both conditions and parameters. */
114
+ export function conditionalSQL(
115
+ template: string,
116
+ conditions: Record<string, unknown>,
117
+ params: Record<string, QueryParamValue> = {},
118
+ ): ConditionalSQLOutput {
119
+ // Step 1: Process conditional blocks.
120
+ const conditionalProcessed = processConditionalSQL(template, conditions);
121
+
122
+ // Step 2: Process parameters.
123
+ return processParams(conditionalProcessed, params);
124
+ }
125
+
126
+ /** Normalize whitespace in SQL (collapse spaces, tidy commas/parens). */
127
+ export function normalizeWhitespace(sql: string): string {
128
+ return sql
129
+ .replace(/\s+/g, " ")
130
+ .replace(/\s*,\s*/g, ", ")
131
+ .replace(/\s*\(\s*/g, "(")
132
+ .replace(/\s*\)\s*/g, ")")
133
+ .trim();
134
+ }
135
+
136
+ // ============================================================================
137
+ // Type-level condition evaluation (ported from OLD conditional/types.ts)
138
+ // ============================================================================
139
+
140
+ /** Get a nested value type using a dot-notation path. */
141
+ export type GetPath<T, Path extends string> = Path extends
142
+ `${infer Key}.${infer Rest}`
143
+ ? Key extends keyof T ? GetPath<T[Key], Rest>
144
+ : undefined
145
+ : Path extends keyof T ? T[Path]
146
+ : undefined;
147
+
148
+ /** Type-level truthiness; resolves to `boolean` for non-literal wide types. */
149
+ export type IsTruthy<T> =
150
+ [T] extends [false | 0 | "" | null | undefined] ? false
151
+ : [T] extends [never] ? false
152
+ : [T] extends [boolean]
153
+ ? ([boolean] extends [T] ? boolean : true)
154
+ : [T] extends [string] ? ([string] extends [T] ? boolean : true)
155
+ : [T] extends [number] ? ([number] extends [T] ? boolean : true)
156
+ : true;
157
+
158
+ /** Evaluate a condition string against a data type (supports `!` negation). */
159
+ export type EvalCondition<Cond extends string, Data> = Cond extends
160
+ `!${infer Key}`
161
+ ? IsTruthy<GetPath<Data, Key>> extends true ? false
162
+ : IsTruthy<GetPath<Data, Key>> extends false ? true
163
+ : boolean
164
+ : IsTruthy<GetPath<Data, Cond>>;
165
+
166
+ /** Check if a string contains a specific pattern. */
167
+ type Contains<S extends string, Pattern extends string> = S extends
168
+ `${string}${Pattern}${string}` ? true : false;
169
+
170
+ /** Check if any condition in the template has an indeterminate (wide) type. */
171
+ type HasIndeterminateCondition<
172
+ Template extends string,
173
+ Data extends Record<string, unknown>,
174
+ > = Template extends `${string}/*if:${infer Cond}*/${infer Rest}`
175
+ ? EvalCondition<Cond, Data> extends boolean
176
+ ? boolean extends EvalCondition<Cond, Data> ? true
177
+ : HasIndeterminateCondition<Rest, Data>
178
+ : HasIndeterminateCondition<Rest, Data>
179
+ : false;
180
+
181
+ /** Process the innermost conditional block (inside-out). */
182
+ type ProcessInnermost<
183
+ Template extends string,
184
+ Data extends Record<string, unknown>,
185
+ > = Template extends
186
+ `${infer Before}/*if:${infer Cond}*/${infer Content}/*endif*/${infer After}`
187
+ ? Contains<Content, "/*if:"> extends true
188
+ ? `${Before}/*if:${Cond}*/${ProcessInnermost<
189
+ `${Content}/*endif*/${After}`,
190
+ Data
191
+ >}`
192
+ : EvalCondition<Cond, Data> extends true ? `${Before}${Content}${After}`
193
+ : EvalCondition<Cond, Data> extends false ? `${Before}${After}`
194
+ : string
195
+ : Template;
196
+
197
+ /** Recursively process all conditional blocks until none remain. */
198
+ export type ProcessConditionalSQL<
199
+ Template extends string,
200
+ Data extends Record<string, unknown>,
201
+ Depth extends number[] = [],
202
+ > =
203
+ HasIndeterminateCondition<Template, Data> extends true ? string
204
+ : Depth["length"] extends 20 ? Template
205
+ : Contains<Template, "/*if:"> extends true ? ProcessConditionalSQL<
206
+ ProcessInnermost<Template, Data>,
207
+ Data,
208
+ [...Depth, 0]
209
+ >
210
+ : Template;
211
+
212
+ /** Force every condition value true (maximal column set). */
213
+ export type AllConditionsTrue<Data extends Record<string, unknown>> = {
214
+ [K in keyof Data]: Data[K] extends Record<string, unknown>
215
+ ? AllConditionsTrue<Data[K]>
216
+ : true;
217
+ };
218
+
219
+ /** Force every condition value false (minimal column set). */
220
+ export type AllConditionsFalse<Data extends Record<string, unknown>> = {
221
+ [K in keyof Data]: Data[K] extends Record<string, unknown>
222
+ ? AllConditionsFalse<Data[K]>
223
+ : false;
224
+ };
225
+
226
+ /** Marker: columns from conditional SELECT clauses are `T | undefined`. */
227
+ export type ConditionalColumn<T> = T | undefined;
228
+
229
+ /** Marker: columns from conditional LEFT JOINs are `T | null | undefined`. */
230
+ export type ConditionalLeftJoinColumn<T> = T | null | undefined;
231
+
232
+ /** Extract :name parameter names from a SQL string. */
233
+ export type ExtractParamNames<
234
+ SQL extends string,
235
+ Acc extends string[] = [],
236
+ > = SQL extends `${string}:${infer Name}${infer Rest}`
237
+ ? Name extends `${infer ParamName}${" " | "," | ")" | "\n" | "\t"}`
238
+ ? ExtractParamNames<Rest, [...Acc, ParamName]>
239
+ : ExtractParamNames<Rest, [...Acc, Name]>
240
+ : Acc;
241
+
242
+ /** Validate that all required params are provided. */
243
+ export type ValidateParams<
244
+ SQL extends string,
245
+ Params extends Record<string, unknown>,
246
+ > = ExtractParamNames<SQL> extends infer Names extends string[]
247
+ ? Names[number] extends keyof Params ? true
248
+ : `Missing parameter: ${Exclude<Names[number], keyof Params>}`
249
+ : true;
250
+
251
+ // ============================================================================
252
+ // Rewired matcher + factory (onto the new core)
253
+ // ============================================================================
254
+
255
+ type Flatten<T> = { [K in keyof T]: T[K] } & {};
256
+
257
+ /**
258
+ * Result type for a conditional SQL query, rewired onto the new core.
259
+ * 1. all conditions TRUE -> full column set (GetReturnType<FullSQL>)
260
+ * 2. all conditions FALSE -> base column set (GetReturnType<BaseSQL>)
261
+ * 3. columns in full but not base -> `| undefined`
262
+ */
263
+ export type ConditionalQueryResult<
264
+ Template extends string,
265
+ Conditions extends Record<string, unknown>,
266
+ Schema extends DatabaseSchema,
267
+ > = ProcessConditionalSQL<Template, AllConditionsTrue<Conditions>> extends infer FullSQL extends string
268
+ ? ProcessConditionalSQL<Template, AllConditionsFalse<Conditions>> extends infer BaseSQL extends string
269
+ ? GetReturnType<FullSQL, Schema> extends infer Full
270
+ ? GetReturnType<BaseSQL, Schema> extends infer Base
271
+ ? MergeConditionalResults<Full, Base>
272
+ : Full
273
+ : {}
274
+ : {}
275
+ : {};
276
+
277
+ export type MergeConditionalResults<Full, Base> = Flatten<
278
+ & { [K in keyof Full as K extends keyof Base ? K : never]: Full[K] }
279
+ & { [K in keyof Full as K extends keyof Base ? never : K]: Full[K] | undefined }
280
+ >;
281
+
282
+ export type ProcessedSQL<
283
+ Template extends string,
284
+ Conditions extends Record<string, unknown>,
285
+ > = ProcessConditionalSQL<Template, Conditions>;
286
+
287
+ export type ValidateConditionalSQL<
288
+ Template extends string,
289
+ Conditions extends Record<string, unknown>,
290
+ Schema extends DatabaseSchema,
291
+ > = ProcessConditionalSQL<Template, AllConditionsTrue<Conditions>> extends infer FullSQL extends string
292
+ ? ValidateSQL<FullSQL, Schema>
293
+ : false;
294
+
295
+ export interface TypedConditionalSQLOutput<Result> extends ConditionalSQLOutput {
296
+ readonly __resultType?: Result;
297
+ }
298
+
299
+ export function createConditionalQuery<Schema extends DatabaseSchema>() {
300
+ function query<
301
+ Template extends string,
302
+ Conditions extends Record<string, unknown>,
303
+ Params extends Record<string, QueryParamValue> = {},
304
+ >(
305
+ template: Template,
306
+ conditions: Conditions,
307
+ params?: Params,
308
+ ): TypedConditionalSQLOutput<ConditionalQueryResult<Template, Conditions, Schema>> {
309
+ const result = conditionalSQL(template, conditions, params ?? {});
310
+ return result as TypedConditionalSQLOutput<
311
+ ConditionalQueryResult<Template, Conditions, Schema>
312
+ >;
313
+ }
314
+ return query;
315
+ }
316
+
317
+ export function withConditions<StaticConditions extends Record<string, unknown>>(
318
+ queryFn: ReturnType<typeof createConditionalQuery>,
319
+ ) {
320
+ return <Template extends string, Params extends Record<string, QueryParamValue> = {}>(
321
+ template: Template,
322
+ conditions: StaticConditions,
323
+ params?: Params,
324
+ ) => queryFn(template, conditions, params);
325
+ }
@@ -0,0 +1,281 @@
1
+ // src/builder/db.ts
2
+ import type { DatabaseSchema } from "../schema.js";
3
+ import type { GetReturnType, ValidateSQL } from "../index.js";
4
+ import type {
5
+ ValidateFromPart,
6
+ ValidateJoinPart,
7
+ } from "../index.js";
8
+ import type {
9
+ ValidateClausePartScoped,
10
+ ValidateSelectIdentifiersScoped,
11
+ } from "../partial.js";
12
+ import type { SelectQueryBuilder } from "./select.js";
13
+ import type { BuilderReturnType, BuilderSQL } from "./return-type.js";
14
+ import type { Frag, SelFrag, SqlTag } from "./sql-tag.js";
15
+ import type { NormalizeQuery } from "../parsing.js";
16
+ import type { TablesInQuery, AliasesInQuery } from "../tables.js";
17
+
18
+ type Prettify<T> = { [K in keyof T]: T[K] } & {};
19
+
20
+ /** String-query validity (core).
21
+ *
22
+ * Size-gated, mirroring `ValidQueryBuilder` below (the gate it gained in the
23
+ * "size-gated whole-query validation" change). The whole-query `ValidateSQL`
24
+ * pass only runs when `Q` is small enough to resolve within the TS
25
+ * instantiation budget.
26
+ *
27
+ * Why the gate is needed on the raw-string path too: `ValidQuery` is used as a
28
+ * PARAMETER type by api.ts's raw-string `typedSelect` overload
29
+ * (`query: ValidQuery<Q, MainDatabase>`), where TS must INFER `Q` from the
30
+ * argument *through* this type. Inferring `Q` while evaluating
31
+ * `ValidateSQL<Q, Schema>` on a heavy query against a large schema (e.g.
32
+ * `MainDatabase`) exhausts the budget — TS abandons inferring `Q`, it collapses
33
+ * to `string`, and `ValidateSQL<string>` then rejects the (valid) argument.
34
+ * Direct `ValidateSQL<concreteLiteral, Schema>` does NOT blow (Q is already
35
+ * known), which is why fn-plain's direct-form mirror passes while the real
36
+ * overload call fails.
37
+ *
38
+ * For an over-gate query we pass `Q` through unvalidated — `Q` stays inferrable
39
+ * and row inference via `GetReturnType<Q>` is unaffected; only the dev-time
40
+ * whole-query validation is skipped. Same trade-off `ValidQueryBuilder` makes
41
+ * for large builder queries. Small queries are still fully validated. */
42
+ export type ValidQuery<Q extends string, Schema extends DatabaseSchema> =
43
+ BuilderSqlSmall<Q> extends true
44
+ ? ValidateSQL<Q, Schema> extends infer V
45
+ ? V extends true
46
+ ? Q
47
+ // `ValidateSQL` reports a boolean `false`, not a string message,
48
+ // so `${V & string}` would interpolate `never` and collapse the
49
+ // whole template to `never` (silently swallowing the rejection).
50
+ // Surface a real `[SQL Error] …` literal instead; the
51
+ // `V extends string` arm future-proofs for descriptive verdicts.
52
+ : V extends string
53
+ ? `[SQL Error] ${V}`
54
+ : `[SQL Error] invalid query: ${Q}`
55
+ : never
56
+ : Q;
57
+
58
+ // --- per-fragment validation over LITERAL fragments only ---
59
+ // A fragment whose text is non-literal `string` is skipped (→ never error).
60
+ // The partial validators return `true` (valid OR out-of-scope/skipped) or a
61
+ // boolean `false` (a reference resolvable WITHIN the fragment that does not
62
+ // exist). Treat any non-`true` verdict as an error: a `string` verdict is used
63
+ // as-is (future-proofing for descriptive messages), and a `false` verdict
64
+ // becomes a labeled "invalid <clause> fragment" error carrying the text.
65
+ type FragErr<Verdict, Label extends string, Text extends string> =
66
+ Verdict extends true ? never
67
+ : Verdict extends string ? Verdict
68
+ : `invalid ${Label} fragment: ${Text}`;
69
+
70
+ // Join all literal select-fragment texts into one comma list; bail to `string`
71
+ // if any fragment text is non-literal (the dispatch already allow-unknowns that).
72
+ type SelectListText<List extends readonly SelFrag[], Acc extends string = ""> =
73
+ List extends readonly [infer H extends SelFrag, ...infer R extends readonly SelFrag[]]
74
+ ? string extends H["text"]
75
+ ? string
76
+ : SelectListText<R, Acc extends "" ? H["text"] : `${Acc}, ${H["text"]}`>
77
+ : Acc;
78
+
79
+ type SelectErrors<List extends readonly SelFrag[], Tables extends string, Aliases extends string, S extends DatabaseSchema> =
80
+ SelectListText<List> extends infer Txt extends string
81
+ ? string extends Txt
82
+ ? never
83
+ : FragErr<ValidateSelectIdentifiersScoped<Txt, Tables, Aliases, S>, "SELECT", Txt>
84
+ : never;
85
+
86
+ type FromError<From extends string | null, S extends DatabaseSchema> =
87
+ From extends null ? never
88
+ : string extends (From & string) ? never
89
+ : FragErr<ValidateFromPart<From & string, S>, "FROM", From & string>;
90
+
91
+ type JoinErrors<List extends readonly Frag[], S extends DatabaseSchema> =
92
+ List extends readonly [infer H extends Frag, ...infer R extends readonly Frag[]]
93
+ ? (string extends H["text"] ? never : FragErr<ValidateJoinPart<H["text"], S>, "JOIN", H["text"]>)
94
+ | JoinErrors<R, S>
95
+ : never;
96
+
97
+ type WhereErrors<List extends readonly Frag[], Tables extends string, Aliases extends string, S extends DatabaseSchema> =
98
+ List extends readonly [infer H extends Frag, ...infer R extends readonly Frag[]]
99
+ ? (string extends H["text"] ? never : FragErr<ValidateClausePartScoped<H["text"], Tables, Aliases, S>, "WHERE", H["text"]>)
100
+ | WhereErrors<R, Tables, Aliases, S>
101
+ : never;
102
+
103
+ type GroupErrors<List extends readonly Frag[], Tables extends string, Aliases extends string, S extends DatabaseSchema> =
104
+ List extends readonly [infer H extends Frag, ...infer R extends readonly Frag[]]
105
+ ? (string extends H["text"] ? never : FragErr<ValidateClausePartScoped<H["text"], Tables, Aliases, S>, "GROUP BY", H["text"]>)
106
+ | GroupErrors<R, Tables, Aliases, S>
107
+ : never;
108
+
109
+ type HavingErrors<List extends readonly Frag[], Tables extends string, Aliases extends string, S extends DatabaseSchema> =
110
+ List extends readonly [infer H extends Frag, ...infer R extends readonly Frag[]]
111
+ ? (string extends H["text"] ? never : FragErr<ValidateClausePartScoped<H["text"], Tables, Aliases, S>, "HAVING", H["text"]>)
112
+ | HavingErrors<R, Tables, Aliases, S>
113
+ : never;
114
+
115
+ type OrderErrors<List extends readonly Frag[], Tables extends string, Aliases extends string, S extends DatabaseSchema> =
116
+ List extends readonly [infer H extends Frag, ...infer R extends readonly Frag[]]
117
+ ? (string extends H["text"] ? never : FragErr<ValidateClausePartScoped<H["text"], Tables, Aliases, S>, "ORDER BY", H["text"]>)
118
+ | OrderErrors<R, Tables, Aliases, S>
119
+ : never;
120
+
121
+ // Concatenate the join fragment texts into one string. Each fragment is
122
+ // prefixed with a ` join ` keyword so the whole-query table/alias collectors
123
+ // recognise it as a JOIN source. Builder join texts MAY omit the leading keyword
124
+ // (e.g. "Other o2 on o2.id = o.id") — without the prefix the collector reads the
125
+ // join table as a stray token and never records its alias. When the text ALREADY
126
+ // carries a keyword (e.g. "left join Other o2 on ..."), the extra ` join ` is a
127
+ // harmless structural sentinel: the spurious `join left`/`join join` cycle
128
+ // resolves no real table (→ no-op) and the genuine keyword still collects the
129
+ // table/alias correctly.
130
+ type JoinSourceText<List extends readonly Frag[]> =
131
+ List extends readonly [infer H extends Frag, ...infer R extends readonly Frag[]]
132
+ ? ` join ${H["text"]}${JoinSourceText<R>}`
133
+ : "";
134
+
135
+ // Build a from-clause-shaped string from the FROM text + all JOIN texts, so the
136
+ // existing whole-query collectors can read the full table scope. `null` FROM
137
+ // (or a non-literal `string` FROM) yields "" → empty scope (everything skipped).
138
+ type ScopeSourceText<Sql extends SqlTag> =
139
+ Sql["from"] extends null
140
+ ? ""
141
+ : string extends (Sql["from"] & string)
142
+ ? ""
143
+ : `from ${Sql["from"] & string}${JoinSourceText<Sql["joins"]>}`;
144
+
145
+ // Table keys in scope (union of `schema.table`), via the depth-safe collector.
146
+ export type ScopeTables<Sql extends SqlTag, S extends DatabaseSchema> =
147
+ ScopeSourceText<Sql> extends infer N extends string
148
+ ? N extends "" ? never : TablesInQuery<NormalizeQuery<N>, S>
149
+ : never;
150
+
151
+ // Alias→key entries in scope, via the depth-safe collector.
152
+ export type ScopeAliases<Sql extends SqlTag, S extends DatabaseSchema> =
153
+ ScopeSourceText<Sql> extends infer N extends string
154
+ ? N extends "" ? never : AliasesInQuery<NormalizeQuery<N>, S>
155
+ : never;
156
+
157
+ // One-time budget tuple of length N (a constant; instantiated once per N).
158
+ type MkBudget<N extends number, Acc extends any[] = []> =
159
+ Acc["length"] extends N ? Acc : MkBudget<N, [any, ...Acc]>;
160
+
161
+ // Char-length threshold below which the precise whole-query ValidateSQL is safe
162
+ // to run. Tunable: raise toward where the whole-query pass starts blowing,
163
+ // lower if a query under it still blows.
164
+ type SqlSizeThreshold = MkBudget<600>;
165
+
166
+ // Walk S against the budget. As soon as chars remain with the budget exhausted,
167
+ // the query is "large" (false). Cost is bounded by the threshold (early-exit),
168
+ // not by the query length.
169
+ type LenWithin<S extends string, Budget extends any[]> =
170
+ S extends `${infer _C}${infer Tail}`
171
+ ? Budget extends [any, ...infer Rest extends any[]]
172
+ ? LenWithin<Tail, Rest>
173
+ : false
174
+ : true;
175
+
176
+ // True = small enough for the precise whole-query pass.
177
+ export type BuilderSqlSmall<SQL extends string> =
178
+ string extends SQL ? false : LenWithin<SQL, SqlSizeThreshold>;
179
+
180
+ /** Per-fragment errors over the literal fragments of B's Sql tag. */
181
+ export type FragmentErrors<B, Schema extends DatabaseSchema> =
182
+ B extends SelectQueryBuilder<Schema, infer Sql extends SqlTag>
183
+ ? ScopeTables<Sql, Schema> extends infer Tbls extends string
184
+ ? ScopeAliases<Sql, Schema> extends infer Als extends string
185
+ ? (
186
+ | SelectErrors<Sql["selects"], Tbls, Als, Schema>
187
+ | FromError<Sql["from"], Schema>
188
+ | JoinErrors<Sql["joins"], Schema>
189
+ | WhereErrors<Sql["wheres"], Tbls, Als, Schema>
190
+ | GroupErrors<Sql["groupBys"], Tbls, Als, Schema>
191
+ | HavingErrors<Sql["havings"], Tbls, Als, Schema>
192
+ | OrderErrors<Sql["orderBys"], Tbls, Als, Schema>
193
+ ) extends infer E
194
+ ? [E] extends [never] ? [] : (E & string)[]
195
+ : []
196
+ : []
197
+ : []
198
+ : [];
199
+
200
+ /**
201
+ * Builder validity: per-fragment literal errors first; else whole-query check
202
+ * with allow-unknown when the assembled SQL is non-literal `string`.
203
+ */
204
+ export type ValidQueryBuilder<Schema extends DatabaseSchema, B extends SelectQueryBuilder<Schema, any>> =
205
+ FragmentErrors<B, Schema> extends []
206
+ ? BuilderSQL<B> extends infer SQL extends string
207
+ ? string extends SQL
208
+ ? B // some fragment text non-literal → allow, untyped
209
+ : BuilderSqlSmall<SQL> extends true
210
+ ? ValidateSQL<SQL, Schema> extends infer V
211
+ ? V extends true
212
+ ? B
213
+ // Boolean `false` (not a string) ⇒ `Extract<…, string>`
214
+ // is `never`, which would collapse the template to
215
+ // `never` and swallow the rejection. Emit a real
216
+ // `[SQL Error] …` literal; `V extends string`
217
+ // future-proofs for descriptive verdicts.
218
+ : V extends string
219
+ ? `[SQL Error] ${V}`
220
+ : `[SQL Error] invalid query: ${SQL}`
221
+ : B
222
+ : B // large query: rely on scope-aware FragmentErrors (depth-safe)
223
+ : B
224
+ : `[SQL Error] ${FragmentErrors<B, Schema>[number]}`;
225
+
226
+ export type SelectResult<SQL extends string, Schema extends DatabaseSchema> =
227
+ Prettify<GetReturnType<SQL, Schema>>;
228
+ export type SelectResultArray<SQL extends string, Schema extends DatabaseSchema> =
229
+ Prettify<GetReturnType<SQL, Schema>>[];
230
+
231
+ type InvalidOverrideKeys<Result, Overrides> = Exclude<keyof Overrides, keyof Result>;
232
+
233
+ export type MergeOverrides<Result, Overrides> = keyof Overrides extends never
234
+ ? Result
235
+ : InvalidOverrideKeys<Result, Overrides> extends never
236
+ ? Prettify<Omit<Result, keyof Overrides> & Overrides>
237
+ : {
238
+ __error: true;
239
+ message: `Override contains keys not in result type: ${InvalidOverrideKeys<Result, Overrides> & string}`;
240
+ };
241
+
242
+ export type SelectBuilderResult<B extends SelectQueryBuilder<any, any>> =
243
+ Prettify<BuilderReturnType<B>>;
244
+ export type SelectBuilderResultArray<B extends SelectQueryBuilder<any, any>> =
245
+ SelectBuilderResult<B>[];
246
+
247
+ export type QueryHandler = (query: string, params?: unknown[]) => unknown;
248
+
249
+ export type IsValidSelect<SQL extends string, Schema extends DatabaseSchema> =
250
+ ValidateSQL<SQL, Schema> extends true ? true : false;
251
+
252
+ export function createSelectFn<
253
+ Schema extends DatabaseSchema,
254
+ Overrides extends Record<string, unknown> = {},
255
+ >(handler: QueryHandler) {
256
+ // String query overload
257
+ function select<Q extends string>(
258
+ query: ValidQuery<Q, Schema>,
259
+ params?: unknown[],
260
+ ): Promise<MergeOverrides<SelectResultArray<Q, Schema>[number], Overrides>[]>;
261
+
262
+ // Typed builder overload
263
+ function select<B extends SelectQueryBuilder<Schema, any>>(
264
+ query: ValidQueryBuilder<Schema, B>,
265
+ params?: unknown[],
266
+ ): Promise<MergeOverrides<SelectBuilderResult<B>, Overrides>[]>;
267
+
268
+ function select(
269
+ query: ValidQuery<string, Schema> | SelectQueryBuilder<Schema, any>,
270
+ params?: unknown[],
271
+ ) {
272
+ if (typeof query === "string") {
273
+ return handler(query, params) as Promise<any>;
274
+ }
275
+ const sql = query.toString();
276
+ const finalParams = params ?? [...query.getParams()];
277
+ return handler(sql, finalParams) as Promise<any>;
278
+ }
279
+
280
+ return select;
281
+ }
@@ -0,0 +1,57 @@
1
+ // src/builder/delete.ts
2
+ import type { DatabaseSchema } from "../schema.js";
3
+ import { assembleDeleteSQL } from "./write-assemble.js";
4
+ import { EMPTY_DELETE_STATE, type RuntimeDeleteState } from "./write-state.js";
5
+ import {
6
+ assertAllProvided, collectScanned, expandScanned, type DriverParamValue,
7
+ } from "./scanner.js";
8
+ import type { DeleteTag, WriteParamsFor } from "./write-tag.js";
9
+ import type { BoundWrite } from "./insert.js";
10
+
11
+ type PushUsing<T extends DeleteTag, Text extends string, Cond extends boolean> =
12
+ Omit<T, "using"> & { readonly using: readonly [...T["using"], { text: Text; cond: Cond }] };
13
+ type PushWhere<T extends DeleteTag, Text extends string, Cond extends boolean> =
14
+ Omit<T, "wheres"> & { readonly wheres: readonly [...T["wheres"], { text: Text; cond: Cond }] };
15
+
16
+ export interface DeleteQueryBuilder<S extends DatabaseSchema, T extends DeleteTag> {
17
+ from<Tbl extends string>(table: Tbl): DeleteQueryBuilder<S, Omit<T, "table"> & { table: Tbl }>;
18
+ using<Text extends string>(source: Text): DeleteQueryBuilder<S, PushUsing<T, Text, false>>;
19
+ usingIf<Text extends string>(cond: boolean, source: Text): DeleteQueryBuilder<S, PushUsing<T, Text, true>>;
20
+ where<Text extends string>(cond: Text): DeleteQueryBuilder<S, PushWhere<T, Text, false>>;
21
+ whereIf<Text extends string>(cond: boolean, clause: Text): DeleteQueryBuilder<S, PushWhere<T, Text, true>>;
22
+ returning<R extends string>(cols: R): DeleteQueryBuilder<S, Omit<T, "returning"> & { returning: R }>;
23
+ withParams(params: WriteParamsFor<T, S>): BoundWrite<S, T>;
24
+ toString(): string;
25
+ }
26
+
27
+ class DeleteImpl<S extends DatabaseSchema, T extends DeleteTag> {
28
+ constructor(private readonly st: RuntimeDeleteState) {}
29
+ private next(st: RuntimeDeleteState): any { return new DeleteImpl<S, any>(st); }
30
+ from(table: string): any { return this.next({ ...this.st, table }); }
31
+ using(src: string): any { return this.next({ ...this.st, usings: [...this.st.usings, src] }); }
32
+ usingIf(c: boolean, src: string): any { return c ? this.using(src) : this.next(this.st); }
33
+ where(cond: string): any { return this.next({ ...this.st, wheres: [...this.st.wheres, cond] }); }
34
+ whereIf(c: boolean, cond: string): any { return c ? this.where(cond) : this.next(this.st); }
35
+ returning(cols: string): any { return this.next({ ...this.st, returning: cols }); }
36
+ withParams(params: Record<string, DriverParamValue>): any {
37
+ return this.next({ ...this.st, namedParams: { ...this.st.namedParams, ...params } });
38
+ }
39
+ toString(): string {
40
+ const sql = assembleDeleteSQL(this.st);
41
+ assertAllProvided(sql, this.st.namedParams);
42
+ return expandScanned(sql, this.st.namedParams);
43
+ }
44
+ getParams(): ReadonlyArray<DriverParamValue> {
45
+ const sql = assembleDeleteSQL(this.st);
46
+ assertAllProvided(sql, this.st.namedParams);
47
+ return collectScanned(sql, this.st.namedParams);
48
+ }
49
+ }
50
+
51
+ export type EmptyDeleteTag = {
52
+ kind: "delete"; table: ""; using: readonly []; wheres: readonly []; returning: null;
53
+ };
54
+
55
+ export function createDeleteQuery<S extends DatabaseSchema>(): DeleteQueryBuilder<S, EmptyDeleteTag> {
56
+ return new DeleteImpl<S, EmptyDeleteTag>(EMPTY_DELETE_STATE) as unknown as DeleteQueryBuilder<S, EmptyDeleteTag>;
57
+ }