@kequtech/tenon-orm 0.2.2 → 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 +43 -42
- package/README.md +4 -4
- 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 -22
- package/dist/connection.js +122 -25
- package/dist/database.js +10 -17
- package/dist/driver/types.d.ts +1 -1
- package/dist/driver/types.js +11 -11
- 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 +1 -2
- package/dist/inspection/catalog.d.ts +4 -3
- package/dist/inspection/catalog.js +13 -10
- package/dist/inspection/connection.d.ts +2 -2
- package/dist/inspection/connection.js +22 -22
- 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 +2 -2
- 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 +4 -4
- package/dist/query.js +2 -2
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +19 -14
- package/dist/types.d.ts +2 -2
- package/docs/adoption-guide.md +30 -27
- package/docs/agent-guide.md +53 -48
- package/docs/human-guide.md +21 -20
- package/docs/mutations.md +13 -10
- package/docs/query-inspection.md +12 -14
- package/docs/reference.md +130 -132
- package/package.json +3 -2
- 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/listener.d.ts +0 -3
- package/dist/driver/listener.js +0 -63
- package/dist/driver/session.d.ts +0 -8
- package/dist/driver/session.js +0 -92
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { quote } from '../sql.js';
|
|
2
2
|
/** Include only domains reached through selected columns, base domains or arrays. */
|
|
3
3
|
export async function domainSummaries(client, typeOids) {
|
|
4
|
-
const domains = await client.
|
|
4
|
+
const domains = await client.query(`
|
|
5
5
|
WITH RECURSIVE referenced(oid) AS (
|
|
6
6
|
SELECT unnest($1::oid[])
|
|
7
7
|
UNION
|
|
@@ -16,10 +16,10 @@ export async function domainSummaries(client, typeOids) {
|
|
|
16
16
|
FROM referenced r JOIN pg_catalog.pg_type t ON t.oid = r.oid
|
|
17
17
|
JOIN pg_catalog.pg_type base ON base.oid = t.typbasetype
|
|
18
18
|
WHERE t.typtype = 'd'
|
|
19
|
-
ORDER BY pg_catalog.format_type(t.oid, NULL) COLLATE "C"`, [
|
|
19
|
+
ORDER BY pg_catalog.format_type(t.oid, NULL) COLLATE "C"`, [typeOids]);
|
|
20
20
|
const summaries = [];
|
|
21
|
-
for (const domain of domains) {
|
|
22
|
-
const constraints = await client.
|
|
21
|
+
for (const domain of domains.rows) {
|
|
22
|
+
const constraints = await client.query(`
|
|
23
23
|
SELECT conname AS name, pg_catalog.pg_get_constraintdef(oid, false) AS definition
|
|
24
24
|
FROM pg_catalog.pg_constraint WHERE contypid = $1::oid
|
|
25
25
|
ORDER BY conname COLLATE "C"`, [domain.oid]);
|
|
@@ -28,14 +28,14 @@ export async function domainSummaries(client, typeOids) {
|
|
|
28
28
|
(domain.collation === null ? '' : ` COLLATE ${domain.collation}`) +
|
|
29
29
|
(domain.expression === null ? '' : ` DEFAULT ${domain.expression}`) +
|
|
30
30
|
(domain.nullable ? '' : ' NOT NULL'),
|
|
31
|
-
...constraints.map((constraint) => ` CONSTRAINT ${quote(constraint.name)} ${constraint.definition}`),
|
|
31
|
+
...constraints.rows.map((constraint) => ` CONSTRAINT ${quote(constraint.name)} ${constraint.definition}`),
|
|
32
32
|
].join('\n'));
|
|
33
33
|
}
|
|
34
34
|
return summaries;
|
|
35
35
|
}
|
|
36
36
|
/** Called inside generation's read-only snapshot, with a fixed local search_path. */
|
|
37
37
|
export async function tableSummary(client, oid, name) {
|
|
38
|
-
const columns = await client.
|
|
38
|
+
const columns = await client.query(`
|
|
39
39
|
SELECT a.attname AS name,
|
|
40
40
|
pg_catalog.format_type(a.atttypid, a.atttypmod)
|
|
41
41
|
|| CASE WHEN a.attnotnull THEN ' NOT NULL' ELSE '' END
|
|
@@ -63,12 +63,12 @@ export async function tableSummary(client, oid, name) {
|
|
|
63
63
|
ORDER BY a.attnum`, [oid]);
|
|
64
64
|
// Column definitions already describe NOT NULL; PostgreSQL 18 also stores it
|
|
65
65
|
// as a table constraint, which would duplicate every required column here.
|
|
66
|
-
const constraints = await client.
|
|
66
|
+
const constraints = await client.query(`
|
|
67
67
|
SELECT conname AS name, pg_catalog.pg_get_constraintdef(oid, false) AS definition
|
|
68
68
|
FROM pg_catalog.pg_constraint
|
|
69
69
|
WHERE conrelid = $1::oid AND conparentid = 0 AND contype IN ('p', 'u', 'f', 'c', 'x')
|
|
70
70
|
ORDER BY conname COLLATE "C"`, [oid]);
|
|
71
|
-
const indexes = await client.
|
|
71
|
+
const indexes = await client.query(`
|
|
72
72
|
SELECT pg_catalog.pg_get_indexdef(i.indexrelid, 0, false) AS definition,
|
|
73
73
|
pg_catalog.format('CREATE %sINDEX %I ON %s',
|
|
74
74
|
CASE WHEN i.indisunique THEN 'UNIQUE ' ELSE '' END, ic.relname,
|
|
@@ -83,9 +83,9 @@ export async function tableSummary(client, oid, name) {
|
|
|
83
83
|
ORDER BY ic.relname COLLATE "C"`, [oid]);
|
|
84
84
|
const lines = [
|
|
85
85
|
`TABLE ${quote(name)}`,
|
|
86
|
-
...columns.map((column) => ` ${quote(column.name)} ${column.definition}`),
|
|
87
|
-
...constraints.map((constraint) => ` CONSTRAINT ${quote(constraint.name)} ${constraint.definition}`),
|
|
88
|
-
...indexes.map((index) => {
|
|
86
|
+
...columns.rows.map((column) => ` ${quote(column.name)} ${column.definition}`),
|
|
87
|
+
...constraints.rows.map((constraint) => ` CONSTRAINT ${quote(constraint.name)} ${constraint.definition}`),
|
|
88
|
+
...indexes.rows.map((index) => {
|
|
89
89
|
// pg_get_indexdef always qualifies its target table. Remove only that
|
|
90
90
|
// known prefix, never matching/replacing namespace text inside SQL literals.
|
|
91
91
|
const qualifiedPrefix = index.prefix + index.qualification;
|
|
@@ -37,7 +37,7 @@ function columnTypes(value, column, onWarning) {
|
|
|
37
37
|
const underlying = value.declaredOid === value.oid
|
|
38
38
|
? ''
|
|
39
39
|
: ` with underlying type ${JSON.stringify(value.typeSchema)}.${JSON.stringify(value.type)} (OID ${value.oid})`;
|
|
40
|
-
onWarning(`Column ${column} uses PostgreSQL type ${JSON.stringify(value.declaredSchema)}.${JSON.stringify(value.declaredType)} (OID ${value.declaredOid})${underlying} without a verified
|
|
40
|
+
onWarning(`Column ${column} uses PostgreSQL type ${JSON.stringify(value.declaredSchema)}.${JSON.stringify(value.declaredType)} (OID ${value.declaredOid})${underlying} without a verified pg mapping; generated as unknown for reads and never for non-null writes.`);
|
|
41
41
|
types = { read: 'unknown', write: 'never' };
|
|
42
42
|
}
|
|
43
43
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
export type { Connection, ConnectionOptions, ConnectionUri, QueryResult } from './connection.js';
|
|
1
|
+
export type { Connection, ConnectionOptions, QueryResult } from './connection.js';
|
|
3
2
|
export type { AffectedRowsConstraint } from './affected-rows.js';
|
|
4
3
|
export { createTenon } from './database.js';
|
|
5
4
|
export type { TenonOptions } from './database.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type { Client } from 'pg';
|
|
2
|
+
import { type Description } from './protocol.js';
|
|
2
3
|
interface CatalogType {
|
|
3
4
|
oid: number;
|
|
4
5
|
schema: string;
|
|
@@ -16,10 +17,10 @@ interface ColumnSource {
|
|
|
16
17
|
notNull: boolean;
|
|
17
18
|
}
|
|
18
19
|
export interface StatementMetadata {
|
|
19
|
-
statement:
|
|
20
|
+
statement: Description;
|
|
20
21
|
types: readonly CatalogType[];
|
|
21
22
|
sources: readonly ColumnSource[];
|
|
22
23
|
}
|
|
23
24
|
/** Describe the statement and read its type and source-column catalog records. */
|
|
24
|
-
export declare function readStatementMetadata(client:
|
|
25
|
+
export declare function readStatementMetadata(client: Client, text: string): Promise<StatementMetadata>;
|
|
25
26
|
export {};
|
|
@@ -1,29 +1,32 @@
|
|
|
1
|
+
import { describe } from './protocol.js';
|
|
1
2
|
/** Describe the statement and read its type and source-column catalog records. */
|
|
2
3
|
export async function readStatementMetadata(client, text) {
|
|
3
|
-
const statement = await client
|
|
4
|
-
const oids = [
|
|
5
|
-
|
|
4
|
+
const statement = await describe(client, text);
|
|
5
|
+
const oids = [
|
|
6
|
+
...new Set([...statement.parameters, ...statement.fields.map((c) => c.dataTypeID)]),
|
|
7
|
+
];
|
|
8
|
+
const types = (await client.query(`
|
|
6
9
|
WITH RECURSIVE wanted(oid) AS (
|
|
7
|
-
SELECT unnest(string_to_array($
|
|
10
|
+
SELECT pg_catalog.unnest(pg_catalog.string_to_array($1, ',')::pg_catalog.oid[])
|
|
8
11
|
UNION
|
|
9
12
|
SELECT t.typbasetype FROM pg_catalog.pg_type t JOIN wanted w ON w.oid = t.oid
|
|
10
13
|
WHERE t.typbasetype <> 0
|
|
11
14
|
)
|
|
12
15
|
SELECT t.oid, n.nspname AS schema, t.typname AS name,
|
|
13
16
|
t.typtype AS kind, t.typbasetype AS base,
|
|
14
|
-
(SELECT to_json(array_agg(e.enumlabel::text ORDER BY e.enumsortorder))
|
|
17
|
+
(SELECT pg_catalog.to_json(pg_catalog.array_agg(e.enumlabel::text ORDER BY e.enumsortorder))
|
|
15
18
|
FROM pg_catalog.pg_enum e WHERE e.enumtypid = t.oid) AS choices
|
|
16
19
|
FROM wanted w JOIN pg_catalog.pg_type t ON t.oid = w.oid
|
|
17
20
|
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
18
|
-
|
|
19
|
-
const tables = [...new Set(statement.
|
|
20
|
-
const sources = await client
|
|
21
|
+
`, [oids.join(',')])).rows;
|
|
22
|
+
const tables = [...new Set(statement.fields.map((c) => c.tableID).filter(Boolean))];
|
|
23
|
+
const sources = (await client.query(`
|
|
21
24
|
SELECT c.oid, a.attnum AS number, n.nspname AS schema,
|
|
22
25
|
c.relname AS table, a.attname AS column, a.attnotnull AS "notNull"
|
|
23
26
|
FROM pg_catalog.pg_attribute a JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
|
|
24
27
|
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
25
|
-
WHERE c.oid = ANY(string_to_array($
|
|
28
|
+
WHERE c.oid = ANY(pg_catalog.string_to_array($1, ',')::pg_catalog.oid[])
|
|
26
29
|
AND a.attnum > 0 AND NOT a.attisdropped
|
|
27
|
-
|
|
30
|
+
`, [tables.join(',')])).rows;
|
|
28
31
|
return { statement, types, sources };
|
|
29
32
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Client } from 'pg';
|
|
2
2
|
import type { DescribeOptions, InspectionContext } from './types.js';
|
|
3
3
|
/** Bound every database statement, including bootstrap and metadata queries. */
|
|
4
|
-
export declare function withInspectionConnection<T>(options: Pick<DescribeOptions, 'connection' | 'session' | 'timeouts'>, work: (client:
|
|
4
|
+
export declare function withInspectionConnection<T>(options: Pick<DescribeOptions, 'connection' | 'session' | 'timeouts'>, work: (client: Client, context: InspectionContext) => Promise<T>): Promise<T>;
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { prepareArrays } from '../driver/array.js';
|
|
1
|
+
import { Client } from 'pg';
|
|
3
2
|
import { inspectionOptions } from './options.js';
|
|
4
3
|
/** Bound every database statement, including bootstrap and metadata queries. */
|
|
5
4
|
export async function withInspectionConnection(options, work) {
|
|
6
5
|
const { url, limits, session } = inspectionOptions(options);
|
|
7
|
-
const client =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
const client = new Client({
|
|
7
|
+
connectionString: url.toString(),
|
|
8
|
+
connectionTimeoutMillis: limits.connectMs,
|
|
9
|
+
statement_timeout: limits.statementMs,
|
|
10
|
+
lock_timeout: limits.lockMs,
|
|
11
|
+
pipeline: false,
|
|
13
12
|
});
|
|
13
|
+
// Disconnection errors outside an active query must still have a listener.
|
|
14
|
+
client.on('error', () => { });
|
|
14
15
|
try {
|
|
15
|
-
await client
|
|
16
|
-
|
|
17
|
-
set_config('
|
|
18
|
-
|
|
16
|
+
await client.connect();
|
|
17
|
+
await client.query(`SELECT
|
|
18
|
+
pg_catalog.set_config('statement_timeout', $1, false),
|
|
19
|
+
pg_catalog.set_config('lock_timeout', $2, false)`, [String(limits.statementMs), String(limits.lockMs)]);
|
|
19
20
|
for (const [key, name] of [
|
|
20
21
|
['role', 'role'],
|
|
21
22
|
['searchPath', 'search_path'],
|
|
@@ -23,22 +24,21 @@ export async function withInspectionConnection(options, work) {
|
|
|
23
24
|
]) {
|
|
24
25
|
const value = session[key];
|
|
25
26
|
if (value !== undefined)
|
|
26
|
-
await client
|
|
27
|
+
await client.query('SELECT pg_catalog.set_config($1, $2, false)', [name, value]);
|
|
27
28
|
}
|
|
28
|
-
const [context] = await client
|
|
29
|
-
SELECT current_database() AS database, session_user AS "sessionUser",
|
|
30
|
-
current_user AS "currentUser", current_setting('search_path') AS "searchPath",
|
|
31
|
-
to_json(current_schemas(true)) AS "resolvedSearchPath",
|
|
32
|
-
current_setting('TimeZone') AS "timeZone",
|
|
29
|
+
const { rows: [context], } = await client.query(`
|
|
30
|
+
SELECT pg_catalog.current_database() AS database, session_user AS "sessionUser",
|
|
31
|
+
current_user AS "currentUser", pg_catalog.current_setting('search_path') AS "searchPath",
|
|
32
|
+
pg_catalog.to_json(pg_catalog.current_schemas(true)) AS "resolvedSearchPath",
|
|
33
|
+
pg_catalog.current_setting('TimeZone') AS "timeZone",
|
|
33
34
|
(SELECT setting::integer FROM pg_catalog.pg_settings WHERE name='statement_timeout') AS "statementTimeoutMs",
|
|
34
35
|
(SELECT setting::integer FROM pg_catalog.pg_settings WHERE name='lock_timeout') AS "lockTimeoutMs"
|
|
35
|
-
|
|
36
|
+
`);
|
|
36
37
|
if (!context)
|
|
37
38
|
throw new Error('Inspection context was not returned.');
|
|
38
|
-
return await work(client, { mode: 'separate-session', readMappings: '
|
|
39
|
+
return await work(client, { mode: 'separate-session', readMappings: 'pg', ...context });
|
|
39
40
|
}
|
|
40
41
|
finally {
|
|
41
|
-
|
|
42
|
-
await client.end({ timeout: 1 });
|
|
42
|
+
await client.end();
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Client, FieldDef } from 'pg';
|
|
2
|
+
export interface Description {
|
|
3
|
+
parameters: number[];
|
|
4
|
+
fields: FieldDef[];
|
|
5
|
+
}
|
|
6
|
+
/** Describe through pg custom query dispatch; send no Bind or Execute messages. */
|
|
7
|
+
export declare function describe(client: Client, text: string): Promise<Description>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Describe through pg custom query dispatch; send no Bind or Execute messages. */
|
|
2
|
+
export function describe(client, text) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
let connection;
|
|
5
|
+
const description = { parameters: [], fields: [] };
|
|
6
|
+
const parameters = (message) => {
|
|
7
|
+
description.parameters = message.dataTypeIDs;
|
|
8
|
+
};
|
|
9
|
+
const cleanup = () => connection?.removeListener('parameterDescription', parameters);
|
|
10
|
+
const query = {
|
|
11
|
+
// pg wraps callback to manage query_timeout, including clearing its timer.
|
|
12
|
+
callback(error) {
|
|
13
|
+
cleanup();
|
|
14
|
+
if (error)
|
|
15
|
+
reject(error);
|
|
16
|
+
else
|
|
17
|
+
resolve(description);
|
|
18
|
+
},
|
|
19
|
+
submit(wire) {
|
|
20
|
+
connection = wire;
|
|
21
|
+
wire.once('parameterDescription', parameters);
|
|
22
|
+
try {
|
|
23
|
+
wire.parse({ name: '', text, types: [] }, false);
|
|
24
|
+
wire.describe({ type: 'S', name: '' }, false);
|
|
25
|
+
wire.sync();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return error instanceof Error
|
|
29
|
+
? error
|
|
30
|
+
: new Error('Describe dispatch failed', { cause: error });
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
handleRowDescription(message) {
|
|
34
|
+
description.fields = message.fields;
|
|
35
|
+
},
|
|
36
|
+
handleReadyForQuery() {
|
|
37
|
+
query.callback(null);
|
|
38
|
+
},
|
|
39
|
+
handleError(error) {
|
|
40
|
+
query.callback(error);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
client.query(query);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { StatementMetadata } from './catalog.js';
|
|
2
2
|
import type { InspectionContext, QueryDescription } from './types.js';
|
|
3
|
-
/** Convert database metadata to conservative
|
|
3
|
+
/** Convert database metadata to conservative pg types and diagnostics. */
|
|
4
4
|
export declare function queryDescription({ statement, types, sources }: StatementMetadata, context: InspectionContext): QueryDescription;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { arrayTypes, scalarTypes, valueTypes } from '../driver/types.js';
|
|
2
|
-
/** Convert database metadata to conservative
|
|
2
|
+
/** Convert database metadata to conservative pg types and diagnostics. */
|
|
3
3
|
export function queryDescription({ statement, types, sources }, context) {
|
|
4
4
|
const catalog = new Map(types.map((t) => [t.oid, t]));
|
|
5
5
|
const issues = new Map();
|
|
@@ -26,14 +26,14 @@ export function queryDescription({ statement, types, sources }, context) {
|
|
|
26
26
|
code: 'unmapped-type',
|
|
27
27
|
oid,
|
|
28
28
|
postgres: `${type.schema}.${type.name}`,
|
|
29
|
-
message: `No verified
|
|
29
|
+
message: `No verified pg mapping for ${type.schema}.${type.name} (OID ${oid}).`,
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
return { oid, postgres: `${type.schema}.${type.name}`, readType };
|
|
33
33
|
}
|
|
34
34
|
const sourceMap = new Map(sources.map((s) => [`${s.oid}:${s.number}`, s]));
|
|
35
35
|
const names = new Set();
|
|
36
|
-
const columns = statement.
|
|
36
|
+
const columns = statement.fields.map((column) => {
|
|
37
37
|
if (names.has(column.name)) {
|
|
38
38
|
issues.set(`column:${column.name}`, {
|
|
39
39
|
code: 'duplicate-column',
|
|
@@ -42,10 +42,10 @@ export function queryDescription({ statement, types, sources }, context) {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
names.add(column.name);
|
|
45
|
-
const source = sourceMap.get(`${column.
|
|
45
|
+
const source = sourceMap.get(`${column.tableID}:${column.columnID}`);
|
|
46
46
|
return {
|
|
47
47
|
name: column.name,
|
|
48
|
-
...describeType(column.
|
|
48
|
+
...describeType(column.dataTypeID),
|
|
49
49
|
nullability: 'unknown',
|
|
50
50
|
source: source
|
|
51
51
|
? {
|
|
@@ -57,7 +57,7 @@ export function queryDescription({ statement, types, sources }, context) {
|
|
|
57
57
|
: null,
|
|
58
58
|
};
|
|
59
59
|
});
|
|
60
|
-
const parameters = statement.
|
|
60
|
+
const parameters = statement.parameters.map(describeType);
|
|
61
61
|
if (columns.length) {
|
|
62
62
|
issues.set('nullability', {
|
|
63
63
|
code: 'nullability-unknown',
|
|
@@ -20,7 +20,7 @@ export interface InspectionContext {
|
|
|
20
20
|
readonly timeZone: string;
|
|
21
21
|
readonly statementTimeoutMs: number;
|
|
22
22
|
readonly lockTimeoutMs: number;
|
|
23
|
-
readonly readMappings: '
|
|
23
|
+
readonly readMappings: 'pg';
|
|
24
24
|
}
|
|
25
25
|
export interface DescribeOptions {
|
|
26
26
|
/** PostgreSQL connection URI. Inspection owns a separate, short-lived connection. */
|
|
@@ -50,7 +50,7 @@ export type DescriptionIssue = {
|
|
|
50
50
|
export interface DescribedType {
|
|
51
51
|
readonly oid: number;
|
|
52
52
|
readonly postgres: string;
|
|
53
|
-
/**
|
|
53
|
+
/** pg read representation, before considering result nullability. */
|
|
54
54
|
readonly readType: string;
|
|
55
55
|
}
|
|
56
56
|
export interface DescribedColumn extends DescribedType {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PoolClient } from 'pg';
|
|
2
|
+
import type { Connection } from '../connection.js';
|
|
2
3
|
export interface DatabaseToolOptions {
|
|
3
4
|
readonly connection: Connection;
|
|
4
5
|
readonly namespace?: string;
|
|
5
6
|
readonly directory?: string;
|
|
6
7
|
}
|
|
7
8
|
/** Coordinate tools independently of application sessions; PostgreSQL owns the wait. */
|
|
8
|
-
export declare function withMigrationLock<T>(options: DatabaseToolOptions, mode: 'shared' | 'exclusive', work: (client:
|
|
9
|
+
export declare function withMigrationLock<T>(options: DatabaseToolOptions, mode: 'shared' | 'exclusive', work: (client: PoolClient, namespace: string) => Promise<T>): Promise<T>;
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createConnection } from '../connection.js';
|
|
1
|
+
import { createConnection, watchClient } from '../connection.js';
|
|
3
2
|
import { validateName } from '../sql.js';
|
|
4
3
|
/** Coordinate tools independently of application sessions; PostgreSQL owns the wait. */
|
|
5
4
|
export async function withMigrationLock(options, mode, work) {
|
|
6
5
|
const namespace = options.namespace ?? 'public';
|
|
7
6
|
validateName(namespace);
|
|
8
|
-
const {
|
|
7
|
+
const { pool, close } = createConnection(options.connection);
|
|
9
8
|
const suffix = mode === 'shared' ? '_shared' : '';
|
|
10
9
|
const lockKey = `tenon:migrations:${namespace}`;
|
|
11
10
|
const errors = [];
|
|
11
|
+
let client;
|
|
12
12
|
let connection;
|
|
13
13
|
let locked = false;
|
|
14
14
|
let result;
|
|
15
15
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
await client.
|
|
16
|
+
client = await pool.connect();
|
|
17
|
+
connection = watchClient(client);
|
|
18
|
+
await client.query(`SELECT pg_advisory_lock${suffix}(hashtextextended($1, 0))`, [lockKey]);
|
|
19
19
|
locked = true;
|
|
20
20
|
result = await work(client, namespace);
|
|
21
21
|
connection.check();
|
|
@@ -24,37 +24,35 @@ export async function withMigrationLock(options, mode, work) {
|
|
|
24
24
|
errors.push(error);
|
|
25
25
|
}
|
|
26
26
|
finally {
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
let discard = false;
|
|
27
|
+
if (client) {
|
|
28
|
+
let releaseError;
|
|
30
29
|
try {
|
|
31
30
|
connection.check();
|
|
32
31
|
if (errors.length)
|
|
33
|
-
await client.
|
|
32
|
+
await client.query('ROLLBACK');
|
|
34
33
|
if (locked) {
|
|
35
|
-
await client.
|
|
34
|
+
await client.query(`SELECT pg_advisory_unlock${suffix}(hashtextextended($1, 0))`, [
|
|
36
35
|
lockKey,
|
|
37
36
|
]);
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
39
|
catch (error) {
|
|
41
40
|
errors.push(error);
|
|
42
|
-
|
|
41
|
+
releaseError =
|
|
42
|
+
error instanceof Error ? error : new Error('Tool connection cleanup failed.');
|
|
43
43
|
}
|
|
44
44
|
try {
|
|
45
|
-
|
|
45
|
+
connection.release(releaseError);
|
|
46
46
|
}
|
|
47
47
|
catch (error) {
|
|
48
48
|
errors.push(error);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
errors.push(error);
|
|
57
|
-
}
|
|
51
|
+
try {
|
|
52
|
+
await close(1000);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
errors.push(error);
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
58
|
if (errors.length === 1) {
|
|
@@ -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
|
@@ -27,9 +27,9 @@ export function queryOutput(options, result) {
|
|
|
27
27
|
if (options.returns === 'native')
|
|
28
28
|
return result;
|
|
29
29
|
if (options.returns === 'many')
|
|
30
|
-
return
|
|
31
|
-
if (result.length > 1 || (options.returns === 'one' && result.length === 0)) {
|
|
32
|
-
throw new CardinalityError(options.returns === 'oneOrNone' ? 'oneOrNone' : 'one', result.length, options.label);
|
|
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>;
|