@kequtech/tenon-orm 0.4.0 → 0.4.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,256 +1,166 @@
1
- # Tenon reference
1
+ # API and CLI reference
2
2
 
3
- Exact API contracts and limitations. The [human guide](human-guide.md) introduces the workflow; the
4
- [agent guide](agent-guide.md) covers routine implementation. Examples below use the `public.users`
5
- table from the human guide and an existing `db` instance, unless stated otherwise.
3
+ For a complete first setup, follow [Getting started](human-guide.md). Task guides explain
4
+ [queries](sql.md), [writes](mutations.md), [schema types](schema-types.md),
5
+ [migrations](migrations.md), and [connections](connections.md).
6
6
 
7
7
  ## Installation and connection
8
8
 
9
- Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and Node
10
- development typings. Tenon includes pgclear 0.3.x as a runtime dependency. A usual Node TypeScript
11
- setup uses `module: "NodeNext"`.
9
+ Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and Node.js
10
+ development typings. Install `@kequtech/tenon-orm`; pgclear is included as a runtime dependency.
12
11
 
13
- ```sh
14
- npm install @kequtech/tenon-orm
12
+ ```text
13
+ createTenon({ connection, onQuery? }): Database
15
14
  ```
16
15
 
17
- Use `sql` from `@kequtech/tenon-orm` for `db.query`. Tenon also re-exports Pgclear's `json`,
18
- `param`, `builtin`, `defaultCodecs`, and `createTemporalCodecs`, plus the `Codec`, `CodecRegistry`,
19
- `Parameter`, `PgArray`, `TemporalConstructors`, and `TemporalInput` types. These are the driver's
20
- original helpers and types; Pgclear continues to own conversion. Generated schema declarations
21
- import their helper types from Tenon.
22
-
23
- `createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, `native`,
24
- and `close`. The required `connection` accepts a PostgreSQL URI, pgclear `PoolOptions`, or an existing
25
- pgclear `Pool`, borrowed by identity. `ConnectionOptions` aliases pgclear's `PoolOptions`.
16
+ `connection` is required: a PostgreSQL URI string, pgclear `PoolOptions`, or an existing pgclear
17
+ `Pool`. `Connection` names this union; `ConnectionOptions` aliases `PoolOptions`. A supplied pool
18
+ retains application ownership. Unknown Tenon or driver configuration options reject.
26
19
 
27
- ```ts
28
- const db = createTenon({
29
- connection: {
30
- connectionString: connection,
31
- maxConnections: 10,
32
- idleTimeoutMs: 30_000,
33
- connectTimeoutMs: 5000,
34
- acquireTimeoutMs: 5000,
35
- startupParameters: { search_path: 'app,public' },
36
- },
37
- });
38
- ```
20
+ The returned `Database` provides:
39
21
 
40
- Configuration follows pgclear. Object fields override URI settings. Environment variables, pgpass,
41
- service files, and `.env` files are not loaded by the driver; pass credentials and deployment
42
- configuration explicitly. An explicit `{}` uses pgclear's localhost/current-user defaults. Use
43
- `startupParameters` for session settings and `tls` for verified TLS. Unknown options reject.
44
- See the installed pgclear public API documentation for driver configuration.
22
+ | Member | Contract |
23
+ | ------------------------ | ---------------------------------------------------------------------------------------- |
24
+ | `query(query, options?)` | Execute a Tenon `SqlQuery` and apply result/count options |
25
+ | `transaction(callback)` | Commit on callback success, roll back on an escaping error; return the callback's result |
26
+ | `generateTypes(options)` | Write or check schema declarations using this instance's pool/codecs |
27
+ | `native` | The underlying pgclear pool |
28
+ | `close({ timeoutMs? }?)` | Disable the wrapper and close an owned pool; default grace 5,000 ms |
45
29
 
46
- Reuse an instance for each database configuration. `db.close({ timeoutMs? })` immediately rejects
47
- new Tenon calls and delegates owned resource shutdown to pgclear. The default grace period is
48
- 5,000 ms; `0` requests immediate shutdown. Remaining work and checkouts expire at the deadline.
49
- Timeout must be an integer from 0 to 2,147,483,647 milliseconds. Shutdown waits for owned driver
50
- resources, not arbitrary JavaScript callbacks or proof of remote rollback.
30
+ `onQuery(event)` observes completed Tenon query calls. It may return void or a promise; it is not
31
+ awaited. Observer failures emit warnings without changing the outcome. `QueryEvent` contains
32
+ `durationMs`, `outcome: 'success' | 'error'`, and optional `label`, `command`, `rowCount`, and
33
+ `errorCode`. SQL and values are excluded. Validation failures before execution are not observed.
51
34
 
52
- The first valid close call fixes its timeout; concurrent and later calls share its promise and
53
- outcome. Invalid initial options reject without closing. For borrowed pools, close disables the
54
- Tenon wrapper while the application retains pool ownership and outstanding work.
55
-
56
- Qualify SQL names or configure runtime `search_path`. CLI `--namespace` selects tooling scope;
57
- it does not configure application queries. Baseline capture and Describe accept connection strings
58
- rather than the runtime connection union.
35
+ See [connection configuration and shutdown](connections.md) for pool settings, TLS, environment
36
+ handling, timeout bounds, and lifecycle examples. Runtime `search_path` is independent of tooling
37
+ `namespace` options.
59
38
 
60
39
  ### Driver updates
61
40
 
62
- Tenon declares `@kequtech/pgclear: ^0.3.0`, allowing compatible 0.3.x patch updates without a new
63
- Tenon release. With npm, run `npm update @kequtech/pgclear`, review the lockfile and run
64
- `npm ls @kequtech/pgclear`, and commit the resolved dependency change after application checks.
65
- An existing lockfile keeps its selected version until updated. A future Pgclear 0.4 release is outside this
66
- range and needs an explicit Tenon compatibility update; it is not an automatic upgrade.
67
-
68
- Install Pgclear directly only when the application imports its native APIs, such as `listen` or
69
- `createPool`. Keep that version compatible with Tenon's declared range to allow a shared copy. If
70
- both SQL tags are needed, alias the native one as `pgSql`: Tenon's tag belongs in `db.query`, while
71
- Pgclear's tag belongs in native `query`. Native `queryClassic(text, values)` avoids a second tag.
72
- TypeScript and runtime validation reject sending one package's query to the other package's API.
73
-
74
- ## Native driver access
75
-
76
- `db.native` is the actual pgclear `Pool`, readonly in TypeScript. Native calls bypass Tenon's result
77
- modes, affected-row guards, SQL checks, and `onQuery`. They do not join a Tenon callback transaction;
78
- callback clients expose only `tx.query`.
79
-
80
- Use `queryClassic(text, values?)`, `acquire`/`withSession`, `prepare`, streams, and COPY through pgclear's
81
- public APIs. Held sessions must be released. Use pgclear `listen` for a dedicated notification
82
- connection with explicit reconnect and lifecycle policy. Notifications have no replay.
83
-
84
- Pgclear's native `query` accepts only its own `sql` tag results. Tenon's `sql` and `unsafeSql` results
85
- belong in `db.query`; use `queryClassic` for finished SQL text at the native boundary.
86
-
87
- ```ts
88
- import { listen } from '@kequtech/pgclear';
89
-
90
- const listener = await listen(connection, {
91
- channels: ['users_changed'],
92
- onNotification: ({ payload }) => console.log(payload),
93
- reconnect: true,
94
- });
95
- // At application shutdown:
96
- await listener.close();
97
- ```
98
-
99
- Close owned pools through `db.close()`. A separate listener has its own lifetime; close it explicitly.
100
- Borrowed pools remain usable until their owner calls `pool.close()`.
41
+ The dependency range is `@kequtech/pgclear: ^0.3.0`. Compatible patches may be updated independently;
42
+ see [driver updates](connections.md#update-the-driver). Tenon re-exports `builtin`, `json`, `param`,
43
+ `defaultCodecs`, and `createTemporalCodecs`, with the driver's `Codec`, `CodecRegistry`, `Parameter`,
44
+ `PgArray`, `TemporalConstructors`, and `TemporalInput` types.
101
45
 
102
46
  ## Queries and result modes
103
47
 
104
- `sql<Row>` creates a detached `SqlQuery<Row>` with SQL `text` and ordered `values`; it does not
105
- execute. `Row` declares one selected row. Without an annotation, fields are `unknown`. An annotation
106
- does not verify the SQL or validate returned data.
48
+ `sql<Row>` is a template tag returning `SqlQuery<Row>` with readonly `text` and `values`. It does
49
+ not execute. `Row` describes one selected record; without it, fields are `unknown`. Neither result
50
+ annotations nor generated types validate data during execution.
107
51
 
108
- ```ts
109
- const user = await db.query(
110
- sql<{ id: number; email: string }>`SELECT id, email FROM public.users WHERE id = ${42}`,
111
- { returns: 'oneOrNone', label: 'user-by-id' },
112
- );
113
- ```
52
+ `db.query` and `tx.query` accept `{ returns?, affectedRows?, label? }`. Unknown options reject.
53
+ A supplied label must be a nonblank string.
114
54
 
115
- `query` options are `{ returns?, affectedRows?, label? }`; unknown keys fail. A supplied `label`
116
- must be a nonempty string.
55
+ | `returns` | Output | Allowed returned rows |
56
+ | ------------------ | ------------------ | --------------------- |
57
+ | `native` (default) | `QueryResult<Row>` | Any |
58
+ | `many` | `readonly Row[]` | Any |
59
+ | `oneOrNone` | `Row \| undefined` | Zero or one |
60
+ | `one` | `Row` | Exactly one |
117
61
 
118
- | `returns` | Output | Allowed returned rows |
119
- | ------------------ | -------------------------------- | --------------------- |
120
- | `native` (default) | `QueryResult<Row>` driver object | Any |
121
- | `many` | `readonly Row[]` | Any |
122
- | `oneOrNone` | `Row \| undefined` | Zero or one |
123
- | `one` | `Row` | Exactly one |
62
+ `QueryResult` follows pgclear and includes `rows`, `rowCount`, `command`, `commandTag`, and `fields`.
63
+ `rowCount` is `number | null`, independent of `RETURNING`; unsafe integer counts reject. SQL
64
+ `count(*)` is a column value and defaults to string. Row arrays are readonly.
124
65
 
125
- Native results expose `rows`, `rowCount`, `command`, `commandTag`, and `fields`, following pgclear's
126
- `QueryResult`. Counts are `number | null`; row arrays are readonly. Counts outside JavaScript's safe
127
- integer range reject instead of rounding. SQL `count(*)` is an int8 column value and reads as a string.
128
- `rowCount` is the command's affected-row count, independent of `RETURNING`; it can be null for
129
- commands without a count. Result modes never add `LIMIT` or `RETURNING`. A returned-row mismatch
130
- alone does not undo an autocommitted write; use `affectedRows` for write bounds that roll back.
66
+ Modes do not add `LIMIT` or `RETURNING`. Returned-row mismatches throw `CardinalityError`; alone,
67
+ that does not undo an autocommitted write. For write bounds that roll back, use `affectedRows`.
131
68
 
132
- Functions accepting `QueryClient` can use either `Database` or `Transaction`. Public types also
133
- include `QueryOptions`, `QueryReturnMode`, and `QueryOutput<Row, Mode>`.
69
+ `QueryClient` is the query-only interface accepted by either a `Database` or callback `Transaction`.
70
+ Other exported types include `QueryOptions`, `QueryReturnMode`, `QueryOutput<Row, Mode>`,
71
+ `SqlQuery`, `SqlFragment`, and `UnknownRow`.
134
72
 
135
73
  ### Adapting transactional results
136
74
 
137
- For a job library or other adapter expecting `{ rows }`, extract the native result's row array:
75
+ For an adapter expecting `{ rows }`, execute its trusted SQL through the callback's `tx`:
138
76
 
139
77
  ```ts
140
78
  import { unsafeSql, type QueryClient } from '@kequtech/tenon-orm';
141
79
 
142
80
  async function queryRows(tx: QueryClient, text: string, values: readonly unknown[]) {
143
81
  const result = await tx.query(unsafeSql(text, values));
144
- return { rows: result.rows };
82
+ return { rows: [...result.rows] };
145
83
  }
146
84
  ```
147
85
 
148
- Alternatively, request `{ returns: 'many' }` and wrap the returned array in `{ rows }`. Supply the
149
- callback's `tx` when the adapter's work must commit with application writes. A native pool query
150
- uses a separate checkout and does not join that transaction. Await adapter calls before returning
151
- from the callback, retain commit/rollback tests, and make recording helpers return pgclear native
152
- results. The adapter's SQL text must be trusted and values must remain separately bound.
86
+ The copy supplies a mutable array if the adapter requires one. Values must already be valid driver
87
+ inputs; external arrays/objects need type-aware `param`, `json`, or text encoding. Await adapter
88
+ work inside the callback. Native pool queries use another checkout and do not join it.
89
+
90
+ For a job-library transaction adapter such as pg-boss's, verify actual SQL inputs and timestamp
91
+ expectations. Enqueueing and returning an ID may need no output conversion; returned timestamps
92
+ follow the database-wide codecs. Keep atomic commit/rollback tests. Tenon provides no per-query
93
+ codec override, and input encoders are independent of output codecs.
153
94
 
154
95
  ## SQL parameters and identifiers
155
96
 
156
- Ordinary `sql` interpolations bind values. Do not quote them or write manual `$n` placeholders
157
- inside templates. Plain parameters accept null, string, finite number, bigint, boolean, Date,
158
- supported native/polyfilled Temporal values, and Uint8Array. Undefined, plain objects, arrays,
159
- invalid Date values, nonfinite numbers, and unsafe integer numbers reject.
160
- Use pgclear `json(value)` for JSON and `param(value, codec)` for arrays or custom encodings.
161
- Encoded strings also work where PostgreSQL accepts text. Cast standalone parameters when SQL must
162
- establish their type. For int8, use exact strings, bigint, or `param(text, builtin.int8)`.
97
+ | API | Contract |
98
+ | ------------------------------ | ------------------------------------------------------------------------------ |
99
+ | `sql<Row>` template tag | Bind ordinary values; interpolate Tenon fragments and identifiers structurally |
100
+ | `identifier(...names)` | Quote components separately, e.g. `"public"."users"` |
101
+ | `unsafeSql<Row>(text, values)` | Trusted external SQL with an explicit values array, including `[]` |
102
+ | `json(value, type?)` | Encode JSON; default type is jsonb |
103
+ | `param(value, codec, format?)` | Choose an explicit driver parameter type and encoder |
163
104
 
164
- Date values encode in UTC. Date, bigint, and supported Temporal inputs encode as text with OID 0,
165
- using server inference; they do not select an output decoder. Temporal calendar dates, wall times,
166
- instants, and durations have different PostgreSQL targets. Use the intended value and SQL cast,
167
- rather than treating every temporal value as an interchangeable timestamp. Pgclear does not install
168
- Temporal or a polyfill automatically.
105
+ Do not quote interpolations or mix manual `$n` placeholders into templates. Plain binds accept null,
106
+ string, finite number, bigint, boolean, valid Date, supported Temporal values, and Uint8Array.
107
+ Undefined, plain objects/arrays, nonfinite numbers, and unsafe integer numbers reject.
169
108
 
170
- ```ts
171
- import { builtin, param } from '@kequtech/tenon-orm';
109
+ Strings, numbers, bigint, Date, and supported Temporal values use server type inference. Date
110
+ encodes in UTC. Cast standalone parameters when PostgreSQL needs type context. Use `json` for JSON
111
+ and `param` for PostgreSQL arrays/custom encodings. Do not mutate bound values before settlement.
172
112
 
173
- const ids = param([1, 2, 3], builtin.int4Array);
174
- const query = sql<{ id: number }>`SELECT id FROM public.users
175
- WHERE id = ANY(${ids}::integer[])`;
176
- ```
113
+ Identifier components require 1–63 UTF-8 bytes and no NUL. Choose approved names; quoting is not
114
+ authorization. SQL syntax such as `DESC` belongs in fixed fragments or query alternatives. Never
115
+ concatenate request input into `unsafeSql` text.
177
116
 
178
- `identifier(schema, table)` quotes components separately. Names must contain 1–63 UTF-8 bytes and no
179
- NUL. Choose application-approved names; quoting does not authorize access. SQL syntax such as `DESC`
180
- belongs in fixed query alternatives or fragments, not a string parameter.
181
-
182
- `unsafeSql<Row>(text, values)` accepts trusted external SQL and an explicit values array, including
183
- `[]`. Never concatenate request input into its text. Full `sql` and `unsafeSql` queries cannot be
184
- nested; use fragments for partial SQL.
185
-
186
- Each Tenon query executes one statement through the extended protocol. Application execution uses
187
- unnamed queries without a `name` or prepared-statement cache. Values remain bound separately.
188
- Migrations deliberately support multi-statement scripts.
117
+ A query executes one statement through the extended protocol. Tenon does not expose a prepared
118
+ statement cache or query `name` option. See [writing queries](sql.md) for examples.
189
119
 
190
120
  ## SQL fragments and joins
191
121
 
192
- `sql.fragment` creates immutable partial SQL with bound values. It has no row type, cannot execute
193
- alone, and rejects blank SQL. Insert it into a complete `sql<Row>` query.
122
+ - `sql.fragment` creates immutable partial SQL with bound values. It has no row type and
123
+ cannot execute alone. Blank fragments reject.
124
+ - `sql.join(fragments, 'AND' | 'OR' | ',')` requires a nonempty array of fragments. It does not add
125
+ parentheses or alter precedence.
126
+ - Nested/repeated fragments preserve ordered parameters. Full `sql`/`unsafeSql` queries cannot be
127
+ nested. Literal `$1` text in SQL strings/comments is not rewritten.
194
128
 
195
- `sql.join(fragments, separator)` requires a nonempty list. Separators are `'AND'`, `'OR'`, and
196
- `','`. It does not add parentheses or change SQL precedence. Each fragment use receives correctly
197
- ordered parameters, including repeated and nested uses; literal `$1` text in strings/comments is
198
- unchanged.
129
+ See [composition examples](sql.md#compose-sql-fragments), including the empty-filter case.
199
130
 
200
- ```ts
201
- const email: string | undefined = 'reader@example.test';
202
- const conditions = [sql.fragment`active = ${true}`];
203
- if (email !== undefined) conditions.push(sql.fragment`email = ${email}`);
204
- const query = sql<{ id: number }>`SELECT id FROM public.users
205
- WHERE ${sql.join(conditions, 'AND')} ORDER BY id`;
131
+ ## Object-based mutation fragments
132
+
133
+ ```text
134
+ sql.insertValues<Input>(rowOrRows, columns?): SqlFragment
135
+ sql.assignments<Input>(patch, columns?): SqlFragment
206
136
  ```
207
137
 
208
- The mandatory predicate keeps the list nonempty. If every filter is optional, choose a complete
209
- query without `WHERE` for the empty case. Group mixed `AND`/`OR` expressions explicitly.
138
+ These build fragments, not complete statements. Keep table, predicates, expressions, conflict
139
+ behavior, and `RETURNING` in authored SQL.
210
140
 
211
- ## Object-based mutation fragments
141
+ | Cell | Insert | Assignment |
142
+ | ----------------------------- | --------------- | --------------- |
143
+ | Missing or undefined | `DEFAULT` | Omitted |
144
+ | Null | Bound SQL NULL | Bound SQL NULL |
145
+ | Other accepted value | Bound parameter | Bound parameter |
146
+ | SQL fragment/identifier/query | Rejected | Rejected |
212
147
 
213
- `sql.insertValues(input, columns?)` and `sql.assignments(input, columns?)` return fragments. Keep
214
- operation, table, predicates, expressions, conflict policy, and `RETURNING` in SQL:
148
+ Inputs must be plain objects, including null-prototype objects. Only own data properties are read;
149
+ selected accessors reject. Helpers capture fields at construction; mutable bound values must remain
150
+ unchanged until execution settles.
215
151
 
216
- ```ts
217
- const query = sql<{ id: number }>`INSERT INTO public.users
218
- ${sql.insertValues({ email: 'reader@example.test' })} RETURNING id`;
219
- await db.query(query, { returns: 'one' });
220
- await db.query(sql`UPDATE public.users SET ${sql.assignments({ active: false })} WHERE id = ${42}`);
221
- ```
152
+ Without `columns`, inserts infer the union of enumerable keys with defined values in first-seen
153
+ order. Assignments use own enumerable keys and omit undefined values. Explicit columns must be
154
+ nonempty, unique, and valid identifiers; they select/order keys and ignore unlisted properties.
155
+ TypeScript checks selected names against known input keys. Selection can discard required values.
222
156
 
223
- Both accept plain objects, including null-prototype objects. Columns are quoted as individual
224
- identifiers. Only own data properties are read; accessors are rejected. Fields are captured at
225
- fragment construction; bound values retain pgclear encoding rules and must remain unchanged until settlement.
226
-
227
- | Input | Insert | Assignment |
228
- | -------------------------------------------- | ---------------------- | --------------- |
229
- | Missing or `undefined` | `DEFAULT` for the cell | Omitted |
230
- | `null` | Bound SQL null | Bound SQL null |
231
- | Other value | Bound parameter | Bound parameter |
232
- | SQL fragment, identifier, or query as a cell | Rejected | Rejected |
233
-
234
- Inserts accept one object or an array. Without `columns`, they infer the union of enumerable keys
235
- with defined values in first-seen order. Heterogeneous rows stay aligned. A single default-only row
236
- emits `DEFAULT VALUES`. Empty batches fail; multiple default-only rows need explicit columns.
237
- Assignments omit undefined values and fail when no assignments remain, even if surrounding SQL has
238
- other assignments.
239
-
240
- A supplied column list must be nonempty, unique, and valid. It selects and orders fields, ignoring
241
- unlisted keys. Missing insert cells use defaults; missing update fields are omitted. TypeScript
242
- checks selected names against known input keys, but selection can still discard required fields.
243
- Helpers have no schema metadata; PostgreSQL enforces identity, generated-column, and constraint
244
- rules. Use [generated inputs](#insert-and-update-inputs) and application validation.
245
-
246
- Write arithmetic, `column = DEFAULT`, and `excluded.column` in the surrounding SQL. There is no
247
- expression/default sentinel. A complete query accepts at most 65,535 bound parameters, including
248
- surrounding interpolations; Tenon does not split batches. See the [mutation cookbook](mutations.md).
157
+ A single default-only insert emits `DEFAULT VALUES`. Empty batches reject; multiple default-only
158
+ rows need explicit columns. Empty assignments always reject. There is no expression/default
159
+ sentinel; author `column = DEFAULT` or arithmetic in SQL. The complete query's limit is 65,535
160
+ parameters; Tenon does not split batches. See [writing data](mutations.md).
249
161
 
250
162
  ## Transactions, guarded writes, and errors
251
163
 
252
- For an existing user with id 42:
253
-
254
164
  ```ts
255
165
  await db.transaction(async (tx) => {
256
166
  await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${42}`, {
@@ -259,329 +169,167 @@ await db.transaction(async (tx) => {
259
169
  });
260
170
  ```
261
171
 
262
- Use `tx` for every operation that must commit together and await the work. The callback commits on
263
- success, rolls back on an escaping error, and expires before cleanup. Transactions belong to one
264
- database instance. Nested callbacks and automatic retries are not provided. pgclear owns
265
- BEGIN/COMMIT/ROLLBACK and recovery.
172
+ Use `tx` for every participating query and await all work. The callback commits on success, rolls
173
+ back on an escaping error, and its client expires when the callback finishes. Transactions belong
174
+ to one database instance. Calling the root `db` uses the pool, not the callback session.
266
175
 
267
- `affectedRows: { exact: n }` or `{ max: n }` accepts a nonnegative safe integer. It checks the final
268
- INSERT/UPDATE/DELETE/MERGE count independently of `RETURNING`. A guarded root call uses a
269
- transaction; a guarded callback call uses a savepoint. Failed checks and other errors from that
270
- guarded operation roll it back before throwing. A caught guarded failure permits continued work;
271
- unguarded SQL errors normally abort the transaction.
176
+ Tenon owns BEGIN/COMMIT/ROLLBACK; direct boundary statements are rejected. `tx` provides no nested
177
+ transaction method. Manual savepoints are supported on the callback session. To choose isolation,
178
+ issue `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE` before data queries; use
179
+ `SET LOCAL statement_timeout = '5s'` to bound statements in that transaction.
272
180
 
273
- Counts exclude extra trigger/cascade and CTE effects and cannot identify the wrong single row.
274
- Sequence advances and external effects are not reversible. A lost commit acknowledgment can leave
275
- the outcome unknown; applications own retry and idempotency policy.
276
-
277
- Set isolation before data queries with `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`, and use
278
- `SET LOCAL statement_timeout = '5s'` to bound statements in that transaction. Manual savepoints are
279
- supported on the owned callback session; pgclear chooses guarded-operation savepoints.
280
-
281
- SQL errors retain native driver details. Tenon's option/composition validation throws ordinary
282
- `Error` objects; driver encoding failures retain `PGCLEAR_*` codes.
283
- `CardinalityError` has code `TENON_CARDINALITY_MISMATCH`, `expected` (`one` or `oneOrNone`),
284
- `actual` (returned row count), and optional `label`. It does not change transaction or autocommit
285
- behavior. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
286
- `TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`; its `actual` is `number | null`.
287
- Work/cleanup failures can produce `AggregateError`.
288
-
289
- Optional `onQuery` observes completed calls with `label`, `durationMs`, `outcome`, and available
290
- `command`, `rowCount` (`number | null`), and `errorCode`. It receives no SQL or values. Validation
291
- failures before execution are not observed. Observers are not awaited; failures emit warnings without changing the
292
- query outcome. Query success inside a callback does not establish outer commit.
181
+ A write guard is exactly one of `{ exact: n }` or `{ max: n }`, with a nonnegative safe integer.
182
+ It checks the final INSERT/UPDATE/DELETE/MERGE count, independently of `RETURNING`:
293
183
 
294
- ## Generated types and checking
184
+ | Call | Guard behavior |
185
+ | ----------------------------------- | ---------------------------------------------------------------------------- |
186
+ | `db.query(query, { affectedRows })` | Wrap in a transaction; roll back the operation on a guard/result/SQL failure |
187
+ | `tx.query(query, { affectedRows })` | Wrap in a savepoint; roll back that operation before rejecting |
295
188
 
296
- `generateDefinitions({ connection, namespace?, onWarning? })` reads schema catalogs and renders
297
- TypeScript declarations in Tenon. It interprets defaults, domains, identities, writable columns, and
298
- schema summaries, coordinates with the migration advisory lock, and excludes its history table.
299
- It needs no migration files/history. The CLI owns atomic output and freshness checks. Decoder
300
- representations come from pgclear's selected registry rather than a second conversion registry.
189
+ A caught guarded failure permits continued work. An unguarded PostgreSQL error normally leaves the
190
+ transaction aborted; let it escape so the callback rolls back. Tenon serializes whole guarded
191
+ operations within a transaction, including savepoint handling.
301
192
 
302
- ```sh
303
- npx tenon generate --out src/generated-schema.d.ts
304
- npx tenon generate --out src/generated-schema.d.ts --check
305
- ```
306
-
307
- The default namespace is `public`; default output is `src/generated-schema.ts`. Commit exact output
308
- and exclude it from formatting. `--check` never rewrites: missing/stale output fails with a bounded
309
- diff. This checks file freshness, not query annotations, pending migrations, or every database object.
310
- Upgrading pgclear can change output without schema drift. Preserve reviewed generator-specific
311
- references for existing adoption guards; see [baseline verification](adoption-guide.md#check-and-adopt-an-existing-database).
312
-
313
- The generated module exports `Database[table]['row' | 'insert' | 'update']`. Ordinary tables and
314
- partitioned roots are included. Views, foreign tables, and attached partitions need authored types.
315
- Relationships, indexes, constraints, and referenced domains appear in comments. Use `Pick` for
316
- projections; joins and expressions need their actual selected shape.
317
-
318
- | PostgreSQL type | Default read | Generated write input |
319
- | ---------------------------------------------- | ------------------------------- | ------------------------------------- |
320
- | smallint, integer, oid, real, double precision | `number` | `number` |
321
- | bigint | `string` | `string \| bigint` |
322
- | numeric | `string` | `string \| bigint` |
323
- | boolean | `boolean` | `boolean` |
324
- | text, UUID, timetz | `string` | `string` |
325
- | date, timestamp, timestamptz | `string` | `string \| Date \| TemporalInput` |
326
- | time, interval | `string` | `string \| TemporalInput` |
327
- | enum | String literal union | Same union |
328
- | json/jsonb | `unknown` | Encoded string or pgclear `Parameter` |
329
- | bytea | `Uint8Array` | `Uint8Array` |
330
- | Supported built-in arrays | Recursive readonly `PgArray<T>` | Encoded string or pgclear `Parameter` |
331
-
332
- Every writable generated field also accepts a pgclear `Parameter`; the scalar entries above show
333
- plain inputs. SQL NULL is separate; nullable fields add null. Numeric and bigint arrays preserve
334
- exact strings, and temporal strings retain precision and infinity spellings. Dynamic enum/domain/extension
335
- arrays remain wire text unless a decoder changes them. Generation follows the selected registry.
336
- Custom text codecs can declare `text.typescript`, and those expressions are preserved in generated
337
- row types. A custom decoder without a known representation warns and becomes unknown. JSON member
338
- shapes remain unknown under default decoding. Database constraints and textual formats still
339
- require application validation. A scalar codec declaration describes every use of that codec's OID;
340
- it does not establish different application meanings for individual columns.
341
-
342
- Default-registry enum rows use catalog label unions. With a custom registry, a declared string
343
- representation stays string because that decoder may transform the labels; an explicit codec
344
- representation can declare a narrower output type. Enum write inputs retain the database choices.
345
-
346
- Programmatic generation uses the registry configured on its connection or supplied pool. The CLI
347
- uses default codecs. Regenerate committed definitions after a driver mapping change before running
348
- `generate --check`; custom decoder types that remain unknown need application-owned contracts.
349
- Catalog queries use default result decoders on the same borrowed session and snapshot, while
350
- application row types follow that session's configured registry. Custom application codecs therefore
351
- do not change the generator's own metadata-reading types.
352
-
353
- Codec type expressions must resolve where they are compiled. Relative import expressions in saved
354
- declarations resolve from the generated file; the checker's in-memory module resolves from the
355
- tsconfig directory. Prefer a package or configured type alias that resolves in both places when one
356
- registry is shared across generation and checking. Tenon does not execute the referenced modules.
357
-
358
- Output conversions are explicit: for example, `defaultCodecs.with(builtin.int8BigInt)` makes int8
359
- reads bigint, and `defaultCodecs.with(builtin.timestamptzDate)` makes timestamptz reads Date.
360
- Pgclear also supplies codecs for caller-provided Temporal implementations. Use the same registry for
361
- execution, programmatic generation, Describe, and query checking. Defaults stay strings regardless
362
- of which temporal features the runtime provides.
363
-
364
- ### Application aliases
365
-
366
- For shorter names, export aliases from a handwritten application module such as `src/schema-types.ts`:
367
-
368
- ```ts
369
- import type { Database } from './generated-schema.js';
370
-
371
- export type UserRow = Database['users']['row'];
372
- export type UserInsert = Database['users']['insert'];
373
- export type UserUpdate = Database['users']['update'];
374
- ```
375
-
376
- These aliases reference the generated definitions directly and follow changes when the schema is
377
- regenerated. Keep them in the application module so regeneration preserves your chosen names.
193
+ Counts exclude extra trigger/cascade and CTE effects and cannot identify the wrong single row.
194
+ Sequence advances and external effects may survive rollback. There are no automatic retries. A lost
195
+ COMMIT acknowledgment can leave the outcome unknown; the application owns retry/idempotency policy.
378
196
 
379
- ### Persistence and domain values
197
+ | Error | Code and fields |
198
+ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
199
+ | `CardinalityError` | `TENON_CARDINALITY_MISMATCH`; `expected` (`one` or `oneOrNone`), `actual` returned rows, optional `label` |
200
+ | `AffectedRowsError` | `TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`; `expected` constraint, `actual: number \| null`, `command` |
201
+ | Tenon option/composition validation | Ordinary `Error` |
202
+ | Driver/server failure | Native details, including driver `PGCLEAR_*` codes or PostgreSQL SQLSTATE |
203
+ | Work and cleanup both fail | May be an `AggregateError` |
380
204
 
381
- Generated types describe storage representations. Text `CHECK` constraints do not become unions, and
382
- JSON reads remain unknown. Validate values before narrowing to application domain types.
205
+ Use codes and structured fields rather than matching message text. Retain unexpected failures.
383
206
 
384
- For example, assume this additional table and regenerate `Database` from the resulting schema:
207
+ ## Generated types and checking
385
208
 
386
- ```sql
387
- CREATE TABLE public.sessions (
388
- id text PRIMARY KEY,
389
- data jsonb NOT NULL,
390
- expires_at timestamptz NOT NULL
391
- );
209
+ ```text
210
+ db.generateTypes({ out, namespace?, check?, onWarning? }): Promise<void>
211
+ generateDefinitions({ connection, namespace?, onWarning? }): Promise<string>
392
212
  ```
393
213
 
394
- Use the same application validator before encoding a write and when restoring a domain value:
214
+ `out` is required for file generation. Namespace defaults to `public`; `check` defaults to false.
215
+ Generation inherits the database registry. Missing/stale output rejects under `check: true` without
216
+ writing. See [schema types and values](schema-types.md) for file guarantees, generated shapes,
217
+ value mappings, codecs, and domain validation. The standalone `tenon generate` command was removed;
218
+ use an application script.
395
219
 
396
- ```ts
397
- import { sql } from '@kequtech/tenon-orm';
398
- import type { Database } from './generated-schema.js';
399
-
400
- function sessionData(value: unknown): { userId: string } {
401
- if (
402
- typeof value !== 'object' ||
403
- value === null ||
404
- Array.isArray(value) ||
405
- !('userId' in value) ||
406
- typeof value.userId !== 'string' ||
407
- value.userId.length === 0
408
- ) {
409
- throw new Error('Expected session data with a nonempty userId');
410
- }
411
- return { userId: value.userId };
412
- }
220
+ ### Application aliases
413
221
 
414
- function finiteDate(value: string): Date {
415
- const date = new Date(value);
416
- if (!Number.isFinite(date.getTime())) {
417
- throw new Error('Expected a finite database timestamp');
418
- }
419
- return date;
420
- }
222
+ Export aliases such as `UserRow = Database['users']['row']` in a handwritten file. See
223
+ [application aliases](schema-types.md#application-aliases).
421
224
 
422
- const input = {
423
- id: 'example-session',
424
- data: JSON.stringify(sessionData({ userId: 'user-42' })),
425
- expires_at: new Date(Date.now() + 60_000).toISOString(),
426
- } satisfies Database['sessions']['insert'];
427
- await db.query(sql`INSERT INTO public.sessions ${sql.insertValues(input)}`);
428
-
429
- const row = await db.query(
430
- sql<Database['sessions']['row']>`SELECT id, data, expires_at FROM public.sessions
431
- WHERE id = ${input.id} AND isfinite(expires_at) AND expires_at > now()`,
432
- { returns: 'oneOrNone' },
433
- );
434
- const session =
435
- row === undefined
436
- ? undefined
437
- : {
438
- id: row.id,
439
- data: sessionData(row.data),
440
- expiresAt: finiteDate(row.expires_at),
441
- };
442
- ```
443
-
444
- The generated JSON read remains `unknown` and the timestamp remains `string`. The projection
445
- validates both without assertions or edits to generated types. Preserve null separately for nullable
446
- columns. For an authentication policy requiring finite expiry, `isfinite(expires_at)` excludes both
447
- infinities before the expiry comparison; `expires_at > now()` alone would accept positive infinity.
448
- Include such predicates in expiry-sensitive updates/deletes too, and test infinite and expired
449
- values. This example illustrates representation validation; applications must also retain their
450
- token verification, revocation, and authorization checks. See
451
- [JSON and PostgreSQL arrays](mutations.md#json-and-postgresql-arrays) for array write boundaries.
225
+ ### Persistence and domain values
452
226
 
453
- Bigint and `count(*)` reads are exact strings. Use `BigInt(value)` for exact arithmetic; convert
454
- to number only after checking the safe integer range. Command `rowCount` values are numbers; the
455
- driver rejects counts outside the safe integer range.
227
+ Generated types describe storage representations. Validate JSON, text refinements, and domain
228
+ values at the application boundary; see [the worked example](schema-types.md#persistence-and-domain-values).
456
229
 
457
230
  ### Insert and update inputs
458
231
 
459
- ```ts
460
- import type { Database } from './generated-schema.js';
461
-
462
- const input: Database['users']['insert'] = { email: 'reader@example.test' };
463
- const patch: Pick<Database['users']['update'], 'active'> = { active: false };
464
- sql.insertValues(input);
465
- sql.assignments(patch);
466
- ```
467
-
468
- Insert fields are required when nonnullable, writable, and without a default. Other writable insert
469
- fields and all writable update fields are optional and accept explicit undefined, including with
470
- `exactOptionalPropertyTypes`. Explicit null requires both column and domain-chain permission. Domain
471
- defaults count; column defaults take precedence.
472
-
473
- Computed and `GENERATED ALWAYS` identity columns use optional `never` fields. Serial and
474
- `GENERATED BY DEFAULT` identities are optional on insert and writable on update; other primary keys
475
- remain writable. Use `Pick`/`Omit` for application restrictions. Apply the generated types through
476
- annotations, `satisfies`, or helper generic arguments; they do not validate constraints, JSON
477
- structure, authorization, or nonempty input.
478
-
479
- For custom representations, provide reviewed application types and encoders. Selected columns
480
- can discard required fields; see [the cookbook](mutations.md#input-types-and-runtime-validation).
232
+ Generated types account for defaults, identity/computed columns, domains, and nullability. See
233
+ [insert/update rules](schema-types.md#insert-and-update-inputs).
481
234
 
482
235
  ## Choosing a validation tool
483
236
 
484
- Run these against the intended development/test schema and role. The database tools use the CLI's
485
- `DATABASE_URL`; TypeScript itself needs no connection.
237
+ Use the intended development/test schema and role. TypeScript itself needs no connection.
486
238
 
487
- | Tool | Question answered | Limits |
488
- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
489
- | `tsc --noEmit` | Does the application satisfy its TypeScript contracts? | Does not inspect SQL or the live schema. |
490
- | `tenon check [file.ts[:line]]` | Does recoverable authored SQL contradict its input/result declarations? | Unsupported composition fails as unchecked; inconclusive properties stay unproven. Does not execute application SQL. |
491
- | `tenon describe query.sql` | What parameter/result metadata does PostgreSQL report for this finished SQL? | Does not compare source annotations or validate bound values. Does not execute the statement. |
492
- | `tenon generate --out src/generated-schema.d.ts --check` | Does saved generated output match this schema and driver mapping? | Does not check query annotations or replace migration status. Use the project's actual output path. |
493
- | Project database tests | Do representative queries produce the required rows, effects, permissions, and rollback behavior? | Cover exercised inputs and paths; retain cases for runtime-dependent composition and application validation. |
239
+ | Tool | Question answered | Main limit |
240
+ | ---------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------- |
241
+ | `tsc --noEmit` | Does application code satisfy TypeScript contracts? | Does not inspect SQL/schema |
242
+ | `tenon check [file.ts[:line]]` | Does recoverable SQL contradict declared input/result types? | Unsupported sites fail; inconclusive properties remain unproven |
243
+ | `tenon describe query.sql` | What metadata does PostgreSQL report for finished SQL? | Does not compare annotations or bind values |
244
+ | `db.generateTypes({ out, check: true })` | Does committed output match generation now? | Does not check queries or replace migration status |
245
+ | Database tests | Do exercised queries produce correct rows, effects, permissions, and rollback? | Cover only tested inputs and paths |
494
246
 
495
- After a schema or driver mapping change, apply migrations to the test database, regenerate and
496
- review declarations, then run the project's typecheck, applicable source checks, and database tests.
497
- In CI, `generate --check` detects stale committed output without replacing it.
247
+ After schema changes, migrate the test database, regenerate/review declarations, and run typechecks,
248
+ applicable source checks, and database tests. In CI, check generated output without replacing it.
249
+ Source checking and inspection parse/analyze application SQL without executing it; analysis can
250
+ take locks. Neither proves actual bound values or write effects.
498
251
 
499
252
  ## Source-aware checking
500
253
 
501
- Use tenon check [file.ts[:line]] [--project tsconfig.json] [--json] to compare authored SQL with its
502
- TypeScript input/result contracts and pgclear Describe. A file selects all recognized queries;
503
- a one-based line selects one complete query. The TypeScript compiler loads on the checking path.
254
+ `checkQueries(options)` is exported from `@kequtech/tenon-orm/check`, alongside its public types.
255
+ The bundled TypeScript compiler loads only through that path or the check command.
504
256
 
505
- The programmatic API is checkQueries from @kequtech/tenon-orm/check. It accepts connection, target,
506
- project, session, timeouts, codecs and optional custom readTypes. It returns ok, project, queries,
507
- diagnostics and available inspection context. Mismatches and unchecked sites fail; unproven types
508
- are trusted and reported as notes. No query sites fails. See [query checking](query-checking.md) for
509
- composition, output contracts, explicit expansion shapes and custom decoder representations.
257
+ Options are `{ connection, target?, project?, session?, timeouts?, codecs?, readTypes? }`.
258
+ Connection is a URI. The report contains `ok`, `project`, `queries`, `diagnostics`, and available
259
+ inspection `context`. Mismatches, unchecked sites, and no sites fail; unproven findings alone do not.
260
+ See [query checking](query-checking.md) for selection, supported composition, and custom decoders.
510
261
 
511
262
  ## Query inspection
512
263
 
513
- `describeQuery({ connection, query, session?, timeouts?, codecs? })` accepts a PostgreSQL URI and a finished
514
- SQL string or `{ text, parameterTypes? }` object. It returns PostgreSQL metadata, conservative read mappings and row
515
- declaration, session context, and coded findings. It does not bind values, execute the statement, or
516
- verify `sql<Row>`.
264
+ ```text
265
+ describeQuery({ connection, query, session?, timeouts?, codecs? }): Promise<QueryDescription>
266
+ ```
267
+
268
+ Connection is a URI; query is finished SQL text or `{ text, parameterTypes? }`. A `SqlQuery` fits
269
+ because it has `text`; values and its row annotation are not checked. The report includes
270
+ `parameters`, `columns`, `rowType`, `issues`, and `context`. It uses a separate session with bounded
271
+ timeouts. See [query inspection](query-inspection.md) for fields and finding codes.
272
+
273
+ ## Native driver access
517
274
 
518
- Inspection owns a separate session with bounded timeouts. Match runtime settings; temporary tables
519
- and uncommitted changes are not inherited. Read mappings use `codecs.typescript(oid)` from the selected pgclear registry; defaults apply when
520
- omitted. Custom decoder types may remain unresolved. Nullability stays unknown. See
521
- [query inspection](query-inspection.md) for settings, result fields, issue codes, and testing.
275
+ `db.native` is the actual pgclear pool. Calls bypass Tenon's guards and observations and do not join
276
+ callback transactions. Use `queryClassic` for text; native `query` accepts only pgclear's own SQL
277
+ tag. See [native driver features](connections.md#use-native-driver-features) for session ownership,
278
+ prepared statements, and dedicated notification listeners.
522
279
 
523
280
  ## Troubleshooting
524
281
 
525
- Use error codes and structured fields rather than matching message wording. Driver errors have
526
- `PGCLEAR_*` codes; PostgreSQL errors use SQLSTATE. Checker diagnostics have their own `kind` and
527
- `code`. Retain unexpected errors instead of treating every failure as an empty query result.
528
-
529
- | Symptom | Likely cause and response |
530
- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
531
- | `PGCLEAR_ENCODE` | A bind is unsupported, a Date is invalid, or an encoder failed. Use `json(value)` for JSON and `param(values, builtin.int4Array)` for integer arrays. Plain bigint, valid Date, and supported Temporal inputs are accepted; plain objects/arrays and direct undefined still reject. Omit undefined through a mutation helper when omission is intended. JSON encoding still rejects bigint, including nested values. |
532
- | `CardinalityError` / `TENON_CARDINALITY_MISMATCH` | `one` received zero/multiple rows, or `oneOrNone` received multiple rows. Review the predicate and intended mode. Use deterministic `ORDER BY` plus `LIMIT 1` only when selecting one of several matches is intended. Result modes do not add limits. |
533
- | `TENON_AFFECTED_ROWS_MISMATCH` | A write exceeded `max` or differed from `exact`. The guarded operation has rolled back. Inspect the predicate and `actual` count; retain the intended write bound. |
534
- | `TENON_AFFECTED_ROWS_UNSUPPORTED` | The final command lacks a supported INSERT/UPDATE/DELETE/MERGE count. Apply the guard to a supported write; use result modes for returned-row requirements. |
535
- | Empty assignments | Every selected patch field was missing/undefined. Skip the update or branch to an expression-only statement when that is intended. Null remains a bound value. |
536
- | SQLSTATE `42P18` / `42P08` during inspection | PostgreSQL cannot establish an unambiguous parameter type. Add the intended SQL cast, or pass `parameterTypes` OID hints to `describeQuery`. Inspection does not bind query values. |
537
- | Query checker reports `unchecked` | SQL structure or an expansion shape could not be recovered, a limit was exceeded, or inspection failed. Follow the diagnostic; optional generated patch fields and dynamic fragment lists can be valid at runtime but remain unchecked. See [responding to findings](query-checking.md#responding-to-findings). |
538
- | Missing/stale generated definitions | Check the namespace, schema, driver version, and output path. Regenerate with the same options without `--check`, review the diff, and commit intended output. Freshness checks never rewrite the file. |
539
-
540
- Inside a transaction, catch a guarded query failure only after its promise rejects; savepoint
541
- cleanup completes before rejection. An unguarded server SQL error normally leaves the transaction
542
- failed, so let the outer callback roll it back. Encoding errors happen before dispatch, but their
543
- inputs still need correction. A timeout or lost commit acknowledgement does not prove rollback;
544
- see [transactions and errors](#transactions-guarded-writes-and-errors) before adding retries.
282
+ | Symptom | Response |
283
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
284
+ | `DATABASE_URL is required` | Supply the variable to CLI commands. No `.env` files are loaded automatically. Application configuration is explicit. |
285
+ | `PGCLEAR_ENCODE` | Check inputs: objects/arrays need `json`/`param`; direct undefined, unsafe integers, nonfinite numbers, and invalid Dates reject. JSON still rejects bigint. |
286
+ | `CardinalityError` | Check the expected row count and predicate. To choose one of several rows, author deterministic `ORDER BY ... LIMIT 1`. |
287
+ | `TENON_AFFECTED_ROWS_MISMATCH` | The guarded operation rolled back. Inspect `actual` and the predicate; retain the intended bound. |
288
+ | `TENON_AFFECTED_ROWS_UNSUPPORTED` | The final command lacks a supported write count. Use guards on INSERT/UPDATE/DELETE/MERGE; result modes check returned rows. |
289
+ | Empty assignments | All selected fields were missing/undefined. Skip the update or branch to an expression-only statement. |
290
+ | Duplicate result columns | Alias columns uniquely, especially in joins. The driver rejects duplicate object keys. |
291
+ | SQLSTATE `42P18` / `42P08` during inspection | Add the intended parameter cast or Describe OID hints. Inspection binds no values. |
292
+ | Checker reports `unchecked` | Follow the diagnostic. Runtime fragments and optional patches can execute correctly yet remain unsupported by source recovery. |
293
+ | Missing/stale generated output | Check schema, namespace, registry, driver version, and path. Regenerate, review, and commit intended output. |
294
+ | Migration history conflict | Restore original applied files. Add new changes in a forward migration. Checksums include down SQL too. |
295
+ | Tooling lock wait | Check concurrent migration/DDL work, use direct or session pooling, and configure server timeouts. |
296
+
297
+ After a guarded query rejects, savepoint cleanup has finished. An unguarded server error normally
298
+ aborts a transaction. A timeout or lost connection does not itself establish whether a commit
299
+ happened; review the transaction contract before adding retries.
545
300
 
546
301
  ## Command reference
547
302
 
548
- Database commands require `DATABASE_URL`. `new` and `--help` work offline. No environment files are
549
- loaded. Paths are relative to the working directory. Explicit flags unsupported by a command are
550
- rejected. The generate --check flag checks file freshness; the check command checks authored SQL.
551
- Neither is a migration dry run.
303
+ All database commands read `DATABASE_URL`. `new` and `--help` work offline. No environment files
304
+ are loaded. Paths resolve from the working directory; unsupported command flags reject.
552
305
 
553
306
  ```text
554
307
  tenon new <name> [--directory migrations]
555
308
  tenon migrate [--directory migrations] [--namespace public]
556
309
  tenon status [--directory migrations] [--namespace public]
557
310
  tenon rollback [--directory migrations] [--namespace public] [--steps 1]
558
- tenon generate [--namespace public] [--out src/generated-schema.ts] [--check]
311
+ tenon baseline capture [--directory migrations] [--namespace public] [--schema name ...]
312
+ tenon baseline adopt [--directory migrations] [--namespace public]
559
313
  tenon check [file.ts[:line]] [--project tsconfig.json] [--json]
560
314
  [--role name] [--search-path value] [--time-zone value]
561
315
  [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
562
316
  tenon describe <query.sql> [--role name] [--search-path value] [--time-zone value]
563
317
  [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
564
- tenon baseline capture [--directory migrations] [--namespace public] [--schema name ...]
565
- tenon baseline adopt [--directory migrations] [--namespace public]
566
318
  ```
567
319
 
568
- `new` scaffolds empty `<numeric-id>_<name>.up.sql` and `.down.sql` files. Names contain letters,
569
- digits, underscores, or hyphens. Fill both files or remove the down file for an irreversible change.
570
- IDs sort numerically. Never edit applied files: checksums cover up and down SQL.
571
-
572
- Each migration and history entry commits together; earlier files remain applied if a later one
573
- fails. Runner search path is the selected namespace followed by `pg_catalog`. `status` returns
574
- applied/pending IDs as JSON and rejects history conflicts. `rollback` validates that every selected
575
- file has down SQL before executing any; `steps` is a positive safe integer. Use dollar-quoted
576
- function bodies and explicit `E'...'` backslash escapes. Nontransactional DDL such as
577
- `CREATE INDEX CONCURRENTLY` requires a separate procedure.
578
-
579
- Migration/status/rollback/adoption use session advisory locks per database and namespace; generation
580
- uses the same lock in shared mode. Use direct or session-pooled connections and server timeouts to
581
- bound waits. Baseline capture uses a local creation lock and native `pg_dump`; it does not acquire
582
- the runner's database advisory lock. See [adoption](adoption-guide.md).
583
-
584
- Programmatic `migrate`, `migrationStatus`, and `rollback` accept
585
- `{ connection, directory?, namespace? }`; rollback also accepts `steps`. Migration/rollback return
586
- completed IDs. `createMigration({ name, directory? })` returns `{ id, upPath, downPath }`;
587
- `readMigrations(directory?)` returns validated SQL and checksums.
320
+ | Command | Output and behavior |
321
+ | ---------------------- | ----------------------------------------------------------------- |
322
+ | `new` | Paths of the new empty up/down pair; fill before applying |
323
+ | `migrate` / `rollback` | Completed IDs or “nothing to do”; each file commits independently |
324
+ | `status` | JSON `{ applied, pending }`; validates history |
325
+ | `baseline capture` | Writes a reviewed schema-only baseline using native `pg_dump` |
326
+ | `baseline adopt` | Records baseline history without executing its SQL |
327
+ | `check` | Diagnostics/counts, or a structured report with `--json` |
328
+ | `describe` | JSON metadata, including unresolved findings |
329
+
330
+ Successful commands exit 0. Invalid input or a failed operation exits 1. `check` also exits 1 for
331
+ mismatches, unchecked sites, or no sites; unproven findings alone do not fail. `describe` can exit 0
332
+ with unresolved findings. There is no migration dry-run command.
333
+
334
+ Programmatic runner APIs are [documented with migrations](migrations.md#programmatic-api).
335
+ Baseline APIs and prerequisites are in [adoption](adoption-guide.md#baseline-api).