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.
88 lines
4.1 KiB
Markdown
88 lines
4.1 KiB
Markdown
# Good Example: Database Schema
|
|
|
|
> A SQL schema that follows Atelier's Data principles. Each aspect cites the principle it satisfies.
|
|
|
|
## The Schema
|
|
|
|
```sql
|
|
-- Users table
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email VARCHAR(255) NOT NULL,
|
|
name VARCHAR(100) NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
deleted_at TIMESTAMPTZ,
|
|
|
|
CONSTRAINT users_email_unique UNIQUE (email),
|
|
CONSTRAINT users_email_format CHECK (email ~ '^[^@]+@[^@]+\.[^@]+$')
|
|
);
|
|
|
|
CREATE INDEX users_email_idx ON users (email) WHERE deleted_at IS NULL;
|
|
CREATE INDEX users_created_at_idx ON users (created_at DESC);
|
|
|
|
-- Orders table
|
|
CREATE TABLE orders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
total_cents INTEGER NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT orders_status_valid CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
|
|
CONSTRAINT orders_total_positive CHECK (total_cents >= 0)
|
|
);
|
|
|
|
CREATE INDEX orders_user_id_idx ON orders (user_id);
|
|
CREATE INDEX orders_status_created_idx ON orders (status, created_at DESC);
|
|
```
|
|
|
|
## What Makes It Good
|
|
|
|
### Truth (Data P1)
|
|
- The schema reflects the domain: `users` have `email`, `name`, lifecycle timestamps. `orders` have `status`, `total`.
|
|
- No column named after a feature (`is_active_for_X`). No application state in the schema.
|
|
|
|
### Invariants in the Schema (Data P3)
|
|
- `NOT NULL` where required: `email`, `name`, `user_id`, `status`, `total_cents`.
|
|
- `UNIQUE (email)` — emails are unique. Enforced in the DB, defended in the app.
|
|
- `CHECK (status IN (...))` — status is a finite domain. Enforced in the DB.
|
|
- `CHECK (total_cents >= 0)` — totals are non-negative. Enforced in the DB.
|
|
- `REFERENCES users(id) ON DELETE RESTRICT` — you cannot delete a user with orders. Referential integrity (P9).
|
|
|
|
### Type Fidelity (Data P7)
|
|
- `id` is `UUID`, not `VARCHAR(36)`. The type matches the domain.
|
|
- `created_at` is `TIMESTAMPTZ`, not `VARCHAR` or `INTEGER`. Timezone-aware.
|
|
- `total_cents` is `INTEGER`, not `FLOAT`. Money in cents avoids floating point (P1 Truth).
|
|
- `status` is `VARCHAR(20)` with a CHECK, not a free `TEXT`. Bounded.
|
|
|
|
### Naming Consistency (Data P6)
|
|
- snake_case: `users`, `orders`, `user_id`, `created_at`.
|
|
- Foreign key: `user_id` (singular table + `_id`), not `uid` or `user`.
|
|
- Timestamps: `created_at`, `updated_at`, `deleted_at` — consistent suffix `_at`.
|
|
|
|
### Lifecycle Awareness (Data P8)
|
|
- `deleted_at` for soft delete. Lifecycle is first-class.
|
|
- The unique index on email is partial: `WHERE deleted_at IS NULL` — allows re-registration after soft delete.
|
|
- Every query must filter `deleted_at IS NULL` (a discipline, not a schema property).
|
|
|
|
### Indexing with Intent (Data P5, P10)
|
|
- `users_email_idx` — queries by email (login, lookup). Partial (excludes soft-deleted).
|
|
- `users_created_at_idx` — list users by recency. DESC matches the typical query.
|
|
- `orders_user_id_idx` — list a user's orders. FK index (join performance).
|
|
- `orders_status_created_idx` — composite for "open orders by recency" (`WHERE status = 'pending' ORDER BY created_at DESC`).
|
|
- No index on every column. Each index serves a query.
|
|
|
|
### Migration Safety (Data P4)
|
|
- This schema is created via a migration with an `up` and a `down`.
|
|
- The `down` drops the tables in reverse order (orders, then users) to respect FKs.
|
|
- Adding a column later uses expand-contract (nullable first, then constrained).
|
|
|
|
## What This Example Does NOT Do (And Why That's Good)
|
|
|
|
- Does not use `FLOAT` for money — floating point errors (P7, P1).
|
|
- Does not use `VARCHAR` for the UUID — wrong type (P7).
|
|
- Does not omit the FK on `orders.user_id` — unenforced relationship (P9).
|
|
- Does not index every column — write amplification (P10, C8).
|
|
- Does not use `is_deleted BOOLEAN` without a timestamp — loses the deletion time (P8).
|
|
- Does not allow `status` to be free text — would lose the finite domain (P3). |