Files
Art-gallery/db/schema.sql
T
Danila KhodjaefandCursor 96285737f5 Rename npm scripts to environment-prefixed names (dev:/prod:/devtoprod:/prodto:dev:/infra:).
Align package.json scripts and their references across scripts/, infra/, and db/ SQL with the dev-first workflow naming scheme.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 12:37:21 +03:00

166 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,
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);