yeg
6 天以前 2222cd054570014ddc3e9a7d72c365c505e1ba5e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package com.battery.business.runtime;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
 
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
 
import org.junit.jupiter.api.Test;
 
class FlywayMigrationContractTest
{
    /**
     * 基线文件的"内容"指纹。**行尾不参与判定**:仓库按 autocrlf 检出,同一份内容在不同机器上
     * 可能是 LF 也可能是 CRLF;而 Flyway 不读 `db/migration`(那是不可变历史,只有本测试读它),
     * 所以把行尾当噪声剔除,守卫只拦"有人改写基线内容"。
     *
     * V1 与 V3 的指纹按提交 `545e467 补提过往数据库改动` 之后的实际内容重新钉过:那次提交改动了
     * 这两份历史脚本,而这里的指纹没跟着更新,之后一直靠 target/classes 里的旧副本"看起来是绿的",
     * `mvn clean` 之后就会红。本次修订没有再次改写基线内容。
     */
    private static final String V1_SHA256 = "dfcef29b236a0e0629accd2de1fd7fa079486c31ec769b7d20406c0e46b24186";
    private static final String V2_SHA256 = "698023958beee6245b099cf4505d35874cd06823e4dbbc3ddcb11b8f76609b89";
    private static final String V3_SHA256 = "51f675a1a28eee2da0ea83379a429cb8337138000e83e29f4a2f4e868dee9680";
    private static final String V6_SHA256 = "2f20752fcb9a318435d27ba4a0c3e0ba9443ba2f61a884f82bace457270d7db8";
 
    @Test
    void keepsImmutableBaselineAndAddsObservationIncrementOnly()
    {
        assertEquals(V1_SHA256, sha256("db/migration/V1__ruoyi_platform_baseline.sql"));
        assertEquals(V2_SHA256, sha256("db/migration/V2__battery_detection_domain.sql"));
        assertEquals(V3_SHA256, sha256("db/migration/V3__battery_detection_permissions.sql"));
 
        String v4 = resource("db/migration/V4__observation_persistence_constraints.sql");
        assertTrue(v4.contains("ADD COLUMN creator_id"));
        assertTrue(v4.contains("uk_exception_open_dedupe"));
        assertTrue(v4.contains("adapter_timezone_id"));
        assertTrue(v4.contains("fk_sample_channel"));
        assertFalse(v4.matches("(?s).*\\bINSERT\\s+INTO\\b.*"));
 
        String v5 = resource("db/migration/V5__adapter_discovery_runtime.sql");
        assertTrue(v5.contains("connection_status"));
        assertTrue(v5.contains("last_observed_at_utc"));
        assertTrue(v5.contains("chk_adapter_connection_status"));
        assertTrue(v5.contains("system:deviceAdapter:list"));
        assertTrue(v5.contains("system:deviceAdapter:query"));
        assertTrue(v5.contains("system:deviceAdapter:add"));
        assertTrue(v5.contains("system:deviceAdapter:edit"));
        assertTrue(v5.contains("system:deviceAdapter:changeStatus"));
        assertTrue(v5.contains("system:deviceAdapter:discover"));
        assertTrue(v5.contains("system:detectionCabinet:list"));
        assertFalse(v5.matches("(?s).*\\bDELETE\\s+FROM\\b.*"));
 
        String v6 = resource("db/migration/V6__separate_discovery_from_registration.sql");
        assertEquals(V6_SHA256, sha256("db/migration/V6__separate_discovery_from_registration.sql"));
        assertTrue(v6.contains("registration_status"));
        assertTrue(v6.contains("DISCOVERED"));
        assertTrue(v6.contains("REGISTERED"));
        assertTrue(v6.contains("chk_cabinet_registration_status"));
        assertTrue(v6.contains("DEFAULT 'DISCOVERED'"));
        assertFalse(v6.matches("(?s).*\\bDELETE\\s+FROM\\b.*"));
 
        String v7 = resource("db/migration/V7__complete_device_registration_boundary.sql");
        assertTrue(v7.contains("WHEN create_by = 'system' THEN 'DISCOVERED'"));
        assertTrue(v7.contains("ELSE 'REGISTERED'"));
        assertTrue(v7.contains("uk_cabinet_mac_del"));
        assertFalse(v7.matches("(?s).*\\bDELETE\\s+FROM\\b.*"));
 
        String v8 = resource("db/migration/V8__complete_collector_page_contract.sql");
        assertTrue(v8.contains("ALTER TABLE t_collector"));
        assertTrue(v8.contains("test_status"));
        assertTrue(v8.contains("current_test"));
        assertTrue(v8.contains("current_sample_name"));
        assertTrue(v8.contains("chk_collector_test_status"));
        assertFalse(v8.matches("(?s).*\\bDELETE\\s+FROM\\b.*"));
 
        String v9 = resource("db/migration/V9__move_discovery_candidates_to_redis.sql");
        assertTrue(v9.contains("adapter_code VARCHAR(64)"));
        assertTrue(v9.contains("adapter.adapter_code = 'dev-sim'"));
        assertTrue(v9.contains("cabinet.registration_status = 'DISCOVERED'"));
        assertTrue(v9.contains("t_test_channel_binding"));
        assertTrue(v9.contains("t_test_sample"));
        assertTrue(v9.contains("t_device_run_event"));
        assertTrue(v9.contains("SELECT protected_cabinet.cabinet_id"));
        assertTrue(v9.contains("SELECT DISTINCT sibling.cabinet_id"));
        assertTrue(v9.contains("uk_channel_stable_identity"));
        assertTrue(v9.contains("ADD KEY idx_cabinet_adapter (adapter_id)"));
        assertTrue(v9.contains("SET adapter_id = NULL WHERE adapter_code = 'dev-sim'"));
        assertFalse(v9.contains("DROP FOREIGN KEY fk_channel_adapter"));
        assertFalse(v9.contains("DROP FOREIGN KEY fk_cabinet_adapter"));
        assertFalse(v9.matches("(?s).*t_test_battery\\s+\\w+\\s+WHERE\\s+\\w+\\.cabinet_id.*"));
 
        String v10 = resource("db/migration/V10__test_configuration_collector.sql");
        assertTrue(v10.contains("ALTER TABLE t_test_battery"));
        assertTrue(v10.contains("collector_id"));
        assertTrue(v10.contains("fk_test_battery_collector"));
        assertFalse(v10.matches("(?s).*\\bINSERT\\s+INTO\\b.*"));
 
        String v11 = resource("db/migration/V11__persist_battery_group_field_values.sql");
        assertTrue(v11.contains("ALTER TABLE t_battery_group_relation_field"));
        assertTrue(v11.contains("field_value VARCHAR(500)"));
        assertFalse(v11.matches("(?s).*\\bINSERT\\s+INTO\\b.*"));
 
        String v12 = resource("db/migration/V12__custom_field_type_constraints.sql");
        assertTrue(v12.contains("chk_battery_group_field_type"));
        assertTrue(v12.contains("'text', 'number', 'time', 'remark'"));
        assertFalse(v12.matches("(?s).*\\bINSERT\\s+INTO\\b.*"));
 
        String v13 = resource("db/migration/V13__test_required_device_bindings.sql");
        assertTrue(v13.contains("reception_status"));
        assertTrue(v13.contains("DEFAULT 'NOT_STARTED'"));
        assertTrue(v13.contains("CREATE TABLE t_test_collector_channel"));
        assertTrue(v13.contains("uk_test_collector_channel"));
        assertTrue(v13.contains("CREATE TABLE t_test_field_snapshot"));
        assertFalse(v13.matches("(?s).*\\bINSERT\\s+INTO\\b.*"));
        assertFalse(v13.matches("(?s).*\\bUPDATE\\s+t_test\\b.*"));
 
        String v14 = resource("db/migration/V14__test_reception_intervals.sql");
        assertTrue(v14.contains("CREATE TABLE t_test_reception_interval"));
        assertTrue(v14.contains("start_boundary_at_utc"));
        assertTrue(v14.contains("CREATE TABLE t_collector_channel_lease"));
        assertTrue(v14.contains("idx_test_reception_status"));
        assertTrue(v14.contains("system:test:start"));
 
        String v16 = resource("db/migration/V16__simplify_test_reception_lifecycle.sql");
        assertTrue(v16.contains("system:test:stop"));
        assertTrue(v16.contains("旧区间表保留为历史数据"));
        assertFalse(v16.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
 
        String v17 = resource("db/migration/V17__persist_runtime_attach_flag.sql");
        assertTrue(v17.contains("ALTER TABLE t_test"));
        assertTrue(v17.contains("ADD COLUMN runtime_attach"));
        assertTrue(v17.contains("TINYINT(1) NOT NULL DEFAULT 0"));
        assertTrue(v17.contains("AFTER ended_at_utc"));
        assertFalse(v17.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
 
        String v18 = resource("db/migration/V18__collector_disconnect_recovery.sql");
        assertTrue(v18.contains("CREATE TABLE t_test_collector_exception_interval"));
        assertTrue(v18.contains("missing_from_utc"));
        assertTrue(v18.contains("recovered_at_utc"));
        assertTrue(v18.contains("uk_collector_exception_open"));
        assertFalse(v18.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
 
        String v19 = resource("db/migration/V19__collector_disconnect_open_dedupe.sql");
        assertTrue(v19.contains("DROP INDEX uk_collector_exception_open"));
        assertTrue(v19.contains("open_dedupe_key VARCHAR(320)"));
        assertTrue(v19.contains("interval_status = 'OPEN'"));
        assertTrue(v19.contains("ELSE NULL"));
        assertTrue(v19.contains("ADD UNIQUE KEY uk_collector_exception_open (open_dedupe_key)"));
        assertFalse(v19.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
 
        String v20 = resource("db/migration/V20__manual_end_reception_intervals.sql");
        assertTrue(v20.contains("chk_reception_interval_boundary_order"));
        assertTrue(v20.contains("idx_reception_interval_source_order"));
        assertTrue(v20.contains("system:test:stop"));
        assertFalse(v20.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
 
        String v31 = resource("db/migration/V31__unified_realtime_snapshots.sql");
        assertTrue(v31.contains("CREATE TABLE t_test_realtime_snapshot"));
        assertTrue(v31.contains("fk_realtime_snapshot_test"));
        assertTrue(v31.contains("fk_realtime_snapshot_binding"));
        assertTrue(v31.contains("fk_realtime_snapshot_channel"));
        assertTrue(v31.contains("fk_realtime_snapshot_collector"));
        assertFalse(v31.contains("fk_snapshot_test"));
        assertFalse(v31.contains("fk_snapshot_binding"));
        assertFalse(v31.contains("fk_snapshot_channel"));
 
        String testMapper = resource("mapper/battery/TTestMapper.xml");
        assertTrue(testMapper.contains("selectReceivingChannelsForAdapter"));
        assertTrue(testMapper.contains("updateReceptionResumed"));
        assertTrue(testMapper.contains("updateReceptionStopped"));
        assertFalse(testMapper.contains("#{testId}</if><if"));
    }
 
    private static String sha256(String path)
    {
        try
        {
            // 行尾是检出环境的产物,不是内容:换行统一成 LF 之后再算指纹。
            String content = resource(path).replace("\r\n", "\n");
            return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
                    .digest(content.getBytes(StandardCharsets.UTF_8)));
        }
        catch (Exception exception)
        {
            throw new AssertionError("无法读取迁移资源 " + path, exception);
        }
    }
 
    private static String resource(String path)
    {
        try (InputStream stream = FlywayMigrationContractTest.class.getClassLoader().getResourceAsStream(path))
        {
            if (stream == null) throw new AssertionError("缺少迁移资源 " + path);
            return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
        }
        catch (Exception exception)
        {
            throw new AssertionError("无法读取迁移资源 " + path, exception);
        }
    }
}