-- Initial embedded Turso schema. Every statement is safe to run repeatedly. CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, login_username TEXT, password_hash TEXT, discord_id TEXT UNIQUE, role TEXT NOT NULL DEFAULT 'user', created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS user_profiles ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, display_name TEXT NOT NULL, updated_at TEXT NOT NULL ); INSERT OR IGNORE INTO user_profiles(user_id,display_name,updated_at) SELECT id,username,created_at FROM users; CREATE TABLE IF NOT EXISTS user_avatars ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, source TEXT NOT NULL CHECK(source IN ('preset', 'upload')), value TEXT NOT NULL, byte_size INTEGER CHECK(byte_size IS NULL OR byte_size >= 0), updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS avatar_reviews ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, avatar_updated_at TEXT NOT NULL, storage_key TEXT NOT NULL, byte_size INTEGER NOT NULL, status TEXT NOT NULL CHECK(status IN ('pending', 'approved', 'removed', 'withdrawn', 'superseded')), submitted_at TEXT NOT NULL, reviewed_at TEXT, reviewed_by TEXT REFERENCES users(id), review_note TEXT ); CREATE TABLE IF NOT EXISTS notifications ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, category TEXT NOT NULL CHECK(category IN ('isac', 'contributions', 'comments', 'builds', 'guides', 'system')), source TEXT NOT NULL, title TEXT NOT NULL, body TEXT NOT NULL, link TEXT, actions_json TEXT, mandatory INTEGER NOT NULL DEFAULT 0 CHECK(mandatory IN (0, 1)), created_at TEXT NOT NULL, read_at TEXT, emailed_at TEXT ); CREATE INDEX IF NOT EXISTS notifications_user_created_idx ON notifications(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS notifications_user_unread_idx ON notifications(user_id, read_at, created_at DESC); CREATE TABLE IF NOT EXISTS contact_messages ( id TEXT PRIMARY KEY, sender_user_id TEXT REFERENCES users(id) ON DELETE SET NULL, sender_name TEXT NOT NULL, sender_email TEXT NOT NULL, team TEXT NOT NULL CHECK(team IN ('oversight', 'administrators', 'dmca')), subject TEXT NOT NULL, body TEXT NOT NULL, dmca_json TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS contact_message_recipients ( message_id TEXT NOT NULL REFERENCES contact_messages(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, PRIMARY KEY(message_id, user_id) ); CREATE INDEX IF NOT EXISTS contact_messages_created_idx ON contact_messages(created_at DESC); CREATE INDEX IF NOT EXISTS contact_message_recipients_user_idx ON contact_message_recipients(user_id, message_id); CREATE TABLE IF NOT EXISTS notification_preferences ( user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, category TEXT NOT NULL CHECK(category IN ('isac', 'contributions', 'comments', 'builds', 'guides', 'system')), in_app_enabled INTEGER NOT NULL DEFAULT 1 CHECK(in_app_enabled IN (0, 1)), browser_enabled INTEGER NOT NULL DEFAULT 0 CHECK(browser_enabled IN (0, 1)), email_frequency TEXT NOT NULL DEFAULT 'none' CHECK(email_frequency IN ('none', 'immediate', 'daily')), updated_at TEXT NOT NULL, PRIMARY KEY(user_id, category) ); CREATE TABLE IF NOT EXISTS oversight_notification_preferences ( user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, oversight_category TEXT NOT NULL CHECK(oversight_category IN ('data_changes', 'avatars')), frequency TEXT NOT NULL DEFAULT 'immediate' CHECK(frequency IN ('immediate', '15_minutes', '1_hour', 'daily')), updated_at TEXT NOT NULL, PRIMARY KEY(user_id, oversight_category) ); CREATE TABLE IF NOT EXISTS oversight_notification_queue ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, oversight_category TEXT NOT NULL CHECK(oversight_category IN ('data_changes', 'avatars')), review_item_id TEXT NOT NULL, title TEXT NOT NULL, summary TEXT NOT NULL, link TEXT NOT NULL, frequency TEXT NOT NULL CHECK(frequency IN ('15_minutes', '1_hour', 'daily')), created_at TEXT NOT NULL, deliver_after TEXT NOT NULL, delivered_at TEXT ); CREATE TABLE IF NOT EXISTS automated_system_messages ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT NOT NULL, event_key TEXT NOT NULL CHECK(event_key IN ('account.created')), enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0,1)), send_delay_minutes INTEGER NOT NULL DEFAULT 0 CHECK(send_delay_minutes BETWEEN 0 AND 10080), source TEXT NOT NULL, title TEXT NOT NULL, body TEXT NOT NULL, link TEXT, actions_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, updated_by TEXT REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS automated_system_message_deliveries ( id TEXT PRIMARY KEY, message_id TEXT NOT NULL REFERENCES automated_system_messages(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, event_key TEXT NOT NULL, event_id TEXT NOT NULL, scheduled_for TEXT NOT NULL, delivered_at TEXT, notification_id TEXT REFERENCES notifications(id) ON DELETE SET NULL, created_at TEXT NOT NULL, UNIQUE(message_id,user_id,event_key,event_id) ); CREATE INDEX IF NOT EXISTS automated_system_message_deliveries_due_idx ON automated_system_message_deliveries(delivered_at,scheduled_for); INSERT OR IGNORE INTO automated_system_messages(id,name,description,event_key,enabled,send_delay_minutes,source,title,body,link,actions_json,created_at,updated_at) VALUES('welcome-agent','Welcome Agent','Introduces newly registered agents to SHD.build community channels and contribution workflows','account.created',1,0,'ISAC COMMUNITY NETWORK','Welcome to SHD.build','Welcome, Agent. SHD.build is your community intelligence hub for The Division Resurgence. Explore field news, search the game database, discover community builds and guides, or configure your own loadout in the Builder. ISAC also lets every registered agent inspect game data and enable Contributor Mode to suggest additions and corrections for Oversight review. Join the SHD Discord to meet other agents, share intelligence, and help the network grow.','/account','[{"label":"EXPLORE NEWS","href":"/news"},{"label":"QUERY DATABASE","href":"/database"},{"label":"VIEW COMMUNITY BUILDS","href":"/builds"},{"label":"READ FIELD GUIDES","href":"/guides"},{"label":"OPEN BUILDER","href":"/builder"},{"label":"CONTRIBUTE THROUGH ISAC","href":"/isac/contributor-guidelines"},{"label":"JOIN SHD DISCORD","href":"https://discord.gg/dhpd33VwQK"}]','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); CREATE INDEX IF NOT EXISTS oversight_notification_queue_due_idx ON oversight_notification_queue(delivered_at, deliver_after); CREATE INDEX IF NOT EXISTS oversight_notification_queue_user_category_idx ON oversight_notification_queue(user_id, oversight_category, delivered_at); CREATE INDEX IF NOT EXISTS avatar_reviews_status_submitted_idx ON avatar_reviews(status, submitted_at DESC); CREATE INDEX IF NOT EXISTS avatar_reviews_user_submitted_idx ON avatar_reviews(user_id, submitted_at DESC); CREATE TABLE IF NOT EXISTS builds (id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id), title TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, visibility TEXT NOT NULL DEFAULT 'private', summary TEXT, guide_body TEXT, source_build_id TEXT REFERENCES builds(id), loadout_json TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS builds_visibility_updated_idx ON builds(visibility, updated_at DESC); CREATE TABLE IF NOT EXISTS build_items (id TEXT PRIMARY KEY, build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, slot_id TEXT NOT NULL, item_definition_id TEXT NOT NULL, configuration_json TEXT NOT NULL); CREATE TABLE IF NOT EXISTS ratings (user_id TEXT NOT NULL REFERENCES users(id), build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, value INTEGER NOT NULL CHECK(value BETWEEN 1 AND 5), PRIMARY KEY(user_id, build_id)); CREATE TABLE IF NOT EXISTS comments (id TEXT PRIMARY KEY, build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id), body TEXT NOT NULL, created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS favorites (user_id TEXT NOT NULL REFERENCES users(id), build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, created_at TEXT NOT NULL, PRIMARY KEY(user_id, build_id)); CREATE TABLE IF NOT EXISTS equipment_types (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, category TEXT NOT NULL); CREATE TABLE IF NOT EXISTS stat_definitions (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, value_kind TEXT NOT NULL, min_value REAL, max_value REAL, unit TEXT, active INTEGER NOT NULL DEFAULT 1); CREATE TABLE IF NOT EXISTS stat_equipment_types (stat_id TEXT NOT NULL REFERENCES stat_definitions(id), equipment_type_id TEXT NOT NULL REFERENCES equipment_types(id), PRIMARY KEY(stat_id, equipment_type_id)); CREATE TABLE IF NOT EXISTS audit_log (id TEXT PRIMARY KEY, actor_id TEXT NOT NULL REFERENCES users(id), entity_type TEXT NOT NULL, entity_id TEXT NOT NULL, action TEXT NOT NULL, before_json TEXT, after_json TEXT, created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS roles (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT); CREATE TABLE IF NOT EXISTS permissions (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT); CREATE TABLE IF NOT EXISTS user_roles (user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, role_id TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE, PRIMARY KEY(user_id, role_id)); CREATE TABLE IF NOT EXISTS role_permissions (role_id TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE, permission_id TEXT NOT NULL REFERENCES permissions(id) ON DELETE CASCADE, scope_json TEXT, PRIMARY KEY(role_id, permission_id)); CREATE TABLE IF NOT EXISTS user_permissions (user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, permission_id TEXT NOT NULL REFERENCES permissions(id) ON DELETE CASCADE, PRIMARY KEY(user_id, permission_id)); CREATE TABLE IF NOT EXISTS user_status ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, last_active_at TEXT, blocked_at TEXT, blocked_by TEXT REFERENCES users(id), banned_until TEXT, moderation_reason TEXT, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS login_events ( id TEXT PRIMARY KEY, user_id TEXT REFERENCES users(id) ON DELETE SET NULL, provider TEXT NOT NULL CHECK(provider IN ('password', 'discord', 'magic_link', 'passkey')), succeeded INTEGER NOT NULL CHECK(succeeded IN (0, 1)), ip_address TEXT, user_agent TEXT, client_name TEXT, operating_system TEXT, device_type TEXT, location_country TEXT, location_region TEXT, location_city TEXT, failure_reason TEXT, risk_flags TEXT, credential_id TEXT, session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL, session_ended_at TEXT, session_end_reason TEXT, sensitive_purged_at TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS user_emails ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, email TEXT NOT NULL COLLATE NOCASE UNIQUE, verified_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS magic_link_tokens ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, email TEXT NOT NULL COLLATE NOCASE, purpose TEXT NOT NULL CHECK(purpose IN ('login', 'verify_email')), token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, consumed_at TEXT, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS magic_link_tokens_expiry_idx ON magic_link_tokens(expires_at); CREATE TABLE IF NOT EXISTS passkeys ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, webauthn_user_id TEXT NOT NULL, public_key BLOB NOT NULL, counter INTEGER NOT NULL DEFAULT 0, device_type TEXT NOT NULL, backed_up INTEGER NOT NULL DEFAULT 0 CHECK(backed_up IN (0, 1)), transports TEXT, name TEXT NOT NULL, last_used_at TEXT, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS passkeys_user_idx ON passkeys(user_id); CREATE TABLE IF NOT EXISTS webauthn_challenges ( id TEXT PRIMARY KEY, user_id TEXT REFERENCES users(id) ON DELETE CASCADE, flow TEXT NOT NULL CHECK(flow IN ('registration', 'authentication')), challenge TEXT NOT NULL, rp_id TEXT NOT NULL, origin TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS webauthn_challenges_expiry_idx ON webauthn_challenges(expires_at); CREATE TABLE IF NOT EXISTS deleted_accounts ( user_id TEXT PRIMARY KEY REFERENCES users(id), original_username TEXT NOT NULL, deleted_at TEXT NOT NULL, deleted_by TEXT NOT NULL REFERENCES users(id), reason TEXT ); CREATE INDEX IF NOT EXISTS login_events_user_created_idx ON login_events(user_id, created_at DESC); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-editor','Data Editor','Access to ISAC game-data management'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-user-operator','User Operator','View and moderate SHD.build users'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-access-manager','Access Manager','Manage user roles and direct permissions'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-admin','Admin','Full administrative access to all current and future ISAC capabilities'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-moderator','Community Oversight','Community oversight role reserved for build, guide, and comment tools'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-system-admin','system.admin','Unrestricted access to all administrative capabilities'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-isac-access','isac.access','Open the ISAC interface'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-data-manage','data.manage','Create and edit builder source data'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-data-view','data.view','View ISAC builder source data without changing live records'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-data-propose','data.propose','Suggest builder source-data additions and edits for approval'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-data-approve','data.approve','Review, approve, and reject proposed builder source-data changes'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-view','users.view','View users, activity, and content history'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-moderate','users.moderate','Block, ban, and reactivate users'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-roles','users.roles','Assign roles and direct permissions'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-delete','users.delete','Soft-delete user accounts while retaining authored content'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-identities','users.identities','View and manage linked external identities'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-audit-view','audit.view','View and search the immutable ISAC audit history'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-audit-revert','audit.revert','Attempt confirmed compensating reversals of supported ISAC audit events'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-metrics-view','metrics.view','View read-only application, runtime, and database health metrics'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-system-notifications-manage','system.notifications.manage','View and edit automated ISAC system notification messages'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-images-manage','images.manage','View, upload, rename, tag, and categorize managed image assets'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-avatars-review','avatars.review','Review and remove custom user avatars'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-calculation-publish','calculation.publish','Activate combat calculation revisions used by every loadout'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-editor','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-editor','permission-data-manage'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-editor','permission-data-view'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-viewer','Data Viewer','Read-only access to ISAC game-data registries'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-contributor','Data Contributor','View game data and submit moderated change proposals'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-approver','Data Oversight','Review, correct, and decide proposals before applying approved changes to live game data'); UPDATE roles SET name='Community Oversight',description='Community oversight role reserved for build, guide, and comment tools' WHERE id='role-moderator'; UPDATE roles SET name='Data Oversight',description='Review, correct, and decide proposals before applying approved changes to live game data' WHERE id='role-data-approver'; INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-viewer','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-viewer','permission-data-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-contributor','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-contributor','permission-data-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-contributor','permission-data-propose'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-approver','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-approver','permission-data-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-approver','permission-data-manage'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-approver','permission-data-approve'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-user-operator','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-user-operator','permission-users-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-user-operator','permission-users-moderate'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-user-operator','permission-avatars-review'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-users-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-users-roles'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-users-identities'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-admin','permission-system-admin'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-auditor','Auditor','Read-only access to ISAC audit history'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-auditor','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-auditor','permission-audit-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-auditor','permission-metrics-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-moderator','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-moderator','permission-avatars-review'); CREATE TABLE IF NOT EXISTS oauth_states (state_hash TEXT PRIMARY KEY, provider TEXT NOT NULL, mode TEXT NOT NULL, user_id TEXT REFERENCES users(id) ON DELETE CASCADE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS data_change_proposals ( id TEXT PRIMARY KEY, module TEXT NOT NULL CHECK(module IN ('configuration','calculations','attributes','talents','brands','gear','weapons','weaponmods','gearmods','protocols','skillchips','specializations')), action TEXT NOT NULL, entity_id TEXT, draft_entity_id TEXT, applied_entity_id TEXT, summary TEXT NOT NULL, payload_json TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','failed')), proposed_by TEXT NOT NULL REFERENCES users(id), reviewed_by TEXT REFERENCES users(id), review_note TEXT, created_at TEXT NOT NULL, reviewed_at TEXT, applied_at TEXT ); CREATE INDEX IF NOT EXISTS data_change_proposals_status_created_idx ON data_change_proposals(status,created_at DESC); CREATE INDEX IF NOT EXISTS data_change_proposals_module_status_idx ON data_change_proposals(module,status); CREATE TABLE IF NOT EXISTS data_change_proposal_dependencies ( proposal_id TEXT NOT NULL REFERENCES data_change_proposals(id) ON DELETE CASCADE, depends_on_proposal_id TEXT NOT NULL REFERENCES data_change_proposals(id) ON DELETE RESTRICT, PRIMARY KEY(proposal_id,depends_on_proposal_id), CHECK(proposal_id<>depends_on_proposal_id) ); CREATE INDEX IF NOT EXISTS data_change_proposal_dependencies_dependency_idx ON data_change_proposal_dependencies(depends_on_proposal_id); CREATE TABLE IF NOT EXISTS contributor_agreements ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, guidelines_version TEXT NOT NULL, accepted_at TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0,1)), enabled_at TEXT NOT NULL, disabled_at TEXT ); CREATE TABLE IF NOT EXISTS discord_profiles ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, discord_id TEXT NOT NULL UNIQUE, username TEXT NOT NULL, display_name TEXT, avatar_hash TEXT, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS users_username_nocase_idx ON users(username COLLATE NOCASE); CREATE UNIQUE INDEX IF NOT EXISTS users_login_username_nocase_idx ON users(login_username COLLATE NOCASE) WHERE login_username IS NOT NULL; CREATE TABLE IF NOT EXISTS stat_attributes ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, type TEXT NOT NULL CHECK(type IN ('Offensive', 'Defensive')), category TEXT NOT NULL CHECK(category IN ('Weapon', 'Skill', 'Character')), calculation_type TEXT NOT NULL CHECK(calculation_type IN ('Additive', 'Multiplicative')), unit TEXT NOT NULL DEFAULT '', gear_min REAL, gear_max REAL, gear_reference_level INTEGER NOT NULL DEFAULT 50 CHECK(gear_reference_level BETWEEN 1 AND 50), weapon_min REAL, weapon_max REAL, weapon_reference_level INTEGER NOT NULL DEFAULT 50 CHECK(weapon_reference_level BETWEEN 1 AND 50), skill_chip_min REAL, skill_chip_max REAL, cap_value REAL, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK((gear_min IS NULL AND gear_max IS NULL) OR (gear_min IS NOT NULL AND gear_max IS NOT NULL AND gear_min <= gear_max)), CHECK((weapon_min IS NULL AND weapon_max IS NULL) OR (weapon_min IS NOT NULL AND weapon_max IS NOT NULL AND weapon_min <= weapon_max)), CHECK((skill_chip_min IS NULL AND skill_chip_max IS NULL) OR (skill_chip_min IS NOT NULL AND skill_chip_max IS NOT NULL AND skill_chip_min <= skill_chip_max)), CHECK(cap_value IS NULL OR cap_value >= 0) ); CREATE UNIQUE INDEX IF NOT EXISTS stat_attributes_name_unit_nocase_idx ON stat_attributes(name COLLATE NOCASE, unit); CREATE TABLE IF NOT EXISTS talents ( id TEXT PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL CHECK(category IN ('Weapon', 'Backpack', 'Body Armor', 'Vest')), cooldown_seconds REAL CHECK(cooldown_seconds IS NULL OR cooldown_seconds > 0), duration_seconds REAL CHECK(duration_seconds IS NULL OR duration_seconds > 0), max_stacks INTEGER CHECK(max_stacks IS NULL OR max_stacks > 0), active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS talents_name_category_nocase_idx ON talents(name COLLATE NOCASE, category); CREATE TABLE IF NOT EXISTS talent_tiers ( id TEXT PRIMARY KEY, talent_id TEXT NOT NULL REFERENCES talents(id) ON DELETE CASCADE, tier TEXT NOT NULL CHECK(tier IN ('I', 'II')), description TEXT NOT NULL, trigger_text TEXT NOT NULL, effect_text TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(talent_id, tier) ); CREATE TABLE IF NOT EXISTS talent_triggers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, supports_uptime INTEGER NOT NULL DEFAULT 0 CHECK(supports_uptime IN (0, 1)), min_uptime REAL, max_uptime REAL, calculation_script TEXT, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK( (supports_uptime = 0 AND min_uptime IS NULL AND max_uptime IS NULL) OR (supports_uptime = 1 AND min_uptime IS NOT NULL AND max_uptime IS NOT NULL AND min_uptime >= 0 AND max_uptime <= 100 AND min_uptime <= max_uptime) ) ); CREATE UNIQUE INDEX IF NOT EXISTS talent_triggers_name_nocase_idx ON talent_triggers(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS talent_tier_triggers ( talent_tier_id TEXT NOT NULL REFERENCES talent_tiers(id) ON DELETE CASCADE, trigger_id TEXT NOT NULL REFERENCES talent_triggers(id), PRIMARY KEY(talent_tier_id, trigger_id) ); CREATE TABLE IF NOT EXISTS talent_tier_effects ( id TEXT PRIMARY KEY, talent_tier_id TEXT NOT NULL REFERENCES talent_tiers(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL, created_at TEXT NOT NULL, UNIQUE(talent_tier_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS talent_description_templates ( talent_id TEXT PRIMARY KEY REFERENCES talents(id) ON DELETE CASCADE, template TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS talent_weapon_scopes ( talent_id TEXT PRIMARY KEY REFERENCES talents(id) ON DELETE CASCADE, scope TEXT NOT NULL CHECK(scope IN ('Standard', 'Exotic')) ); INSERT OR IGNORE INTO talent_weapon_scopes(talent_id,scope) SELECT id,'Standard' FROM talents WHERE category='Weapon'; CREATE TABLE IF NOT EXISTS weapon_talent_slots ( talent_id TEXT PRIMARY KEY REFERENCES talents(id) ON DELETE CASCADE, slot INTEGER NOT NULL CHECK(slot IN (1, 2)) ); CREATE TABLE IF NOT EXISTS brand_sets ( id TEXT PRIMARY KEY, name TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS brand_sets_name_nocase_idx ON brand_sets(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS brand_set_effects ( id TEXT PRIMARY KEY, brand_set_id TEXT NOT NULL REFERENCES brand_sets(id) ON DELETE CASCADE, pieces_required INTEGER NOT NULL CHECK(pieces_required IN (2, 3, 4)), stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value >= 0), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(brand_set_id, pieces_required) ); CREATE TABLE IF NOT EXISTS gear ( id TEXT PRIMARY KEY, name TEXT NOT NULL, slot TEXT NOT NULL CHECK(slot IN ('Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask')), tier INTEGER NOT NULL CHECK(tier BETWEEN 0 AND 2), base_health_min REAL NOT NULL CHECK(base_health_min >= 0), base_health_max REAL NOT NULL CHECK(base_health_max >= base_health_min), base_armor_min REAL, base_armor_max REAL, range_reference_level INTEGER NOT NULL DEFAULT 50 CHECK(range_reference_level BETWEEN 1 AND 50), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK( (slot IN ('Gloves', 'Knee Pads', 'Body Armor') AND base_armor_min IS NOT NULL AND base_armor_max IS NOT NULL AND base_armor_min >= 0 AND base_armor_max >= base_armor_min) OR (slot NOT IN ('Gloves', 'Knee Pads', 'Body Armor') AND base_armor_min IS NULL AND base_armor_max IS NULL) ) ); CREATE UNIQUE INDEX IF NOT EXISTS gear_name_nocase_idx ON gear(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS gear_brand_sets ( gear_id TEXT NOT NULL REFERENCES gear(id) ON DELETE CASCADE, brand_set_id TEXT NOT NULL REFERENCES brand_sets(id), PRIMARY KEY(gear_id, brand_set_id) ); CREATE TABLE IF NOT EXISTS gear_talents ( gear_id TEXT NOT NULL REFERENCES gear(id) ON DELETE CASCADE, talent_id TEXT NOT NULL REFERENCES talents(id), PRIMARY KEY(gear_id, talent_id) ); CREATE TABLE IF NOT EXISTS gear_tactical_augments ( id TEXT PRIMARY KEY, gear_slot TEXT NOT NULL CHECK(gear_slot IN ('Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask')), augment_slot INTEGER NOT NULL CHECK(augment_slot BETWEEN 1 AND 4), effect_type TEXT NOT NULL CHECK(effect_type IN ('Custom', 'Base Percentage')), stat_attribute_id TEXT REFERENCES stat_attributes(id), value REAL NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(gear_slot, augment_slot), CHECK( (gear_slot IN ('Backpack', 'Body Armor') AND augment_slot BETWEEN 1 AND 4) OR (gear_slot NOT IN ('Backpack', 'Body Armor') AND augment_slot BETWEEN 1 AND 3) ), CHECK( (augment_slot = 1 AND effect_type = 'Custom' AND stat_attribute_id IS NOT NULL) OR (augment_slot > 1 AND effect_type = 'Base Percentage' AND stat_attribute_id IS NULL AND value > 0) ) ); CREATE TABLE IF NOT EXISTS gear_tactical_augment_costs ( augment_id TEXT NOT NULL REFERENCES gear_tactical_augments(id) ON DELETE CASCADE, gear_tier INTEGER NOT NULL CHECK(gear_tier BETWEEN 0 AND 2), unlock_cost INTEGER NOT NULL CHECK(unlock_cost >= 0), PRIMARY KEY(augment_id, gear_tier) ); CREATE TABLE IF NOT EXISTS weapons ( id TEXT PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL CHECK(category IN ('Shotgun', 'Rifle', 'MMR', 'AR', 'LMG', 'SMG')), is_exotic INTEGER NOT NULL DEFAULT 0 CHECK(is_exotic IN (0, 1)), native_tier INTEGER NOT NULL CHECK(native_tier BETWEEN 0 AND 2), damage_type TEXT NOT NULL CHECK(damage_type IN ('Blast', 'Pierce', 'Shredding')), rpm REAL NOT NULL CHECK(rpm >= 0), magazine REAL NOT NULL CHECK(magazine >= 0), reload_speed REAL NOT NULL CHECK(reload_speed >= 0), accuracy REAL NOT NULL CHECK(accuracy >= 0), stability REAL NOT NULL CHECK(stability >= 0), exotic_talent_id TEXT REFERENCES talents(id), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK( (is_exotic = 1 AND exotic_talent_id IS NOT NULL) OR (is_exotic = 0 AND exotic_talent_id IS NULL) ) ); CREATE UNIQUE INDEX IF NOT EXISTS weapons_name_nocase_idx ON weapons(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS core_attribute_tier_ranges ( tier INTEGER PRIMARY KEY CHECK(tier BETWEEN 0 AND 2), nominal_min REAL NOT NULL CHECK(nominal_min >= 0), nominal_max REAL NOT NULL CHECK(nominal_max >= nominal_min), reference_level INTEGER NOT NULL DEFAULT 50 CHECK(reference_level BETWEEN 1 AND 50), updated_at TEXT NOT NULL ); INSERT OR IGNORE INTO core_attribute_tier_ranges(tier,nominal_min,nominal_max,reference_level,updated_at) VALUES(0,30,78,50,CURRENT_TIMESTAMP); INSERT OR IGNORE INTO core_attribute_tier_ranges(tier,nominal_min,nominal_max,reference_level,updated_at) VALUES(1,36,94,50,CURRENT_TIMESTAMP); INSERT OR IGNORE INTO core_attribute_tier_ranges(tier,nominal_min,nominal_max,reference_level,updated_at) VALUES(2,44,112,50,CURRENT_TIMESTAMP); CREATE TABLE IF NOT EXISTS weapon_tier_ranges ( weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, tier INTEGER NOT NULL CHECK(tier BETWEEN 0 AND 2), damage_min REAL NOT NULL CHECK(damage_min >= 0), damage_max REAL NOT NULL CHECK(damage_max >= damage_min), firepower_min REAL NOT NULL CHECK(firepower_min >= 0), firepower_max REAL NOT NULL CHECK(firepower_max >= firepower_min), toughness_min REAL NOT NULL CHECK(toughness_min >= 0), toughness_max REAL NOT NULL CHECK(toughness_max >= toughness_min), engineering_min REAL NOT NULL CHECK(engineering_min >= 0), engineering_max REAL NOT NULL CHECK(engineering_max >= engineering_min), reference_level INTEGER NOT NULL DEFAULT 50 CHECK(reference_level BETWEEN 1 AND 50), PRIMARY KEY(weapon_id, tier) ); CREATE TABLE IF NOT EXISTS weapon_talent_options ( weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, talent_slot INTEGER NOT NULL CHECK(talent_slot IN (1, 2)), talent_id TEXT NOT NULL REFERENCES talents(id), PRIMARY KEY(weapon_id, talent_slot, talent_id) ); CREATE TABLE IF NOT EXISTS weapon_augmentation_effects ( id TEXT PRIMARY KEY, weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value > 0), sort_order INTEGER NOT NULL CHECK(sort_order IN (1, 2)), UNIQUE(weapon_id, sort_order), UNIQUE(weapon_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS weapon_base_attribute_bonuses ( id TEXT PRIMARY KEY, weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value > 0), sort_order INTEGER NOT NULL CHECK(sort_order >= 1), UNIQUE(weapon_id, stat_attribute_id), UNIQUE(weapon_id, sort_order) ); CREATE TABLE IF NOT EXISTS os_protocols ( id TEXT PRIMARY KEY, name TEXT NOT NULL, quality TEXT NOT NULL CHECK(quality IN ('Standard', 'Specialized', 'Superior', 'High-end')), primary_attribute_type TEXT NOT NULL CHECK(primary_attribute_type IN ('Firepower', 'Toughness', 'Engineering')), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS os_protocols_name_nocase_idx ON os_protocols(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS os_protocol_primary_effects ( id TEXT PRIMARY KEY, protocol_id TEXT NOT NULL UNIQUE REFERENCES os_protocols(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), base_value REAL NOT NULL CHECK(base_value >= 0) ); CREATE TABLE IF NOT EXISTS os_protocol_secondary_profiles ( id TEXT PRIMARY KEY, quality TEXT NOT NULL CHECK(quality IN ('Standard', 'Specialized', 'Superior', 'High-end')), primary_attribute_type TEXT NOT NULL CHECK(primary_attribute_type IN ('Firepower', 'Toughness', 'Engineering')), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(quality, primary_attribute_type) ); CREATE TABLE IF NOT EXISTS os_protocol_secondary_effects ( id TEXT PRIMARY KEY, profile_id TEXT NOT NULL REFERENCES os_protocol_secondary_profiles(id) ON DELETE CASCADE, effect_role TEXT NOT NULL CHECK(effect_role IN ('Core', 'Damage', 'Health')), stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value >= 0), UNIQUE(profile_id, effect_role), UNIQUE(profile_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS os_protocol_talents ( id TEXT PRIMARY KEY, protocol_id TEXT NOT NULL UNIQUE REFERENCES os_protocols(id) ON DELETE CASCADE, name TEXT NOT NULL, description_template TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS os_protocol_talents_name_nocase_idx ON os_protocol_talents(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS os_protocol_talent_triggers ( talent_id TEXT NOT NULL REFERENCES os_protocol_talents(id) ON DELETE CASCADE, trigger_id TEXT NOT NULL REFERENCES talent_triggers(id), PRIMARY KEY(talent_id, trigger_id) ); CREATE TABLE IF NOT EXISTS os_protocol_talent_effects ( id TEXT PRIMARY KEY, talent_id TEXT NOT NULL REFERENCES os_protocol_talents(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), base_value REAL NOT NULL, sort_order INTEGER NOT NULL CHECK(sort_order BETWEEN 1 AND 12), UNIQUE(talent_id, stat_attribute_id), UNIQUE(talent_id, sort_order) ); CREATE TABLE IF NOT EXISTS skill_chip_sets ( id TEXT PRIMARY KEY, name TEXT NOT NULL, specialization_ability_id TEXT NOT NULL REFERENCES specialization_abilities(id), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS skill_chip_sets_name_nocase_idx ON skill_chip_sets(name COLLATE NOCASE); CREATE INDEX IF NOT EXISTS skill_chip_sets_ability_idx ON skill_chip_sets(specialization_ability_id,active); CREATE TABLE IF NOT EXISTS skill_chip_two_piece_bonuses ( id TEXT PRIMARY KEY, skill_chip_set_id TEXT NOT NULL UNIQUE REFERENCES skill_chip_sets(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), min_value REAL NOT NULL, max_value REAL NOT NULL, CHECK(max_value >= min_value) ); CREATE TABLE IF NOT EXISTS skill_chip_primary_options ( skill_chip_set_id TEXT NOT NULL REFERENCES skill_chip_sets(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), PRIMARY KEY(skill_chip_set_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS skill_chip_secondary_options ( skill_chip_set_id TEXT NOT NULL REFERENCES skill_chip_sets(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), PRIMARY KEY(skill_chip_set_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS skill_chip_talents ( id TEXT PRIMARY KEY, skill_chip_set_id TEXT NOT NULL UNIQUE REFERENCES skill_chip_sets(id) ON DELETE CASCADE, name TEXT NOT NULL, description_template TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS skill_chip_talents_name_nocase_idx ON skill_chip_talents(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS skill_chip_talent_triggers ( talent_id TEXT NOT NULL REFERENCES skill_chip_talents(id) ON DELETE CASCADE, trigger_id TEXT NOT NULL REFERENCES talent_triggers(id), PRIMARY KEY(talent_id, trigger_id) ); CREATE TABLE IF NOT EXISTS skill_chip_talent_effects ( id TEXT PRIMARY KEY, talent_id TEXT NOT NULL REFERENCES skill_chip_talents(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), min_value REAL NOT NULL, max_value REAL NOT NULL, sort_order INTEGER NOT NULL CHECK(sort_order BETWEEN 1 AND 12), CHECK(max_value >= min_value), UNIQUE(talent_id, stat_attribute_id), UNIQUE(talent_id, sort_order) ); CREATE TABLE IF NOT EXISTS weapon_mods ( id TEXT PRIMARY KEY, name TEXT NOT NULL, slot_type TEXT NOT NULL CHECK(slot_type IN ('Optics','Muzzle','Underbarrel','Magazine','Barrel','Upper Receiver','Lower Receiver','Trigger')), exotic_weapon_id TEXT REFERENCES weapons(id), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS weapon_mods_name_nocase_idx ON weapon_mods(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS weapon_mod_compatible_categories ( weapon_mod_id TEXT NOT NULL REFERENCES weapon_mods(id) ON DELETE CASCADE, weapon_category TEXT NOT NULL CHECK(weapon_category IN ('shotgun','rifle','mmr','ar','lmg','smg')), PRIMARY KEY(weapon_mod_id,weapon_category) ); CREATE TABLE IF NOT EXISTS weapon_mod_slots ( weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, tier INTEGER NOT NULL CHECK(tier BETWEEN 0 AND 2), slot_type TEXT NOT NULL CHECK(slot_type IN ('Optics','Muzzle','Underbarrel','Magazine','Barrel','Upper Receiver','Lower Receiver','Trigger')), PRIMARY KEY(weapon_id,tier,slot_type) ); CREATE TABLE IF NOT EXISTS weapon_mod_quality_values ( weapon_mod_id TEXT NOT NULL REFERENCES weapon_mods(id) ON DELETE CASCADE, quality TEXT NOT NULL CHECK(quality IN ('Standard','Specialized','Superior','High-end')), primary_level_1_min REAL NOT NULL, primary_level_1_max REAL NOT NULL, primary_level_20_min REAL NOT NULL, primary_level_20_max REAL NOT NULL, PRIMARY KEY(weapon_mod_id,quality), CHECK(primary_level_1_max>=primary_level_1_min), CHECK(primary_level_20_max>=primary_level_20_min), CHECK(primary_level_20_min>=primary_level_1_min), CHECK(primary_level_20_max>=primary_level_1_max) ); CREATE TABLE IF NOT EXISTS weapon_mod_secondary_options ( weapon_mod_id TEXT NOT NULL REFERENCES weapon_mods(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), PRIMARY KEY(weapon_mod_id,stat_attribute_id) ); CREATE TABLE IF NOT EXISTS weapon_mod_secondary_quality_values ( weapon_mod_id TEXT NOT NULL, stat_attribute_id TEXT NOT NULL, quality TEXT NOT NULL CHECK(quality IN ('Standard','Specialized','Superior','High-end')), min_value REAL NOT NULL, max_value REAL NOT NULL, PRIMARY KEY(weapon_mod_id,stat_attribute_id,quality), FOREIGN KEY(weapon_mod_id,stat_attribute_id) REFERENCES weapon_mod_secondary_options(weapon_mod_id,stat_attribute_id) ON DELETE CASCADE, CHECK(max_value>=min_value) ); CREATE TABLE IF NOT EXISTS weapon_mod_exotic_attributes ( id TEXT PRIMARY KEY, weapon_mod_id TEXT NOT NULL REFERENCES weapon_mods(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), level_1_value REAL NOT NULL, level_20_value REAL NOT NULL, sort_order INTEGER NOT NULL CHECK(sort_order BETWEEN 1 AND 12), UNIQUE(weapon_mod_id,stat_attribute_id), UNIQUE(weapon_mod_id,sort_order) ); CREATE TABLE IF NOT EXISTS weapon_mod_effects ( id TEXT PRIMARY KEY, weapon_mod_id TEXT NOT NULL REFERENCES weapon_mods(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), sort_order INTEGER NOT NULL CHECK(sort_order BETWEEN 1 AND 2), UNIQUE(weapon_mod_id,stat_attribute_id), UNIQUE(weapon_mod_id,sort_order) ); CREATE TABLE IF NOT EXISTS weapon_mod_effect_quality_values ( effect_id TEXT NOT NULL REFERENCES weapon_mod_effects(id) ON DELETE CASCADE, quality TEXT NOT NULL CHECK(quality IN ('Standard','Specialized','Superior','High-end')), value REAL NOT NULL, PRIMARY KEY(effect_id,quality) ); CREATE TABLE IF NOT EXISTS gear_mods ( id TEXT PRIMARY KEY, name TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS gear_mods_name_nocase_idx ON gear_mods(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS gear_mod_quality_values ( gear_mod_id TEXT NOT NULL REFERENCES gear_mods(id) ON DELETE CASCADE, quality TEXT NOT NULL CHECK(quality IN ('Standard','Specialized','Superior','High-end')), primary_level_1_min REAL NOT NULL, primary_level_1_max REAL NOT NULL, primary_level_20_min REAL NOT NULL, primary_level_20_max REAL NOT NULL, PRIMARY KEY(gear_mod_id,quality), CHECK(primary_level_1_max>=primary_level_1_min), CHECK(primary_level_20_max>=primary_level_20_min), CHECK(primary_level_20_min>=primary_level_1_min), CHECK(primary_level_20_max>=primary_level_1_max) ); CREATE TABLE IF NOT EXISTS gear_mod_secondary_options ( gear_mod_id TEXT NOT NULL REFERENCES gear_mods(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), PRIMARY KEY(gear_mod_id,stat_attribute_id) ); CREATE TABLE IF NOT EXISTS gear_mod_secondary_quality_values ( gear_mod_id TEXT NOT NULL, stat_attribute_id TEXT NOT NULL, quality TEXT NOT NULL CHECK(quality IN ('Standard','Specialized','Superior','High-end')), min_value REAL NOT NULL, max_value REAL NOT NULL, PRIMARY KEY(gear_mod_id,stat_attribute_id,quality), FOREIGN KEY(gear_mod_id,stat_attribute_id) REFERENCES gear_mod_secondary_options(gear_mod_id,stat_attribute_id) ON DELETE CASCADE, CHECK(max_value>=min_value) ); CREATE TABLE IF NOT EXISTS specialization_progression_config ( id TEXT PRIMARY KEY CHECK(id='default'), xp_points INTEGER NOT NULL CHECK(xp_points>=0), shd_cache_points INTEGER NOT NULL CHECK(shd_cache_points>=0), updated_at TEXT NOT NULL ); INSERT OR IGNORE INTO specialization_progression_config(id,xp_points,shd_cache_points,updated_at) VALUES('default',30,19,'2026-07-26T00:00:00.000Z'); CREATE TABLE IF NOT EXISTS specialization_rank_cost_profiles ( rank INTEGER PRIMARY KEY CHECK(rank BETWEEN 1 AND 3), point_cost INTEGER NOT NULL CHECK(point_cost>0), credit_cost INTEGER NOT NULL CHECK(credit_cost>=0), updated_at TEXT NOT NULL ); INSERT OR IGNORE INTO specialization_rank_cost_profiles(rank,point_cost,credit_cost,updated_at) VALUES(1,1,2333,'2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO specialization_rank_cost_profiles(rank,point_cost,credit_cost,updated_at) VALUES(2,2,10000,'2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO specialization_rank_cost_profiles(rank,point_cost,credit_cost,updated_at) VALUES(3,3,90000,'2026-07-26T00:00:00.000Z'); CREATE TABLE IF NOT EXISTS upgrade_resources ( id TEXT PRIMARY KEY, name TEXT NOT NULL, resource_type TEXT NOT NULL CHECK(resource_type IN ('Currency','Component','Progression')), quality TEXT CHECK(quality IS NULL OR quality IN ('Standard','Specialized','Superior','High-end')), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS upgrade_resources_name_nocase_idx ON upgrade_resources(name COLLATE NOCASE); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-credits','Credits','Currency',NULL,'2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-electronics-alpha','Electronics Alpha Component','Component','Specialized','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-electronics-beta','Electronics Beta Component','Component','Specialized','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-electronics-gamma','Electronics Gamma Component','Component','Specialized','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-electronics-omega','Electronics Omega Component','Component','High-end','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO upgrade_resources(id,name,resource_type,quality,created_at,updated_at) VALUES('resource-shd-tech-cache','SHD Tech Cache','Progression','Superior','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); CREATE TABLE IF NOT EXISTS specializations ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS specializations_name_nocase_idx ON specializations(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS specialization_focuses ( id TEXT PRIMARY KEY, specialization_id TEXT NOT NULL REFERENCES specializations(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, primary_attribute TEXT NOT NULL CHECK(primary_attribute IN ('Firepower','Toughness','Engineering')), display_order INTEGER NOT NULL DEFAULT 0 CHECK(display_order>=0), builder_ready INTEGER NOT NULL DEFAULT 0 CHECK(builder_ready IN (0,1)), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(specialization_id,name) ); CREATE INDEX IF NOT EXISTS specialization_focuses_specialization_idx ON specialization_focuses(specialization_id,display_order,name); CREATE TABLE IF NOT EXISTS specialization_abilities ( id TEXT PRIMARY KEY, specialization_id TEXT NOT NULL REFERENCES specializations(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT NOT NULL, duration_seconds REAL CHECK(duration_seconds IS NULL OR duration_seconds>0), cooldown_seconds REAL CHECK(cooldown_seconds IS NULL OR cooldown_seconds>0), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(specialization_id,name) ); CREATE INDEX IF NOT EXISTS specialization_abilities_specialization_idx ON specialization_abilities(specialization_id,name); CREATE TABLE IF NOT EXISTS specialization_ability_effects ( id TEXT PRIMARY KEY, ability_id TEXT NOT NULL REFERENCES specialization_abilities(id) ON DELETE CASCADE, effect_order INTEGER NOT NULL CHECK(effect_order>=1), stat_attribute_id TEXT REFERENCES stat_attributes(id), label TEXT NOT NULL, value REAL, unit TEXT, UNIQUE(ability_id,effect_order) ); CREATE TABLE IF NOT EXISTS specialization_focus_abilities ( focus_id TEXT NOT NULL REFERENCES specialization_focuses(id) ON DELETE CASCADE, ability_slot INTEGER NOT NULL CHECK(ability_slot BETWEEN 1 AND 3), ability_id TEXT NOT NULL REFERENCES specialization_abilities(id), PRIMARY KEY(focus_id,ability_slot), UNIQUE(focus_id,ability_id) ); CREATE TABLE IF NOT EXISTS specialization_tree_nodes ( id TEXT PRIMARY KEY, focus_id TEXT NOT NULL REFERENCES specialization_focuses(id) ON DELETE CASCADE, name TEXT NOT NULL, description_template TEXT NOT NULL, row_number INTEGER NOT NULL CHECK(row_number>=1), column_number INTEGER NOT NULL CHECK(column_number>=1), max_rank INTEGER NOT NULL CHECK(max_rank BETWEEN 1 AND 3), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(focus_id,row_number,column_number), UNIQUE(focus_id,name) ); CREATE INDEX IF NOT EXISTS specialization_tree_nodes_focus_idx ON specialization_tree_nodes(focus_id,row_number,column_number); CREATE TABLE IF NOT EXISTS specialization_tree_node_ranks ( node_id TEXT NOT NULL REFERENCES specialization_tree_nodes(id) ON DELETE CASCADE, rank INTEGER NOT NULL CHECK(rank BETWEEN 1 AND 3), description TEXT NOT NULL, PRIMARY KEY(node_id,rank) ); CREATE TABLE IF NOT EXISTS specialization_tree_rank_effects ( id TEXT PRIMARY KEY, node_id TEXT NOT NULL, rank INTEGER NOT NULL, effect_order INTEGER NOT NULL CHECK(effect_order>=1), effect_type TEXT NOT NULL CHECK(effect_type IN ('Attribute','Ability','Custom')), stat_attribute_id TEXT REFERENCES stat_attributes(id), ability_id TEXT REFERENCES specialization_abilities(id), label TEXT NOT NULL, value REAL, unit TEXT, FOREIGN KEY(node_id,rank) REFERENCES specialization_tree_node_ranks(node_id,rank) ON DELETE CASCADE, UNIQUE(node_id,rank,effect_order), CHECK( (effect_type='Attribute' AND stat_attribute_id IS NOT NULL) OR (effect_type='Ability' AND ability_id IS NOT NULL) OR effect_type='Custom' ) ); CREATE TABLE IF NOT EXISTS specialization_tree_rank_costs ( node_id TEXT NOT NULL, rank INTEGER NOT NULL, resource_id TEXT NOT NULL REFERENCES upgrade_resources(id), quantity INTEGER NOT NULL CHECK(quantity>0), PRIMARY KEY(node_id,rank,resource_id), FOREIGN KEY(node_id,rank) REFERENCES specialization_tree_node_ranks(node_id,rank) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS specialization_combat_effects ( id TEXT PRIMARY KEY, specialization_id TEXT NOT NULL REFERENCES specializations(id) ON DELETE CASCADE, name TEXT NOT NULL, description_template TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(specialization_id,name) ); CREATE INDEX IF NOT EXISTS specialization_combat_effects_specialization_idx ON specialization_combat_effects(specialization_id,active,name); CREATE TABLE IF NOT EXISTS specialization_tree_rank_combat_effects ( id TEXT PRIMARY KEY, node_id TEXT NOT NULL, rank INTEGER NOT NULL, combat_effect_id TEXT NOT NULL REFERENCES specialization_combat_effects(id), effect_order INTEGER NOT NULL CHECK(effect_order>=1), trigger_type TEXT NOT NULL CHECK(trigger_type IN ('enemy_hits_within_window')), trigger_count INTEGER NOT NULL CHECK(trigger_count>0), trigger_window_seconds REAL NOT NULL CHECK(trigger_window_seconds>0), duration_seconds REAL NOT NULL CHECK(duration_seconds>0), max_stacks INTEGER NOT NULL CHECK(max_stacks>0), stacks_per_trigger INTEGER NOT NULL DEFAULT 1 CHECK(stacks_per_trigger>0), counter_policy TEXT NOT NULL DEFAULT 'reset_on_proc' CHECK(counter_policy IN ('reset_on_proc')), refresh_policy TEXT NOT NULL DEFAULT 'refresh_on_stack_gain' CHECK(refresh_policy IN ('refresh_on_stack_gain')), expiry_policy TEXT NOT NULL DEFAULT 'clear_all' CHECK(expiry_policy IN ('clear_all')), FOREIGN KEY(node_id,rank) REFERENCES specialization_tree_node_ranks(node_id,rank) ON DELETE CASCADE, UNIQUE(node_id,rank,effect_order) ); CREATE INDEX IF NOT EXISTS specialization_tree_rank_combat_effects_effect_idx ON specialization_tree_rank_combat_effects(combat_effect_id); CREATE TABLE IF NOT EXISTS specialization_tree_rank_combat_modifiers ( id TEXT PRIMARY KEY, rank_combat_effect_id TEXT NOT NULL REFERENCES specialization_tree_rank_combat_effects(id) ON DELETE CASCADE, modifier_order INTEGER NOT NULL CHECK(modifier_order>=1), stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value_per_stack REAL NOT NULL, unit TEXT, UNIQUE(rank_combat_effect_id,modifier_order) ); CREATE TABLE IF NOT EXISTS calculation_revisions ( id TEXT PRIMARY KEY, version INTEGER NOT NULL UNIQUE CHECK(version>0), name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft','active','retired')), notes TEXT, based_on_revision_id TEXT REFERENCES calculation_revisions(id), created_by TEXT REFERENCES users(id), created_at TEXT NOT NULL, published_by TEXT REFERENCES users(id), published_at TEXT ); CREATE UNIQUE INDEX IF NOT EXISTS calculation_revisions_active_idx ON calculation_revisions(status) WHERE status='active'; CREATE TABLE IF NOT EXISTS calculation_stages ( id TEXT PRIMARY KEY, revision_id TEXT NOT NULL REFERENCES calculation_revisions(id) ON DELETE CASCADE, stage_key TEXT NOT NULL, label TEXT NOT NULL, operator TEXT NOT NULL CHECK(operator IN ( 'base_damage','flat_add','additive_percent','multiplicative_percent', 'expected_critical','expected_headshot','rate_of_fire','magazine_size', 'reload_speed','firing_cycle' )), display_order INTEGER NOT NULL CHECK(display_order>=1), locked_position TEXT CHECK(locked_position IS NULL OR locked_position IN ('first','last')), UNIQUE(revision_id,stage_key), UNIQUE(revision_id,display_order) ); CREATE TABLE IF NOT EXISTS attribute_calculation_profiles ( id TEXT PRIMARY KEY, revision_id TEXT NOT NULL REFERENCES calculation_revisions(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), stage_key TEXT NOT NULL, weight REAL NOT NULL DEFAULT 1, soft_cap REAL, hard_cap REAL, curve_type TEXT NOT NULL DEFAULT 'none' CHECK(curve_type IN ('none','power','smoothstep')), curve_parameter REAL, weapon_category TEXT NOT NULL DEFAULT '*', UNIQUE(revision_id,stat_attribute_id,stage_key,weapon_category), FOREIGN KEY(revision_id,stage_key) REFERENCES calculation_stages(revision_id,stage_key) ON DELETE CASCADE, CHECK(soft_cap IS NULL OR soft_cap>=0), CHECK(hard_cap IS NULL OR hard_cap>=0), CHECK(soft_cap IS NULL OR hard_cap IS NULL OR hard_cap>=soft_cap) ); CREATE TABLE IF NOT EXISTS calculation_calibration_cases ( id TEXT PRIMARY KEY, name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft','ready')), loadout_snapshot_json TEXT NOT NULL, selected_weapon INTEGER NOT NULL DEFAULT 0 CHECK(selected_weapon IN (0,1)), game_version TEXT, observed_at TEXT, notes TEXT, body_hit REAL, critical_body_hit REAL, headshot REAL, critical_headshot REAL, magazine_seconds REAL, reload_seconds REAL, encounter_dps REAL, predicted_json TEXT, calculation_revision_id TEXT REFERENCES calculation_revisions(id), created_by TEXT NOT NULL REFERENCES users(id), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK(body_hit IS NULL OR body_hit>=0), CHECK(critical_body_hit IS NULL OR critical_body_hit>=0), CHECK(headshot IS NULL OR headshot>=0), CHECK(critical_headshot IS NULL OR critical_headshot>=0), CHECK(magazine_seconds IS NULL OR magazine_seconds>0), CHECK(reload_seconds IS NULL OR reload_seconds>0), CHECK(encounter_dps IS NULL OR encounter_dps>=0) ); CREATE INDEX IF NOT EXISTS calculation_calibration_status_idx ON calculation_calibration_cases(status,updated_at DESC); INSERT OR IGNORE INTO calculation_revisions(id,version,name,status,notes,created_at,published_at) VALUES('calculation-revision-1',1,'Initial combat model','active','Typed baseline combat stages','2026-07-26T00:00:00.000Z','2026-07-26T00:00:00.000Z'); INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order,locked_position) VALUES('calculation-stage-base-1','calculation-revision-1','base_damage','Base damage','base_damage',1,'first'); INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order) VALUES('calculation-stage-flat-1','calculation-revision-1','flat_damage','Flat damage','flat_add',2); INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order) VALUES('calculation-stage-additive-1','calculation-revision-1','additive_damage','Additive damage','additive_percent',3); INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order) VALUES('calculation-stage-multiplicative-1','calculation-revision-1','multiplicative_damage','Multiplicative damage','multiplicative_percent',4); INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order) VALUES('calculation-stage-critical-chance-1','calculation-revision-1','critical_chance','Critical chance','expected_critical',5); INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order) VALUES('calculation-stage-critical-1','calculation-revision-1','critical_damage','Critical damage','expected_critical',6); INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order) VALUES('calculation-stage-headshot-chance-1','calculation-revision-1','headshot_chance','Headshot chance','expected_headshot',7); INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order) VALUES('calculation-stage-headshot-1','calculation-revision-1','headshot_damage','Headshot damage','expected_headshot',8); INSERT OR IGNORE INTO calculation_stages(id,revision_id,stage_key,label,operator,display_order,locked_position) VALUES('calculation-stage-cycle-1','calculation-revision-1','firing_cycle','Firing cycle','firing_cycle',9,'last'); CREATE TABLE IF NOT EXISTS game_data_icons ( entity_type TEXT NOT NULL CHECK(entity_type IN ('talent', 'brand_set', 'os_protocol', 'skill_chip', 'specialization', 'specialization_ability', 'specialization_node')), entity_id TEXT NOT NULL, storage_key TEXT NOT NULL UNIQUE, byte_size INTEGER NOT NULL CHECK(byte_size > 0), updated_at TEXT NOT NULL, PRIMARY KEY(entity_type, entity_id) ); CREATE TABLE IF NOT EXISTS managed_images ( id TEXT PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, tags_json TEXT NOT NULL DEFAULT '[]', is_icon INTEGER NOT NULL DEFAULT 0 CHECK(is_icon IN (0,1)), force_white INTEGER NOT NULL DEFAULT 0 CHECK(force_white IN (0,1)), storage_key TEXT NOT NULL UNIQUE, original_filename TEXT NOT NULL, byte_size INTEGER NOT NULL CHECK(byte_size > 0), width INTEGER NOT NULL CHECK(width > 0), height INTEGER NOT NULL CHECK(height > 0), source_entity_type TEXT, source_entity_id TEXT, created_by TEXT NOT NULL REFERENCES users(id), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS managed_images_name_nocase_idx ON managed_images(name COLLATE NOCASE); CREATE INDEX IF NOT EXISTS managed_images_category_idx ON managed_images(category); CREATE UNIQUE INDEX IF NOT EXISTS managed_images_source_entity_idx ON managed_images(source_entity_type,source_entity_id) WHERE source_entity_type IS NOT NULL AND source_entity_id IS NOT NULL; CREATE TABLE IF NOT EXISTS external_service_status ( provider TEXT PRIMARY KEY, credits_total REAL, credits_subscription REAL, credits_payg REAL, credits_enterprise REAL, free_calls INTEGER, checked_at TEXT, next_check_at TEXT, last_charge REAL, last_charge_at TEXT, error_message TEXT, severity TEXT NOT NULL DEFAULT 'unknown' CHECK(severity IN ('unknown','ok','warning','critical')), episode_id TEXT, episode_started_at TEXT, recovered_at TEXT, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS external_service_alert_deliveries ( id TEXT PRIMARY KEY, provider TEXT NOT NULL REFERENCES external_service_status(provider), episode_id TEXT NOT NULL, severity TEXT NOT NULL CHECK(severity IN ('warning','critical')), user_id TEXT NOT NULL REFERENCES users(id), notification_id TEXT REFERENCES notifications(id), created_at TEXT NOT NULL, delivered_at TEXT, UNIQUE(provider,episode_id,severity,user_id) ); CREATE INDEX IF NOT EXISTS external_service_alert_pending_idx ON external_service_alert_deliveries(delivered_at,created_at);