kern-db 0.2.0 → 0.4.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.
@@ -1,4 +1,4 @@
1
- import { B as takeOnlyRow, C as renderFragment, D as bindParameter, E as assertUntypedValue, H as toValueRows, L as createFieldDecodeError, O as encodeParameter, P as ResultMode, R as createResultDecodeError, V as toMissingResult, a as SqlQuery, c as StatementSource, l as startSqlQuery, v as ParameterBinder, y as SqlFragment, z as isRecordKey } from "./query-BrvYBvWC.mjs";
1
+ import { B as takeOnlyRow, C as renderFragment, D as bindParameter, E as assertUntypedValue, H as toValueRows, L as createFieldDecodeError, O as encodeParameter, P as ResultMode, R as createResultDecodeError, V as toMissingResult, a as SqlQuery, c as StatementSource, l as startSqlQuery, v as ParameterBinder, y as SqlFragment, z as isRecordKey } from "./query-Dxiy1HJD.mjs";
2
2
  /** Quotes `name` as a PostgreSQL identifier, which keeps its case and any characters in it. */
3
3
  export declare function quoteIdentifier(name: string): string;
4
4
  export { type ParameterBinder, type ResultMode, SqlFragment, SqlQuery, type StatementSource, assertUntypedValue, bindParameter, createFieldDecodeError, createResultDecodeError, encodeParameter, isRecordKey, renderFragment, startSqlQuery, takeOnlyRow, toMissingResult, toValueRows };
@@ -1,6 +1,6 @@
1
1
  import { i as decodeBoolean } from "./bytea-DNZ_ftio.mjs";
2
2
  import { S as createStatement, _ as sql, f as compileFragment, n as startSqlQuery, p as compileScript, x as quoteIdentifier } from "./query-FKBN7BMY.mjs";
3
- import { i as setBrand, n as QueryError, r as isInstance, t as ConnectionError } from "./driver-error-26zcdHeU.mjs";
3
+ import { i as setBrand, n as QueryError, r as isInstance, t as ConnectionError } from "./driver-error-D8D4UQ4F.mjs";
4
4
  import { n as formatArray } from "./array-BLt8ZNR5.mjs";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  // src/transaction/release.ts
@@ -210,6 +210,22 @@ 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
+ * - A `ConnectionError` whose `unsent` is `true`, such as after a failure to get a connection or of a closed driver, gives `false`, since the statement was not sent.
221
+ * - Another `ConnectionError` gives `true`, since a statement outside a transaction may have run before its connection failed.
222
+ * - 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.
223
+ */
224
+ function isOutcomeUnknown(error) {
225
+ if (error instanceof TransactionConnectionError) return error.phase === "commit";
226
+ return error instanceof ConnectionError && error.unsent !== true;
227
+ }
228
+ /**
213
229
  * Reserved connection of a transaction of `transaction` that records the first `ConnectionError` of its statements and scripts, including those of its nested levels.
214
230
  *
215
231
  * A failure before `COMMIT` is sent makes the transaction roll back rather than commit, since the statement may not have run.
@@ -1467,4 +1483,4 @@ function createDatabase({ driver, ...options }) {
1467
1483
  coordinators.set(database, coordinator);
1468
1484
  return database;
1469
1485
  }
1470
- export { TransactionConnectionError as a, isQueryError as i, getCoordinator as n, isDatabase as r, createDatabase as t };
1486
+ export { TransactionConnectionError as a, isQueryError as i, getCoordinator as n, isOutcomeUnknown as o, isDatabase as r, pause as s, createDatabase as t };
@@ -24,6 +24,7 @@ interface DriverResult {
24
24
  *
25
25
  * A driver sends parameters as text without a declared type, so that the statement text alone determines their types, and returns values as text without parsing them.
26
26
  * It rejects with `QueryError` a statement or script that PostgreSQL rejected, whose connection stays usable, and with `ConnectionError` any other failure of a statement, a script, or a connection.
27
+ * The driver sets `unsent` of a `ConnectionError` to `true` when it knows that the statement or script was not sent, such as after a failure to get a connection, and only then.
27
28
  */
28
29
  interface Driver {
29
30
  /**
@@ -40,12 +41,16 @@ interface Driver {
40
41
  * Either way, no transaction stays open on the pool.
41
42
  */
42
43
  executeScript(text: string): Promise<void>;
43
- /** Reserves a dedicated connection, such as for a transaction or a session. */
44
+ /**
45
+ * Reserves a dedicated connection, such as for a transaction or a session.
46
+ *
47
+ * It rejects a failure to get a connection with `ConnectionError` whose `unsent` is `true`.
48
+ */
44
49
  acquire(): Promise<Connection>;
45
50
  /**
46
51
  * Closes the connections once the statements on the pool and the transactions and sessions on reserved connections end.
47
52
  *
48
- * From the call, it rejects a new statement or script on the pool and a new connection with `ConnectionError`.
53
+ * From the call, it rejects a new statement or script on the pool and a new connection with `ConnectionError` whose `unsent` is `true`.
49
54
  */
50
55
  close(): Promise<void>;
51
56
  }
@@ -42,12 +42,19 @@ var QueryError = class QueryError extends Error {
42
42
  /**
43
43
  * Error of a statement or connection that failed for another reason than PostgreSQL rejecting a statement, such as a lost connection, a timeout of the client, or a closed driver.
44
44
  *
45
- * The statement may have run or not, and its connection may be lost or in an unknown state of its transaction.
45
+ * Unless `unsent` is `true`, the statement may have run or not, and its connection may be lost or in an unknown state of its transaction.
46
46
  */
47
47
  var ConnectionError = class ConnectionError extends Error {
48
48
  name = "ConnectionError";
49
+ /**
50
+ * Whether the statement was certainly not sent to the server, such as when the driver was closed or could not get a connection.
51
+ *
52
+ * `false` means that the statement may have been sent.
53
+ */
54
+ unsent;
49
55
  constructor(message, options) {
50
56
  super(message, options);
57
+ this.unsent = options?.unsent ?? false;
51
58
  setBrand(this, connectionErrorBrand);
52
59
  }
53
60
  /** Tells whether `value` is an instance of the class, including one of `ConnectionError` of another copy of kern-db. */
@@ -69,16 +76,18 @@ function isSessionEnding(severity, code) {
69
76
  * Classifies an error of a client as `QueryError` or `ConnectionError`, by the fields of the PostgreSQL error it carries, for a driver to reject with.
70
77
  *
71
78
  * An error that kern-db has already classified returns as it is.
72
- * An error without a severity or a SQLSTATE, such as of a client that lost its connection, is a `ConnectionError`, and so is one of severity `FATAL` or `PANIC`, which ends the session.
79
+ * An error without a severity or a SQLSTATE, such as of a client that lost its connection, is a `ConnectionError`, and so is one of severity `FATAL` or `PANIC`, which ends the session; it gets `unsent` of `fields`.
73
80
  * An error of severity `ERROR` is a `QueryError`.
74
81
  * With a translated severity, the SQLSTATE tells an error that ends the session, so a few errors of other codes that end it give `QueryError`.
75
82
  */
76
83
  function toDriverError(error, fields) {
77
84
  if (error instanceof QueryError || error instanceof ConnectionError) return error;
78
85
  const message = error instanceof Error ? error.message : String(error);
79
- const { severity, code, constraint } = fields ?? {};
80
- if (typeof severity !== "string" || typeof code !== "string" || !sqlState.test(code)) return new ConnectionError(message, { cause: error });
81
- if (isSessionEnding(severity, code)) return new ConnectionError(message, { cause: error });
86
+ const { severity, code, constraint, unsent } = fields ?? {};
87
+ if (typeof severity !== "string" || typeof code !== "string" || !sqlState.test(code) || isSessionEnding(severity, code)) return new ConnectionError(message, {
88
+ cause: error,
89
+ unsent
90
+ });
82
91
  return new QueryError(message, {
83
92
  code,
84
93
  constraint: typeof constraint === "string" ? constraint : void 0,
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { a as TextValue, i as SqlStatement, n as Driver, o as createStatement, r as DriverResult, s as isDriver, t as Connection } from "./driver-CSfbXOBJ.mjs";
1
+ import { a as TextValue, i as SqlStatement, n as Driver, o as createStatement, r as DriverResult, s as isDriver, t as Connection } from "./driver-CZCBkque.mjs";
2
2
  import { a as BaseDerivableType, c as Type, d as TypeOutput, f as TypeTransform, h as typeValues, i as AnyType, l as TypeInput, m as isType, n as AcceptedValues, o as DerivableType, p as defineType, r as AnyDerivableType, s as DerivableTypeWithParameters, t as AcceptedNull, u as TypeOptions } from "./type-CbrtxvDF.mjs";
3
- import { A as OneRow, F as ResultRows, I as ValueRows, M as ResultByRows, N as ResultDecodeError, S as param, T as UntypedValue, _ as isDatabase, a as SqlQuery, b as SqlValue, d as DatabaseOptions, f as IsolationLevel, g as createDatabase, h as TransactionOptions, i as InitialSqlQueryState, j as OneValueRows, k as NotFoundError, m as SessionOptions, n as DecodedFields, o as SqlQueryResult, p as PollLockOptions, r as FieldTypes, s as SqlQueryState, t as AnySqlQuery, u as Database, w as sql, x as identifier, y as SqlFragment } from "./query-BrvYBvWC.mjs";
3
+ import { A as OneRow, F as ResultRows, I as ValueRows, M as ResultByRows, N as ResultDecodeError, S as param, T as UntypedValue, _ as isDatabase, a as SqlQuery, b as SqlValue, d as DatabaseOptions, f as IsolationLevel, g as createDatabase, h as TransactionOptions, i as InitialSqlQueryState, j as OneValueRows, k as NotFoundError, m as SessionOptions, n as DecodedFields, o as SqlQueryResult, p as PollLockOptions, r as FieldTypes, s as SqlQueryState, t as AnySqlQuery, u as Database, w as sql, x as identifier, y as SqlFragment } from "./query-Dxiy1HJD.mjs";
4
4
  /**
5
5
  * Error of a statement that PostgreSQL rejected, such as for a violated constraint; the connection of the statement stays usable.
6
6
  *
@@ -24,17 +24,24 @@ export declare class QueryError extends Error {
24
24
  /**
25
25
  * Error of a statement or connection that failed for another reason than PostgreSQL rejecting a statement, such as a lost connection, a timeout of the client, or a closed driver.
26
26
  *
27
- * The statement may have run or not, and its connection may be lost or in an unknown state of its transaction.
27
+ * Unless `unsent` is `true`, the statement may have run or not, and its connection may be lost or in an unknown state of its transaction.
28
28
  */
29
29
  export declare class ConnectionError extends Error {
30
30
  readonly name: string;
31
+ /**
32
+ * Whether the statement was certainly not sent to the server, such as when the driver was closed or could not get a connection.
33
+ *
34
+ * `false` means that the statement may have been sent.
35
+ */
36
+ readonly unsent: boolean;
31
37
  constructor(message: string, options?: {
32
38
  cause?: unknown;
39
+ unsent?: boolean | undefined;
33
40
  });
34
41
  /** Tells whether `value` is an instance of the class, including one of `ConnectionError` of another copy of kern-db. */
35
42
  static [Symbol.hasInstance]<T>(this: abstract new (...args: never) => T, value: unknown): value is T;
36
43
  }
37
- /** Fields of the PostgreSQL error that an error of a client carries, by which `toDriverError` classifies it. */
44
+ /** Fields of the PostgreSQL error that an error of a client carries, by which `toDriverError` classifies it, and whether the driver knows that the statement was not sent. */
38
45
  interface DriverErrorFields {
39
46
  /**
40
47
  * Severity of the error, such as `ERROR` or `FATAL`.
@@ -46,12 +53,18 @@ interface DriverErrorFields {
46
53
  readonly code?: unknown;
47
54
  /** Name of the constraint that the statement violated. */
48
55
  readonly constraint?: unknown;
56
+ /**
57
+ * Whether the statement was certainly not sent to the server, such as after a failure to get a connection, which `unsent` of a `ConnectionError` keeps.
58
+ *
59
+ * Leave it unset when the statement may have been sent.
60
+ */
61
+ readonly unsent?: boolean | undefined;
49
62
  }
50
63
  /**
51
64
  * Classifies an error of a client as `QueryError` or `ConnectionError`, by the fields of the PostgreSQL error it carries, for a driver to reject with.
52
65
  *
53
66
  * An error that kern-db has already classified returns as it is.
54
- * An error without a severity or a SQLSTATE, such as of a client that lost its connection, is a `ConnectionError`, and so is one of severity `FATAL` or `PANIC`, which ends the session.
67
+ * An error without a severity or a SQLSTATE, such as of a client that lost its connection, is a `ConnectionError`, and so is one of severity `FATAL` or `PANIC`, which ends the session; it gets `unsent` of `fields`.
55
68
  * An error of severity `ERROR` is a `QueryError`.
56
69
  * With a translated severity, the SQLSTATE tells an error that ends the session, so a few errors of other codes that end it give `QueryError`.
57
70
  */
@@ -357,4 +370,17 @@ export declare class TransactionConnectionError extends ConnectionError {
357
370
  /** Tells whether `value` is an instance of the class, including one of `TransactionConnectionError` of another copy of kern-db. */
358
371
  static [Symbol.hasInstance]<T>(this: abstract new (...args: never) => T, value: unknown): value is T;
359
372
  }
373
+ /**
374
+ * Tells whether the outcome of an operation that rejected with `error` is unknown, since its connection failed.
375
+ *
376
+ * 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`.
377
+ * 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.
378
+ *
379
+ * - `TransactionConnectionError` of phase `commit` gives `true`, since `COMMIT` may have succeeded.
380
+ * - `TransactionConnectionError` of another phase gives `false`, since the transaction was not committed.
381
+ * - A `ConnectionError` whose `unsent` is `true`, such as after a failure to get a connection or of a closed driver, gives `false`, since the statement was not sent.
382
+ * - Another `ConnectionError` gives `true`, since a statement outside a transaction may have run before its connection failed.
383
+ * - 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.
384
+ */
385
+ export declare function isOutcomeUnknown(error: unknown): boolean;
360
386
  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
- 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-BkWPkXG5.mjs";
5
- export { ConnectionError, NotFoundError, QueryError, ResultDecodeError, TransactionConnectionError, createDatabase, createStatement, defineType, identifier, isDatabase, isDriver, isQueryError, isType, param, sql, toDriverError };
3
+ import { a as toDriverError, n as QueryError, t as ConnectionError } from "./driver-error-D8D4UQ4F.mjs";
4
+ import { a as TransactionConnectionError, i as isQueryError, o as isOutcomeUnknown, r as isDatabase, t as createDatabase } from "./database-D4ql2RZ0.mjs";
5
+ export { ConnectionError, NotFoundError, QueryError, ResultDecodeError, TransactionConnectionError, createDatabase, createStatement, defineType, identifier, isDatabase, isDriver, isOutcomeUnknown, isQueryError, isType, param, sql, toDriverError };
@@ -1,4 +1,4 @@
1
- import { n as Driver } from "./driver-CSfbXOBJ.mjs";
1
+ import { n as Driver } from "./driver-CZCBkque.mjs";
2
2
  import { Pool } from "pg";
3
3
  /**
4
4
  * Creates a kern-db driver over a node-postgres pool; the pool keeps managing its connections.
@@ -1,4 +1,4 @@
1
- import { a as toDriverError, n as QueryError, t as ConnectionError } from "./driver-error-26zcdHeU.mjs";
1
+ import { a as toDriverError, n as QueryError, t as ConnectionError } from "./driver-error-D8D4UQ4F.mjs";
2
2
  // src/drivers/node-postgres.ts
3
3
  /** Parsers of a query that keep every value as its text, whatever parsers the client has. */
4
4
  const textTypes = { getTypeParser: () => (value) => value };
@@ -89,21 +89,25 @@ function nodePostgresDriver(pool) {
89
89
  function endCheckout() {
90
90
  if (--checkouts === 0) drained?.resolve();
91
91
  }
92
- /** Gets a client from the pool for `operation`, which names it in the error once closing starts. */
92
+ /**
93
+ * Gets a client from the pool for `operation`, which names it in the error once closing starts.
94
+ *
95
+ * Its errors have `unsent`: the statement waits for the client, so nothing is sent before it.
96
+ */
93
97
  async function checkOut(operation) {
94
- if (closing) throw new ConnectionError(`Cannot ${operation}: the driver is closed`);
98
+ if (closing) throw new ConnectionError(`Cannot ${operation}: the driver is closed`, { unsent: true });
95
99
  checkouts++;
96
100
  try {
97
101
  const client = await pool.connect();
98
102
  const unsupported = getUnsupportedClientReason(client);
99
103
  if (unsupported) {
100
104
  client.release();
101
- throw new ConnectionError(unsupported);
105
+ throw new ConnectionError(unsupported, { unsent: true });
102
106
  }
103
107
  return client;
104
108
  } catch (error) {
105
109
  endCheckout();
106
- throw toDriverError(error);
110
+ throw toDriverError(error, { unsent: true });
107
111
  }
108
112
  }
109
113
  /** Returns a client to the pool, which closes it with `destroy` or when its connection is lost. */
@@ -1,4 +1,4 @@
1
- import { n as Driver } from "./driver-CSfbXOBJ.mjs";
1
+ import { n as Driver } from "./driver-CZCBkque.mjs";
2
2
  import { Sql } from "postgres";
3
3
  /**
4
4
  * Creates a kern-db driver over a postgres.js client; the client keeps managing its pool.
@@ -1,14 +1,40 @@
1
- import { a as toDriverError, t as ConnectionError } from "./driver-error-26zcdHeU.mjs";
1
+ import { a as toDriverError, t as ConnectionError } from "./driver-error-D8D4UQ4F.mjs";
2
2
  // src/drivers/postgres-js.ts
3
3
  /** OID of the PostgreSQL `unknown` pseudo-type; postgres.js sends such parameters as they are, and PostgreSQL infers their types. */
4
4
  const unknownType = 705;
5
+ /**
6
+ * Codes of the errors that postgres.js 4 gives a statement only before it sends the statement.
7
+ *
8
+ * A statement on the pool waits for a connection that has started, and postgres.js rejects the waiting statements with the error of a failed start.
9
+ * The list has no code that a statement that postgres.js sent can also get:
10
+ *
11
+ * - `CONNECTION_CLOSED` and `CONNECTION_DESTROYED`, which a statement gets when its connection fails;
12
+ * - a SQLSTATE of the server, even of a failed start, such as `3D000` of a missing database: a sent statement gets its own `FATAL` error, and a function can raise one of any code, such as with `plpy.fatal` of PL/Python.
13
+ */
14
+ const unsentCodes = /* @__PURE__ */ new Set([
15
+ "QUEUE_TIMEOUT",
16
+ "CONNECTION_ENDED",
17
+ "MAX_PARAMETERS_EXCEEDED",
18
+ "ECONNREFUSED",
19
+ "ENOTFOUND",
20
+ "EAI_AGAIN",
21
+ "CONNECT_TIMEOUT",
22
+ "SSL_NOT_SUPPORTED",
23
+ "AUTHENTICATION_INCOMPLETE",
24
+ "AUTH_TYPE_NOT_IMPLEMENTED",
25
+ "SASL_MECHANISM_NOT_SUPPORTED",
26
+ "SASL_NONCE_MISMATCH",
27
+ "SASL_SIGNATURE_MISMATCH",
28
+ "CHANNEL_BINDING_REQUIRED"
29
+ ]);
5
30
  /** Classifies an error of a statement of postgres.js, which gives the severity in English as `severity`, and the constraint as `constraint_name`. */
6
31
  function toPostgresJsError(error) {
7
32
  const fields = error ?? {};
8
33
  return toDriverError(error, {
9
34
  severity: fields.severity,
10
35
  code: fields.code,
11
- constraint: fields.constraint_name
36
+ constraint: fields.constraint_name,
37
+ unsent: unsentCodes.has(fields.code)
12
38
  });
13
39
  }
14
40
  /**
@@ -52,7 +78,7 @@ function postgresJsDriver(sql) {
52
78
  * `sql.end()` ends the pool only after a few microtasks, and the pool still queues and runs a statement sent before then.
53
79
  */
54
80
  function assertOpen(operation) {
55
- if (closing) throw new ConnectionError(`Cannot ${operation}: the driver is closed`);
81
+ if (closing) throw new ConnectionError(`Cannot ${operation}: the driver is closed`, { unsent: true });
56
82
  }
57
83
  return {
58
84
  async execute(statement) {
@@ -69,7 +95,7 @@ function postgresJsDriver(sql) {
69
95
  try {
70
96
  connection = await sql.reserve();
71
97
  } catch (error) {
72
- throw toDriverError(error);
98
+ throw toDriverError(error, { unsent: true });
73
99
  }
74
100
  return {
75
101
  execute: (statement) => execute(connection, statement),
@@ -1,4 +1,4 @@
1
- import { a as TextValue, i as SqlStatement, n as Driver, r as DriverResult } from "./driver-CSfbXOBJ.mjs";
1
+ import { a as TextValue, i as SqlStatement, n as Driver, r as DriverResult } from "./driver-CZCBkque.mjs";
2
2
  import { c as Type, d as TypeOutput, i as AnyType } from "./type-CbrtxvDF.mjs";
3
3
  /**
4
4
  * How a query returns its rows: `many` as an array; `one` as one row, rejecting without it; `nullable` as one row or `null` without it; `optional` as one row or `undefined` without it.
@@ -1,5 +1,5 @@
1
- import { n as Driver } from "./driver-CSfbXOBJ.mjs";
2
- import { f as IsolationLevel, u as Database } from "./query-BrvYBvWC.mjs";
1
+ import { n as Driver, t as Connection } from "./driver-CZCBkque.mjs";
2
+ import { f as IsolationLevel, u as Database } from "./query-Dxiy1HJD.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> {
5
5
  /** Result of the seed of the fixture; reading it while the fixture is not open throws. */
@@ -82,6 +82,8 @@ interface DriverContractOptions {
82
82
  }): Driver;
83
83
  /** Creates a driver over a new client of the test database whose parsers of result values throw; for a client without parsers, the same as `createDriver`. */
84
84
  createDriverWithFailingParsers(): Driver;
85
+ /** Creates a driver over a new client of an address where no server accepts connections, such as a closed port of the local host, which refuses them at once. */
86
+ createUnreachableDriver(): Driver;
85
87
  }
86
88
  /**
87
89
  * Functions of a test runner that declare the tests of `describeDriverContract`.
@@ -98,4 +100,30 @@ interface DriverContractRunner extends TestHooks {
98
100
  * They run in the empty database of its clients and leave it unchanged.
99
101
  */
100
102
  export declare function describeDriverContract(options: DriverContractOptions, runner: DriverContractRunner): void;
101
- export type { DriverContractOptions, DriverContractRunner, OpenedTestFixture, PendingTestFixture, TestFixture, TestFixtureOptions, TestHelpers, TestHooks, TestTransactionOptions };
103
+ /** Options of `waitForLock` and `waitForLockedStatement`. */
104
+ interface LockWaitOptions {
105
+ /** 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. */
106
+ timeout?: number;
107
+ }
108
+ /**
109
+ * Resolves to the process ID of the backend of PostgreSQL that runs the statements of `target`, `pg_backend_pid()`.
110
+ *
111
+ * 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.
112
+ */
113
+ export declare function getBackendPid(target: Pick<Connection, "execute">): Promise<number>;
114
+ /**
115
+ * 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.
116
+ *
117
+ * `observer` runs the checks: a database, a driver, or a connection other than the one that waits.
118
+ * It reads the locks with `pg_blocking_pids`, which reads the current lock state even inside a transaction.
119
+ */
120
+ export declare function waitForLock(observer: Pick<Connection, "execute">, pid: number, options?: LockWaitOptions): Promise<void>;
121
+ /**
122
+ * 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.
123
+ *
124
+ * It finds a backend that the test does not know, such as one of the pool of the code under test.
125
+ * `observer` runs the checks: a database, a driver, or a connection other than the one that waits.
126
+ * It rejects when no such backend waits in time.
127
+ */
128
+ export declare function waitForLockedStatement(observer: Pick<Connection, "execute">, fragment: string, options?: LockWaitOptions): Promise<number>;
129
+ 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
- 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-BkWPkXG5.mjs";
3
+ import { n as QueryError, t as ConnectionError } from "./driver-error-D8D4UQ4F.mjs";
4
+ import { a as TransactionConnectionError, n as getCoordinator, s as pause, t as createDatabase } from "./database-D4ql2RZ0.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";
@@ -51,6 +51,14 @@ function createStatementRunners(target) {
51
51
  }
52
52
  };
53
53
  }
54
+ /** Asserts that `promise` rejects with a `ConnectionError` of a statement that the driver did not send. */
55
+ async function assertRejectsUnsent(promise) {
56
+ await assert.rejects(promise, (error) => {
57
+ assert.ok(error instanceof ConnectionError, `Expected ConnectionError, got ${String(error)}`);
58
+ assert.equal(error.unsent, true, `Expected unsent ConnectionError, got ${String(error)}`);
59
+ return true;
60
+ });
61
+ }
54
62
  /** Creates the condition that each column named by a key of `types` equals the value of the key in `values`, as a parameter of its type. */
55
63
  function equalsEach(types, values) {
56
64
  let condition = sql`TRUE`;
@@ -63,7 +71,7 @@ function equalsEach(types, values) {
63
71
  * They run in the empty database of its clients and leave it unchanged.
64
72
  */
65
73
  function describeDriverContract(options, runner) {
66
- const { createDriver, createDriverWithFailingParsers } = options;
74
+ const { createDriver, createDriverWithFailingParsers, createUnreachableDriver } = options;
67
75
  const { describe, it, before, after } = runner;
68
76
  describe("driver contract", () => {
69
77
  const driver = createDriver({ max: 2 });
@@ -423,6 +431,26 @@ function describeDriverContract(options, runner) {
423
431
  await own.close();
424
432
  }
425
433
  });
434
+ it("rejects a statement whose connection is lost while it runs with unsent false", async () => {
435
+ const own = createDriver({ max: 1 });
436
+ try {
437
+ const error = await own.execute(createStatement("SELECT pg_terminate_backend(pg_backend_pid())")).catch((error) => error);
438
+ assert.ok(error instanceof ConnectionError, `Expected ConnectionError, got ${String(error)}`);
439
+ assert.equal(error.unsent, false);
440
+ } finally {
441
+ await own.close();
442
+ }
443
+ });
444
+ it("rejects a statement, a script, and a connection without a server with unsent ConnectionError", async () => {
445
+ const unreachable = createUnreachableDriver();
446
+ try {
447
+ await assertRejectsUnsent(unreachable.execute(createStatement("SELECT 1")));
448
+ await assertRejectsUnsent(unreachable.executeScript("SELECT 1"));
449
+ await assertRejectsUnsent(unreachable.acquire());
450
+ } finally {
451
+ await unreachable.close();
452
+ }
453
+ });
426
454
  it("replaces an idle connection whose backend exited", async () => {
427
455
  const own = createDriver({ max: 2 });
428
456
  const held = [];
@@ -530,15 +558,15 @@ function describeDriverContract(options, runner) {
530
558
  assert.equal(await query, "slept");
531
559
  assert.deepEqual(settled, ["query", "database"]);
532
560
  });
533
- it("rejects a statement, a script, and a connection requested right after it starts with ConnectionError", async () => {
561
+ it("rejects a statement, a script, and a connection requested right after it starts with unsent ConnectionError", async () => {
534
562
  const driver = createDriver({ max: 1 });
535
563
  const held = await driver.acquire();
536
564
  const closing = driver.close();
537
565
  const acquiring = driver.acquire();
538
566
  const rejections = [
539
- assert.rejects(driver.execute(createStatement("SELECT 1")), ConnectionError),
540
- assert.rejects(driver.executeScript("SELECT 1"), ConnectionError),
541
- assert.rejects(acquiring, ConnectionError)
567
+ assertRejectsUnsent(driver.execute(createStatement("SELECT 1"))),
568
+ assertRejectsUnsent(driver.executeScript("SELECT 1")),
569
+ assertRejectsUnsent(acquiring)
542
570
  ];
543
571
  acquiring.then((connection) => connection.release()).catch(() => {});
544
572
  await held.release();
@@ -738,4 +766,80 @@ function describeDriverContract(options, runner) {
738
766
  });
739
767
  });
740
768
  }
741
- export { createTestHelpers, describeDriverContract };
769
+ // src/lock-wait.ts
770
+ /** Time that the helpers wait by default, in milliseconds. */
771
+ const defaultTimeout = 2e3;
772
+ /** Pause between two checks in milliseconds, so that waiting does not keep the observer busy. */
773
+ const pollInterval = 5;
774
+ /**
775
+ * Resolves to the process ID of the backend of PostgreSQL that runs the statements of `target`, `pg_backend_pid()`.
776
+ *
777
+ * 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.
778
+ */
779
+ async function getBackendPid(target) {
780
+ const { rows } = await target.execute(createStatement("SELECT pg_backend_pid()"));
781
+ return Number(rows[0][0]);
782
+ }
783
+ /**
784
+ * 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.
785
+ *
786
+ * `observer` runs the checks: a database, a driver, or a connection other than the one that waits.
787
+ * It reads the locks with `pg_blocking_pids`, which reads the current lock state even inside a transaction.
788
+ */
789
+ async function waitForLock(observer, pid, options = {}) {
790
+ const timeout = options.timeout ?? defaultTimeout;
791
+ const statement = createStatement("SELECT cardinality(pg_blocking_pids($1::integer)) > 0", [String(pid)]);
792
+ await poll(timeout, `Backend ${pid} did not wait for a lock within ${timeout} ms`, async () => {
793
+ const { rows } = await observer.execute(statement);
794
+ return rows[0][0] === "t" ? true : void 0;
795
+ });
796
+ }
797
+ /**
798
+ * 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.
799
+ *
800
+ * It finds a backend that the test does not know, such as one of the pool of the code under test.
801
+ * `observer` runs the checks: a database, a driver, or a connection other than the one that waits.
802
+ * It rejects when no such backend waits in time.
803
+ */
804
+ async function waitForLockedStatement(observer, fragment, options = {}) {
805
+ const timeout = options.timeout ?? defaultTimeout;
806
+ const clearSnapshot = createStatement("SELECT pg_stat_clear_snapshot()");
807
+ const statement = createStatement(`SELECT pid FROM pg_stat_activity
808
+ WHERE datname = current_database() AND pid <> pg_backend_pid()
809
+ AND position($1::text IN query) > 0 AND cardinality(pg_blocking_pids(pid)) > 0
810
+ ORDER BY pid LIMIT 1`, [fragment]);
811
+ return await poll(timeout, `No statement with "${fragment}" waited for a lock within ${timeout} ms`, async (isWaiting) => {
812
+ await observer.execute(clearSnapshot);
813
+ if (!isWaiting()) return;
814
+ const { rows } = await observer.execute(statement);
815
+ return rows.length > 0 ? Number(rows[0][0]) : void 0;
816
+ });
817
+ }
818
+ /**
819
+ * Calls `check` until it resolves to a value other than `undefined`, pausing between the calls, and resolves to that value.
820
+ *
821
+ * 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.
822
+ * A check must send no further statement once `isWaiting` returns `false`.
823
+ */
824
+ async function poll(timeout, message, check) {
825
+ let waiting = true;
826
+ const isWaiting = () => waiting;
827
+ let timer;
828
+ const expired = new Promise((_, reject) => {
829
+ timer = setTimeout(() => {
830
+ waiting = false;
831
+ reject(new Error(message));
832
+ }, timeout);
833
+ });
834
+ try {
835
+ for (;;) {
836
+ const value = await Promise.race([check(isWaiting), expired]);
837
+ if (value !== void 0) return value;
838
+ await Promise.race([pause(pollInterval, void 0), expired]);
839
+ }
840
+ } finally {
841
+ waiting = false;
842
+ clearTimeout(timer);
843
+ }
844
+ }
845
+ export { createTestHelpers, describeDriverContract, getBackendPid, waitForLock, waitForLockedStatement };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kern-db",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "PostgreSQL database layer with ambient transactions, typed SQL queries, and test transactions",
5
5
  "keywords": [
6
6
  "database",