@happyvertical/sql 0.102.2 → 0.102.4

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
package/dist/index.js CHANGED
@@ -1,8 +1,7 @@
1
- import { _ as raw, a as escapeStringLiteral, b as wrapDatabaseError, c as quotePostgresColumnName, d as validateIndexName, f as validateTableName$1, g as parseConditionKey, h as formatDbError, i as escapeQuotedIdentifier, l as validateColumnName$1, m as buildWhere, n as enableCachedValidation, o as generateAddColumnStatement, p as DatabaseSchemaManager, r as validateDatabaseCacheOptions, s as generateCreateIndexStatement, t as ConnectionCache, u as validateColumnNames, v as resolveInsertColumns, y as unwrapRawKey } from "./chunks/connection-cache-Da5UcY5u.js";
1
+ import { C as parseConditionKey, D as wrapDatabaseError, E as unwrapRawKey, S as formatDbError, T as resolveInsertColumns, _ as validateColumnNames, a as createVectorCapabilities, b as DatabaseSchemaManager, c as createTransactionLock, d as validateDatabaseCacheOptions, f as escapeQuotedIdentifier, i as createTransactionErrorTracker, l as ConnectionCache, n as createGetTableSchema, o as normalizePostgresRawQuery, p as escapeStringLiteral, r as createPostgresDialect, t as createAlterTable, u as enableCachedValidation, w as raw, x as buildWhere } from "./chunks/postgres-dialect-Bfswr4Vs.js";
2
2
  import { n as throwWithDuckDBCleanup, r as convertUniqueIndexesToInlineConstraints, t as createDuckDBResourceCloser } from "./chunks/duckdb-resources-Cci6Lo6H.js";
3
3
  import { a as dropPostgresDatabase, c as postgresEnvFromUrl, i as databaseNameFromUrl, l as redactDatabaseUrl, n as assertCanImportDatabase, o as dumpPostgresDatabase, r as createPostgresDatabase, s as isLocalDatabaseUrl, t as assertCanExportDatabase, u as restorePostgresDatabase } from "./chunks/postgres-cli-DzZcXSIG.js";
4
4
  import { t as redactDatabaseUrl$1 } from "./chunks/redact-database-url-CfMDvSdy.js";
5
- import { i as createTransactionLock, n as resolveSchemas, t as NestedTransactionError } from "./chunks/types-CwWJ-yju.js";
6
5
  import { DatabaseError, loadEnvConfig } from "@happyvertical/utils";
7
6
  import { mkdir, readdir, stat } from "node:fs/promises";
8
7
  import { basename, extname, join } from "node:path";
@@ -299,6 +298,48 @@ function countValueFromRow(value) {
299
298
  return "0";
300
299
  }
301
300
  //#endregion
301
+ //#region src/shared/types.ts
302
+ /**
303
+ * Error thrown when `transaction()` is called on a transaction-scoped interface
304
+ * belonging to an adapter whose engine has no savepoints.
305
+ *
306
+ * DuckDB — and therefore the JSON adapter built on it — does not implement
307
+ * `SAVEPOINT`, so there is no way to re-enter the current transaction. Adapters
308
+ * that can re-enter (PostgreSQL, SQLite) do so under a savepoint and never
309
+ * throw this.
310
+ *
311
+ * The alternative would be to open a second, independent transaction, which is
312
+ * what these adapters used to do; on a shared connection that silently
313
+ * destroyed the enclosing transaction's uncommitted work.
314
+ *
315
+ * @example Ensuring a helper works on every adapter
316
+ * ```typescript
317
+ * // Take the transaction as a parameter rather than opening a nested one.
318
+ * async function recordClick(db: DatabaseInterface, id: string) {
319
+ * await db.update('links', { id }, { clicks: 1 });
320
+ * }
321
+ * await db.transaction(async (tx) => recordClick(tx, id));
322
+ * ```
323
+ */
324
+ var NestedTransactionError = class extends DatabaseError {
325
+ constructor(message, context) {
326
+ super(message, context);
327
+ this.name = "NestedTransactionError";
328
+ }
329
+ };
330
+ /**
331
+ * Resolves a SchemasOption to its concrete value.
332
+ *
333
+ * Adapters that need schemas (JSON, DuckDB) call this to unwrap the
334
+ * lazy function. Adapters that don't (Postgres, SQLite) simply ignore
335
+ * the `schemas` option without calling this.
336
+ */
337
+ function resolveSchemas(schemas) {
338
+ if (!schemas) return void 0;
339
+ if (typeof schemas === "function") return schemas();
340
+ return schemas;
341
+ }
342
+ //#endregion
302
343
  //#region src/json.ts
303
344
  var json_exports = /* @__PURE__ */ __exportAll({
304
345
  clearConnectionCache: () => clearConnectionCache,
@@ -1960,309 +2001,6 @@ async function derivePostgresConnectionCacheKey(options, effective) {
1960
2001
  return `pg:hmac-sha256:v1:${Buffer.from(signature).toString("hex")}`;
1961
2002
  }
1962
2003
  /**
1963
- * Operator for the given distance metric in pgvector
1964
- */
1965
- function vectorOperator(metric) {
1966
- switch (metric) {
1967
- case "cosine": return "<=>";
1968
- case "l2": return "<->";
1969
- case "ip": return "<#>";
1970
- default: return "<=>";
1971
- }
1972
- }
1973
- function getMaxPostgresParameterIndex(sql) {
1974
- let maxIndex = 0;
1975
- for (const match of sql.matchAll(/\$(\d+)/g)) {
1976
- const index = Number(match[1]);
1977
- if (Number.isSafeInteger(index) && index > maxIndex) maxIndex = index;
1978
- }
1979
- return maxIndex;
1980
- }
1981
- function usesSingleArrayParameter(sql) {
1982
- const identifier = String.raw`(?:"[^"]+"|[a-z_][\w$]*)`;
1983
- const qualifiedIdentifier = String.raw`${identifier}(?:\s*\.\s*${identifier})?`;
1984
- const arrayType = String.raw`${qualifiedIdentifier}(?:\s*\([^)]*\))?\s*\[\]`;
1985
- 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);
1986
- }
1987
- function normalizeRawQueryValues(sql, values) {
1988
- if (values.length !== 1 || !Array.isArray(values[0])) return values;
1989
- const valuesArray = values[0];
1990
- const maxParameterIndex = getMaxPostgresParameterIndex(sql);
1991
- if (maxParameterIndex === 1 && usesSingleArrayParameter(sql)) return values;
1992
- if (maxParameterIndex === 0 || maxParameterIndex === valuesArray.length) return valuesArray;
1993
- return values;
1994
- }
1995
- function rewriteQuestionMarkPlaceholders(sql) {
1996
- let output = "";
1997
- let count = 0;
1998
- let index = 0;
1999
- const copyQuoted = (quote, escaped = false) => {
2000
- output += quote;
2001
- index += 1;
2002
- while (index < sql.length) {
2003
- const char = sql[index];
2004
- output += char;
2005
- index += 1;
2006
- if (escaped && char === "\\" && index < sql.length) {
2007
- output += sql[index];
2008
- index += 1;
2009
- continue;
2010
- }
2011
- if (char === quote) {
2012
- if (sql[index] === quote) {
2013
- output += sql[index];
2014
- index += 1;
2015
- continue;
2016
- }
2017
- break;
2018
- }
2019
- }
2020
- };
2021
- const copyUntil = (end) => {
2022
- const endIndex = sql.indexOf(end, index + end.length);
2023
- if (endIndex === -1) {
2024
- output += sql.slice(index);
2025
- index = sql.length;
2026
- return;
2027
- }
2028
- output += sql.slice(index, endIndex + end.length);
2029
- index = endIndex + end.length;
2030
- };
2031
- while (index < sql.length) {
2032
- const char = sql[index];
2033
- const next = sql[index + 1];
2034
- if ((char === "E" || char === "e") && next === "'") {
2035
- output += char;
2036
- index += 1;
2037
- copyQuoted("'", true);
2038
- continue;
2039
- }
2040
- if (char === "'") {
2041
- copyQuoted("'");
2042
- continue;
2043
- }
2044
- if (char === "\"") {
2045
- copyQuoted("\"");
2046
- continue;
2047
- }
2048
- if (char === "-" && next === "-") {
2049
- const endIndex = sql.indexOf("\n", index + 2);
2050
- if (endIndex === -1) {
2051
- output += sql.slice(index);
2052
- index = sql.length;
2053
- } else {
2054
- output += sql.slice(index, endIndex + 1);
2055
- index = endIndex + 1;
2056
- }
2057
- continue;
2058
- }
2059
- if (char === "/" && next === "*") {
2060
- copyUntil("*/");
2061
- continue;
2062
- }
2063
- if (char === "$") {
2064
- const tag = sql.slice(index).match(/^\$[A-Za-z_][\w$]*\$|^\$\$/);
2065
- if (tag) {
2066
- copyUntil(tag[0]);
2067
- continue;
2068
- }
2069
- }
2070
- if (char === "?" && next !== "|" && next !== "&") {
2071
- count += 1;
2072
- output += `$${count}`;
2073
- index += 1;
2074
- continue;
2075
- }
2076
- output += char;
2077
- index += 1;
2078
- }
2079
- return {
2080
- sql: output,
2081
- count
2082
- };
2083
- }
2084
- function usesSingleQuestionArrayParameter(sql) {
2085
- const identifier = String.raw`(?:"[^"]+"|[a-z_][\w$]*)`;
2086
- const qualifiedIdentifier = String.raw`${identifier}(?:\s*\.\s*${identifier})?`;
2087
- const arrayType = String.raw`${qualifiedIdentifier}(?:\s*\([^)]*\))?\s*\[\]`;
2088
- 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);
2089
- }
2090
- function normalizeQuestionMarkQueryValues(sql, values, placeholderCount) {
2091
- if (values.length !== 1 || !Array.isArray(values[0])) return values;
2092
- if (placeholderCount === 1 && usesSingleQuestionArrayParameter(sql)) return values;
2093
- if (placeholderCount === values[0].length) return values[0];
2094
- return values;
2095
- }
2096
- function normalizePostgresRawQuery(sql, values) {
2097
- if (getMaxPostgresParameterIndex(sql) > 0) return {
2098
- sql,
2099
- values: normalizeRawQueryValues(sql, values)
2100
- };
2101
- const rewritten = rewriteQuestionMarkPlaceholders(sql);
2102
- const placeholderCount = rewritten.count;
2103
- const queryValues = normalizeQuestionMarkQueryValues(sql, values, placeholderCount);
2104
- if (placeholderCount > 0 && placeholderCount === queryValues.length) return {
2105
- sql: rewritten.sql,
2106
- values: queryValues
2107
- };
2108
- return {
2109
- sql,
2110
- values: normalizeRawQueryValues(sql, values)
2111
- };
2112
- }
2113
- /**
2114
- * pgvector index operator class for CREATE INDEX
2115
- */
2116
- function vectorOpsClass(metric) {
2117
- switch (metric) {
2118
- case "cosine": return "vector_cosine_ops";
2119
- case "l2": return "vector_l2_ops";
2120
- case "ip": return "vector_ip_ops";
2121
- default: return "vector_cosine_ops";
2122
- }
2123
- }
2124
- /**
2125
- * Convert a number[] to a pgvector literal string: '[0.1,0.2,0.3]'
2126
- */
2127
- function toVectorLiteral(embedding) {
2128
- if (embedding.length === 0) throw new DatabaseError("Vector embedding must not be empty", {});
2129
- for (let i = 0; i < embedding.length; i++) if (!Number.isFinite(embedding[i])) throw new DatabaseError("Vector embedding contains invalid value (NaN or Infinity)", {
2130
- index: i,
2131
- value: embedding[i]
2132
- });
2133
- return `[${embedding.join(",")}]`;
2134
- }
2135
- /**
2136
- * Creates vector capabilities backed by pgvector for a PostgreSQL pool.
2137
- *
2138
- * @param pool - PostgreSQL connection pool
2139
- * @returns VectorCapabilities implementation
2140
- */
2141
- /**
2142
- * `type` is interpolated into `USING <type>`, which cannot be parameterized and
2143
- * is not an identifier the quoting helpers cover. `VectorIndexOptions.type`
2144
- * constrains it at compile time only, so anything reaching this at runtime —
2145
- * a JavaScript caller, or a `filter as VectorIndexOptions` on parsed JSON — has
2146
- * to be checked against the values pgvector actually offers.
2147
- */
2148
- var VECTOR_INDEX_TYPES = /* @__PURE__ */ new Set(["hnsw", "ivfflat"]);
2149
- /**
2150
- * `metric` needs the same treatment as `type`, for a less obvious reason.
2151
- *
2152
- * It never reaches SQL directly — `vectorOpsClass`/`vectorOperator` map it
2153
- * through a closed switch with a safe default, so an unknown metric produces a
2154
- * valid operator class. But it is also concatenated into the *generated index
2155
- * name*, which is then interpolated into a quoted identifier, so a metric
2156
- * carrying a `"` still ends that identifier. The safe-default mapping is what
2157
- * makes this easy to miss: the part of the metric that looks dangerous is
2158
- * handled, and the part that is actually dangerous is somewhere else.
2159
- */
2160
- var VECTOR_METRICS = /* @__PURE__ */ new Set([
2161
- "cosine",
2162
- "l2",
2163
- "ip"
2164
- ]);
2165
- function createVectorCapabilities(pool) {
2166
- return {
2167
- async ensureColumn(table, column, dimensions) {
2168
- if (!Number.isInteger(dimensions) || dimensions <= 0) throw new DatabaseError("Vector dimensions must be a positive integer", { dimensions });
2169
- validateTableName$1(table);
2170
- validateColumnName$1(column);
2171
- try {
2172
- await pool.query("CREATE EXTENSION IF NOT EXISTS vector");
2173
- await pool.query(`ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "${column}" vector(${dimensions})`);
2174
- } catch (e) {
2175
- throw new DatabaseError("Failed to ensure vector column", {
2176
- table,
2177
- column,
2178
- dimensions,
2179
- originalError: formatDbError(e)
2180
- });
2181
- }
2182
- },
2183
- async ensureIndex(table, column, options) {
2184
- validateTableName$1(table);
2185
- validateColumnName$1(column);
2186
- const metric = options?.metric || "cosine";
2187
- const indexType = options?.type || "hnsw";
2188
- if (!VECTOR_INDEX_TYPES.has(indexType)) throw new DatabaseError("Unsupported vector index type", {
2189
- table,
2190
- column,
2191
- type: indexType,
2192
- supported: [...VECTOR_INDEX_TYPES]
2193
- });
2194
- if (!VECTOR_METRICS.has(metric)) throw new DatabaseError("Unsupported vector metric", {
2195
- table,
2196
- column,
2197
- metric,
2198
- supported: [...VECTOR_METRICS]
2199
- });
2200
- const opsClass = vectorOpsClass(metric);
2201
- const indexName = `idx_${table}_${column}_${metric}_${indexType}`;
2202
- try {
2203
- await pool.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" USING ${indexType} ("${column}" ${opsClass})`);
2204
- } catch (e) {
2205
- throw new DatabaseError("Failed to ensure vector index", {
2206
- table,
2207
- column,
2208
- indexName,
2209
- originalError: formatDbError(e)
2210
- });
2211
- }
2212
- },
2213
- async upsertVector(table, where, column, embedding) {
2214
- validateTableName$1(table);
2215
- validateColumnName$1(column);
2216
- const keys = Object.keys(where);
2217
- validateColumnNames(keys);
2218
- const values = Object.values(where);
2219
- const conditions = keys.map((key, i) => `"${key}" IS NOT DISTINCT FROM $${i + 1}`).join(" AND ");
2220
- const vectorParam = `$${values.length + 1}`;
2221
- try {
2222
- await pool.query(`UPDATE "${table}" SET "${column}" = ${vectorParam}::vector WHERE ${conditions}`, [...values, toVectorLiteral(embedding)]);
2223
- } catch (e) {
2224
- throw new DatabaseError("Failed to upsert vector", {
2225
- table,
2226
- column,
2227
- where,
2228
- originalError: formatDbError(e)
2229
- });
2230
- }
2231
- },
2232
- async search(table, column, embedding, options) {
2233
- validateTableName$1(table);
2234
- validateColumnName$1(column);
2235
- const metric = options?.metric || "cosine";
2236
- const limit = options?.limit || 10;
2237
- const op = vectorOperator(metric);
2238
- const queryParams = [toVectorLiteral(embedding)];
2239
- let whereClause = `"${column}" IS NOT NULL`;
2240
- if (options?.where) {
2241
- whereClause += ` AND (${options.where})`;
2242
- if (options.params) queryParams.push(...options.params);
2243
- }
2244
- const limitParam = `$${queryParams.length + 1}`;
2245
- queryParams.push(limit);
2246
- const sql = `SELECT *, ("${column}" ${op} $1::vector) AS distance FROM "${table}" WHERE ${whereClause} ORDER BY "${column}" ${op} $1::vector LIMIT ${limitParam}`;
2247
- try {
2248
- return (await pool.query(sql, queryParams)).rows.map((row) => ({
2249
- ...row,
2250
- id: row.id,
2251
- distance: Number.parseFloat(row.distance)
2252
- }));
2253
- } catch (e) {
2254
- throw new DatabaseError("Failed to execute vector search", {
2255
- table,
2256
- column,
2257
- metric,
2258
- limit,
2259
- originalError: formatDbError(e)
2260
- });
2261
- }
2262
- }
2263
- };
2264
- }
2265
- /**
2266
2004
  * Creates a PostgreSQL database adapter
2267
2005
  *
2268
2006
  * Loads configuration from environment variables with backward compatibility:
@@ -2329,110 +2067,6 @@ async function createDatabase(config) {
2329
2067
  };
2330
2068
  const client = pool;
2331
2069
  /**
2332
- * Serializes a value for database storage
2333
- * Converts objects and arrays to JSON strings
2334
- * Converts Dates to ISO strings
2335
- * Passes through primitives unchanged
2336
- */
2337
- const serializeValue = (value) => {
2338
- if (value === null || value === void 0) return value;
2339
- if (value instanceof Date) return value.toISOString();
2340
- if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) return value;
2341
- if (typeof value === "object") return JSON.stringify(value);
2342
- return value;
2343
- };
2344
- /**
2345
- * Serializes all values in an object for database storage
2346
- */
2347
- const serializeRecord = (record) => {
2348
- const serialized = {};
2349
- for (const [key, value] of Object.entries(record)) {
2350
- if (value === void 0) continue;
2351
- serialized[key] = serializeValue(value);
2352
- }
2353
- return serialized;
2354
- };
2355
- let serverVersionNumPromise;
2356
- const nullsNotDistinctIndexCache = /* @__PURE__ */ new Map();
2357
- const hasNullConflictValue = (conflictColumns, data) => conflictColumns.some((col) => data[col] === null);
2358
- const validateUpsertConflictColumns = (table, conflictColumns, serializedData) => {
2359
- const missingColumns = conflictColumns.filter((col) => !(col in serializedData));
2360
- if (missingColumns.length > 0) throw new DatabaseError("Conflict columns missing from data", {
2361
- table,
2362
- conflictColumns,
2363
- missingColumns,
2364
- availableColumns: Object.keys(serializedData),
2365
- 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."
2366
- });
2367
- };
2368
- const getServerVersionNum = async () => {
2369
- serverVersionNumPromise ??= client.query("SHOW server_version_num").then((result) => Number(result.rows[0]?.server_version_num) || 0);
2370
- return serverVersionNumPromise;
2371
- };
2372
- const hasNativeNullsNotDistinctIndex = async (table, conflictColumns) => {
2373
- if (await getServerVersionNum() < 15e4) return false;
2374
- const cacheKey = `${table}:${conflictColumns.join(",")}`;
2375
- let cached = nullsNotDistinctIndexCache.get(cacheKey);
2376
- if (!cached) {
2377
- cached = client.query(`
2378
- SELECT 1
2379
- FROM pg_index i
2380
- WHERE i.indrelid = to_regclass($1)
2381
- AND i.indisunique
2382
- AND i.indnullsnotdistinct
2383
- AND ARRAY(
2384
- SELECT a.attname::text
2385
- FROM unnest(i.indkey) WITH ORDINALITY AS cols(attnum, ord)
2386
- JOIN pg_attribute a
2387
- ON a.attrelid = i.indrelid
2388
- AND a.attnum = cols.attnum
2389
- ORDER BY cols.ord
2390
- ) = $2::text[]
2391
- LIMIT 1
2392
- `, [table, conflictColumns]).then((result) => result.rows.length > 0).catch(() => false);
2393
- nullsNotDistinctIndexCache.set(cacheKey, cached);
2394
- }
2395
- return cached;
2396
- };
2397
- const buildPostgresUpsertParts = (conflictColumns, serializedData) => {
2398
- const keys = Object.keys(serializedData);
2399
- const columns = keys.map(quotePostgresColumnName);
2400
- return {
2401
- columns,
2402
- values: Object.values(serializedData),
2403
- placeholders: keys.map((_, i) => `$${i + 1}`).join(", "),
2404
- updateSet: columns.map((column, i) => `${column} = $${i + 1}`).join(", "),
2405
- conflict: conflictColumns.map(quotePostgresColumnName).join(", ")
2406
- };
2407
- };
2408
- const executeStandardPostgresUpsert = async (executor, table, conflictColumns, serializedData) => {
2409
- const { columns, values, placeholders, updateSet, conflict } = buildPostgresUpsertParts(conflictColumns, serializedData);
2410
- const sql = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflict}) DO UPDATE SET ${updateSet}`;
2411
- return {
2412
- operation: "upsert",
2413
- affected: (await executor.query(sql, values)).rowCount ?? 0
2414
- };
2415
- };
2416
- const executeNullAwarePostgresUpsert = async (executor, table, conflictColumns, serializedData) => {
2417
- const { columns, values, placeholders, updateSet } = buildPostgresUpsertParts(conflictColumns, serializedData);
2418
- const lockKey = conflictColumns.map((col) => `${col}:${JSON.stringify(serializedData[col])}`).join("|");
2419
- await executor.query("SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))", [`@happyvertical/sql.upsert:${table}`, lockKey]);
2420
- const whereStart = values.length + 1;
2421
- const whereClause = conflictColumns.map((col, i) => `${quotePostgresColumnName(col)} IS NOT DISTINCT FROM $${whereStart + i}`).join(" AND ");
2422
- const whereValues = conflictColumns.map((col) => serializedData[col]);
2423
- const updateSql = `UPDATE ${table} SET ${updateSet} WHERE ${whereClause}`;
2424
- const updateResult = await executor.query(updateSql, [...values, ...whereValues]);
2425
- if ((updateResult.rowCount ?? 0) > 0) return {
2426
- operation: "upsert",
2427
- affected: updateResult.rowCount ?? 0
2428
- };
2429
- const insertSql = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders})`;
2430
- return {
2431
- operation: "upsert",
2432
- affected: (await executor.query(insertSql, values)).rowCount ?? 0
2433
- };
2434
- };
2435
- /**
2436
2070
  * Absorbs 'error' events for the clients currently checked out for a
2437
2071
  * transaction, keyed by client so the listener can be removed on release.
2438
2072
  */
@@ -2508,48 +2142,8 @@ async function createDatabase(config) {
2508
2142
  * implicit rollback look successful.
2509
2143
  */
2510
2144
  const createTransactionExecutor = (txClient) => {
2511
- let firstError;
2512
- let firstUnconfirmedError;
2513
- const pending = /* @__PURE__ */ new Set();
2514
- const isRollbackToSavepoint = (query) => {
2515
- const sql = typeof query === "string" ? query : typeof query === "object" && query !== null && "text" in query && typeof query.text === "string" ? query.text : void 0;
2516
- if (sql === void 0) return false;
2517
- let statement = sql.trimStart();
2518
- while (statement.startsWith("--") || statement.startsWith("/*")) {
2519
- if (statement.startsWith("--")) {
2520
- const newline = statement.search(/[\r\n]/);
2521
- if (newline === -1) return false;
2522
- statement = statement.slice(newline + 1).trimStart();
2523
- continue;
2524
- }
2525
- const commentEnd = statement.indexOf("*/", 2);
2526
- if (commentEnd === -1) return false;
2527
- statement = statement.slice(commentEnd + 2).trimStart();
2528
- }
2529
- return /^ROLLBACK\s+TO(?:\s+SAVEPOINT)?\s+/i.test(statement);
2530
- };
2531
- const errorCode = (error) => {
2532
- if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
2533
- };
2534
- const preserveFirstError = (error) => {
2535
- if (errorCode(error) === "25P02" && firstError !== void 0) return firstError;
2536
- if (errorCode(error) === "25P02" && firstUnconfirmedError !== void 0) return firstUnconfirmedError;
2537
- if (error instanceof DatabaseError$1) firstError ??= error;
2538
- else firstUnconfirmedError ??= error;
2539
- return error;
2540
- };
2541
- const track = (operation, query) => {
2542
- const tracked = operation.then((result) => {
2543
- if (isRollbackToSavepoint(query)) firstError = void 0;
2544
- firstUnconfirmedError = void 0;
2545
- return result;
2546
- }).catch((error) => {
2547
- throw preserveFirstError(error);
2548
- });
2549
- pending.add(tracked);
2550
- tracked.finally(() => pending.delete(tracked)).catch(() => {});
2551
- return tracked;
2552
- };
2145
+ const tracker = createTransactionErrorTracker((error) => error instanceof DatabaseError$1);
2146
+ const { pending, preserve: preserveFirstError } = tracker;
2553
2147
  return {
2554
2148
  client: new Proxy(txClient, { get(target, property) {
2555
2149
  if (property === "query") return (...args) => {
@@ -2584,8 +2178,7 @@ async function createDatabase(config) {
2584
2178
  query.handleReadyForQuery = function(...handlerArgs) {
2585
2179
  try {
2586
2180
  const result = Reflect.apply(handleReadyForQuery, this, handlerArgs);
2587
- if (isRollbackToSavepoint(query)) firstError = void 0;
2588
- firstUnconfirmedError = void 0;
2181
+ tracker.confirm(query);
2589
2182
  return result;
2590
2183
  } finally {
2591
2184
  finish();
@@ -2607,10 +2200,7 @@ async function createDatabase(config) {
2607
2200
  pending.add(completion);
2608
2201
  args[callbackIndex] = (error, result) => {
2609
2202
  const callbackError = error === null || error === void 0 ? error : preserveFirstError(error);
2610
- if (callbackError === null || callbackError === void 0) {
2611
- if (isRollbackToSavepoint(query)) firstError = void 0;
2612
- firstUnconfirmedError = void 0;
2613
- }
2203
+ if (callbackError === null || callbackError === void 0) tracker.confirm(query);
2614
2204
  pending.delete(completion);
2615
2205
  complete();
2616
2206
  callback(callbackError, result);
@@ -2624,7 +2214,7 @@ async function createDatabase(config) {
2624
2214
  }
2625
2215
  }
2626
2216
  const result = Reflect.apply(target.query, target, args);
2627
- if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") return track(Promise.resolve(result), query);
2217
+ if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") return tracker.track(Promise.resolve(result), query);
2628
2218
  if (typeof result === "object" && result !== null && "once" in result && typeof result.once === "function") {
2629
2219
  let complete;
2630
2220
  const completion = new Promise((resolve) => {
@@ -2640,8 +2230,7 @@ async function createDatabase(config) {
2640
2230
  finish();
2641
2231
  });
2642
2232
  result.once("end", () => {
2643
- if (isRollbackToSavepoint(query)) firstError = void 0;
2644
- firstUnconfirmedError = void 0;
2233
+ tracker.confirm(query);
2645
2234
  finish();
2646
2235
  });
2647
2236
  result.once("close", finish);
@@ -2650,15 +2239,11 @@ async function createDatabase(config) {
2650
2239
  };
2651
2240
  return Reflect.get(target, property, target);
2652
2241
  } }),
2653
- executor: { query: (sql, values) => track(txClient.query(sql, values), sql) },
2654
- clearError: () => {
2655
- firstError = void 0;
2656
- },
2657
- drain: async () => {
2658
- while (pending.size > 0) await Promise.allSettled([...pending]);
2659
- },
2660
- getError: () => firstError,
2661
- getUnconfirmedError: () => firstUnconfirmedError
2242
+ executor: { query: (sql, values) => tracker.track(txClient.query(sql, values), sql) },
2243
+ clearError: tracker.clearError,
2244
+ drain: tracker.drain,
2245
+ getError: tracker.getError,
2246
+ getUnconfirmedError: tracker.getUnconfirmedError
2662
2247
  };
2663
2248
  };
2664
2249
  const assertCommitted = (result, transactionState) => {
@@ -2667,733 +2252,124 @@ async function createDatabase(config) {
2667
2252
  if (queryError !== void 0) throw queryError;
2668
2253
  throw new DatabaseError("PostgreSQL rolled back the transaction at commit", { command: result.command });
2669
2254
  };
2670
- const executeNullAwarePostgresUpsertInTransaction = async (table, conflictColumns, serializedData) => {
2255
+ const { serializeRecord, createClientMethods, createNestedTransaction } = createPostgresDialect({
2256
+ client,
2257
+ probeExecutor: () => client,
2258
+ runInTransaction: async (work) => {
2259
+ const txClient = await acquireTxClient();
2260
+ try {
2261
+ await txClient.query("BEGIN");
2262
+ const result = await work(txClient);
2263
+ const commitResult = await txClient.query("COMMIT");
2264
+ assertCommitted(commitResult);
2265
+ releaseTxClient(txClient);
2266
+ return result;
2267
+ } catch (error) {
2268
+ const rollbackError = await discardTxClient(txClient);
2269
+ if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
2270
+ throw error;
2271
+ }
2272
+ }
2273
+ });
2274
+ const { insert, get, list, update, upsert, getOrInsert, delete: deleteRecords, count, table, many, single, pluck, execute, query, oo, oO, ox, xx, tableExists, syncSchema } = createClientMethods(client, false, serializeRecord);
2275
+ const getTableSchema = createGetTableSchema({
2276
+ many,
2277
+ tableExists
2278
+ });
2279
+ const alterTable = createAlterTable(client);
2280
+ /**
2281
+ * Acquire a pinned connection from the pool for session-scoped state such as
2282
+ * `pg_advisory_lock`. Every query on the returned handle runs on the same
2283
+ * client until {@link SessionHandle.release} drops the connection (which
2284
+ * frees any session locks it held). The connection is held for the handle's
2285
+ * lifetime, so hold one session per process, not per operation.
2286
+ */
2287
+ const acquireSession = async () => {
2288
+ const sessionClient = await pool.connect();
2289
+ let released = false;
2290
+ let lost = false;
2291
+ sessionClient.on("error", () => {
2292
+ lost = true;
2293
+ });
2294
+ return {
2295
+ query: async (sql, ...values) => {
2296
+ if (released) throw new DatabaseError("Session has been released", {});
2297
+ if (lost) throw new DatabaseError("Session connection was lost", {});
2298
+ const normalized = normalizePostgresRawQuery(sql, values);
2299
+ try {
2300
+ const result = await sessionClient.query(normalized.sql, normalized.values);
2301
+ return {
2302
+ rows: result.rows,
2303
+ rowCount: result.rowCount ?? 0
2304
+ };
2305
+ } catch (e) {
2306
+ throw wrapDatabaseError("Failed to execute session query", e, {
2307
+ sql: normalized.sql,
2308
+ values: normalized.values
2309
+ });
2310
+ }
2311
+ },
2312
+ isActive: () => !released && !lost,
2313
+ release: async () => {
2314
+ if (released) return;
2315
+ released = true;
2316
+ if (!lost) {
2317
+ try {
2318
+ await sessionClient.query("ROLLBACK");
2319
+ } catch {}
2320
+ try {
2321
+ await sessionClient.query("SELECT pg_advisory_unlock_all()");
2322
+ } catch {}
2323
+ }
2324
+ sessionClient.release(true);
2325
+ }
2326
+ };
2327
+ };
2328
+ /**
2329
+ * Executes a callback within a database transaction
2330
+ * Automatically commits on success or rolls back on error
2331
+ *
2332
+ * @param callback - Function to execute within transaction
2333
+ * @returns Promise resolving to callback result
2334
+ */
2335
+ const transaction = async (callback) => {
2671
2336
  const txClient = await acquireTxClient();
2337
+ let nestedTransaction;
2338
+ const transactionState = createTransactionExecutor(txClient);
2672
2339
  try {
2673
2340
  await txClient.query("BEGIN");
2674
- const result = await executeNullAwarePostgresUpsert(txClient, table, conflictColumns, serializedData);
2341
+ let txDb;
2342
+ nestedTransaction = createNestedTransaction(transactionState.executor, () => txDb, transactionState.clearError, new AsyncLocalStorage());
2343
+ txDb = {
2344
+ url,
2345
+ client: transactionState.client,
2346
+ ...createClientMethods(transactionState.executor, true, serializeRecord, transactionState.clearError),
2347
+ transaction: nestedTransaction
2348
+ };
2349
+ const result = await callback(txDb);
2350
+ await nestedTransaction.drain();
2351
+ await transactionState.drain();
2352
+ const statementError = transactionState.getError();
2353
+ if (statementError !== void 0) throw statementError;
2675
2354
  const commitResult = await txClient.query("COMMIT");
2676
- assertCommitted(commitResult);
2355
+ assertCommitted(commitResult, transactionState);
2677
2356
  releaseTxClient(txClient);
2678
2357
  return result;
2679
2358
  } catch (error) {
2359
+ await nestedTransaction?.drain();
2360
+ await transactionState.drain();
2680
2361
  const rollbackError = await discardTxClient(txClient);
2681
2362
  if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
2682
2363
  throw error;
2683
2364
  }
2684
2365
  };
2685
- const executePostgresUpsert = async (executor, table, conflictColumns, serializedData, options, acquireTransaction) => {
2686
- validateUpsertConflictColumns(table, conflictColumns, serializedData);
2687
- if (options?.nullsDistinct || !hasNullConflictValue(conflictColumns, serializedData)) return executeStandardPostgresUpsert(executor, table, conflictColumns, serializedData);
2688
- if (await hasNativeNullsNotDistinctIndex(table, conflictColumns)) return executeStandardPostgresUpsert(executor, table, conflictColumns, serializedData);
2689
- if (!acquireTransaction) return executeNullAwarePostgresUpsert(executor, table, conflictColumns, serializedData);
2690
- return executeNullAwarePostgresUpsertInTransaction(table, conflictColumns, serializedData);
2691
- };
2692
- /**
2693
- * Parses a tagged template literal into a SQL query and values
2694
- *
2695
- * @param strings - Template strings
2696
- * @param vars - Variables to interpolate into the query
2697
- * @returns Object with SQL query and values array
2698
- */
2699
- const parseTemplate = (strings, ...vars) => {
2700
- let sql = strings[0];
2701
- const values = [];
2702
- for (let i = 0; i < vars.length; i++) {
2703
- values.push(vars[i]);
2704
- sql += `$${i + 1}${strings[i + 1]}`;
2705
- }
2706
- return {
2707
- sql,
2708
- values
2709
- };
2710
- };
2711
2366
  /**
2712
- * Builds the complete set of query methods bound to one PostgreSQL client.
2713
- *
2714
- * The pool-backed interface and every transaction-scoped interface are
2715
- * instantiated from this one factory, so there is exactly one implementation
2716
- * of each method. The transaction interfaces used to be hand-maintained
2717
- * copies of these bodies, and both had drifted:
2718
- *
2719
- * - they called the client bare, so a failure inside a transaction threw the
2720
- * raw `pg` error rather than `DatabaseError`, and `instanceof` answered
2721
- * differently depending on whether the call was in a transaction. `pg`
2722
- * exports its own class also named `DatabaseError`, so nothing a developer
2723
- * would check by eye showed the difference (#1115);
2724
- * - they re-exposed `tableExists` and `syncSchema` from the enclosing scope,
2725
- * which close over the *pool*. Both therefore ran on a different connection
2726
- * than the transaction, so `tx.tableExists()` could not see a table created
2727
- * in the same transaction and `tx.syncSchema()` committed its DDL
2728
- * immediately, surviving a rollback (#1111).
2367
+ * Begins a new transaction and returns a handle for manual control
2729
2368
  *
2730
- * Binding a client is the only difference between the two, so it is the only
2731
- * thing this factory parameterizes.
2369
+ * Unlike transaction(), this gives you explicit control over commit/rollback.
2370
+ * Ideal for test isolation where you want to rollback after each test.
2732
2371
  *
2733
- * @param executor - Pool or checked-out client every statement runs on
2734
- * @param inTransaction - Whether `executor` is already inside a transaction
2735
- * @param serialize - Applied to each record before `insert`/`update` builds
2736
- * its SQL
2737
- * @param clearTransactionError - Clears a statement error after a savepoint
2738
- * has successfully restored the transaction to a usable state
2739
- */
2740
- const createClientMethods = (executor, inTransaction, serialize, clearTransactionError = () => {}) => {
2741
- /**
2742
- * Inserts one or more records into a table
2743
- *
2744
- * @param table - Table name
2745
- * @param data - Single record or array of records to insert
2746
- * @returns Promise resolving to operation result
2747
- * @throws Error if the insert operation fails
2748
- *
2749
- * @example Single record insert:
2750
- * ```typescript
2751
- * await db.insert('users', {
2752
- * name: 'John Doe',
2753
- * email: 'john@example.com'
2754
- * });
2755
- * ```
2756
- *
2757
- * @example Multiple record insert:
2758
- * ```typescript
2759
- * await db.insert('users', [
2760
- * { name: 'John', email: 'john@example.com' },
2761
- * { name: 'Jane', email: 'jane@example.com' }
2762
- * ]);
2763
- * ```
2764
- */
2765
- const insert = async (table, data) => {
2766
- validateTableName$1(table);
2767
- if (Array.isArray(data)) {
2768
- if (data.length === 0) return {
2769
- operation: "insert",
2770
- affected: 0
2771
- };
2772
- const serializedRecords = data.map((record) => serialize(record));
2773
- const keys = resolveInsertColumns(table, serializedRecords);
2774
- validateColumnNames(keys);
2775
- const placeholders = serializedRecords.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
2776
- const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
2777
- const values = serializedRecords.flatMap((row) => keys.map((key) => row[key]));
2778
- return {
2779
- operation: "insert",
2780
- affected: (await executor.query(query, values)).rowCount ?? 0
2781
- };
2782
- }
2783
- const serializedData = serialize(data);
2784
- const keys = Object.keys(serializedData);
2785
- validateColumnNames(keys);
2786
- const values = Object.values(serializedData);
2787
- const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2788
- const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
2789
- return {
2790
- operation: "insert",
2791
- affected: (await executor.query(query, values)).rowCount ?? 0
2792
- };
2793
- };
2794
- /**
2795
- * Retrieves a single record matching the where criteria
2796
- *
2797
- * @param table - Table name
2798
- * @param where - Criteria to match records
2799
- * @returns Promise resolving to query result
2800
- */
2801
- const get = async (table, where) => {
2802
- validateTableName$1(table);
2803
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2804
- if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
2805
- const query = `SELECT * FROM ${table} ${whereClause}`;
2806
- try {
2807
- return (await executor.query(query, values)).rows[0] || null;
2808
- } catch (e) {
2809
- throw new DatabaseError("Failed to retrieve record from table", {
2810
- table,
2811
- sql: query,
2812
- values,
2813
- originalError: formatDbError(e)
2814
- });
2815
- }
2816
- };
2817
- /**
2818
- * Retrieves multiple records matching the where criteria
2819
- *
2820
- * @param table - Table name
2821
- * @param where - Criteria to match records
2822
- * @returns Promise resolving to array of records
2823
- */
2824
- const list = async (table, where) => {
2825
- validateTableName$1(table);
2826
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2827
- const query = `SELECT * FROM ${table} ${whereClause}`;
2828
- try {
2829
- return (await executor.query(query, values)).rows;
2830
- } catch (e) {
2831
- throw new DatabaseError("Failed to list records from table", {
2832
- table,
2833
- sql: query,
2834
- values,
2835
- originalError: formatDbError(e)
2836
- });
2837
- }
2838
- };
2839
- /**
2840
- * Updates records matching the where criteria
2841
- *
2842
- * @param table - Table name
2843
- * @param where - Criteria to match records to update
2844
- * @param data - New data to set
2845
- * @returns Promise resolving to operation result
2846
- */
2847
- const update = async (table, where, data) => {
2848
- validateTableName$1(table);
2849
- const serializedData = serialize(data);
2850
- const keys = Object.keys(serializedData);
2851
- validateColumnNames(keys);
2852
- const values = Object.values(serializedData);
2853
- const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2854
- const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
2855
- if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
2856
- const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
2857
- try {
2858
- return {
2859
- operation: "update",
2860
- affected: (await executor.query(sql, [...values, ...whereValues])).rowCount ?? 0
2861
- };
2862
- } catch (e) {
2863
- throw new DatabaseError("Failed to update records in table", {
2864
- table,
2865
- sql,
2866
- values: [...values, ...whereValues],
2867
- originalError: formatDbError(e)
2868
- });
2869
- }
2870
- };
2871
- /**
2872
- * Inserts a record or updates it if it already exists (UPSERT)
2873
- *
2874
- * @param table - Table name
2875
- * @param conflictColumns - Columns that define the uniqueness constraint
2876
- * @param data - Data to insert or update
2877
- * @returns Promise resolving to operation result
2878
- * @throws Error if the upsert operation fails
2879
- */
2880
- const upsert = async (table, conflictColumns, data, options) => {
2881
- validateTableName$1(table);
2882
- const serializedData = serializeRecord(data);
2883
- const conflictCols = [...conflictColumns];
2884
- validateColumnNames([...conflictCols, ...Object.keys(serializedData)]);
2885
- try {
2886
- return await executePostgresUpsert(executor, table, conflictCols, serializedData, options, !inTransaction);
2887
- } catch (e) {
2888
- if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
2889
- throw new DatabaseError("Failed to upsert record into table", {
2890
- table,
2891
- values: data,
2892
- conflictColumns,
2893
- originalError: formatDbError(e)
2894
- });
2895
- }
2896
- };
2897
- /**
2898
- * Gets a record matching the where criteria or inserts it if not found
2899
- *
2900
- * @param table - Table name
2901
- * @param where - Criteria to match existing record
2902
- * @returns Promise resolving to the query result or insert result
2903
- */
2904
- const getOrInsert = async (table, where, data) => {
2905
- validateTableName$1(table);
2906
- const result = await get(table, where);
2907
- if (result) return result;
2908
- await insert(table, data);
2909
- const inserted = await get(table, where);
2910
- if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
2911
- table,
2912
- where,
2913
- data
2914
- });
2915
- return inserted;
2916
- };
2917
- /**
2918
- * Deletes records from a table matching the where criteria
2919
- *
2920
- * @param table - Table name
2921
- * @param where - Criteria to match records for deletion
2922
- * @returns Promise resolving to operation result with count of deleted rows
2923
- * @throws Error if the delete operation fails
2924
- */
2925
- const deleteRecords = async (table, where) => {
2926
- validateTableName$1(table);
2927
- if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
2928
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2929
- try {
2930
- return {
2931
- operation: "delete",
2932
- affected: (await executor.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
2933
- };
2934
- } catch (e) {
2935
- throw new DatabaseError("Failed to delete records from table", {
2936
- table,
2937
- where,
2938
- originalError: formatDbError(e)
2939
- });
2940
- }
2941
- };
2942
- /**
2943
- * Counts records in a table matching the where criteria
2944
- *
2945
- * @param table - Table name
2946
- * @param where - Criteria to match records (optional, counts all if omitted)
2947
- * @returns Promise resolving to count of matching records
2948
- * @throws Error if the count operation fails
2949
- */
2950
- const count = async (table, where) => {
2951
- validateTableName$1(table);
2952
- try {
2953
- if (!where || Object.keys(where).length === 0) {
2954
- const result = await executor.query(`SELECT COUNT(*) as count FROM ${table}`);
2955
- return Number(result.rows[0]?.count) || 0;
2956
- }
2957
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2958
- const result = await executor.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
2959
- return Number(result.rows[0]?.count) || 0;
2960
- } catch (e) {
2961
- throw new DatabaseError("Failed to count records in table", {
2962
- table,
2963
- where,
2964
- originalError: formatDbError(e)
2965
- });
2966
- }
2967
- };
2968
- /**
2969
- * Creates a table-specific interface for simplified table operations
2970
- *
2971
- * @param tableName - Table name
2972
- * @returns TableMethods interface for the specified table
2973
- */
2974
- const table = (tableName) => {
2975
- return {
2976
- insert: (data) => insert(tableName, data),
2977
- get: (data) => get(tableName, data),
2978
- list: (data) => list(tableName, data)
2979
- };
2980
- };
2981
- /**
2982
- * Executes a SQL query using template literals and returns a single value
2983
- *
2984
- * @param strings - Template strings
2985
- * @param vars - Variables to interpolate into the query
2986
- * @returns Promise resolving to a single value (first column of first row)
2987
- */
2988
- const pluck = async (strings, ...vars) => {
2989
- const { sql, values } = parseTemplate(strings, ...vars);
2990
- try {
2991
- const firstRow = (await executor.query(sql, values)).rows[0];
2992
- if (!firstRow) return null;
2993
- return Object.values(firstRow)[0];
2994
- } catch (e) {
2995
- throw new DatabaseError("Failed to execute pluck query", {
2996
- sql,
2997
- values,
2998
- originalError: formatDbError(e)
2999
- });
3000
- }
3001
- };
3002
- /**
3003
- * Executes a SQL query using template literals and returns a single row
3004
- *
3005
- * @param strings - Template strings
3006
- * @param vars - Variables to interpolate into the query
3007
- * @returns Promise resolving to a single result record or null
3008
- */
3009
- const single = async (strings, ...vars) => {
3010
- const { sql, values } = parseTemplate(strings, ...vars);
3011
- try {
3012
- return (await executor.query(sql, values)).rows[0] || null;
3013
- } catch (e) {
3014
- throw new DatabaseError("Failed to execute single query", {
3015
- sql,
3016
- values,
3017
- originalError: formatDbError(e)
3018
- });
3019
- }
3020
- };
3021
- /**
3022
- * Executes a SQL query using template literals and returns multiple rows
3023
- *
3024
- * @param strings - Template strings
3025
- * @param vars - Variables to interpolate into the query
3026
- * @returns Promise resolving to array of result records
3027
- */
3028
- const many = async (strings, ...vars) => {
3029
- const { sql, values } = parseTemplate(strings, ...vars);
3030
- try {
3031
- return (await executor.query(sql, values)).rows;
3032
- } catch (e) {
3033
- throw new DatabaseError("Failed to execute many query", {
3034
- sql,
3035
- values,
3036
- originalError: formatDbError(e)
3037
- });
3038
- }
3039
- };
3040
- /**
3041
- * Executes a SQL query using template literals without returning results
3042
- *
3043
- * @param strings - Template strings
3044
- * @param vars - Variables to interpolate into the query
3045
- * @returns Promise that resolves when the query completes
3046
- */
3047
- const execute = async (strings, ...vars) => {
3048
- const { sql, values } = parseTemplate(strings, ...vars);
3049
- try {
3050
- await executor.query(sql, values);
3051
- } catch (e) {
3052
- throw new DatabaseError("Failed to execute query", {
3053
- sql,
3054
- values,
3055
- originalError: formatDbError(e)
3056
- });
3057
- }
3058
- };
3059
- /**
3060
- * Executes a raw SQL query with parameterized values
3061
- *
3062
- * Uses PostgreSQL-native placeholders ($1, $2, ...). Legacy ? placeholders
3063
- * are converted only when the placeholder count matches the supplied values,
3064
- * so Postgres operators such as JSONB ? remain intact.
3065
- *
3066
- * @param sql - SQL query string
3067
- * @param values - Variables to use as parameters
3068
- * @returns Promise resolving to query result with rows and count
3069
- */
3070
- const query = async (sql, ...values) => {
3071
- const query = normalizePostgresRawQuery(sql, values);
3072
- try {
3073
- const result = await executor.query(query.sql, query.values);
3074
- return {
3075
- rows: result.rows,
3076
- rowCount: result.rowCount ?? 0
3077
- };
3078
- } catch (e) {
3079
- throw wrapDatabaseError("Failed to execute raw query", e, {
3080
- sql: query.sql,
3081
- values: query.values
3082
- });
3083
- }
3084
- };
3085
- /**
3086
- * Checks if a table exists in the database
3087
- *
3088
- * @param tableName - Name of the table to check
3089
- * @returns Promise resolving to boolean indicating if the table exists
3090
- */
3091
- const tableExists = async (tableName) => {
3092
- return (await executor.query(`SELECT EXISTS (
3093
- SELECT 1
3094
- FROM information_schema.tables
3095
- WHERE table_name = $1
3096
- AND table_schema = (
3097
- SELECT namespace.nspname
3098
- FROM pg_catalog.pg_class AS relation
3099
- JOIN pg_catalog.pg_namespace AS namespace
3100
- ON namespace.oid = relation.relnamespace
3101
- WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident($1))
3102
- )
3103
- )`, [tableName])).rows[0].exists;
3104
- };
3105
- /**
3106
- * Synchronizes database schema with provided SQL DDL
3107
- * Creates tables if they don't exist and adds missing columns
3108
- * Also executes CREATE INDEX statements
3109
- *
3110
- * @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements
3111
- * @returns Promise that resolves when schema is synchronized
3112
- */
3113
- /**
3114
- * Runs a step whose failure `syncSchema` deliberately tolerates.
3115
- *
3116
- * Logging and continuing is self-contained on the pool. Inside a
3117
- * transaction it is not: a failed statement puts PostgreSQL in the aborted
3118
- * state, and `COMMIT` on an aborted transaction *succeeds* — it reports a
3119
- * ROLLBACK command tag and no error. The caller is told the transaction
3120
- * committed while every write in it was discarded, which is the silent
3121
- * data loss this whole epic is about, re-entering through the schema path.
3122
- *
3123
- * A savepoint scopes the failure to the step, which is what "log it and
3124
- * continue" was always supposed to mean.
3125
- */
3126
- const runTolerated = async (work) => {
3127
- if (!inTransaction) return work();
3128
- savepointSequence += 1;
3129
- const name = `hv_sync_${savepointSequence}`;
3130
- await executor.query(`SAVEPOINT ${name}`);
3131
- try {
3132
- const result = await work();
3133
- await executor.query(`RELEASE SAVEPOINT ${name}`);
3134
- return result;
3135
- } catch (error) {
3136
- await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
3137
- clearTransactionError();
3138
- await executor.query(`RELEASE SAVEPOINT ${name}`);
3139
- throw error;
3140
- }
3141
- };
3142
- const syncSchema = async (schema) => {
3143
- const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
3144
- 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;
3145
- const parseCreateIndex = (command) => {
3146
- const { indexName, tableName } = command.match(createIndexRegex)?.groups ?? {};
3147
- return indexName && tableName ? {
3148
- indexName,
3149
- tableName
3150
- } : null;
3151
- };
3152
- const indexTargets = [];
3153
- for (const command of commands) {
3154
- const indexMatch = parseCreateIndex(command.trim());
3155
- if (indexMatch) indexTargets.push(indexMatch);
3156
- }
3157
- const existingIndexes = /* @__PURE__ */ new Set();
3158
- if (indexTargets.length > 0) {
3159
- const result = await executor.query(`SELECT target.table_name, target.index_name
3160
- FROM unnest($1::text[], $2::text[])
3161
- AS target(table_name, index_name)
3162
- JOIN pg_catalog.pg_class AS relation
3163
- ON relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(target.table_name))
3164
- JOIN pg_catalog.pg_namespace AS namespace
3165
- ON namespace.oid = relation.relnamespace
3166
- JOIN pg_catalog.pg_indexes AS existing
3167
- ON existing.schemaname = namespace.nspname
3168
- AND existing.tablename = target.table_name
3169
- AND existing.indexname = target.index_name`, [indexTargets.map((target) => target.tableName), indexTargets.map((target) => target.indexName)]);
3170
- for (const row of result.rows) existingIndexes.add(`${row.table_name}\u0000${row.index_name}`);
3171
- }
3172
- for (const command of commands) {
3173
- const trimmedCommand = command.trim();
3174
- const tableMatch = trimmedCommand.match(/CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(([\s\S]+)\)/i);
3175
- if (tableMatch) {
3176
- const tableName = tableMatch[2];
3177
- const columns = tableMatch[3].trim().split(",\n");
3178
- if (!await tableExists(tableName)) await executor.query(trimmedCommand);
3179
- else for (const column of columns) {
3180
- const columnDef = column.trim();
3181
- if (/^(?:CONSTRAINT\s+(?:"(?:[^"]|"")*"|\w+)\s+)?(?:(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK)\b|EXCLUDE(?=\s+(?:USING\b|\()))/i.test(columnDef)) continue;
3182
- const columnMatch = columnDef.match(/^("(?:[^"]|"")*"|\w+)\s+\w+[^,]*/);
3183
- if (columnMatch) {
3184
- const identifier = columnMatch[1];
3185
- const columnName = identifier.startsWith("\"") ? identifier.slice(1, -1).replace(/""/g, "\"") : identifier;
3186
- if (columnName.toUpperCase() === "PRIMARY" || columnName.toUpperCase() === "FOREIGN" || columnName.toUpperCase() === "UNIQUE" || columnName.toUpperCase() === "CHECK" || columnName.toUpperCase() === "CONSTRAINT") continue;
3187
- try {
3188
- await runTolerated(async () => {
3189
- if (!(await executor.query(`SELECT EXISTS (
3190
- SELECT 1 FROM information_schema.columns
3191
- WHERE table_name = $1
3192
- AND column_name = $2
3193
- AND table_schema = (
3194
- SELECT namespace.nspname
3195
- FROM pg_catalog.pg_class AS relation
3196
- JOIN pg_catalog.pg_namespace AS namespace
3197
- ON namespace.oid = relation.relnamespace
3198
- WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident($1))
3199
- )
3200
- )`, [tableName, columnName])).rows[0].exists) {
3201
- const alterCommand = `ALTER TABLE "${tableName}" ADD COLUMN ${columnDef}`;
3202
- await executor.query(alterCommand);
3203
- }
3204
- });
3205
- } catch (error) {
3206
- console.error(`Error adding column ${columnName} to ${tableName}:`, error);
3207
- }
3208
- }
3209
- }
3210
- continue;
3211
- }
3212
- const indexMatch = parseCreateIndex(trimmedCommand);
3213
- if (indexMatch) {
3214
- const { indexName, tableName: indexTableName } = indexMatch;
3215
- const indexKey = `${indexTableName}\u0000${indexName}`;
3216
- if (!existingIndexes.has(indexKey)) try {
3217
- await runTolerated(() => executor.query(trimmedCommand));
3218
- existingIndexes.add(indexKey);
3219
- } catch (error) {
3220
- console.warn(`Warning: Failed to create index ${indexName} on ${indexTableName}:`, error instanceof Error ? error.message : String(error));
3221
- }
3222
- continue;
3223
- }
3224
- if (trimmedCommand.toUpperCase().startsWith("CREATE ") && !trimmedCommand.toUpperCase().includes("CREATE TABLE")) try {
3225
- await runTolerated(() => executor.query(trimmedCommand));
3226
- } catch (error) {
3227
- console.warn(`Warning: DDL statement may have failed:`, error instanceof Error ? error.message : String(error));
3228
- }
3229
- }
3230
- };
3231
- return {
3232
- insert,
3233
- get,
3234
- list,
3235
- update,
3236
- upsert,
3237
- getOrInsert,
3238
- delete: deleteRecords,
3239
- count,
3240
- table,
3241
- many,
3242
- single,
3243
- pluck,
3244
- execute,
3245
- query,
3246
- oo: many,
3247
- oO: single,
3248
- ox: pluck,
3249
- xx: execute,
3250
- tableExists,
3251
- syncSchema
3252
- };
3253
- };
3254
- const { insert, get, list, update, upsert, getOrInsert, delete: deleteRecords, count, table, many, single, pluck, execute, query, oo, oO, ox, xx, tableExists, syncSchema } = createClientMethods(client, false, serializeRecord);
3255
- /**
3256
- * Acquire a pinned connection from the pool for session-scoped state such as
3257
- * `pg_advisory_lock`. Every query on the returned handle runs on the same
3258
- * client until {@link SessionHandle.release} drops the connection (which
3259
- * frees any session locks it held). The connection is held for the handle's
3260
- * lifetime, so hold one session per process, not per operation.
3261
- */
3262
- const acquireSession = async () => {
3263
- const sessionClient = await pool.connect();
3264
- let released = false;
3265
- let lost = false;
3266
- sessionClient.on("error", () => {
3267
- lost = true;
3268
- });
3269
- return {
3270
- query: async (sql, ...values) => {
3271
- if (released) throw new DatabaseError("Session has been released", {});
3272
- if (lost) throw new DatabaseError("Session connection was lost", {});
3273
- const normalized = normalizePostgresRawQuery(sql, values);
3274
- try {
3275
- const result = await sessionClient.query(normalized.sql, normalized.values);
3276
- return {
3277
- rows: result.rows,
3278
- rowCount: result.rowCount ?? 0
3279
- };
3280
- } catch (e) {
3281
- throw wrapDatabaseError("Failed to execute session query", e, {
3282
- sql: normalized.sql,
3283
- values: normalized.values
3284
- });
3285
- }
3286
- },
3287
- isActive: () => !released && !lost,
3288
- release: async () => {
3289
- if (released) return;
3290
- released = true;
3291
- if (!lost) {
3292
- try {
3293
- await sessionClient.query("ROLLBACK");
3294
- } catch {}
3295
- try {
3296
- await sessionClient.query("SELECT pg_advisory_unlock_all()");
3297
- } catch {}
3298
- }
3299
- sessionClient.release(true);
3300
- }
3301
- };
3302
- };
3303
- let savepointSequence = 0;
3304
- /**
3305
- * Builds the `transaction` exposed on a transaction-scoped interface.
3306
- *
3307
- * Re-entering runs the callback under a SAVEPOINT on the *same* connection,
3308
- * which is what callers assume nesting already did. Re-exposing the top-level
3309
- * `transaction` here (the previous behaviour) checked out a second pooled
3310
- * connection and began an independent transaction, so the nested work could
3311
- * not see the enclosing transaction's uncommitted rows, and if the enclosing
3312
- * transaction held a lock the nested one needed, the two deadlocked in a way
3313
- * PostgreSQL cannot detect: the outer connection is blocked on a promise
3314
- * rather than on a lock, so `deadlock_timeout` never fires and the process
3315
- * hangs until some unrelated timeout.
3316
- *
3317
- * @param executor - Error-preserving executor bound to the enclosing client
3318
- * @param scopeFor - Builds the interface handed to the nested callback
3319
- */
3320
- const createNestedTransaction = (executor, scopeFor, clearTransactionError) => {
3321
- const nestedScope = new AsyncLocalStorage();
3322
- const rootScope = { tail: Promise.resolve() };
3323
- const runScope = async (scope, callback) => {
3324
- savepointSequence += 1;
3325
- const name = `hv_sp_${savepointSequence}`;
3326
- await executor.query(`SAVEPOINT ${name}`);
3327
- try {
3328
- const result = await callback(scopeFor());
3329
- await scope.tail;
3330
- await executor.query(`RELEASE SAVEPOINT ${name}`);
3331
- return result;
3332
- } catch (error) {
3333
- await scope.tail;
3334
- try {
3335
- await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
3336
- clearTransactionError();
3337
- await executor.query(`RELEASE SAVEPOINT ${name}`);
3338
- } catch {}
3339
- throw error;
3340
- }
3341
- };
3342
- const transaction = async (callback) => {
3343
- const parentScope = nestedScope.getStore() ?? rootScope;
3344
- const scope = { tail: Promise.resolve() };
3345
- const current = parentScope.tail.then(() => nestedScope.run(scope, () => runScope(scope, callback)));
3346
- parentScope.tail = current.then(() => void 0, () => void 0);
3347
- return current;
3348
- };
3349
- transaction.drain = () => rootScope.tail;
3350
- return transaction;
3351
- };
3352
- /**
3353
- * Executes a callback within a database transaction
3354
- * Automatically commits on success or rolls back on error
3355
- *
3356
- * @param callback - Function to execute within transaction
3357
- * @returns Promise resolving to callback result
3358
- */
3359
- const transaction = async (callback) => {
3360
- const txClient = await acquireTxClient();
3361
- let nestedTransaction;
3362
- const transactionState = createTransactionExecutor(txClient);
3363
- try {
3364
- await txClient.query("BEGIN");
3365
- let txDb;
3366
- nestedTransaction = createNestedTransaction(transactionState.executor, () => txDb, transactionState.clearError);
3367
- txDb = {
3368
- url,
3369
- client: transactionState.client,
3370
- ...createClientMethods(transactionState.executor, true, serializeRecord, transactionState.clearError),
3371
- transaction: nestedTransaction
3372
- };
3373
- const result = await callback(txDb);
3374
- await nestedTransaction.drain();
3375
- await transactionState.drain();
3376
- const statementError = transactionState.getError();
3377
- if (statementError !== void 0) throw statementError;
3378
- const commitResult = await txClient.query("COMMIT");
3379
- assertCommitted(commitResult, transactionState);
3380
- releaseTxClient(txClient);
3381
- return result;
3382
- } catch (error) {
3383
- await nestedTransaction?.drain();
3384
- await transactionState.drain();
3385
- const rollbackError = await discardTxClient(txClient);
3386
- if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
3387
- throw error;
3388
- }
3389
- };
3390
- /**
3391
- * Begins a new transaction and returns a handle for manual control
3392
- *
3393
- * Unlike transaction(), this gives you explicit control over commit/rollback.
3394
- * Ideal for test isolation where you want to rollback after each test.
3395
- *
3396
- * @returns Promise resolving to a TransactionHandle
2372
+ * @returns Promise resolving to a TransactionHandle
3397
2373
  */
3398
2374
  const beginTransaction = async () => {
3399
2375
  const txClient = await acquireTxClient();
@@ -3428,7 +2404,7 @@ async function createDatabase(config) {
3428
2404
  const rollback = () => end("ROLLBACK");
3429
2405
  const isActive = () => active;
3430
2406
  let txHandle;
3431
- nestedTransaction = createNestedTransaction(transactionState.executor, () => txHandle, transactionState.clearError);
2407
+ nestedTransaction = createNestedTransaction(transactionState.executor, () => txHandle, transactionState.clearError, new AsyncLocalStorage());
3432
2408
  txHandle = {
3433
2409
  url,
3434
2410
  client: transactionState.client,
@@ -3476,137 +2452,6 @@ async function createDatabase(config) {
3476
2452
  };
3477
2453
  await schemaManager.initializeSchemas(currentDb, options);
3478
2454
  };
3479
- /**
3480
- * Retrieves the schema information for a table
3481
- *
3482
- * @param table - Table name
3483
- * @returns Promise resolving to table schema info or null if table doesn't exist
3484
- * @throws Error if the query fails
3485
- */
3486
- const getTableSchema = async (table) => {
3487
- validateTableName$1(table);
3488
- try {
3489
- if (!await tableExists(table)) return null;
3490
- const columnRows = await many`
3491
- SELECT
3492
- column_name,
3493
- data_type,
3494
- is_nullable,
3495
- column_default
3496
- FROM information_schema.columns
3497
- WHERE table_name = ${table}
3498
- AND table_schema = (
3499
- SELECT namespace.nspname
3500
- FROM pg_catalog.pg_class AS relation
3501
- JOIN pg_catalog.pg_namespace AS namespace
3502
- ON namespace.oid = relation.relnamespace
3503
- WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
3504
- )
3505
- ORDER BY ordinal_position
3506
- `;
3507
- const pkRows = await many`
3508
- SELECT kcu.column_name
3509
- FROM information_schema.table_constraints tc
3510
- JOIN information_schema.key_column_usage kcu
3511
- ON tc.constraint_name = kcu.constraint_name
3512
- AND tc.table_schema = kcu.table_schema
3513
- WHERE tc.table_name = ${table}
3514
- AND tc.table_schema = (
3515
- SELECT namespace.nspname
3516
- FROM pg_catalog.pg_class AS relation
3517
- JOIN pg_catalog.pg_namespace AS namespace
3518
- ON namespace.oid = relation.relnamespace
3519
- WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
3520
- )
3521
- AND tc.constraint_type = 'PRIMARY KEY'
3522
- `;
3523
- const pkColumns = new Set(pkRows.map((row) => row.column_name));
3524
- const columns = {};
3525
- for (const row of columnRows) {
3526
- const colName = row.column_name;
3527
- columns[colName] = {
3528
- type: row.data_type,
3529
- primaryKey: pkColumns.has(colName),
3530
- notNull: row.is_nullable === "NO",
3531
- defaultValue: row.column_default
3532
- };
3533
- }
3534
- const indexRows = await many`
3535
- SELECT
3536
- indexname,
3537
- indexdef
3538
- FROM pg_indexes
3539
- WHERE tablename = ${table}
3540
- AND schemaname = (
3541
- SELECT namespace.nspname
3542
- FROM pg_catalog.pg_class AS relation
3543
- JOIN pg_catalog.pg_namespace AS namespace
3544
- ON namespace.oid = relation.relnamespace
3545
- WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
3546
- )
3547
- AND indexname NOT LIKE '%_pkey'
3548
- `;
3549
- const indexes = [];
3550
- for (const row of indexRows) {
3551
- const indexName = row.indexname;
3552
- const indexDef = row.indexdef;
3553
- const colMatch = indexDef.match(/\(([^)]+)\)/);
3554
- const indexColumns = colMatch ? colMatch[1].split(",").map((col) => col.trim()) : [];
3555
- const isUnique = indexDef.toUpperCase().includes("UNIQUE");
3556
- indexes.push({
3557
- name: indexName,
3558
- columns: indexColumns,
3559
- unique: isUnique
3560
- });
3561
- }
3562
- const fkRows = await many`
3563
- SELECT
3564
- kcu.column_name,
3565
- ccu.table_name AS foreign_table_name,
3566
- ccu.column_name AS foreign_column_name,
3567
- rc.delete_rule,
3568
- rc.update_rule
3569
- FROM information_schema.table_constraints AS tc
3570
- JOIN information_schema.key_column_usage AS kcu
3571
- ON tc.constraint_name = kcu.constraint_name
3572
- AND tc.table_schema = kcu.table_schema
3573
- JOIN information_schema.constraint_column_usage AS ccu
3574
- ON ccu.constraint_name = tc.constraint_name
3575
- AND ccu.table_schema = tc.table_schema
3576
- JOIN information_schema.referential_constraints AS rc
3577
- ON rc.constraint_name = tc.constraint_name
3578
- AND rc.constraint_schema = tc.table_schema
3579
- WHERE tc.constraint_type = 'FOREIGN KEY'
3580
- AND tc.table_name = ${table}
3581
- AND tc.table_schema = (
3582
- SELECT namespace.nspname
3583
- FROM pg_catalog.pg_class AS relation
3584
- JOIN pg_catalog.pg_namespace AS namespace
3585
- ON namespace.oid = relation.relnamespace
3586
- WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
3587
- )
3588
- `;
3589
- const foreignKeys = [];
3590
- for (const fkRow of fkRows) foreignKeys.push({
3591
- column: fkRow.column_name,
3592
- referencesTable: fkRow.foreign_table_name,
3593
- referencesColumn: fkRow.foreign_column_name,
3594
- onDelete: fkRow.delete_rule,
3595
- onUpdate: fkRow.update_rule
3596
- });
3597
- return {
3598
- tableName: table,
3599
- columns,
3600
- indexes,
3601
- foreignKeys
3602
- };
3603
- } catch (e) {
3604
- throw new DatabaseError("Failed to retrieve table schema", {
3605
- table,
3606
- originalError: formatDbError(e)
3607
- });
3608
- }
3609
- };
3610
2455
  db = {
3611
2456
  url,
3612
2457
  client,
@@ -3635,55 +2480,7 @@ async function createDatabase(config) {
3635
2480
  beginTransaction,
3636
2481
  acquireSession,
3637
2482
  getTableSchema,
3638
- alterTable: {
3639
- /**
3640
- * Adds a new column to an existing table
3641
- *
3642
- * @param table - Table name
3643
- * @param column - Column definition with name
3644
- * @returns Promise that resolves when column is added
3645
- * @throws Error if the alter operation fails
3646
- */
3647
- addColumn: async (table, column) => {
3648
- validateTableName$1(table);
3649
- validateColumnName$1(column.name);
3650
- let sql;
3651
- try {
3652
- sql = generateAddColumnStatement(table, column, "postgres");
3653
- await client.query(sql);
3654
- } catch (e) {
3655
- throw wrapDatabaseError("Failed to add column to table", e, {
3656
- table,
3657
- column: column.name,
3658
- ...sql ? { sql } : {}
3659
- });
3660
- }
3661
- },
3662
- /**
3663
- * Adds a new index to an existing table
3664
- *
3665
- * @param table - Table name
3666
- * @param index - Index definition
3667
- * @returns Promise that resolves when index is created
3668
- * @throws Error if the create index operation fails
3669
- */
3670
- addIndex: async (table, index) => {
3671
- validateTableName$1(table);
3672
- validateIndexName(index.name);
3673
- for (const col of index.columns) validateColumnName$1(col);
3674
- let sql;
3675
- try {
3676
- sql = generateCreateIndexStatement(table, index);
3677
- await client.query(sql);
3678
- } catch (e) {
3679
- throw wrapDatabaseError("Failed to create index on table", e, {
3680
- table,
3681
- index: index.name,
3682
- ...sql ? { sql } : {}
3683
- });
3684
- }
3685
- }
3686
- },
2483
+ alterTable,
3687
2484
  vector: createVectorCapabilities(pool),
3688
2485
  close: async () => {
3689
2486
  await pool.end();
@@ -3706,7 +2503,7 @@ function isDatabaseInstance(value) {
3706
2503
  * Creates a database connection based on the provided options, or returns an existing database instance
3707
2504
  *
3708
2505
  * Loads configuration from environment variables using the HAVE_SQL_* pattern:
3709
- * - HAVE_SQL_TYPE → type ('sqlite' | 'postgres' | 'duckdb' | 'json')
2506
+ * - HAVE_SQL_TYPE → type ('sqlite' | 'postgres' | 'pglite' | 'duckdb' | 'json')
3710
2507
  * - HAVE_SQL_URL → url (connection string)
3711
2508
  * - HAVE_SQL_HOST → host (database server hostname)
3712
2509
  * - HAVE_SQL_PORT → port (database server port number)
@@ -3752,8 +2549,9 @@ async function getDatabase(options = {}) {
3752
2549
  }));
3753
2550
  if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) options.type = "sqlite";
3754
2551
  if (options.type === "postgres") return (await Promise.resolve().then(() => postgres_exports)).getDatabase(options);
3755
- if (options.type === "sqlite") return (await import("./chunks/sqlite-Cz2NMiDs.js")).getDatabase(options);
3756
- if (options.type === "duckdb") return (await import("./chunks/duckdb-DfikVgov.js")).getDatabase(options);
2552
+ if (options.type === "pglite") return (await import("./pglite.js")).getDatabase(options);
2553
+ if (options.type === "sqlite") return (await import("./chunks/sqlite-0PMv1va2.js")).getDatabase(options);
2554
+ if (options.type === "duckdb") return (await import("./chunks/duckdb-LMyfKZGi.js")).getDatabase(options);
3757
2555
  if (options.type === "json") return (await Promise.resolve().then(() => json_exports)).getDatabase(options);
3758
2556
  throw new Error("Invalid database type");
3759
2557
  }