@kequtech/tenon-orm 0.3.1 → 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 +51 -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 +54 -12
- package/docs/agent-guide.md +56 -28
- package/docs/human-guide.md +50 -19
- package/docs/mutations.md +22 -9
- package/docs/query-checking.md +200 -0
- package/docs/query-inspection.md +40 -13
- package/docs/reference.md +247 -200
- 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,152 +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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
maxConnections: 10,
|
|
32
|
+
idleTimeoutMs: 30_000,
|
|
33
|
+
connectTimeoutMs: 5000,
|
|
34
|
+
acquireTimeoutMs: 5000,
|
|
35
|
+
startupParameters: { search_path: 'app,public' },
|
|
32
36
|
},
|
|
33
37
|
});
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
`
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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).
|
|
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.
|
|
46
45
|
|
|
47
46
|
Reuse an instance for each database configuration. `db.close({ timeoutMs? })` immediately rejects
|
|
48
|
-
new Tenon
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
|
63
58
|
rather than the runtime connection union.
|
|
64
59
|
|
|
65
|
-
|
|
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.
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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.
|
|
70
73
|
|
|
71
|
-
|
|
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.
|
|
74
|
+
## Native driver access
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
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`.
|
|
80
79
|
|
|
81
|
-
|
|
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.
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
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.
|
|
87
86
|
|
|
88
87
|
```ts
|
|
89
|
-
import {
|
|
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
|
-
}
|
|
88
|
+
import { listen } from '@kequtech/pgclear';
|
|
119
89
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
}
|
|
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();
|
|
142
97
|
```
|
|
143
98
|
|
|
144
|
-
|
|
145
|
-
|
|
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.
|
|
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()`.
|
|
156
101
|
|
|
157
102
|
## Queries and result modes
|
|
158
103
|
|
|
@@ -173,11 +118,13 @@ must be a nonempty string.
|
|
|
173
118
|
| `returns` | Output | Allowed returned rows |
|
|
174
119
|
| ------------------ | -------------------------------- | --------------------- |
|
|
175
120
|
| `native` (default) | `QueryResult<Row>` driver object | Any |
|
|
176
|
-
| `many` |
|
|
121
|
+
| `many` | `readonly Row[]` | Any |
|
|
177
122
|
| `oneOrNone` | `Row \| undefined` | Zero or one |
|
|
178
123
|
| `one` | `Row` | Exactly one |
|
|
179
124
|
|
|
180
|
-
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.
|
|
181
128
|
`rowCount` is the command's affected-row count, independent of `RETURNING`; it can be null for
|
|
182
129
|
commands without a count. Result modes never add `LIMIT` or `RETURNING`. A returned-row mismatch
|
|
183
130
|
alone does not undo an autocommitted write; use `affectedRows` for write bounds that roll back.
|
|
@@ -201,19 +148,29 @@ async function queryRows(tx: QueryClient, text: string, values: readonly unknown
|
|
|
201
148
|
Alternatively, request `{ returns: 'many' }` and wrap the returned array in `{ rows }`. Supply the
|
|
202
149
|
callback's `tx` when the adapter's work must commit with application writes. A native pool query
|
|
203
150
|
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
|
|
151
|
+
from the callback, retain commit/rollback tests, and make recording helpers return pgclear native
|
|
205
152
|
results. The adapter's SQL text must be trusted and values must remain separately bound.
|
|
206
153
|
|
|
207
154
|
## SQL parameters and identifiers
|
|
208
155
|
|
|
209
156
|
Ordinary `sql` interpolations bind values. Do not quote them or write manual `$n` placeholders
|
|
210
|
-
inside templates.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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.
|
|
214
169
|
|
|
215
170
|
```ts
|
|
216
|
-
|
|
171
|
+
import { builtin, param } from '@kequtech/tenon-orm';
|
|
172
|
+
|
|
173
|
+
const ids = param([1, 2, 3], builtin.int4Array);
|
|
217
174
|
const query = sql<{ id: number }>`SELECT id FROM public.users
|
|
218
175
|
WHERE id = ANY(${ids}::integer[])`;
|
|
219
176
|
```
|
|
@@ -265,7 +222,7 @@ await db.query(sql`UPDATE public.users SET ${sql.assignments({ active: false })}
|
|
|
265
222
|
|
|
266
223
|
Both accept plain objects, including null-prototype objects. Columns are quoted as individual
|
|
267
224
|
identifiers. Only own data properties are read; accessors are rejected. Fields are captured at
|
|
268
|
-
fragment construction;
|
|
225
|
+
fragment construction; bound values retain pgclear encoding rules and must remain unchanged until settlement.
|
|
269
226
|
|
|
270
227
|
| Input | Insert | Assignment |
|
|
271
228
|
| -------------------------------------------- | ---------------------- | --------------- |
|
|
@@ -304,8 +261,8 @@ await db.transaction(async (tx) => {
|
|
|
304
261
|
|
|
305
262
|
Use `tx` for every operation that must commit together and await the work. The callback commits on
|
|
306
263
|
success, rolls back on an escaping error, and expires before cleanup. Transactions belong to one
|
|
307
|
-
database instance. Nested callbacks and automatic retries are not provided.
|
|
308
|
-
BEGIN/COMMIT/ROLLBACK
|
|
264
|
+
database instance. Nested callbacks and automatic retries are not provided. pgclear owns
|
|
265
|
+
BEGIN/COMMIT/ROLLBACK and recovery.
|
|
309
266
|
|
|
310
267
|
`affectedRows: { exact: n }` or `{ max: n }` accepts a nonnegative safe integer. It checks the final
|
|
311
268
|
INSERT/UPDATE/DELETE/MERGE count independently of `RETURNING`. A guarded root call uses a
|
|
@@ -319,79 +276,112 @@ the outcome unknown; applications own retry and idempotency policy.
|
|
|
319
276
|
|
|
320
277
|
Set isolation before data queries with `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`, and use
|
|
321
278
|
`SET LOCAL statement_timeout = '5s'` to bound statements in that transaction. Manual savepoints are
|
|
322
|
-
supported
|
|
279
|
+
supported on the owned callback session; pgclear chooses guarded-operation savepoints.
|
|
323
280
|
|
|
324
|
-
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.
|
|
325
283
|
`CardinalityError` has code `TENON_CARDINALITY_MISMATCH`, `expected` (`one` or `oneOrNone`),
|
|
326
284
|
`actual` (returned row count), and optional `label`. It does not change transaction or autocommit
|
|
327
285
|
behavior. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
|
|
328
|
-
`TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED
|
|
329
|
-
produce `AggregateError`.
|
|
286
|
+
`TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`; its `actual` is `number | null`.
|
|
287
|
+
Work/cleanup failures can produce `AggregateError`.
|
|
330
288
|
|
|
331
289
|
Optional `onQuery` observes completed calls with `label`, `durationMs`, `outcome`, and available
|
|
332
|
-
`command`, `rowCount
|
|
333
|
-
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
|
|
334
292
|
query outcome. Query success inside a callback does not establish outer commit.
|
|
335
293
|
|
|
336
294
|
## Generated types and checking
|
|
337
295
|
|
|
338
|
-
`generateDefinitions({ connection, namespace?, onWarning? })`
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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.
|
|
342
301
|
|
|
343
302
|
```sh
|
|
344
303
|
npx tenon generate --out src/generated-schema.d.ts
|
|
345
304
|
npx tenon generate --out src/generated-schema.d.ts --check
|
|
346
305
|
```
|
|
347
306
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
|
366
|
-
|
|
|
367
|
-
|
|
|
368
|
-
|
|
|
369
|
-
|
|
|
370
|
-
|
|
|
371
|
-
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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.
|
|
388
378
|
|
|
389
379
|
### Persistence and domain values
|
|
390
380
|
|
|
391
381
|
Generated types describe storage representations. Text `CHECK` constraints do not become unions, and
|
|
392
382
|
JSON reads remain unknown. Validate values before narrowing to application domain types.
|
|
393
383
|
|
|
394
|
-
For example, assume this additional table and regenerate `
|
|
384
|
+
For example, assume this additional table and regenerate `Database` from the resulting schema:
|
|
395
385
|
|
|
396
386
|
```sql
|
|
397
387
|
CREATE TABLE public.sessions (
|
|
@@ -405,7 +395,7 @@ Use the same application validator before encoding a write and when restoring a
|
|
|
405
395
|
|
|
406
396
|
```ts
|
|
407
397
|
import { sql } from '@kequtech/tenon-orm';
|
|
408
|
-
import type {
|
|
398
|
+
import type { Database } from './generated-schema.js';
|
|
409
399
|
|
|
410
400
|
function sessionData(value: unknown): { userId: string } {
|
|
411
401
|
if (
|
|
@@ -421,22 +411,23 @@ function sessionData(value: unknown): { userId: string } {
|
|
|
421
411
|
return { userId: value.userId };
|
|
422
412
|
}
|
|
423
413
|
|
|
424
|
-
function finiteDate(value:
|
|
425
|
-
|
|
414
|
+
function finiteDate(value: string): Date {
|
|
415
|
+
const date = new Date(value);
|
|
416
|
+
if (!Number.isFinite(date.getTime())) {
|
|
426
417
|
throw new Error('Expected a finite database timestamp');
|
|
427
418
|
}
|
|
428
|
-
return
|
|
419
|
+
return date;
|
|
429
420
|
}
|
|
430
421
|
|
|
431
422
|
const input = {
|
|
432
423
|
id: 'example-session',
|
|
433
424
|
data: JSON.stringify(sessionData({ userId: 'user-42' })),
|
|
434
|
-
expires_at: new Date(Date.now() + 60_000),
|
|
435
|
-
} satisfies
|
|
425
|
+
expires_at: new Date(Date.now() + 60_000).toISOString(),
|
|
426
|
+
} satisfies Database['sessions']['insert'];
|
|
436
427
|
await db.query(sql`INSERT INTO public.sessions ${sql.insertValues(input)}`);
|
|
437
428
|
|
|
438
429
|
const row = await db.query(
|
|
439
|
-
sql<
|
|
430
|
+
sql<Database['sessions']['row']>`SELECT id, data, expires_at FROM public.sessions
|
|
440
431
|
WHERE id = ${input.id} AND isfinite(expires_at) AND expires_at > now()`,
|
|
441
432
|
{ returns: 'oneOrNone' },
|
|
442
433
|
);
|
|
@@ -450,7 +441,7 @@ const session =
|
|
|
450
441
|
};
|
|
451
442
|
```
|
|
452
443
|
|
|
453
|
-
The generated JSON read remains `unknown` and the timestamp remains `
|
|
444
|
+
The generated JSON read remains `unknown` and the timestamp remains `string`. The projection
|
|
454
445
|
validates both without assertions or edits to generated types. Preserve null separately for nullable
|
|
455
446
|
columns. For an authentication policy requiring finite expiry, `isfinite(expires_at)` excludes both
|
|
456
447
|
infinities before the expiry comparison; `expires_at > now()` alone would accept positive infinity.
|
|
@@ -459,16 +450,17 @@ values. This example illustrates representation validation; applications must al
|
|
|
459
450
|
token verification, revocation, and authorization checks. See
|
|
460
451
|
[JSON and PostgreSQL arrays](mutations.md#json-and-postgresql-arrays) for array write boundaries.
|
|
461
452
|
|
|
462
|
-
Bigint and `count(*)` reads are
|
|
463
|
-
|
|
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.
|
|
464
456
|
|
|
465
457
|
### Insert and update inputs
|
|
466
458
|
|
|
467
459
|
```ts
|
|
468
|
-
import type {
|
|
460
|
+
import type { Database } from './generated-schema.js';
|
|
469
461
|
|
|
470
|
-
const input:
|
|
471
|
-
const patch: Pick<
|
|
462
|
+
const input: Database['users']['insert'] = { email: 'reader@example.test' };
|
|
463
|
+
const patch: Pick<Database['users']['update'], 'active'> = { active: false };
|
|
472
464
|
sql.insertValues(input);
|
|
473
465
|
sql.assignments(patch);
|
|
474
466
|
```
|
|
@@ -484,27 +476,79 @@ remain writable. Use `Pick`/`Omit` for application restrictions. Apply the gener
|
|
|
484
476
|
annotations, `satisfies`, or helper generic arguments; they do not validate constraints, JSON
|
|
485
477
|
structure, authorization, or nonempty input.
|
|
486
478
|
|
|
487
|
-
For
|
|
488
|
-
`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
|
|
489
480
|
can discard required fields; see [the cookbook](mutations.md#input-types-and-runtime-validation).
|
|
490
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
|
+
|
|
491
511
|
## Query inspection
|
|
492
512
|
|
|
493
|
-
`describeQuery({ connection, query, session?, timeouts? })` accepts a PostgreSQL URI and a finished
|
|
494
|
-
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
|
|
495
515
|
declaration, session context, and coded findings. It does not bind values, execute the statement, or
|
|
496
516
|
verify `sql<Row>`.
|
|
497
517
|
|
|
498
|
-
Inspection owns a separate session with bounded timeouts. Match runtime settings; temporary tables
|
|
499
|
-
and uncommitted changes are not inherited.
|
|
500
|
-
|
|
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
|
|
501
521
|
[query inspection](query-inspection.md) for settings, result fields, issue codes, and testing.
|
|
502
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
|
+
|
|
503
546
|
## Command reference
|
|
504
547
|
|
|
505
548
|
Database commands require `DATABASE_URL`. `new` and `--help` work offline. No environment files are
|
|
506
549
|
loaded. Paths are relative to the working directory. Explicit flags unsupported by a command are
|
|
507
|
-
rejected
|
|
550
|
+
rejected. The generate --check flag checks file freshness; the check command checks authored SQL.
|
|
551
|
+
Neither is a migration dry run.
|
|
508
552
|
|
|
509
553
|
```text
|
|
510
554
|
tenon new <name> [--directory migrations]
|
|
@@ -512,6 +556,9 @@ tenon migrate [--directory migrations] [--namespace public]
|
|
|
512
556
|
tenon status [--directory migrations] [--namespace public]
|
|
513
557
|
tenon rollback [--directory migrations] [--namespace public] [--steps 1]
|
|
514
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]
|
|
515
562
|
tenon describe <query.sql> [--role name] [--search-path value] [--time-zone value]
|
|
516
563
|
[--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
|
|
517
564
|
tenon baseline capture [--directory migrations] [--namespace public] [--schema name ...]
|