@kequtech/tenon-orm 0.1.4 → 0.2.1

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 (81) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +19 -19
  3. package/dist/affected-rows.d.ts +1 -1
  4. package/dist/affected-rows.js +6 -6
  5. package/dist/baseline.d.ts +1 -1
  6. package/dist/baseline.js +5 -5
  7. package/dist/cli.js +18 -39
  8. package/dist/client.js +1 -1
  9. package/dist/connection.d.ts +18 -13
  10. package/dist/connection.js +14 -43
  11. package/dist/database.js +5 -2
  12. package/dist/describe.d.ts +3 -0
  13. package/dist/describe.js +21 -0
  14. package/dist/driver/array.d.ts +5 -0
  15. package/dist/driver/array.js +70 -0
  16. package/dist/driver/lifecycle.d.ts +15 -0
  17. package/dist/driver/lifecycle.js +57 -0
  18. package/dist/driver/session.d.ts +8 -0
  19. package/dist/driver/session.js +92 -0
  20. package/dist/driver/types.d.ts +10 -0
  21. package/dist/driver/types.js +49 -0
  22. package/dist/generate.js +7 -7
  23. package/dist/generation/catalog.d.ts +2 -2
  24. package/dist/generation/catalog.js +4 -4
  25. package/dist/generation/output.d.ts +2 -0
  26. package/dist/generation/output.js +35 -0
  27. package/dist/generation/schema-summary.d.ts +3 -3
  28. package/dist/generation/schema-summary.js +11 -11
  29. package/dist/generation/table-types.js +2 -52
  30. package/dist/index.d.ts +4 -1
  31. package/dist/index.js +1 -0
  32. package/dist/inspection/catalog.d.ts +25 -0
  33. package/dist/inspection/catalog.js +29 -0
  34. package/dist/inspection/cli.d.ts +22 -0
  35. package/dist/inspection/cli.js +34 -0
  36. package/dist/inspection/connection.d.ts +4 -0
  37. package/dist/inspection/connection.js +44 -0
  38. package/dist/inspection/options.d.ts +15 -0
  39. package/dist/inspection/options.js +49 -0
  40. package/dist/inspection/result.d.ts +4 -0
  41. package/dist/inspection/result.js +90 -0
  42. package/dist/inspection/types.d.ts +74 -0
  43. package/dist/inspection/types.js +1 -0
  44. package/dist/migrations/baseline-sql.d.ts +1 -1
  45. package/dist/migrations/baseline-sql.js +1 -1
  46. package/dist/migrations/connection.d.ts +2 -3
  47. package/dist/migrations/connection.js +15 -13
  48. package/dist/migrations/history.d.ts +4 -4
  49. package/dist/migrations/history.js +5 -5
  50. package/dist/migrations.js +7 -7
  51. package/dist/mutations.d.ts +0 -5
  52. package/dist/mutations.js +33 -41
  53. package/dist/query-options.d.ts +2 -2
  54. package/dist/query-options.js +4 -4
  55. package/dist/query.d.ts +3 -4
  56. package/dist/query.js +2 -2
  57. package/dist/sql.d.ts +1 -1
  58. package/dist/sql.js +1 -1
  59. package/dist/transaction-sql.js +1 -1
  60. package/dist/transaction.d.ts +2 -2
  61. package/dist/transaction.js +14 -13
  62. package/dist/types.d.ts +7 -4
  63. package/docs/adoption-guide.md +43 -76
  64. package/docs/agent-guide.md +82 -107
  65. package/docs/human-guide.md +20 -34
  66. package/docs/mutations.md +13 -13
  67. package/docs/query-inspection.md +128 -0
  68. package/docs/reference.md +98 -289
  69. package/package.json +10 -31
  70. package/dist/safeql/composition.d.ts +0 -9
  71. package/dist/safeql/composition.js +0 -168
  72. package/dist/safeql/eslint.d.ts +0 -2
  73. package/dist/safeql/eslint.js +0 -50
  74. package/dist/safeql/mutations.d.ts +0 -3
  75. package/dist/safeql/mutations.js +0 -74
  76. package/dist/safeql/plugin.d.ts +0 -3
  77. package/dist/safeql/plugin.js +0 -43
  78. package/dist/safeql/static.d.ts +0 -10
  79. package/dist/safeql/static.js +0 -59
  80. package/dist/safeql.d.ts +0 -6
  81. package/dist/safeql.js +0 -7
package/docs/reference.md CHANGED
@@ -1,35 +1,63 @@
1
1
  # Tenon reference
2
2
 
3
- Use this document to look up exact API behavior, checker setup, and limitations. The
3
+ Use this document to look up exact API behavior, query inspection, and limitations. The
4
4
  [human guide](human-guide.md) teaches the workflow; the [agent guide](agent-guide.md) is a
5
5
  self-contained implementation guide. [Adoption](adoption-guide.md) covers replacing an existing ORM
6
6
  and transferring migration history.
7
7
 
8
8
  ## Installation and connection
9
9
 
10
- Runtime requirements: Node.js 24+, PostgreSQL 16+, ESM, and `pg`. TypeScript consumers need
11
- TypeScript 6+ and development typings for Node and `pg`. Keep your application's strict TypeScript
10
+ Runtime requirements: Node.js 24+, PostgreSQL 16+, and ESM. The Postgres.js driver is included. TypeScript consumers need
11
+ TypeScript 6+ and Node development typings. Keep your application's strict TypeScript
12
12
  setup; Tenon needs no special compiler configuration. `"type": "module"` and TypeScript's
13
- `"module": "NodeNext"` are a usual Node setup. Checker compatibility is listed separately below.
13
+ `"module": "NodeNext"` are a usual Node setup.
14
14
 
15
15
  ```sh
16
- npm install @kequtech/tenon-orm pg
17
- npm install --save-dev @types/pg
16
+ npm install @kequtech/tenon-orm
18
17
  ```
19
18
 
20
- `createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, and
21
- `close`. The required `connection` accepts a URL, `pg.PoolConfig`, or existing `pg.Pool`. An
22
- explicit `{}` selects pg's environment defaults. Tenon does not load environment files
19
+ `createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, `native`, and
20
+ `close`. The required `connection` accepts a URL, Postgres.js options, or an existing Postgres.js client. An
21
+ explicit `{}` selects the driver's environment defaults. Tenon does not load environment files
23
22
  automatically.
24
23
 
25
24
  Reuse an instance per database connection. `close()` closes pools created by Tenon; supplied pools
26
- remain application-owned. Finish outstanding work before closing. Install your own
27
- `pool.on('error', handler)` on supplied pools; Tenon reports idle errors on owned pools through
28
- `process.emitWarning`.
25
+ remain application-owned. Finish outstanding work before closing. Postgres.js handles connection errors through rejected queries and its native connection callbacks.
29
26
 
30
27
  Qualify SQL names or configure the driver's `search_path`. The CLI's `--namespace` selects the
31
28
  schema used by tooling, not a runtime query namespace.
32
29
 
30
+ ## Native driver access
31
+
32
+ `db.native` is the underlying Postgres.js client, typed as `postgres.Sql`. The property is readonly
33
+ in TypeScript. It exposes the existing pool without creating another client or requiring a separate
34
+ driver installation. A supplied client is returned by identity. Use native APIs for LISTEN, cursors,
35
+ COPY, and other driver features; their behavior follows the included `postgres@3.4.9`.
36
+
37
+ ```ts
38
+ const db = createTenon({ connection });
39
+ await db.native.listen('updates', handlePayload, refreshAfterSubscription);
40
+ // At application shutdown, after outstanding work finishes:
41
+ await db.close();
42
+ ```
43
+
44
+ Postgres.js LISTEN creates a dedicated physical connection alongside the query pool. Applications
45
+ own notification parsing, authorization, callback failures, and catch-up after resubscription. See
46
+ the [Postgres.js native API](https://github.com/porsager/postgres/tree/v3.4.9#listen--notify).
47
+
48
+ Native calls bypass Tenon's result contracts, affected-row guards, SQL boundary checks, and
49
+ `onQuery` observation. They use native preparation defaults. `db.native` always refers to the pool,
50
+ including inside `db.transaction`: its queries do not join that transaction. Transaction callbacks
51
+ expose only `tx.query`; use that for work that must commit or roll back together.
52
+
53
+ Native access is immediate and does not initialize Tenon's array correction. The first Tenon query
54
+ or transaction installs that correction on the shared client; already prepared native statements
55
+ can retain earlier parsers. Raw calls otherwise follow native driver behavior.
56
+
57
+ `db.close()` ends a Tenon-owned client and its native listeners. With a borrowed client, it closes
58
+ only the Tenon wrapper: native queries and listeners remain usable until their owner calls
59
+ `db.native.end()`. Calling `end()` directly also ends the client used by Tenon.
60
+
33
61
  ## Queries and result modes
34
62
 
35
63
  ```ts
@@ -47,17 +75,18 @@ automatically describe joins or aggregates.
47
75
  `query` accepts `{ returns?, affectedRows?, label? }`. Unknown options are rejected. `label` must be
48
76
  a nonempty string when supplied and is used for diagnostics.
49
77
 
50
- | `returns` | Result | Returned rows allowed |
51
- | ------------------ | --------------------- | --------------------- |
52
- | `native` (default) | pg `QueryResult<Row>` | Any |
53
- | `many` | `Row[]` | Any, including zero |
54
- | `oneOrNone` | `Row \| undefined` | Zero or one |
55
- | `one` | `Row` | Exactly one |
78
+ | `returns` | Result | Returned rows allowed |
79
+ | ------------------ | ------------------------------------ | --------------------- |
80
+ | `native` (default) | Postgres.js `QueryResult<Row>` array | Any |
81
+ | `many` | `Row[]` | Any, including zero |
82
+ | `oneOrNone` | `Row \| undefined` | Zero or one |
83
+ | `one` | `Row` | Exactly one |
56
84
 
57
- Native results retain `command`, `rowCount`, `fields`, and `rows`; a null row count means none was
58
- reported. Result modes never add `LIMIT` or `RETURNING`. A count mismatch throws; returned-row
59
- checks alone do not undo an autocommitted write. Use `affectedRows` for write limits that roll back
60
- on failure.
85
+ Native results are arrays of rows with `command`, `count`, `columns`, `statement`, and `state`
86
+ metadata. `count` is the affected-row count, independent of `RETURNING`; it can be null for commands
87
+ without a count. `many` returns a plain row array. Result modes never add `LIMIT` or `RETURNING`.
88
+ A returned-row mismatch does not undo an autocommitted write. Use `affectedRows` for write limits
89
+ that roll back on failure.
61
90
 
62
91
  Reusable queries are ordinary functions. Accept a `QueryClient` and named object inputs when useful;
63
92
  both `Database` and `Transaction` implement `QueryClient`. Keep result annotations on the SQL and
@@ -70,10 +99,14 @@ The `sql` tag binds ordinary interpolations as `$1`, `$2`, etc. Never quote inte
70
99
  manual parameter placeholders inside templates. Every public query executes one statement, including
71
100
  parameterless queries.
72
101
 
73
- Values use pg's conversions, including arrays, JSON, dates, and `undefined` becoming SQL null. Cast
74
- standalone parameter projections to the intended SQL type: `SELECT ${1.5} AS value` returns text,
75
- while `SELECT ${1.5}::float8 AS value` returns a JavaScript number. PostgreSQL infers parameter
76
- types from SQL context.
102
+ Values use Postgres.js's native conversions. An undefined bound parameter is rejected. Objects and
103
+ arrays destined for JSON columns are serialized by the driver; passing pre-serialized JSON text
104
+ stores a JSON string. Arrays bound to PostgreSQL array columns retain null elements. SQL context
105
+ establishes array and JSON parameter types; cast standalone parameters to the intended SQL type.
106
+ Tenon uses the driver's raw-query path with the extended protocol so every public call executes
107
+ one statement, including calls without parameters. Application queries explicitly disable prepared
108
+ statement caching, including when the supplied driver client has `prepare: true`. Parameters remain
109
+ bound separately from SQL text.
77
110
 
78
111
  Use an array parameter for a set of values:
79
112
 
@@ -192,13 +225,13 @@ To reset an update field to its default, write `column = DEFAULT` in the
192
225
  surrounding SQL. Put arithmetic and `excluded.column` expressions there too; these helpers accept
193
226
  values only, with no expression/default sentinel.
194
227
 
195
- A compiled query, including surrounding interpolations, is limited to 65,535 bound parameters.
228
+ A compiled query accepts at most 65,533 bound parameters, including surrounding interpolations.
196
229
  Tenon rejects larger queries and does not split them automatically. Choose batch sizes and
197
230
  transaction boundaries in the caller. Helper validation errors identify invalid rows/columns
198
231
  without printing cell values.
199
232
 
200
233
  See the [mutation cookbook](mutations.md) for common patterns and
201
- [checking mutation fragments](#checking-mutation-fragments) for optional SQL checking.
234
+ [query inspection](#query-inspection) for checking completed statements.
202
235
 
203
236
  ## Transactions, guarded writes, and errors
204
237
 
@@ -232,7 +265,7 @@ before data queries and `SET LOCAL statement_timeout = '5s'`. Manual savepoints
232
265
  through SQL; reserve `tenon_affected_rows_` names for Tenon. Direct BEGIN/COMMIT/ROLLBACK boundaries
233
266
  are owned by `transaction`.
234
267
 
235
- SQL errors retain pg's codes and details. Invalid inputs and returned-row mismatches throw ordinary
268
+ SQL errors retain Postgres.js's codes and details. Invalid inputs and returned-row mismatches throw ordinary
236
269
  `Error` objects. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
237
270
  `TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`. When both work and cleanup
238
271
  fail, `AggregateError` preserves both failures. A lost commit acknowledgment can leave the outcome
@@ -247,8 +280,8 @@ transaction has committed.
247
280
 
248
281
  `generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript source for the
249
282
  connected schema. It does not need migration files or history. Table type generation is optional.
250
- SafeQL reads the database directly and does not require these generated files; they provide
251
- reusable row and mutation input types. The CLI writes that source atomically. This command checks
283
+ Generated files provide reusable row and mutation input types. The CLI writes that source
284
+ atomically. This command checks
252
285
  the existing file without rewriting it:
253
286
 
254
287
  ```sh
@@ -276,24 +309,25 @@ named aliases preserve exact SQL names; the generated `users` alias also means `
276
309
  Views, foreign tables, and attached partitions use query-specific types. A missing namespace
277
310
  fails; an existing empty schema is valid.
278
311
 
279
- | PostgreSQL type | Returned value | Generated write input |
280
- | ---------------------------------------------- | --------------------------------------------- | ------------------------------------- |
281
- | smallint, integer, oid, real, double precision | `number` | `number` |
282
- | bigint | `string` | `string \| bigint` |
283
- | numeric | `string` | `string \| number` |
284
- | boolean | `boolean` | `boolean` |
285
- | text, name, varchar, char, uuid, time, timetz | `string` | `string` |
286
- | date, timestamp, timestamptz | `Date \| number` (numbers include infinities) | `Date \| string` |
287
- | enum | String literal union | Same union; `never` for an empty enum |
288
- | json, jsonb | `unknown` | Serialized JSON text (`string`) |
289
- | bytea | `Buffer` | `Buffer` |
312
+ | PostgreSQL type | Returned value | Generated write input |
313
+ | ---------------------------------------------- | ---------------------------------------- | --------------------------------------- |
314
+ | smallint, integer, oid, real, double precision | `number` | `number` |
315
+ | bigint | `string` | `string \| bigint` |
316
+ | numeric | `string` | `string \| number` |
317
+ | boolean | `boolean` | `boolean` |
318
+ | text, name, varchar, char, uuid, time, timetz | `string` | `string` |
319
+ | date, timestamp, timestamptz | `Date` (infinities become invalid dates) | `Date \| string` |
320
+ | enum | String literal union | Same union; `never` for an empty enum |
321
+ | json, jsonb | `unknown` | `Exclude<JSONValue, null>` (from Tenon) |
322
+ | bytea | `Buffer` | `Buffer` |
290
323
 
291
324
  For returned rows, nullable columns add null. Domains use verified underlying mappings and the
292
325
  column's own nullability. Unverified types warn and become `unknown`. Verified arrays use
293
326
  `PgArray<T> = (T | null | PgArray<T>)[]` for null elements and multiple dimensions. Numeric arrays
294
- contain numbers and can lose precision; cast to text[] for exact values. Enum arrays and arrays of
295
- domains are unverified. Finite dates and timestamps without time zone use Node's local timezone.
296
- Custom pg parsers need matching application types.
327
+ contain exact strings, like numeric scalars. Enum arrays and arrays of
328
+ domains are unverified. Date-only values use the driver's UTC date parsing; timestamps without time zone use Node's local timezone.
329
+ Infinite timestamps remain invalid `Date` objects: check validity before using date methods.
330
+ Custom Postgres.js parsers need matching application types.
297
331
 
298
332
  ### Insert and update inputs
299
333
 
@@ -323,9 +357,10 @@ Computed columns and `GENERATED ALWAYS` identity columns cannot receive values.
323
357
  primary keys remain writable. Use `Pick` or `Omit` for application-specific restrictions and keep
324
358
  expressions, `DEFAULT`, and special identity overrides in SQL.
325
359
 
326
- Write representations in the table above also apply to verified array elements. Serialize JSON
327
- with `JSON.stringify`, including JSON arrays; a JSON string containing `null` differs from SQL null.
328
- Use timestamp strings for infinities. Unverified non-null write values use `never` and warn;
360
+ Write representations in the table above also apply to verified array elements. Pass JavaScript JSON
361
+ values directly; do not pre-serialize them. A bound null is SQL null. To construct JSON null explicitly,
362
+ use SQL such as `'null'::jsonb`. Native temporal serialization requires finite dates; strings such as
363
+ `'infinity'` are not valid bound date values. Use SQL literals for these PostgreSQL values. Unverified non-null write values use `never` and warn;
329
364
  nullable fields still accept null. Supply an application-owned replacement when needed, for example
330
365
  `Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for a reviewed interval-text input.
331
366
 
@@ -333,252 +368,26 @@ Apply these types through annotations, `satisfies`, or helper generic arguments
333
368
  `sql.insertValues<NewProfile>(input)`. They check shapes and representations, not authorization,
334
369
  constraints, JSON contents, or nonempty inputs. Optional fields accept explicit `undefined`,
335
370
  including with `exactOptionalPropertyTypes`. Selected columns can discard required fields; see
336
- the [cookbook example](mutations.md#input-types-sql-checking-and-runtime-validation).
337
-
338
- ## SQL checking with SafeQL
339
-
340
- SafeQL checks SQL against a real PostgreSQL schema and infers result types. ESLint runs that check
341
- on your source files. Tenon runs the queries in the application; it does not import or run either
342
- tool during query execution.
343
-
344
- Configure [SafeQL normally](https://safeql.dev/api/). Tenon's optional `safeql` entry point exports
345
- two objects:
346
-
347
- - `safeqlPlugin` is a SafeQL plugin descriptor. Add it to each Tenon connection's `plugins` list. It
348
- translates constant fragments, official mutation helpers, and identifiers, preserves parameter
349
- behavior, and supplies pg result mappings. SafeQL owns database analysis and annotation fixes.
350
- - `eslintPlugin` supplies `tenon/check-composition`. Register it in ESLint and enable the rule to
351
- report queries that the adapter cannot resolve, including runtime-built fragments and escaped
352
- outer templates.
353
-
354
- Use both. The adapter skips unsupported queries; the companion rule makes those exclusions visible
355
- as ordinary, individually suppressible lint errors. Neither replaces or wraps SafeQL's rule.
356
-
357
- ### Install and configure
358
-
359
- Install the checking tools as application development dependencies, reusing the application's
360
- existing TypeScript installation:
361
-
362
- ```sh
363
- npm install --save-dev eslint typescript-eslint @ts-safeql/eslint-plugin
364
- ```
365
-
366
- These are development dependencies. Reuse your existing type-aware parser configuration and a
367
- TypeScript version supported by the lint toolchain. Tenon's database runtime does not load them.
368
-
369
- For a project with `src/**/*.ts` and a `tsconfig.json`:
370
-
371
- ```js
372
- // eslint.config.mjs
373
- import { defineConfig } from 'eslint/config';
374
- import safeql from '@ts-safeql/eslint-plugin/config';
375
- import tseslint from 'typescript-eslint';
376
- import { safeqlPlugin, eslintPlugin } from '@kequtech/tenon-orm/safeql';
377
-
378
- const databaseUrl = process.env.DATABASE_URL;
379
- if (!databaseUrl) throw new Error('DATABASE_URL is required for SQL checking');
380
-
381
- export default defineConfig({
382
- files: ['src/**/*.ts'],
383
- languageOptions: {
384
- parser: tseslint.parser,
385
- parserOptions: { projectService: true },
386
- },
387
- plugins: { tenon: eslintPlugin },
388
- rules: { 'tenon/check-composition': 'error' },
389
- extends: [
390
- safeql.configs.connections({
391
- databaseUrl,
392
- plugins: [safeqlPlugin],
393
- targets: [{ tag: 'sql' }],
394
- }),
395
- ],
396
- });
397
- ```
398
-
399
- Add these settings to your existing ESLint setup if you have one; reuse its type-aware parser
400
- configuration. `projectService` uses the nearest TypeScript project for each file. Supply
401
- `DATABASE_URL` through your application's environment setup and point it at a development/test
402
- database with migrations applied. This example does not load `.env` files.
403
-
404
- `targets` selects the SQL tag spelling. If you alias `sql`, add the alias to the targets. For a
405
- single database, `targets: [{ tag: { regex: '.*' } }]` selects all tags; Tenon's adapter filters
406
- that down to actual Tenon queries, including aliases and object properties. Neither the adapter nor
407
- the rule executes application code.
408
-
409
- Reuse the application's lint scripts. If none exist, add these, adjusting file selection as needed.
410
- They run all enabled ESLint rules, and `lint:fix` can change non-SQL code too:
411
-
412
- ```json
413
- {
414
- "scripts": {
415
- "lint": "eslint \"src/**/*.ts\"",
416
- "lint:fix": "eslint \"src/**/*.ts\" --fix"
417
- }
418
- }
419
- ```
420
-
421
- ### Generate and check annotations
422
-
423
- For an existing persistence layer, start with the [incremental audit workflow](adoption-guide.md#adopt-sql-checking-incrementally).
424
- It covers a separate SQL lint command, fix previews, and application refinements.
425
-
426
- Write a query without a result annotation:
427
-
428
- ```ts
429
- const query = sql`SELECT id, email FROM public.users WHERE id = ${id}`;
430
- ```
371
+ the [cookbook example](mutations.md#input-types-and-runtime-validation).
431
372
 
432
- Run `npm run lint:fix`. SafeQL can write `sql<{ id: number; email: string }>` using the connected
433
- schema. It can also repair an incorrect annotation. Review the changes, run TypeScript and database
434
- tests, and commit the result. Without an annotation, Tenon's result fields stay `unknown`.
435
-
436
- Run `npm run lint` in CI without `--fix`. Missing/incorrect annotations, invalid SQL, and
437
- unsupported composition fail the check without rewriting files. After schema changes, run the
438
- development [`db:migrate` script](human-guide.md#create-a-table) before checking queries. Inspect
439
- SQL mistakes before accepting types generated from an unintended query. ESLint's fix command may
440
- also apply other enabled rules.
441
-
442
- ### Configuration choices and multiple databases
443
-
444
- All connection, target, and override options belong to SafeQL. There is no Tenon-specific
445
- configuration wrapper or separate database setting. Keep `safeqlPlugin` on each Tenon connection:
446
- without it, SafeQL does not resolve Tenon fragments and can infer different parameter types from
447
- those returned at runtime.
448
-
449
- For multiple databases, pass a connection array to `safeql.configs.connections`. With
450
- `usersDatabaseUrl` and `billingDatabaseUrl` set to your development/test URLs, replace the single
451
- connection in the setup above with:
452
-
453
- ```js
454
- safeql.configs.connections([
455
- {
456
- databaseUrl: usersDatabaseUrl,
457
- plugins: [safeqlPlugin],
458
- targets: [{ tag: 'usersSql' }],
459
- },
460
- {
461
- databaseUrl: billingDatabaseUrl,
462
- plugins: [safeqlPlugin],
463
- targets: [{ tag: 'billingSql' }],
464
- },
465
- ]);
466
- ```
467
-
468
- Queries can then share a file:
469
-
470
- ```ts
471
- import { sql as usersSql, sql as billingSql } from '@kequtech/tenon-orm';
472
-
473
- const users = usersSql`SELECT id, email FROM public.users`;
474
- const invoices = billingSql`SELECT id, total FROM public.invoices`;
475
- ```
373
+ ## Query inspection
476
374
 
477
- The aliases only select a checker connection; they are the same runtime `sql` function. Execute each
478
- query with the correct `createTenon` instance. Neither SafeQL nor Tenon's composition rule follows
479
- `db.query` calls to infer which database will execute a query. Keep connection targets distinct so a
480
- query is checked against its intended database.
481
-
482
- If queries are already separated by file, keep the `sql` tag and use an ESLint config object per
483
- file group, each extending its own `safeql.configs.connections({ ... })`. Use non-overlapping
484
- `files` patterns when database settings differ. Normal SafeQL configurations for other libraries can
485
- coexist; add Tenon's adapter only to connections checking Tenon queries.
486
-
487
- Qualify same-named tables with their schemas, for example `SELECT id FROM billing.accounts`, so
488
- the intended relation is explicit in both checking and execution.
489
-
490
- ### Supported SQL and limitations
491
-
492
- Within the configured targets, the adapter recognizes Tenon's `sql` export through imports, local
493
- aliases, namespace imports, and object properties that retain its TypeScript type. It checks
494
- constant fragments and `sql.join` with inline nonempty array literals, including nested fragments.
495
- Separators and `identifier()` names must resolve to literal strings or const string initializers.
496
- Values use the same parameter placeholders in static checking and execution, inside or outside
497
- fragments. PostgreSQL infers their types from SQL context; Tenon does not add casts based on
498
- JavaScript types.
499
-
500
- The checker follows const fragment initializers, including imported constants with inspectable
501
- source. It does not inspect function bodies, substitute helper arguments/defaults, or choose
502
- JavaScript conditional branches. Function-generated fragments and conditional SQL structure are
503
- unsupported even when a particular helper appears simple or a condition is a literal. Mutable
504
- fragment references and dynamic identifiers are also unsupported. Arrays stored in variables or
505
- assembled in loops are valid `sql.join` inputs at runtime but are not analyzed. Ordinary value
506
- expressions remain bound parameters; calls that produce data for an official mutation helper can
507
- use its explicit column-list contract.
508
-
509
- `tenon/check-composition` points to the first unsupported interpolation in each query and explains
510
- why it cannot be checked. For example, opaque mutation inputs need an inline column list, while
511
- fragment-returning functions remain outside static analysis. Errors reached through shared constants
512
- are reported at the consuming query. ESLint suppressions must cover the reported expression's line;
513
- a scoped disable/enable can cover one multiline query.
514
-
515
- Reusable authorization and other application policy can still live in fragment-returning functions
516
- at runtime. Keep that policy centralized and cover its behavior with database tests. Suppress
517
- `tenon/check-composition` only on the particular query, with a reason, when its variants are tested.
518
- Do not duplicate policy merely to obtain static coverage. SQL and annotation errors in checkable
519
- queries continue to come from `@ts-safeql/check-sql`.
520
-
521
- Ordinary functions containing complete `sql` templates remain checkable. Each template is analyzed
522
- where it is written, including constant fragments defined in that function:
375
+ `describeQuery({ connection, query, session?, timeouts? })` accepts a PostgreSQL connection URI
376
+ and a finished SQL string or `{ text }` query object. It returns parameter/result type metadata,
377
+ source constraints, conservative row declaration, effective session context, and coded findings.
378
+ It does not bind query values, execute the inspected statement, or verify `sql<Row>`.
523
379
 
524
- ```ts
525
- function findUsers(email: string) {
526
- const predicate = sql.fragment`email = ${email}`;
527
- return sql`SELECT id FROM users WHERE ${predicate}`;
528
- }
529
- ```
380
+ `tenon describe <query.sql>` prints that report as JSON using `DATABASE_URL`. Session flags are
381
+ `--role`, `--search-path`, and `--time-zone`. Timeout flags are `--connect-timeout-ms`,
382
+ `--statement-timeout-ms`, and `--lock-timeout-ms`; defaults are 3000, 5000, and 1000 ms.
530
383
 
531
- Use literal line breaks in `sql` templates and bind string values with `${value}`. The composition
532
- rule rejects JavaScript escapes such as `\n` or `\'` in the outer template: SafeQL reads raw
533
- template text, while Tenon executes decoded JavaScript text. This diagnostic prevents checking
534
- different SQL from what runs. Escapes inside bound values are fine; constant fragments are decoded
535
- by Tenon's adapter before checking.
536
-
537
- This checks SQL structure and returned-row annotations, not JavaScript input compatibility or
538
- runtime values. For example, a boolean passed to an integer comparison is rejected by PostgreSQL at
539
- execution, not by this lint rule. Keep typed function parameters, validate external inputs, and test
540
- database behavior. Explicit SQL casts are checked as part of the SQL.
541
-
542
- The adapter supplies scalar mappings for bigint/numeric strings, unknown JSON, and `Date | number`
543
- temporal values (including infinities). For recursive arrays, use column overrides matching
544
- generated types, for example:
545
-
546
- ```js
547
- safeql.configs.connections({
548
- databaseUrl,
549
- plugins: [safeqlPlugin],
550
- targets: [{ tag: 'sql' }],
551
- overrides: {
552
- columns: { 'users.tags': 'PgArray<string>', 'users.amounts': 'PgArray<number>' },
553
- },
554
- });
555
- ```
384
+ Inspection owns a separate connection and uses Postgres.js mappings. Match the application's role
385
+ and settings; temporary tables, uncommitted changes, transaction-local settings, and custom parsers
386
+ are not inherited. Result nullability remains unknown. CLI exit 0 means description succeeded,
387
+ including reports with unresolved issues; execution and type annotations remain unchecked.
556
388
 
557
- Import `PgArray` from generated definitions in files using that annotation. Numeric arrays use
558
- numbers with pg's default parser, even though numeric scalars use strings. Array dimensions/null
559
- elements, custom parsers, and unverified PostgreSQL types need appropriate overrides and database
560
- tests; a checker cannot infer your parser policy. Column keys are `Table.column`, so duplicate table
561
- names across schemas need care. Type overrides merge with the adapter's defaults. `unsafeSql` is
562
- outside static checking, and arbitrary JSON still needs application validation.
563
-
564
- ### Checking mutation fragments
565
-
566
- SafeQL recognizes these official helpers directly through their Tenon function declarations,
567
- including aliases. It never analyzes their implementation or executes application data builders.
568
- Without an explicit column list, checking accepts inline object literals and inline insert arrays
569
- of object literals. Keys must be literal names; object spreads, computed keys, methods, accessors,
570
- and objects stored in variables require an explicit inline column list. Existing constant string
571
- resolution works for entries of that list; column arrays stored in variables are unsupported.
572
-
573
- For runtime inputs with explicit columns, the checker analyzes a representative nonempty insert
574
- row or assignment list containing **all selected columns**. This checks column references, SQL
575
- composition, conflict clauses, and `RETURNING` annotations. Runtime omission/default decisions and
576
- batch length do not require interpreting JavaScript. The check is conservative: an optional
577
- selected column is checked even when a particular runtime call omits it. It does not establish
578
- nonempty input, valid runtime values, successful constraints, or every default's behavior; runtime
579
- validation and database tests cover those contracts. Unknown inputs without explicit columns
580
- produce `tenon/check-composition` diagnostics. See the [mutation cookbook](mutations.md) for
581
- empty patches, conditional upserts, arrays, and batching.
389
+ See [query inspection](query-inspection.md) for public types, finding codes, session behavior, and
390
+ the workflow combining inspection with persistent application database tests.
582
391
 
583
392
  ## Command reference
584
393
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kequtech/tenon-orm",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "A PostgreSQL ORM designed for coding agents, with explicit SQL, typed queries, and migrations",
@@ -28,16 +28,13 @@
28
28
  ".": {
29
29
  "types": "./dist/index.d.ts",
30
30
  "import": "./dist/index.js"
31
- },
32
- "./safeql": {
33
- "types": "./dist/safeql.d.ts",
34
- "import": "./dist/safeql.js"
35
31
  }
36
32
  },
37
33
  "files": [
38
34
  "dist/**/*.js",
39
35
  "dist/**/*.d.ts",
40
- "docs/"
36
+ "docs/",
37
+ "CHANGELOG.md"
41
38
  ],
42
39
  "engines": {
43
40
  "node": ">=24"
@@ -48,41 +45,23 @@
48
45
  "typecheck": "node node_modules/typescript/bin/tsc --noEmit -p tsconfig.json",
49
46
  "test": "npm run build && node node_modules/typescript/bin/tsc -p config/tsconfig.test.json && node .test-dist/test/check-database.js && node --test .test-dist/test/*.test.js",
50
47
  "test:consumer": "npm run build && node node_modules/typescript/bin/tsc -p config/tsconfig.test.json && node --test .test-dist/test/consumer.test.js",
48
+ "test:describe": "node node_modules/typescript/bin/tsc -p config/tsconfig.test.json && node --test .test-dist/test/describe*.test.js",
51
49
  "check": "npm run format:check && npm run typecheck && npm test",
52
50
  "prepack": "npm run build",
53
51
  "prepublishOnly": "npm run check",
54
52
  "format": "prettier --write .",
55
53
  "format:check": "prettier --check .",
56
- "benchmark": "npm run build && node scripts/benchmark.mjs"
54
+ "benchmark": "npm run build && node scripts/benchmark.mjs",
55
+ "test:pilots": "npm run build && node test/pilots/run.mjs"
56
+ },
57
+ "dependencies": {
58
+ "postgres": "3.4.9"
57
59
  },
58
60
  "devDependencies": {
59
61
  "@types/node": "^24.19.2",
60
- "@types/pg": "^8.23.1",
61
- "pg": "^8.23.1",
62
62
  "prettier": "^3.9.10",
63
63
  "typescript": "~6.0.2",
64
- "typescript-next": "npm:typescript@^7.0.2",
65
- "@ts-safeql/eslint-plugin": "^5.4.2",
66
- "typescript-eslint": "^8.58.0",
67
- "eslint": "^10.0.0",
68
- "@ts-safeql/plugin-utils": "^5.3.1"
69
- },
70
- "peerDependencies": {
71
- "pg": "^8.23.1",
72
- "typescript": ">=6",
73
- "@ts-safeql/eslint-plugin": "^5.4.2",
74
- "eslint": "^10.0.0"
75
- },
76
- "peerDependenciesMeta": {
77
- "typescript": {
78
- "optional": true
79
- },
80
- "@ts-safeql/eslint-plugin": {
81
- "optional": true
82
- },
83
- "eslint": {
84
- "optional": true
85
- }
64
+ "typescript-next": "npm:typescript@^7.0.2"
86
65
  },
87
66
  "bin": {
88
67
  "tenon": "./dist/cli.js"
@@ -1,9 +0,0 @@
1
- import ts from 'typescript';
2
- export { CompositionError } from './static.js';
3
- export declare function isExport(node: ts.Node, checker: ts.TypeChecker, name: string): boolean;
4
- export declare function isFragment(type: ts.Type): boolean;
5
- /** Reconstruct only immutable fragments and literal join lists. Never execute application code. */
6
- export declare function renderFragment(node: ts.Node, checker: ts.TypeChecker, seen?: Set<ts.Node>): string;
7
- export declare function renderIdentifier(node: ts.Node, checker: ts.TypeChecker, seen?: Set<ts.Node>): string;
8
- export declare function isIdentifierType(type: ts.Type): boolean;
9
- export declare function renderValue(node: ts.Node, checker: ts.TypeChecker, seen?: Set<ts.Node>): string;