@biref/scanner 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1109 @@
1
+ # @biref/scanner
2
+
3
+ [![CI](https://github.com/joelorzet/biref-db-scanner/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/joelorzet/biref-db-scanner/actions/workflows/ci.yml)
4
+ [![Node](https://img.shields.io/badge/node-%E2%89%A520-brightgreen)](https://nodejs.org)
5
+ [![TypeScript](https://img.shields.io/badge/typescript-strict-blue)](https://www.typescriptlang.org)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
7
+
8
+ Scan any database, inspect every relationship in **both** directions, generate a typed schema from a live scan, and write Prisma-style fluent queries against tables you didn't have to hand-model - with fully hydrated nested results. One SDK, one mental model, for every adapter.
9
+
10
+ ---
11
+
12
+ ## Table of contents
13
+
14
+ - [What it does](#what-it-does)
15
+ - [The differentiating feature: inbound references](#the-differentiating-feature-inbound-references)
16
+ - [Install](#install)
17
+ - [Quick start](#quick-start)
18
+ - [Core concepts](#core-concepts)
19
+ - [Pipeline walkthrough](#pipeline-walkthrough)
20
+ - [1. Wire an adapter](#1-wire-an-adapter)
21
+ - [2. Scan the database](#2-scan-the-database)
22
+ - [3. Inspect the `DataModel`](#3-inspect-the-datamodel)
23
+ - [4. Generate a typed schema (codegen)](#4-generate-a-typed-schema-codegen)
24
+ - [5. Build typed queries](#5-build-typed-queries)
25
+ - [6. Return types - how narrowing works](#6-return-types--how-narrowing-works)
26
+ - [7. Parsers and formatters](#7-parsers-and-formatters)
27
+ - [8. JavaScript consumers (no codegen)](#8-javascript-consumers-no-codegen)
28
+ - [How tables map to the domain model](#how-tables-map-to-the-domain-model)
29
+ - [Relation naming rules](#relation-naming-rules)
30
+ - [Codegen output layout](#codegen-output-layout)
31
+ - [Adapters](#adapters)
32
+ - [API reference](#api-reference)
33
+ - [Contributing](#contributing)
34
+ - [License](#license)
35
+
36
+ ---
37
+
38
+ ## What it does
39
+
40
+ `@biref/scanner` is a headless TypeScript SDK with a four-stage pipeline:
41
+
42
+ 1. **Wire** an adapter - hand the SDK a driver client (`pg.Client`, `pg.Pool`, or anything structurally compatible).
43
+ 2. **Scan** the data store - produce a paradigm-neutral `DataModel` with every entity, field, relationship, constraint, and index discovered from the live database.
44
+ 3. **Generate** a typed schema file from that scan (`biref gen` CLI or `generateSchema` programmatic).
45
+ 4. **Query** with a Prisma-style fluent API whose typed proxy knows your schema, executes against your client, and returns hydrated rows with nested includes.
46
+
47
+ The domain is paradigm-neutral: `Entity`, `Field`, `Reference`, `Relationship`, `Constraint`, `Index` - the same shape comes back for every adapter. A `kind` discriminator on `DataModel` lets you branch on paradigm when needed.
48
+
49
+ ## The differentiating feature: inbound references
50
+
51
+ Most introspection tools only surface the foreign keys an entity *declares*. If you scan `users` you see the columns and the FKs `users` points at, but not the fact that `orders`, `invoices`, and `sessions` all reference `users.id`.
52
+
53
+ `@biref/scanner` walks the graph once and attaches relationships in **both directions** to every entity:
54
+
55
+ - **outbound**: this entity holds the reference (a declared FK pointing outward).
56
+ - **inbound**: another entity holds a reference pointing here.
57
+
58
+ The typed query builder exposes both directions under friendly names you did not have to invent. `.include('orders', ...)` on `users` walks the inbound hop; `.include('user', ...)` on `orders` walks the outbound hop. Both are one call.
59
+
60
+ ---
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ # pnpm
66
+ pnpm add @biref/scanner pg
67
+
68
+ # npm
69
+ npm install @biref/scanner pg
70
+
71
+ # yarn
72
+ yarn add @biref/scanner pg
73
+ ```
74
+
75
+ **Requirements**
76
+
77
+ - Node.js **20.12 or later** (for `process.loadEnvFile` and Corepack)
78
+ - Node.js **24+** if you want to run `.ts` scratch files without a transpiler (`node index.ts`). For Node 22, pass `--experimental-strip-types`.
79
+ - Your own database driver (`pg` for the Postgres adapter). The SDK has **zero runtime dependencies** - it never imports the driver itself, so you pick the version you trust.
80
+
81
+ **Package manager support**
82
+
83
+ Any of pnpm, npm, yarn, or bun works. The `biref` CLI is installed under `node_modules/.bin`:
84
+
85
+ ```bash
86
+ pnpm exec biref gen --url postgres://localhost/mydb
87
+ npm exec biref gen --url postgres://localhost/mydb # or: npx biref gen ...
88
+ yarn exec biref gen --url postgres://localhost/mydb
89
+ ```
90
+
91
+ > The package is not published to npm yet - the name is reserved. Until it ships, install directly from this repo.
92
+
93
+ ---
94
+
95
+ ## Quick start
96
+
97
+ Two commands and one file:
98
+
99
+ ```bash
100
+ # 1. Generate a typed schema from a live database.
101
+ pnpm exec biref gen \
102
+ --url postgres://user:pass@localhost/mydb \
103
+ --all-namespaces
104
+ ```
105
+
106
+ ```ts
107
+ // 2. Scan, query, enjoy typed autocomplete on every namespace,
108
+ // entity, field, and relation the scanner discovered.
109
+ import pg from 'pg';
110
+ import { Biref, postgresAdapter } from '@biref/scanner';
111
+ import type { BirefSchema } from './biref/biref.schema';
112
+
113
+ const client = new pg.Client({ connectionString: 'postgres://localhost/mydb' });
114
+ await client.connect();
115
+
116
+ const biref = Biref.builder()
117
+ .withAdapter(postgresAdapter.create(client))
118
+ .build();
119
+
120
+ const model = await biref.scan({ namespaces: 'all' });
121
+
122
+ const rows = await biref
123
+ .query<BirefSchema>(model)
124
+ .public.users
125
+ .select('id', 'email')
126
+ .where('status', 'eq', 'active')
127
+ .include('orders', (order) =>
128
+ order
129
+ .select('id', 'total')
130
+ .include('order_items', (item) => item.select('variant_id', 'quantity')),
131
+ )
132
+ .findMany();
133
+
134
+ // rows is typed as:
135
+ // readonly {
136
+ // readonly id: bigint;
137
+ // readonly email: string;
138
+ // readonly orders: readonly {
139
+ // readonly id: bigint;
140
+ // readonly total: string;
141
+ // readonly order_items: readonly {
142
+ // readonly variant_id: number;
143
+ // readonly quantity: number;
144
+ // }[];
145
+ // }[];
146
+ // }[]
147
+ ```
148
+
149
+ After step 1, the `./biref/` folder contains:
150
+
151
+ ```
152
+ biref/
153
+ ├── biref.schema.ts # generated every run
154
+ ├── biref.schema.overrides.ts # scaffolded once, never regenerated
155
+ └── (split mode) index.ts + <namespace>/<entity>.ts per table
156
+ ```
157
+
158
+ Step 2 imports `BirefSchema` from the generated file, and every `.select(...)`, `.where(...)`, `.include(...)` call narrows both its input and its return type based on the live schema.
159
+
160
+ ---
161
+
162
+ ## Core concepts
163
+
164
+ These five nouns are all you need to keep in your head:
165
+
166
+ | Concept | One-liner |
167
+ | --- | --- |
168
+ | **Adapter** | A plug-in that bundles an `Introspector`, `QueryEngine`, `RawQueryRunner`, and `RecordParser` for a specific data store. Currently ships with Postgres. |
169
+ | **`DataModel`** | The paradigm-neutral schema produced by a scan. Aggregate of every `Entity` discovered, with relationships in both directions attached. |
170
+ | **Typed query root** | The object returned by `biref.query<Schema>(model)`. A two-layer Proxy keyed by namespace then entity, every leaf a zero-state fluent chain. |
171
+ | **`QueryPlan`** | The immutable tree built up by the chain. Each node is a single-entity `QuerySpec` plus a list of nested includes. |
172
+ | **`QueryPlanExecutor`** | The core driver that walks a plan tree, runs one SQL query per include level, and stitches children into parents in process. Lives in `src/core/` and is paradigm-neutral. |
173
+
174
+ The **typed path** and the **dynamic path** run the exact same runtime. The distinction is compile-time only: pass a schema generic and your editor narrows types; omit it and everything still works with `any`-shaped autocomplete.
175
+
176
+ ---
177
+
178
+ ## Pipeline walkthrough
179
+
180
+ Each subsection is self-contained and builds on the previous. You can stop at step 3 if all you want is schema metadata, or layer the typed query builder on top for real queries.
181
+
182
+ ### 1. Wire an adapter
183
+
184
+ `Biref.builder()` returns a fluent builder. Register one or more adapters, call `.build()`, and you have a facade.
185
+
186
+ ```ts
187
+ import pg from 'pg';
188
+ import { Biref, postgresAdapter } from '@biref/scanner';
189
+
190
+ const pgClient = new pg.Client({ connectionString: 'postgres://localhost/mydb' });
191
+ await pgClient.connect();
192
+
193
+ const biref = Biref.builder()
194
+ .withAdapter(postgresAdapter.create(pgClient))
195
+ .build();
196
+ ```
197
+
198
+ Registering multiple adapters at once:
199
+
200
+ ```ts
201
+ const biref = Biref.builder()
202
+ .withAdapters(postgresAdapter.create(pgClient))
203
+ .build();
204
+ ```
205
+
206
+ The builder stores each adapter together with its engine, parser, and query runner. When you later call `biref.query(...)...findMany()`, the SDK uses the runner to execute the SQL through the driver you provided - you never touch the client again for query execution.
207
+
208
+ **Structural client typing.** `postgresAdapter.create(client)` accepts any object that matches:
209
+
210
+ ```ts
211
+ interface PostgresClient {
212
+ query<TRow>(text: string, params?: readonly unknown[]):
213
+ Promise<{ rows: TRow[] }>;
214
+ }
215
+ ```
216
+
217
+ Both `pg.Client` and `pg.Pool` satisfy this shape. If your driver is compatible, pass it directly - the SDK never imports `pg` itself.
218
+
219
+ ### 2. Scan the database
220
+
221
+ `biref.scan()` produces a `DataModel` - the aggregate of every entity the adapter discovered.
222
+
223
+ **Defaults (Postgres):** scans the `public` schema only.
224
+
225
+ ```ts
226
+ const model = await biref.scan();
227
+ ```
228
+
229
+ **Specific namespaces** (recommended when you know the layout):
230
+
231
+ ```ts
232
+ const model = await biref.scan({
233
+ namespaces: ['public', 'auth', 'billing'],
234
+ });
235
+ ```
236
+
237
+ **Wildcard - every non-system namespace:**
238
+
239
+ ```ts
240
+ const model = await biref.scan({ namespaces: 'all' });
241
+ ```
242
+
243
+ The `'all'` sentinel asks the adapter to query its system catalog for every schema that isn't internal. For Postgres that excludes `pg_catalog`, `information_schema`, `pg_toast*`, and `pg_temp_*`. Use this when you want to bootstrap codegen against a database you didn't design.
244
+
245
+ **Allowlist / denylist entities** (applied after namespace filtering):
246
+
247
+ ```ts
248
+ const model = await biref.scan({
249
+ namespaces: ['public'],
250
+ includeEntities: ['users', 'orders', 'products'],
251
+ // or:
252
+ excludeEntities: ['audit_log', 'schema_migrations'],
253
+ });
254
+ ```
255
+
256
+ **Multi-adapter setups:**
257
+
258
+ ```ts
259
+ // Pick explicitly by adapter name.
260
+ const model = await biref.scan('postgres');
261
+
262
+ // Or route by URL scheme.
263
+ const model = await biref.scanByUrl('postgres://localhost/mydb');
264
+ ```
265
+
266
+ **`IntrospectOptions` reference:**
267
+
268
+ | Field | Type | Effect |
269
+ | --- | --- | --- |
270
+ | `namespaces` | `readonly string[] \| 'all'` | Namespaces to scan. Omit for the adapter's default (Postgres: `['public']`). Pass `'all'` for every non-system namespace. |
271
+ | `includeEntities` | `readonly string[]` | Allowlist applied after namespace filtering. |
272
+ | `excludeEntities` | `readonly string[]` | Denylist applied after namespace filtering. |
273
+
274
+ ### 3. Inspect the `DataModel`
275
+
276
+ The model is the same paradigm-neutral shape you get from the codegen - usable directly when you want metadata rather than queries (schema docs, change detection, migration tools, data explorers).
277
+
278
+ ```ts
279
+ // Count discovered entities.
280
+ console.log(model.entities.length);
281
+
282
+ // Look up a specific entity.
283
+ const users = model.getEntity('public', 'users');
284
+
285
+ // Iterate everything by namespace.
286
+ for (const entity of model.entities) {
287
+ console.log(`${entity.namespace}.${entity.name} (${entity.fields.length} fields)`);
288
+ }
289
+ ```
290
+
291
+ **Entity shape:**
292
+
293
+ ```ts
294
+ users?.namespace; // 'public'
295
+ users?.name; // 'users'
296
+ users?.identifier; // readonly string[] - primary key columns, declaration order preserved
297
+ users?.description; // string | null - the Postgres COMMENT ON TABLE, if any
298
+ ```
299
+
300
+ **Field shape** (`Field`):
301
+
302
+ ```ts
303
+ const email = users?.fields.find((f) => f.name === 'email');
304
+ email?.name; // 'email'
305
+ email?.type.category; // 'string' - normalized category
306
+ email?.type.nativeType; // 'text' - original adapter type
307
+ email?.nullable; // boolean
308
+ email?.isIdentifier; // is this column part of the primary key?
309
+ email?.defaultValue; // string | null - the column default expression
310
+ email?.description; // string | null - Postgres COMMENT ON COLUMN
311
+ ```
312
+
313
+ `FieldTypeCategory` is the enum of normalized categories:
314
+
315
+ ```ts
316
+ type FieldTypeCategory =
317
+ | 'string'
318
+ | 'integer'
319
+ | 'decimal'
320
+ | 'boolean'
321
+ | 'date'
322
+ | 'timestamp'
323
+ | 'time'
324
+ | 'json'
325
+ | 'uuid'
326
+ | 'binary'
327
+ | 'enum'
328
+ | 'array'
329
+ | 'reference'
330
+ | 'unknown';
331
+ ```
332
+
333
+ Enum fields carry their labels on `type.enumValues`; array fields carry an `elementType` recursively, including enum element types.
334
+
335
+ **Constraints:**
336
+
337
+ ```ts
338
+ users?.constraints; // Constraint[]
339
+ // Each has: { name, kind: 'unique' | 'check' | 'exclusion' | 'custom', fields, expression }
340
+ ```
341
+
342
+ **Indexes** (excluding the primary key index):
343
+
344
+ ```ts
345
+ users?.indexes; // Index[]
346
+ // Each has: { name, kind: 'btree' | 'hash' | 'gin' | 'gist' | 'brin' | 'spgist' | 'unknown', fields, unique, partial, definition }
347
+ ```
348
+
349
+ **Relationships - the headline feature:**
350
+
351
+ ```ts
352
+ // Every relationship involving this entity, in both directions.
353
+ model.relationshipsOf('public', 'users');
354
+
355
+ // Just the outbound ones (FKs this entity declares).
356
+ model.outboundRelationshipsOf('public', 'users');
357
+
358
+ // Just the inbound ones (other entities pointing here).
359
+ const incoming = model.inboundRelationshipsOf('public', 'users');
360
+
361
+ for (const rel of incoming) {
362
+ const from = rel.reference.fromEntity; // { namespace, name }
363
+ const cols = rel.reference.fromFields; // columns on the other side
364
+ const onDel = rel.reference.onDelete; // 'no-action' | 'cascade' | 'set-null' | …
365
+ console.log(`${from.namespace}.${from.name}(${cols.join(', ')}) -> users.id (${onDel})`);
366
+ }
367
+ ```
368
+
369
+ Self-referential FKs appear on the same entity as **both** an inbound and an outbound relationship. Composite keys are preserved column-by-column in declaration order on both sides. See [How tables map to the domain model](#how-tables-map-to-the-domain-model) for the full translation rules.
370
+
371
+ ### 4. Generate a typed schema (codegen)
372
+
373
+ The `biref` CLI connects to a live database once, scans it, and emits `.ts` files with a `BirefSchema` type that mirrors the schema. Import that type at your call sites to get fully typed autocomplete and return types on the query builder.
374
+
375
+ **Single-file mode** - everything in one `./biref/biref.schema.ts`:
376
+
377
+ ```bash
378
+ pnpm exec biref gen \
379
+ --url postgres://user:pass@localhost/mydb \
380
+ --namespace public \
381
+ --namespace billing
382
+ ```
383
+
384
+ **Split mode** - one file per entity plus an `index.ts`, ideal for browsing large schemas table-by-table:
385
+
386
+ ```bash
387
+ pnpm exec biref gen \
388
+ --url postgres://user:pass@localhost/mydb \
389
+ --split \
390
+ --all-namespaces
391
+ ```
392
+
393
+ Split-mode output layout:
394
+
395
+ ```
396
+ biref/
397
+ ├── index.ts # re-exports + BirefSchema type
398
+ ├── biref.schema.overrides.ts # scaffolded once, never regenerated
399
+ ├── identity/
400
+ │ ├── users.ts # export interface IdentityUsers { ... }
401
+ │ └── accounts.ts
402
+ ├── catalog/
403
+ │ ├── products.ts
404
+ │ └── ...
405
+ └── commerce/
406
+ └── orders.ts
407
+ ```
408
+
409
+ Both modes import the same type at your call sites:
410
+
411
+ ```ts
412
+ import type { BirefSchema } from './biref/biref.schema'; // single-file
413
+ import type { BirefSchema } from './biref'; // split mode (resolves to biref/index.ts)
414
+ ```
415
+
416
+ **All CLI flags:**
417
+
418
+ | Flag | Effect |
419
+ | --- | --- |
420
+ | `--url <connection>` | Required. Database URL the CLI connects to. |
421
+ | `--out <path>` | Output file (single mode) or folder (split mode). Defaults to `./biref/biref.schema.ts` or `./biref`. |
422
+ | `--split` | Emit per-entity files + an index instead of a single file. |
423
+ | `--namespace <name>` | Scan this namespace. Repeatable. |
424
+ | `--all-namespaces` | Scan every non-system namespace the database exposes. Mutually exclusive with `--namespace`. |
425
+ | `--overwrite` / `--no-overwrite` | Overwrite existing generated files (default) or refuse and error out - useful for CI drift checks. The overrides file is always preserved. |
426
+ | `--adapter <name>` | Explicitly pick an adapter when more than one is known. Optional. |
427
+
428
+ **What the command writes:**
429
+
430
+ | File | Regenerated? | Purpose |
431
+ | --- | --- | --- |
432
+ | `biref.schema.ts` (single) or `index.ts` + `<namespace>/<entity>.ts` (split) | Every run | Generated descriptors + `BirefSchema = ApplySchemaOverrides<Raw, Overrides>`. |
433
+ | `biref.schema.overrides.ts` | **Never** after first write | Type your jsonb columns or override any auto-inferred type. |
434
+
435
+ **Typing a jsonb column** via overrides:
436
+
437
+ ```ts
438
+ // biref.schema.overrides.ts
439
+ export interface Overrides {
440
+ 'identity.users': {
441
+ profile: { plan: 'free' | 'pro'; prefs: { darkMode: boolean } };
442
+ };
443
+ }
444
+ ```
445
+
446
+ After the next regen, `q.identity.users.select('profile').findFirst()` returns `{ profile: { plan: 'free' | 'pro'; prefs: { darkMode: boolean } } | null }`. Overrides deep-merge onto the emitted schema via a public `ApplySchemaOverrides<Raw, Overrides>` type - keyed by qualified entity name `'namespace.entity'`, one level of mapped-type gymnastics.
447
+
448
+ **Programmatic API.** Every piece the CLI uses is exported:
449
+
450
+ ```ts
451
+ import {
452
+ generateSchema, // (model: DataModel): string
453
+ generateSchemaFiles, // (model: DataModel): readonly SchemaFile[]
454
+ overridesScaffold, // (): string
455
+ tsTypeFor, // (fieldType, nullable): string
456
+ type SchemaFile, // { path: string; content: string }
457
+ } from '@biref/scanner';
458
+ ```
459
+
460
+ **Single-file codegen from your own script:**
461
+
462
+ ```ts
463
+ import { writeFileSync } from 'node:fs';
464
+ import { generateSchema, Biref, postgresAdapter } from '@biref/scanner';
465
+ import pg from 'pg';
466
+
467
+ const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
468
+ await client.connect();
469
+ const biref = Biref.builder().withAdapter(postgresAdapter.create(client)).build();
470
+ const model = await biref.scan({ namespaces: 'all' });
471
+ writeFileSync('./biref/biref.schema.ts', generateSchema(model));
472
+ await client.end();
473
+ ```
474
+
475
+ **Split-mode codegen from your own script:**
476
+
477
+ ```ts
478
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
479
+ import { dirname, resolve } from 'node:path';
480
+ import { generateSchemaFiles, overridesScaffold } from '@biref/scanner';
481
+
482
+ const outDir = resolve('./biref');
483
+ mkdirSync(outDir, { recursive: true });
484
+
485
+ for (const file of generateSchemaFiles(model)) {
486
+ const abs = resolve(outDir, file.path);
487
+ mkdirSync(dirname(abs), { recursive: true });
488
+ writeFileSync(abs, file.content);
489
+ }
490
+
491
+ // Scaffold the overrides file only on first run so your edits survive.
492
+ const overridesPath = resolve(outDir, 'biref.schema.overrides.ts');
493
+ if (!existsSync(overridesPath)) {
494
+ writeFileSync(overridesPath, overridesScaffold());
495
+ }
496
+ ```
497
+
498
+ The generated schema always imports `./biref.schema.overrides` (type-only). The CLI writes the scaffold on first run; programmatic callers need to do the same themselves, otherwise the `import type { Overrides }` line at the top of the generated file will fail to resolve.
499
+
500
+ ### 5. Build typed queries
501
+
502
+ `biref.query<BirefSchema>(model)` returns a **two-layer Proxy**: the first layer is keyed by namespace, the second by entity, and every leaf is a fresh `TypedChain` bound to that `(namespace, entity)` pair.
503
+
504
+ ```ts
505
+ const root = biref.query<BirefSchema>(model);
506
+
507
+ // Dot-access surfaces every namespace and entity discovered by the scan.
508
+ const users = root.identity.users; // TypedChain bound to identity.users
509
+ const products = root.catalog.products; // TypedChain bound to catalog.products
510
+ ```
511
+
512
+ The chain is **immutable** - every method returns a new `TypedChain`. Your projection narrows the row shape at compile time, your filters narrow the allowed operators by the field's category, and your includes narrow the nested result shape recursively.
513
+
514
+ Each method below has the runtime behavior first, then the compile-time narrowing.
515
+
516
+ #### `.select(...)` - project a subset of columns
517
+
518
+ Three shapes, all valid:
519
+
520
+ ```ts
521
+ // Explicit fields: narrows the row type to { id, email } and strips everything else from the SQL.
522
+ root.identity.users.select('id', 'email');
523
+
524
+ // Explicit wildcard: same as omitting .select() entirely. Narrows nothing.
525
+ root.identity.users.select('*');
526
+
527
+ // Zero-arg shorthand for the wildcard.
528
+ root.identity.users.select();
529
+ ```
530
+
531
+ **Rules:**
532
+
533
+ - Every named field must exist on the entity. Unknown names throw at chain time before any SQL is emitted.
534
+ - Mixing `'*'` with named fields throws.
535
+ - Wildcard leaves `plan.spec.select` as `undefined`, so the engine emits every column the adapter reported. The parser projects every column into the result record.
536
+
537
+ #### `.where(field, operator, value)` - narrow by predicate
538
+
539
+ The operator set narrows automatically based on the field's category:
540
+
541
+ | Category | Allowed operators |
542
+ | --- | --- |
543
+ | `string`, `uuid`, `enum` | `eq`, `neq`, `in`, `not-in`, `like`, `ilike`, `is-null`, `is-not-null` |
544
+ | `integer`, `decimal`, `date`, `timestamp`, `time` | `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `between`, `in`, `not-in`, `is-null`, `is-not-null` |
545
+ | `boolean` | `eq`, `neq`, `is-null`, `is-not-null` |
546
+ | everything else | `eq`, `neq`, `is-null`, `is-not-null` |
547
+
548
+ **Value shape** depends on the operator:
549
+
550
+ | Operator | Value shape |
551
+ | --- | --- |
552
+ | `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `like`, `ilike` | scalar - same type as the field |
553
+ | `in`, `not-in` | `readonly T[]` - empty array short-circuits to `FALSE` / `TRUE` respectively |
554
+ | `between` | `readonly [min, max]` tuple |
555
+ | `is-null`, `is-not-null` | no value - use the two-argument form |
556
+
557
+ Examples:
558
+
559
+ ```ts
560
+ await root.catalog.products
561
+ .select('id', 'sku', 'price')
562
+ .where('active', 'eq', true)
563
+ .where('price', 'between', ['1.00', '99.99'])
564
+ .where('sku', 'ilike', 'BOOK-%')
565
+ .where('deleted_at', 'is-null') // two-arg form
566
+ .where('status', 'in', ['active', 'archived']) // array value
567
+ .findMany();
568
+ ```
569
+
570
+ Unknown fields in `.where` throw at chain time. Passing the wrong operator for a category (e.g. `.where('email', 'between', ...)`) is a compile-time error in the typed path.
571
+
572
+ #### `.orderBy(field, direction?)` - sort
573
+
574
+ ```ts
575
+ root.commerce.orders
576
+ .orderBy('placed_at', 'desc')
577
+ .orderBy('id', 'asc');
578
+ ```
579
+
580
+ Defaults to `'asc'` when direction is omitted. Multiple `.orderBy` calls compose.
581
+
582
+ #### `.limit(n)` / `.offset(n)` - pagination
583
+
584
+ ```ts
585
+ root.commerce.orders
586
+ .orderBy('placed_at', 'desc')
587
+ .limit(100)
588
+ .offset(200);
589
+ ```
590
+
591
+ Limit and offset are bound as parameters (`$N`), not inlined into the SQL.
592
+
593
+ #### `.include(relation, build?)` - recursive nested hydration
594
+
595
+ Four shapes - pick whichever matches your intent:
596
+
597
+ ```ts
598
+ // 1. Shorthand: every field of the related entity, no nested includes.
599
+ root.identity.users.include('orders');
600
+
601
+ // 2. Narrowing: pick child fields and/or chain further includes.
602
+ root.identity.users.include('orders', (order) =>
603
+ order.select('id', 'total').include('order_items', (item) =>
604
+ item.select('variant_id', 'quantity'),
605
+ ),
606
+ );
607
+
608
+ // 3. Wildcard: every relation the entity has, each with default projection.
609
+ root.identity.users.include('*');
610
+
611
+ // 4. Combined: a single entity can stack multiple includes of any shape.
612
+ root.identity.users
613
+ .include('orders')
614
+ .include('sessions', (s) => s.select('id', 'expires_at'))
615
+ .include('*');
616
+ ```
617
+
618
+ **Runtime behavior:**
619
+
620
+ - The executor runs one SQL query per include level. Root query first, then one query per hop, filtered to the parent keys it just collected (`WHERE child_key IN (...parent_keys)`). No SQL JOINs; all stitching happens in process.
621
+ - This works uniformly for single-column, composite, self-referential, and cross-schema FKs.
622
+ - **Outbound relations** hydrate as a single object (`T | null`) - cardinality `'one'`. The target of an outbound FK is always unique (primary key), so there's at most one match.
623
+ - **Inbound relations** hydrate as an array (`readonly T[]`) - cardinality `'many'`. Any number of rows can reference back.
624
+ - **Self-referential FKs** use the conventional `'parent'` (outbound) and `'children'` (inbound) relation names.
625
+
626
+ **Projection honoring:** the executor transparently injects the parent's join keys into the parent's `SELECT` and the child's join keys into the child's `SELECT` so the stitcher can read them. After stitching, those injected keys are filtered back out - the final rows only expose the columns you actually asked for.
627
+
628
+ **Validation:** unknown relation names throw at chain time with a list of every valid name on the current entity. `.include('*', callback)` throws because the sub-builder shape varies per relation - a single callback can't narrow all of them coherently.
629
+
630
+ #### `.findMany()` / `.findFirst()` - terminals
631
+
632
+ ```ts
633
+ // Fetch many. Returns readonly HydratedRow<...>[].
634
+ const all = await root.catalog.products
635
+ .select('id', 'sku', 'name')
636
+ .where('active', 'eq', true)
637
+ .findMany();
638
+
639
+ // Fetch one. Applies limit: 1 and returns HydratedRow<...> | null.
640
+ const one = await root.catalog.products
641
+ .select('id', 'sku')
642
+ .where('sku', 'eq', 'WIDGET-001')
643
+ .findFirst();
644
+ ```
645
+
646
+ Both execute against the adapter's captured client. You never touch the driver once the chain is assembled.
647
+
648
+ `.toPlan()` is also available for tests and tooling - it returns the accumulated `QueryPlan` without executing. The final Prisma-style `count()` terminal is on the v1.1 roadmap; use `findMany().then(r => r.length)` until then.
649
+
650
+ ### 6. Return types - how narrowing works
651
+
652
+ The typed chain's return type is `HydratedRow<Sel, Inc>` where:
653
+
654
+ - **`Sel`** is the accumulated projection (how `.select()` narrowed it).
655
+ - **`Inc`** is the accumulated include map (every `.include()` grows it).
656
+
657
+ Five things compose to produce the final row type:
658
+
659
+ **(a)** `select('id', 'email')` narrows `Sel` to `{ id: <type>, email: <type> }`. No `.select()` (or `.select('*')`) leaves `Sel` as `DefaultSelect<S, Ns, E>`, which is every field on the entity.
660
+
661
+ **(b)** Each field descriptor carries both its TS type (`ts`) and its nullability (`nullable`). `TypeOf<S, Ns, E, F>` returns `ts` if non-nullable, `ts | null` if nullable. So `.select('deleted_at')` on a nullable column produces `{ deleted_at: Date | null }` - no surprises from `null` at runtime.
662
+
663
+ **(c)** `.include('orders', ...)` grows `Inc` by adding `{ orders: WrapForCardinality<'many', HydratedRow<SubSel, SubInc>> }`. `WrapForCardinality` checks the stored cardinality literal and produces either `readonly T[]` (many) or `T | null` (one).
664
+
665
+ **(d)** Nested includes recurse through `HydratedRow` - a child include builds its own `(SubSel, SubInc)` pair inside the callback and contributes its own `HydratedRow` shape into the parent's `Inc`.
666
+
667
+ **(e)** The final `HydratedRow<Sel, Inc>` is `Sel & { [K in keyof Inc]: Inc[K] }`. Your editor sees the merged shape.
668
+
669
+ **Concrete example:**
670
+
671
+ ```ts
672
+ const rows = await biref
673
+ .query<BirefSchema>(model)
674
+ .identity.users
675
+ .select('id', 'email')
676
+ .include('orders', (order) =>
677
+ order
678
+ .select('id', 'total')
679
+ .include('order_items', (item) => item.select('variant_id', 'quantity')),
680
+ )
681
+ .findMany();
682
+ ```
683
+
684
+ The inferred type is:
685
+
686
+ ```ts
687
+ readonly {
688
+ readonly id: bigint;
689
+ readonly email: string;
690
+ readonly orders: readonly {
691
+ readonly id: bigint;
692
+ readonly total: string;
693
+ readonly order_items: readonly {
694
+ readonly variant_id: number;
695
+ readonly quantity: number;
696
+ }[];
697
+ }[];
698
+ }[]
699
+ ```
700
+
701
+ **Cardinality recap:**
702
+
703
+ | Direction | Include key | Cardinality | Result type |
704
+ | --- | --- | --- | --- |
705
+ | Outbound (this entity holds FK → target) | `user`, `kyc`, `parent_category`, ... | `one` | `HydratedRow<...> \| null` |
706
+ | Inbound (other entity holds FK → this) | `orders`, `sessions`, `children`, ... | `many` | `readonly HydratedRow<...>[]` |
707
+
708
+ ### 7. Parsers and formatters
709
+
710
+ Raw driver output rarely matches what you want to work with: `int8` columns come back as strings, `numeric` columns come back as strings, `timestamp` columns vary per driver. A `RecordParser` normalizes that using the `DataModel`.
711
+
712
+ **The typed query builder uses the adapter's parser automatically** - you don't construct one yourself. This section is for advanced use (custom adapters, direct SQL execution, export pipelines).
713
+
714
+ #### `DefaultRecordParser` - paradigm-neutral coercion
715
+
716
+ | Field category | Coercion |
717
+ | --- | --- |
718
+ | `string`, `uuid`, `enum` | `String(raw)` |
719
+ | `integer` | `Number(raw)`, keeping `bigint` when the driver passes one |
720
+ | `decimal` | kept as string (precision-preserving) |
721
+ | `boolean` | `Boolean(raw)` |
722
+ | `date`, `timestamp`, `time` | `new Date(raw)` if not already a `Date` |
723
+ | `json`, `binary`, `array`, `reference`, `unknown` | pass-through |
724
+
725
+ Missing fields in the row are **omitted** from the parsed record (not filled with `null`), so projection stays tight all the way through.
726
+
727
+ #### `PostgresRecordParser` - Postgres driver specifics
728
+
729
+ Extends `DefaultRecordParser` with:
730
+
731
+ - `int8` / `bigint` native types → JS `bigint` (pg returns these as strings by default, to preserve precision)
732
+ - `json` / `jsonb` arriving as strings → parsed to objects (for drivers that don't auto-parse)
733
+
734
+ **Projection honoring.** Both parsers only emit fields that are actually present in the driver row. `.select('id', 'email')` sends `SELECT "id", "email"` to Postgres, pg returns `{id, email}` rows, and the parser emits `{id, email}` parsed records. Unselected columns never inflate payloads.
735
+
736
+ #### Formatters - JSON, CSV, raw
737
+
738
+ ```ts
739
+ import { JsonFormatter, CsvFormatter, RawFormatter } from '@biref/scanner';
740
+
741
+ // ParsedRecord[] -> JSON string.
742
+ new JsonFormatter({ pretty: true }).serialize(rows);
743
+ // Handles Date -> ISO string, bigint -> decimal string.
744
+
745
+ // ParsedRecord[] -> RFC 4180 CSV.
746
+ new CsvFormatter({ delimiter: ',', includeHeader: true }).serialize(rows);
747
+
748
+ // ParsedRecord[] -> ParsedRecord[] (pass-through).
749
+ new RawFormatter().serialize(rows);
750
+ ```
751
+
752
+ All three implement `Formatter<TOutput>` and accept an optional `{ fields }` override if you want to force a specific column set.
753
+
754
+ ### 8. JavaScript consumers (no codegen)
755
+
756
+ You can use the SDK from plain JavaScript and still get full IntelliSense, via JSDoc type imports that resolve the codegen-generated `.ts` file. The TypeScript language server (VS Code, Cursor, WebStorm, Zed, Neovim with `tsserver`) will then narrow `biref.query(model)` calls exactly as it would for a TS consumer.
757
+
758
+ **Drop this at the top of your JS file:**
759
+
760
+ ```js
761
+ /**
762
+ * @typedef {import('./biref/biref.schema').BirefSchema} BirefSchema
763
+ * @typedef {import('@biref/scanner').TypedQueryRoot<BirefSchema>} TypedRoot
764
+ */
765
+
766
+ const biref = Biref.builder().withAdapter(postgresAdapter.create(client)).build();
767
+ const model = await biref.scan({ namespaces: 'all' });
768
+
769
+ /** @type {TypedRoot} */
770
+ const q = /** @type {any} */ (biref.query(model));
771
+
772
+ // Full autocomplete on q.public.users.select('id', 'email'):
773
+ await q.public.users
774
+ .select('id', 'email')
775
+ .include('orders', (o) => o.select('id', 'total'))
776
+ .findMany();
777
+ ```
778
+
779
+ **One-time project setup.** Add a `jsconfig.json` at the project root so the language server knows to analyze your `.js` files and resolve types from `.ts`:
780
+
781
+ ```json
782
+ {
783
+ "compilerOptions": {
784
+ "target": "es2022",
785
+ "module": "esnext",
786
+ "moduleResolution": "bundler",
787
+ "allowJs": true,
788
+ "checkJs": false,
789
+ "strict": true,
790
+ "noUncheckedIndexedAccess": true
791
+ },
792
+ "include": ["**/*.js", "biref/**/*.ts"]
793
+ }
794
+ ```
795
+
796
+ `checkJs: false` means JSDoc types drive autocomplete without firing type errors on every line of JS - usually what JS users want.
797
+
798
+ **Runtime is identical** in JS and TS - same Proxy, same chain builder, same executor, same queries. The TS/JS distinction exists only in the editor.
799
+
800
+ ---
801
+
802
+ ## How tables map to the domain model
803
+
804
+ The Postgres adapter runs six parallel queries against `pg_catalog` and stitches the results into paradigm-neutral `Entity` objects. Here's what each piece of SQL becomes:
805
+
806
+ **Tables** (`pg_class` where `relkind IN ('r', 'p')`) → `Entity` records with `namespace`, `name`, and `description` (from `pg_description`). Both regular tables and partitioned tables are picked up.
807
+
808
+ **Columns** (`pg_attribute` + `pg_type`) → `Field[]`. Each column becomes one `Field` with:
809
+
810
+ - `name` - the column name
811
+ - `type.category` - one of 14 `FieldTypeCategory` values (see the enum in §3)
812
+ - `type.nativeType` - the original `format_type()` output (`varchar(120)`, `numeric(10,2)`, `text[]`, etc.)
813
+ - `nullable`, `isIdentifier`, `defaultValue`, `description`
814
+
815
+ **Type category mapping.** Every Postgres built-in is mapped to a category:
816
+
817
+ | Postgres type | Category | Notes |
818
+ | --- | --- | --- |
819
+ | `text`, `varchar`, `bpchar`, `char`, `name`, `citext` | `string` | |
820
+ | `int2`, `int4`, `int8`, `smallint`, `integer`, `bigint` | `integer` | `int8` → `bigint` via the Postgres parser |
821
+ | `numeric`, `decimal`, `float4`, `float8`, `money`, `real`, `double precision` | `decimal` | Kept as string to preserve precision |
822
+ | `bool`, `boolean` | `boolean` | |
823
+ | `date` | `date` | |
824
+ | `timestamp`, `timestamptz` | `timestamp` | Parsed as JS `Date` |
825
+ | `time`, `timetz` | `time` | Parsed as JS `Date` |
826
+ | `json`, `jsonb` | `json` | Parsed to object; override type via `biref.schema.overrides.ts` |
827
+ | `uuid` | `uuid` | |
828
+ | `bytea` | `binary` | |
829
+ | Custom `CREATE TYPE AS ENUM` | `enum` | Labels on `type.enumValues` |
830
+ | Arrays (`text[]`, `int4[]`, ...) | `array` | Element type on `type.elementType` (including enum element types) |
831
+ | `interval`, `inet`, `cidr`, `macaddr`, `macaddr8`, `bit`, `varbit`, geometric types, text search, range types, `xml` | `unknown` | Native type string preserved for branching |
832
+
833
+ **Primary keys** (`pg_constraint` where `contype='p'`) → `entity.identifier`. Composite keys are preserved in declaration order; they drive both the typed chain's identifier tuple and the executor's stitching logic for inbound references.
834
+
835
+ **Foreign keys** (`pg_constraint` where `contype='f'`) → two `Relationship` entries per FK, one on the source entity (direction `'outbound'`) and one on the target entity (direction `'inbound'`). Both directions reference the same underlying `Reference` object:
836
+
837
+ ```ts
838
+ interface Reference {
839
+ name: string; // FK constraint name
840
+ fromEntity: { namespace: string; name: string };
841
+ fromFields: readonly string[]; // FK column(s) on the source
842
+ toEntity: { namespace: string; name: string };
843
+ toFields: readonly string[]; // PK column(s) on the target
844
+ confidence: 1; // always 1 for declared FKs
845
+ onUpdate: ReferentialAction | null;
846
+ onDelete: ReferentialAction | null;
847
+ }
848
+ ```
849
+
850
+ **Every FK flavor is supported:**
851
+
852
+ - Single-column FKs
853
+ - Composite FKs (multiple columns on both sides, column order preserved)
854
+ - Self-referential FKs (same entity on both sides)
855
+ - Cross-schema FKs (`billing.invoices.order_id` → `commerce.orders.id`)
856
+ - Every `ON UPDATE` / `ON DELETE` action: `no-action`, `restrict`, `cascade`, `set-null`, `set-default`
857
+
858
+ **Indexes** (`pg_index` excluding primary-key indexes) → `Index[]` with `kind` normalized to `'btree' | 'hash' | 'gin' | 'gist' | 'brin' | 'spgist' | 'unknown'`, plus `unique`, `partial`, `definition`, and the indexed field names.
859
+
860
+ **Constraints** (`pg_constraint` where `contype IN ('c', 'u', 'x')`) → `Constraint[]` with `kind: 'unique' | 'check' | 'exclusion' | 'custom'` and the original expression text.
861
+
862
+ ---
863
+
864
+ ## Relation naming rules
865
+
866
+ This is the most opinionated piece of the SDK. Understanding the rules up front saves a lot of "what's this relation called?" round-trips.
867
+
868
+ ### Outbound relations - named after the FK column
869
+
870
+ When **this entity holds the FK**, the relation is named after the column with the trailing `_id` / `Id` stripped:
871
+
872
+ | FK column | Friendly name | Rationale |
873
+ | --- | --- | --- |
874
+ | `user_id` | `user` | Obvious |
875
+ | `kyc_id` | `kyc` | Matches the target table in simple cases |
876
+ | `created_by_user_id` | `created_by_user` | ORM pattern (`createdByUserId` in camelCase also works) |
877
+ | `primary_contact_id` | `primary_contact` | Column name matters more than target table name |
878
+ | Composite `[tenant_id, user_id]` | `tenant_user` | Each stripped, joined with `_` |
879
+ | Just `id` (rare) | (falls back to target entity name) | |
880
+
881
+ The column's **semantic name** wins over the target table's name. If you have `customers.primary_contact_id` pointing at a `users` table, the relation is `primary_contact`, not `users`. This mirrors how Prisma and TypeORM name their relations.
882
+
883
+ **Multiple FKs to the same target** are automatically distinct because their columns are different:
884
+
885
+ ```ts
886
+ // posts.created_by_id → users.id relation: 'created_by'
887
+ // posts.modified_by_id → users.id relation: 'modified_by'
888
+
889
+ await root.public.posts
890
+ .include('created_by', (u) => u.select('id', 'email'))
891
+ .include('modified_by', (u) => u.select('id', 'email'))
892
+ .findMany();
893
+ ```
894
+
895
+ ### Inbound relations - named after the source entity
896
+
897
+ When **another entity points at this one**, the relation uses the source entity's bare name:
898
+
899
+ | Inbound source | Friendly name |
900
+ | --- | --- |
901
+ | `bank_details.customer_id → customers.id` | `bank_details` (on customers) |
902
+ | `orders.user_id → users.id` | `orders` (on users) |
903
+ | `sessions.user_id → users.id` | `sessions` (on users) |
904
+
905
+ ### Collisions - disambiguation by source column
906
+
907
+ When two inbound FKs come from the **same source table** (e.g. `orders.created_by_id` and `orders.modified_by_id`, both pointing at `users`), the primary name goes to the first one and subsequent ones get `<source>_by_<stripped-column>`:
908
+
909
+ ```ts
910
+ // users receives two inbounds from orders:
911
+ // orders.created_by_id → friendly: 'orders'
912
+ // orders.modified_by_id → friendly: 'orders_by_modified_by'
913
+
914
+ await root.identity.users
915
+ .include('orders', ...)
916
+ .include('orders_by_modified_by', ...)
917
+ .findMany();
918
+ ```
919
+
920
+ ### Self-references - `parent` / `children`
921
+
922
+ A self-referential FK (both sides of the relationship are the same entity) uses the conventional `'parent'` (outbound) and `'children'` (inbound) names instead of colliding on the entity's own name:
923
+
924
+ ```ts
925
+ // catalog.categories has a self-ref FK (parent_id → id).
926
+ await root.catalog.categories
927
+ .include('parent', (p) => p.select('id', 'name'))
928
+ .include('children', (c) => c.select('id', 'name'))
929
+ .findMany();
930
+ ```
931
+
932
+ ### Ultimate fallback
933
+
934
+ If every other strategy still collides (pathological schema), the resolver falls back to the raw FK constraint name with a numeric suffix if needed. You can type the relation using that constraint name - ugly, but always addressable.
935
+
936
+ ---
937
+
938
+ ## Codegen output layout
939
+
940
+ Everything codegen writes lives under `./biref/` by default:
941
+
942
+ ```
943
+ biref/
944
+ ├── biref.schema.ts ← single-file mode (or .ts re-exports below)
945
+ ├── biref.schema.overrides.ts ← scaffolded once, never regenerated
946
+ └── (split mode)
947
+ ├── index.ts ← re-exports every per-entity interface + BirefSchema
948
+ ├── identity/
949
+ │ ├── users.ts ← export interface IdentityUsers { ... }
950
+ │ └── accounts.ts
951
+ ├── catalog/
952
+ │ └── products.ts
953
+ └── commerce/
954
+ └── orders.ts
955
+ ```
956
+
957
+ **When to use each mode.**
958
+
959
+ | Mode | Use when | Import path |
960
+ | --- | --- | --- |
961
+ | Single file | Small to medium schemas, one file is easier to grep | `./biref/biref.schema` |
962
+ | Split | Dozens of tables, you want per-table diffs in git | `./biref` (resolves to `./biref/index.ts`) |
963
+
964
+ Both modes produce the same `BirefSchema` type and work identically with `biref.query<BirefSchema>(model)`. Pick whichever is easier to browse in your editor.
965
+
966
+ **Regeneration contract.**
967
+
968
+ - Every run overwrites every generated file deterministically.
969
+ - `biref.schema.overrides.ts` is **only** written on first run (or if you deleted it). Your jsonb typings survive forever.
970
+ - `--no-overwrite` refuses to touch existing generated files - useful for CI drift checks where you want the build to fail if the schema's out of date.
971
+
972
+ ---
973
+
974
+ ## Adapters
975
+
976
+ Each adapter bundles an `Introspector` (reads the store), a `QueryEngine<TCommand>` (builds parameterized queries), a `RawQueryRunner` (executes them against the driver), and a `RecordParser` (coerces rows). Wiring is uniform: `someAdapter.create(client)`.
977
+
978
+ ### Postgres
979
+
980
+ **Status:** Shipping.
981
+
982
+ Reads `pg_catalog` and covers tables and partitioned tables in a single round trip of parallel queries (tables, columns, primary keys, foreign keys, indexes, constraints).
983
+
984
+ **Wiring:**
985
+
986
+ ```ts
987
+ import pg from 'pg';
988
+ import { Biref, postgresAdapter } from '@biref/scanner';
989
+
990
+ const client = new pg.Client({ connectionString: 'postgres://localhost/mydb' });
991
+ await client.connect();
992
+
993
+ const biref = Biref.builder()
994
+ .withAdapter(postgresAdapter.create(client))
995
+ .build();
996
+ ```
997
+
998
+ The adapter accepts any client that satisfies the structural `PostgresClient` interface - `pg.Client`, `pg.Pool`, or any compatible library. The SDK never imports `pg` itself.
999
+
1000
+ **URL schemes:** `postgres://`, `postgresql://`.
1001
+
1002
+ **Feature coverage:**
1003
+
1004
+ | Area | Supported |
1005
+ | --- | --- |
1006
+ | Namespaces (schemas) | ✅ multiple per scan, cross-schema FKs, `'all'` sentinel |
1007
+ | Tables | ✅ regular and partitioned |
1008
+ | Columns | ✅ nullable, default, description (Postgres COMMENT) |
1009
+ | Primary keys | ✅ single and composite, declaration order preserved |
1010
+ | Foreign keys | ✅ single, composite, self-referential, cross-schema, every `ON UPDATE` / `ON DELETE` action |
1011
+ | Unique / check / exclusion constraints | ✅ with full definition |
1012
+ | Indexes | ✅ btree, hash, gin, gist, brin, spgist (partial + unique detected) |
1013
+ | Enums | ✅ including element detection on `enum[]` columns |
1014
+ | Arrays | ✅ all element types, enum element types, multi-dimensional |
1015
+ | Generated columns | ✅ |
1016
+ | Table / column comments | ✅ |
1017
+ | Typed query builder | ✅ `.query`, `.findMany`, `.findFirst`, `.include('*')`, `.select('*')` |
1018
+
1019
+ **Type coverage:** every Postgres built-in mapped to a `FieldTypeCategory`. Types with no paradigm-neutral home (`interval`, `inet`, `cidr`, `macaddr`, bit strings, geometric types, text search, range types, `xml`) are tagged `unknown` with the native type string preserved, so callers can branch on it explicitly.
1020
+
1021
+ **Parser:** `PostgresRecordParser`. Handles `int8` / `bigint` → JS `bigint`, `json` / `jsonb` strings → parsed objects, and falls back to `DefaultRecordParser` for everything else.
1022
+
1023
+ ---
1024
+
1025
+ ## API reference
1026
+
1027
+ ### Facade
1028
+
1029
+ | Symbol | Purpose |
1030
+ | --- | --- |
1031
+ | `Biref.builder()` | Returns a fluent `BirefBuilder`. |
1032
+ | `BirefBuilder.withAdapter(adapter)` | Registers an adapter. |
1033
+ | `BirefBuilder.withAdapters(...adapters)` | Registers multiple adapters at once. |
1034
+ | `BirefBuilder.build()` | Materializes a `Biref` facade. |
1035
+ | `Biref.scan()` / `.scan(options)` / `.scan(name, options?)` | Introspects a data store. |
1036
+ | `Biref.scanByUrl(url, options?)` | Introspects via the adapter whose URL scheme matches. |
1037
+ | `Biref.query<Schema>(model, adapterName?)` | Returns a typed namespace proxy over `model`. Omit the generic for the untyped fallback. |
1038
+ | `Biref.adapters` | Direct access to the underlying `AdapterRegistry`. |
1039
+
1040
+ ### Typed query builder
1041
+
1042
+ | Symbol | Purpose |
1043
+ | --- | --- |
1044
+ | `TypedChain<S, Ns, E, Sel, Inc>` | Narrowed fluent interface exposed via the proxy. |
1045
+ | `TypedQueryRoot<Schema>` | Two-layer typed root returned from `biref.query<Schema>(model)`. |
1046
+ | `UntypedQueryRoot` | Dynamic fallback for `biref.query(model)` without a generic. |
1047
+ | `ChainBuilder` | Runtime class behind the typed interface. Cast through for advanced use. |
1048
+ | `QueryPlan`, `QueryInclude` | Plan tree produced by the builder. |
1049
+ | `QueryPlanExecutor` | Core driver that walks a plan tree via `engine` + `runner` + `parser`. |
1050
+ | `RawQueryRunner` | Port: executes a `BuiltQuery` against an adapter's client. |
1051
+
1052
+ ### Type-level helpers (consumed by generated schemas)
1053
+
1054
+ | Symbol | Purpose |
1055
+ | --- | --- |
1056
+ | `BirefSchemaShape` | Loose constraint for schemas passed to `biref.query<Schema>`. |
1057
+ | `SchemaFieldDescriptor`, `SchemaRelationDescriptor`, `SchemaEntityDescriptor`, `SchemaNamespaceDescriptor` | Compile-time descriptors emitted by codegen. |
1058
+ | `ApplySchemaOverrides<Raw, Overrides>`, `BirefOverridesShape` | Deep-merge user overrides onto a generated schema. |
1059
+ | `FieldsOf`, `TypeOf`, `CategoryOf`, `DefaultSelect`, `PickSelect`, `HydratedRow`, `Selection`, `IncludeMap`, `RelationsOf` | Row/selection narrowing helpers. |
1060
+ | `OpsFor`, `ValueFor`, `NullaryOps` | `where`-operator narrowing by field category. |
1061
+ | `RelationsOfEntity`, `TargetNs`, `TargetE`, `CardinalityOf`, `Split` | Relation / include narrowing. |
1062
+ | `CategoryOfField`, `WrapForCardinality` | Shared helpers used inside `TypedChain`. |
1063
+
1064
+ ### Codegen
1065
+
1066
+ | Symbol | Purpose |
1067
+ | --- | --- |
1068
+ | `generateSchema(model)` | Pure function: emits the single-file schema text for a `DataModel`. |
1069
+ | `generateSchemaFiles(model)` | Pure function: emits the split-mode file list for a `DataModel`. |
1070
+ | `overridesScaffold()` | Returns the first-run `biref.schema.overrides.ts` template. |
1071
+ | `tsTypeFor(fieldType, nullable)` | Maps a `FieldType` to a TS type literal string. |
1072
+ | `biref gen --url … [--split] [--all-namespaces]` | CLI that connects, scans, and writes `./biref/` (+ overrides scaffold on first run). |
1073
+
1074
+ ### Domain
1075
+
1076
+ | Symbol | Purpose |
1077
+ | --- | --- |
1078
+ | `DataModel`, `Entity`, `Field`, `FieldType`, `FieldTypeCategory` | Paradigm-neutral schema model. |
1079
+ | `Relationship`, `RelationshipDirection`, `Reference`, `EntityRef`, `ReferentialAction` | Bidirectional relationship model. |
1080
+ | `Constraint`, `ConstraintKind`, `Index`, `IndexKind` | Unique, check, exclusion, custom, and index kinds. |
1081
+ | `EngineKind` | Paradigm discriminator. |
1082
+ | `QuerySpec`, `Filter`, `FilterOperator`, `OrderBy`, `BuiltQuery`, `BuiltQueryMetadata` | Low-level declarative query types used internally by the builder. |
1083
+
1084
+ ### Parsers and formatters
1085
+
1086
+ | Symbol | Purpose |
1087
+ | --- | --- |
1088
+ | `RecordParser`, `ParsedRecord`, `ParsedValue` | Parser contract and output shape. |
1089
+ | `DefaultRecordParser`, `PostgresRecordParser` | Generic + adapter-aware parsers. |
1090
+ | `Formatter`, `SerializeOptions`, `JsonFormatter`, `CsvFormatter`, `RawFormatter` | Serialization for parsed rows (dumps, exports). |
1091
+
1092
+ ### Adapter authoring
1093
+
1094
+ Full guide in [CONTRIBUTING.md](./CONTRIBUTING.md). Public ports:
1095
+
1096
+ | Symbol | Purpose |
1097
+ | --- | --- |
1098
+ | `Introspector`, `IntrospectOptions`, `QueryEngine<TCommand>`, `RawQueryRunner`, `AdapterFactory<TClient, TName>` | Ports a new adapter must implement. |
1099
+ | `Adapter<TName>`, `AdapterName`, `KnownAdapterName`, `AdapterRegistry` | Generic adapter shape and the registry that holds them. |
1100
+
1101
+ ---
1102
+
1103
+ ## Contributing
1104
+
1105
+ Architecture notes, repository layout, development workflow, and tooling conventions live in **[CONTRIBUTING.md](./CONTRIBUTING.md)**. PRs welcome.
1106
+
1107
+ ## License
1108
+
1109
+ MIT.