253 lines
12 KiB
SQL
253 lines
12 KiB
SQL
-- ===========================================================================
|
||
-- pbl_runtime_event 表 DDL + 按月 RANGE 分区 + append-only 约束(M11b-1)
|
||
-- 由 scripts/pbl_runtime_event_ddl.py 生成,改表结构只改生成器再 --emit。
|
||
-- 目标引擎 mariadb(projects/pbls/env/test.json: db.engine=mariadb)。
|
||
-- 执行:mysql --protocol=tcp -u<dba> -p <dbname> < 本文件
|
||
-- 说明:触发器含多语句体,须用 mysql CLI(DELIMITER)执行;
|
||
-- 应用侧 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$$
|
||
|
||
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$$
|
||
|
||
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$$
|
||
|
||
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$$
|
||
|
||
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 PARTITION(append-only 表唯一合规的清理手段)
|
||
DROP PROCEDURE IF EXISTS `pbl_runtime_event_prune_partitions`$$
|
||
|
||
CREATE$$
|
||
|
||
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$$
|
||
|
||
DROP EVENT IF EXISTS `ev_pbl_runtime_event_partition_maintain`;
|
||
|
||
CREATE$$
|
||
|
||
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;
|