kern-db 0.3.0 → 0.5.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.
@@ -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";
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";
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
+ 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-DU5oJfh_.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-DpxSVheu.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
  */
@@ -365,10 +378,9 @@ export declare class TransactionConnectionError extends ConnectionError {
365
378
  *
366
379
  * - `TransactionConnectionError` of phase `commit` gives `true`, since `COMMIT` may have succeeded.
367
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.
368
382
  * - Another `ConnectionError` gives `true`, since a statement outside a transaction may have run before its connection failed.
369
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.
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
384
  */
373
385
  export declare function isOutcomeUnknown(error: unknown): boolean;
374
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, o as isOutcomeUnknown, r as isDatabase, t as createDatabase } from "./database-0i2749JZ.mjs";
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-C3LR3pFV.mjs";
5
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,5 +1,5 @@
1
- import { a as TextValue, i as SqlStatement, n as Driver, r as DriverResult } from "./driver-CSfbXOBJ.mjs";
2
- import { c as Type, d as TypeOutput, i as AnyType } from "./type-CbrtxvDF.mjs";
1
+ import { a as TextValue, i as SqlStatement, n as Driver, r as DriverResult } from "./driver-CZCBkque.mjs";
2
+ import { c as Type, d as TypeOutput, i as AnyType } from "./type-DU5oJfh_.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.
5
5
  *
@@ -1,17 +1,39 @@
1
- import { n as Driver, t as Connection } 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-DpxSVheu.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. */
6
6
  readonly data: T;
7
7
  }
8
- /** Test fixture opened by the transaction coordinator: an outer transaction of a database created for tests, which always rolls back when it closes. */
8
+ /** Open test fixture of `openTestFixture`: an outer transaction of a database created for tests, which always rolls back when it closes. */
9
9
  interface OpenedTestFixture<T = unknown> extends AsyncDisposable {
10
10
  /** Result of the seed of the fixture, such as the records it created; reading it after the fixture closes throws. */
11
11
  readonly data: T;
12
12
  /** Rolls back the fixture and the fixtures opened inside it; closing a closed fixture does nothing. */
13
13
  close(): Promise<void>;
14
14
  }
15
+ /** Open test transaction of `openTestTransaction`, which the callbacks of `run`, such as the requests of one browser test to a server, share until it closes. */
16
+ interface OpenedTestTransaction extends AsyncDisposable {
17
+ /**
18
+ * Runs `callback` in the test transaction, so that its queries run there.
19
+ *
20
+ * Each query of the callback outside a transaction runs as a nested transaction of its own, so that a failed query undoes only itself, as in production.
21
+ * A transaction of a callback runs as a nested transaction that the queries and transactions of the other callbacks wait for.
22
+ * It rejects without calling `callback` once the test transaction is closed, and inside a transaction.
23
+ */
24
+ run<T>(callback: () => Promise<T>): Promise<T>;
25
+ /** Rolls back the test transaction; the later queries of its callbacks fail, and closing it again returns the same closing. */
26
+ close(): Promise<void>;
27
+ }
28
+ /** Options of `openTestTransaction`. */
29
+ interface OpenTestTransactionOptions {
30
+ /**
31
+ * Isolation level of the test transaction, such as `serializable`, for code under test that starts a transaction at that level.
32
+ *
33
+ * Inside a test fixture, the test transaction runs at the level of the fixture: given another level, it rejects.
34
+ */
35
+ isolation?: IsolationLevel;
36
+ }
15
37
  /** Test fixture that is being opened, with a `close` that can be called before it opens. */
16
38
  interface PendingTestFixture<T = unknown> {
17
39
  readonly fixture: Promise<OpenedTestFixture<T>>;
@@ -25,6 +47,12 @@ interface PendingTestFixture<T = unknown> {
25
47
  interface TestHooks {
26
48
  before(hook: () => Promise<void>): unknown;
27
49
  after(hook: () => Promise<void>): unknown;
50
+ /**
51
+ * Hook of a test runner that runs a function once the current test finishes, also when it times out, such as `onTestFinished` of `bun:test`.
52
+ *
53
+ * With it, a test that `wrapTest` declares rolls back its test transaction as soon as the runner gives up on the test, as with the abort signal of `testTransaction`.
54
+ */
55
+ onTestFinished?(hook: () => void): unknown;
28
56
  }
29
57
  /** Options of `testTransaction`. */
30
58
  interface TestTransactionOptions {
@@ -44,7 +72,7 @@ interface TestTransactionOptions {
44
72
  */
45
73
  isolation?: IsolationLevel;
46
74
  }
47
- /** Options of `useTestFixture`. */
75
+ /** Options of `useTestFixture` and `openTestFixture`. */
48
76
  interface TestFixtureOptions {
49
77
  /**
50
78
  * Isolation level of the transaction of the fixture, such as `serializable`, at which the test transactions and fixtures nested in it run.
@@ -59,6 +87,7 @@ interface TestHelpers {
59
87
  * Runs `callback` in a transaction that always rolls back.
60
88
  *
61
89
  * Wrap the body of every test in it, so that every run of the test gets its own transaction.
90
+ * A query of the callback outside a transaction runs in a nested transaction of its own, so that a failed query undoes only itself, as in production.
62
91
  * Inside an existing transaction, it starts a nested transaction that always rolls back.
63
92
  */
64
93
  testTransaction<T>(callback: () => Promise<T>, options?: TestTransactionOptions): Promise<T>;
@@ -71,9 +100,34 @@ interface TestHelpers {
71
100
  * A fixture whose `seed` rejects rolls back and closes.
72
101
  */
73
102
  useTestFixture<T>(seed: () => Promise<T>, options?: TestFixtureOptions): TestFixture<T>;
103
+ /**
104
+ * Wraps the `test` function of `node:test` or `bun:test` so that every test it declares runs in a test transaction.
105
+ *
106
+ * The wrapped function keeps the modifiers of the original, such as `test.skip`, `test.only`, and `test.each(table)`, and its options, such as a timeout.
107
+ * Without `onTestFinished` in the hooks, the test transaction takes the abort signal of the test context, such as `t.signal` of `node:test`.
108
+ * Hooks of the test, such as `beforeEach`, run outside of its test transaction.
109
+ * A test function must return a promise or a value rather than take a `done` callback.
110
+ * With Vitest, run tests in test transactions with `aroundEach` instead, which also covers their hooks: the wrapper hides the parameters of a test function from the fixtures of `test.extend`.
111
+ */
112
+ wrapTest<T extends (...args: never[]) => unknown>(test: T): T;
74
113
  }
75
114
  /** Creates the test helpers of a database created with `test: true`, registering fixtures with the hooks of a test runner. */
76
115
  export declare function createTestHelpers(database: Database, hooks: TestHooks): TestHelpers;
116
+ /**
117
+ * Opens a test fixture of a database created with `test: true` without the hooks of a test runner, such as in a server that runs browser tests, and runs `seed` in it.
118
+ *
119
+ * It resolves once `seed` resolves, with the fixture whose `close` rolls it back; a fixture whose `seed` rejects rolls back and closes.
120
+ * Closing the database closes the fixture too.
121
+ * Fixtures nest as with `useTestFixture`: a new one opens inside the innermost open one, and test transactions run in the innermost one.
122
+ */
123
+ export declare function openTestFixture<T>(database: Database, seed: () => Promise<T>, options?: TestFixtureOptions): Promise<OpenedTestFixture<T>>;
124
+ /**
125
+ * Opens a test transaction of a database created with `test: true` that callbacks share through `run` until it closes, such as the requests of one browser test to a server.
126
+ *
127
+ * Inside an open test fixture, it opens as a nested transaction of the innermost one, and rolls back a test transaction or another open test transaction running there; without a fixture, it opens on a connection of its own.
128
+ * Closing the database closes it too.
129
+ */
130
+ export declare function openTestTransaction(database: Database, options?: OpenTestTransactionOptions): Promise<OpenedTestTransaction>;
77
131
  /** Options of `describeDriverContract`: how to create the driver under test. */
78
132
  interface DriverContractOptions {
79
133
  /** Creates a driver over a new client of the test database with `max` connections. */
@@ -82,6 +136,8 @@ interface DriverContractOptions {
82
136
  }): Driver;
83
137
  /** 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
138
  createDriverWithFailingParsers(): Driver;
139
+ /** 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. */
140
+ createUnreachableDriver(): Driver;
85
141
  }
86
142
  /**
87
143
  * Functions of a test runner that declare the tests of `describeDriverContract`.
@@ -124,4 +180,4 @@ export declare function waitForLock(observer: Pick<Connection, "execute">, pid:
124
180
  * It rejects when no such backend waits in time.
125
181
  */
126
182
  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 };
183
+ export type { DriverContractOptions, DriverContractRunner, LockWaitOptions, OpenTestTransactionOptions, OpenedTestFixture, OpenedTestTransaction, PendingTestFixture, TestFixture, TestFixtureOptions, TestHelpers, TestHooks, TestTransactionOptions };
package/dist/testing.mjs CHANGED
@@ -1,8 +1,8 @@
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, s as pause, t as createDatabase } from "./database-0i2749JZ.mjs";
3
+ import { n as QueryError, t as ConnectionError } from "./driver-error-D8D4UQ4F.mjs";
5
4
  import { t as array } from "./array-BLt8ZNR5.mjs";
5
+ import { a as TransactionConnectionError, n as getCoordinator, s as pause, t as createDatabase } from "./database-C3LR3pFV.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";
8
8
  // src/test-helpers.ts
@@ -10,8 +10,32 @@ import assert from "node:assert/strict";
10
10
  function createTestHelpers(database, hooks) {
11
11
  const coordinator = getCoordinator(database);
12
12
  if (!coordinator.test) throw new Error("createTestHelpers requires a database created with test: true");
13
+ /**
14
+ * Runs a test function in a test transaction, which `onTestFinished`, or else the signal of the test context, aborts once the runner finishes the test.
15
+ *
16
+ * The arguments of a test of `test.each` are a row of its table rather than a context, so its `signal` belongs to the data of the test.
17
+ */
18
+ function runTestInTransaction(callback, parameterized) {
19
+ return function(...args) {
20
+ const run = async () => callback.apply(this, args);
21
+ if (!hooks.onTestFinished) {
22
+ const signal = parameterized ? void 0 : readTestSignal(args[0]);
23
+ return coordinator.testTransaction(run, { signal });
24
+ }
25
+ const controller = new AbortController();
26
+ let finished = false;
27
+ hooks.onTestFinished(() => {
28
+ finished = true;
29
+ controller.abort(/* @__PURE__ */ new Error("The test finished before its test transaction ended"));
30
+ });
31
+ return coordinator.testTransaction(run, { signal: controller.signal }).catch((error) => {
32
+ if (!finished) throw error;
33
+ });
34
+ };
35
+ }
13
36
  return {
14
37
  testTransaction: (callback, options) => coordinator.testTransaction(callback, options),
38
+ wrapTest: (test) => wrapTestFunction(test, void 0, false, runTestInTransaction),
15
39
  useTestFixture(seed, options) {
16
40
  let pending;
17
41
  let fixture;
@@ -38,6 +62,52 @@ function createTestHelpers(database, hooks) {
38
62
  }
39
63
  };
40
64
  }
65
+ /**
66
+ * Opens a test fixture of a database created with `test: true` without the hooks of a test runner, such as in a server that runs browser tests, and runs `seed` in it.
67
+ *
68
+ * It resolves once `seed` resolves, with the fixture whose `close` rolls it back; a fixture whose `seed` rejects rolls back and closes.
69
+ * Closing the database closes the fixture too.
70
+ * Fixtures nest as with `useTestFixture`: a new one opens inside the innermost open one, and test transactions run in the innermost one.
71
+ */
72
+ function openTestFixture(database, seed, options) {
73
+ return getCoordinator(database).createTestFixture(seed, options);
74
+ }
75
+ /**
76
+ * Opens a test transaction of a database created with `test: true` that callbacks share through `run` until it closes, such as the requests of one browser test to a server.
77
+ *
78
+ * Inside an open test fixture, it opens as a nested transaction of the innermost one, and rolls back a test transaction or another open test transaction running there; without a fixture, it opens on a connection of its own.
79
+ * Closing the database closes it too.
80
+ */
81
+ function openTestTransaction(database, options) {
82
+ return getCoordinator(database).openTestTransaction(options);
83
+ }
84
+ /** Reads the abort signal of a test from the first argument of its function, the test context of `node:test`. */
85
+ function readTestSignal(context) {
86
+ if (typeof context === "object" && context !== null && "signal" in context) {
87
+ const { signal } = context;
88
+ return signal instanceof AbortSignal ? signal : void 0;
89
+ }
90
+ }
91
+ /**
92
+ * Wraps a `test` function of a test runner and the functions that its properties and calls return, such as `test.skip` and `test.each(table)`, replacing every function argument with `wrapCallback` of it.
93
+ *
94
+ * Calls a modifier with the object it was read from, since modifiers of `bun:test` reject another `this`.
95
+ * Marks the callbacks of functions reached through `each` as parameterized.
96
+ */
97
+ function wrapTestFunction(target, owner, parameterized, wrapCallback) {
98
+ return new Proxy(target, {
99
+ apply(callee, self, args) {
100
+ const wrappedArgs = [];
101
+ for (const arg of args) wrappedArgs.push(typeof arg === "function" ? wrapCallback(arg, parameterized) : arg);
102
+ const result = Reflect.apply(callee, owner ?? self, wrappedArgs);
103
+ return typeof result === "function" ? wrapTestFunction(result, void 0, parameterized, wrapCallback) : result;
104
+ },
105
+ get(object, key) {
106
+ const value = Reflect.get(object, key);
107
+ return typeof value === "function" ? wrapTestFunction(value, object, parameterized || key === "each", wrapCallback) : value;
108
+ }
109
+ });
110
+ }
41
111
  // src/driver-contract.ts
42
112
  /** Creates helpers that run raw statements on `target`: a database, a driver, or a reserved connection. */
43
113
  function createStatementRunners(target) {
@@ -51,6 +121,14 @@ function createStatementRunners(target) {
51
121
  }
52
122
  };
53
123
  }
124
+ /** Asserts that `promise` rejects with a `ConnectionError` of a statement that the driver did not send. */
125
+ async function assertRejectsUnsent(promise) {
126
+ await assert.rejects(promise, (error) => {
127
+ assert.ok(error instanceof ConnectionError, `Expected ConnectionError, got ${String(error)}`);
128
+ assert.equal(error.unsent, true, `Expected unsent ConnectionError, got ${String(error)}`);
129
+ return true;
130
+ });
131
+ }
54
132
  /** 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
133
  function equalsEach(types, values) {
56
134
  let condition = sql`TRUE`;
@@ -63,7 +141,7 @@ function equalsEach(types, values) {
63
141
  * They run in the empty database of its clients and leave it unchanged.
64
142
  */
65
143
  function describeDriverContract(options, runner) {
66
- const { createDriver, createDriverWithFailingParsers } = options;
144
+ const { createDriver, createDriverWithFailingParsers, createUnreachableDriver } = options;
67
145
  const { describe, it, before, after } = runner;
68
146
  describe("driver contract", () => {
69
147
  const driver = createDriver({ max: 2 });
@@ -423,6 +501,26 @@ function describeDriverContract(options, runner) {
423
501
  await own.close();
424
502
  }
425
503
  });
504
+ it("rejects a statement whose connection is lost while it runs with unsent false", async () => {
505
+ const own = createDriver({ max: 1 });
506
+ try {
507
+ const error = await own.execute(createStatement("SELECT pg_terminate_backend(pg_backend_pid())")).catch((error) => error);
508
+ assert.ok(error instanceof ConnectionError, `Expected ConnectionError, got ${String(error)}`);
509
+ assert.equal(error.unsent, false);
510
+ } finally {
511
+ await own.close();
512
+ }
513
+ });
514
+ it("rejects a statement, a script, and a connection without a server with unsent ConnectionError", async () => {
515
+ const unreachable = createUnreachableDriver();
516
+ try {
517
+ await assertRejectsUnsent(unreachable.execute(createStatement("SELECT 1")));
518
+ await assertRejectsUnsent(unreachable.executeScript("SELECT 1"));
519
+ await assertRejectsUnsent(unreachable.acquire());
520
+ } finally {
521
+ await unreachable.close();
522
+ }
523
+ });
426
524
  it("replaces an idle connection whose backend exited", async () => {
427
525
  const own = createDriver({ max: 2 });
428
526
  const held = [];
@@ -530,15 +628,15 @@ function describeDriverContract(options, runner) {
530
628
  assert.equal(await query, "slept");
531
629
  assert.deepEqual(settled, ["query", "database"]);
532
630
  });
533
- it("rejects a statement, a script, and a connection requested right after it starts with ConnectionError", async () => {
631
+ it("rejects a statement, a script, and a connection requested right after it starts with unsent ConnectionError", async () => {
534
632
  const driver = createDriver({ max: 1 });
535
633
  const held = await driver.acquire();
536
634
  const closing = driver.close();
537
635
  const acquiring = driver.acquire();
538
636
  const rejections = [
539
- assert.rejects(driver.execute(createStatement("SELECT 1")), ConnectionError),
540
- assert.rejects(driver.executeScript("SELECT 1"), ConnectionError),
541
- assert.rejects(acquiring, ConnectionError)
637
+ assertRejectsUnsent(driver.execute(createStatement("SELECT 1"))),
638
+ assertRejectsUnsent(driver.executeScript("SELECT 1")),
639
+ assertRejectsUnsent(acquiring)
542
640
  ];
543
641
  acquiring.then((connection) => connection.release()).catch(() => {});
544
642
  await held.release();
@@ -814,4 +912,4 @@ async function poll(timeout, message, check) {
814
912
  clearTimeout(timer);
815
913
  }
816
914
  }
817
- export { createTestHelpers, describeDriverContract, getBackendPid, waitForLock, waitForLockedStatement };
915
+ export { createTestHelpers, describeDriverContract, getBackendPid, openTestFixture, openTestTransaction, waitForLock, waitForLockedStatement };
@@ -61,7 +61,8 @@ interface BaseDerivableType<Output, Input, Parameters extends unknown[] | undefi
61
61
  /**
62
62
  * Narrows the values of this type to `T` in TypeScript, such as to a union of strings that a `CHECK` constraint allows, or to the shape of JSON values.
63
63
  *
64
- * The values are not validated at runtime. `T` must be among the values that this type both returns and accepts, and `nullable` keeps `null` apart from it.
64
+ * The values are not validated at runtime.
65
+ * `T` must be among the values that this type both returns and accepts, and `nullable` keeps `null` apart from it.
65
66
  */
66
67
  of<T extends AcceptedValues<Exclude<Output, null>, Exclude<Input, null>>>(): DerivableType<T | AcceptedNull<Input>, T | AcceptedNull<Input>, Parameters>;
67
68
  }
package/dist/types.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as BaseDerivableType, c as Type, o as DerivableType, s as DerivableTypeWithParameters } from "./type-CbrtxvDF.mjs";
1
+ import { a as BaseDerivableType, c as Type, o as DerivableType, s as DerivableTypeWithParameters } from "./type-DU5oJfh_.mjs";
2
2
  /**
3
3
  * Creates a type of one-dimensional arrays of `element` values.
4
4
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kern-db",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "PostgreSQL database layer with ambient transactions, typed SQL queries, and test transactions",
5
5
  "keywords": [
6
6
  "database",