feat(M11b-1b): 落库迁移 SQL 资产 --emit 产出 scripts/migrations/ 3 个迁移文件

- 20260919_m11b_01_create_pbl_runtime_event.sql (3617B): CREATE TABLE IF NOT EXISTS
  pbl_runtime_event + PARTITION BY RANGE COLUMNS(created_at),6 个 p<YYYYMM> + pmax
- 20260919_m11b_02_pbl_runtime_event_append_only.sql (3212B): pbl_assert_append_only
  过程 + trg_..._append_only_upd/del 两个 BEFORE 触发器护栏(DELIMITER $$ 包裹)
- 20260919_m11b_03_pbl_runtime_event_partition_maintenance.sql (3849B):
  pbl_runtime_event_ensure_partitions / pbl_runtime_event_prune_partitions /
  ev_pbl_runtime_event_partition_maintain 三段完整正文(QC#2 截断缺陷已消除)
- sql/m11b_partitions.sql 与生成器 full_script() 逐字一致(11893B, IDENTICAL=True)
- 幂等:连跑两次 --emit 产物字节数不变;无硬编码连接串
This commit is contained in:
agent.develop 2026-09-19 21:56:46 +08:00
parent 907603653d
commit cda913a694
3 changed files with 241 additions and 0 deletions

View File

@ -0,0 +1,50 @@
-- 生成器: scripts/pbl_runtime_event_ddl.py --emit (M11b-1)
-- 开发说明: projects/pbls/docs/02-develop/dev-notes-m11b1-pbl_runtime_event.md
-- 幂等: CREATE ... IF NOT EXISTS / DROP ... IF EXISTS
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)
);

View File

@ -0,0 +1,87 @@
-- 生成器: scripts/pbl_runtime_event_ddl.py --emit (M11b-1)
-- 开发说明: projects/pbls/docs/02-develop/dev-notes-m11b1-pbl_runtime_event.md
-- 幂等: CREATE ... IF NOT EXISTS / DROP ... IF EXISTS
DELIMITER $$
-- 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'@'%'$$
DELIMITER ;

View File

@ -0,0 +1,104 @@
-- 生成器: scripts/pbl_runtime_event_ddl.py --emit (M11b-1)
-- 开发说明: projects/pbls/docs/02-develop/dev-notes-m11b1-pbl_runtime_event.md
-- 幂等: CREATE ... IF NOT EXISTS / DROP ... IF EXISTS
DELIMITER $$
-- 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 PARTITIONappend-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 ;