496303471d
---ci--- project: atelier phase: 7 milestone: v0.1 status: complete phase_role: final milestone_complete: true requirements: covered: [ATELIER-01, ATELIER-02, ATELIER-03, ATELIER-04, ATELIER-05, ATELIER-06, ATELIER-07, ATELIER-08, ATELIER-09, ATELIER-10, ATELIER-11, ATELIER-12, ATELIER-13, ATELIER-14, ATELIER-15, ATELIER-16, ATELIER-17, ATELIER-18, ATELIER-19, ATELIER-20, ATELIER-21, ATELIER-22, ATELIER-23, ATELIER-24, ATELIER-25, ATELIER-26, ATELIER-27, ATELIER-28, ATELIER-29, ATELIER-30, ATELIER-31, ATELIER-32, ATELIER-33, ATELIER-34, ATELIER-35] partial: [] ship: milestone: v0.1 type: NFR tag: v0.0.7 merge: milestone/v0.1-atelier -> main release: https://git.cloudinit.dev/cloudinit-bot/atelier/releases/tag/v0.0.7 ---/ci--- Milestone v0.1 — Initial Framework (NFR, complete). 8 core principles (C1-C8), 11 domains, 110 domain principles, 27 derived docs, 4 good + 3 bad examples, 4 language docs, full matrix, 3 review docs. All 35 requirements covered. 7 patches (v0.0.0 pre-execution through v0.0.7 final). v0.0.7 IS the v0.1.0 milestone release.
53 lines
2.7 KiB
Markdown
53 lines
2.7 KiB
Markdown
# Schema Design — Derived Rules
|
|
|
|
> Derives from `domains/data/first-principles.md` P1 (Truth), P3 (Invariants in Schema), P7 (Type Fidelity).
|
|
|
|
## The Schema Reflects the Domain (P1 Truth)
|
|
|
|
- A `users` table has columns that are attributes of a user, not attributes of the application.
|
|
- If a column is named `is_active_for_feature_X`, the schema is lying. The domain does not have "feature X."
|
|
- Normalize until it hurts, then denormalize only with evidence (P10 Performance Awareness).
|
|
|
|
## Invariants in the Schema (P3)
|
|
|
|
- NOT NULL where the value is required. UNIQUE where the value is unique.
|
|
- CHECK constraints for range/domain: `age >= 0`, `status IN ('draft', 'published')`.
|
|
- FOREIGN KEY for relationships. The database enforces; the application defends.
|
|
- A constraint in the application but not the schema is a constraint that can be bypassed.
|
|
|
|
## Types (P7 Type Fidelity)
|
|
|
|
- `UUID` for IDs, not `VARCHAR`. `UUID` is a type; `VARCHAR(36)` is a string that looks like a UUID.
|
|
- `TIMESTAMPTZ` for timestamps, not `VARCHAR` or `INTEGER`. Timezone-aware by default.
|
|
- `ENUM` for finite domains, `VARCHAR` with CHECK for evolving domains.
|
|
- `JSONB` for unstructured/semi-structured; not for data that should be a column.
|
|
- `DECIMAL`/`NUMERIC` for money, never `FLOAT`. Floating point is for measurements, not money.
|
|
|
|
## Naming (P6 Naming Consistency)
|
|
|
|
- snake_case for tables and columns (PostgreSQL convention): `user_accounts`, `created_at`.
|
|
- Singular table names (`user` not `users`) OR plural (`users` not `user`) — pick one, be consistent.
|
|
- Foreign keys: `<singular_table>_id` (`user_id`), not `uid` or `user`.
|
|
- Junction tables: alphabetical (`order_products`, not `products_orders`).
|
|
|
|
## Avoid (P2 Normalization Discipline)
|
|
|
|
- Computed columns that duplicate derivable data. Use a view or compute on read.
|
|
- `created_by_name` (denormalized) when `created_by_id` + JOIN suffices. Denormalize only with evidence.
|
|
- Soft-delete columns (`is_deleted`) without a corresponding constraint/behavior. Soft delete is a lifecycle decision (P8).
|
|
|
|
## Soft Delete vs Hard Delete (P8 Lifecycle Awareness)
|
|
|
|
- Soft delete (`deleted_at TIMESTAMP`) preserves auditability but complicates every query.
|
|
- Hard delete loses history. Choose based on the domain's legal/audit requirements.
|
|
- If soft delete: every query filters `WHERE deleted_at IS NULL` by default. A missing filter is a bug.
|
|
|
|
## What Violates Schema Design
|
|
|
|
| Violation | Principle |
|
|
|-----------|-----------|
|
|
| `VARCHAR` for a UUID | P7 Type Fidelity |
|
|
| No FOREIGN KEY on a relationship | P3, P9 Referential Integrity |
|
|
| `FLOAT` for money | P7, P1 Truth |
|
|
| `is_deleted` without consistent filtering | P8 Lifecycle |
|
|
| A column named after a feature, not a domain concept | P1 Truth | |