@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.
- package/CHANGELOG.md +54 -0
- package/README.md +27 -5
- package/dist/affected-rows.js +0 -1
- package/dist/baseline.js +3 -5
- package/dist/check/contracts.d.ts +6 -0
- package/dist/check/contracts.js +206 -0
- package/dist/check/project.d.ts +18 -0
- package/dist/check/project.js +224 -0
- package/dist/check/source.d.ts +51 -0
- package/dist/check/source.js +629 -0
- package/dist/check/types.d.ts +36 -0
- package/dist/check/types.js +1 -0
- package/dist/check.d.ts +4 -0
- package/dist/check.js +147 -0
- package/dist/cli.js +38 -9
- package/dist/connection.d.ts +5 -12
- package/dist/connection.js +12 -121
- package/dist/database.js +5 -8
- package/dist/describe.js +10 -3
- package/dist/generate.d.ts +2 -3
- package/dist/generate.js +26 -24
- package/dist/generation/catalog.d.ts +5 -6
- package/dist/generation/catalog.js +7 -9
- package/dist/generation/render.d.ts +3 -0
- package/dist/generation/render.js +60 -0
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspection/catalog.d.ts +3 -6
- package/dist/inspection/catalog.js +9 -18
- package/dist/inspection/cli.d.ts +12 -0
- package/dist/inspection/cli.js +9 -4
- package/dist/inspection/connection.d.ts +2 -3
- package/dist/inspection/connection.js +20 -22
- package/dist/inspection/options.js +0 -6
- package/dist/inspection/result.d.ts +3 -2
- package/dist/inspection/result.js +13 -26
- package/dist/inspection/types.d.ts +6 -4
- package/dist/migrations/connection.d.ts +3 -3
- package/dist/migrations/connection.js +14 -21
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -9
- package/dist/query-options.d.ts +1 -1
- package/dist/query.js +1 -1
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +21 -77
- package/dist/types.d.ts +1 -1
- package/docs/adoption-guide.md +64 -12
- package/docs/agent-guide.md +57 -27
- package/docs/human-guide.md +53 -18
- package/docs/mutations.md +22 -9
- package/docs/query-checking.md +200 -0
- package/docs/query-inspection.md +40 -13
- package/docs/reference.md +330 -108
- package/package.json +11 -7
- package/dist/driver/types.d.ts +0 -10
- package/dist/driver/types.js +0 -49
- package/dist/generation/table-types.d.ts +0 -2
- package/dist/generation/table-types.js +0 -96
- package/dist/inspection/protocol.d.ts +0 -7
- 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
|
|
11
|
-
|
|
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
|
-
|
|
18
|
-
and `
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
35
|
-
`
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 `
|
|
63
|
-
modes, affected-row guards, SQL checks, and `onQuery`. They
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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` |
|
|
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
|
|
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.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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;
|
|
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.
|
|
208
|
-
BEGIN/COMMIT/ROLLBACK
|
|
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
|
|
279
|
+
supported on the owned callback session; pgclear chooses guarded-operation savepoints.
|
|
223
280
|
|
|
224
|
-
SQL errors retain native driver details.
|
|
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
|
|
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
|
|
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? })`
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
|
260
|
-
|
|
|
261
|
-
|
|
|
262
|
-
|
|
|
263
|
-
|
|
|
264
|
-
|
|
|
265
|
-
|
|
|
266
|
-
|
|
|
267
|
-
|
|
|
268
|
-
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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 {
|
|
460
|
+
import type { Database } from './generated-schema.js';
|
|
294
461
|
|
|
295
|
-
const input:
|
|
296
|
-
const patch: Pick<
|
|
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
|
|
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.
|
|
325
|
-
|
|
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
|
|
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 ...]
|