chore(db): clean up redundant schema migrations and legacy import logic

Remove obsolete ALTER TABLE statements and data migration blocks that are already reflected in base
table definitions.
Simplify data tool import normalization by removing legacy artifact mapping and unused entity types.
This commit is contained in:
2026-05-05 11:51:08 +08:00
parent 5a83a73108
commit 0e2743b469
2 changed files with 7 additions and 526 deletions

View File

@@ -26,8 +26,6 @@ CREATE TABLE IF NOT EXISTS entity_translations (
'skills',
'environments',
'favorite-things',
'item-categories',
'item-usages',
'acquisition-methods',
'items',
'ancient-artifacts',
@@ -51,41 +49,6 @@ CREATE TABLE IF NOT EXISTS entity_translations (
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',
'ancient-artifacts',
'maps',
'habitats',
'daily-checklist-items',
'life-tags',
'game-versions',
'dish-categories',
'dish-flavors',
'dishes'
)
);
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', 'effect', 'mosslaxEffect')
);
CREATE TABLE IF NOT EXISTS users (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
@@ -201,22 +164,10 @@ CREATE TABLE IF NOT EXISTS ai_moderation_settings (
CHECK (length(model) BETWEEN 1 AND 120)
);
ALTER TABLE ai_moderation_settings
ADD COLUMN IF NOT EXISTS api_format text NOT NULL DEFAULT 'gemini-generate-content' CHECK (api_format IN ('gemini-generate-content', 'openai-chat-completions')),
ADD COLUMN IF NOT EXISTS auth_mode text NOT NULL DEFAULT 'bearer-token' CHECK (auth_mode IN ('query-key', 'bearer-token'));
INSERT INTO ai_moderation_settings (id)
VALUES (true)
ON CONFLICT (id) DO NOTHING;
UPDATE ai_moderation_settings
SET api_format = 'gemini-generate-content',
auth_mode = 'bearer-token',
updated_at = now()
WHERE api_format = 'openai-chat-completions'
AND auth_mode = 'query-key'
AND endpoint ~* '/v1beta/?$';
CREATE TABLE IF NOT EXISTS ai_moderation_cache (
content_hash text NOT NULL,
model text NOT NULL,
@@ -229,9 +180,6 @@ CREATE TABLE IF NOT EXISTS ai_moderation_cache (
CHECK (length(model) BETWEEN 1 AND 120)
);
ALTER TABLE ai_moderation_cache
ADD COLUMN IF NOT EXISTS reason text;
CREATE TABLE IF NOT EXISTS rate_limit_settings (
id boolean PRIMARY KEY DEFAULT true CHECK (id = true),
settings jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(settings) = 'object'),
@@ -918,10 +866,6 @@ CREATE TABLE IF NOT EXISTS pokemon (
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE pokemon
ADD COLUMN IF NOT EXISTS data_id integer CHECK (data_id > 0),
ADD COLUMN IF NOT EXISTS data_identifier text NOT NULL DEFAULT '';
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,
@@ -942,26 +886,6 @@ CREATE TABLE IF NOT EXISTS pokemon_favorite_things (
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),
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 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),
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 acquisition_methods (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name text NOT NULL UNIQUE,
@@ -980,8 +904,6 @@ CREATE TABLE IF NOT EXISTS items (
ancient_artifact_category_key text,
category_key text NOT NULL DEFAULT 'other',
usage_key text,
category_id integer 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,
@@ -1071,58 +993,6 @@ CREATE TABLE IF NOT EXISTS dish_categories (
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE dish_categories
ADD COLUMN IF NOT EXISTS main_material_item_id integer REFERENCES items(id);
ALTER TABLE dish_categories
ADD COLUMN IF NOT EXISTS total_material_quantity integer NOT NULL DEFAULT 2;
DO $$
BEGIN
IF to_regclass('public.dishes') IS NOT NULL
AND EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'dishes'
AND column_name = 'main_material_item_id'
)
THEN
EXECUTE '
UPDATE dish_categories dc
SET main_material_item_id = source.main_material_item_id
FROM (
SELECT DISTINCT ON (category_id) category_id, main_material_item_id
FROM dishes
WHERE main_material_item_id IS NOT NULL
ORDER BY category_id, sort_order, id
) AS source
WHERE dc.id = source.category_id
AND dc.main_material_item_id IS NULL
';
END IF;
END $$;
UPDATE dish_categories
SET main_material_item_id = cookware_item_id
WHERE main_material_item_id IS NULL;
ALTER TABLE dish_categories
ALTER COLUMN main_material_item_id SET NOT NULL;
ALTER TABLE dish_categories
ALTER COLUMN total_material_quantity SET DEFAULT 2;
UPDATE dish_categories
SET total_material_quantity = 2
WHERE total_material_quantity < 2;
ALTER TABLE dish_categories
DROP CONSTRAINT IF EXISTS dish_categories_total_material_quantity_check;
ALTER TABLE dish_categories
ADD CONSTRAINT dish_categories_total_material_quantity_check CHECK (total_material_quantity >= 2);
CREATE TABLE IF NOT EXISTS dish_flavors (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name text NOT NULL UNIQUE,
@@ -1154,15 +1024,6 @@ CREATE TABLE IF NOT EXISTS dishes (
)
);
ALTER TABLE dishes
ADD COLUMN IF NOT EXISTS flavor_id integer REFERENCES dish_flavors(id);
ALTER TABLE dishes
ALTER COLUMN secondary_material_1_item_id DROP NOT NULL;
ALTER TABLE dishes
DROP COLUMN IF EXISTS main_material_item_id;
CREATE TABLE IF NOT EXISTS maps (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name text NOT NULL UNIQUE,
@@ -1202,221 +1063,6 @@ CREATE TABLE IF NOT EXISTS habitat_pokemon (
PRIMARY KEY (habitat_id, pokemon_id, map_id, time_of_day, weather)
);
ALTER TABLE life_tags
ADD COLUMN IF NOT EXISTS is_default boolean NOT NULL DEFAULT false;
ALTER TABLE items
ADD COLUMN IF NOT EXISTS details text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS base_price integer,
ADD COLUMN IF NOT EXISTS ancient_artifact_category_key text,
ADD COLUMN IF NOT EXISTS category_key text,
ADD COLUMN IF NOT EXISTS usage_key text;
UPDATE items
SET base_price = NULL
WHERE base_price < 0;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'items'
AND column_name = 'category_id'
AND table_schema = current_schema()
) THEN
ALTER TABLE items ALTER COLUMN category_id DROP NOT NULL;
END IF;
END $$;
UPDATE items i
SET category_key = CASE lower(trim(c.name))
WHEN 'furniture' THEN 'furniture'
WHEN 'misc' THEN 'misc'
WHEN 'outdoor' THEN 'outdoor'
WHEN 'utilities' THEN 'utilities'
WHEN 'buildings' THEN 'buildings'
WHEN 'blocks' THEN 'blocks'
WHEN 'kits' THEN 'kits'
WHEN 'nature' THEN 'nature'
WHEN 'food' THEN 'food'
WHEN 'materials' THEN 'materials'
WHEN 'key items' THEN 'key-items'
WHEN 'key-items' THEN 'key-items'
WHEN 'other' THEN 'other'
ELSE 'other'
END
FROM item_categories c
WHERE i.category_id = c.id
AND (i.category_key IS NULL OR i.category_key = '');
UPDATE items i
SET usage_key = CASE lower(trim(u.name))
WHEN 'decoration' THEN 'decoration'
WHEN 'relaxation' THEN 'relaxation'
WHEN 'toy' THEN 'toy'
WHEN 'road' THEN 'road'
ELSE NULL
END
FROM item_usages u
WHERE i.usage_id = u.id
AND i.usage_key IS NULL;
UPDATE items
SET category_key = 'other'
WHERE category_key IS NULL
OR category_key NOT IN (
'furniture',
'misc',
'outdoor',
'utilities',
'buildings',
'blocks',
'kits',
'nature',
'food',
'materials',
'key-items',
'other'
);
UPDATE items
SET usage_key = NULL
WHERE usage_key IS NOT NULL
AND usage_key NOT IN ('decoration', 'relaxation', 'toy', 'road');
ALTER TABLE items
ALTER COLUMN category_key SET NOT NULL,
ALTER COLUMN category_key SET DEFAULT 'other';
ALTER TABLE items
DROP CONSTRAINT IF EXISTS items_display_id_positive,
DROP CONSTRAINT IF EXISTS items_base_price_check,
DROP CONSTRAINT IF EXISTS items_ancient_artifact_category_key_check,
DROP CONSTRAINT IF EXISTS items_category_key_check,
DROP CONSTRAINT IF EXISTS items_usage_key_check;
ALTER TABLE items
ADD CONSTRAINT items_base_price_check CHECK (base_price IS NULL OR base_price >= 0),
ADD CONSTRAINT items_ancient_artifact_category_key_check CHECK (
ancient_artifact_category_key IS NULL
OR ancient_artifact_category_key IN ('lost-relics-l', 'lost-relics-s', 'fossils')
),
ADD CONSTRAINT items_category_key_check CHECK (category_key IN (
'furniture',
'misc',
'outdoor',
'utilities',
'buildings',
'blocks',
'kits',
'nature',
'food',
'materials',
'key-items',
'other'
)),
ADD CONSTRAINT items_usage_key_check CHECK (usage_key IS NULL OR usage_key IN ('decoration', 'relaxation', 'toy', 'road'));
DO $$
BEGIN
IF to_regclass('ancient_artifacts') IS NOT NULL THEN
ALTER TABLE ancient_artifacts
ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT '';
CREATE TEMP TABLE migrated_ancient_artifact_items (
old_id integer PRIMARY KEY,
item_id integer NOT NULL
) ON COMMIT DROP;
INSERT INTO items (
name,
details,
ancient_artifact_category_key,
category_key,
image_path,
sort_order,
created_by_user_id,
updated_by_user_id,
created_at,
updated_at
)
SELECT
a.name,
a.details,
a.category_key,
'other',
a.image_path,
a.sort_order,
a.created_by_user_id,
a.updated_by_user_id,
a.created_at,
a.updated_at
FROM ancient_artifacts a
ON CONFLICT (name) DO UPDATE
SET ancient_artifact_category_key = EXCLUDED.ancient_artifact_category_key,
details = CASE WHEN items.details = '' THEN EXCLUDED.details ELSE items.details END,
image_path = CASE WHEN items.image_path = '' THEN EXCLUDED.image_path ELSE items.image_path END,
updated_by_user_id = COALESCE(items.updated_by_user_id, EXCLUDED.updated_by_user_id),
updated_at = GREATEST(items.updated_at, EXCLUDED.updated_at);
INSERT INTO migrated_ancient_artifact_items (old_id, item_id)
SELECT a.id, i.id
FROM ancient_artifacts a
JOIN items i ON i.name = a.name
ON CONFLICT (old_id) DO UPDATE SET item_id = EXCLUDED.item_id;
IF to_regclass('ancient_artifact_favorite_things') IS NOT NULL THEN
INSERT INTO item_favorite_things (item_id, favorite_thing_id)
SELECT m.item_id, aft.favorite_thing_id
FROM ancient_artifact_favorite_things aft
JOIN migrated_ancient_artifact_items m ON m.old_id = aft.ancient_artifact_id
ON CONFLICT DO NOTHING;
END IF;
INSERT INTO entity_translations (entity_type, entity_id, locale, field_name, value)
SELECT 'items', m.item_id, et.locale, et.field_name, et.value
FROM entity_translations et
JOIN migrated_ancient_artifact_items m ON m.old_id = et.entity_id
WHERE et.entity_type = 'ancient-artifacts'
ON CONFLICT (entity_type, entity_id, locale, field_name) DO UPDATE
SET value = EXCLUDED.value;
UPDATE wiki_edit_logs wel
SET entity_type = 'items',
entity_id = m.item_id
FROM migrated_ancient_artifact_items m
WHERE wel.entity_type = 'ancient-artifacts'
AND wel.entity_id = m.old_id;
UPDATE entity_image_uploads eiu
SET entity_type = 'items',
entity_id = m.item_id
FROM migrated_ancient_artifact_items m
WHERE eiu.entity_type = 'ancient-artifacts'
AND eiu.entity_id = m.old_id;
UPDATE entity_discussion_comments edc
SET entity_id = m.item_id
FROM migrated_ancient_artifact_items m
WHERE edc.entity_type = 'ancient-artifacts'
AND edc.entity_id = m.old_id;
DELETE FROM entity_translations
WHERE entity_type = 'ancient-artifacts';
END IF;
END $$;
DROP INDEX IF EXISTS items_display_event_item_key;
DROP INDEX IF EXISTS items_display_order_idx;
DROP INDEX IF EXISTS ancient_artifacts_display_order_idx;
ALTER TABLE items
DROP COLUMN IF EXISTS display_id;
DROP TABLE IF EXISTS ancient_artifact_favorite_things;
DROP TABLE IF EXISTS ancient_artifacts;
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);
@@ -1425,8 +1071,6 @@ 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 UNIQUE INDEX IF NOT EXISTS life_tags_single_default_idx ON life_tags(is_default) WHERE is_default = true;
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);
@@ -1468,14 +1112,6 @@ CREATE TABLE IF NOT EXISTS entity_image_uploads (
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', 'ancient-artifacts')
);
CREATE INDEX IF NOT EXISTS entity_image_uploads_entity_idx
ON entity_image_uploads(entity_type, entity_id, created_at DESC, id DESC);
@@ -1524,14 +1160,6 @@ CREATE INDEX IF NOT EXISTS entity_discussion_comment_likes_comment_idx
CREATE INDEX IF NOT EXISTS entity_discussion_comment_likes_user_idx
ON entity_discussion_comment_likes(user_id, created_at DESC, comment_id DESC);
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', 'ancient-artifacts')
);
CREATE TABLE IF NOT EXISTS notifications (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
recipient_user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@@ -1587,9 +1215,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS notifications_life_post_reaction_unique_idx
ON notifications(recipient_user_id, actor_user_id, life_post_id)
WHERE type = 'life_post_reaction' AND actor_user_id IS NOT NULL AND life_post_id IS NOT NULL;
ALTER TABLE notifications
ADD COLUMN IF NOT EXISTS profile_user_id integer REFERENCES users(id) ON DELETE CASCADE;
CREATE UNIQUE INDEX IF NOT EXISTS notifications_user_follow_unique_idx
ON notifications(recipient_user_id, actor_user_id, profile_user_id)
WHERE type = 'user_follow' AND actor_user_id IS NOT NULL AND profile_user_id IS NOT NULL;
@@ -1606,66 +1231,6 @@ CREATE TABLE IF NOT EXISTS notification_ws_tickets (
CREATE INDEX IF NOT EXISTS notification_ws_tickets_user_idx
ON notification_ws_tickets(user_id, expires_at DESC);
ALTER TABLE notifications
ADD COLUMN IF NOT EXISTS moderation_reason text;
ALTER TABLE notifications
ADD COLUMN IF NOT EXISTS profile_user_id integer REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE notifications
DROP CONSTRAINT IF EXISTS notifications_type_check;
ALTER TABLE notifications
ADD CONSTRAINT notifications_type_check CHECK (
type IN (
'life_post_comment',
'life_comment_reply',
'discussion_comment_reply',
'life_post_reaction',
'user_follow',
'moderation_result'
)
);
ALTER TABLE life_tags
ADD COLUMN IF NOT EXISTS is_rateable boolean NOT NULL DEFAULT false;
CREATE TABLE IF NOT EXISTS game_versions (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name text NOT NULL UNIQUE,
change_log text NOT NULL DEFAULT '',
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 game_versions_sort_order_idx
ON game_versions(sort_order, id);
ALTER TABLE life_posts
ADD COLUMN IF NOT EXISTS ai_moderation_status text NOT NULL DEFAULT 'unreviewed' CHECK (ai_moderation_status IN ('unreviewed', 'reviewing', 'approved', 'rejected', 'failed')),
ADD COLUMN IF NOT EXISTS category_id integer REFERENCES life_tags(id) ON DELETE RESTRICT,
ADD COLUMN IF NOT EXISTS game_version_id integer REFERENCES game_versions(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS ai_moderation_language_code text REFERENCES languages(code) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS ai_moderation_reason text,
ADD COLUMN IF NOT EXISTS ai_moderation_content_hash text,
ADD COLUMN IF NOT EXISTS ai_moderation_checked_at timestamptz,
ADD COLUMN IF NOT EXISTS ai_moderation_retry_count integer NOT NULL DEFAULT 0 CHECK (ai_moderation_retry_count >= 0),
ADD COLUMN IF NOT EXISTS ai_moderation_updated_at timestamptz NOT NULL DEFAULT now();
UPDATE life_posts lp
SET category_id = selected.tag_id
FROM (
SELECT DISTINCT ON (lpt.post_id) lpt.post_id, lpt.tag_id
FROM life_post_tags lpt
JOIN life_tags lt ON lt.id = lpt.tag_id
ORDER BY lpt.post_id, lt.sort_order, lt.id
) selected
WHERE lp.id = selected.post_id
AND lp.category_id IS NULL;
CREATE INDEX IF NOT EXISTS life_posts_category_idx
ON life_posts(category_id, created_at DESC, id DESC)
WHERE deleted_at IS NULL;
@@ -1674,39 +1239,6 @@ CREATE INDEX IF NOT EXISTS life_posts_game_version_idx
ON life_posts(game_version_id, created_at DESC, id DESC)
WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS life_post_ratings (
post_id integer NOT NULL REFERENCES life_posts(id) ON DELETE CASCADE,
user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE,
rating integer NOT NULL CHECK (rating BETWEEN 1 AND 5),
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_ratings_post_idx
ON life_post_ratings(post_id, rating);
CREATE INDEX IF NOT EXISTS life_post_ratings_user_idx
ON life_post_ratings(user_id, updated_at DESC, post_id DESC);
ALTER TABLE life_post_comments
ADD COLUMN IF NOT EXISTS ai_moderation_status text NOT NULL DEFAULT 'unreviewed' CHECK (ai_moderation_status IN ('unreviewed', 'reviewing', 'approved', 'rejected', 'failed')),
ADD COLUMN IF NOT EXISTS ai_moderation_language_code text REFERENCES languages(code) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS ai_moderation_reason text,
ADD COLUMN IF NOT EXISTS ai_moderation_content_hash text,
ADD COLUMN IF NOT EXISTS ai_moderation_checked_at timestamptz,
ADD COLUMN IF NOT EXISTS ai_moderation_retry_count integer NOT NULL DEFAULT 0 CHECK (ai_moderation_retry_count >= 0),
ADD COLUMN IF NOT EXISTS ai_moderation_updated_at timestamptz NOT NULL DEFAULT now();
ALTER TABLE entity_discussion_comments
ADD COLUMN IF NOT EXISTS ai_moderation_status text NOT NULL DEFAULT 'unreviewed' CHECK (ai_moderation_status IN ('unreviewed', 'reviewing', 'approved', 'rejected', 'failed')),
ADD COLUMN IF NOT EXISTS ai_moderation_language_code text REFERENCES languages(code) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS ai_moderation_reason text,
ADD COLUMN IF NOT EXISTS ai_moderation_content_hash text,
ADD COLUMN IF NOT EXISTS ai_moderation_checked_at timestamptz,
ADD COLUMN IF NOT EXISTS ai_moderation_retry_count integer NOT NULL DEFAULT 0 CHECK (ai_moderation_retry_count >= 0),
ADD COLUMN IF NOT EXISTS ai_moderation_updated_at timestamptz NOT NULL DEFAULT now();
CREATE INDEX IF NOT EXISTS life_posts_ai_moderation_status_idx
ON life_posts(ai_moderation_status, ai_moderation_updated_at, id)
WHERE deleted_at IS NULL;