@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.
Files changed (47) hide show
  1. package/README.md +7 -0
  2. package/dist/builder/assemble.d.ts.map +1 -1
  3. package/dist/builder/assemble.js +9 -10
  4. package/dist/builder/assemble.js.map +1 -1
  5. package/dist/builder/conditional-sql.d.ts +7 -2
  6. package/dist/builder/conditional-sql.d.ts.map +1 -1
  7. package/dist/builder/conditional-sql.js +9 -22
  8. package/dist/builder/conditional-sql.js.map +1 -1
  9. package/dist/builder/params.d.ts +2 -1
  10. package/dist/builder/params.d.ts.map +1 -1
  11. package/dist/builder/params.js +34 -27
  12. package/dist/builder/params.js.map +1 -1
  13. package/dist/builder/select.d.ts +7 -0
  14. package/dist/builder/select.d.ts.map +1 -1
  15. package/dist/builder/select.js +7 -2
  16. package/dist/builder/select.js.map +1 -1
  17. package/dist/builder/state.d.ts +3 -6
  18. package/dist/builder/state.d.ts.map +1 -1
  19. package/dist/builder/state.js +1 -2
  20. package/dist/builder/state.js.map +1 -1
  21. package/dist/expressions.d.ts +1 -1
  22. package/dist/expressions.d.ts.map +1 -1
  23. package/dist/parsing/extract.d.ts +2 -1
  24. package/dist/parsing/extract.d.ts.map +1 -1
  25. package/dist/parsing/normalize.d.ts +4 -3
  26. package/dist/parsing/normalize.d.ts.map +1 -1
  27. package/dist/parsing/pg-literals.d.ts +2 -2
  28. package/dist/parsing/pg-literals.d.ts.map +1 -1
  29. package/dist/parsing/tokenize.d.ts +8 -6
  30. package/dist/parsing/tokenize.d.ts.map +1 -1
  31. package/dist/validation/dispatch.d.ts +3 -1
  32. package/dist/validation/dispatch.d.ts.map +1 -1
  33. package/dist/validation/validate-columns.d.ts +1 -1
  34. package/dist/validation/validate-columns.d.ts.map +1 -1
  35. package/package.json +3 -2
  36. package/src/builder/assemble.ts +9 -13
  37. package/src/builder/conditional-sql.ts +9 -27
  38. package/src/builder/params.ts +33 -27
  39. package/src/builder/select.ts +19 -2
  40. package/src/builder/state.ts +4 -6
  41. package/src/expressions.ts +8 -1
  42. package/src/parsing/extract.ts +18 -4
  43. package/src/parsing/normalize.ts +83 -46
  44. package/src/parsing/pg-literals.ts +23 -12
  45. package/src/parsing/tokenize.ts +56 -23
  46. package/src/validation/dispatch.ts +21 -3
  47. package/src/validation/validate-columns.ts +13 -7
@@ -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
- const cteIds = Object.keys(state.cteSql);
19
- if (cteIds.length > 0) {
20
- const withParts = cteIds.map(id => state.cteSql[id]).join(", ");
21
- parts.push(`WITH ${withParts}`);
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("SELECT *");
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 { QueryParamValue } from "./params.js";
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
- /** Convert :name placeholders to positional $n; return processed SQL + values. */
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: processedSql,
109
- params: paramValues,
90
+ sql: expandNamedParams(sql, params),
91
+ params: collectParamValues(sql, params),
110
92
  };
111
93
  }
112
94
 
@@ -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
- // Ported verbatim from OLD: trailing negative lookahead stops a short param
20
- // (:te) from clobbering a longer one (:text). Matching the second colon of a
21
- // ::cast is intentional parity (pinned by params.test.ts).
22
- const PARAM_REGEX = /:([a-zA-Z_][a-zA-Z0-9_]*)(?![a-zA-Z0-9_])/g;
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
- sql: string,
29
+ occ: readonly PlaceholderOccurrence[],
27
30
  params: Record<string, QueryParamInput>,
28
31
  ): string[] {
29
32
  const used: string[] = [];
30
- let match: RegExpExecArray | null;
31
- PARAM_REGEX.lastIndex = 0;
32
- while ((match = PARAM_REGEX.exec(sql)) !== null) {
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 used = usedParamNames(sql, params);
50
- let out = sql;
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
- const regex = new RegExp(`:${name}(?![a-zA-Z0-9_])`, "g");
55
- if (Array.isArray(value)) {
56
- const placeholders = value
57
- .map((_, i) => `$${position + i}`)
58
- .join(", ");
59
- out = out.replace(regex, placeholders);
60
- position += value.length;
61
- }
62
- else {
63
- out = out.replace(regex, `$${position}`);
64
- position++;
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];
@@ -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
 
@@ -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
- /** Raw CTE fragments by id. */
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: [],
@@ -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<CE>;
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
@@ -22,7 +22,21 @@ export type ExtractSelectList<N extends string> =
22
22
  export type ExtractReturningList<N extends string> =
23
23
  FirstTopLevelReturningTail<N>;
24
24
 
25
- export type FirstTopLevelReturningTail<
25
+ // Quote-free fast-path (mirrors `HasReturningQuoteAware`): with no `'` and no `"`
26
+ // every ` returning ` is top-level, so the FIRST one's tail is exact via a single
27
+ // leftmost template match — skipping the ~1200-step walk on every quote-free DML.
28
+ // Only quote-bearing queries take the walk. The fast-path pattern matches the walk's
29
+ // own step-cap fallback, so behavior is unchanged.
30
+ export type FirstTopLevelReturningTail<S extends string> =
31
+ string extends S
32
+ ? ""
33
+ : S extends `${string}'${string}`
34
+ ? FirstTopLevelReturningTailWalk<S>
35
+ : S extends `${string}"${string}`
36
+ ? FirstTopLevelReturningTailWalk<S>
37
+ : S extends `${string} returning ${infer After}` ? After : "";
38
+
39
+ type FirstTopLevelReturningTailWalk<
26
40
  S extends string,
27
41
  InString extends boolean = false,
28
42
  InDString extends boolean = false,
@@ -33,16 +47,16 @@ export type FirstTopLevelReturningTail<
33
47
  ? S extends `${string} returning ${infer After}` ? After : ""
34
48
  : InString extends true
35
49
  ? S extends `${infer C}${infer Rest}`
36
- ? FirstTopLevelReturningTail<Rest, C extends "'" ? false : true, InDString, [any, ...Steps]>
50
+ ? FirstTopLevelReturningTailWalk<Rest, C extends "'" ? false : true, InDString, [any, ...Steps]>
37
51
  : ""
38
52
  : InDString extends true
39
53
  ? S extends `${infer C}${infer Rest}`
40
- ? FirstTopLevelReturningTail<Rest, InString, C extends `"` ? false : true, [any, ...Steps]>
54
+ ? FirstTopLevelReturningTailWalk<Rest, InString, C extends `"` ? false : true, [any, ...Steps]>
41
55
  : ""
42
56
  : S extends ` returning ${infer After}`
43
57
  ? After
44
58
  : S extends `${infer C}${infer Rest}`
45
- ? FirstTopLevelReturningTail<Rest, C extends "'" ? true : false, C extends `"` ? true : false, [any, ...Steps]>
59
+ ? FirstTopLevelReturningTailWalk<Rest, C extends "'" ? true : false, C extends `"` ? true : false, [any, ...Steps]>
46
60
  : "";
47
61
 
48
62
  // Given a string whose first non-skipped char is `(`, consume the first
@@ -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
- : S extends `${infer C}${infer Rest}`
59
- ? C extends "'"
60
- ? InDoubleQuote extends true
61
- ? LowercaseOutsideQuotesWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
62
- : InSingleQuote extends true
63
- ? LowercaseOutsideQuotesWorker<Rest, false, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
64
- : LowercaseOutsideQuotesWorker<Rest, true, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
65
- : C extends `"`
66
- ? InSingleQuote extends true
67
- ? LowercaseOutsideQuotesWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
68
- : InDoubleQuote extends true
69
- ? LowercaseOutsideQuotesWorker<Rest, InSingleQuote, false, `${Acc}${C}`, [any, ...Steps]>
70
- : LowercaseOutsideQuotesWorker<Rest, InSingleQuote, true, `${Acc}${C}`, [any, ...Steps]>
71
- : InSingleQuote extends true
72
- ? LowercaseOutsideQuotesWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
73
- : InDoubleQuote extends true
74
- ? LowercaseOutsideQuotesWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
75
- : LowercaseOutsideQuotesWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${Lowercase<C>}`, [any, ...Steps]>
76
- : Acc;
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
- : S extends `${infer C}${infer Rest}`
157
- ? C extends "'"
158
- ? InDoubleQuote extends true
159
- ? LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
160
- : InSingleQuote extends true
161
- ? LcKeepWorker<Rest, false, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
162
- : LcKeepWorker<Rest, true, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
163
- : C extends `"`
164
- ? InSingleQuote extends true
165
- ? LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
166
- : InDoubleQuote extends true
167
- ? LcKeepWorker<Rest, InSingleQuote, false, `${Acc}${C}`, [any, ...Steps]>
168
- : LcKeepWorker<Rest, InSingleQuote, true, `${Acc}${C}`, [any, ...Steps]>
169
- : InSingleQuote extends true
170
- ? LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
171
- : InDoubleQuote extends true
172
- ? LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${C}`, [any, ...Steps]>
173
- // outside quotes: a lone `:` begins a named param (case-
174
- // preserved); `::` is a cast operator consumed as a unit.
175
- : C extends ":"
176
- ? Rest extends `:${infer R2}`
177
- ? LcKeepWorker<R2, InSingleQuote, InDoubleQuote, `${Acc}::`, [any, ...Steps]>
178
- : ReadParamIdent<Rest> extends { name: infer Nm extends string; rest: infer Rr extends string }
179
- ? LcKeepWorker<Rr, InSingleQuote, InDoubleQuote, `${Acc}:${Nm}`, [any, ...Steps]>
180
- : LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}:`, [any, ...Steps]>
181
- : LcKeepWorker<Rest, InSingleQuote, InDoubleQuote, `${Acc}${Lowercase<C>}`, [any, ...Steps]>
182
- : Acc;
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. Bounded; on bail returns the
210
- // best-effort parity so far.
209
+ // an unterminated single-quoted string literal. Marker-jump: each step hops to the
210
+ // next `'` (the `${infer _Pre}` skips a whole run of non-quote chars at once) and
211
+ // flips the parity, so the depth is the NUMBER OF QUOTES — a handful — not the
212
+ // string length. `${infer _Pre}'${infer R}` matches the LEFTMOST `'`, so quotes are
213
+ // counted in order, exactly as the old per-char toggle did. Bounded against a
214
+ // pathological quote-dense string; on bail returns the best-effort parity so far.
211
215
  export type OddSingleQuotes<S extends string, Flag extends boolean = false, Steps extends any[] = []> =
212
216
  string extends S
213
217
  ? false
214
218
  : Steps["length"] extends 400
215
219
  ? Flag
216
- : S extends `${infer C}${infer R}`
217
- ? OddSingleQuotes<R, C extends "'" ? (Flag extends true ? false : true) : Flag, [any, ...Steps]>
220
+ : S extends `${infer _Pre}'${infer R}`
221
+ ? OddSingleQuotes<R, Flag extends true ? false : true, [any, ...Steps]>
218
222
  : Flag;
219
223
 
220
224
  // Strip `/* ... */` block comments AND `-- ...` line comments before any other
@@ -277,13 +281,20 @@ export type StripCommentsWalk<
277
281
 
278
282
  // Skip a line comment body, returning the tail starting at the first newline
279
283
  // (which is kept so words on either side of the comment can't merge). A comment
280
- // that runs to the end of the string yields `""`. Bounded against runaway.
281
- export type LineCommentTail<S extends string, Steps extends any[] = []> =
282
- Steps["length"] extends 1000
283
- ? S
284
- : S extends `${infer C}${infer Rest}`
285
- ? C extends "\n" | "\r"
286
- ? S
287
- : LineCommentTail<Rest, [any, ...Steps]>
284
+ // that runs to the end of the string yields `""`.
285
+ //
286
+ // Marker-jump: locate the first `\n`/`\r` with template matching instead of a
287
+ // per-char walk. `${infer Pre}\n${infer After}` finds the LEFTMOST `\n`; if its
288
+ // prefix `Pre` itself holds a `\r`, that `\r` is the earlier newline, so the tail
289
+ // starts there (`\r${PreB}\n${After}`). With no `\n`, fall back to the leftmost
290
+ // `\r`; with neither, the comment runs to EOF → `""`. No recursion (and so no step
291
+ // cap): a 1000-char single-line comment is now ~2 template instantiations.
292
+ export type LineCommentTail<S extends string> =
293
+ S extends `${infer Pre}\n${infer After}`
294
+ ? Pre extends `${infer _PreA}\r${infer PreB}`
295
+ ? `\r${PreB}\n${After}`
296
+ : `\n${After}`
297
+ : S extends `${infer _P}\r${infer After}`
298
+ ? `\r${After}`
288
299
  : "";
289
300