@kequtech/tenon-orm 0.1.1 → 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/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();
@@ -199,10 +185,10 @@ const patch: { name?: string; bio?: string | null } = readPatch();
199
185
  const update = sql`UPDATE profiles SET ${sql.assignments(patch, ['name', 'bio'])} WHERE id = ${id}`;
200
186
  ```
201
187
 
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
188
+ The helpers have no runtime schema metadata. Use [generated input types](#insert-and-update-inputs)
189
+ or application-owned contracts to check JavaScript values; PostgreSQL enforces identity,
190
+ generation, required-column, and constraint rules. Retain application validation.
191
+ To reset an update field to its default, write `column = DEFAULT` in the
206
192
  surrounding SQL. Put arithmetic and `excluded.column` expressions there too; these helpers accept
207
193
  values only, with no expression/default sentinel.
208
194
 
@@ -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
 
@@ -277,42 +247,85 @@ transaction has committed.
277
247
 
278
248
  `generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript source for the
279
249
  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:
250
+ SafeQL reads the database directly and does not require these generated files; they provide
251
+ reusable row and mutation input types. The CLI writes that source atomically. This command checks
252
+ the existing file without rewriting it:
283
253
 
284
254
  ```sh
285
- npx tenon generate --out src/generated-schema.ts --check
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
 
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
263
+ Generation reads a consistent snapshot of ordinary tables and partitioned roots. `Rows['User']`
264
+ describes one returned row; the generated `User` alias means the same thing. Use `sql<User>` for a
265
+ full row or `Pick<User, 'id' | 'email'>` for a projection. Table keys preserve exact SQL names;
266
+ `type AuditRow = Rows['audit-log']` works for names that cannot be exported as identifiers.
267
+ Views, foreign tables, and attached partitions use query-specific types. A missing namespace
268
+ fails; an existing empty schema is valid.
269
+
270
+ | PostgreSQL type | Returned value | Generated write input |
271
+ | ---------------------------------------------- | --------------------------------------------- | ------------------------------------- |
272
+ | smallint, integer, oid, real, double precision | `number` | `number` |
273
+ | bigint | `string` | `string \| bigint` |
274
+ | numeric | `string` | `string \| number` |
275
+ | boolean | `boolean` | `boolean` |
276
+ | text, name, varchar, char, uuid, time, timetz | `string` | `string` |
277
+ | date, timestamp, timestamptz | `Date \| number` (numbers include infinities) | `Date \| string` |
278
+ | enum | String literal union | Same union; `never` for an empty enum |
279
+ | json, jsonb | `unknown` | Serialized JSON text (`string`) |
280
+ | bytea | `Buffer` | `Buffer` |
281
+
282
+ For returned rows, nullable columns add null. Domains use verified underlying mappings and the
283
+ column's own nullability. Unverified types warn and become `unknown`. Verified arrays use
311
284
  `PgArray<T> = (T | null | PgArray<T>)[]` for null elements and multiple dimensions. Numeric arrays
312
285
  contain numbers and can lose precision; cast to text[] for exact values. Enum arrays and arrays of
313
286
  domains are unverified. Finite dates and timestamps without time zone use Node's local timezone.
314
287
  Custom pg parsers need matching application types.
315
288
 
289
+ ### Insert and update inputs
290
+
291
+ The same file provides `Rows.Insert['table']` and `Rows.Update['table']` for bound write inputs.
292
+ These are ordinary TypeScript types. `--out` accepts `.ts` or `.d.ts`; the examples use `.d.ts`
293
+ because the file contains only declarations.
294
+
295
+ ```ts
296
+ import type { Rows } from './generated-schema.js';
297
+
298
+ type NewProfile = Rows.Insert['profiles'];
299
+ type ProfilePatch = Pick<Rows.Update['profiles'], 'name' | 'bio'>;
300
+
301
+ const profile: NewProfile = { name: 'Ada' };
302
+ const patch: ProfilePatch = { name: undefined, bio: null };
303
+ sql.insertValues(profile, ['name', 'bio', 'score']);
304
+ sql.assignments(patch, ['name', 'bio']);
305
+ ```
306
+
307
+ Insert fields are required when nonnullable and without a default. Other writable insert fields
308
+ are optional; every writable update field is optional. `undefined` requests a default on insert
309
+ and omits an assignment on update. Explicit `null` is accepted only when the column and its domain
310
+ chain permit it. Domain defaults count; column defaults take precedence.
311
+
312
+ Computed columns and `GENERATED ALWAYS` identity columns cannot receive values. Serial and
313
+ `GENERATED BY DEFAULT` identity columns are optional on insert and writable on update. Other
314
+ primary keys remain writable. Use `Pick` or `Omit` for application-specific restrictions and keep
315
+ expressions, `DEFAULT`, and special identity overrides in SQL.
316
+
317
+ Write representations in the table above also apply to verified array elements. Serialize JSON
318
+ with `JSON.stringify`, including JSON arrays; a JSON string containing `null` differs from SQL null.
319
+ Use timestamp strings for infinities. Unverified non-null write values use `never` and warn;
320
+ nullable fields still accept null. Supply an application-owned replacement when needed, for example
321
+ `Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for a reviewed interval-text input.
322
+
323
+ Apply these types through annotations, `satisfies`, or helper generic arguments such as
324
+ `sql.insertValues<NewProfile>(input)`. They check shapes and representations, not authorization,
325
+ constraints, JSON contents, or nonempty inputs. Optional fields accept explicit `undefined`,
326
+ including with `exactOptionalPropertyTypes`. Selected columns can discard required fields; see
327
+ the [cookbook example](mutations.md#input-types-sql-checking-and-runtime-validation).
328
+
316
329
  ## SQL checking with SafeQL
317
330
 
318
331
  SafeQL checks SQL against a real PostgreSQL schema and infers result types. ESLint runs that check
@@ -341,11 +354,8 @@ existing TypeScript installation:
341
354
  npm install --save-dev eslint typescript-eslint @ts-safeql/eslint-plugin
342
355
  ```
343
356
 
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.
357
+ These are development dependencies. Reuse your existing type-aware parser configuration and a
358
+ TypeScript version supported by the lint toolchain. Tenon's database runtime does not load them.
349
359
 
350
360
  For a project with `src/**/*.ts` and a `tsconfig.json`:
351
361
 
@@ -382,9 +392,6 @@ configuration. `projectService` uses the nearest TypeScript project for each fil
382
392
  `DATABASE_URL` through your application's environment setup and point it at a development/test
383
393
  database with migrations applied. This example does not load `.env` files.
384
394
 
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
395
  `targets` selects the SQL tag spelling. If you alias `sql`, add the alias to the targets. For a
389
396
  single database, `targets: [{ tag: { regex: '.*' } }]` selects all tags; Tenon's adapter filters
390
397
  that down to actual Tenon queries, including aliases and object properties. Neither the adapter nor
@@ -404,6 +411,9 @@ They run all enabled ESLint rules, and `lint:fix` can change non-SQL code too:
404
411
 
405
412
  ### Generate and check annotations
406
413
 
414
+ For an existing persistence layer, start with the [incremental audit workflow](adoption-guide.md#adopt-sql-checking-incrementally).
415
+ It covers a separate SQL lint command, fix previews, and application refinements.
416
+
407
417
  Write a query without a result annotation:
408
418
 
409
419
  ```ts
@@ -465,16 +475,14 @@ file group, each extending its own `safeql.configs.connections({ ... })`. Use no
465
475
  `files` patterns when database settings differ. Normal SafeQL configurations for other libraries can
466
476
  coexist; add Tenon's adapter only to connections checking Tenon queries.
467
477
 
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.
478
+ Qualify same-named tables with their schemas, for example `SELECT id FROM billing.accounts`, so
479
+ the intended relation is explicit in both checking and execution.
472
480
 
473
481
  ### Supported SQL and limitations
474
482
 
475
483
  Within the configured targets, the adapter recognizes Tenon's `sql` export through imports, local
476
484
  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.
485
+ constant fragments and `sql.join` with inline nonempty array literals, including nested fragments.
478
486
  Separators and `identifier()` names must resolve to literal strings or const string initializers.
479
487
  Values use the same parameter placeholders in static checking and execution, inside or outside
480
488
  fragments. PostgreSQL infers their types from SQL context; Tenon does not add casts based on
@@ -484,10 +492,17 @@ The checker follows const fragment initializers, including imported constants wi
484
492
  source. It does not inspect function bodies, substitute helper arguments/defaults, or choose
485
493
  JavaScript conditional branches. Function-generated fragments and conditional SQL structure are
486
494
  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
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
488
497
  expressions remain bound parameters; calls that produce data for an official mutation helper can
489
498
  use its explicit column-list contract.
490
499
 
500
+ `tenon/check-composition` points to the first unsupported interpolation in each query and explains
501
+ why it cannot be checked. For example, opaque mutation inputs need an inline column list, while
502
+ fragment-returning functions remain outside static analysis. Errors reached through shared constants
503
+ are reported at the consuming query. ESLint suppressions must cover the reported expression's line;
504
+ a scoped disable/enable can cover one multiline query.
505
+
491
506
  Reusable authorization and other application policy can still live in fragment-returning functions
492
507
  at runtime. Keep that policy centralized and cover its behavior with database tests. Suppress
493
508
  `tenon/check-composition` only on the particular query, with a reason, when its variants are tested.
@@ -537,6 +552,25 @@ tests; a checker cannot infer your parser policy. Column keys are `Table.column`
537
552
  names across schemas need care. Type overrides merge with the adapter's defaults. `unsafeSql` is
538
553
  outside static checking, and arbitrary JSON still needs application validation.
539
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
+
540
574
  ## Command reference
541
575
 
542
576
  All database commands use `DATABASE_URL`; environment files are not loaded automatically. For
@@ -548,7 +582,7 @@ tenon new <name> [--directory migrations]
548
582
  tenon migrate [--directory migrations] [--namespace public]
549
583
  tenon status [--directory migrations] [--namespace public]
550
584
  tenon rollback [--directory migrations] [--namespace public] [--steps 1]
551
- tenon generate [--namespace public] [--out src/generated-schema.ts] [--check]
585
+ tenon generate [--namespace public] [--out path] [--check]
552
586
  tenon baseline capture|adopt [--directory migrations] [--namespace public]
553
587
  ```
554
588
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kequtech/tenon-orm",
3
- "version": "0.1.1",
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",
@@ -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
- }