docs: resolve architecture open questions, add type definitions, consolidate docs
Architecture review session resolving all high-priority open questions and filling documentation gaps identified during review: Decisions resolved: - OQ-04: Flat props with inner escape hatch for column validation (ADR-007) - OQ-05: PG enum pre-declaration returns enums and tables (ADR-008) - OQ-06: Render results accumulate in root.ctx (resolved in hosts.md) - Column references vs fk: references is shorthand, explicit fk takes precedence (ADR-006) - ADR-001, 002, 003 promoted from Proposed to Accepted (probe-validated) Documentation improvements: - Complete DbColumnType mapping tables for all 14 types across 3 dialects - Define ColumnMeta, TableMeta, IndexMeta, FkMeta types in elements.md - Document inner prop, mode prop, and default prop semantics - Add PgRootCtx, SqliteRootCtx, MySqlRootCtx context types - Consolidate schema.md and module.md (remove duplication) - Add end-to-end pipeline walkthrough to README - Add glossary with 13 terms - Add error handling strategy - Remove duplicate content from hosts.md (cross-ref elements.md)
This commit is contained in:
@@ -1,21 +1,13 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-05-22
|
||||
status: stable
|
||||
last_updated: 2026-05-23
|
||||
---
|
||||
|
||||
# Schema: Type.Module as the Schema Bundle
|
||||
# Schema: Domain Data Inside the Module
|
||||
|
||||
How dbtype uses `Type.Module` to store all table schemas, relations, and derived schemas in a single namespace with automatic `Type.Ref` resolution.
|
||||
What goes into a dbtype `Type.Module` — table schemas, relation entries, derived schemas (insert, update, filter), and the schema derivation semantics. For the mechanical details of how `Type.Module` works (construction, validation, serialization, migration diffing, constraints), see [module.md](module.md).
|
||||
|
||||
## Overview
|
||||
|
||||
The `Type.Module` is the central data structure in dbtype. It holds every table's TypeBox schema, all cross-table relations, and derived schemas (insert, update, select variants) in one flat namespace. `Type.Ref` resolves forward and circular references naturally, eliminating the need for separate relation files or import-order management.
|
||||
|
||||
The module is also the serialization boundary: `JSON.stringify(module.Import('Users'))` produces valid JSON Schema with `$defs`, enabling migration diffing via `Value.Diff`.
|
||||
|
||||
## Construction
|
||||
|
||||
### From Element Tree to Module
|
||||
## From Element Tree to Module
|
||||
|
||||
The element tree (`<table>`, `<column>`) is walked to extract a `Record<string, TSchema>` map, then compiled into a module:
|
||||
|
||||
@@ -34,29 +26,24 @@ Each `<column>` element produces a TypeBox type based on its `type` prop:
|
||||
| `timestamp` | `Type.Number()` |
|
||||
| `enum` | `Type.Union(values.map(v => Type.Literal(v)))` |
|
||||
|
||||
### Incremental Construction
|
||||
For incremental construction patterns and compilation mechanics, see [module.md](module.md).
|
||||
|
||||
The defs map is a plain `Record<string, TSchema>` — it can be built incrementally, mutated, and extended before compilation:
|
||||
## Relations
|
||||
|
||||
Relations are stored as separate entries in the module, using `Type.Ref` to reference other tables:
|
||||
|
||||
```typescript
|
||||
const defs: Record<string, any> = {}
|
||||
|
||||
// Add tables one at a time
|
||||
defs.Users = Type.Object({ id: Type.String({ format: 'uuid' }), name: Type.String() })
|
||||
defs.Tasks = Type.Object({ id: Type.String({ format: 'uuid' }), userId: Type.String({ format: 'uuid' }), title: Type.String() })
|
||||
|
||||
// Add columns to an existing table
|
||||
defs.Users = Type.Object({ ...defs.Users.properties, role: Type.String() })
|
||||
|
||||
// Add relations
|
||||
defs.UsersRelations = Type.Object({ tasks: Type.Array(Type.Ref('Tasks')) })
|
||||
defs.TasksRelations = Type.Object({ user: Type.Ref('Users') })
|
||||
|
||||
// Compile
|
||||
const M = Type.Module(defs)
|
||||
```
|
||||
|
||||
Once compiled, `M.Import(key)` returns a `TImport` schema with the full `$defs` namespace embedded.
|
||||
This gives:
|
||||
- **Type-safe validation**: `Value.Check(M.Import('UsersRelations'), { tasks: [...] })` validates the full nested structure
|
||||
- **No circular import issues**: `Type.Ref` resolves within the module namespace regardless of definition order
|
||||
- **Queryable structure**: The `$defs` map is enumerable — you can find all relations for a table by naming convention
|
||||
- **Drizzle integration**: The repo adapter reads relation entries to generate `relations()` calls for drizzle's relational query builder
|
||||
|
||||
Foreign key metadata lives on the column element's `references` prop (`<column name="userId" type="uuid" references="users" />`), not in the relation entry. Relations describe the "from this side, I see many of those" semantics.
|
||||
|
||||
## Schema Derivation
|
||||
|
||||
@@ -93,64 +80,6 @@ defs.UpdateUsers = Type.Partial(Type.Ref('Users'))
|
||||
|
||||
Per-column comparison operators derived from the column type. Generated by the repo adapter, not the core module.
|
||||
|
||||
## Relations
|
||||
|
||||
Relations are stored as separate entries in the module, using `Type.Ref` to reference other tables:
|
||||
|
||||
```typescript
|
||||
defs.UsersRelations = Type.Object({ tasks: Type.Array(Type.Ref('Tasks')) })
|
||||
defs.TasksRelations = Type.Object({ user: Type.Ref('Users') })
|
||||
```
|
||||
|
||||
This gives:
|
||||
- **Type-safe validation**: `Value.Check(M.Import('UsersRelations'), { tasks: [...] })` validates the full nested structure
|
||||
- **No circular import issues**: `Type.Ref` resolves within the module namespace regardless of definition order
|
||||
- **Queryable structure**: The `$defs` map is enumerable — you can find all relations for a table by naming convention
|
||||
- **Drizzle integration**: The repo adapter reads relation entries to generate `relations()` calls for drizzle's relational query builder
|
||||
|
||||
Foreign key metadata lives on the column element's `references` prop (`<column name="userId" type="uuid" references="users" />`), not in the relation entry. Relations describe the "from this side, I see many of those" semantics.
|
||||
|
||||
## Serialization
|
||||
|
||||
`JSON.stringify(M.Import('TableName'))` produces JSON Schema with `$defs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$defs": {
|
||||
"Users": { "$id": "Users", "type": "object", "properties": { ... } },
|
||||
"Tasks": { "$id": "Tasks", "type": "object", "properties": { ... } },
|
||||
"UsersRelations": { "$id": "UsersRelations", "type": "object", "properties": { "tasks": { "items": { "$ref": "Tasks" }, "type": "array" } } }
|
||||
},
|
||||
"$ref": "Users"
|
||||
}
|
||||
```
|
||||
|
||||
Key properties:
|
||||
- Each `$defs` entry gets an `$id` matching its key name
|
||||
- `Type.Ref` leaves `$ref` pointers (not inlined) — consumers must resolve them
|
||||
- The serialized form is valid JSON Schema
|
||||
- `Value.Diff` produces structural edits between two serialized schemas (useful for migration diffing)
|
||||
|
||||
## Migration Diffing
|
||||
|
||||
```typescript
|
||||
const v1 = JSON.parse(JSON.stringify(M.Import('Users')))
|
||||
// ... add a column to defs.Users ...
|
||||
const v2 = JSON.parse(JSON.stringify(M2.Import('Users')))
|
||||
const edits = Value.Diff(v1, v2)
|
||||
// edits: [{ type: 'insert', path: '/$defs/Users/properties/role', value: { type: 'string' } }, ...]
|
||||
```
|
||||
|
||||
The edits use JSON Pointer paths, which can be translated to `ALTER TABLE ADD COLUMN` statements.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Module keys must be unique** — two tables cannot have the same name in the same module
|
||||
- **`Type.Ref` resolves within the module only** — no cross-module references without `Module.Import`
|
||||
- **`Type.Ref` outside a module has `static: unknown`** — always access via `M.Import(key)` for proper type inference
|
||||
- **Defs map is mutable until compiled** — once passed to `Type.Module`, mutations to the original map don't affect the compiled module
|
||||
- **Format validation requires `FormatRegistry.Set`** — `uuid`, `email`, and other custom formats must be registered before `Value.Check` will enforce them
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Should relation entries use a naming convention?** Currently `UsersRelations` / `TasksRelations`. Is this sufficient, or should relations be structured differently (e.g., a `relations` field on the table entry)?
|
||||
@@ -162,5 +91,6 @@ The edits use JSON Pointer paths, which can be translated to `ALTER TABLE ADD CO
|
||||
## References
|
||||
|
||||
- TypeBox Module API: `@alkdev/typebox` source — `type/module/module.ts`, `type/ref/ref.ts`
|
||||
- Mechanical details: [module.md](module.md)
|
||||
- Probe validation: `scripts/probe-e2e.ts`
|
||||
- Research: `docs/research/architecture.md`
|
||||
Reference in New Issue
Block a user