@kuindji/typed-sql 0.1.0 → 0.3.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 (57) hide show
  1. package/README.md +7 -0
  2. package/dist/builder/assemble.d.ts.map +1 -1
  3. package/dist/builder/assemble.js +9 -10
  4. package/dist/builder/assemble.js.map +1 -1
  5. package/dist/builder/conditional-sql.d.ts +7 -2
  6. package/dist/builder/conditional-sql.d.ts.map +1 -1
  7. package/dist/builder/conditional-sql.js +9 -22
  8. package/dist/builder/conditional-sql.js.map +1 -1
  9. package/dist/builder/params.d.ts +2 -1
  10. package/dist/builder/params.d.ts.map +1 -1
  11. package/dist/builder/params.js +34 -27
  12. package/dist/builder/params.js.map +1 -1
  13. package/dist/builder/select.d.ts +7 -0
  14. package/dist/builder/select.d.ts.map +1 -1
  15. package/dist/builder/select.js +7 -2
  16. package/dist/builder/select.js.map +1 -1
  17. package/dist/builder/state.d.ts +3 -6
  18. package/dist/builder/state.d.ts.map +1 -1
  19. package/dist/builder/state.js +1 -2
  20. package/dist/builder/state.js.map +1 -1
  21. package/dist/expressions.d.ts +12 -6
  22. package/dist/expressions.d.ts.map +1 -1
  23. package/dist/parsing/extract.d.ts +2 -1
  24. package/dist/parsing/extract.d.ts.map +1 -1
  25. package/dist/parsing/normalize.d.ts +10 -5
  26. package/dist/parsing/normalize.d.ts.map +1 -1
  27. package/dist/parsing/pg-literals.d.ts +2 -2
  28. package/dist/parsing/pg-literals.d.ts.map +1 -1
  29. package/dist/parsing/tokenize.d.ts +8 -6
  30. package/dist/parsing/tokenize.d.ts.map +1 -1
  31. package/dist/tables.d.ts +2 -2
  32. package/dist/tables.d.ts.map +1 -1
  33. package/dist/validation/dispatch.d.ts +3 -1
  34. package/dist/validation/dispatch.d.ts.map +1 -1
  35. package/dist/validation/return-derived.d.ts +8 -4
  36. package/dist/validation/return-derived.d.ts.map +1 -1
  37. package/dist/validation/return-derived.js.map +1 -1
  38. package/dist/validation/return-types.d.ts +1 -1
  39. package/dist/validation/return-types.d.ts.map +1 -1
  40. package/dist/validation/validate-columns.d.ts +17 -6
  41. package/dist/validation/validate-columns.d.ts.map +1 -1
  42. package/package.json +3 -2
  43. package/src/builder/assemble.ts +9 -13
  44. package/src/builder/conditional-sql.ts +9 -27
  45. package/src/builder/params.ts +33 -27
  46. package/src/builder/select.ts +19 -2
  47. package/src/builder/state.ts +4 -6
  48. package/src/expressions.ts +47 -7
  49. package/src/parsing/extract.ts +18 -4
  50. package/src/parsing/normalize.ts +143 -48
  51. package/src/parsing/pg-literals.ts +23 -12
  52. package/src/parsing/tokenize.ts +56 -23
  53. package/src/tables.ts +35 -4
  54. package/src/validation/dispatch.ts +21 -3
  55. package/src/validation/return-derived.ts +60 -4
  56. package/src/validation/return-types.ts +9 -3
  57. package/src/validation/validate-columns.ts +123 -13
@@ -60,14 +60,28 @@ export type ExprToObject<
60
60
  ? MaybeNullableRow<RowTypeForTable<ResolveTableKey<CleanIdent<T>, Tables, Aliases, S>, S>, T, Nullable>
61
61
  : ExprKey<E, Tables, Aliases, S> extends infer Key extends string | never
62
62
  ? Key extends string
63
- ? { [K in Key]: ApplyProjectionNull<ExprType<RawExpr, Tables, Aliases, S>, RawExpr, Tables, Aliases, S, Nullable> }
63
+ ? { [K in Key]: NeverToUnknown<ApplyProjectionNull<ExprType<RawExpr, Tables, Aliases, S>, RawExpr, Tables, Aliases, S, Nullable>, RawExpr> }
64
64
  : Record<string, unknown>
65
65
  : Record<string, unknown>
66
66
  : Alias extends string
67
- ? { [K in Alias]: ApplyProjectionNull<ExprType<RawExpr, Tables, Aliases, S>, RawExpr, Tables, Aliases, S, Nullable> }
67
+ ? { [K in Alias]: NeverToUnknown<ApplyProjectionNull<ExprType<RawExpr, Tables, Aliases, S>, RawExpr, Tables, Aliases, S, Nullable>, RawExpr> }
68
68
  : Record<string, unknown>
69
69
  : Record<string, unknown>;
70
70
 
71
+ // A projected QUALIFIED ref that resolves to `never` (e.g. one qualified by a
72
+ // CTE name that the core path collected as a bogus base table — `input_ips.x`
73
+ // in `WITH input_ips(...) ... SELECT input_ips.x ... JOIN ...`) must surface as
74
+ // `unknown` in the ROW type — `never` is validation's reject signal, not a value
75
+ // type, and a `never` property poisons every consumer of the row. An UNQUALIFIED
76
+ // invalid column keeps its `never` field: that visibility is deliberate and
77
+ // pinned by the adversarial cast suite (`not_a_col::text` -> `{ x: never }`).
78
+ type NeverToUnknown<T, E extends string> =
79
+ [T] extends [never]
80
+ ? E extends `${string}.${string}`
81
+ ? unknown
82
+ : T
83
+ : T;
84
+
71
85
  // Outer-join nullability for a directly-projected column. `Nullable` is the set
72
86
  // of reference qualifiers (aliases / table names) that are nullable due to an
73
87
  // outer join (see `NullableRelations`). When the projected expression is a plain
@@ -219,7 +233,14 @@ export type FunctionKeyFromExpr<E extends string> =
219
233
  export type IsBoolExpr<CE extends string> =
220
234
  CE extends `case ${string}`
221
235
  ? false
222
- : HasTopLevelCompare<CE>;
236
+ // Pre-gate: `HasTopLevelCompare`'s only `true` branch requires a
237
+ // `<`/`>`/`=`/`!` char; if `CE` contains none, the answer is `false` without
238
+ // the char-walk. Cheap template test short-circuits the common no-comparison
239
+ // projection (a bare column / function call). Must list all four chars the
240
+ // walk's true branch matches.
241
+ : CE extends `${string}${"<" | ">" | "=" | "!"}${string}`
242
+ ? HasTopLevelCompare<CE>
243
+ : false;
223
244
 
224
245
  // Scans for a comparison operator outside parens and outside `'…'`/`"…"` quotes.
225
246
  // `->>`, `#>>` and `::` are consumed as units so their `>`/`:` are not mistaken
@@ -474,13 +495,20 @@ export type ExprValid<
474
495
  E extends string,
475
496
  Tables extends string,
476
497
  Aliases extends string,
477
- S extends DatabaseSchema
498
+ S extends DatabaseSchema,
499
+ LocalRels extends string = never
478
500
  > =
479
501
  IsIgnorableRuntimeExpr<E> extends true
480
502
  ? true
481
503
  : ExtractAlias<E> extends { expr: infer RawExpr extends string }
482
504
  ? IsIgnorableRuntimeExpr<RawExpr> extends true
483
505
  ? true
506
+ // A ref qualified by a query-local relation (CTE name) has no schema
507
+ // surface to resolve against — `ExprType` yields `never` and the
508
+ // token scans reject it. Bless it (lenient contract: the local
509
+ // relation's output shape is validated where it is defined).
510
+ : HasLocalQualifier<RawExpr, LocalRels> extends true
511
+ ? true
484
512
  : ExprType<RawExpr, Tables, Aliases, S> extends never
485
513
  ? false
486
514
  : NeedsTokenRefValidation<RawExpr> extends true
@@ -488,6 +516,17 @@ export type ExprValid<
488
516
  : FuncCompoundArgsValid<RawExpr, Tables, Aliases, S>
489
517
  : true;
490
518
 
519
+ // `true` when E is a qualified ref whose qualifier names a query-local relation.
520
+ // The `[LocalRels] extends [never]` guard keeps the common no-CTE path free.
521
+ type HasLocalQualifier<E extends string, LocalRels extends string> =
522
+ [LocalRels] extends [never]
523
+ ? false
524
+ : E extends `${infer Q}.${string}`
525
+ ? CleanIdent<Q> extends LocalRels
526
+ ? true
527
+ : false
528
+ : false;
529
+
491
530
  // A function-call (or cast) projection skips the token ref-scan above
492
531
  // (`NeedsTokenRefValidation` is false for `${fn}(${args})`), which is why an
493
532
  // invalid column hidden inside an aggregate/function argument — `sum(price +
@@ -587,12 +626,13 @@ export type ExprsValidList<
587
626
  Tables extends string,
588
627
  Aliases extends string,
589
628
  S extends DatabaseSchema,
590
- Steps extends any[] = []
629
+ Steps extends any[] = [],
630
+ LocalRels extends string = never
591
631
  > = Steps["length"] extends 100
592
632
  ? true
593
633
  : Exprs extends [infer H extends string, ...infer Rest extends string[]]
594
- ? ExprValid<H, Tables, Aliases, S> extends true
595
- ? ExprsValidList<Rest, Tables, Aliases, S, [any, ...Steps]>
634
+ ? ExprValid<H, Tables, Aliases, S, LocalRels> extends true
635
+ ? ExprsValidList<Rest, Tables, Aliases, S, [any, ...Steps], LocalRels>
596
636
  : false
597
637
  : true;
598
638
 
@@ -22,7 +22,21 @@ export type ExtractSelectList<N extends string> =
22
22
  export type ExtractReturningList<N extends string> =
23
23
  FirstTopLevelReturningTail<N>;
24
24
 
25
- export type FirstTopLevelReturningTail<
25
+ // Quote-free fast-path (mirrors `HasReturningQuoteAware`): with no `'` and no `"`
26
+ // every ` returning ` is top-level, so the FIRST one's tail is exact via a single
27
+ // leftmost template match — skipping the ~1200-step walk on every quote-free DML.
28
+ // Only quote-bearing queries take the walk. The fast-path pattern matches the walk's
29
+ // own step-cap fallback, so behavior is unchanged.
30
+ export type FirstTopLevelReturningTail<S extends string> =
31
+ string extends S
32
+ ? ""
33
+ : S extends `${string}'${string}`
34
+ ? FirstTopLevelReturningTailWalk<S>
35
+ : S extends `${string}"${string}`
36
+ ? FirstTopLevelReturningTailWalk<S>
37
+ : S extends `${string} returning ${infer After}` ? After : "";
38
+
39
+ type FirstTopLevelReturningTailWalk<
26
40
  S extends string,
27
41
  InString extends boolean = false,
28
42
  InDString extends boolean = false,
@@ -33,16 +47,16 @@ export type FirstTopLevelReturningTail<
33
47
  ? S extends `${string} returning ${infer After}` ? After : ""
34
48
  : InString extends true
35
49
  ? S extends `${infer C}${infer Rest}`
36
- ? FirstTopLevelReturningTail<Rest, C extends "'" ? false : true, InDString, [any, ...Steps]>
50
+ ? FirstTopLevelReturningTailWalk<Rest, C extends "'" ? false : true, InDString, [any, ...Steps]>
37
51
  : ""
38
52
  : InDString extends true
39
53
  ? S extends `${infer C}${infer Rest}`
40
- ? FirstTopLevelReturningTail<Rest, InString, C extends `"` ? false : true, [any, ...Steps]>
54
+ ? FirstTopLevelReturningTailWalk<Rest, InString, C extends `"` ? false : true, [any, ...Steps]>
41
55
  : ""
42
56
  : S extends ` returning ${infer After}`
43
57
  ? After
44
58
  : S extends `${infer C}${infer Rest}`
45
- ? FirstTopLevelReturningTail<Rest, C extends "'" ? true : false, C extends `"` ? true : false, [any, ...Steps]>
59
+ ? FirstTopLevelReturningTailWalk<Rest, C extends "'" ? true : false, C extends `"` ? true : false, [any, ...Steps]>
46
60
  : "";
47
61
 
48
62
  // Given a string whose first non-skipped char is `(`, consume the first
@@ -24,7 +24,65 @@ import type { NeutralizePgLiterals, RewriteExtractCall, StripComments } from "./
24
24
  // length, so the "collapse first to fit the lowercaser under its cap" rationale no
25
25
  // longer requires a second collapse afterwards.
26
26
  export type NormalizeQuery<S extends string> =
27
- RewriteExtractCall<Trim<RemoveTrailingSemicolon<LowercaseOutsideQuotes<CollapseSpaces<ReplaceWhitespace<StripComments<NeutralizePgLiterals<S>>>>>>>>;
27
+ RewriteExtractCall<Trim<RemoveTrailingSemicolon<CollapseDotSpaces<LowercaseOutsideQuotes<CollapseSpaces<ReplaceWhitespace<StripComments<NeutralizePgLiterals<S>>>>>>>>>;
28
+
29
+ // Collapse whitespace around the `.` qualifier separator: `X .Y` / `X. Y` /
30
+ // `X . Y` -> `X.Y`. PostgreSQL allows whitespace around the qualifier dot
31
+ // (`tbl . col` === `tbl.col`); the normalizer's ReplaceWhitespace+CollapseSpaces
32
+ // turns a multi-line qualified ref (`tbl\n .col`) into `tbl .col`, which would
33
+ // otherwise tokenize as the orphan `.col` plus a bare `tbl` that the
34
+ // unqualified-column check then rejects as a (non-existent) column. Rejoining the
35
+ // dot lets `tbl.col` resolve normally.
36
+ //
37
+ // CHEAP PRE-GATE: the overwhelmingly common query has no spaced dot, so a single
38
+ // `" ."` / `". "` membership test short-circuits to identity in ~1 instantiation.
39
+ export type CollapseDotSpaces<S extends string> =
40
+ S extends `${string} .${string}`
41
+ ? MaybeCollapseDotSpaces<S>
42
+ : S extends `${string}. ${string}`
43
+ ? MaybeCollapseDotSpaces<S>
44
+ : S;
45
+
46
+ // Single-quoted literals are already blanked to '' by NeutralizePgLiterals, so
47
+ // only a double-quoted identifier could host a spaced dot (`"a . b"`). With no
48
+ // `"` present the quote-unaware ladder is provably safe; otherwise defer to a
49
+ // quote-aware walk that collapses dots only OUTSIDE double-quoted spans.
50
+ type MaybeCollapseDotSpaces<S extends string> =
51
+ S extends `${string}"${string}`
52
+ ? CollapseDotSpacesQuoteAware<S, false, "", []>
53
+ : CollapseDotSpacesWalk<S>;
54
+
55
+ // O(spaced dots), not O(chars): `${infer A}` jumps a whole non-matching run per
56
+ // step, so a string with no further spaced dot exits in one instantiation. Step
57
+ // cap is a runaway backstop, far under TS's recursion ceiling.
58
+ type CollapseDotSpacesWalk<S extends string, Steps extends any[] = []> =
59
+ Steps["length"] extends 400
60
+ ? S
61
+ : S extends `${infer A} . ${infer B}`
62
+ ? CollapseDotSpacesWalk<`${A}.${B}`, [any, ...Steps]>
63
+ : S extends `${infer A} .${infer B}`
64
+ ? CollapseDotSpacesWalk<`${A}.${B}`, [any, ...Steps]>
65
+ : S extends `${infer A}. ${infer B}`
66
+ ? CollapseDotSpacesWalk<`${A}.${B}`, [any, ...Steps]>
67
+ : S;
68
+
69
+ // Quote-aware tier: copy `"..."` identifier spans verbatim, collapse spaced dots
70
+ // only in the runs OUTSIDE them, so a column literally named `"a . b"` is never
71
+ // corrupted. Depth is O(double-quote boundaries).
72
+ type CollapseDotSpacesQuoteAware<
73
+ S extends string,
74
+ InDQ extends boolean,
75
+ Acc extends string,
76
+ Steps extends any[]
77
+ > = Steps["length"] extends 400
78
+ ? `${Acc}${S}`
79
+ : InDQ extends true
80
+ ? S extends `${infer P}"${infer R}`
81
+ ? CollapseDotSpacesQuoteAware<R, false, `${Acc}${P}"`, [any, ...Steps]>
82
+ : `${Acc}${S}`
83
+ : S extends `${infer P}"${infer R}`
84
+ ? CollapseDotSpacesQuoteAware<R, true, `${Acc}${CollapseDotSpacesWalk<P>}"`, [any, ...Steps]>
85
+ : `${Acc}${CollapseDotSpacesWalk<S>}`;
28
86
 
29
87
  // Quote-aware lowercasing: SQL keywords/identifiers are case-insensitive, but
30
88
  // single-quoted string literals and double-quoted identifiers keep their exact
@@ -47,6 +105,20 @@ type LowercaseOutsideQuotesDrive<R> =
47
105
  ? LowercaseOutsideQuotesDrive<LowercaseOutsideQuotesWorker<S, Q1, Q2, Acc, []>>
48
106
  : R;
49
107
 
108
+ // Segment-jump, not per-char. Each step advances a whole quote-bounded run:
109
+ // outside quotes it jumps to the LEFTMOST of `'`/`"`, lowercasing the run before
110
+ // it in a single `Lowercase<…>` intrinsic; inside a quote it copies verbatim to
111
+ // the matching close-quote. Cost is O(quote boundaries), not O(chars) — the old
112
+ // per-char walk emitted one instantiation per character on every NormalizeQuery.
113
+ // The `Steps` cap now counts JUMPS (a handful even for report-scale queries), so
114
+ // 450 is far past any real query yet still yields `{ __c }` for the driver before
115
+ // TS's recursion ceiling on a pathologically quote-dense input.
116
+ //
117
+ // Exact-equivalent to the walk it replaces: `''` escapes toggle single-quote
118
+ // state twice (exit on the first `'`, the second is re-seen outside with an empty
119
+ // prefix and re-enters); an unterminated quote at EOF copies the rest verbatim
120
+ // (`${Acc}${S}`); a `"` inside a single-quoted run never flips state (the
121
+ // in-single branch only scans for `'`, and vice-versa).
50
122
  type LowercaseOutsideQuotesWorker<
51
123
  S extends string,
52
124
  InSingleQuote extends boolean,
@@ -55,25 +127,26 @@ type LowercaseOutsideQuotesWorker<
55
127
  Steps extends any[]
56
128
  > = Steps["length"] extends 450
57
129
  ? { __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;
130
+ : InSingleQuote extends true
131
+ ? S extends `${infer P}'${infer R}`
132
+ ? LowercaseOutsideQuotesWorker<R, false, InDoubleQuote, `${Acc}${P}'`, [any, ...Steps]>
133
+ : `${Acc}${S}`
134
+ : InDoubleQuote extends true
135
+ ? S extends `${infer P}"${infer R}`
136
+ ? LowercaseOutsideQuotesWorker<R, InSingleQuote, false, `${Acc}${P}"`, [any, ...Steps]>
137
+ : `${Acc}${S}`
138
+ : S extends `${infer P}'${infer R}`
139
+ ? P extends `${string}"${string}`
140
+ // a `"` precedes the first `'` → the double quote is leftmost
141
+ ? S extends `${infer P2}"${infer R2}`
142
+ ? LowercaseOutsideQuotesWorker<R2, InSingleQuote, true, `${Acc}${Lowercase<P2>}"`, [any, ...Steps]>
143
+ : `${Acc}${Lowercase<S>}`
144
+ // `'` is the leftmost quote
145
+ : LowercaseOutsideQuotesWorker<R, true, InDoubleQuote, `${Acc}${Lowercase<P>}'`, [any, ...Steps]>
146
+ // no `'` remains only a `"` could open a verbatim run
147
+ : S extends `${infer P2}"${infer R2}`
148
+ ? LowercaseOutsideQuotesWorker<R2, InSingleQuote, true, `${Acc}${Lowercase<P2>}"`, [any, ...Steps]>
149
+ : `${Acc}${Lowercase<S>}`;
77
150
 
78
151
  // ---------------------------------------------------------------------------
79
152
  // Param-name-preserving lowercaser (write/raw builder path only).
@@ -145,6 +218,15 @@ type ReadParamIdent<S extends string, Acc extends string = "", N extends any[] =
145
218
  : ReadParamIdent<R, `${Acc}${C}`, [any, ...N]>
146
219
  : { name: Acc; rest: S };
147
220
 
221
+ // Segment-jump sibling of `LowercaseOutsideQuotesWorker`, with the extra
222
+ // outside-quote rule that a lone `:` opens a case-PRESERVED `:name` param and
223
+ // `::` is a cast unit. Outside quotes it is therefore a leftmost-of-THREE jump
224
+ // (`'` / `"` / `:`): split on the first `:`; if a quote occurs in the run BEFORE
225
+ // it, the quote is leftmost so defer to `LcKeepQuoteJump` (the same leftmost-of-2
226
+ // quote logic as the plain worker); otherwise the colon is leftmost — lowercase
227
+ // the run, then consume `::` or read the verbatim param ident, exactly as the old
228
+ // per-char branch did. In/out-of-quote verbatim copies and the `''`/unterminated
229
+ // edges match `LowercaseOutsideQuotesWorker`.
148
230
  type LcKeepWorker<
149
231
  S extends string,
150
232
  InSingleQuote extends boolean,
@@ -153,38 +235,51 @@ type LcKeepWorker<
153
235
  Steps extends any[]
154
236
  > = Steps["length"] extends 450
155
237
  ? { __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;
238
+ : InSingleQuote extends true
239
+ ? S extends `${infer P}'${infer R}`
240
+ ? LcKeepWorker<R, false, InDoubleQuote, `${Acc}${P}'`, [any, ...Steps]>
241
+ : `${Acc}${S}`
242
+ : InDoubleQuote extends true
243
+ ? S extends `${infer P}"${infer R}`
244
+ ? LcKeepWorker<R, InSingleQuote, false, `${Acc}${P}"`, [any, ...Steps]>
245
+ : `${Acc}${S}`
246
+ : S extends `${infer Pc}:${infer Rc}`
247
+ // a quote before the first `:` → the quote is leftmost
248
+ ? Pc extends `${string}'${string}`
249
+ ? LcKeepQuoteJump<S, InSingleQuote, InDoubleQuote, Acc, Steps>
250
+ : Pc extends `${string}"${string}`
251
+ ? LcKeepQuoteJump<S, InSingleQuote, InDoubleQuote, Acc, Steps>
252
+ // colon is leftmost: `::` cast unit, else verbatim `:name`
253
+ : Rc extends `:${infer R2}`
254
+ ? LcKeepWorker<R2, InSingleQuote, InDoubleQuote, `${Acc}${Lowercase<Pc>}::`, [any, ...Steps]>
255
+ : ReadParamIdent<Rc> extends { name: infer Nm extends string; rest: infer Rr extends string }
256
+ ? LcKeepWorker<Rr, InSingleQuote, InDoubleQuote, `${Acc}${Lowercase<Pc>}:${Nm}`, [any, ...Steps]>
257
+ : LcKeepWorker<Rc, InSingleQuote, InDoubleQuote, `${Acc}${Lowercase<Pc>}:`, [any, ...Steps]>
258
+ // no `:` remains → only quotes (or nothing) left
259
+ : LcKeepQuoteJump<S, InSingleQuote, InDoubleQuote, Acc, Steps>;
260
+
261
+ // Leftmost-of-2 quote jump (identical shape to the plain worker's outside-quote
262
+ // branch) that hands the continuation back to `LcKeepWorker`.
263
+ type LcKeepQuoteJump<
264
+ S extends string,
265
+ InSingleQuote extends boolean,
266
+ InDoubleQuote extends boolean,
267
+ Acc extends string,
268
+ Steps extends any[]
269
+ > = S extends `${infer P}'${infer R}`
270
+ ? P extends `${string}"${string}`
271
+ ? S extends `${infer P2}"${infer R2}`
272
+ ? LcKeepWorker<R2, InSingleQuote, true, `${Acc}${Lowercase<P2>}"`, [any, ...Steps]>
273
+ : `${Acc}${Lowercase<S>}`
274
+ : LcKeepWorker<R, true, InDoubleQuote, `${Acc}${Lowercase<P>}'`, [any, ...Steps]>
275
+ : S extends `${infer P2}"${infer R2}`
276
+ ? LcKeepWorker<R2, InSingleQuote, true, `${Acc}${Lowercase<P2>}"`, [any, ...Steps]>
277
+ : `${Acc}${Lowercase<S>}`;
183
278
 
184
279
  // NormalizeQuery variant that preserves `:name` param case — used by the
185
280
  // write/raw builder param extraction (ExtractParams) only.
186
281
  export type NormalizeQueryKeepParams<S extends string> =
187
- RewriteExtractCall<Trim<RemoveTrailingSemicolon<LowercaseOutsideQuotesKeepParams<CollapseSpaces<ReplaceWhitespace<StripComments<NeutralizePgLiterals<S>>>>>>>>;
282
+ RewriteExtractCall<Trim<RemoveTrailingSemicolon<CollapseDotSpaces<LowercaseOutsideQuotesKeepParams<CollapseSpaces<ReplaceWhitespace<StripComments<NeutralizePgLiterals<S>>>>>>>>>;
188
283
 
189
284
  // Convert every `\n` / `\t` / `\r` to a space. OCCURRENCE-based (like
190
285
  // `CollapseSpaces`): each step splits at the FIRST remaining line break, so the
@@ -206,15 +206,19 @@ export type RewriteExtractRewriteOne<Pre extends string, AfterOpen extends strin
206
206
  : `${Pre} extract(${AfterOpen}`;
207
207
 
208
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.
209
+ // an unterminated single-quoted string literal. Marker-jump: each step hops to the
210
+ // next `'` (the `${infer _Pre}` skips a whole run of non-quote chars at once) and
211
+ // flips the parity, so the depth is the NUMBER OF QUOTES — a handful — not the
212
+ // string length. `${infer _Pre}'${infer R}` matches the LEFTMOST `'`, so quotes are
213
+ // counted in order, exactly as the old per-char toggle did. Bounded against a
214
+ // pathological quote-dense string; on bail returns the best-effort parity so far.
211
215
  export type OddSingleQuotes<S extends string, Flag extends boolean = false, Steps extends any[] = []> =
212
216
  string extends S
213
217
  ? false
214
218
  : Steps["length"] extends 400
215
219
  ? Flag
216
- : S extends `${infer C}${infer R}`
217
- ? OddSingleQuotes<R, C extends "'" ? (Flag extends true ? false : true) : Flag, [any, ...Steps]>
220
+ : S extends `${infer _Pre}'${infer R}`
221
+ ? OddSingleQuotes<R, Flag extends true ? false : true, [any, ...Steps]>
218
222
  : Flag;
219
223
 
220
224
  // Strip `/* ... */` block comments AND `-- ...` line comments before any other
@@ -277,13 +281,20 @@ export type StripCommentsWalk<
277
281
 
278
282
  // Skip a line comment body, returning the tail starting at the first newline
279
283
  // (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]>
284
+ // that runs to the end of the string yields `""`.
285
+ //
286
+ // Marker-jump: locate the first `\n`/`\r` with template matching instead of a
287
+ // per-char walk. `${infer Pre}\n${infer After}` finds the LEFTMOST `\n`; if its
288
+ // prefix `Pre` itself holds a `\r`, that `\r` is the earlier newline, so the tail
289
+ // starts there (`\r${PreB}\n${After}`). With no `\n`, fall back to the leftmost
290
+ // `\r`; with neither, the comment runs to EOF → `""`. No recursion (and so no step
291
+ // cap): a 1000-char single-line comment is now ~2 template instantiations.
292
+ export type LineCommentTail<S extends string> =
293
+ S extends `${infer Pre}\n${infer After}`
294
+ ? Pre extends `${infer _PreA}\r${infer PreB}`
295
+ ? `\r${PreB}\n${After}`
296
+ : `\n${After}`
297
+ : S extends `${infer _P}\r${infer After}`
298
+ ? `\r${After}`
288
299
  : "";
289
300
 
@@ -1,9 +1,9 @@
1
1
  // Tokenization, sentinels, operators, and SQL keyword sets.
2
- import type { CollapseSpaces, FilterEmpty, MapClean, MapCleanLoose, ReplaceAll, Split } from "./string-utils.js";
2
+ import type { CleanIdent, CleanLooseToken, CollapseSpaces, ReplaceAll, Split, Trim, TrimPunctuation } from "./string-utils.js";
3
3
  import type { ExceedsLengthBudget, HasLineBreaks } from "./normalize.js";
4
4
  // Tokenization & parsing helpers
5
5
 
6
- export type Tokenize<N extends string> = FilterEmpty<MapClean<Split<N, " ">>>;
6
+ export type Tokenize<N extends string> = CleanFilterTokens<Split<N, " ">>;
7
7
 
8
8
  // Sentinel token standing in for a TOP-LEVEL comma. It survives `MapClean`
9
9
  // (no stripped punctuation, non-empty identifier) whereas a bare `,` does not,
@@ -64,12 +64,12 @@ export type TokenizeTables<N extends string> =
64
64
  ? Tokenize<N>
65
65
  : ExceedsLengthBudget<N> extends true
66
66
  ? Tokenize<N>
67
- : FilterEmpty<MapClean<RestoreDQuotedSpaces<Split<MaybeMarkDQuotedSpaces<MarkTopLevelCommas<N>>, " ">>>>;
67
+ : RestoreCleanFilterTokens<Split<MaybeMarkDQuotedSpaces<MarkTopLevelCommas<N>>, " ">>;
68
68
 
69
69
  export type TokenizeLoose<N extends string> =
70
- FilterEmpty<MapCleanLoose<RestoreDQuotedSpaces<
70
+ RestoreCleanLooseFilterTokens<
71
71
  Split<CollapseSpaces<RestoreWildcards<PadOperators<ProtectWildcards<MaybeMarkDQuotedSpaces<MaybeStripDQuotedPunct<N>>>>>>, " ">
72
- >>> extends infer Toks extends string[]
72
+ > extends infer Toks extends string[]
73
73
  ? N extends `${string}distinct ${string}`
74
74
  ? DropDistinctFrom<Toks>
75
75
  : Toks
@@ -168,12 +168,45 @@ export type MarkDQuotedSpaces<
168
168
  : MarkDQuotedSpaces<Rest, InDQ, `${Acc}${C}`, [any, ...Steps]>
169
169
  : Acc;
170
170
 
171
- // Restore the space sentinel to a real space in each token of a token list, so a
172
- // quoted identifier that survived the space-split as one token (`"Order ID"`,
173
- // `"user alias".id`) is cleaned to its true value (`order id`, `"user alias".id`).
174
- export type RestoreDQuotedSpaces<Tokens extends string[], Acc extends string[] = []> =
171
+ // Fused token post-passes: one walk instead of the old
172
+ // `FilterEmpty<MapClean<RestoreDQuotedSpaces<…>>>` three-walk chain. Each pass was
173
+ // an independent element-wise map/filter, so composing them per token yields the
174
+ // identical list (ordering preserved) while building the result spine once.
175
+ //
176
+ // The DQuote-space sentinel restore (`ReplaceAll<H, DQuoteSpaceSentinel, " ">`) lets
177
+ // a quoted identifier that survived the space-split as one token (`"Order ID"`,
178
+ // `"user alias".id`) clean to its true value. `CleanFilterTokens` is the no-restore
179
+ // variant (plain `Tokenize`, which never marks sentinels).
180
+ //
181
+ // MapClean maps each token to `CleanIdent<H> extends "" ? "" : TrimPunctuation<Trim<H>>`
182
+ // and FilterEmpty drops the `""`s. Since `CleanIdent = Lowercase<Unquote<TrimPunctuation<
183
+ // Trim<S>>>>`, a non-empty `CleanIdent<H>` guarantees a non-empty `TrimPunctuation<Trim<H>>`,
184
+ // so the kept value is never empty — the empty-token filter collapses to the single
185
+ // `CleanIdent<H> extends ""` test. (The loose variant keeps an explicit empty filter
186
+ // because `CleanLooseToken` can return `""` for a non-operator empty ident.)
187
+ export type CleanFilterTokens<Tokens extends string[], Acc extends string[] = []> =
175
188
  Tokens extends [infer H extends string, ...infer R extends string[]]
176
- ? RestoreDQuotedSpaces<R, [...Acc, ReplaceAll<H, DQuoteSpaceSentinel, " ">]>
189
+ ? CleanIdent<H> extends ""
190
+ ? CleanFilterTokens<R, Acc>
191
+ : CleanFilterTokens<R, [...Acc, TrimPunctuation<Trim<H>>]>
192
+ : Acc;
193
+
194
+ export type RestoreCleanFilterTokens<Tokens extends string[], Acc extends string[] = []> =
195
+ Tokens extends [infer H0 extends string, ...infer R extends string[]]
196
+ ? ReplaceAll<H0, DQuoteSpaceSentinel, " "> extends infer H extends string
197
+ ? CleanIdent<H> extends ""
198
+ ? RestoreCleanFilterTokens<R, Acc>
199
+ : RestoreCleanFilterTokens<R, [...Acc, TrimPunctuation<Trim<H>>]>
200
+ : never
201
+ : Acc;
202
+
203
+ export type RestoreCleanLooseFilterTokens<Tokens extends string[], Acc extends string[] = []> =
204
+ Tokens extends [infer H0 extends string, ...infer R extends string[]]
205
+ ? CleanLooseToken<ReplaceAll<H0, DQuoteSpaceSentinel, " ">> extends infer M extends string
206
+ ? M extends ""
207
+ ? RestoreCleanLooseFilterTokens<R, Acc>
208
+ : RestoreCleanLooseFilterTokens<R, [...Acc, M]>
209
+ : never
177
210
  : Acc;
178
211
 
179
212
  // A validation-only view of a query: blank the CONTENTS of every single-quoted
@@ -198,26 +231,26 @@ export type ValidationScanView<S extends string> =
198
231
  ? MaybeMarkDQuotedSpaces<BlankSingleQuotedLiterals<S>>
199
232
  : MaybeMarkDQuotedSpaces<S>;
200
233
 
234
+ // Pairwise marker-jump: hop to the opening `'`, then to its closing `'`, emitting
235
+ // the verbatim prefix plus a blanked body `''`, and recurse on the tail. The `''`
236
+ // SQL escape pairs LEFTMOST exactly as the old per-char toggle did (`'it''s'` →
237
+ // `''''`); an UNTERMINATED opener (no closing `'`) is closed off with an appended
238
+ // `'`, matching the old EOF-in-string branch (`${Acc}'`), so `…'xyz` → `…''`. Depth
239
+ // is now the NUMBER OF LITERALS (a handful), not the string length (≤600 before).
240
+ // Step cap retained purely as a runaway backstop for a pathological quote storm.
201
241
  export type BlankSingleQuotedLiterals<
202
242
  S extends string,
203
- InString extends boolean = false,
204
243
  Acc extends string = "",
205
244
  Steps extends any[] = []
206
245
  > = string extends S
207
246
  ? S
208
- : Steps["length"] extends 600
247
+ : Steps["length"] extends 300
209
248
  ? `${Acc}${S}`
210
- : InString extends true
211
- ? S extends `${infer C}${infer R}`
212
- ? C extends "'"
213
- ? BlankSingleQuotedLiterals<R, false, `${Acc}'`, [any, ...Steps]>
214
- : BlankSingleQuotedLiterals<R, true, Acc, [any, ...Steps]>
215
- : `${Acc}'`
216
- : S extends `${infer C}${infer R}`
217
- ? C extends "'"
218
- ? BlankSingleQuotedLiterals<R, true, `${Acc}'`, [any, ...Steps]>
219
- : BlankSingleQuotedLiterals<R, false, `${Acc}${C}`, [any, ...Steps]>
220
- : Acc;
249
+ : S extends `${infer Pre}'${infer Rest}`
250
+ ? Rest extends `${infer _Lit}'${infer After}`
251
+ ? BlankSingleQuotedLiterals<After, `${Acc}${Pre}''`, [any, ...Steps]>
252
+ : `${Acc}${Pre}''`
253
+ : `${Acc}${S}`;
221
254
 
222
255
  export type OperatorToken =
223
256
  | "(" | ")" | "," | "=" | "<" | ">" | "+" | "-" | "*" | "/" | "|" | "&" | "!" | "?"
package/src/tables.ts CHANGED
@@ -70,7 +70,17 @@ export type CollectTables<
70
70
  // the bare `lateral` nor the function name is mistaken for a table.
71
71
  ? Next extends "lateral"
72
72
  ? CollectTables<Rest, S, Acc, true, InDelete>
73
- : CollectTables<Rest, S, Acc | TableKeyFromToken<Next, S>, true, InDelete>
73
+ // A parenthesised FROM/JOIN source a subquery (`from (select ...)`)
74
+ // or VALUES list (`from (values ...)`) — has its `(` stripped by
75
+ // `TokenizeTables`, leaving a leading SQL keyword (`select`/`values`)
76
+ // as the source token. That keyword is NOT a base table; collecting it
77
+ // fabricates a bogus `public.select`/`public.values` key that fails the
78
+ // existence check. A real (unquoted) table is never a SQL keyword, so
79
+ // skipping keyword sources here is safe; the source's `) AS alias` is
80
+ // handled leniently in qualified-ref validation.
81
+ : Next extends SqlKeyword
82
+ ? CollectTables<Rest, S, Acc, true, InDelete>
83
+ : CollectTables<Rest, S, Acc | TableKeyFromToken<Next, S>, true, InDelete>
74
84
  : T extends "update"
75
85
  ? Next extends "set"
76
86
  ? CollectTables<Rest, S, Acc, false, InDelete>
@@ -87,7 +97,16 @@ export type CollectTables<
87
97
  : CollectTables<[Next, ...Rest], S, Acc, InList, InDelete>
88
98
  : T extends CommaSep
89
99
  ? InList extends true
90
- ? CollectTables<Rest, S, Acc | TableKeyFromToken<Next, S>, true, InDelete>
100
+ // `UPDATE t SET a = (select ... from x), b = ...` — the
101
+ // subquery's parens are stripped by tokenization, so its
102
+ // `from x` leaves `InList` on and the TOP-LEVEL SET comma
103
+ // would collect the next SET column (`b`) as a table. A
104
+ // FROM-source name is never followed by `=`, so a comma
105
+ // whose candidate source is followed by `=` is a SET-list
106
+ // separator: skip it and leave source-list mode.
107
+ ? Rest extends ["=", ...infer Rest2 extends string[]]
108
+ ? CollectTables<Rest2, S, Acc, false, InDelete>
109
+ : CollectTables<Rest, S, Acc | TableKeyFromToken<Next, S>, true, InDelete>
91
110
  : CollectTables<[Next, ...Rest], S, Acc, false, InDelete>
92
111
  : T extends "as"
93
112
  ? CollectTables<[Next, ...Rest], S, Acc, InList, InDelete>
@@ -125,14 +144,26 @@ export type CollectAliases<
125
144
  // modifier so it is never parsed as an aliased table source.
126
145
  ? Next extends "lateral"
127
146
  ? CollectAliases<Rest, S, Acc, true, InDelete>
128
- : ParseAliasSource<Next, Rest, S, Acc, InDelete>
147
+ // Mirror `CollectTables`: a parenthesised subquery/VALUES source has
148
+ // its `(` stripped, leaving a leading SQL keyword token. It is not a
149
+ // base-table source, so skip it rather than register a garbage alias
150
+ // from the keyword + the next token.
151
+ : Next extends SqlKeyword
152
+ ? CollectAliases<Rest, S, Acc, true, InDelete>
153
+ : ParseAliasSource<Next, Rest, S, Acc, InDelete>
129
154
  : T extends "using"
130
155
  ? InDelete extends true
131
156
  ? ParseAliasSource<Next, Rest, S, Acc, InDelete>
132
157
  : CollectAliases<[Next, ...Rest], S, Acc, InList, InDelete>
133
158
  : T extends CommaSep
134
159
  ? InList extends true
135
- ? ParseAliasSource<Next, Rest, S, Acc, InDelete>
160
+ // Mirror `CollectTables`: a comma whose candidate source is
161
+ // followed by `=` is an UPDATE SET-list separator (the SET
162
+ // subquery's `from` left `InList` on), not another aliased
163
+ // FROM source.
164
+ ? Rest extends ["=", ...infer Rest2 extends string[]]
165
+ ? CollectAliases<Rest2, S, Acc, false, InDelete>
166
+ : ParseAliasSource<Next, Rest, S, Acc, InDelete>
136
167
  : CollectAliases<[Next, ...Rest], S, Acc, false, InDelete>
137
168
  : T extends "as"
138
169
  ? CollectAliases<[Next, ...Rest], S, Acc, InList, InDelete>