@happyvertical/sql 0.80.6 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +41 -0
  2. package/dist/chunks/{schema-manager-a8RlxIE2.js → alter-utils-D7NGXZi0.js} +190 -2
  3. package/dist/chunks/alter-utils-D7NGXZi0.js.map +1 -0
  4. package/dist/chunks/{duckdb-D_Wu36JL.js → duckdb-pKOTggPW.js} +44 -20
  5. package/dist/chunks/duckdb-pKOTggPW.js.map +1 -0
  6. package/dist/chunks/{sqlite-B4ussRTS.js → sqlite-dbhMszEJ.js} +57 -23
  7. package/dist/chunks/sqlite-dbhMszEJ.js.map +1 -0
  8. package/dist/chunks/{sqlite-native-BgiF4301.js → sqlite-native-DrNaIJRO.js} +68 -20
  9. package/dist/chunks/sqlite-native-DrNaIJRO.js.map +1 -0
  10. package/dist/chunks/types-uVFHgaSU.js +133 -0
  11. package/dist/chunks/types-uVFHgaSU.js.map +1 -0
  12. package/dist/duckdb.d.ts.map +1 -1
  13. package/dist/index.js +638 -747
  14. package/dist/index.js.map +1 -1
  15. package/dist/json.d.ts.map +1 -1
  16. package/dist/postgres.d.ts.map +1 -1
  17. package/dist/shared/alter-utils.d.ts +48 -0
  18. package/dist/shared/alter-utils.d.ts.map +1 -1
  19. package/dist/shared/transaction-lock.d.ts +54 -0
  20. package/dist/shared/transaction-lock.d.ts.map +1 -0
  21. package/dist/shared/types.d.ts +69 -7
  22. package/dist/shared/types.d.ts.map +1 -1
  23. package/dist/sqlite-native.d.ts.map +1 -1
  24. package/dist/sqlite.d.ts +20 -0
  25. package/dist/sqlite.d.ts.map +1 -1
  26. package/package.json +3 -3
  27. package/dist/chunks/alter-utils-K7tiuj5L.js +0 -135
  28. package/dist/chunks/alter-utils-K7tiuj5L.js.map +0 -1
  29. package/dist/chunks/duckdb-D_Wu36JL.js.map +0 -1
  30. package/dist/chunks/schema-manager-a8RlxIE2.js.map +0 -1
  31. package/dist/chunks/sqlite-B4ussRTS.js.map +0 -1
  32. package/dist/chunks/sqlite-native-BgiF4301.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { i as parseConditionKey, n as buildWhere, r as formatDbError, t as DatabaseSchemaManager } from "./chunks/schema-manager-a8RlxIE2.js";
1
+ import { a as validateColumnName$1, c as validateTableName$1, d as formatDbError, f as parseConditionKey, i as generateCreateIndexStatement, l as DatabaseSchemaManager, n as escapeStringLiteral, o as validateColumnNames, r as generateAddColumnStatement, s as validateIndexName, t as escapeQuotedIdentifier, u as buildWhere } from "./chunks/alter-utils-D7NGXZi0.js";
2
2
  import { t as convertUniqueIndexesToInlineConstraints } from "./chunks/duckdb-schema-utils-B-XBP8QF.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
- import { a as validateTableName$1, i as validateIndexName, n as generateCreateIndexStatement, r as validateColumnName$1, t as generateAddColumnStatement } from "./chunks/alter-utils-K7tiuj5L.js";
4
+ import { n as resolveSchemas, r as createTransactionLock, t as NestedTransactionError } from "./chunks/types-uVFHgaSU.js";
5
5
  import { DatabaseError, loadEnvConfig } from "@happyvertical/utils";
6
6
  import { mkdir, readdir } from "node:fs/promises";
7
7
  import { basename, extname, join } from "node:path";
@@ -308,48 +308,6 @@ function countValueFromRow(value) {
308
308
  return "0";
309
309
  }
310
310
  //#endregion
311
- //#region src/shared/types.ts
312
- /**
313
- * Error thrown when `transaction()` is called on a transaction-scoped interface
314
- * belonging to an adapter whose engine has no savepoints.
315
- *
316
- * DuckDB — and therefore the JSON adapter built on it — does not implement
317
- * `SAVEPOINT`, so there is no way to re-enter the current transaction. Adapters
318
- * that can re-enter (PostgreSQL, SQLite) do so under a savepoint and never
319
- * throw this.
320
- *
321
- * The alternative would be to open a second, independent transaction, which is
322
- * what these adapters used to do; on a shared connection that silently
323
- * destroyed the enclosing transaction's uncommitted work.
324
- *
325
- * @example Ensuring a helper works on every adapter
326
- * ```typescript
327
- * // Take the transaction as a parameter rather than opening a nested one.
328
- * async function recordClick(db: DatabaseInterface, id: string) {
329
- * await db.update('links', { id }, { clicks: 1 });
330
- * }
331
- * await db.transaction(async (tx) => recordClick(tx, id));
332
- * ```
333
- */
334
- var NestedTransactionError = class extends DatabaseError {
335
- constructor(message, context) {
336
- super(message, context);
337
- this.name = "NestedTransactionError";
338
- }
339
- };
340
- /**
341
- * Resolves a SchemasOption to its concrete value.
342
- *
343
- * Adapters that need schemas (JSON, DuckDB) call this to unwrap the
344
- * lazy function. Adapters that don't (Postgres, SQLite) simply ignore
345
- * the `schemas` option without calling this.
346
- */
347
- function resolveSchemas(schemas) {
348
- if (!schemas) return void 0;
349
- if (typeof schemas === "function") return schemas();
350
- return schemas;
351
- }
352
- //#endregion
353
311
  //#region src/json.ts
354
312
  var json_exports = /* @__PURE__ */ __exportAll({
355
313
  clearConnectionCache: () => clearConnectionCache,
@@ -503,7 +461,7 @@ async function loadJSONTables(connection, dataDir, providedSchemas = {}, pending
503
461
  }
504
462
  else {
505
463
  if (eagerLoadTables) try {
506
- await connection.run(`CREATE TABLE IF NOT EXISTS "${tableName}" AS SELECT * FROM read_json_auto('${filePath}')`);
464
+ await connection.run(`CREATE TABLE IF NOT EXISTS "${escapeQuotedIdentifier(tableName)}" AS SELECT * FROM read_json_auto('${escapeStringLiteral(filePath)}')`);
507
465
  inferredSchemaTables.add(tableName);
508
466
  console.log(`[json-adapter] Eagerly created ${tableName} with inferred schema (no UNIQUE constraints)`);
509
467
  } catch (error) {
@@ -708,6 +666,7 @@ async function getDatabase$2(options) {
708
666
  const { connection, pendingJSONFiles, inferredSchemaTables } = await createJSONConnection(options);
709
667
  const writeStrategy = options.writeStrategy || "immediate";
710
668
  const { url } = options;
669
+ const connectionLock = createTransactionLock("json", options.transactionQueueTimeout);
711
670
  /**
712
671
  * Inserts one or more records into a table
713
672
  *
@@ -728,6 +687,7 @@ async function getDatabase$2(options) {
728
687
  affected: 0
729
688
  };
730
689
  const keys = Object.keys(records[0]);
690
+ validateColumnNames(keys);
731
691
  const values = [];
732
692
  let paramIdx = 1;
733
693
  const placeholders = records.map((record) => {
@@ -833,6 +793,7 @@ async function getDatabase$2(options) {
833
793
  writeStrategy
834
794
  });
835
795
  const keys = Object.keys(data);
796
+ validateColumnNames(keys);
836
797
  const setClause = keys.map((key, idx) => `${key} = $${idx + 1}`).join(", ");
837
798
  const { sql: whereClause, values: whereValues } = buildWhere(where, keys.length + 1, "json");
838
799
  const dataValues = Object.values(data).map((value) => value instanceof Date ? value.toISOString() : value);
@@ -882,7 +843,8 @@ async function getDatabase$2(options) {
882
843
  const values = [];
883
844
  const paramIdx = { value: 1 };
884
845
  const assignments = keys.map((key) => {
885
- return `"${key}" = ${buildJSONValueExpression(data[key], values, paramIdx)}`;
846
+ const valueExpr = buildJSONValueExpression(data[key], values, paramIdx);
847
+ return `"${escapeQuotedIdentifier(key)}" = ${valueExpr}`;
886
848
  });
887
849
  const { sql: whereClause, values: whereValues } = buildWhere(conflictWhere, paramIdx.value, "json");
888
850
  const sql = `UPDATE "${table}" SET ${assignments.join(", ")} ${whereClause}`;
@@ -981,11 +943,12 @@ async function getDatabase$2(options) {
981
943
  paramIdx++;
982
944
  }
983
945
  }
984
- const conflict = conflictColumns.map((col) => `"${col}"`).join(", ");
985
- const quotedKeys = keys.map((key) => `"${key}"`).join(", ");
946
+ const conflict = conflictColumns.map((col) => `"${escapeQuotedIdentifier(col)}"`).join(", ");
947
+ const quotedKeys = keys.map((key) => `"${escapeQuotedIdentifier(key)}"`).join(", ");
986
948
  const quotedUpdateSetParts = keys.map((key, i) => {
987
949
  const part = updateSetParts[i];
988
- return `"${key}" = ${part.substring(part.indexOf("=") + 1).trim()}`;
950
+ const valueExpr = part.substring(part.indexOf("=") + 1).trim();
951
+ return `"${escapeQuotedIdentifier(key)}" = ${valueExpr}`;
989
952
  });
990
953
  const sql = `INSERT INTO "${table}" (${quotedKeys}) VALUES (${placeholders.join(", ")}) ON CONFLICT(${conflict}) DO UPDATE SET ${quotedUpdateSetParts.join(", ")}`;
991
954
  try {
@@ -1101,6 +1064,7 @@ async function getDatabase$2(options) {
1101
1064
  * @returns Promise resolving to boolean indicating if the table exists
1102
1065
  */
1103
1066
  const tableExists = async (tableName) => {
1067
+ validateTableName(tableName);
1104
1068
  try {
1105
1069
  await connection.runAndReadAll(`SELECT * FROM ${tableName} LIMIT 1`);
1106
1070
  return true;
@@ -1124,6 +1088,7 @@ async function getDatabase$2(options) {
1124
1088
  * @param dataDir - Directory to write files
1125
1089
  */
1126
1090
  const exportTableToJSON = async (connection, table, dataDir) => {
1091
+ validateTableName(table);
1127
1092
  const filePath = join(dataDir, `${table}.json`);
1128
1093
  const schemaPath = join(dataDir, `${table}.schema.sql`);
1129
1094
  try {
@@ -1199,7 +1164,7 @@ async function getDatabase$2(options) {
1199
1164
  const { constants } = await import("node:fs");
1200
1165
  await access(filePath, constants.F_OK);
1201
1166
  console.log(`[json-adapter] Inferring schema for ${tableName} from JSON file`);
1202
- await connection.run(`CREATE TABLE "${tableName}" AS SELECT * FROM read_json_auto('${filePath}')`);
1167
+ await connection.run(`CREATE TABLE "${tableName}" AS SELECT * FROM read_json_auto('${escapeStringLiteral(filePath)}')`);
1203
1168
  pendingJSONFiles.delete(tableName);
1204
1169
  console.log(`[json-adapter] Created ${tableName} with inferred schema (no UNIQUE constraints)`);
1205
1170
  } catch (error) {
@@ -1497,7 +1462,7 @@ async function getDatabase$2(options) {
1497
1462
  * @param callback - Function to execute within transaction
1498
1463
  * @returns Promise resolving to callback result
1499
1464
  */
1500
- const transaction = async (callback) => {
1465
+ const transaction = async (callback) => connectionLock.run(async () => {
1501
1466
  try {
1502
1467
  await connection.run("BEGIN TRANSACTION");
1503
1468
  const result = await callback(createTransactionScope());
@@ -1509,7 +1474,7 @@ async function getDatabase$2(options) {
1509
1474
  } catch {}
1510
1475
  throw error;
1511
1476
  }
1512
- };
1477
+ });
1513
1478
  /**
1514
1479
  * Builds the transaction-scoped interface handed to a transaction callback.
1515
1480
  *
@@ -1558,18 +1523,30 @@ async function getDatabase$2(options) {
1558
1523
  * @returns Promise resolving to a TransactionHandle
1559
1524
  */
1560
1525
  const beginTransaction = async () => {
1561
- await connection.run("BEGIN TRANSACTION");
1526
+ const releaseConnection = await connectionLock.acquire();
1527
+ try {
1528
+ await connection.run("BEGIN TRANSACTION");
1529
+ } catch (error) {
1530
+ releaseConnection();
1531
+ throw error;
1532
+ }
1562
1533
  let active = true;
1563
- const commit = async () => {
1564
- if (!active) throw new DatabaseError("Transaction already ended", {});
1565
- await connection.run("COMMIT");
1566
- active = false;
1567
- };
1568
- const rollback = async () => {
1534
+ const end = async (command) => {
1569
1535
  if (!active) throw new DatabaseError("Transaction already ended", {});
1570
- await connection.run("ROLLBACK");
1571
- active = false;
1536
+ try {
1537
+ await connection.run(command);
1538
+ } catch (error) {
1539
+ try {
1540
+ await connection.run("ROLLBACK");
1541
+ } catch {}
1542
+ throw error;
1543
+ } finally {
1544
+ active = false;
1545
+ releaseConnection();
1546
+ }
1572
1547
  };
1548
+ const commit = () => end("COMMIT");
1549
+ const rollback = () => end("ROLLBACK");
1573
1550
  const isActive = () => active;
1574
1551
  return {
1575
1552
  ...createTransactionScope(),
@@ -1818,10 +1795,36 @@ function toVectorLiteral(embedding) {
1818
1795
  * @param pool - PostgreSQL connection pool
1819
1796
  * @returns VectorCapabilities implementation
1820
1797
  */
1798
+ /**
1799
+ * `type` is interpolated into `USING <type>`, which cannot be parameterized and
1800
+ * is not an identifier the quoting helpers cover. `VectorIndexOptions.type`
1801
+ * constrains it at compile time only, so anything reaching this at runtime —
1802
+ * a JavaScript caller, or a `filter as VectorIndexOptions` on parsed JSON — has
1803
+ * to be checked against the values pgvector actually offers.
1804
+ */
1805
+ var VECTOR_INDEX_TYPES = /* @__PURE__ */ new Set(["hnsw", "ivfflat"]);
1806
+ /**
1807
+ * `metric` needs the same treatment as `type`, for a less obvious reason.
1808
+ *
1809
+ * It never reaches SQL directly — `vectorOpsClass`/`vectorOperator` map it
1810
+ * through a closed switch with a safe default, so an unknown metric produces a
1811
+ * valid operator class. But it is also concatenated into the *generated index
1812
+ * name*, which is then interpolated into a quoted identifier, so a metric
1813
+ * carrying a `"` still ends that identifier. The safe-default mapping is what
1814
+ * makes this easy to miss: the part of the metric that looks dangerous is
1815
+ * handled, and the part that is actually dangerous is somewhere else.
1816
+ */
1817
+ var VECTOR_METRICS = /* @__PURE__ */ new Set([
1818
+ "cosine",
1819
+ "l2",
1820
+ "ip"
1821
+ ]);
1821
1822
  function createVectorCapabilities(pool) {
1822
1823
  return {
1823
1824
  async ensureColumn(table, column, dimensions) {
1824
1825
  if (!Number.isInteger(dimensions) || dimensions <= 0) throw new DatabaseError("Vector dimensions must be a positive integer", { dimensions });
1826
+ validateTableName$1(table);
1827
+ validateColumnName$1(column);
1825
1828
  try {
1826
1829
  await pool.query("CREATE EXTENSION IF NOT EXISTS vector");
1827
1830
  await pool.query(`ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "${column}" vector(${dimensions})`);
@@ -1835,8 +1838,22 @@ function createVectorCapabilities(pool) {
1835
1838
  }
1836
1839
  },
1837
1840
  async ensureIndex(table, column, options) {
1841
+ validateTableName$1(table);
1842
+ validateColumnName$1(column);
1838
1843
  const metric = options?.metric || "cosine";
1839
1844
  const indexType = options?.type || "hnsw";
1845
+ if (!VECTOR_INDEX_TYPES.has(indexType)) throw new DatabaseError("Unsupported vector index type", {
1846
+ table,
1847
+ column,
1848
+ type: indexType,
1849
+ supported: [...VECTOR_INDEX_TYPES]
1850
+ });
1851
+ if (!VECTOR_METRICS.has(metric)) throw new DatabaseError("Unsupported vector metric", {
1852
+ table,
1853
+ column,
1854
+ metric,
1855
+ supported: [...VECTOR_METRICS]
1856
+ });
1840
1857
  const opsClass = vectorOpsClass(metric);
1841
1858
  const indexName = `idx_${table}_${column}_${metric}_${indexType}`;
1842
1859
  try {
@@ -1851,7 +1868,10 @@ function createVectorCapabilities(pool) {
1851
1868
  }
1852
1869
  },
1853
1870
  async upsertVector(table, where, column, embedding) {
1871
+ validateTableName$1(table);
1872
+ validateColumnName$1(column);
1854
1873
  const keys = Object.keys(where);
1874
+ validateColumnNames(keys);
1855
1875
  const values = Object.values(where);
1856
1876
  const conditions = keys.map((key, i) => `"${key}" IS NOT DISTINCT FROM $${i + 1}`).join(" AND ");
1857
1877
  const vectorParam = `$${values.length + 1}`;
@@ -1867,6 +1887,8 @@ function createVectorCapabilities(pool) {
1867
1887
  }
1868
1888
  },
1869
1889
  async search(table, column, embedding, options) {
1890
+ validateTableName$1(table);
1891
+ validateColumnName$1(column);
1870
1892
  const metric = options?.metric || "cosine";
1871
1893
  const limit = options?.limit || 10;
1872
1894
  const op = vectorOperator(metric);
@@ -2014,6 +2036,15 @@ async function createDatabase(options, cacheKey) {
2014
2036
  /**
2015
2037
  * Serializes all values in an object for database storage
2016
2038
  */
2039
+ /**
2040
+ * The serializer the transaction-scoped interfaces have always used: none.
2041
+ *
2042
+ * Identity, deliberately — not even the `undefined`-dropping the pool-backed
2043
+ * `serializeRecord` does. Those copies bound `undefined` straight through to
2044
+ * `pg`, which stores NULL, whereas dropping the key omits the column and lets
2045
+ * its default apply. Preserving that is the point of this parameter.
2046
+ */
2047
+ const passThroughRecord = (record) => record;
2017
2048
  const serializeRecord = (record) => {
2018
2049
  const serialized = {};
2019
2050
  for (const [key, value] of Object.entries(record)) {
@@ -2190,230 +2221,6 @@ async function createDatabase(options, cacheKey) {
2190
2221
  return executeNullAwarePostgresUpsertInTransaction(table, conflictColumns, serializedData);
2191
2222
  };
2192
2223
  /**
2193
- * Inserts one or more records into a table
2194
- *
2195
- * @param table - Table name
2196
- * @param data - Single record or array of records to insert
2197
- * @returns Promise resolving to operation result
2198
- * @throws Error if the insert operation fails
2199
- *
2200
- * @example Single record insert:
2201
- * ```typescript
2202
- * await db.insert('users', {
2203
- * name: 'John Doe',
2204
- * email: 'john@example.com'
2205
- * });
2206
- * ```
2207
- *
2208
- * @example Multiple record insert:
2209
- * ```typescript
2210
- * await db.insert('users', [
2211
- * { name: 'John', email: 'john@example.com' },
2212
- * { name: 'Jane', email: 'jane@example.com' }
2213
- * ]);
2214
- * ```
2215
- */
2216
- const insert = async (table, data) => {
2217
- if (Array.isArray(data)) {
2218
- const serializedRecords = data.map((record) => serializeRecord(record));
2219
- const keys = Object.keys(serializedRecords[0]);
2220
- const placeholders = serializedRecords.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
2221
- const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
2222
- const values = serializedRecords.reduce((acc, row) => acc.concat(Object.values(row)), []);
2223
- return {
2224
- operation: "insert",
2225
- affected: (await client.query(query, values)).rowCount ?? 0
2226
- };
2227
- }
2228
- const serializedData = serializeRecord(data);
2229
- const keys = Object.keys(serializedData);
2230
- const values = Object.values(serializedData);
2231
- const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2232
- const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
2233
- return {
2234
- operation: "insert",
2235
- affected: (await client.query(query, values)).rowCount ?? 0
2236
- };
2237
- };
2238
- /**
2239
- * Retrieves a single record matching the where criteria
2240
- *
2241
- * @param table - Table name
2242
- * @param where - Criteria to match records
2243
- * @returns Promise resolving to query result
2244
- */
2245
- const get = async (table, where) => {
2246
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2247
- if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
2248
- const query = `SELECT * FROM ${table} ${whereClause}`;
2249
- try {
2250
- return (await client.query(query, values)).rows[0] || null;
2251
- } catch (e) {
2252
- throw new DatabaseError("Failed to retrieve record from table", {
2253
- table,
2254
- sql: query,
2255
- values,
2256
- originalError: formatDbError(e)
2257
- });
2258
- }
2259
- };
2260
- /**
2261
- * Retrieves multiple records matching the where criteria
2262
- *
2263
- * @param table - Table name
2264
- * @param where - Criteria to match records
2265
- * @returns Promise resolving to array of records
2266
- */
2267
- const list = async (table, where) => {
2268
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2269
- const query = `SELECT * FROM ${table} ${whereClause}`;
2270
- try {
2271
- return (await client.query(query, values)).rows;
2272
- } catch (e) {
2273
- throw new DatabaseError("Failed to list records from table", {
2274
- table,
2275
- sql: query,
2276
- values,
2277
- originalError: formatDbError(e)
2278
- });
2279
- }
2280
- };
2281
- /**
2282
- * Updates records matching the where criteria
2283
- *
2284
- * @param table - Table name
2285
- * @param where - Criteria to match records to update
2286
- * @param data - New data to set
2287
- * @returns Promise resolving to operation result
2288
- */
2289
- const update = async (table, where, data) => {
2290
- const serializedData = serializeRecord(data);
2291
- const keys = Object.keys(serializedData);
2292
- const values = Object.values(serializedData);
2293
- const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2294
- const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
2295
- if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
2296
- const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
2297
- try {
2298
- return {
2299
- operation: "update",
2300
- affected: (await client.query(sql, [...values, ...whereValues])).rowCount ?? 0
2301
- };
2302
- } catch (e) {
2303
- throw new DatabaseError("Failed to update records in table", {
2304
- table,
2305
- sql,
2306
- values: [...values, ...whereValues],
2307
- originalError: formatDbError(e)
2308
- });
2309
- }
2310
- };
2311
- /**
2312
- * Inserts a record or updates it if it already exists (UPSERT)
2313
- *
2314
- * @param table - Table name
2315
- * @param conflictColumns - Columns that define the uniqueness constraint
2316
- * @param data - Data to insert or update
2317
- * @returns Promise resolving to operation result
2318
- * @throws Error if the upsert operation fails
2319
- */
2320
- const upsert = async (table, conflictColumns, data, options) => {
2321
- try {
2322
- return await executePostgresUpsert(client, table, conflictColumns, data, options, true);
2323
- } catch (e) {
2324
- if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
2325
- throw new DatabaseError("Failed to upsert record into table", {
2326
- table,
2327
- values: data,
2328
- conflictColumns,
2329
- originalError: formatDbError(e)
2330
- });
2331
- }
2332
- };
2333
- /**
2334
- * Gets a record matching the where criteria or inserts it if not found
2335
- *
2336
- * @param table - Table name
2337
- * @param where - Criteria to match existing record
2338
- * @returns Promise resolving to the query result or insert result
2339
- */
2340
- const getOrInsert = async (table, where, data) => {
2341
- const result = await get(table, where);
2342
- if (result) return result;
2343
- await insert(table, data);
2344
- const inserted = await get(table, where);
2345
- if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
2346
- table,
2347
- where,
2348
- data
2349
- });
2350
- return inserted;
2351
- };
2352
- /**
2353
- * Deletes records from a table matching the where criteria
2354
- *
2355
- * @param table - Table name
2356
- * @param where - Criteria to match records for deletion
2357
- * @returns Promise resolving to operation result with count of deleted rows
2358
- * @throws Error if the delete operation fails
2359
- */
2360
- const deleteRecords = async (table, where) => {
2361
- validateTableName$1(table);
2362
- if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
2363
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2364
- try {
2365
- return {
2366
- operation: "delete",
2367
- affected: (await client.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
2368
- };
2369
- } catch (e) {
2370
- throw new DatabaseError("Failed to delete records from table", {
2371
- table,
2372
- where,
2373
- originalError: formatDbError(e)
2374
- });
2375
- }
2376
- };
2377
- /**
2378
- * Counts records in a table matching the where criteria
2379
- *
2380
- * @param table - Table name
2381
- * @param where - Criteria to match records (optional, counts all if omitted)
2382
- * @returns Promise resolving to count of matching records
2383
- * @throws Error if the count operation fails
2384
- */
2385
- const count = async (table, where) => {
2386
- validateTableName$1(table);
2387
- try {
2388
- if (!where || Object.keys(where).length === 0) {
2389
- const result = await client.query(`SELECT COUNT(*) as count FROM ${table}`);
2390
- return Number(result.rows[0]?.count) || 0;
2391
- }
2392
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2393
- const result = await client.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
2394
- return Number(result.rows[0]?.count) || 0;
2395
- } catch (e) {
2396
- throw new DatabaseError("Failed to count records in table", {
2397
- table,
2398
- where,
2399
- originalError: formatDbError(e)
2400
- });
2401
- }
2402
- };
2403
- /**
2404
- * Creates a table-specific interface for simplified table operations
2405
- *
2406
- * @param tableName - Table name
2407
- * @returns TableMethods interface for the specified table
2408
- */
2409
- const table = (tableName) => {
2410
- return {
2411
- insert: (data) => insert(tableName, data),
2412
- get: (data) => get(tableName, data),
2413
- list: (data) => list(tableName, data)
2414
- };
2415
- };
2416
- /**
2417
2224
  * Parses a tagged template literal into a SQL query and values
2418
2225
  *
2419
2226
  * @param strings - Template strings
@@ -2433,139 +2240,557 @@ async function createDatabase(options, cacheKey) {
2433
2240
  };
2434
2241
  };
2435
2242
  /**
2436
- * Executes a SQL query using template literals and returns a single value
2243
+ * Builds the complete set of query methods bound to one PostgreSQL client.
2437
2244
  *
2438
- * @param strings - Template strings
2439
- * @param vars - Variables to interpolate into the query
2440
- * @returns Promise resolving to a single value (first column of first row)
2441
- */
2442
- const pluck = async (strings, ...vars) => {
2443
- const { sql, values } = parseTemplate(strings, ...vars);
2444
- try {
2445
- const firstRow = (await client.query(sql, values)).rows[0];
2446
- if (!firstRow) return null;
2447
- return Object.values(firstRow)[0];
2448
- } catch (e) {
2449
- throw new DatabaseError("Failed to execute pluck query", {
2450
- sql,
2451
- values,
2452
- originalError: formatDbError(e)
2453
- });
2454
- }
2455
- };
2456
- /**
2457
- * Executes a SQL query using template literals and returns a single row
2245
+ * The pool-backed interface and every transaction-scoped interface are
2246
+ * instantiated from this one factory, so there is exactly one implementation
2247
+ * of each method. The transaction interfaces used to be hand-maintained
2248
+ * copies of these bodies, and both had drifted:
2458
2249
  *
2459
- * @param strings - Template strings
2460
- * @param vars - Variables to interpolate into the query
2461
- * @returns Promise resolving to a single result record or null
2462
- */
2463
- const single = async (strings, ...vars) => {
2464
- const { sql, values } = parseTemplate(strings, ...vars);
2465
- try {
2466
- return (await client.query(sql, values)).rows[0] || null;
2467
- } catch (e) {
2468
- throw new DatabaseError("Failed to execute single query", {
2469
- sql,
2470
- values,
2471
- originalError: formatDbError(e)
2472
- });
2473
- }
2474
- };
2475
- /**
2476
- * Executes a SQL query using template literals and returns multiple rows
2250
+ * - they called the client bare, so a failure inside a transaction threw the
2251
+ * raw `pg` error rather than `DatabaseError`, and `instanceof` answered
2252
+ * differently depending on whether the call was in a transaction. `pg`
2253
+ * exports its own class also named `DatabaseError`, so nothing a developer
2254
+ * would check by eye showed the difference (#1115);
2255
+ * - they re-exposed `tableExists` and `syncSchema` from the enclosing scope,
2256
+ * which close over the *pool*. Both therefore ran on a different connection
2257
+ * than the transaction, so `tx.tableExists()` could not see a table created
2258
+ * in the same transaction and `tx.syncSchema()` committed its DDL
2259
+ * immediately, surviving a rollback (#1111).
2477
2260
  *
2478
- * @param strings - Template strings
2479
- * @param vars - Variables to interpolate into the query
2480
- * @returns Promise resolving to array of result records
2481
- */
2482
- const many = async (strings, ...vars) => {
2483
- const { sql, values } = parseTemplate(strings, ...vars);
2484
- try {
2485
- return (await client.query(sql, values)).rows;
2486
- } catch (e) {
2487
- throw new DatabaseError("Failed to execute many query", {
2488
- sql,
2489
- values,
2490
- originalError: formatDbError(e)
2491
- });
2492
- }
2493
- };
2494
- /**
2495
- * Executes a SQL query using template literals without returning results
2261
+ * Binding a client is the only difference between the two, so it is the only
2262
+ * thing this factory parameterizes.
2496
2263
  *
2497
- * @param strings - Template strings
2498
- * @param vars - Variables to interpolate into the query
2499
- * @returns Promise that resolves when the query completes
2500
- */
2501
- const execute = async (strings, ...vars) => {
2502
- const { sql, values } = parseTemplate(strings, ...vars);
2503
- try {
2504
- await client.query(sql, values);
2505
- } catch (e) {
2506
- throw new DatabaseError("Failed to execute query", {
2507
- sql,
2508
- values,
2509
- originalError: formatDbError(e)
2510
- });
2511
- }
2512
- };
2513
- /**
2514
- * Executes a raw SQL query with parameterized values
2264
+ * One thing is deliberately NOT unified: how `insert` and `update` serialize
2265
+ * values. The pool-backed methods run every value through `serializeRecord`,
2266
+ * which JSON-stringifies objects and arrays; the transaction-scoped copies
2267
+ * never did, so they handed the raw value to `pg`. That is not a cosmetic
2268
+ * difference — it decides what lands in the column, and no single answer is
2269
+ * right for both shapes:
2270
+ *
2271
+ * ```
2272
+ * text[] column jsonb column
2273
+ * raw JS array accepted "invalid input syntax for type json"
2274
+ * JSON string "malformed accepted
2275
+ * array literal"
2276
+ * ```
2515
2277
  *
2516
- * Uses PostgreSQL-native placeholders ($1, $2, ...). Legacy ? placeholders
2517
- * are converted only when the placeholder count matches the supplied values,
2518
- * so Postgres operators such as JSONB ? remain intact.
2278
+ * So collapsing the two would have silently broken array-column writes inside
2279
+ * a transaction while fixing jsonb-array writes, or the reverse — and would
2280
+ * have corrupted `bytea` writes, where a Buffer would become the JSON of a
2281
+ * Buffer with no error anywhere. Neither belongs in a fix for the error
2282
+ * contract, so the serializer is a parameter: each interface keeps exactly
2283
+ * the behaviour it had, and the difference is stated here instead of being an
2284
+ * accident of two copies drifting apart.
2519
2285
  *
2520
- * @param sql - SQL query string
2521
- * @param values - Variables to use as parameters
2522
- * @returns Promise resolving to query result with rows and count
2286
+ * @param executor - Pool or checked-out client every statement runs on
2287
+ * @param inTransaction - Whether `executor` is already inside a transaction
2288
+ * @param serialize - Applied to each record before `insert`/`update` builds
2289
+ * its SQL. Pass-through for the transaction-scoped interfaces.
2523
2290
  */
2524
- const query = async (sql, ...values) => {
2525
- const query = normalizePostgresRawQuery(sql, values);
2526
- try {
2527
- const result = await client.query(query.sql, query.values);
2291
+ const createClientMethods = (executor, inTransaction, serialize) => {
2292
+ /**
2293
+ * Inserts one or more records into a table
2294
+ *
2295
+ * @param table - Table name
2296
+ * @param data - Single record or array of records to insert
2297
+ * @returns Promise resolving to operation result
2298
+ * @throws Error if the insert operation fails
2299
+ *
2300
+ * @example Single record insert:
2301
+ * ```typescript
2302
+ * await db.insert('users', {
2303
+ * name: 'John Doe',
2304
+ * email: 'john@example.com'
2305
+ * });
2306
+ * ```
2307
+ *
2308
+ * @example Multiple record insert:
2309
+ * ```typescript
2310
+ * await db.insert('users', [
2311
+ * { name: 'John', email: 'john@example.com' },
2312
+ * { name: 'Jane', email: 'jane@example.com' }
2313
+ * ]);
2314
+ * ```
2315
+ */
2316
+ const insert = async (table, data) => {
2317
+ validateTableName$1(table);
2318
+ if (Array.isArray(data)) {
2319
+ const serializedRecords = data.map((record) => serialize(record));
2320
+ const keys = Object.keys(serializedRecords[0]);
2321
+ validateColumnNames(keys);
2322
+ const placeholders = serializedRecords.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
2323
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
2324
+ const values = serializedRecords.reduce((acc, row) => acc.concat(Object.values(row)), []);
2325
+ return {
2326
+ operation: "insert",
2327
+ affected: (await executor.query(query, values)).rowCount ?? 0
2328
+ };
2329
+ }
2330
+ const serializedData = serialize(data);
2331
+ const keys = Object.keys(serializedData);
2332
+ validateColumnNames(keys);
2333
+ const values = Object.values(serializedData);
2334
+ const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2335
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
2528
2336
  return {
2529
- rows: result.rows,
2530
- rowCount: result.rowCount ?? 0
2337
+ operation: "insert",
2338
+ affected: (await executor.query(query, values)).rowCount ?? 0
2531
2339
  };
2532
- } catch (e) {
2533
- throw new DatabaseError("Failed to execute raw query", {
2534
- sql: query.sql,
2535
- values: query.values,
2536
- originalError: formatDbError(e)
2537
- });
2538
- }
2539
- };
2540
- /**
2541
- * Acquire a pinned connection from the pool for session-scoped state such as
2542
- * `pg_advisory_lock`. Every query on the returned handle runs on the same
2543
- * client until {@link SessionHandle.release} drops the connection (which
2544
- * frees any session locks it held). The connection is held for the handle's
2545
- * lifetime, so hold one session per process, not per operation.
2546
- */
2547
- const acquireSession = async () => {
2548
- const sessionClient = await pool.connect();
2549
- let released = false;
2550
- let lost = false;
2551
- sessionClient.on("error", () => {
2552
- lost = true;
2553
- });
2554
- return {
2555
- query: async (sql, ...values) => {
2556
- if (released) throw new DatabaseError("Session has been released", {});
2557
- if (lost) throw new DatabaseError("Session connection was lost", {});
2558
- const normalized = normalizePostgresRawQuery(sql, values);
2559
- try {
2560
- const result = await sessionClient.query(normalized.sql, normalized.values);
2561
- return {
2562
- rows: result.rows,
2563
- rowCount: result.rowCount ?? 0
2564
- };
2565
- } catch (e) {
2566
- throw new DatabaseError("Failed to execute session query", {
2567
- sql: normalized.sql,
2568
- values: normalized.values,
2340
+ };
2341
+ /**
2342
+ * Retrieves a single record matching the where criteria
2343
+ *
2344
+ * @param table - Table name
2345
+ * @param where - Criteria to match records
2346
+ * @returns Promise resolving to query result
2347
+ */
2348
+ const get = async (table, where) => {
2349
+ validateTableName$1(table);
2350
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2351
+ if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
2352
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2353
+ try {
2354
+ return (await executor.query(query, values)).rows[0] || null;
2355
+ } catch (e) {
2356
+ throw new DatabaseError("Failed to retrieve record from table", {
2357
+ table,
2358
+ sql: query,
2359
+ values,
2360
+ originalError: formatDbError(e)
2361
+ });
2362
+ }
2363
+ };
2364
+ /**
2365
+ * Retrieves multiple records matching the where criteria
2366
+ *
2367
+ * @param table - Table name
2368
+ * @param where - Criteria to match records
2369
+ * @returns Promise resolving to array of records
2370
+ */
2371
+ const list = async (table, where) => {
2372
+ validateTableName$1(table);
2373
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2374
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2375
+ try {
2376
+ return (await executor.query(query, values)).rows;
2377
+ } catch (e) {
2378
+ throw new DatabaseError("Failed to list records from table", {
2379
+ table,
2380
+ sql: query,
2381
+ values,
2382
+ originalError: formatDbError(e)
2383
+ });
2384
+ }
2385
+ };
2386
+ /**
2387
+ * Updates records matching the where criteria
2388
+ *
2389
+ * @param table - Table name
2390
+ * @param where - Criteria to match records to update
2391
+ * @param data - New data to set
2392
+ * @returns Promise resolving to operation result
2393
+ */
2394
+ const update = async (table, where, data) => {
2395
+ validateTableName$1(table);
2396
+ const serializedData = serialize(data);
2397
+ const keys = Object.keys(serializedData);
2398
+ validateColumnNames(keys);
2399
+ const values = Object.values(serializedData);
2400
+ const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2401
+ const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
2402
+ if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
2403
+ const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
2404
+ try {
2405
+ return {
2406
+ operation: "update",
2407
+ affected: (await executor.query(sql, [...values, ...whereValues])).rowCount ?? 0
2408
+ };
2409
+ } catch (e) {
2410
+ throw new DatabaseError("Failed to update records in table", {
2411
+ table,
2412
+ sql,
2413
+ values: [...values, ...whereValues],
2414
+ originalError: formatDbError(e)
2415
+ });
2416
+ }
2417
+ };
2418
+ /**
2419
+ * Inserts a record or updates it if it already exists (UPSERT)
2420
+ *
2421
+ * @param table - Table name
2422
+ * @param conflictColumns - Columns that define the uniqueness constraint
2423
+ * @param data - Data to insert or update
2424
+ * @returns Promise resolving to operation result
2425
+ * @throws Error if the upsert operation fails
2426
+ */
2427
+ const upsert = async (table, conflictColumns, data, options) => {
2428
+ validateTableName$1(table);
2429
+ validateColumnNames([...conflictColumns, ...Object.keys(data)]);
2430
+ try {
2431
+ return await executePostgresUpsert(executor, table, conflictColumns, data, options, !inTransaction);
2432
+ } catch (e) {
2433
+ if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
2434
+ throw new DatabaseError("Failed to upsert record into table", {
2435
+ table,
2436
+ values: data,
2437
+ conflictColumns,
2438
+ originalError: formatDbError(e)
2439
+ });
2440
+ }
2441
+ };
2442
+ /**
2443
+ * Gets a record matching the where criteria or inserts it if not found
2444
+ *
2445
+ * @param table - Table name
2446
+ * @param where - Criteria to match existing record
2447
+ * @returns Promise resolving to the query result or insert result
2448
+ */
2449
+ const getOrInsert = async (table, where, data) => {
2450
+ validateTableName$1(table);
2451
+ const result = await get(table, where);
2452
+ if (result) return result;
2453
+ await insert(table, data);
2454
+ const inserted = await get(table, where);
2455
+ if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
2456
+ table,
2457
+ where,
2458
+ data
2459
+ });
2460
+ return inserted;
2461
+ };
2462
+ /**
2463
+ * Deletes records from a table matching the where criteria
2464
+ *
2465
+ * @param table - Table name
2466
+ * @param where - Criteria to match records for deletion
2467
+ * @returns Promise resolving to operation result with count of deleted rows
2468
+ * @throws Error if the delete operation fails
2469
+ */
2470
+ const deleteRecords = async (table, where) => {
2471
+ validateTableName$1(table);
2472
+ if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
2473
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2474
+ try {
2475
+ return {
2476
+ operation: "delete",
2477
+ affected: (await executor.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
2478
+ };
2479
+ } catch (e) {
2480
+ throw new DatabaseError("Failed to delete records from table", {
2481
+ table,
2482
+ where,
2483
+ originalError: formatDbError(e)
2484
+ });
2485
+ }
2486
+ };
2487
+ /**
2488
+ * Counts records in a table matching the where criteria
2489
+ *
2490
+ * @param table - Table name
2491
+ * @param where - Criteria to match records (optional, counts all if omitted)
2492
+ * @returns Promise resolving to count of matching records
2493
+ * @throws Error if the count operation fails
2494
+ */
2495
+ const count = async (table, where) => {
2496
+ validateTableName$1(table);
2497
+ try {
2498
+ if (!where || Object.keys(where).length === 0) {
2499
+ const result = await executor.query(`SELECT COUNT(*) as count FROM ${table}`);
2500
+ return Number(result.rows[0]?.count) || 0;
2501
+ }
2502
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2503
+ const result = await executor.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
2504
+ return Number(result.rows[0]?.count) || 0;
2505
+ } catch (e) {
2506
+ throw new DatabaseError("Failed to count records in table", {
2507
+ table,
2508
+ where,
2509
+ originalError: formatDbError(e)
2510
+ });
2511
+ }
2512
+ };
2513
+ /**
2514
+ * Creates a table-specific interface for simplified table operations
2515
+ *
2516
+ * @param tableName - Table name
2517
+ * @returns TableMethods interface for the specified table
2518
+ */
2519
+ const table = (tableName) => {
2520
+ return {
2521
+ insert: (data) => insert(tableName, data),
2522
+ get: (data) => get(tableName, data),
2523
+ list: (data) => list(tableName, data)
2524
+ };
2525
+ };
2526
+ /**
2527
+ * Executes a SQL query using template literals and returns a single value
2528
+ *
2529
+ * @param strings - Template strings
2530
+ * @param vars - Variables to interpolate into the query
2531
+ * @returns Promise resolving to a single value (first column of first row)
2532
+ */
2533
+ const pluck = async (strings, ...vars) => {
2534
+ const { sql, values } = parseTemplate(strings, ...vars);
2535
+ try {
2536
+ const firstRow = (await executor.query(sql, values)).rows[0];
2537
+ if (!firstRow) return null;
2538
+ return Object.values(firstRow)[0];
2539
+ } catch (e) {
2540
+ throw new DatabaseError("Failed to execute pluck query", {
2541
+ sql,
2542
+ values,
2543
+ originalError: formatDbError(e)
2544
+ });
2545
+ }
2546
+ };
2547
+ /**
2548
+ * Executes a SQL query using template literals and returns a single row
2549
+ *
2550
+ * @param strings - Template strings
2551
+ * @param vars - Variables to interpolate into the query
2552
+ * @returns Promise resolving to a single result record or null
2553
+ */
2554
+ const single = async (strings, ...vars) => {
2555
+ const { sql, values } = parseTemplate(strings, ...vars);
2556
+ try {
2557
+ return (await executor.query(sql, values)).rows[0] || null;
2558
+ } catch (e) {
2559
+ throw new DatabaseError("Failed to execute single query", {
2560
+ sql,
2561
+ values,
2562
+ originalError: formatDbError(e)
2563
+ });
2564
+ }
2565
+ };
2566
+ /**
2567
+ * Executes a SQL query using template literals and returns multiple rows
2568
+ *
2569
+ * @param strings - Template strings
2570
+ * @param vars - Variables to interpolate into the query
2571
+ * @returns Promise resolving to array of result records
2572
+ */
2573
+ const many = async (strings, ...vars) => {
2574
+ const { sql, values } = parseTemplate(strings, ...vars);
2575
+ try {
2576
+ return (await executor.query(sql, values)).rows;
2577
+ } catch (e) {
2578
+ throw new DatabaseError("Failed to execute many query", {
2579
+ sql,
2580
+ values,
2581
+ originalError: formatDbError(e)
2582
+ });
2583
+ }
2584
+ };
2585
+ /**
2586
+ * Executes a SQL query using template literals without returning results
2587
+ *
2588
+ * @param strings - Template strings
2589
+ * @param vars - Variables to interpolate into the query
2590
+ * @returns Promise that resolves when the query completes
2591
+ */
2592
+ const execute = async (strings, ...vars) => {
2593
+ const { sql, values } = parseTemplate(strings, ...vars);
2594
+ try {
2595
+ await executor.query(sql, values);
2596
+ } catch (e) {
2597
+ throw new DatabaseError("Failed to execute query", {
2598
+ sql,
2599
+ values,
2600
+ originalError: formatDbError(e)
2601
+ });
2602
+ }
2603
+ };
2604
+ /**
2605
+ * Executes a raw SQL query with parameterized values
2606
+ *
2607
+ * Uses PostgreSQL-native placeholders ($1, $2, ...). Legacy ? placeholders
2608
+ * are converted only when the placeholder count matches the supplied values,
2609
+ * so Postgres operators such as JSONB ? remain intact.
2610
+ *
2611
+ * @param sql - SQL query string
2612
+ * @param values - Variables to use as parameters
2613
+ * @returns Promise resolving to query result with rows and count
2614
+ */
2615
+ const query = async (sql, ...values) => {
2616
+ const query = normalizePostgresRawQuery(sql, values);
2617
+ try {
2618
+ const result = await executor.query(query.sql, query.values);
2619
+ return {
2620
+ rows: result.rows,
2621
+ rowCount: result.rowCount ?? 0
2622
+ };
2623
+ } catch (e) {
2624
+ throw new DatabaseError("Failed to execute raw query", {
2625
+ sql: query.sql,
2626
+ values: query.values,
2627
+ originalError: formatDbError(e)
2628
+ });
2629
+ }
2630
+ };
2631
+ /**
2632
+ * Checks if a table exists in the database
2633
+ *
2634
+ * @param tableName - Name of the table to check
2635
+ * @returns Promise resolving to boolean indicating if the table exists
2636
+ */
2637
+ const tableExists = async (tableName) => {
2638
+ return (await executor.query(`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = 'public')`, [tableName])).rows[0].exists;
2639
+ };
2640
+ /**
2641
+ * Synchronizes database schema with provided SQL DDL
2642
+ * Creates tables if they don't exist and adds missing columns
2643
+ * Also executes CREATE INDEX statements
2644
+ *
2645
+ * @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements
2646
+ * @returns Promise that resolves when schema is synchronized
2647
+ */
2648
+ /**
2649
+ * Runs a step whose failure `syncSchema` deliberately tolerates.
2650
+ *
2651
+ * Logging and continuing is self-contained on the pool. Inside a
2652
+ * transaction it is not: a failed statement puts PostgreSQL in the aborted
2653
+ * state, and `COMMIT` on an aborted transaction *succeeds* — it reports a
2654
+ * ROLLBACK command tag and no error. The caller is told the transaction
2655
+ * committed while every write in it was discarded, which is the silent
2656
+ * data loss this whole epic is about, re-entering through the schema path.
2657
+ *
2658
+ * A savepoint scopes the failure to the step, which is what "log it and
2659
+ * continue" was always supposed to mean.
2660
+ */
2661
+ const runTolerated = async (work) => {
2662
+ if (!inTransaction) return work();
2663
+ savepointSequence += 1;
2664
+ const name = `hv_sync_${savepointSequence}`;
2665
+ await executor.query(`SAVEPOINT ${name}`);
2666
+ try {
2667
+ const result = await work();
2668
+ await executor.query(`RELEASE SAVEPOINT ${name}`);
2669
+ return result;
2670
+ } catch (error) {
2671
+ await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
2672
+ await executor.query(`RELEASE SAVEPOINT ${name}`);
2673
+ throw error;
2674
+ }
2675
+ };
2676
+ const syncSchema = async (schema) => {
2677
+ const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
2678
+ const createIndexRegex = /CREATE (UNIQUE )?INDEX (IF NOT EXISTS )?"?(\w+)"? ON "?(\w+)"?\s*\(([^)]+)\)/i;
2679
+ const indexNames = [];
2680
+ for (const command of commands) {
2681
+ const indexMatch = command.trim().match(createIndexRegex);
2682
+ if (indexMatch) indexNames.push(indexMatch[3]);
2683
+ }
2684
+ const existingIndexes = /* @__PURE__ */ new Set();
2685
+ if (indexNames.length > 0) {
2686
+ const result = await executor.query(`SELECT indexname FROM pg_indexes
2687
+ WHERE schemaname = 'public' AND indexname = ANY($1::text[])`, [indexNames]);
2688
+ for (const row of result.rows) existingIndexes.add(row.indexname);
2689
+ }
2690
+ for (const command of commands) {
2691
+ const trimmedCommand = command.trim();
2692
+ const tableMatch = trimmedCommand.match(/CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(([\s\S]+)\)/i);
2693
+ if (tableMatch) {
2694
+ const tableName = tableMatch[2];
2695
+ const columns = tableMatch[3].trim().split(",\n");
2696
+ if (!await tableExists(tableName)) await executor.query(trimmedCommand);
2697
+ else for (const column of columns) {
2698
+ const columnDef = column.trim();
2699
+ const columnMatch = columnDef.match(/"?(\w+)"?\s+(\w+[^,]*)/);
2700
+ if (columnMatch) {
2701
+ const columnName = columnMatch[1];
2702
+ if (columnName.toUpperCase() === "PRIMARY" || columnName.toUpperCase() === "FOREIGN" || columnName.toUpperCase() === "UNIQUE" || columnName.toUpperCase() === "CHECK" || columnName.toUpperCase() === "CONSTRAINT") continue;
2703
+ try {
2704
+ await runTolerated(async () => {
2705
+ if (!(await executor.query(`SELECT EXISTS (
2706
+ SELECT 1 FROM information_schema.columns
2707
+ WHERE table_name = $1
2708
+ AND column_name = $2
2709
+ AND table_schema = 'public'
2710
+ )`, [tableName, columnName])).rows[0].exists) {
2711
+ const alterCommand = `ALTER TABLE "${tableName}" ADD COLUMN ${columnDef}`;
2712
+ await executor.query(alterCommand);
2713
+ }
2714
+ });
2715
+ } catch (error) {
2716
+ console.error(`Error adding column ${columnName} to ${tableName}:`, error);
2717
+ }
2718
+ }
2719
+ }
2720
+ continue;
2721
+ }
2722
+ const indexMatch = trimmedCommand.match(createIndexRegex);
2723
+ if (indexMatch) {
2724
+ const indexName = indexMatch[3];
2725
+ const indexTableName = indexMatch[4];
2726
+ if (!existingIndexes.has(indexName)) try {
2727
+ await runTolerated(() => executor.query(trimmedCommand));
2728
+ existingIndexes.add(indexName);
2729
+ } catch (error) {
2730
+ console.warn(`Warning: Failed to create index ${indexName} on ${indexTableName}:`, error instanceof Error ? error.message : String(error));
2731
+ }
2732
+ continue;
2733
+ }
2734
+ if (trimmedCommand.toUpperCase().startsWith("CREATE ") && !trimmedCommand.toUpperCase().includes("CREATE TABLE")) try {
2735
+ await runTolerated(() => executor.query(trimmedCommand));
2736
+ } catch (error) {
2737
+ console.warn(`Warning: DDL statement may have failed:`, error instanceof Error ? error.message : String(error));
2738
+ }
2739
+ }
2740
+ };
2741
+ return {
2742
+ insert,
2743
+ get,
2744
+ list,
2745
+ update,
2746
+ upsert,
2747
+ getOrInsert,
2748
+ delete: deleteRecords,
2749
+ count,
2750
+ table,
2751
+ many,
2752
+ single,
2753
+ pluck,
2754
+ execute,
2755
+ query,
2756
+ oo: many,
2757
+ oO: single,
2758
+ ox: pluck,
2759
+ xx: execute,
2760
+ tableExists,
2761
+ syncSchema
2762
+ };
2763
+ };
2764
+ 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);
2765
+ /**
2766
+ * Acquire a pinned connection from the pool for session-scoped state such as
2767
+ * `pg_advisory_lock`. Every query on the returned handle runs on the same
2768
+ * client until {@link SessionHandle.release} drops the connection (which
2769
+ * frees any session locks it held). The connection is held for the handle's
2770
+ * lifetime, so hold one session per process, not per operation.
2771
+ */
2772
+ const acquireSession = async () => {
2773
+ const sessionClient = await pool.connect();
2774
+ let released = false;
2775
+ let lost = false;
2776
+ sessionClient.on("error", () => {
2777
+ lost = true;
2778
+ });
2779
+ return {
2780
+ query: async (sql, ...values) => {
2781
+ if (released) throw new DatabaseError("Session has been released", {});
2782
+ if (lost) throw new DatabaseError("Session connection was lost", {});
2783
+ const normalized = normalizePostgresRawQuery(sql, values);
2784
+ try {
2785
+ const result = await sessionClient.query(normalized.sql, normalized.values);
2786
+ return {
2787
+ rows: result.rows,
2788
+ rowCount: result.rowCount ?? 0
2789
+ };
2790
+ } catch (e) {
2791
+ throw new DatabaseError("Failed to execute session query", {
2792
+ sql: normalized.sql,
2793
+ values: normalized.values,
2569
2794
  originalError: formatDbError(e)
2570
2795
  });
2571
2796
  }
@@ -2586,86 +2811,6 @@ async function createDatabase(options, cacheKey) {
2586
2811
  }
2587
2812
  };
2588
2813
  };
2589
- /**
2590
- * Checks if a table exists in the database
2591
- *
2592
- * @param tableName - Name of the table to check
2593
- * @returns Promise resolving to boolean indicating if the table exists
2594
- */
2595
- const tableExists = async (tableName) => {
2596
- return (await client.query(`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = 'public')`, [tableName])).rows[0].exists;
2597
- };
2598
- /**
2599
- * Synchronizes database schema with provided SQL DDL
2600
- * Creates tables if they don't exist and adds missing columns
2601
- * Also executes CREATE INDEX statements
2602
- *
2603
- * @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements
2604
- * @returns Promise that resolves when schema is synchronized
2605
- */
2606
- const syncSchema = async (schema) => {
2607
- const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
2608
- const createIndexRegex = /CREATE (UNIQUE )?INDEX (IF NOT EXISTS )?"?(\w+)"? ON "?(\w+)"?\s*\(([^)]+)\)/i;
2609
- const indexNames = [];
2610
- for (const command of commands) {
2611
- const indexMatch = command.trim().match(createIndexRegex);
2612
- if (indexMatch) indexNames.push(indexMatch[3]);
2613
- }
2614
- const existingIndexes = /* @__PURE__ */ new Set();
2615
- if (indexNames.length > 0) {
2616
- const result = await client.query(`SELECT indexname FROM pg_indexes
2617
- WHERE schemaname = 'public' AND indexname = ANY($1::text[])`, [indexNames]);
2618
- for (const row of result.rows) existingIndexes.add(row.indexname);
2619
- }
2620
- for (const command of commands) {
2621
- const trimmedCommand = command.trim();
2622
- const tableMatch = trimmedCommand.match(/CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(([\s\S]+)\)/i);
2623
- if (tableMatch) {
2624
- const tableName = tableMatch[2];
2625
- const columns = tableMatch[3].trim().split(",\n");
2626
- if (!await tableExists(tableName)) await client.query(trimmedCommand);
2627
- else for (const column of columns) {
2628
- const columnDef = column.trim();
2629
- const columnMatch = columnDef.match(/"?(\w+)"?\s+(\w+[^,]*)/);
2630
- if (columnMatch) {
2631
- const columnName = columnMatch[1];
2632
- if (columnName.toUpperCase() === "PRIMARY" || columnName.toUpperCase() === "FOREIGN" || columnName.toUpperCase() === "UNIQUE" || columnName.toUpperCase() === "CHECK" || columnName.toUpperCase() === "CONSTRAINT") continue;
2633
- try {
2634
- if (!(await client.query(`SELECT EXISTS (
2635
- SELECT 1 FROM information_schema.columns
2636
- WHERE table_name = $1
2637
- AND column_name = $2
2638
- AND table_schema = 'public'
2639
- )`, [tableName, columnName])).rows[0].exists) {
2640
- const alterCommand = `ALTER TABLE "${tableName}" ADD COLUMN ${columnDef}`;
2641
- await client.query(alterCommand);
2642
- }
2643
- } catch (error) {
2644
- console.error(`Error adding column ${columnName} to ${tableName}:`, error);
2645
- }
2646
- }
2647
- }
2648
- continue;
2649
- }
2650
- const indexMatch = trimmedCommand.match(createIndexRegex);
2651
- if (indexMatch) {
2652
- const indexName = indexMatch[3];
2653
- const indexTableName = indexMatch[4];
2654
- if (!existingIndexes.has(indexName)) try {
2655
- await client.query(trimmedCommand);
2656
- existingIndexes.add(indexName);
2657
- } catch (error) {
2658
- console.warn(`Warning: Failed to create index ${indexName} on ${indexTableName}:`, error instanceof Error ? error.message : String(error));
2659
- }
2660
- continue;
2661
- }
2662
- if (trimmedCommand.toUpperCase().startsWith("CREATE ") && !trimmedCommand.toUpperCase().includes("CREATE TABLE")) try {
2663
- await client.query(trimmedCommand);
2664
- } catch (error) {
2665
- console.warn(`Warning: DDL statement may have failed:`, error instanceof Error ? error.message : String(error));
2666
- }
2667
- }
2668
- };
2669
2814
  let savepointSequence = 0;
2670
2815
  /**
2671
2816
  * Builds the `transaction` exposed on a transaction-scoped interface.
@@ -2715,132 +2860,7 @@ async function createDatabase(options, cacheKey) {
2715
2860
  const txDb = {
2716
2861
  url,
2717
2862
  client: txClient,
2718
- insert: async (table, data) => {
2719
- if (Array.isArray(data)) {
2720
- const keys = Object.keys(data[0]);
2721
- const placeholders = data.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
2722
- const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
2723
- const values = data.reduce((acc, row) => acc.concat(Object.values(row)), []);
2724
- return {
2725
- operation: "insert",
2726
- affected: (await txClient.query(query, values)).rowCount ?? 0
2727
- };
2728
- }
2729
- const keys = Object.keys(data);
2730
- const values = Object.values(data);
2731
- const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2732
- const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
2733
- return {
2734
- operation: "insert",
2735
- affected: (await txClient.query(query, values)).rowCount ?? 0
2736
- };
2737
- },
2738
- get: async (table, where) => {
2739
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2740
- if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
2741
- const query = `SELECT * FROM ${table} ${whereClause}`;
2742
- return (await txClient.query(query, values)).rows[0] || null;
2743
- },
2744
- list: async (table, where) => {
2745
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2746
- const query = `SELECT * FROM ${table} ${whereClause}`;
2747
- return (await txClient.query(query, values)).rows;
2748
- },
2749
- update: async (table, where, data) => {
2750
- const keys = Object.keys(data);
2751
- const values = Object.values(data);
2752
- const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2753
- const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
2754
- if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
2755
- const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
2756
- return {
2757
- operation: "update",
2758
- affected: (await txClient.query(sql, [...values, ...whereValues])).rowCount ?? 0
2759
- };
2760
- },
2761
- upsert: async (table, conflictColumns, data, options) => executePostgresUpsert(txClient, table, conflictColumns, data, options, false),
2762
- getOrInsert: async (table, where, data) => {
2763
- const result = await txDb.get(table, where);
2764
- if (result) return result;
2765
- await txDb.insert(table, data);
2766
- const inserted = await txDb.get(table, where);
2767
- if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
2768
- table,
2769
- where,
2770
- data
2771
- });
2772
- return inserted;
2773
- },
2774
- delete: async (table, where) => {
2775
- validateTableName$1(table);
2776
- if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
2777
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2778
- return {
2779
- operation: "delete",
2780
- affected: (await txClient.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
2781
- };
2782
- },
2783
- count: async (table, where) => {
2784
- validateTableName$1(table);
2785
- if (!where || Object.keys(where).length === 0) {
2786
- const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table}`);
2787
- return Number(result.rows[0]?.count) || 0;
2788
- }
2789
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2790
- const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
2791
- return Number(result.rows[0]?.count) || 0;
2792
- },
2793
- table: (tableName) => ({
2794
- insert: (data) => txDb.insert(tableName, data),
2795
- get: (data) => txDb.get(tableName, data),
2796
- list: (data) => txDb.list(tableName, data)
2797
- }),
2798
- many: async (strings, ...vars) => {
2799
- const { sql, values } = parseTemplate(strings, ...vars);
2800
- return (await txClient.query(sql, values)).rows;
2801
- },
2802
- single: async (strings, ...vars) => {
2803
- const { sql, values } = parseTemplate(strings, ...vars);
2804
- return (await txClient.query(sql, values)).rows[0] || null;
2805
- },
2806
- pluck: async (strings, ...vars) => {
2807
- const { sql, values } = parseTemplate(strings, ...vars);
2808
- const firstRow = (await txClient.query(sql, values)).rows[0];
2809
- if (!firstRow) return null;
2810
- return Object.values(firstRow)[0];
2811
- },
2812
- execute: async (strings, ...vars) => {
2813
- const { sql, values } = parseTemplate(strings, ...vars);
2814
- await txClient.query(sql, values);
2815
- },
2816
- query: async (sql, ...values) => {
2817
- const query = normalizePostgresRawQuery(sql, values);
2818
- const result = await txClient.query(query.sql, query.values);
2819
- return {
2820
- rows: result.rows,
2821
- rowCount: result.rowCount ?? 0
2822
- };
2823
- },
2824
- oo: async (strings, ...vars) => {
2825
- const { sql, values } = parseTemplate(strings, ...vars);
2826
- return (await txClient.query(sql, values)).rows;
2827
- },
2828
- oO: async (strings, ...vars) => {
2829
- const { sql, values } = parseTemplate(strings, ...vars);
2830
- return (await txClient.query(sql, values)).rows[0] || null;
2831
- },
2832
- ox: async (strings, ...vars) => {
2833
- const { sql, values } = parseTemplate(strings, ...vars);
2834
- const firstRow = (await txClient.query(sql, values)).rows[0];
2835
- if (!firstRow) return null;
2836
- return Object.values(firstRow)[0];
2837
- },
2838
- xx: async (strings, ...vars) => {
2839
- const { sql, values } = parseTemplate(strings, ...vars);
2840
- await txClient.query(sql, values);
2841
- },
2842
- tableExists,
2843
- syncSchema,
2863
+ ...createClientMethods(txClient, true, passThroughRecord),
2844
2864
  transaction: createNestedTransaction(txClient, () => txDb)
2845
2865
  };
2846
2866
  const result = await callback(txDb);
@@ -2889,132 +2909,7 @@ async function createDatabase(options, cacheKey) {
2889
2909
  const txHandle = {
2890
2910
  url,
2891
2911
  client: txClient,
2892
- insert: async (table, data) => {
2893
- if (Array.isArray(data)) {
2894
- const keys = Object.keys(data[0]);
2895
- const placeholders = data.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
2896
- const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
2897
- const values = data.reduce((acc, row) => acc.concat(Object.values(row)), []);
2898
- return {
2899
- operation: "insert",
2900
- affected: (await txClient.query(query, values)).rowCount ?? 0
2901
- };
2902
- }
2903
- const keys = Object.keys(data);
2904
- const values = Object.values(data);
2905
- const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2906
- const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
2907
- return {
2908
- operation: "insert",
2909
- affected: (await txClient.query(query, values)).rowCount ?? 0
2910
- };
2911
- },
2912
- get: async (table, where) => {
2913
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2914
- if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
2915
- const query = `SELECT * FROM ${table} ${whereClause}`;
2916
- return (await txClient.query(query, values)).rows[0] || null;
2917
- },
2918
- list: async (table, where) => {
2919
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2920
- const query = `SELECT * FROM ${table} ${whereClause}`;
2921
- return (await txClient.query(query, values)).rows;
2922
- },
2923
- update: async (table, where, data) => {
2924
- const keys = Object.keys(data);
2925
- const values = Object.values(data);
2926
- const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2927
- const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
2928
- if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
2929
- const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
2930
- return {
2931
- operation: "update",
2932
- affected: (await txClient.query(sql, [...values, ...whereValues])).rowCount ?? 0
2933
- };
2934
- },
2935
- upsert: async (table, conflictColumns, data, options) => executePostgresUpsert(txClient, table, conflictColumns, data, options, false),
2936
- getOrInsert: async (table, where, data) => {
2937
- const result = await txHandle.get(table, where);
2938
- if (result) return result;
2939
- await txHandle.insert(table, data);
2940
- const inserted = await txHandle.get(table, where);
2941
- if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
2942
- table,
2943
- where,
2944
- data
2945
- });
2946
- return inserted;
2947
- },
2948
- delete: async (table, where) => {
2949
- validateTableName$1(table);
2950
- if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
2951
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2952
- return {
2953
- operation: "delete",
2954
- affected: (await txClient.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
2955
- };
2956
- },
2957
- count: async (table, where) => {
2958
- validateTableName$1(table);
2959
- if (!where || Object.keys(where).length === 0) {
2960
- const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table}`);
2961
- return Number(result.rows[0]?.count) || 0;
2962
- }
2963
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2964
- const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
2965
- return Number(result.rows[0]?.count) || 0;
2966
- },
2967
- table: (tableName) => ({
2968
- insert: (data) => txHandle.insert(tableName, data),
2969
- get: (data) => txHandle.get(tableName, data),
2970
- list: (data) => txHandle.list(tableName, data)
2971
- }),
2972
- many: async (strings, ...vars) => {
2973
- const { sql, values } = parseTemplate(strings, ...vars);
2974
- return (await txClient.query(sql, values)).rows;
2975
- },
2976
- single: async (strings, ...vars) => {
2977
- const { sql, values } = parseTemplate(strings, ...vars);
2978
- return (await txClient.query(sql, values)).rows[0] || null;
2979
- },
2980
- pluck: async (strings, ...vars) => {
2981
- const { sql, values } = parseTemplate(strings, ...vars);
2982
- const firstRow = (await txClient.query(sql, values)).rows[0];
2983
- if (!firstRow) return null;
2984
- return Object.values(firstRow)[0];
2985
- },
2986
- execute: async (strings, ...vars) => {
2987
- const { sql, values } = parseTemplate(strings, ...vars);
2988
- await txClient.query(sql, values);
2989
- },
2990
- query: async (sql, ...values) => {
2991
- const query = normalizePostgresRawQuery(sql, values);
2992
- const result = await txClient.query(query.sql, query.values);
2993
- return {
2994
- rows: result.rows,
2995
- rowCount: result.rowCount ?? 0
2996
- };
2997
- },
2998
- oo: async (strings, ...vars) => {
2999
- const { sql, values } = parseTemplate(strings, ...vars);
3000
- return (await txClient.query(sql, values)).rows;
3001
- },
3002
- oO: async (strings, ...vars) => {
3003
- const { sql, values } = parseTemplate(strings, ...vars);
3004
- return (await txClient.query(sql, values)).rows[0] || null;
3005
- },
3006
- ox: async (strings, ...vars) => {
3007
- const { sql, values } = parseTemplate(strings, ...vars);
3008
- const firstRow = (await txClient.query(sql, values)).rows[0];
3009
- if (!firstRow) return null;
3010
- return Object.values(firstRow)[0];
3011
- },
3012
- xx: async (strings, ...vars) => {
3013
- const { sql, values } = parseTemplate(strings, ...vars);
3014
- await txClient.query(sql, values);
3015
- },
3016
- tableExists,
3017
- syncSchema,
2912
+ ...createClientMethods(txClient, true, passThroughRecord),
3018
2913
  transaction: createNestedTransaction(txClient, () => txHandle),
3019
2914
  commit,
3020
2915
  rollback,
@@ -3022,10 +2917,6 @@ async function createDatabase(options, cacheKey) {
3022
2917
  };
3023
2918
  return txHandle;
3024
2919
  };
3025
- const oo = many;
3026
- const oO = single;
3027
- const ox = pluck;
3028
- const xx = execute;
3029
2920
  /**
3030
2921
  * Initialize database schemas from JSON manifest
3031
2922
  * Supports dependency resolution and schema overrides
@@ -3311,8 +3202,8 @@ async function getDatabase(options = {}) {
3311
3202
  }));
3312
3203
  if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) options.type = "sqlite";
3313
3204
  if (options.type === "postgres") return (await Promise.resolve().then(() => postgres_exports)).getDatabase(options);
3314
- if (options.type === "sqlite") return (await import("./chunks/sqlite-B4ussRTS.js")).getDatabase(options);
3315
- if (options.type === "duckdb") return (await import("./chunks/duckdb-D_Wu36JL.js")).getDatabase(options);
3205
+ if (options.type === "sqlite") return (await import("./chunks/sqlite-dbhMszEJ.js")).getDatabase(options);
3206
+ if (options.type === "duckdb") return (await import("./chunks/duckdb-pKOTggPW.js")).getDatabase(options);
3316
3207
  if (options.type === "json") return (await Promise.resolve().then(() => json_exports)).getDatabase(options);
3317
3208
  throw new Error("Invalid database type");
3318
3209
  }