Introduce Ancient Artifacts with full CRUD and image support Migrate item categories and usages to system-defined lists Add display_id to items and artifacts for custom sorting
1317 lines
53 KiB
SQL
1317 lines
53 KiB
SQL
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',
|
|
'ancient-artifacts',
|
|
'maps',
|
|
'habitats',
|
|
'daily-checklist-items',
|
|
'life-tags',
|
|
'game-versions'
|
|
)
|
|
),
|
|
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',
|
|
'ancient-artifacts',
|
|
'maps',
|
|
'habitats',
|
|
'daily-checklist-items',
|
|
'life-tags',
|
|
'game-versions'
|
|
)
|
|
);
|
|
|
|
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,
|
|
referral_code text,
|
|
referred_by_user_id integer REFERENCES users(id) ON DELETE SET 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),
|
|
CHECK (referral_code IS NULL OR referral_code ~ '^[A-Z0-9]{8,16}$')
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS users_referral_code_idx
|
|
ON users(referral_code)
|
|
WHERE referral_code IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS users_referred_by_user_id_idx
|
|
ON users(referred_by_user_id);
|
|
|
|
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),
|
|
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 roles (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
key text NOT NULL UNIQUE,
|
|
name text NOT NULL,
|
|
description text NOT NULL DEFAULT '',
|
|
level integer NOT NULL DEFAULT 0 CHECK (level >= 0),
|
|
enabled boolean NOT NULL DEFAULT true,
|
|
system_role boolean NOT NULL DEFAULT false,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CHECK (key ~ '^[a-z][a-z0-9-]{1,63}$'),
|
|
CHECK (length(name) BETWEEN 1 AND 80)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS roles_level_idx
|
|
ON roles(level DESC, id);
|
|
|
|
CREATE TABLE IF NOT EXISTS permissions (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
key text NOT NULL UNIQUE,
|
|
name text NOT NULL,
|
|
description text NOT NULL DEFAULT '',
|
|
category text NOT NULL DEFAULT 'General',
|
|
enabled boolean NOT NULL DEFAULT true,
|
|
system_permission boolean NOT NULL DEFAULT false,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CHECK (key ~ '^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$'),
|
|
CHECK (length(name) BETWEEN 1 AND 120),
|
|
CHECK (length(category) BETWEEN 1 AND 80)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS permissions_category_idx
|
|
ON permissions(category, key);
|
|
|
|
CREATE TABLE IF NOT EXISTS role_permissions (
|
|
role_id integer NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
|
permission_id integer NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (role_id, permission_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS role_permissions_permission_id_idx
|
|
ON role_permissions(permission_id, role_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_roles (
|
|
user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
role_id integer NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
|
assigned_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
|
|
assigned_at timestamptz NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (user_id, role_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS user_roles_role_id_idx
|
|
ON user_roles(role_id, user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS ai_moderation_settings (
|
|
id boolean PRIMARY KEY DEFAULT true CHECK (id = true),
|
|
enabled boolean NOT NULL DEFAULT true,
|
|
api_format text NOT NULL DEFAULT 'gemini-generate-content' CHECK (api_format IN ('gemini-generate-content', 'openai-chat-completions')),
|
|
auth_mode text NOT NULL DEFAULT 'bearer-token' CHECK (auth_mode IN ('query-key', 'bearer-token')),
|
|
endpoint text NOT NULL DEFAULT 'https://ai.example.com/v1beta',
|
|
api_key text NOT NULL DEFAULT '',
|
|
model text NOT NULL DEFAULT 'gemini-2.0-flash-lite',
|
|
requests_per_minute integer NOT NULL DEFAULT 10 CHECK (requests_per_minute BETWEEN 1 AND 60),
|
|
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(),
|
|
CHECK (length(endpoint) BETWEEN 1 AND 300),
|
|
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,
|
|
status text NOT NULL CHECK (status IN ('approved', 'rejected')),
|
|
language_code text REFERENCES languages(code) ON DELETE SET NULL,
|
|
checked_at timestamptz NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (content_hash, model),
|
|
CHECK (length(content_hash) BETWEEN 32 AND 128),
|
|
CHECK (length(model) BETWEEN 1 AND 120)
|
|
);
|
|
|
|
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'),
|
|
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()
|
|
);
|
|
|
|
INSERT INTO rate_limit_settings (id)
|
|
VALUES (true)
|
|
ON CONFLICT (id) DO NOTHING;
|
|
|
|
INSERT INTO permissions (key, name, description, category, system_permission)
|
|
VALUES
|
|
('admin.access', 'Access admin', 'Open the management area.', 'Admin', true),
|
|
('admin.users.read', 'View users', 'View user role assignments.', 'Users', true),
|
|
('admin.users.update', 'Manage user roles', 'Assign and remove roles from users.', 'Users', true),
|
|
('admin.users.assign-owner', 'Assign Owner role', 'Assign and remove the Owner role from users.', 'Users', true),
|
|
('admin.roles.read', 'View roles', 'View role configuration.', 'Roles', true),
|
|
('admin.roles.create', 'Create roles', 'Create configurable roles.', 'Roles', true),
|
|
('admin.roles.update', 'Update roles', 'Edit roles and role permission assignments.', 'Roles', true),
|
|
('admin.roles.delete', 'Delete roles', 'Delete configurable roles.', 'Roles', true),
|
|
('admin.permissions.read', 'View permissions', 'View permission configuration.', 'Permissions', true),
|
|
('admin.permissions.create', 'Create permissions', 'Create configurable permissions.', 'Permissions', true),
|
|
('admin.permissions.update', 'Update permissions', 'Edit permission metadata and enabled state.', 'Permissions', true),
|
|
('admin.permissions.delete', 'Delete permissions', 'Delete configurable permissions.', 'Permissions', true),
|
|
('admin.languages.read', 'View languages', 'View language settings.', 'Languages', true),
|
|
('admin.languages.create', 'Create languages', 'Create languages.', 'Languages', true),
|
|
('admin.languages.update', 'Update languages', 'Edit language settings.', 'Languages', true),
|
|
('admin.languages.delete', 'Delete languages', 'Delete languages.', 'Languages', true),
|
|
('admin.languages.order', 'Order languages', 'Reorder languages.', 'Languages', true),
|
|
('admin.wordings.read', 'View system wordings', 'View system wording values.', 'System wordings', true),
|
|
('admin.wordings.update', 'Update system wordings', 'Edit system wording values.', 'System wordings', true),
|
|
('admin.ai-moderation.read', 'View AI moderation settings', 'View AI moderation configuration.', 'AI moderation', true),
|
|
('admin.ai-moderation.update', 'Update AI moderation settings', 'Edit AI moderation configuration.', 'AI moderation', true),
|
|
('admin.rate-limits.read', 'View rate limits', 'View user rate limit settings.', 'Rate limits', true),
|
|
('admin.rate-limits.update', 'Update rate limits', 'Edit user rate limit settings.', 'Rate limits', true),
|
|
('admin.config.read', 'View system config', 'View management configuration records.', 'System config', true),
|
|
('admin.config.create', 'Create system config', 'Create management configuration records.', 'System config', true),
|
|
('admin.config.update', 'Update system config', 'Edit management configuration records.', 'System config', true),
|
|
('admin.config.delete', 'Delete system config', 'Delete management configuration records.', 'System config', true),
|
|
('admin.config.order', 'Order system config', 'Reorder management configuration records.', 'System config', true),
|
|
('admin.data.export', 'Export data', 'Export content data bundles.', 'Data tools', true),
|
|
('admin.data.import', 'Import and wipe data', 'Import content data bundles and wipe content data.', 'Data tools', true),
|
|
('checklist.create', 'Create checklist tasks', 'Create Daily CheckList tasks.', 'CheckList', true),
|
|
('checklist.update', 'Update checklist tasks', 'Edit Daily CheckList tasks.', 'CheckList', true),
|
|
('checklist.delete', 'Delete checklist tasks', 'Delete Daily CheckList tasks.', 'CheckList', true),
|
|
('checklist.order', 'Order checklist tasks', 'Reorder Daily CheckList tasks.', 'CheckList', true),
|
|
('pokemon.create', 'Create Pokemon', 'Create Pokemon records.', 'Pokemon', true),
|
|
('pokemon.update', 'Update Pokemon', 'Edit Pokemon records.', 'Pokemon', true),
|
|
('pokemon.delete', 'Delete Pokemon', 'Delete Pokemon records.', 'Pokemon', true),
|
|
('pokemon.order', 'Order Pokemon', 'Reorder Pokemon records.', 'Pokemon', true),
|
|
('pokemon.fetch', 'Fetch Pokemon data', 'Fetch Pokemon data and sprite candidates.', 'Pokemon', true),
|
|
('pokemon.upload', 'Upload Pokemon images', 'Upload Pokemon images.', 'Pokemon', true),
|
|
('habitats.create', 'Create habitats', 'Create habitat records.', 'Habitats', true),
|
|
('habitats.update', 'Update habitats', 'Edit habitat records.', 'Habitats', true),
|
|
('habitats.delete', 'Delete habitats', 'Delete habitat records.', 'Habitats', true),
|
|
('habitats.order', 'Order habitats', 'Reorder habitat records.', 'Habitats', true),
|
|
('habitats.upload', 'Upload habitat images', 'Upload habitat images.', 'Habitats', true),
|
|
('items.create', 'Create items', 'Create item records.', 'Items', true),
|
|
('items.update', 'Update items', 'Edit item records.', 'Items', true),
|
|
('items.delete', 'Delete items', 'Delete item records.', 'Items', true),
|
|
('items.order', 'Order items', 'Reorder item records.', 'Items', true),
|
|
('items.upload', 'Upload item images', 'Upload item images.', 'Items', true),
|
|
('ancient-artifacts.create', 'Create Ancient Artifacts', 'Create Ancient Artifact records.', 'Ancient Artifacts', true),
|
|
('ancient-artifacts.update', 'Update Ancient Artifacts', 'Edit Ancient Artifact records.', 'Ancient Artifacts', true),
|
|
('ancient-artifacts.delete', 'Delete Ancient Artifacts', 'Delete Ancient Artifact records.', 'Ancient Artifacts', true),
|
|
('ancient-artifacts.order', 'Order Ancient Artifacts', 'Reorder Ancient Artifact records.', 'Ancient Artifacts', true),
|
|
('ancient-artifacts.upload', 'Upload Ancient Artifact images', 'Upload Ancient Artifact images.', 'Ancient Artifacts', true),
|
|
('recipes.create', 'Create recipes', 'Create recipe records.', 'Recipes', true),
|
|
('recipes.update', 'Update recipes', 'Edit recipe records.', 'Recipes', true),
|
|
('recipes.delete', 'Delete recipes', 'Delete recipe records.', 'Recipes', true),
|
|
('recipes.order', 'Order recipes', 'Reorder recipe records.', 'Recipes', true),
|
|
('life.posts.create', 'Create Life posts', 'Create Life posts.', 'Life', true),
|
|
('life.posts.update', 'Update own Life posts', 'Edit own Life posts.', 'Life', true),
|
|
('life.posts.delete', 'Delete own Life posts', 'Delete own Life posts.', 'Life', true),
|
|
('life.posts.update-any', 'Update any Life post', 'Edit any Life post.', 'Life', true),
|
|
('life.posts.delete-any', 'Delete any Life post', 'Delete any Life post.', 'Life', true),
|
|
('life.comments.create', 'Create Life comments', 'Create Life comments and replies.', 'Life', true),
|
|
('life.comments.delete', 'Delete own Life comments', 'Delete own Life comments.', 'Life', true),
|
|
('life.comments.delete-any', 'Delete any Life comment', 'Delete any Life comment.', 'Life', true),
|
|
('life.reactions.set', 'Set Life reactions', 'Set and remove Life reactions.', 'Life', true),
|
|
('life.ratings.set', 'Set Life ratings', 'Set and remove Life star ratings.', 'Life', true),
|
|
('discussions.comments.create', 'Create discussion comments', 'Create entity discussion comments and replies.', 'Discussions', true),
|
|
('discussions.comments.delete', 'Delete own discussion comments', 'Delete own entity discussion comments.', 'Discussions', true),
|
|
('discussions.comments.delete-any', 'Delete any discussion comment', 'Delete any entity discussion comment.', 'Discussions', true)
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
INSERT INTO roles (key, name, description, level, enabled, system_role)
|
|
VALUES
|
|
('owner', 'Owner', 'Highest-level system owner with all permissions.', 1000, true, true),
|
|
('admin', 'Admin', 'System manager with content, configuration and user administration permissions.', 800, true, true),
|
|
('editor', 'Editor', 'Wiki editor with content creation, update, sorting and community permissions.', 500, true, true),
|
|
('member', 'Member', 'Community member with Life and discussion permissions.', 100, true, true),
|
|
('viewer', 'Viewer', 'Read-only role for explicit access grouping.', 0, true, true)
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
CROSS JOIN permissions p
|
|
WHERE r.key = 'owner'
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
JOIN permissions p ON p.key = ANY (ARRAY[
|
|
'admin.access',
|
|
'admin.users.read',
|
|
'admin.users.update',
|
|
'admin.roles.read',
|
|
'admin.roles.create',
|
|
'admin.roles.update',
|
|
'admin.roles.delete',
|
|
'admin.permissions.read',
|
|
'admin.permissions.create',
|
|
'admin.permissions.update',
|
|
'admin.permissions.delete',
|
|
'admin.languages.read',
|
|
'admin.languages.create',
|
|
'admin.languages.update',
|
|
'admin.languages.delete',
|
|
'admin.languages.order',
|
|
'admin.wordings.read',
|
|
'admin.wordings.update',
|
|
'admin.ai-moderation.read',
|
|
'admin.ai-moderation.update',
|
|
'admin.rate-limits.read',
|
|
'admin.rate-limits.update',
|
|
'admin.config.read',
|
|
'admin.config.create',
|
|
'admin.config.update',
|
|
'admin.config.delete',
|
|
'admin.config.order',
|
|
'checklist.create',
|
|
'checklist.update',
|
|
'checklist.delete',
|
|
'checklist.order',
|
|
'pokemon.create',
|
|
'pokemon.update',
|
|
'pokemon.delete',
|
|
'pokemon.order',
|
|
'pokemon.fetch',
|
|
'pokemon.upload',
|
|
'habitats.create',
|
|
'habitats.update',
|
|
'habitats.delete',
|
|
'habitats.order',
|
|
'habitats.upload',
|
|
'items.create',
|
|
'items.update',
|
|
'items.delete',
|
|
'items.order',
|
|
'items.upload',
|
|
'ancient-artifacts.create',
|
|
'ancient-artifacts.update',
|
|
'ancient-artifacts.delete',
|
|
'ancient-artifacts.order',
|
|
'ancient-artifacts.upload',
|
|
'recipes.create',
|
|
'recipes.update',
|
|
'recipes.delete',
|
|
'recipes.order',
|
|
'life.posts.create',
|
|
'life.posts.update',
|
|
'life.posts.delete',
|
|
'life.posts.update-any',
|
|
'life.posts.delete-any',
|
|
'life.comments.create',
|
|
'life.comments.delete',
|
|
'life.comments.delete-any',
|
|
'life.reactions.set',
|
|
'life.ratings.set',
|
|
'discussions.comments.create',
|
|
'discussions.comments.delete',
|
|
'discussions.comments.delete-any'
|
|
])
|
|
WHERE r.key = 'admin'
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM role_permissions existing_role_permission
|
|
WHERE existing_role_permission.role_id = r.id
|
|
)
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
JOIN permissions p ON p.key = ANY (ARRAY[
|
|
'admin.ai-moderation.read',
|
|
'admin.ai-moderation.update'
|
|
])
|
|
WHERE r.key = 'admin'
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
JOIN permissions p ON p.key = ANY (ARRAY[
|
|
'admin.rate-limits.read',
|
|
'admin.rate-limits.update'
|
|
])
|
|
WHERE r.key = 'admin'
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
JOIN permissions p ON p.key = ANY (ARRAY[
|
|
'admin.access',
|
|
'admin.config.read',
|
|
'checklist.create',
|
|
'checklist.update',
|
|
'checklist.order',
|
|
'pokemon.create',
|
|
'pokemon.update',
|
|
'pokemon.order',
|
|
'pokemon.fetch',
|
|
'pokemon.upload',
|
|
'habitats.create',
|
|
'habitats.update',
|
|
'habitats.order',
|
|
'habitats.upload',
|
|
'items.create',
|
|
'items.update',
|
|
'items.order',
|
|
'items.upload',
|
|
'ancient-artifacts.create',
|
|
'ancient-artifacts.update',
|
|
'ancient-artifacts.order',
|
|
'ancient-artifacts.upload',
|
|
'recipes.create',
|
|
'recipes.update',
|
|
'recipes.order',
|
|
'life.posts.create',
|
|
'life.posts.update',
|
|
'life.posts.delete',
|
|
'life.comments.create',
|
|
'life.comments.delete',
|
|
'life.reactions.set',
|
|
'life.ratings.set',
|
|
'discussions.comments.create',
|
|
'discussions.comments.delete'
|
|
])
|
|
WHERE r.key = 'editor'
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM role_permissions existing_role_permission
|
|
WHERE existing_role_permission.role_id = r.id
|
|
)
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
JOIN permissions p ON p.key = ANY (ARRAY[
|
|
'ancient-artifacts.create',
|
|
'ancient-artifacts.update',
|
|
'ancient-artifacts.delete',
|
|
'ancient-artifacts.order',
|
|
'ancient-artifacts.upload'
|
|
])
|
|
WHERE r.key = 'admin'
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
JOIN permissions p ON p.key = ANY (ARRAY[
|
|
'ancient-artifacts.create',
|
|
'ancient-artifacts.update',
|
|
'ancient-artifacts.order',
|
|
'ancient-artifacts.upload'
|
|
])
|
|
WHERE r.key = 'editor'
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
JOIN permissions p ON p.key = ANY (ARRAY[
|
|
'life.posts.create',
|
|
'life.posts.update',
|
|
'life.posts.delete',
|
|
'life.comments.create',
|
|
'life.comments.delete',
|
|
'life.reactions.set',
|
|
'life.ratings.set',
|
|
'discussions.comments.create',
|
|
'discussions.comments.delete'
|
|
])
|
|
WHERE r.key = 'member'
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM role_permissions existing_role_permission
|
|
WHERE existing_role_permission.role_id = r.id
|
|
)
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
JOIN permissions p ON p.key = 'life.ratings.set'
|
|
WHERE r.key IN ('admin', 'editor', 'member')
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
WITH first_owner_user AS (
|
|
SELECT u.id
|
|
FROM users u
|
|
WHERE u.email_verified_at IS NOT NULL
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM user_roles ur
|
|
JOIN roles existing_role ON existing_role.id = ur.role_id
|
|
WHERE existing_role.key = 'owner'
|
|
)
|
|
ORDER BY u.email_verified_at ASC, u.id ASC
|
|
LIMIT 1
|
|
)
|
|
INSERT INTO user_roles (user_id, role_id)
|
|
SELECT first_owner_user.id, r.id
|
|
FROM first_owner_user
|
|
CROSS JOIN roles r
|
|
WHERE r.key = 'owner'
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO user_roles (user_id, role_id)
|
|
SELECT u.id, r.id
|
|
FROM users u
|
|
CROSS JOIN roles r
|
|
WHERE u.email_verified_at IS NOT NULL
|
|
AND r.key = 'editor'
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM user_roles ur
|
|
WHERE ur.user_id = u.id
|
|
)
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
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,
|
|
is_default boolean NOT NULL DEFAULT false,
|
|
is_rateable boolean NOT NULL DEFAULT false,
|
|
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 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);
|
|
|
|
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),
|
|
category_id integer REFERENCES life_tags(id) ON DELETE RESTRICT,
|
|
game_version_id integer REFERENCES game_versions(id) ON DELETE SET NULL,
|
|
ai_moderation_status text NOT NULL DEFAULT 'unreviewed' CHECK (ai_moderation_status IN ('unreviewed', 'reviewing', 'approved', 'rejected', 'failed')),
|
|
ai_moderation_language_code text REFERENCES languages(code) ON DELETE SET NULL,
|
|
ai_moderation_content_hash text,
|
|
ai_moderation_checked_at timestamptz,
|
|
ai_moderation_retry_count integer NOT NULL DEFAULT 0 CHECK (ai_moderation_retry_count >= 0),
|
|
ai_moderation_updated_at timestamptz NOT NULL DEFAULT now(),
|
|
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()
|
|
);
|
|
|
|
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 INDEX IF NOT EXISTS life_posts_user_created_at_idx
|
|
ON life_posts(created_by_user_id, 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),
|
|
ai_moderation_status text NOT NULL DEFAULT 'unreviewed' CHECK (ai_moderation_status IN ('unreviewed', 'reviewing', 'approved', 'rejected', 'failed')),
|
|
ai_moderation_language_code text REFERENCES languages(code) ON DELETE SET NULL,
|
|
ai_moderation_content_hash text,
|
|
ai_moderation_checked_at timestamptz,
|
|
ai_moderation_retry_count integer NOT NULL DEFAULT 0 CHECK (ai_moderation_retry_count >= 0),
|
|
ai_moderation_updated_at timestamptz NOT NULL DEFAULT now(),
|
|
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 INDEX IF NOT EXISTS life_post_comments_user_idx
|
|
ON life_post_comments(created_by_user_id, created_at DESC, id DESC);
|
|
|
|
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 INDEX IF NOT EXISTS life_post_reactions_user_idx
|
|
ON life_post_reactions(user_id, updated_at DESC, post_id DESC);
|
|
|
|
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);
|
|
|
|
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),
|
|
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 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),
|
|
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 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),
|
|
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 pokemon (
|
|
id integer PRIMARY KEY,
|
|
data_id integer CHECK (data_id > 0),
|
|
data_identifier text NOT NULL DEFAULT '',
|
|
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),
|
|
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()
|
|
);
|
|
|
|
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,
|
|
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),
|
|
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,
|
|
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 items (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
display_id integer NOT NULL CHECK (display_id > 0),
|
|
name text NOT NULL UNIQUE,
|
|
details text NOT NULL DEFAULT '',
|
|
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,
|
|
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),
|
|
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(),
|
|
CHECK (category_key IN (
|
|
'furniture',
|
|
'misc',
|
|
'outdoor',
|
|
'utilities',
|
|
'buildings',
|
|
'blocks',
|
|
'kits',
|
|
'nature',
|
|
'food',
|
|
'materials',
|
|
'key-items',
|
|
'other'
|
|
)),
|
|
CHECK (usage_key IS NULL OR usage_key IN ('decoration', 'relaxation', 'toy', 'road'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ancient_artifacts (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
display_id integer NOT NULL UNIQUE CHECK (display_id > 0),
|
|
name text NOT NULL UNIQUE,
|
|
details text NOT NULL DEFAULT '',
|
|
category_key text NOT NULL CHECK (category_key IN ('lost-relics-l', 'lost-relics-s', 'fossils')),
|
|
image_path 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 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),
|
|
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 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)
|
|
);
|
|
|
|
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 ancient_artifact_favorite_things (
|
|
ancient_artifact_id integer NOT NULL REFERENCES ancient_artifacts(id) ON DELETE CASCADE,
|
|
favorite_thing_id integer NOT NULL REFERENCES favorite_things(id),
|
|
PRIMARY KEY (ancient_artifact_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),
|
|
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 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),
|
|
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 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 life_tags
|
|
ADD COLUMN IF NOT EXISTS is_default boolean NOT NULL DEFAULT false;
|
|
|
|
ALTER TABLE items
|
|
ADD COLUMN IF NOT EXISTS display_id integer,
|
|
ADD COLUMN IF NOT EXISTS details text NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS category_key text,
|
|
ADD COLUMN IF NOT EXISTS usage_key text;
|
|
|
|
ALTER TABLE ancient_artifacts
|
|
ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT '';
|
|
|
|
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
|
|
SET display_id = id
|
|
WHERE display_id IS NULL;
|
|
|
|
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 display_id SET NOT NULL,
|
|
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_category_key_check,
|
|
DROP CONSTRAINT IF EXISTS items_usage_key_check;
|
|
|
|
ALTER TABLE items
|
|
ADD CONSTRAINT items_display_id_positive CHECK (display_id > 0),
|
|
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'));
|
|
|
|
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 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 UNIQUE INDEX IF NOT EXISTS items_display_event_item_key ON items(display_id, is_event_item);
|
|
CREATE INDEX IF NOT EXISTS items_display_order_idx ON items(is_event_item, display_id, sort_order, id);
|
|
CREATE INDEX IF NOT EXISTS ancient_artifacts_sort_order_idx ON ancient_artifacts(sort_order, id);
|
|
CREATE INDEX IF NOT EXISTS ancient_artifacts_display_order_idx ON ancient_artifacts(display_id, 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()
|
|
);
|
|
|
|
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', 'ancient-artifacts')),
|
|
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', '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);
|
|
|
|
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', 'ancient-artifacts')),
|
|
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),
|
|
ai_moderation_status text NOT NULL DEFAULT 'unreviewed' CHECK (ai_moderation_status IN ('unreviewed', 'reviewing', 'approved', 'rejected', 'failed')),
|
|
ai_moderation_language_code text REFERENCES languages(code) ON DELETE SET NULL,
|
|
ai_moderation_content_hash text,
|
|
ai_moderation_checked_at timestamptz,
|
|
ai_moderation_retry_count integer NOT NULL DEFAULT 0 CHECK (ai_moderation_retry_count >= 0),
|
|
ai_moderation_updated_at timestamptz NOT NULL DEFAULT now(),
|
|
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 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);
|
|
|
|
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')
|
|
);
|
|
|
|
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_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;
|
|
|
|
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_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_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;
|
|
|
|
CREATE INDEX IF NOT EXISTS life_posts_ai_moderation_language_idx
|
|
ON life_posts(ai_moderation_language_code, created_at DESC, id DESC)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS life_post_comments_ai_moderation_status_idx
|
|
ON life_post_comments(ai_moderation_status, ai_moderation_updated_at, id)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS life_post_comments_ai_moderation_language_idx
|
|
ON life_post_comments(ai_moderation_language_code, created_at, id)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS entity_discussion_comments_ai_moderation_status_idx
|
|
ON entity_discussion_comments(ai_moderation_status, ai_moderation_updated_at, id)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS entity_discussion_comments_ai_moderation_language_idx
|
|
ON entity_discussion_comments(entity_type, entity_id, ai_moderation_language_code, created_at, id)
|
|
WHERE deleted_at IS NULL;
|