ALTER TYPE twinoid_user_id RENAME TO twinoid_user_id_old; -- Twinoid id CREATE DOMAIN twinoid_user_id AS u32; -- User id local to a site CREATE DOMAIN twinoid_site_user_id AS u32; CREATE DOMAIN twinoid_site_id AS u32; CREATE DOMAIN twinoid_site_info_id AS u32; CREATE DOMAIN twinoid_site_name AS VARCHAR(32); CREATE DOMAIN twinoid_site_host AS VARCHAR(32); CREATE DOMAIN twinoid_site_icon_tag AS VARCHAR(32); CREATE DOMAIN twinoid_stat_key AS VARCHAR(128); CREATE DOMAIN twinoid_achievement_key AS VARCHAR(128); CREATE DOMAIN url AS VARCHAR(2048); CREATE DOMAIN html_fragment AS TEXT; CREATE TYPE twinoid_site_status AS ENUM ('hide', 'soon', 'beta', 'open'); CREATE TYPE twinoid_site_icon_type AS ENUM ('icon'); CREATE TYPE twinoid_gender AS ENUM ('female', 'male'); CREATE TYPE old_twinoid_user_display_name AS ( "value" twinoid_user_display_name, "until" instant ); CREATE DOMAIN old_twinoid_user_display_name_array AS old_twinoid_user_display_name[]; -- This is a separate table so we can attach download CREATE TABLE remote_image ( "url" url NOT NULL, "created_at" INSTANT NOT NULL, PRIMARY KEY (url) ); CREATE TABLE twinoid_site ( "twinoid_site_id" twinoid_site_id NOT NULL, "retrieved_at" INSTANT NOT NULL, PRIMARY KEY (twinoid_site_id) ); CREATE TABLE twinoid_site_history ( "period" period_lower NOT NULL, "retrieved_at" instant_set NOT NULL, "twinoid_site_id" twinoid_site_id NOT NULL, "name" twinoid_site_name NOT NULL, "host" twinoid_site_host NOT NULL, "icon" url NOT NULL, "locale" locale_id NULL, "like_url" url NOT NULL, "like_title" html_fragment NULL, "status" twinoid_site_status NOT NULL, PRIMARY KEY (period, twinoid_site_id), EXCLUDE USING gist (twinoid_site_id WITH =, period WITH &&), CONSTRAINT site__twinoid_site__fk FOREIGN KEY (twinoid_site_id) REFERENCES twinoid_site(twinoid_site_id) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT icon__remote_image__fk FOREIGN KEY (icon) REFERENCES remote_image(url) ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE twinoid_site_like_count ( "period" period_lower NOT NULL, "retrieved_at" instant_set NOT NULL, "twinoid_site_id" twinoid_site_id NOT NULL, "like_count" u32 NOT NULL, PRIMARY KEY (period, twinoid_site_id), EXCLUDE USING gist (twinoid_site_id WITH =, period WITH &&), CONSTRAINT site__twinoid_site__fk FOREIGN KEY (twinoid_site_id) REFERENCES twinoid_site(twinoid_site_id) ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE twinoid_site_info ( "period" period_lower NOT NULL, "retrieved_at" instant_set NOT NULL, "twinoid_site_info_id" twinoid_site_info_id NOT NULL, "twinoid_site_id" twinoid_site_id NOT NULL, "locale" locale_id NOT NULL, "cover" url NOT NULL, "tag_line" html_fragment NULL, "description" html_fragment NOT NULL, "tid" html_fragment NOT NULL, PRIMARY KEY (period, twinoid_site_info_id), EXCLUDE USING gist (twinoid_site_info_id WITH =, period WITH &&), EXCLUDE USING gist (twinoid_site_id WITH =, locale WITH =, period WITH &&), CONSTRAINT cover__remote_image__fk FOREIGN KEY (cover) REFERENCES remote_image(url) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT site__twinoid_site__fk FOREIGN KEY (twinoid_site_id) REFERENCES twinoid_site(twinoid_site_id) ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE twinoid_site_icon ( "period" period_lower NOT NULL, "retrieved_at" instant_set NOT NULL, "twinoid_site_id" twinoid_site_id NOT NULL, "tag" twinoid_site_icon_tag NOT NULL, "alt_tag" twinoid_site_icon_tag NULL, "url" url NOT NULL, "type" twinoid_site_icon_type NOT NULL, "_rank" u32 NOT NULL, PRIMARY KEY (period, twinoid_site_id, tag), EXCLUDE USING gist (twinoid_site_id WITH =, _rank WITH =, period WITH &&), CONSTRAINT icon__remote_image__fk FOREIGN KEY (url) REFERENCES remote_image(url) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT site__twinoid_site__fk FOREIGN KEY (twinoid_site_id) REFERENCES twinoid_site(twinoid_site_id) ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE twinoid_user ( "twinoid_user_id" twinoid_user_id NOT NULL, "retrieved_at" INSTANT NOT NULL, PRIMARY KEY (twinoid_user_id) ); CREATE TABLE twinoid_user_history ( "period" period_lower NOT NULL, "retrieved_at" instant_set NOT NULL, "twinoid_user_id" twinoid_user_id NOT NULL, "picture" url NULL, "locale" locale_id NOT NULL, "title" html_fragment NULL, "like_url" url NOT NULL, "like_count" u32 NOT NULL, "gender" twinoid_gender NULL, "birthday" date NULL, PRIMARY KEY (period, twinoid_user_id), EXCLUDE USING gist (twinoid_user_id WITH =, period WITH &&), CONSTRAINT user__twinoid_user__fk FOREIGN KEY (twinoid_user_id) REFERENCES twinoid_user(twinoid_user_id) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT picture__remote_image__fk FOREIGN KEY (picture) REFERENCES remote_image(url) ON DELETE RESTRICT ON UPDATE CASCADE ); -- One way contact state, `friend` in the Twinoid API implies a two-way contact. -- `is_contact` indicates the state -- - `true`: user `twinoid_user_id` added `target` to its contact -- - `false`: user `twinoid_user_id` removed `target` from its contact -- No entry means "unknown", so `false` is a stronger statement. CREATE TABLE twinoid_contact ( "period" period_lower NOT NULL, "retrieved_at" instant_set NOT NULL, "twinoid_user_id" twinoid_user_id NOT NULL, "target" twinoid_user_id NOT NULL, "is_contact" bool NOT NULL, PRIMARY KEY (period, "twinoid_user_id", "target"), EXCLUDE USING gist ("twinoid_user_id" WITH =, "target" WITH =, period WITH &&), CONSTRAINT user__twinoid_user__fk FOREIGN KEY ("twinoid_user_id") REFERENCES twinoid_user(twinoid_user_id) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT target__twinoid_user__fk FOREIGN KEY ("target") REFERENCES twinoid_user(twinoid_user_id) ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE twinoid_user_display_name_history ( "period" period_lower NOT NULL, "retrieved_at" instant_set NOT NULL, "twinoid_user_id" twinoid_user_id NOT NULL, "display_name" twinoid_user_display_name NOT NULL, PRIMARY KEY (period, "twinoid_user_id"), EXCLUDE USING gist ("twinoid_user_id" WITH =, period WITH &&), CONSTRAINT user__twinoid_user__fk FOREIGN KEY ("twinoid_user_id") REFERENCES twinoid_user(twinoid_user_id) ON DELETE RESTRICT ON UPDATE CASCADE ); -- Fields that can be controlled by the user (potentially breaking the API due to encoding issues) CREATE TABLE twinoid_user_custom_fields ( "period" period_lower NOT NULL, "retrieved_at" instant_set NOT NULL, "twinoid_user_id" twinoid_user_id NOT NULL, "description" html_fragment NULL, "status" html_fragment NULL, "old_names" old_twinoid_user_display_name[] NOT NULL, "city" text NULL, "country" text NULL, PRIMARY KEY (period, "twinoid_user_id"), EXCLUDE USING gist ("twinoid_user_id" WITH =, period WITH &&), CONSTRAINT user__twinoid_user__fk FOREIGN KEY ("twinoid_user_id") REFERENCES twinoid_user(twinoid_user_id) ON DELETE RESTRICT ON UPDATE CASCADE ); -- Immutable site user CREATE TABLE twinoid_site_user ( "twinoid_site_id" twinoid_site_id NOT NULL, "twinoid_site_user_id" twinoid_site_user_id NOT NULL, "retrieved_at" INSTANT NOT NULL, PRIMARY KEY (twinoid_site_id, twinoid_site_user_id), CONSTRAINT site__twinoid_site__fk FOREIGN KEY ("twinoid_site_id") REFERENCES twinoid_site(twinoid_site_id) ON DELETE RESTRICT ON UPDATE CASCADE ); -- Link from a Twinoid user to a site user. CREATE TABLE twinoid_site_user_link ( "period" period_lower NOT NULL, "retrieved_at" instant_set NOT NULL, "twinoid_user_id" twinoid_user_id NOT NULL, "twinoid_site_id" twinoid_site_id NOT NULL, -- `null` if not linked "twinoid_site_user_id" twinoid_site_user_id NULL, PRIMARY KEY (period, "twinoid_user_id", "twinoid_site_id"), EXCLUDE USING gist ("twinoid_user_id" WITH =, "twinoid_site_id" WITH =, period WITH &&), EXCLUDE USING gist ("twinoid_site_id" WITH =, "twinoid_site_user_id" WITH =, period WITH &&), CONSTRAINT user__twinoid_user__fk FOREIGN KEY ("twinoid_user_id") REFERENCES twinoid_user(twinoid_user_id) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT site_user__twinoid_site_user__fk FOREIGN KEY ("twinoid_site_id", "twinoid_site_user_id") REFERENCES twinoid_site_user("twinoid_site_id", "twinoid_site_user_id") ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE twinoid_stat ( "twinoid_site_id" twinoid_site_id NOT NULL, "twinoid_stat_key" twinoid_stat_key NOT NULL, "name" text NOT NULL, "description" html_fragment NULL, "icon" url NULL, "rare" i32 NOT NULL, "social" boolean NOT NULL, PRIMARY KEY ("twinoid_site_id", "twinoid_stat_key"), CONSTRAINT site__twinoid_site__fk FOREIGN KEY ("twinoid_site_id") REFERENCES twinoid_site("twinoid_site_id") ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT icon__remote_image__fk FOREIGN KEY ("icon") REFERENCES remote_image("url") ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE twinoid_achievement ( "twinoid_site_id" twinoid_site_id NOT NULL, "twinoid_achievement_key" twinoid_achievement_key NOT NULL, "name" text NOT NULL, "stat" twinoid_stat_key NOT NULL, "score" i32 NOT NULL, "points" i32 NOT NULL, "npoints" float8 NOT NULL, "description" html_fragment NOT NULL, "rank" i32 NOT NULL, "data_type" text NOT NULL, "data_title" html_fragment NULL, "data_url" url NULL, "data_prefix" boolean NULL, "data_suffix" boolean NULL, PRIMARY KEY ("twinoid_site_id", "twinoid_achievement_key"), CONSTRAINT site__twinoid_site__fk FOREIGN KEY ("twinoid_site_id") REFERENCES twinoid_site("twinoid_site_id") ON DELETE RESTRICT ON UPDATE CASCADE, -- Some achievements do not have a corresponding stat exposed in the API (such as the achievement `2_hippo_0` with stat `hippo` -- CONSTRAINT stat__twinoid_stat__fk FOREIGN KEY ("twinoid_site_id", "stat") REFERENCES twinoid_stat("twinoid_site_id", "twinoid_stat_key") ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT data_url__remote_image__fk FOREIGN KEY ("data_url") REFERENCES remote_image("url") ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE DOMAIN twinoid_user_stat_map_id AS UUID; -- Immutable stats (may be shared by different users) CREATE TABLE twinoid_user_stat_map ( "twinoid_user_stat_map_id" twinoid_user_stat_map_id NOT NULL, "twinoid_site_id" twinoid_site_id NOT NULL, -- sha3_256(utf8(json(value))) -- Where `value` is a map from the twinoid_stat_key to the score, sorted by key and json does not use any whitespace -- {"bar":0,"foo":9}. It is prefixed by the site id. _sha3_256 BYTEA NOT NULL, PRIMARY KEY (twinoid_user_stat_map_id), UNIQUE (_sha3_256), UNIQUE (twinoid_user_stat_map_id, twinoid_site_id), CONSTRAINT site__twinoid_site__fk FOREIGN KEY ("twinoid_site_id") REFERENCES twinoid_site("twinoid_site_id") ON DELETE RESTRICT ON UPDATE CASCADE ); -- Content of twinoid_user_stat_map CREATE TABLE twinoid_user_stat_map_item ( twinoid_user_stat_map_id twinoid_user_stat_map_id NOT NULL, twinoid_site_id twinoid_site_id NOT NULL, twinoid_stat_key twinoid_stat_key NOT NULL, score i32 NOT NULL, PRIMARY KEY (twinoid_user_stat_map_id, twinoid_stat_key), CONSTRAINT twinoid_user_stat_map_item__map__fk FOREIGN KEY (twinoid_user_stat_map_id, twinoid_site_id) REFERENCES twinoid_user_stat_map(twinoid_user_stat_map_id, twinoid_site_id) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT twinoid_user_stat_map_item__achievement__fk FOREIGN KEY (twinoid_site_id, twinoid_stat_key) REFERENCES twinoid_stat(twinoid_site_id, twinoid_stat_key) ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE DOMAIN twinoid_user_achievement_set_id AS UUID; -- Immutable unlocked items state (may be shared by different users) CREATE TABLE twinoid_user_achievement_set ( "twinoid_user_achievement_set_id" twinoid_user_achievement_set_id NOT NULL, "twinoid_site_id" twinoid_site_id NOT NULL, -- sha3_256(utf8(json(value))) -- Where `value` is a sorted list of twinoid_achievement_key and json does not -- use any whitespace. It is prefixed by the site id. -- [1,["bar","foo"]] "_sha3_256" BYTEA NOT NULL, PRIMARY KEY ("twinoid_user_achievement_set_id"), UNIQUE ("_sha3_256"), UNIQUE (twinoid_user_achievement_set_id, twinoid_site_id), CONSTRAINT site__twinoid_site__fk FOREIGN KEY ("twinoid_site_id") REFERENCES twinoid_site("twinoid_site_id") ON DELETE RESTRICT ON UPDATE CASCADE ); -- Content of twinoid_user_achievement_set CREATE TABLE twinoid_user_achievement_set_item ( twinoid_user_achievement_set_id twinoid_user_achievement_set_id NOT NULL, twinoid_site_id twinoid_site_id NOT NULL, twinoid_achievement_key twinoid_achievement_key NOT NULL, PRIMARY KEY (twinoid_user_achievement_set_id, twinoid_achievement_key), CONSTRAINT twinoid_user_achievement_set_item__set__fk FOREIGN KEY (twinoid_user_achievement_set_id, twinoid_site_id) REFERENCES twinoid_user_achievement_set(twinoid_user_achievement_set_id, twinoid_site_id) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT twinoid_user_achievement_set_item__achievement__fk FOREIGN KEY (twinoid_site_id, twinoid_achievement_key) REFERENCES twinoid_achievement(twinoid_site_id, twinoid_achievement_key) ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE twinoid_site_user_history ( "period" period_lower NOT NULL, "retrieved_at" instant_set NOT NULL, "twinoid_site_id" twinoid_site_id NOT NULL, "twinoid_site_user_id" twinoid_site_user_id NOT NULL, "link" text NULL, "stats" twinoid_user_stat_map_id NULL, "achievements" twinoid_user_achievement_set_id NULL, "points" i32 NULL, "npoints" float8 NULL, PRIMARY KEY (period, "twinoid_site_id", "twinoid_site_user_id"), EXCLUDE USING gist ("twinoid_site_id" WITH =, "twinoid_site_user_id" WITH =, period WITH &&), CONSTRAINT user__twinoid_site_user__fk FOREIGN KEY ("twinoid_site_id", "twinoid_site_user_id") REFERENCES twinoid_site_user("twinoid_site_id", "twinoid_site_user_id") ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT stat_map__fk FOREIGN KEY ("stats", "twinoid_site_id") REFERENCES twinoid_user_stat_map("twinoid_user_stat_map_id", "twinoid_site_id") ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT achievement_set__fk FOREIGN KEY ("achievements", "twinoid_site_id") REFERENCES twinoid_user_achievement_set("twinoid_user_achievement_set_id", "twinoid_site_id") ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE twinoid_user_achievement_unlock_time ( "twinoid_site_id" twinoid_site_id NOT NULL, "twinoid_site_user_id" twinoid_site_user_id NOT NULL, "twinoid_achievement_key" twinoid_achievement_key NOT NULL, "unlocked_at" instant NULL, PRIMARY KEY ("twinoid_site_id", "twinoid_achievement_key"), CONSTRAINT achievement_unlock__achievement__fk FOREIGN KEY ("twinoid_site_id", "twinoid_achievement_key") REFERENCES twinoid_achievement("twinoid_site_id", "twinoid_achievement_key") ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT user__twinoid_site_user__fk FOREIGN KEY ("twinoid_site_id", "twinoid_site_user_id") REFERENCES twinoid_site_user("twinoid_site_id", "twinoid_site_user_id") ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE twinoid_access_token ( period PERIOD_LOWER NOT NULL, retrieved_at INSTANT_SET NOT NULL, _twinoid_access_token_hash BYTEA NOT NULL, -- twinoid_user_id twinoid_user_id NOT NULL, expired_at INSTANT NOT NULL, -- twinoid_access_token BYTEA NOT NULL, PRIMARY KEY (period, _twinoid_access_token_hash), EXCLUDE USING gist (_twinoid_access_token_hash WITH =, period WITH &&), EXCLUDE USING gist (twinoid_user_id WITH =, period WITH &&), CONSTRAINT twinoid_access_token__twinoid_user__fk FOREIGN KEY (twinoid_user_id) REFERENCES twinoid_user(twinoid_user_id) ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE twinoid_refresh_token ( period PERIOD_LOWER NOT NULL, retrieved_at INSTANT_SET NOT NULL, _twinoid_refresh_token_hash BYTEA NOT NULL, -- twinoid_user_id twinoid_user_id NOT NULL, -- twinoid_refresh_token BYTEA NOT NULL, PRIMARY KEY (period, _twinoid_refresh_token_hash), EXCLUDE USING gist (_twinoid_refresh_token_hash WITH =, period WITH &&), EXCLUDE USING gist (twinoid_user_id WITH =, period WITH &&), CONSTRAINT twinoid_refresh_token__twinoid_user__fk FOREIGN KEY (twinoid_user_id) REFERENCES twinoid_user(twinoid_user_id) ON DELETE RESTRICT ON UPDATE CASCADE );