@kequtech/tenon-orm 0.2.2 → 0.3.1

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 +46 -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 +40 -27
  38. package/docs/agent-guide.md +55 -48
  39. package/docs/human-guide.md +25 -20
  40. package/docs/mutations.md +13 -10
  41. package/docs/query-inspection.md +12 -14
  42. package/docs/reference.md +304 -131
  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/docs/reference.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # Tenon reference
2
2
 
3
- Exact API contracts and limitations. The [human guide](human-guide.md) introduces the workflow;
4
- the [agent guide](agent-guide.md) covers routine implementation. Examples below use the
5
- `public.users` table from the human guide and an existing `db` instance, unless stated otherwise.
3
+ Exact API contracts and limitations. The [human guide](human-guide.md) introduces the workflow; the
4
+ [agent guide](agent-guide.md) covers routine implementation. Examples below use the `public.users`
5
+ table from the human guide and an existing `db` instance, unless stated otherwise.
6
6
 
7
7
  ## Installation and connection
8
8
 
9
9
  Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and Node
10
- development typings. Tenon includes `postgres@3.4.9`; no driver peer or special compiler plugin is
11
- required. A usual Node TypeScript setup uses `module: "NodeNext"`.
10
+ development typings. Tenon includes `pg@8.23.1` and its TypeScript declarations; no driver peer or
11
+ special compiler plugin is required. A usual Node TypeScript setup uses `module: "NodeNext"`.
12
12
 
13
13
  ```sh
14
14
  npm install @kequtech/tenon-orm
@@ -18,71 +18,147 @@ npm install @kequtech/tenon-orm
18
18
  and `close`. The required `connection` accepts:
19
19
 
20
20
  - A PostgreSQL URI string.
21
- - `{ url: string, options?: ConnectionOptions }` to combine a URI with driver settings.
22
- - A Postgres.js options object; an explicit `{}` uses the driver's environment defaults.
23
- - An existing Postgres.js client, borrowed by identity.
21
+ - A pg `PoolConfig` object; an explicit `{}` uses the driver's environment defaults.
22
+ - An existing `pg.Pool`, borrowed by identity.
24
23
 
25
24
  ```ts
26
25
  const db = createTenon({
27
26
  connection: {
28
- url: connection,
29
- options: { connect_timeout: 5, idle_timeout: 10, connection: { search_path: 'app,public' } },
27
+ connectionString: connection,
28
+ max: 10,
29
+ idleTimeoutMillis: 30_000,
30
+ connectionTimeoutMillis: 30_000,
31
+ options: '-c search_path=app,public',
30
32
  },
31
33
  });
32
34
  ```
33
35
 
34
- Tenon forwards `{ url, options }` to `postgres(url, options)`. Explicit driver options override
35
- corresponding URI pool/transport options. URI startup parameters such as `search_path` and
36
- `application_name` override `options.connection`. Timeouts above are seconds; preserve the
37
- deployment's credentials and TLS requirements. `ConnectionUri` and `ConnectionOptions` are
38
- exported. The latter includes `onnotify(channel, payload)` for native notification handling;
39
- configure callbacks at construction.
40
-
41
- Reuse an instance for each database configuration. `db.close({ timeoutMs? })` rejects new Tenon
42
- work immediately and gives owned connections a 5,000 ms grace period by default before requesting
43
- driver termination. Use `0` for immediate termination. The timeout must be an integer from 0 to
44
- 2,147,483,647 milliseconds; `CloseOptions` is exported. Finish outstanding work before closing
45
- when possible. The timeout bounds the shutdown wait and rejects pending client queries; it is not
46
- a PostgreSQL cancellation request. Server work can complete afterward, including an autocommitted
47
- write, and JavaScript inside a transaction callback is not canceled.
48
-
49
- The first valid close call chooses the timeout. Concurrent and later calls return the same promise,
50
- including the same shutdown failure; later options do not change it. Invalid initial options reject
51
- without closing the instance. For a borrowed client, close only disables the Tenon wrapper and
52
- leaves the driver pool and its work to the caller. Tenon does not load environment files.
53
-
54
- Qualify SQL names or configure runtime `search_path`. CLI `--namespace` selects tooling scope;
55
- it does not configure application queries. Baseline capture and Describe accept connection
56
- strings rather than the runtime connection union.
36
+ Strings become pg's `connectionString`; configuration objects pass to `new Pool(config)`.
37
+ `ConnectionOptions` is an alias for pg's `PoolConfig`. Driver options and defaults are unchanged,
38
+ including no acquisition timeout unless `connectionTimeoutMillis` is configured. Pool timeouts use
39
+ milliseconds. pg parses the URI for connection fields; matching URI values override client
40
+ configuration, including an `options` URI parameter overriding the configuration's `options` string.
41
+ The pool sizes and timeouts above are application choices. Configure pool policy in `PoolConfig`;
42
+ old driver-specific URI settings such as `idle_timeout` do not configure pg's idle timeout. Preserve
43
+ existing URI startup options when appending settings such as UTC and `search_path`.
44
+ Preserve deployment credentials and TLS requirements; see
45
+ [pg connection configuration](https://node-postgres.com/features/connecting).
46
+
47
+ Reuse an instance for each database configuration. `db.close({ timeoutMs? })` immediately rejects
48
+ new Tenon work. For owned pools, accepted Tenon operations can drain for up to 5,000 ms by default;
49
+ at the deadline, remaining operations reject and Tenon requests driver termination. `0` skips the
50
+ grace period. The timeout must be an integer from 0 to 2,147,483,647 milliseconds; `CloseOptions` is
51
+ exported. This bounds Tenon's wait, not every native transport's teardown or server execution. It
52
+ sends no PostgreSQL cancellation request and does not cancel JavaScript callbacks. An unbounded
53
+ authentication attempt can keep a connection open after close resolves. Configure pg's
54
+ `connectionTimeoutMillis` when connection establishment also needs a deadline.
55
+
56
+ The first valid close call chooses the timeout. Concurrent and later calls share the same promise
57
+ and outcome. Invalid initial options reject without closing the instance. For borrowed pools, close
58
+ only disables the Tenon wrapper; the application retains the pool and outstanding work. Tenon does
59
+ not load environment files.
60
+
61
+ Qualify SQL names or configure runtime `search_path`. CLI `--namespace` selects tooling scope; it
62
+ does not configure application queries. Baseline capture and Describe accept connection strings
63
+ rather than the runtime connection union.
57
64
 
58
65
  ## Native driver access
59
66
 
60
- `db.native` is the actual pool-level Postgres.js client, readonly in TypeScript. Use it for LISTEN,
61
- cursors, COPY, and other [native APIs](https://github.com/porsager/postgres/tree/v3.4.9#usage).
62
- Native calls bypass Tenon's result modes, affected-row guards, SQL checks, and `onQuery`. They use
63
- native preparation defaults and do not join a Tenon transaction; callback clients expose only
64
- `tx.query`.
67
+ `db.native` is the actual `pg.Pool`, readonly in TypeScript. Native calls bypass Tenon's result
68
+ modes, affected-row guards, SQL checks, and `onQuery`. They follow pg's query configuration and do
69
+ not join a Tenon transaction; callback clients expose only `tx.query`.
65
70
 
66
- Native access alone does not install Tenon's array correction. The first Tenon query or transaction
67
- installs it on the shared client; native statements prepared earlier may retain their old parsers.
71
+ Use pg's [native APIs](https://node-postgres.com/apis/pool) and compatible cursor/COPY packages when
72
+ needed. For LISTEN, check out a client with `await db.native.connect()`, attach `notification`,
73
+ `error`, and `end` listeners, and issue `LISTEN` on that client. Keep it checked out while listening;
74
+ release it after `UNLISTEN`. Native queued acquisitions follow pg's shutdown behavior and must be
75
+ settled by their caller before closing. A disconnected client does not reconnect. The application owns
76
+ resubscription and catch-up; notifications are not replayed.
68
77
 
69
- Native `.listen(channel, onPayload, onReady?)` uses a dedicated physical connection. `onReady`
70
- runs after subscription and resubscription. Missed notifications are not replayed; the application
71
- owns catch-up and callback errors. That dedicated client installs its own close handler, so a
72
- constructor `onclose` does not report its outages. Explicit LISTEN on a dedicated reserved native
73
- connection can use constructor callbacks when disconnect detection is required. Subscription
74
- cancellation follows the pinned driver; known reconnect limitations are recorded in
75
- [maintainer considerations](https://github.com/kequtech/tenon-orm/blob/main/CONSIDERATIONS.md#native-driver-access-and-listen-lifecycle).
78
+ Close owned pools through `db.close()` for bounded shutdown, including checked-out native clients.
79
+ Borrowed pools and their listeners remain usable after wrapper close until their owner ends them.
76
80
 
77
- Close owned clients through `db.close()` to prevent queued LISTEN reconnects from reopening them.
78
- Direct `db.native.end()` bypasses that protection. Borrowed clients and their listeners remain
79
- usable after wrapper close until their owner ends them.
81
+ ### LISTEN lifecycle example
82
+
83
+ This helper owns a dedicated single-client pool. It returns an intentional-close function only
84
+ after LISTEN is ready. Callbacks are synchronous; notification exceptions are isolated, and an
85
+ unexpected disconnect invalidates this generation once before notifying its consumer. Failed
86
+ connection acquisition or LISTEN setup closes the owned resources and rejects the setup promise.
87
+
88
+ ```ts
89
+ import { createTenon } from '@kequtech/tenon-orm';
90
+
91
+ async function listenForChanges(
92
+ connectionString: string,
93
+ onChange: (payload: string | undefined) => void,
94
+ onDisconnect: (error: Error) => void,
95
+ ) {
96
+ const db = createTenon({
97
+ connection: { connectionString, max: 1, idleTimeoutMillis: 0, connectionTimeoutMillis: 30_000 },
98
+ });
99
+ const client = await db.native.connect().catch(async (error: unknown) => {
100
+ await db.close();
101
+ throw error;
102
+ });
103
+ let active = true;
104
+ let closing: Promise<void> | undefined;
105
+
106
+ function stop(error?: Error): Promise<void> {
107
+ if (closing) return closing;
108
+ active = false;
109
+ closing = Promise.resolve().then(async () => {
110
+ try {
111
+ if (error) onDisconnect(error);
112
+ } finally {
113
+ client.release(true);
114
+ await db.close();
115
+ }
116
+ });
117
+ return closing;
118
+ }
119
+
120
+ const failed = (error: Error) => {
121
+ if (active) void stop(error).catch(console.error);
122
+ };
123
+ client.on('error', failed);
124
+ client.on('end', () => failed(new Error('The notification connection ended.')));
125
+ client.on('notification', (event) => {
126
+ if (!active || event.channel !== 'app_changes') return;
127
+ try {
128
+ onChange(event.payload);
129
+ } catch (error) {
130
+ console.error(error);
131
+ }
132
+ });
133
+ try {
134
+ await client.query('LISTEN "app_changes"');
135
+ if (!active) throw new Error('The notification connection closed during setup.');
136
+ } catch (error) {
137
+ await stop();
138
+ throw error;
139
+ }
140
+ return () => stop();
141
+ }
142
+ ```
143
+
144
+ Both failure and intentional close discard the dedicated checkout with `release(true)` before
145
+ closing its pool, so no UNLISTEN is needed and normal shutdown does not wait for Tenon's deadline.
146
+ The active flag suppresses duplicate error/end callbacks and intentional-close notifications.
147
+ Create a new helper instance to recover, and reconcile missed events through persistent data.
148
+
149
+ This helper's close function is available after readiness. A manager that also supports replacement
150
+ or shutdown while setup is pending must assign a generation token before awaiting setup. Only the
151
+ current generation may change shared readiness/subscription state or notify its consumers. If stale
152
+ setup later succeeds, close that setup's own listener; if it fails or finishes cleanup, leave the
153
+ replacement alone. Invalidate the current token before intentional shutdown. Keep those guards in
154
+ the application's subscription manager. The fixed channel above is trusted; validate and quote
155
+ application-selected channel names before placing them in native SQL.
80
156
 
81
157
  ## Queries and result modes
82
158
 
83
159
  `sql<Row>` creates a detached `SqlQuery<Row>` with SQL `text` and ordered `values`; it does not
84
- execute. `Row` declares one selected row. Without an annotation, fields are `unknown`. An
85
- annotation does not verify the SQL or validate returned data.
160
+ execute. `Row` declares one selected row. Without an annotation, fields are `unknown`. An annotation
161
+ does not verify the SQL or validate returned data.
86
162
 
87
163
  ```ts
88
164
  const user = await db.query(
@@ -94,27 +170,47 @@ const user = await db.query(
94
170
  `query` options are `{ returns?, affectedRows?, label? }`; unknown keys fail. A supplied `label`
95
171
  must be a nonempty string.
96
172
 
97
- | `returns` | Output | Allowed returned rows |
98
- | ------------------ | ------------------------------- | --------------------- |
99
- | `native` (default) | `QueryResult<Row>` driver array | Any |
100
- | `many` | Plain `Row[]` | Any |
101
- | `oneOrNone` | `Row \| undefined` | Zero or one |
102
- | `one` | `Row` | Exactly one |
173
+ | `returns` | Output | Allowed returned rows |
174
+ | ------------------ | -------------------------------- | --------------------- |
175
+ | `native` (default) | `QueryResult<Row>` driver object | Any |
176
+ | `many` | Plain `Row[]` | Any |
177
+ | `oneOrNone` | `Row \| undefined` | Zero or one |
178
+ | `one` | `Row` | Exactly one |
103
179
 
104
- Native arrays carry `command`, `count`, `columns`, `statement`, and `state` metadata. `count` is
105
- the command's affected-row count, independent of `RETURNING`; it can be null for commands without
106
- a count. Result modes never add `LIMIT` or `RETURNING`. A returned-row mismatch alone does not
107
- undo an autocommitted write; use `affectedRows` for write bounds that roll back on failure.
180
+ Native results expose `rows`, `rowCount`, `command`, and `fields`, following pg's `QueryResult`.
181
+ `rowCount` is the command's affected-row count, independent of `RETURNING`; it can be null for
182
+ commands without a count. Result modes never add `LIMIT` or `RETURNING`. A returned-row mismatch
183
+ alone does not undo an autocommitted write; use `affectedRows` for write bounds that roll back.
108
184
 
109
185
  Functions accepting `QueryClient` can use either `Database` or `Transaction`. Public types also
110
186
  include `QueryOptions`, `QueryReturnMode`, and `QueryOutput<Row, Mode>`.
111
187
 
188
+ ### Adapting transactional results
189
+
190
+ For a job library or other adapter expecting `{ rows }`, extract the native result's row array:
191
+
192
+ ```ts
193
+ import { unsafeSql, type QueryClient } from '@kequtech/tenon-orm';
194
+
195
+ async function queryRows(tx: QueryClient, text: string, values: readonly unknown[]) {
196
+ const result = await tx.query(unsafeSql(text, values));
197
+ return { rows: result.rows };
198
+ }
199
+ ```
200
+
201
+ Alternatively, request `{ returns: 'many' }` and wrap the returned array in `{ rows }`. Supply the
202
+ callback's `tx` when the adapter's work must commit with application writes. A native pool query
203
+ uses a separate checkout and does not join that transaction. Await adapter calls before returning
204
+ from the callback, retain commit/rollback tests, and make recording helpers return pg-shaped native
205
+ results. The adapter's SQL text must be trusted and values must remain separately bound.
206
+
112
207
  ## SQL parameters and identifiers
113
208
 
114
209
  Ordinary `sql` interpolations bind values. Do not quote them or write manual `$n` placeholders
115
- inside templates. `undefined` bound parameters fail; `null` means SQL null. Pass JSON objects and
116
- arrays directly. SQL context distinguishes PostgreSQL arrays from JSON; cast standalone parameters
117
- when that context is absent.
210
+ inside templates. Values follow pg serialization: `null` and `undefined` bind as SQL null, arrays
211
+ bind as PostgreSQL arrays, and objects use pg's JSON serialization. For JSON input, use
212
+ `JSON.stringify(value)`, including for JSON arrays, strings, and JSON null. Generated JSON write
213
+ types accept encoded strings. Cast standalone parameters when SQL must establish their type.
118
214
 
119
215
  ```ts
120
216
  const ids = [1, 2, 3];
@@ -122,26 +218,27 @@ const query = sql<{ id: number }>`SELECT id FROM public.users
122
218
  WHERE id = ANY(${ids}::integer[])`;
123
219
  ```
124
220
 
125
- `identifier(schema, table)` quotes components separately. Names must contain 1–63 UTF-8 bytes and
126
- no NUL. Choose application-approved names; quoting does not authorize access. SQL syntax such as
127
- `DESC` belongs in fixed query alternatives or fragments, not a string parameter.
221
+ `identifier(schema, table)` quotes components separately. Names must contain 1–63 UTF-8 bytes and no
222
+ NUL. Choose application-approved names; quoting does not authorize access. SQL syntax such as `DESC`
223
+ belongs in fixed query alternatives or fragments, not a string parameter.
128
224
 
129
225
  `unsafeSql<Row>(text, values)` accepts trusted external SQL and an explicit values array, including
130
226
  `[]`. Never concatenate request input into its text. Full `sql` and `unsafeSql` queries cannot be
131
227
  nested; use fragments for partial SQL.
132
228
 
133
- Each Tenon query executes one statement through the extended protocol. Application execution
134
- explicitly disables prepared-statement caching, including on supplied clients with `prepare: true`.
135
- Values remain bound separately. Migrations deliberately support multi-statement scripts.
229
+ Each Tenon query executes one statement through the extended protocol. Application execution uses
230
+ unnamed queries without a `name` or prepared-statement cache. Values remain bound separately.
231
+ Migrations deliberately support multi-statement scripts.
136
232
 
137
233
  ## SQL fragments and joins
138
234
 
139
235
  `sql.fragment` creates immutable partial SQL with bound values. It has no row type, cannot execute
140
236
  alone, and rejects blank SQL. Insert it into a complete `sql<Row>` query.
141
237
 
142
- `sql.join(fragments, separator)` requires a nonempty list. Separators are `'AND'`, `'OR'`, and `','`.
143
- It does not add parentheses or change SQL precedence. Each fragment use receives correctly ordered
144
- parameters, including repeated and nested uses; literal `$1` text in strings/comments is unchanged.
238
+ `sql.join(fragments, separator)` requires a nonempty list. Separators are `'AND'`, `'OR'`, and
239
+ `','`. It does not add parentheses or change SQL precedence. Each fragment use receives correctly
240
+ ordered parameters, including repeated and nested uses; literal `$1` text in strings/comments is
241
+ unchanged.
145
242
 
146
243
  ```ts
147
244
  const email: string | undefined = 'reader@example.test';
@@ -178,10 +275,10 @@ fragment construction; nested objects retain ordinary driver serialization behav
178
275
  | SQL fragment, identifier, or query as a cell | Rejected | Rejected |
179
276
 
180
277
  Inserts accept one object or an array. Without `columns`, they infer the union of enumerable keys
181
- with defined values in first-seen order. Heterogeneous rows stay aligned. A single default-only
182
- row emits `DEFAULT VALUES`. Empty batches fail; multiple default-only rows need explicit columns.
183
- Assignments omit undefined values and fail when no assignments remain, even if surrounding SQL
184
- has other assignments.
278
+ with defined values in first-seen order. Heterogeneous rows stay aligned. A single default-only row
279
+ emits `DEFAULT VALUES`. Empty batches fail; multiple default-only rows need explicit columns.
280
+ Assignments omit undefined values and fail when no assignments remain, even if surrounding SQL has
281
+ other assignments.
185
282
 
186
283
  A supplied column list must be nonempty, unique, and valid. It selects and orders fields, ignoring
187
284
  unlisted keys. Missing insert cells use defaults; missing update fields are omitted. TypeScript
@@ -190,7 +287,7 @@ Helpers have no schema metadata; PostgreSQL enforces identity, generated-column,
190
287
  rules. Use [generated inputs](#insert-and-update-inputs) and application validation.
191
288
 
192
289
  Write arithmetic, `column = DEFAULT`, and `excluded.column` in the surrounding SQL. There is no
193
- expression/default sentinel. A complete query accepts at most 65,533 bound parameters, including
290
+ expression/default sentinel. A complete query accepts at most 65,535 bound parameters, including
194
291
  surrounding interpolations; Tenon does not split batches. See the [mutation cookbook](mutations.md).
195
292
 
196
293
  ## Transactions, guarded writes, and errors
@@ -221,8 +318,8 @@ Sequence advances and external effects are not reversible. A lost commit acknowl
221
318
  the outcome unknown; applications own retry and idempotency policy.
222
319
 
223
320
  Set isolation before data queries with `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`, and use
224
- `SET LOCAL statement_timeout = '5s'` to bound statements in that transaction. Manual savepoints are supported;
225
- reserve names beginning with `tenon_affected_rows_` for Tenon.
321
+ `SET LOCAL statement_timeout = '5s'` to bound statements in that transaction. Manual savepoints are
322
+ supported; reserve names beginning with `tenon_affected_rows_` for Tenon.
226
323
 
227
324
  SQL errors retain native driver details. Invalid inputs throw ordinary `Error` objects.
228
325
  `CardinalityError` has code `TENON_CARDINALITY_MISMATCH`, `expected` (`one` or `oneOrNone`),
@@ -233,59 +330,134 @@ produce `AggregateError`.
233
330
 
234
331
  Optional `onQuery` observes completed calls with `label`, `durationMs`, `outcome`, and available
235
332
  `command`, `rowCount`, and `errorCode`. It receives no SQL or values. Validation failures before
236
- execution are not observed. Observers are not awaited; failures emit warnings without changing
237
- the query outcome. Query success inside a callback does not establish outer commit.
333
+ execution are not observed. Observers are not awaited; failures emit warnings without changing the
334
+ query outcome. Query success inside a callback does not establish outer commit.
238
335
 
239
336
  ## Generated types and checking
240
337
 
241
- `generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript declarations
242
- from a consistent read-only snapshot; it does not need migration files or history. The CLI writes
243
- them atomically. Default namespace is `public`; default output is `src/generated-schema.ts`.
244
- Both `.ts` and `.d.ts` outputs are supported. Commit exact output and exclude it from formatting.
338
+ `generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript declarations from a
339
+ consistent read-only snapshot; it does not need migration files or history. The CLI writes them
340
+ atomically. Default namespace is `public`; default output is `src/generated-schema.ts`. Both `.ts`
341
+ and `.d.ts` outputs are supported. Commit exact output and exclude it from formatting.
245
342
 
246
343
  ```sh
247
344
  npx tenon generate --out src/generated-schema.d.ts
248
345
  npx tenon generate --out src/generated-schema.d.ts --check
249
346
  ```
250
347
 
251
- `--check` never rewrites the file. Missing/stale output fails with a diagnostic and a short diff
252
- for changes. It compares the generated representation, not pending migrations or every database
253
- object. Output includes relation/constraint/index summaries and referenced domains, not function
254
- bodies, grants, or a complete schema fingerprint.
348
+ `--check` never rewrites the file. Missing/stale output fails with a diagnostic and a short diff for
349
+ changes. It compares the generated representation, not pending migrations or every database object.
350
+ Output includes relation/constraint/index summaries and referenced domains, not function bodies,
351
+ grants, or a complete schema fingerprint.
352
+
353
+ **Generated declarations are not a version-stable schema fingerprint.** TypeScript mappings can
354
+ change without schema drift. The schema-summary comments describe SQL column types, defaults,
355
+ constraints, indexes, and referenced domains independently of those mappings, but their formatting
356
+ and PostgreSQL's rendering of definitions are not a versioned comparison contract either. Preserve
357
+ reviewed generator-specific references if an existing guard hashes this output; see
358
+ [baseline verification](adoption-guide.md#check-and-adopt-an-existing-database).
255
359
 
256
360
  Generation covers ordinary tables and partitioned roots. Views, foreign tables, and attached
257
361
  partitions need query-specific types. A missing namespace fails; an existing empty schema is valid.
258
362
  Exact table names are available through `Rows['users']`; valid nonreserved identifiers also receive
259
363
  named aliases. Use `Pick` for projections; a join or aggregate needs its actual selected shape.
260
364
 
261
- | PostgreSQL type | Read representation | Generated write input |
262
- | ---------------------------------------------- | -------------------- | ------------------------------------------------------------ |
263
- | smallint, integer, oid, real, double precision | `number` | `number` |
264
- | bigint | `string` | `string \| bigint` |
265
- | numeric | `string` | `string \| number` |
266
- | boolean | `boolean` | `boolean` |
267
- | text, name, varchar, char, uuid, time, timetz | `string` | `string` |
268
- | date, timestamp, timestamptz | `Date` | `Date \| string` |
269
- | enum | String literal union | Same union; `never` for an empty enum |
270
- | json, jsonb | `unknown` | `Exclude<JSONValue, null>`; `JSONValue` is exported by Tenon |
271
- | bytea | `Buffer` | `Buffer` |
272
-
273
- Nullable columns add null; `unknown` already includes it. Domains retain verified base mappings
274
- and the column's read nullability. Verified arrays use `PgArray<T> = (T | null | PgArray<T>)[]` for
275
- null elements and multiple dimensions. Numeric arrays contain strings; enum arrays and arrays of
276
- domains are unverified. Unmapped types warn and use `unknown` reads / `never` non-null writes.
365
+ | PostgreSQL type | Read representation | Generated write input |
366
+ | ---------------------------------------------- | -------------------- | ------------------------------------- |
367
+ | smallint, integer, oid, real, double precision | `number` | `number` |
368
+ | bigint | `string` | `string \| bigint` |
369
+ | numeric | `string` | `string \| number` |
370
+ | boolean | `boolean` | `boolean` |
371
+ | text, name, varchar, char, uuid, time, timetz | `string` | `string` |
372
+ | date, timestamp, timestamptz | `Date \| number` | `Date \| string` |
373
+ | enum | String literal union | Same union; `never` for an empty enum |
374
+ | json, jsonb | `unknown` | `string` containing encoded JSON |
375
+ | bytea | `Buffer` | `Buffer` |
376
+
377
+ Nullable columns add null; `unknown` already includes it. Domains retain verified base mappings and
378
+ the column's read nullability. Verified arrays use `PgArray<T> = (T | null | PgArray<T>)[]` for null
379
+ elements and multiple dimensions. Numeric arrays contain numbers and can lose precision; select/cast
380
+ them as text when exact values are required. Bigint arrays contain strings; enum arrays and arrays
381
+ of domains are unverified. Unmapped types warn and use `unknown` reads / `never` non-null writes.
277
382
  Custom parsers need matching application types.
278
383
 
279
- Date-only parsing uses UTC; timestamps without time zone use Node's local timezone. Infinite
280
- temporal reads become invalid dates: check `Number.isFinite(value.getTime())` before date methods.
281
- Bound temporal values require finite dates; use SQL literals for PostgreSQL infinity.
384
+ Date-only values and timestamps without time zone use Node's local timezone. Infinite temporal reads
385
+ are numbers (`Infinity` or `-Infinity`); narrow with `value instanceof Date` before date methods.
386
+ Temporal write inputs accept `Date | string`; use PostgreSQL's text `infinity` or `-infinity` when
387
+ needed.
282
388
 
283
389
  ### Persistence and domain values
284
390
 
285
- Generated types describe storage representations. Text `CHECK` constraints do not become unions,
286
- and JSON reads remain unknown; validate before narrowing to domain types. Convert third-party
287
- payloads into validated native JSON-compatible objects rather than asserting compatibility or
288
- pre-serializing them. A bound null means SQL null; use `'null'::jsonb` for JSON null.
391
+ Generated types describe storage representations. Text `CHECK` constraints do not become unions, and
392
+ JSON reads remain unknown. Validate values before narrowing to application domain types.
393
+
394
+ For example, assume this additional table and regenerate `Rows` from the resulting schema:
395
+
396
+ ```sql
397
+ CREATE TABLE public.sessions (
398
+ id text PRIMARY KEY,
399
+ data jsonb NOT NULL,
400
+ expires_at timestamptz NOT NULL
401
+ );
402
+ ```
403
+
404
+ Use the same application validator before encoding a write and when restoring a domain value:
405
+
406
+ ```ts
407
+ import { sql } from '@kequtech/tenon-orm';
408
+ import type { Rows } from './generated-schema.js';
409
+
410
+ function sessionData(value: unknown): { userId: string } {
411
+ if (
412
+ typeof value !== 'object' ||
413
+ value === null ||
414
+ Array.isArray(value) ||
415
+ !('userId' in value) ||
416
+ typeof value.userId !== 'string' ||
417
+ value.userId.length === 0
418
+ ) {
419
+ throw new Error('Expected session data with a nonempty userId');
420
+ }
421
+ return { userId: value.userId };
422
+ }
423
+
424
+ function finiteDate(value: Date | number): Date {
425
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
426
+ throw new Error('Expected a finite database timestamp');
427
+ }
428
+ return value;
429
+ }
430
+
431
+ const input = {
432
+ id: 'example-session',
433
+ data: JSON.stringify(sessionData({ userId: 'user-42' })),
434
+ expires_at: new Date(Date.now() + 60_000),
435
+ } satisfies Rows.Insert['sessions'];
436
+ await db.query(sql`INSERT INTO public.sessions ${sql.insertValues(input)}`);
437
+
438
+ const row = await db.query(
439
+ sql<Rows['sessions']>`SELECT id, data, expires_at FROM public.sessions
440
+ WHERE id = ${input.id} AND isfinite(expires_at) AND expires_at > now()`,
441
+ { returns: 'oneOrNone' },
442
+ );
443
+ const session =
444
+ row === undefined
445
+ ? undefined
446
+ : {
447
+ id: row.id,
448
+ data: sessionData(row.data),
449
+ expiresAt: finiteDate(row.expires_at),
450
+ };
451
+ ```
452
+
453
+ The generated JSON read remains `unknown` and the timestamp remains `Date | number`. The projection
454
+ validates both without assertions or edits to generated types. Preserve null separately for nullable
455
+ columns. For an authentication policy requiring finite expiry, `isfinite(expires_at)` excludes both
456
+ infinities before the expiry comparison; `expires_at > now()` alone would accept positive infinity.
457
+ Include such predicates in expiry-sensitive updates/deletes too, and test infinite and expired
458
+ values. This example illustrates representation validation; applications must also retain their
459
+ token verification, revocation, and authorization checks. See
460
+ [JSON and PostgreSQL arrays](mutations.md#json-and-postgresql-arrays) for array write boundaries.
289
461
 
290
462
  Bigint and `count(*)` reads are decimal strings. Use SQL or BigInt for exact arithmetic and decimal
291
463
  strings at JSON boundaries; convert to number only when bounded or approximate values are intended.
@@ -303,14 +475,14 @@ sql.assignments(patch);
303
475
 
304
476
  Insert fields are required when nonnullable, writable, and without a default. Other writable insert
305
477
  fields and all writable update fields are optional and accept explicit undefined, including with
306
- `exactOptionalPropertyTypes`. Explicit null requires both column and domain-chain permission.
307
- Domain defaults count; column defaults take precedence.
478
+ `exactOptionalPropertyTypes`. Explicit null requires both column and domain-chain permission. Domain
479
+ defaults count; column defaults take precedence.
308
480
 
309
481
  Computed and `GENERATED ALWAYS` identity columns use optional `never` fields. Serial and
310
- `GENERATED BY DEFAULT` identities are optional on insert and writable on update; other primary
311
- keys remain writable. Use `Pick`/`Omit` for application restrictions. Apply the generated types
312
- through annotations, `satisfies`, or helper generic arguments; they do not validate constraints,
313
- JSON structure, authorization, or nonempty input.
482
+ `GENERATED BY DEFAULT` identities are optional on insert and writable on update; other primary keys
483
+ remain writable. Use `Pick`/`Omit` for application restrictions. Apply the generated types through
484
+ annotations, `satisfies`, or helper generic arguments; they do not validate constraints, JSON
485
+ structure, authorization, or nonempty input.
314
486
 
315
487
  For unverified write mappings, provide a reviewed application-owned replacement, for example
316
488
  `Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for interval text input. Selected columns
@@ -319,19 +491,20 @@ can discard required fields; see [the cookbook](mutations.md#input-types-and-run
319
491
  ## Query inspection
320
492
 
321
493
  `describeQuery({ connection, query, session?, timeouts? })` accepts a PostgreSQL URI and a finished
322
- SQL string or `{ text }` object. It returns PostgreSQL metadata, conservative read mappings and
323
- row declaration, session context, and coded findings. It does not bind values, execute the
324
- statement, or verify `sql<Row>`.
494
+ SQL string or `{ text }` object. It returns PostgreSQL metadata, conservative read mappings and row
495
+ declaration, session context, and coded findings. It does not bind values, execute the statement, or
496
+ verify `sql<Row>`.
325
497
 
326
498
  Inspection owns a separate session with bounded timeouts. Match runtime settings; temporary tables,
327
- uncommitted changes, and custom parsers are not inherited. Nullability stays unknown. See
499
+ and uncommitted changes are not inherited. Reported mappings assume pg's default parsers; parser
500
+ overrides need matching application types. Nullability stays unknown. See
328
501
  [query inspection](query-inspection.md) for settings, result fields, issue codes, and testing.
329
502
 
330
503
  ## Command reference
331
504
 
332
- Database commands require `DATABASE_URL`. `new` and `--help` work offline. No environment files
333
- are loaded. Paths are relative to the working directory. Explicit flags unsupported by a command
334
- are rejected; `--check` is only a generation check, not a migration dry run.
505
+ Database commands require `DATABASE_URL`. `new` and `--help` work offline. No environment files are
506
+ loaded. Paths are relative to the working directory. Explicit flags unsupported by a command are
507
+ rejected; `--check` is only a generation check, not a migration dry run.
335
508
 
336
509
  ```text
337
510
  tenon new <name> [--directory migrations]
@@ -356,10 +529,10 @@ file has down SQL before executing any; `steps` is a positive safe integer. Use
356
529
  function bodies and explicit `E'...'` backslash escapes. Nontransactional DDL such as
357
530
  `CREATE INDEX CONCURRENTLY` requires a separate procedure.
358
531
 
359
- Migration/status/rollback/adoption use session advisory locks per database and namespace;
360
- generation uses the same lock in shared mode. Use direct or session-pooled connections and server
361
- timeouts to bound waits. Baseline capture uses a local creation lock and native `pg_dump`;
362
- it does not acquire the runner's database advisory lock. See [adoption](adoption-guide.md).
532
+ Migration/status/rollback/adoption use session advisory locks per database and namespace; generation
533
+ uses the same lock in shared mode. Use direct or session-pooled connections and server timeouts to
534
+ bound waits. Baseline capture uses a local creation lock and native `pg_dump`; it does not acquire
535
+ the runner's database advisory lock. See [adoption](adoption-guide.md).
363
536
 
364
537
  Programmatic `migrate`, `migrationStatus`, and `rollback` accept
365
538
  `{ connection, directory?, namespace? }`; rollback also accepts `steps`. Migration/rollback return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kequtech/tenon-orm",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "A PostgreSQL ORM designed for coding agents, with explicit SQL, typed queries, and migrations",
@@ -54,7 +54,8 @@
54
54
  "benchmark": "npm run build && node scripts/benchmark.mjs"
55
55
  },
56
56
  "dependencies": {
57
- "postgres": "3.4.9"
57
+ "pg": "8.23.1",
58
+ "@types/pg": "8.23.1"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@types/node": "^24.19.2",
@@ -1,5 +0,0 @@
1
- import type postgres from 'postgres';
2
- /** Preserve driver scalar parsers and serializers; replace only lossy array parsing. */
3
- export declare function prepareArrays(sql: postgres.Sql): Promise<void>;
4
- /** PostgreSQL text arrays: only unquoted NULL is null; quoted values use the scalar parser. */
5
- export declare function parseArray(text: string, parse: (value: string) => unknown, delimiter?: string): unknown[];