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