Explorar o código

fix(mdp): restore mdp_entity inbound defaults under CodeFirst | server 1.0.555

1.0.554 把 inbound_enabled / stale_guard_enabled 收口为 TINYINT + DEFAULT,
migration 与 verify 均 Success;但下一次启动时 SqlSugar CodeFirst
(EnableInitTable=true)按实体声明 `public int` 把它们改回 INT,其 ALTER
形态为 `alter table t change column c c int NOT NULL`——不带 DEFAULT 子句,
于是连带抹掉 DEFAULT,S1/S2 的 Error 1364 复现。

对照组证明了正确解法:inbound_max_rows 的类型本就等于实体声明的 INT,
CodeFirst 无差异可纠正、不生成 ALTER,其 DEFAULT 500 完整穿越了 CodeFirst
遍历。即:只要列类型与实体声明一致,CodeFirst 不重写该列,DEFAULT 存活。

故本批类型保持 INT,只补 DEFAULT。类型漂移(INT vs 契约 TINYINT)留 R2-B,
待 CodeFirst schema authority 定案后统一处理。

1.0.554 保持历史不变:不修改、不改 hash、不改 migration log、不重放。
其 inbound_max_rows 的 DEFAULT 500 成果保留,本脚本只对它做前置断言(G5),
若 500 丢失即判 SCOPE CHANGED 并中止,不顺手补。

实现要点:
- 逐列 target-state 比对,CONCAT_WS 拼子句,已正确的列真 no-op
- G6 类型守卫:两列必须已是 INT NOT NULL,否则中止,本脚本不自动改类型
- COLUMN_DEFAULT 比较用 NULL-safe <=>
- ALGORITHM=INSTANT(纯元数据,不重建表,无写阻塞),3307 实测 58ms
- 门禁前附可读诊断 SELECT(SIGNAL 不支持 prepared statement,条件成立时
  实际抛 1295 而非预期消息)

演练补上了上一轮的盲区——这次跑了真实 backend:
  SQL 执行 → 三列 int/0、int/500、int/1
  CodeFirst 完整遍历 285/284 → 三个 DEFAULT 全部存活
  重启再跑一次完整遍历 → schema checksum 逐位不变
  裸 INSERT 省略三列 → 落值 0/500/1
YY968XX hai 2 días
pai
achega
e6ad49a378

+ 9 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.554</AssemblyVersion>
-    <FileVersion>1.0.554</FileVersion>
-    <Version>1.0.554</Version>
+    <AssemblyVersion>1.0.555</AssemblyVersion>
+    <FileVersion>1.0.555</FileVersion>
+    <Version>1.0.555</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -904,6 +904,12 @@
     <None Update="UpdateScripts\1.0.554.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.555.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.555.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="UpdateScripts\UAT-PLACEHOLDER-MENU-HIDE.ops.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>

+ 126 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.555.sql

@@ -0,0 +1,126 @@
+-- ============================================================================
+-- 1.0.555 · R2-A2:mdp_entity 两列补 DEFAULT(短期恢复,不动类型)
+--
+-- 为什么需要本脚本(1.0.554 的效果被撤销,已实证):
+--   1.0.554 把 inbound_enabled / stale_guard_enabled 收口为 TINYINT + DEFAULT,
+--   migration 与 verify 均 Success。但下一次启动时 SqlSugar CodeFirst
+--   (EnableInitTable=true)按实体声明 `public int` 把它们改回 INT,
+--   其 ALTER 形态为 `alter table t change column c c int NOT NULL`——
+--   不带 DEFAULT 子句,于是连带抹掉 DEFAULT。
+--   对照组:inbound_max_rows 类型本就等于实体声明的 INT,CodeFirst 无差异
+--   可纠正、不生成 ALTER,其 DEFAULT 500 完整穿越了一次 CodeFirst 遍历。
+--
+--   结论:只要列类型与实体声明一致,CodeFirst 就不会重写该列,DEFAULT 得以存活。
+--   故本批策略 = 类型保持 INT(与实体一致),只补 DEFAULT。
+--   类型漂移(INT vs 契约 TINYINT)留 R2-B,待 CodeFirst schema authority 定案。
+--
+-- 1.0.554 保持历史不变:不修改、不改 hash、不改 migration log、不重放。
+--
+-- 算法:本地 3307 实测 ALTER COLUMN ... SET DEFAULT 接受 INSTANT / INPLACE,LOCK=NONE
+--   / COPY 三者。取 INSTANT(纯元数据变更,不重建表,无写阻塞),实测 58ms。
+--
+-- 范围:只动 mdp_entity 的 inbound_enabled / stale_guard_enabled 两列的 DEFAULT。
+--   inbound_max_rows 只做前置断言,绝不 ALTER。
+--   不碰 MdpEntity.cs / CodeFirst / EnableInitTable / R2-B / P0 / 1.0.554。
+-- ============================================================================
+
+-- ── G1 表存在 ───────────────────────────────────────────────────────────────
+SET @t := (SELECT COUNT(*) FROM information_schema.`TABLES`
+            WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_entity');
+SELECT IF(@t <> 1, 'R2A2-G1 FAIL: mdp_entity 不存在', 'R2A2-G1 ok') AS gate_g1;
+SET @sql := IF(@t <> 1,
+  "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A2-G1: mdp_entity 不存在,中止'", 'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ── G2 inbound_enabled 无 NULL ─────────────────────────────────────────────
+SET @g2 := (SELECT COUNT(*) FROM `mdp_entity` WHERE `inbound_enabled` IS NULL);
+SELECT @g2 AS g2_null_rows;
+SELECT IF(@g2 <> 0, 'R2A2-G2 FAIL: inbound_enabled 存在 NULL', 'R2A2-G2 ok') AS gate_g2;
+SET @sql := IF(@g2 <> 0,
+  "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A2-G2: inbound_enabled 存在 NULL,中止'", 'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ── G3 stale_guard_enabled 无 NULL ─────────────────────────────────────────
+SET @g3 := (SELECT COUNT(*) FROM `mdp_entity` WHERE `stale_guard_enabled` IS NULL);
+SELECT @g3 AS g3_null_rows;
+SELECT IF(@g3 <> 0, 'R2A2-G3 FAIL: stale_guard_enabled 存在 NULL', 'R2A2-G3 ok') AS gate_g3;
+SET @sql := IF(@g3 <> 0,
+  "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A2-G3: stale_guard_enabled 存在 NULL,中止'", 'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ── G4 行数基线(只记录,不硬编码)────────────────────────────────────────
+SET @rows_before := (SELECT COUNT(*) FROM `mdp_entity`);
+SELECT @rows_before AS rows_before;
+
+-- ── G5 inbound_max_rows 必须已是 INT NOT NULL DEFAULT 500 ──────────────────
+--    它由 1.0.554 修好并已穿越一次 CodeFirst 遍历;若 500 丢失说明前提变了,
+--    属 SCOPE CHANGED,本脚本不得顺手补。
+SET @g5 := (SELECT COUNT(*) FROM information_schema.`COLUMNS`
+             WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_entity'
+               AND `COLUMN_NAME` = 'inbound_max_rows'
+               AND `COLUMN_TYPE` = 'int' AND `IS_NULLABLE` = 'NO'
+               AND `COLUMN_DEFAULT` <=> '500');
+SELECT @g5 AS g5_inbound_max_rows_ok;
+SELECT IF(@g5 <> 1,
+  'R2A2-G5 FAIL: inbound_max_rows 不是 INT NOT NULL DEFAULT 500 — SCOPE CHANGED',
+  'R2A2-G5 ok') AS gate_g5;
+SET @sql := IF(@g5 <> 1,
+  "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A2-G5: inbound_max_rows 不符前提(应为 INT NOT NULL DEFAULT 500),SCOPE CHANGED,中止'",
+  'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ── G6 类型守卫:两列必须已是 INT NOT NULL;本脚本不改类型 ──────────────────
+SET @g6 := (SELECT COUNT(*) FROM information_schema.`COLUMNS`
+             WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_entity'
+               AND `COLUMN_NAME` IN ('inbound_enabled', 'stale_guard_enabled')
+               AND `COLUMN_TYPE` = 'int' AND `IS_NULLABLE` = 'NO');
+SELECT @g6 AS g6_int_notnull_cols;
+SELECT IF(@g6 <> 2,
+  'R2A2-G6 FAIL: 两列类型不是 INT NOT NULL — 本脚本不自动改类型',
+  'R2A2-G6 ok') AS gate_g6;
+SET @sql := IF(@g6 <> 2,
+  "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A2-G6: inbound_enabled / stale_guard_enabled 类型不是 INT NOT NULL,本脚本不自动改类型,中止'",
+  'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ── Step 1 · 逐列目标态比对(NULL-safe <=>),仅对仍漂移的列生成子句 ────────
+SET @need_ie := (SELECT COUNT(*) FROM information_schema.`COLUMNS`
+                  WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_entity'
+                    AND `COLUMN_NAME` = 'inbound_enabled'
+                    AND NOT (`COLUMN_DEFAULT` <=> '0'));
+SET @need_sg := (SELECT COUNT(*) FROM information_schema.`COLUMNS`
+                  WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_entity'
+                    AND `COLUMN_NAME` = 'stale_guard_enabled'
+                    AND NOT (`COLUMN_DEFAULT` <=> '1'));
+SELECT @need_ie AS need_inbound_enabled, @need_sg AS need_stale_guard_enabled;
+
+-- CONCAT_WS 会跳过 NULL,于是只有需要改的列才进入 ALTER 子句
+SET @c1 := IF(@need_ie > 0, 'ALTER COLUMN `inbound_enabled` SET DEFAULT 0', NULL);
+SET @c2 := IF(@need_sg > 0, 'ALTER COLUMN `stale_guard_enabled` SET DEFAULT 1', NULL);
+SET @clauses := CONCAT_WS(', ', @c1, @c2);
+
+-- ── Step 2 · 仅在有漂移列时执行;ALGORITHM=INSTANT(纯元数据,无写阻塞)────
+SET @sql := IF(@clauses IS NULL OR @clauses = '',
+  'DO 0',
+  CONCAT('ALTER TABLE `mdp_entity` ', @clauses, ', ALGORITHM=INSTANT'));
+SELECT @sql AS planned_statement;
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ── Step 3 · 事后自断言:两列 DEFAULT 必须到位 ─────────────────────────────
+SET @after := (SELECT COUNT(*) FROM information_schema.`COLUMNS`
+                WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_entity'
+                  AND ((`COLUMN_NAME` = 'inbound_enabled'     AND `COLUMN_DEFAULT` <=> '0')
+                    OR (`COLUMN_NAME` = 'stale_guard_enabled' AND `COLUMN_DEFAULT` <=> '1')));
+SELECT @after AS defaults_in_place;
+SELECT IF(@after <> 2, 'R2A2-POST1 FAIL: 两列 DEFAULT 未全部到位', 'R2A2-POST1 ok') AS gate_post1;
+SET @sql := IF(@after <> 2,
+  "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A2-POST: 两列 DEFAULT 未全部到位,中止'", 'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ── Step 4 · 行数完整性 ────────────────────────────────────────────────────
+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, 'R2A2-POST2 FAIL: 行数变化', 'R2A2-POST2 ok') AS gate_post2;
+SET @sql := IF(@rows_after <> @rows_before,
+  "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'R2A2-POST: mdp_entity 行数在 ALTER 前后不一致,中止'", 'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;

+ 68 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.555.verify.sql

@@ -0,0 +1,68 @@
+-- ============================================================================
+-- 1.0.555.verify.sql · R2-A2 执行后校验(返回单列 ok:1=全通过)
+--
+--   V1 inbound_enabled     = INT / NOT NULL / DEFAULT 0
+--   V2 stale_guard_enabled = INT / NOT NULL / DEFAULT 1
+--   V3 inbound_max_rows    = INT / NOT NULL / DEFAULT 500(本批不改,只断言)
+--   V4 三列 NULL 计数 = 0
+--   V5 mdp_entity 仍有行
+--   V6 1.0.554 历史未被改动(status/verify/hash 原样)
+--   V7 P0 未被触碰(UK 列序 / CHECK / po_no·po_line NOT NULL)
+--
+--   注:类型断言为 INT 而非 TINYINT —— 本批刻意保持与实体声明一致,
+--       否则 CodeFirst 会重写该列并连带抹掉 DEFAULT(1.0.554 已实证)。
+--       INT vs 契约 TINYINT 的类型漂移留 R2-B。
+--   注:不比对 COLUMN_COMMENT,避免与 CodeFirst 的注释同步互踩。
+-- ============================================================================
+SELECT CASE WHEN
+
+-- V1 · inbound_enabled
+    (SELECT COUNT(*) FROM information_schema.`COLUMNS`
+      WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_entity'
+        AND `COLUMN_NAME` = 'inbound_enabled'
+        AND `COLUMN_TYPE` = 'int' AND `IS_NULLABLE` = 'NO'
+        AND `COLUMN_DEFAULT` <=> '0') = 1
+
+-- V2 · stale_guard_enabled
+AND (SELECT COUNT(*) FROM information_schema.`COLUMNS`
+      WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_entity'
+        AND `COLUMN_NAME` = 'stale_guard_enabled'
+        AND `COLUMN_TYPE` = 'int' AND `IS_NULLABLE` = 'NO'
+        AND `COLUMN_DEFAULT` <=> '1') = 1
+
+-- V3 · inbound_max_rows(1.0.554 产物,本批只读断言)
+AND (SELECT COUNT(*) FROM information_schema.`COLUMNS`
+      WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_entity'
+        AND `COLUMN_NAME` = 'inbound_max_rows'
+        AND `COLUMN_TYPE` = 'int' AND `IS_NULLABLE` = 'NO'
+        AND `COLUMN_DEFAULT` <=> '500') = 1
+
+-- V4 · 三列无 NULL
+AND (SELECT COUNT(*) FROM `mdp_entity`
+      WHERE `inbound_enabled` IS NULL
+         OR `inbound_max_rows` IS NULL
+         OR `stale_guard_enabled` IS NULL) = 0
+
+-- V5 · 表非空
+AND (SELECT COUNT(*) FROM `mdp_entity`) > 0
+
+-- V6 · 1.0.554 历史未被改动
+AND (SELECT COUNT(*) FROM `sys_db_migration_log`
+      WHERE `version` = '1.0.554'
+        AND `status` = 'Success' AND `verify_status` = 'Success'
+        AND UPPER(`file_hash`) = '04F0C650DCC5F8D36F7A5ACED5346C420BF967AA2493E7261FEA311E381EEB68') = 1
+
+-- V7 · P0 未被触碰
+AND (SELECT GROUP_CONCAT(`COLUMN_NAME` ORDER BY `SEQ_IN_INDEX` SEPARATOR ',')
+       FROM information_schema.`STATISTICS`
+      WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'dwd_process_outsource_delivery'
+        AND `INDEX_NAME` = 'uk_po_line_stat') = 'tenant_id,factory_id,stat_date,po_no,po_line'
+AND (SELECT COUNT(*) FROM information_schema.`TABLE_CONSTRAINTS`
+      WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'dwd_process_outsource_delivery'
+        AND `CONSTRAINT_TYPE` = 'CHECK'
+        AND `CONSTRAINT_NAME` = 'ck_dwd_process_outsource_valid_tenant') = 1
+AND (SELECT COUNT(*) FROM information_schema.`COLUMNS`
+      WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'dwd_process_outsource_delivery'
+        AND `COLUMN_NAME` IN ('po_no', 'po_line') AND `IS_NULLABLE` = 'YES') = 0
+
+THEN 1 ELSE 0 END AS ok;