@kequtech/tenon-orm 0.4.0 → 0.4.3

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,165 @@
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.
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.
22
19
 
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`.
20
+ The returned `Database` provides:
26
21
 
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
- ```
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
+ | `native` | The underlying pgclear pool |
27
+ | `close({ timeoutMs? }?)` | Disable the wrapper and close an owned pool; default grace 5,000 ms |
39
28
 
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.
29
+ `onQuery(event)` observes completed Tenon query calls. It may return void or a promise; it is not
30
+ awaited. Observer failures emit warnings without changing the outcome. `QueryEvent` contains
31
+ `durationMs`, `outcome: 'success' | 'error'`, and optional `label`, `command`, `rowCount`, and
32
+ `errorCode`. SQL and values are excluded. Validation failures before execution are not observed.
45
33
 
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.
51
-
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.
34
+ See [connection configuration and shutdown](connections.md) for pool settings, TLS, environment
35
+ handling, timeout bounds, and lifecycle examples. Runtime `search_path` is independent of tooling
36
+ `namespace` options.
59
37
 
60
38
  ### Driver updates
61
39
 
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()`.
40
+ The dependency range is `@kequtech/pgclear: ^0.3.0`. Compatible patches may be updated independently;
41
+ see [driver updates](connections.md#update-the-driver). Tenon re-exports `builtin`, `json`, `param`,
42
+ `defaultCodecs`, and `createTemporalCodecs`, with the driver's `Codec`, `CodecRegistry`, `Parameter`,
43
+ `PgArray`, `TemporalConstructors`, and `TemporalInput` types.
101
44
 
102
45
  ## Queries and result modes
103
46
 
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.
47
+ `sql<Row>` is a template tag returning `SqlQuery<Row>` with readonly `text` and `values`. It does
48
+ not execute. `Row` describes one selected record; without it, fields are `unknown`. Neither result
49
+ annotations nor generated types validate data during execution.
107
50
 
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
- ```
51
+ `db.query` and `tx.query` accept `{ returns?, affectedRows?, label? }`. Unknown options reject.
52
+ A supplied label must be a nonblank string.
114
53
 
115
- `query` options are `{ returns?, affectedRows?, label? }`; unknown keys fail. A supplied `label`
116
- must be a nonempty string.
54
+ | `returns` | Output | Allowed returned rows |
55
+ | ------------------ | ------------------ | --------------------- |
56
+ | `native` (default) | `QueryResult<Row>` | Any |
57
+ | `many` | `readonly Row[]` | Any |
58
+ | `oneOrNone` | `Row \| undefined` | Zero or one |
59
+ | `one` | `Row` | Exactly one |
117
60
 
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 |
61
+ `QueryResult` follows pgclear and includes `rows`, `rowCount`, `command`, `commandTag`, and `fields`.
62
+ `rowCount` is `number | null`, independent of `RETURNING`; unsafe integer counts reject. SQL
63
+ `count(*)` is a column value and defaults to string. Row arrays are readonly.
124
64
 
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.
65
+ Modes do not add `LIMIT` or `RETURNING`. Returned-row mismatches throw `CardinalityError`; alone,
66
+ that does not undo an autocommitted write. For write bounds that roll back, use `affectedRows`.
131
67
 
132
- Functions accepting `QueryClient` can use either `Database` or `Transaction`. Public types also
133
- include `QueryOptions`, `QueryReturnMode`, and `QueryOutput<Row, Mode>`.
68
+ `QueryClient` is the query-only interface accepted by either a `Database` or callback `Transaction`.
69
+ Other exported types include `QueryOptions`, `QueryReturnMode`, `QueryOutput<Row, Mode>`,
70
+ `SqlQuery`, `SqlFragment`, and `UnknownRow`.
134
71
 
135
72
  ### Adapting transactional results
136
73
 
137
- For a job library or other adapter expecting `{ rows }`, extract the native result's row array:
74
+ For an adapter expecting `{ rows }`, execute its trusted SQL through the callback's `tx`:
138
75
 
139
76
  ```ts
140
77
  import { unsafeSql, type QueryClient } from '@kequtech/tenon-orm';
141
78
 
142
79
  async function queryRows(tx: QueryClient, text: string, values: readonly unknown[]) {
143
80
  const result = await tx.query(unsafeSql(text, values));
144
- return { rows: result.rows };
81
+ return { rows: [...result.rows] };
145
82
  }
146
83
  ```
147
84
 
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.
153
-
154
- ## SQL parameters and identifiers
85
+ The copy supplies a mutable array if the adapter requires one. Values must already be valid driver
86
+ inputs; external arrays/objects need type-aware `param`, `json`, or text encoding. Await adapter
87
+ work inside the callback. Native pool queries use another checkout and do not join it.
155
88
 
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)`.
89
+ For a job-library transaction adapter such as pg-boss's, verify actual SQL inputs and timestamp
90
+ expectations. Enqueueing and returning an ID may need no output conversion; returned timestamps
91
+ follow the database-wide codecs. Keep atomic commit/rollback tests. Tenon provides no per-query
92
+ codec override, and input encoders are independent of output codecs.
163
93
 
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.
94
+ ## SQL parameters and identifiers
169
95
 
170
- ```ts
171
- import { builtin, param } from '@kequtech/tenon-orm';
96
+ | API | Contract |
97
+ | ------------------------------ | ------------------------------------------------------------------------------ |
98
+ | `sql<Row>` template tag | Bind ordinary values; interpolate Tenon fragments and identifiers structurally |
99
+ | `identifier(...names)` | Quote components separately, e.g. `"public"."users"` |
100
+ | `unsafeSql<Row>(text, values)` | Trusted external SQL with an explicit values array, including `[]` |
101
+ | `json(value, type?)` | Encode JSON; default type is jsonb |
102
+ | `param(value, codec, format?)` | Choose an explicit driver parameter type and encoder |
172
103
 
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
- ```
104
+ Do not quote interpolations or mix manual `$n` placeholders into templates. Plain binds accept null,
105
+ string, finite number, bigint, boolean, valid Date, supported Temporal values, and Uint8Array.
106
+ Undefined, plain objects/arrays, nonfinite numbers, and unsafe integer numbers reject.
177
107
 
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.
108
+ Strings, numbers, bigint, Date, and supported Temporal values use server type inference. Date
109
+ encodes in UTC. Cast standalone parameters when PostgreSQL needs type context. Use `json` for JSON
110
+ and `param` for PostgreSQL arrays/custom encodings. Do not mutate bound values before settlement.
181
111
 
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.
112
+ Identifier components require 1–63 UTF-8 bytes and no NUL. Choose approved names; quoting is not
113
+ authorization. SQL syntax such as `DESC` belongs in fixed fragments or query alternatives. Never
114
+ concatenate request input into `unsafeSql` text.
185
115
 
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.
116
+ A query executes one statement through the extended protocol. Tenon does not expose a prepared
117
+ statement cache or query `name` option. See [writing queries](sql.md) for examples.
189
118
 
190
119
  ## SQL fragments and joins
191
120
 
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.
121
+ - `sql.fragment` creates immutable partial SQL with bound values. It has no row type and
122
+ cannot execute alone. Blank fragments reject.
123
+ - `sql.join(fragments, 'AND' | 'OR' | ',')` requires a nonempty array of fragments. It does not add
124
+ parentheses or alter precedence.
125
+ - Nested/repeated fragments preserve ordered parameters. Full `sql`/`unsafeSql` queries cannot be
126
+ nested. Literal `$1` text in SQL strings/comments is not rewritten.
194
127
 
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.
128
+ See [composition examples](sql.md#compose-sql-fragments), including the empty-filter case.
199
129
 
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`;
130
+ ## Object-based mutation fragments
131
+
132
+ ```text
133
+ sql.insertValues<Input>(rowOrRows, columns?): SqlFragment
134
+ sql.assignments<Input>(patch, columns?): SqlFragment
206
135
  ```
207
136
 
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.
137
+ These build fragments, not complete statements. Keep table, predicates, expressions, conflict
138
+ behavior, and `RETURNING` in authored SQL.
210
139
 
211
- ## Object-based mutation fragments
140
+ | Cell | Insert | Assignment |
141
+ | ----------------------------- | --------------- | --------------- |
142
+ | Missing or undefined | `DEFAULT` | Omitted |
143
+ | Null | Bound SQL NULL | Bound SQL NULL |
144
+ | Other accepted value | Bound parameter | Bound parameter |
145
+ | SQL fragment/identifier/query | Rejected | Rejected |
212
146
 
213
- `sql.insertValues(input, columns?)` and `sql.assignments(input, columns?)` return fragments. Keep
214
- operation, table, predicates, expressions, conflict policy, and `RETURNING` in SQL:
147
+ Inputs must be plain objects, including null-prototype objects. Only own data properties are read;
148
+ selected accessors reject. Helpers capture fields at construction; mutable bound values must remain
149
+ unchanged until execution settles.
215
150
 
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
- ```
151
+ Without `columns`, inserts infer the union of enumerable keys with defined values in first-seen
152
+ order. Assignments use own enumerable keys and omit undefined values. Explicit columns must be
153
+ nonempty, unique, and valid identifiers; they select/order keys and ignore unlisted properties.
154
+ TypeScript checks selected names against known input keys. Selection can discard required values.
222
155
 
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).
156
+ A single default-only insert emits `DEFAULT VALUES`. Empty batches reject; multiple default-only
157
+ rows need explicit columns. Empty assignments always reject. There is no expression/default
158
+ sentinel; author `column = DEFAULT` or arithmetic in SQL. The complete query's limit is 65,535
159
+ parameters; Tenon does not split batches. See [writing data](mutations.md).
249
160
 
250
161
  ## Transactions, guarded writes, and errors
251
162
 
252
- For an existing user with id 42:
253
-
254
163
  ```ts
255
164
  await db.transaction(async (tx) => {
256
165
  await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${42}`, {
@@ -259,329 +168,180 @@ await db.transaction(async (tx) => {
259
168
  });
260
169
  ```
261
170
 
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.
171
+ Use `tx` for every participating query and await all work. The callback commits on success, rolls
172
+ back on an escaping error, and its client expires when the callback finishes. Transactions belong
173
+ to one database instance. Calling the root `db` uses the pool, not the callback session.
266
174
 
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.
175
+ Tenon owns BEGIN/COMMIT/ROLLBACK; direct boundary statements are rejected. `tx` provides no nested
176
+ transaction method. Manual savepoints are supported on the callback session. To choose isolation,
177
+ issue `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE` before data queries; use
178
+ `SET LOCAL statement_timeout = '5s'` to bound statements in that transaction.
272
179
 
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.
180
+ A write guard is exactly one of `{ exact: n }` or `{ max: n }`, with a nonnegative safe integer.
181
+ It checks the final INSERT/UPDATE/DELETE/MERGE count, independently of `RETURNING`:
293
182
 
294
- ## Generated types and checking
183
+ | Call | Guard behavior |
184
+ | ----------------------------------- | ---------------------------------------------------------------------------- |
185
+ | `db.query(query, { affectedRows })` | Wrap in a transaction; roll back the operation on a guard/result/SQL failure |
186
+ | `tx.query(query, { affectedRows })` | Wrap in a savepoint; roll back that operation before rejecting |
295
187
 
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.
188
+ A caught guarded failure permits continued work. An unguarded PostgreSQL error normally leaves the
189
+ transaction aborted; let it escape so the callback rolls back. Tenon serializes whole guarded
190
+ operations within a transaction, including savepoint handling.
301
191
 
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
- ```
192
+ Counts exclude extra trigger/cascade and CTE effects and cannot identify the wrong single row.
193
+ Sequence advances and external effects may survive rollback. There are no automatic retries. A lost
194
+ COMMIT acknowledgment can leave the outcome unknown; the application owns retry/idempotency policy.
375
195
 
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.
196
+ | Error | Code and fields |
197
+ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
198
+ | `CardinalityError` | `TENON_CARDINALITY_MISMATCH`; `expected` (`one` or `oneOrNone`), `actual` returned rows, optional `label` |
199
+ | `AffectedRowsError` | `TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`; `expected` constraint, `actual: number \| null`, `command` |
200
+ | Tenon option/composition validation | Ordinary `Error` |
201
+ | Driver/server failure | Native details, including driver `PGCLEAR_*` codes or PostgreSQL SQLSTATE |
202
+ | Work and cleanup both fail | May be an `AggregateError` |
378
203
 
379
- ### Persistence and domain values
204
+ Use codes and structured fields rather than matching message text. Retain unexpected failures.
380
205
 
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.
383
-
384
- For example, assume this additional table and regenerate `Database` from the resulting schema:
206
+ ## Generated types and checking
385
207
 
386
- ```sql
387
- CREATE TABLE public.sessions (
388
- id text PRIMARY KEY,
389
- data jsonb NOT NULL,
390
- expires_at timestamptz NOT NULL
391
- );
208
+ ```sh
209
+ tenon generate
210
+ tenon generate --check
392
211
  ```
393
212
 
394
- Use the same application validator before encoding a write and when restoring a domain value:
395
-
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
- }
213
+ Generation uses [shared configuration](configuration.md) for its connection/codecs, output path,
214
+ and namespace. Missing/stale output rejects under `--check` without writing. See
215
+ [schema types and values](schema-types.md) for file guarantees, generated shapes, and value mappings.
216
+ `generateDefinitions({ connection, namespace?, onWarning? }): Promise<string>` returns source text
217
+ without file handling for custom tooling. Database instances expose query execution and lifecycle
218
+ methods; generation is a CLI tool.
413
219
 
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
- }
220
+ ### Application aliases
421
221
 
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
- ```
222
+ Export aliases such as `UserRow = Database['users']['row']` in a handwritten file. See
223
+ [application aliases](schema-types.md#application-aliases).
443
224
 
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 through shared configuration. TypeScript itself
238
+ needs no connection.
486
239
 
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. |
240
+ | Tool | Question answered | Main limit |
241
+ | ------------------------------ | ------------------------------------------------------------------------------ | --------------------------------------------------------------- |
242
+ | `tsc --noEmit` | Does application code satisfy TypeScript contracts? | Does not inspect SQL/schema |
243
+ | `tenon check [file.ts[:line]]` | Does recoverable SQL contradict declared input/result types? | Unsupported sites fail; inconclusive properties remain unproven |
244
+ | `tenon describe query.sql` | What metadata does PostgreSQL report for finished SQL? | Does not compare annotations or bind values |
245
+ | `tenon generate --check` | Does committed output match generation now? | Does not check queries or replace migration status |
246
+ | Database tests | Do exercised queries produce correct rows, effects, permissions, and rollback? | Cover only tested inputs and paths |
494
247
 
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.
248
+ After schema changes, migrate the test database, regenerate/review declarations, and run typechecks,
249
+ applicable source checks, and database tests. In CI, check generated output without replacing it.
250
+ Source checking and inspection parse/analyze application SQL without executing it; analysis can
251
+ take locks. Neither proves actual bound values or write effects.
498
252
 
499
253
  ## Source-aware checking
500
254
 
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.
255
+ `checkQueries(options)` is exported from `@kequtech/tenon-orm/check`, alongside its public types.
256
+ The bundled TypeScript compiler loads only through that path or the check command.
504
257
 
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.
258
+ Options are `{ connection, target?, project?, session?, timeouts?, codecs?, readTypes? }`.
259
+ Connection is a URI or pool-option values; connection codecs are inherited unless `codecs` is explicit. The report contains `ok`, `project`, `queries`, `diagnostics`, and available
260
+ inspection `context`. Mismatches, unchecked sites, and no sites fail; unproven findings alone do not.
261
+ See [query checking](query-checking.md) for selection, supported composition, and custom decoders.
510
262
 
511
263
  ## Query inspection
512
264
 
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>`.
265
+ ```text
266
+ describeQuery({ connection, query, session?, timeouts?, codecs? }): Promise<QueryDescription>
267
+ ```
268
+
269
+ Connection is a URI or pool-option values; query is finished SQL text or `{ text, parameterTypes? }`. A `SqlQuery` fits
270
+ because it has `text`; values and its row annotation are not checked. The report includes
271
+ `parameters`, `columns`, `rowType`, `issues`, and `context`. It uses a separate session with bounded
272
+ timeouts. See [query inspection](query-inspection.md) for fields and finding codes.
517
273
 
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.
274
+ ## Native driver access
275
+
276
+ `db.native` is the actual pgclear pool. Calls bypass Tenon's guards and observations and do not join
277
+ callback transactions. Use `queryClassic` for text; native `query` accepts only pgclear's own SQL
278
+ tag. See [native driver features](connections.md#use-native-driver-features) for session ownership,
279
+ prepared statements, and dedicated notification listeners.
522
280
 
523
281
  ## Troubleshooting
524
282
 
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.
283
+ | Symptom | Response |
284
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
285
+ | Missing shared configuration | Run `tenon init` or select an existing config with `--config`. Generation, checking, and inspection require it. |
286
+ | `DATABASE_URL is required` | Supply the variable for migration commands without config, or from your shared config module. No `.env` files are loaded automatically. |
287
+ | `PGCLEAR_ENCODE` | Check inputs: objects/arrays need `json`/`param`; direct undefined, unsafe integers, nonfinite numbers, and invalid Dates reject. JSON still rejects bigint. |
288
+ | `CardinalityError` | Check the expected row count and predicate. To choose one of several rows, author deterministic `ORDER BY ... LIMIT 1`. |
289
+ | `TENON_AFFECTED_ROWS_MISMATCH` | The guarded operation rolled back. Inspect `actual` and the predicate; retain the intended bound. |
290
+ | `TENON_AFFECTED_ROWS_UNSUPPORTED` | The final command lacks a supported write count. Use guards on INSERT/UPDATE/DELETE/MERGE; result modes check returned rows. |
291
+ | Empty assignments | All selected fields were missing/undefined. Skip the update or branch to an expression-only statement. |
292
+ | Duplicate result columns | Alias columns uniquely, especially in joins. The driver rejects duplicate object keys. |
293
+ | SQLSTATE `42P18` / `42P08` during inspection | Add the intended parameter cast or Describe OID hints. Inspection binds no values. |
294
+ | Checker reports `unchecked` | Follow the diagnostic. Runtime fragments and optional patches can execute correctly yet remain unsupported by source recovery. |
295
+ | Missing/stale generated output | Check schema, namespace, registry, driver version, and path. Regenerate, review, and commit intended output. |
296
+ | Migration history conflict | Restore original applied files. Add new changes in a forward migration. Checksums include down SQL too. |
297
+ | Tooling lock wait | Check concurrent migration/DDL work, use direct or session pooling, and configure server timeouts. |
298
+
299
+ After a guarded query rejects, savepoint cleanup has finished. An unguarded server error normally
300
+ aborts a transaction. A timeout or lost connection does not itself establish whether a commit
301
+ happened; review the transaction contract before adding retries.
545
302
 
546
303
  ## Command reference
547
304
 
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.
305
+ All commands accept `--config path`. `generate`, `check`, and `describe` require
306
+ [shared configuration](configuration.md) and use its connection/codecs. Migration and baseline
307
+ commands use config when present and fall back to `DATABASE_URL` only when no config is found.
308
+ `init` creates config without importing application code; `new` uses the configured migration
309
+ directory without opening a database; `--help` loads no config.
310
+ Config modules may read environment variables, but Tenon loads no `.env` files automatically.
311
+ Configured paths resolve from the config file; CLI path flags resolve from the working directory.
312
+ Unsupported command flags reject.
552
313
 
553
314
  ```text
315
+ tenon init [--config path]
554
316
  tenon new <name> [--directory migrations]
317
+ tenon generate [--out path] [--namespace schema] [--check]
555
318
  tenon migrate [--directory migrations] [--namespace public]
556
319
  tenon status [--directory migrations] [--namespace public]
557
320
  tenon rollback [--directory migrations] [--namespace public] [--steps 1]
558
- tenon generate [--namespace public] [--out src/generated-schema.ts] [--check]
321
+ tenon baseline capture [--directory migrations] [--namespace public] [--schema name ...] [--connection native-uri]
322
+ tenon baseline adopt [--directory migrations] [--namespace public]
559
323
  tenon check [file.ts[:line]] [--project tsconfig.json] [--json]
560
324
  [--role name] [--search-path value] [--time-zone value]
561
325
  [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
562
326
  tenon describe <query.sql> [--role name] [--search-path value] [--time-zone value]
563
327
  [--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
328
  ```
567
329
 
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.
330
+ | Command | Output and behavior |
331
+ | ---------------------- | --------------------------------------------------------------------------------------- |
332
+ | `init` | Creates a minimal TypeScript or JavaScript config, refusing to overwrite existing files |
333
+ | `new` | Paths of the new empty up/down pair; fill before applying |
334
+ | `migrate` / `rollback` | Completed IDs or “nothing to do”; each file commits independently |
335
+ | `status` | JSON `{ applied, pending }`; validates history |
336
+ | `baseline capture` | Writes a reviewed schema-only baseline using native `pg_dump` |
337
+ | `baseline adopt` | Records baseline history without executing its SQL |
338
+ | `generate` | Writes output atomically; `--check` rejects missing/stale output without writing |
339
+ | `check` | Diagnostics/counts, or a structured report with `--json` |
340
+ | `describe` | JSON metadata, including unresolved findings |
341
+
342
+ Successful commands exit 0. Invalid input or a failed operation exits 1. `check` also exits 1 for
343
+ mismatches, unchecked sites, or no sites; unproven findings alone do not fail. `describe` can exit 0
344
+ with unresolved findings. There is no migration dry-run command.
345
+
346
+ Programmatic runner APIs are [documented with migrations](migrations.md#programmatic-api).
347
+ Baseline APIs and prerequisites are in [adoption](adoption-guide.md#baseline-api).