@kequtech/tenon-orm 0.1.0 → 0.1.2

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/docs/reference.md CHANGED
@@ -131,12 +131,105 @@ const query = sql<{ id: number }>`SELECT id FROM public."User" WHERE ${condition
131
131
  Each fragment use receives correctly ordered parameters, including repeated and nested uses. SQL
132
132
  strings and comments containing literal `$1` text are left intact.
133
133
 
134
- Static checking supports constant fragments, literal join arrays, and literal or const-string
135
- separators. Runtime-built lists and selected fragments execute safely with respect to parameter
136
- binding, but the checker cannot verify their SQL or result types. Prefer fixed SQL when it fits.
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.
137
138
  Otherwise keep an explicit result annotation, use a narrow explained lint suppression, and test
138
139
  every variant.
139
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 runtime schema metadata. Use [generated input types](#insert-and-update-inputs)
203
+ or application-owned contracts to check JavaScript values; PostgreSQL enforces identity,
204
+ generation, required-column, and constraint rules. Retain application validation.
205
+ 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
+
140
233
  ## Transactions, guarded writes, and errors
141
234
 
142
235
  ```ts
@@ -184,42 +277,84 @@ transaction has committed.
184
277
 
185
278
  `generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript source for the
186
279
  connected schema. It does not need migration files or history. Table type generation is optional.
187
- SafeQL reads the database directly and does not require these generated files; they are useful when
188
- reusing table row shapes. The CLI writes that source atomically. This command checks the existing
189
- file without rewriting it:
280
+ SafeQL reads the database directly and does not require these generated files; they provide
281
+ reusable row and mutation input types. The CLI writes that source atomically. This command checks
282
+ the existing file without rewriting it:
190
283
 
191
284
  ```sh
192
- npx tenon generate --out src/generated-schema.ts --check
285
+ npx tenon generate --out src/generated-schema.d.ts --check
193
286
  ```
194
287
 
195
288
  `--check` compares the generated representation with the current database. Pending migration files
196
289
  are a separate concern. The representation includes table columns, constraints, indexes, and
197
290
  referenced domains, not every PostgreSQL object.
198
291
 
199
- Generation reads a consistent snapshot of ordinary tables and partitioned roots. Use the generated
200
- named type in queries, for example `sql<User>`. For other table names, define an alias such as
201
- `type AuditRow = Rows['audit-log']` and use `sql<AuditRow>`; SafeQL resolves named aliases but not
202
- indexed access directly in `sql<Rows['audit-log']>`. Views, foreign tables, and attached partitions
203
- use query-specific types. A missing namespace fails; an existing empty schema is valid.
204
-
205
- | PostgreSQL type | Default pg value |
206
- | ---------------------------------------------- | ----------------------------------------------- |
207
- | smallint, integer, oid, real, double precision | `number` |
208
- | bigint, numeric | `string` |
209
- | boolean | `boolean` |
210
- | text, varchar, char, uuid, time, timetz | `string` |
211
- | date, timestamp, timestamptz | `Date \| number` (numbers represent infinities) |
212
- | enum | String literal union; `never` for an empty enum |
213
- | json, jsonb | `unknown` |
214
- | bytea | `Buffer` |
215
-
216
- Nullable columns add null. Domains use verified underlying mappings and the column's own
217
- nullability. Unverified types warn and become `unknown`. Verified arrays use
292
+ Generation reads a consistent snapshot of ordinary tables and partitioned roots. `Rows['User']`
293
+ describes one returned row; the generated `User` alias means the same thing. Use `sql<User>` for a
294
+ full row or `Pick<User, 'id' | 'email'>` for a projection. Table keys preserve exact SQL names;
295
+ `type AuditRow = Rows['audit-log']` works for names that cannot be exported as identifiers.
296
+ Views, foreign tables, and attached partitions use query-specific types. A missing namespace
297
+ fails; an existing empty schema is valid.
298
+
299
+ | PostgreSQL type | Returned value | Generated write input |
300
+ | ---------------------------------------------- | --------------------------------------------- | ------------------------------------- |
301
+ | smallint, integer, oid, real, double precision | `number` | `number` |
302
+ | bigint | `string` | `string \| bigint` |
303
+ | numeric | `string` | `string \| number` |
304
+ | boolean | `boolean` | `boolean` |
305
+ | text, name, varchar, char, uuid, time, timetz | `string` | `string` |
306
+ | date, timestamp, timestamptz | `Date \| number` (numbers include infinities) | `Date \| string` |
307
+ | enum | String literal union | Same union; `never` for an empty enum |
308
+ | json, jsonb | `unknown` | Serialized JSON text (`string`) |
309
+ | bytea | `Buffer` | `Buffer` |
310
+
311
+ For returned rows, nullable columns add null. Domains use verified underlying mappings and the
312
+ column's own nullability. Unverified types warn and become `unknown`. Verified arrays use
218
313
  `PgArray<T> = (T | null | PgArray<T>)[]` for null elements and multiple dimensions. Numeric arrays
219
314
  contain numbers and can lose precision; cast to text[] for exact values. Enum arrays and arrays of
220
315
  domains are unverified. Finite dates and timestamps without time zone use Node's local timezone.
221
316
  Custom pg parsers need matching application types.
222
317
 
318
+ ### Insert and update inputs
319
+
320
+ The same file provides `Rows.Insert['table']` and `Rows.Update['table']` for bound write inputs.
321
+ These are ordinary TypeScript types. `--out` accepts `.ts` or `.d.ts`; the examples use `.d.ts`
322
+ because the file contains only declarations.
323
+
324
+ ```ts
325
+ import type { Rows } from './generated-schema.js';
326
+
327
+ type NewProfile = Rows.Insert['profiles'];
328
+ type ProfilePatch = Pick<Rows.Update['profiles'], 'name' | 'bio'>;
329
+
330
+ const profile: NewProfile = { name: 'Ada' };
331
+ const patch: ProfilePatch = { name: undefined, bio: null };
332
+ sql.insertValues(profile, ['name', 'bio', 'score']);
333
+ sql.assignments(patch, ['name', 'bio']);
334
+ ```
335
+
336
+ Insert fields are required when nonnullable and without a default. Other writable insert fields
337
+ are optional; every writable update field is optional. `undefined` requests a default on insert
338
+ and omits an assignment on update. Explicit `null` is accepted only when the column and its domain
339
+ chain permit it. Domain defaults count; column defaults take precedence.
340
+
341
+ Computed columns and `GENERATED ALWAYS` identity columns cannot receive values. Serial and
342
+ `GENERATED BY DEFAULT` identity columns are optional on insert and writable on update. Other
343
+ primary keys remain writable. Use `Pick` or `Omit` for application-specific restrictions and keep
344
+ expressions, `DEFAULT`, and special identity overrides in SQL.
345
+
346
+ Write representations in the table above also apply to verified array elements. Serialize JSON
347
+ with `JSON.stringify`, including JSON arrays; a JSON string containing `null` differs from SQL null.
348
+ Use timestamp strings for infinities. Unverified non-null write values use `never` and warn;
349
+ nullable fields still accept null. Supply an application-owned replacement when needed, for example
350
+ `Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for a reviewed interval-text input.
351
+
352
+ Apply these types through annotations, `satisfies`, or helper generic arguments such as
353
+ `sql.insertValues<NewProfile>(input)`. They check shapes and representations, not authorization,
354
+ constraints, JSON contents, or nonempty inputs. Optional fields accept explicit `undefined`,
355
+ including with `exactOptionalPropertyTypes`. Selected columns can discard required fields; see
356
+ the [cookbook example](mutations.md#input-types-sql-checking-and-runtime-validation).
357
+
223
358
  ## SQL checking with SafeQL
224
359
 
225
360
  SafeQL checks SQL against a real PostgreSQL schema and infers result types. ESLint runs that check
@@ -230,8 +365,8 @@ Configure [SafeQL normally](https://safeql.dev/api/). Tenon's optional `safeql`
230
365
  two objects:
231
366
 
232
367
  - `safeqlPlugin` is a SafeQL plugin descriptor. Add it to each Tenon connection's `plugins` list. It
233
- translates constant fragments and identifiers, preserves runtime parameter behavior, and supplies
234
- pg result mappings. SafeQL owns database analysis and annotation fixes.
368
+ translates constant fragments, official mutation helpers, and identifiers, preserves parameter
369
+ behavior, and supplies pg result mappings. SafeQL owns database analysis and annotation fixes.
235
370
  - `eslintPlugin` supplies `tenon/check-composition`. Register it in ESLint and enable the rule to
236
371
  report queries that the adapter cannot resolve, including runtime-built fragments and escaped
237
372
  outer templates.
@@ -248,11 +383,8 @@ existing TypeScript installation:
248
383
  npm install --save-dev eslint typescript-eslint @ts-safeql/eslint-plugin
249
384
  ```
250
385
 
251
- SafeQL and ESLint are the integration's hosts, and the adapter imports TypeScript to inspect query
252
- expressions. They are optional peers because Tenon's database runtime does not need them.
253
- `typescript-eslint` is an application-owned parser dependency, not a Tenon peer. Your ESLint
254
- configuration imports SafeQL and the parser explicitly. Use a TypeScript version supported by that
255
- lint toolchain. The `pg` driver remains Tenon's required runtime peer.
386
+ These are development dependencies. Reuse your existing type-aware parser configuration and a
387
+ TypeScript version supported by the lint toolchain. Tenon's database runtime does not load them.
256
388
 
257
389
  For a project with `src/**/*.ts` and a `tsconfig.json`:
258
390
 
@@ -289,9 +421,6 @@ configuration. `projectService` uses the nearest TypeScript project for each fil
289
421
  `DATABASE_URL` through your application's environment setup and point it at a development/test
290
422
  database with migrations applied. This example does not load `.env` files.
291
423
 
292
- The `.mjs` example avoids a current declaration mismatch between SafeQL's configuration helper and
293
- ESLint's `defineConfig`. This affects configuration typechecking, not query result types.
294
-
295
424
  `targets` selects the SQL tag spelling. If you alias `sql`, add the alias to the targets. For a
296
425
  single database, `targets: [{ tag: { regex: '.*' } }]` selects all tags; Tenon's adapter filters
297
426
  that down to actual Tenon queries, including aliases and object properties. Neither the adapter nor
@@ -311,6 +440,9 @@ They run all enabled ESLint rules, and `lint:fix` can change non-SQL code too:
311
440
 
312
441
  ### Generate and check annotations
313
442
 
443
+ For an existing persistence layer, start with the [incremental audit workflow](adoption-guide.md#adopt-sql-checking-incrementally).
444
+ It covers a separate SQL lint command, fix previews, and application refinements.
445
+
314
446
  Write a query without a result annotation:
315
447
 
316
448
  ```ts
@@ -372,25 +504,48 @@ file group, each extending its own `safeql.configs.connections({ ... })`. Use no
372
504
  `files` patterns when database settings differ. Normal SafeQL configurations for other libraries can
373
505
  coexist; add Tenon's adapter only to connections checking Tenon queries.
374
506
 
375
- If a database has same-named tables in different schemas, qualify them in SQL, for example
376
- `SELECT id FROM billing.accounts`. SafeQL can infer the wrong type for an unqualified table even
377
- when the connection's `search_path` selects the correct table at execution. Explicit schema names
378
- avoid that ambiguity.
507
+ Qualify same-named tables with their schemas, for example `SELECT id FROM billing.accounts`, so
508
+ the intended relation is explicit in both checking and execution.
379
509
 
380
510
  ### Supported SQL and limitations
381
511
 
382
512
  Within the configured targets, the adapter recognizes Tenon's `sql` export through imports, local
383
513
  aliases, namespace imports, and object properties that retain its TypeScript type. It checks
384
- constant fragments and `sql.join` with literal arrays, including nested fragments. Separators must
385
- be literal strings or const strings. Literal `identifier()` names and const string names are
386
- supported. Values use the same parameter placeholders in static checking and execution, inside or
387
- outside fragments. PostgreSQL infers their types from the SQL context; Tenon does not add casts
388
- based on JavaScript types. Mutable references, runtime lists, conditional fragments,
389
- function-generated fragments, and dynamic identifiers produce `tenon/check-composition` diagnostics.
390
- Suppress only that rule on the particular query when runtime variants have database tests. SQL and
391
- annotation errors still come from `@ts-safeql/check-sql`. An imported fragment must have an
392
- inspectable source initializer; declarations alone cannot describe its SQL. It does not execute
393
- application code to discover SQL.
514
+ constant fragments and `sql.join` with nonempty literal arrays, including nested fragments.
515
+ Separators and `identifier()` names must resolve to literal strings or const string initializers.
516
+ Values use the same parameter placeholders in static checking and execution, inside or outside
517
+ fragments. PostgreSQL infers their types from SQL context; Tenon does not add casts based on
518
+ JavaScript types.
519
+
520
+ The checker follows const fragment initializers, including imported constants with inspectable
521
+ source. It does not inspect function bodies, substitute helper arguments/defaults, or choose
522
+ JavaScript conditional branches. Function-generated fragments and conditional SQL structure are
523
+ 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
525
+ expressions remain bound parameters; calls that produce data for an official mutation helper can
526
+ use its explicit column-list contract.
527
+
528
+ `tenon/check-composition` points to the first unsupported interpolation in each query and explains
529
+ why it cannot be checked. For example, opaque mutation inputs need an inline column list, while
530
+ fragment-returning functions remain outside static analysis. Errors reached through shared constants
531
+ are reported at the consuming query. ESLint suppressions must cover the reported expression's line;
532
+ a scoped disable/enable can cover one multiline query.
533
+
534
+ Reusable authorization and other application policy can still live in fragment-returning functions
535
+ at runtime. Keep that policy centralized and cover its behavior with database tests. Suppress
536
+ `tenon/check-composition` only on the particular query, with a reason, when its variants are tested.
537
+ Do not duplicate policy merely to obtain static coverage. SQL and annotation errors in checkable
538
+ queries continue to come from `@ts-safeql/check-sql`.
539
+
540
+ Ordinary functions containing complete `sql` templates remain checkable. Each template is analyzed
541
+ where it is written, including constant fragments defined in that function:
542
+
543
+ ```ts
544
+ function findUsers(email: string) {
545
+ const predicate = sql.fragment`email = ${email}`;
546
+ return sql`SELECT id FROM users WHERE ${predicate}`;
547
+ }
548
+ ```
394
549
 
395
550
  Use literal line breaks in `sql` templates and bind string values with `${value}`. The composition
396
551
  rule rejects JavaScript escapes such as `\n` or `\'` in the outer template: SafeQL reads raw
@@ -436,7 +591,7 @@ tenon new <name> [--directory migrations]
436
591
  tenon migrate [--directory migrations] [--namespace public]
437
592
  tenon status [--directory migrations] [--namespace public]
438
593
  tenon rollback [--directory migrations] [--namespace public] [--steps 1]
439
- tenon generate [--namespace public] [--out src/generated-schema.ts] [--check]
594
+ tenon generate [--namespace public] [--out path] [--check]
440
595
  tenon baseline capture|adopt [--directory migrations] [--namespace public]
441
596
  ```
442
597
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kequtech/tenon-orm",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
@@ -1,2 +0,0 @@
1
- import type { TableRow } from './catalog.js';
2
- export declare function renderRowTypes(tables: readonly TableRow[], namespace: string, onWarning: (message: string) => void): string;
@@ -1,115 +0,0 @@
1
- // Every generated table remains accessible through Rows["exact SQL name"].
2
- const reservedTypeNames = new Set(`
3
- as await break case catch class const continue debugger default delete do else enum
4
- export extends false finally for function if implements import in instanceof
5
- interface let new null package private protected public return static super
6
- switch this throw true try typeof var void while with yield any bigint boolean
7
- intrinsic never number object string symbol undefined unknown
8
- Rows globalThis PgArray
9
- `
10
- .trim()
11
- .split(/\s+/));
12
- function canExportRowName(name) {
13
- return (/^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u.test(name) && !reservedTypeNames.has(name));
14
- }
15
- // Exact built-in OIDs with verified default pg text parsers. Array parsers do
16
- // not necessarily use their scalar type's representation (notably numeric[]).
17
- const arrayTypes = {
18
- '1000': 'boolean',
19
- '1001': 'import("node:buffer").Buffer',
20
- '1005': 'number',
21
- '1007': 'number',
22
- '1021': 'number',
23
- '1022': 'number',
24
- '1028': 'number',
25
- '1016': 'string',
26
- '1231': 'number',
27
- '1009': 'string',
28
- '1014': 'string',
29
- '1015': 'string',
30
- '1183': 'string',
31
- '1270': 'string',
32
- '2951': 'string',
33
- '1182': 'globalThis.Date | number',
34
- '1115': 'globalThis.Date | number',
35
- '1185': 'globalThis.Date | number',
36
- '199': 'unknown',
37
- '3807': 'unknown',
38
- };
39
- const scalarTypes = {
40
- int2: 'number',
41
- int4: 'number',
42
- oid: 'number',
43
- float4: 'number',
44
- float8: 'number',
45
- bool: 'boolean',
46
- text: 'string',
47
- varchar: 'string',
48
- bpchar: 'string',
49
- time: 'string',
50
- timetz: 'string',
51
- uuid: 'string',
52
- // pg returns +/-Infinity as numbers. TypeScript has no infinity literal type.
53
- date: 'globalThis.Date | number',
54
- timestamp: 'globalThis.Date | number',
55
- timestamptz: 'globalThis.Date | number',
56
- int8: 'string',
57
- numeric: 'string',
58
- json: 'unknown',
59
- jsonb: 'unknown',
60
- bytea: 'import("node:buffer").Buffer',
61
- };
62
- function isSupportedArray(value) {
63
- return Object.hasOwn(arrayTypes, value.oid);
64
- }
65
- function columnType(value, column, onWarning) {
66
- let scalar;
67
- if (isSupportedArray(value)) {
68
- scalar = `PgArray<${arrayTypes[value.oid]}>`;
69
- }
70
- else if (value.typeKind === 'e') {
71
- scalar = value.choices?.map((choice) => JSON.stringify(choice)).join(' | ') || 'never';
72
- }
73
- else {
74
- const type = value.typeSchema === 'pg_catalog' && Object.hasOwn(scalarTypes, value.type)
75
- ? scalarTypes[value.type]
76
- : undefined;
77
- if (type) {
78
- scalar = type;
79
- }
80
- else {
81
- const underlying = value.declaredOid === value.oid
82
- ? ''
83
- : ` with underlying type ${JSON.stringify(value.typeSchema)}.${JSON.stringify(value.type)} (OID ${value.oid})`;
84
- onWarning(`Column ${column} uses PostgreSQL type ${JSON.stringify(value.declaredSchema)}.${JSON.stringify(value.declaredType)} (OID ${value.declaredOid})${underlying} without a verified pg mapping; generated as unknown.`);
85
- scalar = 'unknown';
86
- }
87
- }
88
- // unknown already includes null, including for nullable JSON and unmapped types.
89
- return `${scalar}${value.nullable && scalar !== 'unknown' ? ' | null' : ''}`;
90
- }
91
- export function renderRowTypes(tables, namespace, onWarning) {
92
- const rows = tables.map((table) => {
93
- const tableName = JSON.stringify(table.name);
94
- const columns = table.columns.map((column) => {
95
- const columnName = JSON.stringify(column.name);
96
- const path = `${JSON.stringify(namespace)}.${tableName}.${columnName}`;
97
- return ` ${columnName}: ${columnType(column, path, onWarning)};`;
98
- });
99
- return ` ${tableName}: {
100
- ${columns.join('\n')}
101
- };`;
102
- });
103
- const aliases = tables
104
- .filter((table) => canExportRowName(table.name))
105
- .map((table) => `export type ${table.name} = Rows[${JSON.stringify(table.name)}];`);
106
- const arrays = tables.some((table) => table.columns.some(isSupportedArray))
107
- ? 'export type PgArray<T> = (T | null | PgArray<T>)[];\n\n'
108
- : '';
109
- return `${arrays}// Full database rows. Compose query types with Pick, indexed access, and intersections.
110
- export type Rows = {
111
- ${rows.join('\n')}
112
- };
113
- ${aliases.join('\n')}
114
- `;
115
- }