CREATE TYPE usertitle_status_t AS ENUM ('finished', 'planned', 'dropped', 'in-progress'); CREATE TYPE storage_type_t AS ENUM ('local', 's3'); CREATE TYPE title_status_t AS ENUM ('finished', 'ongoing', 'planned'); CREATE TYPE release_season_t AS ENUM ('winter', 'spring', 'summer', 'fall'); CREATE TABLE providers ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, provider_name text NOT NULL, credentials jsonb ); CREATE TABLE tags ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- example: { "ru": "Сёдзё", "en": "Shojo", "jp": "少女"} tag_names jsonb NOT NULL ); CREATE TABLE images ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, storage_type storage_type_t NOT NULL, image_path text UNIQUE NOT NULL ); CREATE TABLE users ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, avatar_id bigint REFERENCES images (id) ON DELETE SET NULL, passhash text NOT NULL, mail text CHECK (mail ~ '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+$'), nickname text UNIQUE NOT NULL CHECK (nickname ~ '^[a-zA-Z0-9_-]{3,}$'), disp_name text, user_desc text, creation_date timestamptz NOT NULL DEFAULT NOW(), last_login timestamptz ); CREATE TABLE studios ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, studio_name text NOT NULL UNIQUE, illust_id bigint REFERENCES images (id) ON DELETE SET NULL, studio_desc text ); CREATE TABLE titles ( -- // TODO: anime type (film, season etc) id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- example {"ru": ["Атака титанов", "Атака Титанов"],"en": ["Attack on Titan", "AoT"],"ja": ["進撃の巨人", "しんげきのきょじん"]} title_names jsonb NOT NULL, -- example {"ru": "Кулинарное аниме как правильно приготовить людей.","en": "A culinary anime about how to cook people properly."} title_desc jsonb, studio_id bigint NOT NULL REFERENCES studios (id), poster_id bigint REFERENCES images (id) ON DELETE SET NULL, title_status title_status_t NOT NULL, rating float CHECK (rating >= 0 AND rating <= 10), rating_count int CHECK (rating_count >= 0), release_year int CHECK (release_year >= 1900), release_season release_season_t, season int CHECK (season >= 0), episodes_aired int CHECK (episodes_aired >= 0), episodes_all int CHECK (episodes_all >= 0), -- example { "1": "50.50", "2": "23.23"} episodes_len jsonb, CHECK ((episodes_aired IS NULL AND episodes_all IS NULL) OR (episodes_aired IS NOT NULL AND episodes_all IS NOT NULL AND episodes_aired <= episodes_all)) ); CREATE TABLE reviews ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, data text NOT NULL, rating int CHECK (rating >= 0 AND rating <= 10), user_id bigint REFERENCES users (id) ON DELETE SET NULL, title_id bigint REFERENCES titles (id) ON DELETE CASCADE, created_at timestamptz DEFAULT NOW() ); CREATE TABLE review_images ( PRIMARY KEY (review_id, image_id), review_id bigint NOT NULL REFERENCES reviews(id) ON DELETE CASCADE, image_id bigint NOT NULL REFERENCES images(id) ON DELETE CASCADE ); CREATE TABLE usertitles ( PRIMARY KEY (user_id, title_id), user_id bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE, title_id bigint NOT NULL REFERENCES titles (id) ON DELETE CASCADE, status usertitle_status_t NOT NULL, rate int CHECK (rate > 0 AND rate <= 10), review_id bigint REFERENCES reviews (id) ON DELETE SET NULL, ctime timestamptz NOT NULL DEFAULT now() -- // TODO: series status ); CREATE TABLE title_tags ( PRIMARY KEY (title_id, tag_id), title_id bigint NOT NULL REFERENCES titles (id) ON DELETE CASCADE, tag_id bigint NOT NULL REFERENCES tags (id) ON DELETE CASCADE ); CREATE TABLE signals ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title_id bigint REFERENCES titles (id), raw_data jsonb NOT NULL, provider_id bigint NOT NULL REFERENCES providers (id), pending boolean NOT NULL ); CREATE TABLE external_services ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text UNIQUE NOT NULL ); CREATE TABLE external_ids ( user_id bigint NOT NULL REFERENCES users (id), service_id bigint REFERENCES external_services (id), external_id text NOT NULL ); -- Functions CREATE OR REPLACE FUNCTION update_title_rating() RETURNS TRIGGER AS $$ BEGIN IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE' AND NEW.rate IS DISTINCT FROM OLD.rate) THEN UPDATE titles SET rating = sub.avg_rating, rating_count = sub.rating_count FROM ( SELECT title_id, AVG(rate)::float AS avg_rating, COUNT(rate) AS rating_count FROM usertitles WHERE title_id = NEW.title_id AND rate IS NOT NULL GROUP BY title_id ) AS sub WHERE titles.id = sub.title_id; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION notify_new_signal() RETURNS TRIGGER AS $$ DECLARE payload JSON; BEGIN payload := json_build_object( 'signal_id', NEW.id, 'title_id', NEW.title_id, 'provider_id', NEW.provider_id, 'pending', NEW.pending, 'timestamp', NOW() ); PERFORM pg_notify('new_signal', payload::text); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Triggers CREATE TRIGGER trg_update_title_rating AFTER INSERT OR UPDATE OF rate ON usertitles FOR EACH ROW EXECUTE FUNCTION update_title_rating(); CREATE TRIGGER trg_notify_new_signal AFTER INSERT ON signals FOR EACH ROW EXECUTE FUNCTION notify_new_signal();