Files
dbtype/docs/architecture/hosts.md
glm-5.1 98764086f4 docs: resolve all open architecture questions with ADRs 009-014
Resolve OQ-01 through OQ-10 with formal ADRs and update all architecture
docs to reference decisions. Add two new tracked questions (OQ-11, OQ-12)
surfaced during review.

ADR-009: Keep FooRelations naming convention for relation entries
ADR-010: One module per database, include derived schemas by default
ADR-011: Support JSX/TSX as ergonomic authoring layer
ADR-012: Always .returning() with graceful fallback per dialect
ADR-013: Adapter generates relations from module entries (no <relation> element)
ADR-014: Leverage drizzle-kit for migrations, no native migration generator

Also upgrades elements.md, hosts.md, repo-adapter.md status to stable,
clarifies OQ-06 as design clarification, and specifies MySQL .returning()
detection mechanism in ADR-012.
2026-05-23 12:47:55 +00:00

7.5 KiB

status, last_updated
status last_updated
stable 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

interface DbtypeRootCtx<Dialect extends string> {
  dialect: Dialect
  tables: Record<string, DbtypeTableInst>
}

interface DbtypeTableInst {
  name: string
  columns: Record<string, any>  // Drizzle column builders
  constraints: any[]
  result?: any  // The final drizzle table
}

interface DbtypeColumnInst {
  name: string
  columnType: string
  builder: any  // Drizzle column builder
  dbMeta: Record<string, any>
}

interface PgRootCtx {
  dialect: 'pg'
  tables: Record<string, PgTable>
  enums: Record<string, PgEnum>  // Accumulated during render
}

interface SqliteRootCtx {
  dialect: 'sqlite'
  tables: Record<string, SqliteTable>
}

interface MySqlRootCtx {
  dialect: 'mysql'
  tables: Record<string, MySqlTable>
}

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 for the full default value specification.

Common Column Components

Function components like IdColumn and AuditColumns compose into any table. See elements.md for component definitions.

Rendering Pipeline

  1. Walk elements: The host's createInstance method receives each <table>, <column>, <index>, <fk> 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
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 hostdefault="now" becomes sql\(strftime('%s', 'now'))`on SQLite andsql`now()`` on PG
  • PG enums are accumulated in context — when a <column type="enum" values={[...]}> 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.
  • 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 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<string, SqliteTable> }, PG uses { dialect: 'pg', enums: Record<string, PgEnum>, tables: Record<string, PgTable> }.

  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.

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