@kequtech/tenon-orm 0.3.0 → 0.4.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 (63) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +27 -5
  3. package/dist/affected-rows.js +0 -1
  4. package/dist/baseline.js +3 -5
  5. package/dist/check/contracts.d.ts +6 -0
  6. package/dist/check/contracts.js +206 -0
  7. package/dist/check/project.d.ts +18 -0
  8. package/dist/check/project.js +224 -0
  9. package/dist/check/source.d.ts +51 -0
  10. package/dist/check/source.js +629 -0
  11. package/dist/check/types.d.ts +36 -0
  12. package/dist/check/types.js +1 -0
  13. package/dist/check.d.ts +4 -0
  14. package/dist/check.js +147 -0
  15. package/dist/cli.js +38 -9
  16. package/dist/connection.d.ts +5 -12
  17. package/dist/connection.js +12 -121
  18. package/dist/database.js +5 -8
  19. package/dist/describe.js +10 -3
  20. package/dist/generate.d.ts +2 -3
  21. package/dist/generate.js +26 -24
  22. package/dist/generation/catalog.d.ts +5 -6
  23. package/dist/generation/catalog.js +7 -9
  24. package/dist/generation/render.d.ts +3 -0
  25. package/dist/generation/render.js +60 -0
  26. package/dist/generation/schema-summary.d.ts +3 -3
  27. package/dist/generation/schema-summary.js +2 -2
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +2 -0
  30. package/dist/inspection/catalog.d.ts +3 -6
  31. package/dist/inspection/catalog.js +9 -18
  32. package/dist/inspection/cli.d.ts +12 -0
  33. package/dist/inspection/cli.js +9 -4
  34. package/dist/inspection/connection.d.ts +2 -3
  35. package/dist/inspection/connection.js +20 -22
  36. package/dist/inspection/options.js +0 -6
  37. package/dist/inspection/result.d.ts +3 -2
  38. package/dist/inspection/result.js +13 -26
  39. package/dist/inspection/types.d.ts +6 -4
  40. package/dist/migrations/connection.d.ts +3 -3
  41. package/dist/migrations/connection.js +14 -21
  42. package/dist/migrations/history.d.ts +4 -4
  43. package/dist/migrations/history.js +5 -5
  44. package/dist/migrations.js +7 -9
  45. package/dist/query-options.d.ts +1 -1
  46. package/dist/query.js +1 -1
  47. package/dist/transaction.d.ts +2 -2
  48. package/dist/transaction.js +21 -77
  49. package/dist/types.d.ts +1 -1
  50. package/docs/adoption-guide.md +64 -12
  51. package/docs/agent-guide.md +57 -27
  52. package/docs/human-guide.md +53 -18
  53. package/docs/mutations.md +22 -9
  54. package/docs/query-checking.md +200 -0
  55. package/docs/query-inspection.md +40 -13
  56. package/docs/reference.md +330 -108
  57. package/package.json +11 -7
  58. package/dist/driver/types.d.ts +0 -10
  59. package/dist/driver/types.js +0 -49
  60. package/dist/generation/table-types.d.ts +0 -2
  61. package/dist/generation/table-types.js +0 -96
  62. package/dist/inspection/protocol.d.ts +0 -7
  63. package/dist/inspection/protocol.js +0 -45
package/docs/reference.md CHANGED
@@ -7,71 +7,97 @@ table from the human guide and an existing `db` instance, unless stated otherwis
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 `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"`.
10
+ development typings. Tenon includes pgclear 0.3.x as a runtime dependency. A usual Node TypeScript
11
+ setup uses `module: "NodeNext"`.
12
12
 
13
13
  ```sh
14
14
  npm install @kequtech/tenon-orm
15
15
  ```
16
16
 
17
- `createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, `native`,
18
- and `close`. The required `connection` accepts:
17
+ Use `sql` from `@kequtech/tenon-orm` for `db.query`. Tenon also re-exports Pgclear's `json`,
18
+ `param`, `builtin`, `defaultCodecs`, and `createTemporalCodecs`, plus the `Codec`, `CodecRegistry`,
19
+ `Parameter`, `PgArray`, `TemporalConstructors`, and `TemporalInput` types. These are the driver's
20
+ original helpers and types; Pgclear continues to own conversion. Generated schema declarations
21
+ import their helper types from Tenon.
19
22
 
20
- - A PostgreSQL URI string.
21
- - A pg `PoolConfig` object; an explicit `{}` uses the driver's environment defaults.
22
- - An existing `pg.Pool`, borrowed by identity.
23
+ `createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, `native`,
24
+ and `close`. The required `connection` accepts a PostgreSQL URI, pgclear `PoolOptions`, or an existing
25
+ pgclear `Pool`, borrowed by identity. `ConnectionOptions` aliases pgclear's `PoolOptions`.
23
26
 
24
27
  ```ts
25
28
  const db = createTenon({
26
29
  connection: {
27
30
  connectionString: connection,
28
- connectionTimeoutMillis: 5000,
29
- options: '-c search_path=app,public',
31
+ maxConnections: 10,
32
+ idleTimeoutMs: 30_000,
33
+ connectTimeoutMs: 5000,
34
+ acquireTimeoutMs: 5000,
35
+ startupParameters: { search_path: 'app,public' },
30
36
  },
31
37
  });
32
38
  ```
33
39
 
34
- Strings become pg's `connectionString`; configuration objects pass to `new Pool(config)`.
35
- `ConnectionOptions` is an alias for pg's `PoolConfig`. Driver options and defaults are unchanged,
36
- including no acquisition timeout unless `connectionTimeoutMillis` is configured. Pool timeouts use
37
- milliseconds. pg parses the URI for connection fields; matching URI values override client
38
- configuration, including an `options` URI parameter overriding the configuration's `options` string.
39
- Preserve deployment credentials and TLS requirements; see
40
- [pg connection configuration](https://node-postgres.com/features/connecting).
40
+ Configuration follows pgclear. Object fields override URI settings. Environment variables, pgpass,
41
+ service files, and `.env` files are not loaded by the driver; pass credentials and deployment
42
+ configuration explicitly. An explicit `{}` uses pgclear's localhost/current-user defaults. Use
43
+ `startupParameters` for session settings and `tls` for verified TLS. Unknown options reject.
44
+ See the installed pgclear public API documentation for driver configuration.
41
45
 
42
46
  Reuse an instance for each database configuration. `db.close({ timeoutMs? })` immediately rejects
43
- new Tenon work. For owned pools, accepted Tenon operations can drain for up to 5,000 ms by default;
44
- at the deadline, remaining operations reject and Tenon requests driver termination. `0` skips the
45
- grace period. The timeout must be an integer from 0 to 2,147,483,647 milliseconds; `CloseOptions` is
46
- exported. This bounds Tenon's wait, not every native transport's teardown or server execution. It
47
- sends no PostgreSQL cancellation request and does not cancel JavaScript callbacks. An unbounded
48
- authentication attempt can keep a connection open after close resolves. Configure pg's
49
- `connectionTimeoutMillis` when connection establishment also needs a deadline.
50
-
51
- The first valid close call chooses the timeout. Concurrent and later calls share the same promise
52
- and outcome. Invalid initial options reject without closing the instance. For borrowed pools, close
53
- only disables the Tenon wrapper; the application retains the pool and outstanding work. Tenon does
54
- not load environment files.
55
-
56
- Qualify SQL names or configure runtime `search_path`. CLI `--namespace` selects tooling scope; it
57
- does not configure application queries. Baseline capture and Describe accept connection strings
47
+ new Tenon calls and delegates owned resource shutdown to pgclear. The default grace period is
48
+ 5,000 ms; `0` requests immediate shutdown. Remaining work and checkouts expire at the deadline.
49
+ Timeout must be an integer from 0 to 2,147,483,647 milliseconds. Shutdown waits for owned driver
50
+ resources, not arbitrary JavaScript callbacks or proof of remote rollback.
51
+
52
+ The first valid close call fixes its timeout; concurrent and later calls share its promise and
53
+ outcome. Invalid initial options reject without closing. For borrowed pools, close disables the
54
+ Tenon wrapper while the application retains pool ownership and outstanding work.
55
+
56
+ Qualify SQL names or configure runtime `search_path`. CLI `--namespace` selects tooling scope;
57
+ it does not configure application queries. Baseline capture and Describe accept connection strings
58
58
  rather than the runtime connection union.
59
59
 
60
+ ### Driver updates
61
+
62
+ Tenon declares `@kequtech/pgclear: ^0.3.0`, allowing compatible 0.3.x patch updates without a new
63
+ Tenon release. With npm, run `npm update @kequtech/pgclear`, review the lockfile and run
64
+ `npm ls @kequtech/pgclear`, and commit the resolved dependency change after application checks.
65
+ An existing lockfile keeps its selected version until updated. A future Pgclear 0.4 release is outside this
66
+ range and needs an explicit Tenon compatibility update; it is not an automatic upgrade.
67
+
68
+ Install Pgclear directly only when the application imports its native APIs, such as `listen` or
69
+ `createPool`. Keep that version compatible with Tenon's declared range to allow a shared copy. If
70
+ both SQL tags are needed, alias the native one as `pgSql`: Tenon's tag belongs in `db.query`, while
71
+ Pgclear's tag belongs in native `query`. Native `queryClassic(text, values)` avoids a second tag.
72
+ TypeScript and runtime validation reject sending one package's query to the other package's API.
73
+
60
74
  ## Native driver access
61
75
 
62
- `db.native` is the actual `pg.Pool`, readonly in TypeScript. Native calls bypass Tenon's result
63
- modes, affected-row guards, SQL checks, and `onQuery`. They follow pg's query configuration and do
64
- not join a Tenon transaction; callback clients expose only `tx.query`.
76
+ `db.native` is the actual pgclear `Pool`, readonly in TypeScript. Native calls bypass Tenon's result
77
+ modes, affected-row guards, SQL checks, and `onQuery`. They do not join a Tenon callback transaction;
78
+ callback clients expose only `tx.query`.
79
+
80
+ Use `queryClassic(text, values?)`, `acquire`/`withSession`, `prepare`, streams, and COPY through pgclear's
81
+ public APIs. Held sessions must be released. Use pgclear `listen` for a dedicated notification
82
+ connection with explicit reconnect and lifecycle policy. Notifications have no replay.
83
+
84
+ Pgclear's native `query` accepts only its own `sql` tag results. Tenon's `sql` and `unsafeSql` results
85
+ belong in `db.query`; use `queryClassic` for finished SQL text at the native boundary.
65
86
 
66
- Use pg's [native APIs](https://node-postgres.com/apis/pool) and compatible cursor/COPY packages when
67
- needed. For LISTEN, check out a client with `await db.native.connect()`, attach `notification` and
68
- `error` listeners, and issue `LISTEN` on that client. Keep it checked out while listening; release
69
- it after `UNLISTEN`. Native queued acquisitions follow pg's shutdown behavior and must be settled by
70
- their caller before closing. A disconnected client does not reconnect. The application owns
71
- resubscription and catch-up; notifications are not replayed.
87
+ ```ts
88
+ import { listen } from '@kequtech/pgclear';
72
89
 
73
- Close owned pools through `db.close()` for bounded shutdown, including checked-out native clients.
74
- Borrowed pools and their listeners remain usable after wrapper close until their owner ends them.
90
+ const listener = await listen(connection, {
91
+ channels: ['users_changed'],
92
+ onNotification: ({ payload }) => console.log(payload),
93
+ reconnect: true,
94
+ });
95
+ // At application shutdown:
96
+ await listener.close();
97
+ ```
98
+
99
+ Close owned pools through `db.close()`. A separate listener has its own lifetime; close it explicitly.
100
+ Borrowed pools remain usable until their owner calls `pool.close()`.
75
101
 
76
102
  ## Queries and result modes
77
103
 
@@ -92,11 +118,13 @@ must be a nonempty string.
92
118
  | `returns` | Output | Allowed returned rows |
93
119
  | ------------------ | -------------------------------- | --------------------- |
94
120
  | `native` (default) | `QueryResult<Row>` driver object | Any |
95
- | `many` | Plain `Row[]` | Any |
121
+ | `many` | `readonly Row[]` | Any |
96
122
  | `oneOrNone` | `Row \| undefined` | Zero or one |
97
123
  | `one` | `Row` | Exactly one |
98
124
 
99
- Native results expose `rows`, `rowCount`, `command`, and `fields`, following pg's `QueryResult`.
125
+ Native results expose `rows`, `rowCount`, `command`, `commandTag`, and `fields`, following pgclear's
126
+ `QueryResult`. Counts are `number | null`; row arrays are readonly. Counts outside JavaScript's safe
127
+ integer range reject instead of rounding. SQL `count(*)` is an int8 column value and reads as a string.
100
128
  `rowCount` is the command's affected-row count, independent of `RETURNING`; it can be null for
101
129
  commands without a count. Result modes never add `LIMIT` or `RETURNING`. A returned-row mismatch
102
130
  alone does not undo an autocommitted write; use `affectedRows` for write bounds that roll back.
@@ -104,16 +132,45 @@ alone does not undo an autocommitted write; use `affectedRows` for write bounds
104
132
  Functions accepting `QueryClient` can use either `Database` or `Transaction`. Public types also
105
133
  include `QueryOptions`, `QueryReturnMode`, and `QueryOutput<Row, Mode>`.
106
134
 
135
+ ### Adapting transactional results
136
+
137
+ For a job library or other adapter expecting `{ rows }`, extract the native result's row array:
138
+
139
+ ```ts
140
+ import { unsafeSql, type QueryClient } from '@kequtech/tenon-orm';
141
+
142
+ async function queryRows(tx: QueryClient, text: string, values: readonly unknown[]) {
143
+ const result = await tx.query(unsafeSql(text, values));
144
+ return { rows: result.rows };
145
+ }
146
+ ```
147
+
148
+ Alternatively, request `{ returns: 'many' }` and wrap the returned array in `{ rows }`. Supply the
149
+ callback's `tx` when the adapter's work must commit with application writes. A native pool query
150
+ uses a separate checkout and does not join that transaction. Await adapter calls before returning
151
+ from the callback, retain commit/rollback tests, and make recording helpers return pgclear native
152
+ results. The adapter's SQL text must be trusted and values must remain separately bound.
153
+
107
154
  ## SQL parameters and identifiers
108
155
 
109
156
  Ordinary `sql` interpolations bind values. Do not quote them or write manual `$n` placeholders
110
- inside templates. Values follow pg serialization: `null` and `undefined` bind as SQL null, arrays
111
- bind as PostgreSQL arrays, and objects use pg's JSON serialization. For JSON input, use
112
- `JSON.stringify(value)`, including for JSON arrays, strings, and JSON null. Generated JSON write
113
- types accept encoded strings. Cast standalone parameters when SQL must establish their type.
157
+ inside templates. Plain parameters accept null, string, finite number, bigint, boolean, Date,
158
+ supported native/polyfilled Temporal values, and Uint8Array. Undefined, plain objects, arrays,
159
+ invalid Date values, nonfinite numbers, and unsafe integer numbers reject.
160
+ Use pgclear `json(value)` for JSON and `param(value, codec)` for arrays or custom encodings.
161
+ Encoded strings also work where PostgreSQL accepts text. Cast standalone parameters when SQL must
162
+ establish their type. For int8, use exact strings, bigint, or `param(text, builtin.int8)`.
163
+
164
+ Date values encode in UTC. Date, bigint, and supported Temporal inputs encode as text with OID 0,
165
+ using server inference; they do not select an output decoder. Temporal calendar dates, wall times,
166
+ instants, and durations have different PostgreSQL targets. Use the intended value and SQL cast,
167
+ rather than treating every temporal value as an interchangeable timestamp. Pgclear does not install
168
+ Temporal or a polyfill automatically.
114
169
 
115
170
  ```ts
116
- const ids = [1, 2, 3];
171
+ import { builtin, param } from '@kequtech/tenon-orm';
172
+
173
+ const ids = param([1, 2, 3], builtin.int4Array);
117
174
  const query = sql<{ id: number }>`SELECT id FROM public.users
118
175
  WHERE id = ANY(${ids}::integer[])`;
119
176
  ```
@@ -165,7 +222,7 @@ await db.query(sql`UPDATE public.users SET ${sql.assignments({ active: false })}
165
222
 
166
223
  Both accept plain objects, including null-prototype objects. Columns are quoted as individual
167
224
  identifiers. Only own data properties are read; accessors are rejected. Fields are captured at
168
- fragment construction; nested objects retain ordinary driver serialization behavior.
225
+ fragment construction; bound values retain pgclear encoding rules and must remain unchanged until settlement.
169
226
 
170
227
  | Input | Insert | Assignment |
171
228
  | -------------------------------------------- | ---------------------- | --------------- |
@@ -204,8 +261,8 @@ await db.transaction(async (tx) => {
204
261
 
205
262
  Use `tx` for every operation that must commit together and await the work. The callback commits on
206
263
  success, rolls back on an escaping error, and expires before cleanup. Transactions belong to one
207
- database instance. Nested callbacks and automatic retries are not provided. Tenon owns direct
208
- BEGIN/COMMIT/ROLLBACK boundaries.
264
+ database instance. Nested callbacks and automatic retries are not provided. pgclear owns
265
+ BEGIN/COMMIT/ROLLBACK and recovery.
209
266
 
210
267
  `affectedRows: { exact: n }` or `{ max: n }` accepts a nonnegative safe integer. It checks the final
211
268
  INSERT/UPDATE/DELETE/MERGE count independently of `RETURNING`. A guarded root call uses a
@@ -219,81 +276,191 @@ the outcome unknown; applications own retry and idempotency policy.
219
276
 
220
277
  Set isolation before data queries with `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`, and use
221
278
  `SET LOCAL statement_timeout = '5s'` to bound statements in that transaction. Manual savepoints are
222
- supported; reserve names beginning with `tenon_affected_rows_` for Tenon.
279
+ supported on the owned callback session; pgclear chooses guarded-operation savepoints.
223
280
 
224
- SQL errors retain native driver details. Invalid inputs throw ordinary `Error` objects.
281
+ SQL errors retain native driver details. Tenon's option/composition validation throws ordinary
282
+ `Error` objects; driver encoding failures retain `PGCLEAR_*` codes.
225
283
  `CardinalityError` has code `TENON_CARDINALITY_MISMATCH`, `expected` (`one` or `oneOrNone`),
226
284
  `actual` (returned row count), and optional `label`. It does not change transaction or autocommit
227
285
  behavior. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
228
- `TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`. Work/cleanup failures can
229
- produce `AggregateError`.
286
+ `TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`; its `actual` is `number | null`.
287
+ Work/cleanup failures can produce `AggregateError`.
230
288
 
231
289
  Optional `onQuery` observes completed calls with `label`, `durationMs`, `outcome`, and available
232
- `command`, `rowCount`, and `errorCode`. It receives no SQL or values. Validation failures before
233
- execution are not observed. Observers are not awaited; failures emit warnings without changing the
290
+ `command`, `rowCount` (`number | null`), and `errorCode`. It receives no SQL or values. Validation
291
+ failures before execution are not observed. Observers are not awaited; failures emit warnings without changing the
234
292
  query outcome. Query success inside a callback does not establish outer commit.
235
293
 
236
294
  ## Generated types and checking
237
295
 
238
- `generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript declarations from a
239
- consistent read-only snapshot; it does not need migration files or history. The CLI writes them
240
- atomically. Default namespace is `public`; default output is `src/generated-schema.ts`. Both `.ts`
241
- and `.d.ts` outputs are supported. Commit exact output and exclude it from formatting.
296
+ `generateDefinitions({ connection, namespace?, onWarning? })` reads schema catalogs and renders
297
+ TypeScript declarations in Tenon. It interprets defaults, domains, identities, writable columns, and
298
+ schema summaries, coordinates with the migration advisory lock, and excludes its history table.
299
+ It needs no migration files/history. The CLI owns atomic output and freshness checks. Decoder
300
+ representations come from pgclear's selected registry rather than a second conversion registry.
242
301
 
243
302
  ```sh
244
303
  npx tenon generate --out src/generated-schema.d.ts
245
304
  npx tenon generate --out src/generated-schema.d.ts --check
246
305
  ```
247
306
 
248
- `--check` never rewrites the file. Missing/stale output fails with a diagnostic and a short diff for
249
- changes. It compares the generated representation, not pending migrations or every database object.
250
- Output includes relation/constraint/index summaries and referenced domains, not function bodies,
251
- grants, or a complete schema fingerprint.
252
-
253
- Generation covers ordinary tables and partitioned roots. Views, foreign tables, and attached
254
- partitions need query-specific types. A missing namespace fails; an existing empty schema is valid.
255
- Exact table names are available through `Rows['users']`; valid nonreserved identifiers also receive
256
- named aliases. Use `Pick` for projections; a join or aggregate needs its actual selected shape.
257
-
258
- | PostgreSQL type | Read representation | Generated write input |
259
- | ---------------------------------------------- | -------------------- | ------------------------------------- |
260
- | smallint, integer, oid, real, double precision | `number` | `number` |
261
- | bigint | `string` | `string \| bigint` |
262
- | numeric | `string` | `string \| number` |
263
- | boolean | `boolean` | `boolean` |
264
- | text, name, varchar, char, uuid, time, timetz | `string` | `string` |
265
- | date, timestamp, timestamptz | `Date \| number` | `Date \| string` |
266
- | enum | String literal union | Same union; `never` for an empty enum |
267
- | json, jsonb | `unknown` | `string` containing encoded JSON |
268
- | bytea | `Buffer` | `Buffer` |
269
-
270
- Nullable columns add null; `unknown` already includes it. Domains retain verified base mappings and
271
- the column's read nullability. Verified arrays use `PgArray<T> = (T | null | PgArray<T>)[]` for null
272
- elements and multiple dimensions. Numeric arrays contain numbers and can lose precision; select/cast
273
- them as text when exact values are required. Bigint arrays contain strings; enum arrays and arrays
274
- of domains are unverified. Unmapped types warn and use `unknown` reads / `never` non-null writes.
275
- Custom parsers need matching application types.
276
-
277
- Date-only values and timestamps without time zone use Node's local timezone. Infinite temporal reads
278
- are numbers (`Infinity` or `-Infinity`); narrow with `value instanceof Date` before date methods.
279
- Temporal write inputs accept `Date | string`; use PostgreSQL's text `infinity` or `-infinity` when
280
- needed.
307
+ The default namespace is `public`; default output is `src/generated-schema.ts`. Commit exact output
308
+ and exclude it from formatting. `--check` never rewrites: missing/stale output fails with a bounded
309
+ diff. This checks file freshness, not query annotations, pending migrations, or every database object.
310
+ Upgrading pgclear can change output without schema drift. Preserve reviewed generator-specific
311
+ references for existing adoption guards; see [baseline verification](adoption-guide.md#check-and-adopt-an-existing-database).
312
+
313
+ The generated module exports `Database[table]['row' | 'insert' | 'update']`. Ordinary tables and
314
+ partitioned roots are included. Views, foreign tables, and attached partitions need authored types.
315
+ Relationships, indexes, constraints, and referenced domains appear in comments. Use `Pick` for
316
+ projections; joins and expressions need their actual selected shape.
317
+
318
+ | PostgreSQL type | Default read | Generated write input |
319
+ | ---------------------------------------------- | ------------------------------- | ------------------------------------- |
320
+ | smallint, integer, oid, real, double precision | `number` | `number` |
321
+ | bigint | `string` | `string \| bigint` |
322
+ | numeric | `string` | `string \| bigint` |
323
+ | boolean | `boolean` | `boolean` |
324
+ | text, UUID, timetz | `string` | `string` |
325
+ | date, timestamp, timestamptz | `string` | `string \| Date \| TemporalInput` |
326
+ | time, interval | `string` | `string \| TemporalInput` |
327
+ | enum | String literal union | Same union |
328
+ | json/jsonb | `unknown` | Encoded string or pgclear `Parameter` |
329
+ | bytea | `Uint8Array` | `Uint8Array` |
330
+ | Supported built-in arrays | Recursive readonly `PgArray<T>` | Encoded string or pgclear `Parameter` |
331
+
332
+ Every writable generated field also accepts a pgclear `Parameter`; the scalar entries above show
333
+ plain inputs. SQL NULL is separate; nullable fields add null. Numeric and bigint arrays preserve
334
+ exact strings, and temporal strings retain precision and infinity spellings. Dynamic enum/domain/extension
335
+ arrays remain wire text unless a decoder changes them. Generation follows the selected registry.
336
+ Custom text codecs can declare `text.typescript`, and those expressions are preserved in generated
337
+ row types. A custom decoder without a known representation warns and becomes unknown. JSON member
338
+ shapes remain unknown under default decoding. Database constraints and textual formats still
339
+ require application validation. A scalar codec declaration describes every use of that codec's OID;
340
+ it does not establish different application meanings for individual columns.
341
+
342
+ Default-registry enum rows use catalog label unions. With a custom registry, a declared string
343
+ representation stays string because that decoder may transform the labels; an explicit codec
344
+ representation can declare a narrower output type. Enum write inputs retain the database choices.
345
+
346
+ Programmatic generation uses the registry configured on its connection or supplied pool. The CLI
347
+ uses default codecs. Regenerate committed definitions after a driver mapping change before running
348
+ `generate --check`; custom decoder types that remain unknown need application-owned contracts.
349
+ Catalog queries use default result decoders on the same borrowed session and snapshot, while
350
+ application row types follow that session's configured registry. Custom application codecs therefore
351
+ do not change the generator's own metadata-reading types.
352
+
353
+ Codec type expressions must resolve where they are compiled. Relative import expressions in saved
354
+ declarations resolve from the generated file; the checker's in-memory module resolves from the
355
+ tsconfig directory. Prefer a package or configured type alias that resolves in both places when one
356
+ registry is shared across generation and checking. Tenon does not execute the referenced modules.
357
+
358
+ Output conversions are explicit: for example, `defaultCodecs.with(builtin.int8BigInt)` makes int8
359
+ reads bigint, and `defaultCodecs.with(builtin.timestamptzDate)` makes timestamptz reads Date.
360
+ Pgclear also supplies codecs for caller-provided Temporal implementations. Use the same registry for
361
+ execution, programmatic generation, Describe, and query checking. Defaults stay strings regardless
362
+ of which temporal features the runtime provides.
363
+
364
+ ### Application aliases
365
+
366
+ For shorter names, export aliases from a handwritten application module such as `src/schema-types.ts`:
367
+
368
+ ```ts
369
+ import type { Database } from './generated-schema.js';
370
+
371
+ export type UserRow = Database['users']['row'];
372
+ export type UserInsert = Database['users']['insert'];
373
+ export type UserUpdate = Database['users']['update'];
374
+ ```
375
+
376
+ These aliases reference the generated definitions directly and follow changes when the schema is
377
+ regenerated. Keep them in the application module so regeneration preserves your chosen names.
281
378
 
282
379
  ### Persistence and domain values
283
380
 
284
381
  Generated types describe storage representations. Text `CHECK` constraints do not become unions, and
285
382
  JSON reads remain unknown. Validate values before narrowing to application domain types.
286
383
 
287
- Bigint and `count(*)` reads are decimal strings. Use SQL or BigInt for exact arithmetic and decimal
288
- strings at JSON boundaries; convert to number only when bounded or approximate values are intended.
384
+ For example, assume this additional table and regenerate `Database` from the resulting schema:
385
+
386
+ ```sql
387
+ CREATE TABLE public.sessions (
388
+ id text PRIMARY KEY,
389
+ data jsonb NOT NULL,
390
+ expires_at timestamptz NOT NULL
391
+ );
392
+ ```
393
+
394
+ Use the same application validator before encoding a write and when restoring a domain value:
395
+
396
+ ```ts
397
+ import { sql } from '@kequtech/tenon-orm';
398
+ import type { Database } from './generated-schema.js';
399
+
400
+ function sessionData(value: unknown): { userId: string } {
401
+ if (
402
+ typeof value !== 'object' ||
403
+ value === null ||
404
+ Array.isArray(value) ||
405
+ !('userId' in value) ||
406
+ typeof value.userId !== 'string' ||
407
+ value.userId.length === 0
408
+ ) {
409
+ throw new Error('Expected session data with a nonempty userId');
410
+ }
411
+ return { userId: value.userId };
412
+ }
413
+
414
+ function finiteDate(value: string): Date {
415
+ const date = new Date(value);
416
+ if (!Number.isFinite(date.getTime())) {
417
+ throw new Error('Expected a finite database timestamp');
418
+ }
419
+ return date;
420
+ }
421
+
422
+ const input = {
423
+ id: 'example-session',
424
+ data: JSON.stringify(sessionData({ userId: 'user-42' })),
425
+ expires_at: new Date(Date.now() + 60_000).toISOString(),
426
+ } satisfies Database['sessions']['insert'];
427
+ await db.query(sql`INSERT INTO public.sessions ${sql.insertValues(input)}`);
428
+
429
+ const row = await db.query(
430
+ sql<Database['sessions']['row']>`SELECT id, data, expires_at FROM public.sessions
431
+ WHERE id = ${input.id} AND isfinite(expires_at) AND expires_at > now()`,
432
+ { returns: 'oneOrNone' },
433
+ );
434
+ const session =
435
+ row === undefined
436
+ ? undefined
437
+ : {
438
+ id: row.id,
439
+ data: sessionData(row.data),
440
+ expiresAt: finiteDate(row.expires_at),
441
+ };
442
+ ```
443
+
444
+ The generated JSON read remains `unknown` and the timestamp remains `string`. The projection
445
+ validates both without assertions or edits to generated types. Preserve null separately for nullable
446
+ columns. For an authentication policy requiring finite expiry, `isfinite(expires_at)` excludes both
447
+ infinities before the expiry comparison; `expires_at > now()` alone would accept positive infinity.
448
+ Include such predicates in expiry-sensitive updates/deletes too, and test infinite and expired
449
+ values. This example illustrates representation validation; applications must also retain their
450
+ token verification, revocation, and authorization checks. See
451
+ [JSON and PostgreSQL arrays](mutations.md#json-and-postgresql-arrays) for array write boundaries.
452
+
453
+ Bigint and `count(*)` reads are exact strings. Use `BigInt(value)` for exact arithmetic; convert
454
+ to number only after checking the safe integer range. Command `rowCount` values are numbers; the
455
+ driver rejects counts outside the safe integer range.
289
456
 
290
457
  ### Insert and update inputs
291
458
 
292
459
  ```ts
293
- import type { Rows } from './generated-schema.js';
460
+ import type { Database } from './generated-schema.js';
294
461
 
295
- const input: Rows.Insert['users'] = { email: 'reader@example.test' };
296
- const patch: Pick<Rows.Update['users'], 'active'> = { active: false };
462
+ const input: Database['users']['insert'] = { email: 'reader@example.test' };
463
+ const patch: Pick<Database['users']['update'], 'active'> = { active: false };
297
464
  sql.insertValues(input);
298
465
  sql.assignments(patch);
299
466
  ```
@@ -309,27 +476,79 @@ remain writable. Use `Pick`/`Omit` for application restrictions. Apply the gener
309
476
  annotations, `satisfies`, or helper generic arguments; they do not validate constraints, JSON
310
477
  structure, authorization, or nonempty input.
311
478
 
312
- For unverified write mappings, provide a reviewed application-owned replacement, for example
313
- `Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for interval text input. Selected columns
479
+ For custom representations, provide reviewed application types and encoders. Selected columns
314
480
  can discard required fields; see [the cookbook](mutations.md#input-types-and-runtime-validation).
315
481
 
482
+ ## Choosing a validation tool
483
+
484
+ Run these against the intended development/test schema and role. The database tools use the CLI's
485
+ `DATABASE_URL`; TypeScript itself needs no connection.
486
+
487
+ | Tool | Question answered | Limits |
488
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
489
+ | `tsc --noEmit` | Does the application satisfy its TypeScript contracts? | Does not inspect SQL or the live schema. |
490
+ | `tenon check [file.ts[:line]]` | Does recoverable authored SQL contradict its input/result declarations? | Unsupported composition fails as unchecked; inconclusive properties stay unproven. Does not execute application SQL. |
491
+ | `tenon describe query.sql` | What parameter/result metadata does PostgreSQL report for this finished SQL? | Does not compare source annotations or validate bound values. Does not execute the statement. |
492
+ | `tenon generate --out src/generated-schema.d.ts --check` | Does saved generated output match this schema and driver mapping? | Does not check query annotations or replace migration status. Use the project's actual output path. |
493
+ | Project database tests | Do representative queries produce the required rows, effects, permissions, and rollback behavior? | Cover exercised inputs and paths; retain cases for runtime-dependent composition and application validation. |
494
+
495
+ After a schema or driver mapping change, apply migrations to the test database, regenerate and
496
+ review declarations, then run the project's typecheck, applicable source checks, and database tests.
497
+ In CI, `generate --check` detects stale committed output without replacing it.
498
+
499
+ ## Source-aware checking
500
+
501
+ Use tenon check [file.ts[:line]] [--project tsconfig.json] [--json] to compare authored SQL with its
502
+ TypeScript input/result contracts and pgclear Describe. A file selects all recognized queries;
503
+ a one-based line selects one complete query. The TypeScript compiler loads on the checking path.
504
+
505
+ The programmatic API is checkQueries from @kequtech/tenon-orm/check. It accepts connection, target,
506
+ project, session, timeouts, codecs and optional custom readTypes. It returns ok, project, queries,
507
+ diagnostics and available inspection context. Mismatches and unchecked sites fail; unproven types
508
+ are trusted and reported as notes. No query sites fails. See [query checking](query-checking.md) for
509
+ composition, output contracts, explicit expansion shapes and custom decoder representations.
510
+
316
511
  ## Query inspection
317
512
 
318
- `describeQuery({ connection, query, session?, timeouts? })` accepts a PostgreSQL URI and a finished
319
- SQL string or `{ text }` object. It returns PostgreSQL metadata, conservative read mappings and row
513
+ `describeQuery({ connection, query, session?, timeouts?, codecs? })` accepts a PostgreSQL URI and a finished
514
+ SQL string or `{ text, parameterTypes? }` object. It returns PostgreSQL metadata, conservative read mappings and row
320
515
  declaration, session context, and coded findings. It does not bind values, execute the statement, or
321
516
  verify `sql<Row>`.
322
517
 
323
- Inspection owns a separate session with bounded timeouts. Match runtime settings; temporary tables,
324
- and uncommitted changes are not inherited. Reported mappings assume pg's default parsers; parser
325
- overrides need matching application types. Nullability stays unknown. See
518
+ Inspection owns a separate session with bounded timeouts. Match runtime settings; temporary tables
519
+ and uncommitted changes are not inherited. Read mappings use `codecs.typescript(oid)` from the selected pgclear registry; defaults apply when
520
+ omitted. Custom decoder types may remain unresolved. Nullability stays unknown. See
326
521
  [query inspection](query-inspection.md) for settings, result fields, issue codes, and testing.
327
522
 
523
+ ## Troubleshooting
524
+
525
+ Use error codes and structured fields rather than matching message wording. Driver errors have
526
+ `PGCLEAR_*` codes; PostgreSQL errors use SQLSTATE. Checker diagnostics have their own `kind` and
527
+ `code`. Retain unexpected errors instead of treating every failure as an empty query result.
528
+
529
+ | Symptom | Likely cause and response |
530
+ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
531
+ | `PGCLEAR_ENCODE` | A bind is unsupported, a Date is invalid, or an encoder failed. Use `json(value)` for JSON and `param(values, builtin.int4Array)` for integer arrays. Plain bigint, valid Date, and supported Temporal inputs are accepted; plain objects/arrays and direct undefined still reject. Omit undefined through a mutation helper when omission is intended. JSON encoding still rejects bigint, including nested values. |
532
+ | `CardinalityError` / `TENON_CARDINALITY_MISMATCH` | `one` received zero/multiple rows, or `oneOrNone` received multiple rows. Review the predicate and intended mode. Use deterministic `ORDER BY` plus `LIMIT 1` only when selecting one of several matches is intended. Result modes do not add limits. |
533
+ | `TENON_AFFECTED_ROWS_MISMATCH` | A write exceeded `max` or differed from `exact`. The guarded operation has rolled back. Inspect the predicate and `actual` count; retain the intended write bound. |
534
+ | `TENON_AFFECTED_ROWS_UNSUPPORTED` | The final command lacks a supported INSERT/UPDATE/DELETE/MERGE count. Apply the guard to a supported write; use result modes for returned-row requirements. |
535
+ | Empty assignments | Every selected patch field was missing/undefined. Skip the update or branch to an expression-only statement when that is intended. Null remains a bound value. |
536
+ | SQLSTATE `42P18` / `42P08` during inspection | PostgreSQL cannot establish an unambiguous parameter type. Add the intended SQL cast, or pass `parameterTypes` OID hints to `describeQuery`. Inspection does not bind query values. |
537
+ | Query checker reports `unchecked` | SQL structure or an expansion shape could not be recovered, a limit was exceeded, or inspection failed. Follow the diagnostic; optional generated patch fields and dynamic fragment lists can be valid at runtime but remain unchecked. See [responding to findings](query-checking.md#responding-to-findings). |
538
+ | Missing/stale generated definitions | Check the namespace, schema, driver version, and output path. Regenerate with the same options without `--check`, review the diff, and commit intended output. Freshness checks never rewrite the file. |
539
+
540
+ Inside a transaction, catch a guarded query failure only after its promise rejects; savepoint
541
+ cleanup completes before rejection. An unguarded server SQL error normally leaves the transaction
542
+ failed, so let the outer callback roll it back. Encoding errors happen before dispatch, but their
543
+ inputs still need correction. A timeout or lost commit acknowledgement does not prove rollback;
544
+ see [transactions and errors](#transactions-guarded-writes-and-errors) before adding retries.
545
+
328
546
  ## Command reference
329
547
 
330
548
  Database commands require `DATABASE_URL`. `new` and `--help` work offline. No environment files are
331
549
  loaded. Paths are relative to the working directory. Explicit flags unsupported by a command are
332
- rejected; `--check` is only a generation check, not a migration dry run.
550
+ rejected. The generate --check flag checks file freshness; the check command checks authored SQL.
551
+ Neither is a migration dry run.
333
552
 
334
553
  ```text
335
554
  tenon new <name> [--directory migrations]
@@ -337,6 +556,9 @@ tenon migrate [--directory migrations] [--namespace public]
337
556
  tenon status [--directory migrations] [--namespace public]
338
557
  tenon rollback [--directory migrations] [--namespace public] [--steps 1]
339
558
  tenon generate [--namespace public] [--out src/generated-schema.ts] [--check]
559
+ tenon check [file.ts[:line]] [--project tsconfig.json] [--json]
560
+ [--role name] [--search-path value] [--time-zone value]
561
+ [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
340
562
  tenon describe <query.sql> [--role name] [--search-path value] [--time-zone value]
341
563
  [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
342
564
  tenon baseline capture [--directory migrations] [--namespace public] [--schema name ...]