@kequtech/tenon-orm 0.3.1 → 0.4.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +81 -26
  3. package/dist/affected-rows.js +0 -1
  4. package/dist/baseline.js +3 -5
  5. package/dist/check/contracts.d.ts +6 -0
  6. package/dist/check/contracts.js +206 -0
  7. package/dist/check/project.d.ts +18 -0
  8. package/dist/check/project.js +224 -0
  9. package/dist/check/source.d.ts +51 -0
  10. package/dist/check/source.js +629 -0
  11. package/dist/check/types.d.ts +36 -0
  12. package/dist/check/types.js +1 -0
  13. package/dist/check.d.ts +4 -0
  14. package/dist/check.js +147 -0
  15. package/dist/cli.js +40 -27
  16. package/dist/connection.d.ts +5 -12
  17. package/dist/connection.js +12 -121
  18. package/dist/database.js +10 -8
  19. package/dist/describe.js +10 -3
  20. package/dist/generate.d.ts +13 -3
  21. package/dist/generate.js +52 -25
  22. package/dist/generation/catalog.d.ts +5 -6
  23. package/dist/generation/catalog.js +7 -9
  24. package/dist/generation/output.js +2 -4
  25. package/dist/generation/render.d.ts +3 -0
  26. package/dist/generation/render.js +60 -0
  27. package/dist/generation/schema-summary.d.ts +3 -3
  28. package/dist/generation/schema-summary.js +2 -2
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +2 -0
  31. package/dist/inspection/catalog.d.ts +3 -6
  32. package/dist/inspection/catalog.js +9 -18
  33. package/dist/inspection/cli.d.ts +12 -0
  34. package/dist/inspection/cli.js +9 -4
  35. package/dist/inspection/connection.d.ts +2 -3
  36. package/dist/inspection/connection.js +20 -22
  37. package/dist/inspection/options.js +0 -6
  38. package/dist/inspection/result.d.ts +3 -2
  39. package/dist/inspection/result.js +13 -26
  40. package/dist/inspection/types.d.ts +6 -4
  41. package/dist/migrations/connection.d.ts +3 -3
  42. package/dist/migrations/connection.js +14 -21
  43. package/dist/migrations/history.d.ts +4 -4
  44. package/dist/migrations/history.js +5 -5
  45. package/dist/migrations.js +7 -9
  46. package/dist/query-options.d.ts +1 -1
  47. package/dist/query.js +1 -1
  48. package/dist/transaction.d.ts +2 -2
  49. package/dist/transaction.js +21 -77
  50. package/dist/types.d.ts +4 -1
  51. package/docs/adoption-guide.md +88 -111
  52. package/docs/agent-guide.md +94 -147
  53. package/docs/connections.md +185 -0
  54. package/docs/human-guide.md +133 -146
  55. package/docs/migrations.md +133 -0
  56. package/docs/mutations.md +110 -60
  57. package/docs/query-checking.md +199 -0
  58. package/docs/query-inspection.md +89 -58
  59. package/docs/reference.md +232 -437
  60. package/docs/schema-types.md +263 -0
  61. package/docs/sql.md +212 -0
  62. package/package.json +11 -7
  63. package/dist/driver/types.d.ts +0 -10
  64. package/dist/driver/types.js +0 -49
  65. package/dist/generation/table-types.d.ts +0 -2
  66. package/dist/generation/table-types.js +0 -96
  67. package/dist/inspection/protocol.d.ts +0 -7
  68. package/dist/inspection/protocol.js +0 -45
package/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2
4
+
5
+ - Rewrite the README and consumer guides around a complete getting-started workflow and task-based navigation.
6
+ - Add guides for SQL queries, schema types and values, connections, and migrations, including joins, pagination, codec configuration, rollback, and migration recovery.
7
+ - Clarify query result modes, write guards, generated types, source checking, and their validation limits. This release changes documentation only; no application migration is required.
8
+
9
+ ## 0.4.1
10
+
11
+ - Add `db.generateTypes({ out, namespace?, check?, onWarning? })`, using the instance's configured codecs.
12
+ - Remove `tenon generate`; run generation and freshness checks from an application script.
13
+ Use the configured database instance, set `check: true` for freshness checks, and close it when
14
+ the script finishes. See the [generation script example](docs/human-guide.md#generate-types).
15
+
16
+ ## 0.4.0
17
+
18
+ ### Breaking changes
19
+
20
+ - Replace bundled `pg` and `@types/pg` with the `@kequtech/pgclear` runtime dependency (`^0.3.0`).
21
+ Installing Tenon includes the driver; compatible driver patches can update independently.
22
+ Connections accept a PostgreSQL URI, pgclear `PoolOptions`, or a pgclear `Pool`; translate former
23
+ pg configuration and supply deployment settings explicitly.
24
+ PG environment defaults, pgpass/service files, and pg-specific URI options are not read. See
25
+ [connection configuration](docs/connections.md#configure-a-database-instance).
26
+ - `db.native` is now a pgclear `Pool`. Use `queryClassic(text, values?)` for finished SQL and
27
+ acquire/withSession for held sessions; native `query` accepts pgclear's own sql tag only. Prepared
28
+ statements, streams, and COPY use the public session APIs. Use pgclear `listen` for dedicated
29
+ notifications. Pool shutdown, transaction outcomes, and savepoint recovery delegate to the driver.
30
+ - Command counts remain safe integer numbers; int8/bigserial, SQL count(*) and int8 array elements
31
+ use exact strings. Temporal values and exact numeric arrays are strings, bytes are Uint8Array,
32
+ and result arrays are readonly. JSON/array binds need pgclear json/param or encoded strings.
33
+ Plain bigint, Date, and supported native/polyfilled Temporal inputs use text and server inference;
34
+ output conversions are opt-in. Undefined, plain objects, and arrays reject; mutation-helper
35
+ undefined still omits. JSON encoding still rejects bigint values.
36
+ - Tenon owns schema generation and exports `Database[table]['row' | 'insert' | 'update']`.
37
+ Regenerate committed definitions and review write representations. Replace `Rows[table]`,
38
+ `Rows.Insert[table]`, and `Rows.Update[table]` with `Database[table]['row']`,
39
+ `Database[table]['insert']`, and `Database[table]['update']`.
40
+ Catalog queries, schema summaries, defaults/domains/identities, and TypeScript rendering live in
41
+ Tenon; decoder representations come from pgclear. Import generation from Tenon instead of pgclear.
42
+ Tenon retains migration coordination, atomic file output, and `generate --check` freshness checks.
43
+
44
+ ### Added and improved
45
+
46
+ - Re-export Pgclear parameter/codec helpers and their types through Tenon, so application queries
47
+ and generated declarations use one import path. Use Tenon's `sql` for `db.query`.
48
+ - Add optional `tenon check` for a project, file, or file:line, with readable/JSON diagnostics and a
49
+ programmatic `@kequtech/tenon-orm/check` API. Fixed fragments, joins, explicit object shapes, and
50
+ bounded ternaries are checked through pgclear Describe without importing application code or
51
+ executing application SQL. Definite mismatches and unchecked queries fail; inconclusive
52
+ nullability, JSON shapes, decoder types, and refinements remain unproven notes.
53
+ - Bundle TypeScript 6 for source checking and load it only on the checking path. Ordinary execution
54
+ continues to trust result annotations; database tests remain necessary for rows and write effects.
55
+ - Inspection uses public pgclear Describe with parameter OID hints and a selected codec registry.
56
+ Custom text codecs can declare `text.typescript`; generation and source checking use that metadata,
57
+ including derived arrays, without duplicate readTypes declarations. Unidentified custom decoders
58
+ remain unresolved. Migrations, generation, and inspection use public pgclear sessions without a
59
+ private protocol adapter. Catalog reads keep default decoding on the application's borrowed session.
60
+ - Clarify the documentation reading path, common failures, optional patch checking, and the roles of
61
+ source checking, SQL inspection, and generated-file freshness. Keep historical driver reviews out
62
+ of the published consumer docs.
63
+
64
+ ## 0.3.1
65
+
66
+ - Expand documentation for driver configuration, transaction result adapters, value mappings, and
67
+ validation between persistence representations and application domain values.
68
+
3
69
  ## 0.3.0
4
70
 
5
71
  - **Breaking:** `pg@8.23.1` replaces Postgres.js. Use pg `PoolConfig` (`connectionString` for a URI)
package/README.md CHANGED
@@ -2,16 +2,20 @@
2
2
 
3
3
  A PostgreSQL ORM designed for coding agents. Built for Node.js and TypeScript.
4
4
 
5
- Write SQL directly, bind values with a template tag, and reuse queries through ordinary functions.
6
- Tenon provides transactions, affected-row guards, SQL migrations, generated database types, and
7
- query inspection. It includes its pinned pg driver.
5
+ Tenon keeps database work in forms coding agents can read, write, and check: explicit SQL, ordinary
6
+ TypeScript functions, and SQL migrations. Queries carry their result types and execution options
7
+ together, so an agent and the person reviewing its work can see what an operation does in one place.
8
8
 
9
9
  ```sh
10
10
  npm install @kequtech/tenon-orm
11
11
  ```
12
12
 
13
- Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and Node
14
- development typings.
13
+ Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript applications need TypeScript 6+ and Node.js
14
+ typings. The PostgreSQL driver, [pgclear](https://github.com/kequtech/pgclear), is included.
15
+
16
+ ## Query with SQL
17
+
18
+ This example assumes a `public.users` table with integer `id` and text `email` columns:
15
19
 
16
20
  ```ts
17
21
  import { createTenon, sql } from '@kequtech/tenon-orm';
@@ -28,38 +32,89 @@ try {
28
32
  `,
29
33
  { returns: 'oneOrNone' },
30
34
  );
31
- // user is { id: number; email: string } | undefined
35
+ // user: { id: number; email: string } | undefined
32
36
  } finally {
33
37
  await db.close();
34
38
  }
35
39
  ```
36
40
 
37
- The example assumes `public.users` exists. In a server, reuse the instance and close it at shutdown.
38
- `oneOrNone` rejects multiple rows; use `one` for exactly one row, `many` for an array, or omit
39
- `returns` for the driver's result object and metadata.
41
+ `${email}` becomes `$1` in the SQL, with the value passed separately. `sql<Row>` describes one
42
+ returned row. `returns` chooses how Tenon gives those rows back:
43
+
44
+ | `returns` | You receive | Required rows |
45
+ | ------------------ | --------------------------------------------------- | ------------- |
46
+ | `many` | `readonly Row[]` | Any number |
47
+ | `oneOrNone` | `Row \| undefined` | Zero or one |
48
+ | `one` | `Row` | Exactly one |
49
+ | `native` (default) | Driver result with `rows`, `rowCount`, and `fields` | Any number |
50
+
51
+ A row-count mismatch throws. The modes do not add `LIMIT` or change the SQL. In a server, reuse one
52
+ database instance and close it during shutdown. Supply environment variables through your application;
53
+ Tenon does not load `.env` files.
54
+
55
+ ## Designed for coding agents
56
+
57
+ Tenon's API gives coding agents concrete code to work with and explicit contracts to preserve:
40
58
 
41
- Joins, relationships, aggregates, and upserts use PostgreSQL SQL. Object-based writes can use
42
- `sql.insertValues` and `sql.assignments` while keeping the surrounding statement explicit.
43
- `db.native` exposes the underlying `pg.Pool` for LISTEN, cursors, COPY, and other driver features;
44
- those calls bypass Tenon's query and transaction guards.
59
+ - **Readable SQL.** Selected columns, joins, filters, and conflict handling are visible in the query.
60
+ An agent can inspect the operation directly and use PostgreSQL's existing vocabulary.
61
+ - **Ordinary TypeScript.** Reusable queries are functions. An agent can follow imports, change a
62
+ function, and pass the same query code a database or transaction client.
63
+ - **Explicit write limits.** A query can require exactly one affected row or set a maximum. Tenon
64
+ rolls back the operation if that bound is violated.
65
+ - **Actionable checks.** Generated table types describe storage contracts; `tenon check` reports
66
+ contradictions between recoverable SQL and declared types. Freshness checks catch stale generated
67
+ declarations, and database tests verify results and effects.
45
68
 
46
- `sql<Row>` declares an expected result type. `describeQuery` inspects the finished SQL through
47
- PostgreSQL without executing it; it does not verify that annotation. Keep database tests for actual
48
- rows and write effects. Validation, authorization, and retry policy belong to the application.
49
- Existing migration runners can be retained; Tenon's migrations and type generation are optional.
69
+ The guides and changelog ship with the package, giving an agent documentation for the installed
70
+ version. For work in an existing project, start with the [agent guide](docs/agent-guide.md).
71
+
72
+ ## How Tenon fits together
73
+
74
+ - **SQL defines the operation.** Use PostgreSQL joins, aggregates, constraints, and upserts directly.
75
+ Reuse queries with ordinary functions; use fragments when parts of the SQL need to vary.
76
+ - **TypeScript describes the values.** Declare query projections with `sql<Row>`. Optionally generate
77
+ table read/insert/update types with `db.generateTypes({ out: 'src/generated-schema.d.ts' })`.
78
+ - **Transactions define what commits together.** Use `db.transaction(async (tx) => { ... })` and run
79
+ every participating query through `tx`.
80
+ - **Write guards check affected rows.** `{ affectedRows: { exact: 1 } }` rolls back a write unless
81
+ exactly one row was affected. `{ max: 1 }` allows zero or one.
82
+ - **Migrations change the schema.** Tenon's optional runner applies numbered SQL files. You can keep
83
+ an existing schema and migration runner.
84
+
85
+ For object inputs, `sql.insertValues(input)` and `sql.assignments(patch)` build the column/value
86
+ parts of a statement. The table, conditions, expressions, and conflict behavior stay in your SQL.
87
+ There are no model instances or implicit relationship loading.
88
+
89
+ Query types are declarations. Tenon does not validate returned data against `Row` during execution.
90
+ The optional `tenon check` command compares recoverable source queries with TypeScript types and
91
+ PostgreSQL metadata. Keep database tests for actual results and write effects.
92
+
93
+ Some PostgreSQL values need care: bigint columns and `count(*)` read as exact strings, numeric and
94
+ temporal values also default to strings, and JSON/array inputs use explicit encoding helpers. Command
95
+ `rowCount` is a number or null. See [value mappings](docs/schema-types.md#value-mappings) before
96
+ choosing application types.
50
97
 
51
98
  ## Documentation
52
99
 
53
- - **[Human guide](docs/human-guide.md)** — a table, queries, transactions, and generated types.
54
- - **[Agent guide](docs/agent-guide.md)** — implementation contracts and upgrade workflow.
55
- - **[Reference](docs/reference.md)** — API options, value mappings, CLI, and limitations.
56
- - **[Adoption guide](docs/adoption-guide.md)** — replace an ORM and transfer migration ownership.
57
- - **[Mutation cookbook](docs/mutations.md)** — defaults, patches, upserts, arrays, and batching.
58
- - **[Query inspection](docs/query-inspection.md)** — Describe reports and database tests.
59
- - **[Changelog](CHANGELOG.md)** — versioned changes and upgrade instructions.
100
+ Start with [Getting started](docs/human-guide.md) for a complete table-to-query example. Then choose
101
+ the guide for your task:
102
+
103
+ | Guide | What it covers |
104
+ | ---------------------------------------------------------- | ---------------------------------------------------------------------------- |
105
+ | [Writing queries](docs/sql.md) | Parameters, reusable functions, joins, aggregates, pagination, and fragments |
106
+ | [Writing data](docs/mutations.md) | Inserts, patches, defaults, upserts, JSON, arrays, and batches |
107
+ | [Schema types and values](docs/schema-types.md) | Generation, read/write types, codecs, and domain validation |
108
+ | [Migrations](docs/migrations.md) | Creating, applying, inspecting, and rolling back SQL migrations |
109
+ | [Connections and driver access](docs/connections.md) | Pool configuration, shutdown, custom codecs, and native features |
110
+ | [Checking query types](docs/query-checking.md) | Source-aware checks and their supported SQL composition |
111
+ | [Inspecting and testing queries](docs/query-inspection.md) | PostgreSQL metadata and tests of real behavior |
112
+ | [Adopting Tenon](docs/adoption-guide.md) | Replacing an ORM and transferring migration ownership |
113
+ | [Agent guide](docs/agent-guide.md) | A short implementation workflow for an existing project |
114
+ | [API and CLI reference](docs/reference.md) | Signatures, options, errors, commands, and troubleshooting |
60
115
 
61
- These documents all ship with the npm package, including the changelog so an agent upgrading a
62
- project can read the relevant release entries locally. Repository maintenance instructions are in
116
+ The README, guides, and [changelog](CHANGELOG.md) ship with the npm package. Read the documentation
117
+ for your installed version when upgrading. Library maintenance is covered in
63
118
  [CONTRIBUTING.md](https://github.com/kequtech/tenon-orm/blob/main/CONTRIBUTING.md).
64
119
 
65
120
  [ISC licensed](LICENSE). Maintained by [Kequtech](https://github.com/kequtech).
@@ -18,7 +18,6 @@ export function validateAffectedRows(value) {
18
18
  export function checkAffectedRows(constraint, result) {
19
19
  if (!['INSERT', 'UPDATE', 'DELETE', 'MERGE'].includes(result.command) ||
20
20
  result.rowCount === null ||
21
- !Number.isSafeInteger(result.rowCount) ||
22
21
  result.rowCount < 0) {
23
22
  throw new AffectedRowsError(constraint, result.rowCount, result.command, false);
24
23
  }
package/dist/baseline.js CHANGED
@@ -82,16 +82,14 @@ export async function adoptBaseline(options) {
82
82
  if (applied.length) {
83
83
  return [];
84
84
  }
85
- const schema = await client.query('SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = $1', [
86
- namespace,
87
- ]);
85
+ const schema = await client.queryClassic('SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = $1', [namespace]);
88
86
  if (!schema.rows.length) {
89
87
  throw new Error('The baseline namespace does not exist. Use migrate to initialize a fresh database.');
90
88
  }
91
- await client.query('BEGIN');
89
+ await client.queryClassic('BEGIN', []);
92
90
  await createHistoryTable(client, namespace);
93
91
  await recordMigration(client, namespace, baseline);
94
- const result = await client.query('COMMIT');
92
+ const result = await client.queryClassic('COMMIT', []);
95
93
  if (result.command !== 'COMMIT') {
96
94
  throw new Error('Baseline adoption was rolled back.');
97
95
  }
@@ -0,0 +1,6 @@
1
+ import type { QueryDescription } from '../inspection/types.js';
2
+ import { type Project } from './project.js';
3
+ import type { QueryNode, QueryVariant } from './source.js';
4
+ import type { CheckDiagnostic } from './types.js';
5
+ /** Compare non-NULL representations; nullability and value refinements remain explicit limitations. */
6
+ export declare function compareContracts(project: Project, query: QueryNode, variant: QueryVariant, description: QueryDescription, readTypes: Readonly<Record<number, string>>): CheckDiagnostic[];
@@ -0,0 +1,206 @@
1
+ import ts from 'typescript';
2
+ import { location } from './project.js';
3
+ function kinds(project, type, seen = new Set()) {
4
+ if (seen.has(type))
5
+ return new Set();
6
+ seen.add(type);
7
+ if (type.isUnion())
8
+ return new Set(type.types.flatMap((member) => [...kinds(project, member, seen)]));
9
+ if (type.isIntersection())
10
+ return new Set(type.types.flatMap((member) => [...kinds(project, member, seen)]));
11
+ if (type.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Never))
12
+ return new Set();
13
+ if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.TypeParameter))
14
+ return new Set(['unknown']);
15
+ for (const [flag, name] of [
16
+ [ts.TypeFlags.StringLike, 'string'],
17
+ [ts.TypeFlags.NumberLike, 'number'],
18
+ [ts.TypeFlags.BigIntLike, 'bigint'],
19
+ [ts.TypeFlags.BooleanLike, 'boolean'],
20
+ ])
21
+ if (type.flags & flag)
22
+ return new Set([name]);
23
+ if (project.checker.isArrayType(type) || project.checker.isTupleType(type))
24
+ return new Set(['array']);
25
+ return new Set(['object']);
26
+ }
27
+ function disjoint(project, actual, expected, depth = 0) {
28
+ if (depth > 8)
29
+ return false;
30
+ const a = kinds(project, actual), b = kinds(project, expected);
31
+ if (!b.size)
32
+ return true;
33
+ if (a.has('unknown') || b.has('unknown'))
34
+ return false;
35
+ if (![...a].some((kind) => b.has(kind)))
36
+ return true;
37
+ if (a.size === 1 && a.has('array') && b.size === 1 && b.has('array')) {
38
+ const elements = (type) => (type.isUnion() ? type.types : [type]).flatMap((part) => {
39
+ const element = project.checker.getIndexTypeOfType(part, ts.IndexKind.Number);
40
+ return element ? [element] : [];
41
+ });
42
+ const actualElements = elements(actual), expectedElements = elements(expected);
43
+ return (actualElements.length > 0 &&
44
+ expectedElements.length > 0 &&
45
+ actualElements.every((a) => expectedElements.every((b) => disjoint(project, a, b, depth + 1))));
46
+ }
47
+ if (a.size === 1 && a.has('object') && b.size === 1 && b.has('object')) {
48
+ for (const property of actual.getProperties()) {
49
+ const other = expected.getProperty(property.name);
50
+ if (!other ||
51
+ (property.flags & ts.SymbolFlags.Optional && other.flags & ts.SymbolFlags.Optional))
52
+ continue;
53
+ const context = project.selected[0];
54
+ if (disjoint(project, project.checker.getTypeOfSymbolAtLocation(property, context), project.checker.getTypeOfSymbolAtLocation(other, context), depth + 1))
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+ function rowType(project, query) {
61
+ const explicit = query.typeArguments?.[0];
62
+ if (explicit)
63
+ return project.checker.getTypeFromTypeNode(explicit);
64
+ const contextual = project.checker.getContextualType(query);
65
+ const type = contextual ?? project.checker.getTypeAtLocation(query);
66
+ const property = type.getProperty('__rowType');
67
+ return property
68
+ ? project.checker.getNonNullableType(project.checker.getTypeOfSymbolAtLocation(property, query))
69
+ : undefined;
70
+ }
71
+ /** Compare non-NULL representations; nullability and value refinements remain explicit limitations. */
72
+ export function compareContracts(project, query, variant, description, readTypes) {
73
+ const checker = project.checker;
74
+ const at = location(query);
75
+ const diagnostics = [];
76
+ for (const [index, parameter] of variant.parameters.entries()) {
77
+ const expected = description.parameters[index];
78
+ if (!expected)
79
+ continue;
80
+ const inputKinds = kinds(project, parameter.type);
81
+ if (inputKinds.has('string') || inputKinds.has('number')) {
82
+ diagnostics.push({
83
+ ...location(parameter.node),
84
+ kind: 'unproven',
85
+ code: 'parameter-value-unproven',
86
+ parameter: index + 1,
87
+ expected: expected.postgres,
88
+ observed: checker.typeToString(parameter.type),
89
+ message: `Parameter ${index + 1}${parameter.field ? ` (${parameter.field})` : ''}: ${checker.typeToString(parameter.type)} does not prove PostgreSQL ranges or textual formats for ${expected.postgres}.`,
90
+ });
91
+ }
92
+ }
93
+ if (description.columns.length)
94
+ diagnostics.push({
95
+ ...at,
96
+ kind: 'unproven',
97
+ code: 'nullability-unproven',
98
+ message: 'Describe does not establish result nullability; column representation checks exclude SQL NULL.',
99
+ });
100
+ for (const issue of description.issues) {
101
+ if (issue.code === 'duplicate-column')
102
+ diagnostics.push({
103
+ ...at,
104
+ kind: 'mismatch',
105
+ code: issue.code,
106
+ resultColumn: issue.column,
107
+ message: issue.message,
108
+ });
109
+ }
110
+ const declared = rowType(project, query);
111
+ if (!declared ||
112
+ declared.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.TypeParameter)) {
113
+ diagnostics.push({
114
+ ...at,
115
+ kind: 'unproven',
116
+ code: 'result-type-unproven',
117
+ message: 'No concrete declared result shape was resolved.',
118
+ });
119
+ return diagnostics;
120
+ }
121
+ const alternatives = declared.isUnion() ? declared.types : [declared];
122
+ const results = alternatives.map((row) => {
123
+ const findings = [];
124
+ for (const property of row.getProperties()) {
125
+ if (!(property.flags & ts.SymbolFlags.Optional) &&
126
+ !description.columns.some((column) => column.name === property.name)) {
127
+ findings.push({
128
+ ...at,
129
+ kind: 'mismatch',
130
+ code: 'missing-column',
131
+ resultColumn: property.name,
132
+ expected: checker.typeToString(checker.getTypeOfSymbolAtLocation(property, query)),
133
+ observed: 'absent',
134
+ message: `Required result property ${JSON.stringify(property.name)} is absent from the SQL result.`,
135
+ });
136
+ }
137
+ }
138
+ for (const column of description.columns) {
139
+ const property = row.getProperty(column.name);
140
+ const expected = property
141
+ ? checker.getTypeOfSymbolAtLocation(property, query)
142
+ : checker.getIndexTypeOfType(row, ts.IndexKind.String);
143
+ if (!expected)
144
+ continue; // Structural result types may omit additional selected columns.
145
+ const representation = readTypes[column.oid] ?? column.readType;
146
+ const actual = project.types.get(representation);
147
+ const common = {
148
+ ...at,
149
+ resultColumn: column.name,
150
+ expected: checker.typeToString(expected),
151
+ observed: representation,
152
+ };
153
+ if (!actual || actual.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.Any)) {
154
+ findings.push({
155
+ ...common,
156
+ kind: 'unproven',
157
+ code: 'result-representation-unproven',
158
+ message: `Column ${JSON.stringify(column.name)} has an unresolved decoder representation or JSON shape; ${checker.typeToString(expected)} is unproven.`,
159
+ });
160
+ continue;
161
+ }
162
+ const nonnull = checker.getNonNullableType(expected);
163
+ if (nonnull.flags & ts.TypeFlags.Any) {
164
+ findings.push({
165
+ ...common,
166
+ kind: 'unproven',
167
+ code: 'result-type-unproven',
168
+ message: `Column ${JSON.stringify(column.name)} is declared as any.`,
169
+ });
170
+ }
171
+ else if (!checker.isTypeAssignableTo(actual, nonnull)) {
172
+ const mismatch = disjoint(project, actual, nonnull);
173
+ findings.push({
174
+ ...common,
175
+ kind: mismatch ? 'mismatch' : 'unproven',
176
+ code: mismatch ? 'result-type-mismatch' : 'result-refinement-unproven',
177
+ message: `Column ${JSON.stringify(column.name)} decodes as ${representation}; its declared type ${checker.typeToString(expected)} ${mismatch ? 'contradicts that representation' : 'requires a refinement Describe cannot prove'}.`,
178
+ });
179
+ }
180
+ }
181
+ return findings;
182
+ });
183
+ const compatible = results.filter((findings) => !findings.some((finding) => finding.kind === 'mismatch'));
184
+ if (compatible.length) {
185
+ compatible.sort((a, b) => a.length - b.length);
186
+ diagnostics.push(...compatible[0]);
187
+ }
188
+ else if (results.length === 1)
189
+ diagnostics.push(...results[0]);
190
+ else {
191
+ diagnostics.push({
192
+ ...at,
193
+ kind: 'mismatch',
194
+ code: 'result-union-mismatch',
195
+ expected: checker.typeToString(declared),
196
+ observed: description.rowType ?? 'no row declaration',
197
+ message: `No declared result union member matches this SQL variant: ${results
198
+ .map((findings) => findings
199
+ .filter((finding) => finding.kind === 'mismatch')
200
+ .map((finding) => finding.message)
201
+ .join(' '))
202
+ .join(' / ')}`,
203
+ });
204
+ }
205
+ return diagnostics;
206
+ }
@@ -0,0 +1,18 @@
1
+ import ts from 'typescript';
2
+ import type { CheckOptions, CheckDiagnostic, SourceLocation } from './types.js';
3
+ export declare function location(node: ts.Node): SourceLocation;
4
+ export declare function symbol(checker: ts.TypeChecker, node: ts.Node): ts.Symbol | undefined;
5
+ export declare function loadProject(options: CheckOptions): {
6
+ program: ts.Program;
7
+ checker: ts.TypeChecker;
8
+ types: Map<string, ts.Type>;
9
+ parameterType: ts.Type;
10
+ files: ts.SourceFile[];
11
+ selected: ts.SourceFile[];
12
+ line: number | undefined;
13
+ project: string;
14
+ diagnostics: CheckDiagnostic[];
15
+ };
16
+ export type Project = ReturnType<typeof loadProject>;
17
+ /** Resolve actual exported symbols, including aliases and barrel re-exports. */
18
+ export declare function librarySymbols(project: Project): ReadonlyMap<string, ts.Symbol>;