-- ============================================================ -- Родословное дерево семьи — SQL-схема (PostgreSQL 15+) -- ============================================================ -- Кодировка UTF-8. Запуск: psql -U postgres -d family_tree -f schema.sql CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- быстрый поиск по имени/фамилии -- ---------- Пользователи и роли ---------- CREATE TYPE user_role AS ENUM ('admin', 'editor', 'viewer'); CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, display_name TEXT NOT NULL, role user_role NOT NULL DEFAULT 'viewer', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ---------- Персоны ---------- CREATE TYPE gender_t AS ENUM ('male', 'female', 'other'); CREATE TABLE persons ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), first_name TEXT NOT NULL, last_name TEXT, middle_name TEXT, gender gender_t NOT NULL DEFAULT 'other', birth_date DATE, death_date DATE, birth_place TEXT, photo_url TEXT, bio TEXT, contacts TEXT, notes TEXT, context TEXT, -- произвольный дополнительный контекст resource_url TEXT, -- ссылка на подробный внешний ресурс created_by UUID REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_persons_last_name ON persons USING gin (last_name gin_trgm_ops); CREATE INDEX idx_persons_first_name ON persons USING gin (first_name gin_trgm_ops); -- ---------- Связь родитель → ребёнок (вкл. приёмные) ---------- CREATE TYPE parent_kind AS ENUM ('biological', 'adoptive', 'step', 'guardian'); CREATE TABLE parent_child ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), parent_id UUID NOT NULL REFERENCES persons(id) ON DELETE CASCADE, child_id UUID NOT NULL REFERENCES persons(id) ON DELETE CASCADE, kind parent_kind NOT NULL DEFAULT 'biological', UNIQUE (parent_id, child_id), CHECK (parent_id <> child_id) ); CREATE INDEX idx_pc_parent ON parent_child(parent_id); CREATE INDEX idx_pc_child ON parent_child(child_id); -- ---------- Браки / партнёрства (несколько браков поддерживаются) ---------- CREATE TYPE union_status AS ENUM ('married', 'divorced', 'partner', 'widowed'); CREATE TABLE unions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), partner_a UUID NOT NULL REFERENCES persons(id) ON DELETE CASCADE, partner_b UUID NOT NULL REFERENCES persons(id) ON DELETE CASCADE, status union_status NOT NULL DEFAULT 'married', start_date DATE, end_date DATE, UNIQUE (partner_a, partner_b), CHECK (partner_a <> partner_b) ); -- ---------- Галерея фотографий ---------- CREATE TABLE media ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), person_id UUID NOT NULL REFERENCES persons(id) ON DELETE CASCADE, url TEXT NOT NULL, caption TEXT, sort_order INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_media_person ON media(person_id); -- ---------- История изменений (аудит) ---------- CREATE TABLE change_log ( id BIGSERIAL PRIMARY KEY, user_id UUID REFERENCES users(id) ON DELETE SET NULL, entity TEXT NOT NULL, -- 'person' | 'union' | 'parent_child' | ... entity_id UUID, action TEXT NOT NULL, -- 'create' | 'update' | 'delete' diff JSONB, -- было/стало created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_log_entity ON change_log(entity, entity_id); -- ---------- Авто-обновление updated_at ---------- CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS trigger AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_persons_touch BEFORE UPDATE ON persons FOR EACH ROW EXECUTE FUNCTION touch_updated_at(); CREATE TRIGGER trg_users_touch BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION touch_updated_at(); -- ---------- Удобное представление дерева ---------- CREATE VIEW person_relations AS SELECT p.id, p.first_name, p.last_name, p.middle_name, p.gender, p.birth_date, p.death_date, (SELECT array_agg(pc.parent_id) FROM parent_child pc WHERE pc.child_id = p.id) AS parent_ids, (SELECT array_agg(pc.child_id) FROM parent_child pc WHERE pc.parent_id = p.id) AS child_ids, (SELECT array_agg(CASE WHEN u.partner_a = p.id THEN u.partner_b ELSE u.partner_a END) FROM unions u WHERE u.partner_a = p.id OR u.partner_b = p.id) AS spouse_ids FROM persons p;