# Art Gallery — database structure PostgreSQL schema for the virtual gallery. Canonical DDL is intended to live in `db/schema.sql` when checked in; this document describes the logical model either way. Connection settings come from `.env` (see [setup.md](setup.md)). ## Overview | Item | Typical value | |------|----------------| | Engine | PostgreSQL 14+ | | Database | `Gallery` | | App user | `gallery` | | Time fields | Integer years (negative = BCE) | ## Entity relationship ```mermaid erDiagram historical_eras ||--o{ art_movements : contains art_movements ||--o{ artists : groups artists ||--o{ artist_periods : has artists ||--o{ paintings : created artist_periods ||--o{ paintings : groups paintings ||--o{ painting_influences : influenced_by paintings ||--o{ painting_influences : influences paintings ||--o{ painting_influence_sources : influenced_by artists ||--o{ painting_influence_sources : artist_source art_movements ||--o{ painting_influence_sources : movement_source ``` ## Tables ### `historical_eras` Broad chronological buckets (Ancient, Medieval, Renaissance, …). | Column | Type | Notes | |--------|------|-------| | `id` | SERIAL PK | | | `name` | VARCHAR(100) | Display label | | `start_year`, `end_year` | INTEGER | Inclusive range | | `start_definite`, `end_definite` | BOOLEAN | `false` → render as gradient edge on timeline | | `description` | TEXT | Tooltip / sidebar copy | | `sort_order` | INTEGER | Display order | ### `art_movements` Finer-grained styles (Impressionism, Cubism, Suprematism, …). | Column | Type | Notes | |--------|------|-------| | `id` | SERIAL PK | | | `name` | VARCHAR(150) | | | `start_year`, `end_year` | INTEGER | | | `start_definite`, `end_definite` | BOOLEAN | Same visual semantics as eras | | `era_id` | FK → `historical_eras` | Optional parent era | | `description` | TEXT | | | `color` | VARCHAR(20) | Hex colour for movement stream in the flow diagram | ### `artists` | Column | Type | Notes | |--------|------|-------| | `id` | SERIAL PK | | | `name` | VARCHAR(200) | | | `birth_year`, `death_year` | INTEGER | Nullable; used for timeline portrait placement | | `movement_id` | FK → `art_movements` | Primary movement | | `portrait_path` | VARCHAR(500) | Relative to `data/images/`; nullable after debug **Clear** | | `bio_short`, `bio_full` | TEXT | Wikipedia lead section (`npm run fetch-artist-bios`) | | `wikipedia_title` | VARCHAR(300) | Source page title | | `century` | INTEGER | Rounded century bucket for seeding limits | | `checkup_checked` | BOOLEAN NOT NULL DEFAULT false | Portrait reviewed in debug workflow (gold border on bio when true) | | `checkup_fixed` | BOOLEAN NOT NULL DEFAULT false | Portrait replaced, cleared, or uploaded via debug | Applied by `npm run migrate:artist-checkup-flags` (`db/migrate-artist-checkup-flags.sql`). ### `artist_periods` Phases within an artist’s career (e.g. “Blue Period”, “Roman Period”). | Column | Type | Notes | |--------|------|-------| | `id` | SERIAL PK | | | `artist_id` | FK → `artists` | ON DELETE CASCADE | | `name` | VARCHAR(200) | | | `start_year`, `end_year` | INTEGER | | | `description` | TEXT | | | `sort_order` | INTEGER | Wall order in 3D gallery | ### `paintings` | Column | Type | Notes | |--------|------|-------| | `id` | SERIAL PK | | | `artist_id` | FK → `artists` | ON DELETE CASCADE | | `period_id` | FK → `artist_periods` | Optional grouping | | `title` | VARCHAR(300) | | | `year`, `year_end` | INTEGER | Creation date(s) | | `description` | TEXT | | | `image_path` | VARCHAR(500) | Full-size local file; nullable after debug **Clear** | | `thumbnail_path` | VARCHAR(500) | Smaller variant for lists / 3D; nullable after **Clear** | | `wikipedia_title` | VARCHAR(300) | Used by image fetcher | | `sort_order` | INTEGER | | | `checkup_checked` | BOOLEAN NOT NULL DEFAULT false | Reviewed in image checkup workflow (UI label: **Reviewed**) | | `checkup_fixed` | BOOLEAN NOT NULL DEFAULT false | Image corrected, cleared, or uploaded via checkup / debug | When `checkup_fixed` is true, `checkup_checked` is set automatically and cannot be cleared until **Fixed** is off. A cleared painting (`image_path` and `thumbnail_path` both null, `checkup_fixed` true) is shown as an empty frame in detail view and is not refetched on demand. Applied by `npm run migrate:checkup-flags` (`db/migrate-checkup-flags.sql`). ### `painting_annotations` Short art-history notes shown on painting detail (`PaintingAnnotations.tsx`). | Column | Type | Notes | |--------|------|-------| | `id` | SERIAL PK | | | `painting_id` | FK → `paintings` | ON DELETE CASCADE | | `label` | VARCHAR(80) | Optional short heading (e.g. figure name) | | `body` | TEXT | Note text | | `category` | VARCHAR(30) | Default `subject`; also `technique`, `context`, `symbolism`, etc. | | `pos_x`, `pos_y` | NUMERIC(5,2) | Optional marker position on image (percent 0–100) | | `source_author` | VARCHAR(200) | e.g. Gombrich, Met catalog | | `source` | VARCHAR(500) | Citation label | | `source_url` | VARCHAR(500) | Reference link | | `sort_order` | INTEGER | Display order within the painting | | `confidence` | VARCHAR(20) | Default `curated`; Wikipedia pass uses `wikipedia` | Applied by `npm run migrate:painting-annotations` (`db/migrate-painting-annotations.sql`). Load data with `npm run update-painting-annotations` (curated entries in `scripts/painting-annotations-data.js`; add `--wikipedia` for intro sentences from each work’s `wikipedia_title`). ### `painting_influences` Directed edges: *this painting* was influenced by *that painting*. | Column | Type | Notes | |--------|------|-------| | `id` | SERIAL PK | | | `painting_id` | FK → `paintings` | The work being explained | | `influenced_by_painting_id` | FK → `paintings` | The earlier / source work | | `notes` | TEXT | Curator summary | | `source` | VARCHAR(500) | General attribution | | `aspects` | TEXT | What was borrowed (composition, colour, …) | | `quote` | TEXT | Short citation | | `source_author` | VARCHAR(200) | e.g. Gombrich, Janson | | `source_url` | VARCHAR(500) | Reference link | Unique constraint on `(painting_id, influenced_by_painting_id)`. Used by the 3D gallery exit panel: predecessors are artists of `influenced_by_painting_id` works; successors are artists of paintings influenced by this artist’s works (see [API.md](API.md#get-apiartistsidnavigation)). Legacy table; new polymorphic links live in `painting_influence_sources`. ### `painting_influence_sources` Polymorphic influence links: *this painting* was influenced by a **painting**, **artist**, or **movement**. | Column | Type | Notes | |--------|------|-------| | `id` | SERIAL PK | | | `painting_id` | FK → `paintings` | The work being explained | | `source_type` | VARCHAR(20) | `painting`, `artist`, or `movement` | | `source_painting_id` | FK → `paintings` | When `source_type = painting` | | `source_artist_id` | FK → `artists` | When `source_type = artist` | | `source_movement_id` | FK → `art_movements` | When `source_type = movement` | | `period_note` | VARCHAR(240) | Human-readable period label | | `period_start_year` | INTEGER | Optional span start | | `period_end_year` | INTEGER | Optional span end | | `notes` | TEXT | Curator summary | | `source` | VARCHAR(500) | General attribution | | `aspects` | TEXT | What was borrowed | | `quote` | TEXT | Short citation | | `source_author` | VARCHAR(200) | e.g. Gombrich, Janson | | `source_url` | VARCHAR(500) | Reference link | | `discovered_via` | VARCHAR(120) | e.g. `wikipedia`, `wikidata`, `web:metmuseum.org` | | `confidence` | VARCHAR(20) | `curated` (default) or `discovered` | Unique index on `(painting_id, source_type, source_painting_id, source_artist_id, source_movement_id)` with COALESCE for null FKs. Used by painting detail API (`influencedBy`). Painting-type rows also feed `has_influence_links` and hall navigation (with legacy `painting_influences`). ## Indexes - `artists(movement_id)`, `artists(century)` - `paintings(artist_id)`, `paintings(period_id)`, `paintings(checkup_checked)`, `paintings(checkup_fixed)` - `artists(checkup_checked)`, `artists(checkup_fixed)` - `art_movements(era_id)`, `art_movements(start_year, end_year)` - `painting_influences(painting_id)`, `painting_influences(influenced_by_painting_id)` - `painting_influence_sources(painting_id)`, `painting_influence_sources(source_artist_id)`, `painting_influence_sources(source_movement_id)` ## First-time setup The `gallery` database user needs `CREATE` on schema `public` for migrations. If tables cannot be created, run admin grants as PostgreSQL superuser before `npm run migrate`: ```sql GRANT CREATE ON SCHEMA public TO gallery; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gallery; GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO gallery; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO gallery; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO gallery; ``` Then: ```bash npm run migrate npm run seed npm run fetch-artist-bios npm run expand-catalog ``` ## Data conventions - **Year zero** is not used; BCE years are negative integers. - **Image paths** are relative to `IMAGE_DIR` (default `./data/images`), e.g. `portraits/Claude_Monet.jpg`, `paintings/thumbs/Raphael_The_School_of_Athens_thumb.jpg`. - **Seeding cap**: curated ingest targets at most ~100 artists per century to keep the catalog manageable. - **Catalog expansion**: `scripts/famous-paintings-data.js` plus `npm run expand-catalog` raises thin artist catalogs to at least six notable paintings (`MIN_PAINTINGS`, default 6). - **Biographies**: `bio_short` and `bio_full` are populated by `npm run fetch-artist-bios` from English Wikipedia lead sections; `wikipedia_title` on the artist row is the source article.