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