1.0.554.sql 9.8 KB

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