Add SDD architecture docs for dbtype
Phase 0 architecture specification following the alkdev documentation pattern from @alkdev/flowgraph. Documents the validated architecture (UJSX elements → Type.Module → Drizzle hosts) based on e2e probe results. Docs added: - README: Project overview, architecture, current state - architecture/README: Index, design decisions, relationships - architecture/schema: Type.Module as bundle, construction, serialization - architecture/hosts: HostConfig per dialect, column mapping, symbolic defaults - architecture/elements: UJSX element types, props, function components - architecture/module: Module mechanics, format registration, diffing - architecture/repo-adapter: from-dbtype operations adapter (phase 2) - architecture/build-distribution: Package structure, exports - architecture/open-questions: 10 open questions across all topics - ADRs 001-005: UJSX as IR, Type.Module, HostConfig, format, repo adapter
This commit is contained in:
175
docs/architecture/elements.md
Normal file
175
docs/architecture/elements.md
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-05-22
|
||||
---
|
||||
|
||||
# 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 or literal value |
|
||||
| `references` | `string` | no | FK target table name |
|
||||
| `format` | `string` | no | TypeBox format annotation (uuid, email, etc.) |
|
||||
| `mode` | `'json' \| 'text'` | no | Storage mode for compound 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 |
|
||||
|
||||
### `<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:
|
||||
|
||||
```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<string, ColumnProps>` for Drizzle rendering
|
||||
3. **Table metadata**: `{ name, columns, indexes, foreignKeys }` for the host
|
||||
|
||||
```typescript
|
||||
function extractTable(el: UElement): {
|
||||
name: string
|
||||
schema: TObject // TypeBox schema for Type.Module
|
||||
columns: Record<string, ColumnMeta> // Props + computed metadata
|
||||
indexes: IndexMeta[]
|
||||
foreignKeys: FkMeta[]
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
- **`default` values can be symbolic strings or literals** — symbolic defaults (`now`, `uuid`, `autoincrement`) are resolved by the host; literal values pass through directly
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Should column elements support `inner` TypeBox schemas?** The research docs proposed `DbType.String({ notNull: true, inner: Type.String({ format: 'email', maxLength: 255 }) })`. With UJSX elements, this would be `<column name="email" type="string" notNull format="email" maxLength={255} />`. Is the flat props model sufficient, or do we need nested TypeBox schemas?
|
||||
|
||||
2. **Should `<table>` 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?
|
||||
|
||||
3. **Should we support JSX file extensions?** The current probe uses `h()` calls. JSX syntax (`.tsx` files with `jsxImportSource: '@alkdev/ujsx'`) would be more ergonomic for authoring but requires build configuration.
|
||||
|
||||
## 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)
|
||||
Reference in New Issue
Block a user