1.0.494.sql 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. -- S8-TENANT-ONLY-BATCH6:把 S8 异常单据的唯一身份从 (Tenant, Factory, Code) 收敛为 (Tenant, Code)。
  2. --
  3. -- 本脚本与同批代码是**一个不可分割的单元**,但两边的耦合方向与 1.0.493 不同:
  4. -- 代码侧本批已把全部异常读路径改成只按 tenant_id 过滤,因此**只跑代码不跑脚本不会立刻出错**,
  5. -- 只是唯一约束不再表达真实不变量。真正的问题在于它会**默许**一种错误:
  6. -- 本批之后新建异常的 factory_id 恒为 0,而历史行 factory_id ≠ 0
  7. -- (实测 5 个不同取值:797403760988231 / 838257237676101 / 838257186320453 / 1 / 838257212858437)。
  8. -- 旧唯一键在这种混合状态下允许同一租户内出现两条相同 exception_code —— 一条旧工厂、一条 0。
  9. -- 业务上 exception_code 是对外单号,同租户重号即错。
  10. --
  11. -- 变更(DDL 在 MySQL 下自动提交,脚本按"每步可独立重跑"设计,不依赖事务回滚):
  12. -- ① preflight:(tenant_id, exception_code) 是否已存在重复 —— 有则整脚本中止,不自动挑一条
  13. -- ② 唯一键 uk(tenant_id, factory_id, exception_code) → uk(tenant_id, exception_code)
  14. -- ③ 复验:新唯一键必须存在且旧唯一键必须消失
  15. --
  16. -- 明确**不做**(每一条都是刻意的,不是遗漏):
  17. -- · 不 DROP 任何 factory_id 列。本批移除的是"隔离语义",不是 schema 美化;
  18. -- 历史行的 factory_id 仍是可解释的血缘信息。
  19. -- · 不批量重写 ado_s8_exception.factory_id。把历史行改成 0 会抹掉"这条异常当年属于哪个工厂",
  20. -- 而这既非必要(读路径已不看它)也不可逆。
  21. -- · 不动其余 10 个 (tenant_id, factory_id, ...) 辅助索引。它们的前导列是 tenant_id,
  22. -- 新查询仍能走前缀,不存在性能塌陷;为了整洁重建 10 个索引与本批风险不相称。
  23. -- · 不动 1.0.486 / 1.0.487 / 1.0.493 三个已冻结脚本,也不动它们建立的任何归档表
  24. -- (ado_s8_exception_bak_1_0_487 / *_dedup_bak_1_0_493 是还原点,改它们等于毁掉备份)。
  25. -- · 不动 ado_s8_scene_config / ado_s8_notification_layer 等配置表的 factory_id 值。
  26. -- 配置侧的"租户覆盖 > 平台默认"改判在代码层完成,(0,0) 平台默认哨兵语义保持不变。
  27. --
  28. -- 迁移前实测(2026-09-06,共享库 aidopdev 只读查询):
  29. -- ado_s8_exception 现有唯一键 : uk_ado_s8_exception_tenant_factory_code (tenant_id, factory_id, exception_code)
  30. -- (tenant_id, exception_code) 重复组 : 0
  31. -- factory_id 取值分布 : 797403760988231(42) / 838257237676101(39) / 838257186320453(21) / 1(14) / 838257212858437(1)
  32. -- ============================================================================
  33. -- ① PREFLIGHT:重号门禁
  34. --
  35. -- 若同租户下已存在重复 exception_code(历史上靠 factory 区分过),
  36. -- 收紧唯一键会直接失败在 ALTER 上,报错文案指向索引而不是业务原因。
  37. -- 这里提前挡住,并把明细落表,让运维知道**具体哪几个单号**撞了。
  38. --
  39. -- 门禁实现说明:AutoVersionUpdate 对全文做子串匹配拒绝自定义语句分隔符指令,
  40. -- 连注释里出现那个关键字都会被拦,因此本文件通篇不出现该词,也就用不了存储过程 + SIGNAL。
  41. -- 改用**具名 CHECK 约束**:约束名本身携带失败原因,MySQL 报错文案会带上它,
  42. -- 而该文案会被写进 sys_db_migration_log.error_message,运维在日志里直接看得到为什么被挡。
  43. -- ============================================================================
  44. SET @dup_exception_code := (
  45. SELECT COUNT(*) FROM (
  46. SELECT tenant_id, exception_code, COUNT(*) c
  47. FROM ado_s8_exception
  48. GROUP BY 1, 2 HAVING c > 1) z);
  49. DROP TABLE IF EXISTS ado_s8_tenant_only_dupcode_1_0_494;
  50. CREATE TABLE ado_s8_tenant_only_dupcode_1_0_494 (
  51. tenant_id BIGINT NULL,
  52. exception_code VARCHAR(64) NULL,
  53. factory_ids VARCHAR(512) NULL,
  54. row_count INT NULL
  55. ) COMMENT 'S8-TENANT-ONLY-BATCH6 同租户重复 exception_code 明细(为空表示无重复)';
  56. INSERT INTO ado_s8_tenant_only_dupcode_1_0_494 (tenant_id, exception_code, factory_ids, row_count)
  57. SELECT tenant_id, exception_code, GROUP_CONCAT(DISTINCT factory_id), COUNT(*)
  58. FROM ado_s8_exception
  59. GROUP BY 1, 2 HAVING COUNT(*) > 1;
  60. -- 门禁本体:probe 非 0 即违反 CHECK,脚本在**任何 DDL 之前**失败。
  61. DROP TABLE IF EXISTS tmp_s8_494_preflight_guard;
  62. CREATE TABLE tmp_s8_494_preflight_guard (
  63. probe INT NOT NULL,
  64. CONSTRAINT s8_tenant_only_BLOCKED_see_dupcode_table_1_0_494 CHECK (probe = 0)
  65. ) COMMENT 'S8-TENANT-ONLY-BATCH6 preflight 门禁;成功后自动删除';
  66. INSERT INTO tmp_s8_494_preflight_guard (probe) VALUES (@dup_exception_code);
  67. DROP TABLE IF EXISTS tmp_s8_494_preflight_guard;
  68. -- ============================================================================
  69. -- ② 收紧唯一键
  70. --
  71. -- 顺序是先建新、后删旧:中间态多一个索引没有正确性问题,
  72. -- 而"先删旧"若在建新时失败,表会短暂失去单号唯一性保护。
  73. -- 用 information_schema 判存在性:MySQL 无 CREATE/DROP INDEX IF EXISTS。
  74. -- ============================================================================
  75. SET @has_new_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
  76. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_exception'
  77. AND INDEX_NAME = 'uk_ado_s8_exception_tenant_code');
  78. SET @sql := IF(@has_new_uk = 0,
  79. 'ALTER TABLE ado_s8_exception ADD UNIQUE INDEX uk_ado_s8_exception_tenant_code (tenant_id, exception_code)', 'DO 0');
  80. PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
  81. SET @has_old_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
  82. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_exception'
  83. AND INDEX_NAME = 'uk_ado_s8_exception_tenant_factory_code');
  84. SET @sql := IF(@has_old_uk > 0,
  85. 'ALTER TABLE ado_s8_exception DROP INDEX uk_ado_s8_exception_tenant_factory_code', 'DO 0');
  86. PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
  87. -- ============================================================================
  88. -- ③ POSTCHECK:新键在、旧键不在
  89. --
  90. -- 只做 DDL 不复验,等于把"改没改成"交给下一次故障去告诉你。
  91. -- ============================================================================
  92. SET @post_new_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
  93. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_exception'
  94. AND INDEX_NAME = 'uk_ado_s8_exception_tenant_code' AND NON_UNIQUE = 0);
  95. SET @post_old_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
  96. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_exception'
  97. AND INDEX_NAME = 'uk_ado_s8_exception_tenant_factory_code');
  98. DROP TABLE IF EXISTS tmp_s8_494_postcheck_guard;
  99. CREATE TABLE tmp_s8_494_postcheck_guard (
  100. probe INT NOT NULL,
  101. CONSTRAINT s8_tenant_only_POSTCHECK_FAILED_1_0_494 CHECK (probe = 0)
  102. ) COMMENT 'S8-TENANT-ONLY-BATCH6 postcheck 门禁;成功后自动删除';
  103. -- 期望:新唯一键恰好 2 个列条目(tenant_id, exception_code),旧唯一键 0 个条目。
  104. INSERT INTO tmp_s8_494_postcheck_guard (probe)
  105. VALUES ((CASE WHEN @post_new_uk = 2 THEN 0 ELSE 1 END) + @post_old_uk);
  106. DROP TABLE IF EXISTS tmp_s8_494_postcheck_guard;
  107. -- preflight 明细表为空即无重复;保留空表作为"本次迁移确实检查过"的留痕,
  108. -- 与 1.0.493 的 collision 表同一处理方式(不自动 DROP,交运维按需清理)。