@kequtech/tenon-orm 0.4.0 → 0.4.2

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.
@@ -1,9 +1,16 @@
1
- # Check authored queries
1
+ # Checking query types
2
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.
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 checker reads source without importing the application, evaluating input expressions, binding
8
+ 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
+ Set `DATABASE_URL`, then select a project, file, or one query:
7
14
 
8
15
  ```sh
9
16
  npx tenon check
@@ -12,18 +19,21 @@ 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
- 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.
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 checker discovers `tsconfig.json` from the target's directory or current working directory.
27
+ Use `--project` for another config or project directory; targets must belong to that project.
28
+ It uses the bundled TypeScript 6 compiler and the project's imports, aliases, indexed access,
29
+ `Pick`, unions, and path mappings. `strictNullChecks` is required. Invalid configuration and compiler
30
+ errors in selected files fail the report.
19
31
 
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.
32
+ Match runtime role, search path, timezone, and codecs. Session/timeout flags are the same as
33
+ [Describe](query-inspection.md#cli). CLI checks use default codecs; use the programmatic API for
34
+ a custom registry.
25
35
 
26
- ## Contracts and findings
36
+ ## What gets checked
27
37
 
28
38
  ```ts
29
39
  import { sql } from '@kequtech/tenon-orm';
@@ -35,53 +45,45 @@ export function userQuery(id: number) {
35
45
  }
36
46
  ```
37
47
 
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.
48
+ Recognition follows Tenon's imported symbols through aliases, namespaces, barrel re-exports, and
49
+ const aliases. Unrelated functions named `sql` are ignored. Fixed-text `unsafeSql` calls with fixed
50
+ values lists are also supported. Each recoverable SQL variant goes through PostgreSQL Describe;
51
+ parameter OIDs and result columns are compared with the declared contracts.
71
52
 
72
- ## Responding to findings
53
+ | Finding | Meaning | Fails the command? |
54
+ | ----------- | --------------------------------------------------------------------------- | ------------------ |
55
+ | `mismatch` | A definite contradiction, invalid SQL, or invalid input type | Yes |
56
+ | `unchecked` | SQL cannot be recovered, a limit is exceeded, or inspection cannot complete | Yes |
57
+ | `unproven` | Metadata cannot prove or disprove the declaration | No |
58
+
59
+ Missing required result properties fail; extra selected columns are allowed by structural types.
60
+ Each variant must fit at least one member of a declared result union. Nullability, JSON members,
61
+ literal/branded refinements, and unfamiliar decoder types may remain unproven. A broader metadata
62
+ type alone does not reject a narrower application declaration.
73
63
 
74
- Read each diagnostic's kind and code, then check the selected SQL and input/result declarations:
64
+ Input and output contracts are separate. For example, an int8 input accepts exact text, bigint, or
65
+ an explicit encoder; its default output is a string. Strings, numbers, bigint, Date, and supported
66
+ Temporal inputs use OID 0/server inference. Booleans, bytes, and recoverable `json`/`param` calls
67
+ supply the same OID hints as execution. Unions with different hints create variants. Input types do
68
+ not prove value ranges, valid Dates, or textual formats.
75
69
 
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. |
70
+ A plain object, array, or undefined input contradicts the driver's bind API. An opaque `Parameter`
71
+ is trusted, with its unknown OID reported as unproven. Describe uses server inference in that case;
72
+ dependent result comparisons and inference errors remain unproven. This does not validate the encoder.
73
+
74
+ ## Responding to findings
81
75
 
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:
76
+ - **Mismatch:** correct the SQL or declared contract, then recheck. For int8 output, use string under
77
+ default codecs; for arrays/objects as inputs, choose the documented encoder.
78
+ - **Unchecked:** read the reason. An explicit shape or fixed composition may make it recoverable.
79
+ Runtime-dependent SQL needs database tests. The command fails while unchecked sites remain in
80
+ the selected scope; narrow the target to check supported sites separately and retain visibility
81
+ of the remaining coverage.
82
+ - **Unproven:** retain the declaration when justified, with validation/tests for what metadata cannot
83
+ establish. A note alone does not fail the command.
84
+
85
+ For example, generated update fields are optional and can disappear from SQL. If application
86
+ validation always supplies a boolean, declare that required contract:
85
87
 
86
88
  ```ts
87
89
  type ActivePatch = { active: boolean };
@@ -92,19 +94,14 @@ function setActive(id: number, patch: ActivePatch) {
92
94
  }
93
95
  ```
94
96
 
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.
97
+ If omission is intended, keep optional fields and test the SQL/effects. An explicit column list does
98
+ not make optional fields required. Avoid assertions that merely hide this distinction.
103
99
 
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.
100
+ Exit 0 means no mismatch or unchecked site in the selected scope. It does not establish JSON shape,
101
+ nullability, cardinality, or every runtime branch. No query sites also fails. Independent queries
102
+ continue after an unsupported site, and a recoverable branch can expose a mismatch even when another
103
+ branch is unchecked. Diagnostics include locations, codes, parameter/column identifiers, expected
104
+ and observed types, and variant numbers. `--json` returns the structured report.
108
105
 
109
106
  ## Supported composition
110
107
 
@@ -116,21 +113,22 @@ const query = sql<Summary>`SELECT id, email FROM public.users
116
113
  ```
117
114
 
118
115
  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.
116
+ identifiers, fixed join lists/separators, and ternaries with recoverable branches. Both branches are
117
+ checked without evaluating the condition.
118
+
119
+ Reusing a conditional fragment preserves its branch choice. Separate ternaries using the same const
120
+ condition or an unwritten function parameter also share that choice, including simple negation.
121
+ Repeated mutable guards with unresolved correlation are unchecked. General control-flow reachability
122
+ is not inferred.
123
+
124
+ Limits are 16 complete variants per query, including parameter OID alternatives, and 32 levels of
125
+ composition references. Arbitrary fragment factories, function-body reconstruction, loops, dynamic
126
+ identifiers, mutable/escaping fragment lists, and runtime joins are unsupported. These can be valid
127
+ runtime SQL, but need database tests. Unknown source is never replaced with invented SQL.
130
128
 
131
129
  ## Object expansion
132
130
 
133
- For checking, insertValues and assignments require an explicit finite shape type:
131
+ For checking, `insertValues` and `assignments` need an explicit finite shape type:
134
132
 
135
133
  ```ts
136
134
  type NewUser = { email: string; active: boolean };
@@ -141,24 +139,38 @@ function createUser(input: NewUser) {
141
139
  }
142
140
  ```
143
141
 
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.
142
+ The shape is a trusted expansion contract; the checker does not read the object. Prefer an explicit
143
+ column list so runtime and checking select the same keys/order. Without one, the declared shape is
144
+ assumed to describe exactly the expanded fields. Structural typing cannot prove the absence of
145
+ extra own keys or their order, so that assumption is unproven.
149
146
 
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.
147
+ Selected fields must be required and exclude undefined. Optional presence changes the SQL through
148
+ omission or DEFAULT and is unchecked. Unselected optional properties are fine. Index signatures,
149
+ unresolved generic key sets, shape unions, and unknown shapes are unsupported. Fixed literal batches
150
+ are supported; unknown batch lengths are not. Runtime helpers still accept inferred shapes and
151
+ optional fields.
155
152
 
156
153
  ## Programmatic use and custom codecs
157
154
 
158
- The checking API has its own entry point so ordinary execution does not load TypeScript:
155
+ The checking API has a separate entry point, so ordinary queries do not load TypeScript:
159
156
 
160
157
  ```ts
161
158
  import { checkQueries } from '@kequtech/tenon-orm/check';
159
+
160
+ const report = await checkQueries({
161
+ connection,
162
+ project: 'tsconfig.json',
163
+ target: 'src/users.ts',
164
+ });
165
+ if (!report.ok) process.exitCode = 1;
166
+ ```
167
+
168
+ The report has `ok`, `project`, `queries`, `diagnostics`, and available inspection `context`. Options
169
+ also accept `session`, `timeouts`, `codecs`, and `readTypes`. `connection` is a URI.
170
+
171
+ For a custom codec, pass the same registry as runtime queries. A text codec may declare its output:
172
+
173
+ ```ts
162
174
  import { defaultCodecs } from '@kequtech/tenon-orm';
163
175
 
164
176
  const codecs = defaultCodecs.with({
@@ -169,32 +181,19 @@ const codecs = defaultCodecs.with({
169
181
  typescript: 'import("./src/types.js").DecodedInteger',
170
182
  },
171
183
  });
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;
184
+ const report = await checkQueries({ connection, project: 'tsconfig.json', codecs });
179
185
  ```
180
186
 
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.
187
+ Here `src/types.ts` must export `DecodedInteger = { raw: string }`. Registry metadata describes
188
+ non-NULL output, including derived arrays. Type expressions compile in the application's project
189
+ without importing/executing its modules; referenced dependencies/types must be available.
190
+
191
+ Use `readTypes: { 23: 'import("./src/types.js").DecodedInteger' }` only for a decoder with no known
192
+ registry representation. It cannot replace known metadata or claim JSON members for default JSON
193
+ decoding. Expressions resolve from the tsconfig directory. A codec declaration is trusted output
194
+ metadata, not data validation.
195
+
196
+ Custom configuration belongs in an application-owned checking script; the checker does not load
197
+ application configuration automatically. Generated-file freshness remains a separate
198
+ `db.generateTypes({ out, check: true })` operation. For behavior tests, see
199
+ [query inspection and testing](query-inspection.md).
@@ -1,8 +1,8 @@
1
- # Inspect and test queries
1
+ # Inspecting and testing queries
2
2
 
3
- `describeQuery` asks PostgreSQL for a completed statement's parameter/result metadata without
4
- binding its values or executing it. Inspection is included with Tenon and opens a separate,
5
- short-lived connection. It works with SQL assembled from fragments and mutation helpers.
3
+ `describeQuery` asks PostgreSQL for a finished statement's parameter and result metadata. It does
4
+ not bind values or execute the statement. Use it to inspect SQL and to support database tests. To
5
+ compare TypeScript source annotations instead, use [query checking](query-checking.md).
6
6
 
7
7
  ## Inspect completed SQL
8
8
 
@@ -18,16 +18,19 @@ const query = sql<{ id: number; total: string }>`
18
18
  const description = await describeQuery({ connection, query });
19
19
  ```
20
20
 
21
- `query` accepts SQL text or an object with `text`, including `SqlQuery`. Pass the same completed
22
- query to inspection and execution. Strings use PostgreSQL `$1` placeholders; templates use
23
- `${value}`. Cast standalone parameters so PostgreSQL can infer their types without runtime values.
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.
21
+ Pass the same completed query to inspection and execution. `query` accepts SQL text or an object
22
+ with `text`, including a Tenon `SqlQuery`. Raw SQL uses `$1` placeholders; templates use `${value}`.
23
+ Inspection checks neither `query.values` nor its `sql<Row>` annotation.
24
+
25
+ Cast standalone parameters so PostgreSQL can infer their types without values. Alternatively pass
26
+ `query: { text, parameterTypes: [oid, ...] }` for explicit Describe hints. PostgreSQL still parses
27
+ and analyzes the statement, which can take locks.
28
28
 
29
29
  ## Session and timeouts
30
30
 
31
+ Inspection opens and closes its own short-lived session. `connection` must be a PostgreSQL URI;
32
+ it does not inherit a runtime pool or transaction:
33
+
31
34
  ```ts
32
35
  const description = await describeQuery({
33
36
  connection,
@@ -37,48 +40,50 @@ const description = await describeQuery({
37
40
  });
38
41
  ```
39
42
 
40
- Reuse the runtime URI and session settings. Optional `session.role` uses SET ROLE when permitted; it
41
- does not load that role's login defaults. Omitted settings use the inspection connection's defaults.
42
- Explicit undefined behaves like omission. Unknown keys and invalid settings fail.
43
+ Match runtime settings. Optional `session.role` uses SET ROLE when permitted; it does not load that
44
+ role's login defaults. Omitted settings use the inspection connection's defaults. Explicit undefined
45
+ behaves like omission. Unknown keys and invalid settings reject.
43
46
 
44
47
  `context` reports login/effective roles, configured/resolved search paths, timezone, database, and
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.
49
-
50
- Defaults are a 3-second connection/authentication timeout, a 5-second limit per server statement,
51
- and a 1-second lock wait. Overrides are positive integer milliseconds, at most 2,147,483,647. URI
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
54
- single overall deadline.
55
-
56
- ## Result fields
57
-
58
- | Field | Meaning |
59
- | ----------------------- | ------------------------------------------------------------------------------------------ |
60
- | `parameters` | OIDs, qualified PostgreSQL names, and read representations; not JavaScript input contracts |
61
- | `columns` | Ordered names, PostgreSQL types, read representations, and source metadata where available |
62
- | `columns[].nullability` | Always `"unknown"`; source `notNull` is informational |
63
- | `rowType` | Conservative `DescribedRow` declaration, or null for no columns/duplicate names |
64
- | `issues` | Findings with stable `code` fields and descriptive messages |
65
- | `context` | Effective inspection-session settings |
66
-
67
- Known read types in `rowType` include null; unknown types already include it. Source constraints
68
- cannot establish result nullability through joins, expressions, or grouping. JSON structure stays
69
- unknown. Handle findings by code:
70
-
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 |
48
+ effective statement/lock timeouts. Mode is `separate-session`, read mapping profile `pgclear`.
49
+ Temporary tables, uncommitted changes, and transaction-local state are not inherited. PG environment
50
+ variables, pgpass, service files, and `.env` are not loaded.
51
+
52
+ | Timeout | Default | Scope |
53
+ | ------------- | -------- | ----------------------------- |
54
+ | `connectMs` | 3,000 ms | Connection and authentication |
55
+ | `statementMs` | 5,000 ms | Each server statement |
56
+ | `lockMs` | 1,000 ms | Lock wait |
57
+
58
+ Overrides are positive integer milliseconds up to 2,147,483,647. These are operation bounds, not a
59
+ single overall deadline. URI settings cannot disable statement/lock bounds. pg-specific
60
+ `query_timeout` and `options` URI parameters are unsupported; use the options above.
61
+
62
+ ## Read the report
63
+
64
+ | Field | Meaning |
65
+ | ----------------------- | ----------------------------------------------------------------------------------------- |
66
+ | `parameters` | OIDs, qualified PostgreSQL names, and read representations; these are not input contracts |
67
+ | `columns` | Ordered names, PostgreSQL types, read representations, and available source metadata |
68
+ | `columns[].nullability` | Always `"unknown"`; source `notNull` is informational |
69
+ | `rowType` | Conservative `DescribedRow` declaration, or null for no columns/duplicate names |
70
+ | `issues` | Findings with stable `code` and descriptive messages |
71
+ | `context` | Effective inspection-session settings |
72
+
73
+ Known read types in `rowType` include null; unknown already includes it. A source column's NOT NULL
74
+ constraint cannot prove result nullability through outer joins, expressions, or grouping. JSON
75
+ members remain unknown.
76
+
77
+ | Finding code | Meaning |
78
+ | --------------------- | ----------------------------------------------------- |
79
+ | `nullability-unknown` | Result nullability is unresolved |
80
+ | `shape-unknown` | Listed columns have unknown shapes/read types |
81
+ | `unmapped-type` | A custom decoder has no known representation |
82
+ | `duplicate-column` | Repeated result names would fail object-row execution |
77
83
 
78
84
  ## Custom codecs
79
85
 
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.
86
+ Pass the runtime registry as `codecs` to report its read representations; omission uses defaults:
82
87
 
83
88
  ```ts
84
89
  import { builtin, defaultCodecs } from '@kequtech/tenon-orm';
@@ -87,21 +92,21 @@ const codecs = defaultCodecs.with(builtin.jsonbText);
87
92
  const description = await describeQuery({ connection, query, codecs });
88
93
  ```
89
94
 
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.
95
+ That registry reports JSONB output as string. Scalar overrides affect derived built-in arrays;
96
+ explicit array overrides win. Custom `text.typescript` expressions appear in the conservative row
97
+ declaration and are shared with generation/source checking. Without known metadata, a custom
98
+ decoder remains unknown even for a familiar scalar/enum/domain OID. Catalog base types do not
99
+ replace the wire OID's decoder contract.
100
+
101
+ Inspection's own catalog/context queries use default decoding. Reported parameter read types are
102
+ not encoding rules: exact text can be a valid int8 input while its output also uses text by default.
103
+ See [schema mappings and codecs](schema-types.md#custom-codecs).
99
104
 
100
105
  ## Test the same statement
101
106
 
102
- For the `public.users` table from the [human guide](human-guide.md), assume a dedicated test fixture
103
- has rows `{ id: 1, email: 'ada@example.test', active: true }` and
104
- `{ id: 2, email: 'lin@example.test', active: false }`, and `db` is its runtime connection:
107
+ Use a dedicated test database. For `public.users` from [Getting started](human-guide.md), assume a
108
+ fixture contains `{ id: 1, email: 'ada@example.test', active: true }` and
109
+ `{ id: 2, email: 'lin@example.test', active: false }`. Here `db` and `connection` select that database:
105
110
 
106
111
  ```ts
107
112
  import assert from 'node:assert/strict';
@@ -133,15 +138,15 @@ for (const activeOnly of [false, true]) {
133
138
  }
134
139
  ```
135
140
 
136
- Description and execution establish different facts. Describe can succeed for invalid bound values,
137
- a write that violates constraints, or an operation the runtime role cannot execute. Check actual
138
- rows/effects, JSON, permissions, and rollback in database tests, and run TypeScript. Inspect
139
- meaningful query variants; one instance does not cover unexercised branches. Expect only reviewed
140
- findings and keep unexpected ones visible.
141
+ Describe establishes metadata; execution tests establish behavior. Describe may succeed even when
142
+ bound values are invalid, constraints would reject a write, or the role lacks execution permission.
143
+ Test actual rows, write effects, authorization, JSON validation, and rollback. Exercise meaningful
144
+ composition variants and keep unexpected findings visible. Do not test query behavior by matching
145
+ words in documentation or SQL strings.
141
146
 
142
147
  ## CLI
143
148
 
144
- Save SQL with PostgreSQL placeholders in `query.sql`:
149
+ Save raw SQL in `query.sql`:
145
150
 
146
151
  ```sql
147
152
  SELECT $1::integer AS id, count(*) AS total FROM pg_catalog.pg_class;
@@ -152,12 +157,11 @@ npx tenon describe query.sql
152
157
  npx tenon describe query.sql --search-path app,public --time-zone UTC
153
158
  ```
154
159
 
155
- The CLI uses `DATABASE_URL` and prints JSON. Session flags are `--role`, `--search-path`, and
156
- `--time-zone`. Timeout flags are `--connect-timeout-ms`, `--statement-timeout-ms`, and
157
- `--lock-timeout-ms`. Migration/generation flags such as `--namespace` and `--check` are rejected.
158
-
159
- Exit 0 means description succeeded, including reports with unresolved findings. Exit 1 means input,
160
- connection, timeout, or PostgreSQL analysis failed. The inspected statement is never executed.
160
+ The CLI reads `DATABASE_URL` and prints JSON using default codecs. Session flags are `--role`,
161
+ `--search-path`, and `--time-zone`; timeout flags are `--connect-timeout-ms`,
162
+ `--statement-timeout-ms`, and `--lock-timeout-ms`. Unsupported flags, including `--namespace` and
163
+ `--check`, reject.
161
164
 
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.
165
+ Exit 0 means inspection succeeded, even with unresolved findings. Input, connection, timeout, or
166
+ PostgreSQL analysis failures exit 1. The inspected statement is never executed. Generated-file
167
+ freshness uses `db.generateTypes({ out, check: true })`, a separate operation.