@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
package/CHANGELOG.md CHANGED
@@ -1,19 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - **Breaking:** `pg@8.23.1` replaces Postgres.js. Use pg `PoolConfig` (`connectionString` for a URI)
6
+ or `pg.Pool`; removed `{ url, options }` and `ConnectionUri`. `db.native` is the actual pool;
7
+ results use `rows`, `rowCount`, and `fields`, and error fields use pg names (`table`,
8
+ `constraint`). Native LISTEN uses checked-out clients with application-owned resubscription.
9
+ - Driver defaults apply, including no acquisition timeout and SQL null for bound `undefined`. The
10
+ parameter ceiling now matches PostgreSQL's 65,535 limit.
11
+ - Regenerate types: temporal reads include numeric infinities, date-only values use Node's local
12
+ timezone, and numeric arrays contain numbers that can lose precision. JSON write inputs are
13
+ encoded strings; use `JSON.stringify`, especially for arrays and strings. Removed `JSONValue`.
14
+
3
15
  ## 0.2.2
4
16
 
5
- - Closing a Tenon-owned client through `db.close()` prevents native LISTEN reconnects from
6
- restoring connections after shutdown, including pending subscription setup. Borrowed clients
7
- retain application ownership.
17
+ - Closing a Tenon-owned client through `db.close()` prevents native LISTEN reconnects from restoring
18
+ connections after shutdown, including pending subscription setup. Borrowed clients retain
19
+ application ownership.
8
20
  - Added `connection: { url, options? }` to preserve a deployment URI while configuring the native
9
21
  driver. `ConnectionUri` is exported; `ConnectionOptions` includes the pinned driver's constructor
10
22
  `onnotify` callback. Internally created clients remain Tenon-owned.
11
23
  - Inspection accepts explicit `undefined` for optional session and timeout settings, including in
12
24
  strict TypeScript configurations; unknown keys and invalid defined values still fail.
13
25
  - `db.close({ timeoutMs? })` now shares one shutdown outcome across concurrent/later callers and
14
- defaults to a 5,000 ms grace period before requesting owned driver termination; `0` skips the wait.
15
- - Added `CardinalityError` for `one`/`oneOrNone` mismatches, with code
16
- `TENON_CARDINALITY_MISMATCH`, expected mode, actual count, and optional label.
26
+ defaults to a 5,000 ms grace period before requesting owned driver termination; `0` skips the
27
+ wait.
28
+ - Added `CardinalityError` for `one`/`oneOrNone` mismatches, with code `TENON_CARDINALITY_MISMATCH`,
29
+ expected mode, actual count, and optional label.
17
30
 
18
31
  ## 0.2.1
19
32
 
@@ -29,29 +42,29 @@ Changes since 0.1.0, including improvements shipped in 0.1.1–0.1.4.
29
42
 
30
43
  ### Breaking changes
31
44
 
32
- - Tenon includes `postgres@3.4.9` as its only runtime dependency. Removed `pg`, `@types/pg`,
33
- and driver peer requirements. Connections accept a PostgreSQL URI, Postgres.js options, or
34
- an existing Postgres.js client.
45
+ - Tenon includes `postgres@3.4.9` as its only runtime dependency. Removed `pg`, `@types/pg`, and
46
+ driver peer requirements. Connections accept a PostgreSQL URI, Postgres.js options, or an existing
47
+ Postgres.js client.
35
48
  - Native results are row arrays with `count`, `command`, and `columns` metadata. Replace
36
- `result.rows` with the result itself, `rowCount` with `count`, and `fields` with `columns`.
37
- The `many`, `oneOrNone`, and `one` result modes remain available. Native error fields use
38
- Postgres.js names such as `table_name` and `constraint_name`.
39
- - Value conversions follow Postgres.js defaults: undefined parameters reject, JSON inputs use
40
- native JavaScript values, numeric arrays return strings, and timestamp infinities return
41
- invalid `Date` objects. Pre-serialized JSON text becomes a JSON string.
42
- - Removed the `/safeql` entry point, `safeqlPlugin`, `eslintPlugin`, and their dependencies and
43
- peer requirements. Tenon no longer checks or fixes SQL result annotations automatically.
49
+ `result.rows` with the result itself, `rowCount` with `count`, and `fields` with `columns`. The
50
+ `many`, `oneOrNone`, and `one` result modes remain available. Native error fields use Postgres.js
51
+ names such as `table_name` and `constraint_name`.
52
+ - Value conversions follow Postgres.js defaults: undefined parameters reject, JSON inputs use native
53
+ JavaScript values, numeric arrays return strings, and timestamp infinities return invalid `Date`
54
+ objects. Pre-serialized JSON text becomes a JSON string.
55
+ - Removed the `/safeql` entry point, `safeqlPlugin`, `eslintPlugin`, and their dependencies and peer
56
+ requirements. Tenon no longer checks or fixes SQL result annotations automatically.
44
57
 
45
58
  ### Added
46
59
 
47
60
  - `describeQuery({ connection, query, session?, timeouts? })` and `tenon describe <query.sql>`
48
- report PostgreSQL parameter/result types, source columns, a conservative row declaration,
49
- and unresolved findings. Inspection accepts completed dynamic SQL, uses a separate session,
50
- and supports explicit role, search path, timezone, and timeouts. It does not execute the query
51
- or verify its TypeScript annotation. See [query inspection](docs/query-inspection.md).
52
- - `sql.insertValues(input, columns?)` builds inserts with database defaults for missing or
53
- undefined cells. `sql.assignments(input, columns?)` builds updates that omit undefined fields.
54
- Both preserve explicit null. See the [mutation cookbook](docs/mutations.md).
61
+ report PostgreSQL parameter/result types, source columns, a conservative row declaration, and
62
+ unresolved findings. Inspection accepts completed dynamic SQL, uses a separate session, and
63
+ supports explicit role, search path, timezone, and timeouts. It does not execute the query or
64
+ verify its TypeScript annotation. See [query inspection](docs/query-inspection.md).
65
+ - `sql.insertValues(input, columns?)` builds inserts with database defaults for missing or undefined
66
+ cells. `sql.assignments(input, columns?)` builds updates that omit undefined fields. Both preserve
67
+ explicit null. See the [mutation cookbook](docs/mutations.md).
55
68
  - Generated `Rows.Insert['table']` and `Rows.Update['table']` types account for defaults,
56
69
  nullability, domains, generated columns, and driver input representations.
57
70
 
@@ -59,21 +72,9 @@ Changes since 0.1.0, including improvements shipped in 0.1.1–0.1.4.
59
72
 
60
73
  - SQL NULL array elements remain JavaScript null; quoted `"NULL"`, escapes, nesting, and custom
61
74
  parsers retain their meanings.
62
- - Transactions and migration locks reject work after connection loss. Failed recovery discards
63
- the affected session, and accepted pending work finishes before transaction cleanup.
64
- - Transactions and standalone guarded writes issue one fewer bootstrap query by registering
65
- their connection during BEGIN. Cold-start regressions cover concurrent reservations and tooling locks.
66
- - Large fragment composition avoids argument-spread failures. Queries reject more than 65,533
67
- bound parameters before execution; batching remains application-controlled.
68
- - PostgreSQL `name` columns generate string types. `tenon generate --check` reports missing
69
- files and a short diff for stale definitions without modifying the saved file.
70
-
71
- ### Upgrading
72
-
73
- 1. Remove Tenon's SafeQL imports and plugin configuration, and unused checking dependencies.
74
- 2. Install Tenon and remove `pg`/`@types/pg` when no other code needs them. Replace pg-specific
75
- connection configuration and pools with Postgres.js equivalents.
76
- 3. Update native result/error access, pass JSON values directly, and regenerate table definitions.
77
- Review numeric arrays and date validity. Use generated insert/update types for write inputs.
78
- 4. Inspect meaningful query variants and keep database tests for complete rows, permissions,
79
- ordering, write effects, and rollback. Match inspection settings to the execution session.
75
+ - Transactions and migration locks reject work after connection loss. Failed recovery discards the
76
+ affected session, and accepted pending work finishes before transaction cleanup.
77
+ - Large fragment composition avoids argument-spread failures. Queries reject more than 65,533 bound
78
+ parameters before execution; batching remains application-controlled.
79
+ - PostgreSQL `name` columns generate string types. `tenon generate --check` reports missing files
80
+ and a short diff for stale definitions without modifying the saved file.
package/README.md CHANGED
@@ -4,7 +4,7 @@ A PostgreSQL ORM designed for coding agents. Built for Node.js and TypeScript.
4
4
 
5
5
  Write SQL directly, bind values with a template tag, and reuse queries through ordinary functions.
6
6
  Tenon provides transactions, affected-row guards, SQL migrations, generated database types, and
7
- query inspection. It includes its pinned Postgres.js driver.
7
+ query inspection. It includes its pinned pg driver.
8
8
 
9
9
  ```sh
10
10
  npm install @kequtech/tenon-orm
@@ -36,12 +36,12 @@ try {
36
36
 
37
37
  The example assumes `public.users` exists. In a server, reuse the instance and close it at shutdown.
38
38
  `oneOrNone` rejects multiple rows; use `one` for exactly one row, `many` for an array, or omit
39
- `returns` for the driver's result array and metadata.
39
+ `returns` for the driver's result object and metadata.
40
40
 
41
41
  Joins, relationships, aggregates, and upserts use PostgreSQL SQL. Object-based writes can use
42
42
  `sql.insertValues` and `sql.assignments` while keeping the surrounding statement explicit.
43
- `db.native` exposes the driver for LISTEN, cursors, COPY, and other native features; those calls
44
- bypass Tenon's query and transaction guards.
43
+ `db.native` exposes the underlying `pg.Pool` for LISTEN, cursors, COPY, and other driver features;
44
+ those calls bypass Tenon's query and transaction guards.
45
45
 
46
46
  `sql<Row>` declares an expected result type. `describeQuery` inspects the finished SQL through
47
47
  PostgreSQL without executing it; it does not verify that annotation. Keep database tests for actual
@@ -17,14 +17,14 @@ export function validateAffectedRows(value) {
17
17
  }
18
18
  export function checkAffectedRows(constraint, result) {
19
19
  if (!['INSERT', 'UPDATE', 'DELETE', 'MERGE'].includes(result.command) ||
20
- result.count === null ||
21
- !Number.isSafeInteger(result.count) ||
22
- result.count < 0) {
23
- throw new AffectedRowsError(constraint, result.count, result.command, false);
20
+ result.rowCount === null ||
21
+ !Number.isSafeInteger(result.rowCount) ||
22
+ result.rowCount < 0) {
23
+ throw new AffectedRowsError(constraint, result.rowCount, result.command, false);
24
24
  }
25
25
  const expected = constraint.exact ?? constraint.max;
26
- const matches = constraint.exact === undefined ? result.count <= expected : result.count === expected;
26
+ const matches = constraint.exact === undefined ? result.rowCount <= expected : result.rowCount === expected;
27
27
  if (!matches) {
28
- throw new AffectedRowsError(constraint, result.count, result.command, true);
28
+ throw new AffectedRowsError(constraint, result.rowCount, result.command, true);
29
29
  }
30
30
  }
package/dist/baseline.js CHANGED
@@ -82,16 +82,16 @@ export async function adoptBaseline(options) {
82
82
  if (applied.length) {
83
83
  return [];
84
84
  }
85
- const schema = await client.unsafe('SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = $1', [
85
+ const schema = await client.query('SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = $1', [
86
86
  namespace,
87
87
  ]);
88
- if (!schema.length) {
88
+ if (!schema.rows.length) {
89
89
  throw new Error('The baseline namespace does not exist. Use migrate to initialize a fresh database.');
90
90
  }
91
- await client.unsafe('BEGIN');
91
+ await client.query('BEGIN');
92
92
  await createHistoryTable(client, namespace);
93
93
  await recordMigration(client, namespace, baseline);
94
- const result = await client.unsafe('COMMIT');
94
+ const result = await client.query('COMMIT');
95
95
  if (result.command !== 'COMMIT') {
96
96
  throw new Error('Baseline adoption was rolled back.');
97
97
  }
package/dist/client.js CHANGED
@@ -16,7 +16,7 @@ export function createClient(query, onQuery) {
16
16
  try {
17
17
  const value = await query(statement.text, statement.values, (result) => {
18
18
  command = result.command;
19
- rowCount = result.count;
19
+ rowCount = result.rowCount;
20
20
  if (options.affectedRows)
21
21
  checkAffectedRows(options.affectedRows, result);
22
22
  return queryOutput(options, result);
@@ -1,26 +1,19 @@
1
- import postgres from 'postgres';
1
+ import { Pool, type PoolClient, type PoolConfig, type QueryResult as PgResult } from 'pg';
2
2
  import type { UnknownRow } from './query.js';
3
- export type ConnectionOptions = postgres.Options<{}> & {
4
- readonly onnotify?: ((channel: string, payload: string) => void) | undefined;
5
- };
6
- export interface ConnectionUri {
7
- readonly url: string;
8
- readonly options?: ConnectionOptions | undefined;
9
- }
10
- export type Connection = string | ConnectionOptions | ConnectionUri | postgres.Sql;
11
- export type QueryResult<Row extends object = UnknownRow> = postgres.RowList<Row[]>;
12
- /** The driver operations shared by ordinary and reserved connections. */
13
- export interface SqlConnection {
14
- unsafe<Rows extends object[] = UnknownRow[]>(text: string, input?: postgres.ParameterOrJSON<never>[], options?: postgres.UnsafeQueryOptions & {
15
- simple?: boolean;
16
- }): Promise<postgres.RowList<Rows>>;
17
- array: postgres.Sql['array'];
18
- typed: postgres.Sql['typed'];
19
- }
3
+ export type ConnectionOptions = PoolConfig;
4
+ export type Connection = string | ConnectionOptions | Pool;
5
+ export type QueryResult<Row extends object = UnknownRow> = PgResult<Row>;
20
6
  export type QueryExecutor = <T>(text: string, input: readonly unknown[], readResult: (result: QueryResult) => T, guarded: boolean) => Promise<T>;
21
- /** Own clients created from a URI/config; borrow explicitly supplied Postgres.js clients. */
7
+ /** Own constructed pools; supplied pools retain their application's ownership and error policy. */
22
8
  export declare function createConnection(connection: Connection): {
23
- sql: postgres.Sql;
24
- owned: boolean;
9
+ pool: Pool;
10
+ run<T>(work: () => Promise<T>): Promise<T>;
11
+ close(timeoutMs: number): Promise<void>;
12
+ };
13
+ /** A checked-out pg client remains tied to its session and is discarded after a failure. */
14
+ export declare function watchClient(client: PoolClient): {
15
+ check(): void;
16
+ release(error?: Error): void;
25
17
  };
26
- export declare function executeSql(sql: SqlConnection, text: string, input?: readonly unknown[]): Promise<QueryResult>;
18
+ /** Unnamed extended protocol enforces a single statement, even without parameters. */
19
+ export declare function executeSql(connection: Pool | PoolClient, text: string, input?: readonly unknown[]): Promise<PgResult<UnknownRow>>;
@@ -1,35 +1,132 @@
1
- import postgres from 'postgres';
1
+ import { Pool, } from 'pg';
2
2
  import { plainObject } from './validation.js';
3
- /** Own clients created from a URI/config; borrow explicitly supplied Postgres.js clients. */
3
+ /** A supplied pool may come from another installed copy of pg. */
4
+ function isPool(connection) {
5
+ return (connection !== null &&
6
+ typeof connection === 'object' &&
7
+ 'connect' in connection &&
8
+ typeof connection.connect === 'function' &&
9
+ 'query' in connection &&
10
+ typeof connection.query === 'function' &&
11
+ 'totalCount' in connection &&
12
+ typeof connection.totalCount === 'number' &&
13
+ 'end' in connection &&
14
+ typeof connection.end === 'function');
15
+ }
16
+ /** Own constructed pools; supplied pools retain their application's ownership and error policy. */
4
17
  export function createConnection(connection) {
5
- if (typeof connection === 'function' && typeof connection.unsafe === 'function') {
6
- return { sql: connection, owned: false };
7
- }
18
+ if (isPool(connection))
19
+ return { pool: connection, run: (work) => work(), close: async () => { } };
20
+ let config;
8
21
  if (typeof connection === 'string') {
9
22
  if (!connection.trim())
10
23
  throw new Error('Connection string must not be empty.');
11
- return { sql: postgres(connection), owned: true };
24
+ config = { connectionString: connection };
12
25
  }
13
- plainObject(connection, 'Connection configuration');
14
- if ('url' in connection) {
15
- for (const key of Object.keys(connection)) {
16
- if (key !== 'url' && key !== 'options') {
17
- throw new Error(`Unknown URI connection option: ${key}`);
18
- }
19
- }
20
- if (typeof connection.url !== 'string' || !connection.url.trim()) {
21
- throw new Error('Connection string must not be empty.');
26
+ else {
27
+ plainObject(connection, 'Connection configuration');
28
+ if ('url' in connection) {
29
+ throw new Error('Use pg connectionString in the connection configuration instead of url/options.');
22
30
  }
23
- if (connection.options !== undefined)
24
- plainObject(connection.options, 'Connection options');
25
- return { sql: postgres(connection.url, connection.options), owned: true };
31
+ config = connection;
26
32
  }
27
- return { sql: postgres(connection), owned: true };
33
+ const pool = new Pool(config);
34
+ const clients = new Set();
35
+ const pending = new Map();
36
+ let terminating = false;
37
+ const terminate = (client) => {
38
+ void client.end().catch(() => { });
39
+ };
40
+ // pool.end handles idle clients; the deadline also ends native checkouts.
41
+ pool.on('acquire', (client) => {
42
+ clients.add(client);
43
+ if (terminating)
44
+ terminate(client);
45
+ });
46
+ pool.on('release', (_error, client) => clients.delete(client));
47
+ pool.on('error', (error) => process.emitWarning(error));
48
+ return {
49
+ pool,
50
+ run(work) {
51
+ let expire;
52
+ const task = new Promise((resolve, reject) => {
53
+ expire = reject;
54
+ void Promise.resolve().then(work).then(resolve, reject);
55
+ });
56
+ pending.set(task, expire);
57
+ const forget = () => {
58
+ pending.delete(task);
59
+ };
60
+ void task.then(forget, forget);
61
+ return task;
62
+ },
63
+ async close(timeoutMs) {
64
+ let timer;
65
+ let ending;
66
+ const end = () => (ending ??= pool.end());
67
+ try {
68
+ await Promise.race([
69
+ (async () => {
70
+ await Promise.allSettled([...pending.keys()]);
71
+ await end();
72
+ })(),
73
+ new Promise((resolve) => {
74
+ timer = setTimeout(resolve, timeoutMs);
75
+ }).then(async () => {
76
+ // Native callbacks must reject close on failure, not escape a timer callback.
77
+ terminating = true;
78
+ const error = new Error('The database shutdown deadline expired.');
79
+ for (const reject of pending.values())
80
+ reject(error);
81
+ try {
82
+ void end().catch(() => { });
83
+ }
84
+ finally {
85
+ // Attempt every checkout even when one native end call throws.
86
+ await Promise.all([...clients].map(async (client) => terminate(client)));
87
+ }
88
+ }),
89
+ ]);
90
+ }
91
+ finally {
92
+ clearTimeout(timer);
93
+ }
94
+ },
95
+ };
96
+ }
97
+ /** A checked-out pg client remains tied to its session and is discarded after a failure. */
98
+ export function watchClient(client) {
99
+ let failure;
100
+ const onError = (error) => {
101
+ failure ??= error;
102
+ };
103
+ const onEnd = () => {
104
+ failure ??= new Error('The PostgreSQL connection closed.');
105
+ };
106
+ client.on('error', onError);
107
+ client.on('end', onEnd);
108
+ return {
109
+ check() {
110
+ if (failure)
111
+ throw failure;
112
+ },
113
+ release(error) {
114
+ try {
115
+ client.release(error ?? failure);
116
+ }
117
+ finally {
118
+ client.removeListener('error', onError);
119
+ client.removeListener('end', onEnd);
120
+ }
121
+ },
122
+ };
28
123
  }
29
- export function executeSql(sql, text, input = []) {
30
- // Tenon compiles SQL separately from values; let PostgreSQL infer array/JSON types from the surrounding SQL.
31
- const values = input.map((value) => (Array.isArray(value) ? sql.typed(value, 0) : value));
32
- // Keep preparation off; CONSIDERATIONS.md records parser-cache and pipeline safety constraints.
33
- const options = { simple: false, prepare: false };
34
- return sql.unsafe(text, values, options);
124
+ /** Unnamed extended protocol enforces a single statement, even without parameters. */
125
+ export function executeSql(connection, text, input = []) {
126
+ const config = {
127
+ text,
128
+ values: [...input],
129
+ queryMode: 'extended',
130
+ };
131
+ return connection.query(config);
35
132
  }
package/dist/database.js CHANGED
@@ -1,7 +1,5 @@
1
1
  import { createClient } from './client.js';
2
2
  import { createConnection, executeSql } from './connection.js';
3
- import { prepareArrays } from './driver/array.js';
4
- import { guardListenerShutdown } from './driver/listener.js';
5
3
  import { withTransaction } from './transaction.js';
6
4
  import { plainObject } from './validation.js';
7
5
  function closeTimeout(options) {
@@ -28,28 +26,28 @@ export function createTenon(options) {
28
26
  if (options.onQuery !== undefined && typeof options.onQuery !== 'function') {
29
27
  throw new Error('onQuery must be a function.');
30
28
  }
31
- const { sql: pool, owned } = createConnection(options.connection);
32
- const stopListeners = owned ? guardListenerShutdown(pool) : undefined;
29
+ const { pool, run, close } = createConnection(options.connection);
33
30
  let closing;
34
31
  function checkOpen() {
35
32
  if (closing) {
36
33
  throw new Error('This database instance is closed.');
37
34
  }
38
35
  }
39
- const query = async (text, input, readResult, guarded) => {
36
+ const query = (text, input, readResult, guarded) => {
40
37
  checkOpen();
41
- if (guarded) {
42
- return withTransaction(pool, (query) => query(text, input, readResult, false));
43
- }
44
- await prepareArrays(pool);
45
- return readResult(await executeSql(pool, text, input));
38
+ return run(async () => {
39
+ if (guarded) {
40
+ return withTransaction(pool, (query) => query(text, input, readResult, false));
41
+ }
42
+ return readResult(await executeSql(pool, text, input));
43
+ });
46
44
  };
47
45
  return {
48
46
  ...createClient(query, options.onQuery),
49
47
  native: pool,
50
48
  async transaction(callback) {
51
49
  checkOpen();
52
- return withTransaction(pool, (query) => callback(createClient(query, options.onQuery)));
50
+ return run(() => withTransaction(pool, (query) => callback(createClient(query, options.onQuery))));
53
51
  },
54
52
  close(options) {
55
53
  if (closing)
@@ -57,12 +55,7 @@ export function createTenon(options) {
57
55
  try {
58
56
  const timeoutMs = closeTimeout(options);
59
57
  // Cache before invoking driver callbacks, so reentrant/concurrent callers share shutdown.
60
- closing = Promise.resolve().then(() => {
61
- if (owned) {
62
- stopListeners();
63
- return pool.end({ timeout: timeoutMs / 1000 });
64
- }
65
- });
58
+ closing = Promise.resolve().then(() => close(timeoutMs));
66
59
  return closing;
67
60
  }
68
61
  catch (error) {
@@ -1,4 +1,4 @@
1
- /** Postgres.js default representations (with SQL NULL array elements preserved), shared by generation and query inspection. */
1
+ /** pg default representations, shared by generation and query inspection. */
2
2
  export type ValueTypes = Readonly<{
3
3
  read: string;
4
4
  write: string;
@@ -8,18 +8,18 @@ export const arrayTypes = {
8
8
  '1022': 'number',
9
9
  '1028': 'number',
10
10
  '1016': { read: 'string', write: 'string | bigint' },
11
- '1231': { read: 'string', write: 'string | number' },
11
+ '1231': { read: 'number', write: 'string | number' },
12
12
  '1009': 'string',
13
13
  '1014': 'string',
14
14
  '1015': 'string',
15
15
  '1183': 'string',
16
16
  '1270': 'string',
17
17
  '2951': 'string',
18
- '1182': { read: 'globalThis.Date', write: 'globalThis.Date | string' },
19
- '1115': { read: 'globalThis.Date', write: 'globalThis.Date | string' },
20
- '1185': { read: 'globalThis.Date', write: 'globalThis.Date | string' },
21
- '199': { read: 'unknown', write: 'Exclude<import("@kequtech/tenon-orm").JSONValue, null>' },
22
- '3807': { read: 'unknown', write: 'Exclude<import("@kequtech/tenon-orm").JSONValue, null>' },
18
+ '1182': { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
19
+ '1115': { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
20
+ '1185': { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
21
+ '199': { read: 'unknown', write: 'string' },
22
+ '3807': { read: 'unknown', write: 'string' },
23
23
  };
24
24
  export const scalarTypes = {
25
25
  int2: 'number',
@@ -35,13 +35,13 @@ export const scalarTypes = {
35
35
  time: 'string',
36
36
  timetz: 'string',
37
37
  uuid: 'string',
38
- date: { read: 'globalThis.Date', write: 'globalThis.Date | string' },
39
- timestamp: { read: 'globalThis.Date', write: 'globalThis.Date | string' },
40
- timestamptz: { read: 'globalThis.Date', write: 'globalThis.Date | string' },
38
+ date: { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
39
+ timestamp: { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
40
+ timestamptz: { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
41
41
  int8: { read: 'string', write: 'string | bigint' },
42
42
  numeric: { read: 'string', write: 'string | number' },
43
- json: { read: 'unknown', write: 'Exclude<import("@kequtech/tenon-orm").JSONValue, null>' },
44
- jsonb: { read: 'unknown', write: 'Exclude<import("@kequtech/tenon-orm").JSONValue, null>' },
43
+ json: { read: 'unknown', write: 'string' },
44
+ jsonb: { read: 'unknown', write: 'string' },
45
45
  bytea: 'import("node:buffer").Buffer',
46
46
  };
47
47
  export function valueTypes(mapping) {
package/dist/generate.js CHANGED
@@ -7,17 +7,17 @@ import { quote } from './sql.js';
7
7
  export async function generateDefinitions(options) {
8
8
  const onWarning = options.onWarning ?? ((message) => console.warn(`Warning: ${message}`));
9
9
  return withMigrationLock(options, 'shared', async (client, namespace) => {
10
- await client.unsafe('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
11
- const schema = await client.unsafe('SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = $1', [
10
+ await client.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
11
+ const schema = await client.query('SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = $1', [
12
12
  namespace,
13
13
  ]);
14
- if (!schema.length)
14
+ if (!schema.rows.length)
15
15
  throw new Error(`Schema ${quote(namespace)} does not exist.`);
16
16
  // Keep catalog deparsing stable across connection settings and replay schemas.
17
17
  // Transaction-local settings are restored before returning a borrowed client.
18
- await client.unsafe(`SET LOCAL search_path TO pg_catalog, ${quote(namespace)}`);
19
- await client.unsafe('SET LOCAL quote_all_identifiers TO off');
20
- await client.unsafe('SET LOCAL standard_conforming_strings TO on');
18
+ await client.query(`SET LOCAL search_path TO pg_catalog, ${quote(namespace)}`);
19
+ await client.query('SET LOCAL quote_all_identifiers TO off');
20
+ await client.query('SET LOCAL standard_conforming_strings TO on');
21
21
  const definitions = await readTables(client, namespace);
22
22
  const summaries = [];
23
23
  for (const table of definitions) {
@@ -25,7 +25,7 @@ export async function generateDefinitions(options) {
25
25
  }
26
26
  const domains = await domainSummaries(client, definitions.flatMap((table) => table.columns.map((column) => column.declaredOid)));
27
27
  const types = renderTableTypes(definitions, namespace, onWarning);
28
- await client.unsafe('COMMIT');
28
+ await client.query('COMMIT');
29
29
  return `// Generated by Tenon from the PostgreSQL schema. Do not edit.
30
30
  // Commit this file and regenerate it after changing the schema.
31
31
 
@@ -1,4 +1,4 @@
1
- import type { SqlConnection } from '../connection.js';
1
+ import type { PoolClient } from 'pg';
2
2
  export interface ColumnRow {
3
3
  name: string;
4
4
  oid: string;
@@ -19,4 +19,4 @@ export interface TableRow {
19
19
  name: string;
20
20
  columns: ColumnRow[];
21
21
  }
22
- export declare function readTables(client: SqlConnection, namespace: string): Promise<TableRow[]>;
22
+ export declare function readTables(client: PoolClient, namespace: string): Promise<TableRow[]>;
@@ -1,15 +1,15 @@
1
1
  import { migrationTable } from '../migrations/history.js';
2
2
  export async function readTables(client, namespace) {
3
- const tables = await client.unsafe(`
3
+ const tables = await client.query(`
4
4
  SELECT c.oid::text AS oid, c.relname AS name
5
5
  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
6
6
  WHERE n.nspname = $1 AND c.relname <> $2 AND c.relkind IN ('r', 'p')
7
7
  AND NOT c.relispartition
8
8
  ORDER BY c.relname COLLATE "C"`, [namespace, migrationTable]);
9
9
  const definitions = [];
10
- for (const table of tables) {
10
+ for (const table of tables.rows) {
11
11
  // Attached descendants are storage details, not separate generated models.
12
- const columns = await client.unsafe(`
12
+ const columns = await client.query(`
13
13
  WITH RECURSIVE resolved AS (
14
14
  SELECT a.attnum, t.oid, t.typbasetype, t.typnotnull AS domain_not_null
15
15
  FROM pg_catalog.pg_attribute a JOIN pg_catalog.pg_type t ON t.oid = a.atttypid
@@ -32,7 +32,7 @@ export async function readTables(client, namespace) {
32
32
  JOIN pg_catalog.pg_type declared ON declared.oid = a.atttypid
33
33
  JOIN pg_catalog.pg_namespace dn ON dn.oid = declared.typnamespace
34
34
  WHERE a.attrelid = $1::oid AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum`, [table.oid]);
35
- definitions.push({ ...table, columns: columns });
35
+ definitions.push({ ...table, columns: columns.rows });
36
36
  }
37
37
  return definitions;
38
38
  }
@@ -1,6 +1,6 @@
1
- import type { SqlConnection } from '../connection.js';
1
+ import type { PoolClient } from 'pg';
2
2
  /** Include only domains reached through selected columns, base domains or arrays. */
3
- export declare function domainSummaries(client: SqlConnection, typeOids: readonly string[]): Promise<string[]>;
3
+ export declare function domainSummaries(client: PoolClient, typeOids: readonly string[]): Promise<string[]>;
4
4
  /** Called inside generation's read-only snapshot, with a fixed local search_path. */
5
- export declare function tableSummary(client: SqlConnection, oid: string, name: string): Promise<string>;
5
+ export declare function tableSummary(client: PoolClient, oid: string, name: string): Promise<string>;
6
6
  export declare function schemaSummary(summaries: readonly string[]): string;