@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.
@@ -1,49 +1,74 @@
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 pgclear. 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. Tenon includes pgclear 0.3.x. Supply `DATABASE_URL` through your application's
21
- environment setup; Tenon does not load `.env` files. Import `sql`, `json`, `param`, and codec helpers
22
- from `@kequtech/tenon-orm` for application queries. See [driver updates](reference.md#driver-updates)
23
- when updating Pgclear independently.
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 connection = process.env.DATABASE_URL;
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
- Reuse this instance across the application and call `await db.close()` at shutdown. You can also
35
- supply an existing pgclear `Pool`; your application then owns its shutdown.
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 table
63
+ ## Create and apply a migration
38
64
 
39
- If you already have a schema and migration runner, keep them. For a fresh example database, create a
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
- Fill the generated files:
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
- For development, add a script to `package.json` that applies migrations and updates generated types:
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
- npm run db:migrate
92
+ npx tenon migrate
93
+ npx tenon status
74
94
  ```
75
95
 
76
- Commit migrations and generated types together. In production, run only `npx tenon migrate`; deploy
77
- the already-built application. If you do not use generated table types, omit generation from the
78
- development script. For later schema changes, add a new migration instead of editing an applied one.
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 query
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
- const email = 'reader@example.test';
90
- await db.query(sql`INSERT INTO public.users (email) VALUES (${email})`);
91
- const user = await db.query(
92
- sql<{ id: number; email: string }>`
93
- SELECT id, email FROM public.users WHERE email = ${email}
94
- `,
95
- { returns: 'oneOrNone' },
96
- );
97
- ```
98
-
99
- `${email}` becomes a bound parameter. Do not put quotes around it: the driver passes the value
100
- separately from the SQL text, even when the value contains SQL.
101
-
102
- The type describes one selected row. `returns: 'oneOrNone'` gives you that row or `undefined`; it
103
- throws if the query returns more than one row. Use `'one'` when absence should also be an error,
104
- `'many'` for an array, or omit `returns` for the native result object with `.rows`, `.rowCount`, and
105
- `.fields`. These options do not change your SQL.
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
- For frequently used types, define [application aliases](reference.md#application-aliases) such as
121
- `UserRow`, `UserInsert`, and `UserUpdate` in a handwritten module.
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
- You can then write `sql<UserSummary>`. A table type describes the full table; a join or aggregate
124
- needs a type for what that particular query selects. An annotation is a declaration, not proof that
125
- the SQL matches. Without one, result fields are `unknown`. Use the inspection and database-test
126
- workflow below to check the declared contract against the query's behavior.
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
- Keep storage types conservative and validate them when projecting domain values. The
129
- [persistence-to-domain example](reference.md#persistence-and-domain-values) shows validated JSON
130
- writes and reads, temporal strings, and an SQL predicate that rejects infinite session expiry.
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 with a function
140
+ ## Reuse a query
133
141
 
134
- Use an ordinary function when several callers need the same query. An object input makes the
135
- supplied values easy to read:
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 = Pick<Database['users']['row'], 'id' | 'email'>;
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' })` at the call site, or pass a transaction's
151
- `tx`. Keep the SQL and execution options together in the function.
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
- ## Change data in a transaction
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
- Use `tx` for all work in the callback and await it. Tenon commits when the callback succeeds and
169
- rolls back when an error escapes.
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
- Use `Pick` to limit editable fields. Keep expressions, predicates, conflict handling, and
196
- `RETURNING` in SQL. Selected columns choose fields; they do not validate the complete object. See
197
- [input types](reference.md#insert-and-update-inputs) for mappings and the
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
- ## Check your SQL and result types
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
- The three database tools answer different questions: `tenon check` compares authored queries with
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
- After migrating a development/test database, run the optional source-aware checker:
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 check src/queries.ts
211
- npx tenon check src/queries.ts:23
196
+ npx tenon generate
197
+ npx tenon generate --check
212
198
  ```
213
199
 
214
- A line selects one complete query. Omit the file to check the TypeScript project. The checker reads
215
- source and types without importing the application or executing SQL. It reports definite
216
- contradictions and unsupported composition; nullability, JSON members, and other inconclusive
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
- Use describeQuery on finished SQL, or tenon describe query.sql, when you only need metadata and a
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
- ## Reuse or assemble parts of SQL when needed
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
- A fragment is a piece of SQL that carries its bound values with it. It is useful when multiple
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
- const pattern = '%example%';
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
- Execute each query with `db.query`. Validate application-supplied patterns and page sizes before
245
- building queries. The complete query supplies the result type.
246
- The SQL aggregate's `count` is a string; the native result's `rowCount` counts returned rows.
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 `sql.join` when the number of fragments varies, such as one search condition per word or a set
249
- of optional filters. It combines fragments with `'AND'`, `'OR'`, or `','` and preserves their bound
250
- values:
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
- ```ts
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
- const query = sql<UserSummary>`SELECT id, email FROM public.users
260
- WHERE ${sql.join(filters, 'AND')} ORDER BY id`;
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
- This matches active users whose email contains every term. With no terms, the active condition still
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
- ```ts
268
- const optionalFilters = terms.map((term) => sql.fragment`email ILIKE ${'%' + term + '%'}`);
269
- const unfilteredOrMatching = optionalFilters.length
270
- ? sql<UserSummary>`SELECT id, email FROM public.users WHERE ${sql.join(optionalFilters, 'AND')} ORDER BY id`
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
- `sql.join` and fragments require nonempty input. Keep mandatory authorization predicates in the
275
- initial list when applicable; an empty optional-filter list must not remove them. Use parentheses
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
- Use fixed SQL when composition is unnecessary. See the [reference](reference.md) for arrays,
279
- identifiers, value mappings, CLI options, and error behavior.
280
- Runtime loops and dynamic fragment lists work during execution but remain unchecked by `tenon check`;
281
- use database tests for those queries.
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.