Paintings get editable curator notes with brass plates in the 3D hall, and visit order now uses the far/end wall between left and right. Co-authored-by: Cursor <cursoragent@cursor.com>
167 lines
6.3 KiB
SQL
167 lines
6.3 KiB
SQL
-- 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,
|
|
curator_notes TEXT NOT NULL DEFAULT '',
|
|
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);
|