@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.
- package/CHANGELOG.md +52 -36
- package/README.md +27 -70
- package/dist/affected-rows.js +6 -6
- package/dist/baseline.js +4 -4
- package/dist/client.js +1 -1
- package/dist/connection.d.ts +15 -16
- package/dist/connection.js +124 -14
- package/dist/database.js +36 -17
- package/dist/driver/types.d.ts +1 -1
- package/dist/driver/types.js +11 -11
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -0
- package/dist/generate.js +7 -7
- package/dist/generation/catalog.d.ts +2 -2
- package/dist/generation/catalog.js +4 -4
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +11 -11
- package/dist/generation/table-types.js +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -1
- package/dist/inspection/catalog.d.ts +4 -3
- package/dist/inspection/catalog.js +13 -10
- package/dist/inspection/cli.js +3 -3
- package/dist/inspection/connection.d.ts +2 -2
- package/dist/inspection/connection.js +22 -22
- package/dist/inspection/options.d.ts +3 -3
- package/dist/inspection/options.js +9 -1
- package/dist/inspection/protocol.d.ts +7 -0
- package/dist/inspection/protocol.js +45 -0
- package/dist/inspection/result.d.ts +1 -1
- package/dist/inspection/result.js +6 -6
- package/dist/inspection/types.d.ts +10 -10
- package/dist/migrations/connection.d.ts +3 -2
- package/dist/migrations/connection.js +18 -20
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -7
- package/dist/query-options.js +5 -5
- package/dist/query.js +2 -2
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +19 -14
- package/dist/types.d.ts +7 -3
- package/docs/adoption-guide.md +109 -122
- package/docs/agent-guide.md +124 -228
- package/docs/human-guide.md +44 -55
- package/docs/mutations.md +58 -76
- package/docs/query-inspection.md +97 -89
- package/docs/reference.md +264 -319
- package/package.json +4 -4
- package/dist/driver/array.d.ts +0 -5
- package/dist/driver/array.js +0 -70
- package/dist/driver/lifecycle.d.ts +0 -15
- package/dist/driver/lifecycle.js +0 -57
- package/dist/driver/session.d.ts +0 -8
- package/dist/driver/session.js +0 -92
package/docs/reference.md
CHANGED
|
@@ -1,420 +1,365 @@
|
|
|
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; 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
|
-
|
|
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 `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`,
|
|
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
|
-
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({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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()`
|
|
58
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
label: '
|
|
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
|
-
`
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
119
|
-
NUL
|
|
120
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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.
|
|
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
|
|
164
|
-
operation, table,
|
|
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
|
|
168
|
-
sql
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
|
192
|
-
|
|
|
193
|
-
|
|
|
194
|
-
|
|
|
195
|
-
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
404
|
-
tenon
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
414
|
-
session
|
|
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? })`
|
|
419
|
-
validated SQL and checksums.
|
|
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.
|