@kequtech/tenon-orm 0.2.2 → 0.3.1
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.
- package/CHANGELOG.md +46 -42
- package/README.md +4 -4
- package/dist/affected-rows.js +6 -6
- package/dist/baseline.js +4 -4
- package/dist/client.js +1 -1
- package/dist/connection.d.ts +15 -22
- package/dist/connection.js +122 -25
- package/dist/database.js +10 -17
- package/dist/driver/types.d.ts +1 -1
- package/dist/driver/types.js +11 -11
- package/dist/generate.js +7 -7
- package/dist/generation/catalog.d.ts +2 -2
- package/dist/generation/catalog.js +4 -4
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +11 -11
- package/dist/generation/table-types.js +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/inspection/catalog.d.ts +4 -3
- package/dist/inspection/catalog.js +13 -10
- package/dist/inspection/connection.d.ts +2 -2
- package/dist/inspection/connection.js +22 -22
- package/dist/inspection/protocol.d.ts +7 -0
- package/dist/inspection/protocol.js +45 -0
- package/dist/inspection/result.d.ts +1 -1
- package/dist/inspection/result.js +6 -6
- package/dist/inspection/types.d.ts +2 -2
- package/dist/migrations/connection.d.ts +3 -2
- package/dist/migrations/connection.js +18 -20
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -7
- package/dist/query-options.js +4 -4
- package/dist/query.js +2 -2
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +19 -14
- package/dist/types.d.ts +2 -2
- package/docs/adoption-guide.md +40 -27
- package/docs/agent-guide.md +55 -48
- package/docs/human-guide.md +25 -20
- package/docs/mutations.md +13 -10
- package/docs/query-inspection.md +12 -14
- package/docs/reference.md +304 -131
- package/package.json +3 -2
- package/dist/driver/array.d.ts +0 -5
- package/dist/driver/array.js +0 -70
- package/dist/driver/lifecycle.d.ts +0 -15
- package/dist/driver/lifecycle.js +0 -57
- package/dist/driver/listener.d.ts +0 -3
- package/dist/driver/listener.js +0 -63
- package/dist/driver/session.d.ts +0 -8
- package/dist/driver/session.js +0 -92
package/dist/transaction.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { executeSql } from './connection.js';
|
|
1
|
+
import { executeSql, watchClient } from './connection.js';
|
|
3
2
|
/** The callback owns one connection and expires before commit or rollback begins. */
|
|
4
3
|
export async function withTransaction(pool, work) {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
4
|
+
const client = await pool.connect();
|
|
5
|
+
const connection = watchClient(client);
|
|
7
6
|
let active = true;
|
|
8
|
-
let
|
|
7
|
+
let began = false;
|
|
8
|
+
let releaseError;
|
|
9
9
|
let savepointId = 0;
|
|
10
10
|
let pending = Promise.resolve();
|
|
11
11
|
let recoveryFailure;
|
|
@@ -21,16 +21,16 @@ export async function withTransaction(pool, work) {
|
|
|
21
21
|
if (!guarded)
|
|
22
22
|
return readResult(await executeSql(client, text, input));
|
|
23
23
|
const savepoint = `tenon_affected_rows_${++savepointId}`;
|
|
24
|
-
await client.
|
|
24
|
+
await client.query(`SAVEPOINT ${savepoint}`);
|
|
25
25
|
try {
|
|
26
26
|
const result = readResult(await executeSql(client, text, input));
|
|
27
|
-
await client.
|
|
27
|
+
await client.query(`RELEASE SAVEPOINT ${savepoint}`);
|
|
28
28
|
return result;
|
|
29
29
|
}
|
|
30
30
|
catch (error) {
|
|
31
31
|
try {
|
|
32
|
-
await client.
|
|
33
|
-
await client.
|
|
32
|
+
await client.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
33
|
+
await client.query(`RELEASE SAVEPOINT ${savepoint}`);
|
|
34
34
|
}
|
|
35
35
|
catch (rollbackError) {
|
|
36
36
|
// Even if application code catches this error, the outer transaction
|
|
@@ -45,13 +45,15 @@ export async function withTransaction(pool, work) {
|
|
|
45
45
|
return operation;
|
|
46
46
|
};
|
|
47
47
|
try {
|
|
48
|
+
await client.query('BEGIN');
|
|
49
|
+
began = true;
|
|
48
50
|
const result = await work(query);
|
|
49
51
|
active = false;
|
|
50
52
|
await pending;
|
|
51
53
|
connection.check();
|
|
52
54
|
if (recoveryFailure)
|
|
53
55
|
throw recoveryFailure;
|
|
54
|
-
const committed = await client.
|
|
56
|
+
const committed = await client.query('COMMIT');
|
|
55
57
|
if (committed.command !== 'COMMIT') {
|
|
56
58
|
throw new Error('PostgreSQL rolled back the aborted transaction.');
|
|
57
59
|
}
|
|
@@ -60,17 +62,20 @@ export async function withTransaction(pool, work) {
|
|
|
60
62
|
catch (error) {
|
|
61
63
|
active = false;
|
|
62
64
|
await pending;
|
|
65
|
+
if (!began) {
|
|
66
|
+
releaseError = error instanceof Error ? error : new Error('BEGIN failed.');
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
63
69
|
try {
|
|
64
|
-
|
|
65
|
-
await client.unsafe('ROLLBACK');
|
|
70
|
+
await client.query('ROLLBACK');
|
|
66
71
|
}
|
|
67
72
|
catch (rollbackError) {
|
|
68
|
-
|
|
73
|
+
releaseError = rollbackError instanceof Error ? rollbackError : new Error('Rollback failed.');
|
|
69
74
|
throw new AggregateError([error, rollbackError], 'Transaction and rollback failed.');
|
|
70
75
|
}
|
|
71
76
|
throw error;
|
|
72
77
|
}
|
|
73
78
|
finally {
|
|
74
|
-
|
|
79
|
+
connection.release(releaseError);
|
|
75
80
|
}
|
|
76
81
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type { Pool } from 'pg';
|
|
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';
|
|
@@ -18,7 +18,7 @@ export interface CloseOptions {
|
|
|
18
18
|
}
|
|
19
19
|
export type Database = Transaction & {
|
|
20
20
|
/** The underlying pool-level driver client; native calls bypass Tenon's query and transaction guards. */
|
|
21
|
-
readonly native:
|
|
21
|
+
readonly native: Pool;
|
|
22
22
|
/** Await all work inside the callback. Commit on success; rollback on an escaping error. */
|
|
23
23
|
transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>;
|
|
24
24
|
close(options?: CloseOptions): Promise<void>;
|
package/docs/adoption-guide.md
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
# Adopting Tenon
|
|
2
2
|
|
|
3
|
-
Replace application queries independently of migration ownership. Tenon can query an existing
|
|
4
|
-
|
|
5
|
-
|
|
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
6
|
|
|
7
7
|
## Preserve connections and behavior
|
|
8
8
|
|
|
9
9
|
Keep the existing database URI, credentials, TLS requirements, schema, and data. Tenon can add
|
|
10
|
-
driver settings
|
|
10
|
+
driver settings through pg configuration:
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
13
|
import { createTenon } from '@kequtech/tenon-orm';
|
|
14
14
|
|
|
15
15
|
const db = createTenon({
|
|
16
|
-
connection: {
|
|
16
|
+
connection: {
|
|
17
|
+
connectionString: existingUri,
|
|
18
|
+
connectionTimeoutMillis: 5000,
|
|
19
|
+
},
|
|
17
20
|
});
|
|
18
21
|
```
|
|
19
22
|
|
|
20
|
-
Translate old driver configuration into
|
|
21
|
-
|
|
22
|
-
`
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
runtime queries. See [connection options](reference.md#installation-and-connection).
|
|
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
|
|
27
|
+
[connection options](reference.md#installation-and-connection).
|
|
26
28
|
|
|
27
|
-
Before conversion, identify behavior supplied by the old ORM: IDs/defaults, timestamps,
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
Before conversion, identify behavior supplied by the old ORM: IDs/defaults, timestamps, validation,
|
|
30
|
+
hooks, serialization, relationships, and transaction boundaries. Preserve it explicitly in
|
|
31
|
+
application code or SQL where required.
|
|
30
32
|
|
|
31
33
|
## Replace queries
|
|
32
34
|
|
|
@@ -41,7 +43,7 @@ in application code or SQL where required.
|
|
|
41
43
|
2. Replace queries with parameterized SQL in ordinary functions accepting `QueryClient` where
|
|
42
44
|
callers may supply `db` or `tx`. Retain projections, ordering, null handling, and authorization.
|
|
43
45
|
3. Review value conversions against [Tenon's mappings](reference.md#generated-types-and-checking):
|
|
44
|
-
|
|
46
|
+
numeric values, dates, arrays, and JSON serialization may differ from the previous driver.
|
|
45
47
|
Generated declarations do not replace domain validation.
|
|
46
48
|
4. Inspect meaningful completed query variants and run database assertions for actual rows,
|
|
47
49
|
permissions, write effects, and rollback. See [query inspection](query-inspection.md).
|
|
@@ -74,9 +76,9 @@ For an application owning `app` in a shared database:
|
|
|
74
76
|
npx tenon baseline capture --directory migrations --namespace app --schema app
|
|
75
77
|
```
|
|
76
78
|
|
|
77
|
-
The directory must contain no SQL files. Capture uses native
|
|
78
|
-
--no-privileges` and writes `0001_baseline.up.sql` with no down
|
|
79
|
-
client tools on PATH; prefer the server's major version.
|
|
79
|
+
The directory must contain no SQL files. Capture uses native
|
|
80
|
+
`pg_dump --schema-only --no-owner --no-privileges` and writes `0001_baseline.up.sql` with no down
|
|
81
|
+
file. Install compatible PostgreSQL client tools on PATH; prefer the server's major version.
|
|
80
82
|
|
|
81
83
|
| Option | Scope |
|
|
82
84
|
| --------------------- | ------------------------------------------------------------------------------------ |
|
|
@@ -101,20 +103,30 @@ reference definitions rather than comparing against types regenerated after late
|
|
|
101
103
|
`generateDefinitions` covers relation metadata, not functions or privileges; include reviewed
|
|
102
104
|
catalog checks for other objects the application needs. Tenon does not perform these comparisons.
|
|
103
105
|
|
|
106
|
+
Generated declaration text is not a version-stable schema fingerprint: driver mappings and generator
|
|
107
|
+
changes can alter it without schema drift. The schema-summary comments expose SQL definitions, but
|
|
108
|
+
their formatting and PostgreSQL's rendering of those definitions are not a stable comparison format
|
|
109
|
+
either. If an existing adoption guard hashes generated text, preserve its original reference and
|
|
110
|
+
freeze a separate reference for each supported generator version from a disposable replay of the
|
|
111
|
+
immutable checkpoint. Compare the replay's metadata with the reviewed checkpoint before accepting
|
|
112
|
+
the new reference; never derive a replacement from whichever live database is configured. Keep
|
|
113
|
+
independent function and privilege checks, and test both matching adoption and refusal after drift.
|
|
114
|
+
This application guard is separate from Tenon's migration checksums; keep applied SQL unchanged.
|
|
115
|
+
|
|
104
116
|
```sh
|
|
105
117
|
npx tenon baseline adopt --directory migrations --namespace app
|
|
106
118
|
npx tenon migrate --directory migrations --namespace app
|
|
107
119
|
```
|
|
108
120
|
|
|
109
|
-
Adoption records the baseline checksum without executing its SQL. The namespace must already
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
Adoption records the baseline checksum without executing its SQL. The namespace must already exist.
|
|
122
|
+
Repeating matching adoption is harmless, including after later migrations; history conflicts fail.
|
|
123
|
+
Once Tenon owns history, add forward migrations rather than editing the baseline.
|
|
112
124
|
|
|
113
125
|
### Replay a fresh database
|
|
114
126
|
|
|
115
127
|
Run `tenon migrate --directory migrations --namespace app`, without adoption. It executes the same
|
|
116
|
-
committed baseline and subsequent migrations. Apply the deployment's required roles, ownership,
|
|
117
|
-
|
|
128
|
+
committed baseline and subsequent migrations. Apply the deployment's required roles, ownership, and
|
|
129
|
+
grants before application access.
|
|
118
130
|
|
|
119
131
|
The portable baseline omits ownership, grants/revokes, and default privileges. Objects are recreated
|
|
120
132
|
under the migration role. For `SECURITY DEFINER` functions, that owner controls execution authority,
|
|
@@ -129,7 +141,8 @@ types. Test existing-row preservation, repeatable adoption, subsequent migration
|
|
|
129
141
|
A schema-only baseline and down migration are not backups. The missing down file prevents rollback
|
|
130
142
|
across the baseline; use the application's backup/recovery procedure for that boundary.
|
|
131
143
|
|
|
132
|
-
Programmatic capture is
|
|
133
|
-
|
|
134
|
-
`
|
|
135
|
-
connection
|
|
144
|
+
Programmatic capture is
|
|
145
|
+
`captureBaseline({ connection, directory?, namespace?, schemas?, onWarning? })` and returns
|
|
146
|
+
`{ id, upPath }`. It requires a native libpq connection string; an explicitly empty `schemas` list
|
|
147
|
+
fails. `adoptBaseline({ connection, directory?, namespace? })` accepts Tenon's runtime connection
|
|
148
|
+
forms and returns newly recorded IDs.
|
package/docs/agent-guide.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Tenon agent guide
|
|
2
2
|
|
|
3
|
-
Tenon executes explicit PostgreSQL SQL through
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Tenon executes explicit PostgreSQL SQL through pg. Use ordinary functions for reusable queries. This
|
|
4
|
+
guide covers routine implementation; the [reference](reference.md) defines the full API and the
|
|
5
|
+
[human guide](human-guide.md) gives a step-by-step introduction.
|
|
6
6
|
|
|
7
7
|
## Start with the existing project
|
|
8
8
|
|
|
@@ -11,16 +11,17 @@ commands. Preserve those conventions. Tenon queries and type generation do not r
|
|
|
11
11
|
migration history. Keep input validation, authorization, domain conversion, and retry policy in the
|
|
12
12
|
application.
|
|
13
13
|
|
|
14
|
-
For an upgrade, record the previous resolved Tenon version from the lockfile or installed package
|
|
15
|
-
[Changelog](../CHANGELOG.md) entries through the target version before changing code.
|
|
16
|
-
the package alongside this guide. Follow its upgrade instructions, regenerate types
|
|
17
|
-
change, and run the project's typechecks and relevant database tests. Use the
|
|
14
|
+
For an upgrade, record the previous resolved Tenon version from the lockfile or installed package
|
|
15
|
+
and read the [Changelog](../CHANGELOG.md) entries through the target version before changing code.
|
|
16
|
+
It ships with the package alongside this guide. Follow its upgrade instructions, regenerate types
|
|
17
|
+
when mappings change, and run the project's typechecks and relevant database tests. Use the
|
|
18
18
|
[adoption guide](adoption-guide.md) when replacing another ORM or transferring migration ownership.
|
|
19
19
|
|
|
20
20
|
## Connect
|
|
21
21
|
|
|
22
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
|
|
23
|
+
`npm install @kequtech/tenon-orm`; retain the project's package manager and TypeScript
|
|
24
|
+
configuration.
|
|
24
25
|
|
|
25
26
|
```ts
|
|
26
27
|
import { createTenon } from '@kequtech/tenon-orm';
|
|
@@ -30,13 +31,13 @@ if (!connection) throw new Error('DATABASE_URL is required');
|
|
|
30
31
|
export const db = createTenon({ connection });
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
`connection` accepts a URI string, `
|
|
34
|
-
|
|
35
|
-
pools with a default 5,000 ms grace period; supplied
|
|
34
|
+
`connection` accepts a URI string, pg `PoolConfig` (including `connectionString`), or an existing
|
|
35
|
+
`pg.Pool`. Reuse an instance for each database configuration. `db.close()` ends internally created
|
|
36
|
+
pools with a default 5,000 ms grace period; supplied pools remain application-owned. Configure
|
|
36
37
|
shutdown with `db.close({ timeoutMs })`; concurrent callers share its outcome. Finish outstanding
|
|
37
|
-
work before closing. Tenon
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
work before closing. Tenon loads no environment files. Qualify table names or set runtime
|
|
39
|
+
`search_path`; CLI `--namespace` selects tooling scope, not runtime query names. See
|
|
40
|
+
[connection options](reference.md#installation-and-connection).
|
|
40
41
|
|
|
41
42
|
## Query and result contracts
|
|
42
43
|
|
|
@@ -60,23 +61,23 @@ aggregates need types for their actual output.
|
|
|
60
61
|
|
|
61
62
|
| `returns` | Result | Required returned rows |
|
|
62
63
|
| ------------------ | -------------------------------------------------- | ---------------------- |
|
|
63
|
-
| `native` (default) | Driver
|
|
64
|
+
| `native` (default) | Driver object with `.rows`, `.rowCount`, `.fields` | Any |
|
|
64
65
|
| `many` | Plain `Row[]` | Any |
|
|
65
66
|
| `oneOrNone` | `Row \| undefined` | Zero or one |
|
|
66
67
|
| `one` | `Row` | Exactly one |
|
|
67
68
|
|
|
68
|
-
Options are `{ returns?, affectedRows?, label? }`; unknown keys fail. Modes do not add SQL limits
|
|
69
|
-
|
|
70
|
-
it alone does not undo an autocommitted write.
|
|
69
|
+
Options are `{ returns?, affectedRows?, label? }`; unknown keys fail. Modes do not add SQL limits or
|
|
70
|
+
`RETURNING`. A returned-row mismatch throws `CardinalityError` with code
|
|
71
|
+
`TENON_CARDINALITY_MISMATCH`; it alone does not undo an autocommitted write.
|
|
71
72
|
|
|
72
73
|
## Parameters and composition
|
|
73
74
|
|
|
74
|
-
- Interpolate values without quotes. Do not author `$n` placeholders inside templates.
|
|
75
|
-
`
|
|
75
|
+
- Interpolate values without quotes. Do not author `$n` placeholders inside templates. `undefined`
|
|
76
|
+
and `null` bind as SQL null, following pg.
|
|
76
77
|
- Use `id = ANY(${ids}::integer[])` for a set of IDs. Cast standalone parameters such as
|
|
77
78
|
`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
|
-
|
|
79
|
+
- `identifier(schema, table)` quotes separate approved names. Strings such as `'DESC'` remain bound
|
|
80
|
+
values; choose SQL syntax through fixed query alternatives or fragments.
|
|
80
81
|
- `sql.fragment` creates partial SQL with bound values; `sql.join(parts, 'AND' | 'OR' | ',')`
|
|
81
82
|
combines a nonempty list. Blank fragments fail. Group mixed conditions with parentheses.
|
|
82
83
|
- Full `sql` or `unsafeSql` queries cannot be nested. `unsafeSql<Row>(text, values)` is for trusted
|
|
@@ -87,20 +88,24 @@ mandatory authorization predicates when optional filters are absent.
|
|
|
87
88
|
|
|
88
89
|
## Writes and generated values
|
|
89
90
|
|
|
90
|
-
`sql.insertValues(rowOrRows, columns?)` builds column/value fragments;
|
|
91
|
-
columns?)` builds assignments. Missing/undefined insert fields use defaults;
|
|
92
|
-
omitted. Explicit null stays null. Empty batches and empty assignments fail.
|
|
93
|
-
ignore unlisted fields and can discard required inputs. Put expressions, conflict
|
|
94
|
-
`RETURNING` in the surrounding SQL. Batch in the caller below the query-wide
|
|
91
|
+
`sql.insertValues(rowOrRows, columns?)` builds column/value fragments;
|
|
92
|
+
`sql.assignments(patch, columns?)` builds assignments. Missing/undefined insert fields use defaults;
|
|
93
|
+
update fields are omitted. Explicit null stays null. Empty batches and empty assignments fail.
|
|
94
|
+
Selected columns ignore unlisted fields and can discard required inputs. Put expressions, conflict
|
|
95
|
+
policy, and `RETURNING` in the surrounding SQL. Batch in the caller below the query-wide
|
|
96
|
+
65,535-parameter limit.
|
|
95
97
|
|
|
96
98
|
Use generated `Rows.Insert['users']` and `Rows.Update['users']` inputs; narrow editable fields with
|
|
97
99
|
`Pick`. These types account for defaults, writable columns, domains, and driver representations.
|
|
98
100
|
They do not validate constraints or JSON contents. See [mutation examples](mutations.md).
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Keep domain validation at the persistence boundary. See
|
|
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) and the
|
|
107
|
+
[JSON/finite-date projection](reference.md#persistence-and-domain-values). For authentication policy
|
|
108
|
+
requiring finite expiry, include `isfinite(expires_at)` alongside the SQL expiry comparison.
|
|
104
109
|
|
|
105
110
|
## Transactions and native access
|
|
106
111
|
|
|
@@ -127,34 +132,36 @@ and CTE effects. Reserve `tenon_affected_rows_` savepoint names for Tenon. A los
|
|
|
127
132
|
acknowledgment can leave the outcome unknown.
|
|
128
133
|
|
|
129
134
|
`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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
`onQuery`; they do not join a Tenon transaction. Close owned pools through `db.close()`. Follow the
|
|
136
|
+
[native access contract](reference.md#native-driver-access) for resource ownership and LISTEN.
|
|
137
|
+
Optional `onQuery` reports completion metadata without SQL or values; success inside a callback does
|
|
138
|
+
not establish that the outer transaction committed.
|
|
134
139
|
|
|
135
140
|
## Inspect and validate
|
|
136
141
|
|
|
137
142
|
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
|
|
139
|
-
|
|
140
|
-
unknown and JSON structure is unchecked. A successful description does not
|
|
141
|
-
bound values, execution permission, or effects.
|
|
143
|
+
Inspection owns a separate connection. Match the runtime role, search path, and timezone. Temporary
|
|
144
|
+
tables and transaction-local state are not inherited; reported mappings assume pg's default parsers.
|
|
145
|
+
Result nullability is unknown and JSON structure is unchecked. A successful description does not
|
|
146
|
+
verify `sql<Row>`, bound values, execution permission, or effects.
|
|
142
147
|
|
|
143
|
-
Pair inspection with database assertions for complete relevant rows, meaningful query variants,
|
|
144
|
-
|
|
145
|
-
execution; keep simple queries inline when that is sufficient. See
|
|
148
|
+
Pair inspection with database assertions for complete relevant rows, meaningful query variants, and
|
|
149
|
+
rollback. A small query-building function can share the same statement with inspection and
|
|
150
|
+
execution; keep simple queries inline when that is sufficient. See
|
|
151
|
+
[query inspection](query-inspection.md).
|
|
146
152
|
|
|
147
153
|
## Schema changes
|
|
148
154
|
|
|
149
|
-
With Tenon's runner, development can use
|
|
150
|
-
src/generated-schema.d.ts`. Commit migrations and exact
|
|
151
|
-
generated file from automatic formatting. Production runs
|
|
152
|
-
`generate --check` detects representation drift, not pending
|
|
155
|
+
With Tenon's runner, development can use
|
|
156
|
+
`tenon migrate && tenon generate --out src/generated-schema.d.ts`. Commit migrations and exact
|
|
157
|
+
generated output together; exclude the generated file from automatic formatting. Production runs
|
|
158
|
+
migrations and deploys committed types. `generate --check` detects representation drift, not pending
|
|
159
|
+
migrations or every schema change.
|
|
153
160
|
|
|
154
161
|
All database CLI commands require `DATABASE_URL`. `tenon new <name>` creates empty up/down files
|
|
155
162
|
offline; fill them before migrating. Migration commands use `--directory migrations` and
|
|
156
163
|
`--namespace public` by default; generation also accepts `--namespace`. Pass a custom namespace to
|
|
157
164
|
both migration and generation. Omit the down file for an irreversible migration; never edit applied
|
|
158
165
|
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
|
-
|
|
166
|
+
so use a direct or session-pooled connection. Nontransactional DDL needs a separate procedure. See
|
|
167
|
+
the [command reference](reference.md#command-reference).
|
package/docs/human-guide.md
CHANGED
|
@@ -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
|
|
4
|
+
executes it through pg. This guide follows a small application from its first table to reusable
|
|
5
5
|
queries, transactions, and result checking.
|
|
6
6
|
|
|
7
7
|
Use the [reference](reference.md) for exact contracts and [adoption](adoption-guide.md) when
|
|
@@ -29,12 +29,13 @@ if (!connection) throw new Error('DATABASE_URL is required');
|
|
|
29
29
|
export const db = createTenon({ connection });
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Reuse this instance across the application and call `await db.close()` at shutdown. You can also
|
|
32
|
+
Reuse this instance across the application and call `await db.close()` at shutdown. You can also
|
|
33
|
+
supply an existing `pg.Pool`; your application then owns its shutdown.
|
|
33
34
|
|
|
34
35
|
## Create a table
|
|
35
36
|
|
|
36
|
-
If you already have a schema and migration runner, keep them. For a fresh example database,
|
|
37
|
-
|
|
37
|
+
If you already have a schema and migration runner, keep them. For a fresh example database, create a
|
|
38
|
+
migration:
|
|
38
39
|
|
|
39
40
|
```sh
|
|
40
41
|
npx tenon new create_users
|
|
@@ -98,8 +99,8 @@ separately from the SQL text, even when the value contains SQL.
|
|
|
98
99
|
|
|
99
100
|
The type describes one selected row. `returns: 'oneOrNone'` gives you that row or `undefined`; it
|
|
100
101
|
throws if the query returns more than one row. Use `'one'` when absence should also be an error,
|
|
101
|
-
`'many'` for an array, or omit `returns` for the native result
|
|
102
|
-
These options do not change your SQL.
|
|
102
|
+
`'many'` for an array, or omit `returns` for the native result object with `.rows`, `.rowCount`, and
|
|
103
|
+
`.fields`. These options do not change your SQL.
|
|
103
104
|
|
|
104
105
|
Generated table types can help describe projections without repeating every field:
|
|
105
106
|
|
|
@@ -113,6 +114,10 @@ needs a type for what that particular query selects. An annotation is a declarat
|
|
|
113
114
|
the SQL matches. Without one, result fields are `unknown`. Use the inspection and database-test
|
|
114
115
|
workflow below to check the declared contract against the query's behavior.
|
|
115
116
|
|
|
117
|
+
Keep storage types conservative and validate them when projecting domain values. The
|
|
118
|
+
[persistence-to-domain example](reference.md#persistence-and-domain-values) shows validated JSON
|
|
119
|
+
writes and reads, finite dates, and an SQL predicate that rejects infinite session expiry.
|
|
120
|
+
|
|
116
121
|
## Reuse a query with a function
|
|
117
122
|
|
|
118
123
|
Use an ordinary function when several callers need the same query. An object input makes the
|
|
@@ -129,16 +134,16 @@ export function userByEmail(client: QueryClient, { email }: { email: string }) {
|
|
|
129
134
|
returns: 'oneOrNone',
|
|
130
135
|
});
|
|
131
136
|
}
|
|
132
|
-
|
|
133
|
-
const user = await userByEmail(db, { email: 'reader@example.test' });
|
|
134
137
|
```
|
|
135
138
|
|
|
136
|
-
`
|
|
137
|
-
the SQL and execution options together in the function.
|
|
139
|
+
Call `userByEmail(db, { email: 'reader@example.test' })` at the call site, or pass a transaction's
|
|
140
|
+
`tx`. Keep the SQL and execution options together in the function.
|
|
138
141
|
|
|
139
142
|
## Change data in a transaction
|
|
140
143
|
|
|
141
144
|
```ts
|
|
145
|
+
import { userByEmail } from './queries.js';
|
|
146
|
+
|
|
142
147
|
await db.transaction(async (tx) => {
|
|
143
148
|
const user = await userByEmail(tx, { email: 'reader@example.test' });
|
|
144
149
|
if (!user) throw new Error('User not found');
|
|
@@ -177,20 +182,20 @@ await db.query(sql`UPDATE public.users SET ${sql.assignments(patch, ['active'])}
|
|
|
177
182
|
```
|
|
178
183
|
|
|
179
184
|
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
|
-
|
|
185
|
+
`RETURNING` in SQL. Selected columns choose fields; they do not validate the complete object. See
|
|
186
|
+
[input types](reference.md#insert-and-update-inputs) for mappings and the
|
|
182
187
|
[mutation cookbook](mutations.md) for defaults, batches, and upserts.
|
|
183
188
|
|
|
184
189
|
## Check your SQL and result types
|
|
185
190
|
|
|
186
191
|
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
|
-
|
|
189
|
-
|
|
192
|
+
file. It reports PostgreSQL metadata and a conservative `rowType` declaration; it does not update or
|
|
193
|
+
verify the application's `sql<Row>` annotation. Inspection uses a separate session, so match the
|
|
194
|
+
runtime role and settings. See [query inspection](query-inspection.md) for a worked test.
|
|
190
195
|
|
|
191
196
|
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
|
-
|
|
197
|
+
and test actual rows and effects. Keep those checks in CI. Execution assertions cover values, nulls,
|
|
198
|
+
JSON, authorization, and rollback that description alone cannot establish.
|
|
194
199
|
|
|
195
200
|
## Reuse or assemble parts of SQL when needed
|
|
196
201
|
|
|
@@ -226,9 +231,9 @@ const query = sql<UserSummary>`SELECT id, email FROM public.users
|
|
|
226
231
|
WHERE ${sql.join(filters, 'AND')} ORDER BY id`;
|
|
227
232
|
```
|
|
228
233
|
|
|
229
|
-
This matches active users whose email contains every term. With no terms, the active condition
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
This matches active users whose email contains every term. With no terms, the active condition still
|
|
235
|
+
applies. For an entirely optional set of filters, omit the whole `WHERE` clause when the list is
|
|
236
|
+
empty:
|
|
232
237
|
|
|
233
238
|
```ts
|
|
234
239
|
const optionalFilters = terms.map((term) => sql.fragment`email ILIKE ${'%' + term + '%'}`);
|
package/docs/mutations.md
CHANGED
|
@@ -51,8 +51,8 @@ Annotations, `satisfies`, or helper generic arguments apply generated input type
|
|
|
51
51
|
the file does not check writes. Use `Pick` to narrow accepted fields and retain runtime validation.
|
|
52
52
|
Generated inputs describe defaults and driver representations, not authorization or constraints.
|
|
53
53
|
|
|
54
|
-
Selected columns choose and order fields, ignoring unlisted keys. They can discard a required
|
|
55
|
-
|
|
54
|
+
Selected columns choose and order fields, ignoring unlisted keys. They can discard a required field
|
|
55
|
+
even when the original object is valid:
|
|
56
56
|
|
|
57
57
|
```ts
|
|
58
58
|
const input: Rows.Insert['profiles'] = { name: 'Ada' };
|
|
@@ -97,21 +97,24 @@ if (bio !== undefined) {
|
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
The second query preserves an existing bio. Omitting an insert value uses its database default;
|
|
101
|
-
|
|
100
|
+
The second query preserves an existing bio. Omitting an insert value uses its database default; that
|
|
101
|
+
alone does not decide which fields a conflict update overwrites.
|
|
102
102
|
|
|
103
103
|
## JSON and PostgreSQL arrays
|
|
104
104
|
|
|
105
|
-
For a table with `payload jsonb` and `tags text[]`,
|
|
105
|
+
For a table with `payload jsonb` and `tags text[]`, encode JSON separately from the PostgreSQL
|
|
106
|
+
array:
|
|
106
107
|
|
|
107
108
|
```ts
|
|
108
109
|
const tags = ['ada', 'lin'];
|
|
109
|
-
await db.query(
|
|
110
|
+
await db.query(
|
|
111
|
+
sql`INSERT INTO deliveries ${sql.insertValues({ payload: JSON.stringify(tags), tags })}`,
|
|
112
|
+
);
|
|
110
113
|
```
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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.
|
|
115
118
|
|
|
116
119
|
## Batches in one transaction
|
|
117
120
|
|
|
@@ -127,5 +130,5 @@ await db.transaction(async (tx) => {
|
|
|
127
130
|
```
|
|
128
131
|
|
|
129
132
|
A failure rolls back all batches. Empty input makes no insert calls. Choose a batch size below the
|
|
130
|
-
query-wide 65,
|
|
133
|
+
query-wide 65,535-parameter limit, including surrounding parameters; default cells consume none.
|
|
131
134
|
Tenon does not split batches or choose transaction boundaries for you.
|
package/docs/query-inspection.md
CHANGED
|
@@ -34,20 +34,22 @@ const description = await describeQuery({
|
|
|
34
34
|
});
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Reuse the runtime URI and session settings. Optional `session.role` uses SET ROLE when permitted;
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
Reuse the runtime URI and session settings. Optional `session.role` uses SET ROLE when permitted; it
|
|
38
|
+
does not load that role's login defaults. Omitted settings use the inspection connection's defaults.
|
|
39
|
+
Explicit undefined behaves like omission. Unknown keys and invalid settings fail.
|
|
40
40
|
|
|
41
41
|
`context` reports login/effective roles, configured/resolved search paths, timezone, database, and
|
|
42
|
-
effective statement/lock timeouts. Its mode is `separate-session` and read mapping profile is
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
effective statement/lock timeouts. Its mode is `separate-session` and read mapping profile is `pg`;
|
|
43
|
+
reported read types assume pg's default parsers and do not infer application overrides. Temporary
|
|
44
|
+
tables, uncommitted changes, and transaction-local settings are not inherited. pg reads its normal
|
|
45
|
+
connection environment, including `PGOPTIONS` when URI options are absent. Use explicit URI/session
|
|
46
|
+
settings for reproducibility.
|
|
46
47
|
|
|
47
48
|
Defaults are a 3-second connection/authentication timeout, a 5-second limit per server statement,
|
|
48
|
-
and a 1-second lock wait. Overrides are positive integer milliseconds, at most 2,147,483,647.
|
|
49
|
-
|
|
50
|
-
timeout. These are operation limits, not a
|
|
49
|
+
and a 1-second lock wait. Overrides are positive integer milliseconds, at most 2,147,483,647. URI
|
|
50
|
+
settings cannot disable the statement/lock bounds. A URI `query_timeout` remains pg's separate
|
|
51
|
+
client read timeout. The owned client closes after inspection. These are operation limits, not a
|
|
52
|
+
single overall deadline.
|
|
51
53
|
|
|
52
54
|
## Result fields
|
|
53
55
|
|
|
@@ -113,10 +115,6 @@ rows/effects, JSON, permissions, and rollback in database tests, and run TypeScr
|
|
|
113
115
|
meaningful query variants; one instance does not cover unexercised branches. Expect only reviewed
|
|
114
116
|
findings and keep unexpected ones visible.
|
|
115
117
|
|
|
116
|
-
A query builder is optional. When a test needs to retain executed statements, a test-only
|
|
117
|
-
`QueryClient` wrapper can forward them and retain `{ text }` without parameter values. Production
|
|
118
|
-
`onQuery` deliberately supplies neither SQL nor values.
|
|
119
|
-
|
|
120
118
|
## CLI
|
|
121
119
|
|
|
122
120
|
Save SQL with PostgreSQL placeholders in `query.sql`:
|