nyanimedb/sql/migrations/000001_init.up.sql

173 lines
No EOL
7.2 KiB
PL/PgSQL

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(),
ftime 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,
auth_token text
);
CREATE TABLE external_ids (
user_id bigint NOT NULL REFERENCES users (id),
service_id bigint NOT NULL 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();