@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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Check authored queries
|
|
2
|
+
|
|
3
|
+
`tenon check` compares recoverable Tenon SQL with declared TypeScript contracts and the selected
|
|
4
|
+
PostgreSQL database. It reads source and types without importing the application, evaluating parameter
|
|
5
|
+
expressions, binding values, or executing application SQL. Normal query execution continues to trust
|
|
6
|
+
annotations and performs no additional runtime validation.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npx tenon check
|
|
10
|
+
npx tenon check src/users.ts
|
|
11
|
+
npx tenon check src/users.ts:23
|
|
12
|
+
npx tenon check src/users.ts:23 --project tsconfig.json --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Omit the target to inspect the TypeScript project's source files. A file target checks all recognized
|
|
16
|
+
queries in that file. A one-based line selects the complete query containing that line, including a
|
|
17
|
+
multiline SQL template or its variable declaration. A line with no query or several queries fails
|
|
18
|
+
explicitly. A fragment alone is not a complete query target.
|
|
19
|
+
|
|
20
|
+
The checker discovers tsconfig.json from the file's directory or current working directory. Use
|
|
21
|
+
`--project` for another configuration or project directory. The target must belong to that project.
|
|
22
|
+
The checker uses the bundled TypeScript 6 compiler with the project's context, imports, aliases,
|
|
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.
|
|
25
|
+
|
|
26
|
+
## Contracts and findings
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { sql } from '@kequtech/tenon-orm';
|
|
30
|
+
import type { Database } from './generated-schema.js';
|
|
31
|
+
|
|
32
|
+
type Summary = Pick<Database['users']['row'], 'id' | 'email'>;
|
|
33
|
+
export function userQuery(id: number) {
|
|
34
|
+
return sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Recognition follows imported symbols, including aliases, namespaces, barrel re-exports, and const
|
|
39
|
+
aliases. A different library's function named sql is not a Tenon query. Both sql templates and
|
|
40
|
+
unsafeSql with fixed SQL and a fixed values list are supported.
|
|
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.
|
|
71
|
+
|
|
72
|
+
## Responding to findings
|
|
73
|
+
|
|
74
|
+
Read each diagnostic's kind and code, then check the selected SQL and input/result declarations:
|
|
75
|
+
|
|
76
|
+
| Finding | Action |
|
|
77
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `mismatch` | Correct the SQL or declared contract. For an int8 column, use a string result type; for an invalid bind, use the documented encoder. Recheck after the change. |
|
|
79
|
+
| `unchecked` | Inspect the reason. Recoverable SQL may need an explicit shape or fixed composition; runtime-dependent SQL needs database tests. The command fails until unchecked sites are removed from the selected scope or made checkable. |
|
|
80
|
+
| `unproven` | Retain the declared contract when appropriate, with application validation and tests for the property metadata cannot establish. This note alone does not fail the command. |
|
|
81
|
+
|
|
82
|
+
For example, a generated `Database['users']['update']` makes `active` optional. An assignments
|
|
83
|
+
fragment can omit it, so checking that shape is unsupported even when a particular object currently
|
|
84
|
+
contains `active`. If a function always receives a validated boolean, declare that required shape:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
type ActivePatch = { active: boolean };
|
|
88
|
+
|
|
89
|
+
function setActive(id: number, patch: ActivePatch) {
|
|
90
|
+
return sql`UPDATE public.users SET ${sql.assignments<ActivePatch>(patch, ['active'])}
|
|
91
|
+
WHERE id = ${id}`;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
When omission is intentional, retain the optional input and test the resulting SQL and effects.
|
|
96
|
+
An explicit column list does not make an optional property required. Avoid assertions that merely
|
|
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.
|
|
103
|
+
|
|
104
|
+
The report includes source locations, parameter/column identifiers, expected and observed types, and
|
|
105
|
+
variant numbers. An unsupported site does not stop independent sites. Recoverable branches can still
|
|
106
|
+
expose mismatches when another branch is unchecked. The default output is readable diagnostics and
|
|
107
|
+
counts; --json returns the structured report.
|
|
108
|
+
|
|
109
|
+
## Supported composition
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const direction = descending ? sql.fragment`DESC` : sql.fragment`ASC`;
|
|
113
|
+
const predicates = [sql.fragment`active = ${true}`, sql.fragment`email <> ${''}`] as const;
|
|
114
|
+
const query = sql<Summary>`SELECT id, email FROM public.users
|
|
115
|
+
WHERE ${sql.join(predicates, 'AND')} ORDER BY email ${direction}`;
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The checker follows inline fragments and const bindings with recoverable initializers, fixed
|
|
119
|
+
identifier names, fixed join lists/separators, and ternaries with recoverable branches. It checks
|
|
120
|
+
both branches without evaluating the condition. Reusing the same conditional fragment preserves
|
|
121
|
+
its branch choice. Separate ternaries using a const condition or an unwritten function parameter also share that choice,
|
|
122
|
+
including simple negation. Repeated mutable guards with unresolved correlation are unchecked. Branch
|
|
123
|
+
reachability through general application control flow is not inferred.
|
|
124
|
+
|
|
125
|
+
Each query has a fixed budget of 16 complete variants, including parameter OID alternatives, and a
|
|
126
|
+
32-reference composition depth limit. The checker does not evaluate arbitrary fragment factories,
|
|
127
|
+
walk function bodies to reconstruct returned SQL, execute loops, or accept lists that can be mutated
|
|
128
|
+
or escape to arbitrary application code. A runtime fragment, identifier, mutable fragment binding,
|
|
129
|
+
or dynamic join becomes unchecked. Unknown source is never replaced with invented SQL.
|
|
130
|
+
|
|
131
|
+
## Object expansion
|
|
132
|
+
|
|
133
|
+
For checking, insertValues and assignments require an explicit finite shape type:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
type NewUser = { email: string; active: boolean };
|
|
137
|
+
|
|
138
|
+
function createUser(input: NewUser) {
|
|
139
|
+
return sql<{ id: number }>`INSERT INTO public.users
|
|
140
|
+
${sql.insertValues<NewUser>(input, ['email', 'active'])} RETURNING id`;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The type is a trusted expansion contract; the checker does not read the object. Explicit column
|
|
145
|
+
lists give runtime and checker the same selected keys and order. Without a list, the declared
|
|
146
|
+
shape is assumed to describe exactly the expanded fields; TypeScript's structural typing does not
|
|
147
|
+
prove the absence of extra own properties or their enumeration order. This assumption is reported
|
|
148
|
+
as unproven. Prefer an explicit list when passing application objects.
|
|
149
|
+
|
|
150
|
+
Selected fields must be required and exclude undefined. Optional presence changes SQL through
|
|
151
|
+
omission or DEFAULT, so it is unchecked in this version. Unselected optional properties are fine.
|
|
152
|
+
Index signatures, unresolved generic key sets, unions of shapes, and unknown shapes are unchecked.
|
|
153
|
+
Fixed literal batches are supported; unknown batch lengths are unchecked. Runtime helpers can still
|
|
154
|
+
be used with inferred shapes and optional fields, but those query sites cannot be checked yet.
|
|
155
|
+
|
|
156
|
+
## Programmatic use and custom codecs
|
|
157
|
+
|
|
158
|
+
The checking API has its own entry point so ordinary execution does not load TypeScript:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { checkQueries } from '@kequtech/tenon-orm/check';
|
|
162
|
+
import { defaultCodecs } from '@kequtech/tenon-orm';
|
|
163
|
+
|
|
164
|
+
const codecs = defaultCodecs.with({
|
|
165
|
+
oid: 23,
|
|
166
|
+
text: {
|
|
167
|
+
encode: (value: { raw: string }) => value.raw,
|
|
168
|
+
decode: (raw) => ({ raw }),
|
|
169
|
+
typescript: 'import("./src/types.js").DecodedInteger',
|
|
170
|
+
},
|
|
171
|
+
});
|
|
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;
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The example assumes `src/types.ts` exports `DecodedInteger = { raw: string }`. Pass the same
|
|
182
|
+
registry used by execution. Its known non-NULL read expressions, including declared custom types and
|
|
183
|
+
derived arrays, come from `codecs.typescript(oid)` and `codecs.typescriptTypes()`. Tenon compiles them
|
|
184
|
+
in the application's TypeScript context without importing or executing application modules.
|
|
185
|
+
|
|
186
|
+
Supply `readTypes: { 23: 'import("./src/types.js").DecodedInteger' }` only when that decoder has no
|
|
187
|
+
known representation, for example when an existing codec cannot be annotated. A mapping cannot
|
|
188
|
+
replace a known registry representation or claim a JSON member shape for default JSON decoding.
|
|
189
|
+
Type expressions resolve from the tsconfig directory; referenced application types/dependencies must
|
|
190
|
+
be available there. A decoder declaration describes what conversion returns, not a validation pass
|
|
191
|
+
performed by the checker.
|
|
192
|
+
|
|
193
|
+
The CLI uses default codecs. Custom registry configuration belongs in an application-owned checking
|
|
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.
|
package/docs/query-inspection.md
CHANGED
|
@@ -22,6 +22,9 @@ const description = await describeQuery({ connection, query });
|
|
|
22
22
|
query to inspection and execution. Strings use PostgreSQL `$1` placeholders; templates use
|
|
23
23
|
`${value}`. Cast standalone parameters so PostgreSQL can infer their types without runtime values.
|
|
24
24
|
Inspection does not read `sql<Row>`, check `query.values`, or compare an annotation with SQL.
|
|
25
|
+
For SQL with ambiguous parameters, pass `{ text, parameterTypes: [oid, ...] }`; these public
|
|
26
|
+
pgclear Describe hints require no values. Describe still performs server parsing/analysis and may
|
|
27
|
+
take locks.
|
|
25
28
|
|
|
26
29
|
## Session and timeouts
|
|
27
30
|
|
|
@@ -39,16 +42,15 @@ does not load that role's login defaults. Omitted settings use the inspection co
|
|
|
39
42
|
Explicit undefined behaves like omission. Unknown keys and invalid settings fail.
|
|
40
43
|
|
|
41
44
|
`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
|
-
tables, uncommitted changes, and transaction-local settings are not inherited.
|
|
45
|
-
|
|
46
|
-
settings for reproducibility.
|
|
45
|
+
effective statement/lock timeouts. Its mode is `separate-session` and read mapping profile is `pgclear`. Read types use
|
|
46
|
+
`codecs.typescript(oid)` from the registry passed as `codecs`, or `defaultCodecs` when omitted. Temporary
|
|
47
|
+
tables, uncommitted changes, and transaction-local settings are not inherited. pgclear uses explicit connection configuration; it does not read PG environment variables, pgpass,
|
|
48
|
+
or service files. Use explicit URI/session settings for reproducibility.
|
|
47
49
|
|
|
48
50
|
Defaults are a 3-second connection/authentication timeout, a 5-second limit per server statement,
|
|
49
51
|
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.
|
|
51
|
-
|
|
52
|
+
settings cannot disable the statement/lock bounds. pg-specific `query_timeout` and `options`
|
|
53
|
+
URI parameters are unsupported; use the documented timeout and session options. The owned client closes after inspection. These are operation limits, not a
|
|
52
54
|
single overall deadline.
|
|
53
55
|
|
|
54
56
|
## Result fields
|
|
@@ -66,12 +68,34 @@ Known read types in `rowType` include null; unknown types already include it. So
|
|
|
66
68
|
cannot establish result nullability through joins, expressions, or grouping. JSON structure stays
|
|
67
69
|
unknown. Handle findings by code:
|
|
68
70
|
|
|
69
|
-
| Code | Meaning
|
|
70
|
-
| --------------------- |
|
|
71
|
-
| `nullability-unknown` | Result nullability is unresolved
|
|
72
|
-
| `shape-unknown` | Listed output columns have unknown shapes/read types
|
|
73
|
-
| `unmapped-type` |
|
|
74
|
-
| `duplicate-column` | Repeated result names
|
|
71
|
+
| Code | Meaning |
|
|
72
|
+
| --------------------- | --------------------------------------------------------------------- |
|
|
73
|
+
| `nullability-unknown` | Result nullability is unresolved |
|
|
74
|
+
| `shape-unknown` | Listed output columns have unknown shapes/read types |
|
|
75
|
+
| `unmapped-type` | The selected registry cannot identify a custom decoder representation |
|
|
76
|
+
| `duplicate-column` | Repeated result names reject during object-row execution |
|
|
77
|
+
|
|
78
|
+
## Custom codecs
|
|
79
|
+
|
|
80
|
+
Pass the runtime registry explicitly. Inspection decodes only its own catalog/context queries with
|
|
81
|
+
default codecs; the selected registry determines the reported application read representations.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { builtin, defaultCodecs } from '@kequtech/tenon-orm';
|
|
85
|
+
|
|
86
|
+
const codecs = defaultCodecs.with(builtin.jsonbText);
|
|
87
|
+
const description = await describeQuery({ connection, query, codecs });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A raw JSON decoder reports string instead of unknown. Scalar overrides affect derived built-in
|
|
91
|
+
arrays; explicit array overrides win. A custom decoder can declare `text.typescript` to expose its
|
|
92
|
+
actual output expression, including an application type import. Describe includes that expression
|
|
93
|
+
in its conservative row declaration; generation and source checking consume the same metadata.
|
|
94
|
+
Without a known representation, an unfamiliar decoder reports `unknown` with an `unmapped-type`
|
|
95
|
+
issue, even when its catalog type is a known scalar, enum, or domain. Supply an application
|
|
96
|
+
representation when comparing such results with authored types. Catalog base types do
|
|
97
|
+
not replace the wire OID's selected decoder contract. Parameter read representations are not input
|
|
98
|
+
compatibility rules: text can be a valid int8 input while int8 results decode as exact strings.
|
|
75
99
|
|
|
76
100
|
## Test the same statement
|
|
77
101
|
|
|
@@ -134,3 +158,6 @@ The CLI uses `DATABASE_URL` and prints JSON. Session flags are `--role`, `--sear
|
|
|
134
158
|
|
|
135
159
|
Exit 0 means description succeeded, including reports with unresolved findings. Exit 1 means input,
|
|
136
160
|
connection, timeout, or PostgreSQL analysis failed. The inspected statement is never executed.
|
|
161
|
+
|
|
162
|
+
See [source-aware checking](query-checking.md) for the separate TypeScript/SQL contract
|
|
163
|
+
checking command. `tenon generate --check` only checks generated-file freshness.
|