@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,289 @@
1
+ // Postgres literal neutralization, EXTRACT rewrite, comment stripping.
2
+ import type { Trim } from "./string-utils.js";
3
+ import type { ExceedsLengthBudget } from "./normalize.js";
4
+ import type { SplitBalancedParen } from "./extract.js";
5
+
6
+ // PostgreSQL string literals beyond the plain `'...'` form: dollar-quoted
7
+ // strings (`$$...$$`, `$tag$...$tag$`) and escape-string constants (`E'...'`).
8
+ // Their bodies are opaque string DATA — never SQL structure or column refs — yet
9
+ // clause-looking text inside them (`over (bogus_col)`, ` returning x`, `from y`)
10
+ // would otherwise be scanned as real SQL. Rewrite each whole span to a canonical
11
+ // blank literal `''` BEFORE any case-folding / comment-stripping / structural
12
+ // scan runs. Everything downstream already treats `'...'` as an opaque string
13
+ // literal (typed `string`, skipped by the column-existence scan), so this single
14
+ // rewrite makes BOTH result inference and validation correct in one place.
15
+ // Runs innermost so a `--`/`/* */` or keyword inside such a literal is neutered
16
+ // before StripComments / the table+column collectors ever see it. Gated behind a
17
+ // precise pre-check so only queries that actually contain such a literal pay for
18
+ // the walk; ordinary `$n` params and plain `'...'`/`"..."` literals are untouched.
19
+ export type NeutralizePgLiterals<S extends string> =
20
+ string extends S
21
+ ? S
22
+ : NeedsPgNeutralize<S> extends true
23
+ ? NeutralizePgDrive<NeutralizePgWorker<S, false, false, false, "", []>>
24
+ : S;
25
+
26
+ type NeedsPgNeutralize<S extends string> =
27
+ HasPairedDollar<S> extends true
28
+ ? true
29
+ : HasEStringOpener<S>;
30
+
31
+ // A genuine dollar-quote needs the SAME `$tag$` delimiter twice (tag may be
32
+ // empty → `$$`). `$n` params can never form two matching `$tag$` delimiters
33
+ // (a param `$1` has no trailing `$`), so this never fires on parameter lists.
34
+ // Inferred in two steps because a back-reference within one template pattern
35
+ // (`…$${infer T}$…$${T}$…`) is not legal — `Tag` is only usable once resolved
36
+ // in the true branch, where the nested check re-matches it as a known literal.
37
+ type HasPairedDollar<S extends string> =
38
+ S extends `${string}$${infer Tag}$${infer Rest}`
39
+ ? Rest extends `${string}$${Tag}$${string}`
40
+ ? true
41
+ : false
42
+ : false;
43
+
44
+ // `E'`/`e'` only opens an escape string at a token boundary (start, or after
45
+ // whitespace / `(` / `,`); a trailing `…e'` inside a `'...'` literal is just the
46
+ // closing quote, not an opener — restricting the gate keeps the walk off ordinary
47
+ // string literals that merely end in `e`/`E` (e.g. `'sale'`, `'ACTIVE'`).
48
+ type HasEStringOpener<S extends string> =
49
+ S extends `E'${string}` ? true
50
+ : S extends `e'${string}` ? true
51
+ : S extends `${string} E'${string}` ? true
52
+ : S extends `${string} e'${string}` ? true
53
+ : S extends `${string}(E'${string}` ? true
54
+ : S extends `${string}(e'${string}` ? true
55
+ : S extends `${string},E'${string}` ? true
56
+ : S extends `${string},e'${string}` ? true
57
+ : false;
58
+
59
+ type NeutralizePgDrive<R> =
60
+ R extends { __c: [infer S extends string, infer A extends boolean, infer B extends boolean, infer C extends boolean, infer Acc extends string] }
61
+ ? NeutralizePgDrive<NeutralizePgWorker<S, A, B, C, Acc, []>>
62
+ : R;
63
+
64
+ // A dollar-quote tag follows unquoted-identifier rules (letters/digits/`_`, no
65
+ // leading digit) or is empty (`$$`). This is what distinguishes a real opener
66
+ // from a positional param run: `$2, $3` is NOT `$<tag>$` (the "tag" `2, ` has a
67
+ // leading digit, a space and a comma), so the walk must not treat it as a
68
+ // dollar-quoted span and eat the query body between two such params.
69
+ type DollarTagLower = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z";
70
+ type DollarTagStart = DollarTagLower | Uppercase<DollarTagLower> | "_";
71
+ type DollarTagChar = DollarTagStart | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
72
+
73
+ type IsValidDollarTag<Tag extends string> =
74
+ Tag extends ""
75
+ ? true
76
+ : Tag extends `${infer F}${infer R}`
77
+ ? F extends DollarTagStart
78
+ ? AllDollarTagChars<R>
79
+ : false
80
+ : false;
81
+
82
+ type AllDollarTagChars<S extends string, Steps extends any[] = []> =
83
+ S extends ""
84
+ ? true
85
+ : Steps["length"] extends 64
86
+ ? false
87
+ : S extends `${infer F}${infer R}`
88
+ ? F extends DollarTagChar
89
+ ? AllDollarTagChars<R, [any, ...Steps]>
90
+ : false
91
+ : false;
92
+
93
+ // Char-walk with three independent "inside a literal" states. A dollar-quote or
94
+ // E-string opener is only honored OUTSIDE a `'...'`/`"..."` literal, so a `$$`
95
+ // sitting inside a single-quoted string is copied verbatim. Chunked-driver
96
+ // pattern: yields `{ __c: [...] }` at the step cap so the driver re-invokes with
97
+ // a fresh counter, keeping arbitrarily long queries under TS2589.
98
+ type NeutralizePgWorker<
99
+ S extends string,
100
+ InS extends boolean, // inside '...'
101
+ InD extends boolean, // inside "..."
102
+ InE extends boolean, // inside E'...' (body blanked)
103
+ Acc extends string,
104
+ Steps extends any[]
105
+ > = Steps["length"] extends 400
106
+ ? { __c: [S, InS, InD, InE, Acc] }
107
+ : InE extends true
108
+ // escape-string body: blank content; `\x` escapes the next char; `'` closes.
109
+ ? S extends `\\${infer _Esc}${infer R}`
110
+ ? NeutralizePgWorker<R, InS, InD, true, Acc, [any, ...Steps]>
111
+ : S extends `'${infer R}`
112
+ ? NeutralizePgWorker<R, false, false, false, `${Acc}'`, [any, ...Steps]>
113
+ : S extends `${infer _C}${infer R}`
114
+ ? NeutralizePgWorker<R, InS, InD, true, Acc, [any, ...Steps]>
115
+ : `${Acc}'` // unterminated → close it off
116
+ : InS extends true
117
+ ? S extends `${infer C}${infer R}`
118
+ ? NeutralizePgWorker<R, C extends "'" ? false : true, InD, false, `${Acc}${C}`, [any, ...Steps]>
119
+ : Acc
120
+ : InD extends true
121
+ ? S extends `${infer C}${infer R}`
122
+ ? NeutralizePgWorker<R, InS, C extends `"` ? false : true, false, `${Acc}${C}`, [any, ...Steps]>
123
+ : Acc
124
+ // outside any literal:
125
+ : S extends `E'${infer R}`
126
+ ? NeutralizePgWorker<R, false, false, true, `${Acc}'`, [any, ...Steps]>
127
+ : S extends `e'${infer R}`
128
+ ? NeutralizePgWorker<R, false, false, true, `${Acc}'`, [any, ...Steps]>
129
+ : S extends `$${infer Tag}$${infer Rest}`
130
+ ? IsValidDollarTag<Tag> extends true
131
+ ? Rest extends `${infer _Body}$${Tag}$${infer After}`
132
+ ? NeutralizePgWorker<After, false, false, false, `${Acc}''`, [any, ...Steps]>
133
+ // valid tag but no matching close → not a real span; emit one `$`, advance.
134
+ : S extends `$${infer R2}`
135
+ ? NeutralizePgWorker<R2, false, false, false, `${Acc}$`, [any, ...Steps]>
136
+ : Acc
137
+ // invalid tag (e.g. a `$n` param run): emit one `$`, advance one char.
138
+ : S extends `$${infer R3}`
139
+ ? NeutralizePgWorker<R3, false, false, false, `${Acc}$`, [any, ...Steps]>
140
+ : Acc
141
+ : S extends `'${infer R}`
142
+ ? NeutralizePgWorker<R, true, false, false, `${Acc}'`, [any, ...Steps]>
143
+ : S extends `"${infer R}`
144
+ ? NeutralizePgWorker<R, false, true, false, `${Acc}"`, [any, ...Steps]>
145
+ : S extends `${infer C}${infer R}`
146
+ ? NeutralizePgWorker<R, false, false, false, `${Acc}${C}`, [any, ...Steps]>
147
+ : Acc;
148
+
149
+ // `EXTRACT(field FROM source)` uses keyword grammar: the `field` token (year,
150
+ // month, day, ...) is a date-part keyword, NOT a column, and the inner ` from `
151
+ // is function-local — it is NOT a top-level FROM clause. Left untouched, the
152
+ // token-level table collector treats `from source` as a real FROM source (so the
153
+ // source column is mistaken for a table) and the function-arg validator would
154
+ // flag the date-part `field` as an unknown column. Rewrite each
155
+ // `extract(field from source)` to `extract(source)`: this drops the inner ` from `
156
+ // (so the table collector / ref-scan never see it) and exempts the date-part
157
+ // field, while the source column flows through ordinary function-arg validation.
158
+ // Gated behind a cheap pre-check so only queries containing ` extract(` pay for
159
+ // the walk. Space-anchored so `date_extract(` and similar are left alone.
160
+ export type RewriteExtractCall<S extends string> =
161
+ S extends `${string} extract(${string}`
162
+ // Only small queries that actually contain a single quote risk an
163
+ // ` extract(` sitting inside a string literal (round-12 E1); they take the
164
+ // quote-aware walk. Everything else (no quotes, or report-scale where a
165
+ // parity char-walk would blow the depth budget) uses the plain rewrite.
166
+ ? S extends `${string}'${string}`
167
+ ? ExceedsLengthBudget<S> extends true
168
+ ? RewriteExtractWalk<S>
169
+ : RewriteExtractWalkQuoteAware<S>
170
+ : RewriteExtractWalk<S>
171
+ : S;
172
+
173
+ export type RewriteExtractWalk<S extends string, Steps extends any[] = []> =
174
+ Steps["length"] extends 24
175
+ ? S
176
+ : S extends `${infer Pre} extract(${infer AfterOpen}`
177
+ ? SplitBalancedParen<`(${AfterOpen}`> extends { inner: infer Inner extends string; rest: infer Rest extends string }
178
+ ? Inner extends `${infer _Field} from ${infer Source}`
179
+ ? `${RewriteExtractWalk<Pre, [any, ...Steps]>} extract(${Trim<Source>})${RewriteExtractWalk<Rest, [any, ...Steps]>}`
180
+ : `${RewriteExtractWalk<Pre, [any, ...Steps]>} extract(${Inner})${RewriteExtractWalk<Rest, [any, ...Steps]>}`
181
+ : S
182
+ : S;
183
+
184
+ // As `RewriteExtractWalk`, but an ` extract(` whose prefix has an odd number of
185
+ // single quotes sits INSIDE a string literal and is left verbatim (the literal's
186
+ // text must survive into the result-row value type — round-12 E1). The prefix is
187
+ // still recursed so a genuine EXTRACT earlier in the query is rewritten.
188
+ export type RewriteExtractWalkQuoteAware<S extends string, Steps extends any[] = []> =
189
+ Steps["length"] extends 12
190
+ ? S
191
+ : S extends `${infer Pre} extract(${infer AfterOpen}`
192
+ ? Pre extends `${string}'${string}`
193
+ ? OddSingleQuotes<Pre> extends true
194
+ ? AfterOpen extends `${infer Lit}'${infer Tail}`
195
+ ? `${RewriteExtractWalkQuoteAware<Pre, [any, ...Steps]>} extract(${Lit}'${RewriteExtractWalkQuoteAware<Tail, [any, ...Steps]>}`
196
+ : S
197
+ : RewriteExtractRewriteOne<Pre, AfterOpen, Steps>
198
+ : RewriteExtractRewriteOne<Pre, AfterOpen, Steps>
199
+ : S;
200
+
201
+ export type RewriteExtractRewriteOne<Pre extends string, AfterOpen extends string, Steps extends any[]> =
202
+ SplitBalancedParen<`(${AfterOpen}`> extends { inner: infer Inner extends string; rest: infer Rest extends string }
203
+ ? Inner extends `${infer _Field} from ${infer Source}`
204
+ ? `${RewriteExtractWalkQuoteAware<Pre, [any, ...Steps]>} extract(${Trim<Source>})${RewriteExtractWalkQuoteAware<Rest, [any, ...Steps]>}`
205
+ : `${RewriteExtractWalkQuoteAware<Pre, [any, ...Steps]>} extract(${Inner})${RewriteExtractWalkQuoteAware<Rest, [any, ...Steps]>}`
206
+ : `${Pre} extract(${AfterOpen}`;
207
+
208
+ // True when `S` contains an odd number of single quotes — i.e. its end is inside
209
+ // an unterminated single-quoted string literal. Bounded; on bail returns the
210
+ // best-effort parity so far.
211
+ export type OddSingleQuotes<S extends string, Flag extends boolean = false, Steps extends any[] = []> =
212
+ string extends S
213
+ ? false
214
+ : Steps["length"] extends 400
215
+ ? Flag
216
+ : S extends `${infer C}${infer R}`
217
+ ? OddSingleQuotes<R, C extends "'" ? (Flag extends true ? false : true) : Flag, [any, ...Steps]>
218
+ : Flag;
219
+
220
+ // Strip `/* ... */` block comments AND `-- ...` line comments before any other
221
+ // parsing so a comment between projection items (or anywhere else) doesn't
222
+ // survive into an expression and collapse the projected row to `never`. Runs
223
+ // FIRST in the pipeline, while line breaks are still present, so a line comment
224
+ // can be cut at its terminating newline.
225
+ //
226
+ // Quote-aware: comment markers inside a single-quoted string literal
227
+ // (`'/* not a comment */'`, `'-- nope'`) OR a double-quoted identifier
228
+ // (`"kept /* marker */ name"`) are preserved verbatim. The walk is the expensive
229
+ // part, so it is gated behind a cheap pre-check: only queries that actually
230
+ // contain a `/*` or `--` pay for the char-walk; everything else (the
231
+ // overwhelming common case) passes straight through untouched.
232
+ export type StripComments<S extends string> =
233
+ string extends S
234
+ ? S
235
+ : S extends `${string}/*${string}`
236
+ ? StripCommentsWalk<S>
237
+ : S extends `${string}--${string}`
238
+ ? StripCommentsWalk<S>
239
+ : S;
240
+
241
+ // Char-walk implementation. Each block comment is replaced by a single space;
242
+ // each line comment is dropped up to (but not including) its newline so the
243
+ // surrounding structure is preserved for `ReplaceWhitespace`/`CollapseSpaces`.
244
+ // An unterminated `/*` drops everything from the `/*` onward. Bounded at 500
245
+ // steps — on bail the remainder is appended as-is, mirroring the truncation
246
+ // `LowercaseOutsideQuotes` already accepts for report-scale strings.
247
+ // `InString` tracks a single-quoted string literal; `InDString` tracks a
248
+ // double-quoted identifier. While inside EITHER, characters (including `/*` and
249
+ // `--`) are copied verbatim — only outside both are comment markers honoured.
250
+ export type StripCommentsWalk<
251
+ S extends string,
252
+ InString extends boolean = false,
253
+ Acc extends string = "",
254
+ Steps extends any[] = [],
255
+ InDString extends boolean = false
256
+ > = string extends S
257
+ ? S
258
+ : Steps["length"] extends 500
259
+ ? `${Acc}${S}`
260
+ : InString extends true
261
+ ? S extends `${infer C}${infer Rest}`
262
+ ? StripCommentsWalk<Rest, C extends "'" ? false : true, `${Acc}${C}`, [any, ...Steps], InDString>
263
+ : Acc
264
+ : InDString extends true
265
+ ? S extends `${infer C}${infer Rest}`
266
+ ? StripCommentsWalk<Rest, InString, `${Acc}${C}`, [any, ...Steps], C extends `"` ? false : true>
267
+ : Acc
268
+ : S extends `/*${infer AfterOpen}`
269
+ ? AfterOpen extends `${infer _Body}*/${infer Tail}`
270
+ ? StripCommentsWalk<Tail, false, `${Acc} `, [any, ...Steps], false>
271
+ : `${Acc} `
272
+ : S extends `--${infer AfterDash}`
273
+ ? StripCommentsWalk<LineCommentTail<AfterDash>, false, `${Acc} `, [any, ...Steps], false>
274
+ : S extends `${infer C}${infer Rest}`
275
+ ? StripCommentsWalk<Rest, C extends "'" ? true : false, `${Acc}${C}`, [any, ...Steps], C extends `"` ? true : false>
276
+ : Acc;
277
+
278
+ // Skip a line comment body, returning the tail starting at the first newline
279
+ // (which is kept so words on either side of the comment can't merge). A comment
280
+ // that runs to the end of the string yields `""`. Bounded against runaway.
281
+ export type LineCommentTail<S extends string, Steps extends any[] = []> =
282
+ Steps["length"] extends 1000
283
+ ? S
284
+ : S extends `${infer C}${infer Rest}`
285
+ ? C extends "\n" | "\r"
286
+ ? S
287
+ : LineCommentTail<Rest, [any, ...Steps]>
288
+ : "";
289
+
@@ -0,0 +1,288 @@
1
+ // Top-level / paren-aware splitting + alias extraction.
2
+ import type { CleanIdent, HasSpecial, SplitLast, Trim } from "./string-utils.js";
3
+ import type { SplitBalancedParen } from "./extract.js";
4
+ import type { SqlReserved } from "./tokenize.js";
5
+
6
+ // Split by commas at top level (paren-aware). Safe fallbacks for deep strings.
7
+
8
+ // The per-character worker. A single tail-recursive conditional type can only
9
+ // run ~1000 iterations before TS aborts with TS2589 ("excessively deep") — well
10
+ // below the string lengths real reporting SELECT lists reach (1000+ chars). So
11
+ // instead of one unbounded walk, the worker runs a bounded chunk (CHUNK steps,
12
+ // kept under TS's tail-recursion limit) and then YIELDS its full state as a
13
+ // `{ __c: [...] }` marker. `SplitTopLevel` (the driver below) re-invokes the
14
+ // worker on the remainder with a fresh step counter, which resets TS's internal
15
+ // tail-recursion count per chunk — so arbitrarily long lists split losslessly.
16
+ type SplitTopLevelWorker<
17
+ S extends string,
18
+ Depth extends any[] = [],
19
+ Acc extends string[] = [],
20
+ Cur extends string = "",
21
+ Steps extends any[] = [],
22
+ InQ extends boolean = false,
23
+ InDQ extends boolean = false
24
+ > = Steps["length"] extends 450
25
+ ? { __c: [S, Depth, Acc, Cur, InQ, InDQ] }
26
+ : string extends CleanIdent<S>
27
+ ? [...Acc, `${Cur}string`]
28
+ : S extends `${infer C}${infer Rest}`
29
+ ? C extends "'"
30
+ // A single quote toggles "inside string literal": commas, parens and
31
+ // path braces inside a '...' literal are kept verbatim, not split.
32
+ // Suppressed while inside a double-quoted identifier.
33
+ ? InDQ extends true
34
+ ? SplitTopLevelWorker<Rest, Depth, Acc, `${Cur}${C}`, [any, ...Steps], InQ, InDQ>
35
+ : SplitTopLevelWorker<Rest, Depth, Acc, `${Cur}${C}`, [any, ...Steps], InQ extends true ? false : true, InDQ>
36
+ : InQ extends true
37
+ ? SplitTopLevelWorker<Rest, Depth, Acc, `${Cur}${C}`, [any, ...Steps], InQ, InDQ>
38
+ : C extends `"`
39
+ // A double quote toggles "inside quoted identifier": commas and
40
+ // parens inside `"..."` are part of the identifier, kept verbatim.
41
+ ? SplitTopLevelWorker<Rest, Depth, Acc, `${Cur}${C}`, [any, ...Steps], InQ, InDQ extends true ? false : true>
42
+ : InDQ extends true
43
+ ? SplitTopLevelWorker<Rest, Depth, Acc, `${Cur}${C}`, [any, ...Steps], InQ, InDQ>
44
+ : C extends "("
45
+ ? SplitTopLevelWorker<Rest, [any, ...Depth], Acc, `${Cur}${C}`, [any, ...Steps], InQ, InDQ>
46
+ : C extends ")"
47
+ ? SplitTopLevelWorker<Rest, Depth extends [any, ...infer D] ? D : [], Acc, `${Cur}${C}`, [any, ...Steps], InQ, InDQ>
48
+ : C extends ","
49
+ ? Depth["length"] extends 0
50
+ ? SplitTopLevelWorker<Rest, Depth, [...Acc, Cur], "", [any, ...Steps], InQ, InDQ>
51
+ : SplitTopLevelWorker<Rest, Depth, Acc, `${Cur}${C}`, [any, ...Steps], InQ, InDQ>
52
+ : SplitTopLevelWorker<Rest, Depth, Acc, `${Cur}${C}`, [any, ...Steps], InQ, InDQ>
53
+ : [...Acc, Cur];
54
+
55
+ // Driver: run the worker chunk-by-chunk until it returns the finished `string[]`.
56
+ // Each `{ __c: state }` yield is fed back with a fresh step counter, so no single
57
+ // worker instantiation chain exceeds the per-chunk budget. Driver recursion depth
58
+ // is `length / CHUNK` (≈ a handful for realistic queries), itself well under the
59
+ // tail-recursion limit.
60
+ export type SplitTopLevel<S extends string> =
61
+ SplitTopLevelDrive<SplitTopLevelWorker<S, [], [], "", [], false, false>>;
62
+
63
+ type SplitTopLevelDrive<R> =
64
+ R extends { __c: [infer S extends string, infer D extends any[], infer A extends string[], infer Cur extends string, infer InQ extends boolean, infer InDQ extends boolean] }
65
+ ? SplitTopLevelDrive<SplitTopLevelWorker<S, D, A, Cur, [], InQ, InDQ>>
66
+ : R;
67
+
68
+ // Extract select list before top-level FROM (paren- and quote-aware).
69
+
70
+ // Quote-aware on BOTH single quotes (`'...'` string literals) and double quotes
71
+ // (`"..."` quoted identifiers): a ` from ` token sitting inside a double-quoted
72
+ // output alias (`id as "came from import"`) is part of the identifier, not the
73
+ // SELECT/FROM boundary, so it must not split the list. `InString` tracks single
74
+ // quotes; `InDString` tracks double quotes. Parens and the ` from ` boundary are
75
+ // only honoured when outside BOTH kinds of quote.
76
+ export type ExtractBeforeFromTopLevel<
77
+ S extends string,
78
+ Depth extends any[] = [],
79
+ InString extends boolean = false,
80
+ Acc extends string = "",
81
+ Steps extends any[] = [],
82
+ InDString extends boolean = false
83
+ > = Steps["length"] extends 350
84
+ ? `${Acc}${ExtractBefore<S, " from ">}`
85
+ : InString extends true
86
+ ? S extends `${infer C}${infer Rest}`
87
+ ? C extends "'"
88
+ ? ExtractBeforeFromTopLevel<Rest, Depth, false, `${Acc}${C}`, [any, ...Steps], InDString>
89
+ : ExtractBeforeFromTopLevel<Rest, Depth, InString, `${Acc}${C}`, [any, ...Steps], InDString>
90
+ : Acc
91
+ : InDString extends true
92
+ ? S extends `${infer C}${infer Rest}`
93
+ ? C extends `"`
94
+ ? ExtractBeforeFromTopLevel<Rest, Depth, InString, `${Acc}${C}`, [any, ...Steps], false>
95
+ : ExtractBeforeFromTopLevel<Rest, Depth, InString, `${Acc}${C}`, [any, ...Steps], true>
96
+ : Acc
97
+ : Depth["length"] extends 0
98
+ ? S extends ` from ${string}`
99
+ ? Acc
100
+ : S extends `${infer C}${infer Rest}`
101
+ ? C extends "'"
102
+ ? ExtractBeforeFromTopLevel<Rest, Depth, true, `${Acc}${C}`, [any, ...Steps], InDString>
103
+ : C extends `"`
104
+ ? ExtractBeforeFromTopLevel<Rest, Depth, InString, `${Acc}${C}`, [any, ...Steps], true>
105
+ : C extends "("
106
+ ? ExtractBeforeFromTopLevel<Rest, [any, ...Depth], InString, `${Acc}${C}`, [any, ...Steps], InDString>
107
+ : C extends ")"
108
+ ? ExtractBeforeFromTopLevel<Rest, Depth extends [any, ...infer D] ? D : [], InString, `${Acc}${C}`, [any, ...Steps], InDString>
109
+ : ExtractBeforeFromTopLevel<Rest, Depth, InString, `${Acc}${C}`, [any, ...Steps], InDString>
110
+ : Acc
111
+ : S extends `${infer C}${infer Rest}`
112
+ ? C extends "'"
113
+ ? ExtractBeforeFromTopLevel<Rest, Depth, true, `${Acc}${C}`, [any, ...Steps], InDString>
114
+ : C extends `"`
115
+ ? ExtractBeforeFromTopLevel<Rest, Depth, InString, `${Acc}${C}`, [any, ...Steps], true>
116
+ : C extends "("
117
+ ? ExtractBeforeFromTopLevel<Rest, [any, ...Depth], InString, `${Acc}${C}`, [any, ...Steps], InDString>
118
+ : C extends ")"
119
+ ? ExtractBeforeFromTopLevel<Rest, Depth extends [any, ...infer D] ? D : [], InString, `${Acc}${C}`, [any, ...Steps], InDString>
120
+ : ExtractBeforeFromTopLevel<Rest, Depth, InString, `${Acc}${C}`, [any, ...Steps], InDString>
121
+ : Acc;
122
+
123
+ // Simple comma split (no paren awareness)
124
+
125
+ export type SplitCommaSimple<S extends string> = SplitTopLevel<S>;
126
+
127
+ // Extract before delimiter if present
128
+
129
+ export type ExtractBefore<S extends string, Delim extends string> =
130
+ S extends `${infer Head}${Delim}${infer _}` ? Head : S;
131
+
132
+ // Distinct
133
+
134
+ export type StripDistinct<S extends string> =
135
+ // `DISTINCT ON (cols)` — drop the whole ON-list so it can't leak into the
136
+ // projection. The list is parenthesised; keep everything after the `)`.
137
+ Trim<S> extends `distinct on (${infer _On})${infer Rest}` ? Trim<Rest> :
138
+ Trim<S> extends `distinct on(${infer _On})${infer Rest}` ? Trim<Rest> :
139
+ Trim<S> extends `distinct ${infer R}` ? R :
140
+ Trim<S> extends `all ${infer R}` ? R :
141
+ Trim<S>;
142
+
143
+ // Alias
144
+
145
+ // An implicit output alias written as a trailing double-quoted identifier with
146
+ // no `AS` keyword: `id "implicit id"`. Postgres accepts this. We only recognize
147
+ // the QUOTED form (a bare `expr alias` is too ambiguous to tell from
148
+ // compound/function syntax). The match requires a non-empty expression before
149
+ // the ` "..."` and an alias body with no embedded `"`.
150
+ export type IsImplicitQuotedAlias<E extends string> =
151
+ E extends `${infer Expr} "${infer Alias}"`
152
+ ? Alias extends `${string}"${string}`
153
+ ? false
154
+ : Trim<Expr> extends ""
155
+ ? false
156
+ : true
157
+ : false;
158
+
159
+ // A bare implicit output alias written as `expr alias` with NO `AS` keyword and
160
+ // NO quotes: `id user_id`, `count(*) total`. Postgres accepts this. It is
161
+ // deliberately CONSERVATIVE — only recognized when:
162
+ // - `alias` (the last space-delimited token) is a simple identifier (no dot,
163
+ // quotes, operators, parens) and is NOT a SQL reserved word; AND
164
+ // - the remaining `expr` is EITHER a single simple column reference
165
+ // (unqualified or `a.b` qualified, no spaces/operators) OR a single
166
+ // function-call `fn(...)` with balanced parens and nothing after the `)`.
167
+ // Anything compound/ambiguous (operators, multiple tokens, CASE, etc.) is NOT
168
+ // treated as an implicit alias.
169
+ export type BareImplicitAliasParts<E extends string> =
170
+ Trim<E> extends `${infer Head} ${infer LastTok}`
171
+ ? SplitLast<Trim<E>, " "> extends [infer HExpr extends string, infer HAlias extends string]
172
+ ? { expr: Trim<HExpr>; alias: Trim<HAlias> }
173
+ : { expr: Trim<Head>; alias: Trim<LastTok> }
174
+ : { expr: Trim<E>; alias: "" };
175
+
176
+ export type IsBareImplicitAlias<E extends string> =
177
+ BareImplicitAliasParts<E> extends { expr: infer Expr extends string; alias: infer Alias extends string }
178
+ ? Alias extends ""
179
+ ? false
180
+ : IsSimpleAliasToken<Alias> extends true
181
+ ? IsImplicitAliasExpr<Expr> extends true
182
+ ? true
183
+ : false
184
+ : false
185
+ : false;
186
+
187
+ // The trailing token is a valid bare alias only when it is a plain identifier
188
+ // with no special chars/dots, and is not a SQL reserved word (so `as`, `from`,
189
+ // `where`, `order`, etc. never get mistaken for an alias).
190
+ export type IsSimpleAliasToken<A extends string> =
191
+ A extends "" ? false :
192
+ A extends `${string}.${string}` ? false :
193
+ A extends `${string}"${string}` ? false :
194
+ A extends `${string}'${string}` ? false :
195
+ A extends `${string}*${string}` ? false :
196
+ HasSpecial<A> extends true ? false :
197
+ CleanIdent<A> extends SqlReserved ? false :
198
+ true;
199
+
200
+ // The head expression of a bare implicit alias must be a SINGLE simple token:
201
+ // either a plain (possibly qualified `a.b`) column reference with no spaces or
202
+ // operators, OR a single function call `fn(...)` whose parens are balanced and
203
+ // with nothing trailing the closing `)`.
204
+ export type IsImplicitAliasExpr<Expr extends string> =
205
+ Trim<Expr> extends ""
206
+ ? false
207
+ : Trim<Expr> extends `${infer Func}(${infer AfterOpen}`
208
+ ? IsSimpleFuncName<Trim<Func>> extends true
209
+ ? SplitBalancedParen<`(${AfterOpen}`> extends { rest: infer Rest extends string }
210
+ ? Trim<Rest> extends ""
211
+ ? true
212
+ : false
213
+ : false
214
+ : false
215
+ // Not a function call: must be a bare/qualified column ref — a single
216
+ // token with no spaces, operators, parens, or quotes.
217
+ : Trim<Expr> extends `${string} ${string}` ? false
218
+ : Trim<Expr> extends `${string}'${string}` ? false
219
+ : Trim<Expr> extends `${string}"${string}` ? false
220
+ : Trim<Expr> extends `${string}(${string}` ? false
221
+ : Trim<Expr> extends `${string})${string}` ? false
222
+ : Trim<Expr> extends `${string}+${string}` ? false
223
+ : Trim<Expr> extends `${string}-${string}` ? false
224
+ : Trim<Expr> extends `${string}*${string}` ? false
225
+ : Trim<Expr> extends `${string}/${string}` ? false
226
+ : Trim<Expr> extends `${string}=${string}` ? false
227
+ : Trim<Expr> extends `${string}<${string}` ? false
228
+ : Trim<Expr> extends `${string}>${string}` ? false
229
+ : Trim<Expr> extends `${string},${string}` ? false
230
+ : Trim<Expr> extends `${string}::${string}` ? false
231
+ : Trim<Expr> extends `${string}||${string}` ? false
232
+ : CleanIdent<Expr> extends SqlReserved ? false
233
+ : true;
234
+
235
+ // A function name preceding `(` must be a single simple identifier (`count`,
236
+ // `sum`, `coalesce`), not an operator-bearing expression.
237
+ export type IsSimpleFuncName<F extends string> =
238
+ F extends "" ? false :
239
+ F extends `${string} ${string}` ? false :
240
+ HasSpecial<F> extends true ? false :
241
+ true;
242
+
243
+ // A quoted identifier alias `"..."` may legally contain SQL punctuation,
244
+ // including `)`, `,`, etc. — those are part of the identifier, not structure.
245
+ export type IsQuotedIdentifier<S extends string> =
246
+ Trim<S> extends `"${string}"` ? true : false;
247
+
248
+ export type ExtractAlias<E extends string> =
249
+ SplitLast<Trim<E>, " as "> extends [infer Expr extends string, infer Alias extends string]
250
+ ? Alias extends ""
251
+ ? IsImplicitQuotedAlias<Trim<E>> extends true
252
+ ? Trim<E> extends `${infer IExpr} "${infer IAlias}"`
253
+ ? { expr: Trim<IExpr>; alias: CleanIdent<IAlias> }
254
+ : { expr: Trim<E>; alias: never }
255
+ : IsBareImplicitAlias<Trim<E>> extends true
256
+ ? BareImplicitAliasParts<Trim<E>> extends { expr: infer BExpr extends string; alias: infer BAlias extends string }
257
+ ? { expr: BExpr; alias: CleanIdent<BAlias> }
258
+ : { expr: Trim<E>; alias: never }
259
+ : { expr: Trim<E>; alias: never }
260
+ : IsQuotedIdentifier<Alias> extends true
261
+ ? { expr: Trim<Expr>; alias: CleanIdent<Alias> }
262
+ : Alias extends `${string})${string}`
263
+ ? { expr: Trim<E>; alias: never }
264
+ : { expr: Trim<Expr>; alias: CleanIdent<Alias> }
265
+ : { expr: Trim<E>; alias: never };
266
+
267
+ export type AliasResultKey<S extends string> =
268
+ Trim<S> extends `"${infer Q}"` ? Q : CleanIdent<S>;
269
+
270
+ export type ExtractAliasResult<E extends string> =
271
+ SplitLast<Trim<E>, " as "> extends [infer Expr extends string, infer Alias extends string]
272
+ ? Alias extends ""
273
+ ? IsImplicitQuotedAlias<Trim<E>> extends true
274
+ ? Trim<E> extends `${infer IExpr} "${infer IAlias}"`
275
+ ? { expr: Trim<IExpr>; alias: IAlias }
276
+ : { expr: Trim<E>; alias: never }
277
+ : IsBareImplicitAlias<Trim<E>> extends true
278
+ ? BareImplicitAliasParts<Trim<E>> extends { expr: infer BExpr extends string; alias: infer BAlias extends string }
279
+ ? { expr: BExpr; alias: AliasResultKey<BAlias> }
280
+ : { expr: Trim<E>; alias: never }
281
+ : { expr: Trim<E>; alias: never }
282
+ : IsQuotedIdentifier<Alias> extends true
283
+ ? { expr: Trim<Expr>; alias: AliasResultKey<Alias> }
284
+ : Alias extends `${string})${string}`
285
+ ? { expr: Trim<E>; alias: never }
286
+ : { expr: Trim<Expr>; alias: AliasResultKey<Alias> }
287
+ : { expr: Trim<E>; alias: never };
288
+