@kuindji/typed-sql 0.2.0 → 0.4.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 (48) hide show
  1. package/README.md +11 -3
  2. package/dist/columns.d.ts +11 -3
  3. package/dist/columns.d.ts.map +1 -1
  4. package/dist/expressions.d.ts +84 -13
  5. package/dist/expressions.d.ts.map +1 -1
  6. package/dist/parsing/extract.d.ts +13 -9
  7. package/dist/parsing/extract.d.ts.map +1 -1
  8. package/dist/parsing/normalize.d.ts +9 -3
  9. package/dist/parsing/normalize.d.ts.map +1 -1
  10. package/dist/parsing/pg-literals.d.ts +10 -2
  11. package/dist/parsing/pg-literals.d.ts.map +1 -1
  12. package/dist/parsing/split.d.ts +27 -3
  13. package/dist/parsing/split.d.ts.map +1 -1
  14. package/dist/parsing/string-utils.d.ts +2 -4
  15. package/dist/parsing/string-utils.d.ts.map +1 -1
  16. package/dist/parsing/tokenize.d.ts +27 -17
  17. package/dist/parsing/tokenize.d.ts.map +1 -1
  18. package/dist/partial.d.ts +6 -6
  19. package/dist/partial.d.ts.map +1 -1
  20. package/dist/tables.d.ts +58 -13
  21. package/dist/tables.d.ts.map +1 -1
  22. package/dist/validation/dispatch.d.ts +7 -5
  23. package/dist/validation/dispatch.d.ts.map +1 -1
  24. package/dist/validation/joins.d.ts +3 -3
  25. package/dist/validation/joins.d.ts.map +1 -1
  26. package/dist/validation/return-derived.d.ts +8 -4
  27. package/dist/validation/return-derived.d.ts.map +1 -1
  28. package/dist/validation/return-derived.js.map +1 -1
  29. package/dist/validation/return-types.d.ts +1 -1
  30. package/dist/validation/return-types.d.ts.map +1 -1
  31. package/dist/validation/validate-columns.d.ts +27 -16
  32. package/dist/validation/validate-columns.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/columns.ts +168 -32
  35. package/src/expressions.ts +589 -63
  36. package/src/parsing/extract.ts +72 -32
  37. package/src/parsing/normalize.ts +114 -10
  38. package/src/parsing/pg-literals.ts +32 -10
  39. package/src/parsing/split.ts +236 -72
  40. package/src/parsing/string-utils.ts +15 -15
  41. package/src/parsing/tokenize.ts +224 -146
  42. package/src/partial.ts +9 -15
  43. package/src/tables.ts +546 -183
  44. package/src/validation/dispatch.ts +58 -52
  45. package/src/validation/joins.ts +15 -19
  46. package/src/validation/return-derived.ts +60 -4
  47. package/src/validation/return-types.ts +9 -3
  48. package/src/validation/validate-columns.ts +161 -67
@@ -3,9 +3,9 @@ import type {
3
3
  ColumnRef,
4
4
  ColumnRefValidLooseWith,
5
5
  ParseColumnRef,
6
- QualifiedColumnRefs,
6
+ QualifiedRefScan,
7
7
  ResolveTableKey,
8
- UnqualifiedColumnRefs,
8
+ UnqualifiedRefScan,
9
9
  UnqualifiedColumnValid
10
10
  } from "./columns.js";
11
11
  import type { AliasesInQuery, TablesInQuery } from "./tables.js";
@@ -23,7 +23,6 @@ import type {
23
23
  SqlConstantType,
24
24
  SplitBalancedParen,
25
25
  SplitTopLevel,
26
- TokenizeLoose,
27
26
  Trim
28
27
  } from "./parsing.js";
29
28
  import type { AllTrue } from "./utils.js";
@@ -60,14 +59,28 @@ export type ExprToObject<
60
59
  ? MaybeNullableRow<RowTypeForTable<ResolveTableKey<CleanIdent<T>, Tables, Aliases, S>, S>, T, Nullable>
61
60
  : ExprKey<E, Tables, Aliases, S> extends infer Key extends string | never
62
61
  ? Key extends string
63
- ? { [K in Key]: ApplyProjectionNull<ExprType<RawExpr, Tables, Aliases, S>, RawExpr, Tables, Aliases, S, Nullable> }
62
+ ? { [K in Key]: NeverToUnknown<ApplyProjectionNull<ExprType<RawExpr, Tables, Aliases, S>, RawExpr, Tables, Aliases, S, Nullable>, RawExpr> }
64
63
  : Record<string, unknown>
65
64
  : Record<string, unknown>
66
65
  : Alias extends string
67
- ? { [K in Alias]: ApplyProjectionNull<ExprType<RawExpr, Tables, Aliases, S>, RawExpr, Tables, Aliases, S, Nullable> }
66
+ ? { [K in Alias]: NeverToUnknown<ApplyProjectionNull<ExprType<RawExpr, Tables, Aliases, S>, RawExpr, Tables, Aliases, S, Nullable>, RawExpr> }
68
67
  : Record<string, unknown>
69
68
  : Record<string, unknown>;
70
69
 
70
+ // A projected QUALIFIED ref that resolves to `never` (e.g. one qualified by a
71
+ // CTE name that the core path collected as a bogus base table — `input_ips.x`
72
+ // in `WITH input_ips(...) ... SELECT input_ips.x ... JOIN ...`) must surface as
73
+ // `unknown` in the ROW type — `never` is validation's reject signal, not a value
74
+ // type, and a `never` property poisons every consumer of the row. An UNQUALIFIED
75
+ // invalid column keeps its `never` field: that visibility is deliberate and
76
+ // pinned by the adversarial cast suite (`not_a_col::text` -> `{ x: never }`).
77
+ type NeverToUnknown<T, E extends string> =
78
+ [T] extends [never]
79
+ ? E extends `${string}.${string}`
80
+ ? unknown
81
+ : T
82
+ : T;
83
+
71
84
  // Outer-join nullability for a directly-projected column. `Nullable` is the set
72
85
  // of reference qualifiers (aliases / table names) that are nullable due to an
73
86
  // outer join (see `NullableRelations`). When the projected expression is a plain
@@ -130,7 +143,95 @@ export type ApplyProjectionNull<
130
143
  ? CoalesceAllArgsNullable<SplitTopLevel<Args>, Tables, Aliases, S, Nullable> extends true
131
144
  ? T | null
132
145
  : T
133
- : ApplyJoinNull<T, E, Nullable>;
146
+ : [Nullable] extends [never]
147
+ ? T
148
+ : [T] extends [never]
149
+ ? ApplyJoinNull<T, E, Nullable>
150
+ : [T] extends [number | null]
151
+ ? E extends `${string}${"+" | "-" | "*" | "/" | "%"}${string}`
152
+ ? ArithRefJoinNullable<E, Tables, Aliases, S, Nullable> extends true
153
+ ? T | null
154
+ : ApplyJoinNull<T, E, Nullable>
155
+ : ApplyJoinNull<T, E, Nullable>
156
+ : ApplyJoinNull<T, E, Nullable>;
157
+
158
+ // Outer-join nullability for a TOP-LEVEL ARITHMETIC projection (`A op B`).
159
+ // SQL NULL arithmetic is NULL, so the result is nullable when ANY operand is
160
+ // sourced from the nullable side of an outer join. `RefQualifier` cannot see
161
+ // operand refs (an arithmetic expression is not a plain column ref — or worse,
162
+ // its leftmost dot fakes one: `u.id + o.total` "qualifies" as `u`), so this
163
+ // walks the operands the same way the arithmetic TYPING did: split at the
164
+ // top-level operator and recurse each side. Only consulted when the projection
165
+ // already typed `number`/`number | null` (the arithmetic result types), under
166
+ // a non-empty `Nullable` set, with an operator char present — join-free
167
+ // queries and plain projections pay nothing. A `false` verdict falls back to
168
+ // `ApplyJoinNull`, so a non-arithmetic expression that slips past the op-char
169
+ // gate (e.g. a quoted-punct ref like `"u-1".id`) keeps its plain-ref handling.
170
+ type ArithRefJoinNullable<
171
+ E extends string,
172
+ Tables extends string,
173
+ Aliases extends string,
174
+ S extends DatabaseSchema,
175
+ Nullable extends string,
176
+ Steps extends any[] = []
177
+ > =
178
+ Steps["length"] extends 8
179
+ ? false
180
+ : UnwrapRedundantParens<Trim<E>> extends infer SC extends string
181
+ ? SC extends `${string}${"+" | "-" | "*" | "/" | "%"}${string}`
182
+ ? SplitTopLevelOp<SC> extends infer SR
183
+ ? [SR] extends [never]
184
+ ? ArithOperandJoinNullable<SC, Tables, Aliases, S, Nullable>
185
+ : SR extends { __op: [infer L extends string, infer Op extends string, infer R extends string] }
186
+ ? Op extends "||"
187
+ ? false
188
+ : ArithRefJoinNullable<Trim<L>, Tables, Aliases, S, Nullable, [any, ...Steps]> extends true
189
+ ? true
190
+ : ArithRefJoinNullable<Trim<R>, Tables, Aliases, S, Nullable, [any, ...Steps]>
191
+ : ArithOperandJoinNullable<SC, Tables, Aliases, S, Nullable>
192
+ : false
193
+ : ArithOperandJoinNullable<SC, Tables, Aliases, S, Nullable>
194
+ : false;
195
+
196
+ // A LEAF arithmetic operand (no top-level operator left). A whole-operand
197
+ // `coalesce(...)` keeps its all-args-nullable semantics (`coalesce(o.x, 0)`
198
+ // stays non-null even on the nullable side). A function-call operand
199
+ // (`sum(o.total)`) is conservatively nullable when any nullable-side
200
+ // qualified ref appears inside it — an all-NULL group aggregates to NULL.
201
+ // A plain ref consults its qualifier; literals and params stay non-null.
202
+ type ArithOperandJoinNullable<
203
+ SC extends string,
204
+ Tables extends string,
205
+ Aliases extends string,
206
+ S extends DatabaseSchema,
207
+ Nullable extends string
208
+ > =
209
+ CleanExpr<StripOuterCast<SC>> extends `coalesce(${infer Args})`
210
+ ? CoalesceAllArgsNullable<SplitTopLevel<Args>, Tables, Aliases, S, Nullable>
211
+ : SC extends `${string}(${string}`
212
+ ? NullableQualRefIn<SC, Nullable>
213
+ : RefQualifier<SC> extends infer Q extends string
214
+ ? [Q] extends [never]
215
+ ? false
216
+ : Q extends Nullable
217
+ ? true
218
+ : false
219
+ : false;
220
+
221
+ // True when a `<Q>.`-qualified ref appears in `E` for any nullable qualifier
222
+ // `Q` — at the start, or after a boundary char that cannot be part of an
223
+ // identifier (so alias-suffix lookalikes like `po.x` never match `o`).
224
+ type NullableQualRefIn<E extends string, Nullable extends string> =
225
+ true extends (Nullable extends string ? QualRefIn<E, Nullable> : never)
226
+ ? true
227
+ : false;
228
+
229
+ type QualRefIn<E extends string, Q extends string> =
230
+ E extends `${Q}.${string}`
231
+ ? true
232
+ : E extends `${string}${" " | "(" | "," | "+" | "-" | "*" | "/" | "%"}${Q}.${string}`
233
+ ? true
234
+ : false;
134
235
 
135
236
  // True only when every coalesce argument is nullable. An empty/exhausted list is
136
237
  // vacuously `true`, but the wrapper above only reaches this for a real coalesce call
@@ -229,57 +330,452 @@ export type IsBoolExpr<CE extends string> =
229
330
  : false;
230
331
 
231
332
  // Scans for a comparison operator outside parens and outside `'…'`/`"…"` quotes.
232
- // `->>`, `#>>` and `::` are consumed as units so their `>`/`:` are not mistaken
233
- // for comparisons. Modelled on the char-walker in `SplitTopLevel` (parsing.ts).
234
- export type HasTopLevelCompare<
333
+ // `->>`, `#>>` etc. are consumed as units so their `>` is not mistaken for a
334
+ // comparison.
335
+ //
336
+ // Struct-jump, not per-char (the old walk minted the tail PER CHARACTER over
337
+ // every compare-bearing expression, including whole casted subquery bodies).
338
+ // Each step advances to the leftmost of `'` `"` `(` `)` (pairwise narrowing);
339
+ // the RUN before it — structural-char-free by construction — is tested at
340
+ // depth 0 with `HtcRunCheck`: a run containing `=` or `!` is a comparison
341
+ // outright (no non-comparison unit contains either), and a run with only
342
+ // `<`/`>` is scanned unit-to-unit (`->`, `->>`, `#>`, `#>>`, `@>`, `<@`,
343
+ // `<<`, `>>` consumed by 1-char context; the old `::` consume was a no-op —
344
+ // `:` never matched the compare set). Quote spans are jumped quote-to-quote
345
+ // (the other quote kind inside a span is data, the old InQ/InDQ suppression);
346
+ // an unterminated quote swallows the rest (old walk-to-EOF → `false`). The
347
+ // cap counts jumps, `false` on overflow as before.
348
+ export type HasTopLevelCompare<S extends string> = HtcJump<S, [], []>;
349
+
350
+ type HtcJump<
235
351
  S extends string,
236
- Depth extends any[] = [],
237
- Steps extends any[] = [],
238
- InQ extends boolean = false,
239
- InDQ extends boolean = false
352
+ Depth extends any[],
353
+ Steps extends any[]
240
354
  > = Steps["length"] extends 400
241
355
  ? false
242
- : S extends `${infer C}${infer Rest}`
243
- ? InQ extends true
244
- ? HasTopLevelCompare<Rest, Depth, [any, ...Steps], C extends "'" ? false : true, InDQ>
245
- : InDQ extends true
246
- ? HasTopLevelCompare<Rest, Depth, [any, ...Steps], InQ, C extends `"` ? false : true>
247
- : C extends "'"
248
- ? HasTopLevelCompare<Rest, Depth, [any, ...Steps], true, InDQ>
249
- : C extends `"`
250
- ? HasTopLevelCompare<Rest, Depth, [any, ...Steps], InQ, true>
251
- : C extends "("
252
- ? HasTopLevelCompare<Rest, [any, ...Depth], [any, ...Steps], InQ, InDQ>
253
- : C extends ")"
254
- ? HasTopLevelCompare<Rest, Depth extends [any, ...infer D] ? D : [], [any, ...Steps], InQ, InDQ>
255
- : Depth["length"] extends 0
256
- // Consume multi-char operators whose `<`/`>`/`:` are NOT comparisons:
257
- // JSON access (`->`, `->>`, `#>`, `#>>`), containment (`@>`, `<@`),
258
- // cast (`::`), and bit-shift (`<<`, `>>`). Longer forms first.
259
- ? S extends `->>${infer R}`
260
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
261
- : S extends `->${infer R}`
262
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
263
- : S extends `#>>${infer R}`
264
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
265
- : S extends `#>${infer R}`
266
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
267
- : S extends `@>${infer R}`
268
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
269
- : S extends `<@${infer R}`
270
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
271
- : S extends `::${infer R}`
272
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
273
- : S extends `<<${infer R}`
274
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
275
- : S extends `>>${infer R}`
276
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
277
- : C extends "<" | ">" | "=" | "!"
278
- ? true
279
- : HasTopLevelCompare<Rest, Depth, [any, ...Steps], InQ, InDQ>
280
- : HasTopLevelCompare<Rest, Depth, [any, ...Steps], InQ, InDQ>
356
+ : S extends `${infer P}'${infer R}`
357
+ ? P extends `${string}"${string}` | `${string}(${string}` | `${string})${string}`
358
+ ? HtcJump2<S, Depth, Steps>
359
+ : HtcRunCheck<P, Depth> extends true
360
+ ? true
361
+ : R extends `${string}'${infer R2}`
362
+ ? HtcJump<R2, Depth, [any, ...Steps]>
363
+ : false
364
+ : HtcJump2<S, Depth, Steps>;
365
+
366
+ type HtcJump2<
367
+ S extends string,
368
+ Depth extends any[],
369
+ Steps extends any[]
370
+ > = S extends `${infer P}"${infer R}`
371
+ ? P extends `${string}(${string}` | `${string})${string}`
372
+ ? HtcJump3<S, Depth, Steps>
373
+ : HtcRunCheck<P, Depth> extends true
374
+ ? true
375
+ : R extends `${string}"${infer R2}`
376
+ ? HtcJump<R2, Depth, [any, ...Steps]>
377
+ : false
378
+ : HtcJump3<S, Depth, Steps>;
379
+
380
+ type HtcJump3<
381
+ S extends string,
382
+ Depth extends any[],
383
+ Steps extends any[]
384
+ > = S extends `${infer P}(${infer R}`
385
+ ? P extends `${string})${string}`
386
+ ? HtcJump4<S, Depth, Steps>
387
+ : HtcRunCheck<P, Depth> extends true
388
+ ? true
389
+ : HtcJump<R, [any, ...Depth], [any, ...Steps]>
390
+ : HtcJump4<S, Depth, Steps>;
391
+
392
+ type HtcJump4<
393
+ S extends string,
394
+ Depth extends any[],
395
+ Steps extends any[]
396
+ > = S extends `${infer P})${infer R}`
397
+ ? HtcRunCheck<P, Depth> extends true
398
+ ? true
399
+ : HtcJump<R, Depth extends [any, ...infer D] ? D : [], [any, ...Steps]>
400
+ : HtcRunCheck<S, Depth>;
401
+
402
+ // A structural-char-free run is only inspected at depth 0. `=`/`!` never occur
403
+ // in a non-comparison unit, so their presence alone is a comparison; `<`/`>`
404
+ // need the unit scan.
405
+ type HtcRunCheck<P extends string, Depth extends any[]> =
406
+ Depth["length"] extends 0
407
+ ? P extends `${string}${"=" | "!"}${string}`
408
+ ? true
409
+ : P extends `${string}${"<" | ">"}${string}`
410
+ ? HtcRunScan<P>
411
+ : false
281
412
  : false;
282
413
 
414
+ // Unit-to-unit scan of a `<`/`>`-bearing run (no `=`/`!`, no structural chars):
415
+ // jump to the leftmost `<` or `>` and judge it by 1-char context — part of
416
+ // `<@`/`<<` (next char) or `->`/`->>`/`#>`/`#>>`/`@>`/`>>` (previous/next
417
+ // char) is consumed as a unit; anything else is a bare comparison.
418
+ type HtcRunScan<R extends string, Steps extends any[] = []> =
419
+ Steps["length"] extends 50
420
+ ? false
421
+ : R extends `${infer A}<${infer B}`
422
+ ? A extends `${string}>${string}`
423
+ ? HtcRunGt<R, Steps>
424
+ : B extends `@${infer B2}`
425
+ ? HtcRunScan<B2, [any, ...Steps]>
426
+ : B extends `<${infer B2}`
427
+ ? HtcRunScan<B2, [any, ...Steps]>
428
+ : true
429
+ : HtcRunGt<R, Steps>;
430
+
431
+ type HtcRunGt<R extends string, Steps extends any[]> =
432
+ R extends `${infer A}>${infer B}`
433
+ ? A extends `${string}${"-" | "#" | "@"}`
434
+ ? B extends `>${infer B2}`
435
+ ? HtcRunScan<B2, [any, ...Steps]>
436
+ : HtcRunScan<B, [any, ...Steps]>
437
+ : B extends `>${infer B2}`
438
+ ? HtcRunScan<B2, [any, ...Steps]>
439
+ : true
440
+ : false;
441
+
442
+ // ---------------------------------------------------------------------------
443
+ // Tier-2 arithmetic: top-level operator split.
444
+ //
445
+ // Finds the LEFTMOST operator in {`||`, `+`, `-`, `*`, `/`, `%`} that sits at
446
+ // paren depth 0 outside `'…'`/`"…"` quotes, and splits the expression around
447
+ // it: `{ __op: [left, op, right] }`. A top-level UNMODELED operator char
448
+ // (single `|` bitwise-or, or the `||/` cube-root prefix) yields the abort
449
+ // marker `{ __ab: true }` (consumer: `unknown`); `never` means no top-level
450
+ // modeled operator exists. `->` / `->>` JSON arrows are consumed as units so
451
+ // their `-` is not mistaken for subtraction.
452
+ //
453
+ // Structure mirrors `HasTopLevelCompare` (struct-jump to the leftmost of
454
+ // `'` `"` `(` `)`, pairwise narrowing) but ACCUMULATES the consumed prefix so
455
+ // the split can be returned, and is worker/driver CHUNKED like
456
+ // `SplitTopLevel` (split.ts): each jump costs ~4 structural helpers plus a
457
+ // ≤6-level run scan at depth 0, so 80 jumps/chunk stays well under TS's
458
+ // ~1000 tail-count budget (round-11 lesson: budget chunks in tail counts,
459
+ // not jumps). Quote spans are jumped quote-to-quote (`''` escapes alternate
460
+ // close/re-open across jumps, so escaped content stays "inside"); an
461
+ // unterminated quote means no trustworthy split -> `never`.
462
+ type SplitTopLevelOp<S extends string> = StoDrive<StoWorker<S>>;
463
+
464
+ // `[R] extends [never]` MUST be guarded first — `never` distributes through
465
+ // the `extends {…}` test and would otherwise fall into the infer arm
466
+ // (round-10 lesson).
467
+ type StoDrive<R> =
468
+ [R] extends [never]
469
+ ? never
470
+ : R extends { __c: [infer S2 extends string, infer D extends any[], infer A extends string] }
471
+ ? StoDrive<StoWorker<S2, D, A, []>>
472
+ : R;
473
+
474
+ type StoWorker<
475
+ S extends string,
476
+ Depth extends any[] = [],
477
+ Acc extends string = "",
478
+ Steps extends any[] = []
479
+ > = Steps["length"] extends 80
480
+ ? { __c: [S, Depth, Acc] }
481
+ : StoJump1<S, Depth, Acc, Steps>;
482
+
483
+ // Run-gate: a structural-char-free run is only operator-scanned at depth 0.
484
+ // At depth > 0 every char is data (`sum(x | y) + 1` must not abort on the
485
+ // nested `|`).
486
+ type StoRunGate<P extends string, Depth extends any[], Acc extends string, Tail extends string> =
487
+ Depth["length"] extends 0
488
+ ? StoRunScan<P, Acc, Tail>
489
+ : never;
490
+
491
+ type StoJump1<
492
+ S extends string,
493
+ Depth extends any[],
494
+ Acc extends string,
495
+ Steps extends any[]
496
+ > = S extends `${infer P}'${infer R}`
497
+ ? P extends `${string}"${string}` | `${string}(${string}` | `${string})${string}`
498
+ ? StoJump2<S, Depth, Acc, Steps>
499
+ : StoRunGate<P, Depth, Acc, `'${R}`> extends infer RR
500
+ ? [RR] extends [never]
501
+ ? R extends `${infer Span}'${infer R2}`
502
+ ? StoWorker<R2, Depth, `${Acc}${P}'${Span}'`, [any, ...Steps]>
503
+ : never
504
+ : RR
505
+ : never
506
+ : StoJump2<S, Depth, Acc, Steps>;
507
+
508
+ type StoJump2<
509
+ S extends string,
510
+ Depth extends any[],
511
+ Acc extends string,
512
+ Steps extends any[]
513
+ > = S extends `${infer P}"${infer R}`
514
+ ? P extends `${string}(${string}` | `${string})${string}`
515
+ ? StoJump3<S, Depth, Acc, Steps>
516
+ : StoRunGate<P, Depth, Acc, `"${R}`> extends infer RR
517
+ ? [RR] extends [never]
518
+ ? R extends `${infer Span}"${infer R2}`
519
+ ? StoWorker<R2, Depth, `${Acc}${P}"${Span}"`, [any, ...Steps]>
520
+ : never
521
+ : RR
522
+ : never
523
+ : StoJump3<S, Depth, Acc, Steps>;
524
+
525
+ type StoJump3<
526
+ S extends string,
527
+ Depth extends any[],
528
+ Acc extends string,
529
+ Steps extends any[]
530
+ > = S extends `${infer P}(${infer R}`
531
+ ? P extends `${string})${string}`
532
+ ? StoJump4<S, Depth, Acc, Steps>
533
+ : StoRunGate<P, Depth, Acc, `(${R}`> extends infer RR
534
+ ? [RR] extends [never]
535
+ ? StoWorker<R, [any, ...Depth], `${Acc}${P}(`, [any, ...Steps]>
536
+ : RR
537
+ : never
538
+ : StoJump4<S, Depth, Acc, Steps>;
539
+
540
+ type StoJump4<
541
+ S extends string,
542
+ Depth extends any[],
543
+ Acc extends string,
544
+ Steps extends any[]
545
+ > = S extends `${infer P})${infer R}`
546
+ ? StoRunGate<P, Depth, Acc, `)${R}`> extends infer RR
547
+ ? [RR] extends [never]
548
+ // an unmatched `)` at depth 0 stays at depth 0 (pop of empty = empty)
549
+ ? StoWorker<R, Depth extends [any, ...infer D] ? D : [], `${Acc}${P})`, [any, ...Steps]>
550
+ : RR
551
+ : never
552
+ : StoRunGate<S, Depth, Acc, "">;
553
+
554
+ // Leftmost modeled operator within a structural-free run `P` (no quotes or
555
+ // parens by construction). Pairwise narrowing, same invariant as
556
+ // `StlStructJump`: each level checks the matched prefix for every LATER
557
+ // class, so the level that fires is genuinely the leftmost. `Tail` is the
558
+ // untouched remainder of the whole expression after the run; the returned
559
+ // `right` re-attaches it.
560
+ type StoRunScan<P extends string, Acc extends string, Tail extends string> =
561
+ P extends `${infer A}+${infer B}`
562
+ ? A extends `${string}${"-" | "*" | "/" | "%" | "|"}${string}`
563
+ ? StoRunScan2<P, Acc, Tail>
564
+ : { __op: [`${Acc}${A}`, "+", `${B}${Tail}`] }
565
+ : StoRunScan2<P, Acc, Tail>;
566
+
567
+ type StoRunScan2<P extends string, Acc extends string, Tail extends string> =
568
+ P extends `${infer A}-${infer B}`
569
+ ? A extends `${string}${"*" | "/" | "%" | "|"}${string}`
570
+ ? StoRunScan3<P, Acc, Tail>
571
+ : B extends `>${infer B2}`
572
+ // `->` / `->>` JSON arrow: a unit, not subtraction — keep
573
+ // scanning the rest of the run (a leading `>` from `->>` is
574
+ // not an operator char and is skipped naturally). Non-tail
575
+ // recursion, but bounded by arrows-per-run (tiny in practice).
576
+ ? StoRunScan<B2, `${Acc}${A}->`, Tail>
577
+ : { __op: [`${Acc}${A}`, "-", `${B}${Tail}`] }
578
+ : StoRunScan3<P, Acc, Tail>;
579
+
580
+ type StoRunScan3<P extends string, Acc extends string, Tail extends string> =
581
+ P extends `${infer A}*${infer B}`
582
+ ? A extends `${string}${"/" | "%" | "|"}${string}`
583
+ ? StoRunScan4<P, Acc, Tail>
584
+ : { __op: [`${Acc}${A}`, "*", `${B}${Tail}`] }
585
+ : StoRunScan4<P, Acc, Tail>;
586
+
587
+ type StoRunScan4<P extends string, Acc extends string, Tail extends string> =
588
+ P extends `${infer A}/${infer B}`
589
+ ? A extends `${string}${"%" | "|"}${string}`
590
+ ? StoRunScan5<P, Acc, Tail>
591
+ : { __op: [`${Acc}${A}`, "/", `${B}${Tail}`] }
592
+ : StoRunScan5<P, Acc, Tail>;
593
+
594
+ type StoRunScan5<P extends string, Acc extends string, Tail extends string> =
595
+ P extends `${infer A}%${infer B}`
596
+ ? A extends `${string}|${string}`
597
+ ? StoRunScan6<P, Acc, Tail>
598
+ : { __op: [`${Acc}${A}`, "%", `${B}${Tail}`] }
599
+ : StoRunScan6<P, Acc, Tail>;
600
+
601
+ type StoRunScan6<P extends string, Acc extends string, Tail extends string> =
602
+ P extends `${infer A}|${infer B}`
603
+ ? B extends `|${infer B2}`
604
+ ? B2 extends `/${string}`
605
+ // `||/` cube root — numeric prefix operator, NOT concat
606
+ ? { __ab: true }
607
+ : Trim<`${Acc}${A}`> extends ""
608
+ // leading `||` with no left operand — unmodeled prefix op
609
+ ? { __ab: true }
610
+ : { __op: [`${Acc}${A}`, "||", `${B2}${Tail}`] }
611
+ // single `|` (bitwise) at top level — unmodeled, conservative stop
612
+ : { __ab: true }
613
+ : never;
614
+
615
+ // `A op B` for op in {+, -, *, /, %} types `number` when BOTH operands type
616
+ // `number` (`| null` propagating from either side — SQL NULL arithmetic is
617
+ // NULL). number op number is numeric in Postgres; the interval/date hazards
618
+ // all require a non-number operand, which the schema types as non-number, so
619
+ // the both-number case is unambiguous and contract-legal. Any other operand
620
+ // type — including `never` — degrades to `unknown`: an operand the core path
621
+ // cannot resolve (a ref qualified by a joined-derived alias, a mis-split
622
+ // like `1e+5` -> `1e`) must NOT reject, or `ExprValid`'s never-gate would
623
+ // flip `ValidateSQL` to `false` on valid SQL; genuinely bogus columns are
624
+ // still rejected by the token-scan validators independently.
625
+ type ArithCombineType<
626
+ L extends string,
627
+ R extends string,
628
+ Tables extends string,
629
+ Aliases extends string,
630
+ S extends DatabaseSchema,
631
+ Steps extends any[]
632
+ > =
633
+ Trim<L> extends ""
634
+ ? unknown
635
+ : ArithCombineTypes<ExprType<Trim<L>, Tables, Aliases, S, [any, ...Steps]>, R, Tables, Aliases, S, Steps>;
636
+
637
+ // Same, with the LEFT operand's type already computed (the Func-branch
638
+ // dispatcher gets it from `FunctionReturn` without re-parsing the call).
639
+ type ArithCombineTypes<
640
+ LT,
641
+ R extends string,
642
+ Tables extends string,
643
+ Aliases extends string,
644
+ S extends DatabaseSchema,
645
+ Steps extends any[]
646
+ > =
647
+ Trim<R> extends ""
648
+ ? unknown
649
+ : ArithNumClass<LT> extends infer LN
650
+ ? LN extends false
651
+ ? unknown
652
+ : ArithNumClass<ExprType<Trim<R>, Tables, Aliases, S, [any, ...Steps]>> extends infer RN
653
+ ? RN extends false
654
+ ? unknown
655
+ : "nullable" extends LN | RN
656
+ ? number | null
657
+ : number
658
+ : unknown
659
+ : unknown;
660
+
661
+ // `never` guarded FIRST — `[never]` matches the later arms too. A bare
662
+ // `null` operand classes as nullable (`price + null` -> number | null).
663
+ type ArithNumClass<T> =
664
+ [T] extends [never]
665
+ ? false
666
+ : [T] extends [number]
667
+ ? "num"
668
+ : [T] extends [number | null]
669
+ ? "nullable"
670
+ : false;
671
+
672
+ // Scan-and-combine used where no cheaper dispatch is possible: the
673
+ // fallback-slot path and the op-char-in-Func-prefix path. `NoOp` is the
674
+ // result when the scan finds no top-level modeled operator (today's
675
+ // behavior at the call site); the abort marker is conservative `unknown`.
676
+ type ArithViaScan<
677
+ CE extends string,
678
+ Tables extends string,
679
+ Aliases extends string,
680
+ S extends DatabaseSchema,
681
+ Steps extends any[],
682
+ NoOp
683
+ > =
684
+ SplitTopLevelOp<CE> extends infer SR
685
+ ? [SR] extends [never]
686
+ ? NoOp
687
+ : SR extends { __op: [infer L extends string, infer Op extends string, infer R extends string] }
688
+ ? Op extends "||"
689
+ ? string
690
+ : ArithCombineType<L, R, Tables, Aliases, S, Steps>
691
+ : unknown
692
+ : never;
693
+
694
+ // Final-fallback-slot arithmetic (replaces Tier 1's DivByNumericLiteralType,
695
+ // which it subsumes: a numeric-literal divisor is just a `${number}` right
696
+ // operand). Sits after the column-ref branch fails, so common paths pay
697
+ // nothing; the char pre-gate skips the scan for operator-free expressions.
698
+ // `||` is unreachable here (the naive `${string}||${string}` branch runs
699
+ // earlier in the cascade), so the gate set is the five arithmetic chars.
700
+ type TopLevelArithType<
701
+ CE extends string,
702
+ Tables extends string,
703
+ Aliases extends string,
704
+ S extends DatabaseSchema,
705
+ Steps extends any[]
706
+ > =
707
+ CE extends `${string}${"+" | "-" | "*" | "/" | "%"}${string}`
708
+ ? ArithViaScan<CE, Tables, Aliases, S, Steps, unknown>
709
+ : unknown;
710
+
711
+ // Func-branch dispatcher. The greedy `${Func}(${Args})` match anchors on the
712
+ // LAST `)`, so `sum(price) / count(id)` lands here with Func="sum",
713
+ // Args="price) / count(id" — a function call is only the WHOLE expression
714
+ // when its first paren group is also its last. Dispatch:
715
+ // - Func clean + no `)` in Args (the overwhelmingly common projection:
716
+ // `count(*)`, `sum(price)`, `upper(name)`, ops hidden inside quoted args)
717
+ // -> FunctionReturn directly, zero new cost.
718
+ // - Func clean + `)` in Args -> `SplitBalancedParen` (already-paid chunked
719
+ // primitive) resolves the first call's true extent WITHOUT a scan:
720
+ // rest "" means the call spans the whole expression (nested calls like
721
+ // `coalesce(min(x), 0)`); an operator-leading rest is arithmetic with the
722
+ // call as left operand; anything else (window `over (...)`, `filter`)
723
+ // keeps today's greedy FunctionReturn.
724
+ // - Operator char in Func (`price + count(id)`, `name || upper(b)`) -> the
725
+ // operator precedes the first paren; full top-level scan.
726
+ type FuncOrArithType<
727
+ CE extends string,
728
+ Func extends string,
729
+ Args extends string,
730
+ Tables extends string,
731
+ Aliases extends string,
732
+ S extends DatabaseSchema,
733
+ Steps extends any[]
734
+ > =
735
+ Func extends `${string}${"+" | "-" | "*" | "/" | "%" | "|"}${string}`
736
+ ? ArithViaScan<CE, Tables, Aliases, S, Steps, FunctionReturn<CleanIdent<Func>, Args, Tables, Aliases, S, [any, ...Steps]>>
737
+ : Args extends `${string})${string}`
738
+ ? CE extends `${string}(${infer AfterOpen}`
739
+ ? SplitBalancedParen<`(${AfterOpen}`> extends { inner: infer Inner extends string; rest: infer Rest extends string }
740
+ ? Trim<Rest> extends ""
741
+ ? FunctionReturn<CleanIdent<Func>, Inner, Tables, Aliases, S, [any, ...Steps]>
742
+ : FuncRestDispatch<Trim<Rest>, Func, Args, Inner, Tables, Aliases, S, Steps>
743
+ : FunctionReturn<CleanIdent<Func>, Args, Tables, Aliases, S, [any, ...Steps]>
744
+ : FunctionReturn<CleanIdent<Func>, Args, Tables, Aliases, S, [any, ...Steps]>
745
+ : FunctionReturn<CleanIdent<Func>, Args, Tables, Aliases, S, [any, ...Steps]>;
746
+
747
+ // `RestT` (trimmed) is what follows the first balanced call. An arithmetic
748
+ // operator -> the call (typed via FunctionReturn on its TRUE arg list) is
749
+ // the left operand. `||` -> string (guarding the `||/` cube root). A `->`
750
+ // JSON arrow or any other shape (window clauses, …) -> today's greedy path.
751
+ type FuncRestDispatch<
752
+ RestT extends string,
753
+ Func extends string,
754
+ Args extends string,
755
+ Inner extends string,
756
+ Tables extends string,
757
+ Aliases extends string,
758
+ S extends DatabaseSchema,
759
+ Steps extends any[]
760
+ > =
761
+ RestT extends `||${infer R}`
762
+ ? R extends `/${string}`
763
+ ? unknown
764
+ : string
765
+ : RestT extends `+${infer R}`
766
+ ? ArithCombineTypes<FunctionReturn<CleanIdent<Func>, Inner, Tables, Aliases, S, [any, ...Steps]>, R, Tables, Aliases, S, Steps>
767
+ : RestT extends `-${infer R}`
768
+ ? R extends `>${string}`
769
+ ? FunctionReturn<CleanIdent<Func>, Args, Tables, Aliases, S, [any, ...Steps]>
770
+ : ArithCombineTypes<FunctionReturn<CleanIdent<Func>, Inner, Tables, Aliases, S, [any, ...Steps]>, R, Tables, Aliases, S, Steps>
771
+ : RestT extends `*${infer R}`
772
+ ? ArithCombineTypes<FunctionReturn<CleanIdent<Func>, Inner, Tables, Aliases, S, [any, ...Steps]>, R, Tables, Aliases, S, Steps>
773
+ : RestT extends `/${infer R}`
774
+ ? ArithCombineTypes<FunctionReturn<CleanIdent<Func>, Inner, Tables, Aliases, S, [any, ...Steps]>, R, Tables, Aliases, S, Steps>
775
+ : RestT extends `%${infer R}`
776
+ ? ArithCombineTypes<FunctionReturn<CleanIdent<Func>, Inner, Tables, Aliases, S, [any, ...Steps]>, R, Tables, Aliases, S, Steps>
777
+ : FunctionReturn<CleanIdent<Func>, Args, Tables, Aliases, S, [any, ...Steps]>;
778
+
283
779
  // Strip a redundant fully-wrapping paren pair, repeatedly: `((expr))` and
284
780
  // `((case ...)::text)` -> the inner expression whose parens wrap the WHOLE
285
781
  // thing. Without this an outer operator hidden by a redundant wrap (e.g. the
@@ -349,9 +845,9 @@ export type ExprType<
349
845
  ? never
350
846
  : SqlTypeToTs<CastTypeName>
351
847
  : CE extends `${infer Func}(${infer Args})`
352
- ? FunctionReturn<CleanIdent<Func>, Args, Tables, Aliases, S, [any, ...Steps]>
848
+ ? FuncOrArithType<CE, Func, Args, Tables, Aliases, S, Steps>
353
849
  : CE extends `${infer Func} (${infer Args})`
354
- ? FunctionReturn<CleanIdent<Func>, Args, Tables, Aliases, S, [any, ...Steps]>
850
+ ? FuncOrArithType<CE, Func, Args, Tables, Aliases, S, Steps>
355
851
  : CE extends `${string}||${string}`
356
852
  ? string
357
853
  : CE extends `${infer JBase}->>${string}`
@@ -380,12 +876,12 @@ export type ExprType<
380
876
  ? [Ref] extends [never]
381
877
  ? IsIdentifier<CE> extends true
382
878
  ? never
383
- : unknown
879
+ : TopLevelArithType<CE, Tables, Aliases, S, Steps>
384
880
  : Ref extends ColumnRef<infer TableKey extends string, infer Column extends string>
385
881
  ? ColumnTypeFromTableKey<TableKey, Column, S>
386
882
  : IsIdentifier<CE> extends true
387
883
  ? never
388
- : unknown
884
+ : TopLevelArithType<CE, Tables, Aliases, S, Steps>
389
885
  : unknown
390
886
  // A genuine TOP-LEVEL `::T` cast. As with the JSON-text
391
887
  // operators, a `->>` / `#>>` to the right of the cast type
@@ -466,7 +962,18 @@ export type FunctionReturn<
466
962
  ? string
467
963
  : Func extends "coalesce"
468
964
  ? UnionArgTypes<Args, Tables, Aliases, S, Steps>
469
- : unknown;
965
+ // Postgres EXTRACT always returns a numeric
966
+ // value regardless of field/source, so typing
967
+ // it is unambiguous; it is NULL iff its source
968
+ // is NULL, so propagate the argument's
969
+ // nullability. An unmodeled argument types
970
+ // `unknown` (which may include null) → the
971
+ // conservative answer is `number | null`.
972
+ : Func extends "extract"
973
+ ? null extends FirstArgType<Args, Tables, Aliases, S, Steps>
974
+ ? number | null
975
+ : number
976
+ : unknown;
470
977
 
471
978
  // Expression validation
472
979
 
@@ -481,13 +988,20 @@ export type ExprValid<
481
988
  E extends string,
482
989
  Tables extends string,
483
990
  Aliases extends string,
484
- S extends DatabaseSchema
991
+ S extends DatabaseSchema,
992
+ LocalRels extends string = never
485
993
  > =
486
994
  IsIgnorableRuntimeExpr<E> extends true
487
995
  ? true
488
996
  : ExtractAlias<E> extends { expr: infer RawExpr extends string }
489
997
  ? IsIgnorableRuntimeExpr<RawExpr> extends true
490
998
  ? true
999
+ // A ref qualified by a query-local relation (CTE name) has no schema
1000
+ // surface to resolve against — `ExprType` yields `never` and the
1001
+ // token scans reject it. Bless it (lenient contract: the local
1002
+ // relation's output shape is validated where it is defined).
1003
+ : HasLocalQualifier<RawExpr, LocalRels> extends true
1004
+ ? true
491
1005
  : ExprType<RawExpr, Tables, Aliases, S> extends never
492
1006
  ? false
493
1007
  : NeedsTokenRefValidation<RawExpr> extends true
@@ -495,6 +1009,17 @@ export type ExprValid<
495
1009
  : FuncCompoundArgsValid<RawExpr, Tables, Aliases, S>
496
1010
  : true;
497
1011
 
1012
+ // `true` when E is a qualified ref whose qualifier names a query-local relation.
1013
+ // The `[LocalRels] extends [never]` guard keeps the common no-CTE path free.
1014
+ type HasLocalQualifier<E extends string, LocalRels extends string> =
1015
+ [LocalRels] extends [never]
1016
+ ? false
1017
+ : E extends `${infer Q}.${string}`
1018
+ ? CleanIdent<Q> extends LocalRels
1019
+ ? true
1020
+ : false
1021
+ : false;
1022
+
498
1023
  // A function-call (or cast) projection skips the token ref-scan above
499
1024
  // (`NeedsTokenRefValidation` is false for `${fn}(${args})`), which is why an
500
1025
  // invalid column hidden inside an aggregate/function argument — `sum(price +
@@ -576,7 +1101,7 @@ export type ExprQualifiedRefsValid<
576
1101
  Tables extends string,
577
1102
  Aliases extends string,
578
1103
  S extends DatabaseSchema
579
- > = QualifiedColumnRefs<TokenizeLoose<E>, S, Tables, Aliases> extends infer Cols
1104
+ > = QualifiedRefScan<E> extends infer Cols
580
1105
  ? AllTrue<Cols extends string ? ColumnRefValidLooseWith<Cols, Tables, Aliases, S> : true>
581
1106
  : true;
582
1107
 
@@ -585,7 +1110,7 @@ export type ExprUnqualifiedRefsValid<
585
1110
  Tables extends string,
586
1111
  Aliases extends string,
587
1112
  S extends DatabaseSchema
588
- > = UnqualifiedColumnRefs<TokenizeLoose<E>, S, Tables, Aliases> extends infer Cols
1113
+ > = UnqualifiedRefScan<E, S, Tables, Aliases> extends infer Cols
589
1114
  ? AllTrue<Cols extends string ? UnqualifiedColumnValid<Cols, Tables, Aliases, S> : true>
590
1115
  : true;
591
1116
 
@@ -594,12 +1119,13 @@ export type ExprsValidList<
594
1119
  Tables extends string,
595
1120
  Aliases extends string,
596
1121
  S extends DatabaseSchema,
597
- Steps extends any[] = []
1122
+ Steps extends any[] = [],
1123
+ LocalRels extends string = never
598
1124
  > = Steps["length"] extends 100
599
1125
  ? true
600
1126
  : Exprs extends [infer H extends string, ...infer Rest extends string[]]
601
- ? ExprValid<H, Tables, Aliases, S> extends true
602
- ? ExprsValidList<Rest, Tables, Aliases, S, [any, ...Steps]>
1127
+ ? ExprValid<H, Tables, Aliases, S, LocalRels> extends true
1128
+ ? ExprsValidList<Rest, Tables, Aliases, S, [any, ...Steps], LocalRels>
603
1129
  : false
604
1130
  : true;
605
1131