ALTER TABLE blobs
  ADD COLUMN digest_sha2_256 BYTEA NULL,
  ADD COLUMN digest_sha3_256 BYTEA NULL,
  ADD CONSTRAINT check_digest_sha2_256 CHECK (digest_sha2_256 IS NULL OR LENGTH(digest_sha2_256) = 32),
  ADD CONSTRAINT check_digest_sha3_256 CHECK (digest_sha3_256 IS NULL OR LENGTH(digest_sha3_256) = 32);

CREATE TYPE PERIOD AS RANGE (
  subtype = INSTANT
);
CREATE DOMAIN PERIOD_LOWER AS PERIOD CHECK (NOT lower_inf(VALUE) AND lower_inc(VALUE) AND NOT upper_inc(VALUE));

CREATE DOMAIN game_option_display_name AS VARCHAR(100);
CREATE TYPE game_option_display_name_i18n_item AS(locale LOCALE_ID, value GAME_OPTION_DISPLAY_NAME);

CREATE DOMAIN game_option_display_name_i18n_id AS UUID;

CREATE DOMAIN GIT_COMMIT_REF AS BYTEA CHECK (LENGTH(VALUE) = 20);

CREATE TYPE GAME_CHANNEL_PERMISSION AS ENUM ('None', 'View', 'Play', 'Debug', 'Manage');
CREATE TYPE GAME_ENGINE_TYPE AS ENUM ('V96', 'Custom');
CREATE TYPE GAME_CATEGORY2 AS ENUM ('Big', 'Small', 'Puzzle', 'Challenge', 'Fun', 'Lab', 'Other');

CREATE DOMAIN PATCHER_FRAMEWORK_NAME AS VARCHAR(32) CHECK(value ~ '^[a-z][a-z0-9]*$');

CREATE DOMAIN FAMILIES_STRING AS TEXT CHECK(value ~ '^(?:(?:0|[1-9]\d*)(?:,(?:0|[1-9]\d*))*)?$');

-- Display name for a blob attached to a game (file name in the legacy API)
CREATE DOMAIN GAME_RESOURCE_DISPLAY_NAME AS VARCHAR(100);

--- Checks that arr is an array of unique non-null values sorted in ascending order
CREATE OR REPLACE FUNCTION test_game_option_display_name_i18n_items(
  IN main_locale LOCALE_ID,
  IN items game_option_display_name_i18n_item[]
) RETURNS BOOLEAN
  LANGUAGE sql
  IMMUTABLE STRICT PARALLEL SAFE AS
$$
SELECT
  -- The main locale is present
  (main_locale IN (
  SELECT (item).locale
  FROM (
    SELECT DISTINCT UNNEST(items) AS item
  ) AS items
))
  AND
  -- Entries are sorted by locale (without duplicates)
  (
    (
      SELECT ARRAY_AGG(locale)
      FROM (
        SELECT (item).locale::TEXT AS locale
        FROM (SELECT UNNEST(items) AS item) AS flat
      ) AS locales
    ) = (
      SELECT ARRAY_AGG(locale)
      FROM (
        SELECT DISTINCT (item).locale::TEXT AS locale
        FROM (SELECT UNNEST(items) AS item) AS flat
        ORDER BY locale ASC
      ) AS locales
    )
  );
$$;

CREATE TABLE game_option_display_name_i18n (
  game_option_display_name_id GAME_OPTION_DISPLAY_NAME_I18N_ID NOT NULL,
  main_locale LOCALE_ID NOT NULL,
  items game_option_display_name_i18n_item[] NOT NULL,
  CHECK (test_game_option_display_name_i18n_items(main_locale, items)),
  UNIQUE (game_option_display_name_id, main_locale),
  UNIQUE (main_locale, items)
);

CREATE DOMAIN game_option_id AS UUID;
CREATE TABLE game_option (
  game_option_id GAME_OPTION_ID NOT NULL,
  main_locale LOCALE_ID NOT NULL,
  display_name game_option_display_name_i18n_id NOT NULL,
  is_visible BOOLEAN NOT NULL,
  is_enabled BOOLEAN NOT NULL,
  default_value BOOLEAN NOT NULL,
  PRIMARY KEY (game_option_id),
  CONSTRAINT game_option__display_name__fk FOREIGN KEY (display_name, main_locale) REFERENCES game_option_display_name_i18n(game_option_display_name_id, main_locale)  ON DELETE RESTRICT ON UPDATE RESTRICT,
  UNIQUE (main_locale, display_name, is_visible, is_enabled, default_value),
  UNIQUE (game_option_id, main_locale)
);

CREATE DOMAIN game_mode_display_name AS VARCHAR(100);
CREATE TYPE game_mode_display_name_i18n_item AS(locale LOCALE_ID, value GAME_MODE_DISPLAY_NAME);

CREATE DOMAIN game_mode_display_name_i18n_id AS UUID;

--- Checks that arr is an array of unique non-null values sorted in ascending order
CREATE OR REPLACE FUNCTION test_game_mode_display_name_i18n_items(
  IN main_locale LOCALE_ID,
  IN items game_mode_display_name_i18n_item[]
) RETURNS BOOLEAN
  LANGUAGE sql
  IMMUTABLE STRICT PARALLEL SAFE AS
$$
SELECT
  -- The main locale is present
  (main_locale IN (
  SELECT (item).locale
  FROM (
    SELECT DISTINCT UNNEST(items) AS item
  ) AS items
))
  AND
  -- Entries are sorted by locale (without duplicates)
  (
    (
      SELECT ARRAY_AGG(locale)
      FROM (
        SELECT (item).locale::TEXT AS locale
        FROM (SELECT UNNEST(items) AS item) AS flat
      ) AS locales
    ) = (
      SELECT ARRAY_AGG(locale)
      FROM (
        SELECT DISTINCT (item).locale::TEXT AS locale
        FROM (SELECT UNNEST(items) AS item) AS flat
        ORDER BY locale ASC
      ) AS locales
    )
  );
$$;

CREATE TABLE game_mode_display_name_i18n (
  game_mode_display_name_id GAME_MODE_DISPLAY_NAME_I18N_ID NOT NULL,
  main_locale LOCALE_ID NOT NULL,
  items game_mode_display_name_i18n_item[] NOT NULL,
  CHECK (test_game_mode_display_name_i18n_items(main_locale, items)),
  UNIQUE (game_mode_display_name_id, main_locale),
  UNIQUE (main_locale, items)
);

CREATE DOMAIN game_mode_id AS UUID;
CREATE TABLE game_mode (
  game_mode_id GAME_MODE_ID NOT NULL,
  main_locale LOCALE_ID NOT NULL,
  display_name game_mode_display_name_i18n_id NOT NULL,
  is_visible BOOLEAN NOT NULL,
  -- `sha3(json([["option_key1","option_id1"],["option_key2","option_id2"]])`
  -- TODO: Consider adding the rank of the latest option? (reverse foreign-key)
  _option_list_sha3_256 BYTEA NOT NULL,
  PRIMARY KEY (game_mode_id),
  CONSTRAINT game_mode__display_name__fk FOREIGN KEY (display_name, main_locale) REFERENCES game_mode_display_name_i18n(game_mode_display_name_id, main_locale)  ON DELETE RESTRICT ON UPDATE RESTRICT,
  UNIQUE (main_locale, display_name, is_visible, _option_list_sha3_256),
  UNIQUE (game_mode_id, main_locale)
);

CREATE DOMAIN game_option_key AS VARCHAR(100);

CREATE TABLE game_mode_option (
  game_mode_id GAME_MODE_ID NOT NULL,
  -- TODO: Consider adding the max rank? (for the reverse foreign-key with the game mode)
  main_locale LOCALE_ID NOT NULL,
  rank U32 NOT NULL,
  game_option_key GAME_OPTION_KEY NOT NULL,
  game_option_id GAME_OPTION_ID NOT NULL,
  _prev U32 NULL,
  PRIMARY KEY (game_mode_id, game_option_id),
  UNIQUE (game_mode_id, rank),
  UNIQUE (game_mode_id, game_option_key),
  -- Rank is zero, or prev is one less and points to a predecessor
  CHECK ((_prev IS NULL AND rank = 0) OR (_prev IS NOT NULL AND rank = _prev + 1)),
  CONSTRAINT game_mode_option__mode__fk FOREIGN KEY (game_mode_id, main_locale) REFERENCES game_mode(game_mode_id, main_locale)  ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_mode_option__option__fk FOREIGN KEY (game_option_id, main_locale) REFERENCES game_option(game_option_id, main_locale)  ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_mode_option__prev__fk FOREIGN KEY (game_mode_id, _prev) REFERENCES game_mode_option(game_mode_id, rank)  ON DELETE RESTRICT ON UPDATE RESTRICT
);

CREATE DOMAIN game_id AS UUID;
CREATE DOMAIN game_build_id AS UUID;

CREATE DOMAIN game_display_name AS VARCHAR(100);
CREATE TYPE game_display_name_i18n_item AS(locale LOCALE_ID, value GAME_DISPLAY_NAME);

CREATE DOMAIN game_display_name_i18n_id AS UUID;

--- Checks that arr is an array of unique non-null values sorted in ascending order
CREATE OR REPLACE FUNCTION test_game_display_name_i18n_items(
  IN main_locale LOCALE_ID,
  IN items game_display_name_i18n_item[]
) RETURNS BOOLEAN
  LANGUAGE sql
  IMMUTABLE STRICT PARALLEL SAFE AS
$$
SELECT
  -- The main locale is present
  (main_locale IN (
    SELECT (item).locale
    FROM (
      SELECT DISTINCT UNNEST(items) AS item
    ) AS items
  ))
    AND
    -- Entries are sorted by locale (without duplicates)
    (
      (
        SELECT ARRAY_AGG(locale)
        FROM (
          SELECT (item).locale::TEXT AS locale
          FROM (SELECT UNNEST(items) AS item) AS flat
        ) AS locales
      ) = (
        SELECT ARRAY_AGG(locale)
        FROM (
          SELECT DISTINCT (item).locale::TEXT AS locale
          FROM (SELECT UNNEST(items) AS item) AS flat
          ORDER BY locale ASC
        ) AS locales
      )
      );
$$;

CREATE TABLE game_display_name_i18n (
  game_display_name_id GAME_DISPLAY_NAME_I18N_ID NOT NULL,
  main_locale LOCALE_ID NOT NULL,
  items game_display_name_i18n_item[] NOT NULL,
  CHECK (test_game_display_name_i18n_items(main_locale, items)),
  UNIQUE (game_display_name_id, main_locale),
  UNIQUE (main_locale, items)
);

CREATE DOMAIN game_description AS VARCHAR(1000);
CREATE TYPE game_description_i18n_item AS(locale LOCALE_ID, value GAME_DESCRIPTION);

CREATE DOMAIN game_description_i18n_id AS UUID;

--- Checks that arr is an array of unique non-null values sorted in ascending order
CREATE OR REPLACE FUNCTION test_game_description_i18n_items(
  IN main_locale LOCALE_ID,
  IN items game_description_i18n_item[]
) RETURNS BOOLEAN
  LANGUAGE sql
  IMMUTABLE STRICT PARALLEL SAFE AS
$$
SELECT
  -- The main locale is present
  (main_locale IN (
    SELECT (item).locale
    FROM (
      SELECT DISTINCT UNNEST(items) AS item
    ) AS items
  ))
    AND
    -- Entries are sorted by locale (without duplicates)
    (
      (
        SELECT ARRAY_AGG(locale)
        FROM (
          SELECT (item).locale::TEXT AS locale
          FROM (SELECT UNNEST(items) AS item) AS flat
        ) AS locales
      ) = (
        SELECT ARRAY_AGG(locale)
        FROM (
          SELECT DISTINCT (item).locale::TEXT AS locale
          FROM (SELECT UNNEST(items) AS item) AS flat
          ORDER BY locale ASC
        ) AS locales
      )
      );
$$;

CREATE TABLE game_description_i18n (
  game_description_id GAME_DESCRIPTION_I18N_ID NOT NULL,
  main_locale LOCALE_ID NOT NULL,
  items game_description_i18n_item[] NOT NULL,
  CHECK (test_game_description_i18n_items(main_locale, items)),
  UNIQUE (game_description_id, main_locale),
  UNIQUE (main_locale, items)
);

CREATE DOMAIN blob_i18n_id AS UUID;

CREATE TABLE blob_i18n (
  blob_i18n_id BLOB_I18N_ID NOT NULL,
  main_locale LOCALE_ID NOT NULL,
  _sha3_256 BYTEA NOT NULL,
  PRIMARY KEY (blob_i18n_id),
  UNIQUE (main_locale, _sha3_256),
  UNIQUE (blob_i18n_id, main_locale)
);

-- An entry in the `blob_i18n` sorted map, with key `locale` and value `blob_id`
CREATE TABLE blob_i18n_item (
  blob_i18n_id BLOB_I18N_ID NOT NULL,
  locale LOCALE_ID NOT NULL,
  blob_id BLOB_ID NOT NULL,
  PRIMARY KEY (blob_i18n_id, blob_id),
  UNIQUE (blob_i18n_id, locale),
  CONSTRAINT blob_i18n_item__blob_i18n__fk FOREIGN KEY (blob_i18n_id) REFERENCES blob_i18n(blob_i18n_id) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT blob_i18n_item__blob__fk FOREIGN KEY (blob_id) REFERENCES blobs(blob_id) ON DELETE RESTRICT ON UPDATE RESTRICT
);

ALTER TABLE blob_i18n
  ADD CONSTRAINT blob_i18n_main__blob_i18n_item__fk FOREIGN KEY (blob_i18n_id, main_locale) REFERENCES blob_i18n_item(blob_i18n_id, locale) ON DELETE RESTRICT ON UPDATE RESTRICT DEFERRABLE INITIALLY DEFERRED;

CREATE DOMAIN GAME_RESOURCE_ID AS UUID;
CREATE DOMAIN GAME_RESOURCE_LIST_ID AS UUID;
CREATE DOMAIN game_resource_display_name_i18n_id AS UUID;
CREATE TYPE game_resource_display_name_i18n_item AS(locale LOCALE_ID, value GAME_RESOURCE_DISPLAY_NAME);

--- Checks that arr is an array of unique non-null values sorted in ascending order
CREATE OR REPLACE FUNCTION test_game_resource_display_name_i18n_items(
  IN main_locale LOCALE_ID,
  IN items game_resource_display_name_i18n_item[]
) RETURNS BOOLEAN
  LANGUAGE sql
  IMMUTABLE STRICT PARALLEL SAFE AS
$$
SELECT
  -- The main locale is present
  (main_locale IN (
    SELECT (item).locale
    FROM (
           SELECT DISTINCT UNNEST(items) AS item
         ) AS items
  ))
    AND
    -- Entries are sorted by locale (without duplicates)
  (
      (
        SELECT ARRAY_AGG(locale)
        FROM (
               SELECT (item).locale::TEXT AS locale
               FROM (SELECT UNNEST(items) AS item) AS flat
             ) AS locales
      ) = (
        SELECT ARRAY_AGG(locale)
        FROM (
               SELECT DISTINCT (item).locale::TEXT AS locale
               FROM (SELECT UNNEST(items) AS item) AS flat
               ORDER BY locale ASC
             ) AS locales
      )
    );
$$;

CREATE TABLE game_resource_display_name_i18n (
  game_resource_display_name_id GAME_RESOURCE_DISPLAY_NAME_I18N_ID NOT NULL,
  main_locale LOCALE_ID NOT NULL,
  items game_resource_display_name_i18n_item[] NOT NULL,
  CHECK (test_game_resource_display_name_i18n_items(main_locale, items)),
  UNIQUE (game_resource_display_name_id, main_locale),
  UNIQUE (main_locale, items)
);

CREATE TABLE game_resource (
  game_resource_id GAME_RESOURCE_ID NOT NULL,
  main_locale LOCALE_ID NOT NULL,
  blob_i18n_id blob_i18n_id NOT NULL,
  display_name game_resource_display_name_i18n_id NULL,
  PRIMARY KEY (game_resource_id),
  UNIQUE(game_resource_id, main_locale),
  UNIQUE NULLS NOT DISTINCT (main_locale, blob_i18n_id, display_name),
  CONSTRAINT game_resource__blob__fk FOREIGN KEY (blob_i18n_id, main_locale) REFERENCES blob_i18n(blob_i18n_id, main_locale) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_resource__display_name__fk FOREIGN KEY (display_name, main_locale) REFERENCES game_resource_display_name_i18n(game_resource_display_name_id, main_locale) ON DELETE RESTRICT ON UPDATE RESTRICT
);

CREATE TABLE game_resource_list (
  game_resource_list_id GAME_RESOURCE_LIST_ID NOT NULL,
  main_locale LOCALE_ID NOT NULL,
  size U32 NOT NULL,
  _sha3_256 BYTEA NOT NULL,
  PRIMARY KEY (game_resource_list_id),
  UNIQUE(game_resource_list_id, main_locale),
  UNIQUE NULLS NOT DISTINCT (main_locale, size, _sha3_256)
);

CREATE TABLE game_resource_list_item (
  game_resource_list_id GAME_RESOURCE_LIST_ID NOT NULL,
  -- TODO: Consider adding the max rank? (for the reverse foreign-key with the game mode)
  main_locale LOCALE_ID NOT NULL,
  rank U32 NOT NULL,
  game_resource_id GAME_RESOURCE_ID NOT NULL,
  _prev U32 NULL,
  PRIMARY KEY (game_resource_list_id, rank),
  -- Rank is zero, or prev is one less and points to a predecessor
  CHECK ((_prev IS NULL AND rank = 0) OR (_prev IS NOT NULL AND rank = _prev + 1)),
  CONSTRAINT game_resource_list_item__list__fk FOREIGN KEY (game_resource_list_id, main_locale) REFERENCES game_resource_list(game_resource_list_id, main_locale) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_resource_list_item__resource__fk FOREIGN KEY (game_resource_id, main_locale) REFERENCES game_resource(game_resource_id, main_locale) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_resource_list_item__prev__fk FOREIGN KEY (game_resource_list_id, _prev) REFERENCES game_resource_list_item(game_resource_list_id, rank) ON DELETE RESTRICT ON UPDATE RESTRICT
);

CREATE DOMAIN GAME_PATCHER_ID AS UUID;

CREATE TABLE game_patcher (
  game_patcher_id GAME_PATCHER_ID NOT NULL,
  blob_id BLOB_ID NOT NULL,
  patcher_framework_name PATCHER_FRAMEWORK_NAME NOT NULL,
  patcher_framework_version_major U32 NOT NULL,
  patcher_framework_version_minor U32 NOT NULL,
  patcher_framework_version_patch U32 NOT NULL,
  meta JSONB NULL,
  PRIMARY KEY (game_patcher_id),
  CHECK (pg_column_size(meta) < 1024 * 1024),
  UNIQUE NULLS NOT DISTINCT (blob_id, patcher_framework_name, patcher_framework_version_major, patcher_framework_version_minor, patcher_framework_version_patch, meta)
);

CREATE DOMAIN game_channel_id AS UUID;
CREATE DOMAIN game_channel_key AS VARCHAR(100);
CREATE DOMAIN game_key AS VARCHAR(100);

CREATE TABLE game (
  game_id GAME_ID NOT NULL,
  created_at INSTANT NOT NULL,
  owner USER_ID NOT NULL,
  main_channel_key GAME_CHANNEL_KEY NOT NULL,
  PRIMARY KEY (game_id),
  CONSTRAINT game__owner__fk FOREIGN KEY (owner) REFERENCES users(user_id) ON DELETE RESTRICT ON UPDATE CASCADE
  -- Deferred foreign key (game_id, main_channel_key) -> game_channel(game_id, game_channel_key) defined further.
);

CREATE TABLE game_key_history (
  period PERIOD_LOWER NOT NULL,
  game_key GAME_KEY NOT NULL,
  game_id GAME_ID NOT NULL,
  PRIMARY KEY (period, game_key),
  CONSTRAINT game_key__game__fk FOREIGN KEY (game_id) REFERENCES game(game_id) ON DELETE CASCADE ON UPDATE CASCADE,
  EXCLUDE USING gist (game_key WITH =, period WITH &&)
);

CREATE TABLE game_build (
  game_build_id GAME_BUILD_ID NOT NULL,
  main_locale LOCALE_ID NOT NULL,
  game_display_name_id game_display_name_i18n_id NOT NULL,
  game_description_id game_description_i18n_id NULL,
  icon blob_i18n_id NULL,
  loader_version_major U32 NOT NULL,
  loader_version_minor U32 NOT NULL,
  loader_version_patch U32 NOT NULL,
  engine_type GAME_ENGINE_TYPE NOT NULL,
  custom_engine BLOB_ID NULL,
  patcher GAME_PATCHER_ID NULL,
  debug BLOB_ID NULL,
  content BLOB_ID NULL,
  content_i18n blob_i18n_id NULL,
  music_list GAME_RESOURCE_LIST_ID NOT NULL,
  families FAMILIES_STRING NOT NULL,
  category GAME_CATEGORY2 NOT NULL,
  -- locale_pack GAME_ASSET_ID NULL,
  -- `sha3(json([["mode_key1","mode_id1"],["mode_key2","mode_id2"]])`
  -- TODO: Consider adding the rank of the latest mode? (reverse foreign-key)
  _mode_list_sha3_256 BYTEA NOT NULL,
  PRIMARY KEY (game_build_id),
  CHECK ((engine_type = 'V96' AND custom_engine IS NULL) OR (engine_type = 'Custom' AND custom_engine IS NOT NULL)),
  CONSTRAINT game_build__display_name__fk FOREIGN KEY (game_display_name_id, main_locale) REFERENCES game_display_name_i18n(game_display_name_id, main_locale) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_build__description__fk FOREIGN KEY (game_description_id, main_locale) REFERENCES game_description_i18n(game_description_id, main_locale) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_build__icon__fk FOREIGN KEY (icon, main_locale) REFERENCES blob_i18n(blob_i18n_id, main_locale) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_build__custom_engine__fk FOREIGN KEY (custom_engine) REFERENCES blobs(blob_id) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_build__patcher__fk FOREIGN KEY (patcher) REFERENCES game_patcher(game_patcher_id) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_build__debug__fk FOREIGN KEY (debug) REFERENCES blobs(blob_id) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_build__content__fk FOREIGN KEY (content) REFERENCES blobs(blob_id) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_build__content_i18n__fk FOREIGN KEY (content_i18n, main_locale) REFERENCES blob_i18n(blob_i18n_id, main_locale) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_build__music_list__fk FOREIGN KEY (music_list, main_locale) REFERENCES game_resource_list(game_resource_list_id, main_locale) ON DELETE RESTRICT ON UPDATE RESTRICT,
  -- Used for reverse FKs (e.g. in `game_build_mode`)
  UNIQUE (game_build_id, main_locale),

  UNIQUE NULLS NOT DISTINCT (main_locale, game_display_name_id, game_description_id, icon, loader_version_major, loader_version_minor, loader_version_patch, engine_type, custom_engine, patcher, debug, content, content_i18n, music_list, families, category, _mode_list_sha3_256)
);

CREATE DOMAIN game_mode_key AS VARCHAR(100);

CREATE TABLE game_build_mode (
  game_build_id GAME_MODE_ID NOT NULL,
  -- TODO: Consider adding the max rank? (for the reverse foreign-key with the game build)
  main_locale LOCALE_ID NOT NULL,
  rank U32 NOT NULL,
  game_mode_key GAME_MODE_KEY NOT NULL,
  game_mode_id GAME_MODE_ID NOT NULL,
  _prev U32 NULL,
  PRIMARY KEY (game_build_id, game_mode_id),
  UNIQUE (game_build_id, rank),
  UNIQUE (game_build_id, game_mode_key),
  -- Rank is zero, or prev is one less and points to a predecessor
  CHECK ((_prev IS NULL AND rank = 0) OR (_prev IS NOT NULL AND rank = _prev + 1)),
  CONSTRAINT game_build_mode__build__fk FOREIGN KEY (game_build_id, main_locale) REFERENCES game_build(game_build_id, main_locale)  ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_build_mode__mode__fk FOREIGN KEY (game_mode_id, main_locale) REFERENCES game_mode(game_mode_id, main_locale)  ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT game_build_mode__prev__fk FOREIGN KEY (game_build_id, _prev) REFERENCES game_build_mode(game_build_id, rank)  ON DELETE RESTRICT ON UPDATE RESTRICT
);

--
-- CREATE DOMAIN game_id AS UUID;
-- CREATE DOMAIN game_revision_id AS UUID;
-- CREATE DOMAIN game_version_id AS UUID;
-- CREATE DOMAIN game_release_id AS UUID;
-- CREATE DOMAIN game_option_list_id AS UUID;
--
-- CREATE DOMAIN game_option_display_name_i18n_id AS UUID;
-- CREATE DOMAIN game_display_name AS VARCHAR(100);
-- CREATE DOMAIN game_channel_display_name AS VARCHAR(100);
-- CREATE TYPE game_channel_permission AS ENUM ('Manage', 'Debug', 'Play', 'View');
-- CREATE TYPE role_type AS ENUM ('Group', 'User');
--
-- CREATE TABLE game (
--   game_id GAME_ID PRIMARY KEY NOT NULL,
--   created_at INSTANT NOT NULL
-- --   fallback_display_name game_display_name NOT NULL,
-- --   owner USER_ID NOT NULL
-- );

CREATE TABLE game_version (
  game_id GAME_ID NOT NULL,
  version_major U32 NOT NULL,
  version_minor U32 NOT NULL,
  version_patch U32 NOT NULL,
  git_commit_ref GIT_COMMIT_REF NULL,
  created_at INSTANT NOT NULL,
  game_build_id GAME_BUILD_ID NOT NULL,
  PRIMARY KEY (game_id, version_major, version_minor, version_patch),
  CONSTRAINT game_version__game__fk FOREIGN KEY (game_id) REFERENCES game(game_id)  ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT game_version__game_build__fk FOREIGN KEY (game_build_id) REFERENCES game_build(game_build_id)  ON DELETE CASCADE ON UPDATE CASCADE
);

-- Namespace inside a game
CREATE TABLE game_channel (
--   game_channel_id GAME_CHANNEL_ID PRIMARY KEY NOT NULL,
  game_id GAME_ID NOT NULL,
  game_channel_key GAME_CHANNEL_KEY NOT NULL,
  created_at INSTANT NOT NULL,
  first_history_tt_period PERIOD_LOWER NOT NULL,
  first_history_vt_period PERIOD_LOWER NOT NULL,
  PRIMARY KEY (game_id, game_channel_key),
  UNIQUE (game_id, game_channel_key, created_at),
  CONSTRAINT game_channel__game__fk FOREIGN KEY (game_id) REFERENCES game(game_id)  ON DELETE CASCADE ON UPDATE CASCADE
  -- Deferred foreign key (game_id, game_channel_key, first_history_tt_period, first_history_vt_period) -> game_channel_history(game_id, game_channel_key, tt_period, vt_period) defined further.
);

ALTER TABLE game
  ADD CONSTRAINT game__game_channel__fk FOREIGN KEY (game_id, main_channel_key) REFERENCES game_channel(game_id, game_channel_key) ON DELETE RESTRICT ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED;

-- Semi-infinite bitemporal map with checked absence of gaps
CREATE TABLE game_channel_history (
  game_id GAME_ID NOT NULL,
  game_channel_key GAME_CHANNEL_KEY NOT NULL,
  -- Transaction time period
  tt_period PERIOD_LOWER NOT NULL,
  -- Valid time period
  vt_period PERIOD_LOWER NOT NULL,
  -- Denormalized game creation date; to ensure continuity along both time axis
  _game_channel_created_at INSTANT NOT NULL,
  -- `(tt_next_{vt,tt}_period)` is a pointer to the next tt entry, null if infinite
  _tt_next_tt_period PERIOD_LOWER NULL,
  _tt_next_vt_period PERIOD_LOWER NULL,
  -- `(vt_next_{vt,tt}_period)` is a pointer to the next vt entry, null if infinite
  _vt_next_tt_period PERIOD_LOWER NULL,
  _vt_next_vt_period PERIOD_LOWER NULL,
  -- User who created this history entry
  created_by USER_ID NOT NULL,
  -- Game revision to use at this time
  version_major U32 NOT NULL,
  version_minor U32 NOT NULL,
  version_patch U32 NOT NULL,
  -- Boolean indicating that the channel is enabled, only admins and owners can see disabled channels
  is_enabled BOOLEAN NOT NULL,
  -- Rank of this channel among the other channels. Channels are sorted by (lowest rank first, lowest creation time first).
  rank I32 NOT NULL,
  -- Default permission for authenticated users
  default_permission GAME_CHANNEL_PERMISSION NOT NULL CHECK (default_permission IN ('None', 'View', 'Play')),
  -- Give priority when listing games
  is_pinned BOOLEAN NOT NULL,
  -- Displayed date of publication
  publication_date INSTANT NULL,
  -- Date used when sorting by latest meaningful update
  sort_update_date INSTANT NOT NULL,
  CHECK ((upper(tt_period) IS NULL AND _tt_next_tt_period IS NULL AND _tt_next_vt_period IS NULL) OR (upper(tt_period) IS NOT NULL AND lower(_tt_next_tt_period) = upper(tt_period) AND _tt_next_vt_period @> lower(vt_period))),
  CHECK ((upper(vt_period) IS NULL AND _vt_next_tt_period IS NULL AND _vt_next_vt_period IS NULL) OR (upper(vt_period) IS NOT NULL AND lower(_vt_next_vt_period) = upper(vt_period) AND _vt_next_tt_period @> lower(tt_period))),
  CHECK (publication_date IS NULL OR (publication_date IS NOT NULL AND sort_update_date >= publication_date)),
  PRIMARY KEY (game_id, game_channel_key, tt_period, vt_period),
  EXCLUDE USING gist (game_id WITH =, game_channel_key WITH =, tt_period WITH &&, vt_period WITH &&),
  CONSTRAINT game_channel__fk FOREIGN KEY (game_id, game_channel_key, _game_channel_created_at) REFERENCES game_channel(game_id, game_channel_key, created_at) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT user__fk FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT game_version__fk FOREIGN KEY (game_id, version_major, version_minor, version_patch) REFERENCES game_version(game_id, version_major, version_minor, version_patch) ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT tt_next__fk FOREIGN KEY (game_id, game_channel_key, _tt_next_tt_period, _tt_next_vt_period) REFERENCES game_channel_history(game_id, game_channel_key, tt_period, vt_period) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT vt_next__fk FOREIGN KEY (game_id, game_channel_key, _tt_next_tt_period, _tt_next_vt_period) REFERENCES game_channel_history(game_id, game_channel_key, tt_period, vt_period) ON DELETE CASCADE ON UPDATE CASCADE
);

ALTER TABLE game_channel
  ADD CONSTRAINT game_channel__first_history__fk FOREIGN KEY (game_id, game_channel_key, first_history_tt_period, first_history_vt_period) REFERENCES game_channel_history(game_id, game_channel_key, tt_period, vt_period) ON DELETE RESTRICT ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED;

ALTER TABLE runs
  ADD COLUMN locale LOCALE_ID NOT NULL DEFAULT 'fr-FR',
  ADD COLUMN game_channel_key GAME_CHANNEL_KEY NULL,
  ADD COLUMN version_major U32 NULL,
  ADD COLUMN version_minor U32 NULL,
  ADD COLUMN version_patch U32 NULL,
  ADD CONSTRAINT run_game_channel__fk FOREIGN KEY (game_id, game_channel_key) REFERENCES game_channel(game_id, game_channel_key) ON DELETE CASCADE ON UPDATE CASCADE,
  ADD CONSTRAINT run_game_build__fk FOREIGN KEY (game_id, version_major, version_minor, version_patch) REFERENCES game_version(game_id, version_major, version_minor, version_patch) ON DELETE RESTRICT ON UPDATE CASCADE;

CREATE TABLE user_favorite_game (
  period PERIOD_LOWER NOT NULL,
  user_id USER_ID NOT NULL,
  game_id GAME_ID NOT NULL,
  PRIMARY KEY (period, user_id, game_id),
  CONSTRAINT favorite__user__fk FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT favorite__game__fk FOREIGN KEY (game_id) REFERENCES game(game_id) ON DELETE CASCADE ON UPDATE CASCADE,
  EXCLUDE USING gist (user_id WITH =, game_id WITH =, period WITH &&)
);
