@kequtech/tenon-orm 0.3.1 → 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.
Files changed (68) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +81 -26
  3. package/dist/affected-rows.js +0 -1
  4. package/dist/baseline.js +3 -5
  5. package/dist/check/contracts.d.ts +6 -0
  6. package/dist/check/contracts.js +206 -0
  7. package/dist/check/project.d.ts +18 -0
  8. package/dist/check/project.js +224 -0
  9. package/dist/check/source.d.ts +51 -0
  10. package/dist/check/source.js +629 -0
  11. package/dist/check/types.d.ts +36 -0
  12. package/dist/check/types.js +1 -0
  13. package/dist/check.d.ts +4 -0
  14. package/dist/check.js +147 -0
  15. package/dist/cli.js +40 -27
  16. package/dist/connection.d.ts +5 -12
  17. package/dist/connection.js +12 -121
  18. package/dist/database.js +10 -8
  19. package/dist/describe.js +10 -3
  20. package/dist/generate.d.ts +13 -3
  21. package/dist/generate.js +52 -25
  22. package/dist/generation/catalog.d.ts +5 -6
  23. package/dist/generation/catalog.js +7 -9
  24. package/dist/generation/output.js +2 -4
  25. package/dist/generation/render.d.ts +3 -0
  26. package/dist/generation/render.js +60 -0
  27. package/dist/generation/schema-summary.d.ts +3 -3
  28. package/dist/generation/schema-summary.js +2 -2
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +2 -0
  31. package/dist/inspection/catalog.d.ts +3 -6
  32. package/dist/inspection/catalog.js +9 -18
  33. package/dist/inspection/cli.d.ts +12 -0
  34. package/dist/inspection/cli.js +9 -4
  35. package/dist/inspection/connection.d.ts +2 -3
  36. package/dist/inspection/connection.js +20 -22
  37. package/dist/inspection/options.js +0 -6
  38. package/dist/inspection/result.d.ts +3 -2
  39. package/dist/inspection/result.js +13 -26
  40. package/dist/inspection/types.d.ts +6 -4
  41. package/dist/migrations/connection.d.ts +3 -3
  42. package/dist/migrations/connection.js +14 -21
  43. package/dist/migrations/history.d.ts +4 -4
  44. package/dist/migrations/history.js +5 -5
  45. package/dist/migrations.js +7 -9
  46. package/dist/query-options.d.ts +1 -1
  47. package/dist/query.js +1 -1
  48. package/dist/transaction.d.ts +2 -2
  49. package/dist/transaction.js +21 -77
  50. package/dist/types.d.ts +4 -1
  51. package/docs/adoption-guide.md +88 -111
  52. package/docs/agent-guide.md +94 -147
  53. package/docs/connections.md +185 -0
  54. package/docs/human-guide.md +133 -146
  55. package/docs/migrations.md +133 -0
  56. package/docs/mutations.md +110 -60
  57. package/docs/query-checking.md +199 -0
  58. package/docs/query-inspection.md +89 -58
  59. package/docs/reference.md +232 -437
  60. package/docs/schema-types.md +263 -0
  61. package/docs/sql.md +212 -0
  62. package/package.json +11 -7
  63. package/dist/driver/types.d.ts +0 -10
  64. package/dist/driver/types.js +0 -49
  65. package/dist/generation/table-types.d.ts +0 -2
  66. package/dist/generation/table-types.js +0 -96
  67. package/dist/inspection/protocol.d.ts +0 -7
  68. package/dist/inspection/protocol.js +0 -45
package/docs/reference.md CHANGED
@@ -1,299 +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 `pg@8.23.1` and its TypeScript declarations; no driver peer or
11
- special compiler plugin is required. A usual Node TypeScript 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
15
- ```
16
-
17
- `createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, `native`,
18
- and `close`. The required `connection` accepts:
19
-
20
- - A PostgreSQL URI string.
21
- - A pg `PoolConfig` object; an explicit `{}` uses the driver's environment defaults.
22
- - An existing `pg.Pool`, borrowed by identity.
23
-
24
- ```ts
25
- const db = createTenon({
26
- connection: {
27
- connectionString: connection,
28
- max: 10,
29
- idleTimeoutMillis: 30_000,
30
- connectionTimeoutMillis: 30_000,
31
- options: '-c search_path=app,public',
32
- },
33
- });
12
+ ```text
13
+ createTenon({ connection, onQuery? }): Database
34
14
  ```
35
15
 
36
- Strings become pg's `connectionString`; configuration objects pass to `new Pool(config)`.
37
- `ConnectionOptions` is an alias for pg's `PoolConfig`. Driver options and defaults are unchanged,
38
- including no acquisition timeout unless `connectionTimeoutMillis` is configured. Pool timeouts use
39
- milliseconds. pg parses the URI for connection fields; matching URI values override client
40
- configuration, including an `options` URI parameter overriding the configuration's `options` string.
41
- The pool sizes and timeouts above are application choices. Configure pool policy in `PoolConfig`;
42
- old driver-specific URI settings such as `idle_timeout` do not configure pg's idle timeout. Preserve
43
- existing URI startup options when appending settings such as UTC and `search_path`.
44
- Preserve deployment credentials and TLS requirements; see
45
- [pg connection configuration](https://node-postgres.com/features/connecting).
46
-
47
- Reuse an instance for each database configuration. `db.close({ timeoutMs? })` immediately rejects
48
- new Tenon work. For owned pools, accepted Tenon operations can drain for up to 5,000 ms by default;
49
- at the deadline, remaining operations reject and Tenon requests driver termination. `0` skips the
50
- grace period. The timeout must be an integer from 0 to 2,147,483,647 milliseconds; `CloseOptions` is
51
- exported. This bounds Tenon's wait, not every native transport's teardown or server execution. It
52
- sends no PostgreSQL cancellation request and does not cancel JavaScript callbacks. An unbounded
53
- authentication attempt can keep a connection open after close resolves. Configure pg's
54
- `connectionTimeoutMillis` when connection establishment also needs a deadline.
55
-
56
- The first valid close call chooses the timeout. Concurrent and later calls share the same promise
57
- and outcome. Invalid initial options reject without closing the instance. For borrowed pools, close
58
- only disables the Tenon wrapper; the application retains the pool and outstanding work. Tenon does
59
- not load environment files.
60
-
61
- Qualify SQL names or configure runtime `search_path`. CLI `--namespace` selects tooling scope; it
62
- does not configure application queries. Baseline capture and Describe accept connection strings
63
- rather than the runtime connection union.
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.
64
19
 
65
- ## Native driver access
20
+ The returned `Database` provides:
66
21
 
67
- `db.native` is the actual `pg.Pool`, readonly in TypeScript. Native calls bypass Tenon's result
68
- modes, affected-row guards, SQL checks, and `onQuery`. They follow pg's query configuration and do
69
- not join a Tenon transaction; callback clients expose only `tx.query`.
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 |
70
29
 
71
- Use pg's [native APIs](https://node-postgres.com/apis/pool) and compatible cursor/COPY packages when
72
- needed. For LISTEN, check out a client with `await db.native.connect()`, attach `notification`,
73
- `error`, and `end` listeners, and issue `LISTEN` on that client. Keep it checked out while listening;
74
- release it after `UNLISTEN`. Native queued acquisitions follow pg's shutdown behavior and must be
75
- settled by their caller before closing. A disconnected client does not reconnect. The application owns
76
- resubscription and catch-up; notifications are not replayed.
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.
77
34
 
78
- Close owned pools through `db.close()` for bounded shutdown, including checked-out native clients.
79
- Borrowed pools and their listeners remain usable after wrapper close until their owner ends them.
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.
80
38
 
81
- ### LISTEN lifecycle example
39
+ ### Driver updates
82
40
 
83
- This helper owns a dedicated single-client pool. It returns an intentional-close function only
84
- after LISTEN is ready. Callbacks are synchronous; notification exceptions are isolated, and an
85
- unexpected disconnect invalidates this generation once before notifying its consumer. Failed
86
- connection acquisition or LISTEN setup closes the owned resources and rejects the setup promise.
87
-
88
- ```ts
89
- import { createTenon } from '@kequtech/tenon-orm';
90
-
91
- async function listenForChanges(
92
- connectionString: string,
93
- onChange: (payload: string | undefined) => void,
94
- onDisconnect: (error: Error) => void,
95
- ) {
96
- const db = createTenon({
97
- connection: { connectionString, max: 1, idleTimeoutMillis: 0, connectionTimeoutMillis: 30_000 },
98
- });
99
- const client = await db.native.connect().catch(async (error: unknown) => {
100
- await db.close();
101
- throw error;
102
- });
103
- let active = true;
104
- let closing: Promise<void> | undefined;
105
-
106
- function stop(error?: Error): Promise<void> {
107
- if (closing) return closing;
108
- active = false;
109
- closing = Promise.resolve().then(async () => {
110
- try {
111
- if (error) onDisconnect(error);
112
- } finally {
113
- client.release(true);
114
- await db.close();
115
- }
116
- });
117
- return closing;
118
- }
119
-
120
- const failed = (error: Error) => {
121
- if (active) void stop(error).catch(console.error);
122
- };
123
- client.on('error', failed);
124
- client.on('end', () => failed(new Error('The notification connection ended.')));
125
- client.on('notification', (event) => {
126
- if (!active || event.channel !== 'app_changes') return;
127
- try {
128
- onChange(event.payload);
129
- } catch (error) {
130
- console.error(error);
131
- }
132
- });
133
- try {
134
- await client.query('LISTEN "app_changes"');
135
- if (!active) throw new Error('The notification connection closed during setup.');
136
- } catch (error) {
137
- await stop();
138
- throw error;
139
- }
140
- return () => stop();
141
- }
142
- ```
143
-
144
- Both failure and intentional close discard the dedicated checkout with `release(true)` before
145
- closing its pool, so no UNLISTEN is needed and normal shutdown does not wait for Tenon's deadline.
146
- The active flag suppresses duplicate error/end callbacks and intentional-close notifications.
147
- Create a new helper instance to recover, and reconcile missed events through persistent data.
148
-
149
- This helper's close function is available after readiness. A manager that also supports replacement
150
- or shutdown while setup is pending must assign a generation token before awaiting setup. Only the
151
- current generation may change shared readiness/subscription state or notify its consumers. If stale
152
- setup later succeeds, close that setup's own listener; if it fails or finishes cleanup, leave the
153
- replacement alone. Invalidate the current token before intentional shutdown. Keep those guards in
154
- the application's subscription manager. The fixed channel above is trusted; validate and quote
155
- application-selected channel names before placing them in native SQL.
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.
156
45
 
157
46
  ## Queries and result modes
158
47
 
159
- `sql<Row>` creates a detached `SqlQuery<Row>` with SQL `text` and ordered `values`; it does not
160
- execute. `Row` declares one selected row. Without an annotation, fields are `unknown`. An annotation
161
- 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.
162
51
 
163
- ```ts
164
- const user = await db.query(
165
- sql<{ id: number; email: string }>`SELECT id, email FROM public.users WHERE id = ${42}`,
166
- { returns: 'oneOrNone', label: 'user-by-id' },
167
- );
168
- ```
52
+ `db.query` and `tx.query` accept `{ returns?, affectedRows?, label? }`. Unknown options reject.
53
+ A supplied label must be a nonblank string.
169
54
 
170
- `query` options are `{ returns?, affectedRows?, label? }`; unknown keys fail. A supplied `label`
171
- 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 |
172
61
 
173
- | `returns` | Output | Allowed returned rows |
174
- | ------------------ | -------------------------------- | --------------------- |
175
- | `native` (default) | `QueryResult<Row>` driver object | Any |
176
- | `many` | Plain `Row[]` | Any |
177
- | `oneOrNone` | `Row \| undefined` | Zero or one |
178
- | `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.
179
65
 
180
- Native results expose `rows`, `rowCount`, `command`, and `fields`, following pg's `QueryResult`.
181
- `rowCount` is the command's affected-row count, independent of `RETURNING`; it can be null for
182
- commands without a count. Result modes never add `LIMIT` or `RETURNING`. A returned-row mismatch
183
- 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`.
184
68
 
185
- Functions accepting `QueryClient` can use either `Database` or `Transaction`. Public types also
186
- 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`.
187
72
 
188
73
  ### Adapting transactional results
189
74
 
190
- 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`:
191
76
 
192
77
  ```ts
193
78
  import { unsafeSql, type QueryClient } from '@kequtech/tenon-orm';
194
79
 
195
80
  async function queryRows(tx: QueryClient, text: string, values: readonly unknown[]) {
196
81
  const result = await tx.query(unsafeSql(text, values));
197
- return { rows: result.rows };
82
+ return { rows: [...result.rows] };
198
83
  }
199
84
  ```
200
85
 
201
- Alternatively, request `{ returns: 'many' }` and wrap the returned array in `{ rows }`. Supply the
202
- callback's `tx` when the adapter's work must commit with application writes. A native pool query
203
- uses a separate checkout and does not join that transaction. Await adapter calls before returning
204
- from the callback, retain commit/rollback tests, and make recording helpers return pg-shaped native
205
- 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.
206
94
 
207
95
  ## SQL parameters and identifiers
208
96
 
209
- Ordinary `sql` interpolations bind values. Do not quote them or write manual `$n` placeholders
210
- inside templates. Values follow pg serialization: `null` and `undefined` bind as SQL null, arrays
211
- bind as PostgreSQL arrays, and objects use pg's JSON serialization. For JSON input, use
212
- `JSON.stringify(value)`, including for JSON arrays, strings, and JSON null. Generated JSON write
213
- types accept encoded strings. Cast standalone parameters when SQL must establish their type.
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 |
214
104
 
215
- ```ts
216
- const ids = [1, 2, 3];
217
- const query = sql<{ id: number }>`SELECT id FROM public.users
218
- WHERE id = ANY(${ids}::integer[])`;
219
- ```
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.
220
108
 
221
- `identifier(schema, table)` quotes components separately. Names must contain 1–63 UTF-8 bytes and no
222
- NUL. Choose application-approved names; quoting does not authorize access. SQL syntax such as `DESC`
223
- belongs in fixed query alternatives or fragments, not a string parameter.
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.
224
112
 
225
- `unsafeSql<Row>(text, values)` accepts trusted external SQL and an explicit values array, including
226
- `[]`. Never concatenate request input into its text. Full `sql` and `unsafeSql` queries cannot be
227
- nested; use fragments for partial SQL.
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.
228
116
 
229
- Each Tenon query executes one statement through the extended protocol. Application execution uses
230
- unnamed queries without a `name` or prepared-statement cache. Values remain bound separately.
231
- 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.
232
119
 
233
120
  ## SQL fragments and joins
234
121
 
235
- `sql.fragment` creates immutable partial SQL with bound values. It has no row type, cannot execute
236
- 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.
237
128
 
238
- `sql.join(fragments, separator)` requires a nonempty list. Separators are `'AND'`, `'OR'`, and
239
- `','`. It does not add parentheses or change SQL precedence. Each fragment use receives correctly
240
- ordered parameters, including repeated and nested uses; literal `$1` text in strings/comments is
241
- unchanged.
129
+ See [composition examples](sql.md#compose-sql-fragments), including the empty-filter case.
242
130
 
243
- ```ts
244
- const email: string | undefined = 'reader@example.test';
245
- const conditions = [sql.fragment`active = ${true}`];
246
- if (email !== undefined) conditions.push(sql.fragment`email = ${email}`);
247
- const query = sql<{ id: number }>`SELECT id FROM public.users
248
- 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
249
136
  ```
250
137
 
251
- The mandatory predicate keeps the list nonempty. If every filter is optional, choose a complete
252
- 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.
253
140
 
254
- ## 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 |
255
147
 
256
- `sql.insertValues(input, columns?)` and `sql.assignments(input, columns?)` return fragments. Keep
257
- 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.
258
151
 
259
- ```ts
260
- const query = sql<{ id: number }>`INSERT INTO public.users
261
- ${sql.insertValues({ email: 'reader@example.test' })} RETURNING id`;
262
- await db.query(query, { returns: 'one' });
263
- await db.query(sql`UPDATE public.users SET ${sql.assignments({ active: false })} WHERE id = ${42}`);
264
- ```
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.
265
156
 
266
- Both accept plain objects, including null-prototype objects. Columns are quoted as individual
267
- identifiers. Only own data properties are read; accessors are rejected. Fields are captured at
268
- fragment construction; nested objects retain ordinary driver serialization behavior.
269
-
270
- | Input | Insert | Assignment |
271
- | -------------------------------------------- | ---------------------- | --------------- |
272
- | Missing or `undefined` | `DEFAULT` for the cell | Omitted |
273
- | `null` | Bound SQL null | Bound SQL null |
274
- | Other value | Bound parameter | Bound parameter |
275
- | SQL fragment, identifier, or query as a cell | Rejected | Rejected |
276
-
277
- Inserts accept one object or an array. Without `columns`, they infer the union of enumerable keys
278
- with defined values in first-seen order. Heterogeneous rows stay aligned. A single default-only row
279
- emits `DEFAULT VALUES`. Empty batches fail; multiple default-only rows need explicit columns.
280
- Assignments omit undefined values and fail when no assignments remain, even if surrounding SQL has
281
- other assignments.
282
-
283
- A supplied column list must be nonempty, unique, and valid. It selects and orders fields, ignoring
284
- unlisted keys. Missing insert cells use defaults; missing update fields are omitted. TypeScript
285
- checks selected names against known input keys, but selection can still discard required fields.
286
- Helpers have no schema metadata; PostgreSQL enforces identity, generated-column, and constraint
287
- rules. Use [generated inputs](#insert-and-update-inputs) and application validation.
288
-
289
- Write arithmetic, `column = DEFAULT`, and `excluded.column` in the surrounding SQL. There is no
290
- expression/default sentinel. A complete query accepts at most 65,535 bound parameters, including
291
- 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).
292
161
 
293
162
  ## Transactions, guarded writes, and errors
294
163
 
295
- For an existing user with id 42:
296
-
297
164
  ```ts
298
165
  await db.transaction(async (tx) => {
299
166
  await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${42}`, {
@@ -302,239 +169,167 @@ await db.transaction(async (tx) => {
302
169
  });
303
170
  ```
304
171
 
305
- Use `tx` for every operation that must commit together and await the work. The callback commits on
306
- success, rolls back on an escaping error, and expires before cleanup. Transactions belong to one
307
- database instance. Nested callbacks and automatic retries are not provided. Tenon owns direct
308
- BEGIN/COMMIT/ROLLBACK boundaries.
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.
309
175
 
310
- `affectedRows: { exact: n }` or `{ max: n }` accepts a nonnegative safe integer. It checks the final
311
- INSERT/UPDATE/DELETE/MERGE count independently of `RETURNING`. A guarded root call uses a
312
- transaction; a guarded callback call uses a savepoint. Failed checks and other errors from that
313
- guarded operation roll it back before throwing. A caught guarded failure permits continued work;
314
- 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.
315
180
 
316
- Counts exclude extra trigger/cascade and CTE effects and cannot identify the wrong single row.
317
- Sequence advances and external effects are not reversible. A lost commit acknowledgment can leave
318
- the outcome unknown; applications own retry and idempotency policy.
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`:
319
183
 
320
- Set isolation before data queries with `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`, and use
321
- `SET LOCAL statement_timeout = '5s'` to bound statements in that transaction. Manual savepoints are
322
- supported; reserve names beginning with `tenon_affected_rows_` for Tenon.
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 |
323
188
 
324
- SQL errors retain native driver details. Invalid inputs throw ordinary `Error` objects.
325
- `CardinalityError` has code `TENON_CARDINALITY_MISMATCH`, `expected` (`one` or `oneOrNone`),
326
- `actual` (returned row count), and optional `label`. It does not change transaction or autocommit
327
- behavior. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
328
- `TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`. Work/cleanup failures can
329
- produce `AggregateError`.
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.
330
192
 
331
- Optional `onQuery` observes completed calls with `label`, `durationMs`, `outcome`, and available
332
- `command`, `rowCount`, and `errorCode`. It receives no SQL or values. Validation failures before
333
- execution are not observed. Observers are not awaited; failures emit warnings without changing the
334
- query outcome. Query success inside a callback does not establish outer commit.
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.
335
196
 
336
- ## Generated types and checking
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` |
337
204
 
338
- `generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript declarations from a
339
- consistent read-only snapshot; it does not need migration files or history. The CLI writes them
340
- atomically. Default namespace is `public`; default output is `src/generated-schema.ts`. Both `.ts`
341
- and `.d.ts` outputs are supported. Commit exact output and exclude it from formatting.
205
+ Use codes and structured fields rather than matching message text. Retain unexpected failures.
342
206
 
343
- ```sh
344
- npx tenon generate --out src/generated-schema.d.ts
345
- npx tenon generate --out src/generated-schema.d.ts --check
207
+ ## Generated types and checking
208
+
209
+ ```text
210
+ db.generateTypes({ out, namespace?, check?, onWarning? }): Promise<void>
211
+ generateDefinitions({ connection, namespace?, onWarning? }): Promise<string>
346
212
  ```
347
213
 
348
- `--check` never rewrites the file. Missing/stale output fails with a diagnostic and a short diff for
349
- changes. It compares the generated representation, not pending migrations or every database object.
350
- Output includes relation/constraint/index summaries and referenced domains, not function bodies,
351
- grants, or a complete schema fingerprint.
352
-
353
- **Generated declarations are not a version-stable schema fingerprint.** TypeScript mappings can
354
- change without schema drift. The schema-summary comments describe SQL column types, defaults,
355
- constraints, indexes, and referenced domains independently of those mappings, but their formatting
356
- and PostgreSQL's rendering of definitions are not a versioned comparison contract either. Preserve
357
- reviewed generator-specific references if an existing guard hashes this output; see
358
- [baseline verification](adoption-guide.md#check-and-adopt-an-existing-database).
359
-
360
- Generation covers ordinary tables and partitioned roots. Views, foreign tables, and attached
361
- partitions need query-specific types. A missing namespace fails; an existing empty schema is valid.
362
- Exact table names are available through `Rows['users']`; valid nonreserved identifiers also receive
363
- named aliases. Use `Pick` for projections; a join or aggregate needs its actual selected shape.
364
-
365
- | PostgreSQL type | Read representation | Generated write input |
366
- | ---------------------------------------------- | -------------------- | ------------------------------------- |
367
- | smallint, integer, oid, real, double precision | `number` | `number` |
368
- | bigint | `string` | `string \| bigint` |
369
- | numeric | `string` | `string \| number` |
370
- | boolean | `boolean` | `boolean` |
371
- | text, name, varchar, char, uuid, time, timetz | `string` | `string` |
372
- | date, timestamp, timestamptz | `Date \| number` | `Date \| string` |
373
- | enum | String literal union | Same union; `never` for an empty enum |
374
- | json, jsonb | `unknown` | `string` containing encoded JSON |
375
- | bytea | `Buffer` | `Buffer` |
376
-
377
- Nullable columns add null; `unknown` already includes it. Domains retain verified base mappings and
378
- the column's read nullability. Verified arrays use `PgArray<T> = (T | null | PgArray<T>)[]` for null
379
- elements and multiple dimensions. Numeric arrays contain numbers and can lose precision; select/cast
380
- them as text when exact values are required. Bigint arrays contain strings; enum arrays and arrays
381
- of domains are unverified. Unmapped types warn and use `unknown` reads / `never` non-null writes.
382
- Custom parsers need matching application types.
383
-
384
- Date-only values and timestamps without time zone use Node's local timezone. Infinite temporal reads
385
- are numbers (`Infinity` or `-Infinity`); narrow with `value instanceof Date` before date methods.
386
- Temporal write inputs accept `Date | string`; use PostgreSQL's text `infinity` or `-infinity` when
387
- needed.
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.
388
219
 
389
- ### Persistence and domain values
220
+ ### Application aliases
390
221
 
391
- Generated types describe storage representations. Text `CHECK` constraints do not become unions, and
392
- JSON reads remain unknown. Validate values before narrowing to application domain types.
222
+ Export aliases such as `UserRow = Database['users']['row']` in a handwritten file. See
223
+ [application aliases](schema-types.md#application-aliases).
393
224
 
394
- For example, assume this additional table and regenerate `Rows` from the resulting schema:
225
+ ### Persistence and domain values
395
226
 
396
- ```sql
397
- CREATE TABLE public.sessions (
398
- id text PRIMARY KEY,
399
- data jsonb NOT NULL,
400
- expires_at timestamptz NOT NULL
401
- );
402
- ```
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).
403
229
 
404
- Use the same application validator before encoding a write and when restoring a domain value:
230
+ ### Insert and update inputs
405
231
 
406
- ```ts
407
- import { sql } from '@kequtech/tenon-orm';
408
- import type { Rows } from './generated-schema.js';
409
-
410
- function sessionData(value: unknown): { userId: string } {
411
- if (
412
- typeof value !== 'object' ||
413
- value === null ||
414
- Array.isArray(value) ||
415
- !('userId' in value) ||
416
- typeof value.userId !== 'string' ||
417
- value.userId.length === 0
418
- ) {
419
- throw new Error('Expected session data with a nonempty userId');
420
- }
421
- return { userId: value.userId };
422
- }
232
+ Generated types account for defaults, identity/computed columns, domains, and nullability. See
233
+ [insert/update rules](schema-types.md#insert-and-update-inputs).
423
234
 
424
- function finiteDate(value: Date | number): Date {
425
- if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
426
- throw new Error('Expected a finite database timestamp');
427
- }
428
- return value;
429
- }
235
+ ## Choosing a validation tool
430
236
 
431
- const input = {
432
- id: 'example-session',
433
- data: JSON.stringify(sessionData({ userId: 'user-42' })),
434
- expires_at: new Date(Date.now() + 60_000),
435
- } satisfies Rows.Insert['sessions'];
436
- await db.query(sql`INSERT INTO public.sessions ${sql.insertValues(input)}`);
437
-
438
- const row = await db.query(
439
- sql<Rows['sessions']>`SELECT id, data, expires_at FROM public.sessions
440
- WHERE id = ${input.id} AND isfinite(expires_at) AND expires_at > now()`,
441
- { returns: 'oneOrNone' },
442
- );
443
- const session =
444
- row === undefined
445
- ? undefined
446
- : {
447
- id: row.id,
448
- data: sessionData(row.data),
449
- expiresAt: finiteDate(row.expires_at),
450
- };
451
- ```
237
+ Use the intended development/test schema and role. TypeScript itself needs no connection.
452
238
 
453
- The generated JSON read remains `unknown` and the timestamp remains `Date | number`. The projection
454
- validates both without assertions or edits to generated types. Preserve null separately for nullable
455
- columns. For an authentication policy requiring finite expiry, `isfinite(expires_at)` excludes both
456
- infinities before the expiry comparison; `expires_at > now()` alone would accept positive infinity.
457
- Include such predicates in expiry-sensitive updates/deletes too, and test infinite and expired
458
- values. This example illustrates representation validation; applications must also retain their
459
- token verification, revocation, and authorization checks. See
460
- [JSON and PostgreSQL arrays](mutations.md#json-and-postgresql-arrays) for array write boundaries.
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 |
461
246
 
462
- Bigint and `count(*)` reads are decimal strings. Use SQL or BigInt for exact arithmetic and decimal
463
- strings at JSON boundaries; convert to number only when bounded or approximate values are intended.
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.
464
251
 
465
- ### Insert and update inputs
466
-
467
- ```ts
468
- import type { Rows } from './generated-schema.js';
252
+ ## Source-aware checking
469
253
 
470
- const input: Rows.Insert['users'] = { email: 'reader@example.test' };
471
- const patch: Pick<Rows.Update['users'], 'active'> = { active: false };
472
- sql.insertValues(input);
473
- sql.assignments(patch);
474
- ```
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.
475
256
 
476
- Insert fields are required when nonnullable, writable, and without a default. Other writable insert
477
- fields and all writable update fields are optional and accept explicit undefined, including with
478
- `exactOptionalPropertyTypes`. Explicit null requires both column and domain-chain permission. Domain
479
- defaults count; column defaults take precedence.
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.
480
261
 
481
- Computed and `GENERATED ALWAYS` identity columns use optional `never` fields. Serial and
482
- `GENERATED BY DEFAULT` identities are optional on insert and writable on update; other primary keys
483
- remain writable. Use `Pick`/`Omit` for application restrictions. Apply the generated types through
484
- annotations, `satisfies`, or helper generic arguments; they do not validate constraints, JSON
485
- structure, authorization, or nonempty input.
262
+ ## Query inspection
486
263
 
487
- For unverified write mappings, provide a reviewed application-owned replacement, for example
488
- `Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for interval text input. Selected columns
489
- can discard required fields; see [the cookbook](mutations.md#input-types-and-runtime-validation).
264
+ ```text
265
+ describeQuery({ connection, query, session?, timeouts?, codecs? }): Promise<QueryDescription>
266
+ ```
490
267
 
491
- ## Query inspection
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.
492
272
 
493
- `describeQuery({ connection, query, session?, timeouts? })` accepts a PostgreSQL URI and a finished
494
- SQL string or `{ text }` object. It returns PostgreSQL metadata, conservative read mappings and row
495
- declaration, session context, and coded findings. It does not bind values, execute the statement, or
496
- verify `sql<Row>`.
273
+ ## Native driver access
497
274
 
498
- Inspection owns a separate session with bounded timeouts. Match runtime settings; temporary tables,
499
- and uncommitted changes are not inherited. Reported mappings assume pg's default parsers; parser
500
- overrides need matching application types. Nullability stays unknown. See
501
- [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.
279
+
280
+ ## Troubleshooting
281
+
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.
502
300
 
503
301
  ## Command reference
504
302
 
505
- Database commands require `DATABASE_URL`. `new` and `--help` work offline. No environment files are
506
- loaded. Paths are relative to the working directory. Explicit flags unsupported by a command are
507
- rejected; `--check` is only a generation check, not 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.
508
305
 
509
306
  ```text
510
307
  tenon new <name> [--directory migrations]
511
308
  tenon migrate [--directory migrations] [--namespace public]
512
309
  tenon status [--directory migrations] [--namespace public]
513
310
  tenon rollback [--directory migrations] [--namespace public] [--steps 1]
514
- tenon generate [--namespace public] [--out src/generated-schema.ts] [--check]
515
- tenon describe <query.sql> [--role name] [--search-path value] [--time-zone value]
516
- [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
517
311
  tenon baseline capture [--directory migrations] [--namespace public] [--schema name ...]
518
312
  tenon baseline adopt [--directory migrations] [--namespace public]
313
+ tenon check [file.ts[:line]] [--project tsconfig.json] [--json]
314
+ [--role name] [--search-path value] [--time-zone value]
315
+ [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
316
+ tenon describe <query.sql> [--role name] [--search-path value] [--time-zone value]
317
+ [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
519
318
  ```
520
319
 
521
- `new` scaffolds empty `<numeric-id>_<name>.up.sql` and `.down.sql` files. Names contain letters,
522
- digits, underscores, or hyphens. Fill both files or remove the down file for an irreversible change.
523
- IDs sort numerically. Never edit applied files: checksums cover up and down SQL.
524
-
525
- Each migration and history entry commits together; earlier files remain applied if a later one
526
- fails. Runner search path is the selected namespace followed by `pg_catalog`. `status` returns
527
- applied/pending IDs as JSON and rejects history conflicts. `rollback` validates that every selected
528
- file has down SQL before executing any; `steps` is a positive safe integer. Use dollar-quoted
529
- function bodies and explicit `E'...'` backslash escapes. Nontransactional DDL such as
530
- `CREATE INDEX CONCURRENTLY` requires a separate procedure.
531
-
532
- Migration/status/rollback/adoption use session advisory locks per database and namespace; generation
533
- uses the same lock in shared mode. Use direct or session-pooled connections and server timeouts to
534
- bound waits. Baseline capture uses a local creation lock and native `pg_dump`; it does not acquire
535
- the runner's database advisory lock. See [adoption](adoption-guide.md).
536
-
537
- Programmatic `migrate`, `migrationStatus`, and `rollback` accept
538
- `{ connection, directory?, namespace? }`; rollback also accepts `steps`. Migration/rollback return
539
- completed IDs. `createMigration({ name, directory? })` returns `{ id, upPath, downPath }`;
540
- `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).