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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user