@happyvertical/sql 0.102.1 → 0.102.3

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 (30) hide show
  1. package/AGENT.md +1 -1
  2. package/README.md +99 -4
  3. package/dist/chunks/{duckdb-DfikVgov.js → duckdb-LMyfKZGi.js} +3 -3
  4. package/dist/chunks/{duckdb-DfikVgov.js.map → duckdb-LMyfKZGi.js.map} +1 -1
  5. package/dist/chunks/postgres-dialect-Bfswr4Vs.js +2728 -0
  6. package/dist/chunks/postgres-dialect-Bfswr4Vs.js.map +1 -0
  7. package/dist/chunks/{sqlite-Cz2NMiDs.js → sqlite-0PMv1va2.js} +5 -5
  8. package/dist/chunks/{sqlite-Cz2NMiDs.js.map → sqlite-0PMv1va2.js.map} +1 -1
  9. package/dist/chunks/{sqlite-connection-cache-BAakj2HO.js → sqlite-connection-cache-ChWav9iY.js} +2 -2
  10. package/dist/chunks/{sqlite-connection-cache-BAakj2HO.js.map → sqlite-connection-cache-ChWav9iY.js.map} +1 -1
  11. package/dist/chunks/{sqlite-native-ByGwGEbS.js → sqlite-native--z4BnolL.js} +4 -4
  12. package/dist/chunks/{sqlite-native-ByGwGEbS.js.map → sqlite-native--z4BnolL.js.map} +1 -1
  13. package/dist/index.d.ts +5 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +163 -1365
  16. package/dist/index.js.map +1 -1
  17. package/dist/pglite.d.ts +150 -0
  18. package/dist/pglite.d.ts.map +1 -0
  19. package/dist/pglite.js +339 -0
  20. package/dist/pglite.js.map +1 -0
  21. package/dist/postgres.d.ts.map +1 -1
  22. package/dist/shared/postgres-dialect.d.ts +153 -0
  23. package/dist/shared/postgres-dialect.d.ts.map +1 -0
  24. package/dist/shared/types.d.ts +4 -4
  25. package/metadata.json +1 -0
  26. package/package.json +13 -2
  27. package/dist/chunks/connection-cache-Da5UcY5u.js +0 -1305
  28. package/dist/chunks/connection-cache-Da5UcY5u.js.map +0 -1
  29. package/dist/chunks/types-CwWJ-yju.js +0 -139
  30. package/dist/chunks/types-CwWJ-yju.js.map +0 -1
@@ -0,0 +1,2728 @@
1
+ import { DatabaseError } from "@happyvertical/utils";
2
+ //#region src/shared/utils.ts
3
+ /**
4
+ * Shared SQL utilities that work in both browser and Node.js environments
5
+ */
6
+ /**
7
+ * Formats a database error to include its useful non-secret details.
8
+ *
9
+ * Database drivers often include additional properties beyond just `message`:
10
+ * - PostgreSQL: code, detail, hint, severity
11
+ * - DuckDB: may include additional context
12
+ * - SQLite/LibSQL: may include errno, code
13
+ *
14
+ * This function extracts an allowlist of driver properties and redacts
15
+ * credential-shaped text. When parameter values are available, prefer
16
+ * {@link wrapDatabaseError}; it also redacts values echoed by the driver.
17
+ *
18
+ * @param error - The caught error object
19
+ * @returns Formatted error string with all available error details
20
+ */
21
+ function formatDbError(error) {
22
+ return formatDbErrorWithSecrets(error, []);
23
+ }
24
+ var DRIVER_ERROR_FIELDS = [
25
+ "code",
26
+ "detail",
27
+ "hint",
28
+ "severity",
29
+ "errno"
30
+ ];
31
+ var SENSITIVE_CONTEXT_KEY = /^(?:args?|parameters?|params?|queries|query|sql|values?)$/i;
32
+ var CREDENTIAL_KEY_PART = /^(?:credential|credentials|passphrase|password|pwd|secret|token)$/i;
33
+ var CREDENTIAL_KEY_QUALIFIER = /^(?:access|api|auth|client|private|refresh|service|signing)$/i;
34
+ var DATABASE_USER_KEY = /^(?:user(?:name)?|(?:database|db|pg|postgres|postgresql)[._-]?user(?:name)?)$/i;
35
+ var DATABASE_URL_USERINFO = /\b((?:https?|libsql|postgres|postgresql):\/\/)[^\s/@]+@/giu;
36
+ var DATABASE_URL = /\b(?:https?|libsql|postgres|postgresql):\/\/[^\s"'`<>()]+/giu;
37
+ var CREDENTIAL_ASSIGNMENT = /(?<![\p{L}\p{N}_-])(["']?)([\p{L}\p{N}_.-]+(?: key)?)\1(\s*[:=]\s*)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s,;}\]]+)/giu;
38
+ var BEARER_CREDENTIAL = /\bbearer\s+[^\s,;]+/giu;
39
+ var SQL_NUMERIC_LITERAL = /(?<![\p{L}\p{N}_$])[-+]?(?:\d(?:_?\d)*(?:\.(?:\d(?:_?\d)*)?)?|\.\d(?:_?\d)*)(?:e[-+]?\d(?:_?\d)*)?(?![\p{L}\p{N}_$])/giu;
40
+ var POSTGRES_KEY_VALUE = /(\bKey\s*\([^\r\n)]*\)\s*=\s*\()([\s\S]*?)(\)\s*(?:already exists|is duplicated)\b)/giu;
41
+ var POSTGRES_FAILING_ROW = /(\bFailing row contains\s*\()([\s\S]*)(\)(?:\.|$))/giu;
42
+ var CAST_INPUT_VALUE = /(\bwith value\s+)([\s\S]*?)(\s+(?:can't|cannot)\s+be cast\b)/giu;
43
+ function isCredentialKey(key) {
44
+ if (DATABASE_USER_KEY.test(key)) return true;
45
+ const parts = key.replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, "$1 $2").split(/[^\p{L}\p{N}]+/u).filter(Boolean);
46
+ if (parts.length === 1 && /^(?:auth|credential|key|pass)$/i.test(parts[0])) return true;
47
+ if (parts.some((part) => CREDENTIAL_KEY_PART.test(part))) return true;
48
+ return parts.some((part, index) => /^(?:key|auth)$/i.test(part) && index > 0 && CREDENTIAL_KEY_QUALIFIER.test(parts[index - 1]));
49
+ }
50
+ function toDriverError(error) {
51
+ return error && (typeof error === "object" || typeof error === "function") ? error : {};
52
+ }
53
+ function redactUrl(value) {
54
+ try {
55
+ const url = new URL(value);
56
+ if (url.username) url.username = "[redacted]";
57
+ if (url.password) url.password = "[redacted]";
58
+ for (const key of url.searchParams.keys()) if (isCredentialKey(key)) url.searchParams.set(key, "[redacted]");
59
+ if (url.hash) url.hash = "[redacted]";
60
+ return url.toString();
61
+ } catch {
62
+ return value.replace(/\/\/[^/@\s]+@/u, "//[redacted]@");
63
+ }
64
+ }
65
+ function redactKnownValue(input, value) {
66
+ if (!value || !input.includes(value)) return input;
67
+ if (!/^[\p{L}\p{N}_-]+$/u.test(value)) return input.split(value).join("[redacted]");
68
+ const identifierCharacter = /^[\p{L}\p{N}_-]$/u;
69
+ let cursor = 0;
70
+ let redacted = "";
71
+ let match = input.indexOf(value);
72
+ while (match !== -1) {
73
+ const end = match + value.length;
74
+ const before = [...input.slice(Math.max(0, match - 2), match)].at(-1);
75
+ const afterCodePoint = input.codePointAt(end);
76
+ const after = afterCodePoint === void 0 ? void 0 : String.fromCodePoint(afterCodePoint);
77
+ const bounded = (!before || !identifierCharacter.test(before)) && (!after || !identifierCharacter.test(after));
78
+ redacted += input.slice(cursor, match);
79
+ redacted += bounded ? "[redacted]" : value;
80
+ cursor = end;
81
+ match = input.indexOf(value, cursor);
82
+ }
83
+ return redacted + input.slice(cursor);
84
+ }
85
+ function redactDatabaseErrorText(value, knownSecrets) {
86
+ let redacted = value.replace(DATABASE_URL_USERINFO, "$1[redacted]@").replace(DATABASE_URL, redactUrl).replace(CREDENTIAL_ASSIGNMENT, (assignment, quote, key, delimiter) => isCredentialKey(key) ? `${quote}${key}${quote}${delimiter}[redacted]` : assignment).replace(BEARER_CREDENTIAL, "Bearer [redacted]").replace(POSTGRES_KEY_VALUE, (_match, prefix, _value, suffix) => `${prefix}[redacted]${suffix}`).replace(POSTGRES_FAILING_ROW, (_match, prefix, _value, suffix) => `${prefix}[redacted]${suffix}`).replace(CAST_INPUT_VALUE, (_match, prefix, _value, suffix) => `${prefix}[redacted]${suffix}`);
87
+ for (const secret of [...new Set(knownSecrets)].sort((left, right) => right.length - left.length)) redacted = redactKnownValue(redacted, secret);
88
+ return redacted;
89
+ }
90
+ function collectSensitiveValues(value, output, seen) {
91
+ if (typeof value === "string") {
92
+ if (value) output.add(value);
93
+ return;
94
+ }
95
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
96
+ output.add(String(value));
97
+ return;
98
+ }
99
+ if (!value || typeof value !== "object" || seen.has(value)) return;
100
+ seen.add(value);
101
+ try {
102
+ if (value instanceof Date) {
103
+ output.add(value.toISOString());
104
+ return;
105
+ }
106
+ if (ArrayBuffer.isView(value)) {
107
+ const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
108
+ if (bytes.length > 0) {
109
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
110
+ output.add([...bytes].join(","));
111
+ output.add(hex);
112
+ output.add(`\\x${hex}`);
113
+ output.add(`\\x${hex.toUpperCase()}`);
114
+ output.add(`<Buffer ${[...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join(" ")}>`);
115
+ if (typeof Buffer !== "undefined") output.add(Buffer.from(bytes).toString("base64"));
116
+ }
117
+ return;
118
+ }
119
+ } catch {
120
+ return;
121
+ }
122
+ try {
123
+ const serialized = JSON.stringify(value);
124
+ if (serialized && serialized !== "{}" && serialized !== "[]") output.add(serialized);
125
+ } catch {}
126
+ let descriptors;
127
+ try {
128
+ descriptors = Object.getOwnPropertyDescriptors(value);
129
+ } catch {
130
+ return;
131
+ }
132
+ for (const [key, descriptor] of Object.entries(descriptors)) if (key !== "length" && "value" in descriptor) collectSensitiveValues(descriptor.value, output, seen);
133
+ }
134
+ function collectSqlLiterals(sql, output) {
135
+ if (sql) output.add(sql);
136
+ for (const match of sql.matchAll(/'((?:''|[^'])*)'/gu)) {
137
+ const literal = match[1].replaceAll("''", "'");
138
+ if (literal) output.add(literal);
139
+ }
140
+ for (const match of sql.matchAll(/\$\$([\s\S]*?)\$\$/gu)) if (match[1]) output.add(match[1]);
141
+ for (const match of sql.matchAll(/\$([A-Za-z_][A-Za-z0-9_]*)\$([\s\S]*?)\$\1\$/gu)) if (match[2]) output.add(match[2]);
142
+ for (const match of sql.matchAll(SQL_NUMERIC_LITERAL)) {
143
+ output.add(match[0]);
144
+ output.add(match[0].replaceAll("_", ""));
145
+ }
146
+ }
147
+ function contextSecrets(context) {
148
+ const output = /* @__PURE__ */ new Set();
149
+ for (const [key, value] of Object.entries(context)) if (/^(?:args?|parameters?|params?|values?)$/i.test(key)) collectSensitiveValues(value, output, /* @__PURE__ */ new Set());
150
+ else if (/^(?:queries|query|sql)$/i.test(key) && typeof value === "string") collectSqlLiterals(value, output);
151
+ return [...output];
152
+ }
153
+ function sanitizeContextValue(value, knownSecrets, seen) {
154
+ if (typeof value === "string") return redactDatabaseErrorText(value, knownSecrets);
155
+ if (!value || typeof value !== "object") return value;
156
+ if (seen.has(value)) return "[circular]";
157
+ seen.add(value);
158
+ if (Array.isArray(value)) return value.map((entry) => sanitizeContextValue(entry, knownSecrets, seen));
159
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, isCredentialKey(key) ? "[redacted]" : sanitizeContextValue(entry, knownSecrets, seen)]));
160
+ }
161
+ function sanitizeDatabaseErrorContext(context, knownSecrets) {
162
+ return Object.fromEntries(Object.entries(context).map(([key, value]) => [key, SENSITIVE_CONTEXT_KEY.test(key) || isCredentialKey(key) ? "[redacted]" : sanitizeContextValue(value, knownSecrets, /* @__PURE__ */ new WeakSet())]));
163
+ }
164
+ function formatDbErrorWithSecrets(error, knownSecrets) {
165
+ const dbError = toDriverError(error);
166
+ const parts = [];
167
+ if (dbError.message) parts.push(redactDatabaseErrorText(dbError.message, knownSecrets));
168
+ if (dbError.code) parts.push(`code=${redactDatabaseErrorText(dbError.code, knownSecrets)}`);
169
+ if (dbError.detail) parts.push(`detail=${redactDatabaseErrorText(dbError.detail, knownSecrets)}`);
170
+ if (dbError.hint) parts.push(`hint=${redactDatabaseErrorText(dbError.hint, knownSecrets)}`);
171
+ if (dbError.severity) parts.push(`severity=${redactDatabaseErrorText(dbError.severity, knownSecrets)}`);
172
+ if (dbError.errno !== void 0) parts.push(`errno=${dbError.errno}`);
173
+ return parts.length > 0 ? parts.join(", ") : redactDatabaseErrorText(String(error), knownSecrets);
174
+ }
175
+ function safeDatabaseCause(error, knownSecrets) {
176
+ const dbError = toDriverError(error);
177
+ const message = dbError.message ? redactDatabaseErrorText(dbError.message, knownSecrets) : formatDbErrorWithSecrets(error, knownSecrets);
178
+ const cause = new Error(message);
179
+ cause.name = dbError.name ? redactDatabaseErrorText(dbError.name, knownSecrets) : "DatabaseDriverError";
180
+ for (const field of DRIVER_ERROR_FIELDS) {
181
+ const value = dbError[field];
182
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") Object.defineProperty(cause, field, {
183
+ configurable: true,
184
+ enumerable: true,
185
+ value: typeof value === "string" ? redactDatabaseErrorText(value, knownSecrets) : value,
186
+ writable: true
187
+ });
188
+ }
189
+ return cause;
190
+ }
191
+ /**
192
+ * Wraps an engine failure in a serializable, secret-safe DatabaseError.
193
+ *
194
+ * Raw SQL and parameter values are replaced in context. The native `cause` is
195
+ * a sanitized driver-error snapshot rather than the original object so normal
196
+ * Error inspection cannot bypass the same redaction applied to messages and
197
+ * JSON serialization.
198
+ */
199
+ function wrapDatabaseError(message, error, context = {}) {
200
+ const knownSecrets = contextSecrets(context);
201
+ const originalError = formatDbErrorWithSecrets(error, knownSecrets);
202
+ return new DatabaseError(`${message}: ${originalError}`, {
203
+ ...sanitizeDatabaseErrorContext(context, knownSecrets),
204
+ originalError
205
+ }, safeDatabaseCause(error, knownSecrets));
206
+ }
207
+ /**
208
+ * Resolves the column list for a multi-row INSERT and rejects batches whose
209
+ * records disagree on it.
210
+ *
211
+ * A batch INSERT emits one column list for the whole statement, so it can only
212
+ * come from the first record. That leaves two ways for a later record to be
213
+ * written wrongly, both of them silent:
214
+ *
215
+ * - Its keys are the same but in a different insertion order. Callers that
216
+ * bound values with `Object.values(record)` bound them by position, so the
217
+ * values cross-assigned into each other's columns. Projecting each record
218
+ * through the returned column list — `keys.map((key) => record[key])` — is
219
+ * what makes key order irrelevant, and every caller must do it.
220
+ * - Its key set genuinely differs. An extra key has no column to go to and a
221
+ * missing one has no value, so the record cannot be expressed by this
222
+ * statement at all. Guessing (dropping the extra, writing NULL for the
223
+ * missing) is what makes the failure invisible, so this throws instead.
224
+ *
225
+ * A key whose value is `undefined` counts as absent. Adapters that serialize
226
+ * records first drop such keys before this point while the rest pass them
227
+ * through, so comparing raw key lists would make the same batch throw on some
228
+ * adapters and silently write NULL on others. Only the comparison ignores them;
229
+ * the returned column list is untouched, so each adapter keeps its own
230
+ * single-record handling of `undefined`.
231
+ *
232
+ * @param table - Table being inserted into, for error context
233
+ * @param records - Non-empty batch, as the adapter will bind it
234
+ * @returns Column list taken from the first record
235
+ * @throws DatabaseError if any later record's key set differs from the first's
236
+ */
237
+ function resolveInsertColumns(table, records) {
238
+ const definedKeys = (record) => new Set(Object.keys(record).filter((key) => record[key] !== void 0));
239
+ const keys = Object.keys(records[0]);
240
+ const expected = definedKeys(records[0]);
241
+ for (let index = 1; index < records.length; index++) {
242
+ const recordKeys = definedKeys(records[index]);
243
+ const missing = [...expected].filter((key) => !recordKeys.has(key));
244
+ const extra = [...recordKeys].filter((key) => !expected.has(key));
245
+ if (missing.length === 0 && extra.length === 0) continue;
246
+ const problems = [missing.length > 0 ? `missing ${missing.join(", ")}` : "", extra.length > 0 ? `unexpected ${extra.join(", ")}` : ""].filter(Boolean);
247
+ throw new DatabaseError(`Batch insert records must all have the same keys; record ${index} has ${problems.join(" and ")} relative to record 0 (${[...expected].join(", ")})`, {
248
+ table,
249
+ recordIndex: index,
250
+ expectedKeys: keys,
251
+ recordKeys: [...recordKeys],
252
+ missing,
253
+ extra,
254
+ hint: "A batch INSERT writes one column list for every row, taken from the first record, so record 0 defines the columns even when it is the one that looks wrong. A key whose value is undefined counts as absent. Split records with differing keys into separate insert() calls, or fill the gaps explicitly with null."
255
+ });
256
+ }
257
+ return keys;
258
+ }
259
+ /**
260
+ * Map of valid SQL operators for use in WHERE clauses
261
+ */
262
+ var VALID_OPERATORS = {
263
+ "=": "=",
264
+ ">": ">",
265
+ ">=": ">=",
266
+ "<": "<",
267
+ "<=": "<=",
268
+ "!=": "!=",
269
+ like: "LIKE",
270
+ contains: "CONTAINS",
271
+ in: "IN",
272
+ "not in": "NOT IN"
273
+ };
274
+ function isSimpleSqlIdentifier(field) {
275
+ return /^[a-zA-Z0-9_.]+$/.test(field);
276
+ }
277
+ /**
278
+ * Marks a condition key as caller-authored SQL text.
279
+ *
280
+ * A fixed, non-secret sentinel rather than a random per-process token. A random
281
+ * token has to travel inside the key, which means it reaches every place a key
282
+ * is echoed — error messages, logs, each adapter's `DatabaseError` context — and
283
+ * one disclosure would let a caller mint raw keys for the life of the process.
284
+ * A registry of minted expressions avoids that but is not referentially
285
+ * transparent: it would make `raw('revenue >')` in one query change what the
286
+ * plain key `'revenue >'` means in every other one.
287
+ *
288
+ * The NUL delimiters cannot appear in an identifier or in SQL a developer would
289
+ * write, so the marker never collides with a legitimate key.
290
+ */
291
+ var RAW_KEY_PREFIX = "\0hv-sql-raw\0";
292
+ /**
293
+ * Marks a WHERE-clause key as SQL expression text rather than an identifier.
294
+ *
295
+ * {@link buildWhere} validates every condition key as a plain identifier. Wrap a
296
+ * key in `raw()` to opt one condition out of that check:
297
+ *
298
+ * ```typescript
299
+ * buildWhere({
300
+ * status: 'paid', // validated as an identifier
301
+ * [raw('SUM(total) >')]: 100, // caller-authored SQL, not validated
302
+ * });
303
+ * ```
304
+ *
305
+ * Never build the argument from end-user input. `raw()` is an assertion that
306
+ * the caller, not the request, authored this SQL.
307
+ *
308
+ * The returned key carries a marker, so it is a distinct string from the bare
309
+ * expression: minting `raw('revenue >')` never changes what the plain key
310
+ * `'revenue >'` means anywhere else.
311
+ *
312
+ * The marker is a fixed sentinel, not a secret. It stops an expression key being
313
+ * used by accident — the shape of the key no longer grants raw access, a call to
314
+ * `raw()` does — but a caller that controls a whole key string verbatim,
315
+ * including the NUL-delimited marker, can still reproduce it. Validate at your
316
+ * own trust boundary; do not rely on this as the only barrier against hostile
317
+ * input.
318
+ *
319
+ * Enforcement is at runtime: `WhereClause` keys are plain `string`, so a
320
+ * computed key widens and TypeScript will not reject an unmarked expression key
321
+ * at the call site.
322
+ *
323
+ * A trailing operator is recognised only if it is one of `=`, `!=`, `>`, `>=`,
324
+ * `<`, `<=`, `like`, `contains`, `in`, `not in`. Any other trailing token is
325
+ * treated as part of the expression and `=` is appended, so
326
+ * `raw('name ILIKE')` silently emits `name ILIKE = $1`, which the database then
327
+ * rejects. Fold an unsupported operator into the expression instead:
328
+ * `raw('LOWER(name) like')`.
329
+ *
330
+ * @param expression - SQL field or expression text, optionally ending in a
331
+ * supported operator (for example `SUM(total) >`)
332
+ * @returns A branded key for use in a {@link WhereClause}
333
+ * @throws If the expression is empty
334
+ */
335
+ function raw(expression) {
336
+ const trimmed = unwrapRawKey(expression).key.trim();
337
+ if (!trimmed) throw new Error("raw() requires a non-empty SQL expression");
338
+ return `${RAW_KEY_PREFIX}${trimmed}`;
339
+ }
340
+ /**
341
+ * Splits the raw marker off a condition key.
342
+ * @internal
343
+ */
344
+ function unwrapRawKey(fullKey) {
345
+ return fullKey.startsWith(RAW_KEY_PREFIX) ? {
346
+ key: fullKey.slice(12),
347
+ isRaw: true
348
+ } : {
349
+ key: fullKey,
350
+ isRaw: false
351
+ };
352
+ }
353
+ function parseConditionKey(fullKey) {
354
+ const trimmed = fullKey.trim();
355
+ const lower = trimmed.toLowerCase();
356
+ for (const operator of [
357
+ "not in",
358
+ "contains",
359
+ "like",
360
+ "in",
361
+ "!=",
362
+ ">=",
363
+ "<=",
364
+ ">",
365
+ "<",
366
+ "="
367
+ ]) if (lower.endsWith(` ${operator}`)) return {
368
+ field: trimmed.slice(0, -(operator.length + 1)).trim(),
369
+ operator
370
+ };
371
+ return {
372
+ field: trimmed,
373
+ operator: "="
374
+ };
375
+ }
376
+ /**
377
+ * Builds a single condition for a WHERE clause
378
+ * @internal
379
+ */
380
+ var buildCondition = (fullKey, value, currIndex, adapterType) => {
381
+ const { key, isRaw } = unwrapRawKey(fullKey);
382
+ const { field, operator } = parseConditionKey(key);
383
+ if (!isRaw && !isSimpleSqlIdentifier(field)) throw new Error(`Invalid SQL identifier: ${field}. Condition keys must be plain identifiers; wrap developer-authored SQL expressions in raw().`);
384
+ const sqlOperator = VALID_OPERATORS[operator] || "=";
385
+ const values = [];
386
+ let sql;
387
+ if (sqlOperator === "CONTAINS") {
388
+ if (!adapterType) throw new Error("CONTAINS requires an adapter type so buildWhere can preserve case-sensitive semantics");
389
+ if (typeof value !== "string") throw new Error("CONTAINS requires a string value");
390
+ const placeholder = `$${currIndex.value++}`;
391
+ sql = `${adapterType === "sqlite" ? "instr" : "strpos"}(${field}, ${placeholder}) > 0`;
392
+ values.push(value);
393
+ } else if (value === null) sql = `${field} IS ${sqlOperator === "=" ? "NULL" : "NOT NULL"}`;
394
+ else if ((sqlOperator === "IN" || sqlOperator === "NOT IN") && Array.isArray(value)) {
395
+ if (value.length === 0) throw new Error(`${sqlOperator} requires at least one value`);
396
+ sql = `${field} ${sqlOperator} (${value.map(() => `$${currIndex.value++}`).join(", ")})`;
397
+ values.push(...value);
398
+ } else if (value instanceof Date) {
399
+ if (adapterType === "duckdb" || adapterType === "json") sql = `${field} ${sqlOperator} CAST($${currIndex.value++} AS TIMESTAMP)${sqlOperator === "LIKE" ? " ESCAPE '\\'" : ""}`;
400
+ else sql = `${field} ${sqlOperator} $${currIndex.value++}${sqlOperator === "LIKE" ? " ESCAPE '\\'" : ""}`;
401
+ values.push(value.toISOString());
402
+ } else {
403
+ sql = `${field} ${sqlOperator} $${currIndex.value++}${sqlOperator === "LIKE" ? " ESCAPE '\\'" : ""}`;
404
+ values.push(value);
405
+ }
406
+ return {
407
+ sql,
408
+ values
409
+ };
410
+ };
411
+ /**
412
+ * Builds a SQL WHERE clause with parameterized values and flexible operators
413
+ *
414
+ * @param where - Conditions as object (AND-only) or 2D array (OR/AND compound)
415
+ * @param startIndex - Starting index for parameter numbering (default: 1)
416
+ * @param adapterType - Database adapter type. Controls Date and `contains`
417
+ * handling:
418
+ * - `'duckdb'` / `'json'`: Wraps Date values in `CAST($N AS TIMESTAMP)` to
419
+ * prevent DuckDB ANY-type inference issues.
420
+ * - `'sqlite'` / `'postgres'` / `undefined`: Passes ISO strings directly
421
+ * (these adapters handle ISO timestamp strings natively).
422
+ * - Required for `contains`, whose case-sensitive literal substring SQL is
423
+ * emitted with adapter-specific functions.
424
+ *
425
+ * Every condition key is validated as a plain SQL identifier, with or without
426
+ * an operator suffix. To use expression text as a key, wrap it in {@link raw}:
427
+ *
428
+ * ```typescript
429
+ * buildWhere({ [raw('LOWER(status) =')]: 'paid' });
430
+ * ```
431
+ *
432
+ * `raw()` is the only way to reach expression text, so mapping untrusted input
433
+ * into a condition key throws rather than emitting attacker-controlled SQL.
434
+ *
435
+ * @returns Object containing the SQL clause and array of values
436
+ * @throws If a key that is not wrapped in {@link raw} is not a plain identifier
437
+ *
438
+ * @example Basic Usage (Object format - AND-only):
439
+ * ```typescript
440
+ * buildWhere({
441
+ * 'status': 'active', // equals operator is default
442
+ * 'price >': 100, // greater than
443
+ * 'stock <=': 5, // less than or equal
444
+ * 'category in': ['A', 'B'], // IN clause for arrays
445
+ * 'name like': '%shirt%', // LIKE pattern; backslash escapes `%` / `_`
446
+ * 'notes contains': '100% off' // literal, case-sensitive substring
447
+ * });
448
+ * ```
449
+ *
450
+ * @example 2D Array Format (OR/AND compound logic):
451
+ * ```typescript
452
+ * // WHERE (status = 'active' AND price > 100) OR (status = 'pending' AND priority = 'high')
453
+ * buildWhere([
454
+ * [{ status: 'active' }, { 'price >': 100 }],
455
+ * [{ status: 'pending' }, { priority: 'high' }]
456
+ * ]);
457
+ * ```
458
+ *
459
+ * @example NULL Handling:
460
+ * ```typescript
461
+ * buildWhere({
462
+ * 'deleted_at': null, // becomes "deleted_at IS NULL"
463
+ * 'updated_at !=': null, // becomes "updated_at IS NOT NULL"
464
+ * 'status': 'active' // regular comparison
465
+ * });
466
+ * ```
467
+ *
468
+ * @example Common Patterns:
469
+ * ```typescript
470
+ * // Price range
471
+ * buildWhere({
472
+ * 'price >=': 10,
473
+ * 'price <': 100
474
+ * });
475
+ *
476
+ * // Date filtering
477
+ * buildWhere({
478
+ * 'created_at >': startDate,
479
+ * 'created_at <=': endDate,
480
+ * 'deleted_at': null
481
+ * });
482
+ *
483
+ * // Search with LIKE
484
+ * buildWhere({
485
+ * 'title like': '%search%',
486
+ * 'description like': '%search%',
487
+ * 'status': 'published'
488
+ * });
489
+ *
490
+ * // Multiple values with IN
491
+ * buildWhere({
492
+ * 'role in': ['admin', 'editor'],
493
+ * 'active': true,
494
+ * 'last_login !=': null
495
+ * });
496
+ * ```
497
+ *
498
+ * The function handles:
499
+ * - Standard comparisons (=, >, >=, <, <=, !=)
500
+ * - NULL checks (IS NULL, IS NOT NULL)
501
+ * - IN clauses for arrays
502
+ * - LIKE for pattern matching with an explicit backslash escape character
503
+ * - Case-sensitive literal substring matching with `contains`
504
+ * - Object format: Multiple conditions combined with AND
505
+ * - 2D array format: Inner arrays ANDed, outer array ORed
506
+ */
507
+ var buildWhere = (where, startIndex = 1, adapterType) => {
508
+ let sql = "";
509
+ const values = [];
510
+ const currIndex = { value: startIndex };
511
+ if (Array.isArray(where)) {
512
+ if (where.length === 0) return {
513
+ sql: "",
514
+ values: []
515
+ };
516
+ const orGroups = [];
517
+ for (const andGroup of where) {
518
+ if (!Array.isArray(andGroup) || andGroup.length === 0) continue;
519
+ const andConditions = [];
520
+ for (const conditionObj of andGroup) for (const [fullKey, value] of Object.entries(conditionObj)) {
521
+ const result = buildCondition(fullKey, value, currIndex, adapterType);
522
+ andConditions.push(result.sql);
523
+ values.push(...result.values);
524
+ }
525
+ if (andConditions.length > 0) orGroups.push(`(${andConditions.join(" AND ")})`);
526
+ }
527
+ if (orGroups.length > 0) sql = `WHERE ${orGroups.join(" OR ")}`;
528
+ return {
529
+ sql,
530
+ values
531
+ };
532
+ }
533
+ if (where && Object.keys(where).length > 0) {
534
+ sql = "WHERE ";
535
+ for (const [fullKey, value] of Object.entries(where)) {
536
+ const result = buildCondition(fullKey, value, currIndex, adapterType);
537
+ if (sql !== "WHERE ") sql += " AND ";
538
+ sql += result.sql;
539
+ values.push(...result.values);
540
+ }
541
+ }
542
+ return {
543
+ sql,
544
+ values
545
+ };
546
+ };
547
+ //#endregion
548
+ //#region src/schema-manager.ts
549
+ /**
550
+ * Manages schema initialization from JSON manifests with dependency resolution
551
+ *
552
+ * Handles topological sorting of schema dependencies, table creation,
553
+ * index and trigger setup, and version tracking to avoid redundant re-initialization.
554
+ */
555
+ var DatabaseSchemaManager = class DatabaseSchemaManager {
556
+ static initializationLock = /* @__PURE__ */ new Map();
557
+ initializedSchemas = /* @__PURE__ */ new Set();
558
+ schemaVersions = /* @__PURE__ */ new Map();
559
+ /**
560
+ * Initializes database schemas from a JSON manifest with dependency resolution
561
+ *
562
+ * Schemas are topologically sorted by their declared dependencies and
563
+ * initialized in order. Supports legacy raw-SQL schemas via the `schema` option,
564
+ * JSON manifests via the `manifest` option, and schema overrides for extending
565
+ * base schemas.
566
+ *
567
+ * @param db - Database interface to execute DDL against
568
+ * @param options - Schema initialization options (manifest, overrides, force, debug)
569
+ * @returns Result with lists of initialized/skipped schemas, errors, and execution time
570
+ */
571
+ async initializeSchemas(db, options) {
572
+ const startTime = Date.now();
573
+ const result = {
574
+ initialized: [],
575
+ skipped: [],
576
+ errors: [],
577
+ executionTime: 0
578
+ };
579
+ try {
580
+ let schemas = {};
581
+ if (options.schema) {
582
+ console.log("[schema] Legacy SQL schema provided, converting to manifest format");
583
+ if (db.syncSchema) {
584
+ await db.syncSchema(options.schema);
585
+ result.initialized.push("legacy-sql");
586
+ }
587
+ result.executionTime = Date.now() - startTime;
588
+ return result;
589
+ }
590
+ if (options.manifest) schemas = { ...options.manifest.schemas };
591
+ if (options.overrides) schemas = {
592
+ ...schemas,
593
+ ...options.overrides
594
+ };
595
+ if (Object.keys(schemas).length === 0) {
596
+ console.warn("[schema] No schemas provided for initialization");
597
+ result.executionTime = Date.now() - startTime;
598
+ return result;
599
+ }
600
+ const dependencyGraph = this.buildDependencyGraph(schemas);
601
+ const initializationOrder = this.resolveDependencies(dependencyGraph);
602
+ if (options.debug) console.log("[schema] Initialization order:", initializationOrder);
603
+ for (const schemaName of initializationOrder) {
604
+ const schema = schemas[schemaName];
605
+ if (!schema) continue;
606
+ try {
607
+ await this.initializeSchema(db, schemaName, schema, {
608
+ force: options.force,
609
+ debug: options.debug
610
+ });
611
+ result.initialized.push(schemaName);
612
+ } catch (error) {
613
+ const errorMessage = error instanceof Error ? error.message : String(error);
614
+ result.errors.push({
615
+ schema: schemaName,
616
+ error: errorMessage
617
+ });
618
+ if (options.debug) console.error(`[schema] Failed to initialize ${schemaName}:`, error);
619
+ }
620
+ }
621
+ result.executionTime = Date.now() - startTime;
622
+ if (options.debug) console.log("[schema] Initialization complete:", result);
623
+ return result;
624
+ } catch (error) {
625
+ result.executionTime = Date.now() - startTime;
626
+ const errorMessage = error instanceof Error ? error.message : String(error);
627
+ result.errors.push({
628
+ schema: "dependency-resolution",
629
+ error: errorMessage
630
+ });
631
+ return result;
632
+ }
633
+ }
634
+ /**
635
+ * Initialize a single schema
636
+ */
637
+ async initializeSchema(db, schemaName, schema, options) {
638
+ const { tableName, version } = schema;
639
+ const lockKey = `${schemaName}:${tableName}`;
640
+ if (DatabaseSchemaManager.initializationLock.has(lockKey)) {
641
+ await DatabaseSchemaManager.initializationLock.get(lockKey);
642
+ return;
643
+ }
644
+ if (!options.force && this.isSchemaUpToDate(schemaName, version)) {
645
+ if (options.debug) console.log(`[schema] Skipping ${schemaName} - already up to date`);
646
+ return;
647
+ }
648
+ const initPromise = this.performSchemaInitialization(db, schemaName, schema, options);
649
+ DatabaseSchemaManager.initializationLock.set(lockKey, initPromise);
650
+ try {
651
+ await initPromise;
652
+ this.markSchemaInitialized(schemaName, version);
653
+ } finally {
654
+ DatabaseSchemaManager.initializationLock.delete(lockKey);
655
+ }
656
+ }
657
+ /**
658
+ * Perform the actual schema initialization
659
+ */
660
+ async performSchemaInitialization(db, schemaName, schema, options) {
661
+ const { tableName, columns, indexes, triggers } = schema;
662
+ if (options.debug) console.log(`[schema] Initializing ${schemaName} (${tableName})`);
663
+ if (!await db.tableExists(tableName)) {
664
+ await this.createTable(db, schema);
665
+ for (const index of indexes) await this.createIndex(db, tableName, index);
666
+ for (const trigger of triggers) await this.createTrigger(db, trigger);
667
+ } else if (options.force) {
668
+ await db.query(`DROP TABLE IF EXISTS ${tableName}`);
669
+ await this.createTable(db, schema);
670
+ for (const index of indexes) await this.createIndex(db, tableName, index);
671
+ for (const trigger of triggers) await this.createTrigger(db, trigger);
672
+ } else await this.updateSchemaIfNeeded(db, schema);
673
+ }
674
+ /**
675
+ * Create table from schema definition
676
+ */
677
+ async createTable(db, schema) {
678
+ const { tableName, columns, foreignKeys } = schema;
679
+ const columnDefinitions = [];
680
+ for (const [columnName, columnDef] of Object.entries(columns)) {
681
+ let def = `${columnName} ${columnDef.type}`;
682
+ if (columnDef.primaryKey) def += " PRIMARY KEY";
683
+ if (columnDef.unique && !columnDef.primaryKey) def += " UNIQUE";
684
+ if (columnDef.notNull) def += " NOT NULL";
685
+ if (columnDef.defaultValue !== void 0) def += ` DEFAULT ${columnDef.defaultValue}`;
686
+ if (columnDef.check) def += ` CHECK (${columnDef.check})`;
687
+ columnDefinitions.push(def);
688
+ }
689
+ for (const fk of foreignKeys) {
690
+ let fkDef = `FOREIGN KEY (${fk.column}) REFERENCES ${fk.referencesTable}(${fk.referencesColumn})`;
691
+ if (fk.onDelete) fkDef += ` ON DELETE ${fk.onDelete}`;
692
+ if (fk.onUpdate) fkDef += ` ON UPDATE ${fk.onUpdate}`;
693
+ columnDefinitions.push(fkDef);
694
+ }
695
+ const createTableSQL = `CREATE TABLE ${tableName} (\n ${columnDefinitions.join(",\n ")}\n)`;
696
+ await db.query(createTableSQL);
697
+ }
698
+ /**
699
+ * Create index from definition
700
+ */
701
+ async createIndex(db, tableName, index) {
702
+ const uniqueClause = index.unique ? "UNIQUE " : "";
703
+ const whereClause = index.where ? ` WHERE ${index.where}` : "";
704
+ const createIndexSQL = `CREATE ${uniqueClause}INDEX ${index.name} ON ${tableName} (${index.columns.join(", ")})${whereClause}`;
705
+ await db.query(createIndexSQL);
706
+ }
707
+ /**
708
+ * Create trigger from definition
709
+ */
710
+ async createTrigger(db, trigger) {
711
+ const conditionClause = trigger.condition ? ` WHEN ${trigger.condition}` : "";
712
+ const createTriggerSQL = `CREATE TRIGGER ${trigger.name} ${trigger.when} ${trigger.event} ON ${trigger.table}${conditionClause} BEGIN ${trigger.body} END`;
713
+ await db.query(createTriggerSQL);
714
+ }
715
+ /**
716
+ * Update schema if changes are detected
717
+ */
718
+ async updateSchemaIfNeeded(_db, schema) {
719
+ console.log(`[schema] Schema update logic not yet implemented for ${schema.tableName}`);
720
+ }
721
+ /**
722
+ * Build dependency graph from schemas
723
+ */
724
+ buildDependencyGraph(schemas) {
725
+ const graph = /* @__PURE__ */ new Map();
726
+ for (const [schemaName, schema] of Object.entries(schemas)) {
727
+ const dependencies = schema.dependencies.filter((dep) => Object.values(schemas).some((s) => s.tableName === dep));
728
+ graph.set(schemaName, dependencies.map((dep) => Object.entries(schemas).find(([_, s]) => s.tableName === dep)?.[0] || dep));
729
+ }
730
+ return graph;
731
+ }
732
+ /**
733
+ * Resolve dependencies using topological sort
734
+ */
735
+ resolveDependencies(graph) {
736
+ const resolved = [];
737
+ const visited = /* @__PURE__ */ new Set();
738
+ const visiting = /* @__PURE__ */ new Set();
739
+ const visit = (node) => {
740
+ if (visiting.has(node)) throw new Error(`Circular dependency detected involving ${node}`);
741
+ if (visited.has(node)) return;
742
+ visiting.add(node);
743
+ const dependencies = graph.get(node) || [];
744
+ for (const dep of dependencies) if (graph.has(dep)) visit(dep);
745
+ visiting.delete(node);
746
+ visited.add(node);
747
+ resolved.push(node);
748
+ };
749
+ for (const node of graph.keys()) if (!visited.has(node)) visit(node);
750
+ return resolved;
751
+ }
752
+ /**
753
+ * Check if schema is up to date
754
+ */
755
+ isSchemaUpToDate(schemaName, version) {
756
+ return this.initializedSchemas.has(schemaName) && this.schemaVersions.get(schemaName) === version;
757
+ }
758
+ /**
759
+ * Mark schema as initialized
760
+ */
761
+ markSchemaInitialized(schemaName, version) {
762
+ this.initializedSchemas.add(schemaName);
763
+ this.schemaVersions.set(schemaName, version);
764
+ }
765
+ /**
766
+ * Reset initialization state (for testing)
767
+ */
768
+ reset() {
769
+ this.initializedSchemas.clear();
770
+ this.schemaVersions.clear();
771
+ DatabaseSchemaManager.initializationLock.clear();
772
+ }
773
+ };
774
+ //#endregion
775
+ //#region src/shared/alter-utils.ts
776
+ /**
777
+ * Generates an ALTER TABLE ADD COLUMN statement
778
+ *
779
+ * @param table - Table name
780
+ * @param column - Column definition with name
781
+ * @param dialect - Database dialect
782
+ * @returns SQL statement string
783
+ *
784
+ * @example
785
+ * ```typescript
786
+ * const sql = generateAddColumnStatement('users', {
787
+ * name: 'email',
788
+ * type: 'TEXT',
789
+ * unique: true,
790
+ * notNull: false
791
+ * }, 'sqlite');
792
+ * // Returns: "ALTER TABLE users ADD COLUMN email TEXT UNIQUE"
793
+ * ```
794
+ */
795
+ function generateAddColumnStatement(table, column, _dialect) {
796
+ const parts = [column.name, column.type];
797
+ if (column.primaryKey) parts.push("PRIMARY KEY");
798
+ if (column.notNull) parts.push("NOT NULL");
799
+ if (column.unique) parts.push("UNIQUE");
800
+ if (column.defaultValue !== void 0) {
801
+ const defaultClause = generateDefaultClause(column.type, column.defaultValue);
802
+ parts.push(`DEFAULT ${defaultClause}`);
803
+ }
804
+ if (column.check) parts.push(`CHECK (${column.check})`);
805
+ if (column.foreignKey) {
806
+ const fk = column.foreignKey;
807
+ const fkParts = [`REFERENCES ${fk.table}(${fk.column})`];
808
+ if (fk.onDelete) fkParts.push(`ON DELETE ${fk.onDelete}`);
809
+ if (fk.onUpdate) fkParts.push(`ON UPDATE ${fk.onUpdate}`);
810
+ parts.push(fkParts.join(" "));
811
+ }
812
+ return `ALTER TABLE ${table} ADD COLUMN ${parts.join(" ")}`;
813
+ }
814
+ /**
815
+ * Generates a DEFAULT clause for a column definition
816
+ *
817
+ * Handles proper formatting and escaping of default values based on column type.
818
+ *
819
+ * @param columnType - SQL column type (TEXT, INTEGER, BOOLEAN, etc.)
820
+ * @param defaultValue - Default value to format
821
+ * @returns Formatted default value string
822
+ *
823
+ * @example
824
+ * ```typescript
825
+ * generateDefaultClause('TEXT', 'hello'); // Returns: "'hello'"
826
+ * generateDefaultClause('INTEGER', 42); // Returns: "42"
827
+ * generateDefaultClause('BOOLEAN', true); // Returns: "true"
828
+ * generateDefaultClause('TEXT', null); // Returns: "NULL"
829
+ * ```
830
+ */
831
+ function generateDefaultClause(columnType, defaultValue) {
832
+ if (defaultValue === null) return "NULL";
833
+ const typeUpper = columnType.toUpperCase();
834
+ if (typeUpper.includes("TEXT") || typeUpper.includes("CHAR") || typeUpper.includes("VARCHAR") || typeUpper.includes("STRING")) {
835
+ if (defaultValue === "") return "''";
836
+ return `'${String(defaultValue).replace(/'/g, "''")}'`;
837
+ }
838
+ if (typeUpper.includes("BOOL")) return defaultValue ? "true" : "false";
839
+ if (typeUpper.includes("INT") || typeUpper.includes("REAL") || typeUpper.includes("FLOAT") || typeUpper.includes("DOUBLE") || typeUpper.includes("NUMERIC") || typeUpper.includes("DECIMAL")) return String(defaultValue);
840
+ if (typeUpper.includes("JSON")) {
841
+ if (typeof defaultValue === "string") return `'${defaultValue.replace(/'/g, "''")}'`;
842
+ return `'${JSON.stringify(defaultValue).replace(/'/g, "''")}'`;
843
+ }
844
+ if (typeUpper.includes("DATE") || typeUpper.includes("TIME") || typeUpper.includes("TIMESTAMP")) {
845
+ if (defaultValue instanceof Date) return `'${defaultValue.toISOString()}'`;
846
+ return `'${String(defaultValue).replace(/'/g, "''")}'`;
847
+ }
848
+ if (typeof defaultValue === "string") return `'${defaultValue.replace(/'/g, "''")}'`;
849
+ return String(defaultValue);
850
+ }
851
+ /**
852
+ * Generates a CREATE INDEX statement
853
+ *
854
+ * @param table - Table name
855
+ * @param index - Index definition
856
+ * @returns SQL CREATE INDEX statement
857
+ *
858
+ * @example
859
+ * ```typescript
860
+ * const sql = generateCreateIndexStatement('users', {
861
+ * name: 'idx_users_email',
862
+ * columns: ['email'],
863
+ * unique: true
864
+ * });
865
+ * // Returns: "CREATE UNIQUE INDEX idx_users_email ON users (email)"
866
+ * ```
867
+ */
868
+ function generateCreateIndexStatement(table, index) {
869
+ const parts = ["CREATE"];
870
+ if (index.unique) parts.push("UNIQUE");
871
+ parts.push("INDEX");
872
+ parts.push(index.name);
873
+ parts.push(`ON ${table}`);
874
+ const columns = index.columns.join(", ");
875
+ parts.push(`(${columns})`);
876
+ if (index.where) parts.push(`WHERE ${index.where}`);
877
+ return parts.join(" ");
878
+ }
879
+ /**
880
+ * Validates a table name to prevent SQL injection
881
+ *
882
+ * The argument must be a primitive string. `RegExp.prototype.test` coerces its
883
+ * argument with `toString`, so an object whose `toString` returns a valid name
884
+ * on the call the validator makes and a different one on the call the caller's
885
+ * `${table}` interpolation makes would pass here and then reach SQL. Rejecting
886
+ * non-strings up front closes that gap: the value validated is the value
887
+ * interpolated, because a primitive string cannot observe how many times it is
888
+ * read.
889
+ *
890
+ * @param tableName - Table name to validate
891
+ * @throws Error if the value is not a string or contains invalid characters
892
+ */
893
+ function validateTableName(tableName) {
894
+ if (typeof tableName !== "string" || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) throw new Error(`Invalid table name: ${tableName}. Table names must be a string that starts with a letter or underscore and contains only alphanumeric characters and underscores.`);
895
+ }
896
+ /**
897
+ * Validates a column name to prevent SQL injection
898
+ *
899
+ * Rejects non-strings for the same reason as {@link validateTableName}: a value
900
+ * whose `toString` differs between the validation read and the interpolation
901
+ * read would otherwise slip a different identifier into SQL than the one
902
+ * checked here.
903
+ *
904
+ * @param columnName - Column name to validate
905
+ * @throws Error if the value is not a string or contains invalid characters
906
+ */
907
+ function validateColumnName(columnName) {
908
+ if (typeof columnName !== "string" || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) throw new Error(`Invalid column name: ${columnName}. Column names must be a string that starts with a letter or underscore and contains only alphanumeric characters and underscores.`);
909
+ }
910
+ /**
911
+ * Validates every identifier in a column list to prevent SQL injection
912
+ *
913
+ * Column names reaching the CRUD methods come from `Object.keys(data)` and the
914
+ * caller's `conflictColumns`, both of which are interpolated into SQL. The
915
+ * accepted shape has no character that can end an identifier. Adapters may
916
+ * render a validated name bare, or quote it under an adapter-specific contract.
917
+ * PostgreSQL upsert uses {@link quotePostgresColumnName}, which canonicalizes
918
+ * the name before quoting so its established unquoted case-folding semantics
919
+ * do not change.
920
+ *
921
+ * @param columnNames - Column names to validate
922
+ * @throws Error if any column name contains invalid characters
923
+ */
924
+ function validateColumnNames(columnNames) {
925
+ for (const columnName of columnNames) validateColumnName(columnName);
926
+ }
927
+ /**
928
+ * Quotes a validated column name using PostgreSQL's historic case semantics.
929
+ *
930
+ * PostgreSQL folds every unquoted identifier to lowercase. The CRUD API has
931
+ * always accepted mixed-case inputs while rendering them unquoted, so quoting
932
+ * the caller's spelling directly would silently change which physical column
933
+ * is addressed. Validate the existing plain-identifier contract first, then
934
+ * lowercase before quoting: `mixedCase` still addresses `mixedcase`, while a
935
+ * reserved word such as `end` is no longer parsed as SQL syntax.
936
+ *
937
+ * @param columnName - Plain column name to render for PostgreSQL
938
+ * @returns A lowercase, double-quoted PostgreSQL identifier
939
+ * @throws Error if the column name is outside the CRUD identifier contract
940
+ */
941
+ function quotePostgresColumnName(columnName) {
942
+ validateColumnName(columnName);
943
+ return `"${columnName.toLowerCase()}"`;
944
+ }
945
+ /**
946
+ * Escapes an identifier for interpolation inside double quotes
947
+ *
948
+ * For the positions that already quote the identifier — the DuckDB and JSON
949
+ * upsert statements, which quote to match their schema generator — quoting is
950
+ * what makes the identifier safe, so the only thing that has to be handled is
951
+ * a quote character inside the name. Doubling it is the SQL escape.
952
+ *
953
+ * This is deliberately more permissive than {@link validateColumnName}: those
954
+ * adapters accept column names that are not plain identifiers (`Full Name`,
955
+ * `user-id`, `2024_total`), which matters most for the JSON adapter, whose
956
+ * columns come straight from the keys of whatever JSON it was pointed at.
957
+ * Rejecting them would break working code to fix an injection that quoting
958
+ * already prevents.
959
+ *
960
+ * @param identifier - Identifier to place inside double quotes
961
+ * @returns The identifier with embedded quotes doubled
962
+ * @throws Error if the identifier is empty or contains a NUL byte
963
+ */
964
+ function escapeQuotedIdentifier(identifier) {
965
+ const value = String(identifier);
966
+ if (value.length === 0 || value.includes("\0")) throw new Error(`Invalid column name: ${JSON.stringify(value)}. Column names must be non-empty and must not contain a NUL byte.`);
967
+ return value.replaceAll("\"", "\"\"");
968
+ }
969
+ /**
970
+ * Escapes a value for interpolation inside a single-quoted SQL string literal
971
+ *
972
+ * The DuckDB-backed adapters read JSON through `read_json_auto('<path>')`, and
973
+ * a file path is not something an identifier validator can check — paths
974
+ * legitimately contain spaces, dots and, on Linux and macOS, quotes. Doubling
975
+ * the quote is the SQL escape, and it is what keeps a file named `x'.json`
976
+ * from ending the literal and starting a statement.
977
+ *
978
+ * @param value - Value to place inside single quotes
979
+ * @returns The value with embedded single quotes doubled
980
+ * @throws Error if the value contains a NUL byte
981
+ */
982
+ function escapeStringLiteral(value) {
983
+ const text = String(value);
984
+ if (text.includes("\0")) throw new Error(`Invalid SQL string literal: ${JSON.stringify(text)}. Values must not contain a NUL byte.`);
985
+ return text.replaceAll("'", "''");
986
+ }
987
+ /**
988
+ * Validates an index name to prevent SQL injection
989
+ *
990
+ * @param indexName - Index name to validate
991
+ * @throws Error if index name contains invalid characters
992
+ */
993
+ function validateIndexName(indexName) {
994
+ if (typeof indexName !== "string" || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(indexName)) throw new Error(`Invalid index name: ${indexName}. Index names must be a string that starts with a letter or underscore and contains only alphanumeric characters and underscores.`);
995
+ }
996
+ //#endregion
997
+ //#region src/shared/connection-cache.ts
998
+ var CACHED_VALIDATION_CAPABILITY = Symbol.for("@happyvertical/sql/connection-cache-cached-validation/v1");
999
+ /** Race-safe async resource cache used by database adapters. */
1000
+ var ConnectionCache = class {
1001
+ [CACHED_VALIDATION_CAPABILITY] = true;
1002
+ #connections = /* @__PURE__ */ new Map();
1003
+ #pending = /* @__PURE__ */ new Map();
1004
+ #evictions = /* @__PURE__ */ new Map();
1005
+ #requests = /* @__PURE__ */ new Map();
1006
+ #clearing = /* @__PURE__ */ new Map();
1007
+ #failedClosures = /* @__PURE__ */ new Map();
1008
+ async getOrCreate(key, options, create, close, validateCached) {
1009
+ validateDatabaseCacheOptions(options);
1010
+ const request = key && options.cache !== false ? this.#registerRequest(key) : void 0;
1011
+ try {
1012
+ this.#throwIfCancelled(request);
1013
+ if (key && options.clearCache) {
1014
+ await this.evict(key, close);
1015
+ this.#throwIfCancelled(request);
1016
+ }
1017
+ if (key) this.#throwIfFailedClosure(key);
1018
+ if (!key || options.cache === false) return create();
1019
+ while (true) {
1020
+ if (this.#evictions.has(key)) {
1021
+ await this.#waitForEviction(key);
1022
+ this.#throwIfCancelled(request);
1023
+ }
1024
+ this.#throwIfFailedClosure(key);
1025
+ const cached = this.#connections.get(key);
1026
+ if (cached !== void 0) {
1027
+ if (!validateCached) return cached;
1028
+ const isValid = await validateCached(cached);
1029
+ this.#throwIfCancelled(request);
1030
+ if (isValid && this.#connections.get(key) === cached && !this.#evictions.has(key)) return cached;
1031
+ if (!isValid && this.#connections.get(key) === cached && !this.#evictions.has(key)) {
1032
+ await this.evict(key, close);
1033
+ this.#throwIfCancelled(request);
1034
+ }
1035
+ continue;
1036
+ }
1037
+ let pending = this.#pending.get(key);
1038
+ if (!pending) {
1039
+ this.#throwIfCancelled(request);
1040
+ pending = {
1041
+ invalidated: false,
1042
+ promise: create()
1043
+ };
1044
+ this.#pending.set(key, pending);
1045
+ }
1046
+ let connection;
1047
+ try {
1048
+ connection = await pending.promise;
1049
+ } catch (error) {
1050
+ if (this.#pending.get(key) === pending) this.#pending.delete(key);
1051
+ throw error;
1052
+ }
1053
+ if (pending.invalidated || this.#evictions.has(key)) {
1054
+ if (this.#evictions.has(key)) await this.#waitForEviction(key);
1055
+ this.#throwIfCancelled(request);
1056
+ if (pending.invalidated === "clear") throw this.#clearError();
1057
+ continue;
1058
+ }
1059
+ this.#throwIfCancelled(request);
1060
+ if (this.#pending.get(key) === pending) this.#pending.delete(key);
1061
+ this.#connections.set(key, connection);
1062
+ return connection;
1063
+ }
1064
+ } finally {
1065
+ if (request && key) this.#finishRequest(key, request);
1066
+ }
1067
+ }
1068
+ async evict(key, close, invalidation = "evict") {
1069
+ const previous = this.#evictions.get(key);
1070
+ const state = previous?.state ?? {
1071
+ invalidated: /* @__PURE__ */ new Set(),
1072
+ invalidation
1073
+ };
1074
+ if (invalidation === "clear") {
1075
+ state.invalidation = "clear";
1076
+ for (const pending of state.invalidated) pending.invalidated = "clear";
1077
+ }
1078
+ const eviction = (previous?.promise ?? Promise.resolve()).then(async () => {
1079
+ const resources = new Set(this.#failedClosures.get(key)?.resources ?? []);
1080
+ this.#failedClosures.delete(key);
1081
+ const cached = this.#connections.get(key);
1082
+ this.#connections.delete(key);
1083
+ if (cached) resources.add(cached);
1084
+ const pending = this.#pending.get(key);
1085
+ this.#pending.delete(key);
1086
+ if (pending) {
1087
+ state.invalidated.add(pending);
1088
+ pending.invalidated = state.invalidation;
1089
+ const initialized = await pending.promise.catch(() => void 0);
1090
+ if (initialized !== void 0) resources.add(initialized);
1091
+ }
1092
+ const closeResults = await Promise.allSettled([...resources].map(async (resource) => {
1093
+ await close(resource);
1094
+ return resource;
1095
+ }));
1096
+ const failures = closeResults.filter((result) => result.status === "rejected");
1097
+ if (failures.length) {
1098
+ const resourceList = [...resources];
1099
+ const failedResources = /* @__PURE__ */ new Set();
1100
+ closeResults.forEach((result, index) => {
1101
+ if (result.status === "rejected") failedResources.add(resourceList[index]);
1102
+ });
1103
+ const error = failures.length === 1 ? failures[0].reason : new AggregateError(failures.map((failure) => failure.reason), "Multiple database connections failed to close");
1104
+ this.#failedClosures.set(key, {
1105
+ error,
1106
+ resources: failedResources
1107
+ });
1108
+ throw error;
1109
+ }
1110
+ });
1111
+ const chain = {
1112
+ promise: eviction,
1113
+ state
1114
+ };
1115
+ this.#evictions.set(key, chain);
1116
+ try {
1117
+ await eviction;
1118
+ } finally {
1119
+ if (this.#evictions.get(key) === chain) this.#evictions.delete(key);
1120
+ }
1121
+ }
1122
+ async clear(close) {
1123
+ const keys = /* @__PURE__ */ new Set([
1124
+ ...this.#connections.keys(),
1125
+ ...this.#pending.keys(),
1126
+ ...this.#evictions.keys(),
1127
+ ...this.#requests.keys(),
1128
+ ...this.#clearing.keys(),
1129
+ ...this.#failedClosures.keys()
1130
+ ]);
1131
+ const failures = (await Promise.allSettled([...keys].map((key) => this.#clearKey(key, close)))).filter((result) => result.status === "rejected");
1132
+ if (failures.length === 1) throw failures[0].reason;
1133
+ if (failures.length > 1) throw new AggregateError(failures.map((failure) => failure.reason), "Multiple database connection cache entries failed to clear");
1134
+ }
1135
+ forget(connection) {
1136
+ for (const [key, cached] of this.#connections) if (cached === connection) this.#connections.delete(key);
1137
+ }
1138
+ /** @internal Test-only visibility; includes every retained namespace. */
1139
+ get size() {
1140
+ return this.#connections.size + this.#pending.size + this.#evictions.size + this.#requests.size + this.#clearing.size + this.#failedClosures.size;
1141
+ }
1142
+ async #clearKey(key, close) {
1143
+ this.#clearing.set(key, (this.#clearing.get(key) ?? 0) + 1);
1144
+ try {
1145
+ this.#cancelRequests(key);
1146
+ await this.evict(key, close, "clear");
1147
+ while (this.#requests.get(key)?.size) {
1148
+ this.#cancelRequests(key);
1149
+ await Promise.all([...this.#requests.get(key) ?? []].map((request) => request.done));
1150
+ }
1151
+ } finally {
1152
+ const remaining = (this.#clearing.get(key) ?? 1) - 1;
1153
+ if (remaining === 0) this.#clearing.delete(key);
1154
+ else this.#clearing.set(key, remaining);
1155
+ }
1156
+ }
1157
+ #cancelRequests(key) {
1158
+ for (const request of this.#requests.get(key) ?? []) request.cancelled = true;
1159
+ }
1160
+ #clearError() {
1161
+ return /* @__PURE__ */ new Error("Database connection initialization was cancelled by cache clear");
1162
+ }
1163
+ #finishRequest(key, request) {
1164
+ const requests = this.#requests.get(key);
1165
+ requests?.delete(request);
1166
+ if (requests?.size === 0) this.#requests.delete(key);
1167
+ request.finish();
1168
+ }
1169
+ #registerRequest(key) {
1170
+ let finish = () => {};
1171
+ const done = new Promise((resolve) => {
1172
+ finish = resolve;
1173
+ });
1174
+ const request = {
1175
+ cancelled: this.#clearing.has(key),
1176
+ done,
1177
+ finish
1178
+ };
1179
+ const requests = this.#requests.get(key) ?? /* @__PURE__ */ new Set();
1180
+ requests.add(request);
1181
+ this.#requests.set(key, requests);
1182
+ return request;
1183
+ }
1184
+ #throwIfCancelled(request) {
1185
+ if (request?.cancelled) throw this.#clearError();
1186
+ }
1187
+ #throwIfFailedClosure(key) {
1188
+ const failure = this.#failedClosures.get(key);
1189
+ if (failure) throw failure.error;
1190
+ }
1191
+ async #waitForEviction(key) {
1192
+ while (true) {
1193
+ const eviction = this.#evictions.get(key);
1194
+ if (!eviction) return;
1195
+ await eviction.promise;
1196
+ }
1197
+ }
1198
+ };
1199
+ /**
1200
+ * Adds cached-resource validation to a shared cache created by an older module.
1201
+ *
1202
+ * JSON connection caches live on `globalThis`, so a compatible older SDK copy
1203
+ * can create the shared object before this module loads. Mutating that object
1204
+ * preserves one cache for both module copies while upgrading its four-argument
1205
+ * `getOrCreate` method to the current validation contract.
1206
+ */
1207
+ function enableCachedValidation(cache) {
1208
+ if (cache[CACHED_VALIDATION_CAPABILITY]) return cache;
1209
+ const legacyGetOrCreate = cache.getOrCreate.bind(cache);
1210
+ const legacyClear = cache.clear.bind(cache);
1211
+ const locks = /* @__PURE__ */ new Map();
1212
+ let clearGeneration = 0;
1213
+ let activeClear;
1214
+ const runLocked = async (key, work) => {
1215
+ const previous = locks.get(key) ?? Promise.resolve();
1216
+ let release;
1217
+ const current = new Promise((resolve) => {
1218
+ release = resolve;
1219
+ });
1220
+ const chain = previous.then(() => current, () => current);
1221
+ locks.set(key, chain);
1222
+ await previous.catch(() => {});
1223
+ try {
1224
+ return await work();
1225
+ } finally {
1226
+ release();
1227
+ if (locks.get(key) === chain) locks.delete(key);
1228
+ }
1229
+ };
1230
+ Object.defineProperty(cache, "getOrCreate", {
1231
+ configurable: true,
1232
+ value: async (key, options, create, close, validateCached) => {
1233
+ if (!key || options.cache === false) return legacyGetOrCreate(key, options, create, close);
1234
+ const requestGeneration = clearGeneration;
1235
+ const throwIfCleared = () => {
1236
+ if (activeClear || requestGeneration !== clearGeneration) throw new Error("Database connection initialization was cancelled by cache clear");
1237
+ };
1238
+ return runLocked(key, async () => {
1239
+ throwIfCleared();
1240
+ let requestOptions = options;
1241
+ while (true) {
1242
+ throwIfCleared();
1243
+ const candidate = await legacyGetOrCreate(key, requestOptions, create, close);
1244
+ throwIfCleared();
1245
+ if (!validateCached) return candidate;
1246
+ const isValid = await validateCached(candidate);
1247
+ throwIfCleared();
1248
+ if (isValid) {
1249
+ const confirmed = await legacyGetOrCreate(key, {
1250
+ ...requestOptions,
1251
+ clearCache: false
1252
+ }, create, close);
1253
+ throwIfCleared();
1254
+ if (confirmed === candidate) return candidate;
1255
+ } else await cache.evict(key, close);
1256
+ requestOptions = {
1257
+ ...options,
1258
+ clearCache: false
1259
+ };
1260
+ }
1261
+ });
1262
+ },
1263
+ writable: true
1264
+ });
1265
+ Object.defineProperty(cache, "clear", {
1266
+ configurable: true,
1267
+ value: async (close) => {
1268
+ if (activeClear) return activeClear;
1269
+ clearGeneration += 1;
1270
+ const clearing = (async () => {
1271
+ let clearError;
1272
+ let clearFailed = false;
1273
+ try {
1274
+ await legacyClear(close);
1275
+ } catch (error) {
1276
+ clearFailed = true;
1277
+ clearError = error;
1278
+ }
1279
+ while (locks.size > 0) await Promise.allSettled([...locks.values()]);
1280
+ if (clearFailed) throw clearError;
1281
+ })();
1282
+ activeClear = clearing;
1283
+ try {
1284
+ await clearing;
1285
+ } finally {
1286
+ if (activeClear === clearing) activeClear = void 0;
1287
+ }
1288
+ },
1289
+ writable: true
1290
+ });
1291
+ Object.defineProperty(cache, CACHED_VALIDATION_CAPABILITY, {
1292
+ configurable: false,
1293
+ enumerable: false,
1294
+ value: true,
1295
+ writable: false
1296
+ });
1297
+ return cache;
1298
+ }
1299
+ function validateDatabaseCacheOptions(options) {
1300
+ if (options.dbid === "") throw new Error("Database cache dbid must be a non-empty string");
1301
+ }
1302
+ //#endregion
1303
+ //#region src/shared/transaction-lock.ts
1304
+ /**
1305
+ * How long a queued transaction waits for the connection before giving up.
1306
+ *
1307
+ * A transaction that legitimately runs longer than this will make the ones
1308
+ * behind it fail rather than stall indefinitely. Raise it with the adapter's
1309
+ * `transactionQueueTimeout` option if that is the wrong trade for a workload.
1310
+ */
1311
+ var DEFAULT_TRANSACTION_QUEUE_TIMEOUT_MS = 3e4;
1312
+ /**
1313
+ * Creates a lock that serializes transactions on one connection.
1314
+ *
1315
+ * SQLite, DuckDB and the JSON adapter each drive a single connection, and a
1316
+ * connection can only be in one transaction at a time. `transaction()` used to
1317
+ * issue `BEGIN` on that shared connection with no serialization and no record
1318
+ * of whether one was already open, so two overlapping calls corrupted each
1319
+ * other: the second `BEGIN` threw, its `catch` ran `ROLLBACK`, and that
1320
+ * rollback landed on the *first* transaction — committing part of its work,
1321
+ * discarding the rest, and rejecting its promise. Nesting was not required;
1322
+ * two concurrent requests were enough, which made it reachable from any server
1323
+ * handling more than one at a time.
1324
+ *
1325
+ * The lock spans the whole `BEGIN` … `COMMIT`/`ROLLBACK` sequence, so a queued
1326
+ * transaction starts only once the previous one has ended. It is a
1327
+ * promise-chain mutex, the same shape as the `withNullAwareUpsertLock` the
1328
+ * SQLite adapter already used for the narrower upsert case.
1329
+ *
1330
+ * Re-entrant calls must not come through here. A nested `tx.transaction()`
1331
+ * arrives via the transaction-scoped interface, which re-enters under a
1332
+ * `SAVEPOINT` (SQLite) or refuses (DuckDB, JSON) without ever reaching the
1333
+ * top-level `transaction()`; routing one here instead would make it wait on a
1334
+ * lock its own caller is holding.
1335
+ *
1336
+ * @param adapter - Adapter name, for the timeout error
1337
+ * @param timeoutMs - Ceiling on how long to wait for a turn
1338
+ */
1339
+ function createTransactionLock(adapter, timeoutMs = DEFAULT_TRANSACTION_QUEUE_TIMEOUT_MS) {
1340
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new DatabaseError("transactionQueueTimeout must be a positive, finite number of milliseconds", {
1341
+ adapter,
1342
+ timeoutMs
1343
+ });
1344
+ let tail = Promise.resolve();
1345
+ let held = false;
1346
+ const acquire = async () => {
1347
+ const previous = tail;
1348
+ let release = () => {};
1349
+ const current = new Promise((resolve) => {
1350
+ release = resolve;
1351
+ });
1352
+ tail = previous.then(() => current, () => current);
1353
+ let timer;
1354
+ try {
1355
+ await new Promise((resolve, reject) => {
1356
+ timer = setTimeout(() => {
1357
+ reject(new DatabaseError(`Timed out after ${timeoutMs}ms waiting for the ${adapter} connection's current transaction to finish. This connection runs one transaction at a time. Usually one of: a beginTransaction() handle that was never committed or rolled back; a top-level db.* call that opens its own transaction being made inside a transaction callback, which waits on the connection its own caller holds (use the tx-scoped method instead); or simply more queued transactions than transactionQueueTimeout allows for.`, {
1358
+ adapter,
1359
+ timeoutMs
1360
+ }));
1361
+ }, timeoutMs);
1362
+ previous.then(resolve, resolve);
1363
+ });
1364
+ } catch (error) {
1365
+ release();
1366
+ throw error;
1367
+ } finally {
1368
+ if (timer) clearTimeout(timer);
1369
+ }
1370
+ let released = false;
1371
+ held = true;
1372
+ return () => {
1373
+ if (released) return;
1374
+ released = true;
1375
+ held = false;
1376
+ release();
1377
+ };
1378
+ };
1379
+ const run = async (work) => {
1380
+ const release = await acquire();
1381
+ try {
1382
+ return await work();
1383
+ } finally {
1384
+ release();
1385
+ }
1386
+ };
1387
+ return {
1388
+ acquire,
1389
+ get held() {
1390
+ return held;
1391
+ },
1392
+ run
1393
+ };
1394
+ }
1395
+ //#endregion
1396
+ //#region src/shared/postgres-dialect.ts
1397
+ /**
1398
+ * PostgreSQL dialect logic shared by every adapter that speaks to a Postgres
1399
+ * server: the `pg` adapter (`../postgres`) and the PGlite adapter
1400
+ * (`../pglite`).
1401
+ *
1402
+ * Everything here runs on a {@link PostgresQueryExecutor}, so the SQL the two
1403
+ * adapters emit is the same code rather than two copies that drift. The module
1404
+ * must stay browser-safe: no `node:` built-ins and no import of `pg`, because
1405
+ * the PGlite adapter's import graph reaches it.
1406
+ */
1407
+ /**
1408
+ * Operator for the given distance metric in pgvector
1409
+ */
1410
+ function vectorOperator(metric) {
1411
+ switch (metric) {
1412
+ case "cosine": return "<=>";
1413
+ case "l2": return "<->";
1414
+ case "ip": return "<#>";
1415
+ default: return "<=>";
1416
+ }
1417
+ }
1418
+ function getMaxPostgresParameterIndex(sql) {
1419
+ let maxIndex = 0;
1420
+ for (const match of sql.matchAll(/\$(\d+)/g)) {
1421
+ const index = Number(match[1]);
1422
+ if (Number.isSafeInteger(index) && index > maxIndex) maxIndex = index;
1423
+ }
1424
+ return maxIndex;
1425
+ }
1426
+ function usesSingleArrayParameter(sql) {
1427
+ const identifier = String.raw`(?:"[^"]+"|[a-z_][\w$]*)`;
1428
+ const qualifiedIdentifier = String.raw`${identifier}(?:\s*\.\s*${identifier})?`;
1429
+ const arrayType = String.raw`${qualifiedIdentifier}(?:\s*\([^)]*\))?\s*\[\]`;
1430
+ return new RegExp(String.raw`\$1\s*::\s*${arrayType}`, "i").test(sql) || new RegExp(String.raw`\bCAST\s*\(\s*\$1\s+AS\s+${arrayType}\s*\)`, "i").test(sql) || /\b(?:ANY|ALL|SOME)\s*\(\s*\$1\s*\)/i.test(sql);
1431
+ }
1432
+ function normalizeRawQueryValues(sql, values) {
1433
+ if (values.length !== 1 || !Array.isArray(values[0])) return values;
1434
+ const valuesArray = values[0];
1435
+ const maxParameterIndex = getMaxPostgresParameterIndex(sql);
1436
+ if (maxParameterIndex === 1 && usesSingleArrayParameter(sql)) return values;
1437
+ if (maxParameterIndex === 0 || maxParameterIndex === valuesArray.length) return valuesArray;
1438
+ return values;
1439
+ }
1440
+ function rewriteQuestionMarkPlaceholders(sql) {
1441
+ let output = "";
1442
+ let count = 0;
1443
+ let index = 0;
1444
+ const copyQuoted = (quote, escaped = false) => {
1445
+ output += quote;
1446
+ index += 1;
1447
+ while (index < sql.length) {
1448
+ const char = sql[index];
1449
+ output += char;
1450
+ index += 1;
1451
+ if (escaped && char === "\\" && index < sql.length) {
1452
+ output += sql[index];
1453
+ index += 1;
1454
+ continue;
1455
+ }
1456
+ if (char === quote) {
1457
+ if (sql[index] === quote) {
1458
+ output += sql[index];
1459
+ index += 1;
1460
+ continue;
1461
+ }
1462
+ break;
1463
+ }
1464
+ }
1465
+ };
1466
+ const copyUntil = (end) => {
1467
+ const endIndex = sql.indexOf(end, index + end.length);
1468
+ if (endIndex === -1) {
1469
+ output += sql.slice(index);
1470
+ index = sql.length;
1471
+ return;
1472
+ }
1473
+ output += sql.slice(index, endIndex + end.length);
1474
+ index = endIndex + end.length;
1475
+ };
1476
+ while (index < sql.length) {
1477
+ const char = sql[index];
1478
+ const next = sql[index + 1];
1479
+ if ((char === "E" || char === "e") && next === "'") {
1480
+ output += char;
1481
+ index += 1;
1482
+ copyQuoted("'", true);
1483
+ continue;
1484
+ }
1485
+ if (char === "'") {
1486
+ copyQuoted("'");
1487
+ continue;
1488
+ }
1489
+ if (char === "\"") {
1490
+ copyQuoted("\"");
1491
+ continue;
1492
+ }
1493
+ if (char === "-" && next === "-") {
1494
+ const endIndex = sql.indexOf("\n", index + 2);
1495
+ if (endIndex === -1) {
1496
+ output += sql.slice(index);
1497
+ index = sql.length;
1498
+ } else {
1499
+ output += sql.slice(index, endIndex + 1);
1500
+ index = endIndex + 1;
1501
+ }
1502
+ continue;
1503
+ }
1504
+ if (char === "/" && next === "*") {
1505
+ copyUntil("*/");
1506
+ continue;
1507
+ }
1508
+ if (char === "$") {
1509
+ const tag = sql.slice(index).match(/^\$[A-Za-z_][\w$]*\$|^\$\$/);
1510
+ if (tag) {
1511
+ copyUntil(tag[0]);
1512
+ continue;
1513
+ }
1514
+ }
1515
+ if (char === "?" && next !== "|" && next !== "&") {
1516
+ count += 1;
1517
+ output += `$${count}`;
1518
+ index += 1;
1519
+ continue;
1520
+ }
1521
+ output += char;
1522
+ index += 1;
1523
+ }
1524
+ return {
1525
+ sql: output,
1526
+ count
1527
+ };
1528
+ }
1529
+ function usesSingleQuestionArrayParameter(sql) {
1530
+ const identifier = String.raw`(?:"[^"]+"|[a-z_][\w$]*)`;
1531
+ const qualifiedIdentifier = String.raw`${identifier}(?:\s*\.\s*${identifier})?`;
1532
+ const arrayType = String.raw`${qualifiedIdentifier}(?:\s*\([^)]*\))?\s*\[\]`;
1533
+ return new RegExp(String.raw`\?\s*::\s*${arrayType}`, "i").test(sql) || new RegExp(String.raw`\bCAST\s*\(\s*\?\s+AS\s+${arrayType}\s*\)`, "i").test(sql) || /\b(?:ANY|ALL|SOME)\s*\(\s*\?\s*\)/i.test(sql);
1534
+ }
1535
+ function normalizeQuestionMarkQueryValues(sql, values, placeholderCount) {
1536
+ if (values.length !== 1 || !Array.isArray(values[0])) return values;
1537
+ if (placeholderCount === 1 && usesSingleQuestionArrayParameter(sql)) return values;
1538
+ if (placeholderCount === values[0].length) return values[0];
1539
+ return values;
1540
+ }
1541
+ function normalizePostgresRawQuery(sql, values) {
1542
+ if (getMaxPostgresParameterIndex(sql) > 0) return {
1543
+ sql,
1544
+ values: normalizeRawQueryValues(sql, values)
1545
+ };
1546
+ const rewritten = rewriteQuestionMarkPlaceholders(sql);
1547
+ const placeholderCount = rewritten.count;
1548
+ const queryValues = normalizeQuestionMarkQueryValues(sql, values, placeholderCount);
1549
+ if (placeholderCount > 0 && placeholderCount === queryValues.length) return {
1550
+ sql: rewritten.sql,
1551
+ values: queryValues
1552
+ };
1553
+ return {
1554
+ sql,
1555
+ values: normalizeRawQueryValues(sql, values)
1556
+ };
1557
+ }
1558
+ /**
1559
+ * pgvector index operator class for CREATE INDEX
1560
+ */
1561
+ function vectorOpsClass(metric) {
1562
+ switch (metric) {
1563
+ case "cosine": return "vector_cosine_ops";
1564
+ case "l2": return "vector_l2_ops";
1565
+ case "ip": return "vector_ip_ops";
1566
+ default: return "vector_cosine_ops";
1567
+ }
1568
+ }
1569
+ /**
1570
+ * Convert a number[] to a pgvector literal string: '[0.1,0.2,0.3]'
1571
+ */
1572
+ function toVectorLiteral(embedding) {
1573
+ if (embedding.length === 0) throw new DatabaseError("Vector embedding must not be empty", {});
1574
+ for (let i = 0; i < embedding.length; i++) if (!Number.isFinite(embedding[i])) throw new DatabaseError("Vector embedding contains invalid value (NaN or Infinity)", {
1575
+ index: i,
1576
+ value: embedding[i]
1577
+ });
1578
+ return `[${embedding.join(",")}]`;
1579
+ }
1580
+ /**
1581
+ * Creates vector capabilities backed by pgvector for a PostgreSQL pool.
1582
+ *
1583
+ * @param pool - PostgreSQL connection pool
1584
+ * @returns VectorCapabilities implementation
1585
+ */
1586
+ /**
1587
+ * `type` is interpolated into `USING <type>`, which cannot be parameterized and
1588
+ * is not an identifier the quoting helpers cover. `VectorIndexOptions.type`
1589
+ * constrains it at compile time only, so anything reaching this at runtime —
1590
+ * a JavaScript caller, or a `filter as VectorIndexOptions` on parsed JSON — has
1591
+ * to be checked against the values pgvector actually offers.
1592
+ */
1593
+ var VECTOR_INDEX_TYPES = /* @__PURE__ */ new Set(["hnsw", "ivfflat"]);
1594
+ /**
1595
+ * `metric` needs the same treatment as `type`, for a less obvious reason.
1596
+ *
1597
+ * It never reaches SQL directly — `vectorOpsClass`/`vectorOperator` map it
1598
+ * through a closed switch with a safe default, so an unknown metric produces a
1599
+ * valid operator class. But it is also concatenated into the *generated index
1600
+ * name*, which is then interpolated into a quoted identifier, so a metric
1601
+ * carrying a `"` still ends that identifier. The safe-default mapping is what
1602
+ * makes this easy to miss: the part of the metric that looks dangerous is
1603
+ * handled, and the part that is actually dangerous is somewhere else.
1604
+ */
1605
+ var VECTOR_METRICS = /* @__PURE__ */ new Set([
1606
+ "cosine",
1607
+ "l2",
1608
+ "ip"
1609
+ ]);
1610
+ function createVectorCapabilities(pool) {
1611
+ return {
1612
+ async ensureColumn(table, column, dimensions) {
1613
+ if (!Number.isInteger(dimensions) || dimensions <= 0) throw new DatabaseError("Vector dimensions must be a positive integer", { dimensions });
1614
+ validateTableName(table);
1615
+ validateColumnName(column);
1616
+ try {
1617
+ await pool.query("CREATE EXTENSION IF NOT EXISTS vector");
1618
+ await pool.query(`ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "${column}" vector(${dimensions})`);
1619
+ } catch (e) {
1620
+ throw new DatabaseError("Failed to ensure vector column", {
1621
+ table,
1622
+ column,
1623
+ dimensions,
1624
+ originalError: formatDbError(e)
1625
+ });
1626
+ }
1627
+ },
1628
+ async ensureIndex(table, column, options) {
1629
+ validateTableName(table);
1630
+ validateColumnName(column);
1631
+ const metric = options?.metric || "cosine";
1632
+ const indexType = options?.type || "hnsw";
1633
+ if (!VECTOR_INDEX_TYPES.has(indexType)) throw new DatabaseError("Unsupported vector index type", {
1634
+ table,
1635
+ column,
1636
+ type: indexType,
1637
+ supported: [...VECTOR_INDEX_TYPES]
1638
+ });
1639
+ if (!VECTOR_METRICS.has(metric)) throw new DatabaseError("Unsupported vector metric", {
1640
+ table,
1641
+ column,
1642
+ metric,
1643
+ supported: [...VECTOR_METRICS]
1644
+ });
1645
+ const opsClass = vectorOpsClass(metric);
1646
+ const indexName = `idx_${table}_${column}_${metric}_${indexType}`;
1647
+ try {
1648
+ await pool.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" USING ${indexType} ("${column}" ${opsClass})`);
1649
+ } catch (e) {
1650
+ throw new DatabaseError("Failed to ensure vector index", {
1651
+ table,
1652
+ column,
1653
+ indexName,
1654
+ originalError: formatDbError(e)
1655
+ });
1656
+ }
1657
+ },
1658
+ async upsertVector(table, where, column, embedding) {
1659
+ validateTableName(table);
1660
+ validateColumnName(column);
1661
+ const keys = Object.keys(where);
1662
+ validateColumnNames(keys);
1663
+ const values = Object.values(where);
1664
+ const conditions = keys.map((key, i) => `"${key}" IS NOT DISTINCT FROM $${i + 1}`).join(" AND ");
1665
+ const vectorParam = `$${values.length + 1}`;
1666
+ try {
1667
+ await pool.query(`UPDATE "${table}" SET "${column}" = ${vectorParam}::vector WHERE ${conditions}`, [...values, toVectorLiteral(embedding)]);
1668
+ } catch (e) {
1669
+ throw new DatabaseError("Failed to upsert vector", {
1670
+ table,
1671
+ column,
1672
+ where,
1673
+ originalError: formatDbError(e)
1674
+ });
1675
+ }
1676
+ },
1677
+ async search(table, column, embedding, options) {
1678
+ validateTableName(table);
1679
+ validateColumnName(column);
1680
+ const metric = options?.metric || "cosine";
1681
+ const limit = options?.limit || 10;
1682
+ const op = vectorOperator(metric);
1683
+ const queryParams = [toVectorLiteral(embedding)];
1684
+ let whereClause = `"${column}" IS NOT NULL`;
1685
+ if (options?.where) {
1686
+ whereClause += ` AND (${options.where})`;
1687
+ if (options.params) queryParams.push(...options.params);
1688
+ }
1689
+ const limitParam = `$${queryParams.length + 1}`;
1690
+ queryParams.push(limit);
1691
+ const sql = `SELECT *, ("${column}" ${op} $1::vector) AS distance FROM "${table}" WHERE ${whereClause} ORDER BY "${column}" ${op} $1::vector LIMIT ${limitParam}`;
1692
+ try {
1693
+ return (await pool.query(sql, queryParams)).rows.map((row) => ({
1694
+ ...row,
1695
+ id: row.id,
1696
+ distance: Number.parseFloat(row.distance)
1697
+ }));
1698
+ } catch (e) {
1699
+ throw new DatabaseError("Failed to execute vector search", {
1700
+ table,
1701
+ column,
1702
+ metric,
1703
+ limit,
1704
+ originalError: formatDbError(e)
1705
+ });
1706
+ }
1707
+ }
1708
+ };
1709
+ }
1710
+ /**
1711
+ * Keeps the first PostgreSQL statement failure attached to a transaction.
1712
+ *
1713
+ * PostgreSQL reports every later statement in an aborted transaction as
1714
+ * 25P02. Replacing the first failure with that generic state error hides the
1715
+ * actionable code and diagnostics from callers, so the tracker rethrows the
1716
+ * first failure whenever PostgreSQL reports 25P02, and exposes the recorded
1717
+ * failure so a swallowed statement error cannot make COMMIT's implicit
1718
+ * rollback look successful: PostgreSQL answers COMMIT on an aborted
1719
+ * transaction with a ROLLBACK tag and no error.
1720
+ *
1721
+ * @param isStatementError - Recognizes the driver's server-reported error
1722
+ * class (`pg`'s or PGlite's `DatabaseError`). Anything else is an
1723
+ * unconfirmed failure — it may be the transport, not the statement.
1724
+ */
1725
+ function createTransactionErrorTracker(isStatementError) {
1726
+ let firstError;
1727
+ let firstUnconfirmedError;
1728
+ const pending = /* @__PURE__ */ new Set();
1729
+ const isRollbackToSavepoint = (query) => {
1730
+ const sql = typeof query === "string" ? query : typeof query === "object" && query !== null && "text" in query && typeof query.text === "string" ? query.text : void 0;
1731
+ if (sql === void 0) return false;
1732
+ let statement = sql.trimStart();
1733
+ while (statement.startsWith("--") || statement.startsWith("/*")) {
1734
+ if (statement.startsWith("--")) {
1735
+ const newline = statement.search(/[\r\n]/);
1736
+ if (newline === -1) return false;
1737
+ statement = statement.slice(newline + 1).trimStart();
1738
+ continue;
1739
+ }
1740
+ const commentEnd = statement.indexOf("*/", 2);
1741
+ if (commentEnd === -1) return false;
1742
+ statement = statement.slice(commentEnd + 2).trimStart();
1743
+ }
1744
+ return /^ROLLBACK\s+TO(?:\s+SAVEPOINT)?\s+/i.test(statement);
1745
+ };
1746
+ const errorCode = (error) => {
1747
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
1748
+ };
1749
+ /** Records a failure and returns the error the caller should see. */
1750
+ const preserve = (error) => {
1751
+ if (errorCode(error) === "25P02" && firstError !== void 0) return firstError;
1752
+ if (errorCode(error) === "25P02" && firstUnconfirmedError !== void 0) return firstUnconfirmedError;
1753
+ if (isStatementError(error)) firstError ??= error;
1754
+ else firstUnconfirmedError ??= error;
1755
+ return error;
1756
+ };
1757
+ /**
1758
+ * Records that a statement succeeded. A successful rollback to a savepoint
1759
+ * restores the transaction to the state at that savepoint, so a recorded
1760
+ * statement failure has been contained.
1761
+ */
1762
+ const confirm = (query) => {
1763
+ if (isRollbackToSavepoint(query)) firstError = void 0;
1764
+ firstUnconfirmedError = void 0;
1765
+ };
1766
+ const track = (operation, query) => {
1767
+ const tracked = operation.then((result) => {
1768
+ confirm(query);
1769
+ return result;
1770
+ }).catch((error) => {
1771
+ throw preserve(error);
1772
+ });
1773
+ pending.add(tracked);
1774
+ tracked.finally(() => pending.delete(tracked)).catch(() => {});
1775
+ return tracked;
1776
+ };
1777
+ return {
1778
+ pending,
1779
+ preserve,
1780
+ confirm,
1781
+ track,
1782
+ clearError: () => {
1783
+ firstError = void 0;
1784
+ },
1785
+ drain: async () => {
1786
+ while (pending.size > 0) await Promise.allSettled([...pending]);
1787
+ },
1788
+ getError: () => firstError,
1789
+ getUnconfirmedError: () => firstUnconfirmedError
1790
+ };
1791
+ }
1792
+ /**
1793
+ * Creates the dialect: every statement-level method plus nested-transaction
1794
+ * and schema-introspection builders, all bound to `host`.
1795
+ */
1796
+ function createPostgresDialect(host) {
1797
+ let savepointSequence = 0;
1798
+ /**
1799
+ * Serializes a value for database storage
1800
+ * Converts objects and arrays to JSON strings
1801
+ * Converts Dates to ISO strings
1802
+ * Passes through primitives unchanged
1803
+ */
1804
+ const serializeValue = (value) => {
1805
+ if (value === null || value === void 0) return value;
1806
+ if (value instanceof Date) return value.toISOString();
1807
+ if (ArrayBuffer.isView(value)) return value;
1808
+ if (typeof value === "object") return JSON.stringify(value);
1809
+ return value;
1810
+ };
1811
+ /**
1812
+ * Serializes all values in an object for database storage
1813
+ */
1814
+ const serializeRecord = (record) => {
1815
+ const serialized = {};
1816
+ for (const [key, value] of Object.entries(record)) {
1817
+ if (value === void 0) continue;
1818
+ serialized[key] = serializeValue(value);
1819
+ }
1820
+ return serialized;
1821
+ };
1822
+ let serverVersionNumPromise;
1823
+ const nullsNotDistinctIndexCache = /* @__PURE__ */ new Map();
1824
+ const hasNullConflictValue = (conflictColumns, data) => conflictColumns.some((col) => data[col] === null);
1825
+ const validateUpsertConflictColumns = (table, conflictColumns, serializedData) => {
1826
+ const missingColumns = conflictColumns.filter((col) => !(col in serializedData));
1827
+ if (missingColumns.length > 0) throw new DatabaseError("Conflict columns missing from data", {
1828
+ table,
1829
+ conflictColumns,
1830
+ missingColumns,
1831
+ availableColumns: Object.keys(serializedData),
1832
+ hint: "All columns specified in ON CONFLICT must be present in the data being inserted. Undefined values should be replaced with null or an appropriate default."
1833
+ });
1834
+ };
1835
+ const getServerVersionNum = async (executor) => {
1836
+ serverVersionNumPromise ??= executor.query("SHOW server_version_num").then((result) => Number(result.rows[0]?.server_version_num) || 0).catch((error) => {
1837
+ serverVersionNumPromise = void 0;
1838
+ throw error;
1839
+ });
1840
+ return serverVersionNumPromise;
1841
+ };
1842
+ const hasNativeNullsNotDistinctIndex = async (executor, table, conflictColumns) => {
1843
+ if (await getServerVersionNum(executor) < 15e4) return false;
1844
+ const cacheKey = `${table}:${conflictColumns.join(",")}`;
1845
+ let cached = nullsNotDistinctIndexCache.get(cacheKey);
1846
+ if (!cached) {
1847
+ cached = executor.query(`
1848
+ SELECT 1
1849
+ FROM pg_index i
1850
+ WHERE i.indrelid = to_regclass($1)
1851
+ AND i.indisunique
1852
+ AND i.indnullsnotdistinct
1853
+ AND ARRAY(
1854
+ SELECT a.attname::text
1855
+ FROM unnest(i.indkey) WITH ORDINALITY AS cols(attnum, ord)
1856
+ JOIN pg_attribute a
1857
+ ON a.attrelid = i.indrelid
1858
+ AND a.attnum = cols.attnum
1859
+ ORDER BY cols.ord
1860
+ ) = $2::text[]
1861
+ LIMIT 1
1862
+ `, [table, conflictColumns]).then((result) => result.rows.length > 0).catch(() => {
1863
+ nullsNotDistinctIndexCache.delete(cacheKey);
1864
+ return false;
1865
+ });
1866
+ nullsNotDistinctIndexCache.set(cacheKey, cached);
1867
+ }
1868
+ return cached;
1869
+ };
1870
+ const buildPostgresUpsertParts = (conflictColumns, serializedData) => {
1871
+ const keys = Object.keys(serializedData);
1872
+ const columns = keys.map(quotePostgresColumnName);
1873
+ return {
1874
+ columns,
1875
+ values: Object.values(serializedData),
1876
+ placeholders: keys.map((_, i) => `$${i + 1}`).join(", "),
1877
+ updateSet: columns.map((column, i) => `${column} = $${i + 1}`).join(", "),
1878
+ conflict: conflictColumns.map(quotePostgresColumnName).join(", ")
1879
+ };
1880
+ };
1881
+ const executeStandardPostgresUpsert = async (executor, table, conflictColumns, serializedData) => {
1882
+ const { columns, values, placeholders, updateSet, conflict } = buildPostgresUpsertParts(conflictColumns, serializedData);
1883
+ const sql = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflict}) DO UPDATE SET ${updateSet}`;
1884
+ return {
1885
+ operation: "upsert",
1886
+ affected: (await executor.query(sql, values)).rowCount ?? 0
1887
+ };
1888
+ };
1889
+ const executeNullAwarePostgresUpsert = async (executor, table, conflictColumns, serializedData) => {
1890
+ const { columns, values, placeholders, updateSet } = buildPostgresUpsertParts(conflictColumns, serializedData);
1891
+ const lockKey = conflictColumns.map((col) => `${col}:${JSON.stringify(serializedData[col])}`).join("|");
1892
+ await executor.query("SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))", [`@happyvertical/sql.upsert:${table}`, lockKey]);
1893
+ const whereStart = values.length + 1;
1894
+ const whereClause = conflictColumns.map((col, i) => `${quotePostgresColumnName(col)} IS NOT DISTINCT FROM $${whereStart + i}`).join(" AND ");
1895
+ const whereValues = conflictColumns.map((col) => serializedData[col]);
1896
+ const updateSql = `UPDATE ${table} SET ${updateSet} WHERE ${whereClause}`;
1897
+ const updateResult = await executor.query(updateSql, [...values, ...whereValues]);
1898
+ if ((updateResult.rowCount ?? 0) > 0) return {
1899
+ operation: "upsert",
1900
+ affected: updateResult.rowCount ?? 0
1901
+ };
1902
+ const insertSql = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders})`;
1903
+ return {
1904
+ operation: "upsert",
1905
+ affected: (await executor.query(insertSql, values)).rowCount ?? 0
1906
+ };
1907
+ };
1908
+ const executePostgresUpsert = async (executor, table, conflictColumns, serializedData, options, acquireTransaction) => {
1909
+ validateUpsertConflictColumns(table, conflictColumns, serializedData);
1910
+ if (options?.nullsDistinct || !hasNullConflictValue(conflictColumns, serializedData)) return executeStandardPostgresUpsert(executor, table, conflictColumns, serializedData);
1911
+ if (await hasNativeNullsNotDistinctIndex(host.probeExecutor(executor), table, conflictColumns)) return executeStandardPostgresUpsert(executor, table, conflictColumns, serializedData);
1912
+ if (!acquireTransaction) return executeNullAwarePostgresUpsert(executor, table, conflictColumns, serializedData);
1913
+ return host.runInTransaction((transactionExecutor) => executeNullAwarePostgresUpsert(transactionExecutor, table, conflictColumns, serializedData));
1914
+ };
1915
+ /**
1916
+ * Parses a tagged template literal into a SQL query and values
1917
+ *
1918
+ * @param strings - Template strings
1919
+ * @param vars - Variables to interpolate into the query
1920
+ * @returns Object with SQL query and values array
1921
+ */
1922
+ const parseTemplate = (strings, ...vars) => {
1923
+ let sql = strings[0];
1924
+ const values = [];
1925
+ for (let i = 0; i < vars.length; i++) {
1926
+ values.push(vars[i]);
1927
+ sql += `$${i + 1}${strings[i + 1]}`;
1928
+ }
1929
+ return {
1930
+ sql,
1931
+ values
1932
+ };
1933
+ };
1934
+ /**
1935
+ * Builds the complete set of query methods bound to one PostgreSQL client.
1936
+ *
1937
+ * The pool-backed interface and every transaction-scoped interface are
1938
+ * instantiated from this one factory, so there is exactly one implementation
1939
+ * of each method. The transaction interfaces used to be hand-maintained
1940
+ * copies of these bodies, and both had drifted:
1941
+ *
1942
+ * - they called the client bare, so a failure inside a transaction threw the
1943
+ * raw `pg` error rather than `DatabaseError`, and `instanceof` answered
1944
+ * differently depending on whether the call was in a transaction. `pg`
1945
+ * exports its own class also named `DatabaseError`, so nothing a developer
1946
+ * would check by eye showed the difference (#1115);
1947
+ * - they re-exposed `tableExists` and `syncSchema` from the enclosing scope,
1948
+ * which close over the *pool*. Both therefore ran on a different connection
1949
+ * than the transaction, so `tx.tableExists()` could not see a table created
1950
+ * in the same transaction and `tx.syncSchema()` committed its DDL
1951
+ * immediately, surviving a rollback (#1111).
1952
+ *
1953
+ * Binding a client is the only difference between the two, so it is the only
1954
+ * thing this factory parameterizes.
1955
+ *
1956
+ * @param executor - Pool or checked-out client every statement runs on
1957
+ * @param inTransaction - Whether `executor` is already inside a transaction
1958
+ * @param serialize - Applied to each record before `insert`/`update` builds
1959
+ * its SQL
1960
+ * @param clearTransactionError - Clears a statement error after a savepoint
1961
+ * has successfully restored the transaction to a usable state
1962
+ */
1963
+ const createClientMethods = (executor, inTransaction, serialize, clearTransactionError = () => {}) => {
1964
+ /**
1965
+ * Inserts one or more records into a table
1966
+ *
1967
+ * @param table - Table name
1968
+ * @param data - Single record or array of records to insert
1969
+ * @returns Promise resolving to operation result
1970
+ * @throws Error if the insert operation fails
1971
+ *
1972
+ * @example Single record insert:
1973
+ * ```typescript
1974
+ * await db.insert('users', {
1975
+ * name: 'John Doe',
1976
+ * email: 'john@example.com'
1977
+ * });
1978
+ * ```
1979
+ *
1980
+ * @example Multiple record insert:
1981
+ * ```typescript
1982
+ * await db.insert('users', [
1983
+ * { name: 'John', email: 'john@example.com' },
1984
+ * { name: 'Jane', email: 'jane@example.com' }
1985
+ * ]);
1986
+ * ```
1987
+ */
1988
+ const insert = async (table, data) => {
1989
+ validateTableName(table);
1990
+ if (Array.isArray(data)) {
1991
+ if (data.length === 0) return {
1992
+ operation: "insert",
1993
+ affected: 0
1994
+ };
1995
+ const serializedRecords = data.map((record) => serialize(record));
1996
+ const keys = resolveInsertColumns(table, serializedRecords);
1997
+ validateColumnNames(keys);
1998
+ const placeholders = serializedRecords.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
1999
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
2000
+ const values = serializedRecords.flatMap((row) => keys.map((key) => row[key]));
2001
+ return {
2002
+ operation: "insert",
2003
+ affected: (await executor.query(query, values)).rowCount ?? 0
2004
+ };
2005
+ }
2006
+ const serializedData = serialize(data);
2007
+ const keys = Object.keys(serializedData);
2008
+ validateColumnNames(keys);
2009
+ const values = Object.values(serializedData);
2010
+ const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2011
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
2012
+ return {
2013
+ operation: "insert",
2014
+ affected: (await executor.query(query, values)).rowCount ?? 0
2015
+ };
2016
+ };
2017
+ /**
2018
+ * Retrieves a single record matching the where criteria
2019
+ *
2020
+ * @param table - Table name
2021
+ * @param where - Criteria to match records
2022
+ * @returns Promise resolving to query result
2023
+ */
2024
+ const get = async (table, where) => {
2025
+ validateTableName(table);
2026
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2027
+ if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
2028
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2029
+ try {
2030
+ return (await executor.query(query, values)).rows[0] || null;
2031
+ } catch (e) {
2032
+ throw new DatabaseError("Failed to retrieve record from table", {
2033
+ table,
2034
+ sql: query,
2035
+ values,
2036
+ originalError: formatDbError(e)
2037
+ });
2038
+ }
2039
+ };
2040
+ /**
2041
+ * Retrieves multiple records matching the where criteria
2042
+ *
2043
+ * @param table - Table name
2044
+ * @param where - Criteria to match records
2045
+ * @returns Promise resolving to array of records
2046
+ */
2047
+ const list = async (table, where) => {
2048
+ validateTableName(table);
2049
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2050
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2051
+ try {
2052
+ return (await executor.query(query, values)).rows;
2053
+ } catch (e) {
2054
+ throw new DatabaseError("Failed to list records from table", {
2055
+ table,
2056
+ sql: query,
2057
+ values,
2058
+ originalError: formatDbError(e)
2059
+ });
2060
+ }
2061
+ };
2062
+ /**
2063
+ * Updates records matching the where criteria
2064
+ *
2065
+ * @param table - Table name
2066
+ * @param where - Criteria to match records to update
2067
+ * @param data - New data to set
2068
+ * @returns Promise resolving to operation result
2069
+ */
2070
+ const update = async (table, where, data) => {
2071
+ validateTableName(table);
2072
+ const serializedData = serialize(data);
2073
+ const keys = Object.keys(serializedData);
2074
+ validateColumnNames(keys);
2075
+ const values = Object.values(serializedData);
2076
+ const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2077
+ const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
2078
+ if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
2079
+ const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
2080
+ try {
2081
+ return {
2082
+ operation: "update",
2083
+ affected: (await executor.query(sql, [...values, ...whereValues])).rowCount ?? 0
2084
+ };
2085
+ } catch (e) {
2086
+ throw new DatabaseError("Failed to update records in table", {
2087
+ table,
2088
+ sql,
2089
+ values: [...values, ...whereValues],
2090
+ originalError: formatDbError(e)
2091
+ });
2092
+ }
2093
+ };
2094
+ /**
2095
+ * Inserts a record or updates it if it already exists (UPSERT)
2096
+ *
2097
+ * @param table - Table name
2098
+ * @param conflictColumns - Columns that define the uniqueness constraint
2099
+ * @param data - Data to insert or update
2100
+ * @returns Promise resolving to operation result
2101
+ * @throws Error if the upsert operation fails
2102
+ */
2103
+ const upsert = async (table, conflictColumns, data, options) => {
2104
+ validateTableName(table);
2105
+ const serializedData = serializeRecord(data);
2106
+ const conflictCols = [...conflictColumns];
2107
+ validateColumnNames([...conflictCols, ...Object.keys(serializedData)]);
2108
+ try {
2109
+ return await executePostgresUpsert(executor, table, conflictCols, serializedData, options, !inTransaction);
2110
+ } catch (e) {
2111
+ if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
2112
+ throw new DatabaseError("Failed to upsert record into table", {
2113
+ table,
2114
+ values: data,
2115
+ conflictColumns,
2116
+ originalError: formatDbError(e)
2117
+ });
2118
+ }
2119
+ };
2120
+ /**
2121
+ * Gets a record matching the where criteria or inserts it if not found
2122
+ *
2123
+ * @param table - Table name
2124
+ * @param where - Criteria to match existing record
2125
+ * @returns Promise resolving to the query result or insert result
2126
+ */
2127
+ const getOrInsert = async (table, where, data) => {
2128
+ validateTableName(table);
2129
+ const result = await get(table, where);
2130
+ if (result) return result;
2131
+ await insert(table, data);
2132
+ const inserted = await get(table, where);
2133
+ if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
2134
+ table,
2135
+ where,
2136
+ data
2137
+ });
2138
+ return inserted;
2139
+ };
2140
+ /**
2141
+ * Deletes records from a table matching the where criteria
2142
+ *
2143
+ * @param table - Table name
2144
+ * @param where - Criteria to match records for deletion
2145
+ * @returns Promise resolving to operation result with count of deleted rows
2146
+ * @throws Error if the delete operation fails
2147
+ */
2148
+ const deleteRecords = async (table, where) => {
2149
+ validateTableName(table);
2150
+ if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
2151
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2152
+ try {
2153
+ return {
2154
+ operation: "delete",
2155
+ affected: (await executor.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
2156
+ };
2157
+ } catch (e) {
2158
+ throw new DatabaseError("Failed to delete records from table", {
2159
+ table,
2160
+ where,
2161
+ originalError: formatDbError(e)
2162
+ });
2163
+ }
2164
+ };
2165
+ /**
2166
+ * Counts records in a table matching the where criteria
2167
+ *
2168
+ * @param table - Table name
2169
+ * @param where - Criteria to match records (optional, counts all if omitted)
2170
+ * @returns Promise resolving to count of matching records
2171
+ * @throws Error if the count operation fails
2172
+ */
2173
+ const count = async (table, where) => {
2174
+ validateTableName(table);
2175
+ try {
2176
+ if (!where || Object.keys(where).length === 0) {
2177
+ const result = await executor.query(`SELECT COUNT(*) as count FROM ${table}`);
2178
+ return Number(result.rows[0]?.count) || 0;
2179
+ }
2180
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2181
+ const result = await executor.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
2182
+ return Number(result.rows[0]?.count) || 0;
2183
+ } catch (e) {
2184
+ throw new DatabaseError("Failed to count records in table", {
2185
+ table,
2186
+ where,
2187
+ originalError: formatDbError(e)
2188
+ });
2189
+ }
2190
+ };
2191
+ /**
2192
+ * Creates a table-specific interface for simplified table operations
2193
+ *
2194
+ * @param tableName - Table name
2195
+ * @returns TableMethods interface for the specified table
2196
+ */
2197
+ const table = (tableName) => {
2198
+ return {
2199
+ insert: (data) => insert(tableName, data),
2200
+ get: (data) => get(tableName, data),
2201
+ list: (data) => list(tableName, data)
2202
+ };
2203
+ };
2204
+ /**
2205
+ * Executes a SQL query using template literals and returns a single value
2206
+ *
2207
+ * @param strings - Template strings
2208
+ * @param vars - Variables to interpolate into the query
2209
+ * @returns Promise resolving to a single value (first column of first row)
2210
+ */
2211
+ const pluck = async (strings, ...vars) => {
2212
+ const { sql, values } = parseTemplate(strings, ...vars);
2213
+ try {
2214
+ const firstRow = (await executor.query(sql, values)).rows[0];
2215
+ if (!firstRow) return null;
2216
+ return Object.values(firstRow)[0];
2217
+ } catch (e) {
2218
+ throw new DatabaseError("Failed to execute pluck query", {
2219
+ sql,
2220
+ values,
2221
+ originalError: formatDbError(e)
2222
+ });
2223
+ }
2224
+ };
2225
+ /**
2226
+ * Executes a SQL query using template literals and returns a single row
2227
+ *
2228
+ * @param strings - Template strings
2229
+ * @param vars - Variables to interpolate into the query
2230
+ * @returns Promise resolving to a single result record or null
2231
+ */
2232
+ const single = async (strings, ...vars) => {
2233
+ const { sql, values } = parseTemplate(strings, ...vars);
2234
+ try {
2235
+ return (await executor.query(sql, values)).rows[0] || null;
2236
+ } catch (e) {
2237
+ throw new DatabaseError("Failed to execute single query", {
2238
+ sql,
2239
+ values,
2240
+ originalError: formatDbError(e)
2241
+ });
2242
+ }
2243
+ };
2244
+ /**
2245
+ * Executes a SQL query using template literals and returns multiple rows
2246
+ *
2247
+ * @param strings - Template strings
2248
+ * @param vars - Variables to interpolate into the query
2249
+ * @returns Promise resolving to array of result records
2250
+ */
2251
+ const many = async (strings, ...vars) => {
2252
+ const { sql, values } = parseTemplate(strings, ...vars);
2253
+ try {
2254
+ return (await executor.query(sql, values)).rows;
2255
+ } catch (e) {
2256
+ throw new DatabaseError("Failed to execute many query", {
2257
+ sql,
2258
+ values,
2259
+ originalError: formatDbError(e)
2260
+ });
2261
+ }
2262
+ };
2263
+ /**
2264
+ * Executes a SQL query using template literals without returning results
2265
+ *
2266
+ * @param strings - Template strings
2267
+ * @param vars - Variables to interpolate into the query
2268
+ * @returns Promise that resolves when the query completes
2269
+ */
2270
+ const execute = async (strings, ...vars) => {
2271
+ const { sql, values } = parseTemplate(strings, ...vars);
2272
+ try {
2273
+ await executor.query(sql, values);
2274
+ } catch (e) {
2275
+ throw new DatabaseError("Failed to execute query", {
2276
+ sql,
2277
+ values,
2278
+ originalError: formatDbError(e)
2279
+ });
2280
+ }
2281
+ };
2282
+ /**
2283
+ * Executes a raw SQL query with parameterized values
2284
+ *
2285
+ * Uses PostgreSQL-native placeholders ($1, $2, ...). Legacy ? placeholders
2286
+ * are converted only when the placeholder count matches the supplied values,
2287
+ * so Postgres operators such as JSONB ? remain intact.
2288
+ *
2289
+ * @param sql - SQL query string
2290
+ * @param values - Variables to use as parameters
2291
+ * @returns Promise resolving to query result with rows and count
2292
+ */
2293
+ const query = async (sql, ...values) => {
2294
+ const query = normalizePostgresRawQuery(sql, values);
2295
+ try {
2296
+ const result = await executor.query(query.sql, query.values);
2297
+ return {
2298
+ rows: result.rows,
2299
+ rowCount: result.rowCount ?? 0
2300
+ };
2301
+ } catch (e) {
2302
+ throw wrapDatabaseError("Failed to execute raw query", e, {
2303
+ sql: query.sql,
2304
+ values: query.values
2305
+ });
2306
+ }
2307
+ };
2308
+ /**
2309
+ * Checks if a table exists in the database
2310
+ *
2311
+ * @param tableName - Name of the table to check
2312
+ * @returns Promise resolving to boolean indicating if the table exists
2313
+ */
2314
+ const tableExists = async (tableName) => {
2315
+ return (await executor.query(`SELECT EXISTS (
2316
+ SELECT 1
2317
+ FROM information_schema.tables
2318
+ WHERE table_name = $1
2319
+ AND table_schema = (
2320
+ SELECT namespace.nspname
2321
+ FROM pg_catalog.pg_class AS relation
2322
+ JOIN pg_catalog.pg_namespace AS namespace
2323
+ ON namespace.oid = relation.relnamespace
2324
+ WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident($1))
2325
+ )
2326
+ )`, [tableName])).rows[0].exists;
2327
+ };
2328
+ /**
2329
+ * Synchronizes database schema with provided SQL DDL
2330
+ * Creates tables if they don't exist and adds missing columns
2331
+ * Also executes CREATE INDEX statements
2332
+ *
2333
+ * @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements
2334
+ * @returns Promise that resolves when schema is synchronized
2335
+ */
2336
+ /**
2337
+ * Runs a step whose failure `syncSchema` deliberately tolerates.
2338
+ *
2339
+ * Logging and continuing is self-contained on the pool. Inside a
2340
+ * transaction it is not: a failed statement puts PostgreSQL in the aborted
2341
+ * state, and `COMMIT` on an aborted transaction *succeeds* — it reports a
2342
+ * ROLLBACK command tag and no error. The caller is told the transaction
2343
+ * committed while every write in it was discarded, which is the silent
2344
+ * data loss this whole epic is about, re-entering through the schema path.
2345
+ *
2346
+ * A savepoint scopes the failure to the step, which is what "log it and
2347
+ * continue" was always supposed to mean.
2348
+ */
2349
+ const runTolerated = async (work) => {
2350
+ if (!inTransaction) return work();
2351
+ savepointSequence += 1;
2352
+ const name = `hv_sync_${savepointSequence}`;
2353
+ await executor.query(`SAVEPOINT ${name}`);
2354
+ try {
2355
+ const result = await work();
2356
+ await executor.query(`RELEASE SAVEPOINT ${name}`);
2357
+ return result;
2358
+ } catch (error) {
2359
+ await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
2360
+ clearTransactionError();
2361
+ await executor.query(`RELEASE SAVEPOINT ${name}`);
2362
+ throw error;
2363
+ }
2364
+ };
2365
+ const syncSchema = async (schema) => {
2366
+ const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
2367
+ const createIndexRegex = /CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?"?(?<indexName>\w+)"?\s+ON\s+"?(?<tableName>\w+)"?\s*(?:USING\s+"?\w+"?\s*)?\([^)]+\)/i;
2368
+ const parseCreateIndex = (command) => {
2369
+ const { indexName, tableName } = command.match(createIndexRegex)?.groups ?? {};
2370
+ return indexName && tableName ? {
2371
+ indexName,
2372
+ tableName
2373
+ } : null;
2374
+ };
2375
+ const indexTargets = [];
2376
+ for (const command of commands) {
2377
+ const indexMatch = parseCreateIndex(command.trim());
2378
+ if (indexMatch) indexTargets.push(indexMatch);
2379
+ }
2380
+ const existingIndexes = /* @__PURE__ */ new Set();
2381
+ if (indexTargets.length > 0) {
2382
+ const result = await executor.query(`SELECT target.table_name, target.index_name
2383
+ FROM unnest($1::text[], $2::text[])
2384
+ AS target(table_name, index_name)
2385
+ JOIN pg_catalog.pg_class AS relation
2386
+ ON relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(target.table_name))
2387
+ JOIN pg_catalog.pg_namespace AS namespace
2388
+ ON namespace.oid = relation.relnamespace
2389
+ JOIN pg_catalog.pg_indexes AS existing
2390
+ ON existing.schemaname = namespace.nspname
2391
+ AND existing.tablename = target.table_name
2392
+ AND existing.indexname = target.index_name`, [indexTargets.map((target) => target.tableName), indexTargets.map((target) => target.indexName)]);
2393
+ for (const row of result.rows) existingIndexes.add(`${row.table_name}\u0000${row.index_name}`);
2394
+ }
2395
+ for (const command of commands) {
2396
+ const trimmedCommand = command.trim();
2397
+ const tableMatch = trimmedCommand.match(/CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(([\s\S]+)\)/i);
2398
+ if (tableMatch) {
2399
+ const tableName = tableMatch[2];
2400
+ const columns = tableMatch[3].trim().split(",\n");
2401
+ if (!await tableExists(tableName)) await executor.query(trimmedCommand);
2402
+ else for (const column of columns) {
2403
+ const columnDef = column.trim();
2404
+ if (/^(?:CONSTRAINT\s+(?:"(?:[^"]|"")*"|\w+)\s+)?(?:(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK)\b|EXCLUDE(?=\s+(?:USING\b|\()))/i.test(columnDef)) continue;
2405
+ const columnMatch = columnDef.match(/^("(?:[^"]|"")*"|\w+)\s+\w+[^,]*/);
2406
+ if (columnMatch) {
2407
+ const identifier = columnMatch[1];
2408
+ const columnName = identifier.startsWith("\"") ? identifier.slice(1, -1).replace(/""/g, "\"") : identifier;
2409
+ if (columnName.toUpperCase() === "PRIMARY" || columnName.toUpperCase() === "FOREIGN" || columnName.toUpperCase() === "UNIQUE" || columnName.toUpperCase() === "CHECK" || columnName.toUpperCase() === "CONSTRAINT") continue;
2410
+ try {
2411
+ await runTolerated(async () => {
2412
+ if (!(await executor.query(`SELECT EXISTS (
2413
+ SELECT 1 FROM information_schema.columns
2414
+ WHERE table_name = $1
2415
+ AND column_name = $2
2416
+ AND table_schema = (
2417
+ SELECT namespace.nspname
2418
+ FROM pg_catalog.pg_class AS relation
2419
+ JOIN pg_catalog.pg_namespace AS namespace
2420
+ ON namespace.oid = relation.relnamespace
2421
+ WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident($1))
2422
+ )
2423
+ )`, [tableName, columnName])).rows[0].exists) {
2424
+ const alterCommand = `ALTER TABLE "${tableName}" ADD COLUMN ${columnDef}`;
2425
+ await executor.query(alterCommand);
2426
+ }
2427
+ });
2428
+ } catch (error) {
2429
+ console.error(`Error adding column ${columnName} to ${tableName}:`, error);
2430
+ }
2431
+ }
2432
+ }
2433
+ continue;
2434
+ }
2435
+ const indexMatch = parseCreateIndex(trimmedCommand);
2436
+ if (indexMatch) {
2437
+ const { indexName, tableName: indexTableName } = indexMatch;
2438
+ const indexKey = `${indexTableName}\u0000${indexName}`;
2439
+ if (!existingIndexes.has(indexKey)) try {
2440
+ await runTolerated(() => executor.query(trimmedCommand));
2441
+ existingIndexes.add(indexKey);
2442
+ } catch (error) {
2443
+ console.warn(`Warning: Failed to create index ${indexName} on ${indexTableName}:`, error instanceof Error ? error.message : String(error));
2444
+ }
2445
+ continue;
2446
+ }
2447
+ if (trimmedCommand.toUpperCase().startsWith("CREATE ") && !trimmedCommand.toUpperCase().includes("CREATE TABLE")) try {
2448
+ await runTolerated(() => executor.query(trimmedCommand));
2449
+ } catch (error) {
2450
+ console.warn(`Warning: DDL statement may have failed:`, error instanceof Error ? error.message : String(error));
2451
+ }
2452
+ }
2453
+ };
2454
+ return {
2455
+ insert,
2456
+ get,
2457
+ list,
2458
+ update,
2459
+ upsert,
2460
+ getOrInsert,
2461
+ delete: deleteRecords,
2462
+ count,
2463
+ table,
2464
+ many,
2465
+ single,
2466
+ pluck,
2467
+ execute,
2468
+ query,
2469
+ oo: many,
2470
+ oO: single,
2471
+ ox: pluck,
2472
+ xx: execute,
2473
+ tableExists,
2474
+ syncSchema
2475
+ };
2476
+ };
2477
+ /**
2478
+ * Builds the `transaction` exposed on a transaction-scoped interface.
2479
+ *
2480
+ * Re-entering runs the callback under a SAVEPOINT on the *same* connection,
2481
+ * which is what callers assume nesting already did. Re-exposing the top-level
2482
+ * `transaction` here (the previous behaviour) checked out a second pooled
2483
+ * connection and began an independent transaction, so the nested work could
2484
+ * not see the enclosing transaction's uncommitted rows, and if the enclosing
2485
+ * transaction held a lock the nested one needed, the two deadlocked in a way
2486
+ * PostgreSQL cannot detect: the outer connection is blocked on a promise
2487
+ * rather than on a lock, so `deadlock_timeout` never fires and the process
2488
+ * hangs until some unrelated timeout.
2489
+ *
2490
+ * @param executor - Error-preserving executor bound to the enclosing client
2491
+ * @param scopeFor - Builds the interface handed to the nested callback
2492
+ */
2493
+ const createNestedTransaction = (executor, scopeFor, clearTransactionError, scopes) => {
2494
+ const rootScope = { tail: Promise.resolve() };
2495
+ const runScope = async (scope, callback) => {
2496
+ savepointSequence += 1;
2497
+ const name = `hv_sp_${savepointSequence}`;
2498
+ await executor.query(`SAVEPOINT ${name}`);
2499
+ try {
2500
+ const result = await callback(scopes ? scopeFor() : scopeFor(bind(scope)));
2501
+ await scope.tail;
2502
+ await executor.query(`RELEASE SAVEPOINT ${name}`);
2503
+ return result;
2504
+ } catch (error) {
2505
+ await scope.tail;
2506
+ try {
2507
+ await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
2508
+ clearTransactionError();
2509
+ await executor.query(`RELEASE SAVEPOINT ${name}`);
2510
+ } catch {}
2511
+ throw error;
2512
+ }
2513
+ };
2514
+ const bind = (bound) => {
2515
+ const nested = async (callback) => {
2516
+ const parentScope = bound ?? scopes?.getStore() ?? rootScope;
2517
+ const scope = { tail: Promise.resolve() };
2518
+ const current = parentScope.tail.then(() => scopes ? scopes.run(scope, () => runScope(scope, callback)) : runScope(scope, callback));
2519
+ parentScope.tail = current.then(() => void 0, () => void 0);
2520
+ return current;
2521
+ };
2522
+ nested.drain = () => rootScope.tail;
2523
+ return nested;
2524
+ };
2525
+ return bind();
2526
+ };
2527
+ return {
2528
+ serializeRecord,
2529
+ createClientMethods,
2530
+ createNestedTransaction
2531
+ };
2532
+ }
2533
+ /**
2534
+ * Builds `getTableSchema` on top of a dialect's statement methods.
2535
+ */
2536
+ function createGetTableSchema(methods) {
2537
+ const { many, tableExists } = methods;
2538
+ /**
2539
+ * Retrieves the schema information for a table
2540
+ *
2541
+ * @param table - Table name
2542
+ * @returns Promise resolving to table schema info or null if table doesn't exist
2543
+ * @throws Error if the query fails
2544
+ */
2545
+ const getTableSchema = async (table) => {
2546
+ validateTableName(table);
2547
+ try {
2548
+ if (!await tableExists(table)) return null;
2549
+ const columnRows = await many`
2550
+ SELECT
2551
+ column_name,
2552
+ data_type,
2553
+ is_nullable,
2554
+ column_default
2555
+ FROM information_schema.columns
2556
+ WHERE table_name = ${table}
2557
+ AND table_schema = (
2558
+ SELECT namespace.nspname
2559
+ FROM pg_catalog.pg_class AS relation
2560
+ JOIN pg_catalog.pg_namespace AS namespace
2561
+ ON namespace.oid = relation.relnamespace
2562
+ WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
2563
+ )
2564
+ ORDER BY ordinal_position
2565
+ `;
2566
+ const pkRows = await many`
2567
+ SELECT kcu.column_name
2568
+ FROM information_schema.table_constraints tc
2569
+ JOIN information_schema.key_column_usage kcu
2570
+ ON tc.constraint_name = kcu.constraint_name
2571
+ AND tc.table_schema = kcu.table_schema
2572
+ WHERE tc.table_name = ${table}
2573
+ AND tc.table_schema = (
2574
+ SELECT namespace.nspname
2575
+ FROM pg_catalog.pg_class AS relation
2576
+ JOIN pg_catalog.pg_namespace AS namespace
2577
+ ON namespace.oid = relation.relnamespace
2578
+ WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
2579
+ )
2580
+ AND tc.constraint_type = 'PRIMARY KEY'
2581
+ `;
2582
+ const pkColumns = new Set(pkRows.map((row) => row.column_name));
2583
+ const columns = {};
2584
+ for (const row of columnRows) {
2585
+ const colName = row.column_name;
2586
+ columns[colName] = {
2587
+ type: row.data_type,
2588
+ primaryKey: pkColumns.has(colName),
2589
+ notNull: row.is_nullable === "NO",
2590
+ defaultValue: row.column_default
2591
+ };
2592
+ }
2593
+ const indexRows = await many`
2594
+ SELECT
2595
+ indexname,
2596
+ indexdef
2597
+ FROM pg_indexes
2598
+ WHERE tablename = ${table}
2599
+ AND schemaname = (
2600
+ SELECT namespace.nspname
2601
+ FROM pg_catalog.pg_class AS relation
2602
+ JOIN pg_catalog.pg_namespace AS namespace
2603
+ ON namespace.oid = relation.relnamespace
2604
+ WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
2605
+ )
2606
+ AND indexname NOT LIKE '%_pkey'
2607
+ `;
2608
+ const indexes = [];
2609
+ for (const row of indexRows) {
2610
+ const indexName = row.indexname;
2611
+ const indexDef = row.indexdef;
2612
+ const colMatch = indexDef.match(/\(([^)]+)\)/);
2613
+ const indexColumns = colMatch ? colMatch[1].split(",").map((col) => col.trim()) : [];
2614
+ const isUnique = indexDef.toUpperCase().includes("UNIQUE");
2615
+ indexes.push({
2616
+ name: indexName,
2617
+ columns: indexColumns,
2618
+ unique: isUnique
2619
+ });
2620
+ }
2621
+ const fkRows = await many`
2622
+ SELECT
2623
+ kcu.column_name,
2624
+ ccu.table_name AS foreign_table_name,
2625
+ ccu.column_name AS foreign_column_name,
2626
+ rc.delete_rule,
2627
+ rc.update_rule
2628
+ FROM information_schema.table_constraints AS tc
2629
+ JOIN information_schema.key_column_usage AS kcu
2630
+ ON tc.constraint_name = kcu.constraint_name
2631
+ AND tc.table_schema = kcu.table_schema
2632
+ JOIN information_schema.constraint_column_usage AS ccu
2633
+ ON ccu.constraint_name = tc.constraint_name
2634
+ AND ccu.table_schema = tc.table_schema
2635
+ JOIN information_schema.referential_constraints AS rc
2636
+ ON rc.constraint_name = tc.constraint_name
2637
+ AND rc.constraint_schema = tc.table_schema
2638
+ WHERE tc.constraint_type = 'FOREIGN KEY'
2639
+ AND tc.table_name = ${table}
2640
+ AND tc.table_schema = (
2641
+ SELECT namespace.nspname
2642
+ FROM pg_catalog.pg_class AS relation
2643
+ JOIN pg_catalog.pg_namespace AS namespace
2644
+ ON namespace.oid = relation.relnamespace
2645
+ WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
2646
+ )
2647
+ `;
2648
+ const foreignKeys = [];
2649
+ for (const fkRow of fkRows) foreignKeys.push({
2650
+ column: fkRow.column_name,
2651
+ referencesTable: fkRow.foreign_table_name,
2652
+ referencesColumn: fkRow.foreign_column_name,
2653
+ onDelete: fkRow.delete_rule,
2654
+ onUpdate: fkRow.update_rule
2655
+ });
2656
+ return {
2657
+ tableName: table,
2658
+ columns,
2659
+ indexes,
2660
+ foreignKeys
2661
+ };
2662
+ } catch (e) {
2663
+ throw new DatabaseError("Failed to retrieve table schema", {
2664
+ table,
2665
+ originalError: formatDbError(e)
2666
+ });
2667
+ }
2668
+ };
2669
+ return getTableSchema;
2670
+ }
2671
+ /**
2672
+ * Builds the `alterTable` operations on top of a statement executor.
2673
+ */
2674
+ function createAlterTable(client) {
2675
+ return {
2676
+ /**
2677
+ * Adds a new column to an existing table
2678
+ *
2679
+ * @param table - Table name
2680
+ * @param column - Column definition with name
2681
+ * @returns Promise that resolves when column is added
2682
+ * @throws Error if the alter operation fails
2683
+ */
2684
+ addColumn: async (table, column) => {
2685
+ validateTableName(table);
2686
+ validateColumnName(column.name);
2687
+ let sql;
2688
+ try {
2689
+ sql = generateAddColumnStatement(table, column, "postgres");
2690
+ await client.query(sql);
2691
+ } catch (e) {
2692
+ throw wrapDatabaseError("Failed to add column to table", e, {
2693
+ table,
2694
+ column: column.name,
2695
+ ...sql ? { sql } : {}
2696
+ });
2697
+ }
2698
+ },
2699
+ /**
2700
+ * Adds a new index to an existing table
2701
+ *
2702
+ * @param table - Table name
2703
+ * @param index - Index definition
2704
+ * @returns Promise that resolves when index is created
2705
+ * @throws Error if the create index operation fails
2706
+ */
2707
+ addIndex: async (table, index) => {
2708
+ validateTableName(table);
2709
+ validateIndexName(index.name);
2710
+ for (const col of index.columns) validateColumnName(col);
2711
+ let sql;
2712
+ try {
2713
+ sql = generateCreateIndexStatement(table, index);
2714
+ await client.query(sql);
2715
+ } catch (e) {
2716
+ throw wrapDatabaseError("Failed to create index on table", e, {
2717
+ table,
2718
+ index: index.name,
2719
+ ...sql ? { sql } : {}
2720
+ });
2721
+ }
2722
+ }
2723
+ };
2724
+ }
2725
+ //#endregion
2726
+ export { parseConditionKey as C, wrapDatabaseError as D, unwrapRawKey as E, formatDbError as S, resolveInsertColumns as T, validateColumnNames as _, createVectorCapabilities as a, DatabaseSchemaManager as b, createTransactionLock as c, validateDatabaseCacheOptions as d, escapeQuotedIdentifier as f, validateColumnName as g, generateCreateIndexStatement as h, createTransactionErrorTracker as i, ConnectionCache as l, generateAddColumnStatement as m, createGetTableSchema as n, normalizePostgresRawQuery as o, escapeStringLiteral as p, createPostgresDialect as r, DEFAULT_TRANSACTION_QUEUE_TIMEOUT_MS as s, createAlterTable as t, enableCachedValidation as u, validateIndexName as v, raw as w, buildWhere as x, validateTableName as y };
2727
+
2728
+ //# sourceMappingURL=postgres-dialect-Bfswr4Vs.js.map