| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- -- ============================================================================
- -- 1.0.554 · R2-A:mdp_entity 三列 schema drift 收口
- --
- -- 背景(已取证):
- -- S1_MDP_SYNC_TRANSFORM / S2_MDP_SYNC_TRANSFORM 在服务启动期用裸 SQL
- -- INSERT INTO mdp_entity (...) ... ON DUPLICATE KEY UPDATE 注册实体,
- -- 其显式列清单不含 inbound_enabled / inbound_max_rows / stale_guard_enabled。
- -- 生产这三列为 NOT NULL 且缺 DEFAULT,sql_mode 含 STRICT_TRANS_TABLES,
- -- 于是 MySQL 报 Error 1364: Field 'xxx' doesn't have a default value。
- -- MySQL 每次只报第一个缺失列,故三列必须同批收口,否则要三次部署才收敛。
- --
- -- 漂移成因(已取证,非 migration 写错):
- -- 1.0.536 的 SQL 文本对三列都写了正确的 TYPE + DEFAULT(见其 :118/:124/:130),
- -- 但 mdp_inbound_* 六表的 create_time 实测为 2026-09-15 23:03:31~33,
- -- 由 SqlSugar CodeFirst 抢先创建;1.0.536 正式执行是 2026-09-16 20:36:37,
- -- 晚 21 小时,其 IF(@exist=0, ADD COLUMN ...) 守卫整段 no-op。
- -- CodeFirst 按实体生成 int / 无 DEFAULT,与 contract 不符。
- --
- -- 设计原则:TARGET-STATE RECONCILIATION,不是 OBJECT-EXISTENCE MIGRATION。
- -- 判定「定义是否等于目标」,而非「列是否存在」——后者正是 1.0.536 的失败模式。
- -- COLUMN_DEFAULT 比较必须用 NULL-safe 的 <=>:COLUMN_DEFAULT 为 NULL 时
- -- `COLUMN_DEFAULT = '500'` 求值为 NULL(非 FALSE),WHERE 按假丢弃 → 漏检。
- -- 实证:仅 inbound_max_rows 缺 DEFAULT 的环境下,`=` 谓词 drift=0(静默跳过),
- -- `<=>` 谓词 drift=1(正确检出),而该环境实际 INSERT 确实报 1364。
- --
- -- 注释处理:drift 判定只比对 type / nullable / default,不比对 COMMENT,
- -- 避免与 CodeFirst 的注释同步反复互踩。inbound_enabled 保留生产现有注释
- -- (信息量更大),另两列原为空,补回 1.0.536 的契约注释。
- --
- -- 算法:本地 3307 同构演练实测 —— INSTANT 被拒(Need to rebuild the table)、
- -- INPLACE 被拒(Cannot change column type INPLACE)、LOCK=NONE 被拒
- -- (COPY algorithm requires a lock)。故显式声明 ALGORITHM=COPY, LOCK=SHARED,
- -- 让锁行为在脚本内可见可审计,且未来 MySQL 行为变化会显式报错而非静默改变。
- -- 202 行实测耗时 72ms,写阻塞约 72ms,读不受影响。
- --
- -- 范围边界:只动 mdp_entity 三列。
- -- 不碰 uk_inbound_grant(1.0.553 已建,仅回归断言)。
- -- 不碰 R2-B 的 7 项 dormant drift。不碰任何 P0 对象。
- -- ============================================================================
- -- ── Step 0 · G1 表存在 ──────────────────────────────────────────────────────
- SET @t := (SELECT COUNT(*) FROM information_schema.TABLES
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mdp_entity');
- SELECT IF(@t <> 1, 'R2A-G1 FAIL: mdp_entity 不存在', 'R2A-G1 ok') AS gate_g1;
- SET @sql := IF(@t <> 1,
- "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A-G1: mdp_entity 不存在,中止'",
- 'DO 0');
- PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
- -- ── Step 0 · G2 inbound_enabled data-fit(TINYINT 值域 + 无 NULL)───────────
- SET @g2_null := (SELECT COUNT(*) FROM `mdp_entity` WHERE `inbound_enabled` IS NULL);
- SET @g2_oor := (SELECT COUNT(*) FROM `mdp_entity` WHERE `inbound_enabled` NOT BETWEEN -128 AND 127);
- SELECT @g2_null AS g2_null_rows, @g2_oor AS g2_out_of_range;
- SELECT IF(@g2_null <> 0 OR @g2_oor <> 0, 'R2A-G2 FAIL: inbound_enabled 存在 NULL 或超出 TINYINT 值域', 'R2A-G2 ok') AS gate_g2;
- SET @sql := IF(@g2_null <> 0 OR @g2_oor <> 0,
- "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A-G2: inbound_enabled 存在 NULL 或超出 TINYINT 值域,中止'",
- 'DO 0');
- PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
- -- ── Step 0 · G3 stale_guard_enabled data-fit ───────────────────────────────
- SET @g3_null := (SELECT COUNT(*) FROM `mdp_entity` WHERE `stale_guard_enabled` IS NULL);
- SET @g3_oor := (SELECT COUNT(*) FROM `mdp_entity` WHERE `stale_guard_enabled` NOT BETWEEN -128 AND 127);
- SELECT @g3_null AS g3_null_rows, @g3_oor AS g3_out_of_range;
- SELECT IF(@g3_null <> 0 OR @g3_oor <> 0, 'R2A-G3 FAIL: stale_guard_enabled 存在 NULL 或超出 TINYINT 值域', 'R2A-G3 ok') AS gate_g3;
- SET @sql := IF(@g3_null <> 0 OR @g3_oor <> 0,
- "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A-G3: stale_guard_enabled 存在 NULL 或超出 TINYINT 值域,中止'",
- 'DO 0');
- PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
- -- ── Step 0 · G4 inbound_max_rows data-fit(类型不变,只查 NULL;不新增业务范围)──
- SET @g4_null := (SELECT COUNT(*) FROM `mdp_entity` WHERE `inbound_max_rows` IS NULL);
- SELECT @g4_null AS g4_null_rows;
- SELECT IF(@g4_null <> 0, 'R2A-G4 FAIL: inbound_max_rows 存在 NULL', 'R2A-G4 ok') AS gate_g4;
- SET @sql := IF(@g4_null <> 0,
- "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A-G4: inbound_max_rows 存在 NULL,中止'",
- 'DO 0');
- PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
- -- ── Step 0 · G5 行数基线(只记录,不作硬编码门禁)──────────────────────────
- SET @rows_before := (SELECT COUNT(*) FROM `mdp_entity`);
- SELECT @rows_before AS rows_before;
- -- ── Step 0 · G6 回归断言:uk_inbound_grant 必须仍存在且定义正确 ─────────────
- -- 该索引由 1.0.553 创建,本脚本绝不修改它,只确认未被破坏。
- SET @uk_cols := (SELECT GROUP_CONCAT(`COLUMN_NAME` ORDER BY `SEQ_IN_INDEX` SEPARATOR ',')
- FROM information_schema.`STATISTICS`
- WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_inbound_grant'
- AND `INDEX_NAME` = 'uk_inbound_grant');
- SET @uk_unique := (SELECT MIN(`NON_UNIQUE`) FROM information_schema.`STATISTICS`
- WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_inbound_grant'
- AND `INDEX_NAME` = 'uk_inbound_grant');
- SELECT @uk_cols AS uk_inbound_grant_cols, @uk_unique AS uk_non_unique;
- SELECT IF(NOT (@uk_cols <=> 'access_key,entity_code') OR NOT (@uk_unique <=> 0), 'R2A-G6 FAIL: uk_inbound_grant 缺失或定义不符(应由 1.0.553 保证)', 'R2A-G6 ok') AS gate_g6;
- SET @sql := IF(NOT (@uk_cols <=> 'access_key,entity_code') OR NOT (@uk_unique <=> 0),
- "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A-G6: uk_inbound_grant 缺失或定义不符(应由 1.0.553 保证),中止'",
- 'DO 0');
- PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
- -- ── Step 1 · 目标态比对(NULL-safe <=>;只比 type / nullable / default)─────
- SET @drift := (SELECT COUNT(*) FROM information_schema.`COLUMNS`
- WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_entity' AND (
- (`COLUMN_NAME` = 'inbound_enabled'
- AND NOT (`COLUMN_TYPE` = 'tinyint' AND `IS_NULLABLE` = 'NO' AND `COLUMN_DEFAULT` <=> '0'))
- OR (`COLUMN_NAME` = 'inbound_max_rows'
- AND NOT (`COLUMN_TYPE` = 'int' AND `IS_NULLABLE` = 'NO' AND `COLUMN_DEFAULT` <=> '500'))
- OR (`COLUMN_NAME` = 'stale_guard_enabled'
- AND NOT (`COLUMN_TYPE` = 'tinyint' AND `IS_NULLABLE` = 'NO' AND `COLUMN_DEFAULT` <=> '1'))
- ));
- SELECT @drift AS drift_before;
- -- ── Step 2 · 仅 drift > 0 时执行;三列同一条 ALTER;显式算法与锁 ────────────
- SET @sql := IF(@drift > 0,
- 'ALTER TABLE `mdp_entity`
- MODIFY COLUMN `inbound_enabled` TINYINT NOT NULL DEFAULT 0
- COMMENT ''入站开通开关。不走 CodeFirst 自动加列,以 UpdateScripts 为准。'',
- MODIFY COLUMN `inbound_max_rows` INT NOT NULL DEFAULT 500
- COMMENT ''单请求行数上限(快照类 2000)'',
- MODIFY COLUMN `stale_guard_enabled` TINYINT NOT NULL DEFAULT 1
- COMMENT ''乱序 stale 守卫'',
- ALGORITHM=COPY, LOCK=SHARED',
- 'DO 0');
- PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
- -- ── Step 3 · 事后自断言:drift 必须归零 ────────────────────────────────────
- SET @drift_after := (SELECT COUNT(*) FROM information_schema.`COLUMNS`
- WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_entity' AND (
- (`COLUMN_NAME` = 'inbound_enabled'
- AND NOT (`COLUMN_TYPE` = 'tinyint' AND `IS_NULLABLE` = 'NO' AND `COLUMN_DEFAULT` <=> '0'))
- OR (`COLUMN_NAME` = 'inbound_max_rows'
- AND NOT (`COLUMN_TYPE` = 'int' AND `IS_NULLABLE` = 'NO' AND `COLUMN_DEFAULT` <=> '500'))
- OR (`COLUMN_NAME` = 'stale_guard_enabled'
- AND NOT (`COLUMN_TYPE` = 'tinyint' AND `IS_NULLABLE` = 'NO' AND `COLUMN_DEFAULT` <=> '1'))
- ));
- SELECT @drift_after AS drift_after;
- SELECT IF(@drift_after <> 0, 'R2A-POST1 FAIL: 三列未全部收口到目标态', 'R2A-POST1 ok') AS gate_post1;
- SET @sql := IF(@drift_after <> 0,
- "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A-POST: 三列未全部收口到目标态,中止'",
- 'DO 0');
- PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
- -- ── Step 4 · 行数完整性:ALTER 不得增减行 ──────────────────────────────────
- SET @rows_after := (SELECT COUNT(*) FROM `mdp_entity`);
- SELECT @rows_before AS rows_before, @rows_after AS rows_after;
- SELECT IF(@rows_after <> @rows_before, 'R2A-POST2 FAIL: mdp_entity 行数在 ALTER 前后不一致', 'R2A-POST2 ok') AS gate_post2;
- SET @sql := IF(@rows_after <> @rows_before,
- "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A-POST: mdp_entity 行数在 ALTER 前后不一致,中止'",
- 'DO 0');
- PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|