docs: resolve architecture open questions, add type definitions, consolidate docs

Architecture review session resolving all high-priority open questions and
filling documentation gaps identified during review:

Decisions resolved:
- OQ-04: Flat props with inner escape hatch for column validation (ADR-007)
- OQ-05: PG enum pre-declaration returns enums and tables (ADR-008)
- OQ-06: Render results accumulate in root.ctx (resolved in hosts.md)
- Column references vs fk: references is shorthand, explicit fk takes
  precedence (ADR-006)
- ADR-001, 002, 003 promoted from Proposed to Accepted (probe-validated)

Documentation improvements:
- Complete DbColumnType mapping tables for all 14 types across 3 dialects
- Define ColumnMeta, TableMeta, IndexMeta, FkMeta types in elements.md
- Document inner prop, mode prop, and default prop semantics
- Add PgRootCtx, SqliteRootCtx, MySqlRootCtx context types
- Consolidate schema.md and module.md (remove duplication)
- Add end-to-end pipeline walkthrough to README
- Add glossary with 13 terms
- Add error handling strategy
- Remove duplicate content from hosts.md (cross-ref elements.md)
This commit is contained in:
2026-05-23 12:06:51 +00:00
parent 4644e1b362
commit d4fd67f4d2
12 changed files with 476 additions and 221 deletions

View File

@@ -32,6 +32,22 @@ interface DbtypeColumnInst {
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:
@@ -41,90 +57,43 @@ The `HostConfig` generic parameters:
## Column Type Mapping
### SQLite
| Column Type Prop | Drizzle Builder | Notes |
|-----------------|----------------|-------|
| `uuid` | `text(name).primaryKey().$defaultFn(crypto.randomUUID)` | No native UUID type |
| `string` | `text(name)` | |
| `integer` | `integer(name)` | |
| `boolean` | `integer(name, { mode: 'boolean' })` | |
| `timestamp` | `integer(name, { mode: 'timestamp' })` | |
| `enum` | `text(name, { enum: values })` | SQLite has no native enum |
| `json` | `text(name, { mode: 'json' })` | Stored as JSON text |
### PostgreSQL
| Column Type Prop | Drizzle Builder | Notes |
|-----------------|----------------|-------|
| `uuid` | `uuid(name).defaultRandom()` or `.primaryKey()` | Native UUID |
| `string` | `text(name)` or `varchar(name, n)` | |
| `integer` | `integer(name)` | |
| `boolean` | `boolean(name)` | |
| `timestamp` | `timestamptz(name, { withTimezone: true })` | |
| `enum` | `pgEnum(name, values)` | Requires pre-declaration |
| `json` | `jsonb(name)` | |
### MySQL
| Column Type Prop | Drizzle Builder | Notes |
|-----------------|----------------|-------|
| `uuid` | `varchar(name, { length: 36 })` | No native UUID |
| `string` | `text(name)` or `varchar(name, n)` | |
| `integer` | `int(name)` | |
| `boolean` | `boolean(name)` or `tinyint(name)` | |
| `timestamp` | `timestamp(name)` | |
| `enum` | `mysqlEnum(name, values)` | |
| `json` | `json(name)` | |
| 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
Column props like `default="now"` and `default="uuid"` are translated to dialect-specific SQL or JS functions by the host:
| Symbol | SQLite | PostgreSQL | MySQL |
|--------|--------|------------|-------|
| `now` | `sql\`(strftime('%s', 'now'))\`` | `sql\`now()\`` | `sql\`NOW()\`` |
| `uuid` | `.$defaultFn(() => crypto.randomUUID())` | `.defaultRandom()` | `.$defaultFn(() => crypto.randomUUID())` |
| `autoincrement` | Implicit on `INTEGER PRIMARY KEY` | `serial()` type | `.autoincrement()` |
Symbolic defaults (`'now'`, `'uuid'`, `'autoincrement'`, `'current_timestamp'`) are resolved to dialect-specific SQL or JS functions by the host. See [elements.md](elements.md#default-values) for the full default value specification.
## Common Column Components
Function components that compose into any table:
```typescript
const IdColumn = createComponent('IdColumn', () =>
h('column', { name: 'id', type: 'uuid', primaryKey: true, default: 'uuid' })
)
const AuditColumns = createComponent('AuditColumns', () => [
h('column', { name: 'createdAt', type: 'timestamp', notNull: true, default: 'now' }),
h('column', { name: 'updatedAt', type: 'timestamp', notNull: true, default: 'now' }),
])
```
Used by spreading into a table:
```typescript
const UsersEl = h('table', { name: 'users' },
h(IdColumn, {}),
h('column', { name: 'name', type: 'string', notNull: true }),
h(AuditColumns, {}),
)
```
This replaces the storage_sqlite pattern of `const commonCols = { id: text('id').primaryKey(), ... }` with reusable, dialect-agnostic components.
Function components like `IdColumn` and `AuditColumns` compose into any table. See [elements.md](elements.md#function-components) 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
4. **Finalize**: `finalizeInstance(tableInst, ctx)` calls `sqliteTable(name, columns)` (or `pgTable`, `mysqlTable`) and stores the result on the table instance
```typescript
const root = createRoot(sqliteHost, {})
root.render(UsersEl)
// root's context now contains: { tables: { users: drizzleSqliteTable } }
// root.ctx.tables: { users: drizzleSqliteTable } — accessible after render
// For PG hosts, root.ctx also accumulates auxiliary state like pgEnum declarations
```
## Constraints
@@ -132,16 +101,16 @@ root.render(UsersEl)
- **`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 host** — `default="now"` becomes `sql\`(strftime('%s', 'now'))\`` on SQLite and `sql\`now()\`` on PG
- **PG enums require pre-declaration** — the host must track enum types and emit `pgEnum()` calls before tables that reference them
- **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](decisions/008-pg-enum-predeclaration.md).
- **A host renders one dialect at a time** — to generate schemas for multiple dialects, render multiple times with different hosts
## Open Questions
1. **How to handle PG enum pre-declaration?** PG requires `pgEnum()` at module scope before tables. Options: (A) return both enums and tables from render, (B) start with text for all enums, (C) per-column opt-in. Leaning toward A.
1. **[RESOLVED — ADR-008](decisions/008-pg-enum-predeclaration.md) 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. **Should hosts return the rendered table or store it in context?** The probe scripts use context (`ctx.tables`), but returning from render would be more functional. Need to resolve this.
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 could diff old and new element trees to produce ALTER TABLE statements. This is a future feature, not phase 1.
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 out of scope for phase 1, but the reconciler architecture positions us for it in a future phase.
## References