CREATE DOMAIN valid_period AS PERIOD CHECK (NOT LOWER_INF(value) AND NOT UPPER_INF(value));

CREATE DOMAIN i8 AS INT2 CHECK (-128 <= value AND value < 128);
CREATE DOMAIN u8 AS INT2 CHECK (0 <= value AND value < 256);
CREATE DOMAIN i16 AS INT2;
CREATE DOMAIN u16 AS INT4 CHECK (0 <= value AND value < 65536);
CREATE DOMAIN i32 AS INT4;
CREATE DOMAIN u32 AS INT8 CHECK (0 <= value AND value < 4294967296);
CREATE DOMAIN i64 AS INT8;

CREATE DOMAIN hammerfest_item_count_map_id AS UUID;
CREATE DOMAIN hammerfest_quest_status_map_id AS UUID;
CREATE DOMAIN hammerfest_unlocked_item_set_id AS UUID;

CREATE DOMAIN hammerfest_forum_theme_id AS VARCHAR(10) CHECK (value ~ '^[1-9]\d{0,9}$');
CREATE DOMAIN hammerfest_forum_thread_id AS VARCHAR(10) CHECK (value ~ '^[1-9]\d{0,9}$');
CREATE DOMAIN hammerfest_forum_message_id AS VARCHAR(10) CHECK (value ~ '^[1-9]\d{0,9}$');
CREATE DOMAIN hammerfest_item_id AS VARCHAR(4) CHECK (value ~ '^(?:0|[1-9]\d{0,3})$');
CREATE DOMAIN hammerfest_quest_id AS VARCHAR(4) CHECK (value ~ '^(?:0|[1-9]\d{0,3})$');
-- Pyramid level: 0 (Hall of Fame) to 4 (Level 4).
CREATE DOMAIN hammerfest_ladder_level AS U8 CHECK (value < 5);
CREATE DOMAIN hammerfest_forum_theme_title AS VARCHAR(100);
CREATE DOMAIN hammerfest_forum_theme_description AS VARCHAR(500);
CREATE DOMAIN hammerfest_forum_thread_title AS VARCHAR(100);

CREATE DOMAIN rfc_oauth_access_token_key AS TEXT;
CREATE DOMAIN rfc_oauth_refresh_token_key AS TEXT;

--- Checks that arr is an array of unique non-null values sorted in ascending order
CREATE OR REPLACE FUNCTION array_is_ordered_set(
  IN arr ANYARRAY
) RETURNS BOOLEAN
  LANGUAGE sql
  IMMUTABLE STRICT PARALLEL SAFE AS
$$
SELECT arr = (
  SELECT ARRAY_AGG(item)
  FROM (
    SELECT DISTINCT UNNEST(arr) AS item
    ORDER BY item ASC
  ) AS items
  WHERE item IS NOT NULL
);
$$;

--- Checks that arr is ascendingly-sorted array of unique non-null instants with at most 2 instants in any period of duration `sampling_window`
CREATE OR REPLACE FUNCTION array_is_sampled_instant_set(
  IN arr INSTANT ARRAY,
  IN sampling_window INTERVAL
) RETURNS BOOLEAN
  LANGUAGE sql
  IMMUTABLE STRICT PARALLEL SAFE AS
$$
SELECT array_is_ordered_set(arr) AND (
  SELECT MAX(sample_count_in_window)
  FROM (
    SELECT COUNT(item) OVER (ORDER BY item RANGE sampling_window PRECEDING) AS sample_count_in_window
    FROM (
      SELECT UNNEST(arr) AS item
    ) AS items
  ) AS counts
) <= 2;
$$;

--- Insert a value at the end of a `samplied_instant_set`, see `array_is_sampled_instant_set`
CREATE OR REPLACE FUNCTION sampled_instant_set_insert_back(
  IN arr INSTANT ARRAY,
  IN sampling_window INTERVAL,
  IN new_value INSTANT
) RETURNS INSTANT ARRAY
  LANGUAGE sql
  IMMUTABLE STRICT PARALLEL SAFE AS
$$
SELECT CASE WHEN ARRAY_LENGTH(arr, 1) = 0
              THEN ARRAY [new_value]
            WHEN ARRAY_LENGTH(arr, 1) = 1 AND arr[1] <> new_value
              THEN arr || new_value
            WHEN ARRAY_LENGTH(arr, 1) >= 1 AND arr[ARRAY_LENGTH(arr, 1)] = new_value
              THEN arr
            WHEN ARRAY_LENGTH(arr, 1) >= 2 AND new_value - arr[ARRAY_LENGTH(arr, 1) - 1] < sampling_window
              THEN arr[1:ARRAY_LENGTH(arr, 1) - 1] || new_value
            ELSE arr || new_value END
$$;

-- Ordered set of instants, such as for each period of time T, there are at most 2 values
-- Where `T` depen
CREATE DOMAIN sampled_instant_set AS INSTANT ARRAY CHECK (array_is_ordered_set(value));

CREATE TYPE HAMMERFEST_FORUM_ROLE AS ENUM ('None', 'Moderator', 'Administrator');
CREATE TYPE HAMMERFEST_QUEST_STATUS AS ENUM ('None', 'Pending', 'Complete');

CREATE TYPE RAW_HAMMERFEST_DATE AS (
  -- 1-12
  month U8,
  -- 1-31
  day U8,
  -- Day of week: Monday(1) to Sunday(7)
  isodow U8
);

CREATE DOMAIN hammerfest_date AS RAW_HAMMERFEST_DATE;

CREATE TYPE RAW_HAMMERFEST_DATETIME AS (
  -- 1-12
  month U8,
  -- 1-31
  day U8,
  -- Day of week: Monday(1) to Sunday(7)
  isodow U8,
  -- 0-23
  hour U8,
  -- 0-59
  minute U8
);

CREATE DOMAIN hammerfest_datetime AS RAW_HAMMERFEST_DATETIME CHECK ( (value).month IS NOT NULL AND 1 <= (value).month AND (value).month <= 12 AND (value).day IS NOT NULL AND 1 <= (value).day
  AND (value).day <= 31 AND (value).isodow IS NOT NULL AND 1 <= (value).isodow AND (value).isodow <= 7 AND (value).hour IS NOT NULL AND 1 <= (value).hour AND (value).hour <= 23
  AND (value).minute IS NOT NULL AND 1 <= (value).minute AND (value).minute <= 59 );

-- The list of items in Hammerfest (official game)
CREATE TABLE hammerfest_items (
  hammerfest_item_id HAMMERFEST_ITEM_ID NOT NULL,
  is_hidden BOOLEAN NOT NULL,
  PRIMARY KEY (hammerfest_item_id)
);

-- Global constant: sampling window size for sampled instant sets
CREATE OR REPLACE FUNCTION const_sampling_window() RETURNS INTERVAL
  LANGUAGE sql
  IMMUTABLE STRICT PARALLEL SAFE AS
$$
SELECT '1day'::INTERVAL
$$;

-- The list of quests in Hammerfest (official game)
CREATE TABLE hammerfest_quests (
  hammerfest_quest_id HAMMERFEST_QUEST_ID NOT NULL,
  PRIMARY KEY (hammerfest_quest_id)
);

-- Immutable quest status maps (may be shared by different users)
CREATE TABLE hammerfest_quest_status_maps (
  hammerfest_quest_status_map_id HAMMERFEST_QUEST_STATUS_MAP_ID NOT NULL,
-- sha3_256(utf8(json(value)))
-- Where `value` is a map from the id to the status, sorted by id and json does not use any whitespace
-- {"0":"None","1":"Pending",2:"Complete"}
  _sha3_256 BYTEA NOT NULL,
  PRIMARY KEY (hammerfest_quest_status_map_id),
  UNIQUE (_sha3_256)
);

-- Content of hammerfest_quest_status_maps
CREATE TABLE hammerfest_quest_status_map_items (
  hammerfest_quest_status_map_id HAMMERFEST_QUEST_STATUS_MAP_ID NOT NULL,
  hammerfest_quest_id HAMMERFEST_QUEST_ID NOT NULL,
  status HAMMERFEST_QUEST_STATUS NOT NULL,
  PRIMARY KEY (hammerfest_quest_status_map_id, hammerfest_quest_id),
  CONSTRAINT hammerfest_quest_status_map_item__map__fk FOREIGN KEY (hammerfest_quest_status_map_id) REFERENCES hammerfest_quest_status_maps(hammerfest_quest_status_map_id) ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT hammerfest_quest_status_map_item__quest__fk FOREIGN KEY (hammerfest_quest_id) REFERENCES hammerfest_quests(hammerfest_quest_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Immutable unlocked items state (may be shared by different users)
CREATE TABLE hammerfest_unlocked_item_sets (
  hammerfest_unlocked_item_set_id HAMMERFEST_UNLOCKED_ITEM_SET_ID NOT NULL,
-- sha3_256(utf8(json(value)))
-- Where `value` is a sorted list of item ids and json does not use any whitespace
-- [0,2,100]
  _sha3_256 BYTEA NOT NULL,
  PRIMARY KEY (hammerfest_unlocked_item_set_id),
  UNIQUE (_sha3_256)
);

-- Content of hammerfest_unlocked_items_snapshots
CREATE TABLE hammerfest_unlocked_item_set_items (
  hammerfest_unlocked_item_set_id HAMMERFEST_UNLOCKED_ITEM_SET_ID NOT NULL,
  hammerfest_item_id HAMMERFEST_ITEM_ID NOT NULL,
  PRIMARY KEY (hammerfest_unlocked_item_set_id, hammerfest_item_id),
  CONSTRAINT hammerfest_unlocked_item_set_item__map__fk FOREIGN KEY (hammerfest_unlocked_item_set_id) REFERENCES hammerfest_unlocked_item_sets(hammerfest_unlocked_item_set_id) ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT hammerfest_unlocked_item_set_item__item__fk FOREIGN KEY (hammerfest_item_id) REFERENCES hammerfest_items(hammerfest_item_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Immutable item counts (may be shared by different users)
CREATE TABLE hammerfest_item_count_maps (
  hammerfest_item_count_map_id HAMMERFEST_ITEM_COUNT_MAP_ID NOT NULL,
-- sha3_256(utf8(json(value)))
-- Where `value` is a map from the item id to the count, sorted by id and json does not use any whitespace
-- {"0":0,"2":9,2:5}
  _sha3_256 BYTEA NOT NULL,
  PRIMARY KEY (hammerfest_item_count_map_id),
  UNIQUE (_sha3_256)
);

-- Content of hammerfest_quest_statuses_snapshots
CREATE TABLE hammerfest_item_count_map_items (
  hammerfest_item_count_map_id HAMMERFEST_ITEM_COUNT_MAP_ID NOT NULL,
  hammerfest_item_id HAMMERFEST_ITEM_ID NOT NULL,
  count U32 NOT NULL,
  PRIMARY KEY (hammerfest_item_count_map_id, hammerfest_item_id),
  CONSTRAINT hammerfest_item_count_map_item__map__fk FOREIGN KEY (hammerfest_item_count_map_id) REFERENCES hammerfest_item_count_maps(hammerfest_item_count_map_id) ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT hammerfest_item_count_map_item__item__fk FOREIGN KEY (hammerfest_item_id) REFERENCES hammerfest_items(hammerfest_item_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant data for hammerfest inventories
CREATE TABLE hammerfest_inventories (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_user_id HAMMERFEST_USER_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  item_counts HAMMERFEST_ITEM_COUNT_MAP_ID NOT NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_user_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_user_id WITH =, period WITH &&),
  CONSTRAINT hammerfest_inventory__user__fk FOREIGN KEY (hammerfest_server, hammerfest_user_id) REFERENCES hammerfest_users(hammerfest_server, hammerfest_user_id) ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT hammerfest_inventory__item_counts__fk FOREIGN KEY (item_counts) REFERENCES hammerfest_item_count_maps(hammerfest_item_count_map_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant data unique to the public profile
CREATE TABLE hammerfest_profiles (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_user_id HAMMERFEST_USER_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  best_score U32 NOT NULL,
  best_level U8 NOT NULL CHECK (best_level < 120),
-- Null if not played
  season_score U32 NULL,
  quest_statuses HAMMERFEST_QUEST_STATUS_MAP_ID NOT NULL,
  unlocked_items HAMMERFEST_UNLOCKED_ITEM_SET_ID NOT NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_user_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_user_id WITH =, period WITH &&),
  CONSTRAINT hammerfest_profiles__user__fk FOREIGN KEY (hammerfest_server, hammerfest_user_id) REFERENCES hammerfest_users(hammerfest_server, hammerfest_user_id) ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT hammerfest_profiles__quest_statuses__fk FOREIGN KEY (quest_statuses) REFERENCES hammerfest_quest_status_maps(hammerfest_quest_status_map_id) ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT hammerfest_profiles__unlocked_items__fk FOREIGN KEY (unlocked_items) REFERENCES hammerfest_unlocked_item_sets(hammerfest_unlocked_item_set_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant linked email
CREATE TABLE hammerfest_emails (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_user_id HAMMERFEST_USER_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  email EMAIL_ADDRESS_HASH NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_user_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_user_id WITH =, period WITH &&),
  EXCLUDE USING gist (hammerfest_server WITH =, email WITH =, period WITH &&),
  CONSTRAINT hammerfest_email__user__fk FOREIGN KEY (hammerfest_server, hammerfest_user_id) REFERENCES hammerfest_users(hammerfest_server, hammerfest_user_id) ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT hammerfest_email__email__fk FOREIGN KEY (email) REFERENCES email_addresses(_hash) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant data shared by the public profile and forum author
CREATE TABLE hammerfest_user_achievements (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_user_id HAMMERFEST_USER_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  has_carrot BOOLEAN NOT NULL,
  ladder_level HAMMERFEST_LADDER_LEVEL NOT NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_user_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_user_id WITH =, period WITH &&),
  CONSTRAINT hammerfest_user_achievements__user__fk FOREIGN KEY (hammerfest_server, hammerfest_user_id) REFERENCES hammerfest_users(hammerfest_server, hammerfest_user_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant best season rank, as displayed on the forum
CREATE TABLE hammerfest_best_season_rank (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_user_id HAMMERFEST_USER_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
--   Null if the forum displayed `--`
  best_season_rank U32 NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_user_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_user_id WITH =, period WITH &&),
  CONSTRAINT hammerfest_user_achievements__user__fk FOREIGN KEY (hammerfest_server, hammerfest_user_id) REFERENCES hammerfest_users(hammerfest_server, hammerfest_user_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant data unique to the forum author
CREATE TABLE hammerfest_forum_roles (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_user_id HAMMERFEST_USER_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  role HAMMERFEST_FORUM_ROLE NOT NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_user_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_user_id WITH =, period WITH &&),
  CONSTRAINT hammerfest_user_ranks__user__fk FOREIGN KEY (hammerfest_server, hammerfest_user_id) REFERENCES hammerfest_users(hammerfest_server, hammerfest_user_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant data unique to the shop
CREATE TABLE hammerfest_shop_history (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_user_id HAMMERFEST_USER_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  weekly_tokens U8 NOT NULL,
--   0-249 is exact, 250 or more is represented with NULL (inf)
  purchased_tokens U8 NULL,
  has_quest_bonus BOOL NOT NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_user_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_user_id WITH =, period WITH &&),
  CONSTRAINT hammerfest_shop_history__user__fk FOREIGN KEY (hammerfest_server, hammerfest_user_id) REFERENCES hammerfest_users(hammerfest_server, hammerfest_user_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant game tokens
CREATE TABLE hammerfest_tokens (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_user_id HAMMERFEST_USER_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  tokens U32 NOT NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_user_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_user_id WITH =, period WITH &&),
  CONSTRAINT hammerfest_tokens__user__fk FOREIGN KEY (hammerfest_server, hammerfest_user_id) REFERENCES hammerfest_users(hammerfest_server, hammerfest_user_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant Hammerfest godfather links
CREATE TABLE hammerfest_godfathers (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_user_id HAMMERFEST_USER_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  godfather_id HAMMERFEST_USER_ID NOT NULL,
-- Tokens granted to the godfather
  tokens U32 NOT NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_user_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_user_id WITH =, period WITH &&),
  CONSTRAINT hammerfest_godfathers__child__fk FOREIGN KEY (hammerfest_server, hammerfest_user_id) REFERENCES hammerfest_users(hammerfest_server, hammerfest_user_id) ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT hammerfest_godfathers__father__fk FOREIGN KEY (hammerfest_server, godfather_id) REFERENCES hammerfest_users(hammerfest_server, hammerfest_user_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Permanent data for forum themes
CREATE TABLE hammerfest_forum_themes (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_theme_id HAMMERFEST_FORUM_THEME_ID NOT NULL,
--
  archived_at INSTANT NOT NULL,
  title HAMMERFEST_FORUM_THEME_TITLE NOT NULL,
  description HAMMERFEST_FORUM_THEME_DESCRIPTION NULL,
  is_public BOOLEAN NOT NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_theme_id),
  CONSTRAINT hammerfest_forum_themes__servers__fk FOREIGN KEY (hammerfest_server) REFERENCES hammerfest_servers(hammerfest_server) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant meta for forum threads, shared by the thread list and thread page
CREATE TABLE hammerfest_forum_theme_page_counts (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_theme_id HAMMERFEST_FORUM_THREAD_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  page_count U32 NOT NULL CHECK (page_count > 0),
  PRIMARY KEY (hammerfest_server, hammerfest_theme_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_theme_id WITH =, period WITH &&),
  CONSTRAINT hammerfest_threads_history__theme__fk FOREIGN KEY (hammerfest_server, hammerfest_theme_id) REFERENCES hammerfest_forum_themes(hammerfest_server, hammerfest_theme_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Permanent data for forum threads
CREATE TABLE hammerfest_forum_threads (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_thread_id HAMMERFEST_FORUM_THREAD_ID NOT NULL,
  archived_at INSTANT NOT NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_thread_id),
  CONSTRAINT hammerfest_forum_threads__servers__fk FOREIGN KEY (hammerfest_server) REFERENCES hammerfest_servers(hammerfest_server) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant meta for forum threads, shared by the thread list and thread page
CREATE TABLE hammerfest_forum_thread_shared_meta (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_thread_id HAMMERFEST_FORUM_THREAD_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  hammerfest_theme_id HAMMERFEST_FORUM_THEME_ID NOT NULL,
  title HAMMERFEST_FORUM_THREAD_TITLE NOT NULL,
  is_closed BOOLEAN NOT NULL,
  page_count U32 NOT NULL CHECK (page_count > 0),
  PRIMARY KEY (hammerfest_server, hammerfest_thread_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_thread_id WITH =, period WITH &&),
  CONSTRAINT hammerfest_threads_history__theme__fk FOREIGN KEY (hammerfest_server, hammerfest_theme_id) REFERENCES hammerfest_forum_themes(hammerfest_server, hammerfest_theme_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant meta for forum threads unique to the thread list
CREATE TABLE hammerfest_forum_thread_list_meta (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_thread_id HAMMERFEST_FORUM_THREAD_ID NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  -- Current theme page for this thread
  page U16 NOT NULL CHECK (page > 0),
  is_sticky BOOLEAN NOT NULL,
  latest_message_at HAMMERFEST_DATE NULL,
  author hammerfest_user_id NOT NULL,
  reply_count U16 NOT NULL,
  CHECK ((is_sticky AND latest_message_at IS NULL) OR (NOT is_sticky AND latest_message_at IS NOT NULL)),
  PRIMARY KEY (hammerfest_server, hammerfest_thread_id, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_thread_id WITH =, period WITH &&)
);

-- Time-variant data for forum messages
CREATE TABLE hammerfest_forum_messages_history (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_thread_id HAMMERFEST_FORUM_THREAD_ID NOT NULL,
  page U16 NOT NULL CHECK (page > 0),
  offset_in_page U8 NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  author HAMMERFEST_USER_ID NOT NULL,
  posted_at HAMMERFEST_DATETIME NOT NULL,
  -- Raw HTML content as found on the remote website
  remote_html_body TEXT NOT NULL,
  -- Marktwin body
  _mkt_body TEXT NULL,
  -- Rendered Marktwin body
  _html_body TEXT NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_thread_id, page, offset_in_page, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_thread_id WITH =, page WITH =, offset_in_page WITH =, period WITH &&),
  CONSTRAINT hammerfest_messages_history__thread__fk FOREIGN KEY (hammerfest_server, hammerfest_thread_id) REFERENCES hammerfest_forum_threads(hammerfest_server, hammerfest_thread_id) ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT hammerfest_messages_history__author__fk FOREIGN KEY (hammerfest_server, author) REFERENCES hammerfest_users(hammerfest_server, hammerfest_user_id) ON DELETE RESTRICT ON UPDATE CASCADE
);

-- Time-variant data for message-position/message id relationship
CREATE TABLE hammerfest_forum_message_ids (
  hammerfest_server HAMMERFEST_SERVER NOT NULL,
  hammerfest_thread_id HAMMERFEST_FORUM_THREAD_ID NOT NULL,
  page U16 NOT NULL CHECK (page > 0),
  offset_in_page U8 NOT NULL,
  period PERIOD_FROM NOT NULL,
  retrieved_at SAMPLED_INSTANT_SET NOT NULL CHECK (array_is_sampled_instant_set(retrieved_at, const_sampling_window())),
--
  hammerfest_message_id HAMMERFEST_FORUM_MESSAGE_ID NOT NULL,
  PRIMARY KEY (hammerfest_server, hammerfest_thread_id, page, offset_in_page, period),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_thread_id WITH =, page WITH =, offset_in_page WITH =, period WITH &&),
  EXCLUDE USING gist (hammerfest_server WITH =, hammerfest_message_id WITH =, period WITH &&),
  CONSTRAINT hammerfest_messages_history__thread__fk FOREIGN KEY (hammerfest_server, hammerfest_thread_id) REFERENCES hammerfest_forum_threads(hammerfest_server, hammerfest_thread_id) ON DELETE RESTRICT ON UPDATE CASCADE
);
