@kequtech/tenon-orm 0.2.1 → 0.2.2

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.
@@ -1,35 +1,26 @@
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 Postgres.js. Use ordinary functions for reusable
4
+ queries. This guide covers routine implementation; the [reference](reference.md) defines the full
5
+ API and the [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 and read the
15
+ [Changelog](../CHANGELOG.md) entries through the target version before changing code. It ships with
16
+ the package alongside this guide. Follow its upgrade instructions, regenerate types when mappings
17
+ 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 configuration.
33
24
 
34
25
  ```ts
35
26
  import { createTenon } from '@kequtech/tenon-orm';
@@ -39,19 +30,15 @@ if (!connection) throw new Error('DATABASE_URL is required');
39
30
  export const db = createTenon({ connection });
40
31
  ```
41
32
 
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.
33
+ `connection` accepts a URI string, `{ url, options? }`, Postgres.js options, or an existing driver
34
+ client. Reuse an instance for each database configuration. `db.close()` ends internally created
35
+ pools with a default 5,000 ms grace period; supplied clients remain application-owned. Configure
36
+ shutdown with `db.close({ timeoutMs })`; concurrent callers share its outcome. Finish outstanding
37
+ work before closing. Tenon
38
+ loads no environment files. Qualify table names or set runtime `search_path`; CLI `--namespace`
39
+ selects tooling scope, not runtime query names. See [connection options](reference.md#installation-and-connection).
47
40
 
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.
53
-
54
- ## Query contracts
41
+ ## Query and result contracts
55
42
 
56
43
  ```ts
57
44
  import { sql, type QueryClient } from '@kequtech/tenon-orm';
@@ -59,211 +46,115 @@ import type { Rows } from './generated-schema.js';
59
46
 
60
47
  type Summary = Pick<Rows['users'], 'id' | 'email'>;
61
48
 
62
- function userByIdQuery({ id }: { id: number }) {
63
- return sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`;
64
- }
65
-
66
- function userById(client: QueryClient, input: { id: number }) {
67
- return client.query(userByIdQuery(input), { returns: 'oneOrNone' });
49
+ function userById(client: QueryClient, { id }: { id: number }) {
50
+ return client.query(sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`, {
51
+ returns: 'oneOrNone',
52
+ });
68
53
  }
69
-
70
- const user = await userById(db, { id: 42 });
71
- // In a transaction, call userById(tx, { id: 42 }).
72
54
  ```
73
55
 
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.
56
+ `QueryClient` accepts either `db` or a transaction's `tx`. Keep SQL and execution options together.
57
+ `sql<Row>` declares one selected row; it does not verify SQL or validate data. Unannotated fields
58
+ are `unknown`. Generated table types can describe full rows or `Pick` projections; joins and
59
+ aggregates need types for their actual output.
77
60
 
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 |
61
+ | `returns` | Result | Required returned rows |
62
+ | ------------------ | -------------------------------------------------- | ---------------------- |
63
+ | `native` (default) | Driver array with `.count`, `.command`, `.columns` | Any |
64
+ | `many` | Plain `Row[]` | Any |
65
+ | `oneOrNone` | `Row \| undefined` | Zero or one |
66
+ | `one` | `Row` | Exactly one |
84
67
 
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.
68
+ Options are `{ returns?, affectedRows?, label? }`; unknown keys fail. Modes do not add SQL limits
69
+ or `RETURNING`. A returned-row mismatch throws `CardinalityError` with code `TENON_CARDINALITY_MISMATCH`;
70
+ it alone does not undo an autocommitted write.
94
71
 
95
72
  ## Parameters and composition
96
73
 
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:
114
-
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
- ```
122
-
123
- `pattern` and `limit` are validated application inputs. See the
124
- [composition reference](reference.md#sql-fragments-and-joins) for grouping and list examples.
125
-
126
- ## Object-based writes
74
+ - Interpolate values without quotes. Do not author `$n` placeholders inside templates.
75
+ `undefined` bound values fail; `null` means SQL null.
76
+ - Use `id = ANY(${ids}::integer[])` for a set of IDs. Cast standalone parameters such as
77
+ `SELECT ${value}::integer AS value` when SQL must establish the type for inspection.
78
+ - `identifier(schema, table)` quotes separate approved names. Strings such as `'DESC'` remain
79
+ bound values; choose SQL syntax through fixed query alternatives or fragments.
80
+ - `sql.fragment` creates partial SQL with bound values; `sql.join(parts, 'AND' | 'OR' | ',')`
81
+ combines a nonempty list. Blank fragments fail. Group mixed conditions with parentheses.
82
+ - Full `sql` or `unsafeSql` queries cannot be nested. `unsafeSql<Row>(text, values)` is for trusted
83
+ external SQL with an explicit values array. Each Tenon query executes one statement.
127
84
 
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.
85
+ Use fragments for actual reuse or variable composition. Keep fixed structure in SQL, and retain
86
+ mandatory authorization predicates when optional filters are absent.
135
87
 
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).
88
+ ## Writes and generated values
140
89
 
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).
90
+ `sql.insertValues(rowOrRows, columns?)` builds column/value fragments; `sql.assignments(patch,
91
+ columns?)` builds assignments. Missing/undefined insert fields use defaults; update fields are
92
+ omitted. Explicit null stays null. Empty batches and empty assignments fail. Selected columns
93
+ ignore unlisted fields and can discard required inputs. Put expressions, conflict policy, and
94
+ `RETURNING` in the surrounding SQL. Batch in the caller below the query-wide 65,533-parameter limit.
144
95
 
145
- ## Inspect and test completed queries
96
+ Use generated `Rows.Insert['users']` and `Rows.Update['users']` inputs; narrow editable fields with
97
+ `Pick`. These types account for defaults, writable columns, domains, and driver representations.
98
+ They do not validate constraints or JSON contents. See [mutation examples](mutations.md).
146
99
 
147
- Inspection ships with Tenon and needs no additional setup:
100
+ Pass native JSON objects/arrays rather than pre-serialized text. Bigint and numeric reads are
101
+ strings; bigint writes accept `string | bigint`. Dates read as `Date`, including invalid dates for
102
+ infinities; validate before using date methods. SQL NULL array elements are JavaScript null.
103
+ Keep domain validation at the persistence boundary. See [value mappings](reference.md#generated-types-and-checking).
148
104
 
149
- ```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
164
- ```
165
-
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.
105
+ ## Transactions and native access
170
106
 
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:
107
+ For an existing user with id 42:
176
108
 
177
109
  ```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);
110
+ await db.transaction(async (tx) => {
111
+ await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${42}`, {
112
+ affectedRows: { exact: 1 },
113
+ });
114
+ });
189
115
  ```
190
116
 
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.
117
+ Use `tx` for every operation in the callback and await the work. Tenon commits on success and rolls
118
+ back on an escaping error; the callback client expires when the callback finishes. Direct
119
+ BEGIN/COMMIT/ROLLBACK boundaries, nested callback transactions, and automatic retries are not
120
+ provided. Transactions belong to one database instance.
201
121
 
202
- ## Transactions and writes
122
+ `affectedRows: { exact: n }` or `{ max: n }` checks the final INSERT/UPDATE/DELETE/MERGE count.
123
+ Bounds are nonnegative safe integers. A guarded `db.query` uses a transaction; a guarded `tx.query`
124
+ uses a savepoint. Rejection rolls back that operation. A caught guarded failure permits continued
125
+ work; an unguarded SQL error normally aborts the transaction. Counts exclude extra trigger/cascade
126
+ and CTE effects. Reserve `tenon_affected_rows_` savepoint names for Tenon. A lost commit
127
+ acknowledgment can leave the outcome unknown.
203
128
 
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.
129
+ `db.native` exposes the existing pool for driver features. Native calls bypass Tenon's guards and
130
+ `onQuery`; they do not join a Tenon transaction. Close owned clients through `db.close()`. Follow
131
+ the [native access contract](reference.md#native-driver-access) for parser initialization and LISTEN
132
+ limitations. Optional `onQuery` reports completion metadata without SQL or values; success inside
133
+ a callback does not establish that the outer transaction committed.
208
134
 
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_`.
135
+ ## Inspect and validate
214
136
 
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`.
137
+ Use `describeQuery({ connection, query })` on completed SQL or `tenon describe query.sql` on a file.
138
+ Inspection owns a separate connection. Match the runtime role, search path, and timezone; custom
139
+ parsers, temporary tables, and transaction-local state are not inherited. Result nullability is
140
+ unknown and JSON structure is unchecked. A successful description does not verify `sql<Row>`,
141
+ bound values, execution permission, or effects.
220
142
 
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.
143
+ Pair inspection with database assertions for complete relevant rows, meaningful query variants,
144
+ and rollback. A small query-building function can share the same statement with inspection and
145
+ execution; keep simple queries inline when that is sufficient. See [query inspection](query-inspection.md).
224
146
 
225
147
  ## Schema changes
226
148
 
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.
149
+ With Tenon's runner, development can use `tenon migrate && tenon generate --out
150
+ src/generated-schema.d.ts`. Commit migrations and exact generated output together; exclude the
151
+ generated file from automatic formatting. Production runs migrations and deploys committed types.
152
+ `generate --check` detects representation drift, not pending migrations or every schema change.
153
+
154
+ All database CLI commands require `DATABASE_URL`. `tenon new <name>` creates empty up/down files
155
+ offline; fill them before migrating. Migration commands use `--directory migrations` and
156
+ `--namespace public` by default; generation also accepts `--namespace`. Pass a custom namespace to
157
+ both migration and generation. Omit the down file for an irreversible migration; never edit applied
158
+ history. Each file commits independently with its history entry. Tools use session advisory locks,
159
+ so use a direct or session-pooled connection. Nontransactional DDL needs a separate procedure.
160
+ See the [command reference](reference.md#command-reference).
@@ -4,9 +4,9 @@ Tenon lets you use PostgreSQL directly from TypeScript. You write SQL; Tenon bin
4
4
  executes it through Postgres.js. 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,12 @@ 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 supply an existing Postgres.js client; your application then owns its shutdown.
35
33
 
36
34
  ## Create a table
37
35
 
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:
36
+ If you already have a schema and migration runner, keep them. For a fresh example database,
37
+ create a migration:
41
38
 
42
39
  ```sh
43
40
  npx tenon new create_users
@@ -179,50 +176,31 @@ await db.query(sql`UPDATE public.users SET ${sql.assignments(patch, ['active'])}
179
176
  WHERE email = ${input.email}`);
180
177
  ```
181
178
 
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.
179
+ Use `Pick` to limit editable fields. Keep expressions, predicates, conflict handling, and
180
+ `RETURNING` in SQL. Selected columns choose fields; they do not validate the complete object.
181
+ See [input types](reference.md#insert-and-update-inputs) for mappings and the
182
+ [mutation cookbook](mutations.md) for defaults, batches, and upserts.
189
183
 
190
184
  ## Check your SQL and result types
191
185
 
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.
201
-
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.
186
+ Use `describeQuery({ connection, query })` on completed SQL, or `npx tenon describe query.sql` on a
187
+ file. It reports PostgreSQL metadata and a conservative `rowType` declaration; it does not update
188
+ or verify the application's `sql<Row>` annotation. Inspection uses a separate session, so match
189
+ the runtime role and settings. See [query inspection](query-inspection.md) for a worked test.
205
190
 
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.
191
+ After migrating the development/test database, inspect meaningful query variants, run TypeScript,
192
+ and test actual rows and effects. Keep those checks in CI. Execution assertions cover values,
193
+ nulls, JSON, authorization, and rollback that description alone cannot establish.
209
194
 
210
195
  ## Reuse or assemble parts of SQL when needed
211
196
 
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
197
  A fragment is a piece of SQL that carries its bound values with it. It is useful when multiple
222
198
  queries share a condition. For example, a search can use the same predicate for its result page and
223
199
  its count:
224
200
 
225
201
  ```ts
202
+ const pattern = '%example%';
203
+ const limit = 20;
226
204
  const search = sql.fragment`active = ${true} AND email ILIKE ${pattern}`;
227
205
  const page = sql<UserSummary>`SELECT id, email FROM public.users
228
206
  WHERE ${search} ORDER BY id LIMIT ${limit}`;
@@ -230,8 +208,8 @@ const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
230
208
  WHERE ${search}`;
231
209
  ```
232
210
 
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.
211
+ Execute each query with `db.query`. Validate application-supplied patterns and page sizes before
212
+ building queries. The complete query supplies the result type.
235
213
 
236
214
  Use `sql.join` when the number of fragments varies, such as one search condition per word or a set
237
215
  of optional filters. It combines fragments with `'AND'`, `'OR'`, or `','` and preserves their bound
@@ -249,9 +227,19 @@ const query = sql<UserSummary>`SELECT id, email FROM public.users
249
227
  ```
250
228
 
251
229
  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
+ still applies. For an entirely optional set of filters, omit the whole `WHERE` clause when the list
231
+ is empty:
232
+
233
+ ```ts
234
+ const optionalFilters = terms.map((term) => sql.fragment`email ILIKE ${'%' + term + '%'}`);
235
+ const unfilteredOrMatching = optionalFilters.length
236
+ ? sql<UserSummary>`SELECT id, email FROM public.users WHERE ${sql.join(optionalFilters, 'AND')} ORDER BY id`
237
+ : sql<UserSummary>`SELECT id, email FROM public.users ORDER BY id`;
238
+ ```
239
+
240
+ `sql.join` and fragments require nonempty input. Keep mandatory authorization predicates in the
241
+ initial list when applicable; an empty optional-filter list must not remove them. Use parentheses
253
242
  when mixing `AND` and `OR`. See the [composition reference](reference.md#sql-fragments-and-joins).
254
243
 
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.
244
+ Use fixed SQL when composition is unnecessary. See the [reference](reference.md) for arrays,
245
+ identifiers, value mappings, CLI options, and error behavior.