pbl_runtime_ext/sql/m11b_partitions.sql
2026-09-19 22:24:59 +08:00

238 lines
12 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- ===========================================================================
-- pbl_runtime_event 表 DDL + 按月 RANGE 分区 + append-only 约束M11b-1
-- 由 scripts/pbl_runtime_event_ddl.py 生成,改表结构只改生成器再 --emit。
-- 目标引擎 mariadbprojects/pbls/env/test.json: db.engine=mariadb
-- 执行mysql --protocol=tcp -u<dba> -p <dbname> < 本文件
-- 说明:触发器含多语句体,须用 mysql CLIDELIMITER执行
-- 应用侧 ensure_tables() 只执行 A 段单语句 CREATE TABLE。
-- 开发说明projects/pbls/docs/02-develop/dev-notes-m11b1-pbl_runtime_event.md
-- ===========================================================================
-- ---------------------------------------------------------------- A. 建表(分区)
CREATE TABLE IF NOT EXISTS `pbl_runtime_event` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增ID(逻辑主键;物理主键=(id,created_at))',
`tenant_id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '租户ID(多租户强制打头)',
`event_uid` VARCHAR(64) NOT NULL COMMENT '事件UID(uuid,应用层生成)',
`event_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '事件编码',
`idem_key` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '幂等键(同键重复投递只落一条)',
`world_id` BIGINT NOT NULL DEFAULT 0 COMMENT '世界ID(引用 world 基表,只读)',
`scene_id` BIGINT NOT NULL DEFAULT 0 COMMENT '场景/会话ID(引用 scense 基表,只读)',
`session_id` BIGINT NOT NULL DEFAULT 0 COMMENT '游戏会话ID(M11a 语义,与 scene_id 同域)',
`entity_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '实体ID(引用 entity 基表,只读)',
`actor_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '触发者(服务端会话解析,客户端禁填)',
`event_type` VARCHAR(64) NOT NULL COMMENT '事件类型',
`payload` TEXT NULL COMMENT '事件负载JSON(任务书字段)',
`payload_json` LONGTEXT NULL COMMENT '事件负载JSON(M11a 写路径列名,与 payload 同义留宽)',
`causation_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '因果链:引发本事件的事件UID',
`seq` BIGINT NOT NULL DEFAULT 0 COMMENT '会话内单调序号(轮询游标,应用层生成)',
`seq_no` BIGINT NOT NULL DEFAULT 0 COMMENT '会话内单调序号(M11a 写路径列名)',
`source` VARCHAR(32) NOT NULL DEFAULT 'runtime' COMMENT '来源:runtime/agent/script/client_intent',
`state` VARCHAR(16) NOT NULL DEFAULT 'applied' COMMENT 'applied/rejected/rolled_back',
`state_version` BIGINT NOT NULL DEFAULT 0 COMMENT '本事件推进到的服务端权威版本',
`tx_group` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '事务组(同组同事务)',
`broadcast` TINYINT NOT NULL DEFAULT 1 COMMENT '是否参与广播 0/1',
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(分区键,禁TIMESTAMP)',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间戳(append-only:永不 UPDATE)',
`occurred_at` DATETIME NULL COMMENT '兼容列(M11a 旧分区键,新写入可空)',
PRIMARY KEY (`id`, `created_at`),
UNIQUE KEY `uk_event_uid` (`tenant_id`, `event_uid`, `created_at`),
UNIQUE KEY `uk_tenant_idem` (`tenant_id`, `idem_key`, `created_at`),
KEY `ix_scene_created` (`scene_id`, `created_at`),
KEY `ix_tenant_session_seq` (`tenant_id`, `session_id`, `seq`),
KEY `ix_tenant_session_seqno` (`tenant_id`, `session_id`, `seq_no`),
KEY `ix_tenant_world_type` (`tenant_id`, `world_id`, `event_type`),
KEY `ix_tx_group` (`tx_group`),
KEY `ix_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='运行时事件流(append-only,按月RANGE分区,保留14个月)'
PARTITION BY RANGE COLUMNS(`created_at`) (
PARTITION `p202609` VALUES LESS THAN (DATE '2026-10-01'),
PARTITION `p202610` VALUES LESS THAN (DATE '2026-11-01'),
PARTITION `p202611` VALUES LESS THAN (DATE '2026-12-01'),
PARTITION `p202612` VALUES LESS THAN (DATE '2027-01-01'),
PARTITION `p202701` VALUES LESS THAN (DATE '2027-02-01'),
PARTITION `p202702` VALUES LESS THAN (DATE '2027-03-01'),
PARTITION `pmax` VALUES LESS THAN (MAXVALUE)
);
DELIMITER $$
-- ---------------------------------------------------------------- B. append-only
-- append-only 断言过程:非白名单操作一律 SIGNAL 拒绝
DROP PROCEDURE IF EXISTS `pbl_assert_append_only`$$
CREATE PROCEDURE `pbl_assert_append_only`(
IN p_table VARCHAR(64), IN p_priv VARCHAR(16), IN p_detail VARCHAR(255))
READS SQL DATA
BEGIN
IF p_table <> 'pbl_runtime_event' THEN
RETURN; -- 只守护事件表,其他表不受影响
END IF;
IF IFNULL(@pbl_guard_ctx, '') = 'append_only_admin' THEN
RETURN; -- break-glass一次性历史数据回填
END IF;
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'PBL_APPEND_ONLY_DENIED',
MYSQL_ERRNO = 45001;
END$$
-- BEFORE UPDATE 护栏:把违规语句原文带进错误消息,便于定位调用方
DROP TRIGGER IF EXISTS `trg_pbl_runtime_event_append_only_upd`$$
CREATE TRIGGER `trg_pbl_runtime_event_append_only_upd`
BEFORE UPDATE ON `pbl_runtime_event` FOR EACH ROW
BEGIN
DECLARE v_sql TEXT;
DECLARE v_msg VARCHAR(255);
SET v_sql = '';
BEGIN
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_sql = '';
SELECT INFO INTO v_sql FROM information_schema.processlist
WHERE ID = CONNECTION_ID();
END;
IF IFNULL(@pbl_guard_ctx, '') = 'append_only_admin' THEN
SET v_msg = CONCAT('break-glass update on ', 'pbl_runtime_event');
ELSE
SET v_msg = CONCAT('UPDATE denied: ', LEFT(IFNULL(v_sql, 'n/a'), 120));
END IF;
CALL pbl_assert_append_only('pbl_runtime_event', 'update', v_msg);
IF IFNULL(@pbl_guard_ctx, '') <> 'append_only_admin' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PBL_APPEND_ONLY_DENIED_UPDATE',
MYSQL_ERRNO = 45002;
END IF;
END$$
-- BEFORE DELETE 护栏
DROP TRIGGER IF EXISTS `trg_pbl_runtime_event_append_only_del`$$
CREATE TRIGGER `trg_pbl_runtime_event_append_only_del`
BEFORE DELETE ON `pbl_runtime_event` FOR EACH ROW
BEGIN
DECLARE v_sql TEXT;
DECLARE v_msg VARCHAR(255);
SET v_sql = '';
BEGIN
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_sql = '';
SELECT INFO INTO v_sql FROM information_schema.processlist
WHERE ID = CONNECTION_ID();
END;
IF IFNULL(@pbl_guard_ctx, '') = 'append_only_admin' THEN
SET v_msg = CONCAT('break-glass delete on ', 'pbl_runtime_event');
ELSE
SET v_msg = CONCAT('DELETE denied: ', LEFT(IFNULL(v_sql, 'n/a'), 120));
END IF;
CALL pbl_assert_append_only('pbl_runtime_event', 'delete', v_msg);
IF IFNULL(@pbl_guard_ctx, '') <> 'append_only_admin' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PBL_APPEND_ONLY_DENIED_DELETE',
MYSQL_ERRNO = 45003;
END IF;
END$$
-- 权限层加固(可选,需 DBA 账号执行app 账号只保留增/查)
-- GRANT INSERT, SELECT, UPDATE, DELETE ON `pbls`.`pbl_runtime_event` TO NULL; -- 占位,勿执行
-- REVOKE UPDATE, DELETE ON `pbls`.`pbl_runtime_event` FROM 'pbls'@'%';
-- ---------------------------------------------------------------- C. 分区维护
-- 1) 幂等补建未来月分区pmax 存在时用 REORGANIZE PARTITION 拆出新月份(不丢数据)
DROP PROCEDURE IF EXISTS `pbl_runtime_event_ensure_partitions`$$
CREATE PROCEDURE `pbl_runtime_event_ensure_partitions`(IN p_months_ahead INT)
MODIFIES SQL DATA
BEGIN
DECLARE v_done INT DEFAULT 0;
DECLARE v_name VARCHAR(16);
DECLARE v_upper DATE;
DECLARE v_exists INT DEFAULT 0;
DECLARE v_sql TEXT;
DECLARE cur CURSOR FOR
WITH RECURSIVE ms(month_start) AS (
SELECT DATE_FORMAT(CURDATE(), '%Y-%m-01')
UNION ALL
SELECT DATE_ADD(month_start, INTERVAL 1 MONTH) FROM ms
WHERE month_start < DATE_ADD(DATE_FORMAT(CURDATE(), '%Y-%m-01'),
INTERVAL p_months_ahead MONTH)
)
SELECT CONCAT('p', DATE_FORMAT(month_start, '%Y%m')),
DATE_ADD(month_start, INTERVAL 1 MONTH)
FROM ms;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_name, v_upper;
IF v_done = 1 THEN
LEAVE read_loop;
END IF;
SELECT COUNT(*) INTO v_exists FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pbl_runtime_event'
AND PARTITION_NAME = v_name;
IF v_exists = 0 THEN
SET @pbl_part_sql = CONCAT(
'ALTER TABLE `pbl_runtime_event` REORGANIZE PARTITION `pmax` INTO (',
'PARTITION `', v_name, '` VALUES LESS THAN (DATE ''', v_upper, '''), ',
'PARTITION `pmax` VALUES LESS THAN (MAXVALUE))');
PREPARE pbl_part_st FROM @pbl_part_sql;
EXECUTE pbl_part_st;
DEALLOCATE PREPARE pbl_part_st;
END IF;
END LOOP;
CLOSE cur;
END$$
-- 2) 到期归档:整月 DROP PARTITIONappend-only 表唯一合规的清理手段)
DROP PROCEDURE IF EXISTS `pbl_runtime_event_prune_partitions`$$
CREATE PROCEDURE `pbl_runtime_event_prune_partitions`(IN p_keep_months INT)
MODIFIES SQL DATA
BEGIN
DECLARE v_done INT DEFAULT 0;
DECLARE v_name VARCHAR(64);
DECLARE v_cutoff DATE;
DECLARE cur CURSOR FOR
SELECT PARTITION_NAME FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pbl_runtime_event'
AND PARTITION_NAME IS NOT NULL
AND PARTITION_NAME <> 'pmax'
AND PARTITION_NAME < CONCAT('p', DATE_FORMAT(v_cutoff, '%Y%m'));
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
SET v_cutoff = DATE_ADD(DATE_FORMAT(CURDATE(), '%Y-%m-01'), INTERVAL -p_keep_months MONTH);
OPEN cur;
prune_loop: LOOP
FETCH cur INTO v_name;
IF v_done = 1 THEN
LEAVE prune_loop;
END IF;
SET @pbl_drop_sql = CONCAT('ALTER TABLE `pbl_runtime_event` DROP PARTITION `', v_name, '`');
PREPARE pbl_drop_st FROM @pbl_drop_sql;
EXECUTE pbl_drop_st;
DEALLOCATE PREPARE pbl_drop_st;
END LOOP;
CLOSE cur;
END$$
-- 3) 每月自动补分区(需 event_scheduler=ON否则用 cron见开发说明
-- SET GLOBAL event_scheduler = ON;
DROP EVENT IF EXISTS `ev_pbl_runtime_event_partition_maintain`$$
CREATE EVENT IF NOT EXISTS `ev_pbl_runtime_event_partition_maintain`
ON SCHEDULE EVERY 1 MONTH
STARTS TIMESTAMP(DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01 03:00:00'))
ON COMPLETION PRESERVE
ENABLE
COMMENT '每月预建未来 6 个月分区'
DO CALL pbl_runtime_event_ensure_partitions(6)$$
DELIMITER ;
-- ---------------------------------------------------------------- D. 一次性改造
-- 一次性改造(仅当 pbl_runtime_event 已存在且未分区时手工执行;先备份):
-- 1) CREATE TABLE pbl_runtime_event_new ... (本文件 A 段 DDL改名 _new
-- 2) SET @pbl_guard_ctx = 'append_only_admin'; -- break-glass仅回填期间
-- INSERT INTO pbl_runtime_event_new (...) SELECT ... FROM pbl_runtime_event;
-- SET @pbl_guard_ctx = NULL;
-- 3) RENAME TABLE pbl_runtime_event TO pbl_runtime_event_m11a_bak,
-- pbl_runtime_event_new TO pbl_runtime_event;
-- 4) 重放 append-only 触发器段B 段),核对 EXPLAIN 分区裁剪后删备份表。
-- 本脚本不自动执行改造DROP/RENAME 生产表必须人工确认(见开发说明「回滚」段)。
SELECT 'm11b-1 migrate: 人工执行,见注释' AS note;