@kequtech/tenon-orm 0.2.1 → 0.3.0

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