@kuindji/typed-sql 0.1.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 (208) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/dist/builder/assemble.d.ts +13 -0
  4. package/dist/builder/assemble.d.ts.map +1 -0
  5. package/dist/builder/assemble.js +86 -0
  6. package/dist/builder/assemble.js.map +1 -0
  7. package/dist/builder/condition-tree.d.ts +27 -0
  8. package/dist/builder/condition-tree.d.ts.map +1 -0
  9. package/dist/builder/condition-tree.js +91 -0
  10. package/dist/builder/condition-tree.js.map +1 -0
  11. package/dist/builder/conditional-sql.d.ts +80 -0
  12. package/dist/builder/conditional-sql.d.ts.map +1 -0
  13. package/dist/builder/conditional-sql.js +88 -0
  14. package/dist/builder/conditional-sql.js.map +1 -0
  15. package/dist/builder/db.d.ts +76 -0
  16. package/dist/builder/db.d.ts.map +1 -0
  17. package/dist/builder/db.js +12 -0
  18. package/dist/builder/db.js.map +1 -0
  19. package/dist/builder/delete.d.ts +39 -0
  20. package/dist/builder/delete.d.ts.map +1 -0
  21. package/dist/builder/delete.js +33 -0
  22. package/dist/builder/delete.js.map +1 -0
  23. package/dist/builder/extract-params.d.ts +97 -0
  24. package/dist/builder/extract-params.d.ts.map +1 -0
  25. package/dist/builder/extract-params.js +2 -0
  26. package/dist/builder/extract-params.js.map +1 -0
  27. package/dist/builder/index.d.ts +23 -0
  28. package/dist/builder/index.d.ts.map +1 -0
  29. package/dist/builder/index.js +14 -0
  30. package/dist/builder/index.js.map +1 -0
  31. package/dist/builder/insert.d.ts +51 -0
  32. package/dist/builder/insert.d.ts.map +1 -0
  33. package/dist/builder/insert.js +39 -0
  34. package/dist/builder/insert.js.map +1 -0
  35. package/dist/builder/mutate.d.ts +28 -0
  36. package/dist/builder/mutate.d.ts.map +1 -0
  37. package/dist/builder/mutate.js +17 -0
  38. package/dist/builder/mutate.js.map +1 -0
  39. package/dist/builder/params.d.ts +22 -0
  40. package/dist/builder/params.d.ts.map +1 -0
  41. package/dist/builder/params.js +65 -0
  42. package/dist/builder/params.js.map +1 -0
  43. package/dist/builder/return-type.d.ts +39 -0
  44. package/dist/builder/return-type.d.ts.map +1 -0
  45. package/dist/builder/return-type.js +2 -0
  46. package/dist/builder/return-type.js.map +1 -0
  47. package/dist/builder/scanner.d.ts +49 -0
  48. package/dist/builder/scanner.d.ts.map +1 -0
  49. package/dist/builder/scanner.js +240 -0
  50. package/dist/builder/scanner.js.map +1 -0
  51. package/dist/builder/select.d.ts +76 -0
  52. package/dist/builder/select.d.ts.map +1 -0
  53. package/dist/builder/select.js +240 -0
  54. package/dist/builder/select.js.map +1 -0
  55. package/dist/builder/sql-tag.d.ts +319 -0
  56. package/dist/builder/sql-tag.d.ts.map +1 -0
  57. package/dist/builder/sql-tag.js +3 -0
  58. package/dist/builder/sql-tag.js.map +1 -0
  59. package/dist/builder/sql.d.ts +17 -0
  60. package/dist/builder/sql.d.ts.map +1 -0
  61. package/dist/builder/sql.js +36 -0
  62. package/dist/builder/sql.js.map +1 -0
  63. package/dist/builder/state.d.ts +53 -0
  64. package/dist/builder/state.d.ts.map +1 -0
  65. package/dist/builder/state.js +18 -0
  66. package/dist/builder/state.js.map +1 -0
  67. package/dist/builder/update.d.ts +60 -0
  68. package/dist/builder/update.d.ts.map +1 -0
  69. package/dist/builder/update.js +40 -0
  70. package/dist/builder/update.js.map +1 -0
  71. package/dist/builder/write-assemble.d.ts +5 -0
  72. package/dist/builder/write-assemble.d.ts.map +1 -0
  73. package/dist/builder/write-assemble.js +57 -0
  74. package/dist/builder/write-assemble.js.map +1 -0
  75. package/dist/builder/write-state.d.ts +39 -0
  76. package/dist/builder/write-state.d.ts.map +1 -0
  77. package/dist/builder/write-state.js +6 -0
  78. package/dist/builder/write-state.js.map +1 -0
  79. package/dist/builder/write-tag.d.ts +91 -0
  80. package/dist/builder/write-tag.d.ts.map +1 -0
  81. package/dist/builder/write-tag.js +2 -0
  82. package/dist/builder/write-tag.js.map +1 -0
  83. package/dist/columns.d.ts +33 -0
  84. package/dist/columns.d.ts.map +1 -0
  85. package/dist/columns.js +2 -0
  86. package/dist/columns.js.map +1 -0
  87. package/dist/expressions.d.ts +71 -0
  88. package/dist/expressions.d.ts.map +1 -0
  89. package/dist/expressions.js +2 -0
  90. package/dist/expressions.js.map +1 -0
  91. package/dist/index.d.ts +22 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +5 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/parsing/extract.d.ts +47 -0
  96. package/dist/parsing/extract.d.ts.map +1 -0
  97. package/dist/parsing/extract.js +2 -0
  98. package/dist/parsing/extract.js.map +1 -0
  99. package/dist/parsing/normalize.d.ts +44 -0
  100. package/dist/parsing/normalize.d.ts.map +1 -0
  101. package/dist/parsing/normalize.js +2 -0
  102. package/dist/parsing/normalize.js.map +1 -0
  103. package/dist/parsing/pg-literals.d.ts +37 -0
  104. package/dist/parsing/pg-literals.d.ts.map +1 -0
  105. package/dist/parsing/pg-literals.js +2 -0
  106. package/dist/parsing/pg-literals.js.map +1 -0
  107. package/dist/parsing/split.d.ts +100 -0
  108. package/dist/parsing/split.d.ts.map +1 -0
  109. package/dist/parsing/split.js +2 -0
  110. package/dist/parsing/split.js.map +1 -0
  111. package/dist/parsing/string-utils.d.ts +29 -0
  112. package/dist/parsing/string-utils.d.ts.map +1 -0
  113. package/dist/parsing/string-utils.js +2 -0
  114. package/dist/parsing/string-utils.js.map +1 -0
  115. package/dist/parsing/tokenize.d.ts +27 -0
  116. package/dist/parsing/tokenize.d.ts.map +1 -0
  117. package/dist/parsing/tokenize.js +2 -0
  118. package/dist/parsing/tokenize.js.map +1 -0
  119. package/dist/parsing.d.ts +7 -0
  120. package/dist/parsing.d.ts.map +1 -0
  121. package/dist/parsing.js +9 -0
  122. package/dist/parsing.js.map +1 -0
  123. package/dist/partial.d.ts +30 -0
  124. package/dist/partial.d.ts.map +1 -0
  125. package/dist/partial.js +10 -0
  126. package/dist/partial.js.map +1 -0
  127. package/dist/schema.d.ts +28 -0
  128. package/dist/schema.d.ts.map +1 -0
  129. package/dist/schema.js +2 -0
  130. package/dist/schema.js.map +1 -0
  131. package/dist/tables.d.ts +34 -0
  132. package/dist/tables.d.ts.map +1 -0
  133. package/dist/tables.js +2 -0
  134. package/dist/tables.js.map +1 -0
  135. package/dist/utils.d.ts +13 -0
  136. package/dist/utils.d.ts.map +1 -0
  137. package/dist/utils.js +3 -0
  138. package/dist/utils.js.map +1 -0
  139. package/dist/validation/cte.d.ts +54 -0
  140. package/dist/validation/cte.d.ts.map +1 -0
  141. package/dist/validation/cte.js +2 -0
  142. package/dist/validation/cte.js.map +1 -0
  143. package/dist/validation/dispatch.d.ts +31 -0
  144. package/dist/validation/dispatch.d.ts.map +1 -0
  145. package/dist/validation/dispatch.js +2 -0
  146. package/dist/validation/dispatch.js.map +1 -0
  147. package/dist/validation/joins.d.ts +16 -0
  148. package/dist/validation/joins.d.ts.map +1 -0
  149. package/dist/validation/joins.js +2 -0
  150. package/dist/validation/joins.js.map +1 -0
  151. package/dist/validation/return-derived.d.ts +67 -0
  152. package/dist/validation/return-derived.d.ts.map +1 -0
  153. package/dist/validation/return-derived.js +5 -0
  154. package/dist/validation/return-derived.js.map +1 -0
  155. package/dist/validation/return-types.d.ts +41 -0
  156. package/dist/validation/return-types.d.ts.map +1 -0
  157. package/dist/validation/return-types.js +2 -0
  158. package/dist/validation/return-types.js.map +1 -0
  159. package/dist/validation/validate-columns.d.ts +63 -0
  160. package/dist/validation/validate-columns.d.ts.map +1 -0
  161. package/dist/validation/validate-columns.js +2 -0
  162. package/dist/validation/validate-columns.js.map +1 -0
  163. package/dist/validation.d.ts +7 -0
  164. package/dist/validation.d.ts.map +1 -0
  165. package/dist/validation.js +9 -0
  166. package/dist/validation.js.map +1 -0
  167. package/package.json +64 -0
  168. package/src/builder/assemble.ts +100 -0
  169. package/src/builder/condition-tree.ts +162 -0
  170. package/src/builder/conditional-sql.ts +325 -0
  171. package/src/builder/db.ts +281 -0
  172. package/src/builder/delete.ts +57 -0
  173. package/src/builder/extract-params.ts +507 -0
  174. package/src/builder/index.ts +58 -0
  175. package/src/builder/insert.ts +75 -0
  176. package/src/builder/mutate.ts +55 -0
  177. package/src/builder/params.ts +95 -0
  178. package/src/builder/return-type.ts +66 -0
  179. package/src/builder/scanner.ts +254 -0
  180. package/src/builder/select.ts +470 -0
  181. package/src/builder/sql-tag.ts +422 -0
  182. package/src/builder/sql.ts +51 -0
  183. package/src/builder/state.ts +55 -0
  184. package/src/builder/update.ts +77 -0
  185. package/src/builder/write-assemble.ts +52 -0
  186. package/src/builder/write-state.ts +43 -0
  187. package/src/builder/write-tag.ts +119 -0
  188. package/src/columns.ts +336 -0
  189. package/src/expressions.ts +745 -0
  190. package/src/index.ts +81 -0
  191. package/src/parsing/extract.ts +260 -0
  192. package/src/parsing/normalize.ts +243 -0
  193. package/src/parsing/pg-literals.ts +289 -0
  194. package/src/parsing/split.ts +288 -0
  195. package/src/parsing/string-utils.ts +172 -0
  196. package/src/parsing/tokenize.ts +321 -0
  197. package/src/parsing.ts +8 -0
  198. package/src/partial.ts +241 -0
  199. package/src/schema.ts +130 -0
  200. package/src/tables.ts +278 -0
  201. package/src/utils.ts +43 -0
  202. package/src/validation/cte.ts +198 -0
  203. package/src/validation/dispatch.ts +312 -0
  204. package/src/validation/joins.ts +198 -0
  205. package/src/validation/return-derived.ts +253 -0
  206. package/src/validation/return-types.ts +271 -0
  207. package/src/validation/validate-columns.ts +489 -0
  208. package/src/validation.ts +8 -0
@@ -0,0 +1,55 @@
1
+ // src/builder/mutate.ts
2
+ import type { DatabaseSchema } from "../schema.js";
3
+ import type { ExtractParams, ExtractReturning } from "./extract-params.js";
4
+ import { prepareScanned, type DriverParamValue } from "./scanner.js";
5
+
6
+ /** Driver contract: returns the RETURNING rows, or [] for a no-RETURNING mutation. */
7
+ export type MutationHandler = (
8
+ sql: string,
9
+ params: DriverParamValue[],
10
+ ) => Promise<unknown[]>;
11
+
12
+ /** Row type a bound builder / createSql object yields. */
13
+ export type MutationReturnType<B> =
14
+ B extends { readonly __returning?: infer R } ? R : {};
15
+
16
+ /**
17
+ * Minimal structural shape both `BoundWrite` (builders) and `BoundSql`
18
+ * (`createSql`) satisfy. Used as the object-overload constraint instead of the
19
+ * deep `BoundWrite<S, any> | BoundSql<any, S>` union — matching a value against
20
+ * that union forces TS to compare the phantom `__returning` types of both arms,
21
+ * which recurses without bound. A shallow structural constraint avoids that; the
22
+ * row type is still derived from the value's own `__returning` via
23
+ * `MutationReturnType<B>`.
24
+ */
25
+ interface Executable {
26
+ toString(): string;
27
+ getParams(): ReadonlyArray<DriverParamValue>;
28
+ }
29
+
30
+ export function createMutateFn<S extends DatabaseSchema>(handler: MutationHandler) {
31
+ // Builder / createSql object overload.
32
+ function mutate<B extends Executable>(
33
+ query: B,
34
+ ): Promise<MutationReturnType<B>[]>;
35
+ // Raw string + named params overload (brand-checked).
36
+ function mutate<Q extends string>(
37
+ query: Q,
38
+ params: ExtractParams<Q, S>,
39
+ ): Promise<ExtractReturning<Q, S>[]>;
40
+
41
+ // async so a prep-time / assembly throw (missing live placeholder, empty
42
+ // INSERT/SET, etc.) surfaces as a rejected promise rather than a synchronous
43
+ // throw — consistent with the promise-returning executor contract.
44
+ async function mutate(query: Executable | string, params?: Record<string, DriverParamValue>) {
45
+ if (typeof query === "string") {
46
+ const { sql, values } = prepareScanned(query, params ?? {});
47
+ return handler(sql, values) as Promise<any>;
48
+ }
49
+ const sql = query.toString(); // already expanded + live-checked
50
+ const values = [...query.getParams()];
51
+ return handler(sql, values) as Promise<any>;
52
+ }
53
+
54
+ return mutate;
55
+ }
@@ -0,0 +1,95 @@
1
+ // src/builder/params.ts
2
+
3
+ /** Runtime parameter value type supported by query builders. */
4
+ export type QueryParamValue = string | number | boolean | null;
5
+
6
+ /**
7
+ * Input parameter value type — allows arrays (expanded to multiple
8
+ * placeholders, e.g. :ids with [1,2,3] → "$1, $2, $3") and undefined
9
+ * (throws at runtime only if the param is actually used).
10
+ *
11
+ * Array expansion is BUILDER-ONLY. Conditional SQL keeps a scalar-only
12
+ * signature (see conditional-sql.ts) for parity with the old package.
13
+ */
14
+ export type QueryParamInput =
15
+ | QueryParamValue
16
+ | readonly QueryParamValue[]
17
+ | undefined;
18
+
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;
23
+
24
+ /** Param names in order of first appearance that are present in `params`. */
25
+ function usedParamNames(
26
+ sql: string,
27
+ params: Record<string, QueryParamInput>,
28
+ ): string[] {
29
+ 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);
36
+ }
37
+ }
38
+ return used;
39
+ }
40
+
41
+ /**
42
+ * Replace :name placeholders with $n positional placeholders, ordered by
43
+ * first appearance. Array values expand to consecutive placeholders.
44
+ */
45
+ export function expandNamedParams(
46
+ sql: string,
47
+ params: Record<string, QueryParamInput>,
48
+ ): string {
49
+ const used = usedParamNames(sql, params);
50
+ let out = sql;
51
+ let position = 1;
52
+ for (const name of used) {
53
+ 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
+ }
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /**
71
+ * Flattened param values in placeholder order. Throws if a used param's
72
+ * value is undefined.
73
+ */
74
+ export function collectParamValues(
75
+ sql: string,
76
+ params: Record<string, QueryParamInput>,
77
+ ): QueryParamValue[] {
78
+ const used = usedParamNames(sql, params);
79
+ const result: QueryParamValue[] = [];
80
+ for (const name of used) {
81
+ const value = params[name];
82
+ if (value === undefined) {
83
+ throw new Error(
84
+ `Query parameter ":${name}" is used but its value is undefined`,
85
+ );
86
+ }
87
+ if (Array.isArray(value)) {
88
+ result.push(...value);
89
+ }
90
+ else {
91
+ result.push(value as QueryParamValue);
92
+ }
93
+ }
94
+ return result;
95
+ }
@@ -0,0 +1,66 @@
1
+ // src/builder/return-type.ts
2
+ import type { DatabaseSchema } from "../schema.js";
3
+ import type { GetReturnType } from "../index.js";
4
+ import type { BuildSQL, SqlTag, SelFrag } from "./sql-tag.js";
5
+
6
+ /** Type-level canonical SQL: the maximal query (all select fragments present). */
7
+ export type BuilderSQLFor<Sql extends SqlTag> = BuildSQL<Sql, "max">;
8
+
9
+ /** True iff some select fragment is unconditional. */
10
+ type HasUncond<List extends readonly SelFrag[]> =
11
+ List extends readonly [infer H extends SelFrag, ...infer R extends readonly SelFrag[]]
12
+ ? H["cond"] extends false ? true : HasUncond<R>
13
+ : false;
14
+
15
+ /** True iff NO select fragment is conditional (req-list === max-list). */
16
+ type AllUncond<List extends readonly SelFrag[]> =
17
+ List extends readonly [infer H extends SelFrag, ...infer R extends readonly SelFrag[]]
18
+ ? H["cond"] extends false ? AllUncond<R> : false
19
+ : true;
20
+
21
+ // Merge a "required" row and a "max" row into the partition:
22
+ // keys in ReqRow are required; keys only in Row are optional.
23
+ type Partition<Row, ReqRow> =
24
+ & { [K in keyof Row as K extends keyof ReqRow ? K : never]: Row[K] }
25
+ & { [K in keyof Row as K extends keyof ReqRow ? never : K]?: Row[K] };
26
+
27
+ /**
28
+ * Required/optional partition over GetReturnType of MaxSQL / ReqSQL / ScopeSQL.
29
+ * - allUncond: every selected column is unconditional, so the req-list and the
30
+ * max-list are identical and every key is required. Skip the second parse and
31
+ * the Partition entirely — both are pure overhead here, and parsing a very
32
+ * wide SELECT twice can cross TS's instantiation limit (TS2589).
33
+ * - hasUncond (but some conditional): Row = GetReturnType<MaxSQL>;
34
+ * ReqRow = GetReturnType<ReqSQL>; partition keys: required iff in ReqRow.
35
+ * - else (no unconditional select → all-false runtime path is SELECT *):
36
+ * Partial<GetReturnType<MaxSQL> & GetReturnType<ScopeSQL>>.
37
+ */
38
+ export type BuilderReturnTypeFor<Schema extends DatabaseSchema, Sql extends SqlTag> =
39
+ HasUncond<Sql["selects"]> extends true
40
+ ? AllUncond<Sql["selects"]> extends true
41
+ ? GetReturnType<BuildSQL<Sql, "max">, Schema>
42
+ : GetReturnType<BuildSQL<Sql, "max">, Schema> extends infer Row
43
+ ? GetReturnType<BuildSQL<Sql, "req">, Schema> extends infer ReqRow
44
+ ? Partition<Row, ReqRow>
45
+ : Row
46
+ : {}
47
+ : Partial<
48
+ & GetReturnType<BuildSQL<Sql, "max">, Schema>
49
+ & GetReturnType<BuildSQL<Sql, "scope">, Schema>
50
+ >;
51
+
52
+ /** Brand carried by toBrandedString(); not used at runtime. */
53
+ export interface BuilderResultBrand<Schema extends DatabaseSchema, Sql extends SqlTag> {
54
+ readonly __schema?: Schema;
55
+ readonly __sql?: Sql;
56
+ }
57
+
58
+ // --- B-keyed public aliases (extract Schema/Sql from a builder type) ---
59
+ import type { SelectQueryBuilder } from "./select.js";
60
+
61
+ /** Extract the Sql tag from a builder type. */
62
+ export type SqlOf<B> = B extends SelectQueryBuilder<any, infer Sql extends SqlTag> ? Sql : never;
63
+ type SchemaOf<B> = B extends SelectQueryBuilder<infer S extends DatabaseSchema, any> ? S : never;
64
+
65
+ export type BuilderSQL<B> = BuilderSQLFor<SqlOf<B>>;
66
+ export type BuilderReturnType<B> = BuilderReturnTypeFor<SchemaOf<B>, SqlOf<B>>;
@@ -0,0 +1,254 @@
1
+ // src/builder/scanner.ts
2
+
3
+ /**
4
+ * Value domain at the typed boundary (spec §6.5). Default `unknown` accepts
5
+ * scalars, branded scalars, arrays, dates, and JSON/object columns. The driver
6
+ * adapter is responsible for serialization.
7
+ */
8
+ export type DriverParamValue = unknown;
9
+
10
+ /** One real `:name` occurrence found by the scanner. */
11
+ export interface PlaceholderOccurrence {
12
+ /** Param name without leading ":" and without any `::cast` suffix. */
13
+ readonly name: string;
14
+ /** True iff this occurrence sits directly inside an `IN (...)` / `NOT IN (...)` group. */
15
+ readonly inExpansion: boolean;
16
+ /** Index of the ":" in the source SQL. */
17
+ readonly start: number;
18
+ /** Index just past the last char of the name. */
19
+ readonly end: number;
20
+ }
21
+
22
+ const isIdentStart = (c: string) => /[a-zA-Z_]/.test(c);
23
+ const isIdentChar = (c: string) => /[a-zA-Z0-9_]/.test(c);
24
+
25
+ /**
26
+ * Scan `sql` and return every real placeholder occurrence, skipping string
27
+ * literals (single-quote + dollar-quote), `--` line comments, and block
28
+ * comments, and treating `::type` casts as non-placeholders. Tracks, per paren
29
+ * group, whether the group opened immediately after `in` / `not in`, so each
30
+ * occurrence carries an accurate `inExpansion` flag (spec §6.3/§6.5).
31
+ */
32
+ export function scanPlaceholders(sql: string): PlaceholderOccurrence[] {
33
+ const out: PlaceholderOccurrence[] = [];
34
+ // Stack of paren contexts: true = this "(" opened right after IN / NOT IN.
35
+ const parenStack: boolean[] = [];
36
+ let i = 0;
37
+ const n = sql.length;
38
+
39
+ // Returns true if the run of word chars ending just before `idx` (skipping
40
+ // trailing whitespace) is `in`, optionally preceded by `not`.
41
+ const opensInList = (idx: number): boolean => {
42
+ let j = idx - 1;
43
+ while (j >= 0 && /\s/.test(sql[j])) j--;
44
+ let end = j + 1;
45
+ while (j >= 0 && isIdentChar(sql[j])) j--;
46
+ const w1 = sql.slice(j + 1, end).toLowerCase();
47
+ return w1 === "in";
48
+ };
49
+
50
+ while (i < n) {
51
+ const c = sql[i];
52
+
53
+ // -- line comment
54
+ if (c === "-" && sql[i + 1] === "-") {
55
+ i += 2;
56
+ while (i < n && sql[i] !== "\n") i++;
57
+ continue;
58
+ }
59
+ // /* block comment */
60
+ if (c === "/" && sql[i + 1] === "*") {
61
+ i += 2;
62
+ while (i < n && !(sql[i] === "*" && sql[i + 1] === "/")) i++;
63
+ i += 2;
64
+ continue;
65
+ }
66
+ // double-quoted identifier (with "" escape) — a `:name`-looking run
67
+ // inside a quoted identifier (`"tenant:region"`) is part of the name,
68
+ // not a placeholder.
69
+ if (c === '"') {
70
+ i++;
71
+ while (i < n) {
72
+ if (sql[i] === '"' && sql[i + 1] === '"') { i += 2; continue; }
73
+ if (sql[i] === '"') { i++; break; }
74
+ i++;
75
+ }
76
+ continue;
77
+ }
78
+ // PostgreSQL escape string E'...' — backslash escapes are active, so a
79
+ // `\'` (and the usual `''`) escapes a quote rather than closing the
80
+ // literal. A bare `'` scan would mis-read `\'` as the terminator and leak
81
+ // any `:name` that follows back into the placeholder stream. The `E` must
82
+ // be a standalone string prefix, not the tail of an identifier.
83
+ if ((c === "E" || c === "e") && sql[i + 1] === "'" &&
84
+ !(i > 0 && isIdentChar(sql[i - 1]))) {
85
+ i += 2; // skip the `E` and the opening quote
86
+ while (i < n) {
87
+ if (sql[i] === "\\") { i += 2; continue; } // backslash escapes next char
88
+ if (sql[i] === "'" && sql[i + 1] === "'") { i += 2; continue; }
89
+ if (sql[i] === "'") { i++; break; }
90
+ i++;
91
+ }
92
+ continue;
93
+ }
94
+ // single-quoted string (with '' escape)
95
+ if (c === "'") {
96
+ i++;
97
+ while (i < n) {
98
+ if (sql[i] === "'" && sql[i + 1] === "'") { i += 2; continue; }
99
+ if (sql[i] === "'") { i++; break; }
100
+ i++;
101
+ }
102
+ continue;
103
+ }
104
+ // dollar-quoted string: $tag$ ... $tag$
105
+ if (c === "$") {
106
+ const m = /^\$([a-zA-Z_]\w*)?\$/.exec(sql.slice(i));
107
+ if (m) {
108
+ const tag = m[0];
109
+ const close = sql.indexOf(tag, i + tag.length);
110
+ i = close === -1 ? n : close + tag.length;
111
+ continue;
112
+ }
113
+ }
114
+ // parens — track IN-list context
115
+ if (c === "(") {
116
+ parenStack.push(opensInList(i));
117
+ i++;
118
+ continue;
119
+ }
120
+ if (c === ")") {
121
+ parenStack.pop();
122
+ i++;
123
+ continue;
124
+ }
125
+ // cast `::` — skip both colons, never a placeholder
126
+ if (c === ":" && sql[i + 1] === ":") {
127
+ i += 2;
128
+ continue;
129
+ }
130
+ // placeholder `:name`
131
+ if (c === ":" && isIdentStart(sql[i + 1] ?? "")) {
132
+ const start = i;
133
+ i++;
134
+ const from = i;
135
+ while (i < n && isIdentChar(sql[i])) i++;
136
+ const name = sql.slice(from, i);
137
+ const inExpansion = parenStack.length > 0 && parenStack[parenStack.length - 1];
138
+ out.push({ name, inExpansion, start, end: i });
139
+ continue;
140
+ }
141
+ i++;
142
+ }
143
+ return out;
144
+ }
145
+
146
+ /** Ordered unique names (first appearance) with their merged expansion flag. */
147
+ function uniqueNames(
148
+ occ: readonly PlaceholderOccurrence[],
149
+ ): { name: string; inExpansion: boolean }[] {
150
+ const order: string[] = [];
151
+ const expand = new Map<string, boolean>();
152
+ const seenNonExpand = new Map<string, boolean>();
153
+ for (const o of occ) {
154
+ if (!order.includes(o.name)) order.push(o.name);
155
+ if (o.inExpansion) expand.set(o.name, true);
156
+ else seenNonExpand.set(o.name, true);
157
+ }
158
+ // Mixed IN / non-IN reuse is unsound (spec §6.5) — one value cannot be both
159
+ // N positional slots and one slot.
160
+ for (const name of order) {
161
+ if (expand.get(name) && seenNonExpand.get(name)) {
162
+ throw new Error(
163
+ `Query parameter ":${name}" is used in mixed IN and non-IN positions; ` +
164
+ `a name cannot be both an expanded IN list and a scalar.`,
165
+ );
166
+ }
167
+ }
168
+ return order.map(name => ({ name, inExpansion: expand.get(name) ?? false }));
169
+ }
170
+
171
+ /**
172
+ * Replace `:name` with `$n` (first-appearance order; repeats reuse the same
173
+ * `$n`). Only IN-list occurrences with an array value expand to multiple slots;
174
+ * every other value (including array-VALUED columns and JSON objects) is a
175
+ * single slot. Driven entirely by the shared scanner (spec §6.5).
176
+ */
177
+ export function expandScanned(
178
+ sql: string,
179
+ params: Record<string, DriverParamValue>,
180
+ ): string {
181
+ const occ = scanPlaceholders(sql);
182
+ const names = uniqueNames(occ).filter(u => u.name in params);
183
+ // Assign starting positions in appearance order.
184
+ const startPos = new Map<string, number>();
185
+ let pos = 1;
186
+ for (const u of names) {
187
+ startPos.set(u.name, pos);
188
+ const v = params[u.name];
189
+ pos += u.inExpansion && Array.isArray(v) ? v.length : 1;
190
+ }
191
+ // Rewrite right-to-left so indices stay valid; skip occurrences whose name
192
+ // isn't supplied (left as a literal — caught by assertAllProvided when live).
193
+ let out = sql;
194
+ for (let k = occ.length - 1; k >= 0; k--) {
195
+ const o = occ[k];
196
+ if (!(o.name in params)) continue;
197
+ const p = startPos.get(o.name)!;
198
+ const v = params[o.name];
199
+ const replacement = o.inExpansion && Array.isArray(v)
200
+ ? v.map((_, idx) => `$${p + idx}`).join(", ")
201
+ : `$${p}`;
202
+ out = out.slice(0, o.start) + replacement + out.slice(o.end);
203
+ }
204
+ return out;
205
+ }
206
+
207
+ /**
208
+ * Flattened param values in placeholder order. IN-list arrays are spread; all
209
+ * other values pass through as one entry. Throws if a used value is undefined.
210
+ */
211
+ export function collectScanned(
212
+ sql: string,
213
+ params: Record<string, DriverParamValue>,
214
+ ): DriverParamValue[] {
215
+ const occ = scanPlaceholders(sql);
216
+ const names = uniqueNames(occ).filter(u => u.name in params);
217
+ const result: DriverParamValue[] = [];
218
+ for (const u of names) {
219
+ const v = params[u.name];
220
+ if (v === undefined) {
221
+ throw new Error(
222
+ `Query parameter ":${u.name}" is used but its value is undefined`,
223
+ );
224
+ }
225
+ if (u.inExpansion && Array.isArray(v)) result.push(...v);
226
+ else result.push(v);
227
+ }
228
+ return result;
229
+ }
230
+
231
+ /**
232
+ * Live-placeholder check (spec §6.3): throw for any real placeholder in the
233
+ * assembled SQL whose name is absent from `params`. Conditional fragments that
234
+ * were excluded contribute no placeholder, so they never trip this.
235
+ */
236
+ export function assertAllProvided(
237
+ sql: string,
238
+ params: Record<string, DriverParamValue>,
239
+ ): void {
240
+ for (const o of scanPlaceholders(sql)) {
241
+ if (!(o.name in params)) {
242
+ throw new Error(`Missing value for query parameter ":${o.name}"`);
243
+ }
244
+ }
245
+ }
246
+
247
+ /** One-shot: live-check then return `{ sql: expanded, values }`. */
248
+ export function prepareScanned(
249
+ sql: string,
250
+ params: Record<string, DriverParamValue>,
251
+ ): { sql: string; values: DriverParamValue[] } {
252
+ assertAllProvided(sql, params);
253
+ return { sql: expandScanned(sql, params), values: collectScanned(sql, params) };
254
+ }