@kequtech/tenon-orm 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +43 -42
  2. package/README.md +4 -4
  3. package/dist/affected-rows.js +6 -6
  4. package/dist/baseline.js +4 -4
  5. package/dist/client.js +1 -1
  6. package/dist/connection.d.ts +15 -22
  7. package/dist/connection.js +122 -25
  8. package/dist/database.js +10 -17
  9. package/dist/driver/types.d.ts +1 -1
  10. package/dist/driver/types.js +11 -11
  11. package/dist/generate.js +7 -7
  12. package/dist/generation/catalog.d.ts +2 -2
  13. package/dist/generation/catalog.js +4 -4
  14. package/dist/generation/schema-summary.d.ts +3 -3
  15. package/dist/generation/schema-summary.js +11 -11
  16. package/dist/generation/table-types.js +1 -1
  17. package/dist/index.d.ts +1 -2
  18. package/dist/inspection/catalog.d.ts +4 -3
  19. package/dist/inspection/catalog.js +13 -10
  20. package/dist/inspection/connection.d.ts +2 -2
  21. package/dist/inspection/connection.js +22 -22
  22. package/dist/inspection/protocol.d.ts +7 -0
  23. package/dist/inspection/protocol.js +45 -0
  24. package/dist/inspection/result.d.ts +1 -1
  25. package/dist/inspection/result.js +6 -6
  26. package/dist/inspection/types.d.ts +2 -2
  27. package/dist/migrations/connection.d.ts +3 -2
  28. package/dist/migrations/connection.js +18 -20
  29. package/dist/migrations/history.d.ts +4 -4
  30. package/dist/migrations/history.js +5 -5
  31. package/dist/migrations.js +7 -7
  32. package/dist/query-options.js +4 -4
  33. package/dist/query.js +2 -2
  34. package/dist/transaction.d.ts +2 -2
  35. package/dist/transaction.js +19 -14
  36. package/dist/types.d.ts +2 -2
  37. package/docs/adoption-guide.md +30 -27
  38. package/docs/agent-guide.md +53 -48
  39. package/docs/human-guide.md +21 -20
  40. package/docs/mutations.md +13 -10
  41. package/docs/query-inspection.md +12 -14
  42. package/docs/reference.md +130 -132
  43. package/package.json +3 -2
  44. package/dist/driver/array.d.ts +0 -5
  45. package/dist/driver/array.js +0 -70
  46. package/dist/driver/lifecycle.d.ts +0 -15
  47. package/dist/driver/lifecycle.js +0 -57
  48. package/dist/driver/listener.d.ts +0 -3
  49. package/dist/driver/listener.js +0 -63
  50. package/dist/driver/session.d.ts +0 -8
  51. package/dist/driver/session.js +0 -92
package/docs/reference.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # Tenon reference
2
2
 
3
- Exact API contracts and limitations. The [human guide](human-guide.md) introduces the workflow;
4
- the [agent guide](agent-guide.md) covers routine implementation. Examples below use the
5
- `public.users` table from the human guide and an existing `db` instance, unless stated otherwise.
3
+ Exact API contracts and limitations. The [human guide](human-guide.md) introduces the workflow; the
4
+ [agent guide](agent-guide.md) covers routine implementation. Examples below use the `public.users`
5
+ table from the human guide and an existing `db` instance, unless stated otherwise.
6
6
 
7
7
  ## Installation and connection
8
8
 
9
9
  Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and Node
10
- development typings. Tenon includes `postgres@3.4.9`; no driver peer or special compiler plugin is
11
- required. A usual Node TypeScript setup uses `module: "NodeNext"`.
10
+ development typings. Tenon includes `pg@8.23.1` and its TypeScript declarations; no driver peer or
11
+ special compiler plugin is required. A usual Node TypeScript setup uses `module: "NodeNext"`.
12
12
 
13
13
  ```sh
14
14
  npm install @kequtech/tenon-orm
@@ -18,71 +18,66 @@ npm install @kequtech/tenon-orm
18
18
  and `close`. The required `connection` accepts:
19
19
 
20
20
  - A PostgreSQL URI string.
21
- - `{ url: string, options?: ConnectionOptions }` to combine a URI with driver settings.
22
- - A Postgres.js options object; an explicit `{}` uses the driver's environment defaults.
23
- - An existing Postgres.js client, borrowed by identity.
21
+ - A pg `PoolConfig` object; an explicit `{}` uses the driver's environment defaults.
22
+ - An existing `pg.Pool`, borrowed by identity.
24
23
 
25
24
  ```ts
26
25
  const db = createTenon({
27
26
  connection: {
28
- url: connection,
29
- options: { connect_timeout: 5, idle_timeout: 10, connection: { search_path: 'app,public' } },
27
+ connectionString: connection,
28
+ connectionTimeoutMillis: 5000,
29
+ options: '-c search_path=app,public',
30
30
  },
31
31
  });
32
32
  ```
33
33
 
34
- Tenon forwards `{ url, options }` to `postgres(url, options)`. Explicit driver options override
35
- corresponding URI pool/transport options. URI startup parameters such as `search_path` and
36
- `application_name` override `options.connection`. Timeouts above are seconds; preserve the
37
- deployment's credentials and TLS requirements. `ConnectionUri` and `ConnectionOptions` are
38
- exported. The latter includes `onnotify(channel, payload)` for native notification handling;
39
- configure callbacks at construction.
40
-
41
- Reuse an instance for each database configuration. `db.close({ timeoutMs? })` rejects new Tenon
42
- work immediately and gives owned connections a 5,000 ms grace period by default before requesting
43
- driver termination. Use `0` for immediate termination. The timeout must be an integer from 0 to
44
- 2,147,483,647 milliseconds; `CloseOptions` is exported. Finish outstanding work before closing
45
- when possible. The timeout bounds the shutdown wait and rejects pending client queries; it is not
46
- a PostgreSQL cancellation request. Server work can complete afterward, including an autocommitted
47
- write, and JavaScript inside a transaction callback is not canceled.
48
-
49
- The first valid close call chooses the timeout. Concurrent and later calls return the same promise,
50
- including the same shutdown failure; later options do not change it. Invalid initial options reject
51
- without closing the instance. For a borrowed client, close only disables the Tenon wrapper and
52
- leaves the driver pool and its work to the caller. Tenon does not load environment files.
53
-
54
- Qualify SQL names or configure runtime `search_path`. CLI `--namespace` selects tooling scope;
55
- it does not configure application queries. Baseline capture and Describe accept connection
56
- strings rather than the runtime connection union.
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).
41
+
42
+ 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
58
+ rather than the runtime connection union.
57
59
 
58
60
  ## Native driver access
59
61
 
60
- `db.native` is the actual pool-level Postgres.js client, readonly in TypeScript. Use it for LISTEN,
61
- cursors, COPY, and other [native APIs](https://github.com/porsager/postgres/tree/v3.4.9#usage).
62
- Native calls bypass Tenon's result modes, affected-row guards, SQL checks, and `onQuery`. They use
63
- native preparation defaults and do not join a Tenon transaction; callback clients expose only
64
- `tx.query`.
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`.
65
65
 
66
- Native access alone does not install Tenon's array correction. The first Tenon query or transaction
67
- installs it on the shared client; native statements prepared earlier may retain their old parsers.
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.
68
72
 
69
- Native `.listen(channel, onPayload, onReady?)` uses a dedicated physical connection. `onReady`
70
- runs after subscription and resubscription. Missed notifications are not replayed; the application
71
- owns catch-up and callback errors. That dedicated client installs its own close handler, so a
72
- constructor `onclose` does not report its outages. Explicit LISTEN on a dedicated reserved native
73
- connection can use constructor callbacks when disconnect detection is required. Subscription
74
- cancellation follows the pinned driver; known reconnect limitations are recorded in
75
- [maintainer considerations](https://github.com/kequtech/tenon-orm/blob/main/CONSIDERATIONS.md#native-driver-access-and-listen-lifecycle).
76
-
77
- Close owned clients through `db.close()` to prevent queued LISTEN reconnects from reopening them.
78
- Direct `db.native.end()` bypasses that protection. Borrowed clients and their listeners remain
79
- usable after wrapper close until their owner ends them.
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.
80
75
 
81
76
  ## Queries and result modes
82
77
 
83
78
  `sql<Row>` creates a detached `SqlQuery<Row>` with SQL `text` and ordered `values`; it does not
84
- execute. `Row` declares one selected row. Without an annotation, fields are `unknown`. An
85
- annotation does not verify the SQL or validate returned data.
79
+ execute. `Row` declares one selected row. Without an annotation, fields are `unknown`. An annotation
80
+ does not verify the SQL or validate returned data.
86
81
 
87
82
  ```ts
88
83
  const user = await db.query(
@@ -94,17 +89,17 @@ const user = await db.query(
94
89
  `query` options are `{ returns?, affectedRows?, label? }`; unknown keys fail. A supplied `label`
95
90
  must be a nonempty string.
96
91
 
97
- | `returns` | Output | Allowed returned rows |
98
- | ------------------ | ------------------------------- | --------------------- |
99
- | `native` (default) | `QueryResult<Row>` driver array | Any |
100
- | `many` | Plain `Row[]` | Any |
101
- | `oneOrNone` | `Row \| undefined` | Zero or one |
102
- | `one` | `Row` | Exactly one |
92
+ | `returns` | Output | Allowed returned rows |
93
+ | ------------------ | -------------------------------- | --------------------- |
94
+ | `native` (default) | `QueryResult<Row>` driver object | Any |
95
+ | `many` | Plain `Row[]` | Any |
96
+ | `oneOrNone` | `Row \| undefined` | Zero or one |
97
+ | `one` | `Row` | Exactly one |
103
98
 
104
- Native arrays carry `command`, `count`, `columns`, `statement`, and `state` metadata. `count` is
105
- the command's affected-row count, independent of `RETURNING`; it can be null for commands without
106
- a count. Result modes never add `LIMIT` or `RETURNING`. A returned-row mismatch alone does not
107
- undo an autocommitted write; use `affectedRows` for write bounds that roll back on failure.
99
+ Native results expose `rows`, `rowCount`, `command`, and `fields`, following pg's `QueryResult`.
100
+ `rowCount` is the command's affected-row count, independent of `RETURNING`; it can be null for
101
+ commands without a count. Result modes never add `LIMIT` or `RETURNING`. A returned-row mismatch
102
+ alone does not undo an autocommitted write; use `affectedRows` for write bounds that roll back.
108
103
 
109
104
  Functions accepting `QueryClient` can use either `Database` or `Transaction`. Public types also
110
105
  include `QueryOptions`, `QueryReturnMode`, and `QueryOutput<Row, Mode>`.
@@ -112,9 +107,10 @@ include `QueryOptions`, `QueryReturnMode`, and `QueryOutput<Row, Mode>`.
112
107
  ## SQL parameters and identifiers
113
108
 
114
109
  Ordinary `sql` interpolations bind values. Do not quote them or write manual `$n` placeholders
115
- inside templates. `undefined` bound parameters fail; `null` means SQL null. Pass JSON objects and
116
- arrays directly. SQL context distinguishes PostgreSQL arrays from JSON; cast standalone parameters
117
- when that context is absent.
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.
118
114
 
119
115
  ```ts
120
116
  const ids = [1, 2, 3];
@@ -122,26 +118,27 @@ const query = sql<{ id: number }>`SELECT id FROM public.users
122
118
  WHERE id = ANY(${ids}::integer[])`;
123
119
  ```
124
120
 
125
- `identifier(schema, table)` quotes components separately. Names must contain 1–63 UTF-8 bytes and
126
- no NUL. Choose application-approved names; quoting does not authorize access. SQL syntax such as
127
- `DESC` belongs in fixed query alternatives or fragments, not a string parameter.
121
+ `identifier(schema, table)` quotes components separately. Names must contain 1–63 UTF-8 bytes and no
122
+ NUL. Choose application-approved names; quoting does not authorize access. SQL syntax such as `DESC`
123
+ belongs in fixed query alternatives or fragments, not a string parameter.
128
124
 
129
125
  `unsafeSql<Row>(text, values)` accepts trusted external SQL and an explicit values array, including
130
126
  `[]`. Never concatenate request input into its text. Full `sql` and `unsafeSql` queries cannot be
131
127
  nested; use fragments for partial SQL.
132
128
 
133
- Each Tenon query executes one statement through the extended protocol. Application execution
134
- explicitly disables prepared-statement caching, including on supplied clients with `prepare: true`.
135
- Values remain bound separately. Migrations deliberately support multi-statement scripts.
129
+ Each Tenon query executes one statement through the extended protocol. Application execution uses
130
+ unnamed queries without a `name` or prepared-statement cache. Values remain bound separately.
131
+ Migrations deliberately support multi-statement scripts.
136
132
 
137
133
  ## SQL fragments and joins
138
134
 
139
135
  `sql.fragment` creates immutable partial SQL with bound values. It has no row type, cannot execute
140
136
  alone, and rejects blank SQL. Insert it into a complete `sql<Row>` query.
141
137
 
142
- `sql.join(fragments, separator)` requires a nonempty list. Separators are `'AND'`, `'OR'`, and `','`.
143
- It does not add parentheses or change SQL precedence. Each fragment use receives correctly ordered
144
- parameters, including repeated and nested uses; literal `$1` text in strings/comments is unchanged.
138
+ `sql.join(fragments, separator)` requires a nonempty list. Separators are `'AND'`, `'OR'`, and
139
+ `','`. It does not add parentheses or change SQL precedence. Each fragment use receives correctly
140
+ ordered parameters, including repeated and nested uses; literal `$1` text in strings/comments is
141
+ unchanged.
145
142
 
146
143
  ```ts
147
144
  const email: string | undefined = 'reader@example.test';
@@ -178,10 +175,10 @@ fragment construction; nested objects retain ordinary driver serialization behav
178
175
  | SQL fragment, identifier, or query as a cell | Rejected | Rejected |
179
176
 
180
177
  Inserts accept one object or an array. Without `columns`, they infer the union of enumerable keys
181
- with defined values in first-seen order. Heterogeneous rows stay aligned. A single default-only
182
- row emits `DEFAULT VALUES`. Empty batches fail; multiple default-only rows need explicit columns.
183
- Assignments omit undefined values and fail when no assignments remain, even if surrounding SQL
184
- has other assignments.
178
+ with defined values in first-seen order. Heterogeneous rows stay aligned. A single default-only row
179
+ emits `DEFAULT VALUES`. Empty batches fail; multiple default-only rows need explicit columns.
180
+ Assignments omit undefined values and fail when no assignments remain, even if surrounding SQL has
181
+ other assignments.
185
182
 
186
183
  A supplied column list must be nonempty, unique, and valid. It selects and orders fields, ignoring
187
184
  unlisted keys. Missing insert cells use defaults; missing update fields are omitted. TypeScript
@@ -190,7 +187,7 @@ Helpers have no schema metadata; PostgreSQL enforces identity, generated-column,
190
187
  rules. Use [generated inputs](#insert-and-update-inputs) and application validation.
191
188
 
192
189
  Write arithmetic, `column = DEFAULT`, and `excluded.column` in the surrounding SQL. There is no
193
- expression/default sentinel. A complete query accepts at most 65,533 bound parameters, including
190
+ expression/default sentinel. A complete query accepts at most 65,535 bound parameters, including
194
191
  surrounding interpolations; Tenon does not split batches. See the [mutation cookbook](mutations.md).
195
192
 
196
193
  ## Transactions, guarded writes, and errors
@@ -221,8 +218,8 @@ Sequence advances and external effects are not reversible. A lost commit acknowl
221
218
  the outcome unknown; applications own retry and idempotency policy.
222
219
 
223
220
  Set isolation before data queries with `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`, and use
224
- `SET LOCAL statement_timeout = '5s'` to bound statements in that transaction. Manual savepoints are supported;
225
- reserve names beginning with `tenon_affected_rows_` for Tenon.
221
+ `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.
226
223
 
227
224
  SQL errors retain native driver details. Invalid inputs throw ordinary `Error` objects.
228
225
  `CardinalityError` has code `TENON_CARDINALITY_MISMATCH`, `expected` (`one` or `oneOrNone`),
@@ -233,59 +230,59 @@ produce `AggregateError`.
233
230
 
234
231
  Optional `onQuery` observes completed calls with `label`, `durationMs`, `outcome`, and available
235
232
  `command`, `rowCount`, and `errorCode`. It receives no SQL or values. Validation failures before
236
- execution are not observed. Observers are not awaited; failures emit warnings without changing
237
- the query outcome. Query success inside a callback does not establish outer commit.
233
+ execution are not observed. Observers are not awaited; failures emit warnings without changing the
234
+ query outcome. Query success inside a callback does not establish outer commit.
238
235
 
239
236
  ## Generated types and checking
240
237
 
241
- `generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript declarations
242
- from a consistent read-only snapshot; it does not need migration files or history. The CLI writes
243
- them atomically. Default namespace is `public`; default output is `src/generated-schema.ts`.
244
- Both `.ts` and `.d.ts` outputs are supported. Commit exact output and exclude it from formatting.
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.
245
242
 
246
243
  ```sh
247
244
  npx tenon generate --out src/generated-schema.d.ts
248
245
  npx tenon generate --out src/generated-schema.d.ts --check
249
246
  ```
250
247
 
251
- `--check` never rewrites the file. Missing/stale output fails with a diagnostic and a short diff
252
- for changes. It compares the generated representation, not pending migrations or every database
253
- object. Output includes relation/constraint/index summaries and referenced domains, not function
254
- bodies, grants, or a complete schema fingerprint.
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.
255
252
 
256
253
  Generation covers ordinary tables and partitioned roots. Views, foreign tables, and attached
257
254
  partitions need query-specific types. A missing namespace fails; an existing empty schema is valid.
258
255
  Exact table names are available through `Rows['users']`; valid nonreserved identifiers also receive
259
256
  named aliases. Use `Pick` for projections; a join or aggregate needs its actual selected shape.
260
257
 
261
- | PostgreSQL type | Read representation | Generated write input |
262
- | ---------------------------------------------- | -------------------- | ------------------------------------------------------------ |
263
- | smallint, integer, oid, real, double precision | `number` | `number` |
264
- | bigint | `string` | `string \| bigint` |
265
- | numeric | `string` | `string \| number` |
266
- | boolean | `boolean` | `boolean` |
267
- | text, name, varchar, char, uuid, time, timetz | `string` | `string` |
268
- | date, timestamp, timestamptz | `Date` | `Date \| string` |
269
- | enum | String literal union | Same union; `never` for an empty enum |
270
- | json, jsonb | `unknown` | `Exclude<JSONValue, null>`; `JSONValue` is exported by Tenon |
271
- | bytea | `Buffer` | `Buffer` |
272
-
273
- Nullable columns add null; `unknown` already includes it. Domains retain verified base mappings
274
- and the column's read nullability. Verified arrays use `PgArray<T> = (T | null | PgArray<T>)[]` for
275
- null elements and multiple dimensions. Numeric arrays contain strings; enum arrays and arrays of
276
- domains are unverified. Unmapped types warn and use `unknown` reads / `never` non-null writes.
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.
277
275
  Custom parsers need matching application types.
278
276
 
279
- Date-only parsing uses UTC; timestamps without time zone use Node's local timezone. Infinite
280
- temporal reads become invalid dates: check `Number.isFinite(value.getTime())` before date methods.
281
- Bound temporal values require finite dates; use SQL literals for PostgreSQL infinity.
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.
282
281
 
283
282
  ### Persistence and domain values
284
283
 
285
- Generated types describe storage representations. Text `CHECK` constraints do not become unions,
286
- and JSON reads remain unknown; validate before narrowing to domain types. Convert third-party
287
- payloads into validated native JSON-compatible objects rather than asserting compatibility or
288
- pre-serializing them. A bound null means SQL null; use `'null'::jsonb` for JSON null.
284
+ Generated types describe storage representations. Text `CHECK` constraints do not become unions, and
285
+ JSON reads remain unknown. Validate values before narrowing to application domain types.
289
286
 
290
287
  Bigint and `count(*)` reads are decimal strings. Use SQL or BigInt for exact arithmetic and decimal
291
288
  strings at JSON boundaries; convert to number only when bounded or approximate values are intended.
@@ -303,14 +300,14 @@ sql.assignments(patch);
303
300
 
304
301
  Insert fields are required when nonnullable, writable, and without a default. Other writable insert
305
302
  fields and all writable update fields are optional and accept explicit undefined, including with
306
- `exactOptionalPropertyTypes`. Explicit null requires both column and domain-chain permission.
307
- Domain defaults count; column defaults take precedence.
303
+ `exactOptionalPropertyTypes`. Explicit null requires both column and domain-chain permission. Domain
304
+ defaults count; column defaults take precedence.
308
305
 
309
306
  Computed and `GENERATED ALWAYS` identity columns use optional `never` fields. Serial and
310
- `GENERATED BY DEFAULT` identities are optional on insert and writable on update; other primary
311
- keys remain writable. Use `Pick`/`Omit` for application restrictions. Apply the generated types
312
- through annotations, `satisfies`, or helper generic arguments; they do not validate constraints,
313
- JSON structure, authorization, or nonempty input.
307
+ `GENERATED BY DEFAULT` identities are optional on insert and writable on update; other primary keys
308
+ remain writable. Use `Pick`/`Omit` for application restrictions. Apply the generated types through
309
+ annotations, `satisfies`, or helper generic arguments; they do not validate constraints, JSON
310
+ structure, authorization, or nonempty input.
314
311
 
315
312
  For unverified write mappings, provide a reviewed application-owned replacement, for example
316
313
  `Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for interval text input. Selected columns
@@ -319,19 +316,20 @@ can discard required fields; see [the cookbook](mutations.md#input-types-and-run
319
316
  ## Query inspection
320
317
 
321
318
  `describeQuery({ connection, query, session?, timeouts? })` accepts a PostgreSQL URI and a finished
322
- SQL string or `{ text }` object. It returns PostgreSQL metadata, conservative read mappings and
323
- row declaration, session context, and coded findings. It does not bind values, execute the
324
- statement, or verify `sql<Row>`.
319
+ SQL string or `{ text }` object. It returns PostgreSQL metadata, conservative read mappings and row
320
+ declaration, session context, and coded findings. It does not bind values, execute the statement, or
321
+ verify `sql<Row>`.
325
322
 
326
323
  Inspection owns a separate session with bounded timeouts. Match runtime settings; temporary tables,
327
- uncommitted changes, and custom parsers are not inherited. Nullability stays unknown. See
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
328
326
  [query inspection](query-inspection.md) for settings, result fields, issue codes, and testing.
329
327
 
330
328
  ## Command reference
331
329
 
332
- Database commands require `DATABASE_URL`. `new` and `--help` work offline. No environment files
333
- are loaded. Paths are relative to the working directory. Explicit flags unsupported by a command
334
- are rejected; `--check` is only a generation check, not a migration dry run.
330
+ Database commands require `DATABASE_URL`. `new` and `--help` work offline. No environment files are
331
+ 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.
335
333
 
336
334
  ```text
337
335
  tenon new <name> [--directory migrations]
@@ -356,10 +354,10 @@ file has down SQL before executing any; `steps` is a positive safe integer. Use
356
354
  function bodies and explicit `E'...'` backslash escapes. Nontransactional DDL such as
357
355
  `CREATE INDEX CONCURRENTLY` requires a separate procedure.
358
356
 
359
- Migration/status/rollback/adoption use session advisory locks per database and namespace;
360
- generation uses the same lock in shared mode. Use direct or session-pooled connections and server
361
- timeouts to bound waits. Baseline capture uses a local creation lock and native `pg_dump`;
362
- it does not acquire the runner's database advisory lock. See [adoption](adoption-guide.md).
357
+ Migration/status/rollback/adoption use session advisory locks per database and namespace; generation
358
+ uses the same lock in shared mode. Use direct or session-pooled connections and server timeouts to
359
+ bound waits. Baseline capture uses a local creation lock and native `pg_dump`; it does not acquire
360
+ the runner's database advisory lock. See [adoption](adoption-guide.md).
363
361
 
364
362
  Programmatic `migrate`, `migrationStatus`, and `rollback` accept
365
363
  `{ connection, directory?, namespace? }`; rollback also accepts `steps`. Migration/rollback return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kequtech/tenon-orm",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "A PostgreSQL ORM designed for coding agents, with explicit SQL, typed queries, and migrations",
@@ -54,7 +54,8 @@
54
54
  "benchmark": "npm run build && node scripts/benchmark.mjs"
55
55
  },
56
56
  "dependencies": {
57
- "postgres": "3.4.9"
57
+ "pg": "8.23.1",
58
+ "@types/pg": "8.23.1"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@types/node": "^24.19.2",
@@ -1,5 +0,0 @@
1
- import type postgres from 'postgres';
2
- /** Preserve driver scalar parsers and serializers; replace only lossy array parsing. */
3
- export declare function prepareArrays(sql: postgres.Sql): Promise<void>;
4
- /** PostgreSQL text arrays: only unquoted NULL is null; quoted values use the scalar parser. */
5
- export declare function parseArray(text: string, parse: (value: string) => unknown, delimiter?: string): unknown[];
@@ -1,70 +0,0 @@
1
- const initialization = new WeakMap();
2
- /** Preserve driver scalar parsers and serializers; replace only lossy array parsing. */
3
- export function prepareArrays(sql) {
4
- const existing = initialization.get(sql);
5
- if (existing)
6
- return existing;
7
- const ready = (async () => {
8
- const customArrays = new Set(Object.values(sql.options.types).flatMap((type) => type.from));
9
- const arrays = await sql `
10
- SELECT t.typarray AS oid, t.oid AS element, t.typdelim AS delimiter
11
- FROM pg_catalog.pg_type t WHERE t.typarray <> 0`;
12
- for (const type of arrays) {
13
- if (customArrays.has(type.oid))
14
- continue;
15
- const scalar = sql.options.parsers[type.element] ?? ((value) => value);
16
- sql.options.parsers[type.oid] = (value) => parseArray(value, scalar, type.delimiter);
17
- }
18
- })();
19
- initialization.set(sql, ready);
20
- void ready.catch(() => initialization.delete(sql));
21
- return ready;
22
- }
23
- /** PostgreSQL text arrays: only unquoted NULL is null; quoted values use the scalar parser. */
24
- export function parseArray(text, parse, delimiter = ',') {
25
- let position = text.startsWith('[') ? text.indexOf('=') + 1 : 0;
26
- function array() {
27
- if (text[position++] !== '{')
28
- throw new Error('Invalid PostgreSQL array.');
29
- const values = [];
30
- while (position < text.length && text[position] !== '}') {
31
- if (text[position] === '{')
32
- values.push(array());
33
- else {
34
- let value = '';
35
- const quoted = text[position] === '"';
36
- if (quoted)
37
- position++;
38
- while (position < text.length) {
39
- const char = text[position];
40
- if (char === '\\') {
41
- position++;
42
- value += text[position++] ?? '';
43
- }
44
- else if (quoted && char === '"') {
45
- position++;
46
- break;
47
- }
48
- else if (!quoted && (char === delimiter || char === '}'))
49
- break;
50
- else {
51
- value += char;
52
- position++;
53
- }
54
- }
55
- values.push(!quoted && value === 'NULL' ? null : parse(value));
56
- }
57
- if (text[position] === delimiter)
58
- position++;
59
- else if (text[position] !== '}')
60
- throw new Error('Invalid PostgreSQL array separator.');
61
- }
62
- if (text[position++] !== '}')
63
- throw new Error('Unterminated PostgreSQL array.');
64
- return values;
65
- }
66
- const result = array();
67
- if (position !== text.length)
68
- throw new Error('Unexpected PostgreSQL array suffix.');
69
- return result;
70
- }
@@ -1,15 +0,0 @@
1
- import type postgres from 'postgres';
2
- export interface Watch {
3
- failure?: Error;
4
- check(): void;
5
- closed: Promise<never>;
6
- reject(error: Error): void;
7
- }
8
- interface State {
9
- acquiring: Set<(error: Error) => void>;
10
- active: Map<number, Watch>;
11
- capture(marker: string, watch: Watch): () => void;
12
- }
13
- /** Observe the driver's public connection callbacks; never reuse a reconnected transaction. */
14
- export declare function observeConnections(sql: postgres.Sql): State;
15
- export {};
@@ -1,57 +0,0 @@
1
- const states = new WeakMap();
2
- function disconnected() {
3
- return Object.assign(new Error('The reserved PostgreSQL connection closed.'), {
4
- code: 'CONNECTION_CLOSED',
5
- });
6
- }
7
- /** Observe the driver's public connection callbacks; never reuse a reconnected transaction. */
8
- export function observeConnections(sql) {
9
- const previous = states.get(sql);
10
- if (previous)
11
- return previous;
12
- const markers = new Map();
13
- let debug = sql.options.debug;
14
- // Capture the bootstrap connection ID, then restore debug before application work.
15
- // Leaving it enabled would make the driver include SQL and values in enumerable errors.
16
- const captureDebug = (id, text, values, types) => {
17
- const watch = markers.get(text);
18
- if (watch) {
19
- state.active.set(id, watch);
20
- markers.delete(text);
21
- }
22
- if (!markers.size)
23
- sql.options.debug = debug;
24
- if (typeof debug === 'function')
25
- debug(id, text, values, types);
26
- };
27
- const state = {
28
- acquiring: new Set(),
29
- active: new Map(),
30
- capture(marker, watch) {
31
- if (!markers.size) {
32
- debug = sql.options.debug;
33
- sql.options.debug = captureDebug;
34
- }
35
- markers.set(marker, watch);
36
- return () => {
37
- markers.delete(marker);
38
- if (!markers.size)
39
- sql.options.debug = debug;
40
- };
41
- },
42
- };
43
- const onclose = sql.options.onclose;
44
- sql.options.onclose = (id) => {
45
- const error = disconnected();
46
- const watch = state.active.get(id);
47
- if (watch) {
48
- watch.failure ??= error;
49
- watch.reject(watch.failure);
50
- }
51
- for (const reject of state.acquiring)
52
- reject(error);
53
- onclose?.(id);
54
- };
55
- states.set(sql, state);
56
- return state;
57
- }
@@ -1,3 +0,0 @@
1
- import type postgres from 'postgres';
2
- /** Prevent an owned LISTEN client's queued reconnect from reopening it after shutdown. */
3
- export declare function guardListenerShutdown(sql: postgres.Sql): () => void;