@kequtech/tenon-orm 0.2.1 → 0.2.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ - Closing a Tenon-owned client through `db.close()` prevents native LISTEN reconnects from
6
+ restoring connections after shutdown, including pending subscription setup. Borrowed clients
7
+ retain application ownership.
8
+ - Added `connection: { url, options? }` to preserve a deployment URI while configuring the native
9
+ driver. `ConnectionUri` is exported; `ConnectionOptions` includes the pinned driver's constructor
10
+ `onnotify` callback. Internally created clients remain Tenon-owned.
11
+ - Inspection accepts explicit `undefined` for optional session and timeout settings, including in
12
+ strict TypeScript configurations; unknown keys and invalid defined values still fail.
13
+ - `db.close({ timeoutMs? })` now shares one shutdown outcome across concurrent/later callers and
14
+ defaults to a 5,000 ms grace period before requesting owned driver termination; `0` skips the wait.
15
+ - Added `CardinalityError` for `one`/`oneOrNone` mismatches, with code
16
+ `TENON_CARDINALITY_MISMATCH`, expected mode, actual count, and optional label.
17
+
3
18
  ## 0.2.1
4
19
 
5
20
  - Added readonly `db.native` access to Tenon's existing Postgres.js client for LISTEN, cursors,
package/README.md CHANGED
@@ -2,21 +2,16 @@
2
2
 
3
3
  A PostgreSQL ORM designed for coding agents. Built for Node.js and TypeScript.
4
4
 
5
- Your coding agent already knows SQL. Tenon gives it a small API for parameterized queries,
6
- transactions, migrations, query inspection, and generated database types. Queries
7
- stay readable, reusable operations are ordinary functions, and humans can review exactly what will
8
- run against the database.
9
-
10
- Tenon takes the place of an ORM without hiding PostgreSQL behind a query builder. You or your agent
11
- write the SQL; Tenon handles binding values and returning results. Your application controls the
12
- connection configuration and validation. Tenon includes its pinned Postgres.js driver.
5
+ Write SQL directly, bind values with a template tag, and reuse queries through ordinary functions.
6
+ Tenon provides transactions, affected-row guards, SQL migrations, generated database types, and
7
+ query inspection. It includes its pinned Postgres.js driver.
13
8
 
14
9
  ```sh
15
10
  npm install @kequtech/tenon-orm
16
11
  ```
17
12
 
18
- Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and
19
- Node development typings.
13
+ Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and Node
14
+ development typings.
20
15
 
21
16
  ```ts
22
17
  import { createTenon, sql } from '@kequtech/tenon-orm';
@@ -39,70 +34,32 @@ try {
39
34
  }
40
35
  ```
41
36
 
42
- This standalone example assumes the table exists. In a server, reuse an instance per database and
43
- close it at shutdown, not after each request. Use `returns: 'many'` for an array, `'one'` when a row
44
- must exist, or omit the option for the driver's full result.
45
-
46
- Use [`db.native`](docs/reference.md#native-driver-access) to access the underlying Postgres.js client
47
- for LISTEN, cursors, COPY, and other native features without installing another driver. Native calls
48
- use the pool directly and bypass Tenon's query and transaction guards.
49
-
50
- Tenon also provides:
51
-
52
- - Transactions and affected-row limits that roll back rejected writes.
53
- - SQL migration files and a CLI to apply, inspect, and roll back migrations.
54
- - TypeScript row, insert, and update types generated from your database.
55
- - SQL fragments for sharing query parts or assembling variable conditions.
56
- - Query descriptions from PostgreSQL, including parameter/result types and session context.
57
-
58
- Joins, relationships, aggregates, and upserts use ordinary PostgreSQL SQL. Tenon does not introduce
59
- model classes or automatically load related records. Input validation, authorization, and database
60
- performance remain application concerns. You can keep an existing migration runner; Tenon's
61
- migrations and table type generation are independent conveniences.
62
-
63
- ## Here's what a coding agent says about it
37
+ The example assumes `public.users` exists. In a server, reuse the instance and close it at shutdown.
38
+ `oneOrNone` rejects multiple rows; use `one` for exactly one row, `many` for an array, or omit
39
+ `returns` for the driver's result array and metadata.
64
40
 
65
- > "The appeal is that I can read the query, see where the transaction starts and ends, and put
66
- > reusable logic in a function. There isn't much framework machinery to learn. The tradeoff is that
67
- > someone still has to write the SQL and check the result types. I'm happy to do that part. Please
68
- > still review my SQL."
69
- >
70
- > — Codex, after working on Tenon. An interested party, to put it mildly.
41
+ Joins, relationships, aggregates, and upserts use PostgreSQL SQL. Object-based writes can use
42
+ `sql.insertValues` and `sql.assignments` while keeping the surrounding statement explicit.
43
+ `db.native` exposes the driver for LISTEN, cursors, COPY, and other native features; those calls
44
+ bypass Tenon's query and transaction guards.
71
45
 
72
- ## Object-based writes
73
-
74
- For object-based writes, `sql.insertValues(rows)` preserves database defaults across heterogeneous
75
- rows and `sql.assignments(patch)` omits undefined fields. Keep the surrounding SQL explicit. Both
76
- support selected columns.
77
- Use [generated input types](docs/reference.md#insert-and-update-inputs) to check those objects
78
- against your schema.
79
-
80
- ## Checking query results
81
-
82
- The type in `sql<{ id: number; email: string }>` describes what you expect back. TypeScript alone
83
- cannot verify that your SQL actually selects those columns.
84
-
85
- Use `describeQuery({ connection, query })` to inspect the finished SQL, including dynamic fragments,
86
- or `npx tenon describe query.sql` for a SQL file. It reports PostgreSQL types, Postgres.js values,
87
- session context, and unresolved findings without executing the statement. No checking packages or
88
- compiler API are required. Keep database tests for actual values, nullability, authorization, and
89
- write effects; descriptions do not verify `sql<Row>`. See [query inspection](docs/query-inspection.md).
46
+ `sql<Row>` declares an expected result type. `describeQuery` inspects the finished SQL through
47
+ PostgreSQL without executing it; it does not verify that annotation. Keep database tests for actual
48
+ rows and write effects. Validation, authorization, and retry policy belong to the application.
49
+ Existing migration runners can be retained; Tenon's migrations and type generation are optional.
90
50
 
91
51
  ## Documentation
92
52
 
93
- - **[Changelog](CHANGELOG.md)** — release changes and upgrade notes.
94
- - **[Human guide](docs/human-guide.md)** — learn the workflow through practical examples.
95
- - **[Agent guide](docs/agent-guide.md)** — self-contained setup, API contracts, and implementation
96
- conventions.
97
- - **[Reference](docs/reference.md)** — complete options, query inspection, type mappings, and
98
- limitations.
99
- - **[Adoption guide](docs/adoption-guide.md)** — replace an existing ORM and, if needed, transfer
100
- migration ownership, update local write helpers, and validate converted queries.
101
- - **[Mutation cookbook](docs/mutations.md)** — defaults, patches, upserts, arrays, batching, and
102
- the boundaries between input typing and database validation.
103
-
104
- The complete `docs/` folder ships with the npm package. Maintainer instructions are in
105
- [CONTRIBUTING.md](https://github.com/kequtech/tenon-orm/blob/main/CONTRIBUTING.md), which is not
106
- bundled.
53
+ - **[Human guide](docs/human-guide.md)** — a table, queries, transactions, and generated types.
54
+ - **[Agent guide](docs/agent-guide.md)** — implementation contracts and upgrade workflow.
55
+ - **[Reference](docs/reference.md)** — API options, value mappings, CLI, and limitations.
56
+ - **[Adoption guide](docs/adoption-guide.md)** — replace an ORM and transfer migration ownership.
57
+ - **[Mutation cookbook](docs/mutations.md)** — defaults, patches, upserts, arrays, and batching.
58
+ - **[Query inspection](docs/query-inspection.md)** — Describe reports and database tests.
59
+ - **[Changelog](CHANGELOG.md)** — versioned changes and upgrade instructions.
60
+
61
+ These documents all ship with the npm package, including the changelog so an agent upgrading a
62
+ project can read the relevant release entries locally. Repository maintenance instructions are in
63
+ [CONTRIBUTING.md](https://github.com/kequtech/tenon-orm/blob/main/CONTRIBUTING.md).
107
64
 
108
65
  [ISC licensed](LICENSE). Maintained by [Kequtech](https://github.com/kequtech).
@@ -1,7 +1,13 @@
1
1
  import postgres from 'postgres';
2
2
  import type { UnknownRow } from './query.js';
3
- export type ConnectionOptions = postgres.Options<{}>;
4
- export type Connection = string | ConnectionOptions | postgres.Sql;
3
+ export type ConnectionOptions = postgres.Options<{}> & {
4
+ readonly onnotify?: ((channel: string, payload: string) => void) | undefined;
5
+ };
6
+ export interface ConnectionUri {
7
+ readonly url: string;
8
+ readonly options?: ConnectionOptions | undefined;
9
+ }
10
+ export type Connection = string | ConnectionOptions | ConnectionUri | postgres.Sql;
5
11
  export type QueryResult<Row extends object = UnknownRow> = postgres.RowList<Row[]>;
6
12
  /** The driver operations shared by ordinary and reserved connections. */
7
13
  export interface SqlConnection {
@@ -11,6 +11,19 @@ export function createConnection(connection) {
11
11
  return { sql: postgres(connection), owned: true };
12
12
  }
13
13
  plainObject(connection, 'Connection configuration');
14
+ if ('url' in connection) {
15
+ for (const key of Object.keys(connection)) {
16
+ if (key !== 'url' && key !== 'options') {
17
+ throw new Error(`Unknown URI connection option: ${key}`);
18
+ }
19
+ }
20
+ if (typeof connection.url !== 'string' || !connection.url.trim()) {
21
+ throw new Error('Connection string must not be empty.');
22
+ }
23
+ if (connection.options !== undefined)
24
+ plainObject(connection.options, 'Connection options');
25
+ return { sql: postgres(connection.url, connection.options), owned: true };
26
+ }
14
27
  return { sql: postgres(connection), owned: true };
15
28
  }
16
29
  export function executeSql(sql, text, input = []) {
package/dist/database.js CHANGED
@@ -1,8 +1,24 @@
1
1
  import { createClient } from './client.js';
2
2
  import { createConnection, executeSql } from './connection.js';
3
3
  import { prepareArrays } from './driver/array.js';
4
+ import { guardListenerShutdown } from './driver/listener.js';
4
5
  import { withTransaction } from './transaction.js';
5
6
  import { plainObject } from './validation.js';
7
+ function closeTimeout(options) {
8
+ const settings = plainObject(options === undefined ? {} : options, 'Close options');
9
+ for (const key of Object.keys(settings)) {
10
+ if (key !== 'timeoutMs')
11
+ throw new Error(`Unknown close option: ${key}`);
12
+ }
13
+ const timeoutMs = settings.timeoutMs === undefined ? 5000 : settings.timeoutMs;
14
+ if (typeof timeoutMs !== 'number' ||
15
+ !Number.isInteger(timeoutMs) ||
16
+ timeoutMs < 0 ||
17
+ timeoutMs > 2147483647) {
18
+ throw new Error('Close timeoutMs must be an integer from 0 to 2147483647 milliseconds.');
19
+ }
20
+ return timeoutMs;
21
+ }
6
22
  export function createTenon(options) {
7
23
  for (const key of Object.keys(plainObject(options, 'Tenon options'))) {
8
24
  if (key !== 'connection' && key !== 'onQuery') {
@@ -13,9 +29,10 @@ export function createTenon(options) {
13
29
  throw new Error('onQuery must be a function.');
14
30
  }
15
31
  const { sql: pool, owned } = createConnection(options.connection);
16
- let closed = false;
32
+ const stopListeners = owned ? guardListenerShutdown(pool) : undefined;
33
+ let closing;
17
34
  function checkOpen() {
18
- if (closed) {
35
+ if (closing) {
19
36
  throw new Error('This database instance is closed.');
20
37
  }
21
38
  }
@@ -34,13 +51,22 @@ export function createTenon(options) {
34
51
  checkOpen();
35
52
  return withTransaction(pool, (query) => callback(createClient(query, options.onQuery)));
36
53
  },
37
- async close() {
38
- if (closed) {
39
- return;
54
+ close(options) {
55
+ if (closing)
56
+ return closing;
57
+ try {
58
+ const timeoutMs = closeTimeout(options);
59
+ // Cache before invoking driver callbacks, so reentrant/concurrent callers share shutdown.
60
+ closing = Promise.resolve().then(() => {
61
+ if (owned) {
62
+ stopListeners();
63
+ return pool.end({ timeout: timeoutMs / 1000 });
64
+ }
65
+ });
66
+ return closing;
40
67
  }
41
- closed = true;
42
- if (owned) {
43
- await pool.end();
68
+ catch (error) {
69
+ return Promise.reject(error);
44
70
  }
45
71
  },
46
72
  };
@@ -0,0 +1,3 @@
1
+ import type postgres from 'postgres';
2
+ /** Prevent an owned LISTEN client's queued reconnect from reopening it after shutdown. */
3
+ export declare function guardListenerShutdown(sql: postgres.Sql): () => void;
@@ -0,0 +1,63 @@
1
+ import { createConnection } from 'node:net';
2
+ function closedError() {
3
+ return Object.assign(new Error('The owned PostgreSQL listener is closed.'), {
4
+ code: 'CONNECTION_ENDED',
5
+ });
6
+ }
7
+ /** Prevent an owned LISTEN client's queued reconnect from reopening it after shutdown. */
8
+ export function guardListenerShutdown(sql) {
9
+ const options = sql.options;
10
+ const listen = sql.listen;
11
+ const sockets = new Set();
12
+ let closed = false;
13
+ let hostIndex = 0;
14
+ function track(socket) {
15
+ // TLS adoption removes raw socket listeners; prune those closed transports on reconnect.
16
+ for (const previous of sockets)
17
+ if (previous.destroyed)
18
+ sockets.delete(previous);
19
+ sockets.add(socket);
20
+ socket.once('close', () => sockets.delete(socket));
21
+ if (closed) {
22
+ socket.destroy();
23
+ throw closedError();
24
+ }
25
+ return socket;
26
+ }
27
+ sql.listen = (...args) => {
28
+ if (closed)
29
+ return Promise.reject(closedError());
30
+ const previous = options.socket;
31
+ // Postgres.js synchronously clones these options for its dedicated max:1 listener.
32
+ // Restore immediately so ordinary queries retain their native transport configuration.
33
+ options.socket = (settings) => {
34
+ if (closed)
35
+ throw closedError();
36
+ if (previous)
37
+ return Promise.resolve(previous(settings)).then(track);
38
+ const host = settings.host[hostIndex];
39
+ const port = settings.port[hostIndex];
40
+ const socket = createConnection(settings.path ? { path: settings.path } : { host, port });
41
+ if (!settings.path) {
42
+ hostIndex = (hostIndex + 1) % settings.port.length;
43
+ // The driver's TLS negotiation uses this metadata for SNI and verification.
44
+ Object.assign(socket, { host, port });
45
+ }
46
+ // Node buffers startup writes while connecting; the driver retains its connect timeout,
47
+ // authentication, TLS negotiation and reconnect handling.
48
+ return track(socket);
49
+ };
50
+ try {
51
+ return listen(...args);
52
+ }
53
+ finally {
54
+ options.socket = previous;
55
+ }
56
+ };
57
+ return () => {
58
+ closed = true;
59
+ for (const socket of sockets)
60
+ socket.destroy();
61
+ sockets.clear();
62
+ };
63
+ }
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,12 @@
1
1
  import type { AffectedRowsConstraint } from './affected-rows.js';
2
+ export declare class CardinalityError extends Error {
3
+ name: string;
4
+ readonly code = "TENON_CARDINALITY_MISMATCH";
5
+ readonly expected: 'one' | 'oneOrNone';
6
+ readonly actual: number;
7
+ readonly label?: string;
8
+ constructor(expected: 'one' | 'oneOrNone', actual: number, label?: string);
9
+ }
2
10
  export declare class AffectedRowsError extends Error {
3
11
  name: string;
4
12
  readonly code: 'TENON_AFFECTED_ROWS_MISMATCH' | 'TENON_AFFECTED_ROWS_UNSUPPORTED';
package/dist/errors.js CHANGED
@@ -1,3 +1,18 @@
1
+ export class CardinalityError extends Error {
2
+ name = 'CardinalityError';
3
+ code = 'TENON_CARDINALITY_MISMATCH';
4
+ expected;
5
+ actual;
6
+ label;
7
+ constructor(expected, actual, label) {
8
+ const count = expected === 'one' ? 'exactly one row' : 'at most one row';
9
+ super(`${label ?? 'Query'} expected ${count}, but returned ${actual} rows.`);
10
+ this.expected = expected;
11
+ this.actual = actual;
12
+ if (label !== undefined)
13
+ this.label = label;
14
+ }
15
+ }
1
16
  export class AffectedRowsError extends Error {
2
17
  name = 'AffectedRowsError';
3
18
  code;
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  export type { JSONValue } from 'postgres';
2
- export type { Connection, ConnectionOptions, QueryResult } from './connection.js';
2
+ export type { Connection, ConnectionOptions, ConnectionUri, QueryResult } from './connection.js';
3
3
  export type { AffectedRowsConstraint } from './affected-rows.js';
4
4
  export { createTenon } from './database.js';
5
5
  export type { TenonOptions } from './database.js';
6
- export { AffectedRowsError } from './errors.js';
6
+ export { AffectedRowsError, CardinalityError } from './errors.js';
7
7
  export type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
8
8
  export { identifier, sql, unsafeSql } from './query.js';
9
9
  export type { SqlFragment, SqlQuery, UnknownRow } from './query.js';
10
- export type { Database, QueryClient, QueryEvent, Transaction } from './types.js';
10
+ export type { CloseOptions, Database, QueryClient, QueryEvent, Transaction } from './types.js';
11
11
  export { describeQuery } from './describe.js';
12
12
  export type { DescribeOptions, DescribedColumn, DescribedType, DescriptionIssue, InspectionContext, InspectionSession, InspectionTimeouts, QueryDescription, } from './inspection/types.js';
13
13
  export { adoptBaseline, captureBaseline } from './baseline.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { createTenon } from './database.js';
2
- export { AffectedRowsError } from './errors.js';
2
+ export { AffectedRowsError, CardinalityError } from './errors.js';
3
3
  export { identifier, sql, unsafeSql } from './query.js';
4
4
  export { describeQuery } from './describe.js';
5
5
  export { adoptBaseline, captureBaseline } from './baseline.js';
@@ -14,9 +14,9 @@ export async function runDescribe(filename, connection, values) {
14
14
  connection,
15
15
  query: await readFile(filename, 'utf8'),
16
16
  session: {
17
- ...(values.role !== undefined ? { role: values.role } : {}),
18
- ...(values['search-path'] !== undefined ? { searchPath: values['search-path'] } : {}),
19
- ...(values['time-zone'] !== undefined ? { timeZone: values['time-zone'] } : {}),
17
+ role: values.role,
18
+ searchPath: values['search-path'],
19
+ timeZone: values['time-zone'],
20
20
  },
21
21
  timeouts: {
22
22
  ...(values['connect-timeout-ms'] !== undefined
@@ -8,8 +8,8 @@ export declare function inspectionOptions(options: Pick<DescribeOptions, 'connec
8
8
  lockMs: number;
9
9
  };
10
10
  session: {
11
- role?: string;
12
- searchPath?: string;
13
- timeZone?: string;
11
+ role?: string | undefined;
12
+ searchPath?: string | undefined;
13
+ timeZone?: string | undefined;
14
14
  };
15
15
  };
@@ -10,6 +10,8 @@ export function inspectionOptions(options) {
10
10
  if (!['role', 'searchPath', 'timeZone'].includes(key)) {
11
11
  throw new Error(`Unknown inspection session option: ${key}`);
12
12
  }
13
+ if (value === undefined)
14
+ continue;
13
15
  if (typeof value !== 'string' || !value.trim() || value.includes('\0')) {
14
16
  throw new Error(`Inspection session ${key} requires a nonempty string without NUL.`);
15
17
  }
@@ -20,11 +22,17 @@ export function inspectionOptions(options) {
20
22
  for (const [key, value] of Object.entries(timeouts)) {
21
23
  if (!Object.hasOwn(defaults, key))
22
24
  throw new Error(`Unknown inspection timeout: ${key}`);
25
+ if (value === undefined)
26
+ continue;
23
27
  if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 2147483647) {
24
28
  throw new Error(`Inspection timeout ${key} must be a positive integer in milliseconds.`);
25
29
  }
26
30
  }
27
- const limits = { ...defaults, ...timeouts };
31
+ const limits = {
32
+ connectMs: timeouts.connectMs ?? defaults.connectMs,
33
+ statementMs: timeouts.statementMs ?? defaults.statementMs,
34
+ lockMs: timeouts.lockMs ?? defaults.lockMs,
35
+ };
28
36
  // URL startup options must not disable the inspection's server-side bounds.
29
37
  let url;
30
38
  try {
@@ -1,14 +1,14 @@
1
1
  import type { SqlQuery } from '../query.js';
2
2
  export interface InspectionSession {
3
- readonly role?: string;
3
+ readonly role?: string | undefined;
4
4
  /** PostgreSQL's search_path setting, as used in the application's connection options. */
5
- readonly searchPath?: string;
6
- readonly timeZone?: string;
5
+ readonly searchPath?: string | undefined;
6
+ readonly timeZone?: string | undefined;
7
7
  }
8
8
  export interface InspectionTimeouts {
9
- readonly connectMs?: number;
10
- readonly statementMs?: number;
11
- readonly lockMs?: number;
9
+ readonly connectMs?: number | undefined;
10
+ readonly statementMs?: number | undefined;
11
+ readonly lockMs?: number | undefined;
12
12
  }
13
13
  export interface InspectionContext {
14
14
  readonly mode: 'separate-session';
@@ -27,8 +27,8 @@ export interface DescribeOptions {
27
27
  readonly connection: string;
28
28
  /** Finished SQL; query values are neither bound nor executed. */
29
29
  readonly query: string | Pick<SqlQuery, 'text'>;
30
- readonly session?: InspectionSession;
31
- readonly timeouts?: InspectionTimeouts;
30
+ readonly session?: InspectionSession | undefined;
31
+ readonly timeouts?: InspectionTimeouts | undefined;
32
32
  }
33
33
  export type DescriptionIssue = {
34
34
  readonly code: 'nullability-unknown';
@@ -1,5 +1,6 @@
1
1
  import { validateAffectedRows } from './affected-rows.js';
2
2
  import { plainObject } from './validation.js';
3
+ import { CardinalityError } from './errors.js';
3
4
  export function validateOptions(options) {
4
5
  const config = options === undefined ? {} : plainObject(options, 'Query options');
5
6
  for (const key of Object.keys(config)) {
@@ -27,9 +28,8 @@ export function queryOutput(options, result) {
27
28
  return result;
28
29
  if (options.returns === 'many')
29
30
  return [...result];
30
- const expected = options.returns === 'oneOrNone' ? 'at most one row' : 'exactly one row';
31
31
  if (result.length > 1 || (options.returns === 'one' && result.length === 0)) {
32
- throw new Error(`${options.label ?? 'Query'} expected ${expected}, but returned ${result.length} rows.`);
32
+ throw new CardinalityError(options.returns === 'oneOrNone' ? 'oneOrNone' : 'one', result.length, options.label);
33
33
  }
34
34
  return result[0];
35
35
  }
package/dist/types.d.ts CHANGED
@@ -12,12 +12,16 @@ export interface QueryClient {
12
12
  query<Row extends object>(query: SqlQuery<Row>, options: QueryOptions | undefined): NoInfer<Promise<QueryOutput<Row, QueryReturnMode>>>;
13
13
  }
14
14
  export type Transaction = QueryClient;
15
+ export interface CloseOptions {
16
+ /** Grace period before requesting owned driver termination; milliseconds, default 5000. */
17
+ readonly timeoutMs?: number | undefined;
18
+ }
15
19
  export type Database = Transaction & {
16
20
  /** The underlying pool-level driver client; native calls bypass Tenon's query and transaction guards. */
17
21
  readonly native: postgres.Sql;
18
22
  /** Await all work inside the callback. Commit on success; rollback on an escaping error. */
19
23
  transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>;
20
- close(): Promise<void>;
24
+ close(options?: CloseOptions): Promise<void>;
21
25
  };
22
26
  /** One completed public query call, including rollback/commit time for guarded calls. */
23
27
  export interface QueryEvent {