@kequtech/tenon-orm 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +52 -36
  2. package/README.md +27 -70
  3. package/dist/affected-rows.js +6 -6
  4. package/dist/baseline.js +4 -4
  5. package/dist/client.js +1 -1
  6. package/dist/connection.d.ts +15 -16
  7. package/dist/connection.js +124 -14
  8. package/dist/database.js +36 -17
  9. package/dist/driver/types.d.ts +1 -1
  10. package/dist/driver/types.js +11 -11
  11. package/dist/errors.d.ts +8 -0
  12. package/dist/errors.js +15 -0
  13. package/dist/generate.js +7 -7
  14. package/dist/generation/catalog.d.ts +2 -2
  15. package/dist/generation/catalog.js +4 -4
  16. package/dist/generation/schema-summary.d.ts +3 -3
  17. package/dist/generation/schema-summary.js +11 -11
  18. package/dist/generation/table-types.js +1 -1
  19. package/dist/index.d.ts +2 -3
  20. package/dist/index.js +1 -1
  21. package/dist/inspection/catalog.d.ts +4 -3
  22. package/dist/inspection/catalog.js +13 -10
  23. package/dist/inspection/cli.js +3 -3
  24. package/dist/inspection/connection.d.ts +2 -2
  25. package/dist/inspection/connection.js +22 -22
  26. package/dist/inspection/options.d.ts +3 -3
  27. package/dist/inspection/options.js +9 -1
  28. package/dist/inspection/protocol.d.ts +7 -0
  29. package/dist/inspection/protocol.js +45 -0
  30. package/dist/inspection/result.d.ts +1 -1
  31. package/dist/inspection/result.js +6 -6
  32. package/dist/inspection/types.d.ts +10 -10
  33. package/dist/migrations/connection.d.ts +3 -2
  34. package/dist/migrations/connection.js +18 -20
  35. package/dist/migrations/history.d.ts +4 -4
  36. package/dist/migrations/history.js +5 -5
  37. package/dist/migrations.js +7 -7
  38. package/dist/query-options.js +5 -5
  39. package/dist/query.js +2 -2
  40. package/dist/transaction.d.ts +2 -2
  41. package/dist/transaction.js +19 -14
  42. package/dist/types.d.ts +7 -3
  43. package/docs/adoption-guide.md +109 -122
  44. package/docs/agent-guide.md +124 -228
  45. package/docs/human-guide.md +44 -55
  46. package/docs/mutations.md +58 -76
  47. package/docs/query-inspection.md +97 -89
  48. package/docs/reference.md +264 -319
  49. package/package.json +4 -4
  50. package/dist/driver/array.d.ts +0 -5
  51. package/dist/driver/array.js +0 -70
  52. package/dist/driver/lifecycle.d.ts +0 -15
  53. package/dist/driver/lifecycle.js +0 -57
  54. package/dist/driver/session.d.ts +0 -8
  55. package/dist/driver/session.js +0 -92
@@ -1,4 +1,4 @@
1
- import type { SqlConnection } from '../connection.js';
1
+ import type { PoolClient } from 'pg';
2
2
  import type { SqlMigration } from './files.js';
3
3
  export declare const migrationTable = "__tenon_migrations";
4
4
  interface AppliedMigration {
@@ -6,7 +6,7 @@ interface AppliedMigration {
6
6
  name: string;
7
7
  checksum: string;
8
8
  }
9
- export declare function migrationState(client: SqlConnection, namespace: string, migrations: readonly SqlMigration[]): Promise<readonly AppliedMigration[]>;
10
- export declare function createHistoryTable(client: SqlConnection, namespace: string): Promise<void>;
11
- export declare function recordMigration(client: SqlConnection, namespace: string, migration: SqlMigration): Promise<void>;
9
+ export declare function migrationState(client: PoolClient, namespace: string, migrations: readonly SqlMigration[]): Promise<readonly AppliedMigration[]>;
10
+ export declare function createHistoryTable(client: PoolClient, namespace: string): Promise<void>;
11
+ export declare function recordMigration(client: PoolClient, namespace: string, migration: SqlMigration): Promise<void>;
12
12
  export {};
@@ -1,11 +1,11 @@
1
1
  import { qualified } from '../sql.js';
2
2
  export const migrationTable = '__tenon_migrations';
3
3
  export async function migrationState(client, namespace, migrations) {
4
- const exists = await client.unsafe('SELECT to_regclass($1)::text AS name', [qualified(namespace, migrationTable)]);
5
- if (exists[0].name === null) {
4
+ const exists = await client.query('SELECT to_regclass($1)::text AS name', [qualified(namespace, migrationTable)]);
5
+ if (exists.rows[0].name === null) {
6
6
  return [];
7
7
  }
8
- const applied = await client.unsafe(`SELECT id, name, checksum FROM ${qualified(namespace, migrationTable)} ORDER BY id::numeric`);
8
+ const { rows: applied } = await client.query(`SELECT id, name, checksum FROM ${qualified(namespace, migrationTable)} ORDER BY id::numeric`);
9
9
  for (const [i, record] of applied.entries()) {
10
10
  const file = migrations[i];
11
11
  if (!file ||
@@ -18,7 +18,7 @@ export async function migrationState(client, namespace, migrations) {
18
18
  return applied;
19
19
  }
20
20
  export async function createHistoryTable(client, namespace) {
21
- await client.unsafe(`CREATE TABLE IF NOT EXISTS ${qualified(namespace, migrationTable)} (
21
+ await client.query(`CREATE TABLE IF NOT EXISTS ${qualified(namespace, migrationTable)} (
22
22
  id TEXT PRIMARY KEY CHECK (id ~ '^[0-9]+$'),
23
23
  name TEXT NOT NULL,
24
24
  checksum TEXT NOT NULL,
@@ -26,5 +26,5 @@ export async function createHistoryTable(client, namespace) {
26
26
  )`);
27
27
  }
28
28
  export async function recordMigration(client, namespace, migration) {
29
- await client.unsafe(`INSERT INTO ${qualified(namespace, migrationTable)} (id, name, checksum) VALUES ($1, $2, $3)`, [migration.id, migration.name, migration.checksum]);
29
+ await client.query(`INSERT INTO ${qualified(namespace, migrationTable)} (id, name, checksum) VALUES ($1, $2, $3)`, [migration.id, migration.name, migration.checksum]);
30
30
  }
@@ -28,25 +28,25 @@ async function execute(options, direction, steps) {
28
28
  if (!selected.length) {
29
29
  return [];
30
30
  }
31
- await client.unsafe(`CREATE SCHEMA IF NOT EXISTS ${quote(namespace)}`);
31
+ await client.query(`CREATE SCHEMA IF NOT EXISTS ${quote(namespace)}`);
32
32
  await createHistoryTable(client, namespace);
33
33
  const completed = [];
34
34
  for (const file of selected) {
35
35
  try {
36
- await client.unsafe('BEGIN');
36
+ await client.query('BEGIN');
37
37
  // Match the string-literal rules used by the authored migration scanner.
38
- await client.unsafe('SET LOCAL standard_conforming_strings TO on');
39
- await client.unsafe(`SET LOCAL search_path TO ${quote(namespace)}, pg_catalog`);
40
- await client.unsafe(direction === 'up' ? file.up : file.down);
38
+ await client.query('SET LOCAL standard_conforming_strings TO on');
39
+ await client.query(`SET LOCAL search_path TO ${quote(namespace)}, pg_catalog`);
40
+ await client.query(direction === 'up' ? file.up : file.down);
41
41
  if (direction === 'up') {
42
42
  await recordMigration(client, namespace, file);
43
43
  }
44
44
  else {
45
- await client.unsafe(`DELETE FROM ${qualified(namespace, migrationTable)} WHERE id = $1`, [
45
+ await client.query(`DELETE FROM ${qualified(namespace, migrationTable)} WHERE id = $1`, [
46
46
  file.id,
47
47
  ]);
48
48
  }
49
- const result = await client.unsafe('COMMIT');
49
+ const result = await client.query('COMMIT');
50
50
  if (result.command !== 'COMMIT') {
51
51
  throw new Error('The migration transaction was rolled back.');
52
52
  }
@@ -1,5 +1,6 @@
1
1
  import { validateAffectedRows } from './affected-rows.js';
2
2
  import { plainObject } from './validation.js';
3
+ import { CardinalityError } from './errors.js';
3
4
  export function validateOptions(options) {
4
5
  const config = options === undefined ? {} : plainObject(options, 'Query options');
5
6
  for (const key of Object.keys(config)) {
@@ -26,10 +27,9 @@ export function queryOutput(options, result) {
26
27
  if (options.returns === 'native')
27
28
  return result;
28
29
  if (options.returns === 'many')
29
- return [...result];
30
- const expected = options.returns === 'oneOrNone' ? 'at most one row' : 'exactly one row';
31
- if (result.length > 1 || (options.returns === 'one' && result.length === 0)) {
32
- throw new Error(`${options.label ?? 'Query'} expected ${expected}, but returned ${result.length} rows.`);
30
+ return result.rows;
31
+ if (result.rows.length > 1 || (options.returns === 'one' && result.rows.length === 0)) {
32
+ throw new CardinalityError(options.returns === 'oneOrNone' ? 'oneOrNone' : 'one', result.rows.length, options.label);
33
33
  }
34
- return result[0];
34
+ return result.rows[0];
35
35
  }
package/dist/query.js CHANGED
@@ -110,8 +110,8 @@ export function unsafeSql(text, values) {
110
110
  throw new Error('SQL must be a nonempty string.');
111
111
  if (!Array.isArray(values))
112
112
  throw new Error('SQL input must be an explicit array.');
113
- if (values.length > 65533)
114
- throw new Error(`Postgres.js 3.4.9 supports at most 65533 bound parameters; received ${values.length}. Split large batches explicitly.`);
113
+ if (values.length > 65535)
114
+ throw new Error(`PostgreSQL supports at most 65535 bound parameters; received ${values.length}. Split large batches explicitly.`);
115
115
  return Object.freeze({ [queryTag]: true, text, values: Object.freeze([...values]) });
116
116
  }
117
117
  export function assertQuery(value) {
@@ -1,4 +1,4 @@
1
- import type postgres from 'postgres';
1
+ import type { Pool } from 'pg';
2
2
  import type { QueryExecutor } from './connection.js';
3
3
  /** The callback owns one connection and expires before commit or rollback begins. */
4
- export declare function withTransaction<T>(pool: postgres.Sql, work: (query: QueryExecutor) => Promise<T>): Promise<T>;
4
+ export declare function withTransaction<T>(pool: Pool, work: (query: QueryExecutor) => Promise<T>): Promise<T>;
@@ -1,11 +1,11 @@
1
- import { reserveConnection } from './driver/session.js';
2
- import { executeSql } from './connection.js';
1
+ import { executeSql, watchClient } from './connection.js';
3
2
  /** The callback owns one connection and expires before commit or rollback begins. */
4
3
  export async function withTransaction(pool, work) {
5
- const connection = await reserveConnection(pool, 'BEGIN');
6
- const client = connection.sql;
4
+ const client = await pool.connect();
5
+ const connection = watchClient(client);
7
6
  let active = true;
8
- let discard = false;
7
+ let began = false;
8
+ let releaseError;
9
9
  let savepointId = 0;
10
10
  let pending = Promise.resolve();
11
11
  let recoveryFailure;
@@ -21,16 +21,16 @@ export async function withTransaction(pool, work) {
21
21
  if (!guarded)
22
22
  return readResult(await executeSql(client, text, input));
23
23
  const savepoint = `tenon_affected_rows_${++savepointId}`;
24
- await client.unsafe(`SAVEPOINT ${savepoint}`);
24
+ await client.query(`SAVEPOINT ${savepoint}`);
25
25
  try {
26
26
  const result = readResult(await executeSql(client, text, input));
27
- await client.unsafe(`RELEASE SAVEPOINT ${savepoint}`);
27
+ await client.query(`RELEASE SAVEPOINT ${savepoint}`);
28
28
  return result;
29
29
  }
30
30
  catch (error) {
31
31
  try {
32
- await client.unsafe(`ROLLBACK TO SAVEPOINT ${savepoint}`);
33
- await client.unsafe(`RELEASE SAVEPOINT ${savepoint}`);
32
+ await client.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
33
+ await client.query(`RELEASE SAVEPOINT ${savepoint}`);
34
34
  }
35
35
  catch (rollbackError) {
36
36
  // Even if application code catches this error, the outer transaction
@@ -45,13 +45,15 @@ export async function withTransaction(pool, work) {
45
45
  return operation;
46
46
  };
47
47
  try {
48
+ await client.query('BEGIN');
49
+ began = true;
48
50
  const result = await work(query);
49
51
  active = false;
50
52
  await pending;
51
53
  connection.check();
52
54
  if (recoveryFailure)
53
55
  throw recoveryFailure;
54
- const committed = await client.unsafe('COMMIT');
56
+ const committed = await client.query('COMMIT');
55
57
  if (committed.command !== 'COMMIT') {
56
58
  throw new Error('PostgreSQL rolled back the aborted transaction.');
57
59
  }
@@ -60,17 +62,20 @@ export async function withTransaction(pool, work) {
60
62
  catch (error) {
61
63
  active = false;
62
64
  await pending;
65
+ if (!began) {
66
+ releaseError = error instanceof Error ? error : new Error('BEGIN failed.');
67
+ throw error;
68
+ }
63
69
  try {
64
- connection.check();
65
- await client.unsafe('ROLLBACK');
70
+ await client.query('ROLLBACK');
66
71
  }
67
72
  catch (rollbackError) {
68
- discard = true;
73
+ releaseError = rollbackError instanceof Error ? rollbackError : new Error('Rollback failed.');
69
74
  throw new AggregateError([error, rollbackError], 'Transaction and rollback failed.');
70
75
  }
71
76
  throw error;
72
77
  }
73
78
  finally {
74
- await connection.release(discard);
79
+ connection.release(releaseError);
75
80
  }
76
81
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type postgres from 'postgres';
1
+ import type { Pool } from 'pg';
2
2
  import type { QueryResult } from './connection.js';
3
3
  import type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
4
4
  import type { SqlQuery } from './query.js';
@@ -12,12 +12,16 @@ export interface QueryClient {
12
12
  query<Row extends object>(query: SqlQuery<Row>, options: QueryOptions | undefined): NoInfer<Promise<QueryOutput<Row, QueryReturnMode>>>;
13
13
  }
14
14
  export type Transaction = QueryClient;
15
+ export interface CloseOptions {
16
+ /** Grace period before requesting owned driver termination; milliseconds, default 5000. */
17
+ readonly timeoutMs?: number | undefined;
18
+ }
15
19
  export type Database = Transaction & {
16
20
  /** The underlying pool-level driver client; native calls bypass Tenon's query and transaction guards. */
17
- readonly native: postgres.Sql;
21
+ readonly native: Pool;
18
22
  /** Await all work inside the callback. Commit on success; rollback on an escaping error. */
19
23
  transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>;
20
- close(): Promise<void>;
24
+ close(options?: CloseOptions): Promise<void>;
21
25
  };
22
26
  /** One completed public query call, including rollback/commit time for guarded calls. */
23
27
  export interface QueryEvent {
@@ -1,151 +1,138 @@
1
- # Adopting Tenon from another ORM
1
+ # Adopting Tenon
2
2
 
3
- This guide covers replacing an existing ORM and, separately, transferring migration ownership. For a
4
- query tutorial use the [human guide](human-guide.md); for API details use the
5
- [reference](reference.md). Preserve the existing schema, connection configuration, data, and
6
- application behavior while replacing the old ORM. Tenon does not need to own migrations to execute
7
- queries or generate types; keep the existing migration runner if that is the intended scope.
3
+ Replace application queries independently of migration ownership. Tenon can query an existing schema
4
+ and generate types while the current migration runner stays in place. For a Tenon version upgrade,
5
+ read the [Changelog](../CHANGELOG.md).
8
6
 
9
- ## Replace application queries
7
+ ## Preserve connections and behavior
10
8
 
11
- 1. Locate the old ORM's connection setup, schema, migrations, and call sites. Identify behavior
12
- supplied by the ORM: validation, IDs, timestamps, hooks, defaults, relation loading,
13
- serialization, and transaction boundaries.
14
- 2. If row and mutation input types will help your application, generate them from the existing database:
9
+ Keep the existing database URI, credentials, TLS requirements, schema, and data. Tenon can add
10
+ driver settings through pg configuration:
15
11
 
16
- ```sh
17
- npx tenon generate --out src/generated-schema.d.ts
18
- ```
19
-
20
- Use `--namespace name` for a non-public schema. Generation reads the actual database and requires
21
- no Tenon migration history.
22
-
23
- 3. Replace queries with parameterized SQL in ordinary functions with named object inputs where
24
- useful. Preserve each caller's projection, null handling, ordering, and transaction behavior.
25
- Generated types describe Postgres.js values: check bigint/numeric strings, dates, JSON, and arrays when
26
- the old ORM transformed them.
27
- 4. Inspect completed queries and test their behavior using the
28
- [inspection workflow](#inspect-queries-during-adoption) below. Declare result types explicitly
29
- and run tests against a dedicated test database. Verify
30
- writes and rollback, relationships, validation, and any hooks you moved into application code or
31
- SQL. A schema-preserving first release makes application rollback easier.
32
- 5. Remove the old runtime dependency and unused generated code after its callers are gone. Keep the
33
- migration tooling until the migration transition below is complete, if that transition is part of
34
- the work.
12
+ ```ts
13
+ import { createTenon } from '@kequtech/tenon-orm';
14
+
15
+ const db = createTenon({
16
+ connection: {
17
+ connectionString: existingUri,
18
+ connectionTimeoutMillis: 5000,
19
+ },
20
+ });
21
+ ```
35
22
 
36
- For driver features such as LISTEN or COPY, use [`db.native`](reference.md#native-driver-access).
37
- Preserve the application's notification recovery and resource ownership. Native pool queries do
38
- not join Tenon transactions; use `tx.query` for operations that must commit together.
23
+ Translate old driver configuration into pg `PoolConfig` rather than passing it through. Pool
24
+ timeouts use milliseconds. Preserve runtime `search_path` through the pg startup options string, for
25
+ example `options: '-c search_path=app,public'`. URI startup options take precedence when both are
26
+ supplied. CLI `--namespace` does not change runtime queries. See
27
+ [connection options](reference.md#installation-and-connection).
39
28
 
40
- ## Replace local write helpers
29
+ Before conversion, identify behavior supplied by the old ORM: IDs/defaults, timestamps, validation,
30
+ hooks, serialization, relationships, and transaction boundaries. Preserve it explicitly in
31
+ application code or SQL where required.
41
32
 
42
- Use `sql.insertValues(rows)` and `sql.assignments(patch)` in place of local object-to-SQL utilities:
33
+ ## Replace queries
43
34
 
44
- ```ts
45
- import { sql } from '@kequtech/tenon-orm';
35
+ 1. Optionally generate persistence types from the existing schema:
46
36
 
47
- const insert = sql`INSERT INTO profiles ${sql.insertValues(rows)} RETURNING id`;
48
- const update = sql`UPDATE profiles SET ${sql.assignments(patch)} WHERE id = ${id}`;
49
- ```
37
+ ```sh
38
+ npx tenon generate --namespace public --out src/generated-schema.d.ts
39
+ ```
50
40
 
51
- Compare omission, defaults, nulls, and empty-input behavior with the old helpers. Keep expressions
52
- in SQL and remove the local utilities after their callers move. Generated `Rows.Insert` and
53
- `Rows.Update` types can replace handwritten table inputs; retain application-specific refinements
54
- and validation. Run the existing write and rollback workflows. See the [mutation cookbook](mutations.md).
41
+ This needs no Tenon migration history. Use the application's namespace and commit exact output.
55
42
 
56
- ## Inspect queries during adoption
43
+ 2. Replace queries with parameterized SQL in ordinary functions accepting `QueryClient` where
44
+ callers may supply `db` or `tx`. Retain projections, ordering, null handling, and authorization.
45
+ 3. Review value conversions against [Tenon's mappings](reference.md#generated-types-and-checking):
46
+ numeric values, dates, arrays, and JSON serialization may differ from the previous driver.
47
+ Generated declarations do not replace domain validation.
48
+ 4. Inspect meaningful completed query variants and run database assertions for actual rows,
49
+ permissions, write effects, and rollback. See [query inspection](query-inspection.md).
50
+ 5. Remove unused runtime dependencies and generated code after their callers are gone. Dependencies
51
+ retained by other application code or installed transitively may still be needed.
57
52
 
58
- Use `describeQuery` from `@kequtech/tenon-orm` on the completed query object, or
59
- `npx tenon describe query.sql` on a SQL file. Inspection is available with the normal Tenon
60
- installation and uses no TypeScript compiler API. It works with helpers and runtime-built
61
- fragments because it receives the final SQL. See [query inspection](query-inspection.md).
53
+ Check cardinality when replacing a first-row accessor. `oneOrNone` rejects multiple rows and `one`
54
+ also rejects zero rows. Use deterministic `ORDER BY` plus `LIMIT 1` when choosing one of several
55
+ matches is intended; modes do not add a limit.
62
56
 
63
- Reuse the application's connection URI and session configuration; compare the reported role,
64
- search path, and timezone with execution. Inspection uses a separate connection and Postgres.js
65
- read mappings. Custom parsers and transaction-local state remain application responsibilities.
57
+ Use `sql.insertValues` and `sql.assignments` to replace object-to-SQL utilities, checking omission,
58
+ default, null, and empty-input semantics. Generated `Rows.Insert`/`Rows.Update` types can replace
59
+ handwritten persistence inputs. See the [mutation cookbook](mutations.md).
66
60
 
67
- Pair descriptions with durable database tests of actual rows and effects. For each converted
68
- query, cover the meaningful filter/ownership branches, null relationships, JSON structure, empty
69
- results, ordering/cursors, and transaction failure/replay behavior as applicable. Describe's success
70
- does not verify a TypeScript annotation or prove that execution is permitted or succeeds. Preserve
71
- existing authorization policy and transaction boundaries while making queries available to tests.
61
+ Use `tx.query` for work that must commit together. Native pool calls do not join Tenon transactions.
62
+ For driver features, preserve resource ownership and recovery policy under the
63
+ [native access contract](reference.md#native-driver-access).
72
64
 
73
- Keep these tests in routine validation. A described query instance does not cover untested source
74
- queries or detect stale annotations elsewhere.
65
+ ## Transfer migration ownership
75
66
 
76
- ## Move migration ownership to Tenon
67
+ Choose one final checkpoint for the old runner. Bring existing databases to it and disable that
68
+ runner before handover; the runners do not share locks. Capture from a trusted reference database,
69
+ preferably a disposable replay of the legacy migrations.
77
70
 
78
- Choose one final schema checkpoint for the old runner and bring existing target databases to it.
79
- Disable its migration/schema synchronization before handing ownership to Tenon. The tools do not
80
- share locks.
71
+ ### Capture a baseline
81
72
 
82
- Capture one baseline from the reference database into an empty migration directory:
73
+ For an application owning `app` in a shared database:
83
74
 
84
75
  ```sh
85
- npx tenon baseline capture --directory migrations --namespace public
76
+ npx tenon baseline capture --directory migrations --namespace app --schema app
86
77
  ```
87
78
 
88
- This uses native `pg_dump --schema-only --no-owner --no-privileges` and writes
89
- `0001_baseline.up.sql` without a down file. It requires a compatible `pg_dump` on PATH; prefer the
90
- server's major. Default scope is the whole database schema. Repeat `--schema name` to select schemas
91
- explicitly, accounting for dependencies, extensions, and required roles. Tenon's selected history
92
- table is excluded; old ORM metadata is preserved for deliberate cleanup later.
93
-
94
- The baseline recreates application schema, not a complete deployment environment. It omits
95
- ownership, grants/revokes, and default privileges. PostgreSQL creates replayed objects under the
96
- migration role. Role definitions and memberships also remain outside the dump. Functions,
97
- policies, or other object definitions can still refer to roles; provision any referenced roles
98
- before replay. See the [PostgreSQL pg_dump reference](https://www.postgresql.org/docs/16/app-pgdump.html).
99
-
100
- For `SECURITY DEFINER` functions, the owner determines execution authority, and newly created
101
- functions ordinarily permit `PUBLIC` execution. A captured function body alone does not preserve
102
- its original security boundary. Keep fixed application policy, such as revoking `PUBLIC` execution,
103
- in reviewed SQL in the baseline before its first adoption or in a subsequent application migration.
104
- Deployment provisioning assigns the intended owners, schema access, and grants to runtime roles.
105
- Keeping all security setup in provisioning is also supported, provided it runs before application access.
106
-
107
- On fresh environments, provision required roles, apply migrations, then apply the deployment's
108
- ownership/grant policy before starting the application. Verify both allowed and denied operations,
109
- including execution of sensitive functions and access to their underlying tables. For a shared
110
- database, apply only the selected application's policy. On existing environments, confirm the same
111
- provisioning procedure is repeatable. After adoption, use subsequent migrations rather than editing
112
- checksummed history.
113
-
114
- Capture changes no source data, migration history, or sequence positions. Review and commit the SQL.
115
- Keep the capture header: it identifies native dump SQL that bypasses the authored-migration scanner.
116
- It is a convention, not proof of provenance; do not use it on handwritten migrations. Normal
117
- transactional execution and checksums still apply.
118
-
119
- On each existing database already at that checkpoint:
120
-
121
- ```sh
122
- npx tenon baseline adopt
123
- ```
79
+ The directory must contain no SQL files. Capture uses native
80
+ `pg_dump --schema-only --no-owner --no-privileges` and writes `0001_baseline.up.sql` with no down
81
+ file. Install compatible PostgreSQL client tools on PATH; prefer the server's major version.
124
82
 
125
- Adoption records the baseline checksum without executing the SQL or comparing schemas. It requires
126
- the selected namespace to exist. Repeating matching adoption is harmless, including after later
127
- migrations; conflicting history fails.
83
+ | Option | Scope |
84
+ | --------------------- | ------------------------------------------------------------------------------------ |
85
+ | `--namespace app` | Tenon history table to exclude from capture; history/runner scope for later commands |
86
+ | `--schema app` | Objects selected by native `pg_dump`; repeat to select additional schemas |
87
+ | Runtime `search_path` | Configured independently on the application connection |
128
88
 
129
- On a fresh database, run migrations instead:
89
+ Omitting `--schema` captures the whole database schema, even with a custom namespace. Capture does
90
+ not modify source data/history or preserve sequence positions. It excludes Tenon's selected history
91
+ table but can retain the old runner's metadata tables. Review and commit the SQL. Preserve the
92
+ capture header, which opts this reviewed dump into native baseline parsing; do not add it to
93
+ handwritten migrations.
130
94
 
131
- ```sh
132
- npx tenon migrate
133
- ```
95
+ A scoped dump can refer to objects it does not include. Provision external schemas, functions,
96
+ extensions, and referenced roles before they are needed. A fresh replay must execute representative
97
+ operations, not just reproduce generated relation types. Tenon requires no particular extension.
134
98
 
135
- That executes the baseline and subsequent migrations. Use the same committed baseline for every
136
- environment. Do not run adoption unconditionally on fresh databases. Existing databases adopt once
137
- and then use ordinary `migrate`.
99
+ ### Check and adopt an existing database
138
100
 
139
- Use the development [`db:migrate` script](human-guide.md#create-a-table) to apply subsequent
140
- migrations and update table types together. Production continues to run only `npx tenon migrate`.
141
- Verify both an existing database's upgrade and a fresh replay. Baselines contain schema only; they
142
- do not back up rows or sequence positions. The missing down file prevents normal rollback through
143
- the baseline. Keep the application's backup and recovery procedure.
101
+ Verify the final legacy checkpoint before adoption. If using schema comparisons, freeze separate
102
+ reference definitions rather than comparing against types regenerated after later migrations.
103
+ `generateDefinitions` covers relation metadata, not functions or privileges; include reviewed
104
+ catalog checks for other objects the application needs. Tenon does not perform these comparisons.
144
105
 
145
- Programmatic equivalents:
106
+ ```sh
107
+ npx tenon baseline adopt --directory migrations --namespace app
108
+ npx tenon migrate --directory migrations --namespace app
109
+ ```
146
110
 
147
- - `captureBaseline({ connection, directory?, namespace?, schemas?, onWarning? })` requires a
148
- nonempty native libpq connection string and returns `{ id, upPath }`. Omit `schemas` for the whole
149
- database schema; an explicitly empty list is rejected.
150
- - `adoptBaseline({ connection, directory?, namespace? })` accepts the usual Tenon connection options
151
- and returns newly recorded migration IDs.
111
+ Adoption records the baseline checksum without executing its SQL. The namespace must already exist.
112
+ Repeating matching adoption is harmless, including after later migrations; history conflicts fail.
113
+ Once Tenon owns history, add forward migrations rather than editing the baseline.
114
+
115
+ ### Replay a fresh database
116
+
117
+ Run `tenon migrate --directory migrations --namespace app`, without adoption. It executes the same
118
+ committed baseline and subsequent migrations. Apply the deployment's required roles, ownership, and
119
+ grants before application access.
120
+
121
+ The portable baseline omits ownership, grants/revokes, and default privileges. Objects are recreated
122
+ under the migration role. For `SECURITY DEFINER` functions, that owner controls execution authority,
123
+ and new functions ordinarily permit `PUBLIC` execution. Keep fixed policy such as revoking public
124
+ execution in reviewed baseline SQL before first adoption or in a forward migration. Deployment
125
+ provisioning supplies named roles, owners, and grants. Verify permitted and denied operations on
126
+ both adopted and fresh environments. See the
127
+ [pg_dump contract](https://www.postgresql.org/docs/16/app-pgdump.html).
128
+
129
+ For development, generate types after migrating; production runs migrations and consumes committed
130
+ types. Test existing-row preservation, repeatable adoption, subsequent migrations, and fresh replay.
131
+ A schema-only baseline and down migration are not backups. The missing down file prevents rollback
132
+ across the baseline; use the application's backup/recovery procedure for that boundary.
133
+
134
+ Programmatic capture is
135
+ `captureBaseline({ connection, directory?, namespace?, schemas?, onWarning? })` and returns
136
+ `{ id, upPath }`. It requires a native libpq connection string; an explicitly empty `schemas` list
137
+ fails. `adoptBaseline({ connection, directory?, namespace? })` accepts Tenon's runtime connection
138
+ forms and returns newly recorded IDs.