Files

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_idx UNIQUE on hash — supports fast duplicate detection

Validation rules:

  • mime_type MUST be one of: image/jpeg, image/png, image/gif, image/webp
  • size_bytes MUST be > 0 and ≤ MAX_UPLOAD_BYTES (default 52 428 800)
  • hash MUST 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_idx UNIQUE on name — supports upsert by name
  • tags_name_prefix_idx on name with varchar_pattern_ops — supports prefix search (LIKE 'prefix%')

Validation rules:

  • name MUST 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: images table
  • M2: tags table + image_tags table + indexes