@kuindji/typed-sql 0.3.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 (41) 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 +73 -8
  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 +3 -1
  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/validate-columns.d.ts +14 -14
  27. package/dist/validation/validate-columns.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/columns.ts +168 -32
  30. package/src/expressions.ts +550 -57
  31. package/src/parsing/extract.ts +72 -32
  32. package/src/parsing/normalize.ts +54 -8
  33. package/src/parsing/pg-literals.ts +32 -10
  34. package/src/parsing/split.ts +236 -72
  35. package/src/parsing/string-utils.ts +15 -15
  36. package/src/parsing/tokenize.ts +224 -146
  37. package/src/partial.ts +9 -15
  38. package/src/tables.ts +546 -214
  39. package/src/validation/dispatch.ts +58 -52
  40. package/src/validation/joins.ts +15 -19
  41. package/src/validation/validate-columns.ts +54 -64
@@ -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";
@@ -144,7 +143,95 @@ export type ApplyProjectionNull<
144
143
  ? CoalesceAllArgsNullable<SplitTopLevel<Args>, Tables, Aliases, S, Nullable> extends true
145
144
  ? T | null
146
145
  : T
147
- : 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;
148
235
 
149
236
  // True only when every coalesce argument is nullable. An empty/exhausted list is
150
237
  // vacuously `true`, but the wrapper above only reaches this for a real coalesce call
@@ -243,57 +330,452 @@ export type IsBoolExpr<CE extends string> =
243
330
  : false;
244
331
 
245
332
  // Scans for a comparison operator outside parens and outside `'…'`/`"…"` quotes.
246
- // `->>`, `#>>` and `::` are consumed as units so their `>`/`:` are not mistaken
247
- // for comparisons. Modelled on the char-walker in `SplitTopLevel` (parsing.ts).
248
- 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<
249
351
  S extends string,
250
- Depth extends any[] = [],
251
- Steps extends any[] = [],
252
- InQ extends boolean = false,
253
- InDQ extends boolean = false
352
+ Depth extends any[],
353
+ Steps extends any[]
254
354
  > = Steps["length"] extends 400
255
355
  ? false
256
- : S extends `${infer C}${infer Rest}`
257
- ? InQ extends true
258
- ? HasTopLevelCompare<Rest, Depth, [any, ...Steps], C extends "'" ? false : true, InDQ>
259
- : InDQ extends true
260
- ? HasTopLevelCompare<Rest, Depth, [any, ...Steps], InQ, C extends `"` ? false : true>
261
- : C extends "'"
262
- ? HasTopLevelCompare<Rest, Depth, [any, ...Steps], true, InDQ>
263
- : C extends `"`
264
- ? HasTopLevelCompare<Rest, Depth, [any, ...Steps], InQ, true>
265
- : C extends "("
266
- ? HasTopLevelCompare<Rest, [any, ...Depth], [any, ...Steps], InQ, InDQ>
267
- : C extends ")"
268
- ? HasTopLevelCompare<Rest, Depth extends [any, ...infer D] ? D : [], [any, ...Steps], InQ, InDQ>
269
- : Depth["length"] extends 0
270
- // Consume multi-char operators whose `<`/`>`/`:` are NOT comparisons:
271
- // JSON access (`->`, `->>`, `#>`, `#>>`), containment (`@>`, `<@`),
272
- // cast (`::`), and bit-shift (`<<`, `>>`). Longer forms first.
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
- : S extends `#>>${infer R}`
278
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
279
- : S extends `#>${infer R}`
280
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
281
- : S extends `@>${infer R}`
282
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
283
- : S extends `<@${infer R}`
284
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
285
- : S extends `::${infer R}`
286
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
287
- : S extends `<<${infer R}`
288
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
289
- : S extends `>>${infer R}`
290
- ? HasTopLevelCompare<R, Depth, [any, ...Steps], InQ, InDQ>
291
- : C extends "<" | ">" | "=" | "!"
292
- ? true
293
- : HasTopLevelCompare<Rest, Depth, [any, ...Steps], InQ, InDQ>
294
- : 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
412
+ : false;
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
295
440
  : false;
296
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
+
297
779
  // Strip a redundant fully-wrapping paren pair, repeatedly: `((expr))` and
298
780
  // `((case ...)::text)` -> the inner expression whose parens wrap the WHOLE
299
781
  // thing. Without this an outer operator hidden by a redundant wrap (e.g. the
@@ -363,9 +845,9 @@ export type ExprType<
363
845
  ? never
364
846
  : SqlTypeToTs<CastTypeName>
365
847
  : CE extends `${infer Func}(${infer Args})`
366
- ? FunctionReturn<CleanIdent<Func>, Args, Tables, Aliases, S, [any, ...Steps]>
848
+ ? FuncOrArithType<CE, Func, Args, Tables, Aliases, S, Steps>
367
849
  : CE extends `${infer Func} (${infer Args})`
368
- ? FunctionReturn<CleanIdent<Func>, Args, Tables, Aliases, S, [any, ...Steps]>
850
+ ? FuncOrArithType<CE, Func, Args, Tables, Aliases, S, Steps>
369
851
  : CE extends `${string}||${string}`
370
852
  ? string
371
853
  : CE extends `${infer JBase}->>${string}`
@@ -394,12 +876,12 @@ export type ExprType<
394
876
  ? [Ref] extends [never]
395
877
  ? IsIdentifier<CE> extends true
396
878
  ? never
397
- : unknown
879
+ : TopLevelArithType<CE, Tables, Aliases, S, Steps>
398
880
  : Ref extends ColumnRef<infer TableKey extends string, infer Column extends string>
399
881
  ? ColumnTypeFromTableKey<TableKey, Column, S>
400
882
  : IsIdentifier<CE> extends true
401
883
  ? never
402
- : unknown
884
+ : TopLevelArithType<CE, Tables, Aliases, S, Steps>
403
885
  : unknown
404
886
  // A genuine TOP-LEVEL `::T` cast. As with the JSON-text
405
887
  // operators, a `->>` / `#>>` to the right of the cast type
@@ -480,7 +962,18 @@ export type FunctionReturn<
480
962
  ? string
481
963
  : Func extends "coalesce"
482
964
  ? UnionArgTypes<Args, Tables, Aliases, S, Steps>
483
- : 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;
484
977
 
485
978
  // Expression validation
486
979
 
@@ -608,7 +1101,7 @@ export type ExprQualifiedRefsValid<
608
1101
  Tables extends string,
609
1102
  Aliases extends string,
610
1103
  S extends DatabaseSchema
611
- > = QualifiedColumnRefs<TokenizeLoose<E>, S, Tables, Aliases> extends infer Cols
1104
+ > = QualifiedRefScan<E> extends infer Cols
612
1105
  ? AllTrue<Cols extends string ? ColumnRefValidLooseWith<Cols, Tables, Aliases, S> : true>
613
1106
  : true;
614
1107
 
@@ -617,7 +1110,7 @@ export type ExprUnqualifiedRefsValid<
617
1110
  Tables extends string,
618
1111
  Aliases extends string,
619
1112
  S extends DatabaseSchema
620
- > = UnqualifiedColumnRefs<TokenizeLoose<E>, S, Tables, Aliases> extends infer Cols
1113
+ > = UnqualifiedRefScan<E, S, Tables, Aliases> extends infer Cols
621
1114
  ? AllTrue<Cols extends string ? UnqualifiedColumnValid<Cols, Tables, Aliases, S> : true>
622
1115
  : true;
623
1116