@kequtech/tenon-orm 0.2.1 → 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 (55) hide show
  1. package/CHANGELOG.md +52 -36
  2. package/README.md +27 -70
  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 -16
  7. package/dist/connection.js +124 -14
  8. package/dist/database.js +36 -17
  9. package/dist/driver/types.d.ts +1 -1
  10. package/dist/driver/types.js +11 -11
  11. package/dist/errors.d.ts +8 -0
  12. package/dist/errors.js +15 -0
  13. package/dist/generate.js +7 -7
  14. package/dist/generation/catalog.d.ts +2 -2
  15. package/dist/generation/catalog.js +4 -4
  16. package/dist/generation/schema-summary.d.ts +3 -3
  17. package/dist/generation/schema-summary.js +11 -11
  18. package/dist/generation/table-types.js +1 -1
  19. package/dist/index.d.ts +2 -3
  20. package/dist/index.js +1 -1
  21. package/dist/inspection/catalog.d.ts +4 -3
  22. package/dist/inspection/catalog.js +13 -10
  23. package/dist/inspection/cli.js +3 -3
  24. package/dist/inspection/connection.d.ts +2 -2
  25. package/dist/inspection/connection.js +22 -22
  26. package/dist/inspection/options.d.ts +3 -3
  27. package/dist/inspection/options.js +9 -1
  28. package/dist/inspection/protocol.d.ts +7 -0
  29. package/dist/inspection/protocol.js +45 -0
  30. package/dist/inspection/result.d.ts +1 -1
  31. package/dist/inspection/result.js +6 -6
  32. package/dist/inspection/types.d.ts +10 -10
  33. package/dist/migrations/connection.d.ts +3 -2
  34. package/dist/migrations/connection.js +18 -20
  35. package/dist/migrations/history.d.ts +4 -4
  36. package/dist/migrations/history.js +5 -5
  37. package/dist/migrations.js +7 -7
  38. package/dist/query-options.js +5 -5
  39. package/dist/query.js +2 -2
  40. package/dist/transaction.d.ts +2 -2
  41. package/dist/transaction.js +19 -14
  42. package/dist/types.d.ts +7 -3
  43. package/docs/adoption-guide.md +109 -122
  44. package/docs/agent-guide.md +124 -228
  45. package/docs/human-guide.md +44 -55
  46. package/docs/mutations.md +58 -76
  47. package/docs/query-inspection.md +97 -89
  48. package/docs/reference.md +264 -319
  49. package/package.json +4 -4
  50. package/dist/driver/array.d.ts +0 -5
  51. package/dist/driver/array.js +0 -70
  52. package/dist/driver/lifecycle.d.ts +0 -15
  53. package/dist/driver/lifecycle.js +0 -57
  54. package/dist/driver/session.d.ts +0 -8
  55. package/dist/driver/session.js +0 -92
@@ -1,35 +1,27 @@
1
1
  # Tenon agent guide
2
2
 
3
- Tenon executes PostgreSQL SQL through Postgres.js, with parameter binding, transactions, optional write
4
- limits, SQL migrations, and generated row and mutation input types. Use ordinary functions for reusable queries.
5
- This guide contains the setup and contracts needed for routine implementation; the
6
- [reference](reference.md) covers additional options.
7
-
8
- ## Working conventions
9
-
10
- 1. Inspect the application's connection setup, migrations, generated types, query modules, and check
11
- commands. Reuse them. Do not replace a working migration runner just to use Tenon queries.
12
- For new schemas, prefer plural `snake_case` table names and `snake_case` column names; preserve
13
- existing schemas' names.
14
- 2. Write straightforward SQL with explicit selected columns. Use separate complete queries when
15
- variants return different shapes. Introduce fragments only for actual reuse or variable
16
- composition.
17
- 3. Put reusable queries in functions. Accept a `QueryClient` and named object inputs when useful;
18
- keep SQL and query options together. Call the same function with `db` or `tx`.
19
- 4. After SQL changes, inspect the completed queries and run typechecking and relevant database
20
- assertions. Cover meaningful runtime variants and verify declared result contracts against
21
- complete relevant rows.
22
- 5. Keep validation, authorization, retries, and parser configuration in the application. A quoted
23
- identifier or correct row count does not prove authorization.
24
-
25
- ## Install and connect
26
-
27
- Requires Node.js 24+, PostgreSQL 16+, ESM, TypeScript 6+, and Node development typings. Keep the
28
- application's strict TypeScript configuration.
29
-
30
- ```sh
31
- npm install @kequtech/tenon-orm
32
- ```
3
+ Tenon executes explicit PostgreSQL SQL through pg. Use ordinary functions for reusable queries. This
4
+ guide covers routine implementation; the [reference](reference.md) defines the full API and the
5
+ [human guide](human-guide.md) gives a step-by-step introduction.
6
+
7
+ ## Start with the existing project
8
+
9
+ Inspect its connection configuration, migration runner, generated types, query functions, and check
10
+ commands. Preserve those conventions. Tenon queries and type generation do not require Tenon
11
+ migration history. Keep input validation, authorization, domain conversion, and retry policy in the
12
+ application.
13
+
14
+ For an upgrade, record the previous resolved Tenon version from the lockfile or installed package
15
+ and read the [Changelog](../CHANGELOG.md) entries through the target version before changing code.
16
+ It ships with the package alongside this guide. Follow its upgrade instructions, regenerate types
17
+ when mappings change, and run the project's typechecks and relevant database tests. Use the
18
+ [adoption guide](adoption-guide.md) when replacing another ORM or transferring migration ownership.
19
+
20
+ ## Connect
21
+
22
+ Requires Node.js 24+, PostgreSQL 16+, ESM, TypeScript 6+, and Node development typings. Install with
23
+ `npm install @kequtech/tenon-orm`; retain the project's package manager and TypeScript
24
+ configuration.
33
25
 
34
26
  ```ts
35
27
  import { createTenon } from '@kequtech/tenon-orm';
@@ -39,19 +31,15 @@ if (!connection) throw new Error('DATABASE_URL is required');
39
31
  export const db = createTenon({ connection });
40
32
  ```
41
33
 
42
- `connection` accepts a URL, Postgres.js options, or an existing Postgres.js client. An explicit `{}` selects the driver's
43
- environment defaults. Reuse one instance per database connection. `close()` closes owned pools;
44
- supplied clients remain application-owned. The driver retains its native connection callbacks. Await
45
- outstanding work before closing. Environment files are not loaded automatically. Qualify SQL names
46
- or configure `search_path`; CLI `--namespace` does not change runtime query names.
47
-
48
- Use `db.native` for Postgres.js features such as LISTEN, cursors, and COPY without another driver
49
- installation. It is the existing pool-level client. Native calls bypass Tenon's guards and `onQuery`,
50
- and do not join a Tenon transaction; transaction callbacks expose only `tx.query`. `db.close()` ends
51
- owned clients and their listeners; borrowed clients stay application-owned. Follow the
52
- [native access contract](reference.md#native-driver-access) for initialization and shutdown.
34
+ `connection` accepts a URI string, pg `PoolConfig` (including `connectionString`), or an existing
35
+ `pg.Pool`. Reuse an instance for each database configuration. `db.close()` ends internally created
36
+ pools with a default 5,000 ms grace period; supplied pools remain application-owned. Configure
37
+ shutdown with `db.close({ timeoutMs })`; concurrent callers share its outcome. Finish outstanding
38
+ work before closing. Tenon loads no environment files. Qualify table names or set runtime
39
+ `search_path`; CLI `--namespace` selects tooling scope, not runtime query names. See
40
+ [connection options](reference.md#installation-and-connection).
53
41
 
54
- ## Query contracts
42
+ ## Query and result contracts
55
43
 
56
44
  ```ts
57
45
  import { sql, type QueryClient } from '@kequtech/tenon-orm';
@@ -59,211 +47,119 @@ import type { Rows } from './generated-schema.js';
59
47
 
60
48
  type Summary = Pick<Rows['users'], 'id' | 'email'>;
61
49
 
62
- function userByIdQuery({ id }: { id: number }) {
63
- return sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`;
50
+ function userById(client: QueryClient, { id }: { id: number }) {
51
+ return client.query(sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`, {
52
+ returns: 'oneOrNone',
53
+ });
64
54
  }
65
-
66
- function userById(client: QueryClient, input: { id: number }) {
67
- return client.query(userByIdQuery(input), { returns: 'oneOrNone' });
68
- }
69
-
70
- const user = await userById(db, { id: 42 });
71
- // In a transaction, call userById(tx, { id: 42 }).
72
55
  ```
73
56
 
74
- `sql<Row>` describes one selected row. It does not prove SQL/type agreement or validate JSON.
75
- Unannotated fields stay `unknown`. Generated types describe full table rows; joins, aliases,
76
- aggregates, and expressions need projection-specific types.
57
+ `QueryClient` accepts either `db` or a transaction's `tx`. Keep SQL and execution options together.
58
+ `sql<Row>` declares one selected row; it does not verify SQL or validate data. Unannotated fields
59
+ are `unknown`. Generated table types can describe full rows or `Pick` projections; joins and
60
+ aggregates need types for their actual output.
77
61
 
78
- | `returns` | Result | Row-count contract |
79
- | ------------------ | ------------------------------------ | ------------------ |
80
- | `native` (default) | Postgres.js `QueryResult<Row>` array | Any |
81
- | `many` | `Row[]` | Any |
82
- | `oneOrNone` | `Row \| undefined` | At most one |
83
- | `one` | `Row` | Exactly one |
62
+ | `returns` | Result | Required returned rows |
63
+ | ------------------ | -------------------------------------------------- | ---------------------- |
64
+ | `native` (default) | Driver object with `.rows`, `.rowCount`, `.fields` | Any |
65
+ | `many` | Plain `Row[]` | Any |
66
+ | `oneOrNone` | `Row \| undefined` | Zero or one |
67
+ | `one` | `Row` | Exactly one |
84
68
 
85
- `query` options are `{ returns?, affectedRows?, label? }`; unknown options fail. Return modes never
86
- insert `LIMIT` or `RETURNING`. Returned-row checks alone do not undo autocommitted writes. `label`
87
- is an optional nonempty diagnostic string. Native results are row arrays with `.count`, `.command`,
88
- and `.columns`; `many` returns a plain row array.
89
-
90
- The Postgres.js driver is included. Undefined bound parameters fail. Pass JSON values directly,
91
- including arrays, rather than calling `JSON.stringify`. Numeric and bigint scalars and arrays read
92
- as strings. Dates read as `Date`, including invalid dates for timestamp infinities; check validity
93
- before using them. SQL NULL array elements read as JavaScript null.
69
+ Options are `{ returns?, affectedRows?, label? }`; unknown keys fail. Modes do not add SQL limits or
70
+ `RETURNING`. A returned-row mismatch throws `CardinalityError` with code
71
+ `TENON_CARDINALITY_MISMATCH`; it alone does not undo an autocommitted write.
94
72
 
95
73
  ## Parameters and composition
96
74
 
97
- - Ordinary interpolations bind values. Do not quote them or author manual `$n` placeholders inside
98
- templates. `undefined` is rejected; use `null` for SQL null.
99
- - PostgreSQL infers parameter types from SQL. Cast standalone projections, e.g.
100
- `SELECT ${value}::float8 AS value`, so inspection and execution use the same type.
101
- - Use `id = ANY(${ids}::integer[])` for an array of IDs. Do not build a comma-separated string of
102
- values.
103
- - `identifier(schema, table)` quotes separate application-approved components. Ordinary strings such
104
- as `'DESC'` remain bound values, not SQL syntax.
105
- - `sql.fragment` carries SQL and bound values without a row type. Use it for shared predicates or
106
- partial SQL. Fragments cannot execute alone; full `sql` and `unsafeSql` queries cannot be nested.
107
- - `sql.join(fragments, separator)` combines a variable-length list of fragments using `'AND'`,
108
- `'OR'`, or `','`. Use it for optional filters, search terms, or lists of SQL expressions. The list
109
- must be nonempty; group mixed `AND`/`OR` predicates with parentheses.
110
- - `unsafeSql<Row>(text, values)` accepts trusted external SQL with an explicit values array. Never
111
- concatenate request input into its text. Each query executes one statement.
112
-
113
- For example, share a search condition between a page and count query:
75
+ - Interpolate values without quotes. Do not author `$n` placeholders inside templates. `undefined`
76
+ and `null` bind as SQL null, following pg.
77
+ - Use `id = ANY(${ids}::integer[])` for a set of IDs. Cast standalone parameters such as
78
+ `SELECT ${value}::integer AS value` when SQL must establish the type for inspection.
79
+ - `identifier(schema, table)` quotes separate approved names. Strings such as `'DESC'` remain bound
80
+ values; choose SQL syntax through fixed query alternatives or fragments.
81
+ - `sql.fragment` creates partial SQL with bound values; `sql.join(parts, 'AND' | 'OR' | ',')`
82
+ combines a nonempty list. Blank fragments fail. Group mixed conditions with parentheses.
83
+ - Full `sql` or `unsafeSql` queries cannot be nested. `unsafeSql<Row>(text, values)` is for trusted
84
+ external SQL with an explicit values array. Each Tenon query executes one statement.
114
85
 
115
- ```ts
116
- const search = sql.fragment`active = ${true} AND email ILIKE ${pattern}`;
117
- const page = sql<Summary>`SELECT id, email FROM public.users
118
- WHERE ${search} ORDER BY id LIMIT ${limit}`;
119
- const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
120
- WHERE ${search}`;
121
- ```
86
+ Use fragments for actual reuse or variable composition. Keep fixed structure in SQL, and retain
87
+ mandatory authorization predicates when optional filters are absent.
122
88
 
123
- `pattern` and `limit` are validated application inputs. See the
124
- [composition reference](reference.md#sql-fragments-and-joins) for grouping and list examples.
89
+ ## Writes and generated values
125
90
 
126
- ## Object-based writes
91
+ `sql.insertValues(rowOrRows, columns?)` builds column/value fragments;
92
+ `sql.assignments(patch, columns?)` builds assignments. Missing/undefined insert fields use defaults;
93
+ update fields are omitted. Explicit null stays null. Empty batches and empty assignments fail.
94
+ Selected columns ignore unlisted fields and can discard required inputs. Put expressions, conflict
95
+ policy, and `RETURNING` in the surrounding SQL. Batch in the caller below the query-wide
96
+ 65,535-parameter limit.
127
97
 
128
- Use official `sql.insertValues(rowOrRows, columns?)` and `sql.assignments(patch, columns?)` fragments
129
- instead of application helpers that enumerate object keys. Keep tables, authorization predicates,
130
- expressions, conflict behavior and `RETURNING` visible in SQL. Insert missing/undefined fields use
131
- PostgreSQL defaults; update missing/undefined fields are omitted. Explicit null stays null. Empty
132
- batches/updates throw; a single default-only insert works. Omit generated columns and batch large
133
- writes explicitly below the query-wide limit of 65,533 parameters. Expressions and queries are
134
- rejected as cell values; write arithmetic, `DEFAULT`, and `excluded.column` in the surrounding SQL.
98
+ Use generated `Rows.Insert['users']` and `Rows.Update['users']` inputs; narrow editable fields with
99
+ `Pick`. These types account for defaults, writable columns, domains, and driver representations.
100
+ They do not validate constraints or JSON contents. See [mutation examples](mutations.md).
135
101
 
136
- Use generated `Rows.Insert['table']` and `Rows.Update['table']` for mutation inputs when available;
137
- narrow editable fields with `Pick`. They account for defaults, nullability, domains, and generated
138
- columns. Write mappings differ from returned rows: timestamps use `Date | string`, bigint uses
139
- `string | bigint`, and JSON accepts native JavaScript values through Tenon's exported `JSONValue` type. See the [input types](reference.md#insert-and-update-inputs).
102
+ Use `JSON.stringify(value)` for JSON write inputs; arrays otherwise bind as PostgreSQL arrays.
103
+ Bigint and numeric scalars read as strings, while numeric arrays read as numbers and can lose
104
+ precision. Temporal reads use `Date | number` for finite dates and infinities. SQL NULL array
105
+ elements remain JavaScript null. Keep domain validation at the persistence boundary. See
106
+ [value mappings](reference.md#generated-types-and-checking).
140
107
 
141
- Selected columns choose and order fields, e.g. `sql.insertValues(rows, ['name', 'score'])`. Unlisted
142
- fields are ignored. See the [mutation contract](reference.md#object-based-mutation-fragments) and
143
- [cookbook](mutations.md).
108
+ ## Transactions and native access
144
109
 
145
- ## Inspect and test completed queries
146
-
147
- Inspection ships with Tenon and needs no additional setup:
110
+ For an existing user with id 42:
148
111
 
149
112
  ```ts
150
- import { describeQuery } from '@kequtech/tenon-orm';
151
-
152
- const query = userByIdQuery({ id: 42 });
153
- const description = await describeQuery({ connection, query });
154
- ```
155
-
156
- Pass the actual query object that will go to `db.query`, including fragments and conditional
157
- predicates. A small ordinary query-building function can make the same statement available to
158
- inspection and execution tests. Keep authorization policy inside the builder or shared fragments.
159
-
160
- For a SQL file with PostgreSQL `$n` placeholders:
161
-
162
- ```sh
163
- npx tenon describe query.sql
113
+ await db.transaction(async (tx) => {
114
+ await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${42}`, {
115
+ affectedRows: { exact: 1 },
116
+ });
117
+ });
164
118
  ```
165
119
 
166
- Supply `DATABASE_URL` through the application's existing environment setup. Match its role,
167
- `search_path`, and timezone through the connection URI or optional `session` settings. Review
168
- the returned `context`; this is a separate connection with Postgres.js read mappings. It does not
169
- inherit custom parsers, temporary tables, or transaction-local state.
170
-
171
- The report contains parameter/column types and findings with stable issue codes. Result nullability
172
- is unknown, source `NOT NULL` is informational, and JSON remains unknown. CLI success means the
173
- statement was described, not that `sql<Row>`, values, permissions, or business behavior were verified.
174
-
175
- With a test fixture containing user 42, check the database projection and the actual value separately:
176
-
177
- ```ts
178
- import assert from 'node:assert/strict';
179
-
180
- assert.deepEqual(
181
- description.columns.map(({ name, readType }) => [name, readType]),
182
- [
183
- ['id', 'number'],
184
- ['email', 'string'],
185
- ],
186
- );
187
- const expected = { id: 42, email: 'reader@example.test' } satisfies Summary;
188
- assert.deepEqual(await userById(db, { id: expected.id }), expected);
189
- ```
190
-
191
- The builder keeps inspection and application SQL together; the query function keeps execution options
192
- and its `db`/`tx` contract together. Keep simple queries inline when they do not need a shared builder.
193
- The typed fixture checks the declared shape, the description checks PostgreSQL's projection, and
194
- execution checks the actual row. None of these proves coverage of other inputs or authorization rules.
195
-
196
- Run the same query through the real application connection against its test database. Assert full
197
- relevant rows/effects and exercise meaningful branches: ownership, optional filters, absent joins,
198
- empty aggregates, pagination ties, JSON validation, and failed/replayed/rolled-back writes as
199
- applicable. Keep these tests alongside the query; describing a single instance covers only that
200
- instance. See [query inspection](query-inspection.md) for session options, limits, and issue codes.
201
-
202
- ## Transactions and writes
203
-
204
- Use `db.transaction(async (tx) => ...)`; use `tx` for every query in scope and await all work. The
205
- callback commits on success and rolls back on an escaping error. Do not issue direct
206
- BEGIN/COMMIT/ROLLBACK boundaries or use `tx` after return. Transactions do not span database
207
- instances. Nested callback transactions and automatic retries are not provided.
208
-
209
- Use `affectedRows: { exact: n }` or `{ max: n }` for INSERT/UPDATE/DELETE/MERGE when a write must
210
- respect a count. Bounds are nonnegative safe integers. A guarded `db.query` uses a transaction; a
211
- guarded `tx.query` uses a savepoint. Rejected writes are rolled back before throwing. A caught
212
- guarded failure permits continued work; ordinary SQL failures may abort the transaction. Reserve
213
- savepoint names beginning with `tenon_affected_rows_`.
214
-
215
- Affected counts cover the final command, not extra trigger/cascade/CTE effects. They cannot detect
216
- the wrong single row. Sequence advances and external effects are not reversible. A lost commit
217
- acknowledgment may leave the outcome unknown; retry only according to application policy. Native Postgres.js
218
- errors retain their details; `AffectedRowsError` has codes `TENON_AFFECTED_ROWS_MISMATCH` and
219
- `TENON_AFFECTED_ROWS_UNSUPPORTED`. Cleanup failures can produce `AggregateError`.
220
-
221
- Optional `onQuery` observes labels, duration, outcome, and available command/count/ error code,
222
- without SQL or values. Observers are not awaited. Query success inside a callback does not mean the
223
- outer transaction committed.
120
+ Use `tx` for every operation in the callback and await the work. Tenon commits on success and rolls
121
+ back on an escaping error; the callback client expires when the callback finishes. Direct
122
+ BEGIN/COMMIT/ROLLBACK boundaries, nested callback transactions, and automatic retries are not
123
+ provided. Transactions belong to one database instance.
124
+
125
+ `affectedRows: { exact: n }` or `{ max: n }` checks the final INSERT/UPDATE/DELETE/MERGE count.
126
+ Bounds are nonnegative safe integers. A guarded `db.query` uses a transaction; a guarded `tx.query`
127
+ uses a savepoint. Rejection rolls back that operation. A caught guarded failure permits continued
128
+ work; an unguarded SQL error normally aborts the transaction. Counts exclude extra trigger/cascade
129
+ and CTE effects. Reserve `tenon_affected_rows_` savepoint names for Tenon. A lost commit
130
+ acknowledgment can leave the outcome unknown.
131
+
132
+ `db.native` exposes the existing pool for driver features. Native calls bypass Tenon's guards and
133
+ `onQuery`; they do not join a Tenon transaction. Close owned pools through `db.close()`. Follow the
134
+ [native access contract](reference.md#native-driver-access) for resource ownership and LISTEN.
135
+ Optional `onQuery` reports completion metadata without SQL or values; success inside a callback does
136
+ not establish that the outer transaction committed.
137
+
138
+ ## Inspect and validate
139
+
140
+ Use `describeQuery({ connection, query })` on completed SQL or `tenon describe query.sql` on a file.
141
+ Inspection owns a separate connection. Match the runtime role, search path, and timezone. Temporary
142
+ tables and transaction-local state are not inherited; reported mappings assume pg's default parsers.
143
+ Result nullability is unknown and JSON structure is unchecked. A successful description does not
144
+ verify `sql<Row>`, bound values, execution permission, or effects.
145
+
146
+ Pair inspection with database assertions for complete relevant rows, meaningful query variants, and
147
+ rollback. A small query-building function can share the same statement with inspection and
148
+ execution; keep simple queries inline when that is sufficient. See
149
+ [query inspection](query-inspection.md).
224
150
 
225
151
  ## Schema changes
226
152
 
227
- Reuse the application's migration setup. With Tenon's runner and generated table types, use this
228
- development script in `package.json`:
229
-
230
- ```json
231
- {
232
- "scripts": {
233
- "db:migrate": "tenon migrate && tenon generate --out src/generated-schema.d.ts"
234
- }
235
- }
236
- ```
237
-
238
- ```sh
239
- npx tenon new create_users
240
- # Fill the generated up/down SQL before migrating.
241
- npm run db:migrate
242
- npx tenon generate --out src/generated-schema.d.ts --check
243
- npx tenon status
244
- npx tenon rollback --steps 1
245
- ```
246
-
247
- Commit migrations and generated types together. Production runs only `npx tenon migrate` and deploys
248
- the already-built application. Omit generation from the development script if table types are
249
- unused.
250
-
251
- Database commands require `DATABASE_URL`. Migration commands accept `--directory` (default
252
- `migrations`) and `--namespace` (default `public`); generation accepts `--namespace`. Files use
253
- `<numeric-id>_<name>.up.sql` with optional matching down SQL. Omit a down file for an intentionally
254
- irreversible migration. Never edit applied files. For a custom namespace, pass it to both commands
255
- in the development script.
256
-
257
- Each migration commits with its history entry; earlier files remain applied if a later one fails.
258
- Tools use session advisory locks: use a direct connection or session pooling and application
259
- timeouts. Nontransactional DDL needs a separate procedure. `generate --check` detects representation
260
- drift, not pending migrations or every possible schema change. Generation covers ordinary tables and
261
- partitioned roots; views/foreign tables need authored query types.
262
-
263
- When replacing another ORM, preserve its schema and behavior. If transferring migration ownership,
264
- capture one reviewed baseline with `tenon baseline capture`, record it only on matching existing
265
- databases with `tenon baseline adopt`, and use `tenon migrate` on fresh databases. Adoption does not
266
- compare schemas or run baseline SQL. Fresh replay omits ownership and grants; apply deployment
267
- security provisioning before application access, especially for `SECURITY DEFINER` functions.
268
- See [adoption](adoption-guide.md) for the full transition procedure. A down migration or schema-only
269
- baseline is not a data backup.
153
+ With Tenon's runner, development can use
154
+ `tenon migrate && tenon generate --out src/generated-schema.d.ts`. Commit migrations and exact
155
+ generated output together; exclude the generated file from automatic formatting. Production runs
156
+ migrations and deploys committed types. `generate --check` detects representation drift, not pending
157
+ migrations or every schema change.
158
+
159
+ All database CLI commands require `DATABASE_URL`. `tenon new <name>` creates empty up/down files
160
+ offline; fill them before migrating. Migration commands use `--directory migrations` and
161
+ `--namespace public` by default; generation also accepts `--namespace`. Pass a custom namespace to
162
+ both migration and generation. Omit the down file for an irreversible migration; never edit applied
163
+ history. Each file commits independently with its history entry. Tools use session advisory locks,
164
+ so use a direct or session-pooled connection. Nontransactional DDL needs a separate procedure. See
165
+ the [command reference](reference.md#command-reference).
@@ -1,12 +1,12 @@
1
1
  # Using Tenon: a human guide
2
2
 
3
3
  Tenon lets you use PostgreSQL directly from TypeScript. You write SQL; Tenon binds its inputs and
4
- executes it through Postgres.js. This guide follows a small application from its first table to reusable
4
+ executes it through pg. This guide follows a small application from its first table to reusable
5
5
  queries, transactions, and result checking.
6
6
 
7
- Use the [reference](reference.md) when you need exact options or unusual cases. The
8
- [agent guide](agent-guide.md) is a separate working guide for coding agents;
9
- [adoption](adoption-guide.md) covers moving an existing application to Tenon.
7
+ Use the [reference](reference.md) for exact contracts and [adoption](adoption-guide.md) when
8
+ replacing an existing ORM. Code blocks build on the connection and table introduced below;
9
+ query-function examples belong in their own module.
10
10
 
11
11
  ## Connect to your database
12
12
 
@@ -29,15 +29,13 @@ if (!connection) throw new Error('DATABASE_URL is required');
29
29
  export const db = createTenon({ connection });
30
30
  ```
31
31
 
32
- Reuse this instance across the application and call `await db.close()` at shutdown. For multiple
33
- databases, create an instance for each connection. You can also supply an existing Postgres.js client; your
34
- application then closes that pool.
32
+ Reuse this instance across the application and call `await db.close()` at shutdown. You can also
33
+ supply an existing `pg.Pool`; your application then owns its shutdown.
35
34
 
36
35
  ## Create a table
37
36
 
38
- If you already have a schema and migration runner, you can keep them. For new schemas, prefer plural
39
- `snake_case` table names and `snake_case` column names. Tenon preserves your schema's exact names.
40
- Create a migration:
37
+ If you already have a schema and migration runner, keep them. For a fresh example database, create a
38
+ migration:
41
39
 
42
40
  ```sh
43
41
  npx tenon new create_users
@@ -101,8 +99,8 @@ separately from the SQL text, even when the value contains SQL.
101
99
 
102
100
  The type describes one selected row. `returns: 'oneOrNone'` gives you that row or `undefined`; it
103
101
  throws if the query returns more than one row. Use `'one'` when absence should also be an error,
104
- `'many'` for an array, or omit `returns` for the native result array with `.count` and `.columns`.
105
- These options do not change your SQL.
102
+ `'many'` for an array, or omit `returns` for the native result object with `.rows`, `.rowCount`, and
103
+ `.fields`. These options do not change your SQL.
106
104
 
107
105
  Generated table types can help describe projections without repeating every field:
108
106
 
@@ -132,16 +130,16 @@ export function userByEmail(client: QueryClient, { email }: { email: string }) {
132
130
  returns: 'oneOrNone',
133
131
  });
134
132
  }
135
-
136
- const user = await userByEmail(db, { email: 'reader@example.test' });
137
133
  ```
138
134
 
139
- `QueryClient` means the function accepts either your database instance or a transaction client. Keep
140
- the SQL and execution options together in the function.
135
+ Call `userByEmail(db, { email: 'reader@example.test' })` at the call site, or pass a transaction's
136
+ `tx`. Keep the SQL and execution options together in the function.
141
137
 
142
138
  ## Change data in a transaction
143
139
 
144
140
  ```ts
141
+ import { userByEmail } from './queries.js';
142
+
145
143
  await db.transaction(async (tx) => {
146
144
  const user = await userByEmail(tx, { email: 'reader@example.test' });
147
145
  if (!user) throw new Error('User not found');
@@ -179,50 +177,31 @@ await db.query(sql`UPDATE public.users SET ${sql.assignments(patch, ['active'])}
179
177
  WHERE email = ${input.email}`);
180
178
  ```
181
179
 
182
- Generated insert types distinguish required fields from defaults and nullable fields. Update types
183
- make writable fields optional; use `Pick` to limit what an action may edit. See the
184
- [input-type reference](reference.md#insert-and-update-inputs) for write-value mappings.
185
-
186
- Keep expressions, authorization, conflict handling and `RETURNING` in SQL. Selected column lists
187
- choose which fields to write; they do not validate the complete object. See the
188
- [mutation cookbook](mutations.md) for batches and upserts.
180
+ Use `Pick` to limit editable fields. Keep expressions, predicates, conflict handling, and
181
+ `RETURNING` in SQL. Selected columns choose fields; they do not validate the complete object. See
182
+ [input types](reference.md#insert-and-update-inputs) for mappings and the
183
+ [mutation cookbook](mutations.md) for defaults, batches, and upserts.
189
184
 
190
185
  ## Check your SQL and result types
191
186
 
192
- Use `describeQuery({ connection, query })` on the completed query object, or
193
- `npx tenon describe query.sql` on a SQL file, to inspect the server's parameter and result types.
194
- This works with dynamic fragments and needs no additional checking setup. Match the application's
195
- connection settings and keep database assertions for actual rows, nulls, JSON, permissions, and
196
- transaction effects. See [query inspection](query-inspection.md).
197
-
198
- TypeScript checks the application code and the result types you declare. Describe reports
199
- PostgreSQL metadata; it does not generate or verify a TypeScript annotation. Use generated table
200
- types where appropriate, declare projections explicitly, and check complete returned rows in tests.
187
+ Use `describeQuery({ connection, query })` on completed SQL, or `npx tenon describe query.sql` on a
188
+ file. It reports PostgreSQL metadata and a conservative `rowType` declaration; it does not update or
189
+ verify the application's `sql<Row>` annotation. Inspection uses a separate session, so match the
190
+ runtime role and settings. See [query inspection](query-inspection.md) for a worked test.
201
191
 
202
- 1. Run `npm run db:migrate` against the development/test database.
203
- 2. Inspect the completed SQL and review the reported types and unresolved findings.
204
- 3. Run TypeScript and database tests. Keep these checks in routine CI validation.
205
-
206
- Input validation and authorization remain application responsibilities. Describe cannot establish
207
- that a request is authorized, that its values are valid, or that returned JSON has an
208
- application-specific shape.
192
+ After migrating the development/test database, inspect meaningful query variants, run TypeScript,
193
+ and test actual rows and effects. Keep those checks in CI. Execution assertions cover values, nulls,
194
+ JSON, authorization, and rollback that description alone cannot establish.
209
195
 
210
196
  ## Reuse or assemble parts of SQL when needed
211
197
 
212
- For fixed conditions, write them directly:
213
-
214
- ```ts
215
- const users = await db.query(
216
- sql<UserSummary>`SELECT id, email FROM public.users WHERE active = ${true}`,
217
- { returns: 'many' },
218
- );
219
- ```
220
-
221
198
  A fragment is a piece of SQL that carries its bound values with it. It is useful when multiple
222
199
  queries share a condition. For example, a search can use the same predicate for its result page and
223
200
  its count:
224
201
 
225
202
  ```ts
203
+ const pattern = '%example%';
204
+ const limit = 20;
226
205
  const search = sql.fragment`active = ${true} AND email ILIKE ${pattern}`;
227
206
  const page = sql<UserSummary>`SELECT id, email FROM public.users
228
207
  WHERE ${search} ORDER BY id LIMIT ${limit}`;
@@ -230,8 +209,8 @@ const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
230
209
  WHERE ${search}`;
231
210
  ```
232
211
 
233
- Here `pattern` is an application-supplied search pattern and `limit` is a validated page size.
234
- Execute each query with `db.query`. The complete query supplies the result type.
212
+ Execute each query with `db.query`. Validate application-supplied patterns and page sizes before
213
+ building queries. The complete query supplies the result type.
235
214
 
236
215
  Use `sql.join` when the number of fragments varies, such as one search condition per word or a set
237
216
  of optional filters. It combines fragments with `'AND'`, `'OR'`, or `','` and preserves their bound
@@ -248,10 +227,20 @@ const query = sql<UserSummary>`SELECT id, email FROM public.users
248
227
  WHERE ${sql.join(filters, 'AND')} ORDER BY id`;
249
228
  ```
250
229
 
251
- This matches active users whose email contains every term. With no terms, the active condition
252
- still applies. The list must be nonempty; `join` inserts separators, so use parentheses as usual
230
+ This matches active users whose email contains every term. With no terms, the active condition still
231
+ applies. For an entirely optional set of filters, omit the whole `WHERE` clause when the list is
232
+ empty:
233
+
234
+ ```ts
235
+ const optionalFilters = terms.map((term) => sql.fragment`email ILIKE ${'%' + term + '%'}`);
236
+ const unfilteredOrMatching = optionalFilters.length
237
+ ? sql<UserSummary>`SELECT id, email FROM public.users WHERE ${sql.join(optionalFilters, 'AND')} ORDER BY id`
238
+ : sql<UserSummary>`SELECT id, email FROM public.users ORDER BY id`;
239
+ ```
240
+
241
+ `sql.join` and fragments require nonempty input. Keep mandatory authorization predicates in the
242
+ initial list when applicable; an empty optional-filter list must not remove them. Use parentheses
253
243
  when mixing `AND` and `OR`. See the [composition reference](reference.md#sql-fragments-and-joins).
254
244
 
255
- For arrays, identifiers, type mappings, CLI options, and error behavior, use the
256
- [reference](reference.md). You do not need fragments or a query builder to use PostgreSQL joins,
257
- aggregates, upserts, or other SQL features.
245
+ Use fixed SQL when composition is unnecessary. See the [reference](reference.md) for arrays,
246
+ identifiers, value mappings, CLI options, and error behavior.