@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/index.ts ADDED
@@ -0,0 +1,81 @@
1
+ // Pure type-level SQL validation and result inference.
2
+ // The parser is intentionally shallow and uses safe fallbacks to avoid TS depth limits.
3
+
4
+ export type { DatabaseSchema } from "./schema.js";
5
+
6
+ import type { DatabaseSchema } from "./schema.js";
7
+ import type { NormalizeQuery } from "./parsing.js";
8
+ import type { DeleteTargetTable, InsertTargetTable, UpdateTargetTable } from "./tables.js";
9
+ import type { ValidateSQLNormalized, GetReturnTypeNormalized, QueryKind } from "./validation.js";
10
+ import type { RowTypeForTable } from "./schema.js";
11
+
12
+ // -----------------------------
13
+ // Public API
14
+ // -----------------------------
15
+
16
+ export type ValidateSQL<Query extends string, Schema extends DatabaseSchema> =
17
+ string extends Query
18
+ ? false
19
+ : Query extends any
20
+ // Distribute over a union of query strings (e.g. a column drawn from
21
+ // a literal union) so each branch is validated independently; the
22
+ // result is the union of per-branch booleans.
23
+ ? NormalizeQuery<Query> extends infer N extends string
24
+ ? ValidateSQLNormalized<N, Schema>
25
+ : false
26
+ : false;
27
+
28
+ export type GetReturnType<Query extends string, Schema extends DatabaseSchema> =
29
+ string extends Query
30
+ ? {}
31
+ : NormalizeQuery<Query> extends infer N extends string
32
+ ? QueryKind<N> extends "unknown"
33
+ ? {}
34
+ : GetReturnTypeNormalized<N, Schema>
35
+ : {};
36
+
37
+ // Compatibility aliases for adapted external tests
38
+ export type QueryResult<Query extends string, Schema extends DatabaseSchema> =
39
+ GetReturnType<Query, Schema>;
40
+ export type ValidateSelectSQL<Query extends string, Schema extends DatabaseSchema> =
41
+ ValidateSQL<Query, Schema>;
42
+ export type ValidateInsertSQL<Query extends string, Schema extends DatabaseSchema> =
43
+ ValidateSQL<Query, Schema>;
44
+ export type ValidateUpdateSQL<Query extends string, Schema extends DatabaseSchema> =
45
+ ValidateSQL<Query, Schema>;
46
+ export type ValidateDeleteSQL<Query extends string, Schema extends DatabaseSchema> =
47
+ ValidateSQL<Query, Schema>;
48
+
49
+ export type IsValidInsert<Query extends string, Schema extends DatabaseSchema> =
50
+ ValidateSQL<Query, Schema>;
51
+ export type IsValidUpdate<Query extends string, Schema extends DatabaseSchema> =
52
+ ValidateSQL<Query, Schema>;
53
+ export type IsValidDelete<Query extends string, Schema extends DatabaseSchema> =
54
+ ValidateSQL<Query, Schema>;
55
+
56
+ export type GetInsertTableColumns<Query extends string, Schema extends DatabaseSchema> =
57
+ InsertTargetTable<NormalizeQuery<Query>, Schema> extends infer TableKey extends string
58
+ ? RowTypeForTable<TableKey, Schema>
59
+ : never;
60
+ export type GetUpdateTableColumns<Query extends string, Schema extends DatabaseSchema> =
61
+ UpdateTargetTable<NormalizeQuery<Query>, Schema> extends infer TableKey extends string
62
+ ? RowTypeForTable<TableKey, Schema>
63
+ : never;
64
+ export type GetDeleteTableColumns<Query extends string, Schema extends DatabaseSchema> =
65
+ DeleteTargetTable<NormalizeQuery<Query>, Schema> extends infer TableKey extends string
66
+ ? RowTypeForTable<TableKey, Schema>
67
+ : never;
68
+
69
+ // Partial (fragment) validation entry points — for the query builder.
70
+ export type {
71
+ ValidateFromPart,
72
+ ValidateJoinPart,
73
+ ValidateSelectPart,
74
+ ValidateWherePart,
75
+ ValidateHavingPart,
76
+ ValidateGroupByPart,
77
+ ValidateOrderByPart
78
+ } from "./partial.js";
79
+
80
+ // Runtime query builder (values + kept types).
81
+ export * from "./builder/index.js";
@@ -0,0 +1,260 @@
1
+ // SELECT/RETURNING/clause + column-list extraction.
2
+ import type { CleanIdent, FilterEmpty, HasSpecial, MapClean, Split, Trim, TrimLeft } from "./string-utils.js";
3
+ import type { ExtractBefore, ExtractBeforeFromTopLevel, SplitCommaSimple, SplitTopLevel, StripDistinct } from "./split.js";
4
+ // Select / returning list parsing
5
+
6
+ export type ExtractSelectList<N extends string> =
7
+ N extends `${infer _}select ${infer After}`
8
+ ? StripDistinct<ExtractBeforeFromTopLevel<After>>
9
+ : N extends `${infer _}with ${string} select ${infer After}`
10
+ ? StripDistinct<ExtractBeforeFromTopLevel<After>>
11
+ : "";
12
+
13
+ // The projection list after the REAL ` returning ` clause. A ` returning `
14
+ // substring can also appear inside a string literal — either assigned earlier in
15
+ // the statement (`SET title = ' returning x'`, round-12 R1) OR returned by the
16
+ // clause itself (`RETURNING ' returning bogus_col' AS marker`, round-13 R1). The
17
+ // real clause is the FIRST ` returning ` at TOP LEVEL (outside single-quoted
18
+ // literals / double-quoted identifiers); a quote-aware char-walk skips any
19
+ // ` returning ` sitting inside quotes. Neither "first raw" nor "last raw" is
20
+ // correct in general — the literal can sit on either side of the real clause.
21
+ // Step-bounded (mirrors `HasReturningQuoteAware`) so it never blows the budget.
22
+ export type ExtractReturningList<N extends string> =
23
+ FirstTopLevelReturningTail<N>;
24
+
25
+ export type FirstTopLevelReturningTail<
26
+ S extends string,
27
+ InString extends boolean = false,
28
+ InDString extends boolean = false,
29
+ Steps extends any[] = []
30
+ > = string extends S
31
+ ? ""
32
+ : Steps["length"] extends 1200
33
+ ? S extends `${string} returning ${infer After}` ? After : ""
34
+ : InString extends true
35
+ ? S extends `${infer C}${infer Rest}`
36
+ ? FirstTopLevelReturningTail<Rest, C extends "'" ? false : true, InDString, [any, ...Steps]>
37
+ : ""
38
+ : InDString extends true
39
+ ? S extends `${infer C}${infer Rest}`
40
+ ? FirstTopLevelReturningTail<Rest, InString, C extends `"` ? false : true, [any, ...Steps]>
41
+ : ""
42
+ : S extends ` returning ${infer After}`
43
+ ? After
44
+ : S extends `${infer C}${infer Rest}`
45
+ ? FirstTopLevelReturningTail<Rest, C extends "'" ? true : false, C extends `"` ? true : false, [any, ...Steps]>
46
+ : "";
47
+
48
+ // Given a string whose first non-skipped char is `(`, consume the first
49
+ // balanced parenthesised group (quote-aware) and return its inner content plus
50
+ // whatever follows the matching `)`. Naive template matching can't do this
51
+ // because `${infer Body})` is lazy and stops at the first `)` (e.g. inside
52
+ // `count(*)`).
53
+ //
54
+ // A single char-walk can only run ~1000 iterations before TS aborts with TS2589;
55
+ // a fixed cap (the previous 400) silently truncates long CTE/subquery bodies and
56
+ // derails the caller (e.g. multi-CTE OUTER-query extraction reads garbage). So
57
+ // the walk is split into a bounded worker that YIELDS its state (`{ __c: [...] }`)
58
+ // every CHUNK steps and a driver that re-invokes it with a fresh step counter —
59
+ // resetting TS's per-chain tail-recursion count, so arbitrarily long groups split
60
+ // losslessly. Mirrors the `SplitTopLevel` worker/driver above.
61
+ type SplitBalancedParenWorker<
62
+ S extends string,
63
+ Depth extends any[] = [],
64
+ Acc extends string = "",
65
+ InString extends boolean = false,
66
+ Steps extends any[] = []
67
+ > = Steps["length"] extends 350
68
+ ? { __c: [S, Depth, Acc, InString] }
69
+ : S extends `${infer C}${infer Rest}`
70
+ ? C extends "'"
71
+ ? SplitBalancedParenWorker<Rest, Depth, `${Acc}${C}`, InString extends true ? false : true, [any, ...Steps]>
72
+ : InString extends true
73
+ ? SplitBalancedParenWorker<Rest, Depth, `${Acc}${C}`, InString, [any, ...Steps]>
74
+ : C extends "("
75
+ ? Depth["length"] extends 0
76
+ ? SplitBalancedParenWorker<Rest, [any], Acc, InString, [any, ...Steps]>
77
+ : SplitBalancedParenWorker<Rest, [any, ...Depth], `${Acc}${C}`, InString, [any, ...Steps]>
78
+ : C extends ")"
79
+ ? Depth extends [any, ...infer D extends any[]]
80
+ ? D["length"] extends 0
81
+ ? { inner: Acc; rest: Rest }
82
+ : SplitBalancedParenWorker<Rest, D, `${Acc}${C}`, InString, [any, ...Steps]>
83
+ : { inner: Acc; rest: Rest }
84
+ : SplitBalancedParenWorker<Rest, Depth, `${Acc}${C}`, InString, [any, ...Steps]>
85
+ : { inner: Acc; rest: "" };
86
+
87
+ export type SplitBalancedParen<S extends string> =
88
+ SplitBalancedParenDrive<SplitBalancedParenWorker<S>>;
89
+
90
+ type SplitBalancedParenDrive<R> =
91
+ R extends { __c: [infer S extends string, infer Depth extends any[], infer Acc extends string, infer InString extends boolean] }
92
+ ? SplitBalancedParenDrive<SplitBalancedParenWorker<S, Depth, Acc, InString, []>>
93
+ : R;
94
+
95
+ // Remove every SUBQUERY parenthesised group (a balanced `(...)` whose body is a
96
+ // `select ...`) from a query, leaving a single space in its place. Function-call
97
+ // and grouping parens (`coalesce(...)`, `sum(...)`, `(a + b)`) are kept verbatim
98
+ // so a function name never degrades into a dangling bare identifier. Used by
99
+ // scope validation to recover the OUTER relations and refs of a query with its
100
+ // subquery bodies excised. Bounded against runaway (≤30 groups); on overflow the
101
+ // remainder is appended as-is.
102
+ export type StripSubqueries<
103
+ S extends string,
104
+ Acc extends string = "",
105
+ Steps extends any[] = []
106
+ > = Steps["length"] extends 30
107
+ ? `${Acc}${S}`
108
+ : S extends `${infer Before}(${infer AfterOpen}`
109
+ ? SplitBalancedParen<`(${AfterOpen}`> extends { inner: infer Inner extends string; rest: infer Rest extends string }
110
+ ? Trim<Inner> extends `select ${string}`
111
+ ? StripSubqueries<Rest, `${Acc}${Before} `, [any, ...Steps]>
112
+ : StripSubqueries<Rest, `${Acc}${Before}(${Inner})`, [any, ...Steps]>
113
+ : `${Acc}${S}`
114
+ : `${Acc}${S}`;
115
+
116
+ // Collect the inner contents of every `<marker>(...)` group (quote/paren-aware),
117
+ // space-joined. Used to surface columns sitting inside `over (...)` / `filter
118
+ // (...)` clauses, which live in the SELECT list (before the top-level FROM) and
119
+ // would otherwise escape column validation entirely. Bounded against runaway.
120
+ export type ExtractCallParenBodies<
121
+ S extends string,
122
+ Marker extends string,
123
+ Acc extends string = "",
124
+ Steps extends any[] = []
125
+ > = Steps["length"] extends 12
126
+ ? Acc
127
+ : S extends `${infer _Head}${Marker}${infer AfterOpen}`
128
+ ? SplitBalancedParen<`(${AfterOpen}`> extends { inner: infer Inner extends string; rest: infer Rest extends string }
129
+ ? ExtractCallParenBodies<Rest, Marker, `${Acc} ${Inner}`, [any, ...Steps]>
130
+ : Acc
131
+ : Acc;
132
+
133
+ // The predicate after the LAST ` where ` (drop any trailing RETURNING). In an
134
+ // UPDATE the giant SET expression — including any subquery WHEREs — comes BEFORE
135
+ // the statement's own WHERE, so the text after the last ` where ` is the
136
+ // top-level predicate. Implemented by discarding each `${head} where ` prefix
137
+ // (no string rebuild, unlike SplitLast — that concatenation is what blew up
138
+ // TS2589/memory on the largest correlated updates). Capped so a pathological
139
+ // number of nested WHEREs can't run away; on bail the caller's subquery guard
140
+ // handles the (paren-bearing) remainder.
141
+ export type ExtractLastWhere<N extends string> =
142
+ ExtractBefore<Trim<LastWhereTail<N>>, " returning ">;
143
+
144
+ type LastWhereTail<S extends string, Steps extends any[] = []> =
145
+ Steps["length"] extends 16
146
+ ? S
147
+ : S extends `${infer _Head} where ${infer Rest}`
148
+ ? LastWhereTail<Rest, [any, ...Steps]>
149
+ : S;
150
+
151
+ // Everything after the top-level FROM (paren/quote-aware): the FROM clause and
152
+ // any trailing WHERE/GROUP/ORDER/etc. Used to detect derived tables.
153
+ export type ExtractFromClause<N extends string> =
154
+ N extends `${infer _}select ${infer After}`
155
+ ? ExtractBeforeFromTopLevel<After> extends infer SL extends string
156
+ ? After extends `${SL} from ${infer FromRest}`
157
+ ? FromRest
158
+ : ""
159
+ : ""
160
+ : "";
161
+
162
+ // Select list helpers
163
+
164
+ export type SplitSelectList<S extends string> =
165
+ S extends "" ? [] : SplitTopLevel<S>;
166
+
167
+ // Insert / update parsing
168
+
169
+ export type ExtractInsertColumns<N extends string> =
170
+ N extends `${infer _}insert into ${infer Rest}`
171
+ ? Rest extends `${infer _Table}(${infer Cols})${infer _}`
172
+ ? SplitCommaSimple<Cols>
173
+ : Rest extends `${infer _Table} (${infer Cols})${infer _}`
174
+ ? SplitCommaSimple<Cols>
175
+ : []
176
+ : [];
177
+
178
+ export type ExtractConflictColumns<N extends string> =
179
+ N extends `${string} on conflict ${infer Rest}`
180
+ ? Rest extends `(${infer Cols})${string}`
181
+ ? SplitCommaSimple<Cols>
182
+ : Rest extends ` (${infer Cols})${string}`
183
+ ? SplitCommaSimple<Cols>
184
+ : []
185
+ : [];
186
+
187
+ export type ExtractUpdateSetColumns<N extends string> =
188
+ N extends `${infer _} set ${infer Rest}`
189
+ ? ExtractBefore<Rest, " where "> extends infer Block1 extends string
190
+ ? ExtractBefore<Block1, " returning "> extends infer Block2 extends string
191
+ ? SplitAssignments<Block2>
192
+ : []
193
+ : []
194
+ : [];
195
+
196
+ export type ExtractConflictUpdateSetColumns<N extends string> =
197
+ N extends `${string} do update set ${infer Rest}`
198
+ ? ExtractBefore<Rest, " where "> extends infer Block1 extends string
199
+ ? ExtractBefore<Block1, " returning "> extends infer Block2 extends string
200
+ ? SplitAssignments<Block2>
201
+ : []
202
+ : []
203
+ : [];
204
+
205
+ // The RHS of a `DO UPDATE SET col = excluded.<x>` assignment references the
206
+ // `excluded` pseudo-row, which mirrors the INSERT target table's columns. Collect
207
+ // the referenced `<x>` names from each simple `left = excluded.<x>` assignment so
208
+ // they can be existence-checked against the target table. Only the direct
209
+ // `excluded.<col>` form is captured — a column wrapped in a larger expression
210
+ // (`coalesce(excluded.x, ...)`) is left to the lenient path rather than risk a
211
+ // false reject.
212
+ export type ExtractConflictUpdateExcludedCols<N extends string> =
213
+ N extends `${string} do update set ${infer Rest}`
214
+ ? ExtractBefore<Rest, " where "> extends infer Block1 extends string
215
+ ? ExtractBefore<Block1, " returning "> extends infer Block2 extends string
216
+ ? MapExcludedRHS<Split<Block2, ",">>
217
+ : []
218
+ : []
219
+ : [];
220
+
221
+ export type MapExcludedRHS<Parts extends string[], Acc extends string[] = []> =
222
+ Parts extends [infer P extends string, ...infer Rest extends string[]]
223
+ ? P extends `${string}=${infer Right}`
224
+ ? CleanIdent<Right> extends `excluded.${infer Col}`
225
+ ? MapExcludedRHS<Rest, [...Acc, Col]>
226
+ : MapExcludedRHS<Rest, Acc>
227
+ : MapExcludedRHS<Rest, Acc>
228
+ : Acc;
229
+
230
+ // Update set list parsing
231
+
232
+ export type SplitAssignments<S extends string> =
233
+ TrimLeft<S> extends `(${string}`
234
+ // Row-assignment form `SET (a, b) = (v1, v2)`: the targets are the
235
+ // parenthesised column list, not a single `left = right` assignment.
236
+ // The naive comma split would break on the commas inside the tuples, so
237
+ // pull the first balanced `(...)` group and split its inner column list.
238
+ ? ExtractRowAssignTargets<TrimLeft<S>>
239
+ : Split<S, ","> extends infer Parts extends string[]
240
+ ? MapLeftSide<Parts>
241
+ : [];
242
+
243
+ export type ExtractRowAssignTargets<S extends string> =
244
+ SplitBalancedParen<S> extends { inner: infer Cols extends string; rest: infer _Rest extends string }
245
+ ? FilterEmpty<MapClean<SplitCommaSimple<Cols>>>
246
+ : [];
247
+
248
+ export type MapLeftSide<Parts extends string[], Acc extends string[] = []> =
249
+ Parts extends [infer P extends string, ...infer Rest extends string[]]
250
+ ? P extends `${infer Left}=${string}`
251
+ ? Trim<Left> extends infer L extends string
252
+ ? L extends ""
253
+ ? MapLeftSide<Rest, Acc>
254
+ : HasSpecial<L> extends true
255
+ ? MapLeftSide<Rest, Acc>
256
+ : MapLeftSide<Rest, [...Acc, L]>
257
+ : MapLeftSide<Rest, Acc>
258
+ : MapLeftSide<Rest, Acc>
259
+ : Acc;
260
+
@@ -0,0 +1,243 @@
1
+ // NormalizeQuery pipeline + quote-aware lowercasing.
2
+ import type { CollapseSpaces, Trim } from "./string-utils.js";
3
+ import type { NeutralizePgLiterals, RewriteExtractCall, StripComments } from "./pg-literals.js";
4
+
5
+ // The lowercaser walks char-by-char under a step cap (a proxy for TS's
6
+ // tail-recursion limit). Report-scale queries carry hundreds of indentation
7
+ // chars that exhaust that budget BEFORE the lowercaser reaches the tail of the
8
+ // SELECT list, forcing a blanket quote-UNAWARE `Lowercase<S>` bail that
9
+ // corrupts the case of single-quoted literals and double-quoted output aliases
10
+ // past the cap (e.g. `'GBP'`→`'gbp'`, `"currencyCount"`→key `currencycount`).
11
+ // Collapsing whitespace FIRST shrinks the input so the whole query stays under
12
+ // the cap (raising the cap instead blows TS2589 near the ~1000 recursion
13
+ // limit). The OUTER ReplaceWhitespace still runs on the now-lowercased string,
14
+ // so its `update`-clause detection is unchanged. The redundant outer
15
+ // ReplaceWhitespace/CollapseSpaces are cheap no-ops once whitespace is already
16
+ // normalized.
17
+ // The lowercaser preserves every character except case — it never introduces
18
+ // `\n\t\r` or extra spaces — so once the INNER `CollapseSpaces<ReplaceWhitespace>`
19
+ // has normalized whitespace, an OUTER pass is a no-op: `ReplaceWhitespace` sees no
20
+ // line breaks (cheap `HasLineBreaks` guard returns `S` without walking) and
21
+ // `CollapseSpaces` would re-walk an already single-spaced string. The outer
22
+ // `CollapseSpaces` is an 800-step char-walk that ran on every query for nothing, so
23
+ // it's dropped. The chunked `LowercaseOutsideQuotes` already handles arbitrary
24
+ // length, so the "collapse first to fit the lowercaser under its cap" rationale no
25
+ // longer requires a second collapse afterwards.
26
+ export type NormalizeQuery<S extends string> =
27
+ RewriteExtractCall<Trim<RemoveTrailingSemicolon<LowercaseOutsideQuotes<CollapseSpaces<ReplaceWhitespace<StripComments<NeutralizePgLiterals<S>>>>>>>>;
28
+
29
+ // Quote-aware lowercasing: SQL keywords/identifiers are case-insensitive, but
30
+ // single-quoted string literals and double-quoted identifiers keep their exact
31
+ // case — case-sensitive QUOTED OUTPUT ALIASES become projected row KEYS that MUST
32
+ // keep their case (e.g. `"linkHash"` → key `linkHash`, not `linkhash`).
33
+ //
34
+ // A single tail-recursive char-walk caps out at TS's ~1000-iteration limit, so it
35
+ // cannot lossly process the 1000+-char SELECT lists real reporting queries reach.
36
+ // The worker runs a bounded chunk and then YIELDS its state (`{ __c: [...] }`); the
37
+ // driver re-invokes it on the remainder with a fresh step counter, resetting TS's
38
+ // internal tail-recursion count per chunk. This processes arbitrarily long queries
39
+ // WITHOUT the old force-lowercase-the-tail bail that corrupted late aliases.
40
+ export type LowercaseOutsideQuotes<S extends string> =
41
+ string extends S
42
+ ? string
43
+ : LowercaseOutsideQuotesDrive<LowercaseOutsideQuotesWorker<S, false, false, "", []>>;
44
+
45
+ type LowercaseOutsideQuotesDrive<R> =
46
+ R extends { __c: [infer S extends string, infer Q1 extends boolean, infer Q2 extends boolean, infer Acc extends string] }
47
+ ? LowercaseOutsideQuotesDrive<LowercaseOutsideQuotesWorker<S, Q1, Q2, Acc, []>>
48
+ : R;
49
+
50
+ type LowercaseOutsideQuotesWorker<
51
+ S extends string,
52
+ InSingleQuote extends boolean,
53
+ InDoubleQuote extends boolean,
54
+ Acc extends string,
55
+ Steps extends any[]
56
+ > = Steps["length"] extends 450
57
+ ? { __c: [S, InSingleQuote, InDoubleQuote, Acc] }
58
+ : S extends `${infer C}${infer Rest}`
59
+ ? C extends "'"
60
+ ? InDoubleQuote extends true
61
+ ? LowercaseOutsideQuotesWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
62
+ : InSingleQuote extends true
63
+ ? LowercaseOutsideQuotesWorker<Rest, false, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
64
+ : LowercaseOutsideQuotesWorker<Rest, true, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
65
+ : C extends `"`
66
+ ? InSingleQuote extends true
67
+ ? LowercaseOutsideQuotesWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
68
+ : InDoubleQuote extends true
69
+ ? LowercaseOutsideQuotesWorker<Rest, InSingleQuote, false, `${Acc}${C}`, [any, ...Steps]>
70
+ : LowercaseOutsideQuotesWorker<Rest, InSingleQuote, true, `${Acc}${C}`, [any, ...Steps]>
71
+ : InSingleQuote extends true
72
+ ? LowercaseOutsideQuotesWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
73
+ : InDoubleQuote extends true
74
+ ? LowercaseOutsideQuotesWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
75
+ : LowercaseOutsideQuotesWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${Lowercase<C>}`, [any, ...Steps]>
76
+ : Acc;
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Param-name-preserving lowercaser (write/raw builder path only).
80
+ //
81
+ // Identical to LowercaseOutsideQuotes, but the identifier of a `:name` named
82
+ // parameter keeps its EXACT case. Named params become object keys in the
83
+ // builder's `withParams({ … })`, so folding `:teamId`→`:teamid` produced keys
84
+ // the caller could not write (F1). Quoted literals/identifiers are preserved as
85
+ // before; a `::cast` operator is consumed as a unit so the type name after it
86
+ // still lowercases and the second colon is never mistaken for a param start.
87
+ // The quote-aware char-walk below (`LcKeepDrive`→`LcKeepWorker`) exists only to
88
+ // preserve the case of `:name` params — its single consumer, `ExtractParams`, emits
89
+ // those as `withParams` keys. Everything else the walk preserves (quoted
90
+ // literals/identifiers) is INVISIBLE to the param→type result: literals are
91
+ // stripped/widened, quoted column refs are lowercased by `CleanIdent` anyway, and an
92
+ // aliased qualifier and its uses fold consistently. So when NO `:name` param
93
+ // identifier carries an uppercase letter, every param name is already lowercase and
94
+ // the `Lowercase<S>` intrinsic yields a byte-identical `ExtractParams` result —
95
+ // replacing the only un-truncated O(length) char-walk on the builder param-extraction
96
+ // hot path with a single depth-1 native operation. That trims this chain's
97
+ // instantiation COUNT and DEPTH (both TS2589 drivers), buying recursion-limit headroom
98
+ // at the cost of a little more compiler memory (the intrinsic interns the full
99
+ // lowercased literal) — a trade we take deliberately to make large builder queries
100
+ // less likely to hit the depth ceiling.
101
+ //
102
+ // `ParamsHaveUpper` is the gate. It jumps colon-to-colon (leftmost template match), so
103
+ // its depth is the NUMBER OF COLONS — a handful — not the query length, staying far
104
+ // cheaper than the walk it guards. `::cast` skips both colons (cast type names always
105
+ // lowercase safely). `Lowercase<Nm> extends Nm` is false exactly when the param name
106
+ // `Nm` carries an uppercase letter. It is conservative: a colon-dense overrun, or ANY
107
+ // uppercase param, falls back to the exact-fidelity walk — false positives only cost
108
+ // speed, never correctness.
109
+ type ParamsHaveUpper<S extends string, Steps extends any[] = []> =
110
+ Steps["length"] extends 64
111
+ ? true
112
+ : S extends `${infer _Pre}:${infer Rest}`
113
+ ? Rest extends `:${infer R2}`
114
+ ? ParamsHaveUpper<R2, [any, ...Steps]>
115
+ : ReadParamIdent<Rest> extends { name: infer Nm extends string; rest: infer Rr extends string }
116
+ ? Lowercase<Nm> extends Nm
117
+ ? ParamsHaveUpper<Rr, [any, ...Steps]>
118
+ : true
119
+ : ParamsHaveUpper<Rest, [any, ...Steps]>
120
+ : false;
121
+
122
+ export type LowercaseOutsideQuotesKeepParams<S extends string> =
123
+ string extends S
124
+ ? string
125
+ : ParamsHaveUpper<S> extends true
126
+ ? LcKeepDrive<LcKeepWorker<S, false, false, "", []>>
127
+ : Lowercase<S>;
128
+
129
+ type LcKeepDrive<R> =
130
+ R extends { __c: [infer S extends string, infer Q1 extends boolean, infer Q2 extends boolean, infer Acc extends string] }
131
+ ? LcKeepDrive<LcKeepWorker<S, Q1, Q2, Acc, []>>
132
+ : R;
133
+
134
+ // Chars that terminate a `:name` parameter identifier in a SQL fragment.
135
+ type ParamNameStop =
136
+ | " " | "\t" | "\n" | "," | ";" | ")" | "(" | "'" | '"' | ":" | "."
137
+ | "=" | "+" | "-" | "*" | "/" | "|" | "%" | ">" | "<" | "!" | "~"
138
+ | "@" | "#" | "&" | "^" | "[" | "]" | "{" | "}";
139
+ // Copy a param identifier verbatim (case-preserved) up to the first stop char.
140
+ // Capped so a pathological no-stop tail cannot blow the recursion budget.
141
+ type ReadParamIdent<S extends string, Acc extends string = "", N extends any[] = []> =
142
+ N["length"] extends 128 ? { name: Acc; rest: S }
143
+ : S extends `${infer C}${infer R}`
144
+ ? C extends ParamNameStop ? { name: Acc; rest: S }
145
+ : ReadParamIdent<R, `${Acc}${C}`, [any, ...N]>
146
+ : { name: Acc; rest: S };
147
+
148
+ type LcKeepWorker<
149
+ S extends string,
150
+ InSingleQuote extends boolean,
151
+ InDoubleQuote extends boolean,
152
+ Acc extends string,
153
+ Steps extends any[]
154
+ > = Steps["length"] extends 450
155
+ ? { __c: [S, InSingleQuote, InDoubleQuote, Acc] }
156
+ : S extends `${infer C}${infer Rest}`
157
+ ? C extends "'"
158
+ ? InDoubleQuote extends true
159
+ ? LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
160
+ : InSingleQuote extends true
161
+ ? LcKeepWorker<Rest, false, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
162
+ : LcKeepWorker<Rest, true, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
163
+ : C extends `"`
164
+ ? InSingleQuote extends true
165
+ ? LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
166
+ : InDoubleQuote extends true
167
+ ? LcKeepWorker<Rest, InSingleQuote, false, `${Acc}${C}`, [any, ...Steps]>
168
+ : LcKeepWorker<Rest, InSingleQuote, true, `${Acc}${C}`, [any, ...Steps]>
169
+ : InSingleQuote extends true
170
+ ? LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
171
+ : InDoubleQuote extends true
172
+ ? LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
173
+ // outside quotes: a lone `:` begins a named param (case-
174
+ // preserved); `::` is a cast operator consumed as a unit.
175
+ : C extends ":"
176
+ ? Rest extends `:${infer R2}`
177
+ ? LcKeepWorker<R2, InSingleQuote, InDoubleQuote, `${Acc}::`, [any, ...Steps]>
178
+ : ReadParamIdent<Rest> extends { name: infer Nm extends string; rest: infer Rr extends string }
179
+ ? LcKeepWorker<Rr, InSingleQuote, InDoubleQuote, `${Acc}:${Nm}`, [any, ...Steps]>
180
+ : LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}:`, [any, ...Steps]>
181
+ : LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${Lowercase<C>}`, [any, ...Steps]>
182
+ : Acc;
183
+
184
+ // NormalizeQuery variant that preserves `:name` param case — used by the
185
+ // write/raw builder param extraction (ExtractParams) only.
186
+ export type NormalizeQueryKeepParams<S extends string> =
187
+ RewriteExtractCall<Trim<RemoveTrailingSemicolon<LowercaseOutsideQuotesKeepParams<CollapseSpaces<ReplaceWhitespace<StripComments<NeutralizePgLiterals<S>>>>>>>>;
188
+
189
+ // Convert every `\n` / `\t` / `\r` to a space. OCCURRENCE-based (like
190
+ // `CollapseSpaces`): each step splits at the FIRST remaining line break, so the
191
+ // `${infer A}` skips a whole run of non-whitespace and the cost is O(line breaks),
192
+ // NOT O(chars). The old per-char form (`ReplaceWhitespaceLimited`) capped at ~900
193
+ // CHARS, so a report-scale multi-line query left the TAIL of its SELECT list
194
+ // un-normalized — trailing projections kept raw newlines and degraded to
195
+ // `unknown`/dropped. The occurrence form covers the whole query under a step cap
196
+ // that stays far from TS's tail-recursion limit. (`HasLineBreaks` keeps the common
197
+ // single-line query a true no-op.)
198
+ export type ReplaceWhitespace<S extends string> =
199
+ HasLineBreaks<S> extends true
200
+ ? ReplaceWhitespaceRuns<S>
201
+ : S;
202
+
203
+ type ReplaceWhitespaceRuns<S extends string, Steps extends any[] = []> =
204
+ string extends S
205
+ ? S
206
+ : Steps["length"] extends 1500
207
+ ? S
208
+ : S extends `${infer A}\n${infer B}`
209
+ ? ReplaceWhitespaceRuns<`${A} ${B}`, [any, ...Steps]>
210
+ : S extends `${infer A}\t${infer B}`
211
+ ? ReplaceWhitespaceRuns<`${A} ${B}`, [any, ...Steps]>
212
+ : S extends `${infer A}\r${infer B}`
213
+ ? ReplaceWhitespaceRuns<`${A} ${B}`, [any, ...Steps]>
214
+ : S;
215
+
216
+ // Cheap "is this string longer than ~500 chars" check: drop 10 chars per step
217
+ // for up to 50 steps. If content survives all 50 drops the string exceeds the
218
+ // budget. Used to keep the expensive full validator off report-scale queries
219
+ // (which the high-complexity bypass protects) while still fully validating
220
+ // ordinary small queries. Chunked (10/step) so it stays far cheaper than a
221
+ // char-by-char walk.
222
+ export type ExceedsLengthBudget<S extends string, Steps extends any[] = []> =
223
+ string extends S
224
+ ? true
225
+ : Steps["length"] extends 50
226
+ ? S extends "" ? false : true
227
+ : S extends ""
228
+ ? false
229
+ : ExceedsLengthBudget<Drop10Chars<S>, [any, ...Steps]>;
230
+
231
+ export type Drop10Chars<S extends string> =
232
+ S extends `${infer _A}${infer _B}${infer _C}${infer _D}${infer _E}${infer _F}${infer _G}${infer _H}${infer _I}${infer _J}${infer R}`
233
+ ? R
234
+ : "";
235
+
236
+ export type HasLineBreaks<S extends string> =
237
+ S extends `${string}\n${string}` ? true :
238
+ S extends `${string}\t${string}` ? true :
239
+ S extends `${string}\r${string}` ? true :
240
+ false;
241
+
242
+ export type RemoveTrailingSemicolon<S extends string> =
243
+ Trim<S> extends `${infer R};` ? Trim<R> : Trim<S>;