@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.
- package/CHANGELOG.md +52 -36
- package/README.md +27 -70
- package/dist/affected-rows.js +6 -6
- package/dist/baseline.js +4 -4
- package/dist/client.js +1 -1
- package/dist/connection.d.ts +15 -16
- package/dist/connection.js +124 -14
- package/dist/database.js +36 -17
- package/dist/driver/types.d.ts +1 -1
- package/dist/driver/types.js +11 -11
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -0
- package/dist/generate.js +7 -7
- package/dist/generation/catalog.d.ts +2 -2
- package/dist/generation/catalog.js +4 -4
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +11 -11
- package/dist/generation/table-types.js +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -1
- package/dist/inspection/catalog.d.ts +4 -3
- package/dist/inspection/catalog.js +13 -10
- package/dist/inspection/cli.js +3 -3
- package/dist/inspection/connection.d.ts +2 -2
- package/dist/inspection/connection.js +22 -22
- package/dist/inspection/options.d.ts +3 -3
- package/dist/inspection/options.js +9 -1
- package/dist/inspection/protocol.d.ts +7 -0
- package/dist/inspection/protocol.js +45 -0
- package/dist/inspection/result.d.ts +1 -1
- package/dist/inspection/result.js +6 -6
- package/dist/inspection/types.d.ts +10 -10
- package/dist/migrations/connection.d.ts +3 -2
- package/dist/migrations/connection.js +18 -20
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -7
- package/dist/query-options.js +5 -5
- package/dist/query.js +2 -2
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +19 -14
- package/dist/types.d.ts +7 -3
- package/docs/adoption-guide.md +109 -122
- package/docs/agent-guide.md +124 -228
- package/docs/human-guide.md +44 -55
- package/docs/mutations.md +58 -76
- package/docs/query-inspection.md +97 -89
- package/docs/reference.md +264 -319
- package/package.json +4 -4
- package/dist/driver/array.d.ts +0 -5
- package/dist/driver/array.js +0 -70
- package/dist/driver/lifecycle.d.ts +0 -15
- package/dist/driver/lifecycle.js +0 -57
- package/dist/driver/session.d.ts +0 -8
- package/dist/driver/session.js +0 -92
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
10
|
-
export declare function createHistoryTable(client:
|
|
11
|
-
export declare function recordMigration(client:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
29
|
+
await client.query(`INSERT INTO ${qualified(namespace, migrationTable)} (id, name, checksum) VALUES ($1, $2, $3)`, [migration.id, migration.name, migration.checksum]);
|
|
30
30
|
}
|
package/dist/migrations.js
CHANGED
|
@@ -28,25 +28,25 @@ async function execute(options, direction, steps) {
|
|
|
28
28
|
if (!selected.length) {
|
|
29
29
|
return [];
|
|
30
30
|
}
|
|
31
|
-
await client.
|
|
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.
|
|
36
|
+
await client.query('BEGIN');
|
|
37
37
|
// Match the string-literal rules used by the authored migration scanner.
|
|
38
|
-
await client.
|
|
39
|
-
await client.
|
|
40
|
-
await client.
|
|
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.
|
|
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.
|
|
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
|
}
|
package/dist/query-options.js
CHANGED
|
@@ -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
|
|
30
|
-
|
|
31
|
-
|
|
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 >
|
|
114
|
-
throw new Error(`
|
|
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) {
|
package/dist/transaction.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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:
|
|
4
|
+
export declare function withTransaction<T>(pool: Pool, work: (query: QueryExecutor) => Promise<T>): Promise<T>;
|
package/dist/transaction.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
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
|
|
6
|
-
const
|
|
4
|
+
const client = await pool.connect();
|
|
5
|
+
const connection = watchClient(client);
|
|
7
6
|
let active = true;
|
|
8
|
-
let
|
|
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.
|
|
24
|
+
await client.query(`SAVEPOINT ${savepoint}`);
|
|
25
25
|
try {
|
|
26
26
|
const result = readResult(await executeSql(client, text, input));
|
|
27
|
-
await client.
|
|
27
|
+
await client.query(`RELEASE SAVEPOINT ${savepoint}`);
|
|
28
28
|
return result;
|
|
29
29
|
}
|
|
30
30
|
catch (error) {
|
|
31
31
|
try {
|
|
32
|
-
await client.
|
|
33
|
-
await client.
|
|
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.
|
|
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
|
-
|
|
65
|
-
await client.unsafe('ROLLBACK');
|
|
70
|
+
await client.query('ROLLBACK');
|
|
66
71
|
}
|
|
67
72
|
catch (rollbackError) {
|
|
68
|
-
|
|
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
|
-
|
|
79
|
+
connection.release(releaseError);
|
|
75
80
|
}
|
|
76
81
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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:
|
|
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 {
|
package/docs/adoption-guide.md
CHANGED
|
@@ -1,151 +1,138 @@
|
|
|
1
|
-
# Adopting Tenon
|
|
1
|
+
# Adopting Tenon
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[
|
|
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
|
-
##
|
|
7
|
+
## Preserve connections and behavior
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
+
## Replace queries
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
import { sql } from '@kequtech/tenon-orm';
|
|
35
|
+
1. Optionally generate persistence types from the existing schema:
|
|
46
36
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```
|
|
37
|
+
```sh
|
|
38
|
+
npx tenon generate --namespace public --out src/generated-schema.d.ts
|
|
39
|
+
```
|
|
50
40
|
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
queries or detect stale annotations elsewhere.
|
|
65
|
+
## Transfer migration ownership
|
|
75
66
|
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
For an application owning `app` in a shared database:
|
|
83
74
|
|
|
84
75
|
```sh
|
|
85
|
-
npx tenon baseline capture --directory migrations --namespace
|
|
76
|
+
npx tenon baseline capture --directory migrations --namespace app --schema app
|
|
86
77
|
```
|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
`
|
|
90
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
106
|
+
```sh
|
|
107
|
+
npx tenon baseline adopt --directory migrations --namespace app
|
|
108
|
+
npx tenon migrate --directory migrations --namespace app
|
|
109
|
+
```
|
|
146
110
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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.
|