@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.
- package/CHANGELOG.md +54 -0
- package/README.md +27 -5
- package/dist/affected-rows.js +0 -1
- package/dist/baseline.js +3 -5
- package/dist/check/contracts.d.ts +6 -0
- package/dist/check/contracts.js +206 -0
- package/dist/check/project.d.ts +18 -0
- package/dist/check/project.js +224 -0
- package/dist/check/source.d.ts +51 -0
- package/dist/check/source.js +629 -0
- package/dist/check/types.d.ts +36 -0
- package/dist/check/types.js +1 -0
- package/dist/check.d.ts +4 -0
- package/dist/check.js +147 -0
- package/dist/cli.js +38 -9
- package/dist/connection.d.ts +5 -12
- package/dist/connection.js +12 -121
- package/dist/database.js +5 -8
- package/dist/describe.js +10 -3
- package/dist/generate.d.ts +2 -3
- package/dist/generate.js +26 -24
- package/dist/generation/catalog.d.ts +5 -6
- package/dist/generation/catalog.js +7 -9
- package/dist/generation/render.d.ts +3 -0
- package/dist/generation/render.js +60 -0
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspection/catalog.d.ts +3 -6
- package/dist/inspection/catalog.js +9 -18
- package/dist/inspection/cli.d.ts +12 -0
- package/dist/inspection/cli.js +9 -4
- package/dist/inspection/connection.d.ts +2 -3
- package/dist/inspection/connection.js +20 -22
- package/dist/inspection/options.js +0 -6
- package/dist/inspection/result.d.ts +3 -2
- package/dist/inspection/result.js +13 -26
- package/dist/inspection/types.d.ts +6 -4
- package/dist/migrations/connection.d.ts +3 -3
- package/dist/migrations/connection.js +14 -21
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -9
- package/dist/query-options.d.ts +1 -1
- package/dist/query.js +1 -1
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +21 -77
- package/dist/types.d.ts +1 -1
- package/docs/adoption-guide.md +64 -12
- package/docs/agent-guide.md +57 -27
- package/docs/human-guide.md +53 -18
- package/docs/mutations.md +22 -9
- package/docs/query-checking.md +200 -0
- package/docs/query-inspection.md +40 -13
- package/docs/reference.md +330 -108
- package/package.json +11 -7
- package/dist/driver/types.d.ts +0 -10
- package/dist/driver/types.js +0 -49
- package/dist/generation/table-types.d.ts +0 -2
- package/dist/generation/table-types.js +0 -96
- package/dist/inspection/protocol.d.ts +0 -7
- package/dist/inspection/protocol.js +0 -45
package/dist/transaction.js
CHANGED
|
@@ -1,81 +1,25 @@
|
|
|
1
|
-
import { executeSql
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
18
|
+
return await work(query);
|
|
71
19
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
20
|
+
finally {
|
|
21
|
+
active = false;
|
|
22
|
+
await pending;
|
|
75
23
|
}
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
finally {
|
|
79
|
-
connection.release(releaseError);
|
|
80
|
-
}
|
|
24
|
+
});
|
|
81
25
|
}
|
package/dist/types.d.ts
CHANGED
package/docs/adoption-guide.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
57
|
+
connectTimeoutMs: 5000,
|
|
19
58
|
},
|
|
20
59
|
});
|
|
21
60
|
```
|
|
22
61
|
|
|
23
|
-
Translate old driver configuration into
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 `
|
|
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
|
package/docs/agent-guide.md
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
# Tenon agent guide
|
|
2
2
|
|
|
3
|
-
Tenon executes explicit PostgreSQL SQL through
|
|
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,
|
|
35
|
-
`
|
|
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 {
|
|
56
|
+
import type { Database } from './generated-schema.js';
|
|
47
57
|
|
|
48
|
-
type Summary = Pick<
|
|
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` |
|
|
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. `
|
|
76
|
-
and
|
|
77
|
-
|
|
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 `
|
|
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 `
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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.
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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 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
|
|
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 `
|
|
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 {
|
|
109
|
-
type UserSummary = Pick<
|
|
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 {
|
|
139
|
+
import type { Database } from './generated-schema.js';
|
|
125
140
|
|
|
126
|
-
type UserSummary = Pick<
|
|
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 {
|
|
185
|
+
import type { Database } from './generated-schema.js';
|
|
171
186
|
|
|
172
|
-
const input:
|
|
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<
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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 `
|
|
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 {
|
|
37
|
+
import type { Database } from './generated-schema.js';
|
|
38
38
|
|
|
39
|
-
type ProfileInput = Pick<
|
|
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:
|
|
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<
|
|
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:
|
|
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
|
|
116
|
-
strings, and JSON null;
|
|
117
|
-
PostgreSQL
|
|
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
|
|