@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.
- package/README.md +7 -0
- package/dist/builder/assemble.d.ts.map +1 -1
- package/dist/builder/assemble.js +9 -10
- package/dist/builder/assemble.js.map +1 -1
- package/dist/builder/conditional-sql.d.ts +7 -2
- package/dist/builder/conditional-sql.d.ts.map +1 -1
- package/dist/builder/conditional-sql.js +9 -22
- package/dist/builder/conditional-sql.js.map +1 -1
- package/dist/builder/params.d.ts +2 -1
- package/dist/builder/params.d.ts.map +1 -1
- package/dist/builder/params.js +34 -27
- package/dist/builder/params.js.map +1 -1
- package/dist/builder/select.d.ts +7 -0
- package/dist/builder/select.d.ts.map +1 -1
- package/dist/builder/select.js +7 -2
- package/dist/builder/select.js.map +1 -1
- package/dist/builder/state.d.ts +3 -6
- package/dist/builder/state.d.ts.map +1 -1
- package/dist/builder/state.js +1 -2
- package/dist/builder/state.js.map +1 -1
- package/dist/expressions.d.ts +12 -6
- package/dist/expressions.d.ts.map +1 -1
- package/dist/parsing/extract.d.ts +2 -1
- package/dist/parsing/extract.d.ts.map +1 -1
- package/dist/parsing/normalize.d.ts +10 -5
- package/dist/parsing/normalize.d.ts.map +1 -1
- package/dist/parsing/pg-literals.d.ts +2 -2
- package/dist/parsing/pg-literals.d.ts.map +1 -1
- package/dist/parsing/tokenize.d.ts +8 -6
- package/dist/parsing/tokenize.d.ts.map +1 -1
- package/dist/tables.d.ts +2 -2
- package/dist/tables.d.ts.map +1 -1
- package/dist/validation/dispatch.d.ts +3 -1
- package/dist/validation/dispatch.d.ts.map +1 -1
- package/dist/validation/return-derived.d.ts +8 -4
- package/dist/validation/return-derived.d.ts.map +1 -1
- package/dist/validation/return-derived.js.map +1 -1
- package/dist/validation/return-types.d.ts +1 -1
- package/dist/validation/return-types.d.ts.map +1 -1
- package/dist/validation/validate-columns.d.ts +17 -6
- package/dist/validation/validate-columns.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/builder/assemble.ts +9 -13
- package/src/builder/conditional-sql.ts +9 -27
- package/src/builder/params.ts +33 -27
- package/src/builder/select.ts +19 -2
- package/src/builder/state.ts +4 -6
- package/src/expressions.ts +47 -7
- package/src/parsing/extract.ts +18 -4
- package/src/parsing/normalize.ts +143 -48
- package/src/parsing/pg-literals.ts +23 -12
- package/src/parsing/tokenize.ts +56 -23
- package/src/tables.ts +35 -4
- package/src/validation/dispatch.ts +21 -3
- package/src/validation/return-derived.ts +60 -4
- package/src/validation/return-types.ts +9 -3
- package/src/validation/validate-columns.ts +123 -13
package/src/expressions.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/src/parsing/extract.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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
|
package/src/parsing/normalize.ts
CHANGED
|
@@ -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
|
-
:
|
|
59
|
-
?
|
|
60
|
-
? InDoubleQuote
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
: InDoubleQuote
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
:
|
|
157
|
-
?
|
|
158
|
-
? InDoubleQuote
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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.
|
|
210
|
-
//
|
|
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
|
|
217
|
-
? OddSingleQuotes<R,
|
|
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 `""`.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
package/src/parsing/tokenize.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Tokenization, sentinels, operators, and SQL keyword sets.
|
|
2
|
-
import type {
|
|
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> =
|
|
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
|
-
:
|
|
67
|
+
: RestoreCleanFilterTokens<Split<MaybeMarkDQuotedSpaces<MarkTopLevelCommas<N>>, " ">>;
|
|
68
68
|
|
|
69
69
|
export type TokenizeLoose<N extends string> =
|
|
70
|
-
|
|
70
|
+
RestoreCleanLooseFilterTokens<
|
|
71
71
|
Split<CollapseSpaces<RestoreWildcards<PadOperators<ProtectWildcards<MaybeMarkDQuotedSpaces<MaybeStripDQuotedPunct<N>>>>>>, " ">
|
|
72
|
-
|
|
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
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
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
|
-
?
|
|
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
|
|
247
|
+
: Steps["length"] extends 300
|
|
209
248
|
? `${Acc}${S}`
|
|
210
|
-
:
|
|
211
|
-
?
|
|
212
|
-
?
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|