@kequtech/tenon-orm 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +27 -5
  3. package/dist/affected-rows.js +0 -1
  4. package/dist/baseline.js +3 -5
  5. package/dist/check/contracts.d.ts +6 -0
  6. package/dist/check/contracts.js +206 -0
  7. package/dist/check/project.d.ts +18 -0
  8. package/dist/check/project.js +224 -0
  9. package/dist/check/source.d.ts +51 -0
  10. package/dist/check/source.js +629 -0
  11. package/dist/check/types.d.ts +36 -0
  12. package/dist/check/types.js +1 -0
  13. package/dist/check.d.ts +4 -0
  14. package/dist/check.js +147 -0
  15. package/dist/cli.js +38 -9
  16. package/dist/connection.d.ts +5 -12
  17. package/dist/connection.js +12 -121
  18. package/dist/database.js +5 -8
  19. package/dist/describe.js +10 -3
  20. package/dist/generate.d.ts +2 -3
  21. package/dist/generate.js +26 -24
  22. package/dist/generation/catalog.d.ts +5 -6
  23. package/dist/generation/catalog.js +7 -9
  24. package/dist/generation/render.d.ts +3 -0
  25. package/dist/generation/render.js +60 -0
  26. package/dist/generation/schema-summary.d.ts +3 -3
  27. package/dist/generation/schema-summary.js +2 -2
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +2 -0
  30. package/dist/inspection/catalog.d.ts +3 -6
  31. package/dist/inspection/catalog.js +9 -18
  32. package/dist/inspection/cli.d.ts +12 -0
  33. package/dist/inspection/cli.js +9 -4
  34. package/dist/inspection/connection.d.ts +2 -3
  35. package/dist/inspection/connection.js +20 -22
  36. package/dist/inspection/options.js +0 -6
  37. package/dist/inspection/result.d.ts +3 -2
  38. package/dist/inspection/result.js +13 -26
  39. package/dist/inspection/types.d.ts +6 -4
  40. package/dist/migrations/connection.d.ts +3 -3
  41. package/dist/migrations/connection.js +14 -21
  42. package/dist/migrations/history.d.ts +4 -4
  43. package/dist/migrations/history.js +5 -5
  44. package/dist/migrations.js +7 -9
  45. package/dist/query-options.d.ts +1 -1
  46. package/dist/query.js +1 -1
  47. package/dist/transaction.d.ts +2 -2
  48. package/dist/transaction.js +21 -77
  49. package/dist/types.d.ts +1 -1
  50. package/docs/adoption-guide.md +64 -12
  51. package/docs/agent-guide.md +57 -27
  52. package/docs/human-guide.md +53 -18
  53. package/docs/mutations.md +22 -9
  54. package/docs/query-checking.md +200 -0
  55. package/docs/query-inspection.md +40 -13
  56. package/docs/reference.md +330 -108
  57. package/package.json +11 -7
  58. package/dist/driver/types.d.ts +0 -10
  59. package/dist/driver/types.js +0 -49
  60. package/dist/generation/table-types.d.ts +0 -2
  61. package/dist/generation/table-types.js +0 -96
  62. package/dist/inspection/protocol.d.ts +0 -7
  63. package/dist/inspection/protocol.js +0 -45
@@ -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.
@@ -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 `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.
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. 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
+ 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` | A parameter or result type has no verified driver mapping |
74
- | `duplicate-column` | Repeated result names would overwrite earlier object properties |
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.