--- status: stable last_updated: 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 ### `` The top-level schema element. Contains ``, ``, and `` 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). ### `` 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 `` entry internally by extractTable(). For composite FKs or FKs with ON DELETE/ON UPDATE, use the `` 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()`. ### `` 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 | ### `` 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: ```typescript 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 ```typescript 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: ```typescript // 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` for Drizzle rendering 3. **Table metadata**: `{ name, columns, indexes, foreignKeys }` for the host ```typescript 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 `` 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 `` elements. When both `references` on a column and an explicit `` referencing the same column exist, the explicit `` 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](decisions/007-inner-escape-hatch.md). ## Type Definitions ```typescript /** Column metadata extracted from a 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 element */ interface IndexMeta { name: string columns: string[] unique?: boolean } /** Foreign key metadata extracted from 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 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 `
` 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](open-questions.md)) 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)