CREATE TABLE IF NOT EXISTS environments ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); CREATE TABLE IF NOT EXISTS languages ( code text PRIMARY KEY, name text NOT NULL, enabled boolean NOT NULL DEFAULT true, is_default boolean NOT NULL DEFAULT false, sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0), CHECK (code ~ '^[a-z]{2}(-[A-Z]{2})?$'), CHECK (length(name) BETWEEN 1 AND 80) ); CREATE UNIQUE INDEX IF NOT EXISTS languages_single_default_idx ON languages (is_default) WHERE is_default = true; INSERT INTO languages (code, name, enabled, is_default, sort_order) VALUES ('en', 'English', true, true, 10), ('zh-CN', '简体中文', true, false, 20) ON CONFLICT (code) DO NOTHING; CREATE TABLE IF NOT EXISTS entity_translations ( entity_type text NOT NULL CHECK ( entity_type IN ( 'pokemon', 'pokemon-types', 'skills', 'environments', 'favorite-things', 'item-categories', 'item-usages', 'acquisition-methods', 'items', 'maps', 'habitats', 'daily-checklist-items', 'life-tags' ) ), entity_id integer NOT NULL, locale text NOT NULL REFERENCES languages(code) ON DELETE CASCADE, field_name text NOT NULL CHECK (field_name IN ('name', 'title', 'details', 'genus')), value text NOT NULL, PRIMARY KEY (entity_type, entity_id, locale, field_name) ); CREATE INDEX IF NOT EXISTS entity_translations_lookup_idx ON entity_translations (entity_type, entity_id, field_name, locale); ALTER TABLE entity_translations DROP CONSTRAINT IF EXISTS entity_translations_entity_type_check; ALTER TABLE entity_translations ADD CONSTRAINT entity_translations_entity_type_check CHECK ( entity_type IN ( 'pokemon', 'pokemon-types', 'skills', 'environments', 'favorite-things', 'item-categories', 'item-usages', 'acquisition-methods', 'items', 'maps', 'habitats', 'daily-checklist-items', 'life-tags' ) ); ALTER TABLE entity_translations DROP CONSTRAINT IF EXISTS entity_translations_field_name_check; ALTER TABLE entity_translations ADD CONSTRAINT entity_translations_field_name_check CHECK (field_name IN ('name', 'title', 'details', 'genus')); CREATE TABLE IF NOT EXISTS users ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, email text NOT NULL UNIQUE, display_name text NOT NULL, password_hash text NOT NULL, email_verified_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CHECK (email = lower(email)), CHECK (length(display_name) BETWEEN 1 AND 40) ); CREATE TABLE IF NOT EXISTS system_wording_keys ( key text PRIMARY KEY, module text NOT NULL, surface text NOT NULL CHECK (surface IN ('frontend', 'backend', 'email')), description text NOT NULL DEFAULT '', placeholders jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(placeholders) = 'array'), enabled boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CHECK (key ~ '^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z][A-Za-z0-9]*)+$'), CHECK (length(module) BETWEEN 1 AND 80) ); CREATE INDEX IF NOT EXISTS system_wording_keys_module_idx ON system_wording_keys(module, key); CREATE INDEX IF NOT EXISTS system_wording_keys_surface_idx ON system_wording_keys(surface, key); CREATE TABLE IF NOT EXISTS system_wording_values ( key text NOT NULL REFERENCES system_wording_keys(key) ON DELETE CASCADE, locale text NOT NULL REFERENCES languages(code) ON DELETE CASCADE, value text NOT NULL CHECK (length(value) > 0), created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (key, locale) ); CREATE INDEX IF NOT EXISTS system_wording_values_locale_idx ON system_wording_values(locale, key); CREATE TABLE IF NOT EXISTS email_verification_tokens ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash text NOT NULL UNIQUE, expires_at timestamptz NOT NULL, used_at timestamptz, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS email_verification_tokens_user_id_idx ON email_verification_tokens(user_id); CREATE TABLE IF NOT EXISTS password_reset_tokens ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash text NOT NULL UNIQUE, expires_at timestamptz NOT NULL, used_at timestamptz, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS password_reset_tokens_user_id_idx ON password_reset_tokens(user_id); CREATE TABLE IF NOT EXISTS user_sessions ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash text NOT NULL UNIQUE, expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS user_sessions_user_id_idx ON user_sessions(user_id); CREATE TABLE IF NOT EXISTS daily_checklist_items ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, title text NOT NULL, sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0), created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS daily_checklist_items_sort_order_idx ON daily_checklist_items(sort_order, id); CREATE TABLE IF NOT EXISTS life_tags ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0), created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS life_posts ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, body text NOT NULL CHECK (length(body) BETWEEN 1 AND 2000), created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, deleted_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, deleted_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE life_posts DROP COLUMN IF EXISTS link_url; ALTER TABLE life_posts DROP COLUMN IF EXISTS link_title; ALTER TABLE life_posts ADD COLUMN IF NOT EXISTS deleted_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE life_posts ADD COLUMN IF NOT EXISTS deleted_at timestamptz; CREATE INDEX IF NOT EXISTS life_posts_created_at_idx ON life_posts(created_at DESC, id DESC); CREATE INDEX IF NOT EXISTS life_posts_active_created_at_idx ON life_posts(created_at DESC, id DESC) WHERE deleted_at IS NULL; CREATE TABLE IF NOT EXISTS life_post_tags ( post_id integer NOT NULL REFERENCES life_posts(id) ON DELETE CASCADE, tag_id integer NOT NULL REFERENCES life_tags(id) ON DELETE CASCADE, PRIMARY KEY (post_id, tag_id) ); CREATE INDEX IF NOT EXISTS life_post_tags_tag_idx ON life_post_tags(tag_id, post_id); CREATE TABLE IF NOT EXISTS life_post_comments ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, post_id integer NOT NULL REFERENCES life_posts(id) ON DELETE CASCADE, parent_comment_id integer REFERENCES life_post_comments(id) ON DELETE SET NULL, body text NOT NULL CHECK (length(body) BETWEEN 1 AND 1000), created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, deleted_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, deleted_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS life_post_comments_post_idx ON life_post_comments(post_id, created_at, id); CREATE INDEX IF NOT EXISTS life_post_comments_parent_idx ON life_post_comments(parent_comment_id, created_at, id); CREATE TABLE IF NOT EXISTS life_post_reactions ( post_id integer NOT NULL REFERENCES life_posts(id) ON DELETE CASCADE, user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE, reaction_type text NOT NULL CHECK (reaction_type IN ('like', 'helpful', 'fun', 'thanks')), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (post_id, user_id) ); CREATE INDEX IF NOT EXISTS life_post_reactions_post_idx ON life_post_reactions(post_id, reaction_type); CREATE TABLE IF NOT EXISTS skills ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, has_item_drop boolean NOT NULL DEFAULT false, sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); ALTER TABLE skills DROP COLUMN IF EXISTS subcategory; ALTER TABLE skills ADD COLUMN IF NOT EXISTS has_item_drop boolean NOT NULL DEFAULT false; CREATE UNIQUE INDEX IF NOT EXISTS skills_name_key ON skills(name); CREATE TABLE IF NOT EXISTS favorite_things ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); CREATE TABLE IF NOT EXISTS pokemon_types ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); CREATE TABLE IF NOT EXISTS pokemon ( id integer PRIMARY KEY, display_id integer NOT NULL CHECK (display_id > 0), name text NOT NULL UNIQUE, is_event_item boolean NOT NULL DEFAULT false, genus text NOT NULL DEFAULT '', details text NOT NULL DEFAULT '', height_inches double precision NOT NULL DEFAULT 0 CHECK (height_inches >= 0), weight_pounds double precision NOT NULL DEFAULT 0 CHECK (weight_pounds >= 0), environment_id integer NOT NULL REFERENCES environments(id), hp integer NOT NULL DEFAULT 0 CHECK (hp >= 0), attack integer NOT NULL DEFAULT 0 CHECK (attack >= 0), defense integer NOT NULL DEFAULT 0 CHECK (defense >= 0), special_attack integer NOT NULL DEFAULT 0 CHECK (special_attack >= 0), special_defense integer NOT NULL DEFAULT 0 CHECK (special_defense >= 0), speed integer NOT NULL DEFAULT 0 CHECK (speed >= 0), image_path text NOT NULL DEFAULT '', image_style text NOT NULL DEFAULT '', image_version text NOT NULL DEFAULT '', image_variant text NOT NULL DEFAULT '', image_description text NOT NULL DEFAULT '', sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); CREATE TABLE IF NOT EXISTS pokemon_pokemon_types ( pokemon_id integer NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE, type_id integer NOT NULL REFERENCES pokemon_types(id) ON DELETE CASCADE, slot_order integer NOT NULL CHECK (slot_order BETWEEN 1 AND 2), PRIMARY KEY (pokemon_id, type_id), UNIQUE (pokemon_id, slot_order) ); CREATE TABLE IF NOT EXISTS pokemon_skills ( pokemon_id integer NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE, skill_id integer NOT NULL REFERENCES skills(id), PRIMARY KEY (pokemon_id, skill_id) ); CREATE TABLE IF NOT EXISTS pokemon_favorite_things ( pokemon_id integer NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE, favorite_thing_id integer NOT NULL REFERENCES favorite_things(id), PRIMARY KEY (pokemon_id, favorite_thing_id) ); CREATE TABLE IF NOT EXISTS item_categories ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); CREATE TABLE IF NOT EXISTS item_usages ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); CREATE TABLE IF NOT EXISTS acquisition_methods ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); CREATE TABLE IF NOT EXISTS items ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, category_id integer NOT NULL REFERENCES item_categories(id), usage_id integer REFERENCES item_usages(id), dyeable boolean NOT NULL DEFAULT false, dual_dyeable boolean NOT NULL DEFAULT false, pattern_editable boolean NOT NULL DEFAULT false, no_recipe boolean NOT NULL DEFAULT false, is_event_item boolean NOT NULL DEFAULT false, image_path text NOT NULL DEFAULT '', sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); ALTER TABLE items ALTER COLUMN usage_id DROP NOT NULL; ALTER TABLE items ADD COLUMN IF NOT EXISTS no_recipe boolean NOT NULL DEFAULT false; ALTER TABLE items ADD COLUMN IF NOT EXISTS is_event_item boolean NOT NULL DEFAULT false; ALTER TABLE items ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT ''; ALTER TABLE items DROP COLUMN IF EXISTS no_habitat; CREATE TABLE IF NOT EXISTS recipes ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, item_id integer NOT NULL UNIQUE REFERENCES items(id), sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); ALTER TABLE recipes ADD COLUMN IF NOT EXISTS item_id integer REFERENCES items(id); DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'items' AND column_name = 'recipe_id' ) THEN EXECUTE ' UPDATE recipes r SET item_id = linked.item_id FROM ( SELECT DISTINCT ON (recipe_id) recipe_id, id AS item_id FROM items WHERE recipe_id IS NOT NULL ORDER BY recipe_id, id ) linked WHERE r.id = linked.recipe_id AND r.item_id IS NULL '; END IF; END $$; DELETE FROM recipes WHERE item_id IS NULL; ALTER TABLE recipes ALTER COLUMN item_id SET NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS recipes_item_id_key ON recipes(item_id); ALTER TABLE recipes DROP COLUMN IF EXISTS name; ALTER TABLE items DROP COLUMN IF EXISTS recipe_id; CREATE TABLE IF NOT EXISTS recipe_acquisition_methods ( recipe_id integer NOT NULL REFERENCES recipes(id) ON DELETE CASCADE, acquisition_method_id integer NOT NULL REFERENCES acquisition_methods(id), PRIMARY KEY (recipe_id, acquisition_method_id) ); DROP TABLE IF EXISTS item_item_tags; DROP TABLE IF EXISTS item_tags; CREATE TABLE IF NOT EXISTS item_acquisition_methods ( item_id integer NOT NULL REFERENCES items(id) ON DELETE CASCADE, acquisition_method_id integer NOT NULL REFERENCES acquisition_methods(id), PRIMARY KEY (item_id, acquisition_method_id) ); CREATE TABLE IF NOT EXISTS item_favorite_things ( item_id integer NOT NULL REFERENCES items(id) ON DELETE CASCADE, favorite_thing_id integer NOT NULL REFERENCES favorite_things(id), PRIMARY KEY (item_id, favorite_thing_id) ); CREATE TABLE IF NOT EXISTS pokemon_skill_item_drops ( pokemon_id integer NOT NULL, skill_id integer NOT NULL, item_id integer NOT NULL REFERENCES items(id), PRIMARY KEY (pokemon_id, skill_id), FOREIGN KEY (pokemon_id, skill_id) REFERENCES pokemon_skills(pokemon_id, skill_id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS recipe_materials ( recipe_id integer NOT NULL REFERENCES recipes(id) ON DELETE CASCADE, item_id integer NOT NULL REFERENCES items(id), quantity integer NOT NULL CHECK (quantity > 0), PRIMARY KEY (recipe_id, item_id) ); CREATE TABLE IF NOT EXISTS maps ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); CREATE TABLE IF NOT EXISTS habitats ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, is_event_item boolean NOT NULL DEFAULT false, image_path text NOT NULL DEFAULT '', sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); CREATE TABLE IF NOT EXISTS habitat_recipe_items ( habitat_id integer NOT NULL REFERENCES habitats(id) ON DELETE CASCADE, item_id integer NOT NULL REFERENCES items(id), quantity integer NOT NULL CHECK (quantity > 0), PRIMARY KEY (habitat_id, item_id) ); CREATE TABLE IF NOT EXISTS habitat_pokemon ( habitat_id integer NOT NULL REFERENCES habitats(id) ON DELETE CASCADE, pokemon_id integer NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE, map_id integer NOT NULL REFERENCES maps(id), time_of_day text NOT NULL CHECK (time_of_day IN ('早晨', '中午', '傍晚', '晚上')), weather text NOT NULL CHECK (weather IN ('晴天', '阴天', '雨天')), rarity integer NOT NULL CHECK (rarity BETWEEN 1 AND 3), PRIMARY KEY (habitat_id, pokemon_id, map_id, time_of_day, weather) ); ALTER TABLE environments ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE environments ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE environments ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE environments ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE environments ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE skills ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE skills ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE skills ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE skills ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE skills ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE favorite_things ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE favorite_things ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE favorite_things ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE favorite_things ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE favorite_things ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE pokemon_types ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE pokemon_types ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE pokemon_types ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE pokemon_types ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE pokemon_types ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS display_id integer; UPDATE pokemon SET display_id = id WHERE display_id IS NULL; ALTER TABLE pokemon ALTER COLUMN display_id SET NOT NULL; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS is_event_item boolean NOT NULL DEFAULT false; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS genus text NOT NULL DEFAULT ''; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS details text NOT NULL DEFAULT ''; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS height_inches double precision NOT NULL DEFAULT 0 CHECK (height_inches >= 0); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS weight_pounds double precision NOT NULL DEFAULT 0 CHECK (weight_pounds >= 0); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS hp integer NOT NULL DEFAULT 0 CHECK (hp >= 0); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS attack integer NOT NULL DEFAULT 0 CHECK (attack >= 0); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS defense integer NOT NULL DEFAULT 0 CHECK (defense >= 0); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS special_attack integer NOT NULL DEFAULT 0 CHECK (special_attack >= 0); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS special_defense integer NOT NULL DEFAULT 0 CHECK (special_defense >= 0); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS speed integer NOT NULL DEFAULT 0 CHECK (speed >= 0); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT ''; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS image_style text NOT NULL DEFAULT ''; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS image_version text NOT NULL DEFAULT ''; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS image_variant text NOT NULL DEFAULT ''; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS image_description text NOT NULL DEFAULT ''; ALTER TABLE life_tags ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE life_tags ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE life_tags ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE life_tags ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE life_tags ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE item_categories ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE item_categories ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE item_categories ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE item_categories ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE item_categories ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE item_usages ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE item_usages ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE item_usages ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE item_usages ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE item_usages ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE acquisition_methods ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE acquisition_methods ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE acquisition_methods ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE acquisition_methods ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE acquisition_methods ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE items ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE items ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE items ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE items ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE items ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE items ADD COLUMN IF NOT EXISTS is_event_item boolean NOT NULL DEFAULT false; ALTER TABLE recipes ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE recipes ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE recipes ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE recipes ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE recipes ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE maps ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE maps ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE maps ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE maps ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE maps ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE habitats ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE habitats ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE habitats ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE habitats ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE habitats ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); ALTER TABLE habitats ADD COLUMN IF NOT EXISTS is_event_item boolean NOT NULL DEFAULT false; ALTER TABLE habitats ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT ''; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM environments WHERE sort_order = 0 ) UPDATE environments target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM skills WHERE sort_order = 0 ) UPDATE skills target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM favorite_things WHERE sort_order = 0 ) UPDATE favorite_things target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM pokemon_types WHERE sort_order = 0 ) UPDATE pokemon_types target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM pokemon WHERE sort_order = 0 ) UPDATE pokemon target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM life_tags WHERE sort_order = 0 ) UPDATE life_tags target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM item_categories WHERE sort_order = 0 ) UPDATE item_categories target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM item_usages WHERE sort_order = 0 ) UPDATE item_usages target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM acquisition_methods WHERE sort_order = 0 ) UPDATE acquisition_methods target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM items WHERE sort_order = 0 ) UPDATE items target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM recipes WHERE sort_order = 0 ) UPDATE recipes target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM maps WHERE sort_order = 0 ) UPDATE maps target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; WITH ordered AS ( SELECT id, (row_number() OVER (ORDER BY created_at, id) * 10)::integer AS next_sort_order FROM habitats WHERE sort_order = 0 ) UPDATE habitats target SET sort_order = ordered.next_sort_order FROM ordered WHERE target.id = ordered.id; CREATE INDEX IF NOT EXISTS environments_sort_order_idx ON environments(sort_order, id); CREATE INDEX IF NOT EXISTS skills_sort_order_idx ON skills(sort_order, id); CREATE INDEX IF NOT EXISTS favorite_things_sort_order_idx ON favorite_things(sort_order, id); CREATE INDEX IF NOT EXISTS pokemon_types_sort_order_idx ON pokemon_types(sort_order, id); CREATE INDEX IF NOT EXISTS pokemon_sort_order_idx ON pokemon(sort_order, id); CREATE UNIQUE INDEX IF NOT EXISTS pokemon_display_event_item_key ON pokemon(display_id, is_event_item); CREATE INDEX IF NOT EXISTS life_tags_sort_order_idx ON life_tags(sort_order, id); CREATE INDEX IF NOT EXISTS item_categories_sort_order_idx ON item_categories(sort_order, id); CREATE INDEX IF NOT EXISTS item_usages_sort_order_idx ON item_usages(sort_order, id); CREATE INDEX IF NOT EXISTS acquisition_methods_sort_order_idx ON acquisition_methods(sort_order, id); CREATE INDEX IF NOT EXISTS items_sort_order_idx ON items(sort_order, id); CREATE INDEX IF NOT EXISTS recipes_sort_order_idx ON recipes(sort_order, id); CREATE INDEX IF NOT EXISTS maps_sort_order_idx ON maps(sort_order, id); CREATE INDEX IF NOT EXISTS habitats_sort_order_idx ON habitats(sort_order, id); CREATE TABLE IF NOT EXISTS wiki_edit_logs ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, entity_type text NOT NULL, entity_id integer NOT NULL, action text NOT NULL CHECK (action IN ('create', 'update', 'delete')), user_id integer REFERENCES users(id) ON DELETE SET NULL, changes jsonb NOT NULL DEFAULT '[]'::jsonb, created_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE wiki_edit_logs ADD COLUMN IF NOT EXISTS changes jsonb NOT NULL DEFAULT '[]'::jsonb; CREATE INDEX IF NOT EXISTS wiki_edit_logs_entity_idx ON wiki_edit_logs(entity_type, entity_id, created_at DESC); CREATE INDEX IF NOT EXISTS wiki_edit_logs_user_id_idx ON wiki_edit_logs(user_id); CREATE TABLE IF NOT EXISTS entity_image_uploads ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, entity_type text NOT NULL CHECK (entity_type IN ('pokemon', 'items', 'habitats')), entity_id integer, entity_name text NOT NULL, path text NOT NULL UNIQUE, original_filename text NOT NULL DEFAULT '', mime_type text NOT NULL CHECK (mime_type IN ('image/png', 'image/jpeg', 'image/webp', 'image/gif')), byte_size integer NOT NULL CHECK (byte_size > 0), created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), CHECK (length(entity_name) BETWEEN 1 AND 120), CHECK (path !~ '(^/|\\.\\.)') ); ALTER TABLE entity_image_uploads DROP CONSTRAINT IF EXISTS entity_image_uploads_entity_type_check; ALTER TABLE entity_image_uploads ADD CONSTRAINT entity_image_uploads_entity_type_check CHECK ( entity_type IN ('pokemon', 'items', 'habitats') ); ALTER TABLE entity_image_uploads DROP CONSTRAINT IF EXISTS entity_image_uploads_mime_type_check; ALTER TABLE entity_image_uploads ADD CONSTRAINT entity_image_uploads_mime_type_check CHECK ( mime_type IN ('image/png', 'image/jpeg', 'image/webp', 'image/gif') ); CREATE INDEX IF NOT EXISTS entity_image_uploads_entity_idx ON entity_image_uploads(entity_type, entity_id, created_at DESC, id DESC); CREATE INDEX IF NOT EXISTS entity_image_uploads_user_idx ON entity_image_uploads(created_by_user_id); CREATE TABLE IF NOT EXISTS entity_discussion_comments ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, entity_type text NOT NULL CHECK (entity_type IN ('pokemon', 'items', 'recipes', 'habitats')), entity_id integer NOT NULL, parent_comment_id integer REFERENCES entity_discussion_comments(id) ON DELETE CASCADE, body text NOT NULL CHECK (length(body) BETWEEN 1 AND 1000), created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, deleted_by_user_id integer REFERENCES users(id) ON DELETE SET NULL, deleted_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE entity_discussion_comments DROP CONSTRAINT IF EXISTS entity_discussion_comments_entity_type_check; ALTER TABLE entity_discussion_comments ADD CONSTRAINT entity_discussion_comments_entity_type_check CHECK ( entity_type IN ('pokemon', 'items', 'recipes', 'habitats') ); ALTER TABLE entity_discussion_comments ADD COLUMN IF NOT EXISTS deleted_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE entity_discussion_comments ADD COLUMN IF NOT EXISTS deleted_at timestamptz; ALTER TABLE entity_discussion_comments ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); CREATE INDEX IF NOT EXISTS entity_discussion_comments_entity_idx ON entity_discussion_comments(entity_type, entity_id, created_at, id); CREATE INDEX IF NOT EXISTS entity_discussion_comments_parent_idx ON entity_discussion_comments(parent_comment_id, created_at, id); CREATE INDEX IF NOT EXISTS entity_discussion_comments_user_idx ON entity_discussion_comments(created_by_user_id);