@kequtech/tenon-orm 0.0.0-stage → 0.1.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/LICENSE +15 -0
- package/README.md +99 -2
- package/dist/affected-rows.d.ts +11 -0
- package/dist/affected-rows.js +30 -0
- package/dist/baseline.d.ts +19 -0
- package/dist/baseline.js +100 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +158 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +54 -0
- package/dist/connection.d.ts +15 -0
- package/dist/connection.js +51 -0
- package/dist/database.d.ts +7 -0
- package/dist/database.js +44 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +18 -0
- package/dist/generate.d.ts +9 -0
- package/dist/generate.js +34 -0
- package/dist/generation/catalog.d.ts +19 -0
- package/dist/generation/catalog.js +35 -0
- package/dist/generation/row-types.d.ts +2 -0
- package/dist/generation/row-types.js +115 -0
- package/dist/generation/schema-summary.d.ts +6 -0
- package/dist/generation/schema-summary.js +114 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +7 -0
- package/dist/migrations/baseline-sql.d.ts +3 -0
- package/dist/migrations/baseline-sql.js +38 -0
- package/dist/migrations/connection.d.ts +9 -0
- package/dist/migrations/connection.js +65 -0
- package/dist/migrations/files.d.ts +21 -0
- package/dist/migrations/files.js +131 -0
- package/dist/migrations/history.d.ts +12 -0
- package/dist/migrations/history.js +30 -0
- package/dist/migrations/sql.d.ts +2 -0
- package/dist/migrations/sql.js +94 -0
- package/dist/migrations.d.ts +10 -0
- package/dist/migrations.js +68 -0
- package/dist/query-options.d.ts +15 -0
- package/dist/query-options.js +35 -0
- package/dist/query.d.ts +31 -0
- package/dist/query.js +110 -0
- package/dist/safeql/composition.d.ts +12 -0
- package/dist/safeql/composition.js +194 -0
- package/dist/safeql/eslint.d.ts +2 -0
- package/dist/safeql/eslint.js +45 -0
- package/dist/safeql/plugin.d.ts +3 -0
- package/dist/safeql/plugin.js +43 -0
- package/dist/safeql.d.ts +6 -0
- package/dist/safeql.js +7 -0
- package/dist/sql.d.ts +5 -0
- package/dist/sql.js +28 -0
- package/dist/transaction-sql.d.ts +2 -0
- package/dist/transaction-sql.js +67 -0
- package/dist/transaction.d.ts +4 -0
- package/dist/transaction.js +75 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +1 -0
- package/dist/validation.js +10 -0
- package/docs/adoption-guide.md +91 -0
- package/docs/agent-guide.md +289 -0
- package/docs/human-guide.md +253 -0
- package/docs/reference.md +456 -0
- package/package.json +92 -4
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { CompositionError, isExport, renderValue } from './composition.js';
|
|
3
|
+
/** Loaded through the exported SafeQL plugin descriptor. */
|
|
4
|
+
export function factory() {
|
|
5
|
+
return {
|
|
6
|
+
name: 'tenon',
|
|
7
|
+
connectionDefaults: {
|
|
8
|
+
inferLiterals: false,
|
|
9
|
+
overrides: {
|
|
10
|
+
types: {
|
|
11
|
+
int8: 'string',
|
|
12
|
+
numeric: 'string',
|
|
13
|
+
json: 'unknown',
|
|
14
|
+
jsonb: 'unknown',
|
|
15
|
+
date: 'Date | number',
|
|
16
|
+
timestamp: 'Date | number',
|
|
17
|
+
timestamptz: 'Date | number',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
onTarget({ node, context }) {
|
|
22
|
+
const template = context.parser.esTreeNodeToTSNodeMap.get(node);
|
|
23
|
+
if (!ts.isTaggedTemplateExpression(template) ||
|
|
24
|
+
!isExport(template.tag, context.checker, 'sql'))
|
|
25
|
+
return false;
|
|
26
|
+
// The companion ESLint rule reports unsupported forms. Skip their SQL analysis.
|
|
27
|
+
if (node.quasi.quasis.some(({ value }) => value.raw !== value.cooked))
|
|
28
|
+
return false;
|
|
29
|
+
// Defer to SafeQL's configured targets, including per-connection tag selection.
|
|
30
|
+
return undefined;
|
|
31
|
+
},
|
|
32
|
+
onExpression({ context }) {
|
|
33
|
+
try {
|
|
34
|
+
return renderValue(context.tsNode, context.checker);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (!(error instanceof CompositionError))
|
|
38
|
+
throw error;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/safeql.d.ts
ADDED
package/dist/safeql.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
/** Add to a connection's plugins in normal SafeQL configuration. */
|
|
3
|
+
export const safeqlPlugin = {
|
|
4
|
+
package: fileURLToPath(new URL('./safeql/plugin.js', import.meta.url)),
|
|
5
|
+
};
|
|
6
|
+
/** Register as `tenon` in ESLint and enable `tenon/check-composition`. */
|
|
7
|
+
export { eslintPlugin } from './safeql/eslint.js';
|
package/dist/sql.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function validateName(value: string): void;
|
|
2
|
+
export declare function quote(name: string): string;
|
|
3
|
+
export declare function qualified(namespace: string, table: string): string;
|
|
4
|
+
/** Shared by query assembly and static checking so separators produce identical SQL. */
|
|
5
|
+
export declare function joinSeparator(value: unknown): string;
|
package/dist/sql.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function validateName(value) {
|
|
2
|
+
if (typeof value !== 'string' ||
|
|
3
|
+
!value ||
|
|
4
|
+
value.includes('\0') ||
|
|
5
|
+
Buffer.byteLength(value) > 63) {
|
|
6
|
+
throw new Error('Identifiers must contain 1–63 UTF-8 bytes and no NUL character.');
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function quote(name) {
|
|
10
|
+
validateName(name);
|
|
11
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
12
|
+
}
|
|
13
|
+
export function qualified(namespace, table) {
|
|
14
|
+
return `${quote(namespace)}.${quote(table)}`;
|
|
15
|
+
}
|
|
16
|
+
/** Shared by query assembly and static checking so separators produce identical SQL. */
|
|
17
|
+
export function joinSeparator(value) {
|
|
18
|
+
switch (value) {
|
|
19
|
+
case 'AND':
|
|
20
|
+
return ' AND ';
|
|
21
|
+
case 'OR':
|
|
22
|
+
return ' OR ';
|
|
23
|
+
case ',':
|
|
24
|
+
return ', ';
|
|
25
|
+
default:
|
|
26
|
+
throw new Error('sql.join separator must be "AND", "OR", or ",".');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Read only the command prefix, leaving strings, bodies and SQL syntax to pg. */
|
|
2
|
+
function commandPrefix(sql) {
|
|
3
|
+
const words = [];
|
|
4
|
+
let i = 0;
|
|
5
|
+
while (i < sql.length && words.length < 3) {
|
|
6
|
+
if (/\s/.test(sql[i])) {
|
|
7
|
+
i++;
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
if (!words.length && sql[i] === ';') {
|
|
11
|
+
i++;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (sql.startsWith('--', i)) {
|
|
15
|
+
i += 2;
|
|
16
|
+
while (i < sql.length && sql[i] !== '\n' && sql[i] !== '\r') {
|
|
17
|
+
i++;
|
|
18
|
+
}
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (sql.startsWith('/*', i)) {
|
|
22
|
+
let depth = 1;
|
|
23
|
+
i += 2;
|
|
24
|
+
while (i < sql.length && depth) {
|
|
25
|
+
if (sql.startsWith('/*', i)) {
|
|
26
|
+
depth++;
|
|
27
|
+
i += 2;
|
|
28
|
+
}
|
|
29
|
+
else if (sql.startsWith('*/', i)) {
|
|
30
|
+
depth--;
|
|
31
|
+
i += 2;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (sql[i] === '(') {
|
|
40
|
+
words.push('(');
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
const word = /^[A-Za-z_\u0080-\uFFFF][A-Za-z_0-9$\u0080-\uFFFF]*/.exec(sql.slice(i));
|
|
44
|
+
if (!word) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
words.push(word[0].toUpperCase());
|
|
48
|
+
i += word[0].length;
|
|
49
|
+
}
|
|
50
|
+
return words;
|
|
51
|
+
}
|
|
52
|
+
/** Public SQL cannot take ownership away from Tenon's callback transaction. */
|
|
53
|
+
export function validateTransactionSql(sql, name) {
|
|
54
|
+
const [first, second, third] = commandPrefix(sql);
|
|
55
|
+
const rollbackTo = first === 'ROLLBACK' &&
|
|
56
|
+
(second === 'TO' || ((second === 'WORK' || second === 'TRANSACTION') && third === 'TO'));
|
|
57
|
+
if (first === 'BEGIN' ||
|
|
58
|
+
first === 'COMMIT' ||
|
|
59
|
+
first === 'END' ||
|
|
60
|
+
first === 'ABORT' ||
|
|
61
|
+
(first === 'ROLLBACK' && !rollbackTo) ||
|
|
62
|
+
(first === 'START' && second === 'TRANSACTION') ||
|
|
63
|
+
// A SQL prepared statement named transaction is not a prepared transaction.
|
|
64
|
+
(first === 'PREPARE' && second === 'TRANSACTION' && third !== 'AS' && third !== '(')) {
|
|
65
|
+
throw new Error(`${name} cannot start or end a transaction; use db.transaction() to manage transaction boundaries.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
import type { QueryExecutor } from './connection.js';
|
|
3
|
+
/** The callback owns one connection and expires before commit or rollback begins. */
|
|
4
|
+
export declare function withTransaction<T>(pool: Pool, work: (query: QueryExecutor) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,75 @@
|
|
|
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 releaseError;
|
|
8
|
+
let savepointId = 0;
|
|
9
|
+
let pending = Promise.resolve();
|
|
10
|
+
let recoveryFailure;
|
|
11
|
+
const query = async (text, input, readResult, guarded) => {
|
|
12
|
+
if (!active)
|
|
13
|
+
throw new Error('The transaction has finished.');
|
|
14
|
+
// Queue whole operations, including savepoint cleanup. Otherwise concurrent
|
|
15
|
+
// calls could place another write inside a savepoint that gets rolled back.
|
|
16
|
+
const operation = pending.then(async () => {
|
|
17
|
+
connection.check();
|
|
18
|
+
if (recoveryFailure)
|
|
19
|
+
throw recoveryFailure;
|
|
20
|
+
if (!guarded)
|
|
21
|
+
return readResult(await executeSql(client, text, input));
|
|
22
|
+
const savepoint = `tenon_affected_rows_${++savepointId}`;
|
|
23
|
+
await client.query(`SAVEPOINT ${savepoint}`);
|
|
24
|
+
try {
|
|
25
|
+
const result = readResult(await executeSql(client, text, input));
|
|
26
|
+
await client.query(`RELEASE SAVEPOINT ${savepoint}`);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
try {
|
|
31
|
+
await client.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
32
|
+
await client.query(`RELEASE SAVEPOINT ${savepoint}`);
|
|
33
|
+
}
|
|
34
|
+
catch (rollbackError) {
|
|
35
|
+
// Even if application code catches this error, the outer transaction
|
|
36
|
+
// must not commit when the rejected write could still be present.
|
|
37
|
+
recoveryFailure = new AggregateError([error, rollbackError], 'Operation and savepoint recovery failed.');
|
|
38
|
+
throw recoveryFailure;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
pending = operation.then(() => { }, () => { });
|
|
44
|
+
return operation;
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
await client.query('BEGIN');
|
|
48
|
+
const result = await work(query);
|
|
49
|
+
active = false;
|
|
50
|
+
await pending;
|
|
51
|
+
connection.check();
|
|
52
|
+
if (recoveryFailure)
|
|
53
|
+
throw recoveryFailure;
|
|
54
|
+
const committed = await client.query('COMMIT');
|
|
55
|
+
if (committed.command !== 'COMMIT') {
|
|
56
|
+
throw new Error('PostgreSQL rolled back the aborted transaction.');
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
active = false;
|
|
62
|
+
await pending;
|
|
63
|
+
try {
|
|
64
|
+
await client.query('ROLLBACK');
|
|
65
|
+
}
|
|
66
|
+
catch (rollbackError) {
|
|
67
|
+
releaseError = rollbackError instanceof Error ? rollbackError : new Error('Rollback failed.');
|
|
68
|
+
throw new AggregateError([error, rollbackError], 'Transaction and rollback failed.');
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
connection.release(releaseError);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { QueryResult, QueryResultRow } from 'pg';
|
|
2
|
+
import type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
|
|
3
|
+
import type { SqlQuery } from './query.js';
|
|
4
|
+
export interface QueryClient {
|
|
5
|
+
query<Row extends QueryResultRow>(query: SqlQuery<Row>, options?: QueryOptions & {
|
|
6
|
+
readonly returns?: 'native';
|
|
7
|
+
}): NoInfer<Promise<QueryResult<Row>>>;
|
|
8
|
+
query<Row extends QueryResultRow, Mode extends QueryReturnMode>(query: SqlQuery<Row>, options: QueryOptions & {
|
|
9
|
+
readonly returns: Mode;
|
|
10
|
+
}): NoInfer<Promise<QueryOutput<Row, Mode>>>;
|
|
11
|
+
query<Row extends QueryResultRow>(query: SqlQuery<Row>, options: QueryOptions | undefined): NoInfer<Promise<QueryOutput<Row, QueryReturnMode>>>;
|
|
12
|
+
}
|
|
13
|
+
export type Transaction = QueryClient;
|
|
14
|
+
export type Database = Transaction & {
|
|
15
|
+
/** Await all work inside the callback. Commit on success; rollback on an escaping error. */
|
|
16
|
+
transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
/** One completed public query call, including rollback/commit time for guarded calls. */
|
|
20
|
+
export interface QueryEvent {
|
|
21
|
+
readonly label?: string;
|
|
22
|
+
readonly durationMs: number;
|
|
23
|
+
readonly outcome: 'success' | 'error';
|
|
24
|
+
readonly command?: string;
|
|
25
|
+
readonly rowCount?: number | null;
|
|
26
|
+
/** Stable Tenon or PostgreSQL error code, when available. */
|
|
27
|
+
readonly errorCode?: string;
|
|
28
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function plainObject(value: unknown, name: string): Record<string, unknown>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function plainObject(value, name) {
|
|
2
|
+
if (value === null || typeof value !== 'object') {
|
|
3
|
+
throw new Error(`${name} must be a plain object.`);
|
|
4
|
+
}
|
|
5
|
+
const prototype = Object.getPrototypeOf(value);
|
|
6
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
7
|
+
throw new Error(`${name} must be a plain object.`);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Adopting Tenon from another ORM
|
|
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.
|
|
8
|
+
|
|
9
|
+
## Replace application queries
|
|
10
|
+
|
|
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 table row types will help your application, generate them from the existing database:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx tenon generate --out src/generated-schema.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 pg values: check bigint/numeric strings, dates, JSON, and arrays when
|
|
26
|
+
the old ORM transformed them.
|
|
27
|
+
4. If using optional SQL checking, configure the
|
|
28
|
+
[SafeQL integration](reference.md#sql-checking-with-safeql) to generate query annotations and
|
|
29
|
+
check them against the intended schema. 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.
|
|
35
|
+
|
|
36
|
+
## Move migration ownership to Tenon
|
|
37
|
+
|
|
38
|
+
Choose one final schema checkpoint for the old runner and bring existing target databases to it.
|
|
39
|
+
Disable its migration/schema synchronization before handing ownership to Tenon. The tools do not
|
|
40
|
+
share locks.
|
|
41
|
+
|
|
42
|
+
Capture one baseline from the reference database into an empty migration directory:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npx tenon baseline capture --directory migrations --namespace public
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This uses native `pg_dump --schema-only --no-owner --no-privileges` and writes
|
|
49
|
+
`0001_baseline.up.sql` without a down file. It requires a compatible `pg_dump` on PATH; prefer the
|
|
50
|
+
server's major. Default scope is the whole database schema. Repeat `--schema name` to select schemas
|
|
51
|
+
explicitly, accounting for dependencies, extensions, and required roles. Tenon's selected history
|
|
52
|
+
table is excluded; old ORM metadata is preserved for deliberate cleanup later.
|
|
53
|
+
|
|
54
|
+
Capture changes no source data, migration history, or sequence positions. Review and commit the SQL.
|
|
55
|
+
Keep the capture header: it identifies native dump SQL that bypasses the authored-migration scanner.
|
|
56
|
+
It is a convention, not proof of provenance; do not use it on handwritten migrations. Normal
|
|
57
|
+
transactional execution and checksums still apply.
|
|
58
|
+
|
|
59
|
+
On each existing database already at that checkpoint:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npx tenon baseline adopt
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Adoption records the baseline checksum without executing the SQL or comparing schemas. It requires
|
|
66
|
+
the selected namespace to exist. Repeating matching adoption is harmless, including after later
|
|
67
|
+
migrations; conflicting history fails.
|
|
68
|
+
|
|
69
|
+
On a fresh database, run migrations instead:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
npx tenon migrate
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
That executes the baseline and subsequent migrations. Use the same committed baseline for every
|
|
76
|
+
environment. Do not run adoption unconditionally on fresh databases. Existing databases adopt once
|
|
77
|
+
and then use ordinary `migrate`.
|
|
78
|
+
|
|
79
|
+
Use the development [`db:migrate` script](human-guide.md#create-a-table) to apply subsequent
|
|
80
|
+
migrations and update table types together. Production continues to run only `npx tenon migrate`.
|
|
81
|
+
Verify both an existing database's upgrade and a fresh replay. Baselines contain schema only; they
|
|
82
|
+
do not back up rows or sequence positions. The missing down file prevents normal rollback through
|
|
83
|
+
the baseline. Keep the application's backup and recovery procedure.
|
|
84
|
+
|
|
85
|
+
Programmatic equivalents:
|
|
86
|
+
|
|
87
|
+
- `captureBaseline({ connection, directory?, namespace?, schemas?, onWarning? })` requires a
|
|
88
|
+
nonempty native libpq connection string and returns `{ id, upPath }`. Omit `schemas` for the whole
|
|
89
|
+
database schema; an explicitly empty list is rejected.
|
|
90
|
+
- `adoptBaseline({ connection, directory?, namespace? })` accepts the usual Tenon connection options
|
|
91
|
+
and returns newly recorded migration IDs.
|