@kequtech/tenon-orm 0.4.0 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -2
- package/README.md +93 -51
- package/dist/check.js +4 -2
- package/dist/cli.js +97 -40
- package/dist/config/init.d.ts +2 -0
- package/dist/config/init.js +34 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.js +159 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +1 -0
- package/dist/connection.d.ts +5 -0
- package/dist/describe.js +3 -1
- package/dist/generation/output.js +0 -2
- package/dist/index.d.ts +2 -1
- package/dist/inspection/cli.d.ts +3 -4
- package/dist/inspection/cli.js +3 -13
- package/dist/inspection/connection.js +23 -19
- package/dist/inspection/options.d.ts +5 -7
- package/dist/inspection/options.js +49 -22
- package/dist/inspection/types.d.ts +3 -2
- package/dist/migrations/history.d.ts +1 -1
- package/dist/migrations/history.js +3 -2
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +6 -0
- package/docs/adoption-guide.md +94 -153
- package/docs/agent-guide.md +93 -176
- package/docs/configuration.md +146 -0
- package/docs/connections.md +187 -0
- package/docs/human-guide.md +150 -179
- package/docs/migrations.md +140 -0
- package/docs/mutations.md +104 -67
- package/docs/query-checking.md +122 -120
- package/docs/query-inspection.md +83 -76
- package/docs/reference.md +234 -474
- package/docs/schema-types.md +266 -0
- package/docs/sql.md +212 -0
- package/package.json +1 -1
package/docs/mutations.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Writing data
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Write `INSERT`, `UPDATE`, `DELETE`, and `ON CONFLICT` in SQL. Use `sql.insertValues` and
|
|
4
|
+
`sql.assignments` when you have an object of field values; they return fragments for the column/value
|
|
5
|
+
parts of your statement. The [API reference](reference.md#object-based-mutation-fragments) defines
|
|
6
|
+
edge cases and validation rules.
|
|
7
|
+
|
|
8
|
+
Examples use an existing `db`, import `sql` from Tenon, and assume this table:
|
|
6
9
|
|
|
7
10
|
```sql
|
|
8
|
-
CREATE TABLE profiles (
|
|
11
|
+
CREATE TABLE public.profiles (
|
|
9
12
|
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
10
13
|
name text NOT NULL UNIQUE,
|
|
11
14
|
bio text DEFAULT 'unwritten',
|
|
@@ -14,23 +17,41 @@ CREATE TABLE profiles (
|
|
|
14
17
|
);
|
|
15
18
|
```
|
|
16
19
|
|
|
17
|
-
##
|
|
20
|
+
## Insert and return a row
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { sql } from '@kequtech/tenon-orm';
|
|
24
|
+
|
|
25
|
+
const profile = await db.query(
|
|
26
|
+
sql<{ id: number; name: string }>`INSERT INTO public.profiles
|
|
27
|
+
${sql.insertValues({ name: 'Ada' })} RETURNING id, name`,
|
|
28
|
+
{ returns: 'one' },
|
|
29
|
+
);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The helper emits `("name") VALUES ($1)` and binds `'Ada'`. PostgreSQL fills defaults. You choose
|
|
33
|
+
`RETURNING` and the result mode explicitly.
|
|
34
|
+
|
|
35
|
+
## Defaults, undefined, and null
|
|
18
36
|
|
|
19
37
|
```ts
|
|
20
38
|
await db.query(
|
|
21
|
-
sql`INSERT INTO profiles ${sql.insertValues([
|
|
22
|
-
{ name: '
|
|
23
|
-
{ name: '
|
|
24
|
-
])}
|
|
25
|
-
{ returns: 'many' },
|
|
39
|
+
sql`INSERT INTO public.profiles ${sql.insertValues([
|
|
40
|
+
{ name: 'Lin', bio: null },
|
|
41
|
+
{ name: 'Jo', score: 4, bio: undefined },
|
|
42
|
+
])}`,
|
|
26
43
|
);
|
|
27
44
|
```
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
aligned
|
|
31
|
-
|
|
46
|
+
Lin gets score 0 and SQL null for bio. Jo gets score 4 and the bio default. Heterogeneous rows stay
|
|
47
|
+
aligned. Missing/undefined insert cells use `DEFAULT`; explicit null remains SQL NULL.
|
|
48
|
+
|
|
49
|
+
A single default-only object emits `DEFAULT VALUES`, which still fails if the table has an unmet
|
|
50
|
+
required column. Empty batches fail. Multiple default-only rows need an explicit column list.
|
|
51
|
+
|
|
52
|
+
## Type and select accepted fields
|
|
32
53
|
|
|
33
|
-
|
|
54
|
+
Generate `Database` from the same schema, then narrow the fields the application accepts:
|
|
34
55
|
|
|
35
56
|
```ts
|
|
36
57
|
import { sql, type QueryClient } from '@kequtech/tenon-orm';
|
|
@@ -40,108 +61,124 @@ type ProfileInput = Pick<Database['profiles']['insert'], 'name' | 'score'>;
|
|
|
40
61
|
|
|
41
62
|
function createProfile(client: QueryClient, input: ProfileInput) {
|
|
42
63
|
return client.query(
|
|
43
|
-
sql<{ id: number }>`INSERT INTO profiles
|
|
44
|
-
|
|
64
|
+
sql<{ id: number }>`INSERT INTO public.profiles
|
|
65
|
+
${sql.insertValues(input, ['name', 'score'])} RETURNING id`,
|
|
45
66
|
{ returns: 'one' },
|
|
46
67
|
);
|
|
47
68
|
}
|
|
48
69
|
```
|
|
49
70
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
The explicit column list selects and orders fields and ignores unlisted keys. Keep required insert
|
|
72
|
+
fields in that list: a well-typed object can still produce an invalid insert if selection discards
|
|
73
|
+
its required `name`. Helpers have no schema metadata; PostgreSQL enforces constraints.
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
Types apply through annotations, `satisfies`, or helper generic arguments. They do not validate
|
|
76
|
+
external data, authorization, JSON contents, or CHECK constraints. See
|
|
77
|
+
[generated inputs](schema-types.md#insert-and-update-inputs).
|
|
56
78
|
|
|
57
|
-
|
|
58
|
-
const input: Database['profiles']['insert'] = { name: 'Ada' };
|
|
59
|
-
const query = sql`INSERT INTO profiles ${sql.insertValues(input, ['bio', 'score'])}`;
|
|
60
|
-
// Executing this fails: name was discarded and has no default.
|
|
61
|
-
```
|
|
79
|
+
## Patch fields and use expressions
|
|
62
80
|
|
|
63
|
-
|
|
81
|
+
For a validated `id` and patch:
|
|
64
82
|
|
|
65
83
|
```ts
|
|
66
84
|
const patch: Pick<Database['profiles']['update'], 'name' | 'bio'> = { bio: null };
|
|
67
|
-
await db.query(
|
|
68
|
-
|
|
85
|
+
await db.query(
|
|
86
|
+
sql`UPDATE public.profiles SET ${sql.assignments(patch)}, revision = revision + 1
|
|
87
|
+
WHERE id = ${id}`,
|
|
88
|
+
{ affectedRows: { exact: 1 } },
|
|
89
|
+
);
|
|
69
90
|
```
|
|
70
91
|
|
|
71
|
-
|
|
72
|
-
`column = DEFAULT
|
|
92
|
+
Assignments omit missing/undefined fields. Null clears a nullable field. Put arithmetic,
|
|
93
|
+
`column = DEFAULT`, and other SQL expressions in the surrounding statement; helper cells accept
|
|
94
|
+
bound values, not SQL fragments or expression objects.
|
|
73
95
|
|
|
74
|
-
|
|
75
|
-
expression-only update is intended:
|
|
96
|
+
`sql.assignments` throws if no defined fields remain, even if the surrounding query has another
|
|
97
|
+
assignment. Skip an empty patch, or branch when an expression-only update is intended:
|
|
76
98
|
|
|
77
99
|
```ts
|
|
78
100
|
if (patch.name !== undefined || patch.bio !== undefined) {
|
|
79
|
-
await db.query(
|
|
80
|
-
|
|
101
|
+
await db.query(
|
|
102
|
+
sql`UPDATE public.profiles SET ${sql.assignments(patch)}, revision = revision + 1
|
|
103
|
+
WHERE id = ${id}`,
|
|
104
|
+
{ affectedRows: { exact: 1 } },
|
|
105
|
+
);
|
|
81
106
|
} else {
|
|
82
|
-
await db.query(sql`UPDATE profiles SET revision = revision + 1 WHERE id = ${id}
|
|
107
|
+
await db.query(sql`UPDATE public.profiles SET revision = revision + 1 WHERE id = ${id}`, {
|
|
108
|
+
affectedRows: { exact: 1 },
|
|
109
|
+
});
|
|
83
110
|
}
|
|
84
111
|
```
|
|
85
112
|
|
|
86
|
-
##
|
|
113
|
+
## Upsert without overwriting an omitted field
|
|
87
114
|
|
|
88
|
-
|
|
115
|
+
For validated `name`, `score`, and optional `bio`, choose conflict behavior explicitly:
|
|
89
116
|
|
|
90
117
|
```ts
|
|
91
118
|
if (bio !== undefined) {
|
|
92
|
-
await db.query(sql`INSERT INTO profiles ${sql.insertValues({ name, score, bio })}
|
|
119
|
+
await db.query(sql`INSERT INTO public.profiles ${sql.insertValues({ name, score, bio })}
|
|
93
120
|
ON CONFLICT (name) DO UPDATE SET score = excluded.score, bio = excluded.bio`);
|
|
94
121
|
} else {
|
|
95
|
-
await db.query(sql`INSERT INTO profiles ${sql.insertValues({ name, score })}
|
|
122
|
+
await db.query(sql`INSERT INTO public.profiles ${sql.insertValues({ name, score })}
|
|
96
123
|
ON CONFLICT (name) DO UPDATE SET score = excluded.score`);
|
|
97
124
|
}
|
|
98
125
|
```
|
|
99
126
|
|
|
100
|
-
The second
|
|
101
|
-
|
|
127
|
+
The second branch preserves an existing bio. Omitting an insert field uses its database default;
|
|
128
|
+
it does not decide whether an upsert overwrites that field.
|
|
129
|
+
|
|
130
|
+
## Delete with a bound
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
await db.query(sql`DELETE FROM public.profiles WHERE id = ${id}`, {
|
|
134
|
+
affectedRows: { max: 1 },
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Zero or one deletion is allowed; more causes rollback. Use `exact: 1` when absence is also an error.
|
|
139
|
+
Guards work without `RETURNING`. See [transactions and guards](reference.md#transactions-guarded-writes-and-errors).
|
|
102
140
|
|
|
103
141
|
## JSON and PostgreSQL arrays
|
|
104
142
|
|
|
105
|
-
For
|
|
106
|
-
array:
|
|
143
|
+
For an additional table with `payload jsonb` and `tags text[]`:
|
|
107
144
|
|
|
108
145
|
```ts
|
|
109
146
|
import { builtin, json, param } from '@kequtech/tenon-orm';
|
|
110
147
|
|
|
111
148
|
const tags = ['ada', 'lin'];
|
|
112
|
-
await db.query(
|
|
113
|
-
|
|
114
|
-
);
|
|
149
|
+
await db.query(sql`INSERT INTO public.deliveries
|
|
150
|
+
${sql.insertValues({ payload: json(tags), tags: param(tags, builtin.textArray) })}`);
|
|
115
151
|
```
|
|
116
152
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
153
|
+
`json(value)` encodes a JSON document; `param(value, codec)` chooses a PostgreSQL type and encoder.
|
|
154
|
+
A JSON array and a PostgreSQL array are separate formats. Generated writes accept encoded strings
|
|
155
|
+
or explicit parameters; plain objects/arrays reject. `json(null)` means JSON null, while plain
|
|
156
|
+
`null` means SQL NULL. `json('text')` encodes a JSON string, not an already-encoded document.
|
|
157
|
+
JSON encoding rejects bigint, including nested bigint values.
|
|
127
158
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
159
|
+
For int8, use exact strings, bigint, or `param('9007199254740993', builtin.int8)`. For an int8 array,
|
|
160
|
+
use `param(['9007199254740993', null], builtin.int8Array)`. Reads remain strings by default. Plain
|
|
161
|
+
Date and supported Temporal values work as scalar inputs; Date encodes in UTC. Neither input form
|
|
162
|
+
changes the output decoder. See [value mappings](schema-types.md#value-mappings).
|
|
131
163
|
|
|
132
|
-
##
|
|
164
|
+
## Batch within one transaction
|
|
133
165
|
|
|
134
|
-
For
|
|
166
|
+
For `rows` containing generated profile insert inputs:
|
|
135
167
|
|
|
136
168
|
```ts
|
|
137
169
|
await db.transaction(async (tx) => {
|
|
138
170
|
for (let offset = 0; offset < rows.length; offset += 300) {
|
|
139
171
|
const batch = rows.slice(offset, offset + 300);
|
|
140
|
-
await tx.query(sql`INSERT INTO profiles
|
|
172
|
+
await tx.query(sql`INSERT INTO public.profiles
|
|
173
|
+
${sql.insertValues(batch, ['name', 'bio', 'score'])}`);
|
|
141
174
|
}
|
|
142
175
|
});
|
|
143
176
|
```
|
|
144
177
|
|
|
145
|
-
A failure rolls back
|
|
146
|
-
query-wide 65,535-parameter limit,
|
|
147
|
-
Tenon does not split batches or choose transaction boundaries
|
|
178
|
+
A failure rolls back every batch; empty input makes no calls. Choose a batch size below the
|
|
179
|
+
query-wide 65,535-parameter limit, counting surrounding interpolations too. Default cells consume
|
|
180
|
+
no parameters. Tenon does not split batches or choose transaction boundaries.
|
|
181
|
+
|
|
182
|
+
These helpers support optional fields at runtime. The optional source checker needs explicit shape
|
|
183
|
+
types with required selected fields and fixed batch lengths; ordinary generated patch types can
|
|
184
|
+
remain unchecked. See [object expansion checking](query-checking.md#object-expansion).
|
package/docs/query-checking.md
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Checking query types
|
|
2
2
|
|
|
3
|
-
`tenon check` compares
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
`tenon check` compares Tenon SQL in TypeScript source with declared input/result types and PostgreSQL
|
|
4
|
+
metadata. It can catch a missing result property, an int8 result declared as number, or an unsupported
|
|
5
|
+
bound input. It is optional; normal execution still trusts `sql<Row>` annotations.
|
|
6
|
+
|
|
7
|
+
The CLI imports the selected configuration module; the checker then reads query source without
|
|
8
|
+
importing those modules, evaluating input expressions, binding values, or executing application SQL. PostgreSQL still parses/analyzes each recovered statement,
|
|
9
|
+
which can take locks. Use a representative migrated development/test database.
|
|
10
|
+
|
|
11
|
+
## Run a check
|
|
12
|
+
|
|
13
|
+
Create [shared configuration](configuration.md), then select a project, file, or one query:
|
|
7
14
|
|
|
8
15
|
```sh
|
|
9
16
|
npx tenon check
|
|
@@ -12,18 +19,22 @@ npx tenon check src/users.ts:23
|
|
|
12
19
|
npx tenon check src/users.ts:23 --project tsconfig.json --json
|
|
13
20
|
```
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
A one-based line selects the complete query containing it, including multiline templates and their
|
|
23
|
+
variable declaration line. A line with no query or multiple queries fails. A fragment alone is not
|
|
24
|
+
a complete query target.
|
|
25
|
+
|
|
26
|
+
The CLI uses `check.project`, defaulting to `tsconfig.json` beside the shared config. Use `--project`
|
|
27
|
+
for another TypeScript config or project directory; targets must belong to that project. Configured
|
|
28
|
+
paths resolve from the shared config; CLI targets and path flags resolve from the working directory.
|
|
29
|
+
It uses the bundled TypeScript 6 compiler and the project's imports, aliases, indexed access,
|
|
30
|
+
`Pick`, unions, and path mappings. `strictNullChecks` is required. Invalid configuration and compiler
|
|
31
|
+
errors in selected files fail the report.
|
|
19
32
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
indexed access, Pick, unions, and path mappings. strictNullChecks is required. Compiler errors in
|
|
24
|
-
selected source files and invalid compiler configuration fail the report.
|
|
33
|
+
Match runtime role, search path, timezone, and codecs. Session/timeout flags are the same as
|
|
34
|
+
[Describe](query-inspection.md#cli). CLI checks inherit the shared connection
|
|
35
|
+
and codec registry. Inspection-only session settings and flags override runtime startup settings.
|
|
25
36
|
|
|
26
|
-
##
|
|
37
|
+
## What gets checked
|
|
27
38
|
|
|
28
39
|
```ts
|
|
29
40
|
import { sql } from '@kequtech/tenon-orm';
|
|
@@ -35,53 +46,45 @@ export function userQuery(id: number) {
|
|
|
35
46
|
}
|
|
36
47
|
```
|
|
37
48
|
|
|
38
|
-
Recognition follows imported symbols
|
|
39
|
-
aliases.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
PostgreSQL Describe parses/analyzes each complete recovered variant and returns parameter OIDs and
|
|
43
|
-
ordered result metadata. It can take analysis locks. Run against the intended schema, database role,
|
|
44
|
-
and search path; a representative development/test database is appropriate. Session and timeout
|
|
45
|
-
flags are the same as [Describe](query-inspection.md#cli): --role, --search-path, --time-zone,
|
|
46
|
-
--connect-timeout-ms, --statement-timeout-ms, and --lock-timeout-ms. DATABASE_URL is required by the CLI.
|
|
47
|
-
|
|
48
|
-
Findings have three kinds:
|
|
49
|
-
|
|
50
|
-
| Kind | Meaning | Exit status |
|
|
51
|
-
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
|
52
|
-
| mismatch | Definite contradiction, such as a missing required result property, int8 declared as number, incompatible array elements, invalid SQL, or an invalid driver input type | Failure |
|
|
53
|
-
| unchecked | SQL structure cannot be recovered, a composition limit is exceeded, or inspection cannot complete | Failure |
|
|
54
|
-
| unproven | Available evidence cannot prove or disprove the declared type | Does not fail |
|
|
55
|
-
|
|
56
|
-
Declared types receive the benefit of the doubt when evidence is inconclusive. SQL result nullability,
|
|
57
|
-
JSON members, branded/literal refinements, and unfamiliar custom decoder types remain unproven. A
|
|
58
|
-
narrow declaration is not rejected merely because Describe supplies a broader representation. Each
|
|
59
|
-
SQL variant must fit at least one member of a declared result union. Additional selected columns are
|
|
60
|
-
allowed by structural result types; missing required properties fail.
|
|
61
|
-
|
|
62
|
-
Input and read contracts are separate: int8 accepts exact text, bigint, or an explicit encoder and decodes
|
|
63
|
-
as exact strings by default. Plain bigint, Date, and supported native/polyfilled Temporal inputs use
|
|
64
|
-
OID 0 and server inference, just as strings and numbers do. Boolean, bytes, and recoverable pgclear
|
|
65
|
-
json/param calls supply the same explicit OID hints as execution. Unions with different hints produce
|
|
66
|
-
variants. Accepted input types do not prove server ranges, valid Date values, or textual formats.
|
|
67
|
-
Plain objects, arrays, and undefined contradict pgclear's bind API; use explicit parameters or omission. A declared opaque pgclear Parameter is trusted, with its OID
|
|
68
|
-
reported as unproven. Describe then uses server inference; dependent result comparisons and inference
|
|
69
|
-
errors are unproven rather than contradictions. This is a stated limitation, not validation of the
|
|
70
|
-
unknown encoder.
|
|
49
|
+
Recognition follows Tenon's imported symbols through aliases, namespaces, barrel re-exports, and
|
|
50
|
+
const aliases. Unrelated functions named `sql` are ignored. Fixed-text `unsafeSql` calls with fixed
|
|
51
|
+
values lists are also supported. Each recoverable SQL variant goes through PostgreSQL Describe;
|
|
52
|
+
parameter OIDs and result columns are compared with the declared contracts.
|
|
71
53
|
|
|
72
|
-
|
|
54
|
+
| Finding | Meaning | Fails the command? |
|
|
55
|
+
| ----------- | --------------------------------------------------------------------------- | ------------------ |
|
|
56
|
+
| `mismatch` | A definite contradiction, invalid SQL, or invalid input type | Yes |
|
|
57
|
+
| `unchecked` | SQL cannot be recovered, a limit is exceeded, or inspection cannot complete | Yes |
|
|
58
|
+
| `unproven` | Metadata cannot prove or disprove the declaration | No |
|
|
59
|
+
|
|
60
|
+
Missing required result properties fail; extra selected columns are allowed by structural types.
|
|
61
|
+
Each variant must fit at least one member of a declared result union. Nullability, JSON members,
|
|
62
|
+
literal/branded refinements, and unfamiliar decoder types may remain unproven. A broader metadata
|
|
63
|
+
type alone does not reject a narrower application declaration.
|
|
73
64
|
|
|
74
|
-
|
|
65
|
+
Input and output contracts are separate. For example, an int8 input accepts exact text, bigint, or
|
|
66
|
+
an explicit encoder; its default output is a string. Strings, numbers, bigint, Date, and supported
|
|
67
|
+
Temporal inputs use OID 0/server inference. Booleans, bytes, and recoverable `json`/`param` calls
|
|
68
|
+
supply the same OID hints as execution. Unions with different hints create variants. Input types do
|
|
69
|
+
not prove value ranges, valid Dates, or textual formats.
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
A plain object, array, or undefined input contradicts the driver's bind API. An opaque `Parameter`
|
|
72
|
+
is trusted, with its unknown OID reported as unproven. Describe uses server inference in that case;
|
|
73
|
+
dependent result comparisons and inference errors remain unproven. This does not validate the encoder.
|
|
74
|
+
|
|
75
|
+
## Responding to findings
|
|
81
76
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
- **Mismatch:** correct the SQL or declared contract, then recheck. For int8 output, use string under
|
|
78
|
+
default codecs; for arrays/objects as inputs, choose the documented encoder.
|
|
79
|
+
- **Unchecked:** read the reason. An explicit shape or fixed composition may make it recoverable.
|
|
80
|
+
Runtime-dependent SQL needs database tests. The command fails while unchecked sites remain in
|
|
81
|
+
the selected scope; narrow the target to check supported sites separately and retain visibility
|
|
82
|
+
of the remaining coverage.
|
|
83
|
+
- **Unproven:** retain the declaration when justified, with validation/tests for what metadata cannot
|
|
84
|
+
establish. A note alone does not fail the command.
|
|
85
|
+
|
|
86
|
+
For example, generated update fields are optional and can disappear from SQL. If application
|
|
87
|
+
validation always supplies a boolean, declare that required contract:
|
|
85
88
|
|
|
86
89
|
```ts
|
|
87
90
|
type ActivePatch = { active: boolean };
|
|
@@ -92,19 +95,14 @@ function setActive(id: number, patch: ActivePatch) {
|
|
|
92
95
|
}
|
|
93
96
|
```
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
hide this difference. Narrow a CLI target to check supported sites independently; report the
|
|
98
|
-
remaining unchecked sites in the project's validation workflow.
|
|
99
|
-
|
|
100
|
-
Exit zero means no definite mismatch or unchecked site was reported in the selected scope. It does
|
|
101
|
-
not mean JSON members, nullability, cardinality, or every runtime branch was verified. No query sites
|
|
102
|
-
also fails, so an empty or incorrectly selected file cannot appear to pass.
|
|
98
|
+
If omission is intended, keep optional fields and test the SQL/effects. An explicit column list does
|
|
99
|
+
not make optional fields required. Avoid assertions that merely hide this distinction.
|
|
103
100
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
Exit 0 means no mismatch or unchecked site in the selected scope. It does not establish JSON shape,
|
|
102
|
+
nullability, cardinality, or every runtime branch. No query sites also fails. Independent queries
|
|
103
|
+
continue after an unsupported site, and a recoverable branch can expose a mismatch even when another
|
|
104
|
+
branch is unchecked. Diagnostics include locations, codes, parameter/column identifiers, expected
|
|
105
|
+
and observed types, and variant numbers. `--json` returns the structured report.
|
|
108
106
|
|
|
109
107
|
## Supported composition
|
|
110
108
|
|
|
@@ -116,21 +114,22 @@ const query = sql<Summary>`SELECT id, email FROM public.users
|
|
|
116
114
|
```
|
|
117
115
|
|
|
118
116
|
The checker follows inline fragments and const bindings with recoverable initializers, fixed
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
117
|
+
identifiers, fixed join lists/separators, and ternaries with recoverable branches. Both branches are
|
|
118
|
+
checked without evaluating the condition.
|
|
119
|
+
|
|
120
|
+
Reusing a conditional fragment preserves its branch choice. Separate ternaries using the same const
|
|
121
|
+
condition or an unwritten function parameter also share that choice, including simple negation.
|
|
122
|
+
Repeated mutable guards with unresolved correlation are unchecked. General control-flow reachability
|
|
123
|
+
is not inferred.
|
|
124
|
+
|
|
125
|
+
Limits are 16 complete variants per query, including parameter OID alternatives, and 32 levels of
|
|
126
|
+
composition references. Arbitrary fragment factories, function-body reconstruction, loops, dynamic
|
|
127
|
+
identifiers, mutable/escaping fragment lists, and runtime joins are unsupported. These can be valid
|
|
128
|
+
runtime SQL, but need database tests. Unknown source is never replaced with invented SQL.
|
|
130
129
|
|
|
131
130
|
## Object expansion
|
|
132
131
|
|
|
133
|
-
For checking, insertValues and assignments
|
|
132
|
+
For checking, `insertValues` and `assignments` need an explicit finite shape type:
|
|
134
133
|
|
|
135
134
|
```ts
|
|
136
135
|
type NewUser = { email: string; active: boolean };
|
|
@@ -141,24 +140,40 @@ function createUser(input: NewUser) {
|
|
|
141
140
|
}
|
|
142
141
|
```
|
|
143
142
|
|
|
144
|
-
The
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
as unproven. Prefer an explicit list when passing application objects.
|
|
143
|
+
The shape is a trusted expansion contract; the checker does not read the object. Prefer an explicit
|
|
144
|
+
column list so runtime and checking select the same keys/order. Without one, the declared shape is
|
|
145
|
+
assumed to describe exactly the expanded fields. Structural typing cannot prove the absence of
|
|
146
|
+
extra own keys or their order, so that assumption is unproven.
|
|
149
147
|
|
|
150
|
-
Selected fields must be required and exclude undefined. Optional presence changes SQL through
|
|
151
|
-
omission or DEFAULT
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
Selected fields must be required and exclude undefined. Optional presence changes the SQL through
|
|
149
|
+
omission or DEFAULT and is unchecked. Unselected optional properties are fine. Index signatures,
|
|
150
|
+
unresolved generic key sets, shape unions, and unknown shapes are unsupported. Fixed literal batches
|
|
151
|
+
are supported; unknown batch lengths are not. Runtime helpers still accept inferred shapes and
|
|
152
|
+
optional fields.
|
|
155
153
|
|
|
156
154
|
## Programmatic use and custom codecs
|
|
157
155
|
|
|
158
|
-
The checking API has
|
|
156
|
+
The checking API has a separate entry point, so ordinary queries do not load TypeScript:
|
|
159
157
|
|
|
160
158
|
```ts
|
|
161
159
|
import { checkQueries } from '@kequtech/tenon-orm/check';
|
|
160
|
+
|
|
161
|
+
const report = await checkQueries({
|
|
162
|
+
connection,
|
|
163
|
+
project: 'tsconfig.json',
|
|
164
|
+
target: 'src/users.ts',
|
|
165
|
+
});
|
|
166
|
+
if (!report.ok) process.exitCode = 1;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The report has `ok`, `project`, `queries`, `diagnostics`, and available inspection `context`. Options
|
|
170
|
+
also accept `session`, `timeouts`, `codecs`, and `readTypes`. `connection` is a URI or pool-option
|
|
171
|
+
values; its `codecs` are inherited unless the explicit inspection `codecs` argument is supplied.
|
|
172
|
+
The programmatic API does not discover shared configuration.
|
|
173
|
+
|
|
174
|
+
For a custom codec, pass the same registry as runtime queries. A text codec may declare its output:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
162
177
|
import { defaultCodecs } from '@kequtech/tenon-orm';
|
|
163
178
|
|
|
164
179
|
const codecs = defaultCodecs.with({
|
|
@@ -169,32 +184,19 @@ const codecs = defaultCodecs.with({
|
|
|
169
184
|
typescript: 'import("./src/types.js").DecodedInteger',
|
|
170
185
|
},
|
|
171
186
|
});
|
|
172
|
-
const report = await checkQueries({
|
|
173
|
-
connection,
|
|
174
|
-
project: 'tsconfig.json',
|
|
175
|
-
target: 'src/users.ts:23',
|
|
176
|
-
codecs,
|
|
177
|
-
});
|
|
178
|
-
if (!report.ok) process.exitCode = 1;
|
|
187
|
+
const report = await checkQueries({ connection, project: 'tsconfig.json', codecs });
|
|
179
188
|
```
|
|
180
189
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
script calling this API. The application chooses that script's configuration; the checker does not
|
|
195
|
-
import configuration or an application entry point automatically.
|
|
196
|
-
|
|
197
|
-
pgclear owns execution, Describe, value conversion, and codec representation metadata. Tenon owns
|
|
198
|
-
schema queries and declaration rendering as well as source discovery, composition recovery,
|
|
199
|
-
TypeScript comparison, diagnostics, and file output.
|
|
200
|
-
`tenon generate --check` remains a separate generated-file freshness check.
|
|
190
|
+
Here `src/types.ts` must export `DecodedInteger = { raw: string }`. Registry metadata describes
|
|
191
|
+
non-NULL output, including derived arrays. Type expressions compile in the application's project
|
|
192
|
+
without importing/executing its modules; referenced dependencies/types must be available.
|
|
193
|
+
|
|
194
|
+
Use `readTypes: { 23: 'import("./src/types.js").DecodedInteger' }` only for a decoder with no known
|
|
195
|
+
registry representation. It cannot replace known metadata or claim JSON members for default JSON
|
|
196
|
+
decoding. Expressions resolve from the tsconfig directory. A codec declaration is trusted output
|
|
197
|
+
metadata, not data validation.
|
|
198
|
+
|
|
199
|
+
The CLI loads the shared configuration for custom codecs; no checking script is required. An
|
|
200
|
+
application-owned script may still call the programmatic API with explicit options. Generated-file
|
|
201
|
+
freshness remains a separate `tenon generate --check` operation. For behavior tests, see
|
|
202
|
+
[query inspection and testing](query-inspection.md).
|