@kequtech/tenon-orm 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +27 -5
  3. package/dist/affected-rows.js +0 -1
  4. package/dist/baseline.js +3 -5
  5. package/dist/check/contracts.d.ts +6 -0
  6. package/dist/check/contracts.js +206 -0
  7. package/dist/check/project.d.ts +18 -0
  8. package/dist/check/project.js +224 -0
  9. package/dist/check/source.d.ts +51 -0
  10. package/dist/check/source.js +629 -0
  11. package/dist/check/types.d.ts +36 -0
  12. package/dist/check/types.js +1 -0
  13. package/dist/check.d.ts +4 -0
  14. package/dist/check.js +147 -0
  15. package/dist/cli.js +38 -9
  16. package/dist/connection.d.ts +5 -12
  17. package/dist/connection.js +12 -121
  18. package/dist/database.js +5 -8
  19. package/dist/describe.js +10 -3
  20. package/dist/generate.d.ts +2 -3
  21. package/dist/generate.js +26 -24
  22. package/dist/generation/catalog.d.ts +5 -6
  23. package/dist/generation/catalog.js +7 -9
  24. package/dist/generation/render.d.ts +3 -0
  25. package/dist/generation/render.js +60 -0
  26. package/dist/generation/schema-summary.d.ts +3 -3
  27. package/dist/generation/schema-summary.js +2 -2
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +2 -0
  30. package/dist/inspection/catalog.d.ts +3 -6
  31. package/dist/inspection/catalog.js +9 -18
  32. package/dist/inspection/cli.d.ts +12 -0
  33. package/dist/inspection/cli.js +9 -4
  34. package/dist/inspection/connection.d.ts +2 -3
  35. package/dist/inspection/connection.js +20 -22
  36. package/dist/inspection/options.js +0 -6
  37. package/dist/inspection/result.d.ts +3 -2
  38. package/dist/inspection/result.js +13 -26
  39. package/dist/inspection/types.d.ts +6 -4
  40. package/dist/migrations/connection.d.ts +3 -3
  41. package/dist/migrations/connection.js +14 -21
  42. package/dist/migrations/history.d.ts +4 -4
  43. package/dist/migrations/history.js +5 -5
  44. package/dist/migrations.js +7 -9
  45. package/dist/query-options.d.ts +1 -1
  46. package/dist/query.js +1 -1
  47. package/dist/transaction.d.ts +2 -2
  48. package/dist/transaction.js +21 -77
  49. package/dist/types.d.ts +1 -1
  50. package/docs/adoption-guide.md +64 -12
  51. package/docs/agent-guide.md +57 -27
  52. package/docs/human-guide.md +53 -18
  53. package/docs/mutations.md +22 -9
  54. package/docs/query-checking.md +200 -0
  55. package/docs/query-inspection.md +40 -13
  56. package/docs/reference.md +330 -108
  57. package/package.json +11 -7
  58. package/dist/driver/types.d.ts +0 -10
  59. package/dist/driver/types.js +0 -49
  60. package/dist/generation/table-types.d.ts +0 -2
  61. package/dist/generation/table-types.js +0 -96
  62. package/dist/inspection/protocol.d.ts +0 -7
  63. package/dist/inspection/protocol.js +0 -45
@@ -1,81 +1,25 @@
1
- import { executeSql, watchClient } from './connection.js';
2
- /** The callback owns one connection and expires before commit or rollback begins. */
3
- export async function withTransaction(pool, work) {
4
- const client = await pool.connect();
5
- const connection = watchClient(client);
6
- let active = true;
7
- let began = false;
8
- let releaseError;
9
- let savepointId = 0;
10
- let pending = Promise.resolve();
11
- let recoveryFailure;
12
- const query = async (text, input, readResult, guarded) => {
13
- if (!active)
14
- throw new Error('The transaction has finished.');
15
- // Queue whole operations, including savepoint cleanup. Otherwise concurrent
16
- // calls could place another write inside a savepoint that gets rolled back.
17
- const operation = pending.then(async () => {
18
- connection.check();
19
- if (recoveryFailure)
20
- throw recoveryFailure;
21
- if (!guarded)
22
- return readResult(await executeSql(client, text, input));
23
- const savepoint = `tenon_affected_rows_${++savepointId}`;
24
- await client.query(`SAVEPOINT ${savepoint}`);
25
- try {
26
- const result = readResult(await executeSql(client, text, input));
27
- await client.query(`RELEASE SAVEPOINT ${savepoint}`);
28
- return result;
29
- }
30
- catch (error) {
31
- try {
32
- await client.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
33
- await client.query(`RELEASE SAVEPOINT ${savepoint}`);
34
- }
35
- catch (rollbackError) {
36
- // Even if application code catches this error, the outer transaction
37
- // must not commit when the rejected write could still be present.
38
- recoveryFailure = new AggregateError([error, rollbackError], 'Operation and savepoint recovery failed.');
39
- throw recoveryFailure;
40
- }
41
- throw error;
42
- }
43
- });
44
- pending = operation.then(() => { }, () => { });
45
- return operation;
46
- };
47
- try {
48
- await client.query('BEGIN');
49
- began = true;
50
- const result = await work(query);
51
- active = false;
52
- await pending;
53
- connection.check();
54
- if (recoveryFailure)
55
- throw recoveryFailure;
56
- const committed = await client.query('COMMIT');
57
- if (committed.command !== 'COMMIT') {
58
- throw new Error('PostgreSQL rolled back the aborted transaction.');
59
- }
60
- return result;
61
- }
62
- catch (error) {
63
- active = false;
64
- await pending;
65
- if (!began) {
66
- releaseError = error instanceof Error ? error : new Error('BEGIN failed.');
67
- throw error;
68
- }
1
+ import { executeSql } from './connection.js';
2
+ /** Serialize whole Tenon policies; pgclear owns transaction, savepoint and transport recovery. */
3
+ export function withTransaction(pool, work) {
4
+ return pool.transaction(async (transaction) => {
5
+ let active = true;
6
+ let pending = Promise.resolve();
7
+ const query = (text, input, readResult, guarded) => {
8
+ if (!active)
9
+ return Promise.reject(new Error('The transaction has finished.'));
10
+ const operation = pending.then(() => {
11
+ const execute = async (scope) => readResult(await executeSql(scope, text, input));
12
+ return guarded ? transaction.savepoint(execute) : execute(transaction);
13
+ });
14
+ pending = operation.then(() => { }, () => { });
15
+ return operation;
16
+ };
69
17
  try {
70
- await client.query('ROLLBACK');
18
+ return await work(query);
71
19
  }
72
- catch (rollbackError) {
73
- releaseError = rollbackError instanceof Error ? rollbackError : new Error('Rollback failed.');
74
- throw new AggregateError([error, rollbackError], 'Transaction and rollback failed.');
20
+ finally {
21
+ active = false;
22
+ await pending;
75
23
  }
76
- throw error;
77
- }
78
- finally {
79
- connection.release(releaseError);
80
- }
24
+ });
81
25
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Pool } from 'pg';
1
+ import type { Pool } from '@kequtech/pgclear';
2
2
  import type { QueryResult } from './connection.js';
3
3
  import type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
4
4
  import type { SqlQuery } from './query.js';
@@ -4,10 +4,49 @@ Replace application queries independently of migration ownership. Tenon can quer
4
4
  and generate types while the current migration runner stays in place. For a Tenon version upgrade,
5
5
  read the [Changelog](../CHANGELOG.md).
6
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.
45
+
7
46
  ## Preserve connections and behavior
8
47
 
9
48
  Keep the existing database URI, credentials, TLS requirements, schema, and data. Tenon can add
10
- driver settings through pg configuration:
49
+ driver settings through pgclear configuration:
11
50
 
12
51
  ```ts
13
52
  import { createTenon } from '@kequtech/tenon-orm';
@@ -15,15 +54,15 @@ import { createTenon } from '@kequtech/tenon-orm';
15
54
  const db = createTenon({
16
55
  connection: {
17
56
  connectionString: existingUri,
18
- connectionTimeoutMillis: 5000,
57
+ connectTimeoutMs: 5000,
19
58
  },
20
59
  });
21
60
  ```
22
61
 
23
- Translate old driver configuration into pg `PoolConfig` rather than passing it through. Pool
24
- timeouts use milliseconds. Preserve runtime `search_path` through the pg startup options string, for
25
- example `options: '-c search_path=app,public'`. URI startup options take precedence when both are
26
- supplied. CLI `--namespace` does not change runtime queries. See
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
27
66
  [connection options](reference.md#installation-and-connection).
28
67
 
29
68
  Before conversion, identify behavior supplied by the old ORM: IDs/defaults, timestamps, validation,
@@ -42,11 +81,14 @@ application code or SQL where required.
42
81
 
43
82
  2. Replace queries with parameterized SQL in ordinary functions accepting `QueryClient` where
44
83
  callers may supply `db` or `tx`. Retain projections, ordering, null handling, and authorization.
45
- 3. Review value conversions against [Tenon's mappings](reference.md#generated-types-and-checking):
46
- numeric values, dates, arrays, and JSON serialization may differ from the previous driver.
47
- Generated declarations do not replace domain validation.
48
- 4. Inspect meaningful completed query variants and run database assertions for actual rows,
49
- permissions, write effects, and rollback. See [query inspection](query-inspection.md).
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).
50
92
  5. Remove unused runtime dependencies and generated code after their callers are gone. Dependencies
51
93
  retained by other application code or installed transitively may still be needed.
52
94
 
@@ -55,7 +97,7 @@ also rejects zero rows. Use deterministic `ORDER BY` plus `LIMIT 1` when choosin
55
97
  matches is intended; modes do not add a limit.
56
98
 
57
99
  Use `sql.insertValues` and `sql.assignments` to replace object-to-SQL utilities, checking omission,
58
- default, null, and empty-input semantics. Generated `Rows.Insert`/`Rows.Update` types can replace
100
+ default, null, and empty-input semantics. Generated `Database[table]['insert']`/`Database[table]['update']` types can replace
59
101
  handwritten persistence inputs. See the [mutation cookbook](mutations.md).
60
102
 
61
103
  Use `tx.query` for work that must commit together. Native pool calls do not join Tenon transactions.
@@ -103,6 +145,16 @@ reference definitions rather than comparing against types regenerated after late
103
145
  `generateDefinitions` covers relation metadata, not functions or privileges; include reviewed
104
146
  catalog checks for other objects the application needs. Tenon does not perform these comparisons.
105
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.
157
+
106
158
  ```sh
107
159
  npx tenon baseline adopt --directory migrations --namespace app
108
160
  npx tenon migrate --directory migrations --namespace app
@@ -1,16 +1,24 @@
1
1
  # Tenon agent guide
2
2
 
3
- Tenon executes explicit PostgreSQL SQL through pg. Use ordinary functions for reusable queries. This
3
+ Tenon executes explicit PostgreSQL SQL through pgclear. Use ordinary functions for reusable queries. This
4
4
  guide covers routine implementation; the [reference](reference.md) defines the full API and the
5
5
  [human guide](human-guide.md) gives a step-by-step introduction.
6
6
 
7
7
  ## Start with the existing project
8
8
 
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.
12
+
9
13
  Inspect its connection configuration, migration runner, generated types, query functions, and check
10
14
  commands. Preserve those conventions. Tenon queries and type generation do not require Tenon
11
15
  migration history. Keep input validation, authorization, domain conversion, and retry policy in the
12
16
  application.
13
17
 
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
+
14
22
  For an upgrade, record the previous resolved Tenon version from the lockfile or installed package
15
23
  and read the [Changelog](../CHANGELOG.md) entries through the target version before changing code.
16
24
  It ships with the package alongside this guide. Follow its upgrade instructions, regenerate types
@@ -21,7 +29,9 @@ when mappings change, and run the project's typechecks and relevant database tes
21
29
 
22
30
  Requires Node.js 24+, PostgreSQL 16+, ESM, TypeScript 6+, and Node development typings. Install with
23
31
  `npm install @kequtech/tenon-orm`; retain the project's package manager and TypeScript
24
- configuration.
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.
25
35
 
26
36
  ```ts
27
37
  import { createTenon } from '@kequtech/tenon-orm';
@@ -31,8 +41,8 @@ if (!connection) throw new Error('DATABASE_URL is required');
31
41
  export const db = createTenon({ connection });
32
42
  ```
33
43
 
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
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
36
46
  pools with a default 5,000 ms grace period; supplied pools remain application-owned. Configure
37
47
  shutdown with `db.close({ timeoutMs })`; concurrent callers share its outcome. Finish outstanding
38
48
  work before closing. Tenon loads no environment files. Qualify table names or set runtime
@@ -43,9 +53,9 @@ work before closing. Tenon loads no environment files. Qualify table names or se
43
53
 
44
54
  ```ts
45
55
  import { sql, type QueryClient } from '@kequtech/tenon-orm';
46
- import type { Rows } from './generated-schema.js';
56
+ import type { Database } from './generated-schema.js';
47
57
 
48
- type Summary = Pick<Rows['users'], 'id' | 'email'>;
58
+ type Summary = Pick<Database['users']['row'], 'id' | 'email'>;
49
59
 
50
60
  function userById(client: QueryClient, { id }: { id: number }) {
51
61
  return client.query(sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`, {
@@ -62,7 +72,7 @@ aggregates need types for their actual output.
62
72
  | `returns` | Result | Required returned rows |
63
73
  | ------------------ | -------------------------------------------------- | ---------------------- |
64
74
  | `native` (default) | Driver object with `.rows`, `.rowCount`, `.fields` | Any |
65
- | `many` | Plain `Row[]` | Any |
75
+ | `many` | `readonly Row[]` | Any |
66
76
  | `oneOrNone` | `Row \| undefined` | Zero or one |
67
77
  | `one` | `Row` | Exactly one |
68
78
 
@@ -72,9 +82,10 @@ Options are `{ returns?, affectedRows?, label? }`; unknown keys fail. Modes do n
72
82
 
73
83
  ## Parameters and composition
74
84
 
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
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
78
89
  `SELECT ${value}::integer AS value` when SQL must establish the type for inspection.
79
90
  - `identifier(schema, table)` quotes separate approved names. Strings such as `'DESC'` remain bound
80
91
  values; choose SQL syntax through fixed query alternatives or fragments.
@@ -95,15 +106,20 @@ Selected columns ignore unlisted fields and can discard required inputs. Put exp
95
106
  policy, and `RETURNING` in the surrounding SQL. Batch in the caller below the query-wide
96
107
  65,535-parameter limit.
97
108
 
98
- Use generated `Rows.Insert['users']` and `Rows.Update['users']` inputs; narrow editable fields with
109
+ Use generated `Database['users']['insert']` and `Database['users']['update']` inputs; narrow editable fields with
99
110
  `Pick`. These types account for defaults, writable columns, domains, and driver representations.
100
111
  They do not validate constraints or JSON contents. See [mutation examples](mutations.md).
101
112
 
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).
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).
107
123
 
108
124
  ## Transactions and native access
109
125
 
@@ -126,7 +142,7 @@ provided. Transactions belong to one database instance.
126
142
  Bounds are nonnegative safe integers. A guarded `db.query` uses a transaction; a guarded `tx.query`
127
143
  uses a savepoint. Rejection rolls back that operation. A caught guarded failure permits continued
128
144
  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
145
+ and CTE effects. pgclear owns savepoint recovery. A lost commit
130
146
  acknowledgment can leave the outcome unknown.
131
147
 
132
148
  `db.native` exposes the existing pool for driver features. Native calls bypass Tenon's guards and
@@ -137,16 +153,30 @@ not establish that the outer transaction committed.
137
153
 
138
154
  ## Inspect and validate
139
155
 
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).
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
174
+ [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.
150
180
 
151
181
  ## Schema changes
152
182
 
@@ -1,7 +1,7 @@
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 pg. This guide follows a small application from its first table to reusable
4
+ executes it through pgclear. This guide follows a small application from its first table to reusable
5
5
  queries, transactions, and result checking.
6
6
 
7
7
  Use the [reference](reference.md) for exact contracts and [adoption](adoption-guide.md) when
@@ -17,8 +17,10 @@ npm install @kequtech/tenon-orm
17
17
  ```
18
18
 
19
19
  Use PostgreSQL 16+ and TypeScript 6+. Keep your existing strict TypeScript configuration and Node
20
- typings. Supply `DATABASE_URL` through your application's environment setup; Tenon does not load
21
- `.env` files.
20
+ typings. Tenon includes pgclear 0.3.x. Supply `DATABASE_URL` through your application's
21
+ environment setup; Tenon does not load `.env` files. Import `sql`, `json`, `param`, and codec helpers
22
+ from `@kequtech/tenon-orm` for application queries. See [driver updates](reference.md#driver-updates)
23
+ when updating Pgclear independently.
22
24
 
23
25
  ```ts
24
26
  // src/db.ts
@@ -30,7 +32,7 @@ export const db = createTenon({ connection });
30
32
  ```
31
33
 
32
34
  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
+ supply an existing pgclear `Pool`; your application then owns its shutdown.
34
36
 
35
37
  ## Create a table
36
38
 
@@ -102,18 +104,31 @@ throws if the query returns more than one row. Use `'one'` when absence should a
102
104
  `'many'` for an array, or omit `returns` for the native result object with `.rows`, `.rowCount`, and
103
105
  `.fields`. These options do not change your SQL.
104
106
 
107
+ `rowCount` is a number or null when the command has no count. SQL `count(*)` and bigint/bigserial
108
+ column values are exact strings, including values beyond JavaScript's safe integer range. Use
109
+ strings, bigint, or pgclear `param(text, builtin.int8)` for int8 parameters. Plain Date and supported
110
+ Temporal inputs also work; they do not change the default string outputs. Date inputs encode in UTC.
111
+ Opt-in codecs can return Date, bigint, or Temporal values when the application needs them.
112
+
105
113
  Generated table types can help describe projections without repeating every field:
106
114
 
107
115
  ```ts
108
- import type { Rows } from './generated-schema.js';
109
- type UserSummary = Pick<Rows['users'], 'id' | 'email'>;
116
+ import type { Database } from './generated-schema.js';
117
+ type UserSummary = Pick<Database['users']['row'], 'id' | 'email'>;
110
118
  ```
111
119
 
120
+ For frequently used types, define [application aliases](reference.md#application-aliases) such as
121
+ `UserRow`, `UserInsert`, and `UserUpdate` in a handwritten module.
122
+
112
123
  You can then write `sql<UserSummary>`. A table type describes the full table; a join or aggregate
113
124
  needs a type for what that particular query selects. An annotation is a declaration, not proof that
114
125
  the SQL matches. Without one, result fields are `unknown`. Use the inspection and database-test
115
126
  workflow below to check the declared contract against the query's behavior.
116
127
 
128
+ Keep storage types conservative and validate them when projecting domain values. The
129
+ [persistence-to-domain example](reference.md#persistence-and-domain-values) shows validated JSON
130
+ writes and reads, temporal strings, and an SQL predicate that rejects infinite session expiry.
131
+
117
132
  ## Reuse a query with a function
118
133
 
119
134
  Use an ordinary function when several callers need the same query. An object input makes the
@@ -121,9 +136,9 @@ supplied values easy to read:
121
136
 
122
137
  ```ts
123
138
  import { sql, type QueryClient } from '@kequtech/tenon-orm';
124
- import type { Rows } from './generated-schema.js';
139
+ import type { Database } from './generated-schema.js';
125
140
 
126
- type UserSummary = Pick<Rows['users'], 'id' | 'email'>;
141
+ type UserSummary = Pick<Database['users']['row'], 'id' | 'email'>;
127
142
 
128
143
  export function userByEmail(client: QueryClient, { email }: { email: string }) {
129
144
  return client.query(sql<UserSummary>`SELECT id, email FROM public.users WHERE email = ${email}`, {
@@ -167,12 +182,12 @@ Explicit `null` stays null. Empty batches and updates throw. Heterogeneous inser
167
182
  aligned, with `DEFAULT` for missing cells.
168
183
 
169
184
  ```ts
170
- import type { Rows } from './generated-schema.js';
185
+ import type { Database } from './generated-schema.js';
171
186
 
172
- const input: Rows.Insert['users'] = { email: 'another@example.test' };
187
+ const input: Database['users']['insert'] = { email: 'another@example.test' };
173
188
  await db.query(sql`INSERT INTO public.users ${sql.insertValues(input, ['email'])}`);
174
189
 
175
- const patch: Pick<Rows.Update['users'], 'active'> = { active: false };
190
+ const patch: Pick<Database['users']['update'], 'active'> = { active: false };
176
191
  await db.query(sql`UPDATE public.users SET ${sql.assignments(patch, ['active'])}
177
192
  WHERE email = ${input.email}`);
178
193
  ```
@@ -184,14 +199,31 @@ Use `Pick` to limit editable fields. Keep expressions, predicates, conflict hand
184
199
 
185
200
  ## Check your SQL and result types
186
201
 
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.
202
+ The three database tools answer different questions: `tenon check` compares authored queries with
203
+ their TypeScript contracts, `tenon describe` reports metadata for finished SQL, and
204
+ `tenon generate --check` checks whether saved database declarations are current. See
205
+ [validation tools](reference.md#choosing-a-validation-tool) for their limits.
206
+
207
+ After migrating a development/test database, run the optional source-aware checker:
208
+
209
+ ```sh
210
+ npx tenon check src/queries.ts
211
+ npx tenon check src/queries.ts:23
212
+ ```
213
+
214
+ A line selects one complete query. Omit the file to check the TypeScript project. The checker reads
215
+ source and types without importing the application or executing SQL. It reports definite
216
+ contradictions and unsupported composition; nullability, JSON members, and other inconclusive
217
+ contracts receive the benefit of the declared type and remain unproven. See
218
+ [query checking](query-checking.md) for supported fragments and explicit object expansion shapes.
191
219
 
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.
220
+ Use describeQuery on finished SQL, or tenon describe query.sql, when you only need metadata and a
221
+ conservative row declaration. Match runtime role/session settings. Retain TypeScript and database
222
+ tests for actual rows, effects, permissions, and rollback. The checker does not replace those tests.
223
+ Generated patch types contain optional fields and can therefore be unchecked even when execution is
224
+ valid. Keep optional patches when needed; see [responding to findings](query-checking.md#responding-to-findings).
225
+ For rejected values, result-count errors, and other common failures, see
226
+ [troubleshooting](reference.md#troubleshooting).
195
227
 
196
228
  ## Reuse or assemble parts of SQL when needed
197
229
 
@@ -211,6 +243,7 @@ const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
211
243
 
212
244
  Execute each query with `db.query`. Validate application-supplied patterns and page sizes before
213
245
  building queries. The complete query supplies the result type.
246
+ The SQL aggregate's `count` is a string; the native result's `rowCount` counts returned rows.
214
247
 
215
248
  Use `sql.join` when the number of fragments varies, such as one search condition per word or a set
216
249
  of optional filters. It combines fragments with `'AND'`, `'OR'`, or `','` and preserves their bound
@@ -244,3 +277,5 @@ when mixing `AND` and `OR`. See the [composition reference](reference.md#sql-fra
244
277
 
245
278
  Use fixed SQL when composition is unnecessary. See the [reference](reference.md) for arrays,
246
279
  identifiers, value mappings, CLI options, and error behavior.
280
+ Runtime loops and dynamic fragment lists work during execution but remain unchecked by `tenon check`;
281
+ use database tests for those queries.
package/docs/mutations.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Use `sql.insertValues` and `sql.assignments` for object fields while keeping SQL structure visible.
4
4
  The [reference](reference.md#object-based-mutation-fragments) defines the complete contract.
5
- Examples assume an existing `db` and this table, with generated `Rows` from the same schema:
5
+ Examples assume an existing `db` and this table, with generated `Database` from the same schema:
6
6
 
7
7
  ```sql
8
8
  CREATE TABLE profiles (
@@ -34,9 +34,9 @@ aligned across heterogeneous rows. Empty batches fail; a single default-only obj
34
34
 
35
35
  ```ts
36
36
  import { sql, type QueryClient } from '@kequtech/tenon-orm';
37
- import type { Rows } from './generated-schema.js';
37
+ import type { Database } from './generated-schema.js';
38
38
 
39
- type ProfileInput = Pick<Rows.Insert['profiles'], 'name' | 'score'>;
39
+ type ProfileInput = Pick<Database['profiles']['insert'], 'name' | 'score'>;
40
40
 
41
41
  function createProfile(client: QueryClient, input: ProfileInput) {
42
42
  return client.query(
@@ -55,7 +55,7 @@ Selected columns choose and order fields, ignoring unlisted keys. They can disca
55
55
  even when the original object is valid:
56
56
 
57
57
  ```ts
58
- const input: Rows.Insert['profiles'] = { name: 'Ada' };
58
+ const input: Database['profiles']['insert'] = { name: 'Ada' };
59
59
  const query = sql`INSERT INTO profiles ${sql.insertValues(input, ['bio', 'score'])}`;
60
60
  // Executing this fails: name was discarded and has no default.
61
61
  ```
@@ -63,7 +63,7 @@ const query = sql`INSERT INTO profiles ${sql.insertValues(input, ['bio', 'score'
63
63
  ## Patches mixed with expressions
64
64
 
65
65
  ```ts
66
- const patch: Pick<Rows.Update['profiles'], 'name' | 'bio'> = { bio: null };
66
+ const patch: Pick<Database['profiles']['update'], 'name' | 'bio'> = { bio: null };
67
67
  await db.query(sql`UPDATE profiles SET ${sql.assignments(patch)}, revision = revision + 1
68
68
  WHERE id = ${id}`);
69
69
  ```
@@ -106,15 +106,28 @@ For a table with `payload jsonb` and `tags text[]`, encode JSON separately from
106
106
  array:
107
107
 
108
108
  ```ts
109
+ import { builtin, json, param } from '@kequtech/tenon-orm';
110
+
109
111
  const tags = ['ada', 'lin'];
110
112
  await db.query(
111
- sql`INSERT INTO deliveries ${sql.insertValues({ payload: JSON.stringify(tags), tags })}`,
113
+ sql`INSERT INTO deliveries ${sql.insertValues({ payload: json(tags), tags: param(tags, builtin.textArray) })}`,
112
114
  );
113
115
  ```
114
116
 
115
- Generated JSON writes accept encoded JSON strings. Use `JSON.stringify(value)` for objects, arrays,
116
- strings, and JSON null; a bound JavaScript null means SQL null. Arrays passed directly are
117
- PostgreSQL arrays. Tenon leaves serialization to pg.
117
+ Generated JSON writes accept pgclear parameters or encoded strings. `json(value)` encodes objects,
118
+ arrays, strings, and JSON null; plain `null` means SQL NULL. Arrays need `param(value, codec)` or
119
+ encoded PostgreSQL text. Plain arrays, objects, and undefined reject as bound values. Tenon passes
120
+ explicit parameters unchanged to pgclear. `json()` still rejects bigint values, including bigint
121
+ nested inside objects; bigint support for ordinary SQL parameters does not change JSON encoding.
122
+
123
+ For bigint/bigserial fields, generated writes accept exact strings, bigint, or explicit parameters;
124
+ reads remain strings by default. Use `param('9007199254740993', builtin.int8)` for an explicit int8
125
+ parameter and `param(['9007199254740993', null], builtin.int8Array)` for an int8 array. The bigint
126
+ output codec and matching array encoder are separate opt-in choices.
127
+
128
+ Date and supported Temporal values can be ordinary scalar inputs, including mutation fields. Date
129
+ encodes in UTC; Temporal calendar dates, wall times, and instants retain their distinct meanings.
130
+ Choose the value appropriate to the PostgreSQL column and retain application validation.
118
131
 
119
132
  ## Batches in one transaction
120
133