@kequtech/tenon-orm 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,7 +68,7 @@ migrations and table type generation are independent conveniences.
68
68
 
69
69
  For object-based writes, `sql.insertValues(rows)` preserves database defaults across heterogeneous
70
70
  rows and `sql.assignments(patch)` omits undefined fields. Keep the surrounding SQL explicit. Both
71
- support selected columns and [SafeQL checking](docs/reference.md#object-based-mutation-fragments).
71
+ support selected columns and [SafeQL checking](docs/reference.md#checking-mutation-fragments).
72
72
  Use [generated input types](docs/reference.md#insert-and-update-inputs) to check those objects
73
73
  against your schema.
74
74
 
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import { dirname, resolve } from 'node:path';
5
5
  import { parseArgs } from 'node:util';
6
6
  import { adoptBaseline, captureBaseline } from './baseline.js';
7
7
  import { generateDefinitions } from './generate.js';
8
+ import { definitionDiff } from './generation/diff.js';
8
9
  import { migrate, migrationStatus, rollback } from './migrations.js';
9
10
  import { createMigration } from './migrations/files.js';
10
11
  const help = `tenon migrate|status|rollback [--directory migrations] [--namespace public]
@@ -30,9 +31,10 @@ async function writeDefinitions(source, filename, check) {
30
31
  if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
31
32
  throw error;
32
33
  }
34
+ throw new Error(`Generated definitions file is missing: ${output}\nRun tenon generate with the same options, omitting --check, then commit the result.`);
33
35
  }
34
36
  if (existing !== source) {
35
- throw new Error('Generated definitions are stale or missing; run tenon generate and commit the result.');
37
+ throw new Error(`Generated definitions differ: ${output}\n${definitionDiff(existing, source)}\nRun tenon generate with the same options, omitting --check, then commit the result.`);
36
38
  }
37
39
  process.stdout.write('Generated definitions are current.\n');
38
40
  return;
@@ -152,7 +154,7 @@ async function main() {
152
154
  }
153
155
  }
154
156
  main().catch((error) => {
155
- // Do not print connection configuration or raw SQL/parameter payloads.
157
+ // Report diagnostics without connection configuration or query parameter payloads.
156
158
  process.stderr.write(`${error instanceof Error ? error.message : 'Database command failed.'}\n`);
157
159
  process.exitCode = 1;
158
160
  });
@@ -0,0 +1,2 @@
1
+ /** A bounded text excerpt, not a schema comparison or an applicable patch. */
2
+ export declare function definitionDiff(saved: string, generated: string): string;
@@ -0,0 +1,30 @@
1
+ /** A bounded text excerpt, not a schema comparison or an applicable patch. */
2
+ export function definitionDiff(saved, generated) {
3
+ const before = saved.split('\n');
4
+ const after = generated.split('\n');
5
+ let start = 0;
6
+ while (start < before.length && start < after.length && before[start] === after[start]) {
7
+ start++;
8
+ }
9
+ if (start === before.length && start === after.length)
10
+ return '';
11
+ let beforeEnd = before.length;
12
+ let afterEnd = after.length;
13
+ while (beforeEnd > start && afterEnd > start && before[beforeEnd - 1] === after[afterEnd - 1]) {
14
+ beforeEnd--;
15
+ afterEnd--;
16
+ }
17
+ function excerpt(lines, end, marker) {
18
+ const shown = lines.slice(start, Math.min(end, start + 8));
19
+ const output = shown.map((line) => `${marker} ${line.length > 200 ? line.slice(0, 200) + '…' : line}`);
20
+ if (end - start > shown.length) {
21
+ output.push(` … ${end - start - shown.length} more ${marker} lines omitted`);
22
+ }
23
+ return output;
24
+ }
25
+ return [
26
+ `Diff excerpt from line ${start + 1} (- saved, + generated):`,
27
+ ...excerpt(before, beforeEnd, '-'),
28
+ ...excerpt(after, afterEnd, '+'),
29
+ ].join('\n');
30
+ }
@@ -88,11 +88,9 @@ is an optional nonempty diagnostic string.
88
88
  as `'DESC'` remain bound values, not SQL syntax.
89
89
  - `sql.fragment` carries SQL and bound values without a row type. Use it for shared predicates or
90
90
  partial SQL. Fragments cannot execute alone; full `sql` and `unsafeSql` queries cannot be nested.
91
- - `sql.join(fragments, separator)` requires a nonempty array of fragments and exactly `'AND'`,
92
- `'OR'`, or `','`. No fragment or arbitrary-string separator is accepted. Empty lists and blank
93
- fragments throw. Choose empty-filter behavior explicitly.
94
- - Joins add no parentheses. Group mixed `AND`/`OR` predicates explicitly and preserve authorization
95
- conditions when adding optional filters.
91
+ - `sql.join(fragments, separator)` combines a variable-length list of fragments using `'AND'`,
92
+ `'OR'`, or `','`. Use it for optional filters, search terms, or lists of SQL expressions. The list
93
+ must be nonempty; group mixed `AND`/`OR` predicates with parentheses.
96
94
  - `unsafeSql<Row>(text, values)` accepts trusted external SQL with an explicit values array. Never
97
95
  concatenate request input into its text. Each query executes one statement.
98
96
 
@@ -106,13 +104,8 @@ const count = sql<{ count: string }>`SELECT count(*) AS count FROM public."User"
106
104
  WHERE ${search}`;
107
105
  ```
108
106
 
109
- `pattern` and `limit` are validated application inputs. To compose existing predicates, use
110
- `sql.join([first, second], 'AND')`. The checker supports literal join arrays, constant fragments,
111
- literal/const-string separators, and [official mutation helpers](reference.md#object-based-mutation-fragments).
112
- It rejects mutable or runtime-dependent SQL structure. Prefer fixed SQL for simple optional filters, e.g.
113
- `(${email}::text IS NULL OR email = ${email})`. If runtime composition is required, retain an
114
- explicit result type, suppress `tenon/check-composition` only on that query with a reason, and test
115
- each branch. Parameter binding alone does not establish result correctness.
107
+ `pattern` and `limit` are validated application inputs. See the
108
+ [composition reference](reference.md#sql-fragments-and-joins) for grouping and list examples.
116
109
 
117
110
  ## Object-based writes
118
111
 
@@ -129,11 +122,9 @@ narrow editable fields with `Pick`. They account for defaults, nullability, doma
129
122
  columns. Write mappings differ from returned rows: timestamps use `Date | string`, bigint uses
130
123
  `string | bigint`, and JSON uses serialized text. See the [input types](reference.md#insert-and-update-inputs).
131
124
 
132
- SafeQL reads inline object/array literals directly. Runtime inputs, spreads and data-builder results
133
- need explicit inline selected columns, e.g. `sql.insertValues(rows, ['name', 'score'])`. Checking
134
- uses all selected columns conservatively, without interpreting application JavaScript. It verifies
135
- SQL and result annotations, not input values, constraints, or nonempty runtime input. See the
136
- [mutation contract](reference.md#object-based-mutation-fragments) and [cookbook](mutations.md).
125
+ Selected columns choose and order fields, e.g. `sql.insertValues(rows, ['name', 'score'])`. Unlisted
126
+ fields are ignored. See the [mutation contract](reference.md#object-based-mutation-fragments) and
127
+ [cookbook](mutations.md).
137
128
 
138
129
  ## Configure and run SQL checking
139
130
 
@@ -205,6 +196,15 @@ Reuse existing lint scripts or add:
205
196
  SQL and annotation errors come from `@ts-safeql/check-sql`; unsupported composition errors come from
206
197
  `tenon/check-composition`. Keep both rules enabled.
207
198
 
199
+ The checker supports constant fragments and `sql.join` with inline nonempty arrays and literal or
200
+ const-string separators. It does not follow runtime-built lists or fragment-returning functions.
201
+ For tested runtime variants, keep an explicit result type and suppress `tenon/check-composition`
202
+ only on the particular query with a reason.
203
+
204
+ Mutation helpers accept inline object literals for checking. Runtime inputs, spreads, and
205
+ data-builder results need inline selected columns, e.g. `sql.insertValues(rows, ['name', 'score'])`.
206
+ The check uses all selected columns; it does not validate runtime input values or omission decisions.
207
+
208
208
  For existing applications, use the [incremental audit workflow](adoption-guide.md#adopt-sql-checking-incrementally)
209
209
  to preview fixes and review database types versus application refinements.
210
210
 
@@ -184,9 +184,9 @@ Generated insert types distinguish required fields from defaults and nullable fi
184
184
  make writable fields optional; use `Pick` to limit what an action may edit. See the
185
185
  [input-type reference](reference.md#insert-and-update-inputs) for write-value mappings.
186
186
 
187
- Keep expressions, authorization, conflict handling and `RETURNING` in SQL. The selected column lists
188
- above also let the optional SQL checker inspect runtime-built inputs. They select fields rather
189
- than validate the complete object. See the [mutation cookbook](mutations.md) for batches and upserts.
187
+ Keep expressions, authorization, conflict handling and `RETURNING` in SQL. Selected column lists
188
+ choose which fields to write; they do not validate the complete object. See the
189
+ [mutation cookbook](mutations.md) for batches and upserts.
190
190
 
191
191
  ## Check your SQL and result types
192
192
 
@@ -215,6 +215,12 @@ an incorrect declared result type before the query runs.
215
215
  Input validation and database tests still matter. Checking a query does not prove that a request is
216
216
  authorized, that its values are valid, or that returned JSON has an application-specific shape.
217
217
 
218
+ The checker supports constant fragments and `sql.join` with inline arrays. Runtime-built lists and
219
+ fragment-returning functions remain outside static analysis. Keep result annotations and database
220
+ tests for those queries; when using the checker, suppress `tenon/check-composition` only on the
221
+ tested query with a reason. Runtime-built mutation inputs need inline selected columns for SQL
222
+ checking. See the [checking reference](reference.md#supported-sql-and-limitations) for details.
223
+
218
224
  ## Reuse or assemble parts of SQL when needed
219
225
 
220
226
  For fixed conditions, write them directly:
@@ -239,37 +245,26 @@ const count = sql<{ count: string }>`SELECT count(*) AS count FROM public."User"
239
245
  ```
240
246
 
241
247
  Here `pattern` is an application-supplied search pattern and `limit` is a validated page size.
242
- Execute each query with `db.query`. A fragment cannot execute alone; the complete query supplies the
243
- result type. The checker can inspect these constant fragments.
248
+ Execute each query with `db.query`. The complete query supplies the result type.
244
249
 
245
- For a simple optional filter, a fixed SQL predicate often suffices:
246
-
247
- ```ts
248
- // email is string | null; null means no email filter.
249
- const query = sql<UserSummary>`SELECT id, email FROM public."User"
250
- WHERE active = ${true} AND (${email}::text IS NULL OR email = ${email})`;
251
- ```
252
-
253
- If you need a variable list of conditions, `sql.join` combines fragments using `'AND'`, `'OR'`, or
254
- `','`. **SafeQL cannot check runtime-built lists.** Keep an explicit result type and test each
255
- variant; if you use SafeQL, suppress only this query with an explanation. Parameter binding still
256
- works, but it does not prove that every variant is valid SQL or returns the declared columns.
250
+ Use `sql.join` when the number of fragments varies, such as one search condition per word or a set
251
+ of optional filters. It combines fragments with `'AND'`, `'OR'`, or `','` and preserves their bound
252
+ values:
257
253
 
258
254
  ```ts
255
+ const terms = ['reader', 'example'];
259
256
  const filters = [sql.fragment`active = ${true}`];
260
- if (email !== undefined) filters.push(sql.fragment`email = ${email}`);
257
+ for (const term of terms) {
258
+ filters.push(sql.fragment`email ILIKE ${'%' + term + '%'}`);
259
+ }
261
260
 
262
- /* eslint-disable tenon/check-composition -- Runtime filters are covered by database tests. */
263
261
  const query = sql<UserSummary>`SELECT id, email FROM public."User"
264
262
  WHERE ${sql.join(filters, 'AND')} ORDER BY id`;
265
- /* eslint-enable tenon/check-composition */
266
263
  ```
267
264
 
268
- The initial condition keeps this list nonempty. Empty lists throw so that missing filters cannot
269
- silently remove a condition. `join` adds separators, not parentheses; wrap groups explicitly when
270
- mixing `AND` and `OR`. Full queries cannot be used as fragments. See the
271
- [composition reference](reference.md#sql-fragments-and-joins) for static checking rules and other
272
- details.
265
+ This matches active users whose email contains every term. With no terms, the active condition
266
+ still applies. The list must be nonempty; `join` inserts separators, so use parentheses as usual
267
+ when mixing `AND` and `OR`. See the [composition reference](reference.md#sql-fragments-and-joins).
273
268
 
274
269
  For arrays, identifiers, type mappings, CLI options, and error behavior, use the
275
270
  [reference](reference.md). You do not need fragments or a query builder to use PostgreSQL joins,
package/docs/mutations.md CHANGED
@@ -19,7 +19,7 @@ The first row uses the score default. The second uses the bio default. Explicit
19
19
  row stays null. Columns are aligned across rows. Omit generated columns and let PostgreSQL enforce
20
20
  required fields and constraints.
21
21
 
22
- For runtime-built rows, supply an inline column list for SafeQL:
22
+ Use a column list to select and order fields in runtime-built rows:
23
23
 
24
24
  ```ts
25
25
  import type { Rows } from './generated-schema.js';
@@ -97,8 +97,7 @@ if (patch.name !== undefined || patch.bio !== undefined) {
97
97
 
98
98
  ## Preserve an omitted field during an upsert
99
99
 
100
- Use complete query variants when an option changes conflict behavior. Both templates can be
101
- checked independently; the checker need not choose a JavaScript branch:
100
+ Use separate queries when an option changes conflict behavior:
102
101
 
103
102
  ```ts
104
103
  if (email !== undefined) {
@@ -151,7 +150,3 @@ calls for empty input. The query-wide limit is 65,535 bound parameters, includin
151
150
  the mutation fragment. An oversized query reports its actual count, for example
152
151
  `at most 65535 bound parameters; received 65536`. Defaults consume no parameters. Account for the
153
152
  selected columns and surrounding query when choosing a batch size; Tenon never splits queries automatically.
154
-
155
- Reusable authorization fragments remain valid at runtime. Keep shared policy in application
156
- functions and test it rather than duplicating it for static coverage. Function-generated SQL,
157
- conditional fragments, and dynamic filter lists remain outside static checking.
package/docs/reference.md CHANGED
@@ -86,22 +86,22 @@ const query = sql<{ id: number }>`SELECT id FROM public."User"
86
86
  NUL, and fit PostgreSQL's 63-byte identifier limit. Choose names from application-approved
87
87
  alternatives; quoting does not authorize access. Ordinary strings such as `'DESC'` are bound values,
88
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.
89
+ containing `ASC` or `DESC`.
90
90
 
91
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
92
+ array, including `[]` when empty. Never concatenate request input into its text.
93
+ Full `sql` or `unsafeSql` queries cannot be inserted into other queries; use
94
94
  fragments for partial SQL.
95
95
 
96
96
  ## SQL fragments and joins
97
97
 
98
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.
99
+ execute alone. Insert it into a complete `sql<Row>` query. Use fragments to share predicates or
100
+ other SQL expressions between queries. Blank fragments throw.
102
101
 
103
- `sql.join(fragments, separator)` combines a nonempty array of fragments into a new fragment.
104
- Separators are exactly:
102
+ `sql.join(fragments, separator)` combines fragments into a new fragment. Use it when the number of
103
+ parts varies: optional filters, a condition for each search term, or a list of SQL expressions.
104
+ Write fixed structure directly in SQL. Supported separators are:
105
105
 
106
106
  | Argument | Inserted SQL | Typical use |
107
107
  | -------- | ------------ | -------------------------- |
@@ -109,35 +109,22 @@ Separators are exactly:
109
109
  | `'OR'` | `OR` | Any condition may hold |
110
110
  | `','` | `, ` | Projections or assignments |
111
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
112
  ```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}`;
113
+ const conditions = [sql.fragment`active = ${true}`];
114
+ if (email !== undefined) conditions.push(sql.fragment`email = ${email}`);
115
+ if (afterId !== undefined) conditions.push(sql.fragment`id > ${afterId}`);
116
+
117
+ const query = sql<{ id: number }>`SELECT id FROM public."User"
118
+ WHERE ${sql.join(conditions, 'AND')} ORDER BY id`;
129
119
  ```
130
120
 
121
+ The list must be nonempty; the example keeps the active condition even when both optional filters
122
+ are absent. Joins insert separators without adding parentheses or changing SQL precedence. Group
123
+ mixed AND/OR expressions with parentheses, as in ordinary SQL.
124
+
131
125
  Each fragment use receives correctly ordered parameters, including repeated and nested uses. SQL
132
126
  strings and comments containing literal `$1` text are left intact.
133
127
 
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
128
  ## Object-based mutation fragments
142
129
 
143
130
  `sql.insertValues(input, columns?)` and `sql.assignments(input, columns?)` return fragments. Keep the
@@ -188,8 +175,7 @@ intended update consists only of those expressions.
188
175
  An optional nonempty column list selects and orders fields. It does not validate that the input
189
176
  object has no additional keys. Unlisted fields are ignored; missing insert cells use `DEFAULT`,
190
177
  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:
178
+ names throw. TypeScript checks selected names against the input's known keys:
193
179
 
194
180
  ```ts
195
181
  const rows: { name: string; bio?: string | null }[] = buildRows();
@@ -211,24 +197,8 @@ Tenon rejects larger queries and does not split them automatically. Choose batch
211
197
  transaction boundaries in the caller. Helper validation errors identify invalid rows/columns
212
198
  without printing cell values.
213
199
 
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.
200
+ See the [mutation cookbook](mutations.md) for common patterns and
201
+ [checking mutation fragments](#checking-mutation-fragments) for optional SQL checking.
232
202
 
233
203
  ## Transactions, guarded writes, and errors
234
204
 
@@ -285,7 +255,8 @@ the existing file without rewriting it:
285
255
  npx tenon generate --out src/generated-schema.d.ts --check
286
256
  ```
287
257
 
288
- `--check` compares the generated representation with the current database. Pending migration files
258
+ `--check` reports a missing file or shows a short text diff when the saved output differs from the
259
+ current database. It never rewrites the file. Pending migration files
289
260
  are a separate concern. The representation includes table columns, constraints, indexes, and
290
261
  referenced domains, not every PostgreSQL object.
291
262
 
@@ -511,7 +482,7 @@ the intended relation is explicit in both checking and execution.
511
482
 
512
483
  Within the configured targets, the adapter recognizes Tenon's `sql` export through imports, local
513
484
  aliases, namespace imports, and object properties that retain its TypeScript type. It checks
514
- constant fragments and `sql.join` with nonempty literal arrays, including nested fragments.
485
+ constant fragments and `sql.join` with inline nonempty array literals, including nested fragments.
515
486
  Separators and `identifier()` names must resolve to literal strings or const string initializers.
516
487
  Values use the same parameter placeholders in static checking and execution, inside or outside
517
488
  fragments. PostgreSQL infers their types from SQL context; Tenon does not add casts based on
@@ -521,7 +492,8 @@ The checker follows const fragment initializers, including imported constants wi
521
492
  source. It does not inspect function bodies, substitute helper arguments/defaults, or choose
522
493
  JavaScript conditional branches. Function-generated fragments and conditional SQL structure are
523
494
  unsupported even when a particular helper appears simple or a condition is a literal. Mutable
524
- references, runtime filter lists, and dynamic identifiers are also unsupported. Ordinary value
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
525
497
  expressions remain bound parameters; calls that produce data for an official mutation helper can
526
498
  use its explicit column-list contract.
527
499
 
@@ -580,6 +552,25 @@ tests; a checker cannot infer your parser policy. Column keys are `Table.column`
580
552
  names across schemas need care. Type overrides merge with the adapter's defaults. `unsafeSql` is
581
553
  outside static checking, and arbitrary JSON still needs application validation.
582
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.
573
+
583
574
  ## Command reference
584
575
 
585
576
  All database commands use `DATABASE_URL`; environment files are not loaded automatically. For
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kequtech/tenon-orm",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",