@happyvertical/sql 0.80.6 → 0.81.1

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 (34) hide show
  1. package/README.md +49 -0
  2. package/dist/chunks/{schema-manager-a8RlxIE2.js → alter-utils-B900pfK_.js} +246 -2
  3. package/dist/chunks/alter-utils-B900pfK_.js.map +1 -0
  4. package/dist/chunks/{duckdb-D_Wu36JL.js → duckdb-CWy7Nziq.js} +45 -21
  5. package/dist/chunks/duckdb-CWy7Nziq.js.map +1 -0
  6. package/dist/chunks/{sqlite-B4ussRTS.js → sqlite-kBmA-x_C.js} +68 -29
  7. package/dist/chunks/sqlite-kBmA-x_C.js.map +1 -0
  8. package/dist/chunks/{sqlite-native-BgiF4301.js → sqlite-native-Dnlo3Tux.js} +73 -21
  9. package/dist/chunks/sqlite-native-Dnlo3Tux.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 +715 -733
  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/shared/utils.d.ts +31 -0
  24. package/dist/shared/utils.d.ts.map +1 -1
  25. package/dist/sqlite-native.d.ts.map +1 -1
  26. package/dist/sqlite.d.ts +20 -0
  27. package/dist/sqlite.d.ts.map +1 -1
  28. package/package.json +5 -5
  29. package/dist/chunks/alter-utils-K7tiuj5L.js +0 -135
  30. package/dist/chunks/alter-utils-K7tiuj5L.js.map +0 -1
  31. package/dist/chunks/duckdb-D_Wu36JL.js.map +0 -1
  32. package/dist/chunks/schema-manager-a8RlxIE2.js.map +0 -1
  33. package/dist/chunks/sqlite-B4ussRTS.js.map +0 -1
  34. 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, p as resolveInsertColumns, r as generateAddColumnStatement, s as validateIndexName, t as escapeQuotedIdentifier, u as buildWhere } from "./chunks/alter-utils-B900pfK_.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,
@@ -430,21 +388,36 @@ async function createJSONConnection(options) {
430
388
  * This function only loads data - it assumes the table already exists with proper schema.
431
389
  * Used after DDL creates the table to load persisted JSON data.
432
390
  *
391
+ * An unreadable or malformed file is tolerated with a warning - the table
392
+ * structure is what matters and there is no data to lose.
393
+ *
394
+ * A failure to insert records that DID parse is logged loudly rather than
395
+ * swallowed with the old dismissive "that's okay" warning: the resulting empty
396
+ * table is a real surprise the caller should see (issue #1132). It is logged
397
+ * rather than thrown so that one unloadable table cannot abort the load of every
398
+ * other table sharing the same data directory or syncSchema() call - both of
399
+ * those paths already load each table independently.
400
+ *
433
401
  * @param connection - DuckDB connection
434
402
  * @param tableName - Name of the table to load data into
435
403
  * @param filePath - Path to the JSON file
436
404
  */
437
405
  async function loadJSONData(connection, tableName, filePath) {
406
+ let records;
438
407
  try {
439
408
  const { readFile } = await import("node:fs/promises");
440
409
  const jsonContent = await readFile(filePath, "utf-8");
441
- const records = JSON.parse(jsonContent);
442
- if (Array.isArray(records) && records.length > 0) {
443
- await insertRecordsWithCast(connection, tableName, records);
444
- console.log(`[json-adapter] Loaded ${records.length} records into ${tableName} from JSON`);
445
- }
410
+ records = JSON.parse(jsonContent);
411
+ } catch (error) {
412
+ console.warn(`[json-adapter] Could not read data for ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
413
+ return;
414
+ }
415
+ if (!Array.isArray(records) || records.length === 0) return;
416
+ try {
417
+ const inserted = await insertRecordsWithCast(connection, tableName, records);
418
+ if (inserted > 0) console.log(`[json-adapter] Loaded ${inserted} records into ${tableName} from JSON`);
446
419
  } catch (error) {
447
- console.warn(`[json-adapter] Could not load data for ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
420
+ console.error(`[json-adapter] Failed to load data for ${tableName}; the table will be empty:`, error);
448
421
  }
449
422
  }
450
423
  /**
@@ -503,7 +476,7 @@ async function loadJSONTables(connection, dataDir, providedSchemas = {}, pending
503
476
  }
504
477
  else {
505
478
  if (eagerLoadTables) try {
506
- await connection.run(`CREATE TABLE IF NOT EXISTS "${tableName}" AS SELECT * FROM read_json_auto('${filePath}')`);
479
+ await connection.run(`CREATE TABLE IF NOT EXISTS "${escapeQuotedIdentifier(tableName)}" AS SELECT * FROM read_json_auto('${escapeStringLiteral(filePath)}')`);
507
480
  inferredSchemaTables.add(tableName);
508
481
  console.log(`[json-adapter] Eagerly created ${tableName} with inferred schema (no UNIQUE constraints)`);
509
482
  } catch (error) {
@@ -635,25 +608,87 @@ async function createTableFromSchema(connection, tableName, schema) {
635
608
  }
636
609
  }
637
610
  /**
611
+ * Reads the column names of an existing table, keyed by lowercase name
612
+ *
613
+ * The table-name predicate is case-insensitive: DuckDB resolves identifiers
614
+ * case-insensitively, so a data file named `Items.json` can back a table
615
+ * created as `items`. Matching table_name exactly would find no columns and
616
+ * make the loader wrongly report that every field is unmatched (issue #1132).
617
+ *
618
+ * @param connection - DuckDB connection
619
+ * @param tableName - Name of the table to describe
620
+ * @returns Map of lowercase column name to the column's declared name
621
+ */
622
+ async function getTableColumns(connection, tableName) {
623
+ const reader = await connection.runAndReadAll("SELECT column_name FROM information_schema.columns WHERE LOWER(table_name) = LOWER($1) ORDER BY ordinal_position", [tableName]);
624
+ return new Map(reader.getRowObjects().map((row) => [String(row.column_name).toLowerCase(), String(row.column_name)]));
625
+ }
626
+ /**
638
627
  * Inserts records with proper CAST handling for empty strings
639
628
  *
640
629
  * This helper avoids using read_json() with auto_detect, which causes DuckDB
641
630
  * to re-infer column types from data (causing ANY type for empty strings).
642
631
  * Instead, it uses parameterized INSERT with CAST to maintain table schema.
643
632
  *
633
+ * Used only for loading a JSON data file, where records having different key
634
+ * sets is normal input rather than a caller error: a document with an optional
635
+ * field omits that key entirely on the records that lack it. Columns are
636
+ * therefore unioned across ALL records and any record missing a key binds NULL.
637
+ * Deriving them from the first record alone bound `undefined` for a missing key
638
+ * (which DuckDB rejects, losing every row) and dropped keys the first record
639
+ * did not have (losing the column silently). See issue #1132.
640
+ *
641
+ * The caller-facing insert() API is a separate implementation (it resolves
642
+ * columns through `resolveInsertColumns`) and is not affected by this
643
+ * file-loading tolerance.
644
+ *
644
645
  * @param connection - DuckDB connection
645
646
  * @param tableName - Name of the table to insert into
646
647
  * @param records - Array of records to insert
648
+ * @returns Number of records inserted (0 if there was nothing loadable)
649
+ * @throws DatabaseError if the resolved records cannot be inserted
647
650
  */
648
651
  async function insertRecordsWithCast(connection, tableName, records) {
649
- if (records.length === 0) return;
650
- const keys = Object.keys(records[0]);
652
+ if (records.length === 0) return 0;
653
+ const objectRecords = records.filter((record) => record != null && typeof record === "object" && !Array.isArray(record));
654
+ if (objectRecords.length === 0) return 0;
655
+ const unionedKeys = [];
656
+ const seenKeys = /* @__PURE__ */ new Set();
657
+ for (const record of objectRecords) for (const key of Object.keys(record)) if (!seenKeys.has(key)) {
658
+ seenKeys.add(key);
659
+ unionedKeys.push(key);
660
+ }
661
+ if (unionedKeys.length === 0) {
662
+ console.warn(`[json-adapter] Skipping data load for ${tableName}: records have no fields`);
663
+ return 0;
664
+ }
665
+ const tableColumns = await getTableColumns(connection, tableName);
666
+ const keys = [];
667
+ const columnNames = [];
668
+ const unmatchedKeys = [];
669
+ const claimedColumns = /* @__PURE__ */ new Set();
670
+ for (const key of unionedKeys) {
671
+ const column = tableColumns.get(key.toLowerCase());
672
+ if (column === void 0 || claimedColumns.has(column.toLowerCase())) unmatchedKeys.push(key);
673
+ else {
674
+ claimedColumns.add(column.toLowerCase());
675
+ keys.push(key);
676
+ columnNames.push(column);
677
+ }
678
+ }
679
+ if (unmatchedKeys.length > 0) console.warn(`[json-adapter] Ignoring ${unmatchedKeys.length} field(s) in ${tableName} data with no matching column: ${unmatchedKeys.join(", ")}`);
680
+ if (keys.length === 0) throw new DatabaseError(`No fields in the ${tableName} data file match a column on the table`, {
681
+ tableName,
682
+ fields: unionedKeys,
683
+ columns: [...tableColumns.values()]
684
+ });
685
+ const insertableRecords = objectRecords.filter((record) => keys.some((key) => key in record));
651
686
  const values = [];
652
687
  let paramIdx = 1;
653
- const placeholders = records.map((record) => {
688
+ const placeholders = insertableRecords.map((record) => {
654
689
  return `(${keys.map((key) => {
655
690
  const value = record[key];
656
- if (value === null) return "NULL";
691
+ if (value === null || value === void 0) return "NULL";
657
692
  else if (value === "" && typeof value === "string") {
658
693
  values.push(value);
659
694
  return `CAST($${paramIdx++} AS TEXT)`;
@@ -672,8 +707,18 @@ async function insertRecordsWithCast(connection, tableName, records) {
672
707
  }
673
708
  }).join(", ")})`;
674
709
  }).join(", ");
675
- const sql = `INSERT INTO ${tableName} (${keys.join(", ")}) VALUES ${placeholders}`;
676
- await connection.run(sql, values);
710
+ const sql = `INSERT INTO "${tableName}" (${columnNames.map((name) => `"${name}"`).join(", ")}) VALUES ${placeholders}`;
711
+ try {
712
+ await connection.run(sql, values);
713
+ } catch (e) {
714
+ throw new DatabaseError("Failed to load records into table", {
715
+ tableName,
716
+ recordCount: insertableRecords.length,
717
+ columns: columnNames,
718
+ originalError: formatDbError(e)
719
+ });
720
+ }
721
+ return insertableRecords.length;
677
722
  }
678
723
  /**
679
724
  * Validates that a table name is valid (alphanumeric + underscores only)
@@ -708,6 +753,7 @@ async function getDatabase$2(options) {
708
753
  const { connection, pendingJSONFiles, inferredSchemaTables } = await createJSONConnection(options);
709
754
  const writeStrategy = options.writeStrategy || "immediate";
710
755
  const { url } = options;
756
+ const connectionLock = createTransactionLock("json", options.transactionQueueTimeout);
711
757
  /**
712
758
  * Inserts one or more records into a table
713
759
  *
@@ -727,13 +773,14 @@ async function getDatabase$2(options) {
727
773
  operation: "insert",
728
774
  affected: 0
729
775
  };
730
- const keys = Object.keys(records[0]);
776
+ const keys = resolveInsertColumns(table, records);
777
+ validateColumnNames(keys);
731
778
  const values = [];
732
779
  let paramIdx = 1;
733
780
  const placeholders = records.map((record) => {
734
781
  return `(${keys.map((key) => {
735
782
  const value = record[key];
736
- if (value === null) return "NULL";
783
+ if (value === null || value === void 0) return "NULL";
737
784
  else if (value === "" && typeof value === "string") {
738
785
  values.push(value);
739
786
  return `CAST($${paramIdx++} AS TEXT)`;
@@ -833,6 +880,7 @@ async function getDatabase$2(options) {
833
880
  writeStrategy
834
881
  });
835
882
  const keys = Object.keys(data);
883
+ validateColumnNames(keys);
836
884
  const setClause = keys.map((key, idx) => `${key} = $${idx + 1}`).join(", ");
837
885
  const { sql: whereClause, values: whereValues } = buildWhere(where, keys.length + 1, "json");
838
886
  const dataValues = Object.values(data).map((value) => value instanceof Date ? value.toISOString() : value);
@@ -882,7 +930,8 @@ async function getDatabase$2(options) {
882
930
  const values = [];
883
931
  const paramIdx = { value: 1 };
884
932
  const assignments = keys.map((key) => {
885
- return `"${key}" = ${buildJSONValueExpression(data[key], values, paramIdx)}`;
933
+ const valueExpr = buildJSONValueExpression(data[key], values, paramIdx);
934
+ return `"${escapeQuotedIdentifier(key)}" = ${valueExpr}`;
886
935
  });
887
936
  const { sql: whereClause, values: whereValues } = buildWhere(conflictWhere, paramIdx.value, "json");
888
937
  const sql = `UPDATE "${table}" SET ${assignments.join(", ")} ${whereClause}`;
@@ -981,11 +1030,12 @@ async function getDatabase$2(options) {
981
1030
  paramIdx++;
982
1031
  }
983
1032
  }
984
- const conflict = conflictColumns.map((col) => `"${col}"`).join(", ");
985
- const quotedKeys = keys.map((key) => `"${key}"`).join(", ");
1033
+ const conflict = conflictColumns.map((col) => `"${escapeQuotedIdentifier(col)}"`).join(", ");
1034
+ const quotedKeys = keys.map((key) => `"${escapeQuotedIdentifier(key)}"`).join(", ");
986
1035
  const quotedUpdateSetParts = keys.map((key, i) => {
987
1036
  const part = updateSetParts[i];
988
- return `"${key}" = ${part.substring(part.indexOf("=") + 1).trim()}`;
1037
+ const valueExpr = part.substring(part.indexOf("=") + 1).trim();
1038
+ return `"${escapeQuotedIdentifier(key)}" = ${valueExpr}`;
989
1039
  });
990
1040
  const sql = `INSERT INTO "${table}" (${quotedKeys}) VALUES (${placeholders.join(", ")}) ON CONFLICT(${conflict}) DO UPDATE SET ${quotedUpdateSetParts.join(", ")}`;
991
1041
  try {
@@ -1101,6 +1151,7 @@ async function getDatabase$2(options) {
1101
1151
  * @returns Promise resolving to boolean indicating if the table exists
1102
1152
  */
1103
1153
  const tableExists = async (tableName) => {
1154
+ validateTableName(tableName);
1104
1155
  try {
1105
1156
  await connection.runAndReadAll(`SELECT * FROM ${tableName} LIMIT 1`);
1106
1157
  return true;
@@ -1124,6 +1175,7 @@ async function getDatabase$2(options) {
1124
1175
  * @param dataDir - Directory to write files
1125
1176
  */
1126
1177
  const exportTableToJSON = async (connection, table, dataDir) => {
1178
+ validateTableName(table);
1127
1179
  const filePath = join(dataDir, `${table}.json`);
1128
1180
  const schemaPath = join(dataDir, `${table}.schema.sql`);
1129
1181
  try {
@@ -1199,7 +1251,7 @@ async function getDatabase$2(options) {
1199
1251
  const { constants } = await import("node:fs");
1200
1252
  await access(filePath, constants.F_OK);
1201
1253
  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}')`);
1254
+ await connection.run(`CREATE TABLE "${tableName}" AS SELECT * FROM read_json_auto('${escapeStringLiteral(filePath)}')`);
1203
1255
  pendingJSONFiles.delete(tableName);
1204
1256
  console.log(`[json-adapter] Created ${tableName} with inferred schema (no UNIQUE constraints)`);
1205
1257
  } catch (error) {
@@ -1497,7 +1549,7 @@ async function getDatabase$2(options) {
1497
1549
  * @param callback - Function to execute within transaction
1498
1550
  * @returns Promise resolving to callback result
1499
1551
  */
1500
- const transaction = async (callback) => {
1552
+ const transaction = async (callback) => connectionLock.run(async () => {
1501
1553
  try {
1502
1554
  await connection.run("BEGIN TRANSACTION");
1503
1555
  const result = await callback(createTransactionScope());
@@ -1509,7 +1561,7 @@ async function getDatabase$2(options) {
1509
1561
  } catch {}
1510
1562
  throw error;
1511
1563
  }
1512
- };
1564
+ });
1513
1565
  /**
1514
1566
  * Builds the transaction-scoped interface handed to a transaction callback.
1515
1567
  *
@@ -1558,18 +1610,30 @@ async function getDatabase$2(options) {
1558
1610
  * @returns Promise resolving to a TransactionHandle
1559
1611
  */
1560
1612
  const beginTransaction = async () => {
1561
- await connection.run("BEGIN TRANSACTION");
1613
+ const releaseConnection = await connectionLock.acquire();
1614
+ try {
1615
+ await connection.run("BEGIN TRANSACTION");
1616
+ } catch (error) {
1617
+ releaseConnection();
1618
+ throw error;
1619
+ }
1562
1620
  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 () => {
1621
+ const end = async (command) => {
1569
1622
  if (!active) throw new DatabaseError("Transaction already ended", {});
1570
- await connection.run("ROLLBACK");
1571
- active = false;
1623
+ try {
1624
+ await connection.run(command);
1625
+ } catch (error) {
1626
+ try {
1627
+ await connection.run("ROLLBACK");
1628
+ } catch {}
1629
+ throw error;
1630
+ } finally {
1631
+ active = false;
1632
+ releaseConnection();
1633
+ }
1572
1634
  };
1635
+ const commit = () => end("COMMIT");
1636
+ const rollback = () => end("ROLLBACK");
1573
1637
  const isActive = () => active;
1574
1638
  return {
1575
1639
  ...createTransactionScope(),
@@ -1818,10 +1882,36 @@ function toVectorLiteral(embedding) {
1818
1882
  * @param pool - PostgreSQL connection pool
1819
1883
  * @returns VectorCapabilities implementation
1820
1884
  */
1885
+ /**
1886
+ * `type` is interpolated into `USING <type>`, which cannot be parameterized and
1887
+ * is not an identifier the quoting helpers cover. `VectorIndexOptions.type`
1888
+ * constrains it at compile time only, so anything reaching this at runtime —
1889
+ * a JavaScript caller, or a `filter as VectorIndexOptions` on parsed JSON — has
1890
+ * to be checked against the values pgvector actually offers.
1891
+ */
1892
+ var VECTOR_INDEX_TYPES = /* @__PURE__ */ new Set(["hnsw", "ivfflat"]);
1893
+ /**
1894
+ * `metric` needs the same treatment as `type`, for a less obvious reason.
1895
+ *
1896
+ * It never reaches SQL directly — `vectorOpsClass`/`vectorOperator` map it
1897
+ * through a closed switch with a safe default, so an unknown metric produces a
1898
+ * valid operator class. But it is also concatenated into the *generated index
1899
+ * name*, which is then interpolated into a quoted identifier, so a metric
1900
+ * carrying a `"` still ends that identifier. The safe-default mapping is what
1901
+ * makes this easy to miss: the part of the metric that looks dangerous is
1902
+ * handled, and the part that is actually dangerous is somewhere else.
1903
+ */
1904
+ var VECTOR_METRICS = /* @__PURE__ */ new Set([
1905
+ "cosine",
1906
+ "l2",
1907
+ "ip"
1908
+ ]);
1821
1909
  function createVectorCapabilities(pool) {
1822
1910
  return {
1823
1911
  async ensureColumn(table, column, dimensions) {
1824
1912
  if (!Number.isInteger(dimensions) || dimensions <= 0) throw new DatabaseError("Vector dimensions must be a positive integer", { dimensions });
1913
+ validateTableName$1(table);
1914
+ validateColumnName$1(column);
1825
1915
  try {
1826
1916
  await pool.query("CREATE EXTENSION IF NOT EXISTS vector");
1827
1917
  await pool.query(`ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "${column}" vector(${dimensions})`);
@@ -1835,8 +1925,22 @@ function createVectorCapabilities(pool) {
1835
1925
  }
1836
1926
  },
1837
1927
  async ensureIndex(table, column, options) {
1928
+ validateTableName$1(table);
1929
+ validateColumnName$1(column);
1838
1930
  const metric = options?.metric || "cosine";
1839
1931
  const indexType = options?.type || "hnsw";
1932
+ if (!VECTOR_INDEX_TYPES.has(indexType)) throw new DatabaseError("Unsupported vector index type", {
1933
+ table,
1934
+ column,
1935
+ type: indexType,
1936
+ supported: [...VECTOR_INDEX_TYPES]
1937
+ });
1938
+ if (!VECTOR_METRICS.has(metric)) throw new DatabaseError("Unsupported vector metric", {
1939
+ table,
1940
+ column,
1941
+ metric,
1942
+ supported: [...VECTOR_METRICS]
1943
+ });
1840
1944
  const opsClass = vectorOpsClass(metric);
1841
1945
  const indexName = `idx_${table}_${column}_${metric}_${indexType}`;
1842
1946
  try {
@@ -1851,7 +1955,10 @@ function createVectorCapabilities(pool) {
1851
1955
  }
1852
1956
  },
1853
1957
  async upsertVector(table, where, column, embedding) {
1958
+ validateTableName$1(table);
1959
+ validateColumnName$1(column);
1854
1960
  const keys = Object.keys(where);
1961
+ validateColumnNames(keys);
1855
1962
  const values = Object.values(where);
1856
1963
  const conditions = keys.map((key, i) => `"${key}" IS NOT DISTINCT FROM $${i + 1}`).join(" AND ");
1857
1964
  const vectorParam = `$${values.length + 1}`;
@@ -1867,6 +1974,8 @@ function createVectorCapabilities(pool) {
1867
1974
  }
1868
1975
  },
1869
1976
  async search(table, column, embedding, options) {
1977
+ validateTableName$1(table);
1978
+ validateColumnName$1(column);
1870
1979
  const metric = options?.metric || "cosine";
1871
1980
  const limit = options?.limit || 10;
1872
1981
  const op = vectorOperator(metric);
@@ -2014,6 +2123,15 @@ async function createDatabase(options, cacheKey) {
2014
2123
  /**
2015
2124
  * Serializes all values in an object for database storage
2016
2125
  */
2126
+ /**
2127
+ * The serializer the transaction-scoped interfaces have always used: none.
2128
+ *
2129
+ * Identity, deliberately — not even the `undefined`-dropping the pool-backed
2130
+ * `serializeRecord` does. Those copies bound `undefined` straight through to
2131
+ * `pg`, which stores NULL, whereas dropping the key omits the column and lets
2132
+ * its default apply. Preserving that is the point of this parameter.
2133
+ */
2134
+ const passThroughRecord = (record) => record;
2017
2135
  const serializeRecord = (record) => {
2018
2136
  const serialized = {};
2019
2137
  for (const [key, value] of Object.entries(record)) {
@@ -2190,230 +2308,6 @@ async function createDatabase(options, cacheKey) {
2190
2308
  return executeNullAwarePostgresUpsertInTransaction(table, conflictColumns, serializedData);
2191
2309
  };
2192
2310
  /**
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
2311
  * Parses a tagged template literal into a SQL query and values
2418
2312
  *
2419
2313
  * @param strings - Template strings
@@ -2433,111 +2327,533 @@ async function createDatabase(options, cacheKey) {
2433
2327
  };
2434
2328
  };
2435
2329
  /**
2436
- * Executes a SQL query using template literals and returns a single value
2330
+ * Builds the complete set of query methods bound to one PostgreSQL client.
2437
2331
  *
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
2332
+ * The pool-backed interface and every transaction-scoped interface are
2333
+ * instantiated from this one factory, so there is exactly one implementation
2334
+ * of each method. The transaction interfaces used to be hand-maintained
2335
+ * copies of these bodies, and both had drifted:
2458
2336
  *
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
2337
+ * - they called the client bare, so a failure inside a transaction threw the
2338
+ * raw `pg` error rather than `DatabaseError`, and `instanceof` answered
2339
+ * differently depending on whether the call was in a transaction. `pg`
2340
+ * exports its own class also named `DatabaseError`, so nothing a developer
2341
+ * would check by eye showed the difference (#1115);
2342
+ * - they re-exposed `tableExists` and `syncSchema` from the enclosing scope,
2343
+ * which close over the *pool*. Both therefore ran on a different connection
2344
+ * than the transaction, so `tx.tableExists()` could not see a table created
2345
+ * in the same transaction and `tx.syncSchema()` committed its DDL
2346
+ * immediately, surviving a rollback (#1111).
2477
2347
  *
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
2348
+ * Binding a client is the only difference between the two, so it is the only
2349
+ * thing this factory parameterizes.
2496
2350
  *
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
2351
+ * One thing is deliberately NOT unified: how `insert` and `update` serialize
2352
+ * values. The pool-backed methods run every value through `serializeRecord`,
2353
+ * which JSON-stringifies objects and arrays; the transaction-scoped copies
2354
+ * never did, so they handed the raw value to `pg`. That is not a cosmetic
2355
+ * difference — it decides what lands in the column, and no single answer is
2356
+ * right for both shapes:
2515
2357
  *
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.
2358
+ * ```
2359
+ * text[] column jsonb column
2360
+ * raw JS array accepted "invalid input syntax for type json"
2361
+ * JSON string "malformed accepted
2362
+ * array literal"
2363
+ * ```
2519
2364
  *
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
2365
+ * So collapsing the two would have silently broken array-column writes inside
2366
+ * a transaction while fixing jsonb-array writes, or the reverse — and would
2367
+ * have corrupted `bytea` writes, where a Buffer would become the JSON of a
2368
+ * Buffer with no error anywhere. Neither belongs in a fix for the error
2369
+ * contract, so the serializer is a parameter: each interface keeps exactly
2370
+ * the behaviour it had, and the difference is stated here instead of being an
2371
+ * accident of two copies drifting apart.
2372
+ *
2373
+ * @param executor - Pool or checked-out client every statement runs on
2374
+ * @param inTransaction - Whether `executor` is already inside a transaction
2375
+ * @param serialize - Applied to each record before `insert`/`update` builds
2376
+ * its SQL. Pass-through for the transaction-scoped interfaces.
2523
2377
  */
2524
- const query = async (sql, ...values) => {
2525
- const query = normalizePostgresRawQuery(sql, values);
2526
- try {
2527
- const result = await client.query(query.sql, query.values);
2378
+ const createClientMethods = (executor, inTransaction, serialize) => {
2379
+ /**
2380
+ * Inserts one or more records into a table
2381
+ *
2382
+ * @param table - Table name
2383
+ * @param data - Single record or array of records to insert
2384
+ * @returns Promise resolving to operation result
2385
+ * @throws Error if the insert operation fails
2386
+ *
2387
+ * @example Single record insert:
2388
+ * ```typescript
2389
+ * await db.insert('users', {
2390
+ * name: 'John Doe',
2391
+ * email: 'john@example.com'
2392
+ * });
2393
+ * ```
2394
+ *
2395
+ * @example Multiple record insert:
2396
+ * ```typescript
2397
+ * await db.insert('users', [
2398
+ * { name: 'John', email: 'john@example.com' },
2399
+ * { name: 'Jane', email: 'jane@example.com' }
2400
+ * ]);
2401
+ * ```
2402
+ */
2403
+ const insert = async (table, data) => {
2404
+ validateTableName$1(table);
2405
+ if (Array.isArray(data)) {
2406
+ if (data.length === 0) return {
2407
+ operation: "insert",
2408
+ affected: 0
2409
+ };
2410
+ const serializedRecords = data.map((record) => serialize(record));
2411
+ const keys = resolveInsertColumns(table, serializedRecords);
2412
+ validateColumnNames(keys);
2413
+ const placeholders = serializedRecords.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
2414
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
2415
+ const values = serializedRecords.flatMap((row) => keys.map((key) => row[key]));
2416
+ return {
2417
+ operation: "insert",
2418
+ affected: (await executor.query(query, values)).rowCount ?? 0
2419
+ };
2420
+ }
2421
+ const serializedData = serialize(data);
2422
+ const keys = Object.keys(serializedData);
2423
+ validateColumnNames(keys);
2424
+ const values = Object.values(serializedData);
2425
+ const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2426
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
2528
2427
  return {
2529
- rows: result.rows,
2530
- rowCount: result.rowCount ?? 0
2428
+ operation: "insert",
2429
+ affected: (await executor.query(query, values)).rowCount ?? 0
2531
2430
  };
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
- /**
2431
+ };
2432
+ /**
2433
+ * Retrieves a single record matching the where criteria
2434
+ *
2435
+ * @param table - Table name
2436
+ * @param where - Criteria to match records
2437
+ * @returns Promise resolving to query result
2438
+ */
2439
+ const get = async (table, where) => {
2440
+ validateTableName$1(table);
2441
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2442
+ if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
2443
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2444
+ try {
2445
+ return (await executor.query(query, values)).rows[0] || null;
2446
+ } catch (e) {
2447
+ throw new DatabaseError("Failed to retrieve record from table", {
2448
+ table,
2449
+ sql: query,
2450
+ values,
2451
+ originalError: formatDbError(e)
2452
+ });
2453
+ }
2454
+ };
2455
+ /**
2456
+ * Retrieves multiple records matching the where criteria
2457
+ *
2458
+ * @param table - Table name
2459
+ * @param where - Criteria to match records
2460
+ * @returns Promise resolving to array of records
2461
+ */
2462
+ const list = async (table, where) => {
2463
+ validateTableName$1(table);
2464
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2465
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2466
+ try {
2467
+ return (await executor.query(query, values)).rows;
2468
+ } catch (e) {
2469
+ throw new DatabaseError("Failed to list records from table", {
2470
+ table,
2471
+ sql: query,
2472
+ values,
2473
+ originalError: formatDbError(e)
2474
+ });
2475
+ }
2476
+ };
2477
+ /**
2478
+ * Updates records matching the where criteria
2479
+ *
2480
+ * @param table - Table name
2481
+ * @param where - Criteria to match records to update
2482
+ * @param data - New data to set
2483
+ * @returns Promise resolving to operation result
2484
+ */
2485
+ const update = async (table, where, data) => {
2486
+ validateTableName$1(table);
2487
+ const serializedData = serialize(data);
2488
+ const keys = Object.keys(serializedData);
2489
+ validateColumnNames(keys);
2490
+ const values = Object.values(serializedData);
2491
+ const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2492
+ const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
2493
+ if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
2494
+ const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
2495
+ try {
2496
+ return {
2497
+ operation: "update",
2498
+ affected: (await executor.query(sql, [...values, ...whereValues])).rowCount ?? 0
2499
+ };
2500
+ } catch (e) {
2501
+ throw new DatabaseError("Failed to update records in table", {
2502
+ table,
2503
+ sql,
2504
+ values: [...values, ...whereValues],
2505
+ originalError: formatDbError(e)
2506
+ });
2507
+ }
2508
+ };
2509
+ /**
2510
+ * Inserts a record or updates it if it already exists (UPSERT)
2511
+ *
2512
+ * @param table - Table name
2513
+ * @param conflictColumns - Columns that define the uniqueness constraint
2514
+ * @param data - Data to insert or update
2515
+ * @returns Promise resolving to operation result
2516
+ * @throws Error if the upsert operation fails
2517
+ */
2518
+ const upsert = async (table, conflictColumns, data, options) => {
2519
+ validateTableName$1(table);
2520
+ validateColumnNames([...conflictColumns, ...Object.keys(data)]);
2521
+ try {
2522
+ return await executePostgresUpsert(executor, table, conflictColumns, data, options, !inTransaction);
2523
+ } catch (e) {
2524
+ if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
2525
+ throw new DatabaseError("Failed to upsert record into table", {
2526
+ table,
2527
+ values: data,
2528
+ conflictColumns,
2529
+ originalError: formatDbError(e)
2530
+ });
2531
+ }
2532
+ };
2533
+ /**
2534
+ * Gets a record matching the where criteria or inserts it if not found
2535
+ *
2536
+ * @param table - Table name
2537
+ * @param where - Criteria to match existing record
2538
+ * @returns Promise resolving to the query result or insert result
2539
+ */
2540
+ const getOrInsert = async (table, where, data) => {
2541
+ validateTableName$1(table);
2542
+ const result = await get(table, where);
2543
+ if (result) return result;
2544
+ await insert(table, data);
2545
+ const inserted = await get(table, where);
2546
+ if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
2547
+ table,
2548
+ where,
2549
+ data
2550
+ });
2551
+ return inserted;
2552
+ };
2553
+ /**
2554
+ * Deletes records from a table matching the where criteria
2555
+ *
2556
+ * @param table - Table name
2557
+ * @param where - Criteria to match records for deletion
2558
+ * @returns Promise resolving to operation result with count of deleted rows
2559
+ * @throws Error if the delete operation fails
2560
+ */
2561
+ const deleteRecords = async (table, where) => {
2562
+ validateTableName$1(table);
2563
+ if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
2564
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2565
+ try {
2566
+ return {
2567
+ operation: "delete",
2568
+ affected: (await executor.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
2569
+ };
2570
+ } catch (e) {
2571
+ throw new DatabaseError("Failed to delete records from table", {
2572
+ table,
2573
+ where,
2574
+ originalError: formatDbError(e)
2575
+ });
2576
+ }
2577
+ };
2578
+ /**
2579
+ * Counts records in a table matching the where criteria
2580
+ *
2581
+ * @param table - Table name
2582
+ * @param where - Criteria to match records (optional, counts all if omitted)
2583
+ * @returns Promise resolving to count of matching records
2584
+ * @throws Error if the count operation fails
2585
+ */
2586
+ const count = async (table, where) => {
2587
+ validateTableName$1(table);
2588
+ try {
2589
+ if (!where || Object.keys(where).length === 0) {
2590
+ const result = await executor.query(`SELECT COUNT(*) as count FROM ${table}`);
2591
+ return Number(result.rows[0]?.count) || 0;
2592
+ }
2593
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2594
+ const result = await executor.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
2595
+ return Number(result.rows[0]?.count) || 0;
2596
+ } catch (e) {
2597
+ throw new DatabaseError("Failed to count records in table", {
2598
+ table,
2599
+ where,
2600
+ originalError: formatDbError(e)
2601
+ });
2602
+ }
2603
+ };
2604
+ /**
2605
+ * Creates a table-specific interface for simplified table operations
2606
+ *
2607
+ * @param tableName - Table name
2608
+ * @returns TableMethods interface for the specified table
2609
+ */
2610
+ const table = (tableName) => {
2611
+ return {
2612
+ insert: (data) => insert(tableName, data),
2613
+ get: (data) => get(tableName, data),
2614
+ list: (data) => list(tableName, data)
2615
+ };
2616
+ };
2617
+ /**
2618
+ * Executes a SQL query using template literals and returns a single value
2619
+ *
2620
+ * @param strings - Template strings
2621
+ * @param vars - Variables to interpolate into the query
2622
+ * @returns Promise resolving to a single value (first column of first row)
2623
+ */
2624
+ const pluck = async (strings, ...vars) => {
2625
+ const { sql, values } = parseTemplate(strings, ...vars);
2626
+ try {
2627
+ const firstRow = (await executor.query(sql, values)).rows[0];
2628
+ if (!firstRow) return null;
2629
+ return Object.values(firstRow)[0];
2630
+ } catch (e) {
2631
+ throw new DatabaseError("Failed to execute pluck query", {
2632
+ sql,
2633
+ values,
2634
+ originalError: formatDbError(e)
2635
+ });
2636
+ }
2637
+ };
2638
+ /**
2639
+ * Executes a SQL query using template literals and returns a single row
2640
+ *
2641
+ * @param strings - Template strings
2642
+ * @param vars - Variables to interpolate into the query
2643
+ * @returns Promise resolving to a single result record or null
2644
+ */
2645
+ const single = async (strings, ...vars) => {
2646
+ const { sql, values } = parseTemplate(strings, ...vars);
2647
+ try {
2648
+ return (await executor.query(sql, values)).rows[0] || null;
2649
+ } catch (e) {
2650
+ throw new DatabaseError("Failed to execute single query", {
2651
+ sql,
2652
+ values,
2653
+ originalError: formatDbError(e)
2654
+ });
2655
+ }
2656
+ };
2657
+ /**
2658
+ * Executes a SQL query using template literals and returns multiple rows
2659
+ *
2660
+ * @param strings - Template strings
2661
+ * @param vars - Variables to interpolate into the query
2662
+ * @returns Promise resolving to array of result records
2663
+ */
2664
+ const many = async (strings, ...vars) => {
2665
+ const { sql, values } = parseTemplate(strings, ...vars);
2666
+ try {
2667
+ return (await executor.query(sql, values)).rows;
2668
+ } catch (e) {
2669
+ throw new DatabaseError("Failed to execute many query", {
2670
+ sql,
2671
+ values,
2672
+ originalError: formatDbError(e)
2673
+ });
2674
+ }
2675
+ };
2676
+ /**
2677
+ * Executes a SQL query using template literals without returning results
2678
+ *
2679
+ * @param strings - Template strings
2680
+ * @param vars - Variables to interpolate into the query
2681
+ * @returns Promise that resolves when the query completes
2682
+ */
2683
+ const execute = async (strings, ...vars) => {
2684
+ const { sql, values } = parseTemplate(strings, ...vars);
2685
+ try {
2686
+ await executor.query(sql, values);
2687
+ } catch (e) {
2688
+ throw new DatabaseError("Failed to execute query", {
2689
+ sql,
2690
+ values,
2691
+ originalError: formatDbError(e)
2692
+ });
2693
+ }
2694
+ };
2695
+ /**
2696
+ * Executes a raw SQL query with parameterized values
2697
+ *
2698
+ * Uses PostgreSQL-native placeholders ($1, $2, ...). Legacy ? placeholders
2699
+ * are converted only when the placeholder count matches the supplied values,
2700
+ * so Postgres operators such as JSONB ? remain intact.
2701
+ *
2702
+ * @param sql - SQL query string
2703
+ * @param values - Variables to use as parameters
2704
+ * @returns Promise resolving to query result with rows and count
2705
+ */
2706
+ const query = async (sql, ...values) => {
2707
+ const query = normalizePostgresRawQuery(sql, values);
2708
+ try {
2709
+ const result = await executor.query(query.sql, query.values);
2710
+ return {
2711
+ rows: result.rows,
2712
+ rowCount: result.rowCount ?? 0
2713
+ };
2714
+ } catch (e) {
2715
+ throw new DatabaseError("Failed to execute raw query", {
2716
+ sql: query.sql,
2717
+ values: query.values,
2718
+ originalError: formatDbError(e)
2719
+ });
2720
+ }
2721
+ };
2722
+ /**
2723
+ * Checks if a table exists in the database
2724
+ *
2725
+ * @param tableName - Name of the table to check
2726
+ * @returns Promise resolving to boolean indicating if the table exists
2727
+ */
2728
+ const tableExists = async (tableName) => {
2729
+ return (await executor.query(`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = 'public')`, [tableName])).rows[0].exists;
2730
+ };
2731
+ /**
2732
+ * Synchronizes database schema with provided SQL DDL
2733
+ * Creates tables if they don't exist and adds missing columns
2734
+ * Also executes CREATE INDEX statements
2735
+ *
2736
+ * @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements
2737
+ * @returns Promise that resolves when schema is synchronized
2738
+ */
2739
+ /**
2740
+ * Runs a step whose failure `syncSchema` deliberately tolerates.
2741
+ *
2742
+ * Logging and continuing is self-contained on the pool. Inside a
2743
+ * transaction it is not: a failed statement puts PostgreSQL in the aborted
2744
+ * state, and `COMMIT` on an aborted transaction *succeeds* — it reports a
2745
+ * ROLLBACK command tag and no error. The caller is told the transaction
2746
+ * committed while every write in it was discarded, which is the silent
2747
+ * data loss this whole epic is about, re-entering through the schema path.
2748
+ *
2749
+ * A savepoint scopes the failure to the step, which is what "log it and
2750
+ * continue" was always supposed to mean.
2751
+ */
2752
+ const runTolerated = async (work) => {
2753
+ if (!inTransaction) return work();
2754
+ savepointSequence += 1;
2755
+ const name = `hv_sync_${savepointSequence}`;
2756
+ await executor.query(`SAVEPOINT ${name}`);
2757
+ try {
2758
+ const result = await work();
2759
+ await executor.query(`RELEASE SAVEPOINT ${name}`);
2760
+ return result;
2761
+ } catch (error) {
2762
+ await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
2763
+ await executor.query(`RELEASE SAVEPOINT ${name}`);
2764
+ throw error;
2765
+ }
2766
+ };
2767
+ const syncSchema = async (schema) => {
2768
+ const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
2769
+ const createIndexRegex = /CREATE (UNIQUE )?INDEX (IF NOT EXISTS )?"?(\w+)"? ON "?(\w+)"?\s*\(([^)]+)\)/i;
2770
+ const indexNames = [];
2771
+ for (const command of commands) {
2772
+ const indexMatch = command.trim().match(createIndexRegex);
2773
+ if (indexMatch) indexNames.push(indexMatch[3]);
2774
+ }
2775
+ const existingIndexes = /* @__PURE__ */ new Set();
2776
+ if (indexNames.length > 0) {
2777
+ const result = await executor.query(`SELECT indexname FROM pg_indexes
2778
+ WHERE schemaname = 'public' AND indexname = ANY($1::text[])`, [indexNames]);
2779
+ for (const row of result.rows) existingIndexes.add(row.indexname);
2780
+ }
2781
+ for (const command of commands) {
2782
+ const trimmedCommand = command.trim();
2783
+ const tableMatch = trimmedCommand.match(/CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(([\s\S]+)\)/i);
2784
+ if (tableMatch) {
2785
+ const tableName = tableMatch[2];
2786
+ const columns = tableMatch[3].trim().split(",\n");
2787
+ if (!await tableExists(tableName)) await executor.query(trimmedCommand);
2788
+ else for (const column of columns) {
2789
+ const columnDef = column.trim();
2790
+ const columnMatch = columnDef.match(/"?(\w+)"?\s+(\w+[^,]*)/);
2791
+ if (columnMatch) {
2792
+ const columnName = columnMatch[1];
2793
+ if (columnName.toUpperCase() === "PRIMARY" || columnName.toUpperCase() === "FOREIGN" || columnName.toUpperCase() === "UNIQUE" || columnName.toUpperCase() === "CHECK" || columnName.toUpperCase() === "CONSTRAINT") continue;
2794
+ try {
2795
+ await runTolerated(async () => {
2796
+ if (!(await executor.query(`SELECT EXISTS (
2797
+ SELECT 1 FROM information_schema.columns
2798
+ WHERE table_name = $1
2799
+ AND column_name = $2
2800
+ AND table_schema = 'public'
2801
+ )`, [tableName, columnName])).rows[0].exists) {
2802
+ const alterCommand = `ALTER TABLE "${tableName}" ADD COLUMN ${columnDef}`;
2803
+ await executor.query(alterCommand);
2804
+ }
2805
+ });
2806
+ } catch (error) {
2807
+ console.error(`Error adding column ${columnName} to ${tableName}:`, error);
2808
+ }
2809
+ }
2810
+ }
2811
+ continue;
2812
+ }
2813
+ const indexMatch = trimmedCommand.match(createIndexRegex);
2814
+ if (indexMatch) {
2815
+ const indexName = indexMatch[3];
2816
+ const indexTableName = indexMatch[4];
2817
+ if (!existingIndexes.has(indexName)) try {
2818
+ await runTolerated(() => executor.query(trimmedCommand));
2819
+ existingIndexes.add(indexName);
2820
+ } catch (error) {
2821
+ console.warn(`Warning: Failed to create index ${indexName} on ${indexTableName}:`, error instanceof Error ? error.message : String(error));
2822
+ }
2823
+ continue;
2824
+ }
2825
+ if (trimmedCommand.toUpperCase().startsWith("CREATE ") && !trimmedCommand.toUpperCase().includes("CREATE TABLE")) try {
2826
+ await runTolerated(() => executor.query(trimmedCommand));
2827
+ } catch (error) {
2828
+ console.warn(`Warning: DDL statement may have failed:`, error instanceof Error ? error.message : String(error));
2829
+ }
2830
+ }
2831
+ };
2832
+ return {
2833
+ insert,
2834
+ get,
2835
+ list,
2836
+ update,
2837
+ upsert,
2838
+ getOrInsert,
2839
+ delete: deleteRecords,
2840
+ count,
2841
+ table,
2842
+ many,
2843
+ single,
2844
+ pluck,
2845
+ execute,
2846
+ query,
2847
+ oo: many,
2848
+ oO: single,
2849
+ ox: pluck,
2850
+ xx: execute,
2851
+ tableExists,
2852
+ syncSchema
2853
+ };
2854
+ };
2855
+ 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);
2856
+ /**
2541
2857
  * Acquire a pinned connection from the pool for session-scoped state such as
2542
2858
  * `pg_advisory_lock`. Every query on the returned handle runs on the same
2543
2859
  * client until {@link SessionHandle.release} drops the connection (which
@@ -2586,86 +2902,6 @@ async function createDatabase(options, cacheKey) {
2586
2902
  }
2587
2903
  };
2588
2904
  };
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
2905
  let savepointSequence = 0;
2670
2906
  /**
2671
2907
  * Builds the `transaction` exposed on a transaction-scoped interface.
@@ -2715,132 +2951,7 @@ async function createDatabase(options, cacheKey) {
2715
2951
  const txDb = {
2716
2952
  url,
2717
2953
  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,
2954
+ ...createClientMethods(txClient, true, passThroughRecord),
2844
2955
  transaction: createNestedTransaction(txClient, () => txDb)
2845
2956
  };
2846
2957
  const result = await callback(txDb);
@@ -2889,132 +3000,7 @@ async function createDatabase(options, cacheKey) {
2889
3000
  const txHandle = {
2890
3001
  url,
2891
3002
  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,
3003
+ ...createClientMethods(txClient, true, passThroughRecord),
3018
3004
  transaction: createNestedTransaction(txClient, () => txHandle),
3019
3005
  commit,
3020
3006
  rollback,
@@ -3022,10 +3008,6 @@ async function createDatabase(options, cacheKey) {
3022
3008
  };
3023
3009
  return txHandle;
3024
3010
  };
3025
- const oo = many;
3026
- const oO = single;
3027
- const ox = pluck;
3028
- const xx = execute;
3029
3011
  /**
3030
3012
  * Initialize database schemas from JSON manifest
3031
3013
  * Supports dependency resolution and schema overrides
@@ -3311,8 +3293,8 @@ async function getDatabase(options = {}) {
3311
3293
  }));
3312
3294
  if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) options.type = "sqlite";
3313
3295
  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);
3296
+ if (options.type === "sqlite") return (await import("./chunks/sqlite-kBmA-x_C.js")).getDatabase(options);
3297
+ if (options.type === "duckdb") return (await import("./chunks/duckdb-CWy7Nziq.js")).getDatabase(options);
3316
3298
  if (options.type === "json") return (await Promise.resolve().then(() => json_exports)).getDatabase(options);
3317
3299
  throw new Error("Invalid database type");
3318
3300
  }