Files
dbtype/docs/architecture/elements.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

12 KiB

status, last_updated
status last_updated
stable 2026-05-23

Elements: UJSX Element Definitions

The UJSX element types, their props, function components, and how they compose.

Overview

dbtype elements are UJSX elements (h() calls or JSX) with a constrained set of tags: table, column, index, fk. Each element carries typed props that encode both validation metadata (for TypeBox) and database metadata (for Drizzle rendering).

Element Types

<table>

The top-level schema element. Contains <column>, <index>, and <fk> children.

Prop Type Required Description
name string yes Table name in the database

Children are column, index, and foreign key elements. Function component children that return column elements are transparent (their output is used, not the component itself).

<column>

A single column definition within a table.

Prop Type Required Description
name string yes Column name
type DbColumnType yes Column type vocabulary
notNull boolean no Column is NOT NULL
primaryKey boolean no Column is primary key
unique boolean no Column has UNIQUE constraint
default DbDefault | unknown no Symbolic default ('now', 'uuid', 'autoincrement', 'current_timestamp'), a literal value (string, number, boolean), or a Drizzle sql template expression. Symbolic defaults are resolved by the host to dialect-specific SQL. Literal values use .default(value). SQL expressions use .default(sql\...`)`.
references string no FK target table name (shorthand — references the target table's primary key). Normalized to an <fk> entry internally by extractTable(). For composite FKs or FKs with ON DELETE/ON UPDATE, use the <fk> element instead.
format string no TypeBox format annotation (uuid, email, etc.)
mode 'json' | 'text' no Storage mode for compound types. 'json' stores the value as JSON text (SQLite/MySQL) or JSONB (PG). 'text' stores as plain text. Only meaningful for type: 'json', type: 'array', and type: 'object'. Defaults to 'json' for those types.
values string[] no Enum values (for type: 'enum')
length number no Max length (for varchar)
precision number no Numeric precision
scale number no Numeric scale
postgres PgColumnOpts no PG-specific overrides
sqlite SqliteColumnOpts no SQLite-specific overrides
mysql MySqlColumnOpts no MySQL-specific overrides
inner TSchema no Override the auto-generated TypeBox schema. The host ignores this — it's purely for validation. When provided, extractTable() uses this schema directly instead of calling colToTypeBox().

Storage Mode

The mode prop controls how compound types (json, array, object) are stored in the database:

  • mode: 'json' (default for json/array/object) — stored as JSON text in SQLite/MySQL, JSONB in PG
  • mode: 'text' — stored as plain text, useful for columns that need JSON validation but plain-text storage

For scalar types, mode has no effect. The host maps mode: 'json' to the appropriate dialect storage (e.g., text({ mode: 'json' }) in SQLite, jsonb() in PG).

Default Values

Column defaults support three categories:

Category Example Drizzle Output
Symbolic default: 'now' .default(sql\now()`)(PG) /.default(sql`(strftime('%s', 'now'))`)` (SQLite)
Symbolic default: 'uuid' .defaultRandom() (PG) / .$defaultFn(() => crypto.randomUUID()) (SQLite)
Symbolic default: 'autoincrement' Implicit on INTEGER PRIMARY KEY (SQLite) / serial() type (PG)
Literal default: true .default(true)
Literal default: 0 .default(0)
SQL expression default: sql\CURRENT_TIMESTAMP`` .default(sql\CURRENT_TIMESTAMP`)`
JS function default: () => crypto.randomUUID() .$defaultFn(() => crypto.randomUUID())

Symbolic defaults are translated by the host. Literal and SQL expression defaults pass through directly. JS function defaults use Drizzle's .$defaultFn().

<index>

An index definition on a table.

Prop Type Required Description
name string yes Index name
columns string[] yes Column names in the index
unique boolean no Whether the index is unique

<fk>

A foreign key constraint.

Prop Type Required Description
columns string[] yes Local column names
references string yes Target table name
foreignColumns string[] yes Target column names
onDelete 'cascade' | 'set null' | 'restrict' | 'no action' no ON DELETE action
onUpdate 'cascade' | 'set null' | 'restrict' | 'no action' no ON UPDATE action

Column Type Vocabulary

DbColumnType is the cross-dialect type vocabulary. Each value maps to a specific Drizzle column builder per dialect:

type DbColumnType =
  | 'uuid'
  | 'string'
  | 'text'
  | 'varchar'
  | 'integer'
  | 'bigint'
  | 'boolean'
  | 'timestamp'
  | 'real'
  | 'numeric'
  | 'enum'
  | 'json'
  | 'array'
  | 'object'

The mapping to TypeBox and Drizzle types is defined in the host, but the element tree uses DbColumnType as the universal vocabulary.

Symbolic Defaults

type DbDefault =
  | 'now'            // Current timestamp (dialect-specific SQL)
  | 'uuid'           // UUID generation (dialect-specific mechanism)
  | 'autoincrement'  // Auto-incrementing integer
  | 'current_timestamp' // CURRENT_TIMESTAMP
  | unknown          // Literal value or SQL expression

Function Components

Reusable column groups defined as UJSX components:

// Common ID column
const IdColumn = createComponent('IdColumn', () =>
  h('column', { name: 'id', type: 'uuid', primaryKey: true, default: 'uuid' })
)

// Audit timestamp columns
const AuditColumns = createComponent('AuditColumns', () => [
  h('column', { name: 'createdAt', type: 'timestamp', notNull: true, default: 'now' }),
  h('column', { name: 'updatedAt', type: 'timestamp', notNull: true, default: 'now' }),
])

// Usage
const UsersEl = h('table', { name: 'users' },
  h(IdColumn, {}),
  h('column', { name: 'name', type: 'string', notNull: true }),
  h(AuditColumns, {}),
)

Function components are transparent — the host never sees them. The createInstance method only receives resolved intrinsic elements (column, table).

Tree Walking

The extractTable() function walks an element tree, resolving function components, and produces:

  1. TypeBox schema: Type.Object({ ... }) for the module entry
  2. Column metadata: Record<string, ColumnProps> for Drizzle rendering
  3. Table metadata: { name, columns, indexes, foreignKeys } for the host
function extractTable(el: UElement): TableMeta

For each column, extractTable:

  • Calls colToTypeBox(type, props) to produce the inner TypeBox schema
  • Stores the full props object as metadata for the host
  • Tracks primaryKey, default, notNull, references for schema derivation (insert, update)

Constraints

  • Column elements must have name and type props — these are required for both TypeBox schema construction and Drizzle rendering
  • Function components must return column elements (or arrays of column elements) — returning other element types inside a table is undefined
  • The references prop is metadata-only — it's not part of the TypeBox schema. It informs the host about FK constraints and the repo adapter about relation structure
  • references is a shorthand — a <column references="users"> prop is syntactic sugar for {columns: [column_name], references: 'users', foreignColumns: ['id']} (targeting the referenced table's primary key by convention). Internally, extractTable() normalizes all references props into FkMeta entries alongside <fk> elements. When both references on a column and an explicit <fk> referencing the same column exist, the explicit <fk> takes precedence.
  • default values can be symbolic strings or literals — symbolic defaults (now, uuid, autoincrement) are resolved by the host; literal values pass through directly
  • The inner prop overrides the auto-generated TypeBox schema — when inner is provided on a column element, extractTable() uses it directly without calling colToTypeBox(). The host ignores inner entirely — database rendering uses only type and DB metadata props. This is the escape hatch for validation constraints that don't have column prop shortcuts (e.g., minItems, maximum, pattern). See ADR-007.

Type Definitions

/** Column metadata extracted from a <column> element by extractTable() */
interface ColumnMeta {
  name: string
  type: DbColumnType
  notNull?: boolean
  primaryKey?: boolean
  unique?: boolean
  default?: DbDefault | unknown
  references?: string              // Shorthand: FK target table name
  format?: string                  // TypeBox format annotation
  mode?: 'json' | 'text'           // Storage mode for compound types
  values?: string[]                // Enum values (for type: 'enum')
  length?: number                  // Max length (for varchar)
  precision?: number               // Numeric precision
  scale?: number                   // Numeric scale
  postgres?: PgColumnOpts          // PG-specific overrides
  sqlite?: SqliteColumnOpts        // SQLite-specific overrides
  mysql?: MySqlColumnOpts          // MySQL-specific overrides
  inner?: TSchema                  // Override TypeBox schema (escape hatch)
}

/** Index metadata extracted from an <index> element */
interface IndexMeta {
  name: string
  columns: string[]
  unique?: boolean
}

/** Foreign key metadata extracted from <fk> elements and references props */
interface FkMeta {
  columns: string[]                // Local column names
  references: string              // Target table name
  foreignColumns: string[]        // Target column names
  onDelete?: 'cascade' | 'set null' | 'restrict' | 'no action'
  onUpdate?: 'cascade' | 'set null' | 'restrict' | 'no action'
}

/** Table metadata result from extractTable() */
interface TableMeta {
  name: string
  schema: TObject                 // TypeBox schema for Type.Module entry
  columns: Record<string, ColumnMeta>
  indexes: IndexMeta[]
  foreignKeys: FkMeta[]
}

Open Questions

  1. Should column elements support inner TypeBox schemas? Resolved — Yes. ADR-007: flat props for common cases, inner as escape hatch for custom validation. The inner prop is now documented in the column props table.

  2. Should <table> accept extraConfig props? Drizzle tables accept a third callback argument for indexes and unique constraints defined in terms of column references. How does this map to element props? (See OQ-11)

  3. Should we support JSX file extensions? Resolved — ADR-011. JSX/TSX is supported as an ergonomic authoring layer. JSX desugars to h() calls via jsxImportSource: '@alkdev/ujsx'. The h() API remains the universal fallback. TSConfig must set jsx: 'react-jsx' and jsxImportSource: '@alkdev/ujsx'.

References

  • UJSX element factory: @alkdev/ujsx/src/core/h.ts
  • UJSX HostConfig: @alkdev/ujsx/src/host/config.ts
  • Probe element construction: scripts/probe-e2e.ts
  • Research: docs/research/architecture.md (DbTypeBuilder section)