-- IM Server schema changes for the GedaDa management-console adaptation. -- Target: MySQL 8.0+, one independent tenant business database. -- Default local tenant database: `tinode`. -- IMPORTANT: execute this migration once for every tenant database. Tenant -- business tables intentionally do not contain tenant_id. -- Review before execution. DDL statements auto-commit in MySQL. SET NAMES utf8mb4; USE `tinode`; -- ============================================================ -- 0. User account number. -- account_no is the short user number shown in Admin. The real primary key -- im_users.id remains the internal Tinode/Snowflake user ID. -- Existing users are backfilled from 1000 by created time; new users are -- assigned by IM Server in the user creation transaction. -- ============================================================ SET @sql := IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_users' AND COLUMN_NAME = 'account_no') = 0, 'ALTER TABLE `im_users` ADD COLUMN `account_no` int unsigned NOT NULL DEFAULT 0 COMMENT ''用户编号'' AFTER `id`', 'SELECT 1' ); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @im_user_account_no := ( SELECT GREATEST(COALESCE(MAX(`account_no`), 999), 999) FROM `im_users` WHERE `account_no` > 0 ); UPDATE `im_users` AS u JOIN ( SELECT ordered.`id`, (@im_user_account_no := @im_user_account_no + 1) AS next_no FROM ( SELECT `id` FROM `im_users` WHERE `account_no` = 0 ORDER BY `createdat`, `id` ) AS ordered ) AS seq ON seq.`id` = u.`id` SET u.`account_no` = seq.next_no WHERE u.`account_no` = 0; SET @sql := IF( (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_users' AND INDEX_NAME = 'uk_im_users_account_no') = 0, 'CREATE UNIQUE INDEX `uk_im_users_account_no` ON `im_users` (`account_no`)', 'SELECT 1' ); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- ============================================================ -- 1. Applied runtime policy snapshots. -- policy_type: security, user_policy_groups, sensitive_words. -- The application uses the latest row with apply_status=applied and also keeps -- an in-memory cache. This table makes restart and Kafka replay deterministic. -- ============================================================ CREATE TABLE IF NOT EXISTS `im_runtime_policy_snapshots` ( `id` bigint NOT NULL AUTO_INCREMENT, `policy_type` varchar(32) NOT NULL, `policy_version` bigint unsigned NOT NULL, `payload` json NOT NULL, `payload_sha256` char(64) NOT NULL, `source_event_id` varchar(96) DEFAULT NULL COMMENT 'NULL for a manual bootstrap snapshot', `apply_status` varchar(16) NOT NULL DEFAULT 'applied' COMMENT 'applied or failed', `apply_error` varchar(1000) NOT NULL DEFAULT '', `applied_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), UNIQUE KEY `uk_im_runtime_policy_version` (`policy_type`, `policy_version`), UNIQUE KEY `uk_im_runtime_policy_event` (`source_event_id`), KEY `idx_im_runtime_policy_latest` (`policy_type`, `apply_status`, `policy_version` DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Last applied Admin policy payloads'; -- ============================================================ -- 2. User login history and login failure history. -- Never store plaintext passwords, verification codes, access tokens or refresh -- tokens. account_hash is SHA-256 of the normalized login account; account_mask -- is the value shown in Admin. -- login_type: password, sms_code, sso, qr_code, token_refresh. -- event_type: login, refresh, logout, kicked, session_expired. -- failure_type: password_error, code_error, account_not_found, -- account_disabled, rate_limited, risk_blocked. -- ============================================================ CREATE TABLE IF NOT EXISTS `im_user_login_logs` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `username` varchar(64) NOT NULL DEFAULT '', `account_type` varchar(24) NOT NULL DEFAULT '', `account_hash` char(64) NOT NULL DEFAULT '', `account_mask` varchar(128) NOT NULL DEFAULT '', `login_type` varchar(24) NOT NULL, `event_type` varchar(24) NOT NULL DEFAULT 'login', `session_id` varchar(96) NOT NULL DEFAULT '' COMMENT 'Opaque session identifier, never an auth token', `device_id` varchar(255) NOT NULL DEFAULT '', `platform` varchar(32) NOT NULL DEFAULT 'unknown', `app_version` varchar(32) NOT NULL DEFAULT '', `ip` varchar(64) NOT NULL DEFAULT '', `region` varchar(128) NOT NULL DEFAULT '', `user_agent` varchar(512) NOT NULL DEFAULT '', `risk_level` varchar(16) NOT NULL DEFAULT 'low', `request_id` varchar(64) NOT NULL DEFAULT '', `login_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `logout_at` datetime(3) DEFAULT NULL, `metadata` json DEFAULT NULL, `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), KEY `idx_im_user_login_user_time` (`user_id`, `login_at` DESC, `id` DESC), KEY `idx_im_user_login_ip_time` (`ip`, `login_at` DESC, `id` DESC), KEY `idx_im_user_login_device_time` (`device_id`(64), `login_at` DESC), KEY `idx_im_user_login_request` (`request_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='User login and session event history'; CREATE TABLE IF NOT EXISTS `im_login_failure_logs` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint DEFAULT NULL COMMENT 'NULL when the account cannot be resolved', `account_type` varchar(24) NOT NULL DEFAULT '', `account_hash` char(64) NOT NULL DEFAULT '', `account_mask` varchar(128) NOT NULL DEFAULT '', `failure_type` varchar(32) NOT NULL, `reason` varchar(96) NOT NULL DEFAULT '' COMMENT 'Stable backend reason code', `detail` varchar(500) NOT NULL DEFAULT '' COMMENT 'Sanitized detail without credentials', `ip` varchar(64) NOT NULL DEFAULT '', `region` varchar(128) NOT NULL DEFAULT '', `device_id` varchar(255) NOT NULL DEFAULT '', `platform` varchar(32) NOT NULL DEFAULT 'unknown', `app_version` varchar(32) NOT NULL DEFAULT '', `user_agent` varchar(512) NOT NULL DEFAULT '', `risk_level` varchar(16) NOT NULL DEFAULT 'medium', `risk_action` varchar(24) NOT NULL DEFAULT 'audit_only', `request_id` varchar(64) NOT NULL DEFAULT '', `attempt_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `metadata` json DEFAULT NULL, `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), KEY `idx_im_login_failure_account_time` (`account_hash`, `attempt_at` DESC, `id` DESC), KEY `idx_im_login_failure_user_time` (`user_id`, `attempt_at` DESC, `id` DESC), KEY `idx_im_login_failure_ip_time` (`ip`, `attempt_at` DESC, `id` DESC), KEY `idx_im_login_failure_type_time` (`failure_type`, `attempt_at` DESC, `id` DESC), KEY `idx_im_login_failure_request` (`request_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Authentication failure and risk-block history'; -- ============================================================ -- 3. Message administration actions. -- Messages remain in im_messages/im_group_messages. This table is an immutable -- action overlay so hide/delete can be audited and restored. -- conversation_type: private, group. -- action: hide, unhide, delete, restore. -- action_status: pending, success, failed, dead. -- ============================================================ CREATE TABLE IF NOT EXISTS `im_message_admin_actions` ( `id` bigint NOT NULL AUTO_INCREMENT, `action_id` varchar(96) NOT NULL, `command_id` varchar(96) NOT NULL, `conversation_type` varchar(16) NOT NULL, `topic` varchar(64) NOT NULL, `seqid` bigint NOT NULL, `sender_user_id` bigint DEFAULT NULL, `action` varchar(16) NOT NULL, `action_status` varchar(16) NOT NULL DEFAULT 'pending', `reason` varchar(500) NOT NULL DEFAULT '', `actor_admin_id` bigint NOT NULL DEFAULT 0, `request_id` varchar(64) NOT NULL DEFAULT '', `before_snapshot` json DEFAULT NULL, `after_snapshot` json DEFAULT NULL, `failure_reason` varchar(96) NOT NULL DEFAULT '', `failure_message` varchar(1000) NOT NULL DEFAULT '', `processed_at` datetime(3) DEFAULT NULL, `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), UNIQUE KEY `uk_im_message_admin_action_id` (`action_id`), UNIQUE KEY `uk_im_message_admin_command` (`command_id`), KEY `idx_im_message_admin_target` (`conversation_type`, `topic`, `seqid`, `id` DESC), KEY `idx_im_message_admin_status` (`action_status`, `created_at`, `id`), KEY `idx_im_message_admin_actor` (`actor_admin_id`, `created_at` DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Auditable Admin message actions'; -- ============================================================ -- 4. Sensitive-word hits and policy decisions. -- Store only a digest and a short masked excerpt. Do not duplicate complete -- private messages in log tables. -- policy decision reasons include FRIEND_DISABLED and MESSAGE_CURFEW_ACTIVE. -- ============================================================ CREATE TABLE IF NOT EXISTS `im_sensitive_word_hits` ( `id` bigint NOT NULL AUTO_INCREMENT, `word_id` bigint NOT NULL DEFAULT 0 COMMENT 'Admin control-database word ID', `word_group_id` bigint NOT NULL DEFAULT 0, `word_snapshot` varchar(255) NOT NULL DEFAULT '' COMMENT 'Masked word snapshot', `scene` varchar(32) NOT NULL, `user_id` bigint NOT NULL DEFAULT 0, `target_user_id` bigint DEFAULT NULL, `topic` varchar(64) NOT NULL DEFAULT '', `seqid` bigint DEFAULT NULL, `content_sha256` char(64) NOT NULL DEFAULT '', `matched_excerpt` varchar(500) NOT NULL DEFAULT '', `match_type` varchar(24) NOT NULL DEFAULT 'contains', `action` varchar(24) NOT NULL, `risk_level` varchar(16) NOT NULL DEFAULT 'medium', `request_id` varchar(64) NOT NULL DEFAULT '', `metadata` json DEFAULT NULL, `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), KEY `idx_im_sensitive_hit_scene_time` (`scene`, `created_at` DESC, `id` DESC), KEY `idx_im_sensitive_hit_user_time` (`user_id`, `created_at` DESC, `id` DESC), KEY `idx_im_sensitive_hit_target` (`topic`, `seqid`, `id` DESC), KEY `idx_im_sensitive_hit_word` (`word_id`, `created_at` DESC), KEY `idx_im_sensitive_hit_request` (`request_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Sensitive-word match events'; CREATE TABLE IF NOT EXISTS `im_policy_decision_logs` ( `id` bigint NOT NULL AUTO_INCREMENT, `policy_type` varchar(32) NOT NULL COMMENT 'security or user_policy_group', `policy_version` bigint unsigned NOT NULL DEFAULT 0, `policy_group_id` bigint NOT NULL DEFAULT 0, `scene` varchar(64) NOT NULL COMMENT 'friend_request, qrcode_resolve, private_message and so on', `action` varchar(24) NOT NULL COMMENT 'deny, challenge or audit_only', `reason_code` varchar(96) NOT NULL COMMENT 'For example FRIEND_DISABLED or MESSAGE_CURFEW_ACTIVE', `user_id` bigint NOT NULL DEFAULT 0, `target_user_id` bigint DEFAULT NULL, `topic` varchar(64) NOT NULL DEFAULT '', `request_id` varchar(64) NOT NULL DEFAULT '', `ip` varchar(64) NOT NULL DEFAULT '', `metadata` json DEFAULT NULL, `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), KEY `idx_im_policy_decision_user_time` (`user_id`, `created_at` DESC, `id` DESC), KEY `idx_im_policy_decision_reason_time` (`reason_code`, `created_at` DESC, `id` DESC), KEY `idx_im_policy_decision_scene_time` (`scene`, `created_at` DESC, `id` DESC), KEY `idx_im_policy_decision_request` (`request_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Runtime policy deny/challenge decisions'; -- ============================================================ -- 5. Group event history and daily IP/account statistics. -- im_group_events_outbox remains the delivery table; this log is the queryable -- business history after the operation result is known. -- ============================================================ CREATE TABLE IF NOT EXISTS `im_group_event_logs` ( `id` bigint NOT NULL AUTO_INCREMENT, `event_id` varchar(96) NOT NULL, `topic` varchar(64) NOT NULL, `group_id` bigint DEFAULT NULL, `event_type` varchar(64) NOT NULL, `actor_user_id` bigint NOT NULL DEFAULT 0, `target_user_id` bigint DEFAULT NULL, `result` varchar(16) NOT NULL DEFAULT 'success', `reason` varchar(500) NOT NULL DEFAULT '', `request_id` varchar(64) NOT NULL DEFAULT '', `before_snapshot` json DEFAULT NULL, `after_snapshot` json DEFAULT NULL, `metadata` json DEFAULT NULL, `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), UNIQUE KEY `uk_im_group_event_log_event` (`event_id`), KEY `idx_im_group_event_topic_time` (`topic`, `created_at` DESC, `id` DESC), KEY `idx_im_group_event_type_time` (`event_type`, `created_at` DESC, `id` DESC), KEY `idx_im_group_event_actor_time` (`actor_user_id`, `created_at` DESC, `id` DESC), KEY `idx_im_group_event_request` (`request_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Queryable group event audit history'; CREATE TABLE IF NOT EXISTS `im_ip_account_stats_daily` ( `id` bigint NOT NULL AUTO_INCREMENT, `stat_date` date NOT NULL, `ip` varchar(64) NOT NULL, `region` varchar(128) NOT NULL DEFAULT '', `account_count` int unsigned NOT NULL DEFAULT 0, `user_count` int unsigned NOT NULL DEFAULT 0, `login_success_count` int unsigned NOT NULL DEFAULT 0, `login_failure_count` int unsigned NOT NULL DEFAULT 0, `first_seen_at` datetime(3) DEFAULT NULL, `last_seen_at` datetime(3) DEFAULT NULL, `risk_level` varchar(16) NOT NULL DEFAULT 'low', `risk_action` varchar(24) NOT NULL DEFAULT 'audit_only', `sample_user_ids` json DEFAULT NULL COMMENT 'Bounded sample only, not an unlimited array', `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), UNIQUE KEY `uk_im_ip_account_stats_day_ip` (`stat_date`, `ip`), KEY `idx_im_ip_account_stats_ip_day` (`ip`, `stat_date` DESC), KEY `idx_im_ip_account_stats_risk_day` (`risk_level`, `stat_date` DESC, `id` DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Daily IP and account aggregation'; -- ============================================================ -- 6. Compatible enhancements to existing tables. -- Every ALTER is guarded by information_schema and can be rerun. -- ============================================================ -- Message management queries need topic/time and sender/time indexes. SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_messages' AND INDEX_NAME = 'idx_im_messages_topic_created') = 0, 'ALTER TABLE `im_messages` ADD INDEX `idx_im_messages_topic_created` (`topic`, `createdat`, `id`)', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_messages' AND INDEX_NAME = 'idx_im_messages_from_created') = 0, 'ALTER TABLE `im_messages` ADD INDEX `idx_im_messages_from_created` (`from`, `createdat`, `id`)', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; -- Extend the existing Tinode device record for device and login-history views. SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_devices' AND COLUMN_NAME = 'firstseen') = 0, 'ALTER TABLE `im_devices` ADD COLUMN `firstseen` DATETIME(3) DEFAULT NULL AFTER `platform`', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_devices' AND COLUMN_NAME = 'lastip') = 0, 'ALTER TABLE `im_devices` ADD COLUMN `lastip` VARCHAR(64) NOT NULL DEFAULT '''' AFTER `lastseen`', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_devices' AND COLUMN_NAME = 'useragent') = 0, 'ALTER TABLE `im_devices` ADD COLUMN `useragent` VARCHAR(512) NOT NULL DEFAULT '''' AFTER `lastip`', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_devices' AND COLUMN_NAME = 'appversion') = 0, 'ALTER TABLE `im_devices` ADD COLUMN `appversion` VARCHAR(32) NOT NULL DEFAULT '''' AFTER `useragent`', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_devices' AND COLUMN_NAME = 'status') = 0, 'ALTER TABLE `im_devices` ADD COLUMN `status` VARCHAR(16) NOT NULL DEFAULT ''active'' AFTER `appversion`', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; -- Feedback handling fields required by the Admin processing workflow. SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' AND COLUMN_NAME = 'assigned_to') = 0, 'ALTER TABLE `im_feedback_submissions` ADD COLUMN `assigned_to` BIGINT DEFAULT NULL AFTER `status`', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' AND COLUMN_NAME = 'handled_by') = 0, 'ALTER TABLE `im_feedback_submissions` ADD COLUMN `handled_by` BIGINT DEFAULT NULL AFTER `assigned_to`', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' AND COLUMN_NAME = 'handled_at') = 0, 'ALTER TABLE `im_feedback_submissions` ADD COLUMN `handled_at` DATETIME(3) DEFAULT NULL AFTER `handled_by`', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' AND COLUMN_NAME = 'resolution') = 0, 'ALTER TABLE `im_feedback_submissions` ADD COLUMN `resolution` VARCHAR(1000) NOT NULL DEFAULT '''' AFTER `handled_at`', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' AND COLUMN_NAME = 'admin_remark') = 0, 'ALTER TABLE `im_feedback_submissions` ADD COLUMN `admin_remark` VARCHAR(1000) NOT NULL DEFAULT '''' AFTER `resolution`', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' AND INDEX_NAME = 'idx_im_feedback_status_time') = 0, 'ALTER TABLE `im_feedback_submissions` ADD INDEX `idx_im_feedback_status_time` (`status`, `created_at`, `id`)', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_audit_logs' AND INDEX_NAME = 'idx_im_audit_logs_ip_time') = 0, 'ALTER TABLE `im_audit_logs` ADD INDEX `idx_im_audit_logs_ip_time` (`ip`, `created_at`, `id`)', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; -- Post-deployment checks (read-only): -- SELECT policy_type, policy_version, apply_status, applied_at -- FROM im_runtime_policy_snapshots ORDER BY policy_type, policy_version DESC; -- SELECT reason_code, scene, COUNT(*) AS deny_count -- FROM im_policy_decision_logs -- WHERE created_at >= CURRENT_DATE -- GROUP BY reason_code, scene; -- SELECT stat_date, ip, account_count, login_failure_count, risk_level -- FROM im_ip_account_stats_daily ORDER BY stat_date DESC, login_failure_count DESC;