-- Virtual Art Gallery — canonical PostgreSQL schema -- Applied by: npm run dev:migrate (server/migrate.js) -- Incremental ALTER migrations in db/migrate-*.sql run after this on existing databases. CREATE TABLE IF NOT EXISTS historical_eras ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, start_year INTEGER NOT NULL, end_year INTEGER NOT NULL, start_definite BOOLEAN DEFAULT true, end_definite BOOLEAN DEFAULT true, description TEXT, sort_order INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS art_movements ( id SERIAL PRIMARY KEY, name VARCHAR(150) NOT NULL, start_year INTEGER NOT NULL, end_year INTEGER NOT NULL, start_definite BOOLEAN DEFAULT true, end_definite BOOLEAN DEFAULT true, era_id INTEGER REFERENCES historical_eras(id), description TEXT, color VARCHAR(20) DEFAULT '#8B7355' ); CREATE TABLE IF NOT EXISTS artists ( id SERIAL PRIMARY KEY, name VARCHAR(200) NOT NULL, birth_year INTEGER, death_year INTEGER, movement_id INTEGER REFERENCES art_movements(id), portrait_path VARCHAR(500), portrait_thumb_path VARCHAR(500), bio_short TEXT, bio_full TEXT, wikipedia_title VARCHAR(300), century INTEGER, checkup_checked BOOLEAN NOT NULL DEFAULT false, checkup_fixed BOOLEAN NOT NULL DEFAULT false, palette_metadata JSONB ); CREATE TABLE IF NOT EXISTS artist_periods ( id SERIAL PRIMARY KEY, artist_id INTEGER NOT NULL REFERENCES artists(id) ON DELETE CASCADE, name VARCHAR(200) NOT NULL, start_year INTEGER, end_year INTEGER, description TEXT, sort_order INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS paintings ( id SERIAL PRIMARY KEY, artist_id INTEGER NOT NULL REFERENCES artists(id) ON DELETE CASCADE, period_id INTEGER REFERENCES artist_periods(id), title VARCHAR(300) NOT NULL, year INTEGER, year_end INTEGER, description TEXT, image_path VARCHAR(500), thumbnail_path VARCHAR(500), wikipedia_title VARCHAR(300), sort_order INTEGER DEFAULT 0, checkup_checked BOOLEAN NOT NULL DEFAULT false, checkup_fixed BOOLEAN NOT NULL DEFAULT false ); CREATE TABLE IF NOT EXISTS painting_influences ( id SERIAL PRIMARY KEY, painting_id INTEGER NOT NULL REFERENCES paintings(id) ON DELETE CASCADE, influenced_by_painting_id INTEGER NOT NULL REFERENCES paintings(id) ON DELETE CASCADE, notes TEXT, source VARCHAR(500), aspects TEXT, quote TEXT, source_author VARCHAR(200), source_url VARCHAR(500), UNIQUE(painting_id, influenced_by_painting_id) ); CREATE TABLE IF NOT EXISTS painting_influence_sources ( id SERIAL PRIMARY KEY, painting_id INTEGER NOT NULL REFERENCES paintings(id) ON DELETE CASCADE, source_type VARCHAR(20) NOT NULL CHECK (source_type IN ('painting', 'artist', 'movement')), source_painting_id INTEGER REFERENCES paintings(id) ON DELETE CASCADE, source_artist_id INTEGER REFERENCES artists(id) ON DELETE CASCADE, source_movement_id INTEGER REFERENCES art_movements(id) ON DELETE CASCADE, period_note VARCHAR(240), period_start_year INTEGER, period_end_year INTEGER, notes TEXT, source VARCHAR(500), aspects TEXT, quote TEXT, source_author VARCHAR(200), source_url VARCHAR(500), discovered_via VARCHAR(120), confidence VARCHAR(20) NOT NULL DEFAULT 'curated', CONSTRAINT painting_influence_sources_source_shape CHECK ( (source_type = 'painting' AND source_painting_id IS NOT NULL AND source_artist_id IS NULL AND source_movement_id IS NULL) OR (source_type = 'artist' AND source_artist_id IS NOT NULL AND source_painting_id IS NULL AND source_movement_id IS NULL) OR (source_type = 'movement' AND source_movement_id IS NOT NULL AND source_painting_id IS NULL AND source_artist_id IS NULL) ) ); CREATE TABLE IF NOT EXISTS painting_annotations ( id SERIAL PRIMARY KEY, painting_id INTEGER NOT NULL REFERENCES paintings(id) ON DELETE CASCADE, label VARCHAR(80), body TEXT NOT NULL, category VARCHAR(30) NOT NULL DEFAULT 'subject', pos_x NUMERIC(5, 2), pos_y NUMERIC(5, 2), source_author VARCHAR(200), source VARCHAR(500), source_url VARCHAR(500), sort_order INTEGER NOT NULL DEFAULT 0, confidence VARCHAR(20) NOT NULL DEFAULT 'curated' ); CREATE INDEX IF NOT EXISTS idx_artists_movement ON artists(movement_id); CREATE INDEX IF NOT EXISTS idx_artists_birth_year ON artists(birth_year); CREATE INDEX IF NOT EXISTS idx_artists_death_year ON artists(death_year); CREATE INDEX IF NOT EXISTS idx_artists_century ON artists(century); CREATE INDEX IF NOT EXISTS artists_checkup_checked_idx ON artists (checkup_checked); CREATE INDEX IF NOT EXISTS artists_checkup_fixed_idx ON artists (checkup_fixed); CREATE INDEX IF NOT EXISTS artists_palette_metadata_idx ON artists USING gin (palette_metadata); CREATE INDEX IF NOT EXISTS idx_paintings_artist ON paintings(artist_id); CREATE INDEX IF NOT EXISTS idx_paintings_period ON paintings(period_id); CREATE INDEX IF NOT EXISTS paintings_checkup_checked_idx ON paintings (checkup_checked); CREATE INDEX IF NOT EXISTS paintings_checkup_fixed_idx ON paintings (checkup_fixed); CREATE INDEX IF NOT EXISTS idx_movements_era ON art_movements(era_id); CREATE INDEX IF NOT EXISTS idx_movements_years ON art_movements(start_year, end_year); CREATE INDEX IF NOT EXISTS idx_influences_painting ON painting_influences(painting_id); CREATE INDEX IF NOT EXISTS idx_influences_by ON painting_influences(influenced_by_painting_id); CREATE UNIQUE INDEX IF NOT EXISTS painting_influence_sources_unique ON painting_influence_sources ( painting_id, source_type, COALESCE(source_painting_id, 0), COALESCE(source_artist_id, 0), COALESCE(source_movement_id, 0) ); CREATE INDEX IF NOT EXISTS painting_influence_sources_painting_idx ON painting_influence_sources (painting_id); CREATE INDEX IF NOT EXISTS idx_influence_sources_source_painting ON painting_influence_sources (source_painting_id); CREATE INDEX IF NOT EXISTS painting_influence_sources_artist_idx ON painting_influence_sources (source_artist_id); CREATE INDEX IF NOT EXISTS painting_influence_sources_movement_idx ON painting_influence_sources (source_movement_id); CREATE INDEX IF NOT EXISTS painting_annotations_painting_idx ON painting_annotations (painting_id);