@happyvertical/sql 0.89.4 → 0.89.6
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/README.md +7 -0
- package/dist/index.js +205 -49
- package/dist/index.js.map +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -353,6 +353,13 @@ child rollback therefore cannot silently remove a successful parent operation.
|
|
|
353
353
|
The enclosing commit or rollback drains all
|
|
354
354
|
accepted child scopes before ending the transaction. PostgreSQL pools separate
|
|
355
355
|
connections, so top-level transactions there run concurrently and never queue.
|
|
356
|
+
PostgreSQL CRUD helpers apply the same value serialization both inside and
|
|
357
|
+
outside a transaction: objects and arrays are encoded for JSON/JSONB columns,
|
|
358
|
+
dates use ISO timestamps, binary buffers and views retain their native `bytea`
|
|
359
|
+
representation, `null` remains SQL `NULL`, and scalar values pass through
|
|
360
|
+
unchanged. If a PostgreSQL statement aborts a transaction, later
|
|
361
|
+
operations and commit preserve the first statement error rather than replacing
|
|
362
|
+
it with the generic `25P02 current transaction is aborted` state error.
|
|
356
363
|
Every SQLite transaction-scoped operation is registered when its public method
|
|
357
364
|
is called. Ending a callback or invoking a manual handle's commit/rollback
|
|
358
365
|
seals the scope synchronously, drains operations already accepted, and rejects
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { mkdir, readdir, stat } from "node:fs/promises";
|
|
|
8
8
|
import { basename, extname, join } from "node:path";
|
|
9
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
10
|
import { webcrypto } from "node:crypto";
|
|
11
|
-
import { Pool } from "pg";
|
|
11
|
+
import { DatabaseError as DatabaseError$1, Pool } from "pg";
|
|
12
12
|
//#region \0rolldown/runtime.js
|
|
13
13
|
var __defProp = Object.defineProperty;
|
|
14
14
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -2337,21 +2337,13 @@ async function createDatabase(config) {
|
|
|
2337
2337
|
const serializeValue = (value) => {
|
|
2338
2338
|
if (value === null || value === void 0) return value;
|
|
2339
2339
|
if (value instanceof Date) return value.toISOString();
|
|
2340
|
+
if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) return value;
|
|
2340
2341
|
if (typeof value === "object") return JSON.stringify(value);
|
|
2341
2342
|
return value;
|
|
2342
2343
|
};
|
|
2343
2344
|
/**
|
|
2344
2345
|
* Serializes all values in an object for database storage
|
|
2345
2346
|
*/
|
|
2346
|
-
/**
|
|
2347
|
-
* The serializer the transaction-scoped interfaces have always used: none.
|
|
2348
|
-
*
|
|
2349
|
-
* Identity, deliberately — not even the `undefined`-dropping the pool-backed
|
|
2350
|
-
* `serializeRecord` does. Those copies bound `undefined` straight through to
|
|
2351
|
-
* `pg`, which stores NULL, whereas dropping the key omits the column and lets
|
|
2352
|
-
* its default apply. Preserving that is the point of this parameter.
|
|
2353
|
-
*/
|
|
2354
|
-
const passThroughRecord = (record) => record;
|
|
2355
2347
|
const serializeRecord = (record) => {
|
|
2356
2348
|
const serialized = {};
|
|
2357
2349
|
for (const [key, value] of Object.entries(record)) {
|
|
@@ -2505,12 +2497,183 @@ async function createDatabase(config) {
|
|
|
2505
2497
|
return rollbackError;
|
|
2506
2498
|
}
|
|
2507
2499
|
};
|
|
2500
|
+
/**
|
|
2501
|
+
* Keeps the first PostgreSQL statement failure attached to a transaction.
|
|
2502
|
+
*
|
|
2503
|
+
* PostgreSQL reports every later statement in an aborted transaction as
|
|
2504
|
+
* 25P02. Replacing the first failure with that generic state error hides the
|
|
2505
|
+
* actionable code and diagnostics from callers. The scoped executor instead
|
|
2506
|
+
* rethrows the first failure whenever PostgreSQL reports 25P02, and exposes
|
|
2507
|
+
* the recorded failure so a swallowed statement error cannot make COMMIT's
|
|
2508
|
+
* implicit rollback look successful.
|
|
2509
|
+
*/
|
|
2510
|
+
const createTransactionExecutor = (txClient) => {
|
|
2511
|
+
let firstError;
|
|
2512
|
+
let firstUnconfirmedError;
|
|
2513
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2514
|
+
const isRollbackToSavepoint = (query) => {
|
|
2515
|
+
const sql = typeof query === "string" ? query : typeof query === "object" && query !== null && "text" in query && typeof query.text === "string" ? query.text : void 0;
|
|
2516
|
+
if (sql === void 0) return false;
|
|
2517
|
+
let statement = sql.trimStart();
|
|
2518
|
+
while (statement.startsWith("--") || statement.startsWith("/*")) {
|
|
2519
|
+
if (statement.startsWith("--")) {
|
|
2520
|
+
const newline = statement.search(/[\r\n]/);
|
|
2521
|
+
if (newline === -1) return false;
|
|
2522
|
+
statement = statement.slice(newline + 1).trimStart();
|
|
2523
|
+
continue;
|
|
2524
|
+
}
|
|
2525
|
+
const commentEnd = statement.indexOf("*/", 2);
|
|
2526
|
+
if (commentEnd === -1) return false;
|
|
2527
|
+
statement = statement.slice(commentEnd + 2).trimStart();
|
|
2528
|
+
}
|
|
2529
|
+
return /^ROLLBACK\s+TO(?:\s+SAVEPOINT)?\s+/i.test(statement);
|
|
2530
|
+
};
|
|
2531
|
+
const errorCode = (error) => {
|
|
2532
|
+
if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
|
|
2533
|
+
};
|
|
2534
|
+
const preserveFirstError = (error) => {
|
|
2535
|
+
if (errorCode(error) === "25P02" && firstError !== void 0) return firstError;
|
|
2536
|
+
if (errorCode(error) === "25P02" && firstUnconfirmedError !== void 0) return firstUnconfirmedError;
|
|
2537
|
+
if (error instanceof DatabaseError$1) firstError ??= error;
|
|
2538
|
+
else firstUnconfirmedError ??= error;
|
|
2539
|
+
return error;
|
|
2540
|
+
};
|
|
2541
|
+
const track = (operation, query) => {
|
|
2542
|
+
const tracked = operation.then((result) => {
|
|
2543
|
+
if (isRollbackToSavepoint(query)) firstError = void 0;
|
|
2544
|
+
firstUnconfirmedError = void 0;
|
|
2545
|
+
return result;
|
|
2546
|
+
}).catch((error) => {
|
|
2547
|
+
throw preserveFirstError(error);
|
|
2548
|
+
});
|
|
2549
|
+
pending.add(tracked);
|
|
2550
|
+
tracked.finally(() => pending.delete(tracked)).catch(() => {});
|
|
2551
|
+
return tracked;
|
|
2552
|
+
};
|
|
2553
|
+
return {
|
|
2554
|
+
client: new Proxy(txClient, { get(target, property) {
|
|
2555
|
+
if (property === "query") return (...args) => {
|
|
2556
|
+
const query = args[0];
|
|
2557
|
+
const callbackIndex = typeof args.at(-1) === "function" ? args.length - 1 : -1;
|
|
2558
|
+
if (typeof query === "object" && query !== null && "submit" in query && typeof query.submit === "function") {
|
|
2559
|
+
if (!("handleError" in query) || typeof query.handleError !== "function" || !("handleReadyForQuery" in query) || typeof query.handleReadyForQuery !== "function") return Reflect.apply(target.query, target, args);
|
|
2560
|
+
const handleError = query.handleError;
|
|
2561
|
+
const handleReadyForQuery = query.handleReadyForQuery;
|
|
2562
|
+
let complete;
|
|
2563
|
+
const completion = new Promise((resolve) => {
|
|
2564
|
+
complete = resolve;
|
|
2565
|
+
});
|
|
2566
|
+
let completed = false;
|
|
2567
|
+
const finish = () => {
|
|
2568
|
+
if (completed) return;
|
|
2569
|
+
completed = true;
|
|
2570
|
+
query.handleError = handleError;
|
|
2571
|
+
query.handleReadyForQuery = handleReadyForQuery;
|
|
2572
|
+
pending.delete(completion);
|
|
2573
|
+
complete();
|
|
2574
|
+
};
|
|
2575
|
+
pending.add(completion);
|
|
2576
|
+
query.handleError = function(...handlerArgs) {
|
|
2577
|
+
const error = preserveFirstError(handlerArgs[0]);
|
|
2578
|
+
try {
|
|
2579
|
+
return Reflect.apply(handleError, this, [error, ...handlerArgs.slice(1)]);
|
|
2580
|
+
} finally {
|
|
2581
|
+
finish();
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
query.handleReadyForQuery = function(...handlerArgs) {
|
|
2585
|
+
try {
|
|
2586
|
+
const result = Reflect.apply(handleReadyForQuery, this, handlerArgs);
|
|
2587
|
+
if (isRollbackToSavepoint(query)) firstError = void 0;
|
|
2588
|
+
firstUnconfirmedError = void 0;
|
|
2589
|
+
return result;
|
|
2590
|
+
} finally {
|
|
2591
|
+
finish();
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
try {
|
|
2595
|
+
return Reflect.apply(target.query, target, args);
|
|
2596
|
+
} catch (error) {
|
|
2597
|
+
finish();
|
|
2598
|
+
throw preserveFirstError(error);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
if (callbackIndex !== -1) {
|
|
2602
|
+
const callback = args[callbackIndex];
|
|
2603
|
+
let complete;
|
|
2604
|
+
const completion = new Promise((resolve) => {
|
|
2605
|
+
complete = resolve;
|
|
2606
|
+
});
|
|
2607
|
+
pending.add(completion);
|
|
2608
|
+
args[callbackIndex] = (error, result) => {
|
|
2609
|
+
const callbackError = error === null || error === void 0 ? error : preserveFirstError(error);
|
|
2610
|
+
if (callbackError === null || callbackError === void 0) {
|
|
2611
|
+
if (isRollbackToSavepoint(query)) firstError = void 0;
|
|
2612
|
+
firstUnconfirmedError = void 0;
|
|
2613
|
+
}
|
|
2614
|
+
pending.delete(completion);
|
|
2615
|
+
complete();
|
|
2616
|
+
callback(callbackError, result);
|
|
2617
|
+
};
|
|
2618
|
+
try {
|
|
2619
|
+
return Reflect.apply(target.query, target, args);
|
|
2620
|
+
} catch (error) {
|
|
2621
|
+
pending.delete(completion);
|
|
2622
|
+
complete();
|
|
2623
|
+
throw preserveFirstError(error);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
const result = Reflect.apply(target.query, target, args);
|
|
2627
|
+
if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") return track(Promise.resolve(result), query);
|
|
2628
|
+
if (typeof result === "object" && result !== null && "once" in result && typeof result.once === "function") {
|
|
2629
|
+
let complete;
|
|
2630
|
+
const completion = new Promise((resolve) => {
|
|
2631
|
+
complete = resolve;
|
|
2632
|
+
});
|
|
2633
|
+
const finish = () => {
|
|
2634
|
+
pending.delete(completion);
|
|
2635
|
+
complete();
|
|
2636
|
+
};
|
|
2637
|
+
pending.add(completion);
|
|
2638
|
+
result.once("error", (error) => {
|
|
2639
|
+
preserveFirstError(error);
|
|
2640
|
+
finish();
|
|
2641
|
+
});
|
|
2642
|
+
result.once("end", () => {
|
|
2643
|
+
if (isRollbackToSavepoint(query)) firstError = void 0;
|
|
2644
|
+
firstUnconfirmedError = void 0;
|
|
2645
|
+
finish();
|
|
2646
|
+
});
|
|
2647
|
+
result.once("close", finish);
|
|
2648
|
+
}
|
|
2649
|
+
return result;
|
|
2650
|
+
};
|
|
2651
|
+
return Reflect.get(target, property, target);
|
|
2652
|
+
} }),
|
|
2653
|
+
executor: { query: (sql, values) => track(txClient.query(sql, values), sql) },
|
|
2654
|
+
clearError: () => {
|
|
2655
|
+
firstError = void 0;
|
|
2656
|
+
},
|
|
2657
|
+
drain: async () => {
|
|
2658
|
+
while (pending.size > 0) await Promise.allSettled([...pending]);
|
|
2659
|
+
},
|
|
2660
|
+
getError: () => firstError,
|
|
2661
|
+
getUnconfirmedError: () => firstUnconfirmedError
|
|
2662
|
+
};
|
|
2663
|
+
};
|
|
2664
|
+
const assertCommitted = (result, transactionState) => {
|
|
2665
|
+
if (result.command === "COMMIT") return;
|
|
2666
|
+
const queryError = transactionState?.getError() ?? transactionState?.getUnconfirmedError();
|
|
2667
|
+
if (queryError !== void 0) throw queryError;
|
|
2668
|
+
throw new DatabaseError("PostgreSQL rolled back the transaction at commit", { command: result.command });
|
|
2669
|
+
};
|
|
2508
2670
|
const executeNullAwarePostgresUpsertInTransaction = async (table, conflictColumns, serializedData) => {
|
|
2509
2671
|
const txClient = await acquireTxClient();
|
|
2510
2672
|
try {
|
|
2511
2673
|
await txClient.query("BEGIN");
|
|
2512
2674
|
const result = await executeNullAwarePostgresUpsert(txClient, table, conflictColumns, serializedData);
|
|
2513
|
-
await txClient.query("COMMIT");
|
|
2675
|
+
const commitResult = await txClient.query("COMMIT");
|
|
2676
|
+
assertCommitted(commitResult);
|
|
2514
2677
|
releaseTxClient(txClient);
|
|
2515
2678
|
return result;
|
|
2516
2679
|
} catch (error) {
|
|
@@ -2567,34 +2730,14 @@ async function createDatabase(config) {
|
|
|
2567
2730
|
* Binding a client is the only difference between the two, so it is the only
|
|
2568
2731
|
* thing this factory parameterizes.
|
|
2569
2732
|
*
|
|
2570
|
-
* One thing is deliberately NOT unified: how `insert` and `update` serialize
|
|
2571
|
-
* values. The pool-backed methods run every value through `serializeRecord`,
|
|
2572
|
-
* which JSON-stringifies objects and arrays; the transaction-scoped copies
|
|
2573
|
-
* never did, so they handed the raw value to `pg`. That is not a cosmetic
|
|
2574
|
-
* difference — it decides what lands in the column, and no single answer is
|
|
2575
|
-
* right for both shapes:
|
|
2576
|
-
*
|
|
2577
|
-
* ```
|
|
2578
|
-
* text[] column jsonb column
|
|
2579
|
-
* raw JS array accepted "invalid input syntax for type json"
|
|
2580
|
-
* JSON string "malformed accepted
|
|
2581
|
-
* array literal"
|
|
2582
|
-
* ```
|
|
2583
|
-
*
|
|
2584
|
-
* So collapsing the two would have silently broken array-column writes inside
|
|
2585
|
-
* a transaction while fixing jsonb-array writes, or the reverse — and would
|
|
2586
|
-
* have corrupted `bytea` writes, where a Buffer would become the JSON of a
|
|
2587
|
-
* Buffer with no error anywhere. Neither belongs in a fix for the error
|
|
2588
|
-
* contract, so the serializer is a parameter: each interface keeps exactly
|
|
2589
|
-
* the behaviour it had, and the difference is stated here instead of being an
|
|
2590
|
-
* accident of two copies drifting apart.
|
|
2591
|
-
*
|
|
2592
2733
|
* @param executor - Pool or checked-out client every statement runs on
|
|
2593
2734
|
* @param inTransaction - Whether `executor` is already inside a transaction
|
|
2594
2735
|
* @param serialize - Applied to each record before `insert`/`update` builds
|
|
2595
|
-
* its SQL
|
|
2736
|
+
* its SQL
|
|
2737
|
+
* @param clearTransactionError - Clears a statement error after a savepoint
|
|
2738
|
+
* has successfully restored the transaction to a usable state
|
|
2596
2739
|
*/
|
|
2597
|
-
const createClientMethods = (executor, inTransaction, serialize) => {
|
|
2740
|
+
const createClientMethods = (executor, inTransaction, serialize, clearTransactionError = () => {}) => {
|
|
2598
2741
|
/**
|
|
2599
2742
|
* Inserts one or more records into a table
|
|
2600
2743
|
*
|
|
@@ -2980,6 +3123,7 @@ async function createDatabase(config) {
|
|
|
2980
3123
|
return result;
|
|
2981
3124
|
} catch (error) {
|
|
2982
3125
|
await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
3126
|
+
clearTransactionError();
|
|
2983
3127
|
await executor.query(`RELEASE SAVEPOINT ${name}`);
|
|
2984
3128
|
throw error;
|
|
2985
3129
|
}
|
|
@@ -3143,26 +3287,27 @@ async function createDatabase(config) {
|
|
|
3143
3287
|
* rather than on a lock, so `deadlock_timeout` never fires and the process
|
|
3144
3288
|
* hangs until some unrelated timeout.
|
|
3145
3289
|
*
|
|
3146
|
-
* @param
|
|
3290
|
+
* @param executor - Error-preserving executor bound to the enclosing client
|
|
3147
3291
|
* @param scopeFor - Builds the interface handed to the nested callback
|
|
3148
3292
|
*/
|
|
3149
|
-
const createNestedTransaction = (
|
|
3293
|
+
const createNestedTransaction = (executor, scopeFor, clearTransactionError) => {
|
|
3150
3294
|
const nestedScope = new AsyncLocalStorage();
|
|
3151
3295
|
const rootScope = { tail: Promise.resolve() };
|
|
3152
3296
|
const runScope = async (scope, callback) => {
|
|
3153
3297
|
savepointSequence += 1;
|
|
3154
3298
|
const name = `hv_sp_${savepointSequence}`;
|
|
3155
|
-
await
|
|
3299
|
+
await executor.query(`SAVEPOINT ${name}`);
|
|
3156
3300
|
try {
|
|
3157
3301
|
const result = await callback(scopeFor());
|
|
3158
3302
|
await scope.tail;
|
|
3159
|
-
await
|
|
3303
|
+
await executor.query(`RELEASE SAVEPOINT ${name}`);
|
|
3160
3304
|
return result;
|
|
3161
3305
|
} catch (error) {
|
|
3162
3306
|
await scope.tail;
|
|
3163
3307
|
try {
|
|
3164
|
-
await
|
|
3165
|
-
|
|
3308
|
+
await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
3309
|
+
clearTransactionError();
|
|
3310
|
+
await executor.query(`RELEASE SAVEPOINT ${name}`);
|
|
3166
3311
|
} catch {}
|
|
3167
3312
|
throw error;
|
|
3168
3313
|
}
|
|
@@ -3187,23 +3332,29 @@ async function createDatabase(config) {
|
|
|
3187
3332
|
const transaction = async (callback) => {
|
|
3188
3333
|
const txClient = await acquireTxClient();
|
|
3189
3334
|
let nestedTransaction;
|
|
3335
|
+
const transactionState = createTransactionExecutor(txClient);
|
|
3190
3336
|
try {
|
|
3191
3337
|
await txClient.query("BEGIN");
|
|
3192
3338
|
let txDb;
|
|
3193
|
-
nestedTransaction = createNestedTransaction(
|
|
3339
|
+
nestedTransaction = createNestedTransaction(transactionState.executor, () => txDb, transactionState.clearError);
|
|
3194
3340
|
txDb = {
|
|
3195
3341
|
url,
|
|
3196
|
-
client:
|
|
3197
|
-
...createClientMethods(
|
|
3342
|
+
client: transactionState.client,
|
|
3343
|
+
...createClientMethods(transactionState.executor, true, serializeRecord, transactionState.clearError),
|
|
3198
3344
|
transaction: nestedTransaction
|
|
3199
3345
|
};
|
|
3200
3346
|
const result = await callback(txDb);
|
|
3201
3347
|
await nestedTransaction.drain();
|
|
3202
|
-
await
|
|
3348
|
+
await transactionState.drain();
|
|
3349
|
+
const statementError = transactionState.getError();
|
|
3350
|
+
if (statementError !== void 0) throw statementError;
|
|
3351
|
+
const commitResult = await txClient.query("COMMIT");
|
|
3352
|
+
assertCommitted(commitResult, transactionState);
|
|
3203
3353
|
releaseTxClient(txClient);
|
|
3204
3354
|
return result;
|
|
3205
3355
|
} catch (error) {
|
|
3206
3356
|
await nestedTransaction?.drain();
|
|
3357
|
+
await transactionState.drain();
|
|
3207
3358
|
const rollbackError = await discardTxClient(txClient);
|
|
3208
3359
|
if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
|
|
3209
3360
|
throw error;
|
|
@@ -3227,11 +3378,16 @@ async function createDatabase(config) {
|
|
|
3227
3378
|
}
|
|
3228
3379
|
let active = true;
|
|
3229
3380
|
let nestedTransaction;
|
|
3381
|
+
const transactionState = createTransactionExecutor(txClient);
|
|
3230
3382
|
const end = async (command) => {
|
|
3231
3383
|
if (!active) throw new DatabaseError("Transaction already ended", {});
|
|
3232
3384
|
try {
|
|
3233
3385
|
await nestedTransaction?.drain();
|
|
3234
|
-
await
|
|
3386
|
+
await transactionState.drain();
|
|
3387
|
+
const statementError = transactionState.getError();
|
|
3388
|
+
if (command === "COMMIT" && statementError !== void 0) throw statementError;
|
|
3389
|
+
const result = await txClient.query(command);
|
|
3390
|
+
if (command === "COMMIT") assertCommitted(result, transactionState);
|
|
3235
3391
|
active = false;
|
|
3236
3392
|
releaseTxClient(txClient);
|
|
3237
3393
|
} catch (error) {
|
|
@@ -3245,11 +3401,11 @@ async function createDatabase(config) {
|
|
|
3245
3401
|
const rollback = () => end("ROLLBACK");
|
|
3246
3402
|
const isActive = () => active;
|
|
3247
3403
|
let txHandle;
|
|
3248
|
-
nestedTransaction = createNestedTransaction(
|
|
3404
|
+
nestedTransaction = createNestedTransaction(transactionState.executor, () => txHandle, transactionState.clearError);
|
|
3249
3405
|
txHandle = {
|
|
3250
3406
|
url,
|
|
3251
|
-
client:
|
|
3252
|
-
...createClientMethods(
|
|
3407
|
+
client: transactionState.client,
|
|
3408
|
+
...createClientMethods(transactionState.executor, true, serializeRecord, transactionState.clearError),
|
|
3253
3409
|
transaction: nestedTransaction,
|
|
3254
3410
|
commit,
|
|
3255
3411
|
rollback,
|