yeg
2026-09-05 92f32acd847ea1700590bc669750f23ef499840e
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
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
{
    private static final String V1_SHA256 = "37de76546e5aa0aa0ce7e976b73383a2d80ef97fd2db923d9e23dbc2b34534c9";
    private static final String V2_SHA256 = "698023958beee6245b099cf4505d35874cd06823e4dbbc3ddcb11b8f76609b89";
    private static final String V3_SHA256 = "763de1dfaa96e6f07f7ed3b3975e6f281dbc0c2e7f139da3bed6a2f1bfc39727";
    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
        {
            return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
                    .digest(resource(path).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);
        }
    }
}