ADR-015: <table> accepts extraConfig prop mirroring Drizzle's third callback argument for composite unique constraints and other column-reference-based features. ADR-016: Adapter accepts Type.Module compiled bundle (not individual schemas). M.Import() handles ref resolution automatically, eliminating potential complications with separate schema wiring. Update FromDbTypeConfig interface to use module instead of schema, update element types table with extraConfig prop.
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 |
extraConfig |
(columns: Record<string, any>) => Record<string, any> |
no | Drizzle third-argument callback. Receives rendered column builders, returns an object for composite unique constraints, partial indexes, and other column-reference-based features. See ADR-015. |
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. -
ShouldResolved — ADR-015. Yes — the<table>acceptextraConfigprops?<table>element accepts anextraConfigprop that mirrors Drizzle's third callback argument. It receives the rendered column builders and returns an object for composite unique constraints, partial indexes, and other column-reference-based features. Consistent with theinnerescape hatch pattern on<column>. -
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)