@kequtech/tenon-orm 0.4.0 → 0.4.3

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,190 +1,131 @@
1
1
  # Adopting Tenon
2
2
 
3
- Replace application queries independently of migration ownership. Tenon can query an existing schema
4
- and generate types while the current migration runner stays in place. For a Tenon version upgrade,
5
- read the [Changelog](../CHANGELOG.md).
6
-
7
- ## Upgrade from Tenon 0.3.x
8
-
9
- Tenon 0.4 uses pgclear 0.3.x in place of pg. Keep the existing database, credentials,
10
- migration history, and application query functions. Pgclear is included as a runtime dependency.
11
- With npm, update Tenon:
12
-
13
- ```sh
14
- npm install @kequtech/tenon-orm@^0.4.0
15
- ```
16
-
17
- Use the project's existing package manager and then review these boundaries:
18
-
19
- 1. Translate pg `PoolConfig` fields to pgclear `PoolOptions`, or replace a borrowed pg Pool with a
20
- pgclear Pool. A URI remains supported. Supply connection credentials explicitly; the driver does
21
- not read PG environment variables, pgpass, service files, or pg-specific URI options.
22
- 2. Keep Tenon `sql`, `QueryClient`, result modes, and callback transactions. Update native calls to
23
- pgclear APIs: use `queryClassic(text, values?)` for finished SQL, release acquired sessions, and
24
- close dedicated listeners separately. Import `sql`, `json`, `param`, and codec helpers from
25
- `@kequtech/tenon-orm`. If native driver code also uses Pgclear's tag, alias it as `pgSql`; its query
26
- objects belong in native `query`, while Tenon's query objects belong in `db.query`.
27
- 3. Review temporal strings, readonly result arrays, Uint8Array bytes, exact string integer/numeric
28
- arrays, and JSON/array inputs. Use `json(value)`, `param(value, codec)`, or encoded text for JSON
29
- and arrays. Plain Date, bigint, and supported Temporal inputs are accepted with server inference;
30
- Date encodes in UTC. They do not change output types. Command rowCount remains a number or null.
31
- See [value mappings](reference.md#generated-types-and-checking).
32
- 4. Regenerate database declarations against the intended development/test schema. Replace former
33
- `Rows['users']`, `Rows.Insert['users']`, and `Rows.Update['users']` accesses with
34
- `Database['users']['row']`, `Database['users']['insert']`, and `Database['users']['update']`.
35
- Tenon owns generation and uses the configured driver's codec representations. If code imports
36
- `generateDefinitions` from pgclear, import it from Tenon and use Tenon's options instead. Review
37
- the generated diff and exclude exact generator output from formatting. Generated helper types
38
- now import from Tenon; applications do not need a separate Pgclear dependency for them.
39
- 5. Run the project's typecheck and database tests for queries, transactions, JSON/arrays, and native
40
- integrations. Add optional source checking where composition is recoverable; optional patches
41
- and runtime-dependent SQL may remain unchecked. See [validation tools](reference.md#choosing-a-validation-tool).
42
-
43
- If other code still uses pg directly, retain it until those callers are migrated. Updating Tenon
44
- does not require transferring migration ownership or adopting a new baseline.
3
+ You can replace application queries without changing migration ownership. Tenon queries and type
4
+ generation work with an existing schema and any migration runner. For a Tenon upgrade, read the
5
+ [shipped changelog](../CHANGELOG.md) from your resolved version through the target version.
45
6
 
46
7
  ## Preserve connections and behavior
47
8
 
48
- Keep the existing database URI, credentials, TLS requirements, schema, and data. Tenon can add
49
- driver settings through pgclear configuration:
9
+ Reuse the existing database URI, credentials, schema, TLS requirements, and data. Pool configuration
10
+ uses pgclear options; timeout names include `Ms`. Configure runtime names with
11
+ `startupParameters: { search_path: 'app,public' }`, or qualify tables in SQL. Tooling `namespace`
12
+ does not configure runtime queries.
50
13
 
51
- ```ts
52
- import { createTenon } from '@kequtech/tenon-orm';
14
+ Before replacing an ORM, identify behavior it currently supplies: IDs/defaults, timestamps,
15
+ validation, hooks, serialization, relationship loading, and transaction boundaries. Preserve needed
16
+ behavior in application code, constraints, or SQL. Tenon has no model lifecycle or implicit loading.
53
17
 
54
- const db = createTenon({
55
- connection: {
56
- connectionString: existingUri,
57
- connectTimeoutMs: 5000,
58
- },
59
- });
60
- ```
61
-
62
- Translate old driver configuration into pgclear `PoolOptions`. Pool timeouts use milliseconds.
63
- Set `startupParameters: { search_path: 'app,public' }` for runtime names. pgclear does not read
64
- PG environment variables, pgpass, or service files; supply connection settings explicitly. Preserve
65
- credentials and verified TLS requirements. CLI `--namespace` does not change runtime queries. See
66
- [connection options](reference.md#installation-and-connection).
67
-
68
- Before conversion, identify behavior supplied by the old ORM: IDs/defaults, timestamps, validation,
69
- hooks, serialization, relationships, and transaction boundaries. Preserve it explicitly in
70
- application code or SQL where required.
18
+ Use [shared configuration](configuration.md) to reuse connection settings and codecs across runtime,
19
+ generation, checking, inspection, and migrations. Keep credentials supplied by the deployment.
71
20
 
72
21
  ## Replace queries
73
22
 
74
- 1. Optionally generate persistence types from the existing schema:
75
-
76
- ```sh
77
- npx tenon generate --namespace public --out src/generated-schema.d.ts
78
- ```
79
-
80
- This needs no Tenon migration history. Use the application's namespace and commit exact output.
81
-
82
- 2. Replace queries with parameterized SQL in ordinary functions accepting `QueryClient` where
83
- callers may supply `db` or `tx`. Retain projections, ordering, null handling, and authorization.
84
- 3. Review value conversions against [Tenon's mappings](reference.md#generated-types-and-checking).
85
- Pgclear defaults read int8/bigserial and SQL count(*) as exact strings; command rowCount is a
86
- number. Bigint, Date, and supported Temporal inputs use server inference. Numeric values,
87
- dates, arrays, and JSON serialization may also differ from the previous driver. Regenerate
88
- declarations when mappings change; they do not replace domain validation.
89
- 4. Run the optional [source-aware checker](query-checking.md) for recoverable queries, inspect
90
- meaningful completed query variants, and run database assertions for actual rows, permissions,
91
- write effects, and rollback. See [query inspection](query-inspection.md).
92
- 5. Remove unused runtime dependencies and generated code after their callers are gone. Dependencies
93
- retained by other application code or installed transitively may still be needed.
94
-
95
- Check cardinality when replacing a first-row accessor. `oneOrNone` rejects multiple rows and `one`
96
- also rejects zero rows. Use deterministic `ORDER BY` plus `LIMIT 1` when choosing one of several
97
- matches is intended; modes do not add a limit.
98
-
99
- Use `sql.insertValues` and `sql.assignments` to replace object-to-SQL utilities, checking omission,
100
- default, null, and empty-input semantics. Generated `Database[table]['insert']`/`Database[table]['update']` types can replace
101
- handwritten persistence inputs. See the [mutation cookbook](mutations.md).
102
-
103
- Use `tx.query` for work that must commit together. Native pool calls do not join Tenon transactions.
104
- For driver features, preserve resource ownership and recovery policy under the
105
- [native access contract](reference.md#native-driver-access).
23
+ 1. Optionally run `tenon generate` with the application's shared configuration.
24
+ Use the application's schema and codec registry; no Tenon migration history is required.
25
+ 2. Replace one workflow at a time with parameterized SQL in ordinary functions. Accept
26
+ `QueryClient` when callers may use either `db` or `tx`. Preserve selected columns, ordering,
27
+ null handling, authorization predicates, and transaction boundaries.
28
+ 3. Review input/output mappings and validate domain values at the persistence boundary. Generated
29
+ types do not validate JSON members or application constraints.
30
+ 4. Test actual rows, writes, permissions, and rollback. Use source checking and inspection where
31
+ useful. See [choosing a validation tool](reference.md#choosing-a-validation-tool).
32
+ 5. Remove unused dependencies/generated code once all their callers are gone.
33
+
34
+ When replacing a first-row accessor, decide whether multiple matches should be an error.
35
+ `oneOrNone` rejects multiple rows and `one` also rejects absence. To choose one match deliberately,
36
+ author deterministic `ORDER BY ... LIMIT 1`; result modes do not add a limit.
37
+
38
+ For object-to-SQL utilities, review omission, defaults, null, and empty patches against
39
+ [the write guide](mutations.md). Use `tx.query` for every operation that must commit together;
40
+ native pool calls do not join Tenon transactions.
106
41
 
107
42
  ## Transfer migration ownership
108
43
 
109
- Choose one final checkpoint for the old runner. Bring existing databases to it and disable that
110
- runner before handover; the runners do not share locks. Capture from a trusted reference database,
111
- preferably a disposable replay of the legacy migrations.
44
+ Choose the old runner's final schema checkpoint. Bring every existing database to it and disable
45
+ that runner before handover; the runners do not share locks. Capture one baseline from a trusted
46
+ reference database, preferably a disposable replay of the old migrations.
112
47
 
113
48
  ### Capture a baseline
114
49
 
115
50
  For an application owning `app` in a shared database:
116
51
 
117
52
  ```sh
118
- npx tenon baseline capture --directory migrations --namespace app --schema app
53
+ npx tenon baseline capture --directory migrations --namespace app --schema app --connection "$DATABASE_URL"
119
54
  ```
120
55
 
56
+ This example supplies the native libpq URI for the same database through `DATABASE_URL`.
57
+ The native connection can instead be stored in `baseline.connection`, and the captured schemas in
58
+ `baseline.schemas`. A string main connection can be used directly. Preserve required native authentication and TLS settings.
59
+
121
60
  The directory must contain no SQL files. Capture uses native
122
- `pg_dump --schema-only --no-owner --no-privileges` and writes `0001_baseline.up.sql` with no down
123
- file. Install compatible PostgreSQL client tools on PATH; prefer the server's major version.
61
+ `pg_dump --schema-only --no-owner --no-privileges` and writes `0001_baseline.up.sql` without a down
62
+ file. Install compatible PostgreSQL client tools on PATH, preferably matching the server's major
63
+ version. Capture does not modify source data or history and does not preserve sequence positions.
124
64
 
125
- | Option | Scope |
126
- | --------------------- | ------------------------------------------------------------------------------------ |
127
- | `--namespace app` | Tenon history table to exclude from capture; history/runner scope for later commands |
128
- | `--schema app` | Objects selected by native `pg_dump`; repeat to select additional schemas |
129
- | Runtime `search_path` | Configured independently on the application connection |
65
+ | Setting | What it selects |
66
+ | --------------------- | ----------------------------------------------------------------------- |
67
+ | `--namespace app` | Tenon history table to exclude; history/runner scope for later commands |
68
+ | `--schema app` | Schema objects included by pg_dump; repeat for more schemas |
69
+ | Runtime `search_path` | Application name resolution, configured separately |
130
70
 
131
- Omitting `--schema` captures the whole database schema, even with a custom namespace. Capture does
132
- not modify source data/history or preserve sequence positions. It excludes Tenon's selected history
133
- table but can retain the old runner's metadata tables. Review and commit the SQL. Preserve the
134
- capture header, which opts this reviewed dump into native baseline parsing; do not add it to
135
- handwritten migrations.
71
+ Omitting `--schema` captures the whole database schema, even with a custom namespace. The selected
72
+ Tenon history table is excluded; old-runner metadata tables may remain. Review and commit the SQL.
73
+ Preserve the capture header: it opts this reviewed dump into native baseline parsing. Do not add
74
+ that header to handwritten migrations.
136
75
 
137
- A scoped dump can refer to objects it does not include. Provision external schemas, functions,
138
- extensions, and referenced roles before they are needed. A fresh replay must execute representative
139
- operations, not just reproduce generated relation types. Tenon requires no particular extension.
76
+ A scoped dump may depend on objects outside its scope. Provision required external schemas,
77
+ extensions, functions, and roles. Fresh replay must exercise representative application operations,
78
+ not merely reproduce table types. Tenon itself requires no particular extension.
140
79
 
141
80
  ### Check and adopt an existing database
142
81
 
143
- Verify the final legacy checkpoint before adoption. If using schema comparisons, freeze separate
144
- reference definitions rather than comparing against types regenerated after later migrations.
145
- `generateDefinitions` covers relation metadata, not functions or privileges; include reviewed
146
- catalog checks for other objects the application needs. Tenon does not perform these comparisons.
147
-
148
- Generated declaration text is not a version-stable schema fingerprint: driver mappings and generator
149
- changes can alter it without schema drift. The schema-summary comments expose SQL definitions, but
150
- their formatting and PostgreSQL's rendering of those definitions are not a stable comparison format
151
- either. If an existing adoption guard hashes generated text, preserve its original reference and
152
- freeze a separate reference for each supported generator version from a disposable replay of the
153
- immutable checkpoint. Compare the replay's metadata with the reviewed checkpoint before accepting
154
- the new reference; never derive a replacement from whichever live database is configured. Keep
155
- independent function and privilege checks, and test both matching adoption and refusal after drift.
156
- This application guard is separate from Tenon's migration checksums; keep applied SQL unchanged.
82
+ Confirm that each existing database matches the reviewed final checkpoint. Tenon does not compare
83
+ the live schema with the baseline. Adoption records its checksum without executing its SQL:
157
84
 
158
85
  ```sh
159
86
  npx tenon baseline adopt --directory migrations --namespace app
160
87
  npx tenon migrate --directory migrations --namespace app
161
88
  ```
162
89
 
163
- Adoption records the baseline checksum without executing its SQL. The namespace must already exist.
164
- Repeating matching adoption is harmless, including after later migrations; history conflicts fail.
165
- Once Tenon owns history, add forward migrations rather than editing the baseline.
90
+ The namespace must already exist. Repeating adoption with matching history is harmless, including
91
+ after later migrations; conflicting history fails. Once adopted, leave the baseline unchanged and
92
+ add forward migrations.
93
+
94
+ For an application-owned schema guard, keep a frozen checkpoint reference. Generated relation types
95
+ do not cover functions or privileges; check required objects separately. Neither generated text nor
96
+ schema-summary formatting is a version-stable fingerprint. If a guard hashes that text, preserve
97
+ its original reference and freeze a separate reviewed reference for each generator version, taken
98
+ from a disposable replay of the immutable checkpoint. Verify that replay against the checkpoint
99
+ before accepting the reference; do not derive it from whichever live database is configured.
100
+ Test both matching adoption and refusal after drift. This is separate from migration checksums.
166
101
 
167
102
  ### Replay a fresh database
168
103
 
169
- Run `tenon migrate --directory migrations --namespace app`, without adoption. It executes the same
170
- committed baseline and subsequent migrations. Apply the deployment's required roles, ownership, and
171
- grants before application access.
172
-
173
- The portable baseline omits ownership, grants/revokes, and default privileges. Objects are recreated
174
- under the migration role. For `SECURITY DEFINER` functions, that owner controls execution authority,
175
- and new functions ordinarily permit `PUBLIC` execution. Keep fixed policy such as revoking public
176
- execution in reviewed baseline SQL before first adoption or in a forward migration. Deployment
177
- provisioning supplies named roles, owners, and grants. Verify permitted and denied operations on
178
- both adopted and fresh environments. See the
179
- [pg_dump contract](https://www.postgresql.org/docs/16/app-pgdump.html).
180
-
181
- For development, generate types after migrating; production runs migrations and consumes committed
182
- types. Test existing-row preservation, repeatable adoption, subsequent migrations, and fresh replay.
183
- A schema-only baseline and down migration are not backups. The missing down file prevents rollback
184
- across the baseline; use the application's backup/recovery procedure for that boundary.
185
-
186
- Programmatic capture is
187
- `captureBaseline({ connection, directory?, namespace?, schemas?, onWarning? })` and returns
188
- `{ id, upPath }`. It requires a native libpq connection string; an explicitly empty `schemas` list
189
- fails. `adoptBaseline({ connection, directory?, namespace? })` accepts Tenon's runtime connection
190
- forms and returns newly recorded IDs.
104
+ ```sh
105
+ npx tenon migrate --directory migrations --namespace app
106
+ ```
107
+
108
+ Do not adopt a fresh database. Migration executes the committed baseline and subsequent files.
109
+ The dump omits ownership, grants/revokes, and default privileges; objects are recreated under the
110
+ migration role. Deployment provisioning must supply required roles, owners, and grants.
111
+
112
+ For `SECURITY DEFINER` functions, the recreated owner controls authority. PostgreSQL ordinarily grants
113
+ `PUBLIC` execution on new functions. Put fixed policy such as revoking public execution in reviewed
114
+ baseline SQL before first adoption, or in a forward migration. Supply deployment-specific role names
115
+ and grants during provisioning. Verify permitted and denied operations on adopted and fresh databases.
116
+
117
+ Test preserved existing rows, repeatable adoption, later migrations, and fresh replay. A schema-only
118
+ baseline is not a backup; the missing down file prevents rollback across it. Use the application's
119
+ backup/recovery procedure for that boundary.
120
+
121
+ ### Baseline API
122
+
123
+ - `captureBaseline({ connection, directory?, namespace?, schemas?, onWarning? })` returns
124
+ `{ id, upPath }`. Connection is a native libpq connection string passed to pg_dump. Omitting
125
+ `schemas` captures the whole database schema; an empty list rejects. Warnings default to
126
+ `console.warn`.
127
+ - `adoptBaseline({ connection, directory?, namespace? })` accepts Tenon's runtime connection forms
128
+ and returns newly recorded IDs. It requires `0001_baseline.up.sql` with no down file.
129
+
130
+ Defaults are directory `migrations` and namespace `public`. See [migrations](migrations.md) for
131
+ history validation, per-file transactions, and locking.
@@ -1,55 +1,20 @@
1
- # Tenon agent guide
1
+ # Agent guide
2
2
 
3
- Tenon executes explicit PostgreSQL SQL through pgclear. 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.
3
+ Use this as a short workflow for changing an existing Tenon application. For first setup, follow
4
+ [Getting started](human-guide.md). Exact contracts are in the [reference](reference.md).
6
5
 
7
- ## Start with the existing project
6
+ ## Read the project first
8
7
 
9
- Read this guide after the README; consult the [reference](reference.md) for specific contracts.
10
- For a first setup, follow the [human guide](human-guide.md). Prefer documentation from the installed
11
- package when the repository's main branch describes a newer release.
8
+ Inspect `tenon.config.ts` (or its JavaScript equivalent), the database module, nearby query
9
+ functions, migration runner, generated types, and check commands. Reuse the application's package manager, connection configuration, namespace, and
10
+ conventions. Read the docs shipped with the installed Tenon version.
12
11
 
13
- Inspect its connection configuration, migration runner, generated types, query functions, and check
14
- commands. Preserve those conventions. Tenon queries and type generation do not require Tenon
15
- migration history. Keep input validation, authorization, domain conversion, and retry policy in the
16
- application.
12
+ For an upgrade, find the previous resolved version in the lockfile and read the
13
+ [changelog](../CHANGELOG.md) through the target version and follow its API and driver upgrade
14
+ instructions. Check both Tenon and pgclear versions before changing native
15
+ calls or representations.
17
16
 
18
- Start from the existing database module and a nearby query function. Reuse their connection,
19
- generated-schema imports, namespace, and `QueryClient` convention. Check both resolved Tenon and
20
- pgclear versions before changing driver calls or value representations.
21
-
22
- For an upgrade, record the previous resolved Tenon version from the lockfile or installed package
23
- and read the [Changelog](../CHANGELOG.md) entries through the target version before changing code.
24
- It ships with the package alongside this guide. Follow its upgrade instructions, regenerate types
25
- when mappings change, and run the project's typechecks and relevant database tests. Use the
26
- [adoption guide](adoption-guide.md) when replacing another ORM or transferring migration ownership.
27
-
28
- ## Connect
29
-
30
- Requires Node.js 24+, PostgreSQL 16+, ESM, TypeScript 6+, and Node development typings. Install with
31
- `npm install @kequtech/tenon-orm`; retain the project's package manager and TypeScript
32
- configuration. Tenon includes pgclear 0.3.x as a runtime dependency. Import `sql`, `json`, `param`,
33
- and codec helpers from Tenon. See [driver updates](reference.md#driver-updates) for independent
34
- compatible updates; regenerate changed types and rerun database checks.
35
-
36
- ```ts
37
- import { createTenon } from '@kequtech/tenon-orm';
38
-
39
- const connection = process.env.DATABASE_URL;
40
- if (!connection) throw new Error('DATABASE_URL is required');
41
- export const db = createTenon({ connection });
42
- ```
43
-
44
- `connection` accepts a URI string, pgclear `PoolOptions` (including `connectionString`), or an existing
45
- pgclear `Pool`. Reuse an instance for each database configuration. `db.close()` ends internally created
46
- pools with a default 5,000 ms grace period; supplied pools remain application-owned. Configure
47
- shutdown with `db.close({ timeoutMs })`; concurrent callers share its outcome. Finish outstanding
48
- work before closing. Tenon loads no environment files. Qualify table names or set runtime
49
- `search_path`; CLI `--namespace` selects tooling scope, not runtime query names. See
50
- [connection options](reference.md#installation-and-connection).
51
-
52
- ## Query and result contracts
17
+ ## Write a query function
53
18
 
54
19
  ```ts
55
20
  import { sql, type QueryClient } from '@kequtech/tenon-orm';
@@ -57,139 +22,91 @@ import type { Database } from './generated-schema.js';
57
22
 
58
23
  type Summary = Pick<Database['users']['row'], 'id' | 'email'>;
59
24
 
60
- function userById(client: QueryClient, { id }: { id: number }) {
25
+ export function userById(client: QueryClient, { id }: { id: number }) {
61
26
  return client.query(sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`, {
62
27
  returns: 'oneOrNone',
63
28
  });
64
29
  }
65
30
  ```
66
31
 
67
- `QueryClient` accepts either `db` or a transaction's `tx`. Keep SQL and execution options together.
68
- `sql<Row>` declares one selected row; it does not verify SQL or validate data. Unannotated fields
69
- are `unknown`. Generated table types can describe full rows or `Pick` projections; joins and
70
- aggregates need types for their actual output.
71
-
72
- | `returns` | Result | Required returned rows |
73
- | ------------------ | -------------------------------------------------- | ---------------------- |
74
- | `native` (default) | Driver object with `.rows`, `.rowCount`, `.fields` | Any |
75
- | `many` | `readonly Row[]` | Any |
76
- | `oneOrNone` | `Row \| undefined` | Zero or one |
77
- | `one` | `Row` | Exactly one |
78
-
79
- Options are `{ returns?, affectedRows?, label? }`; unknown keys fail. Modes do not add SQL limits or
80
- `RETURNING`. A returned-row mismatch throws `CardinalityError` with code
81
- `TENON_CARDINALITY_MISMATCH`; it alone does not undo an autocommitted write.
82
-
83
- ## Parameters and composition
84
-
85
- - Interpolate values without quotes. Do not author `$n` placeholders inside templates. `null` binds
86
- SQL NULL. Plain bigint, Date, and supported native/polyfilled Temporal values encode as text with
87
- server inference; Date uses UTC. Undefined, plain objects, and arrays require omission or explicit encoding.
88
- - Use pgclear `param(ids, builtin.int4Array)` with `ANY` for a set of IDs. Cast standalone parameters such as
89
- `SELECT ${value}::integer AS value` when SQL must establish the type for inspection.
90
- - `identifier(schema, table)` quotes separate approved names. Strings such as `'DESC'` remain bound
91
- values; choose SQL syntax through fixed query alternatives or fragments.
92
- - `sql.fragment` creates partial SQL with bound values; `sql.join(parts, 'AND' | 'OR' | ',')`
93
- combines a nonempty list. Blank fragments fail. Group mixed conditions with parentheses.
94
- - Full `sql` or `unsafeSql` queries cannot be nested. `unsafeSql<Row>(text, values)` is for trusted
95
- external SQL with an explicit values array. Each Tenon query executes one statement.
96
-
97
- Use fragments for actual reuse or variable composition. Keep fixed structure in SQL, and retain
98
- mandatory authorization predicates when optional filters are absent.
99
-
100
- ## Writes and generated values
101
-
102
- `sql.insertValues(rowOrRows, columns?)` builds column/value fragments;
103
- `sql.assignments(patch, columns?)` builds assignments. Missing/undefined insert fields use defaults;
104
- update fields are omitted. Explicit null stays null. Empty batches and empty assignments fail.
105
- Selected columns ignore unlisted fields and can discard required inputs. Put expressions, conflict
106
- policy, and `RETURNING` in the surrounding SQL. Batch in the caller below the query-wide
107
- 65,535-parameter limit.
108
-
109
- Use generated `Database['users']['insert']` and `Database['users']['update']` inputs; narrow editable fields with
110
- `Pick`. These types account for defaults, writable columns, domains, and driver representations.
111
- They do not validate constraints or JSON contents. See [mutation examples](mutations.md).
112
-
113
- Use pgclear `json(value)` or encoded text for JSON, and `param(value, codec)` for arrays and
114
- custom encodings. Bigint scalars and SQL `count(*)` read as exact strings; command `rowCount` uses
115
- numbers. Plain bigint, Date, and supported Temporal inputs do not change those output defaults.
116
- Choose explicit codecs for bigint, Date, or Temporal outputs. Decimals and temporal values otherwise
117
- read as strings. Numeric and bigint arrays preserve exact strings. Bytes use Uint8Array.
118
- SQL NULL array elements remain null. Tenon generates declarations from the schema and selected
119
- registry; a custom decoder can declare `text.typescript` so generation and checking agree without
120
- duplicating its type in `readTypes`. Keep domain validation at the persistence boundary. See
121
- [value mappings](reference.md#generated-types-and-checking) and the
122
- [JSON/temporal projection](reference.md#persistence-and-domain-values).
123
-
124
- ## Transactions and native access
125
-
126
- For an existing user with id 42:
127
-
128
- ```ts
129
- await db.transaction(async (tx) => {
130
- await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${42}`, {
131
- affectedRows: { exact: 1 },
132
- });
133
- });
134
- ```
135
-
136
- Use `tx` for every operation in the callback and await the work. Tenon commits on success and rolls
137
- back on an escaping error; the callback client expires when the callback finishes. Direct
138
- BEGIN/COMMIT/ROLLBACK boundaries, nested callback transactions, and automatic retries are not
139
- provided. Transactions belong to one database instance.
140
-
141
- `affectedRows: { exact: n }` or `{ max: n }` checks the final INSERT/UPDATE/DELETE/MERGE count.
142
- Bounds are nonnegative safe integers. A guarded `db.query` uses a transaction; a guarded `tx.query`
143
- uses a savepoint. Rejection rolls back that operation. A caught guarded failure permits continued
144
- work; an unguarded SQL error normally aborts the transaction. Counts exclude extra trigger/cascade
145
- and CTE effects. pgclear owns savepoint recovery. A lost commit
146
- acknowledgment can leave the outcome unknown.
147
-
148
- `db.native` exposes the existing pool for driver features. Native calls bypass Tenon's guards and
149
- `onQuery`; they do not join a Tenon transaction. Close owned pools through `db.close()`. Follow the
150
- [native access contract](reference.md#native-driver-access) for resource ownership and LISTEN.
151
- Optional `onQuery` reports completion metadata without SQL or values; success inside a callback does
152
- not establish that the outer transaction committed.
153
-
154
- ## Inspect and validate
155
-
156
- Choose the tool for the task: `tenon check` compares source contracts, `tenon describe` reports
157
- finished SQL metadata, and `tenon generate --check` checks generated-file freshness. None replaces
158
- TypeScript or database tests. See [validation tools](reference.md#choosing-a-validation-tool).
159
-
160
- Run tenon check on the TypeScript project, a file, or file:line. It recovers authored SQL and types
161
- without importing application code, then uses pgclear Describe. Mismatches, unchecked queries and
162
- zero query sites fail; unproven properties do not. Trust declared types when metadata is inconclusive.
163
-
164
- For checking, object expansions need an explicit finite shape type and required, defined selected
165
- properties. Prefer explicit column lists. Fixed fragments, const aliases, fixed joins and finite
166
- ternaries are supported within 16 variants per query. Dynamic fragments, optional-field SQL
167
- expansion and unknown batch lengths remain unchecked. See [query checking](query-checking.md).
168
-
169
- A generated update type usually has optional properties. Even with an explicit column list, those
170
- properties can change the SQL by omission, so the checker may report an ordinary patch as unchecked.
171
- Keep that runtime behavior when intended. If selected fields are always supplied, validate/narrow
172
- the input into an explicit required shape. Do not use casts just to suppress a finding. See
173
- [responding to findings](query-checking.md#responding-to-findings) and
32
+ Use a `QueryClient` parameter when callers may pass `db` or `tx`. Keep SQL and execution options
33
+ together. Qualify table names or follow the configured runtime search path.
34
+
35
+ - Bind values without quotes. Do not mix manual `$n` placeholders into templates.
36
+ - Declare the actual projection with `sql<Row>`. Generated table rows do not describe arbitrary
37
+ joins/aggregates, and annotations do not validate runtime output.
38
+ - Choose `many`, `oneOrNone`, `one`, or default `native` deliberately. Modes add no SQL limits.
39
+ A cardinality error alone does not undo an autocommitted write.
40
+ - Use approved `identifier(...)` names and fixed fragments for SQL syntax. Keep mandatory
41
+ authorization predicates in every branch.
42
+ - Use fragments for real reuse/composition. Runtime loops work but may remain unchecked by the
43
+ optional checker. See [writing queries](sql.md).
44
+
45
+ ## Preserve representations and validate inputs
46
+
47
+ Bigint/bigserial and `count(*)` read as exact strings by default; command `rowCount` is a number or
48
+ null. Decimals and temporal values default to strings, bytes to Uint8Array, and JSON to unknown.
49
+ Built-in arrays preserve scalar representations and null elements.
50
+
51
+ Plain bigint, valid Date, and supported Temporal inputs are accepted; inputs do not change output
52
+ decoders. Plain arrays/objects require `json`, `param`, or encoded text. Direct undefined rejects.
53
+ JSON encoding still rejects bigint. Import parameter/codec helpers from Tenon.
54
+
55
+ Keep the registry in shared `connection.codecs` so queries and CLI tools agree. Programmatic
56
+ inspection/checking inherit codecs from connection-option values; pass the registry explicitly
57
+ when using a URI. Retain application validation, authorization, domain conversion, and retry
58
+ policy. See [schema types and values](schema-types.md).
59
+
60
+ ## Change data deliberately
61
+
62
+ Use `sql.insertValues` and `sql.assignments` for object fields while keeping table, predicates,
63
+ expressions, conflicts, and `RETURNING` in SQL. Apply generated insert/update types and narrow
64
+ editable fields with `Pick`.
65
+
66
+ Missing/undefined insert fields use defaults; assignment fields are omitted; null remains null.
67
+ Empty batches and assignments reject. Explicit column lists select/order fields and can discard
68
+ required inputs. Batch below the complete query's 65,535-parameter limit. See [writing data](mutations.md).
69
+
70
+ For work that commits together, use `db.transaction` and await every operation through `tx`. A
71
+ callback client expires when it finishes; root/native pool queries do not join it. Tenon owns
72
+ transaction boundaries and provides no automatic retries.
73
+
74
+ Use `affectedRows: { exact: n }` or `{ max: n }` for write bounds. A root guard uses a transaction;
75
+ a callback guard uses a savepoint. A rejected guarded operation rolls back before throwing, so a
76
+ caught guard failure permits continued work. An unguarded server error normally aborts the
77
+ transaction. Counts exclude additional trigger/cascade/CTE effects. A lost commit response can
78
+ leave an unknown outcome. See [transactions](reference.md#transactions-guarded-writes-and-errors).
79
+
80
+ ## Make schema changes
81
+
82
+ Keep an existing migration runner. With Tenon's runner, create a new numbered SQL pair, fill both
83
+ files or omit an irreversible down file, and apply it. Never change applied SQL; checksums include
84
+ both directions. Each file commits separately. Use direct or session pooling for tooling advisory
85
+ locks. See [migrations](migrations.md).
86
+
87
+ After migrations commit, run `tenon generate` with the project's
88
+ [shared configuration](configuration.md). Review the diff and commit migrations with exact
89
+ generated output. Keep handwritten aliases separate and generated files out of formatting. In CI,
90
+ migrate the test database and run `tenon generate --check` without first regenerating. Production
91
+ consumes committed declarations.
92
+
93
+ ## Verify the changed workflow
94
+
95
+ Run the project's typecheck and database tests for actual rows, effects, permissions, and rollback.
96
+ Use `tenon check` for source contracts and `tenon describe` for finished SQL metadata when useful.
97
+ See [validation tools](reference.md#choosing-a-validation-tool).
98
+
99
+ The checker fails on definite mismatches and unchecked sites; unproven properties remain notes.
100
+ Fixed composition is supported within bounded variants. Object expansion needs an explicit finite
101
+ shape with required, defined selected fields; optional generated patches may be unchecked. Keep
102
+ intentional omission behavior and test it. Narrow to required fields only when application validation
103
+ establishes that contract; do not cast merely to silence a diagnostic. See
104
+ [responding to findings](query-checking.md#responding-to-findings).
105
+
106
+ Share runtime connection/codecs through `tenon.config.ts`; CLI generation, checking, inspection,
107
+ and migrations read it. Use `--config` for another role/deployment. Match runtime role, search path,
108
+ timezone, and codecs. Describe uses a separate session and
109
+ cannot establish result nullability, JSON members, cardinality, or execution permissions. Native
110
+ calls bypass Tenon's guards/observations and need explicit resource ownership; see
111
+ [connections and driver access](connections.md). For common failures, use
174
112
  [troubleshooting](reference.md#troubleshooting).
175
-
176
- For metadata alone, use describeQuery or tenon describe query.sql. Match role, search path, timezone,
177
- and custom codecs. Temporary tables and transaction-local state are not inherited. Retain tests for
178
- complete rows, meaningful query variants, authorization, effects, and rollback. Describe does not
179
- prove bound values, JSON members, nullability, cardinality or runtime execution permission.
180
-
181
- ## Schema changes
182
-
183
- With Tenon's runner, development can use
184
- `tenon migrate && tenon generate --out src/generated-schema.d.ts`. Commit migrations and exact
185
- generated output together; exclude the generated file from automatic formatting. Production runs
186
- migrations and deploys committed types. `generate --check` detects representation drift, not pending
187
- migrations or every schema change.
188
-
189
- All database CLI commands require `DATABASE_URL`. `tenon new <name>` creates empty up/down files
190
- offline; fill them before migrating. Migration commands use `--directory migrations` and
191
- `--namespace public` by default; generation also accepts `--namespace`. Pass a custom namespace to
192
- both migration and generation. Omit the down file for an irreversible migration; never edit applied
193
- history. Each file commits independently with its history entry. Tools use session advisory locks,
194
- so use a direct or session-pooled connection. Nontransactional DDL needs a separate procedure. See
195
- the [command reference](reference.md#command-reference).