@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.
@@ -1,16 +1,17 @@
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
 
9
9
  ```ts
10
+ // scripts/inspect-query.ts
10
11
  import { describeQuery, sql } from '@kequtech/tenon-orm';
12
+ import config from '../tenon.config.ts';
11
13
 
12
- const connection = process.env.DATABASE_URL;
13
- if (!connection) throw new Error('DATABASE_URL is required');
14
+ const connection = config.connection;
14
15
 
15
16
  const query = sql<{ id: number; total: string }>`
16
17
  SELECT ${42}::integer AS id, count(*) AS total FROM pg_catalog.pg_class
@@ -18,16 +19,19 @@ const query = sql<{ id: number; total: string }>`
18
19
  const description = await describeQuery({ connection, query });
19
20
  ```
20
21
 
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.
22
+ Pass the same completed query to inspection and execution. `query` accepts SQL text or an object
23
+ with `text`, including a Tenon `SqlQuery`. Raw SQL uses `$1` placeholders; templates use `${value}`.
24
+ Inspection checks neither `query.values` nor its `sql<Row>` annotation.
25
+
26
+ Cast standalone parameters so PostgreSQL can infer their types without values. Alternatively pass
27
+ `query: { text, parameterTypes: [oid, ...] }` for explicit Describe hints. PostgreSQL still parses
28
+ and analyzes the statement, which can take locks.
28
29
 
29
30
  ## Session and timeouts
30
31
 
32
+ Inspection opens and closes its own short-lived session. `connection` accepts a PostgreSQL URI or pgclear pool-option values;
33
+ it does not reuse a runtime pool or transaction:
34
+
31
35
  ```ts
32
36
  const description = await describeQuery({
33
37
  connection,
@@ -37,48 +41,51 @@ const description = await describeQuery({
37
41
  });
38
42
  ```
39
43
 
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.
44
+ Match runtime settings. Optional `session.role` uses SET ROLE when permitted; it does not load that
45
+ role's login defaults. Omitted settings use the inspection connection's defaults. Explicit undefined
46
+ behaves like omission. Unknown keys and invalid settings reject.
43
47
 
44
48
  `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 |
49
+ effective statement/lock timeouts. Mode is `separate-session`, read mapping profile `pgclear`.
50
+ Temporary tables, uncommitted changes, and transaction-local state are not inherited. PG environment
51
+ variables, pgpass, service files, and `.env` are not loaded.
52
+
53
+ | Timeout | Default | Scope |
54
+ | ------------- | -------- | ----------------------------- |
55
+ | `connectMs` | 3,000 ms | Connection and authentication |
56
+ | `statementMs` | 5,000 ms | Each server statement |
57
+ | `lockMs` | 1,000 ms | Lock wait |
58
+
59
+ Overrides are positive integer milliseconds up to 2,147,483,647. These are operation bounds, not a
60
+ single overall deadline. URI settings cannot disable statement/lock bounds. pg-specific
61
+ `query_timeout` and `options` URI parameters are unsupported; use the options above.
62
+
63
+ ## Read the report
64
+
65
+ | Field | Meaning |
66
+ | ----------------------- | ----------------------------------------------------------------------------------------- |
67
+ | `parameters` | OIDs, qualified PostgreSQL names, and read representations; these are not input contracts |
68
+ | `columns` | Ordered names, PostgreSQL types, read representations, and available source metadata |
69
+ | `columns[].nullability` | Always `"unknown"`; source `notNull` is informational |
70
+ | `rowType` | Conservative `DescribedRow` declaration, or null for no columns/duplicate names |
71
+ | `issues` | Findings with stable `code` and descriptive messages |
72
+ | `context` | Effective inspection-session settings |
73
+
74
+ Known read types in `rowType` include null; unknown already includes it. A source column's NOT NULL
75
+ constraint cannot prove result nullability through outer joins, expressions, or grouping. JSON
76
+ members remain unknown.
77
+
78
+ | Finding code | Meaning |
79
+ | --------------------- | ----------------------------------------------------- |
80
+ | `nullability-unknown` | Result nullability is unresolved |
81
+ | `shape-unknown` | Listed columns have unknown shapes/read types |
82
+ | `unmapped-type` | A custom decoder has no known representation |
83
+ | `duplicate-column` | Repeated result names would fail object-row execution |
77
84
 
78
85
  ## Custom codecs
79
86
 
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.
87
+ Programmatic inspection inherits `connection.codecs` when connection-option values are supplied.
88
+ With a URI, pass the runtime registry as `codecs`; omission uses defaults:
82
89
 
83
90
  ```ts
84
91
  import { builtin, defaultCodecs } from '@kequtech/tenon-orm';
@@ -87,21 +94,21 @@ const codecs = defaultCodecs.with(builtin.jsonbText);
87
94
  const description = await describeQuery({ connection, query, codecs });
88
95
  ```
89
96
 
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.
97
+ That registry reports JSONB output as string. Scalar overrides affect derived built-in arrays;
98
+ explicit array overrides win. Custom `text.typescript` expressions appear in the conservative row
99
+ declaration and are shared with generation/source checking. Without known metadata, a custom
100
+ decoder remains unknown even for a familiar scalar/enum/domain OID. Catalog base types do not
101
+ replace the wire OID's decoder contract.
102
+
103
+ Inspection's own catalog/context queries use default decoding. Reported parameter read types are
104
+ not encoding rules: exact text can be a valid int8 input while its output also uses text by default.
105
+ See [schema mappings and codecs](schema-types.md#custom-codecs).
99
106
 
100
107
  ## Test the same statement
101
108
 
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:
109
+ Use a dedicated test database. For `public.users` from [Getting started](human-guide.md), assume a
110
+ fixture contains `{ id: 1, email: 'ada@example.test', active: true }` and
111
+ `{ id: 2, email: 'lin@example.test', active: false }`. Here `db` and `connection` select that database:
105
112
 
106
113
  ```ts
107
114
  import assert from 'node:assert/strict';
@@ -133,15 +140,15 @@ for (const activeOnly of [false, true]) {
133
140
  }
134
141
  ```
135
142
 
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.
143
+ Describe establishes metadata; execution tests establish behavior. Describe may succeed even when
144
+ bound values are invalid, constraints would reject a write, or the role lacks execution permission.
145
+ Test actual rows, write effects, authorization, JSON validation, and rollback. Exercise meaningful
146
+ composition variants and keep unexpected findings visible. Do not test query behavior by matching
147
+ words in documentation or SQL strings.
141
148
 
142
149
  ## CLI
143
150
 
144
- Save SQL with PostgreSQL placeholders in `query.sql`:
151
+ Save raw SQL in `query.sql`:
145
152
 
146
153
  ```sql
147
154
  SELECT $1::integer AS id, count(*) AS total FROM pg_catalog.pg_class;
@@ -152,12 +159,12 @@ npx tenon describe query.sql
152
159
  npx tenon describe query.sql --search-path app,public --time-zone UTC
153
160
  ```
154
161
 
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.
162
+ The CLI requires [shared configuration](configuration.md) and prints JSON using its connection
163
+ and codec registry. Pass `--config path` to select a specific module. Session flags are `--role`,
164
+ `--search-path`, and `--time-zone`; timeout flags are `--connect-timeout-ms`,
165
+ `--statement-timeout-ms`, and `--lock-timeout-ms`. Unsupported flags, including `--namespace` and
166
+ `--check`, reject.
161
167
 
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.
168
+ Exit 0 means inspection succeeded, even with unresolved findings. Input, connection, timeout, or
169
+ PostgreSQL analysis failures exit 1. The inspected statement is never executed. Generated-file
170
+ freshness uses `tenon generate --check`, a separate operation.