--- status: draft last_updated: 2026-05-22 --- # Schema: Type.Module as the Schema Bundle How dbtype uses `Type.Module` to store all table schemas, relations, and derived schemas in a single namespace with automatic `Type.Ref` resolution. ## 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 The element tree (``, ``) is walked to extract a `Record` map, then compiled into a module: ``` UJSX elements → extractTable() → { name, schema, columns } → defs map → Type.Module(defs) ``` Each `` element produces a TypeBox type based on its `type` prop: | Column Type Prop | TypeBox Schema | |-----------------|---------------| | `uuid` | `Type.String({ format: 'uuid' })` | | `string` | `Type.String()` | | `integer` | `Type.Integer()` | | `boolean` | `Type.Boolean()` | | `timestamp` | `Type.Number()` | | `enum` | `Type.Union(values.map(v => Type.Literal(v)))` | ### Incremental Construction The defs map is a plain `Record` — it can be built incrementally, mutated, and extended before compilation: ```typescript const defs: Record = {} // 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. ## Schema Derivation ### Select Schema The module entry as-is is the select schema. Every column is present, nullable columns become `Type.Union([innerType, Type.Null()])`. ### Insert Schema Derive from the table entry by: - Removing auto-generated primary keys (columns with `primaryKey: true` and `default` set) - Making nullable columns and columns with defaults `Type.Optional` - Keeping required (`notNull` without default) columns mandatory Implemented by adding a computed entry to the module: ```typescript defs.InsertUsers = Type.Object({ name: Type.String(), email: Type.String(), // id, createdAt, updatedAt omitted (auto-generated) }) ``` ### Update Schema All columns optional. Use `Type.Partial(Type.Ref('TableName'))`: ```typescript defs.UpdateUsers = Type.Partial(Type.Ref('Users')) ``` ### Filter Schema 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 (``), 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)? 2. **Derived schemas in the module or separate?** Insert/update schemas can be added as module entries (`InsertUsers`, `UpdateUsers`) or extracted by walking the module schema. Which is cleaner for the repo adapter? 3. **Should the module support multiple databases?** One module per database, or one module with all tables across all databases? Probably one per database namespace. ## References - TypeBox Module API: `@alkdev/typebox` source — `type/module/module.ts`, `type/ref/ref.ts` - Probe validation: `scripts/probe-e2e.ts` - Research: `docs/research/architecture.md`