@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/docs/reference.md CHANGED
@@ -1,420 +1,367 @@
1
1
  # Tenon reference
2
2
 
3
- Use this document to look up exact API behavior, query inspection, and limitations. The
4
- [human guide](human-guide.md) teaches the workflow; the [agent guide](agent-guide.md) is a
5
- self-contained implementation guide. [Adoption](adoption-guide.md) covers replacing an existing ORM
6
- and transferring migration history.
3
+ Exact API contracts and limitations. The [human guide](human-guide.md) introduces the workflow;
4
+ the [agent guide](agent-guide.md) covers routine implementation. Examples below use the
5
+ `public.users` table from the human guide and an existing `db` instance, unless stated otherwise.
7
6
 
8
7
  ## Installation and connection
9
8
 
10
- Runtime requirements: Node.js 24+, PostgreSQL 16+, and ESM. The Postgres.js driver is included. TypeScript consumers need
11
- TypeScript 6+ and Node development typings. Keep your application's strict TypeScript
12
- setup; Tenon needs no special compiler configuration. `"type": "module"` and TypeScript's
13
- `"module": "NodeNext"` are a usual Node setup.
9
+ Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and Node
10
+ development typings. Tenon includes `postgres@3.4.9`; no driver peer or special compiler plugin is
11
+ required. A usual Node TypeScript setup uses `module: "NodeNext"`.
14
12
 
15
13
  ```sh
16
14
  npm install @kequtech/tenon-orm
17
15
  ```
18
16
 
19
- `createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, `native`, and
20
- `close`. The required `connection` accepts a URL, Postgres.js options, or an existing Postgres.js client. An
21
- explicit `{}` selects the driver's environment defaults. Tenon does not load environment files
22
- automatically.
17
+ `createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, `native`,
18
+ and `close`. The required `connection` accepts:
23
19
 
24
- Reuse an instance per database connection. `close()` closes pools created by Tenon; supplied pools
25
- remain application-owned. Finish outstanding work before closing. Postgres.js handles connection errors through rejected queries and its native connection callbacks.
26
-
27
- Qualify SQL names or configure the driver's `search_path`. The CLI's `--namespace` selects the
28
- schema used by tooling, not a runtime query namespace.
29
-
30
- ## Native driver access
31
-
32
- `db.native` is the underlying Postgres.js client, typed as `postgres.Sql`. The property is readonly
33
- in TypeScript. It exposes the existing pool without creating another client or requiring a separate
34
- driver installation. A supplied client is returned by identity. Use native APIs for LISTEN, cursors,
35
- COPY, and other driver features; their behavior follows the included `postgres@3.4.9`.
20
+ - A PostgreSQL URI string.
21
+ - `{ url: string, options?: ConnectionOptions }` to combine a URI with driver settings.
22
+ - A Postgres.js options object; an explicit `{}` uses the driver's environment defaults.
23
+ - An existing Postgres.js client, borrowed by identity.
36
24
 
37
25
  ```ts
38
- const db = createTenon({ connection });
39
- await db.native.listen('updates', handlePayload, refreshAfterSubscription);
40
- // At application shutdown, after outstanding work finishes:
41
- await db.close();
26
+ const db = createTenon({
27
+ connection: {
28
+ url: connection,
29
+ options: { connect_timeout: 5, idle_timeout: 10, connection: { search_path: 'app,public' } },
30
+ },
31
+ });
42
32
  ```
43
33
 
44
- Postgres.js LISTEN creates a dedicated physical connection alongside the query pool. Applications
45
- own notification parsing, authorization, callback failures, and catch-up after resubscription. See
46
- the [Postgres.js native API](https://github.com/porsager/postgres/tree/v3.4.9#listen--notify).
34
+ Tenon forwards `{ url, options }` to `postgres(url, options)`. Explicit driver options override
35
+ corresponding URI pool/transport options. URI startup parameters such as `search_path` and
36
+ `application_name` override `options.connection`. Timeouts above are seconds; preserve the
37
+ deployment's credentials and TLS requirements. `ConnectionUri` and `ConnectionOptions` are
38
+ exported. The latter includes `onnotify(channel, payload)` for native notification handling;
39
+ configure callbacks at construction.
40
+
41
+ Reuse an instance for each database configuration. `db.close({ timeoutMs? })` rejects new Tenon
42
+ work immediately and gives owned connections a 5,000 ms grace period by default before requesting
43
+ driver termination. Use `0` for immediate termination. The timeout must be an integer from 0 to
44
+ 2,147,483,647 milliseconds; `CloseOptions` is exported. Finish outstanding work before closing
45
+ when possible. The timeout bounds the shutdown wait and rejects pending client queries; it is not
46
+ a PostgreSQL cancellation request. Server work can complete afterward, including an autocommitted
47
+ write, and JavaScript inside a transaction callback is not canceled.
48
+
49
+ The first valid close call chooses the timeout. Concurrent and later calls return the same promise,
50
+ including the same shutdown failure; later options do not change it. Invalid initial options reject
51
+ without closing the instance. For a borrowed client, close only disables the Tenon wrapper and
52
+ leaves the driver pool and its work to the caller. Tenon does not load environment files.
53
+
54
+ Qualify SQL names or configure runtime `search_path`. CLI `--namespace` selects tooling scope;
55
+ it does not configure application queries. Baseline capture and Describe accept connection
56
+ strings rather than the runtime connection union.
47
57
 
48
- Native calls bypass Tenon's result contracts, affected-row guards, SQL boundary checks, and
49
- `onQuery` observation. They use native preparation defaults. `db.native` always refers to the pool,
50
- including inside `db.transaction`: its queries do not join that transaction. Transaction callbacks
51
- expose only `tx.query`; use that for work that must commit or roll back together.
58
+ ## Native driver access
59
+
60
+ `db.native` is the actual pool-level Postgres.js client, readonly in TypeScript. Use it for LISTEN,
61
+ cursors, COPY, and other [native APIs](https://github.com/porsager/postgres/tree/v3.4.9#usage).
62
+ Native calls bypass Tenon's result modes, affected-row guards, SQL checks, and `onQuery`. They use
63
+ native preparation defaults and do not join a Tenon transaction; callback clients expose only
64
+ `tx.query`.
65
+
66
+ Native access alone does not install Tenon's array correction. The first Tenon query or transaction
67
+ installs it on the shared client; native statements prepared earlier may retain their old parsers.
52
68
 
53
- Native access is immediate and does not initialize Tenon's array correction. The first Tenon query
54
- or transaction installs that correction on the shared client; already prepared native statements
55
- can retain earlier parsers. Raw calls otherwise follow native driver behavior.
69
+ Native `.listen(channel, onPayload, onReady?)` uses a dedicated physical connection. `onReady`
70
+ runs after subscription and resubscription. Missed notifications are not replayed; the application
71
+ owns catch-up and callback errors. That dedicated client installs its own close handler, so a
72
+ constructor `onclose` does not report its outages. Explicit LISTEN on a dedicated reserved native
73
+ connection can use constructor callbacks when disconnect detection is required. Subscription
74
+ cancellation follows the pinned driver; known reconnect limitations are recorded in
75
+ [maintainer considerations](https://github.com/kequtech/tenon-orm/blob/main/CONSIDERATIONS.md#native-driver-access-and-listen-lifecycle).
56
76
 
57
- `db.close()` ends a Tenon-owned client and its native listeners. With a borrowed client, it closes
58
- only the Tenon wrapper: native queries and listeners remain usable until their owner calls
59
- `db.native.end()`. Calling `end()` directly also ends the client used by Tenon.
77
+ Close owned clients through `db.close()` to prevent queued LISTEN reconnects from reopening them.
78
+ Direct `db.native.end()` bypasses that protection. Borrowed clients and their listeners remain
79
+ usable after wrapper close until their owner ends them.
60
80
 
61
81
  ## Queries and result modes
62
82
 
83
+ `sql<Row>` creates a detached `SqlQuery<Row>` with SQL `text` and ordered `values`; it does not
84
+ execute. `Row` declares one selected row. Without an annotation, fields are `unknown`. An
85
+ annotation does not verify the SQL or validate returned data.
86
+
63
87
  ```ts
64
- const result = await db.query(sql<Row>`SELECT ...`, {
65
- returns: 'many',
66
- label: 'list-users',
67
- });
88
+ const user = await db.query(
89
+ sql<{ id: number; email: string }>`SELECT id, email FROM public.users WHERE id = ${42}`,
90
+ { returns: 'oneOrNone', label: 'user-by-id' },
91
+ );
68
92
  ```
69
93
 
70
- `sql<Row>` produces a `SqlQuery<Row>` with SQL `text` and ordered `values`. It does not execute.
71
- `Row` describes one selected row; fields are `unknown` when no annotation is supplied. Type
72
- annotations do not verify SQL or validate JSON. Full table types from generation do not
73
- automatically describe joins or aggregates.
74
-
75
- `query` accepts `{ returns?, affectedRows?, label? }`. Unknown options are rejected. `label` must be
76
- a nonempty string when supplied and is used for diagnostics.
94
+ `query` options are `{ returns?, affectedRows?, label? }`; unknown keys fail. A supplied `label`
95
+ must be a nonempty string.
77
96
 
78
- | `returns` | Result | Returned rows allowed |
79
- | ------------------ | ------------------------------------ | --------------------- |
80
- | `native` (default) | Postgres.js `QueryResult<Row>` array | Any |
81
- | `many` | `Row[]` | Any, including zero |
82
- | `oneOrNone` | `Row \| undefined` | Zero or one |
83
- | `one` | `Row` | Exactly one |
97
+ | `returns` | Output | Allowed returned rows |
98
+ | ------------------ | ------------------------------- | --------------------- |
99
+ | `native` (default) | `QueryResult<Row>` driver array | Any |
100
+ | `many` | Plain `Row[]` | Any |
101
+ | `oneOrNone` | `Row \| undefined` | Zero or one |
102
+ | `one` | `Row` | Exactly one |
84
103
 
85
- Native results are arrays of rows with `command`, `count`, `columns`, `statement`, and `state`
86
- metadata. `count` is the affected-row count, independent of `RETURNING`; it can be null for commands
87
- without a count. `many` returns a plain row array. Result modes never add `LIMIT` or `RETURNING`.
88
- A returned-row mismatch does not undo an autocommitted write. Use `affectedRows` for write limits
89
- that roll back on failure.
104
+ Native arrays carry `command`, `count`, `columns`, `statement`, and `state` metadata. `count` is
105
+ the command's affected-row count, independent of `RETURNING`; it can be null for commands without
106
+ a count. Result modes never add `LIMIT` or `RETURNING`. A returned-row mismatch alone does not
107
+ undo an autocommitted write; use `affectedRows` for write bounds that roll back on failure.
90
108
 
91
- Reusable queries are ordinary functions. Accept a `QueryClient` and named object inputs when useful;
92
- both `Database` and `Transaction` implement `QueryClient`. Keep result annotations on the SQL and
93
- execution options at the query call. Public types include `QueryOptions`, `QueryReturnMode`, and
94
- `QueryOutput<Row, Mode>`.
109
+ Functions accepting `QueryClient` can use either `Database` or `Transaction`. Public types also
110
+ include `QueryOptions`, `QueryReturnMode`, and `QueryOutput<Row, Mode>`.
95
111
 
96
112
  ## SQL parameters and identifiers
97
113
 
98
- The `sql` tag binds ordinary interpolations as `$1`, `$2`, etc. Never quote interpolations or author
99
- manual parameter placeholders inside templates. Every public query executes one statement, including
100
- parameterless queries.
101
-
102
- Values use Postgres.js's native conversions. An undefined bound parameter is rejected. Objects and
103
- arrays destined for JSON columns are serialized by the driver; passing pre-serialized JSON text
104
- stores a JSON string. Arrays bound to PostgreSQL array columns retain null elements. SQL context
105
- establishes array and JSON parameter types; cast standalone parameters to the intended SQL type.
106
- Tenon uses the driver's raw-query path with the extended protocol so every public call executes
107
- one statement, including calls without parameters. Application queries explicitly disable prepared
108
- statement caching, including when the supplied driver client has `prepare: true`. Parameters remain
109
- bound separately from SQL text.
110
-
111
- Use an array parameter for a set of values:
114
+ Ordinary `sql` interpolations bind values. Do not quote them or write manual `$n` placeholders
115
+ inside templates. `undefined` bound parameters fail; `null` means SQL null. Pass JSON objects and
116
+ arrays directly. SQL context distinguishes PostgreSQL arrays from JSON; cast standalone parameters
117
+ when that context is absent.
112
118
 
113
119
  ```ts
120
+ const ids = [1, 2, 3];
114
121
  const query = sql<{ id: number }>`SELECT id FROM public.users
115
122
  WHERE id = ANY(${ids}::integer[])`;
116
123
  ```
117
124
 
118
- `identifier(schema, table)` quotes each component separately. Names must be nonempty, contain no
119
- NUL, and fit PostgreSQL's 63-byte identifier limit. Choose names from application-approved
120
- alternatives; quoting does not authorize access. Ordinary strings such as `'DESC'` are bound values,
121
- not SQL syntax. For ordering, use complete query alternatives or application-selected fragments
122
- containing `ASC` or `DESC`.
125
+ `identifier(schema, table)` quotes components separately. Names must contain 1–63 UTF-8 bytes and
126
+ no NUL. Choose application-approved names; quoting does not authorize access. SQL syntax such as
127
+ `DESC` belongs in fixed query alternatives or fragments, not a string parameter.
123
128
 
124
- `unsafeSql<Row>(text, values)` accepts externally authored, trusted SQL and an explicit values
125
- array, including `[]` when empty. Never concatenate request input into its text.
126
- Full `sql` or `unsafeSql` queries cannot be inserted into other queries; use
127
- fragments for partial SQL.
129
+ `unsafeSql<Row>(text, values)` accepts trusted external SQL and an explicit values array, including
130
+ `[]`. Never concatenate request input into its text. Full `sql` and `unsafeSql` queries cannot be
131
+ nested; use fragments for partial SQL.
128
132
 
129
- ## SQL fragments and joins
133
+ Each Tenon query executes one statement through the extended protocol. Application execution
134
+ explicitly disables prepared-statement caching, including on supplied clients with `prepare: true`.
135
+ Values remain bound separately. Migrations deliberately support multi-statement scripts.
130
136
 
131
- `sql.fragment` creates an immutable piece of SQL with bound values. It has no row type and cannot
132
- execute alone. Insert it into a complete `sql<Row>` query. Use fragments to share predicates or
133
- other SQL expressions between queries. Blank fragments throw.
137
+ ## SQL fragments and joins
134
138
 
135
- `sql.join(fragments, separator)` combines fragments into a new fragment. Use it when the number of
136
- parts varies: optional filters, a condition for each search term, or a list of SQL expressions.
137
- Write fixed structure directly in SQL. Supported separators are:
139
+ `sql.fragment` creates immutable partial SQL with bound values. It has no row type, cannot execute
140
+ alone, and rejects blank SQL. Insert it into a complete `sql<Row>` query.
138
141
 
139
- | Argument | Inserted SQL | Typical use |
140
- | -------- | ------------ | -------------------------- |
141
- | `'AND'` | `AND` | All conditions must hold |
142
- | `'OR'` | `OR` | Any condition may hold |
143
- | `','` | `, ` | Projections or assignments |
142
+ `sql.join(fragments, separator)` requires a nonempty list. Separators are `'AND'`, `'OR'`, and `','`.
143
+ It does not add parentheses or change SQL precedence. Each fragment use receives correctly ordered
144
+ parameters, including repeated and nested uses; literal `$1` text in strings/comments is unchanged.
144
145
 
145
146
  ```ts
147
+ const email: string | undefined = 'reader@example.test';
146
148
  const conditions = [sql.fragment`active = ${true}`];
147
149
  if (email !== undefined) conditions.push(sql.fragment`email = ${email}`);
148
- if (afterId !== undefined) conditions.push(sql.fragment`id > ${afterId}`);
149
-
150
150
  const query = sql<{ id: number }>`SELECT id FROM public.users
151
151
  WHERE ${sql.join(conditions, 'AND')} ORDER BY id`;
152
152
  ```
153
153
 
154
- The list must be nonempty; the example keeps the active condition even when both optional filters
155
- are absent. Joins insert separators without adding parentheses or changing SQL precedence. Group
156
- mixed AND/OR expressions with parentheses, as in ordinary SQL.
157
-
158
- Each fragment use receives correctly ordered parameters, including repeated and nested uses. SQL
159
- strings and comments containing literal `$1` text are left intact.
154
+ The mandatory predicate keeps the list nonempty. If every filter is optional, choose a complete
155
+ query without `WHERE` for the empty case. Group mixed `AND`/`OR` expressions explicitly.
160
156
 
161
157
  ## Object-based mutation fragments
162
158
 
163
- `sql.insertValues(input, columns?)` and `sql.assignments(input, columns?)` return fragments. Keep the
164
- operation, table, authorization predicate, conflict policy, expressions, and `RETURNING` in SQL:
165
-
166
- ```ts
167
- const created = await db.query(
168
- sql<{ id: number }>`
169
- INSERT INTO profiles ${sql.insertValues([
170
- { name: 'Ada', bio: null },
171
- { name: 'Lin', score: 4 },
172
- ])}
173
- ON CONFLICT (name) DO NOTHING
174
- RETURNING id
175
- `,
176
- { returns: 'many' },
177
- );
178
-
179
- await db.query(sql`
180
- UPDATE profiles SET ${sql.assignments({ name: input.name, bio: input.bio })},
181
- revision = revision + 1
182
- WHERE id = ${id} AND owner_id = ${viewer}
183
- `);
184
- ```
185
-
186
- Both helpers accept plain objects (including null-prototype objects), quote each column as one
187
- identifier, and bind cell values as data. Inputs are read when the fragment is created; replacing a
188
- field afterward does not change its captured value. Nested values retain the driver's usual
189
- serialization behavior. Only own data properties are read; accessors are rejected.
190
-
191
- | Input | Insert behavior | Assignment behavior |
192
- | -------------------------------------------- | ---------------------------------- | --------------------------------------- |
193
- | Missing or `undefined` field | PostgreSQL `DEFAULT` for that cell | Omitted; the stored column is unchanged |
194
- | Explicit `null` | Bound SQL null | Bound SQL null |
195
- | Other value | Bound parameter | Bound parameter |
196
- | SQL fragment, identifier, or query as a cell | Rejected | Rejected |
197
-
198
- Without `columns`, inserts infer the union of enumerable keys with defined values across all rows,
199
- in first-seen order. Heterogeneous rows stay aligned; missing cells use `DEFAULT`. One object or a
200
- one-row batch with no defined fields emits `DEFAULT VALUES`. An empty batch throws. A multi-row
201
- batch with no inferred columns throws; supply explicit columns to emit multiple default rows.
202
-
203
- Assignments infer enumerable keys and omit undefined values. An empty object or a selection with
204
- no defined values throws, so the helper never emits an empty `SET` clause. This also applies when
205
- the surrounding statement contains arithmetic or other assignments: branch explicitly when the
206
- intended update consists only of those expressions.
207
-
208
- An optional nonempty column list selects and orders fields. It does not validate that the input
209
- object has no additional keys. Unlisted fields are ignored; missing insert cells use `DEFAULT`,
210
- while missing update fields are omitted. Duplicate or invalid column
211
- names throw. TypeScript checks selected names against the input's known keys:
159
+ `sql.insertValues(input, columns?)` and `sql.assignments(input, columns?)` return fragments. Keep
160
+ operation, table, predicates, expressions, conflict policy, and `RETURNING` in SQL:
212
161
 
213
162
  ```ts
214
- const rows: { name: string; bio?: string | null }[] = buildRows();
215
- const insert = sql`INSERT INTO profiles ${sql.insertValues(rows, ['name', 'bio'])} RETURNING id`;
216
-
217
- const patch: { name?: string; bio?: string | null } = readPatch();
218
- const update = sql`UPDATE profiles SET ${sql.assignments(patch, ['name', 'bio'])} WHERE id = ${id}`;
163
+ const query = sql<{ id: number }>`INSERT INTO public.users
164
+ ${sql.insertValues({ email: 'reader@example.test' })} RETURNING id`;
165
+ await db.query(query, { returns: 'one' });
166
+ await db.query(sql`UPDATE public.users SET ${sql.assignments({ active: false })} WHERE id = ${42}`);
219
167
  ```
220
168
 
221
- The helpers have no runtime schema metadata. Use [generated input types](#insert-and-update-inputs)
222
- or application-owned contracts to check JavaScript values; PostgreSQL enforces identity,
223
- generation, required-column, and constraint rules. Retain application validation.
224
- To reset an update field to its default, write `column = DEFAULT` in the
225
- surrounding SQL. Put arithmetic and `excluded.column` expressions there too; these helpers accept
226
- values only, with no expression/default sentinel.
227
-
228
- A compiled query accepts at most 65,533 bound parameters, including surrounding interpolations.
229
- Tenon rejects larger queries and does not split them automatically. Choose batch sizes and
230
- transaction boundaries in the caller. Helper validation errors identify invalid rows/columns
231
- without printing cell values.
232
-
233
- See the [mutation cookbook](mutations.md) for common patterns and
234
- [query inspection](#query-inspection) for checking completed statements.
169
+ Both accept plain objects, including null-prototype objects. Columns are quoted as individual
170
+ identifiers. Only own data properties are read; accessors are rejected. Fields are captured at
171
+ fragment construction; nested objects retain ordinary driver serialization behavior.
172
+
173
+ | Input | Insert | Assignment |
174
+ | -------------------------------------------- | ---------------------- | --------------- |
175
+ | Missing or `undefined` | `DEFAULT` for the cell | Omitted |
176
+ | `null` | Bound SQL null | Bound SQL null |
177
+ | Other value | Bound parameter | Bound parameter |
178
+ | SQL fragment, identifier, or query as a cell | Rejected | Rejected |
179
+
180
+ Inserts accept one object or an array. Without `columns`, they infer the union of enumerable keys
181
+ with defined values in first-seen order. Heterogeneous rows stay aligned. A single default-only
182
+ row emits `DEFAULT VALUES`. Empty batches fail; multiple default-only rows need explicit columns.
183
+ Assignments omit undefined values and fail when no assignments remain, even if surrounding SQL
184
+ has other assignments.
185
+
186
+ A supplied column list must be nonempty, unique, and valid. It selects and orders fields, ignoring
187
+ unlisted keys. Missing insert cells use defaults; missing update fields are omitted. TypeScript
188
+ checks selected names against known input keys, but selection can still discard required fields.
189
+ Helpers have no schema metadata; PostgreSQL enforces identity, generated-column, and constraint
190
+ rules. Use [generated inputs](#insert-and-update-inputs) and application validation.
191
+
192
+ Write arithmetic, `column = DEFAULT`, and `excluded.column` in the surrounding SQL. There is no
193
+ expression/default sentinel. A complete query accepts at most 65,533 bound parameters, including
194
+ surrounding interpolations; Tenon does not split batches. See the [mutation cookbook](mutations.md).
235
195
 
236
196
  ## Transactions, guarded writes, and errors
237
197
 
198
+ For an existing user with id 42:
199
+
238
200
  ```ts
239
201
  await db.transaction(async (tx) => {
240
- const id = 42;
241
- await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${id}`, {
202
+ await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${42}`, {
242
203
  affectedRows: { exact: 1 },
243
204
  });
244
- // Use tx for every query that belongs to this transaction.
245
205
  });
246
206
  ```
247
207
 
248
- Transactions belong to one database instance; they do not coordinate writes across databases. The
249
- callback commits on success and rolls back when an error escapes. Await all work before returning;
250
- the callback client expires afterward. A helper accepting `QueryClient` works with either `db` or
251
- `tx`. Nested callback transactions and automatic retries are not provided.
252
-
253
- `affectedRows: { exact: n }` requires n affected rows; `{ max: n }` permits up to n. Both require
254
- nonnegative safe integers. Checks apply to INSERT, UPDATE, DELETE, and MERGE, independent of
255
- `RETURNING`. A root call uses a transaction; a call on `tx` uses a savepoint. A mismatch rolls back
256
- before throwing. You may catch a guarded failure and continue the outer transaction. Unguarded
257
- PostgreSQL errors normally abort that transaction, even when caught.
258
-
259
- The count covers the final command, excluding additional trigger, cascade, or CTE effects. It cannot
260
- identify the wrong single row. Returned-row checks alone do not undo autocommitted writes. Rollback
261
- cannot undo sequence advances or external side effects of database functions.
262
-
263
- Set isolation and timeouts with SQL, for example `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`
264
- before data queries and `SET LOCAL statement_timeout = '5s'`. Manual savepoints are available
265
- through SQL; reserve `tenon_affected_rows_` names for Tenon. Direct BEGIN/COMMIT/ROLLBACK boundaries
266
- are owned by `transaction`.
267
-
268
- SQL errors retain Postgres.js's codes and details. Invalid inputs and returned-row mismatches throw ordinary
269
- `Error` objects. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
270
- `TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`. When both work and cleanup
271
- fail, `AggregateError` preserves both failures. A lost commit acknowledgment can leave the outcome
272
- unknown; retry only when the application's operation makes that safe.
273
-
274
- Optional `createTenon({ connection, onQuery })` observes completed calls with `label`, `durationMs`,
275
- `outcome`, and available `command`, `rowCount`, `errorCode`. It receives no SQL or values. Observers
276
- are not awaited; failures emit warnings. A successful callback query does not mean its outer
277
- transaction has committed.
208
+ Use `tx` for every operation that must commit together and await the work. The callback commits on
209
+ success, rolls back on an escaping error, and expires before cleanup. Transactions belong to one
210
+ database instance. Nested callbacks and automatic retries are not provided. Tenon owns direct
211
+ BEGIN/COMMIT/ROLLBACK boundaries.
212
+
213
+ `affectedRows: { exact: n }` or `{ max: n }` accepts a nonnegative safe integer. It checks the final
214
+ INSERT/UPDATE/DELETE/MERGE count independently of `RETURNING`. A guarded root call uses a
215
+ transaction; a guarded callback call uses a savepoint. Failed checks and other errors from that
216
+ guarded operation roll it back before throwing. A caught guarded failure permits continued work;
217
+ unguarded SQL errors normally abort the transaction.
218
+
219
+ Counts exclude extra trigger/cascade and CTE effects and cannot identify the wrong single row.
220
+ Sequence advances and external effects are not reversible. A lost commit acknowledgment can leave
221
+ the outcome unknown; applications own retry and idempotency policy.
222
+
223
+ Set isolation before data queries with `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`, and use
224
+ `SET LOCAL statement_timeout = '5s'` to bound statements in that transaction. Manual savepoints are supported;
225
+ reserve names beginning with `tenon_affected_rows_` for Tenon.
226
+
227
+ SQL errors retain native driver details. Invalid inputs throw ordinary `Error` objects.
228
+ `CardinalityError` has code `TENON_CARDINALITY_MISMATCH`, `expected` (`one` or `oneOrNone`),
229
+ `actual` (returned row count), and optional `label`. It does not change transaction or autocommit
230
+ behavior. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
231
+ `TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`. Work/cleanup failures can
232
+ produce `AggregateError`.
233
+
234
+ Optional `onQuery` observes completed calls with `label`, `durationMs`, `outcome`, and available
235
+ `command`, `rowCount`, and `errorCode`. It receives no SQL or values. Validation failures before
236
+ execution are not observed. Observers are not awaited; failures emit warnings without changing
237
+ the query outcome. Query success inside a callback does not establish outer commit.
278
238
 
279
239
  ## Generated types and checking
280
240
 
281
- `generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript source for the
282
- connected schema. It does not need migration files or history. Table type generation is optional.
283
- Generated files provide reusable row and mutation input types. The CLI writes that source
284
- atomically. This command checks
285
- the existing file without rewriting it:
241
+ `generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript declarations
242
+ from a consistent read-only snapshot; it does not need migration files or history. The CLI writes
243
+ them atomically. Default namespace is `public`; default output is `src/generated-schema.ts`.
244
+ Both `.ts` and `.d.ts` outputs are supported. Commit exact output and exclude it from formatting.
286
245
 
287
246
  ```sh
247
+ npx tenon generate --out src/generated-schema.d.ts
288
248
  npx tenon generate --out src/generated-schema.d.ts --check
289
249
  ```
290
250
 
291
- `--check` reports a missing file or shows a short text diff when the saved output differs from the
292
- current database. It never rewrites the file. Pending migration files
293
- are a separate concern. The representation includes table columns, constraints, indexes, and
294
- referenced domains, not every PostgreSQL object.
295
-
296
- Generation reads a consistent snapshot of ordinary tables and partitioned roots. `Rows['users']`
297
- describes one returned row. Application type names can use PascalCase:
298
-
299
- ```ts
300
- import type { Rows } from './generated-schema.js';
301
-
302
- type User = Rows['users'];
303
- type UserSummary = Pick<User, 'id' | 'email'>;
304
- ```
305
-
306
- Use `sql<User>` for a full row or `sql<UserSummary>` for that projection. Generated table keys and
307
- named aliases preserve exact SQL names; the generated `users` alias also means `Rows['users']`.
308
- `type AuditRow = Rows['audit-log']` works for names that cannot be exported as identifiers.
309
- Views, foreign tables, and attached partitions use query-specific types. A missing namespace
310
- fails; an existing empty schema is valid.
311
-
312
- | PostgreSQL type | Returned value | Generated write input |
313
- | ---------------------------------------------- | ---------------------------------------- | --------------------------------------- |
314
- | smallint, integer, oid, real, double precision | `number` | `number` |
315
- | bigint | `string` | `string \| bigint` |
316
- | numeric | `string` | `string \| number` |
317
- | boolean | `boolean` | `boolean` |
318
- | text, name, varchar, char, uuid, time, timetz | `string` | `string` |
319
- | date, timestamp, timestamptz | `Date` (infinities become invalid dates) | `Date \| string` |
320
- | enum | String literal union | Same union; `never` for an empty enum |
321
- | json, jsonb | `unknown` | `Exclude<JSONValue, null>` (from Tenon) |
322
- | bytea | `Buffer` | `Buffer` |
323
-
324
- For returned rows, nullable columns add null. Domains use verified underlying mappings and the
325
- column's own nullability. Unverified types warn and become `unknown`. Verified arrays use
326
- `PgArray<T> = (T | null | PgArray<T>)[]` for null elements and multiple dimensions. Numeric arrays
327
- contain exact strings, like numeric scalars. Enum arrays and arrays of
328
- domains are unverified. Date-only values use the driver's UTC date parsing; timestamps without time zone use Node's local timezone.
329
- Infinite timestamps remain invalid `Date` objects: check validity before using date methods.
330
- Custom Postgres.js parsers need matching application types.
251
+ `--check` never rewrites the file. Missing/stale output fails with a diagnostic and a short diff
252
+ for changes. It compares the generated representation, not pending migrations or every database
253
+ object. Output includes relation/constraint/index summaries and referenced domains, not function
254
+ bodies, grants, or a complete schema fingerprint.
255
+
256
+ Generation covers ordinary tables and partitioned roots. Views, foreign tables, and attached
257
+ partitions need query-specific types. A missing namespace fails; an existing empty schema is valid.
258
+ Exact table names are available through `Rows['users']`; valid nonreserved identifiers also receive
259
+ named aliases. Use `Pick` for projections; a join or aggregate needs its actual selected shape.
260
+
261
+ | PostgreSQL type | Read representation | Generated write input |
262
+ | ---------------------------------------------- | -------------------- | ------------------------------------------------------------ |
263
+ | smallint, integer, oid, real, double precision | `number` | `number` |
264
+ | bigint | `string` | `string \| bigint` |
265
+ | numeric | `string` | `string \| number` |
266
+ | boolean | `boolean` | `boolean` |
267
+ | text, name, varchar, char, uuid, time, timetz | `string` | `string` |
268
+ | date, timestamp, timestamptz | `Date` | `Date \| string` |
269
+ | enum | String literal union | Same union; `never` for an empty enum |
270
+ | json, jsonb | `unknown` | `Exclude<JSONValue, null>`; `JSONValue` is exported by Tenon |
271
+ | bytea | `Buffer` | `Buffer` |
272
+
273
+ Nullable columns add null; `unknown` already includes it. Domains retain verified base mappings
274
+ and the column's read nullability. Verified arrays use `PgArray<T> = (T | null | PgArray<T>)[]` for
275
+ null elements and multiple dimensions. Numeric arrays contain strings; enum arrays and arrays of
276
+ domains are unverified. Unmapped types warn and use `unknown` reads / `never` non-null writes.
277
+ Custom parsers need matching application types.
278
+
279
+ Date-only parsing uses UTC; timestamps without time zone use Node's local timezone. Infinite
280
+ temporal reads become invalid dates: check `Number.isFinite(value.getTime())` before date methods.
281
+ Bound temporal values require finite dates; use SQL literals for PostgreSQL infinity.
282
+
283
+ ### Persistence and domain values
284
+
285
+ Generated types describe storage representations. Text `CHECK` constraints do not become unions,
286
+ and JSON reads remain unknown; validate before narrowing to domain types. Convert third-party
287
+ payloads into validated native JSON-compatible objects rather than asserting compatibility or
288
+ pre-serializing them. A bound null means SQL null; use `'null'::jsonb` for JSON null.
289
+
290
+ Bigint and `count(*)` reads are decimal strings. Use SQL or BigInt for exact arithmetic and decimal
291
+ strings at JSON boundaries; convert to number only when bounded or approximate values are intended.
331
292
 
332
293
  ### Insert and update inputs
333
294
 
334
- The same file provides `Rows.Insert['table']` and `Rows.Update['table']` for bound write inputs.
335
- These are ordinary TypeScript types. `--out` accepts `.ts` or `.d.ts`; the examples use `.d.ts`
336
- because the file contains only declarations.
337
-
338
295
  ```ts
339
296
  import type { Rows } from './generated-schema.js';
340
297
 
341
- type NewProfile = Rows.Insert['profiles'];
342
- type ProfilePatch = Pick<Rows.Update['profiles'], 'name' | 'bio'>;
343
-
344
- const profile: NewProfile = { name: 'Ada' };
345
- const patch: ProfilePatch = { name: undefined, bio: null };
346
- sql.insertValues(profile, ['name', 'bio', 'score']);
347
- sql.assignments(patch, ['name', 'bio']);
298
+ const input: Rows.Insert['users'] = { email: 'reader@example.test' };
299
+ const patch: Pick<Rows.Update['users'], 'active'> = { active: false };
300
+ sql.insertValues(input);
301
+ sql.assignments(patch);
348
302
  ```
349
303
 
350
- Insert fields are required when nonnullable and without a default. Other writable insert fields
351
- are optional; every writable update field is optional. `undefined` requests a default on insert
352
- and omits an assignment on update. Explicit `null` is accepted only when the column and its domain
353
- chain permit it. Domain defaults count; column defaults take precedence.
354
-
355
- Computed columns and `GENERATED ALWAYS` identity columns cannot receive values. Serial and
356
- `GENERATED BY DEFAULT` identity columns are optional on insert and writable on update. Other
357
- primary keys remain writable. Use `Pick` or `Omit` for application-specific restrictions and keep
358
- expressions, `DEFAULT`, and special identity overrides in SQL.
359
-
360
- Write representations in the table above also apply to verified array elements. Pass JavaScript JSON
361
- values directly; do not pre-serialize them. A bound null is SQL null. To construct JSON null explicitly,
362
- use SQL such as `'null'::jsonb`. Native temporal serialization requires finite dates; strings such as
363
- `'infinity'` are not valid bound date values. Use SQL literals for these PostgreSQL values. Unverified non-null write values use `never` and warn;
364
- nullable fields still accept null. Supply an application-owned replacement when needed, for example
365
- `Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for a reviewed interval-text input.
366
-
367
- Apply these types through annotations, `satisfies`, or helper generic arguments such as
368
- `sql.insertValues<NewProfile>(input)`. They check shapes and representations, not authorization,
369
- constraints, JSON contents, or nonempty inputs. Optional fields accept explicit `undefined`,
370
- including with `exactOptionalPropertyTypes`. Selected columns can discard required fields; see
371
- the [cookbook example](mutations.md#input-types-and-runtime-validation).
304
+ Insert fields are required when nonnullable, writable, and without a default. Other writable insert
305
+ fields and all writable update fields are optional and accept explicit undefined, including with
306
+ `exactOptionalPropertyTypes`. Explicit null requires both column and domain-chain permission.
307
+ Domain defaults count; column defaults take precedence.
372
308
 
373
- ## Query inspection
309
+ Computed and `GENERATED ALWAYS` identity columns use optional `never` fields. Serial and
310
+ `GENERATED BY DEFAULT` identities are optional on insert and writable on update; other primary
311
+ keys remain writable. Use `Pick`/`Omit` for application restrictions. Apply the generated types
312
+ through annotations, `satisfies`, or helper generic arguments; they do not validate constraints,
313
+ JSON structure, authorization, or nonempty input.
374
314
 
375
- `describeQuery({ connection, query, session?, timeouts? })` accepts a PostgreSQL connection URI
376
- and a finished SQL string or `{ text }` query object. It returns parameter/result type metadata,
377
- source constraints, conservative row declaration, effective session context, and coded findings.
378
- It does not bind query values, execute the inspected statement, or verify `sql<Row>`.
315
+ For unverified write mappings, provide a reviewed application-owned replacement, for example
316
+ `Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for interval text input. Selected columns
317
+ can discard required fields; see [the cookbook](mutations.md#input-types-and-runtime-validation).
379
318
 
380
- `tenon describe <query.sql>` prints that report as JSON using `DATABASE_URL`. Session flags are
381
- `--role`, `--search-path`, and `--time-zone`. Timeout flags are `--connect-timeout-ms`,
382
- `--statement-timeout-ms`, and `--lock-timeout-ms`; defaults are 3000, 5000, and 1000 ms.
319
+ ## Query inspection
383
320
 
384
- Inspection owns a separate connection and uses Postgres.js mappings. Match the application's role
385
- and settings; temporary tables, uncommitted changes, transaction-local settings, and custom parsers
386
- are not inherited. Result nullability remains unknown. CLI exit 0 means description succeeded,
387
- including reports with unresolved issues; execution and type annotations remain unchecked.
321
+ `describeQuery({ connection, query, session?, timeouts? })` accepts a PostgreSQL URI and a finished
322
+ SQL string or `{ text }` object. It returns PostgreSQL metadata, conservative read mappings and
323
+ row declaration, session context, and coded findings. It does not bind values, execute the
324
+ statement, or verify `sql<Row>`.
388
325
 
389
- See [query inspection](query-inspection.md) for public types, finding codes, session behavior, and
390
- the workflow combining inspection with persistent application database tests.
326
+ Inspection owns a separate session with bounded timeouts. Match runtime settings; temporary tables,
327
+ uncommitted changes, and custom parsers are not inherited. Nullability stays unknown. See
328
+ [query inspection](query-inspection.md) for settings, result fields, issue codes, and testing.
391
329
 
392
330
  ## Command reference
393
331
 
394
- All database commands use `DATABASE_URL`; environment files are not loaded automatically. For
395
- multiple databases, run commands with the intended database's URL and migration directory, and use
396
- distinct generated output files if generating table types.
332
+ Database commands require `DATABASE_URL`. `new` and `--help` work offline. No environment files
333
+ are loaded. Paths are relative to the working directory. Explicit flags unsupported by a command
334
+ are rejected; `--check` is only a generation check, not a migration dry run.
397
335
 
398
336
  ```text
399
337
  tenon new <name> [--directory migrations]
400
338
  tenon migrate [--directory migrations] [--namespace public]
401
339
  tenon status [--directory migrations] [--namespace public]
402
340
  tenon rollback [--directory migrations] [--namespace public] [--steps 1]
403
- tenon generate [--namespace public] [--out path] [--check]
404
- tenon baseline capture|adopt [--directory migrations] [--namespace public]
341
+ tenon generate [--namespace public] [--out src/generated-schema.ts] [--check]
342
+ tenon describe <query.sql> [--role name] [--search-path value] [--time-zone value]
343
+ [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
344
+ tenon baseline capture [--directory migrations] [--namespace public] [--schema name ...]
345
+ tenon baseline adopt [--directory migrations] [--namespace public]
405
346
  ```
406
347
 
407
- Migration filenames are `<numeric-id>_<name>.up.sql` and optional matching down files. IDs sort
408
- numerically. Each migration and its history entry commit together; earlier files remain applied if a
409
- later file fails. Status reports applied/pending IDs. Rollback checks that every selected file has
410
- down SQL before executing any. Use dollar-quoted function bodies and explicit `E'...'` for backslash
411
- escapes. Nontransactional DDL such as `CREATE INDEX CONCURRENTLY` needs a separate procedure.
348
+ `new` scaffolds empty `<numeric-id>_<name>.up.sql` and `.down.sql` files. Names contain letters,
349
+ digits, underscores, or hyphens. Fill both files or remove the down file for an irreversible change.
350
+ IDs sort numerically. Never edit applied files: checksums cover up and down SQL.
351
+
352
+ Each migration and history entry commits together; earlier files remain applied if a later one
353
+ fails. Runner search path is the selected namespace followed by `pg_catalog`. `status` returns
354
+ applied/pending IDs as JSON and rejects history conflicts. `rollback` validates that every selected
355
+ file has down SQL before executing any; `steps` is a positive safe integer. Use dollar-quoted
356
+ function bodies and explicit `E'...'` backslash escapes. Nontransactional DDL such as
357
+ `CREATE INDEX CONCURRENTLY` requires a separate procedure.
412
358
 
413
- Tools coordinate through session advisory locks per database/namespace. Use direct connections or
414
- session pooling, with PostgreSQL timeouts to bound waiting.
359
+ Migration/status/rollback/adoption use session advisory locks per database and namespace;
360
+ generation uses the same lock in shared mode. Use direct or session-pooled connections and server
361
+ timeouts to bound waits. Baseline capture uses a local creation lock and native `pg_dump`;
362
+ it does not acquire the runner's database advisory lock. See [adoption](adoption-guide.md).
415
363
 
416
364
  Programmatic `migrate`, `migrationStatus`, and `rollback` accept
417
- `{ connection, directory?, namespace? }`; rollback also accepts `steps`.
418
- `createMigration({ name, directory? })` scaffolds files; `readMigrations(directory?)` reads
419
- validated SQL and checksums. Baseline capture/adoption are covered in the
420
- [adoption guide](adoption-guide.md).
365
+ `{ connection, directory?, namespace? }`; rollback also accepts `steps`. Migration/rollback return
366
+ completed IDs. `createMigration({ name, directory? })` returns `{ id, upPath, downPath }`;
367
+ `readMigrations(directory?)` returns validated SQL and checksums.