kern-db 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{database-BkWPkXG5.mjs → database-0i2749JZ.mjs} +18 -1
- package/dist/index.d.mts +14 -0
- package/dist/index.mjs +2 -2
- package/dist/testing.d.mts +28 -2
- package/dist/testing.mjs +78 -2
- package/package.json +1 -1
|
@@ -210,6 +210,23 @@ var TransactionConnectionError = class TransactionConnectionError extends Connec
|
|
|
210
210
|
}
|
|
211
211
|
};
|
|
212
212
|
/**
|
|
213
|
+
* Tells whether the outcome of an operation that rejected with `error` is unknown, since its connection failed.
|
|
214
|
+
*
|
|
215
|
+
* Pass it the error that a whole operation rejected with: `execute` or `query` of a database outside a transaction, a statement in a session, or a whole `transaction`.
|
|
216
|
+
* A `ConnectionError` of a statement caught in the callback of a transaction is not such an error: the transaction rolls back anyway, and its own error tells its outcome.
|
|
217
|
+
*
|
|
218
|
+
* - `TransactionConnectionError` of phase `commit` gives `true`, since `COMMIT` may have succeeded.
|
|
219
|
+
* - `TransactionConnectionError` of another phase gives `false`, since the transaction was not committed.
|
|
220
|
+
* - Another `ConnectionError` gives `true`, since a statement outside a transaction may have run before its connection failed.
|
|
221
|
+
* - Any other error gives `false`, such as a `QueryError`, or a `ResultDecodeError` of a statement that has run: `false` does not mean that the operation applied nothing.
|
|
222
|
+
*
|
|
223
|
+
* Classification depends only on the error class, so failures before the statement was sent, such as a failure to acquire a connection, also give `true`.
|
|
224
|
+
*/
|
|
225
|
+
function isOutcomeUnknown(error) {
|
|
226
|
+
if (error instanceof TransactionConnectionError) return error.phase === "commit";
|
|
227
|
+
return error instanceof ConnectionError;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
213
230
|
* Reserved connection of a transaction of `transaction` that records the first `ConnectionError` of its statements and scripts, including those of its nested levels.
|
|
214
231
|
*
|
|
215
232
|
* A failure before `COMMIT` is sent makes the transaction roll back rather than commit, since the statement may not have run.
|
|
@@ -1467,4 +1484,4 @@ function createDatabase({ driver, ...options }) {
|
|
|
1467
1484
|
coordinators.set(database, coordinator);
|
|
1468
1485
|
return database;
|
|
1469
1486
|
}
|
|
1470
|
-
export { TransactionConnectionError as a, isQueryError as i, getCoordinator as n, isDatabase as r, createDatabase as t };
|
|
1487
|
+
export { TransactionConnectionError as a, isQueryError as i, getCoordinator as n, isOutcomeUnknown as o, isDatabase as r, pause as s, createDatabase as t };
|
package/dist/index.d.mts
CHANGED
|
@@ -357,4 +357,18 @@ export declare class TransactionConnectionError extends ConnectionError {
|
|
|
357
357
|
/** Tells whether `value` is an instance of the class, including one of `TransactionConnectionError` of another copy of kern-db. */
|
|
358
358
|
static [Symbol.hasInstance]<T>(this: abstract new (...args: never) => T, value: unknown): value is T;
|
|
359
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Tells whether the outcome of an operation that rejected with `error` is unknown, since its connection failed.
|
|
362
|
+
*
|
|
363
|
+
* Pass it the error that a whole operation rejected with: `execute` or `query` of a database outside a transaction, a statement in a session, or a whole `transaction`.
|
|
364
|
+
* A `ConnectionError` of a statement caught in the callback of a transaction is not such an error: the transaction rolls back anyway, and its own error tells its outcome.
|
|
365
|
+
*
|
|
366
|
+
* - `TransactionConnectionError` of phase `commit` gives `true`, since `COMMIT` may have succeeded.
|
|
367
|
+
* - `TransactionConnectionError` of another phase gives `false`, since the transaction was not committed.
|
|
368
|
+
* - Another `ConnectionError` gives `true`, since a statement outside a transaction may have run before its connection failed.
|
|
369
|
+
* - Any other error gives `false`, such as a `QueryError`, or a `ResultDecodeError` of a statement that has run: `false` does not mean that the operation applied nothing.
|
|
370
|
+
*
|
|
371
|
+
* Classification depends only on the error class, so failures before the statement was sent, such as a failure to acquire a connection, also give `true`.
|
|
372
|
+
*/
|
|
373
|
+
export declare function isOutcomeUnknown(error: unknown): boolean;
|
|
360
374
|
export { type AcceptedNull, type AcceptedValues, type AnyDerivableType, type AnySqlQuery, type AnyType, type BaseDerivableType, type Connection, type Database, type DatabaseOptions, type DecodedFields, type DerivableType, type DerivableTypeWithParameters, type Driver, type DriverErrorFields, type DriverResult, type FieldTypes, type InitialSqlQueryState, type IsolationLevel, NotFoundError, type OneRow, type OneValueRows, type PollLockOptions, type QueryErrorCode, type QueryErrorCondition, type ResultByRows, ResultDecodeError, type ResultRows, type SessionOptions, type SqlFragment, type SqlQuery, type SqlQueryResult, type SqlQueryState, type SqlStatement, type SqlValue, type TextValue, type TransactionOptions, type Type, type TypeInput, type TypeOptions, type TypeOutput, type TypeTransform, type UntypedValue, type ValueRows, createDatabase, createStatement, defineType, identifier, isDatabase, isDriver, isType, param, sql, type typeValues };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { o as defineType, s as isType } from "./bytea-DNZ_ftio.mjs";
|
|
2
2
|
import { C as isDriver, S as createStatement, _ as sql, h as param, i as ResultDecodeError, m as identifier, r as NotFoundError } from "./query-FKBN7BMY.mjs";
|
|
3
3
|
import { a as toDriverError, n as QueryError, t as ConnectionError } from "./driver-error-26zcdHeU.mjs";
|
|
4
|
-
import { a as TransactionConnectionError, i as isQueryError, r as isDatabase, t as createDatabase } from "./database-
|
|
5
|
-
export { ConnectionError, NotFoundError, QueryError, ResultDecodeError, TransactionConnectionError, createDatabase, createStatement, defineType, identifier, isDatabase, isDriver, isQueryError, isType, param, sql, toDriverError };
|
|
4
|
+
import { a as TransactionConnectionError, i as isQueryError, o as isOutcomeUnknown, r as isDatabase, t as createDatabase } from "./database-0i2749JZ.mjs";
|
|
5
|
+
export { ConnectionError, NotFoundError, QueryError, ResultDecodeError, TransactionConnectionError, createDatabase, createStatement, defineType, identifier, isDatabase, isDriver, isOutcomeUnknown, isQueryError, isType, param, sql, toDriverError };
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as Driver } from "./driver-CSfbXOBJ.mjs";
|
|
1
|
+
import { n as Driver, t as Connection } from "./driver-CSfbXOBJ.mjs";
|
|
2
2
|
import { f as IsolationLevel, u as Database } from "./query-BrvYBvWC.mjs";
|
|
3
3
|
/** Test fixture of `useTestFixture`, open while the tests of its `describe` block or test file run. */
|
|
4
4
|
interface TestFixture<T = unknown> {
|
|
@@ -98,4 +98,30 @@ interface DriverContractRunner extends TestHooks {
|
|
|
98
98
|
* They run in the empty database of its clients and leave it unchanged.
|
|
99
99
|
*/
|
|
100
100
|
export declare function describeDriverContract(options: DriverContractOptions, runner: DriverContractRunner): void;
|
|
101
|
-
|
|
101
|
+
/** Options of `waitForLock` and `waitForLockedStatement`. */
|
|
102
|
+
interface LockWaitOptions {
|
|
103
|
+
/** Milliseconds to wait before rejecting; 2000 by default, shorter than the default timeout of a test, so that the helper rejects with its own error. */
|
|
104
|
+
timeout?: number;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolves to the process ID of the backend of PostgreSQL that runs the statements of `target`, `pg_backend_pid()`.
|
|
108
|
+
*
|
|
109
|
+
* Call it on the connection that will wait for a lock, such as inside its transaction or session: on the pool of a database or a driver, it gives the backend of whichever connection runs the statement.
|
|
110
|
+
*/
|
|
111
|
+
export declare function getBackendPid(target: Pick<Connection, "execute">): Promise<number>;
|
|
112
|
+
/**
|
|
113
|
+
* Waits until the backend `pid` waits for a lock, such as of a row or an advisory lock that another transaction holds, and rejects when it does not in time.
|
|
114
|
+
*
|
|
115
|
+
* `observer` runs the checks: a database, a driver, or a connection other than the one that waits.
|
|
116
|
+
* It reads the locks with `pg_blocking_pids`, which reads the current lock state even inside a transaction.
|
|
117
|
+
*/
|
|
118
|
+
export declare function waitForLock(observer: Pick<Connection, "execute">, pid: number, options?: LockWaitOptions): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Waits until a backend of the current database other than that of `observer` waits for a lock in a statement whose text contains `fragment`, and resolves to its backend PID.
|
|
121
|
+
*
|
|
122
|
+
* It finds a backend that the test does not know, such as one of the pool of the code under test.
|
|
123
|
+
* `observer` runs the checks: a database, a driver, or a connection other than the one that waits.
|
|
124
|
+
* It rejects when no such backend waits in time.
|
|
125
|
+
*/
|
|
126
|
+
export declare function waitForLockedStatement(observer: Pick<Connection, "execute">, fragment: string, options?: LockWaitOptions): Promise<number>;
|
|
127
|
+
export type { DriverContractOptions, DriverContractRunner, LockWaitOptions, OpenedTestFixture, PendingTestFixture, TestFixture, TestFixtureOptions, TestHelpers, TestHooks, TestTransactionOptions };
|
package/dist/testing.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { r as boolean, t as bytea } from "./bytea-DNZ_ftio.mjs";
|
|
2
2
|
import { S as createStatement, _ as sql, h as param, m as identifier } from "./query-FKBN7BMY.mjs";
|
|
3
3
|
import { n as QueryError, t as ConnectionError } from "./driver-error-26zcdHeU.mjs";
|
|
4
|
-
import { a as TransactionConnectionError, n as getCoordinator, t as createDatabase } from "./database-
|
|
4
|
+
import { a as TransactionConnectionError, n as getCoordinator, s as pause, t as createDatabase } from "./database-0i2749JZ.mjs";
|
|
5
5
|
import { t as array } from "./array-BLt8ZNR5.mjs";
|
|
6
6
|
import { _ as interval, a as bigint, b as timestamptz, c as numeric, f as inet, g as date, h as enumOf, m as jsonb, n as char, o as doublePrecision, r as text, s as integer, t as uuid, v as time, y as timestamp } from "./types-Dl2oicjV.mjs";
|
|
7
7
|
import assert from "node:assert/strict";
|
|
@@ -738,4 +738,80 @@ function describeDriverContract(options, runner) {
|
|
|
738
738
|
});
|
|
739
739
|
});
|
|
740
740
|
}
|
|
741
|
-
|
|
741
|
+
// src/lock-wait.ts
|
|
742
|
+
/** Time that the helpers wait by default, in milliseconds. */
|
|
743
|
+
const defaultTimeout = 2e3;
|
|
744
|
+
/** Pause between two checks in milliseconds, so that waiting does not keep the observer busy. */
|
|
745
|
+
const pollInterval = 5;
|
|
746
|
+
/**
|
|
747
|
+
* Resolves to the process ID of the backend of PostgreSQL that runs the statements of `target`, `pg_backend_pid()`.
|
|
748
|
+
*
|
|
749
|
+
* Call it on the connection that will wait for a lock, such as inside its transaction or session: on the pool of a database or a driver, it gives the backend of whichever connection runs the statement.
|
|
750
|
+
*/
|
|
751
|
+
async function getBackendPid(target) {
|
|
752
|
+
const { rows } = await target.execute(createStatement("SELECT pg_backend_pid()"));
|
|
753
|
+
return Number(rows[0][0]);
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Waits until the backend `pid` waits for a lock, such as of a row or an advisory lock that another transaction holds, and rejects when it does not in time.
|
|
757
|
+
*
|
|
758
|
+
* `observer` runs the checks: a database, a driver, or a connection other than the one that waits.
|
|
759
|
+
* It reads the locks with `pg_blocking_pids`, which reads the current lock state even inside a transaction.
|
|
760
|
+
*/
|
|
761
|
+
async function waitForLock(observer, pid, options = {}) {
|
|
762
|
+
const timeout = options.timeout ?? defaultTimeout;
|
|
763
|
+
const statement = createStatement("SELECT cardinality(pg_blocking_pids($1::integer)) > 0", [String(pid)]);
|
|
764
|
+
await poll(timeout, `Backend ${pid} did not wait for a lock within ${timeout} ms`, async () => {
|
|
765
|
+
const { rows } = await observer.execute(statement);
|
|
766
|
+
return rows[0][0] === "t" ? true : void 0;
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Waits until a backend of the current database other than that of `observer` waits for a lock in a statement whose text contains `fragment`, and resolves to its backend PID.
|
|
771
|
+
*
|
|
772
|
+
* It finds a backend that the test does not know, such as one of the pool of the code under test.
|
|
773
|
+
* `observer` runs the checks: a database, a driver, or a connection other than the one that waits.
|
|
774
|
+
* It rejects when no such backend waits in time.
|
|
775
|
+
*/
|
|
776
|
+
async function waitForLockedStatement(observer, fragment, options = {}) {
|
|
777
|
+
const timeout = options.timeout ?? defaultTimeout;
|
|
778
|
+
const clearSnapshot = createStatement("SELECT pg_stat_clear_snapshot()");
|
|
779
|
+
const statement = createStatement(`SELECT pid FROM pg_stat_activity
|
|
780
|
+
WHERE datname = current_database() AND pid <> pg_backend_pid()
|
|
781
|
+
AND position($1::text IN query) > 0 AND cardinality(pg_blocking_pids(pid)) > 0
|
|
782
|
+
ORDER BY pid LIMIT 1`, [fragment]);
|
|
783
|
+
return await poll(timeout, `No statement with "${fragment}" waited for a lock within ${timeout} ms`, async (isWaiting) => {
|
|
784
|
+
await observer.execute(clearSnapshot);
|
|
785
|
+
if (!isWaiting()) return;
|
|
786
|
+
const { rows } = await observer.execute(statement);
|
|
787
|
+
return rows.length > 0 ? Number(rows[0][0]) : void 0;
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Calls `check` until it resolves to a value other than `undefined`, pausing between the calls, and resolves to that value.
|
|
792
|
+
*
|
|
793
|
+
* It rejects with `message` once `timeout` passes, also while a check still runs, such as on a pool that has no free connection; such a check settles without the helper.
|
|
794
|
+
* A check must send no further statement once `isWaiting` returns `false`.
|
|
795
|
+
*/
|
|
796
|
+
async function poll(timeout, message, check) {
|
|
797
|
+
let waiting = true;
|
|
798
|
+
const isWaiting = () => waiting;
|
|
799
|
+
let timer;
|
|
800
|
+
const expired = new Promise((_, reject) => {
|
|
801
|
+
timer = setTimeout(() => {
|
|
802
|
+
waiting = false;
|
|
803
|
+
reject(new Error(message));
|
|
804
|
+
}, timeout);
|
|
805
|
+
});
|
|
806
|
+
try {
|
|
807
|
+
for (;;) {
|
|
808
|
+
const value = await Promise.race([check(isWaiting), expired]);
|
|
809
|
+
if (value !== void 0) return value;
|
|
810
|
+
await Promise.race([pause(pollInterval, void 0), expired]);
|
|
811
|
+
}
|
|
812
|
+
} finally {
|
|
813
|
+
waiting = false;
|
|
814
|
+
clearTimeout(timer);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
export { createTestHelpers, describeDriverContract, getBackendPid, waitForLock, waitForLockedStatement };
|