@kequtech/tenon-orm 0.3.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/README.md +81 -26
- package/dist/affected-rows.js +0 -1
- package/dist/baseline.js +3 -5
- package/dist/check/contracts.d.ts +6 -0
- package/dist/check/contracts.js +206 -0
- package/dist/check/project.d.ts +18 -0
- package/dist/check/project.js +224 -0
- package/dist/check/source.d.ts +51 -0
- package/dist/check/source.js +629 -0
- package/dist/check/types.d.ts +36 -0
- package/dist/check/types.js +1 -0
- package/dist/check.d.ts +4 -0
- package/dist/check.js +147 -0
- package/dist/cli.js +40 -27
- package/dist/connection.d.ts +5 -12
- package/dist/connection.js +12 -121
- package/dist/database.js +10 -8
- package/dist/describe.js +10 -3
- package/dist/generate.d.ts +13 -3
- package/dist/generate.js +52 -25
- package/dist/generation/catalog.d.ts +5 -6
- package/dist/generation/catalog.js +7 -9
- package/dist/generation/output.js +2 -4
- package/dist/generation/render.d.ts +3 -0
- package/dist/generation/render.js +60 -0
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/inspection/catalog.d.ts +3 -6
- package/dist/inspection/catalog.js +9 -18
- package/dist/inspection/cli.d.ts +12 -0
- package/dist/inspection/cli.js +9 -4
- package/dist/inspection/connection.d.ts +2 -3
- package/dist/inspection/connection.js +20 -22
- package/dist/inspection/options.js +0 -6
- package/dist/inspection/result.d.ts +3 -2
- package/dist/inspection/result.js +13 -26
- package/dist/inspection/types.d.ts +6 -4
- package/dist/migrations/connection.d.ts +3 -3
- package/dist/migrations/connection.js +14 -21
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -9
- package/dist/query-options.d.ts +1 -1
- package/dist/query.js +1 -1
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +21 -77
- package/dist/types.d.ts +4 -1
- package/docs/adoption-guide.md +88 -111
- package/docs/agent-guide.md +94 -147
- package/docs/connections.md +185 -0
- package/docs/human-guide.md +133 -146
- package/docs/migrations.md +133 -0
- package/docs/mutations.md +110 -60
- package/docs/query-checking.md +199 -0
- package/docs/query-inspection.md +89 -58
- package/docs/reference.md +232 -437
- package/docs/schema-types.md +263 -0
- package/docs/sql.md +212 -0
- package/package.json +11 -7
- package/dist/driver/types.d.ts +0 -10
- package/dist/driver/types.js +0 -49
- package/dist/generation/table-types.d.ts +0 -2
- package/dist/generation/table-types.js +0 -96
- package/dist/inspection/protocol.d.ts +0 -7
- package/dist/inspection/protocol.js +0 -45
package/docs/human-guide.md
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Getting started
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This guide creates a table, inserts and reads a user, reuses a query in a transaction, and generates
|
|
4
|
+
TypeScript types. You need an existing PostgreSQL database. If your application already has tables
|
|
5
|
+
and a migration runner, keep them and start at [Write and read a row](#write-and-read-a-row).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
replacing an existing ORM. Code blocks build on the connection and table introduced below;
|
|
9
|
-
query-function examples belong in their own module.
|
|
7
|
+
## Install and connect
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
In an existing Node.js 24+ TypeScript project using ESM:
|
|
9
|
+
Use Node.js 24+, PostgreSQL 16+, ESM, and TypeScript 6+ with Node.js development typings. In an
|
|
10
|
+
existing TypeScript project:
|
|
14
11
|
|
|
15
12
|
```sh
|
|
16
13
|
npm install @kequtech/tenon-orm
|
|
17
14
|
```
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
Keep the project's TypeScript configuration; a typical Node.js application uses `module: "NodeNext"`
|
|
17
|
+
and `strict: true`. Tenon includes its PostgreSQL driver, pgclear.
|
|
18
|
+
|
|
19
|
+
Supply `DATABASE_URL` through your environment setup. For a local example database:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
export DATABASE_URL=postgresql://postgres:postgres@localhost:5432/your_database
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Tenon does not create the database or load `.env` files. Create one shared database module:
|
|
22
26
|
|
|
23
27
|
```ts
|
|
24
28
|
// src/db.ts
|
|
@@ -29,19 +33,18 @@ if (!connection) throw new Error('DATABASE_URL is required');
|
|
|
29
33
|
export const db = createTenon({ connection });
|
|
30
34
|
```
|
|
31
35
|
|
|
32
|
-
Reuse
|
|
33
|
-
|
|
36
|
+
Reuse `db` across requests. Call `await db.close()` when a standalone script finishes or a server
|
|
37
|
+
shuts down. See [connections](connections.md) for pool options and application-owned pools.
|
|
34
38
|
|
|
35
|
-
## Create a
|
|
39
|
+
## Create and apply a migration
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
migration:
|
|
41
|
+
In a fresh migrations directory:
|
|
39
42
|
|
|
40
43
|
```sh
|
|
41
44
|
npx tenon new create_users
|
|
42
45
|
```
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
This creates two empty files. Put the table definition in the up file:
|
|
45
48
|
|
|
46
49
|
```sql
|
|
47
50
|
-- migrations/0001_create_users.up.sql
|
|
@@ -52,82 +55,74 @@ CREATE TABLE public.users (
|
|
|
52
55
|
);
|
|
53
56
|
```
|
|
54
57
|
|
|
58
|
+
Put the reverse operation in the down file:
|
|
59
|
+
|
|
55
60
|
```sql
|
|
56
61
|
-- migrations/0001_create_users.down.sql
|
|
57
62
|
DROP TABLE public.users;
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```json
|
|
63
|
-
{
|
|
64
|
-
"scripts": {
|
|
65
|
-
"db:migrate": "tenon migrate && tenon generate --out src/generated-schema.d.ts"
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
```
|
|
65
|
+
Apply it to the database selected by `DATABASE_URL`:
|
|
69
66
|
|
|
70
67
|
```sh
|
|
71
|
-
|
|
68
|
+
npx tenon migrate
|
|
69
|
+
npx tenon status
|
|
72
70
|
```
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
If a change cannot be reversed, omit its down file. A down migration is not a backup.
|
|
72
|
+
For later changes, create a new migration. Applied files are checked against recorded checksums and
|
|
73
|
+
must stay unchanged. If a migration cannot be reversed, remove its down file before applying it.
|
|
74
|
+
See [migrations](migrations.md) for rollback and deployment behavior.
|
|
78
75
|
|
|
79
|
-
## Write a
|
|
80
|
-
|
|
81
|
-
Create a row, then read the fields you need:
|
|
76
|
+
## Write and read a row
|
|
82
77
|
|
|
83
78
|
```ts
|
|
79
|
+
// src/example.ts
|
|
84
80
|
import { sql } from '@kequtech/tenon-orm';
|
|
85
81
|
import { db } from './db.js';
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
83
|
+
try {
|
|
84
|
+
const email = 'reader@example.test';
|
|
85
|
+
const created = await db.query(
|
|
86
|
+
sql<{ id: number }>`INSERT INTO public.users (email) VALUES (${email}) RETURNING id`,
|
|
87
|
+
{ returns: 'one' },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const user = await db.query(
|
|
91
|
+
sql<{ id: number; email: string; active: boolean }>`
|
|
92
|
+
SELECT id, email, active FROM public.users WHERE id = ${created.id}
|
|
93
|
+
`,
|
|
94
|
+
{ returns: 'oneOrNone' },
|
|
95
|
+
);
|
|
96
|
+
console.log(user);
|
|
97
|
+
} finally {
|
|
98
|
+
await db.close();
|
|
99
|
+
}
|
|
95
100
|
```
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
The type describes one selected row. `returns: 'oneOrNone'` gives you that row or `undefined`; it
|
|
101
|
-
throws if the query returns more than one row. Use `'one'` when absence should also be an error,
|
|
102
|
-
`'many'` for an array, or omit `returns` for the native result object with `.rows`, `.rowCount`, and
|
|
103
|
-
`.fields`. These options do not change your SQL.
|
|
104
|
-
|
|
105
|
-
Generated table types can help describe projections without repeating every field:
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
import type { Rows } from './generated-schema.js';
|
|
109
|
-
type UserSummary = Pick<Rows['users'], 'id' | 'email'>;
|
|
110
|
-
```
|
|
102
|
+
This inserts a new email; rerunning it with the same email will fail the unique constraint. Run it
|
|
103
|
+
through your project's TypeScript runner or compile it with your application. The `.js` imports
|
|
104
|
+
follow NodeNext's convention for compiled TypeScript modules.
|
|
111
105
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
106
|
+
The template binds `${email}` separately from the SQL. Do not put quotes around an interpolation.
|
|
107
|
+
`sql<{ id: number }>` describes one returned row; `RETURNING id` asks PostgreSQL to return it.
|
|
108
|
+
`returns: 'one'` requires exactly one row, while `oneOrNone` permits absence as `undefined`.
|
|
109
|
+
Use `many` for an array or omit `returns` for the driver result with `.rows` and `.rowCount`.
|
|
116
110
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
The annotation does not check SQL or validate the row at runtime. Without an annotation, field values
|
|
112
|
+
are `unknown`. Bigint columns and SQL `count(*)` return strings by default; the integer ID in this
|
|
113
|
+
example returns a number. See [value mappings](schema-types.md#value-mappings) for JSON, arrays,
|
|
114
|
+
decimals, and dates.
|
|
120
115
|
|
|
121
|
-
## Reuse a query
|
|
116
|
+
## Reuse a query
|
|
122
117
|
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
Put reusable queries in ordinary functions. Accept `QueryClient` when a caller may supply either
|
|
119
|
+
`db` or a transaction's `tx`:
|
|
125
120
|
|
|
126
121
|
```ts
|
|
122
|
+
// src/queries.ts
|
|
127
123
|
import { sql, type QueryClient } from '@kequtech/tenon-orm';
|
|
128
|
-
import type { Rows } from './generated-schema.js';
|
|
129
124
|
|
|
130
|
-
type UserSummary =
|
|
125
|
+
type UserSummary = { id: number; email: string };
|
|
131
126
|
|
|
132
127
|
export function userByEmail(client: QueryClient, { email }: { email: string }) {
|
|
133
128
|
return client.query(sql<UserSummary>`SELECT id, email FROM public.users WHERE email = ${email}`, {
|
|
@@ -136,12 +131,15 @@ export function userByEmail(client: QueryClient, { email }: { email: string }) {
|
|
|
136
131
|
}
|
|
137
132
|
```
|
|
138
133
|
|
|
139
|
-
Call `userByEmail(db, { email: 'reader@example.test' })
|
|
140
|
-
|
|
134
|
+
Call `userByEmail(db, { email: 'reader@example.test' })`. Keep the SQL and result options together in
|
|
135
|
+
the function. Joins and aggregates work the same way; see [writing queries](sql.md).
|
|
141
136
|
|
|
142
|
-
##
|
|
137
|
+
## Use a transaction and a write guard
|
|
138
|
+
|
|
139
|
+
In code using the shared `db`:
|
|
143
140
|
|
|
144
141
|
```ts
|
|
142
|
+
import { sql } from '@kequtech/tenon-orm';
|
|
145
143
|
import { userByEmail } from './queries.js';
|
|
146
144
|
|
|
147
145
|
await db.transaction(async (tx) => {
|
|
@@ -154,97 +152,86 @@ await db.transaction(async (tx) => {
|
|
|
154
152
|
});
|
|
155
153
|
```
|
|
156
154
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
`affectedRows: { exact: 1 }` requires exactly one affected row. `{ max: 1 }` permits zero or one.
|
|
161
|
-
Tenon rolls back a write that violates the bound. You can also use these guards on `db.query`
|
|
162
|
-
outside a callback transaction. The guard checks a count; you still need the correct `WHERE` clause
|
|
163
|
-
and application authorization. Checking returned rows with `returns` alone does not undo an
|
|
164
|
-
autocommitted write.
|
|
155
|
+
Run all participating queries through `tx` and await them. The callback commits on success and rolls
|
|
156
|
+
back when an error escapes.
|
|
165
157
|
|
|
166
|
-
|
|
158
|
+
The guard requires exactly one affected row and rolls back the update if the count differs. Use
|
|
159
|
+
`{ max: 1 }` to allow zero or one. Guards also work with `db.query` outside a callback. Result modes
|
|
160
|
+
such as `one` check returned rows; by themselves they do not undo an autocommitted write.
|
|
167
161
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
aligned, with `DEFAULT` for missing cells.
|
|
162
|
+
You still author the `WHERE` clause and application authorization. The
|
|
163
|
+
[transaction reference](reference.md#transactions-guarded-writes-and-errors) covers caught errors,
|
|
164
|
+
savepoints, and retry limits. For object-based inserts and patches, see [writing data](mutations.md).
|
|
172
165
|
|
|
173
|
-
|
|
174
|
-
import type { Rows } from './generated-schema.js';
|
|
166
|
+
## Generate types
|
|
175
167
|
|
|
176
|
-
|
|
177
|
-
|
|
168
|
+
Generation is optional. It reads the migrated database and writes table read/insert/update types.
|
|
169
|
+
Run it through the configured instance so the types use the same value conversions as your queries:
|
|
178
170
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
171
|
+
```ts
|
|
172
|
+
// scripts/generate-types.ts
|
|
173
|
+
import { parseArgs } from 'node:util';
|
|
174
|
+
import { db } from '../src/db.ts';
|
|
175
|
+
|
|
176
|
+
const { values } = parseArgs({ options: { check: { type: 'boolean', default: false } } });
|
|
177
|
+
try {
|
|
178
|
+
await db.generateTypes({ out: 'src/generated-schema.d.ts', check: values.check });
|
|
179
|
+
} finally {
|
|
180
|
+
await db.close();
|
|
181
|
+
}
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
`
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
This script runs directly on Node.js 24+ using its TypeScript support. For this execution path,
|
|
185
|
+
`src/db.ts` must be runnable without build-time transformations or extension rewriting. If your
|
|
186
|
+
project compiles scripts, use its normal runner and import conventions instead. To typecheck `.ts`
|
|
187
|
+
imports with `noEmit`, enable `allowImportingTsExtensions`; emitting projects can use
|
|
188
|
+
`rewriteRelativeImportExtensions`.
|
|
188
189
|
|
|
189
|
-
|
|
190
|
+
Add these scripts to your existing `package.json`:
|
|
190
191
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
## Reuse or assemble parts of SQL when needed
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"scripts": {
|
|
195
|
+
"db:generate": "node scripts/generate-types.ts",
|
|
196
|
+
"db:check-types": "npm run db:generate -- --check",
|
|
197
|
+
"db:migrate": "tenon migrate && npm run db:generate"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
queries share a condition. For example, a search can use the same predicate for its result page and
|
|
204
|
-
its count:
|
|
202
|
+
Run `npm run db:generate` now. The output exports a `Database` type:
|
|
205
203
|
|
|
206
204
|
```ts
|
|
207
|
-
|
|
208
|
-
const limit = 20;
|
|
209
|
-
const search = sql.fragment`active = ${true} AND email ILIKE ${pattern}`;
|
|
210
|
-
const page = sql<UserSummary>`SELECT id, email FROM public.users
|
|
211
|
-
WHERE ${search} ORDER BY id LIMIT ${limit}`;
|
|
212
|
-
const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
|
|
213
|
-
WHERE ${search}`;
|
|
214
|
-
```
|
|
205
|
+
import type { Database } from './generated-schema.js';
|
|
215
206
|
|
|
216
|
-
|
|
217
|
-
|
|
207
|
+
type UserRow = Database['users']['row'];
|
|
208
|
+
type UserSummary = Pick<UserRow, 'id' | 'email'>;
|
|
209
|
+
type UserInsert = Database['users']['insert'];
|
|
210
|
+
type UserUpdate = Database['users']['update'];
|
|
211
|
+
```
|
|
218
212
|
|
|
219
|
-
Use `
|
|
220
|
-
|
|
221
|
-
|
|
213
|
+
Use `UserSummary` with `sql<UserSummary>` or `UserInsert` to type an insert input. These types
|
|
214
|
+
follow table columns, defaults, nullability, and codec representations. They do not validate
|
|
215
|
+
application inputs. Keep aliases in handwritten files and leave generated output unchanged.
|
|
216
|
+
See [schema types](schema-types.md) for an output example and mapping details.
|
|
222
217
|
|
|
223
|
-
|
|
224
|
-
const terms = ['reader', 'example'];
|
|
225
|
-
const filters = [sql.fragment`active = ${true}`];
|
|
226
|
-
for (const term of terms) {
|
|
227
|
-
filters.push(sql.fragment`email ILIKE ${'%' + term + '%'}`);
|
|
228
|
-
}
|
|
218
|
+
## Keep schema and code in step
|
|
229
219
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
```
|
|
220
|
+
During development, run `npm run db:migrate`, review the generated diff, and commit migrations and
|
|
221
|
+
generated declarations together. Exclude the generated file from automatic formatting.
|
|
233
222
|
|
|
234
|
-
|
|
235
|
-
applies. For an entirely optional set of filters, omit the whole `WHERE` clause when the list is
|
|
236
|
-
empty:
|
|
223
|
+
In CI, migrate a dedicated test database and check committed output without rewriting it:
|
|
237
224
|
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
: sql<UserSummary>`SELECT id, email FROM public.users ORDER BY id`;
|
|
225
|
+
```sh
|
|
226
|
+
npx tenon migrate
|
|
227
|
+
npm run db:check-types
|
|
228
|
+
npx tsc --noEmit
|
|
243
229
|
```
|
|
244
230
|
|
|
245
|
-
`
|
|
246
|
-
|
|
247
|
-
when mixing `AND` and `OR`. See the [composition reference](reference.md#sql-fragments-and-joins).
|
|
231
|
+
Do not run the regenerating `db:migrate` script before the freshness check: it would replace the file
|
|
232
|
+
you meant to verify. Production runs migrations and deploys the already-built application.
|
|
248
233
|
|
|
249
|
-
|
|
250
|
-
|
|
234
|
+
Optionally run `npx tenon check src/queries.ts` against the migrated database to compare source query
|
|
235
|
+
types with PostgreSQL metadata. Keep database tests for rows, permissions, and rollback. See
|
|
236
|
+
[choosing a validation tool](reference.md#choosing-a-validation-tool) and
|
|
237
|
+
[troubleshooting](reference.md#troubleshooting) as needed.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# SQL migrations
|
|
2
|
+
|
|
3
|
+
Tenon's optional runner applies numbered SQL files and records their checksums in the database. It
|
|
4
|
+
can be used independently of query execution and type generation. Keep an existing runner unless
|
|
5
|
+
you intend to [transfer migration ownership](adoption-guide.md#transfer-migration-ownership).
|
|
6
|
+
|
|
7
|
+
## Create and apply a change
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx tenon new add_user_display_name
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`new` works offline. It creates the next numeric pair in `migrations/`, starting at `0001` in an empty
|
|
14
|
+
directory. For a directory whose latest migration is `0001`:
|
|
15
|
+
|
|
16
|
+
```sql
|
|
17
|
+
-- migrations/0002_add_user_display_name.up.sql
|
|
18
|
+
ALTER TABLE public.users ADD COLUMN display_name text;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```sql
|
|
22
|
+
-- migrations/0002_add_user_display_name.down.sql
|
|
23
|
+
ALTER TABLE public.users DROP COLUMN display_name;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Set `DATABASE_URL` through your environment, then run:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npx tenon status
|
|
30
|
+
npx tenon migrate
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`status` prints JSON with `applied` and `pending` ID arrays. `migrate` applies every pending up file
|
|
34
|
+
in numeric order and reports completed IDs. No pending files means nothing changes. No command
|
|
35
|
+
loads `.env` automatically.
|
|
36
|
+
|
|
37
|
+
After migrating a development database, [generate types](schema-types.md#generate-and-check-a-file),
|
|
38
|
+
review the diff, and commit migrations with generated declarations. Production applies migrations
|
|
39
|
+
and deploys the already-built application. Use a dedicated migrated database for tests.
|
|
40
|
+
|
|
41
|
+
## File rules and history
|
|
42
|
+
|
|
43
|
+
Files use `<numeric-id>_<name>.up.sql` and optional matching `.down.sql`. Names contain letters,
|
|
44
|
+
digits, underscores, or hyphens. Each ID has one name and an up file. SQL files with invalid names,
|
|
45
|
+
empty contents, duplicate numeric IDs, or down files without an up file fail validation.
|
|
46
|
+
|
|
47
|
+
The runner records ID, name, checksum, and application time in `<namespace>.__tenon_migrations`.
|
|
48
|
+
Checksums cover both up SQL and the presence/content of down SQL. Never edit, rename, remove, or
|
|
49
|
+
add a down file to an already-applied migration. Add a new forward migration instead. History must
|
|
50
|
+
match the beginning of the ordered file list; inserting an older migration also causes a conflict.
|
|
51
|
+
|
|
52
|
+
Migrations may contain multiple statements. Tenon owns the transaction, so authored files must not
|
|
53
|
+
contain transaction-control statements. Use dollar-quoted bodies for functions and explicit `E'...'`
|
|
54
|
+
for backslash escapes. A scaffold's files are empty until you fill them; both are checked even if
|
|
55
|
+
only the up direction is about to run.
|
|
56
|
+
|
|
57
|
+
## Transaction and failure behavior
|
|
58
|
+
|
|
59
|
+
Each migration file and its history entry commit together. The whole run is not one transaction:
|
|
60
|
+
if file three fails, files one and two may already have committed. Correct the cause, inspect
|
|
61
|
+
`status`, and rerun. The failed file is rolled back and remains pending. Restore original applied
|
|
62
|
+
files if history validation reports a conflict.
|
|
63
|
+
|
|
64
|
+
SQL that cannot run in a transaction, such as `CREATE INDEX CONCURRENTLY`, needs a separate,
|
|
65
|
+
application-owned procedure. Tenon has no nontransactional migration flag. It also does not infer
|
|
66
|
+
schema changes or generate migration SQL from TypeScript.
|
|
67
|
+
|
|
68
|
+
## Roll back
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npx tenon rollback
|
|
72
|
+
npx tenon rollback --steps 2
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Rollback runs the most recent down files in reverse order and removes their history records. It
|
|
76
|
+
prechecks that every selected migration has down SQL before executing any. `steps` must be a
|
|
77
|
+
positive safe integer; requesting more steps than exist selects all applied migrations.
|
|
78
|
+
|
|
79
|
+
For an irreversible change, omit the down file before first application. The missing file prevents
|
|
80
|
+
rollback across that migration. A down migration is not a backup: dropped data and sequence changes
|
|
81
|
+
may not be recoverable. Use the application's backup and recovery procedure when needed.
|
|
82
|
+
|
|
83
|
+
## Directory, schema, and locking
|
|
84
|
+
|
|
85
|
+
Defaults are `--directory migrations` and `--namespace public`. For a custom schema, use the same
|
|
86
|
+
options consistently:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npx tenon new add_setting --directory db/migrations
|
|
90
|
+
npx tenon migrate --directory db/migrations --namespace app
|
|
91
|
+
npx tenon status --directory db/migrations --namespace app
|
|
92
|
+
npx tenon rollback --directory db/migrations --namespace app --steps 1
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The namespace selects history location, migration search path, and lock scope. During a file,
|
|
96
|
+
`search_path` is `<namespace>, pg_catalog`; qualify cross-schema names explicitly. The runner creates
|
|
97
|
+
the namespace when applying pending migrations. It does not configure runtime application queries.
|
|
98
|
+
Pass the matching namespace to type generation when generating that schema.
|
|
99
|
+
|
|
100
|
+
Migration and adoption operations use an exclusive session advisory lock for this database and
|
|
101
|
+
namespace. Status and generation use a shared lock. Different namespaces have independent lock
|
|
102
|
+
scopes. These locks do not coordinate with a legacy runner or arbitrary DDL.
|
|
103
|
+
|
|
104
|
+
Use direct or session-pooled connections for these tools; transaction pooling does not preserve
|
|
105
|
+
their session locks. Configure server timeouts on the tooling connection to bound waits. Migration
|
|
106
|
+
commands do not accept Describe's timeout flags.
|
|
107
|
+
|
|
108
|
+
Offline file creation uses `.tenon-create.lock` in the directory. Remove a stale lock only after
|
|
109
|
+
confirming that the process which created it has stopped. Baseline capture uses this local lock
|
|
110
|
+
and `pg_dump`; it does not take the runner's database advisory lock.
|
|
111
|
+
|
|
112
|
+
## Existing databases
|
|
113
|
+
|
|
114
|
+
An existing schema needs no baseline merely to execute queries or generate types. A baseline is
|
|
115
|
+
needed when Tenon takes over migration history: capture one reviewed schema at the old runner's
|
|
116
|
+
final checkpoint, record it on existing databases without execution, and execute it on fresh ones.
|
|
117
|
+
Follow [the adoption workflow](adoption-guide.md#transfer-migration-ownership).
|
|
118
|
+
|
|
119
|
+
## Programmatic API
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { migrate, migrationStatus, rollback } from '@kequtech/tenon-orm';
|
|
123
|
+
|
|
124
|
+
const options = { connection, directory: 'migrations', namespace: 'public' };
|
|
125
|
+
const before = await migrationStatus(options); // { applied, pending }
|
|
126
|
+
const applied = await migrate(options); // readonly string[] of completed IDs
|
|
127
|
+
const reverted = await rollback({ ...options, steps: 1 });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`connection` accepts the same URI/options/pool forms as `createTenon`. A supplied pool retains
|
|
131
|
+
application ownership. `createMigration({ name, directory? })` returns `{ id, upPath, downPath }`;
|
|
132
|
+
`readMigrations(directory?)` returns validated `{ id, name, up, down?, checksum }` records.
|
|
133
|
+
See [the command reference](reference.md#command-reference) for all CLI forms.
|