3.9 KiB
3.9 KiB
Data Model: Reaction Image Board v1
Date: 2026-05-02
Entities
Image
Represents a single uploaded image file.
| Field | Type | Constraints | Notes |
|---|---|---|---|
id |
UUID | PK, not null | Generated on insert |
hash |
VARCHAR(64) | UNIQUE, not null | SHA-256 hex digest of file bytes |
filename |
VARCHAR | not null | Original filename; display only |
mime_type |
VARCHAR(20) | not null | image/jpeg, image/png, image/gif, image/webp |
size_bytes |
BIGINT | not null, > 0 | File size |
width |
INTEGER | not null, > 0 | Pixel width |
height |
INTEGER | not null, > 0 | Pixel height |
storage_key |
VARCHAR(64) | not null | S3 object key; equals hash in v1 |
created_at |
TIMESTAMPTZ | not null, default now() | Set on insert; never updated |
Indexes:
images_hash_idxUNIQUE onhash— supports fast duplicate detection
Validation rules:
mime_typeMUST be one of:image/jpeg,image/png,image/gif,image/webpsize_bytesMUST be > 0 and ≤MAX_UPLOAD_BYTES(default 52 428 800)hashMUST be a 64-character lowercase hex string (SHA-256)
Tag
Represents a single normalised tag string.
| Field | Type | Constraints | Notes |
|---|---|---|---|
id |
UUID | PK, not null | Generated on insert |
name |
VARCHAR(64) | UNIQUE, not null | Normalised: lowercase, trimmed |
created_at |
TIMESTAMPTZ | not null, default now() | Set on insert |
Indexes:
tags_name_idxUNIQUE onname— supports upsert by nametags_name_prefix_idxonnamewithvarchar_pattern_ops— supports prefix search (LIKE 'prefix%')
Validation rules:
nameMUST match^[a-z0-9_-]{1,64}$(after normalisation)- Normalisation applied before validation: lowercase + whitespace trim
Lifecycle:
- Tags are created implicitly on first use; no explicit creation endpoint
- Tag records are never deleted even when all image associations are removed
ImageTag (join)
Many-to-many association between Image and Tag.
| Field | Type | Constraints | Notes |
|---|---|---|---|
image_id |
UUID | FK → images.id ON DELETE CASCADE | |
tag_id |
UUID | FK → tags.id ON DELETE RESTRICT |
Primary key: composite (image_id, tag_id)
Notes:
- Deleting an image cascades to all its ImageTag rows
- Deleting a tag is RESTRICT (not permitted while image associations exist)
- In practice, tags are never deleted in v1 so RESTRICT is never triggered
Relationships
Image ──< ImageTag >── Tag
(1) (M:M) (1)
- One Image has zero or more Tags (through ImageTag)
- One Tag is applied to zero or more Images (through ImageTag)
State Transitions
Image lifecycle
[upload received]
│
▼
Validate MIME + size
│
Compute SHA-256
│
Existing hash? ──yes──► return existing record (duplicate: true)
│
no
│
Write to S3
│
Insert images row
│
Upsert tags + insert image_tag rows
│
Return new record (duplicate: false)
│
[user deletes image]
│
Delete image_tag rows (cascade)
Delete images row
Delete S3 object
│
[gone]
Tag set update (PATCH)
[PATCH /api/v1/images/{id}/tags with new_tags=[...]]
│
Validate each tag name (post-normalisation)
│
Fetch current tag set for image
│
Compute removed = current \ new_tags
Compute added = new_tags \ current
│
Delete ImageTag rows for removed tags
Upsert Tag records for added tags
Insert ImageTag rows for added tags
│
Return full updated image record
Database Migration Strategy
- Alembic manages all schema changes
- Migration files are committed to
api/alembic/versions/ - Schema is applied on API startup (
alembic upgrade head) - M0: initial empty migration (no tables)
- M1:
imagestable - M2:
tagstable +image_tagstable + indexes