@kuindji/typed-sql 0.1.0 → 0.2.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 +1 -1
- 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 +4 -3
- 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/validation/dispatch.d.ts +3 -1
- package/dist/validation/dispatch.d.ts.map +1 -1
- package/dist/validation/validate-columns.d.ts +1 -1
- 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 +8 -1
- package/src/parsing/extract.ts +18 -4
- package/src/parsing/normalize.ts +83 -46
- package/src/parsing/pg-literals.ts +23 -12
- package/src/parsing/tokenize.ts +56 -23
- package/src/validation/dispatch.ts +21 -3
- package/src/validation/validate-columns.ts +13 -7
package/src/builder/assemble.ts
CHANGED
|
@@ -15,15 +15,17 @@ import type { RuntimeSelectState } from "./state.js";
|
|
|
15
15
|
export function assembleSelectSQL(state: RuntimeSelectState): string {
|
|
16
16
|
const parts: string[] = [];
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// `SELECT` / `SELECT DISTINCT` / `SELECT DISTINCT ON (...)` prefix, shared
|
|
19
|
+
// by the projected and `*` paths.
|
|
20
|
+
const distinctPrefix = state.distinctOn
|
|
21
|
+
? `SELECT DISTINCT ON (${state.distinctOn})`
|
|
22
|
+
: state.distinct
|
|
23
|
+
? "SELECT DISTINCT"
|
|
24
|
+
: "SELECT";
|
|
23
25
|
|
|
24
26
|
const selectIds = Object.keys(state.selectSql);
|
|
25
27
|
if (selectIds.length === 0) {
|
|
26
|
-
parts.push(
|
|
28
|
+
parts.push(`${distinctPrefix} *`);
|
|
27
29
|
}
|
|
28
30
|
else {
|
|
29
31
|
const selectFragments: string[] = [];
|
|
@@ -36,9 +38,7 @@ export function assembleSelectSQL(state: RuntimeSelectState): string {
|
|
|
36
38
|
const selectSql = selectFragments.length > 0
|
|
37
39
|
? selectFragments.join(", ")
|
|
38
40
|
: "*";
|
|
39
|
-
parts.push(
|
|
40
|
-
state.distinct ? `SELECT DISTINCT ${selectSql}` : `SELECT ${selectSql}`,
|
|
41
|
-
);
|
|
41
|
+
parts.push(`${distinctPrefix} ${selectSql}`);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
if (state.fromSql) {
|
|
@@ -87,10 +87,6 @@ export function assembleSelectSQL(state: RuntimeSelectState): string {
|
|
|
87
87
|
parts.push(`OFFSET ${state.offset}`);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
if (state.unionSql) {
|
|
91
|
-
parts.push(state.unionSql);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
90
|
const sql = parts.join(" ");
|
|
95
91
|
const namedParams = state.namedParams;
|
|
96
92
|
if (namedParams && Object.keys(namedParams).length > 0) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// (GetReturnType / ValidateSQL).
|
|
7
7
|
import type { DatabaseSchema } from "../schema.js";
|
|
8
8
|
import type { GetReturnType, ValidateSQL } from "../index.js";
|
|
9
|
-
import type
|
|
9
|
+
import { collectParamValues, expandNamedParams, type QueryParamValue } from "./params.js";
|
|
10
10
|
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// Runtime (ported from OLD conditional/runtime.ts)
|
|
@@ -76,37 +76,19 @@ export function processConditionalSQL(
|
|
|
76
76
|
return result;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Convert :name placeholders to positional $n; return processed SQL + values.
|
|
81
|
+
* Delegates to the shared scanner-backed expander (params.ts) so a `:name`
|
|
82
|
+
* inside a string literal, comment, or `::cast` is left alone, and a used param
|
|
83
|
+
* whose value is `undefined` throws instead of silently passing through.
|
|
84
|
+
*/
|
|
80
85
|
export function processParams(
|
|
81
86
|
sql: string,
|
|
82
87
|
params: Record<string, QueryParamValue>,
|
|
83
88
|
): ConditionalSQLOutput {
|
|
84
|
-
// Find all param references in order of first appearance.
|
|
85
|
-
const paramRegex = /:([a-zA-Z_][a-zA-Z0-9_]*)(?![a-zA-Z0-9_])/g;
|
|
86
|
-
const usedParams: string[] = [];
|
|
87
|
-
let match;
|
|
88
|
-
|
|
89
|
-
while ((match = paramRegex.exec(sql)) !== null) {
|
|
90
|
-
const name = match[1];
|
|
91
|
-
if (name in params && !usedParams.includes(name)) {
|
|
92
|
-
usedParams.push(name);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Replace each param with positional placeholder.
|
|
97
|
-
let processedSql = sql;
|
|
98
|
-
for (let i = 0; i < usedParams.length; i++) {
|
|
99
|
-
const name = usedParams[i];
|
|
100
|
-
const regex = new RegExp(`:${name}(?![a-zA-Z0-9_])`, "g");
|
|
101
|
-
processedSql = processedSql.replace(regex, `$${i + 1}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Extract param values in order.
|
|
105
|
-
const paramValues = usedParams.map(name => params[name]);
|
|
106
|
-
|
|
107
89
|
return {
|
|
108
|
-
sql:
|
|
109
|
-
params:
|
|
90
|
+
sql: expandNamedParams(sql, params),
|
|
91
|
+
params: collectParamValues(sql, params),
|
|
110
92
|
};
|
|
111
93
|
}
|
|
112
94
|
|
package/src/builder/params.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/builder/params.ts
|
|
2
|
+
import { scanPlaceholders, type PlaceholderOccurrence } from "./scanner.js";
|
|
2
3
|
|
|
3
4
|
/** Runtime parameter value type supported by query builders. */
|
|
4
5
|
export type QueryParamValue = string | number | boolean | null;
|
|
@@ -16,23 +17,22 @@ export type QueryParamInput =
|
|
|
16
17
|
| readonly QueryParamValue[]
|
|
17
18
|
| undefined;
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
20
|
+
// Placeholder detection is delegated to the shared scanner (scanner.ts), so the
|
|
21
|
+
// SELECT / conditional-SQL path skips string literals, comments, dollar-quotes,
|
|
22
|
+
// and `::cast` types exactly like the write builders — a single source of truth
|
|
23
|
+
// for what counts as a real `:name`. Array values still expand UNCONDITIONALLY
|
|
24
|
+
// here (any position), which is the builder's long-standing semantics and
|
|
25
|
+
// differs from the scanner's IN-list-gated expansion used by createSql/mutate.
|
|
23
26
|
|
|
24
27
|
/** Param names in order of first appearance that are present in `params`. */
|
|
25
28
|
function usedParamNames(
|
|
26
|
-
|
|
29
|
+
occ: readonly PlaceholderOccurrence[],
|
|
27
30
|
params: Record<string, QueryParamInput>,
|
|
28
31
|
): string[] {
|
|
29
32
|
const used: string[] = [];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const name = match[1];
|
|
34
|
-
if (name in params && !used.includes(name)) {
|
|
35
|
-
used.push(name);
|
|
33
|
+
for (const o of occ) {
|
|
34
|
+
if (o.name in params && !used.includes(o.name)) {
|
|
35
|
+
used.push(o.name);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
return used;
|
|
@@ -40,29 +40,35 @@ function usedParamNames(
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Replace :name placeholders with $n positional placeholders, ordered by
|
|
43
|
-
* first appearance. Array values expand to consecutive placeholders.
|
|
43
|
+
* first appearance. Array values expand to consecutive placeholders. A `:name`
|
|
44
|
+
* inside a string literal / comment / `::cast` is not a placeholder.
|
|
44
45
|
*/
|
|
45
46
|
export function expandNamedParams(
|
|
46
47
|
sql: string,
|
|
47
48
|
params: Record<string, QueryParamInput>,
|
|
48
49
|
): string {
|
|
49
|
-
const
|
|
50
|
-
|
|
50
|
+
const occ = scanPlaceholders(sql);
|
|
51
|
+
const used = usedParamNames(occ, params);
|
|
52
|
+
// First-appearance starting position for each used name; arrays reserve a
|
|
53
|
+
// contiguous block, repeats reuse the same block.
|
|
54
|
+
const startPos = new Map<string, number>();
|
|
51
55
|
let position = 1;
|
|
52
56
|
for (const name of used) {
|
|
57
|
+
startPos.set(name, position);
|
|
53
58
|
const value = params[name];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
position += Array.isArray(value) ? value.length : 1;
|
|
60
|
+
}
|
|
61
|
+
// Rewrite right-to-left so earlier indices stay valid as we splice.
|
|
62
|
+
let out = sql;
|
|
63
|
+
for (let k = occ.length - 1; k >= 0; k--) {
|
|
64
|
+
const o = occ[k];
|
|
65
|
+
const p = startPos.get(o.name);
|
|
66
|
+
if (p === undefined) continue; // not provided → left as literal :name
|
|
67
|
+
const value = params[o.name];
|
|
68
|
+
const replacement = Array.isArray(value)
|
|
69
|
+
? value.map((_, i) => `$${p + i}`).join(", ")
|
|
70
|
+
: `$${p}`;
|
|
71
|
+
out = out.slice(0, o.start) + replacement + out.slice(o.end);
|
|
66
72
|
}
|
|
67
73
|
return out;
|
|
68
74
|
}
|
|
@@ -75,7 +81,7 @@ export function collectParamValues(
|
|
|
75
81
|
sql: string,
|
|
76
82
|
params: Record<string, QueryParamInput>,
|
|
77
83
|
): QueryParamValue[] {
|
|
78
|
-
const used = usedParamNames(sql, params);
|
|
84
|
+
const used = usedParamNames(scanPlaceholders(sql), params);
|
|
79
85
|
const result: QueryParamValue[] = [];
|
|
80
86
|
for (const name of used) {
|
|
81
87
|
const value = params[name];
|
package/src/builder/select.ts
CHANGED
|
@@ -155,6 +155,16 @@ export interface SelectQueryBuilder<Schema extends DatabaseSchema, Sql extends S
|
|
|
155
155
|
id?: Id,
|
|
156
156
|
): SelectQueryBuilder<Schema, WithOrderBy<Sql, ColsText<Cols>, ResolveId<Id, "order", Sql["orderBys"]>>>;
|
|
157
157
|
|
|
158
|
+
/** Emit `SELECT DISTINCT`. Does not change the result column set. */
|
|
159
|
+
distinct(): SelectQueryBuilder<Schema, Sql>;
|
|
160
|
+
/**
|
|
161
|
+
* Emit `SELECT DISTINCT ON (columns)` (PostgreSQL). Does not change the
|
|
162
|
+
* result column set; pair with a matching ORDER BY for deterministic rows.
|
|
163
|
+
*/
|
|
164
|
+
distinctOn<const Cols extends string | readonly string[]>(
|
|
165
|
+
columns: Cols,
|
|
166
|
+
): SelectQueryBuilder<Schema, Sql>;
|
|
167
|
+
|
|
158
168
|
limit<const L extends number>(limit: L): SelectQueryBuilder<Schema, WithLimit<Sql, L>>;
|
|
159
169
|
limitIf<const L extends number>(condition: boolean, limit: L): SelectQueryBuilder<Schema, WithLimit<Sql, L>>;
|
|
160
170
|
offset<const O extends number>(offset: O): SelectQueryBuilder<Schema, WithOffset<Sql, O>>;
|
|
@@ -319,6 +329,15 @@ class SelectQueryBuilderImpl<Schema extends DatabaseSchema, Sql extends SqlTag>
|
|
|
319
329
|
return condition ? this.orderBy(columns, id) : this.next(this._state);
|
|
320
330
|
}
|
|
321
331
|
|
|
332
|
+
distinct(): any {
|
|
333
|
+
return this.next(this.clone({ distinct: true }));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
distinctOn(columns: string | readonly string[]): any {
|
|
337
|
+
const cols = Array.isArray(columns) ? columns.join(", ") : (columns as string);
|
|
338
|
+
return this.next(this.clone({ distinctOn: cols }));
|
|
339
|
+
}
|
|
340
|
+
|
|
322
341
|
limit(limit: number): any {
|
|
323
342
|
return this.next(this.clone({ limit }));
|
|
324
343
|
}
|
|
@@ -453,7 +472,6 @@ class SelectQueryBuilderImpl<Schema extends DatabaseSchema, Sql extends SqlTag>
|
|
|
453
472
|
// getParams scans fragments joined by " " BEFORE $n substitution).
|
|
454
473
|
function assembleSelectSQLPreSub(state: RuntimeSelectState): string {
|
|
455
474
|
return [
|
|
456
|
-
...Object.values(state.cteSql),
|
|
457
475
|
...Object.values(state.selectSql).flat(),
|
|
458
476
|
state.fromSql ?? "",
|
|
459
477
|
...Object.values(state.joinSql),
|
|
@@ -461,7 +479,6 @@ function assembleSelectSQLPreSub(state: RuntimeSelectState): string {
|
|
|
461
479
|
...Object.values(state.groupBySql),
|
|
462
480
|
...Object.values(state.havingSql),
|
|
463
481
|
...Object.values(state.orderBySql),
|
|
464
|
-
state.unionSql ?? "",
|
|
465
482
|
].join(" ");
|
|
466
483
|
}
|
|
467
484
|
|
package/src/builder/state.ts
CHANGED
|
@@ -23,11 +23,10 @@ export interface RuntimeSelectState {
|
|
|
23
23
|
readonly havingSql: { readonly [id: string]: string };
|
|
24
24
|
/** Raw ORDER BY fragments by id (joined with ", "). */
|
|
25
25
|
readonly orderBySql: { readonly [id: string]: string };
|
|
26
|
-
/**
|
|
27
|
-
readonly cteSql: { readonly [id: string]: string };
|
|
28
|
-
/** Raw UNION fragment (if any). */
|
|
29
|
-
readonly unionSql?: string;
|
|
26
|
+
/** SELECT DISTINCT when true (ignored if `distinctOn` is set). */
|
|
30
27
|
readonly distinct: boolean;
|
|
28
|
+
/** SELECT DISTINCT ON (...) expression list, if set (already joined). */
|
|
29
|
+
readonly distinctOn?: string;
|
|
31
30
|
readonly limit?: number;
|
|
32
31
|
readonly offset?: number;
|
|
33
32
|
/** Legacy positional params (kept for getParams() fallback). */
|
|
@@ -45,9 +44,8 @@ export const EMPTY_RUNTIME_STATE: RuntimeSelectState = {
|
|
|
45
44
|
groupBySql: {},
|
|
46
45
|
havingSql: {},
|
|
47
46
|
orderBySql: {},
|
|
48
|
-
cteSql: {},
|
|
49
|
-
unionSql: undefined,
|
|
50
47
|
distinct: false,
|
|
48
|
+
distinctOn: undefined,
|
|
51
49
|
limit: undefined,
|
|
52
50
|
offset: undefined,
|
|
53
51
|
params: [],
|
package/src/expressions.ts
CHANGED
|
@@ -219,7 +219,14 @@ export type FunctionKeyFromExpr<E extends string> =
|
|
|
219
219
|
export type IsBoolExpr<CE extends string> =
|
|
220
220
|
CE extends `case ${string}`
|
|
221
221
|
? false
|
|
222
|
-
: HasTopLevelCompare
|
|
222
|
+
// Pre-gate: `HasTopLevelCompare`'s only `true` branch requires a
|
|
223
|
+
// `<`/`>`/`=`/`!` char; if `CE` contains none, the answer is `false` without
|
|
224
|
+
// the char-walk. Cheap template test short-circuits the common no-comparison
|
|
225
|
+
// projection (a bare column / function call). Must list all four chars the
|
|
226
|
+
// walk's true branch matches.
|
|
227
|
+
: CE extends `${string}${"<" | ">" | "=" | "!"}${string}`
|
|
228
|
+
? HasTopLevelCompare<CE>
|
|
229
|
+
: false;
|
|
223
230
|
|
|
224
231
|
// Scans for a comparison operator outside parens and outside `'…'`/`"…"` quotes.
|
|
225
232
|
// `->>`, `#>>` and `::` are consumed as units so their `>`/`:` are not mistaken
|
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
|
@@ -47,6 +47,20 @@ type LowercaseOutsideQuotesDrive<R> =
|
|
|
47
47
|
? LowercaseOutsideQuotesDrive<LowercaseOutsideQuotesWorker<S, Q1, Q2, Acc, []>>
|
|
48
48
|
: R;
|
|
49
49
|
|
|
50
|
+
// Segment-jump, not per-char. Each step advances a whole quote-bounded run:
|
|
51
|
+
// outside quotes it jumps to the LEFTMOST of `'`/`"`, lowercasing the run before
|
|
52
|
+
// it in a single `Lowercase<…>` intrinsic; inside a quote it copies verbatim to
|
|
53
|
+
// the matching close-quote. Cost is O(quote boundaries), not O(chars) — the old
|
|
54
|
+
// per-char walk emitted one instantiation per character on every NormalizeQuery.
|
|
55
|
+
// The `Steps` cap now counts JUMPS (a handful even for report-scale queries), so
|
|
56
|
+
// 450 is far past any real query yet still yields `{ __c }` for the driver before
|
|
57
|
+
// TS's recursion ceiling on a pathologically quote-dense input.
|
|
58
|
+
//
|
|
59
|
+
// Exact-equivalent to the walk it replaces: `''` escapes toggle single-quote
|
|
60
|
+
// state twice (exit on the first `'`, the second is re-seen outside with an empty
|
|
61
|
+
// prefix and re-enters); an unterminated quote at EOF copies the rest verbatim
|
|
62
|
+
// (`${Acc}${S}`); a `"` inside a single-quoted run never flips state (the
|
|
63
|
+
// in-single branch only scans for `'`, and vice-versa).
|
|
50
64
|
type LowercaseOutsideQuotesWorker<
|
|
51
65
|
S extends string,
|
|
52
66
|
InSingleQuote extends boolean,
|
|
@@ -55,25 +69,26 @@ type LowercaseOutsideQuotesWorker<
|
|
|
55
69
|
Steps extends any[]
|
|
56
70
|
> = Steps["length"] extends 450
|
|
57
71
|
? { __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
|
-
|
|
72
|
+
: InSingleQuote extends true
|
|
73
|
+
? S extends `${infer P}'${infer R}`
|
|
74
|
+
? LowercaseOutsideQuotesWorker<R, false, InDoubleQuote, `${Acc}${P}'`, [any, ...Steps]>
|
|
75
|
+
: `${Acc}${S}`
|
|
76
|
+
: InDoubleQuote extends true
|
|
77
|
+
? S extends `${infer P}"${infer R}`
|
|
78
|
+
? LowercaseOutsideQuotesWorker<R, InSingleQuote, false, `${Acc}${P}"`, [any, ...Steps]>
|
|
79
|
+
: `${Acc}${S}`
|
|
80
|
+
: S extends `${infer P}'${infer R}`
|
|
81
|
+
? P extends `${string}"${string}`
|
|
82
|
+
// a `"` precedes the first `'` → the double quote is leftmost
|
|
83
|
+
? S extends `${infer P2}"${infer R2}`
|
|
84
|
+
? LowercaseOutsideQuotesWorker<R2, InSingleQuote, true, `${Acc}${Lowercase<P2>}"`, [any, ...Steps]>
|
|
85
|
+
: `${Acc}${Lowercase<S>}`
|
|
86
|
+
// `'` is the leftmost quote
|
|
87
|
+
: LowercaseOutsideQuotesWorker<R, true, InDoubleQuote, `${Acc}${Lowercase<P>}'`, [any, ...Steps]>
|
|
88
|
+
// no `'` remains → only a `"` could open a verbatim run
|
|
89
|
+
: S extends `${infer P2}"${infer R2}`
|
|
90
|
+
? LowercaseOutsideQuotesWorker<R2, InSingleQuote, true, `${Acc}${Lowercase<P2>}"`, [any, ...Steps]>
|
|
91
|
+
: `${Acc}${Lowercase<S>}`;
|
|
77
92
|
|
|
78
93
|
// ---------------------------------------------------------------------------
|
|
79
94
|
// Param-name-preserving lowercaser (write/raw builder path only).
|
|
@@ -145,6 +160,15 @@ type ReadParamIdent<S extends string, Acc extends string = "", N extends any[] =
|
|
|
145
160
|
: ReadParamIdent<R, `${Acc}${C}`, [any, ...N]>
|
|
146
161
|
: { name: Acc; rest: S };
|
|
147
162
|
|
|
163
|
+
// Segment-jump sibling of `LowercaseOutsideQuotesWorker`, with the extra
|
|
164
|
+
// outside-quote rule that a lone `:` opens a case-PRESERVED `:name` param and
|
|
165
|
+
// `::` is a cast unit. Outside quotes it is therefore a leftmost-of-THREE jump
|
|
166
|
+
// (`'` / `"` / `:`): split on the first `:`; if a quote occurs in the run BEFORE
|
|
167
|
+
// it, the quote is leftmost so defer to `LcKeepQuoteJump` (the same leftmost-of-2
|
|
168
|
+
// quote logic as the plain worker); otherwise the colon is leftmost — lowercase
|
|
169
|
+
// the run, then consume `::` or read the verbatim param ident, exactly as the old
|
|
170
|
+
// per-char branch did. In/out-of-quote verbatim copies and the `''`/unterminated
|
|
171
|
+
// edges match `LowercaseOutsideQuotesWorker`.
|
|
148
172
|
type LcKeepWorker<
|
|
149
173
|
S extends string,
|
|
150
174
|
InSingleQuote extends boolean,
|
|
@@ -153,33 +177,46 @@ type LcKeepWorker<
|
|
|
153
177
|
Steps extends any[]
|
|
154
178
|
> = Steps["length"] extends 450
|
|
155
179
|
? { __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
|
-
|
|
180
|
+
: InSingleQuote extends true
|
|
181
|
+
? S extends `${infer P}'${infer R}`
|
|
182
|
+
? LcKeepWorker<R, false, InDoubleQuote, `${Acc}${P}'`, [any, ...Steps]>
|
|
183
|
+
: `${Acc}${S}`
|
|
184
|
+
: InDoubleQuote extends true
|
|
185
|
+
? S extends `${infer P}"${infer R}`
|
|
186
|
+
? LcKeepWorker<R, InSingleQuote, false, `${Acc}${P}"`, [any, ...Steps]>
|
|
187
|
+
: `${Acc}${S}`
|
|
188
|
+
: S extends `${infer Pc}:${infer Rc}`
|
|
189
|
+
// a quote before the first `:` → the quote is leftmost
|
|
190
|
+
? Pc extends `${string}'${string}`
|
|
191
|
+
? LcKeepQuoteJump<S, InSingleQuote, InDoubleQuote, Acc, Steps>
|
|
192
|
+
: Pc extends `${string}"${string}`
|
|
193
|
+
? LcKeepQuoteJump<S, InSingleQuote, InDoubleQuote, Acc, Steps>
|
|
194
|
+
// colon is leftmost: `::` cast unit, else verbatim `:name`
|
|
195
|
+
: Rc extends `:${infer R2}`
|
|
196
|
+
? LcKeepWorker<R2, InSingleQuote, InDoubleQuote, `${Acc}${Lowercase<Pc>}::`, [any, ...Steps]>
|
|
197
|
+
: ReadParamIdent<Rc> extends { name: infer Nm extends string; rest: infer Rr extends string }
|
|
198
|
+
? LcKeepWorker<Rr, InSingleQuote, InDoubleQuote, `${Acc}${Lowercase<Pc>}:${Nm}`, [any, ...Steps]>
|
|
199
|
+
: LcKeepWorker<Rc, InSingleQuote, InDoubleQuote, `${Acc}${Lowercase<Pc>}:`, [any, ...Steps]>
|
|
200
|
+
// no `:` remains → only quotes (or nothing) left
|
|
201
|
+
: LcKeepQuoteJump<S, InSingleQuote, InDoubleQuote, Acc, Steps>;
|
|
202
|
+
|
|
203
|
+
// Leftmost-of-2 quote jump (identical shape to the plain worker's outside-quote
|
|
204
|
+
// branch) that hands the continuation back to `LcKeepWorker`.
|
|
205
|
+
type LcKeepQuoteJump<
|
|
206
|
+
S extends string,
|
|
207
|
+
InSingleQuote extends boolean,
|
|
208
|
+
InDoubleQuote extends boolean,
|
|
209
|
+
Acc extends string,
|
|
210
|
+
Steps extends any[]
|
|
211
|
+
> = S extends `${infer P}'${infer R}`
|
|
212
|
+
? P extends `${string}"${string}`
|
|
213
|
+
? S extends `${infer P2}"${infer R2}`
|
|
214
|
+
? LcKeepWorker<R2, InSingleQuote, true, `${Acc}${Lowercase<P2>}"`, [any, ...Steps]>
|
|
215
|
+
: `${Acc}${Lowercase<S>}`
|
|
216
|
+
: LcKeepWorker<R, true, InDoubleQuote, `${Acc}${Lowercase<P>}'`, [any, ...Steps]>
|
|
217
|
+
: S extends `${infer P2}"${infer R2}`
|
|
218
|
+
? LcKeepWorker<R2, InSingleQuote, true, `${Acc}${Lowercase<P2>}"`, [any, ...Steps]>
|
|
219
|
+
: `${Acc}${Lowercase<S>}`;
|
|
183
220
|
|
|
184
221
|
// NormalizeQuery variant that preserves `:name` param case — used by the
|
|
185
222
|
// write/raw builder param extraction (ExtractParams) only.
|
|
@@ -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
|
|