@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +81 -26
  3. package/dist/affected-rows.js +0 -1
  4. package/dist/baseline.js +3 -5
  5. package/dist/check/contracts.d.ts +6 -0
  6. package/dist/check/contracts.js +206 -0
  7. package/dist/check/project.d.ts +18 -0
  8. package/dist/check/project.js +224 -0
  9. package/dist/check/source.d.ts +51 -0
  10. package/dist/check/source.js +629 -0
  11. package/dist/check/types.d.ts +36 -0
  12. package/dist/check/types.js +1 -0
  13. package/dist/check.d.ts +4 -0
  14. package/dist/check.js +147 -0
  15. package/dist/cli.js +40 -27
  16. package/dist/connection.d.ts +5 -12
  17. package/dist/connection.js +12 -121
  18. package/dist/database.js +10 -8
  19. package/dist/describe.js +10 -3
  20. package/dist/generate.d.ts +13 -3
  21. package/dist/generate.js +52 -25
  22. package/dist/generation/catalog.d.ts +5 -6
  23. package/dist/generation/catalog.js +7 -9
  24. package/dist/generation/output.js +2 -4
  25. package/dist/generation/render.d.ts +3 -0
  26. package/dist/generation/render.js +60 -0
  27. package/dist/generation/schema-summary.d.ts +3 -3
  28. package/dist/generation/schema-summary.js +2 -2
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +2 -0
  31. package/dist/inspection/catalog.d.ts +3 -6
  32. package/dist/inspection/catalog.js +9 -18
  33. package/dist/inspection/cli.d.ts +12 -0
  34. package/dist/inspection/cli.js +9 -4
  35. package/dist/inspection/connection.d.ts +2 -3
  36. package/dist/inspection/connection.js +20 -22
  37. package/dist/inspection/options.js +0 -6
  38. package/dist/inspection/result.d.ts +3 -2
  39. package/dist/inspection/result.js +13 -26
  40. package/dist/inspection/types.d.ts +6 -4
  41. package/dist/migrations/connection.d.ts +3 -3
  42. package/dist/migrations/connection.js +14 -21
  43. package/dist/migrations/history.d.ts +4 -4
  44. package/dist/migrations/history.js +5 -5
  45. package/dist/migrations.js +7 -9
  46. package/dist/query-options.d.ts +1 -1
  47. package/dist/query.js +1 -1
  48. package/dist/transaction.d.ts +2 -2
  49. package/dist/transaction.js +21 -77
  50. package/dist/types.d.ts +4 -1
  51. package/docs/adoption-guide.md +88 -111
  52. package/docs/agent-guide.md +94 -147
  53. package/docs/connections.md +185 -0
  54. package/docs/human-guide.md +133 -146
  55. package/docs/migrations.md +133 -0
  56. package/docs/mutations.md +110 -60
  57. package/docs/query-checking.md +199 -0
  58. package/docs/query-inspection.md +89 -58
  59. package/docs/reference.md +232 -437
  60. package/docs/schema-types.md +263 -0
  61. package/docs/sql.md +212 -0
  62. package/package.json +11 -7
  63. package/dist/driver/types.d.ts +0 -10
  64. package/dist/driver/types.js +0 -49
  65. package/dist/generation/table-types.d.ts +0 -2
  66. package/dist/generation/table-types.js +0 -96
  67. package/dist/inspection/protocol.d.ts +0 -7
  68. package/dist/inspection/protocol.js +0 -45
@@ -1,24 +1,28 @@
1
- # Using Tenon: a human guide
1
+ # Getting started
2
2
 
3
- Tenon lets you use PostgreSQL directly from TypeScript. You write SQL; Tenon binds its inputs and
4
- executes it through pg. This guide follows a small application from its first table to reusable
5
- queries, transactions, and result checking.
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
- Use the [reference](reference.md) for exact contracts and [adoption](adoption-guide.md) when
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
- ## Connect to your database
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
- Use PostgreSQL 16+ and TypeScript 6+. Keep your existing strict TypeScript configuration and Node
20
- typings. Supply `DATABASE_URL` through your application's environment setup; Tenon does not load
21
- `.env` files.
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 this instance across the application and call `await db.close()` at shutdown. You can also
33
- supply an existing `pg.Pool`; your application then owns its shutdown.
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 table
39
+ ## Create and apply a migration
36
40
 
37
- If you already have a schema and migration runner, keep them. For a fresh example database, create a
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
- Fill the generated files:
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
- For development, add a script to `package.json` that applies migrations and updates generated types:
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
- npm run db:migrate
68
+ npx tenon migrate
69
+ npx tenon status
72
70
  ```
73
71
 
74
- Commit migrations and generated types together. In production, run only `npx tenon migrate`; deploy
75
- the already-built application. If you do not use generated table types, omit generation from the
76
- development script. For later schema changes, add a new migration instead of editing an applied one.
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 query
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
- const email = 'reader@example.test';
88
- await db.query(sql`INSERT INTO public.users (email) VALUES (${email})`);
89
- const user = await db.query(
90
- sql<{ id: number; email: string }>`
91
- SELECT id, email FROM public.users WHERE email = ${email}
92
- `,
93
- { returns: 'oneOrNone' },
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
- `${email}` becomes a bound parameter. Do not put quotes around it: the driver passes the value
98
- separately from the SQL text, even when the value contains SQL.
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
- You can then write `sql<UserSummary>`. A table type describes the full table; a join or aggregate
113
- needs a type for what that particular query selects. An annotation is a declaration, not proof that
114
- the SQL matches. Without one, result fields are `unknown`. Use the inspection and database-test
115
- workflow below to check the declared contract against the query's behavior.
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
- Keep storage types conservative and validate them when projecting domain values. The
118
- [persistence-to-domain example](reference.md#persistence-and-domain-values) shows validated JSON
119
- writes and reads, finite dates, and an SQL predicate that rejects infinite session expiry.
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 with a function
116
+ ## Reuse a query
122
117
 
123
- Use an ordinary function when several callers need the same query. An object input makes the
124
- supplied values easy to read:
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 = Pick<Rows['users'], 'id' | 'email'>;
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' })` at the call site, or pass a transaction's
140
- `tx`. Keep the SQL and execution options together in the function.
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
- ## Change data in a transaction
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
- Use `tx` for all work in the callback and await it. Tenon commits when the callback succeeds and
158
- rolls back when an error escapes.
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
- ## Write objects without building column lists by hand
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
- Use `sql.insertValues(rowOrRows)` inside an authored `INSERT` and `sql.assignments(patch)` after
169
- `SET`. Inserts use database defaults for missing/undefined fields; updates omit those fields.
170
- Explicit `null` stays null. Empty batches and updates throw. Heterogeneous insert rows remain
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
- ```ts
174
- import type { Rows } from './generated-schema.js';
166
+ ## Generate types
175
167
 
176
- const input: Rows.Insert['users'] = { email: 'another@example.test' };
177
- await db.query(sql`INSERT INTO public.users ${sql.insertValues(input, ['email'])}`);
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
- const patch: Pick<Rows.Update['users'], 'active'> = { active: false };
180
- await db.query(sql`UPDATE public.users SET ${sql.assignments(patch, ['active'])}
181
- WHERE email = ${input.email}`);
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
- Use `Pick` to limit editable fields. Keep expressions, predicates, conflict handling, and
185
- `RETURNING` in SQL. Selected columns choose fields; they do not validate the complete object. See
186
- [input types](reference.md#insert-and-update-inputs) for mappings and the
187
- [mutation cookbook](mutations.md) for defaults, batches, and upserts.
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
- ## Check your SQL and result types
190
+ Add these scripts to your existing `package.json`:
190
191
 
191
- Use `describeQuery({ connection, query })` on completed SQL, or `npx tenon describe query.sql` on a
192
- file. It reports PostgreSQL metadata and a conservative `rowType` declaration; it does not update or
193
- verify the application's `sql<Row>` annotation. Inspection uses a separate session, so match the
194
- runtime role and settings. See [query inspection](query-inspection.md) for a worked test.
195
-
196
- After migrating the development/test database, inspect meaningful query variants, run TypeScript,
197
- and test actual rows and effects. Keep those checks in CI. Execution assertions cover values, nulls,
198
- JSON, authorization, and rollback that description alone cannot establish.
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
- A fragment is a piece of SQL that carries its bound values with it. It is useful when multiple
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
- const pattern = '%example%';
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
- Execute each query with `db.query`. Validate application-supplied patterns and page sizes before
217
- building queries. The complete query supplies the result type.
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 `sql.join` when the number of fragments varies, such as one search condition per word or a set
220
- of optional filters. It combines fragments with `'AND'`, `'OR'`, or `','` and preserves their bound
221
- values:
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
- ```ts
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
- const query = sql<UserSummary>`SELECT id, email FROM public.users
231
- WHERE ${sql.join(filters, 'AND')} ORDER BY id`;
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
- This matches active users whose email contains every term. With no terms, the active condition still
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
- ```ts
239
- const optionalFilters = terms.map((term) => sql.fragment`email ILIKE ${'%' + term + '%'}`);
240
- const unfilteredOrMatching = optionalFilters.length
241
- ? sql<UserSummary>`SELECT id, email FROM public.users WHERE ${sql.join(optionalFilters, 'AND')} ORDER BY id`
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
- `sql.join` and fragments require nonempty input. Keep mandatory authorization predicates in the
246
- initial list when applicable; an empty optional-filter list must not remove them. Use parentheses
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
- Use fixed SQL when composition is unnecessary. See the [reference](reference.md) for arrays,
250
- identifiers, value mappings, CLI options, and error behavior.
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.