@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +43 -42
  2. package/README.md +4 -4
  3. package/dist/affected-rows.js +6 -6
  4. package/dist/baseline.js +4 -4
  5. package/dist/client.js +1 -1
  6. package/dist/connection.d.ts +15 -22
  7. package/dist/connection.js +122 -25
  8. package/dist/database.js +10 -17
  9. package/dist/driver/types.d.ts +1 -1
  10. package/dist/driver/types.js +11 -11
  11. package/dist/generate.js +7 -7
  12. package/dist/generation/catalog.d.ts +2 -2
  13. package/dist/generation/catalog.js +4 -4
  14. package/dist/generation/schema-summary.d.ts +3 -3
  15. package/dist/generation/schema-summary.js +11 -11
  16. package/dist/generation/table-types.js +1 -1
  17. package/dist/index.d.ts +1 -2
  18. package/dist/inspection/catalog.d.ts +4 -3
  19. package/dist/inspection/catalog.js +13 -10
  20. package/dist/inspection/connection.d.ts +2 -2
  21. package/dist/inspection/connection.js +22 -22
  22. package/dist/inspection/protocol.d.ts +7 -0
  23. package/dist/inspection/protocol.js +45 -0
  24. package/dist/inspection/result.d.ts +1 -1
  25. package/dist/inspection/result.js +6 -6
  26. package/dist/inspection/types.d.ts +2 -2
  27. package/dist/migrations/connection.d.ts +3 -2
  28. package/dist/migrations/connection.js +18 -20
  29. package/dist/migrations/history.d.ts +4 -4
  30. package/dist/migrations/history.js +5 -5
  31. package/dist/migrations.js +7 -7
  32. package/dist/query-options.js +4 -4
  33. package/dist/query.js +2 -2
  34. package/dist/transaction.d.ts +2 -2
  35. package/dist/transaction.js +19 -14
  36. package/dist/types.d.ts +2 -2
  37. package/docs/adoption-guide.md +30 -27
  38. package/docs/agent-guide.md +53 -48
  39. package/docs/human-guide.md +21 -20
  40. package/docs/mutations.md +13 -10
  41. package/docs/query-inspection.md +12 -14
  42. package/docs/reference.md +130 -132
  43. package/package.json +3 -2
  44. package/dist/driver/array.d.ts +0 -5
  45. package/dist/driver/array.js +0 -70
  46. package/dist/driver/lifecycle.d.ts +0 -15
  47. package/dist/driver/lifecycle.js +0 -57
  48. package/dist/driver/listener.d.ts +0 -3
  49. package/dist/driver/listener.js +0 -63
  50. package/dist/driver/session.d.ts +0 -8
  51. 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.unsafe(`
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"`, [client.array([...typeOids])]);
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.unsafe(`
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.unsafe(`
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.unsafe(`
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.unsafe(`
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 Postgres.js mapping; generated as unknown for reads and never for non-null writes.`);
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 { JSONValue } from 'postgres';
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 postgres from 'postgres';
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: postgres.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: postgres.Sql, text: string): Promise<StatementMetadata>;
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.unsafe(text, [], { prepare: true }).describe();
4
- const oids = [...new Set([...statement.types, ...statement.columns.map((c) => c.type)])];
5
- const types = await client `
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(${oids.join(',')}, ',')::oid[])
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.columns.map((c) => c.table).filter(Boolean))];
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(${tables.join(',')}, ',')::oid[])
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 postgres from 'postgres';
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: postgres.Sql, context: InspectionContext) => Promise<T>): Promise<T>;
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 postgres from 'postgres';
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 = postgres(url.toString(), {
8
- max: 1,
9
- connect_timeout: limits.connectMs / 1000,
10
- // The inspector reads its own metadata; avoid an implicit array-catalog fetch.
11
- fetch_types: false,
12
- onnotice() { },
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 `SELECT
16
- set_config('statement_timeout', ${String(limits.statementMs)}, false),
17
- set_config('lock_timeout', ${String(limits.lockMs)}, false)`;
18
- await prepareArrays(client);
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 `SELECT set_config(${name}, ${value}, false)`;
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: 'postgres', ...context });
39
+ return await work(client, { mode: 'separate-session', readMappings: 'pg', ...context });
39
40
  }
40
41
  finally {
41
- // Bound cleanup as well; never leave an unbounded end() after a server/network failure.
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 Postgres.js types and diagnostics. */
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 Postgres.js types and diagnostics. */
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 Postgres.js mapping for ${type.schema}.${type.name} (OID ${oid}).`,
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.columns.map((column) => {
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.table}:${column.number}`);
45
+ const source = sourceMap.get(`${column.tableID}:${column.columnID}`);
46
46
  return {
47
47
  name: column.name,
48
- ...describeType(column.type),
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.types.map(describeType);
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: 'postgres';
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
- /** Postgres.js read representation, before considering result nullability. */
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 { Connection, SqlConnection } from '../connection.js';
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: SqlConnection, namespace: string) => Promise<T>): Promise<T>;
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 { reserveConnection } from '../driver/session.js';
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 { sql: pool, owned } = createConnection(options.connection);
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
- connection = await reserveConnection(pool);
17
- const client = connection.sql;
18
- await client.unsafe(`SELECT pg_advisory_lock${suffix}(hashtextextended($1, 0))`, [lockKey]);
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 (connection) {
28
- const client = connection.sql;
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.unsafe('ROLLBACK');
32
+ await client.query('ROLLBACK');
34
33
  if (locked) {
35
- await client.unsafe(`SELECT pg_advisory_unlock${suffix}(hashtextextended($1, 0))`, [
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
- discard = true;
41
+ releaseError =
42
+ error instanceof Error ? error : new Error('Tool connection cleanup failed.');
43
43
  }
44
44
  try {
45
- await connection.release(discard);
45
+ connection.release(releaseError);
46
46
  }
47
47
  catch (error) {
48
48
  errors.push(error);
49
49
  }
50
50
  }
51
- if (owned) {
52
- try {
53
- await pool.end();
54
- }
55
- catch (error) {
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 { SqlConnection } from '../connection.js';
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: SqlConnection, namespace: string, migrations: readonly SqlMigration[]): Promise<readonly AppliedMigration[]>;
10
- export declare function createHistoryTable(client: SqlConnection, namespace: string): Promise<void>;
11
- export declare function recordMigration(client: SqlConnection, namespace: string, migration: SqlMigration): Promise<void>;
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.unsafe('SELECT to_regclass($1)::text AS name', [qualified(namespace, migrationTable)]);
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.unsafe(`SELECT id, name, checksum FROM ${qualified(namespace, migrationTable)} ORDER BY id::numeric`);
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.unsafe(`CREATE TABLE IF NOT EXISTS ${qualified(namespace, migrationTable)} (
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.unsafe(`INSERT INTO ${qualified(namespace, migrationTable)} (id, name, checksum) VALUES ($1, $2, $3)`, [migration.id, migration.name, migration.checksum]);
29
+ await client.query(`INSERT INTO ${qualified(namespace, migrationTable)} (id, name, checksum) VALUES ($1, $2, $3)`, [migration.id, migration.name, migration.checksum]);
30
30
  }
@@ -28,25 +28,25 @@ async function execute(options, direction, steps) {
28
28
  if (!selected.length) {
29
29
  return [];
30
30
  }
31
- await client.unsafe(`CREATE SCHEMA IF NOT EXISTS ${quote(namespace)}`);
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.unsafe('BEGIN');
36
+ await client.query('BEGIN');
37
37
  // Match the string-literal rules used by the authored migration scanner.
38
- await client.unsafe('SET LOCAL standard_conforming_strings TO on');
39
- await client.unsafe(`SET LOCAL search_path TO ${quote(namespace)}, pg_catalog`);
40
- await client.unsafe(direction === 'up' ? file.up : file.down);
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.unsafe(`DELETE FROM ${qualified(namespace, migrationTable)} WHERE id = $1`, [
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.unsafe('COMMIT');
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
  }
@@ -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 [...result];
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 > 65533)
114
- throw new Error(`Postgres.js 3.4.9 supports at most 65533 bound parameters; received ${values.length}. Split large batches explicitly.`);
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) {
@@ -1,4 +1,4 @@
1
- import type postgres from 'postgres';
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: postgres.Sql, work: (query: QueryExecutor) => Promise<T>): Promise<T>;
4
+ export declare function withTransaction<T>(pool: Pool, work: (query: QueryExecutor) => Promise<T>): Promise<T>;