--- status: stable last_updated: 2026-05-23 --- # Hosts: Dialect Rendering via HostConfig How dbtype renders UJSX element trees to Drizzle table definitions for each supported database dialect. ## Overview Each database dialect (SQLite, PostgreSQL, MySQL) is a `HostConfig` implementation. The same UJSX element tree renders to `sqliteTable`, `pgTable`, or `mysqlTable` depending on which host is chosen. The host translates column type props, symbolic defaults, and constraints to the appropriate Drizzle column builder methods. ## HostConfig Interface ```typescript interface DbtypeRootCtx { dialect: Dialect tables: Record } interface DbtypeTableInst { name: string columns: Record // Drizzle column builders constraints: any[] result?: any // The final drizzle table } interface DbtypeColumnInst { name: string columnType: string builder: any // Drizzle column builder dbMeta: Record } interface PgRootCtx { dialect: 'pg' tables: Record enums: Record // Accumulated during render } interface SqliteRootCtx { dialect: 'sqlite' tables: Record } interface MySqlRootCtx { dialect: 'mysql' tables: Record } ``` The `HostConfig` generic parameters: - `TTag`: `"table" | "column" | "index" | "fk"` — the allowed element types - `TInstance`: The union of table and column instance types for this dialect - `TRootCtx`: The dialect-specific root context ## Column Type Mapping | DbColumnType | SQLite | PostgreSQL | MySQL | TypeBox | Notes | |---|---|---|---|---|---| | `uuid` | `text(name).primaryKey().$defaultFn(crypto.randomUUID)` | `uuid(name).defaultRandom()` / `.primaryKey()` | `varchar(name, { length: 36 })` | `Type.String({ format: 'uuid' })` | No native UUID in SQLite/MySQL | | `string` | `text(name)` | `text(name)` | `text(name)` | `Type.String()` | General unbounded text; for bounded strings use `varchar` | | `text` | `text(name)` | `text(name)` | `text(name)` | `Type.String()` | Explicitly unbounded text content | | `varchar` | `text(name)` | `varchar(name, { length })` | `varchar(name, { length })` | `Type.String({ maxLength })` | Maps to `text()` in SQLite (no varchar); PG/MySQL have native varchar with length | | `integer` | `integer(name)` | `integer(name)` | `int(name)` | `Type.Integer()` | | | `bigint` | `blob(name, { mode: 'bigint' })` | `bigint(name)` | `bigint(name)` | custom `TypeBigInt()` | SQLite uses blob with bigint mode; TypeBox has no standard BigInt type, needs annotation | | `boolean` | `integer(name, { mode: 'boolean' })` | `boolean(name)` | `boolean(name)` / `tinyint(name)` | `Type.Boolean()` | SQLite has no native boolean type | | `timestamp` | `integer(name, { mode: 'timestamp' })` | `timestamptz(name, { withTimezone: true })` | `timestamp(name)` | `Type.Number()` | | | `real` | `real(name)` | `real(name)` | `float(name)` | `Type.Number()` | SQLite/PG use `real()`; MySQL uses `float()`; all map to `Type.Number()` | | `numeric` | `numeric(name)` | `numeric(name, { precision, scale })` | `decimal(name, { precision, scale })` | `Type.String()` | Stored as string; precision/scale from column props | | `enum` | `text(name, { enum: values })` | `pgEnum(name, values)` | `mysqlEnum(name, values)` | `Type.Union([...])` | SQLite has no native enum; PG requires pre-declaration | | `json` | `text(name, { mode: 'json' })` | `jsonb(name)` | `json(name)` | `Type.Any()` | Stored as JSON text; PG uses `jsonb` by default | | `array` | N/A (not supported) | `column(name).array()` | N/A (not supported) | `Type.Array(inner)` | Only PG supports native arrays; other dialects should use json mode; requires special handling | | `object` | `text(name, { mode: 'json' })` | `jsonb(name)` | `json(name)` | `Type.Object(properties)` | Always stored as JSON; same as `json` type but with structured inner schema | ## Symbolic Defaults Symbolic defaults (`'now'`, `'uuid'`, `'autoincrement'`, `'current_timestamp'`) are resolved to dialect-specific SQL or JS functions by the host. See [elements.md](elements.md#default-values) for the full default value specification. ## Common Column Components Function components like `IdColumn` and `AuditColumns` compose into any table. See [elements.md](elements.md#function-components) for component definitions. ## Rendering Pipeline 1. **Walk elements**: The host's `createInstance` method receives each ``, ``, ``, `` element 2. **Build columns**: `createInstance('column', props, ctx)` maps `props.type` to the dialect-specific Drizzle builder, applying `notNull`, `primaryKey`, `unique`, `default` constraints 3. **Assemble tables**: `appendChild(tableInst, columnInst, ctx)` attaches column builders to the table's columns record 4. **Finalize**: `finalizeInstance(tableInst, ctx)` calls `sqliteTable(name, columns)` (or `pgTable`, `mysqlTable`) and stores the result on the table instance ```typescript const root = createRoot(sqliteHost, {}) root.render(UsersEl) // root.ctx.tables: { users: drizzleSqliteTable } — accessible after render // For PG hosts, root.ctx also accumulates auxiliary state like pgEnum declarations ``` ## Constraints - **`createTextInstance` is not supported** — DB schemas are purely structural, no text nodes - **Column type props must be from the `DbColumnType` vocabulary** — hosts map these to dialect-specific builders; unknown types fall back to `text()` - **Symbolic defaults are resolved by the host** — `default="now"` becomes `sql\`(strftime('%s', 'now'))\`` on SQLite and `sql\`now()\`` on PG - **PG enums are accumulated in context** — when a `` is encountered, the PG host registers the enum in `ctx.enums` (using `table_column` naming convention) and references it in the column builder. The consumer includes both `ctx.enums` and `ctx.tables` in their Drizzle schema. See [ADR-008](decisions/008-pg-enum-predeclaration.md). - **A host renders one dialect at a time** — to generate schemas for multiple dialects, render multiple times with different hosts ## Open Questions 1. **[RESOLVED — ADR-008](decisions/008-pg-enum-predeclaration.md) PG enum pre-declaration**: PG requires `pgEnum()` at module scope before tables. The host accumulates enum declarations in `root.ctx.enums` during the render walk, and `finalizeRoot` emits them. This is option A — enum state lives in context alongside tables. 2. **[Resolved] render() return value vs context**: `render()` on the UJSX `Root` returns `void` — the rendered Drizzle tables are collected in `root.ctx.tables`. Context is the right home for this because PG hosts need auxiliary state (enum declarations) alongside the primary output. The context shape varies by dialect: SQLite uses `{ dialect: 'sqlite', tables: Record }`, PG uses `{ dialect: 'pg', enums: Record, tables: Record }`. 3. **`prepareUpdate`/`commitUpdate` for migrations**: The UJSX reconciler's `prepareUpdate`/`commitUpdate` hooks could diff old and new element trees to produce ALTER TABLE statements. This is now deferred indefinitely — ADR-014 establishes that dbtype leverages `drizzle-kit` for migrations rather than implementing its own migration generator. The reconciler architecture remains available if this decision is revisited in the future. See [ADR-014](decisions/014-leverage-drizzle-kit-for-migrations.md). ## References - UJSX HostConfig: `@alkdev/ujsx/src/host/config.ts` - Drizzle column diffs: `docs/research/dizzle-column-diffs.md` - Storage pattern: `@alkdev/storage_sqlite/` - Probe: `scripts/probe-e2e.ts`