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.
2.7 KiB
2.7 KiB
Schema Design — Derived Rules
Derives from
domains/data/first-principles.mdP1 (Truth), P3 (Invariants in Schema), P7 (Type Fidelity).
The Schema Reflects the Domain (P1 Truth)
- A
userstable 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)
UUIDfor IDs, notVARCHAR.UUIDis a type;VARCHAR(36)is a string that looks like a UUID.TIMESTAMPTZfor timestamps, notVARCHARorINTEGER. Timezone-aware by default.ENUMfor finite domains,VARCHARwith CHECK for evolving domains.JSONBfor unstructured/semi-structured; not for data that should be a column.DECIMAL/NUMERICfor money, neverFLOAT. 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 (
usernotusers) OR plural (usersnotuser) — pick one, be consistent. - Foreign keys:
<singular_table>_id(user_id), notuidoruser. - Junction tables: alphabetical (
order_products, notproducts_orders).
Avoid (P2 Normalization Discipline)
- Computed columns that duplicate derivable data. Use a view or compute on read.
created_by_name(denormalized) whencreated_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 NULLby 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 |