From 98764086f49c7f14bef7fa413a48abb92fffe0fe Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Sat, 23 May 2026 12:47:55 +0000 Subject: [PATCH] docs: resolve all open architecture questions with ADRs 009-014 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 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. --- .../009-relation-naming-convention.md | 55 ++++++++++++ .../decisions/010-one-module-per-database.md | 87 +++++++++++++++++++ .../decisions/011-jsx-tsx-support.md | 78 +++++++++++++++++ .../012-always-returning-graceful-fallback.md | 68 +++++++++++++++ .../013-relation-rendering-adapter.md | 61 +++++++++++++ ...014-leverage-drizzle-kit-for-migrations.md | 64 ++++++++++++++ docs/architecture/elements.md | 8 +- docs/architecture/hosts.md | 6 +- docs/architecture/module.md | 2 + docs/architecture/open-questions.md | 73 ++++++++++------ docs/architecture/repo-adapter.md | 15 ++-- docs/architecture/schema.md | 6 +- 12 files changed, 481 insertions(+), 42 deletions(-) create mode 100644 docs/architecture/decisions/009-relation-naming-convention.md create mode 100644 docs/architecture/decisions/010-one-module-per-database.md create mode 100644 docs/architecture/decisions/011-jsx-tsx-support.md create mode 100644 docs/architecture/decisions/012-always-returning-graceful-fallback.md create mode 100644 docs/architecture/decisions/013-relation-rendering-adapter.md create mode 100644 docs/architecture/decisions/014-leverage-drizzle-kit-for-migrations.md diff --git a/docs/architecture/decisions/009-relation-naming-convention.md b/docs/architecture/decisions/009-relation-naming-convention.md new file mode 100644 index 0000000..e0fef9c --- /dev/null +++ b/docs/architecture/decisions/009-relation-naming-convention.md @@ -0,0 +1,55 @@ +# ADR-009: Relation Entry Naming Convention — `FooRelations` + +## Status + +Accepted + +## Context + +Relation entries in the module describe what a table "sees" from its side of a relationship (e.g., `UsersRelations` lists the tasks belonging to a user). The question was whether these entries should use the `FooRelations` naming convention currently in place, or whether relations should be structured differently — for example, as a `relations` field on the table entry itself. + +Alternative approaches considered: + +**Option A: Separate `FooRelations` entries (current pattern)** — `defs.UsersRelations = Type.Object({ tasks: Type.Array(Type.Ref('Tasks')) })`. Relations are their own entries in the module's `$defs` map, alongside table schemas. + +**Option B: `relations` field on the table entry** — Adding a `relations` property to the table's TypeBox object, coupling relation data directly to the table schema. + +**Option C: Different naming convention** — e.g., `UserToTasks`, `TaskToUser`, or `Users$relations`. + +## Decision + +Use Option A: the `FooRelations` naming convention. Relation entries remain separate module entries named `{TableName}Relations`. + +## Rationale + +1. **Aligns with Drizzle's convention**: Drizzle's relational query builder uses `relations(table, ({ one, many }) => ({ ... }))` which is similarly named — e.g., `usersRelations`. The `FooRelations` pattern maps directly. + +2. **Stays within Type.Module's flat namespace**: Module keys are a flat namespace (no nested paths like `"tables/Users"`). Coupling relations onto the table entry would either require a special `relations` property (breaking the TypeBox schema model) or a nested `$defs` structure. Separate entries keep things flat and simple. + +3. **Queryable by convention**: The `$defs` map is enumerable. Finding all relations for a table is straightforward: look for `${tableName}Relations` in the defs. This is discoverable and doesn't require special tooling. + +4. **Independent lifecycle**: Relations can be added, modified, or removed independently of the table schema. This matters for the repo adapter, which reads relation entries to generate Drizzle `relations()` calls. + +5. **No circular dependency risk**: `Type.Ref` resolves within the module namespace regardless of definition order. `UsersRelations` references `Tasks` and vice versa without import cycles. + +6. **Consistent with existing probes**: The probe scripts already use this pattern successfully. + +## Consequences + +### Positive + +- Simple, discoverable convention +- Aligns with Drizzle ecosystem naming +- No special-casing needed in module or extract logic +- Relations are independently addressable via `M.Import('UsersRelations')` + +### Negative + +- Relation names are coupled to table names by convention, not by type system. Renaming a table requires updating the corresponding `FooRelations` entry name. +- The `Relations` suffix occupies the namespace — a table named `UsersRelations` would conflict. This is unlikely but theoretically possible. + +## References + +- [schema.md](../schema.md) — Relations section +- [module.md](../module.md) — Module flat namespace constraint +- [open-questions.md](../open-questions.md) — OQ-01 \ No newline at end of file diff --git a/docs/architecture/decisions/010-one-module-per-database.md b/docs/architecture/decisions/010-one-module-per-database.md new file mode 100644 index 0000000..c8cefe2 --- /dev/null +++ b/docs/architecture/decisions/010-one-module-per-database.md @@ -0,0 +1,87 @@ +# ADR-010: One Module per Database, Include All Derived Schemas + +## Status + +Accepted + +## Context + +Two related questions about module structure: + +**OQ-02**: Should derived schemas (insert, update) live in the module as entries, or be extracted separately by walking the module? Adding them to the module increases its size but makes everything accessible via `Import`. Extracting separately is more flexible but requires separate code paths. + +**OQ-03**: Should the module support multiple database namespaces? One module per database, or one module with all tables across all databases? + +These are intertwined because derived schemas only make sense in the context of a single database's tables, and the module scope decision affects how derived schemas are organized. + +### OQ-02: Derived schemas in module vs. separate + +**Option A: Include as module entries** — `defs.InsertUsers`, `defs.UpdateUsers`, `defs.FilterUsers` are added to the module alongside tables and relations. Accessible via `M.Import('InsertUsers')`. + +**Option B: Extract by walking** — Derived schemas are generated on-the-fly from the table entry and column metadata, without being stored in the module. + +**Option C: Hybrid** — Include by default, but allow `includeDerived: false` to skip them. + +### OQ-03: Module per database vs. single module + +**Option A: One module per database namespace** — Each database (or schema) gets its own `Type.Module`. No cross-database references within one module. + +**Option B: One module with all tables** — All tables across databases in one module, with naming conventions to avoid collisions. + +## Decision + +**OQ-02**: Include all derived schemas in the module by default (Option C, defaulting to inclusion). + +A `repo_from` interface (or similar) will be added to generate CRUD operations in the same vein as drizzle-graphql, using dbtype's own operations system. This interface needs access to select, insert, update, and filter schemas. Since both the "include everything" and "extract separately" paths have valid use cases and the decision isn't hard to reverse, we default to including everything — making it accessible via `M.Import()` — and can add an opt-out later if needed. + +**OQ-03**: One module per database namespace (Option A). + +Each database namespace (e.g., `public`, `tenant_a`) gets its own `Type.Module` instance. This keeps the module's flat namespace clean, avoids table name collisions across databases, and aligns with how Drizzle organizes schemas. + +## Rationale + +### For including derived schemas (OQ-02) + +1. **Convenience for the adapter**: The repo adapter needs insert, update, and filter schemas anyway. Having them in the module means one import path (`M.Import('InsertUsers')`) rather than a separate extraction step. + +2. **`repo_from` needs them**: The `repo_from` interface generates operation specs from the module. It needs access to all derived schemas. Having them in the module is the simplest path. + +3. **Not hard to reverse**: If including everything becomes a problem (e.g., module size for very large schemas), we can add `includeDerived: false` and generate them on-the-fly. The generation logic is the same either way. + +4. **Consistency with the module pattern**: The module is already the single source of truth for table schemas and relations. Derived schemas are computed from the same source — they belong alongside. + +### For one module per database (OQ-03) + +1. **Drizzle alignment**: Drizzle organizes tables by schema/namespace. One module per database maps naturally to one Drizzle schema object. + +2. **No namespace collisions**: The module is a flat namespace. Two databases could have tables with the same name — one module per database avoids collisions without naming hacks like `db1_users`. + +3. **Independent evolution**: Each database's module can be versioned, validated, and diffed independently. Cross-database changes are separate concerns. + +4. **Type.Ref stays local**: Cross-database references would require `Module.Import` nesting (see [module.md](../module.md) cross-module references), which increases payload size. One module per database keeps things simple. + +## Consequences + +### Positive + +- All schemas accessible via `M.Import()` — one consistent API +- Module mirrors database structure — one module = one database +- No namespace collision risk +- `repo_from` has everything it needs in one place +- Easy to reverse the derived-schema decision if needed + +### Negative + +- Larger module size (insert, update, filter schemas for every table). For a schema with 20 tables, this adds ~80 additional entries (3–4 per table). This is acceptable given that modules are compiled once and imported many times. +- Cross-database joins would require consuming code to merge contexts from multiple modules. This is a phase 2+ concern. + +### Future Considerations + +- An `includeDerived: false` opt-out could be added to exclude insert/update/filter entries from the module, for consumers who only need table and relation schemas. This can be added without breaking the default behavior. +- The `repo_from` interface (phase 2) will be the primary consumer of derived schemas. Its design should be informed by how derived schemas are accessed in practice. + +## References + +- [schema.md](../schema.md) — Schema derivation section and OQ-02/OQ-03 +- [module.md](../module.md) — Module construction and cross-module references +- [repo-adapter.md](../repo-adapter.md) — The `from-dbtype` adapter that will use `repo_from` \ No newline at end of file diff --git a/docs/architecture/decisions/011-jsx-tsx-support.md b/docs/architecture/decisions/011-jsx-tsx-support.md new file mode 100644 index 0000000..f8a61a5 --- /dev/null +++ b/docs/architecture/decisions/011-jsx-tsx-support.md @@ -0,0 +1,78 @@ +# ADR-011: JSX/TSX File Support + +## Status + +Accepted + +## Context + +dbtype elements can be authored using `h()` calls from `@alkdev/ujsx`. The probe scripts use this pattern: + +```typescript +const UsersEl = h('table', { name: 'users' }, + h('column', { name: 'id', type: 'uuid', primaryKey: true, default: 'uuid' }), + h('column', { name: 'name', type: 'string', notNull: true }), +) +``` + +JSX syntax would be more ergonomic for authoring, especially for larger schemas: + +```tsx +const UsersEl = ( + + + +
+) +``` + +JSX requires: +- `.tsx` file extensions +- TSConfig `jsx` and `jsxImportSource` settings (`jsx: 'react-jsx'`, `jsxImportSource: '@alkdev/ujsx'`) +- Bundler/transpiler support for JSX transformation + +The question is whether to support JSX as a first-class authoring format, and if so, what the implications are. + +## Decision + +Support JSX/TSX file extensions as an ergonomic authoring layer. JSX desugars to `h()` calls via the configured `jsxImportSource`, so the runtime representation is identical. + +JSX support means: +- Element definition files can use `.tsx` extensions with ``, ``, ``, `` tags +- TSConfig must set `jsx: 'react-jsx'` and `jsxImportSource: '@alkdev/ujsx'` +- The `h()` function remains the underlying API and works universally (in `.ts` files, in build steps, in non-JSX environments) +- Generated output (from the host render pipeline) will never use JSX — it produces Drizzle table objects directly + +## Rationale + +1. **Ergonomics for authoring**: Manually writing `h()` calls is verbose and error-prone for complex schemas. JSX provides a familiar, declarative syntax that is easier to read and write. This is especially important for dbtype's target audience — developers defining database schemas. + +2. **Zero-cost abstraction**: JSX compiles to `h()` calls. There is no runtime overhead, no separate JSX runtime — just a different authoring syntax for the same element tree. The `jsxImportSource: '@alkdev/ujsx'` ensures the `h` import is automatic. + +3. **Precedent in ecosystem**: React, Solid, Preact, and other UI libraries use this pattern. Developers are familiar with JSX for declarative tree structures. dbtype applies the same pattern to database schemas. + +4. **No architectural impact**: JSX is purely an authoring convenience. The element tree, `extractTable()`, host rendering, and module compilation are all unchanged. If JSX support is removed, all schemas still work with `h()` calls. + +5. **HostConfig already handles intrinsic elements**: The UJSX host only sees resolved intrinsic elements (`table`, `column`, `index`, `fk`). Whether those elements were created via `h()` or JSX is irrelevant to the rendering pipeline. + +## Consequences + +### Positive + +- More ergonomic schema authoring +- Familiar declarative syntax for developers +- Zero runtime cost — compiles to same `h()` calls +- No changes to rendering pipeline or module architecture + +### Negative + +- Requires TSConfig configuration (`jsx`, `jsxImportSource`) for projects using JSX +- Some build tools may need additional configuration for `.tsx` files +- Two authoring styles (`h()` and JSX) could confuse new users — should be clearly documented with JSX as the recommended authoring format +- IDE support for custom JSX element types (autocompletion for `
`, `` props) requires TypeScript type definitions for the UJSX element props + +## References + +- [elements.md](../elements.md) — Element definitions and function components +- [open-questions.md](../open-questions.md) — OQ-07 +- UJSX HostConfig: `@alkdev/ujsx/src/host/config.ts` \ No newline at end of file diff --git a/docs/architecture/decisions/012-always-returning-graceful-fallback.md b/docs/architecture/decisions/012-always-returning-graceful-fallback.md new file mode 100644 index 0000000..7264bcc --- /dev/null +++ b/docs/architecture/decisions/012-always-returning-graceful-fallback.md @@ -0,0 +1,68 @@ +# ADR-012: Always `.returning()` with Graceful Fallback + +## Status + +Accepted + +## Context + +Different database dialects have different capabilities for returning data from mutation operations: + +- **PostgreSQL**: Supports `.returning()` on all mutations (`INSERT`, `UPDATE`, `DELETE`). Returns the affected rows with requested columns. +- **SQLite**: Supports `.returning()` on all mutations (since SQLite 3.35.0 / Drizzle support). +- **MySQL**: Does **not** support `.returning()` on most mutations. `INSERT ... RETURNING` is not part of the MySQL protocol.Affected row count is available, but not the row data. + +This creates a per-dialect handler difference for the repo adapter. The generated `OperationSpec` handlers for mutations (insert, update, delete) need to handle the fact that some dialects can return the modified rows and some cannot. + +Options considered: + +**Option A: Always use `.returning()`, let the dialect handle it** — Generate `.returning()` on all mutation handlers. Dialects that support it return the data. Dialects that don't will throw or silently drop it. + +**Option B: Explicit per-dialect config** — The adapter config specifies whether `.returning()` is available. Handlers are generated differently per dialect. + +**Option C: Always attempt `.returning()`, gracefully fallback** — Use `.returning()` on all mutations. For dialects where `.returning()` is not available, fall back to returning the input data (for inserts) or a count of affected rows (for updates/deletes). + +## Decision + +Use Option C: always use `.returning()` on mutation handlers, with graceful fallback for dialects that don't support it. + +The adapter generates handlers that: +1. Always call `.returning()` on the Drizzle query builder +2. If `.returning()` succeeds, return the result rows +3. If `.returning()` is not supported by the dialect (MySQL), fall back to: + - `insertOne` / `insert`: Return the input data (validated by the insert schema) + - `update` / `delete`: Return a count of affected rows + +The detection mechanism: the adapter checks the dialect string from the host config (`root.ctx.dialect`) to determine whether to include `.returning()` in the generated query, rather than catching runtime errors from Drizzle. This avoids try-catch in the query path and makes the behavior deterministic and predictable. + +## Rationale + +1. **Consistent API shape**: Callers of operation specs get the same return type regardless of dialect. This means the operations system doesn't need dialect-aware code paths in consumer logic. + +2. **The operations system is an abstraction layer**: The whole point of the repo adapter is to hide dialect differences behind a uniform interface. Exposing `.returning()` availability in the config would leak dialect details to consumers. + +3. **Graceful fallback is practical**: For inserts, the input data is already validated by the insert schema — returning it is a reasonable approximation of what `.returning()` would provide (minus auto-generated fields like IDs, which the caller can fetch separately if needed). For updates and deletes, the count of affected rows is useful and predictable. + +4. **Drizzle already handles the dialect difference**: Drizzle's query builder throws on MySQL if you call `.returning()`. The adapter should catch this condition (or check dialect capabilities) rather than forcing consumers to handle it. + +5. **Not hard to reverse**: If per-dialect configuration becomes necessary later (e.g., some operations need to opt out of `.returning()` for performance), the adapter can add a `returning: false` per-table override without breaking the default behavior. + +## Consequences + +### Positive + +- Uniform mutation return types across all dialects +- No dialect-specific configuration needed for basic CRUD +- Consumers don't need to know whether the underlying DB supports `.returning()` +- SQLite and PG get full `.returning()` data; MySQL gets reasonable fallbacks + +### Negative + +- MySQL insert operations lose auto-generated column values in the return (they return validated input, not the DB-generated row). Consumers who need the generated ID must do a follow-up query. +- The return type for mutations is technically dialect-dependent at runtime, even if the TypeScript type says otherwise. This should be clearly documented. +- The fallback behavior adds a small amount of complexity to the adapter's handler generation logic. + +## References + +- [repo-adapter.md](../repo-adapter.md) — Handler generation section and OQ-08 +- [open-questions.md](../open-questions.md) — OQ-08 \ No newline at end of file diff --git a/docs/architecture/decisions/013-relation-rendering-adapter.md b/docs/architecture/decisions/013-relation-rendering-adapter.md new file mode 100644 index 0000000..50f5235 --- /dev/null +++ b/docs/architecture/decisions/013-relation-rendering-adapter.md @@ -0,0 +1,61 @@ +# ADR-013: Relation Rendering is the Adapter's Responsibility + +## Status + +Accepted + +## Context + +The repo adapter needs Drizzle `relations()` objects to power the relational query builder (`db.query[tableName].findMany({ with: ... })`). The question is who renders these relation objects: + +**Option A: The host renders relations** — Add a new `` element type. The host would render it alongside tables, producing both `ctx.tables` and `ctx.relations`. This couples relation rendering to the host pipeline. + +**Option B: The adapter generates relations** — The adapter reads the module's relation entries (`FooRelations`), uses the already-rendered table objects from `ctx.tables`, and generates the Drizzle `relations()` calls. The host doesn't need to know about relations at all. + +**Option C: Hybrid — host renders, adapter consumes** — The host renders relation elements during the render walk, but the adapter is the primary consumer and dictates what shape the output takes. + +## Decision + +Use Option B: the adapter generates relation objects from the module's relation entries and the rendered table objects. + +The adapter: +1. Reads relation entries from the compiled module (`M.Import('UsersRelations')`, `M.Import('TasksRelations')`) +2. Uses the rendered table objects available in `root.ctx.tables` +3. Generates the appropriate Drizzle `relations()` calls for each relation entry +4. Passes the full `{ tables, relations }` schema to the Drizzle relational query builder + +The host rendering pipeline does not change — it produces `ctx.tables` (and `ctx.enums` for PG) as before. Relation generation is entirely in the adapter's domain. + +## Rationale + +1. **The adapter has the right context**: The adapter consumes both the element tree (for schema derivation) and the rendered Drizzle objects (for query execution). It has the table objects, the module compiled bundle, and the operation config. Generating relations from these inputs is a natural fit. + +2. **The module already defines relations**: `UsersRelations = Type.Object({ tasks: Type.Array(Type.Ref('Tasks')) })` is already in the module. The adapter just needs to walk this definition and produce `relations(users, ({ many }) => ({ tasks: many(tasks) }))`. No new element type is needed. + +3. **Relations are a query concern, not a schema concern**: Relations exist to power Drizzle's relational query builder. They don't affect the database schema (that's what FKs do). They're about how you query data, which is the adapter's domain. + +4. **The host doesn't need another element type**: Adding `` to the host pipeline would require a new intrinsic element, a new `createInstance` path, and new rendering logic. This adds complexity to the host for a concern that's better handled by the adapter. + +5. **The adapter knows which tables are rendered**: The adapter receives `root.ctx.tables` — the actual rendered Drizzle table objects. It can construct `relations()` calls using these real objects, which is exactly what Drizzle's API expects. + +6. **Consistent with `repo_from` direction**: The `repo_from` interface will generate operation specs from the module and rendered objects. Relations are part of this generation — they define what `with` parameters are available in `findMany`/`findFirst` operations. + +## Consequences + +### Positive + +- No new host element type — simpler host pipeline +- Relations are generated where they're consumed — the adapter +- The module's relation entries are the single source of truth +- The adapter can customize relation generation (e.g., one-to-one vs. one-to-many) without host changes + +### Negative + +- The adapter must implement relation generation logic, mapping from `Type.Object({ field: Type.Array(Type.Ref('Target')) })` to `relations(source, ({ one, many }) => ({ field: many(target) }))`. This is straightforward but not trivial for complex relation patterns. +- If a consumer uses the host directly (without the repo adapter), they won't get relation objects. This is acceptable — relations are only needed for the relational query builder, which is an adapter-level concern. + +## References + +- [repo-adapter.md](../repo-adapter.md) — Relations section and OQ-09 +- [schema.md](../schema.md) — Relations section +- [open-questions.md](../open-questions.md) — OQ-09 \ No newline at end of file diff --git a/docs/architecture/decisions/014-leverage-drizzle-kit-for-migrations.md b/docs/architecture/decisions/014-leverage-drizzle-kit-for-migrations.md new file mode 100644 index 0000000..3919d1b --- /dev/null +++ b/docs/architecture/decisions/014-leverage-drizzle-kit-for-migrations.md @@ -0,0 +1,64 @@ +# ADR-014: Leverage drizzle-kit for Migrations (Phase 1+) + +## Status + +Accepted + +## Context + +The module architecture supports structural diffing via `Value.Diff` on serialized schemas. Translating these structural edit lists (insert/delete/update property) to `ALTER TABLE` statements is straightforward for additive changes (new column) but complex for destructive ones (drop column, change column type). + +The question is how far the migration diffing should go in phase 1, and whether dbtype should implement its own migration generator. + +Options considered: + +**Option A: No migration generation in phase 1** — Rely entirely on `drizzle-kit` for migrations. dbtype focuses on schema definition and validation; `drizzle-kit` handles the migration workflow. + +**Option B: Basic additive migrations in phase 1** — Implement `ALTER TABLE ADD COLUMN` for new columns only. Skip destructive migrations. + +**Option C: Full migration generation** — Translate all `Value.Diff` edit types to SQL migrations. + +## Decision + +Use Option A: leverage `drizzle-kit`'s existing migration tools. dbtype does not implement migration generation in phase 1 or the foreseeable future. + +The rationale is that dbtype's primary output for the host is Drizzle table definitions. `drizzle-kit` already handles migration generation from Drizzle table definitions. Since dbtype renders to Drizzle tables, the natural migration workflow is: + +1. Define schemas with dbtype elements +2. Render to Drizzle table definitions via the host +3. Run `drizzle-kit generate` to produce migration files from the Drizzle tables +4. Run `drizzle-kit migrate` to apply them + +The module structure (with `Value.Diff` support) should not be changed or constrained — it remains available for future migration generation if needed. + +## Rationale + +1. **drizzle-kit already solves this**: Drizzle has invested significant effort in migration generation, introspection, and push workflows. Reimplementing this in dbtype would be duplicative and fragile. + +2. **dbtype's output is Drizzle tables**: The host renders element trees to Drizzle table objects. `drizzle-kit generate` works on Drizzle table objects. There's no gap — dbtype produces exactly what `drizzle-kit` needs. + +3. **Migration generation is a hard problem**: Correct handling of destructive migrations, column type changes, constraint additions/removals, data migrations, and rollback support requires significant investment. This is not a differentiating feature for dbtype. + +4. **`Value.Diff` remains available**: The module's structural diffing capability (see [module.md](../module.md)) is not removed or disabled. It remains as a foundation for future migration tooling if the need arises. + +5. **The module structure doesn't prevent future migration generation**: The `Value.Diff` edit list format (insert/delete/update property) and the serialized JSON Schema form are deliberately general. If dbtype ever needs to generate migrations directly, the foundation is there. + +## Consequences + +### Positive + +- No migration infrastructure to build and maintain +- Leverages drizzle-kit's mature, well-tested migration workflow +- Phase 1 scope stays focused on schema definition, validation, and rendering +- Module structure remains migration-ready without committing to implementation + +### Negative + +- Consumers must use `drizzle-kit` for migrations — there's no dbtype-native migration path +- If `drizzle-kit` doesn't support a particular dialect's migration feature, dbtype can't fill that gap without implementing its own migration generator +- The `Value.Diff` capability in the module is documented but not exercised by any production code path yet + +## References + +- [module.md](../module.md) — Migration diffing section +- [open-questions.md](../open-questions.md) — OQ-10 \ No newline at end of file diff --git a/docs/architecture/elements.md b/docs/architecture/elements.md index 079c9a8..fe43142 100644 --- a/docs/architecture/elements.md +++ b/docs/architecture/elements.md @@ -1,6 +1,6 @@ --- -status: draft -last_updated: 2026-05-22 +status: stable +last_updated: 2026-05-23 --- # Elements: UJSX Element Definitions @@ -235,9 +235,9 @@ interface TableMeta { 1. ~~Should column elements support `inner` TypeBox schemas?~~ **Resolved — Yes. ADR-007: flat props for common cases, `inner` as escape hatch for custom validation.** The `inner` prop is now documented in the column props table. -2. **Should `
` 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? +2. **Should `
` 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? (See [OQ-11](open-questions.md)) -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. +3. ~~**Should we support JSX file extensions?**~~ **Resolved — ADR-011**. JSX/TSX is supported as an ergonomic authoring layer. JSX desugars to `h()` calls via `jsxImportSource: '@alkdev/ujsx'`. The `h()` API remains the universal fallback. TSConfig must set `jsx: 'react-jsx'` and `jsxImportSource: '@alkdev/ujsx'`. ## References diff --git a/docs/architecture/hosts.md b/docs/architecture/hosts.md index fa1ef0b..e657881 100644 --- a/docs/architecture/hosts.md +++ b/docs/architecture/hosts.md @@ -1,6 +1,6 @@ --- -status: draft -last_updated: 2026-05-22 +status: stable +last_updated: 2026-05-23 --- # Hosts: Dialect Rendering via HostConfig @@ -110,7 +110,7 @@ root.render(UsersEl) 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 }`, PG uses `{ dialect: 'pg', enums: Record, tables: Record }`. -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. +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 now deferred indefinitely — ADR-014 establishes that dbtype leverages `drizzle-kit` for migrations rather than implementing its own migration generator. The reconciler architecture remains available if this decision is revisited in the future. See [ADR-014](decisions/014-leverage-drizzle-kit-for-migrations.md). ## References diff --git a/docs/architecture/module.md b/docs/architecture/module.md index f38dfc4..5d9b57b 100644 --- a/docs/architecture/module.md +++ b/docs/architecture/module.md @@ -137,6 +137,8 @@ Edits use JSON Pointer paths. A migration generator can translate these to: This is structural diffing, not semantic — it doesn't understand that changing `Type.String()` to `Type.String({ maxLength: 255 })` is a constraint addition, not a type change. Semantic diffing is a future concern. +**Migration generation**: Leverage `drizzle-kit` for migrations. Since dbtype's primary output is Drizzle table definitions, `drizzle-kit generate` handles migration generation from those tables. The module's `Value.Diff` capability remains available as a foundation for future migration tooling, but is not used for production migrations in phase 1 or the foreseeable future. See [ADR-014](decisions/014-leverage-drizzle-kit-for-migrations.md). + ## Cross-Module References `Module.Import()` embeds the source module's `$defs` in the resulting `TImport` schema. This enables referencing types from another module: diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 1197eab..36ad7a2 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -1,36 +1,39 @@ --- -status: draft +status: stable last_updated: 2026-05-23 --- # Open Questions Tracker -All unresolved architectural questions for dbtype, organized by theme. +Architectural questions for dbtype, organized by theme. Resolved questions reference their ADR. ## Schema & Module ### OQ-01: Should relation entries use a naming convention? - **Origin**: [schema.md](schema.md) -- **Status**: Open -- **Priority**: Medium +- **Status**: Resolved +- **Priority**: ~~Medium~~ Resolved - **Context**: Currently `UsersRelations` / `TasksRelations`. Is this naming convention sufficient, or should relations be structured differently? A `relations` field on the table entry would couple relations to table definitions. +- **Resolution**: Keep the `FooRelations` naming convention. Separate module entries for relations align with Drizzle's naming convention, stay within Type.Module's flat namespace, and are independently addressable via `M.Import`. A `relations` field on the table entry would break the TypeBox schema model. See [ADR-009](decisions/009-relation-naming-convention.md). - **Cross-references**: OQ-03 ### OQ-02: Should derived schemas live in the module or be extracted separately? - **Origin**: [schema.md](schema.md) -- **Status**: Open -- **Priority**: Low +- **Status**: Resolved +- **Priority**: ~~Low~~ Resolved - **Context**: Insert/update schemas can be added as module entries (`InsertUsers`, `UpdateUsers`) or extracted by walking the module. Adding them to the module increases size but makes everything accessible via `Import`. Extracting separately is more flexible but requires separate code paths. +- **Resolution**: Include all derived schemas (insert, update, filter) in the module by default. A `repo_from` interface will generate CRUD operation specs from the module using the operations system, and needs access to all derived schemas. The decision isn't hard to reverse — an `includeDerived: false` opt-out can be added later. See [ADR-010](decisions/010-one-module-per-database.md). - **Cross-references**: OQ-03 ### OQ-03: Should the module support multiple database namespaces? - **Origin**: [schema.md](schema.md) -- **Status**: Open -- **Priority**: Low +- **Status**: Resolved +- **Priority**: ~~Low~~ Resolved - **Context**: One module per database, or one module with all tables across all databases? Probably one per database namespace, but this needs confirmation from real use cases. +- **Resolution**: One module per database namespace. This keeps the module's flat namespace clean, avoids table name collisions across databases, and aligns with how Drizzle organizes schemas. See [ADR-010](decisions/010-one-module-per-database.md). ## Element & Host @@ -54,53 +57,73 @@ All unresolved architectural questions for dbtype, organized by theme. - **Origin**: [hosts.md](hosts.md) - **Status**: Resolved -- **Priority**: Medium +- **Priority**: ~~Medium~~ Resolved - **Context**: The probe scripts use `ctx.tables` to collect rendered tables. A more functional approach would have `render()` return the rendered table directly. Need to resolve this before implementation. -- **Resolution**: render() accumulates results in root.ctx (tables, enums). The context shape varies by dialect — SQLite has ctx.tables, PG has ctx.tables and ctx.enums. See hosts.md Rendering Pipeline. +- **Resolution**: render() accumulates results in root.ctx (tables, enums). The context shape varies by dialect — SQLite has ctx.tables, PG has ctx.tables and ctx.enums. This is a design clarification confirmed by implementation, not a significant architectural decision requiring its own ADR. See hosts.md Rendering Pipeline. ### OQ-07: Should we support JSX file extensions? - **Origin**: [elements.md](elements.md) -- **Status**: Open -- **Priority**: Low +- **Status**: Resolved +- **Priority**: ~~Low~~ Resolved - **Context**: JSX syntax (`.tsx` with `jsxImportSource: '@alkdev/ujsx'`) would be more ergonomic than `h()` calls. This requires build configuration (TSConfig `jsx`, bundler support). The `h()` API works universally; JSX is a convenience layer. +- **Resolution**: Yes — support JSX/TSX as an ergonomic authoring layer. JSX desugars to `h()` calls via `jsxImportSource: '@alkdev/ujsx'`, so the runtime representation is identical. The `h()` API remains the universal fallback. See [ADR-011](decisions/011-jsx-tsx-support.md). ## Repo Adapter ### OQ-08: Per-dialect handler differences? - **Origin**: [repo-adapter.md](repo-adapter.md) -- **Status**: Open -- **Priority**: Medium +- **Status**: Resolved +- **Priority**: ~~Medium~~ Resolved - **Context**: PG has `.returning()` on all mutations, MySQL often doesn't. Should the adapter handle this transparently (always try `.returning()`, fall back gracefully), or should it be explicit in the config? +- **Resolution**: Always use `.returning()` and gracefully fall back. For dialects that support it (PG, SQLite), return the full result rows. For dialects that don't (MySQL), fall back to returning validated input data (for inserts) or affected row count (for updates/deletes). This keeps the operations API uniform across dialects. See [ADR-012](decisions/012-always-returning-graceful-fallback.md). ### OQ-09: Relation rendering responsibility? - **Origin**: [repo-adapter.md](repo-adapter.md) -- **Status**: Open -- **Priority**: Medium +- **Status**: Resolved +- **Priority**: ~~Medium~~ Resolved - **Context**: Should the host render relations (new element type ``), or should the adapter generate them from the module's relation entries? The adapter knows the rendered table objects; the module knows the relation structure. Leaning toward the adapter generating them. +- **Resolution**: The adapter generates relation objects from the module's relation entries and the rendered table objects. No new `` element type is needed. The adapter reads `FooRelations` entries from the module, maps them to Drizzle `relations()` calls using the rendered table objects in `ctx.tables`. See [ADR-013](decisions/013-relation-rendering-adapter.md). ## Migration & Diffing ### OQ-10: How far should migration diffing go in phase 1? - **Origin**: [module.md](module.md) +- **Status**: Resolved +- **Priority**: ~~Low~~ Resolved +- **Context**: `Value.Diff` on serialized schemas produces structural edit lists (insert/delete/update property). Translating these to `ALTER TABLE` statements is straightforward for additive changes (new column) but complex for destructive ones (drop column, change type). Phase 1 likely skips migration generation entirely (rely on `drizzle-kit`), but the module structure should not prevent it later. +- **Resolution**: Leverage `drizzle-kit` for migrations. dbtype renders to Drizzle table definitions, and `drizzle-kit generate` already handles migration generation from those. No dbtype-native migration generator in phase 1 or the foreseeable future. The module's `Value.Diff` capability remains available as a foundation for future use. See [ADR-014](decisions/014-leverage-drizzle-kit-for-migrations.md). + +### OQ-11: Should `
` accept `extraConfig` props? + +- **Origin**: [elements.md](elements.md) - **Status**: Open - **Priority**: Low -- **Context**: `Value.Diff` on serialized schemas produces structural edit lists (insert/delete/update property). Translating these to `ALTER TABLE` statements is straightforward for additive changes (new column) but complex for destructive ones (drop column, change type). Phase 1 likely skips migration generation entirely (rely on `drizzle-kit`), but the module structure should not prevent it later. +- **Context**: Drizzle tables accept a third callback argument for indexes and unique constraints defined in terms of column references (e.g., `t => ({ uniqueEmail: unique().on(t.email) })`). The `` element handles most index cases, but composite unique constraints using the Drizzle callback style may not be expressible via ``. How should this map to element props, or is it needed at all? + +### OQ-12: Should the adapter accept the Type.Module bundle or individual schemas? + +- **Origin**: [repo-adapter.md](repo-adapter.md) +- **Status**: Open +- **Priority**: Low +- **Context**: The adapter needs access to select, insert, update, and filter schemas. Accepting the `Type.Module` compiled bundle is convenient (everything in one place, accessible via `M.Import()`) but couples the adapter to TypeBox's module format. Accepting individual schemas is more flexible but requires more config. With ADR-010 resolving that derived schemas are included in the module, the module bundle approach is the natural default, but the adapter could expose both interfaces. ## Summary Table | ID | Question | Origin | Priority | Status | |----|----------|--------|----------|--------| -| OQ-01 | Relation naming convention | schema.md | Medium | Open | -| OQ-02 | Derived schemas in module or separate | schema.md | Low | Open | -| OQ-03 | Multiple database namespaces | schema.md | Low | Open | +| OQ-01 | Relation naming convention | schema.md | ~~Medium~~ Resolved | **Resolved** — ADR-009 | +| OQ-02 | Derived schemas in module or separate | schema.md | ~~Low~~ Resolved | **Resolved** — ADR-010 | +| OQ-03 | Multiple database namespaces | schema.md | ~~Low~~ Resolved | **Resolved** — ADR-010 | | OQ-04 | Nested TypeBox schemas in column props | elements.md | ~~High~~ Resolved | **Resolved** — ADR-007 | | OQ-05 | PG enum pre-declaration | hosts.md | ~~High~~ Resolved | **Resolved** — ADR-008 | -| OQ-06 | Host render return value vs context | hosts.md | Medium | Resolved | -| OQ-07 | JSX file extensions | elements.md | Low | Open | -| OQ-08 | Per-dialect handler differences | repo-adapter.md | Medium | Open | -| OQ-09 | Relation rendering responsibility | repo-adapter.md | Medium | Open | -| OQ-10 | Migration diffing scope | module.md | Low | Open | \ No newline at end of file +| OQ-06 | Host render return value vs context | hosts.md | ~~Medium~~ Resolved | **Resolved** — design clarification | +| OQ-07 | JSX file extensions | elements.md | ~~Low~~ Resolved | **Resolved** — ADR-011 | +| OQ-08 | Per-dialect handler differences | repo-adapter.md | ~~Medium~~ Resolved | **Resolved** — ADR-012 | +| OQ-09 | Relation rendering responsibility | repo-adapter.md | ~~Medium~~ Resolved | **Resolved** — ADR-013 | +| OQ-10 | Migration diffing scope | module.md | ~~Low~~ Resolved | **Resolved** — ADR-014 | +| OQ-11 | Table extraConfig props | elements.md | Low | Open | +| OQ-12 | Module bundle vs. individual schemas | repo-adapter.md | Low | Open | \ No newline at end of file diff --git a/docs/architecture/repo-adapter.md b/docs/architecture/repo-adapter.md index a774ec3..1fd1220 100644 --- a/docs/architecture/repo-adapter.md +++ b/docs/architecture/repo-adapter.md @@ -1,6 +1,6 @@ --- -status: draft -last_updated: 2026-05-22 +status: stable +last_updated: 2026-05-23 --- # Repo Adapter: from-dbtype for @alkdev/operations @@ -137,16 +137,17 @@ Overrides replace the auto-generated handler but keep the auto-generated `Operat - **Phase 2 concern** — this adapter is not part of phase 1. Phase 1 delivers the schema, module, and hosts. - **The adapter depends on both the element tree and the rendered Drizzle objects** — it needs the tree for schema derivation and the rendered tables for query execution - **Filter operators are column-type-dependent** — a `uuid` column gets different operators than a `boolean` column -- **The `with` parameter for relations requires the Drizzle `relations` object** — the host must also render relations, not just tables -- **Returning clauses are dialect-specific** — PG supports `.returning()` on all mutations, MySQL does not. The adapter must handle this per-dialect. +- **The `with` parameter for relations requires the Drizzle `relations` object** — the adapter generates relations from module entries and rendered table objects (ADR-013) +- **Returning clauses use graceful fallback** — all mutations call `.returning()`. The adapter checks the dialect string from the host config (`root.ctx.dialect`) to determine whether to include `.returning()`. Dialects that support it (PG, SQLite) return full result rows. Dialects that don't (MySQL) fall back to validated input (inserts) or affected row count (updates/deletes). This keeps the operations API uniform. See [ADR-012](decisions/012-always-returning-graceful-fallback.md). +- **Relations are generated by the adapter, not the host** — the adapter reads `FooRelations` entries from the module and generates Drizzle `relations()` calls using the rendered table objects. No `` element type is needed. See [ADR-013](decisions/013-relation-rendering-adapter.md). ## Open Questions -1. **Per-dialect handler differences?** PG has `.returning()` on all mutations, MySQL often doesn't. Should the adapter handle this transparently, or expose it in the config? +1. ~~**Per-dialect handler differences?**~~ **Resolved — ADR-012**. Always use `.returning()` with graceful fallback. Dialects that support it return data; dialects that don't fall back to validated input or affected row count. -2. **Schema bundle or separate schemas?** Should the adapter accept the `Type.Module` compiled bundle, or individual schemas? The module is convenient (everything in one place) but couples the adapter to TypeBox's module format. +2. **Schema bundle or separate schemas?** Should the adapter accept the `Type.Module` compiled bundle, or individual schemas? The module is convenient (everything in one place) but couples the adapter to TypeBox's module format. (See [OQ-12](open-questions.md)) -3. **Relation rendering responsibility?** Should the host render relations (new `` element), or should the adapter generate them from the module's relation entries? +3. ~~**Relation rendering responsibility?**~~ **Resolved — ADR-013**. The adapter generates relations from module entries and rendered table objects. No new `` element type is needed. ## References diff --git a/docs/architecture/schema.md b/docs/architecture/schema.md index dfc5666..9488a74 100644 --- a/docs/architecture/schema.md +++ b/docs/architecture/schema.md @@ -82,11 +82,11 @@ Per-column comparison operators derived from the column type. Generated by the r ## Open Questions -1. **Should relation entries use a naming convention?** Currently `UsersRelations` / `TasksRelations`. Is this sufficient, or should relations be structured differently (e.g., a `relations` field on the table entry)? +1. ~~**Should relation entries use a naming convention?**~~ **Resolved — ADR-009**. Keep the `FooRelations` naming convention. Separate module entries for relations align with Drizzle's naming, stay within the module's flat namespace, and are independently addressable. -2. **Derived schemas in the module or separate?** Insert/update schemas can be added as module entries (`InsertUsers`, `UpdateUsers`) or extracted by walking the module schema. Which is cleaner for the repo adapter? +2. ~~**Derived schemas in the module or separate?**~~ **Resolved — ADR-010**. Include all derived schemas (insert, update, filter) in the module by default. A `repo_from` interface will generate CRUD operation specs from the module using the operations system. An `includeDerived: false` opt-out can be added later if needed. -3. **Should the module support multiple databases?** One module per database, or one module with all tables across all databases? Probably one per database namespace. +3. ~~**Should the module support multiple databases?**~~ **Resolved — ADR-010**. One module per database namespace. This keeps the module's flat namespace clean, avoids table name collisions, and aligns with Drizzle's schema organization. ## References