@happyvertical/sql 0.80.3 → 0.80.5

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.
package/dist/index.js CHANGED
@@ -310,6 +310,34 @@ function countValueFromRow(value) {
310
310
  //#endregion
311
311
  //#region src/shared/types.ts
312
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
+ /**
313
341
  * Resolves a SchemasOption to its concrete value.
314
342
  *
315
343
  * Adapters that need schemas (JSON, DuckDB) call this to unwrap the
@@ -1472,60 +1500,27 @@ async function getDatabase$2(options) {
1472
1500
  const transaction = async (callback) => {
1473
1501
  try {
1474
1502
  await connection.run("BEGIN TRANSACTION");
1475
- const result = await callback({
1476
- url,
1477
- client: connection,
1478
- insert,
1479
- get,
1480
- list,
1481
- update,
1482
- upsert,
1483
- getOrInsert,
1484
- delete: deleteRecords,
1485
- count,
1486
- table,
1487
- many,
1488
- single,
1489
- pluck,
1490
- execute,
1491
- query,
1492
- oo,
1493
- oO,
1494
- ox,
1495
- xx,
1496
- tableExists,
1497
- syncSchema,
1498
- transaction
1499
- });
1503
+ const result = await callback(createTransactionScope());
1500
1504
  await connection.run("COMMIT");
1501
1505
  return result;
1502
1506
  } catch (error) {
1503
- await connection.run("ROLLBACK");
1507
+ try {
1508
+ await connection.run("ROLLBACK");
1509
+ } catch {}
1504
1510
  throw error;
1505
1511
  }
1506
1512
  };
1507
1513
  /**
1508
- * Begins a new transaction and returns a handle for manual control
1509
- *
1510
- * Unlike transaction(), this gives you explicit control over commit/rollback.
1511
- * Ideal for test isolation where you want to rollback after each test.
1514
+ * Builds the transaction-scoped interface handed to a transaction callback.
1512
1515
  *
1513
- * @returns Promise resolving to a TransactionHandle
1516
+ * The only difference from the top-level interface is `transaction`, which
1517
+ * refuses to nest. DuckDB has no SAVEPOINT, so there is no way to re-enter
1518
+ * the transaction already in progress; re-exposing the top-level
1519
+ * `transaction` (the previous behaviour) sent the nested call into a second
1520
+ * BEGIN on the same connection, which throws and then rolls back the
1521
+ * *enclosing* transaction, silently discarding its work.
1514
1522
  */
1515
- const beginTransaction = async () => {
1516
- await connection.run("BEGIN TRANSACTION");
1517
- let active = true;
1518
- const commit = async () => {
1519
- if (!active) throw new DatabaseError("Transaction already ended", {});
1520
- await connection.run("COMMIT");
1521
- active = false;
1522
- };
1523
- const rollback = async () => {
1524
- if (!active) throw new DatabaseError("Transaction already ended", {});
1525
- await connection.run("ROLLBACK");
1526
- active = false;
1527
- };
1528
- const isActive = () => active;
1523
+ const createTransactionScope = () => {
1529
1524
  return {
1530
1525
  url,
1531
1526
  client: connection,
@@ -1549,7 +1544,35 @@ async function getDatabase$2(options) {
1549
1544
  xx,
1550
1545
  tableExists,
1551
1546
  syncSchema,
1552
- transaction,
1547
+ transaction: async () => {
1548
+ throw new NestedTransactionError("Nested transactions are not supported by this adapter because its engine has no SAVEPOINT. Pass the existing transaction to the callee instead of opening a new one.", { adapter: "json" });
1549
+ }
1550
+ };
1551
+ };
1552
+ /**
1553
+ * Begins a new transaction and returns a handle for manual control
1554
+ *
1555
+ * Unlike transaction(), this gives you explicit control over commit/rollback.
1556
+ * Ideal for test isolation where you want to rollback after each test.
1557
+ *
1558
+ * @returns Promise resolving to a TransactionHandle
1559
+ */
1560
+ const beginTransaction = async () => {
1561
+ await connection.run("BEGIN TRANSACTION");
1562
+ 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 () => {
1569
+ if (!active) throw new DatabaseError("Transaction already ended", {});
1570
+ await connection.run("ROLLBACK");
1571
+ active = false;
1572
+ };
1573
+ const isActive = () => active;
1574
+ return {
1575
+ ...createTransactionScope(),
1553
1576
  commit,
1554
1577
  rollback,
1555
1578
  isActive
@@ -1963,6 +1986,9 @@ async function createDatabase(options, cacheKey) {
1963
1986
  database,
1964
1987
  max: poolMax
1965
1988
  });
1989
+ pool.on("error", (error) => {
1990
+ console.warn("Warning: PostgreSQL pool client error (connection discarded):", error instanceof Error ? error.message : String(error));
1991
+ });
1966
1992
  const originalEnd = pool.end.bind(pool);
1967
1993
  const evictFromCache = () => {
1968
1994
  for (const [key, cached] of connectionCache.entries()) if (cached.client === pool) connectionCache.delete(key);
@@ -2076,18 +2102,83 @@ async function createDatabase(options, cacheKey) {
2076
2102
  affected: (await executor.query(insertSql, values)).rowCount ?? 0
2077
2103
  };
2078
2104
  };
2079
- const executeNullAwarePostgresUpsertInTransaction = async (table, conflictColumns, serializedData) => {
2105
+ /**
2106
+ * Absorbs 'error' events for the clients currently checked out for a
2107
+ * transaction, keyed by client so the listener can be removed on release.
2108
+ */
2109
+ const txErrorAbsorbers = /* @__PURE__ */ new WeakMap();
2110
+ const releasedTxClients = /* @__PURE__ */ new WeakSet();
2111
+ /**
2112
+ * Checks out a pooled client for a transaction.
2113
+ *
2114
+ * pg-pool drops its own idle 'error' listener while a client is checked out,
2115
+ * so a backend that dies mid-transaction would emit an unhandled 'error' and
2116
+ * terminate the process. Absorb it here — the failure still reaches the
2117
+ * caller as a rejected query — and drop the listener again on release so
2118
+ * repeated checkouts of the same client cannot accumulate them.
2119
+ */
2120
+ const acquireTxClient = async () => {
2080
2121
  const txClient = await client.connect();
2122
+ releasedTxClients.delete(txClient);
2123
+ const absorb = () => {};
2124
+ txClient.on("error", absorb);
2125
+ txErrorAbsorbers.set(txClient, absorb);
2126
+ return txClient;
2127
+ };
2128
+ /**
2129
+ * Returns a transaction client to the pool exactly once.
2130
+ *
2131
+ * Every teardown path must run this, including the ones reached by a throwing
2132
+ * COMMIT or ROLLBACK — a client that is never released stays checked out for
2133
+ * the life of the process, and enough of them exhaust the pool. Releasing
2134
+ * twice throws, so the released clients are tracked rather than assumed.
2135
+ *
2136
+ * @param txClient - The pooled client backing a transaction
2137
+ * @param destroy - Destroy the connection instead of reusing it, for when the
2138
+ * transaction state could not be cleaned up
2139
+ */
2140
+ const releaseTxClient = (txClient, destroy = false) => {
2141
+ if (releasedTxClients.has(txClient)) return;
2142
+ releasedTxClients.add(txClient);
2143
+ const absorb = txErrorAbsorbers.get(txClient);
2144
+ if (absorb) {
2145
+ txClient.off("error", absorb);
2146
+ txErrorAbsorbers.delete(txClient);
2147
+ }
2148
+ txClient.release(destroy || void 0);
2149
+ };
2150
+ /**
2151
+ * Rolls back a failed transaction and returns its client to the pool.
2152
+ *
2153
+ * The connection may still be inside a transaction, so ROLLBACK is attempted
2154
+ * to normalize it. If that works the connection is clean and worth reusing;
2155
+ * if it does not, the state is unknown and the connection is destroyed rather
2156
+ * than handed to unrelated code later.
2157
+ *
2158
+ * @returns The rollback failure, if the connection could not be normalized
2159
+ */
2160
+ const discardTxClient = async (txClient) => {
2161
+ try {
2162
+ await txClient.query("ROLLBACK");
2163
+ releaseTxClient(txClient);
2164
+ return;
2165
+ } catch (rollbackError) {
2166
+ releaseTxClient(txClient, true);
2167
+ return rollbackError;
2168
+ }
2169
+ };
2170
+ const executeNullAwarePostgresUpsertInTransaction = async (table, conflictColumns, serializedData) => {
2171
+ const txClient = await acquireTxClient();
2081
2172
  try {
2082
2173
  await txClient.query("BEGIN");
2083
2174
  const result = await executeNullAwarePostgresUpsert(txClient, table, conflictColumns, serializedData);
2084
2175
  await txClient.query("COMMIT");
2176
+ releaseTxClient(txClient);
2085
2177
  return result;
2086
2178
  } catch (error) {
2087
- await txClient.query("ROLLBACK");
2179
+ const rollbackError = await discardTxClient(txClient);
2180
+ if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
2088
2181
  throw error;
2089
- } finally {
2090
- txClient.release();
2091
2182
  }
2092
2183
  };
2093
2184
  const executePostgresUpsert = async (executor, table, conflictColumns, data, options, acquireTransaction) => {
@@ -2583,7 +2674,7 @@ async function createDatabase(options, cacheKey) {
2583
2674
  * @returns Promise resolving to callback result
2584
2675
  */
2585
2676
  const transaction = async (callback) => {
2586
- const txClient = await client.connect();
2677
+ const txClient = await acquireTxClient();
2587
2678
  try {
2588
2679
  await txClient.query("BEGIN");
2589
2680
  const txDb = {
@@ -2719,12 +2810,12 @@ async function createDatabase(options, cacheKey) {
2719
2810
  };
2720
2811
  const result = await callback(txDb);
2721
2812
  await txClient.query("COMMIT");
2813
+ releaseTxClient(txClient);
2722
2814
  return result;
2723
2815
  } catch (error) {
2724
- await txClient.query("ROLLBACK");
2816
+ const rollbackError = await discardTxClient(txClient);
2817
+ if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
2725
2818
  throw error;
2726
- } finally {
2727
- txClient.release();
2728
2819
  }
2729
2820
  };
2730
2821
  /**
@@ -2736,21 +2827,29 @@ async function createDatabase(options, cacheKey) {
2736
2827
  * @returns Promise resolving to a TransactionHandle
2737
2828
  */
2738
2829
  const beginTransaction = async () => {
2739
- const txClient = await client.connect();
2740
- await txClient.query("BEGIN");
2830
+ const txClient = await acquireTxClient();
2831
+ try {
2832
+ await txClient.query("BEGIN");
2833
+ } catch (error) {
2834
+ releaseTxClient(txClient, true);
2835
+ throw error;
2836
+ }
2741
2837
  let active = true;
2742
- const commit = async () => {
2743
- if (!active) throw new DatabaseError("Transaction already ended", {});
2744
- await txClient.query("COMMIT");
2745
- active = false;
2746
- txClient.release();
2747
- };
2748
- const rollback = async () => {
2838
+ const end = async (command) => {
2749
2839
  if (!active) throw new DatabaseError("Transaction already ended", {});
2750
- await txClient.query("ROLLBACK");
2751
- active = false;
2752
- txClient.release();
2840
+ try {
2841
+ await txClient.query(command);
2842
+ active = false;
2843
+ releaseTxClient(txClient);
2844
+ } catch (error) {
2845
+ active = false;
2846
+ const rollbackError = await discardTxClient(txClient);
2847
+ if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
2848
+ throw error;
2849
+ }
2753
2850
  };
2851
+ const commit = () => end("COMMIT");
2852
+ const rollback = () => end("ROLLBACK");
2754
2853
  const isActive = () => active;
2755
2854
  const txHandle = {
2756
2855
  url,
@@ -3177,8 +3276,8 @@ async function getDatabase(options = {}) {
3177
3276
  }));
3178
3277
  if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) options.type = "sqlite";
3179
3278
  if (options.type === "postgres") return (await Promise.resolve().then(() => postgres_exports)).getDatabase(options);
3180
- if (options.type === "sqlite") return (await import("./chunks/sqlite-YgU9LCVy.js")).getDatabase(options);
3181
- if (options.type === "duckdb") return (await import("./chunks/duckdb-DykBBzd6.js")).getDatabase(options);
3279
+ if (options.type === "sqlite") return (await import("./chunks/sqlite-B4ussRTS.js")).getDatabase(options);
3280
+ if (options.type === "duckdb") return (await import("./chunks/duckdb-D_Wu36JL.js")).getDatabase(options);
3182
3281
  if (options.type === "json") return (await Promise.resolve().then(() => json_exports)).getDatabase(options);
3183
3282
  throw new Error("Invalid database type");
3184
3283
  }
@@ -3240,6 +3339,6 @@ var src_default = {
3240
3339
  /** @internal */
3241
3340
  var PACKAGE_VERSION_INITIALIZED = true;
3242
3341
  //#endregion
3243
- export { DatabaseSchemaManager, PACKAGE_VERSION_INITIALIZED, assertCanExportDatabase, assertCanImportDatabase, bucketExpr, buildAggregate, buildWhere, checkExpectedTables, checkRelationship, checkUniqueColumn, clearConnectionCache, clearPostgresConnectionCache, convertUniqueIndexesToInlineConstraints, createPostgresDatabase, databaseNameFromUrl, src_default as default, dropPostgresDatabase, dumpPostgresDatabase, escapeSqlValue, formatDbError, getDatabase, isLocalDatabaseUrl, postgresEnvFromUrl, redactDatabaseUrl, resolveSchemas, restorePostgresDatabase, runDoctor, syncSchema, tableExists, validateColumnName };
3342
+ export { DatabaseSchemaManager, NestedTransactionError, PACKAGE_VERSION_INITIALIZED, assertCanExportDatabase, assertCanImportDatabase, bucketExpr, buildAggregate, buildWhere, checkExpectedTables, checkRelationship, checkUniqueColumn, clearConnectionCache, clearPostgresConnectionCache, convertUniqueIndexesToInlineConstraints, createPostgresDatabase, databaseNameFromUrl, src_default as default, dropPostgresDatabase, dumpPostgresDatabase, escapeSqlValue, formatDbError, getDatabase, isLocalDatabaseUrl, postgresEnvFromUrl, redactDatabaseUrl, resolveSchemas, restorePostgresDatabase, runDoctor, syncSchema, tableExists, validateColumnName };
3244
3343
 
3245
3344
  //# sourceMappingURL=index.js.map