@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,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.query('SELECT to_regclass($1)::text AS name', [qualified(namespace, migrationTable)]);
4
+ const exists = await client.queryClassic('SELECT to_regclass($1)::text AS name', [qualified(namespace, migrationTable)]);
5
5
  if (exists.rows[0].name === null) {
6
6
  return [];
7
7
  }
8
- const { rows: applied } = await client.query(`SELECT id, name, checksum FROM ${qualified(namespace, migrationTable)} ORDER BY id::numeric`);
8
+ const { rows: applied } = await client.queryClassic(`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,13 +18,13 @@ export async function migrationState(client, namespace, migrations) {
18
18
  return applied;
19
19
  }
20
20
  export async function createHistoryTable(client, namespace) {
21
- await client.query(`CREATE TABLE IF NOT EXISTS ${qualified(namespace, migrationTable)} (
21
+ await client.queryClassic(`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,
25
25
  applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
26
- )`);
26
+ )`, []);
27
27
  }
28
28
  export async function recordMigration(client, namespace, migration) {
29
- await client.query(`INSERT INTO ${qualified(namespace, migrationTable)} (id, name, checksum) VALUES ($1, $2, $3)`, [migration.id, migration.name, migration.checksum]);
29
+ await client.queryClassic(`INSERT INTO ${qualified(namespace, migrationTable)} (id, name, checksum) VALUES ($1, $2, $3)`, [migration.id, migration.name, migration.checksum]);
30
30
  }
@@ -28,25 +28,23 @@ async function execute(options, direction, steps) {
28
28
  if (!selected.length) {
29
29
  return [];
30
30
  }
31
- await client.query(`CREATE SCHEMA IF NOT EXISTS ${quote(namespace)}`);
31
+ await client.queryClassic(`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.query('BEGIN');
36
+ await client.queryClassic('BEGIN', []);
37
37
  // Match the string-literal rules used by the authored migration scanner.
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);
38
+ await client.queryClassic('SET LOCAL standard_conforming_strings TO on', []);
39
+ await client.queryClassic(`SET LOCAL search_path TO ${quote(namespace)}, pg_catalog`, []);
40
+ await client.script(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.query(`DELETE FROM ${qualified(namespace, migrationTable)} WHERE id = $1`, [
46
- file.id,
47
- ]);
45
+ await client.queryClassic(`DELETE FROM ${qualified(namespace, migrationTable)} WHERE id = $1`, [file.id]);
48
46
  }
49
- const result = await client.query('COMMIT');
47
+ const result = await client.queryClassic('COMMIT', []);
50
48
  if (result.command !== 'COMMIT') {
51
49
  throw new Error('The migration transaction was rolled back.');
52
50
  }
@@ -8,7 +8,7 @@ export interface QueryOptions {
8
8
  /** Optional diagnostic label. No SQL or input values are sent to observers. */
9
9
  readonly label?: string;
10
10
  }
11
- export type QueryOutput<Row extends object, Mode extends QueryReturnMode> = Mode extends 'native' ? QueryResult<Row> : Mode extends 'many' ? Row[] : Mode extends 'oneOrNone' ? Row | undefined : Row;
11
+ export type QueryOutput<Row extends object, Mode extends QueryReturnMode> = Mode extends 'native' ? QueryResult<Row> : Mode extends 'many' ? readonly Row[] : Mode extends 'oneOrNone' ? Row | undefined : Row;
12
12
  export declare function validateOptions(options?: QueryOptions): Readonly<QueryOptions & {
13
13
  returns: QueryReturnMode;
14
14
  }>;
package/dist/query.js CHANGED
@@ -124,6 +124,6 @@ export function assertQuery(value) {
124
124
  !value.text.trim() ||
125
125
  !('values' in value) ||
126
126
  !Array.isArray(value.values)) {
127
- throw new Error('Expected a query created with sql or unsafeSql.');
127
+ throw new Error('Expected a query created with sql or unsafeSql from @kequtech/tenon-orm.');
128
128
  }
129
129
  }
@@ -1,4 +1,4 @@
1
- import type { Pool } from 'pg';
1
+ import type { Pool } from '@kequtech/pgclear';
2
2
  import type { QueryExecutor } from './connection.js';
3
- /** The callback owns one connection and expires before commit or rollback begins. */
3
+ /** Serialize whole Tenon policies; pgclear owns transaction, savepoint and transport recovery. */
4
4
  export declare function withTransaction<T>(pool: Pool, work: (query: QueryExecutor) => Promise<T>): Promise<T>;
@@ -1,81 +1,25 @@
1
- import { executeSql, watchClient } from './connection.js';
2
- /** The callback owns one connection and expires before commit or rollback begins. */
3
- export async function withTransaction(pool, work) {
4
- const client = await pool.connect();
5
- const connection = watchClient(client);
6
- let active = true;
7
- let began = false;
8
- let releaseError;
9
- let savepointId = 0;
10
- let pending = Promise.resolve();
11
- let recoveryFailure;
12
- const query = async (text, input, readResult, guarded) => {
13
- if (!active)
14
- throw new Error('The transaction has finished.');
15
- // Queue whole operations, including savepoint cleanup. Otherwise concurrent
16
- // calls could place another write inside a savepoint that gets rolled back.
17
- const operation = pending.then(async () => {
18
- connection.check();
19
- if (recoveryFailure)
20
- throw recoveryFailure;
21
- if (!guarded)
22
- return readResult(await executeSql(client, text, input));
23
- const savepoint = `tenon_affected_rows_${++savepointId}`;
24
- await client.query(`SAVEPOINT ${savepoint}`);
25
- try {
26
- const result = readResult(await executeSql(client, text, input));
27
- await client.query(`RELEASE SAVEPOINT ${savepoint}`);
28
- return result;
29
- }
30
- catch (error) {
31
- try {
32
- await client.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
33
- await client.query(`RELEASE SAVEPOINT ${savepoint}`);
34
- }
35
- catch (rollbackError) {
36
- // Even if application code catches this error, the outer transaction
37
- // must not commit when the rejected write could still be present.
38
- recoveryFailure = new AggregateError([error, rollbackError], 'Operation and savepoint recovery failed.');
39
- throw recoveryFailure;
40
- }
41
- throw error;
42
- }
43
- });
44
- pending = operation.then(() => { }, () => { });
45
- return operation;
46
- };
47
- try {
48
- await client.query('BEGIN');
49
- began = true;
50
- const result = await work(query);
51
- active = false;
52
- await pending;
53
- connection.check();
54
- if (recoveryFailure)
55
- throw recoveryFailure;
56
- const committed = await client.query('COMMIT');
57
- if (committed.command !== 'COMMIT') {
58
- throw new Error('PostgreSQL rolled back the aborted transaction.');
59
- }
60
- return result;
61
- }
62
- catch (error) {
63
- active = false;
64
- await pending;
65
- if (!began) {
66
- releaseError = error instanceof Error ? error : new Error('BEGIN failed.');
67
- throw error;
68
- }
1
+ import { executeSql } from './connection.js';
2
+ /** Serialize whole Tenon policies; pgclear owns transaction, savepoint and transport recovery. */
3
+ export function withTransaction(pool, work) {
4
+ return pool.transaction(async (transaction) => {
5
+ let active = true;
6
+ let pending = Promise.resolve();
7
+ const query = (text, input, readResult, guarded) => {
8
+ if (!active)
9
+ return Promise.reject(new Error('The transaction has finished.'));
10
+ const operation = pending.then(() => {
11
+ const execute = async (scope) => readResult(await executeSql(scope, text, input));
12
+ return guarded ? transaction.savepoint(execute) : execute(transaction);
13
+ });
14
+ pending = operation.then(() => { }, () => { });
15
+ return operation;
16
+ };
69
17
  try {
70
- await client.query('ROLLBACK');
18
+ return await work(query);
71
19
  }
72
- catch (rollbackError) {
73
- releaseError = rollbackError instanceof Error ? rollbackError : new Error('Rollback failed.');
74
- throw new AggregateError([error, rollbackError], 'Transaction and rollback failed.');
20
+ finally {
21
+ active = false;
22
+ await pending;
75
23
  }
76
- throw error;
77
- }
78
- finally {
79
- connection.release(releaseError);
80
- }
24
+ });
81
25
  }
package/dist/types.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { Pool } from 'pg';
1
+ import type { Pool } from '@kequtech/pgclear';
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';
5
+ import type { GenerateTypesOptions } from './generate.js';
5
6
  export interface QueryClient {
6
7
  query<Row extends object>(query: SqlQuery<Row>, options?: QueryOptions & {
7
8
  readonly returns?: 'native';
@@ -19,6 +20,8 @@ export interface CloseOptions {
19
20
  export type Database = Transaction & {
20
21
  /** The underlying pool-level driver client; native calls bypass Tenon's query and transaction guards. */
21
22
  readonly native: Pool;
23
+ /** Write or check schema declarations using this instance's configured codecs. */
24
+ generateTypes(options: GenerateTypesOptions): Promise<void>;
22
25
  /** Await all work inside the callback. Commit on success; rollback on an escaping error. */
23
26
  transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>;
24
27
  close(options?: CloseOptions): Promise<void>;
@@ -1,72 +1,47 @@
1
1
  # Adopting Tenon
2
2
 
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).
3
+ You can replace application queries without changing migration ownership. Tenon queries and type
4
+ generation work with an existing schema and any migration runner. For a Tenon upgrade, read the
5
+ [shipped changelog](../CHANGELOG.md) from your resolved version through the target version.
6
6
 
7
7
  ## Preserve connections and behavior
8
8
 
9
- Keep the existing database URI, credentials, TLS requirements, schema, and data. Tenon can add
10
- driver settings through pg configuration:
9
+ Reuse the existing database URI, credentials, schema, TLS requirements, and data. Pool configuration
10
+ uses pgclear options; timeout names include `Ms`. Configure runtime names with
11
+ `startupParameters: { search_path: 'app,public' }`, or qualify tables in SQL. Tooling `namespace`
12
+ does not configure runtime queries.
11
13
 
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
- ```
22
-
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).
28
-
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.
14
+ Before replacing an ORM, identify behavior it currently supplies: IDs/defaults, timestamps,
15
+ validation, hooks, serialization, relationship loading, and transaction boundaries. Preserve needed
16
+ behavior in application code, constraints, or SQL. Tenon has no model lifecycle or implicit loading.
32
17
 
33
18
  ## Replace queries
34
19
 
35
- 1. Optionally generate persistence types from the existing schema:
36
-
37
- ```sh
38
- npx tenon generate --namespace public --out src/generated-schema.d.ts
39
- ```
40
-
41
- This needs no Tenon migration history. Use the application's namespace and commit exact output.
42
-
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.
52
-
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.
56
-
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).
60
-
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).
20
+ 1. Optionally generate persistence types with
21
+ `await db.generateTypes({ out: 'src/generated-schema.d.ts', namespace: 'public' })`.
22
+ Use the application's schema and codec registry; no Tenon migration history is required.
23
+ 2. Replace one workflow at a time with parameterized SQL in ordinary functions. Accept
24
+ `QueryClient` when callers may use either `db` or `tx`. Preserve selected columns, ordering,
25
+ null handling, authorization predicates, and transaction boundaries.
26
+ 3. Review input/output mappings and validate domain values at the persistence boundary. Generated
27
+ types do not validate JSON members or application constraints.
28
+ 4. Test actual rows, writes, permissions, and rollback. Use source checking and inspection where
29
+ useful. See [choosing a validation tool](reference.md#choosing-a-validation-tool).
30
+ 5. Remove unused dependencies/generated code once all their callers are gone.
31
+
32
+ When replacing a first-row accessor, decide whether multiple matches should be an error.
33
+ `oneOrNone` rejects multiple rows and `one` also rejects absence. To choose one match deliberately,
34
+ author deterministic `ORDER BY ... LIMIT 1`; result modes do not add a limit.
35
+
36
+ For object-to-SQL utilities, review omission, defaults, null, and empty patches against
37
+ [the write guide](mutations.md). Use `tx.query` for every operation that must commit together;
38
+ native pool calls do not join Tenon transactions.
64
39
 
65
40
  ## Transfer migration ownership
66
41
 
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.
42
+ Choose the old runner's final schema checkpoint. Bring every existing database to it and disable
43
+ that runner before handover; the runners do not share locks. Capture one baseline from a trusted
44
+ reference database, preferably a disposable replay of the old migrations.
70
45
 
71
46
  ### Capture a baseline
72
47
 
@@ -77,72 +52,74 @@ npx tenon baseline capture --directory migrations --namespace app --schema app
77
52
  ```
78
53
 
79
54
  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.
55
+ `pg_dump --schema-only --no-owner --no-privileges` and writes `0001_baseline.up.sql` without a down
56
+ file. Install compatible PostgreSQL client tools on PATH, preferably matching the server's major
57
+ version. Capture does not modify source data or history and does not preserve sequence positions.
82
58
 
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 |
59
+ | Setting | What it selects |
60
+ | --------------------- | ----------------------------------------------------------------------- |
61
+ | `--namespace app` | Tenon history table to exclude; history/runner scope for later commands |
62
+ | `--schema app` | Schema objects included by pg_dump; repeat for more schemas |
63
+ | Runtime `search_path` | Application name resolution, configured separately |
88
64
 
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.
65
+ Omitting `--schema` captures the whole database schema, even with a custom namespace. The selected
66
+ Tenon history table is excluded; old-runner metadata tables may remain. Review and commit the SQL.
67
+ Preserve the capture header: it opts this reviewed dump into native baseline parsing. Do not add
68
+ that header to handwritten migrations.
94
69
 
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.
70
+ A scoped dump may depend on objects outside its scope. Provision required external schemas,
71
+ extensions, functions, and roles. Fresh replay must exercise representative application operations,
72
+ not merely reproduce table types. Tenon itself requires no particular extension.
98
73
 
99
74
  ### Check and adopt an existing database
100
75
 
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.
105
-
106
- Generated declaration text is not a version-stable schema fingerprint: driver mappings and generator
107
- changes can alter it without schema drift. The schema-summary comments expose SQL definitions, but
108
- their formatting and PostgreSQL's rendering of those definitions are not a stable comparison format
109
- either. If an existing adoption guard hashes generated text, preserve its original reference and
110
- freeze a separate reference for each supported generator version from a disposable replay of the
111
- immutable checkpoint. Compare the replay's metadata with the reviewed checkpoint before accepting
112
- the new reference; never derive a replacement from whichever live database is configured. Keep
113
- independent function and privilege checks, and test both matching adoption and refusal after drift.
114
- This application guard is separate from Tenon's migration checksums; keep applied SQL unchanged.
76
+ Confirm that each existing database matches the reviewed final checkpoint. Tenon does not compare
77
+ the live schema with the baseline. Adoption records its checksum without executing its SQL:
115
78
 
116
79
  ```sh
117
80
  npx tenon baseline adopt --directory migrations --namespace app
118
81
  npx tenon migrate --directory migrations --namespace app
119
82
  ```
120
83
 
121
- Adoption records the baseline checksum without executing its SQL. The namespace must already exist.
122
- Repeating matching adoption is harmless, including after later migrations; history conflicts fail.
123
- Once Tenon owns history, add forward migrations rather than editing the baseline.
84
+ The namespace must already exist. Repeating adoption with matching history is harmless, including
85
+ after later migrations; conflicting history fails. Once adopted, leave the baseline unchanged and
86
+ add forward migrations.
87
+
88
+ For an application-owned schema guard, keep a frozen checkpoint reference. Generated relation types
89
+ do not cover functions or privileges; check required objects separately. Neither generated text nor
90
+ schema-summary formatting is a version-stable fingerprint. If a guard hashes that text, preserve
91
+ its original reference and freeze a separate reviewed reference for each generator version, taken
92
+ from a disposable replay of the immutable checkpoint. Verify that replay against the checkpoint
93
+ before accepting the reference; do not derive it from whichever live database is configured.
94
+ Test both matching adoption and refusal after drift. This is separate from migration checksums.
124
95
 
125
96
  ### Replay a fresh database
126
97
 
127
- Run `tenon migrate --directory migrations --namespace app`, without adoption. It executes the same
128
- committed baseline and subsequent migrations. Apply the deployment's required roles, ownership, and
129
- grants before application access.
130
-
131
- The portable baseline omits ownership, grants/revokes, and default privileges. Objects are recreated
132
- under the migration role. For `SECURITY DEFINER` functions, that owner controls execution authority,
133
- and new functions ordinarily permit `PUBLIC` execution. Keep fixed policy such as revoking public
134
- execution in reviewed baseline SQL before first adoption or in a forward migration. Deployment
135
- provisioning supplies named roles, owners, and grants. Verify permitted and denied operations on
136
- both adopted and fresh environments. See the
137
- [pg_dump contract](https://www.postgresql.org/docs/16/app-pgdump.html).
138
-
139
- For development, generate types after migrating; production runs migrations and consumes committed
140
- types. Test existing-row preservation, repeatable adoption, subsequent migrations, and fresh replay.
141
- A schema-only baseline and down migration are not backups. The missing down file prevents rollback
142
- across the baseline; use the application's backup/recovery procedure for that boundary.
143
-
144
- Programmatic capture is
145
- `captureBaseline({ connection, directory?, namespace?, schemas?, onWarning? })` and returns
146
- `{ id, upPath }`. It requires a native libpq connection string; an explicitly empty `schemas` list
147
- fails. `adoptBaseline({ connection, directory?, namespace? })` accepts Tenon's runtime connection
148
- forms and returns newly recorded IDs.
98
+ ```sh
99
+ npx tenon migrate --directory migrations --namespace app
100
+ ```
101
+
102
+ Do not adopt a fresh database. Migration executes the committed baseline and subsequent files.
103
+ The dump omits ownership, grants/revokes, and default privileges; objects are recreated under the
104
+ migration role. Deployment provisioning must supply required roles, owners, and grants.
105
+
106
+ For `SECURITY DEFINER` functions, the recreated owner controls authority. PostgreSQL ordinarily grants
107
+ `PUBLIC` execution on new functions. Put fixed policy such as revoking public execution in reviewed
108
+ baseline SQL before first adoption, or in a forward migration. Supply deployment-specific role names
109
+ and grants during provisioning. Verify permitted and denied operations on adopted and fresh databases.
110
+
111
+ Test preserved existing rows, repeatable adoption, later migrations, and fresh replay. A schema-only
112
+ baseline is not a backup; the missing down file prevents rollback across it. Use the application's
113
+ backup/recovery procedure for that boundary.
114
+
115
+ ### Baseline API
116
+
117
+ - `captureBaseline({ connection, directory?, namespace?, schemas?, onWarning? })` returns
118
+ `{ id, upPath }`. Connection is a native libpq connection string passed to pg_dump. Omitting
119
+ `schemas` captures the whole database schema; an empty list rejects. Warnings default to
120
+ `console.warn`.
121
+ - `adoptBaseline({ connection, directory?, namespace? })` accepts Tenon's runtime connection forms
122
+ and returns newly recorded IDs. It requires `0001_baseline.up.sql` with no down file.
123
+
124
+ Defaults are directory `migrations` and namespace `public`. See [migrations](migrations.md) for
125
+ history validation, per-file transactions, and locking.