@kequtech/tenon-orm 0.1.3 → 0.2.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 +56 -0
- package/README.md +16 -20
- package/dist/affected-rows.d.ts +1 -1
- package/dist/affected-rows.js +6 -6
- package/dist/baseline.d.ts +1 -1
- package/dist/baseline.js +5 -5
- package/dist/cli.js +18 -39
- package/dist/client.js +1 -1
- package/dist/connection.d.ts +18 -13
- package/dist/connection.js +14 -43
- package/dist/database.js +4 -2
- package/dist/describe.d.ts +3 -0
- package/dist/describe.js +21 -0
- package/dist/driver/array.d.ts +5 -0
- package/dist/driver/array.js +70 -0
- package/dist/driver/lifecycle.d.ts +15 -0
- package/dist/driver/lifecycle.js +57 -0
- package/dist/driver/session.d.ts +8 -0
- package/dist/driver/session.js +92 -0
- package/dist/driver/types.d.ts +10 -0
- package/dist/driver/types.js +49 -0
- package/dist/generate.js +7 -7
- package/dist/generation/catalog.d.ts +2 -2
- package/dist/generation/catalog.js +4 -4
- package/dist/generation/output.d.ts +2 -0
- package/dist/generation/output.js +35 -0
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +11 -11
- package/dist/generation/table-types.js +2 -52
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/inspection/catalog.d.ts +25 -0
- package/dist/inspection/catalog.js +29 -0
- package/dist/inspection/cli.d.ts +22 -0
- package/dist/inspection/cli.js +34 -0
- package/dist/inspection/connection.d.ts +4 -0
- package/dist/inspection/connection.js +44 -0
- package/dist/inspection/options.d.ts +15 -0
- package/dist/inspection/options.js +49 -0
- package/dist/inspection/result.d.ts +4 -0
- package/dist/inspection/result.js +90 -0
- package/dist/inspection/types.d.ts +74 -0
- package/dist/inspection/types.js +1 -0
- package/dist/migrations/baseline-sql.d.ts +1 -1
- package/dist/migrations/baseline-sql.js +1 -1
- package/dist/migrations/connection.d.ts +2 -3
- package/dist/migrations/connection.js +15 -13
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -7
- package/dist/mutations.d.ts +0 -5
- package/dist/mutations.js +33 -41
- package/dist/query-options.d.ts +2 -2
- package/dist/query-options.js +4 -4
- package/dist/query.d.ts +3 -4
- package/dist/query.js +2 -2
- package/dist/sql.d.ts +1 -1
- package/dist/sql.js +1 -1
- package/dist/transaction-sql.js +1 -1
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +14 -13
- package/dist/types.d.ts +4 -4
- package/docs/adoption-guide.md +39 -76
- package/docs/agent-guide.md +82 -111
- package/docs/human-guide.md +42 -56
- package/docs/mutations.md +13 -13
- package/docs/query-inspection.md +128 -0
- package/docs/reference.md +81 -294
- package/package.json +10 -31
- package/dist/safeql/composition.d.ts +0 -9
- package/dist/safeql/composition.js +0 -168
- package/dist/safeql/eslint.d.ts +0 -2
- package/dist/safeql/eslint.js +0 -50
- package/dist/safeql/mutations.d.ts +0 -3
- package/dist/safeql/mutations.js +0 -74
- package/dist/safeql/plugin.d.ts +0 -3
- package/dist/safeql/plugin.js +0 -43
- package/dist/safeql/static.d.ts +0 -10
- package/dist/safeql/static.js +0 -59
- package/dist/safeql.d.ts +0 -6
- package/dist/safeql.js +0 -7
package/docs/human-guide.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Using Tenon: a human guide
|
|
2
2
|
|
|
3
3
|
Tenon lets you use PostgreSQL directly from TypeScript. You write SQL; Tenon binds its inputs and
|
|
4
|
-
executes it through
|
|
4
|
+
executes it through Postgres.js. This guide follows a small application from its first table to reusable
|
|
5
5
|
queries, transactions, and result checking.
|
|
6
6
|
|
|
7
7
|
Use the [reference](reference.md) when you need exact options or unusual cases. The
|
|
@@ -13,8 +13,7 @@ Use the [reference](reference.md) when you need exact options or unusual cases.
|
|
|
13
13
|
In an existing Node.js 24+ TypeScript project using ESM:
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
npm install @kequtech/tenon-orm
|
|
17
|
-
npm install --save-dev @types/pg
|
|
16
|
+
npm install @kequtech/tenon-orm
|
|
18
17
|
```
|
|
19
18
|
|
|
20
19
|
Use PostgreSQL 16+ and TypeScript 6+. Keep your existing strict TypeScript configuration and Node
|
|
@@ -31,13 +30,14 @@ export const db = createTenon({ connection });
|
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
Reuse this instance across the application and call `await db.close()` at shutdown. For multiple
|
|
34
|
-
databases, create an instance for each connection. You can also supply an existing
|
|
33
|
+
databases, create an instance for each connection. You can also supply an existing Postgres.js client; your
|
|
35
34
|
application then closes that pool.
|
|
36
35
|
|
|
37
36
|
## Create a table
|
|
38
37
|
|
|
39
|
-
If you already have a schema and migration runner, you can keep them. For
|
|
40
|
-
|
|
38
|
+
If you already have a schema and migration runner, you can keep them. For new schemas, prefer plural
|
|
39
|
+
`snake_case` table names and `snake_case` column names. Tenon preserves your schema's exact names.
|
|
40
|
+
Create a migration:
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
43
|
npx tenon new create_users
|
|
@@ -47,7 +47,7 @@ Fill the generated files:
|
|
|
47
47
|
|
|
48
48
|
```sql
|
|
49
49
|
-- migrations/0001_create_users.up.sql
|
|
50
|
-
CREATE TABLE public.
|
|
50
|
+
CREATE TABLE public.users (
|
|
51
51
|
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
52
52
|
email text NOT NULL UNIQUE,
|
|
53
53
|
active boolean NOT NULL DEFAULT true
|
|
@@ -56,7 +56,7 @@ CREATE TABLE public."User" (
|
|
|
56
56
|
|
|
57
57
|
```sql
|
|
58
58
|
-- migrations/0001_create_users.down.sql
|
|
59
|
-
DROP TABLE public.
|
|
59
|
+
DROP TABLE public.users;
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
For development, add a script to `package.json` that applies migrations and updates generated types:
|
|
@@ -87,10 +87,10 @@ import { sql } from '@kequtech/tenon-orm';
|
|
|
87
87
|
import { db } from './db.js';
|
|
88
88
|
|
|
89
89
|
const email = 'reader@example.test';
|
|
90
|
-
await db.query(sql`INSERT INTO public.
|
|
90
|
+
await db.query(sql`INSERT INTO public.users (email) VALUES (${email})`);
|
|
91
91
|
const user = await db.query(
|
|
92
92
|
sql<{ id: number; email: string }>`
|
|
93
|
-
SELECT id, email FROM public.
|
|
93
|
+
SELECT id, email FROM public.users WHERE email = ${email}
|
|
94
94
|
`,
|
|
95
95
|
{ returns: 'oneOrNone' },
|
|
96
96
|
);
|
|
@@ -101,20 +101,20 @@ separately from the SQL text, even when the value contains SQL.
|
|
|
101
101
|
|
|
102
102
|
The type describes one selected row. `returns: 'oneOrNone'` gives you that row or `undefined`; it
|
|
103
103
|
throws if the query returns more than one row. Use `'one'` when absence should also be an error,
|
|
104
|
-
`'many'` for an array, or omit `returns` for the
|
|
104
|
+
`'many'` for an array, or omit `returns` for the native result array with `.count` and `.columns`.
|
|
105
105
|
These options do not change your SQL.
|
|
106
106
|
|
|
107
107
|
Generated table types can help describe projections without repeating every field:
|
|
108
108
|
|
|
109
109
|
```ts
|
|
110
|
-
import type {
|
|
111
|
-
type UserSummary = Pick<
|
|
110
|
+
import type { Rows } from './generated-schema.js';
|
|
111
|
+
type UserSummary = Pick<Rows['users'], 'id' | 'email'>;
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
You can then write `sql<UserSummary>`. A table type describes the full table; a join or aggregate
|
|
115
115
|
needs a type for what that particular query selects. An annotation is a declaration, not proof that
|
|
116
|
-
the SQL matches. Without one, result fields are `unknown`.
|
|
117
|
-
|
|
116
|
+
the SQL matches. Without one, result fields are `unknown`. Use the inspection and database-test
|
|
117
|
+
workflow below to check the declared contract against the query's behavior.
|
|
118
118
|
|
|
119
119
|
## Reuse a query with a function
|
|
120
120
|
|
|
@@ -123,15 +123,14 @@ supplied values easy to read:
|
|
|
123
123
|
|
|
124
124
|
```ts
|
|
125
125
|
import { sql, type QueryClient } from '@kequtech/tenon-orm';
|
|
126
|
-
import type {
|
|
126
|
+
import type { Rows } from './generated-schema.js';
|
|
127
127
|
|
|
128
|
-
type UserSummary = Pick<
|
|
128
|
+
type UserSummary = Pick<Rows['users'], 'id' | 'email'>;
|
|
129
129
|
|
|
130
130
|
export function userByEmail(client: QueryClient, { email }: { email: string }) {
|
|
131
|
-
return client.query(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
);
|
|
131
|
+
return client.query(sql<UserSummary>`SELECT id, email FROM public.users WHERE email = ${email}`, {
|
|
132
|
+
returns: 'oneOrNone',
|
|
133
|
+
});
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
const user = await userByEmail(db, { email: 'reader@example.test' });
|
|
@@ -147,7 +146,7 @@ await db.transaction(async (tx) => {
|
|
|
147
146
|
const user = await userByEmail(tx, { email: 'reader@example.test' });
|
|
148
147
|
if (!user) throw new Error('User not found');
|
|
149
148
|
|
|
150
|
-
await tx.query(sql`UPDATE public.
|
|
149
|
+
await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${user.id}`, {
|
|
151
150
|
affectedRows: { exact: 1 },
|
|
152
151
|
});
|
|
153
152
|
});
|
|
@@ -172,11 +171,11 @@ aligned, with `DEFAULT` for missing cells.
|
|
|
172
171
|
```ts
|
|
173
172
|
import type { Rows } from './generated-schema.js';
|
|
174
173
|
|
|
175
|
-
const input: Rows.Insert['
|
|
176
|
-
await db.query(sql`INSERT INTO public.
|
|
174
|
+
const input: Rows.Insert['users'] = { email: 'another@example.test' };
|
|
175
|
+
await db.query(sql`INSERT INTO public.users ${sql.insertValues(input, ['email'])}`);
|
|
177
176
|
|
|
178
|
-
const patch: Pick<Rows.Update['
|
|
179
|
-
await db.query(sql`UPDATE public.
|
|
177
|
+
const patch: Pick<Rows.Update['users'], 'active'> = { active: false };
|
|
178
|
+
await db.query(sql`UPDATE public.users SET ${sql.assignments(patch, ['active'])}
|
|
180
179
|
WHERE email = ${input.email}`);
|
|
181
180
|
```
|
|
182
181
|
|
|
@@ -190,36 +189,23 @@ choose which fields to write; they do not validate the complete object. See the
|
|
|
190
189
|
|
|
191
190
|
## Check your SQL and result types
|
|
192
191
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
192
|
+
Use `describeQuery({ connection, query })` on the completed query object, or
|
|
193
|
+
`npx tenon describe query.sql` on a SQL file, to inspect the server's parameter and result types.
|
|
194
|
+
This works with dynamic fragments and needs no additional checking setup. Match the application's
|
|
195
|
+
connection settings and keep database assertions for actual rows, nulls, JSON, permissions, and
|
|
196
|
+
transaction effects. See [query inspection](query-inspection.md).
|
|
198
197
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
Use the application's existing lint commands. If it has none, the setup shows `lint` and `lint:fix`.
|
|
204
|
-
They run ESLint, including all other enabled rules. Once configured, the normal workflow is:
|
|
198
|
+
TypeScript checks the application code and the result types you declare. Describe reports
|
|
199
|
+
PostgreSQL metadata; it does not generate or verify a TypeScript annotation. Use generated table
|
|
200
|
+
types where appropriate, declare projections explicitly, and check complete returned rows in tests.
|
|
205
201
|
|
|
206
202
|
1. Run `npm run db:migrate` against the development/test database.
|
|
207
|
-
2.
|
|
208
|
-
3.
|
|
209
|
-
4. Run TypeScript and database tests. CI runs the check without fixing files.
|
|
210
|
-
|
|
211
|
-
For example, SafeQL can turn `sql` followed by a query selecting `id, email` into
|
|
212
|
-
`sql<{ id: number; email: string }>` based on your actual schema. It can detect a renamed column or
|
|
213
|
-
an incorrect declared result type before the query runs.
|
|
214
|
-
|
|
215
|
-
Input validation and database tests still matter. Checking a query does not prove that a request is
|
|
216
|
-
authorized, that its values are valid, or that returned JSON has an application-specific shape.
|
|
203
|
+
2. Inspect the completed SQL and review the reported types and unresolved findings.
|
|
204
|
+
3. Run TypeScript and database tests. Keep these checks in routine CI validation.
|
|
217
205
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
tested query with a reason. Runtime-built mutation inputs need inline selected columns for SQL
|
|
222
|
-
checking. See the [checking reference](reference.md#supported-sql-and-limitations) for details.
|
|
206
|
+
Input validation and authorization remain application responsibilities. Describe cannot establish
|
|
207
|
+
that a request is authorized, that its values are valid, or that returned JSON has an
|
|
208
|
+
application-specific shape.
|
|
223
209
|
|
|
224
210
|
## Reuse or assemble parts of SQL when needed
|
|
225
211
|
|
|
@@ -227,7 +213,7 @@ For fixed conditions, write them directly:
|
|
|
227
213
|
|
|
228
214
|
```ts
|
|
229
215
|
const users = await db.query(
|
|
230
|
-
sql<UserSummary>`SELECT id, email FROM public.
|
|
216
|
+
sql<UserSummary>`SELECT id, email FROM public.users WHERE active = ${true}`,
|
|
231
217
|
{ returns: 'many' },
|
|
232
218
|
);
|
|
233
219
|
```
|
|
@@ -238,9 +224,9 @@ its count:
|
|
|
238
224
|
|
|
239
225
|
```ts
|
|
240
226
|
const search = sql.fragment`active = ${true} AND email ILIKE ${pattern}`;
|
|
241
|
-
const page = sql<UserSummary>`SELECT id, email FROM public.
|
|
227
|
+
const page = sql<UserSummary>`SELECT id, email FROM public.users
|
|
242
228
|
WHERE ${search} ORDER BY id LIMIT ${limit}`;
|
|
243
|
-
const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.
|
|
229
|
+
const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
|
|
244
230
|
WHERE ${search}`;
|
|
245
231
|
```
|
|
246
232
|
|
|
@@ -258,7 +244,7 @@ for (const term of terms) {
|
|
|
258
244
|
filters.push(sql.fragment`email ILIKE ${'%' + term + '%'}`);
|
|
259
245
|
}
|
|
260
246
|
|
|
261
|
-
const query = sql<UserSummary>`SELECT id, email FROM public.
|
|
247
|
+
const query = sql<UserSummary>`SELECT id, email FROM public.users
|
|
262
248
|
WHERE ${sql.join(filters, 'AND')} ORDER BY id`;
|
|
263
249
|
```
|
|
264
250
|
|
package/docs/mutations.md
CHANGED
|
@@ -34,10 +34,10 @@ entire input object contains only allowed keys. Keep narrower application input
|
|
|
34
34
|
`tenon generate` also provides `Rows.Update['profiles']`. Use `Pick` to limit a patch to fields
|
|
35
35
|
the caller may edit, for example `Pick<Rows.Update['profiles'], 'name' | 'bio'>`. Generated inputs
|
|
36
36
|
understand defaults, nullability, domains, and generated columns. They use write representations:
|
|
37
|
-
timestamps accept `Date | string`, bigint accepts `string | bigint`, and JSON
|
|
37
|
+
timestamps accept `Date | string`, bigint accepts `string | bigint`, and JSON accepts native JavaScript values through Tenon's exported `JSONValue` type.
|
|
38
38
|
See the [input-type reference](reference.md#insert-and-update-inputs) for the complete mapping.
|
|
39
39
|
|
|
40
|
-
## Input types
|
|
40
|
+
## Input types and runtime validation
|
|
41
41
|
|
|
42
42
|
Apply input types with an annotation, `satisfies`, or a helper generic argument. Generating the
|
|
43
43
|
file alone does not check every write. Narrow the fields each operation accepts:
|
|
@@ -55,9 +55,9 @@ function createProfile(client: QueryClient, input: ProfileInput) {
|
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
Here `id` and `score` are assumed to be integers. TypeScript rejects an incompatible typed input
|
|
58
|
-
such as `{ name: 'Ada', score: true }`.
|
|
59
|
-
|
|
60
|
-
constraints and application authorization in place.
|
|
58
|
+
such as `{ name: 'Ada', score: true }`. Inspect the completed SQL with `describeQuery` and test its
|
|
59
|
+
returned rows and write effects against the database. Validate external inputs before calling this
|
|
60
|
+
function and keep database constraints and application authorization in place.
|
|
61
61
|
|
|
62
62
|
**Selected columns can discard required fields.** For a table requiring integer `id` and text
|
|
63
63
|
`name` and `location`, this input is valid but the query fails:
|
|
@@ -115,22 +115,22 @@ database default; that alone does not decide what an upsert should overwrite.
|
|
|
115
115
|
|
|
116
116
|
## JSONB arrays and PostgreSQL arrays
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
SQL context determines whether a bound JavaScript array is a PostgreSQL array or a JSON value.
|
|
119
|
+
Pass native JavaScript values; Postgres.js serializes them for the target type:
|
|
120
120
|
|
|
121
121
|
```ts
|
|
122
122
|
const handles = ['ada', 'lin'];
|
|
123
123
|
await db.query(
|
|
124
124
|
sql`INSERT INTO deliveries ${sql.insertValues({
|
|
125
|
-
recipients_json:
|
|
125
|
+
recipients_json: handles, // jsonb column
|
|
126
126
|
recipient_handles: handles, // text[] column
|
|
127
127
|
})}`,
|
|
128
128
|
);
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
Objects used as JSON values retain
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
Objects used as JSON values retain Postgres.js's ordinary JSON conversion. The helper does not need runtime schema metadata. Generated JSON input types use Tenon's exported
|
|
132
|
+
`JSONValue` type, excluding SQL null for nonnullable columns. Pre-serialized text becomes a JSON
|
|
133
|
+
string; use SQL such as `'null'::jsonb` when JSON null is required. Validate JSON contents in the application.
|
|
134
134
|
|
|
135
135
|
## Batches in one transaction
|
|
136
136
|
|
|
@@ -146,7 +146,7 @@ await db.transaction(async (tx) => {
|
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
A failure rolls back all batches. The helper rejects an empty batch; this loop makes no insert
|
|
149
|
-
calls for empty input.
|
|
149
|
+
calls for empty input. Each query accepts at most 65,533 bound parameters, including parameters outside
|
|
150
150
|
the mutation fragment. An oversized query reports its actual count, for example
|
|
151
|
-
`at most
|
|
151
|
+
`at most 65533 bound parameters; received 65534`. Defaults consume no parameters. Account for the
|
|
152
152
|
selected columns and surrounding query when choosing a batch size; Tenon never splits queries automatically.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Inspect and test queries
|
|
2
|
+
|
|
3
|
+
`describeQuery` asks PostgreSQL what a completed SQL statement accepts and returns, without
|
|
4
|
+
binding its values or executing it. Use it while writing a query or in a database test. It handles
|
|
5
|
+
the actual SQL produced by fragments, helpers, loops, and conditional filters.
|
|
6
|
+
Its `postgres` driver installs with Tenon; no extra
|
|
7
|
+
consumer dependency or configuration is needed. Calling inspection opens a separate connection.
|
|
8
|
+
|
|
9
|
+
## Inspect the query you will execute
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { describeQuery, sql } from '@kequtech/tenon-orm';
|
|
13
|
+
|
|
14
|
+
const connection = process.env.DATABASE_URL;
|
|
15
|
+
if (!connection) throw new Error('DATABASE_URL is required');
|
|
16
|
+
|
|
17
|
+
const query = sql<{ id: number; total: string }>`
|
|
18
|
+
SELECT ${42}::integer AS id, count(*) AS total FROM pg_catalog.pg_class
|
|
19
|
+
`;
|
|
20
|
+
const description = await describeQuery({ connection, query });
|
|
21
|
+
// description.columns reports number/string read types; nullability remains unknown.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For an application, pass the same query object that will go to `db.query`. Use representative inputs when calling its query-building
|
|
25
|
+
function. Keep authorization policy centralized inside that builder or its fragments.
|
|
26
|
+
|
|
27
|
+
`query` accepts a SQL string or an object with a `text` property, including a Tenon `SqlQuery`.
|
|
28
|
+
For SQL strings, use PostgreSQL placeholders such as `$1`. PostgreSQL infers their types from SQL
|
|
29
|
+
context; `SELECT $1::integer AS id` can be described without supplying a value. Ordinary Tenon
|
|
30
|
+
templates bind values with `${value}`.
|
|
31
|
+
|
|
32
|
+
Inspection does not read the type argument of `sql<Row>`, validate `query.values`, or compare an
|
|
33
|
+
annotation with the result. A successful description is useful schema/type evidence, not proof
|
|
34
|
+
that the query will execute or do the intended job.
|
|
35
|
+
|
|
36
|
+
## Match the application's context
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const description = await describeQuery({
|
|
40
|
+
connection,
|
|
41
|
+
query,
|
|
42
|
+
session: { searchPath: 'mail,public', timeZone: 'UTC' },
|
|
43
|
+
timeouts: { connectMs: 3000, statementMs: 5000, lockMs: 1000 },
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Reuse the application's connection URI and the source of its session settings. All overrides are
|
|
48
|
+
optional. Connection URI startup options are preserved. `session.role` can assume a role when the
|
|
49
|
+
connecting account has permission; omitted settings use the new connection's defaults. `SET ROLE`
|
|
50
|
+
does not load that role's login defaults. Unknown options and invalid settings fail explicitly.
|
|
51
|
+
|
|
52
|
+
The result's `context` reports the database, login/effective roles, configured and resolved search
|
|
53
|
+
paths, timezone, and effective server timeouts. Compare these with the execution connection.
|
|
54
|
+
Inspection reports `mode: 'separate-session'` and `readMappings: 'postgres'`. SQL NULL array
|
|
55
|
+
elements are represented as JavaScript null.
|
|
56
|
+
|
|
57
|
+
The default connection/authentication limit is 3 seconds. Every server statement, including
|
|
58
|
+
metadata reads, has a 5-second limit, and each lock wait a 1-second limit. Cleanup is bounded to
|
|
59
|
+
1 second. Timeout overrides are positive integer milliseconds; URI settings cannot disable these
|
|
60
|
+
bounds. Limits apply to individual operations, not one overall deadline.
|
|
61
|
+
|
|
62
|
+
The API owns and closes its connection. It does not borrow an application client or a Tenon transaction.
|
|
63
|
+
Temporary tables, uncommitted changes, transaction-local settings, custom parsers, and other
|
|
64
|
+
pool-only state are not inherited. `PGOPTIONS` environment settings are not automatically copied;
|
|
65
|
+
use URI startup options or explicit session values. Standard read mappings describe Postgres.js
|
|
66
|
+
values, so custom parser behavior needs a deliberate application decision.
|
|
67
|
+
|
|
68
|
+
## Read the result
|
|
69
|
+
|
|
70
|
+
| Field | Meaning |
|
|
71
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
72
|
+
| `parameters` | Parameter type OIDs, qualified PostgreSQL type names, and Postgres.js read representations. These are not accepted JavaScript input contracts. |
|
|
73
|
+
| `columns` | Ordered output names, PostgreSQL types, Postgres.js read representations, and source-column metadata where available. |
|
|
74
|
+
| `columns[].nullability` | Always `"unknown"`. A source column's `notNull` constraint does not establish result nullability. |
|
|
75
|
+
| `rowType` | A conservative `DescribedRow` TypeScript declaration. Fields include null; JSON/unmapped types stay unknown. Null when there are no output columns or names are duplicated. |
|
|
76
|
+
| `issues` | Findings with stable `code` fields, human-readable messages, and relevant column/type details. |
|
|
77
|
+
| `context` | The separate inspection session's effective settings and read mapping profile. |
|
|
78
|
+
|
|
79
|
+
Use issue codes for tooling; message text is descriptive:
|
|
80
|
+
|
|
81
|
+
- `nullability-unknown`: result nullability has not been established.
|
|
82
|
+
- `shape-unknown`: listed columns have unknown read types; JSON object structure is not inferred.
|
|
83
|
+
- `unmapped-type`: the PostgreSQL type/OID lacks a verified Postgres.js mapping.
|
|
84
|
+
- `duplicate-column`: repeated names would overwrite earlier values in object rows.
|
|
85
|
+
|
|
86
|
+
Source `notNull` remains informational. Outer joins, grouping, and expressions can introduce nulls.
|
|
87
|
+
An empty result set supplies no runtime samples. Unknown mappings and duplicate names are findings
|
|
88
|
+
to handle explicitly, not a successful verification of a handwritten result type.
|
|
89
|
+
|
|
90
|
+
## CLI
|
|
91
|
+
|
|
92
|
+
Save the statement in `query.sql`, using `$1`, `$2`, and so on for parameters:
|
|
93
|
+
|
|
94
|
+
```sql
|
|
95
|
+
SELECT $1::integer AS id, count(*) AS total FROM pg_catalog.pg_class;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Supply `DATABASE_URL` through the application's normal environment setup:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
npx tenon describe query.sql
|
|
102
|
+
npx tenon describe query.sql --search-path mail,public --time-zone UTC
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The CLI prints the description as JSON. Options are `--role`, `--search-path`, `--time-zone`,
|
|
106
|
+
`--connect-timeout-ms`, `--statement-timeout-ms`, and `--lock-timeout-ms`. Migration/generation
|
|
107
|
+
options such as `--namespace` and `--check` are rejected. No environment files are loaded.
|
|
108
|
+
|
|
109
|
+
Exit code 0 means PostgreSQL described the statement, even if the report contains unresolved
|
|
110
|
+
issues. Exit code 1 means input, connection, timeout, or PostgreSQL analysis failed. No query values
|
|
111
|
+
are bound and no inspected statement is executed, including writes and locking statements.
|
|
112
|
+
|
|
113
|
+
## Keep execution assertions
|
|
114
|
+
|
|
115
|
+
For each meaningful query-builder variant, describe the finished SQL and run it through the
|
|
116
|
+
application's actual Tenon/Postgres.js connection against its test database. Assert complete relevant rows
|
|
117
|
+
and effects: nulls, JSON contents, precision, ordering, empty results, ownership, and rollback.
|
|
118
|
+
Include absent relationships, pagination ties, and empty aggregates where relevant. Keep those
|
|
119
|
+
tests alongside the query and run the application's normal TypeScript compiler.
|
|
120
|
+
|
|
121
|
+
Description can succeed for an invalid bound value, a write that violates a constraint, or an
|
|
122
|
+
operation the runtime role may not execute. Execution tests establish behavior for the exercised
|
|
123
|
+
inputs; they are not a proof for every possible input. Inspecting one variant does not cover
|
|
124
|
+
other branches or detect stale TypeScript annotations elsewhere in the application.
|
|
125
|
+
|
|
126
|
+
Describe does not bind runtime values. Cast standalone parameter projections, for example
|
|
127
|
+
`SELECT $1::boolean AS enabled`, so SQL establishes their types. Execution can otherwise infer a
|
|
128
|
+
type from a JavaScript value that an inspection without values cannot establish.
|