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