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.
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 PGmode: '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:
- TypeBox schema:
Type.Object({ ... })for the module entry - Column metadata:
Record<string, ColumnProps>for Drizzle rendering - 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,referencesfor schema derivation (insert, update)
Constraints
- Column elements must have
nameandtypeprops — 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
referencesprop 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 referencesis 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 allreferencesprops intoFkMetaentries alongside<fk>elements. When bothreferenceson a column and an explicit<fk>referencing the same column exist, the explicit<fk>takes precedence.defaultvalues can be symbolic strings or literals — symbolic defaults (now,uuid,autoincrement) are resolved by the host; literal values pass through directly- The
innerprop overrides the auto-generated TypeBox schema — wheninneris provided on a column element,extractTable()uses it directly without callingcolToTypeBox(). The host ignoresinnerentirely — database rendering uses onlytypeand 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
-
Should column elements supportResolved — Yes. ADR-007: flat props for common cases,innerTypeBox schemas?inneras escape hatch for custom validation. Theinnerprop is now documented in the column props table. -
Should
<table>acceptextraConfigprops? 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) -
Should we support JSX file extensions?Resolved — ADR-011. JSX/TSX is supported as an ergonomic authoring layer. JSX desugars toh()calls viajsxImportSource: '@alkdev/ujsx'. Theh()API remains the universal fallback. TSConfig must setjsx: 'react-jsx'andjsxImportSource: '@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)