@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.
- package/CHANGELOG.md +66 -0
- package/README.md +81 -26
- package/dist/affected-rows.js +0 -1
- package/dist/baseline.js +3 -5
- package/dist/check/contracts.d.ts +6 -0
- package/dist/check/contracts.js +206 -0
- package/dist/check/project.d.ts +18 -0
- package/dist/check/project.js +224 -0
- package/dist/check/source.d.ts +51 -0
- package/dist/check/source.js +629 -0
- package/dist/check/types.d.ts +36 -0
- package/dist/check/types.js +1 -0
- package/dist/check.d.ts +4 -0
- package/dist/check.js +147 -0
- package/dist/cli.js +40 -27
- package/dist/connection.d.ts +5 -12
- package/dist/connection.js +12 -121
- package/dist/database.js +10 -8
- package/dist/describe.js +10 -3
- package/dist/generate.d.ts +13 -3
- package/dist/generate.js +52 -25
- package/dist/generation/catalog.d.ts +5 -6
- package/dist/generation/catalog.js +7 -9
- package/dist/generation/output.js +2 -4
- package/dist/generation/render.d.ts +3 -0
- package/dist/generation/render.js +60 -0
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/inspection/catalog.d.ts +3 -6
- package/dist/inspection/catalog.js +9 -18
- package/dist/inspection/cli.d.ts +12 -0
- package/dist/inspection/cli.js +9 -4
- package/dist/inspection/connection.d.ts +2 -3
- package/dist/inspection/connection.js +20 -22
- package/dist/inspection/options.js +0 -6
- package/dist/inspection/result.d.ts +3 -2
- package/dist/inspection/result.js +13 -26
- package/dist/inspection/types.d.ts +6 -4
- package/dist/migrations/connection.d.ts +3 -3
- package/dist/migrations/connection.js +14 -21
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -9
- package/dist/query-options.d.ts +1 -1
- package/dist/query.js +1 -1
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +21 -77
- package/dist/types.d.ts +4 -1
- package/docs/adoption-guide.md +88 -111
- package/docs/agent-guide.md +94 -147
- package/docs/connections.md +185 -0
- package/docs/human-guide.md +133 -146
- package/docs/migrations.md +133 -0
- package/docs/mutations.md +110 -60
- package/docs/query-checking.md +199 -0
- package/docs/query-inspection.md +89 -58
- package/docs/reference.md +232 -437
- package/docs/schema-types.md +263 -0
- package/docs/sql.md +212 -0
- package/package.json +11 -7
- package/dist/driver/types.d.ts +0 -10
- package/dist/driver/types.js +0 -49
- package/dist/generation/table-types.d.ts +0 -2
- package/dist/generation/table-types.js +0 -96
- package/dist/inspection/protocol.d.ts +0 -7
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
29
|
+
await client.queryClassic(`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,23 @@ async function execute(options, direction, steps) {
|
|
|
28
28
|
if (!selected.length) {
|
|
29
29
|
return [];
|
|
30
30
|
}
|
|
31
|
-
await client.
|
|
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.
|
|
36
|
+
await client.queryClassic('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.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.
|
|
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.
|
|
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
|
}
|
package/dist/query-options.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/transaction.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Pool } from '
|
|
1
|
+
import type { Pool } from '@kequtech/pgclear';
|
|
2
2
|
import type { QueryExecutor } from './connection.js';
|
|
3
|
-
/**
|
|
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>;
|
package/dist/transaction.js
CHANGED
|
@@ -1,81 +1,25 @@
|
|
|
1
|
-
import { executeSql
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
18
|
+
return await work(query);
|
|
71
19
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
20
|
+
finally {
|
|
21
|
+
active = false;
|
|
22
|
+
await pending;
|
|
75
23
|
}
|
|
76
|
-
|
|
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 '
|
|
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>;
|
package/docs/adoption-guide.md
CHANGED
|
@@ -1,72 +1,47 @@
|
|
|
1
1
|
# Adopting Tenon
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
68
|
-
runner before handover; the runners do not share locks. Capture from a trusted
|
|
69
|
-
preferably a disposable replay of the
|
|
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`
|
|
81
|
-
file. Install compatible PostgreSQL client tools on PATH
|
|
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
|
-
|
|
|
84
|
-
| --------------------- |
|
|
85
|
-
| `--namespace app` | Tenon history table to exclude
|
|
86
|
-
| `--schema app` |
|
|
87
|
-
| Runtime `search_path` |
|
|
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.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
96
|
-
extensions, and
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`{
|
|
147
|
-
|
|
148
|
-
|
|
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.
|