@kequtech/tenon-orm 0.0.0-stage → 0.1.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 (72) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +105 -2
  3. package/dist/affected-rows.d.ts +11 -0
  4. package/dist/affected-rows.js +30 -0
  5. package/dist/baseline.d.ts +19 -0
  6. package/dist/baseline.js +100 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +158 -0
  9. package/dist/client.d.ts +4 -0
  10. package/dist/client.js +54 -0
  11. package/dist/connection.d.ts +15 -0
  12. package/dist/connection.js +51 -0
  13. package/dist/database.d.ts +7 -0
  14. package/dist/database.js +44 -0
  15. package/dist/errors.d.ts +9 -0
  16. package/dist/errors.js +18 -0
  17. package/dist/generate.d.ts +9 -0
  18. package/dist/generate.js +34 -0
  19. package/dist/generation/catalog.d.ts +19 -0
  20. package/dist/generation/catalog.js +35 -0
  21. package/dist/generation/row-types.d.ts +2 -0
  22. package/dist/generation/row-types.js +115 -0
  23. package/dist/generation/schema-summary.d.ts +6 -0
  24. package/dist/generation/schema-summary.js +114 -0
  25. package/dist/index.d.ts +18 -0
  26. package/dist/index.js +7 -0
  27. package/dist/migrations/baseline-sql.d.ts +3 -0
  28. package/dist/migrations/baseline-sql.js +38 -0
  29. package/dist/migrations/connection.d.ts +9 -0
  30. package/dist/migrations/connection.js +65 -0
  31. package/dist/migrations/files.d.ts +21 -0
  32. package/dist/migrations/files.js +131 -0
  33. package/dist/migrations/history.d.ts +12 -0
  34. package/dist/migrations/history.js +30 -0
  35. package/dist/migrations/sql.d.ts +2 -0
  36. package/dist/migrations/sql.js +94 -0
  37. package/dist/migrations.d.ts +10 -0
  38. package/dist/migrations.js +68 -0
  39. package/dist/mutations.d.ts +11 -0
  40. package/dist/mutations.js +87 -0
  41. package/dist/query-options.d.ts +15 -0
  42. package/dist/query-options.js +35 -0
  43. package/dist/query.d.ts +38 -0
  44. package/dist/query.js +129 -0
  45. package/dist/safeql/composition.d.ts +9 -0
  46. package/dist/safeql/composition.js +159 -0
  47. package/dist/safeql/eslint.d.ts +2 -0
  48. package/dist/safeql/eslint.js +45 -0
  49. package/dist/safeql/mutations.d.ts +3 -0
  50. package/dist/safeql/mutations.js +74 -0
  51. package/dist/safeql/plugin.d.ts +3 -0
  52. package/dist/safeql/plugin.js +43 -0
  53. package/dist/safeql/static.d.ts +10 -0
  54. package/dist/safeql/static.js +59 -0
  55. package/dist/safeql.d.ts +6 -0
  56. package/dist/safeql.js +7 -0
  57. package/dist/sql.d.ts +5 -0
  58. package/dist/sql.js +28 -0
  59. package/dist/transaction-sql.d.ts +2 -0
  60. package/dist/transaction-sql.js +67 -0
  61. package/dist/transaction.d.ts +4 -0
  62. package/dist/transaction.js +75 -0
  63. package/dist/types.d.ts +28 -0
  64. package/dist/types.js +1 -0
  65. package/dist/validation.d.ts +1 -0
  66. package/dist/validation.js +10 -0
  67. package/docs/adoption-guide.md +91 -0
  68. package/docs/agent-guide.md +310 -0
  69. package/docs/human-guide.md +271 -0
  70. package/docs/mutations.md +115 -0
  71. package/docs/reference.md +568 -0
  72. package/package.json +92 -4
@@ -0,0 +1,115 @@
1
+ # Mutation cookbook
2
+
3
+ Keep SQL structure visible and let the official helpers handle object fields. See the
4
+ [reference](reference.md#object-based-mutation-fragments) for the complete contract. Examples use
5
+ application-owned inputs and clients; add the authorization predicates required by your product.
6
+
7
+ ## Defaults, undefined, and null in batches
8
+
9
+ ```ts
10
+ await db.query(
11
+ sql`INSERT INTO profiles ${sql.insertValues([
12
+ { name: 'Ada', bio: null },
13
+ { name: 'Lin', score: 4, bio: undefined },
14
+ ])} RETURNING id`,
15
+ );
16
+ ```
17
+
18
+ The first row uses the score default. The second uses the bio default. Explicit null in the first
19
+ row stays null. Columns are aligned across rows. Omit generated columns and let PostgreSQL enforce
20
+ required fields and constraints.
21
+
22
+ For runtime-built rows, supply an inline column list for SafeQL:
23
+
24
+ ```ts
25
+ const rows: { name: string; bio?: string | null; score?: number }[] = buildRows();
26
+ await db.query(sql`INSERT INTO profiles ${sql.insertValues(rows, ['name', 'bio', 'score'])}`);
27
+ ```
28
+
29
+ This list selects and orders fields; unlisted fields are ignored. It does not assert that the
30
+ entire input object contains only allowed keys. Keep narrower application input validation.
31
+
32
+ ## Patches mixed with SQL expressions
33
+
34
+ ```ts
35
+ await db.query(sql`UPDATE profiles
36
+ SET ${sql.assignments({ name, bio })}, revision = revision + 1, score = DEFAULT
37
+ WHERE id = ${id}`);
38
+ ```
39
+
40
+ Undefined fields are omitted; null clears a field. Expressions, `DEFAULT`, and references to
41
+ `excluded.column` belong in SQL, not in mutation objects.
42
+
43
+ An empty patch throws even if the surrounding statement has other assignments. Choose an
44
+ expression-only query explicitly when that is the intended behavior:
45
+
46
+ ```ts
47
+ const patch: { name?: string; bio?: string | null } = readPatch();
48
+ if (patch.name !== undefined || patch.bio !== undefined) {
49
+ await db.query(sql`UPDATE profiles
50
+ SET ${sql.assignments(patch, ['name', 'bio'])}, revision = revision + 1
51
+ WHERE id = ${id}`);
52
+ } else {
53
+ await db.query(sql`UPDATE profiles SET revision = revision + 1 WHERE id = ${id}`);
54
+ }
55
+ ```
56
+
57
+ ## Preserve an omitted field during an upsert
58
+
59
+ Use complete query variants when an option changes conflict behavior. Both templates can be
60
+ checked independently; the checker need not choose a JavaScript branch:
61
+
62
+ ```ts
63
+ if (email !== undefined) {
64
+ await db.query(sql`INSERT INTO preferences ${sql.insertValues({ profile_id, in_app, email })}
65
+ ON CONFLICT (profile_id) DO UPDATE
66
+ SET in_app = excluded.in_app, email = excluded.email`);
67
+ } else {
68
+ await db.query(sql`INSERT INTO preferences ${sql.insertValues({ profile_id, in_app })}
69
+ ON CONFLICT (profile_id) DO UPDATE SET in_app = excluded.in_app`);
70
+ }
71
+ ```
72
+
73
+ The second query leaves an existing email value unchanged. Missing an insert field uses its
74
+ database default; that alone does not decide what an upsert should overwrite.
75
+
76
+ ## JSONB arrays and PostgreSQL arrays
77
+
78
+ Tenon preserves pg's value conversion. A JavaScript array binds as a PostgreSQL array. To write an
79
+ array into a JSON/JSONB column, serialize it explicitly:
80
+
81
+ ```ts
82
+ const handles = ['ada', 'lin'];
83
+ await db.query(
84
+ sql`INSERT INTO deliveries ${sql.insertValues({
85
+ recipients_json: JSON.stringify(handles), // jsonb column
86
+ recipient_handles: handles, // text[] column
87
+ })}`,
88
+ );
89
+ ```
90
+
91
+ Objects used as JSON values retain pg's ordinary JSON conversion. The helper does not infer JSON
92
+ versus array encoding from the table schema. SafeQL's projection checks do not validate stored JSON
93
+ contents or runtime input values.
94
+
95
+ ## Batches in one transaction
96
+
97
+ Choose the batch size in the caller and keep all writes on the transaction client:
98
+
99
+ ```ts
100
+ await db.transaction(async (tx) => {
101
+ for (let offset = 0; offset < rows.length; offset += 300) {
102
+ const batch = rows.slice(offset, offset + 300);
103
+ await tx.query(sql`INSERT INTO profiles ${sql.insertValues(batch, ['name', 'bio', 'score'])}`);
104
+ }
105
+ });
106
+ ```
107
+
108
+ A failure rolls back all batches. The helper rejects an empty batch; this loop makes no insert
109
+ calls for empty input. The query-wide limit is 65,535 bound parameters, including parameters outside
110
+ the mutation fragment. Defaults consume no parameters. Account for your selected column count and
111
+ surrounding query when choosing a batch size; Tenon never splits queries automatically.
112
+
113
+ Reusable authorization fragments remain valid at runtime. Keep shared policy in application
114
+ functions and test it rather than duplicating it for static coverage. Function-generated SQL,
115
+ conditional fragments, and dynamic filter lists remain outside static checking.
@@ -0,0 +1,568 @@
1
+ # Tenon reference
2
+
3
+ Use this document to look up exact API behavior, checker setup, and limitations. The
4
+ [human guide](human-guide.md) teaches the workflow; the [agent guide](agent-guide.md) is a
5
+ self-contained implementation guide. [Adoption](adoption-guide.md) covers replacing an existing ORM
6
+ and transferring migration history.
7
+
8
+ ## Installation and connection
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
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.
14
+
15
+ ```sh
16
+ npm install @kequtech/tenon-orm pg
17
+ npm install --save-dev @types/pg
18
+ ```
19
+
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
23
+ automatically.
24
+
25
+ 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`.
29
+
30
+ Qualify SQL names or configure the driver's `search_path`. The CLI's `--namespace` selects the
31
+ schema used by tooling, not a runtime query namespace.
32
+
33
+ ## Queries and result modes
34
+
35
+ ```ts
36
+ const result = await db.query(sql<Row>`SELECT ...`, {
37
+ returns: 'many',
38
+ label: 'list-users',
39
+ });
40
+ ```
41
+
42
+ `sql<Row>` produces a `SqlQuery<Row>` with SQL `text` and ordered `values`. It does not execute.
43
+ `Row` describes one selected row; fields are `unknown` when no annotation is supplied. Type
44
+ annotations do not verify SQL or validate JSON. Full table types from generation do not
45
+ automatically describe joins or aggregates.
46
+
47
+ `query` accepts `{ returns?, affectedRows?, label? }`. Unknown options are rejected. `label` must be
48
+ a nonempty string when supplied and is used for diagnostics.
49
+
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 |
56
+
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.
61
+
62
+ Reusable queries are ordinary functions. Accept a `QueryClient` and named object inputs when useful;
63
+ both `Database` and `Transaction` implement `QueryClient`. Keep result annotations on the SQL and
64
+ execution options at the query call. Public types include `QueryOptions`, `QueryReturnMode`, and
65
+ `QueryOutput<Row, Mode>`.
66
+
67
+ ## SQL parameters and identifiers
68
+
69
+ The `sql` tag binds ordinary interpolations as `$1`, `$2`, etc. Never quote interpolations or author
70
+ manual parameter placeholders inside templates. Every public query executes one statement, including
71
+ parameterless queries.
72
+
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.
77
+
78
+ Use an array parameter for a set of values:
79
+
80
+ ```ts
81
+ const query = sql<{ id: number }>`SELECT id FROM public."User"
82
+ WHERE id = ANY(${ids}::integer[])`;
83
+ ```
84
+
85
+ `identifier(schema, table)` quotes each component separately. Names must be nonempty, contain no
86
+ NUL, and fit PostgreSQL's 63-byte identifier limit. Choose names from application-approved
87
+ alternatives; quoting does not authorize access. Ordinary strings such as `'DESC'` are bound values,
88
+ not SQL syntax. For ordering, use complete query alternatives or application-selected fragments
89
+ containing `ASC` or `DESC`; runtime-selected fragments need checker exclusions and tests.
90
+
91
+ `unsafeSql<Row>(text, values)` accepts externally authored, trusted SQL and an explicit values
92
+ array, including `[]` when empty. It bypasses static SQL checking. Never concatenate request input
93
+ into its text. Full `sql` or `unsafeSql` queries cannot be inserted into other queries; use
94
+ fragments for partial SQL.
95
+
96
+ ## SQL fragments and joins
97
+
98
+ `sql.fragment` creates an immutable piece of SQL with bound values. It has no row type and cannot
99
+ execute alone. Insert it into a complete `sql<Row>` query. Constant fragments can share a predicate
100
+ between, for example, a list query and a count query. Ordinary fixed conditions are usually clearer
101
+ directly in the query.
102
+
103
+ `sql.join(fragments, separator)` combines a nonempty array of fragments into a new fragment.
104
+ Separators are exactly:
105
+
106
+ | Argument | Inserted SQL | Typical use |
107
+ | -------- | ------------ | -------------------------- |
108
+ | `'AND'` | `AND` | All conditions must hold |
109
+ | `'OR'` | `OR` | Any condition may hold |
110
+ | `','` | `, ` | Projections or assignments |
111
+
112
+ No default, arbitrary string, or fragment separator is accepted. Runtime validation also protects
113
+ JavaScript callers. A single item is returned as a new fragment without extra separators. Empty
114
+ lists and blank fragments throw; choose empty-list behavior explicitly in application code.
115
+
116
+ Joins do not add parentheses or alter SQL precedence. Group alternatives before combining them with
117
+ other conditions:
118
+
119
+ ```ts
120
+ const alternatives = sql.join(
121
+ [sql.fragment`email = ${primary}`, sql.fragment`email = ${secondary}`],
122
+ 'OR',
123
+ );
124
+ const conditions = sql.join(
125
+ [sql.fragment`(${alternatives})`, sql.fragment`active = ${true}`],
126
+ 'AND',
127
+ );
128
+ const query = sql<{ id: number }>`SELECT id FROM public."User" WHERE ${conditions}`;
129
+ ```
130
+
131
+ Each fragment use receives correctly ordered parameters, including repeated and nested uses. SQL
132
+ strings and comments containing literal `$1` text are left intact.
133
+
134
+ Static checking supports constant fragments, literal join arrays, literal or const-string
135
+ separators, and [official mutation helpers](#object-based-mutation-fragments). Runtime-built lists
136
+ and selected fragments execute safely with respect to parameter binding, but the checker cannot
137
+ verify their SQL or result types. Prefer fixed SQL when it fits.
138
+ Otherwise keep an explicit result annotation, use a narrow explained lint suppression, and test
139
+ every variant.
140
+
141
+ ## Object-based mutation fragments
142
+
143
+ `sql.insertValues(input, columns?)` and `sql.assignments(input, columns?)` return fragments. Keep the
144
+ operation, table, authorization predicate, conflict policy, expressions, and `RETURNING` in SQL:
145
+
146
+ ```ts
147
+ const created = await db.query(
148
+ sql<{ id: number }>`
149
+ INSERT INTO profiles ${sql.insertValues([
150
+ { name: 'Ada', bio: null },
151
+ { name: 'Lin', score: 4 },
152
+ ])}
153
+ ON CONFLICT (name) DO NOTHING
154
+ RETURNING id
155
+ `,
156
+ { returns: 'many' },
157
+ );
158
+
159
+ await db.query(sql`
160
+ UPDATE profiles SET ${sql.assignments({ name: input.name, bio: input.bio })},
161
+ revision = revision + 1
162
+ WHERE id = ${id} AND owner_id = ${viewer}
163
+ `);
164
+ ```
165
+
166
+ Both helpers accept plain objects (including null-prototype objects), quote each column as one
167
+ identifier, and bind cell values as data. Inputs are read when the fragment is created; replacing a
168
+ field afterward does not change its captured value. Nested values retain the driver's usual
169
+ serialization behavior. Only own data properties are read; accessors are rejected.
170
+
171
+ | Input | Insert behavior | Assignment behavior |
172
+ | -------------------------------------------- | ---------------------------------- | --------------------------------------- |
173
+ | Missing or `undefined` field | PostgreSQL `DEFAULT` for that cell | Omitted; the stored column is unchanged |
174
+ | Explicit `null` | Bound SQL null | Bound SQL null |
175
+ | Other value | Bound parameter | Bound parameter |
176
+ | SQL fragment, identifier, or query as a cell | Rejected | Rejected |
177
+
178
+ Without `columns`, inserts infer the union of enumerable keys with defined values across all rows,
179
+ in first-seen order. Heterogeneous rows stay aligned; missing cells use `DEFAULT`. One object or a
180
+ one-row batch with no defined fields emits `DEFAULT VALUES`. An empty batch throws. A multi-row
181
+ batch with no inferred columns throws; supply explicit columns to emit multiple default rows.
182
+
183
+ Assignments infer enumerable keys and omit undefined values. An empty object or a selection with
184
+ no defined values throws, so the helper never emits an empty `SET` clause. This also applies when
185
+ the surrounding statement contains arithmetic or other assignments: branch explicitly when the
186
+ intended update consists only of those expressions.
187
+
188
+ An optional nonempty column list selects and orders fields. It does not validate that the input
189
+ object has no additional keys. Unlisted fields are ignored; missing insert cells use `DEFAULT`,
190
+ while missing update fields are omitted. Duplicate or invalid column
191
+ names throw. TypeScript checks selected names against the input's known keys. The list is also the
192
+ way to make runtime objects and batches available to static SQL checking:
193
+
194
+ ```ts
195
+ const rows: { name: string; bio?: string | null }[] = buildRows();
196
+ const insert = sql`INSERT INTO profiles ${sql.insertValues(rows, ['name', 'bio'])} RETURNING id`;
197
+
198
+ const patch: { name?: string; bio?: string | null } = readPatch();
199
+ const update = sql`UPDATE profiles SET ${sql.assignments(patch, ['name', 'bio'])} WHERE id = ${id}`;
200
+ ```
201
+
202
+ The helpers have no schema metadata. Omit generated columns; PostgreSQL enforces identity,
203
+ generation, required-column, and constraint rules. They do not generate insert/update input types
204
+ or prove that the JavaScript input values match database column types. Retain application input
205
+ contracts and validation. To reset an update field to its default, write `column = DEFAULT` in the
206
+ surrounding SQL. Put arithmetic and `excluded.column` expressions there too; these helpers accept
207
+ values only, with no expression/default sentinel.
208
+
209
+ A compiled query, including surrounding interpolations, is limited to 65,535 bound parameters.
210
+ Tenon rejects larger queries and does not split them automatically. Choose batch sizes and
211
+ transaction boundaries in the caller. Helper validation errors identify invalid rows/columns
212
+ without printing cell values.
213
+
214
+ ### Checking mutation fragments
215
+
216
+ SafeQL recognizes these official helpers directly through their Tenon function declarations,
217
+ including aliases. It never analyzes their implementation or executes application data builders.
218
+ Without an explicit column list, checking accepts inline object literals and inline insert arrays
219
+ of object literals. Keys must be literal names; object spreads, computed keys, methods, accessors,
220
+ and objects stored in variables require an explicit inline column list. Existing constant string
221
+ resolution works for entries of that list; column arrays stored in variables are unsupported.
222
+
223
+ For runtime inputs with explicit columns, the checker analyzes a representative nonempty insert
224
+ row or assignment list containing **all selected columns**. This checks column references, SQL
225
+ composition, conflict clauses, and `RETURNING` annotations. Runtime omission/default decisions and
226
+ batch length do not require interpreting JavaScript. The check is conservative: an optional
227
+ selected column is checked even when a particular runtime call omits it. It does not establish
228
+ nonempty input, valid runtime values, successful constraints, or every default's behavior; runtime
229
+ validation and database tests cover those contracts. Unknown inputs without explicit columns
230
+ produce `tenon/check-composition` diagnostics. See the [mutation cookbook](mutations.md) for
231
+ empty patches, conditional upserts, arrays, and batching.
232
+
233
+ ## Transactions, guarded writes, and errors
234
+
235
+ ```ts
236
+ await db.transaction(async (tx) => {
237
+ const id = 42;
238
+ await tx.query(sql`UPDATE public."User" SET active = false WHERE id = ${id}`, {
239
+ affectedRows: { exact: 1 },
240
+ });
241
+ // Use tx for every query that belongs to this transaction.
242
+ });
243
+ ```
244
+
245
+ Transactions belong to one database instance; they do not coordinate writes across databases. The
246
+ callback commits on success and rolls back when an error escapes. Await all work before returning;
247
+ the callback client expires afterward. A helper accepting `QueryClient` works with either `db` or
248
+ `tx`. Nested callback transactions and automatic retries are not provided.
249
+
250
+ `affectedRows: { exact: n }` requires n affected rows; `{ max: n }` permits up to n. Both require
251
+ nonnegative safe integers. Checks apply to INSERT, UPDATE, DELETE, and MERGE, independent of
252
+ `RETURNING`. A root call uses a transaction; a call on `tx` uses a savepoint. A mismatch rolls back
253
+ before throwing. You may catch a guarded failure and continue the outer transaction. Unguarded
254
+ PostgreSQL errors normally abort that transaction, even when caught.
255
+
256
+ The count covers the final command, excluding additional trigger, cascade, or CTE effects. It cannot
257
+ identify the wrong single row. Returned-row checks alone do not undo autocommitted writes. Rollback
258
+ cannot undo sequence advances or external side effects of database functions.
259
+
260
+ Set isolation and timeouts with SQL, for example `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`
261
+ before data queries and `SET LOCAL statement_timeout = '5s'`. Manual savepoints are available
262
+ through SQL; reserve `tenon_affected_rows_` names for Tenon. Direct BEGIN/COMMIT/ROLLBACK boundaries
263
+ are owned by `transaction`.
264
+
265
+ SQL errors retain pg's codes and details. Invalid inputs and returned-row mismatches throw ordinary
266
+ `Error` objects. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
267
+ `TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`. When both work and cleanup
268
+ fail, `AggregateError` preserves both failures. A lost commit acknowledgment can leave the outcome
269
+ unknown; retry only when the application's operation makes that safe.
270
+
271
+ Optional `createTenon({ connection, onQuery })` observes completed calls with `label`, `durationMs`,
272
+ `outcome`, and available `command`, `rowCount`, `errorCode`. It receives no SQL or values. Observers
273
+ are not awaited; failures emit warnings. A successful callback query does not mean its outer
274
+ transaction has committed.
275
+
276
+ ## Generated types and checking
277
+
278
+ `generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript source for the
279
+ connected schema. It does not need migration files or history. Table type generation is optional.
280
+ SafeQL reads the database directly and does not require these generated files; they are useful when
281
+ reusing table row shapes. The CLI writes that source atomically. This command checks the existing
282
+ file without rewriting it:
283
+
284
+ ```sh
285
+ npx tenon generate --out src/generated-schema.ts --check
286
+ ```
287
+
288
+ `--check` compares the generated representation with the current database. Pending migration files
289
+ are a separate concern. The representation includes table columns, constraints, indexes, and
290
+ referenced domains, not every PostgreSQL object.
291
+
292
+ Generation reads a consistent snapshot of ordinary tables and partitioned roots. Use the generated
293
+ named type in queries, for example `sql<User>`. For other table names, define an alias such as
294
+ `type AuditRow = Rows['audit-log']` and use `sql<AuditRow>`; SafeQL resolves named aliases but not
295
+ indexed access directly in `sql<Rows['audit-log']>`. Views, foreign tables, and attached partitions
296
+ use query-specific types. A missing namespace fails; an existing empty schema is valid.
297
+
298
+ | PostgreSQL type | Default pg value |
299
+ | ---------------------------------------------- | ----------------------------------------------- |
300
+ | smallint, integer, oid, real, double precision | `number` |
301
+ | bigint, numeric | `string` |
302
+ | boolean | `boolean` |
303
+ | text, varchar, char, uuid, time, timetz | `string` |
304
+ | date, timestamp, timestamptz | `Date \| number` (numbers represent infinities) |
305
+ | enum | String literal union; `never` for an empty enum |
306
+ | json, jsonb | `unknown` |
307
+ | bytea | `Buffer` |
308
+
309
+ Nullable columns add null. Domains use verified underlying mappings and the column's own
310
+ nullability. Unverified types warn and become `unknown`. Verified arrays use
311
+ `PgArray<T> = (T | null | PgArray<T>)[]` for null elements and multiple dimensions. Numeric arrays
312
+ contain numbers and can lose precision; cast to text[] for exact values. Enum arrays and arrays of
313
+ domains are unverified. Finite dates and timestamps without time zone use Node's local timezone.
314
+ Custom pg parsers need matching application types.
315
+
316
+ ## SQL checking with SafeQL
317
+
318
+ SafeQL checks SQL against a real PostgreSQL schema and infers result types. ESLint runs that check
319
+ on your source files. Tenon runs the queries in the application; it does not import or run either
320
+ tool during query execution.
321
+
322
+ Configure [SafeQL normally](https://safeql.dev/api/). Tenon's optional `safeql` entry point exports
323
+ two objects:
324
+
325
+ - `safeqlPlugin` is a SafeQL plugin descriptor. Add it to each Tenon connection's `plugins` list. It
326
+ translates constant fragments, official mutation helpers, and identifiers, preserves parameter
327
+ behavior, and supplies pg result mappings. SafeQL owns database analysis and annotation fixes.
328
+ - `eslintPlugin` supplies `tenon/check-composition`. Register it in ESLint and enable the rule to
329
+ report queries that the adapter cannot resolve, including runtime-built fragments and escaped
330
+ outer templates.
331
+
332
+ Use both. The adapter skips unsupported queries; the companion rule makes those exclusions visible
333
+ as ordinary, individually suppressible lint errors. Neither replaces or wraps SafeQL's rule.
334
+
335
+ ### Install and configure
336
+
337
+ Install the checking tools as application development dependencies, reusing the application's
338
+ existing TypeScript installation:
339
+
340
+ ```sh
341
+ npm install --save-dev eslint typescript-eslint @ts-safeql/eslint-plugin
342
+ ```
343
+
344
+ SafeQL and ESLint are the integration's hosts, and the adapter imports TypeScript to inspect query
345
+ expressions. They are optional peers because Tenon's database runtime does not need them.
346
+ `typescript-eslint` is an application-owned parser dependency, not a Tenon peer. Your ESLint
347
+ configuration imports SafeQL and the parser explicitly. Use a TypeScript version supported by that
348
+ lint toolchain. The `pg` driver remains Tenon's required runtime peer.
349
+
350
+ For a project with `src/**/*.ts` and a `tsconfig.json`:
351
+
352
+ ```js
353
+ // eslint.config.mjs
354
+ import { defineConfig } from 'eslint/config';
355
+ import safeql from '@ts-safeql/eslint-plugin/config';
356
+ import tseslint from 'typescript-eslint';
357
+ import { safeqlPlugin, eslintPlugin } from '@kequtech/tenon-orm/safeql';
358
+
359
+ const databaseUrl = process.env.DATABASE_URL;
360
+ if (!databaseUrl) throw new Error('DATABASE_URL is required for SQL checking');
361
+
362
+ export default defineConfig({
363
+ files: ['src/**/*.ts'],
364
+ languageOptions: {
365
+ parser: tseslint.parser,
366
+ parserOptions: { projectService: true },
367
+ },
368
+ plugins: { tenon: eslintPlugin },
369
+ rules: { 'tenon/check-composition': 'error' },
370
+ extends: [
371
+ safeql.configs.connections({
372
+ databaseUrl,
373
+ plugins: [safeqlPlugin],
374
+ targets: [{ tag: 'sql' }],
375
+ }),
376
+ ],
377
+ });
378
+ ```
379
+
380
+ Add these settings to your existing ESLint setup if you have one; reuse its type-aware parser
381
+ configuration. `projectService` uses the nearest TypeScript project for each file. Supply
382
+ `DATABASE_URL` through your application's environment setup and point it at a development/test
383
+ database with migrations applied. This example does not load `.env` files.
384
+
385
+ The `.mjs` example avoids a current declaration mismatch between SafeQL's configuration helper and
386
+ ESLint's `defineConfig`. This affects configuration typechecking, not query result types.
387
+
388
+ `targets` selects the SQL tag spelling. If you alias `sql`, add the alias to the targets. For a
389
+ single database, `targets: [{ tag: { regex: '.*' } }]` selects all tags; Tenon's adapter filters
390
+ that down to actual Tenon queries, including aliases and object properties. Neither the adapter nor
391
+ the rule executes application code.
392
+
393
+ Reuse the application's lint scripts. If none exist, add these, adjusting file selection as needed.
394
+ They run all enabled ESLint rules, and `lint:fix` can change non-SQL code too:
395
+
396
+ ```json
397
+ {
398
+ "scripts": {
399
+ "lint": "eslint \"src/**/*.ts\"",
400
+ "lint:fix": "eslint \"src/**/*.ts\" --fix"
401
+ }
402
+ }
403
+ ```
404
+
405
+ ### Generate and check annotations
406
+
407
+ Write a query without a result annotation:
408
+
409
+ ```ts
410
+ const query = sql`SELECT id, email FROM public."User" WHERE id = ${id}`;
411
+ ```
412
+
413
+ Run `npm run lint:fix`. SafeQL can write `sql<{ id: number; email: string }>` using the connected
414
+ schema. It can also repair an incorrect annotation. Review the changes, run TypeScript and database
415
+ tests, and commit the result. Without an annotation, Tenon's result fields stay `unknown`.
416
+
417
+ Run `npm run lint` in CI without `--fix`. Missing/incorrect annotations, invalid SQL, and
418
+ unsupported composition fail the check without rewriting files. After schema changes, run the
419
+ development [`db:migrate` script](human-guide.md#create-a-table) before checking queries. Inspect
420
+ SQL mistakes before accepting types generated from an unintended query. ESLint's fix command may
421
+ also apply other enabled rules.
422
+
423
+ ### Configuration choices and multiple databases
424
+
425
+ All connection, target, and override options belong to SafeQL. There is no Tenon-specific
426
+ configuration wrapper or separate database setting. Keep `safeqlPlugin` on each Tenon connection:
427
+ without it, SafeQL does not resolve Tenon fragments and can infer different parameter types from
428
+ those returned at runtime.
429
+
430
+ For multiple databases, pass a connection array to `safeql.configs.connections`. With
431
+ `usersDatabaseUrl` and `billingDatabaseUrl` set to your development/test URLs, replace the single
432
+ connection in the setup above with:
433
+
434
+ ```js
435
+ safeql.configs.connections([
436
+ {
437
+ databaseUrl: usersDatabaseUrl,
438
+ plugins: [safeqlPlugin],
439
+ targets: [{ tag: 'usersSql' }],
440
+ },
441
+ {
442
+ databaseUrl: billingDatabaseUrl,
443
+ plugins: [safeqlPlugin],
444
+ targets: [{ tag: 'billingSql' }],
445
+ },
446
+ ]);
447
+ ```
448
+
449
+ Queries can then share a file:
450
+
451
+ ```ts
452
+ import { sql as usersSql, sql as billingSql } from '@kequtech/tenon-orm';
453
+
454
+ const users = usersSql`SELECT id, email FROM public.users`;
455
+ const invoices = billingSql`SELECT id, total FROM public.invoices`;
456
+ ```
457
+
458
+ The aliases only select a checker connection; they are the same runtime `sql` function. Execute each
459
+ query with the correct `createTenon` instance. Neither SafeQL nor Tenon's composition rule follows
460
+ `db.query` calls to infer which database will execute a query. Keep connection targets distinct so a
461
+ query is checked against its intended database.
462
+
463
+ If queries are already separated by file, keep the `sql` tag and use an ESLint config object per
464
+ file group, each extending its own `safeql.configs.connections({ ... })`. Use non-overlapping
465
+ `files` patterns when database settings differ. Normal SafeQL configurations for other libraries can
466
+ coexist; add Tenon's adapter only to connections checking Tenon queries.
467
+
468
+ If a database has same-named tables in different schemas, qualify them in SQL, for example
469
+ `SELECT id FROM billing.accounts`. SafeQL can infer the wrong type for an unqualified table even
470
+ when the connection's `search_path` selects the correct table at execution. Explicit schema names
471
+ avoid that ambiguity.
472
+
473
+ ### Supported SQL and limitations
474
+
475
+ Within the configured targets, the adapter recognizes Tenon's `sql` export through imports, local
476
+ aliases, namespace imports, and object properties that retain its TypeScript type. It checks
477
+ constant fragments and `sql.join` with nonempty literal arrays, including nested fragments.
478
+ Separators and `identifier()` names must resolve to literal strings or const string initializers.
479
+ Values use the same parameter placeholders in static checking and execution, inside or outside
480
+ fragments. PostgreSQL infers their types from SQL context; Tenon does not add casts based on
481
+ JavaScript types.
482
+
483
+ The checker follows const fragment initializers, including imported constants with inspectable
484
+ source. It does not inspect function bodies, substitute helper arguments/defaults, or choose
485
+ JavaScript conditional branches. Function-generated fragments and conditional SQL structure are
486
+ unsupported even when a particular helper appears simple or a condition is a literal. Mutable
487
+ references, runtime filter lists, and dynamic identifiers are also unsupported. Ordinary value
488
+ expressions remain bound parameters; calls that produce data for an official mutation helper can
489
+ use its explicit column-list contract.
490
+
491
+ Reusable authorization and other application policy can still live in fragment-returning functions
492
+ at runtime. Keep that policy centralized and cover its behavior with database tests. Suppress
493
+ `tenon/check-composition` only on the particular query, with a reason, when its variants are tested.
494
+ Do not duplicate policy merely to obtain static coverage. SQL and annotation errors in checkable
495
+ queries continue to come from `@ts-safeql/check-sql`.
496
+
497
+ Ordinary functions containing complete `sql` templates remain checkable. Each template is analyzed
498
+ where it is written, including constant fragments defined in that function:
499
+
500
+ ```ts
501
+ function findUsers(email: string) {
502
+ const predicate = sql.fragment`email = ${email}`;
503
+ return sql`SELECT id FROM users WHERE ${predicate}`;
504
+ }
505
+ ```
506
+
507
+ Use literal line breaks in `sql` templates and bind string values with `${value}`. The composition
508
+ rule rejects JavaScript escapes such as `\n` or `\'` in the outer template: SafeQL reads raw
509
+ template text, while Tenon executes decoded JavaScript text. This diagnostic prevents checking
510
+ different SQL from what runs. Escapes inside bound values are fine; constant fragments are decoded
511
+ by Tenon's adapter before checking.
512
+
513
+ This checks SQL structure and returned-row annotations, not JavaScript input compatibility or
514
+ runtime values. For example, a boolean passed to an integer comparison is rejected by PostgreSQL at
515
+ execution, not by this lint rule. Keep typed function parameters, validate external inputs, and test
516
+ database behavior. Explicit SQL casts are checked as part of the SQL.
517
+
518
+ The adapter supplies scalar mappings for bigint/numeric strings, unknown JSON, and `Date | number`
519
+ temporal values (including infinities). For recursive arrays, use column overrides matching
520
+ generated types, for example:
521
+
522
+ ```js
523
+ safeql.configs.connections({
524
+ databaseUrl,
525
+ plugins: [safeqlPlugin],
526
+ targets: [{ tag: 'sql' }],
527
+ overrides: {
528
+ columns: { 'User.tags': 'PgArray<string>', 'User.amounts': 'PgArray<number>' },
529
+ },
530
+ });
531
+ ```
532
+
533
+ Import `PgArray` from generated definitions in files using that annotation. Numeric arrays use
534
+ numbers with pg's default parser, even though numeric scalars use strings. Array dimensions/null
535
+ elements, custom parsers, and unverified PostgreSQL types need appropriate overrides and database
536
+ tests; a checker cannot infer your parser policy. Column keys are `Table.column`, so duplicate table
537
+ names across schemas need care. Type overrides merge with the adapter's defaults. `unsafeSql` is
538
+ outside static checking, and arbitrary JSON still needs application validation.
539
+
540
+ ## Command reference
541
+
542
+ All database commands use `DATABASE_URL`; environment files are not loaded automatically. For
543
+ multiple databases, run commands with the intended database's URL and migration directory, and use
544
+ distinct generated output files if generating table types.
545
+
546
+ ```text
547
+ tenon new <name> [--directory migrations]
548
+ tenon migrate [--directory migrations] [--namespace public]
549
+ tenon status [--directory migrations] [--namespace public]
550
+ tenon rollback [--directory migrations] [--namespace public] [--steps 1]
551
+ tenon generate [--namespace public] [--out src/generated-schema.ts] [--check]
552
+ tenon baseline capture|adopt [--directory migrations] [--namespace public]
553
+ ```
554
+
555
+ Migration filenames are `<numeric-id>_<name>.up.sql` and optional matching down files. IDs sort
556
+ numerically. Each migration and its history entry commit together; earlier files remain applied if a
557
+ later file fails. Status reports applied/pending IDs. Rollback checks that every selected file has
558
+ down SQL before executing any. Use dollar-quoted function bodies and explicit `E'...'` for backslash
559
+ escapes. Nontransactional DDL such as `CREATE INDEX CONCURRENTLY` needs a separate procedure.
560
+
561
+ Tools coordinate through session advisory locks per database/namespace. Use direct connections or
562
+ session pooling, with PostgreSQL timeouts to bound waiting.
563
+
564
+ Programmatic `migrate`, `migrationStatus`, and `rollback` accept
565
+ `{ connection, directory?, namespace? }`; rollback also accepts `steps`.
566
+ `createMigration({ name, directory? })` scaffolds files; `readMigrations(directory?)` reads
567
+ validated SQL and checksums. Baseline capture/adoption are covered in the
568
+ [adoption guide](adoption-guide.md).