Viacheslav Klimov 1 day ago
committed by GitHub
parent
commit
e05df09e09
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 14
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 25
      application/src/main/data/upgrade/lts/4.4.0.0/schema_update.sql
  3. 24
      application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java
  4. 41
      application/src/main/java/org/thingsboard/server/service/install/lts/V4_4_0_0Migration.java
  5. 8
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  6. 21
      application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationIntegrationTest.java
  7. 75
      application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java

14
application/src/main/data/upgrade/basic/schema_update.sql

@ -14,14 +14,6 @@
-- limitations under the License.
--
-- CALCULATED FIELD ADDITIONAL INFO ADDITION START
ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar;
-- CALCULATED FIELD ADDITIONAL INFO ADDITION END
-- RULE CHAIN NOTES MIGRATION START
ALTER TABLE rule_chain ADD COLUMN IF NOT EXISTS notes varchar(1000000);
-- RULE CHAIN NOTES MIGRATION END
-- Intentionally empty. The offline upgrade path is now fully bean-driven: every schema/data change lives in
-- an LtsMigration bean and its data/upgrade/lts/<version>/schema_update.sql (4.4 baseline DDL is in
-- V4_4_0_0Migration / lts/4.4.0.0/schema_update.sql). This file is kept present only so loadSql does not fail.

25
application/src/main/data/upgrade/lts/4.4.0.0/schema_update.sql

@ -0,0 +1,25 @@
--
-- Copyright © 2016-2026 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- 4.4 baseline flat DDL, applied by V4_4_0_0Migration during a 4.3.x -> 4.4 offline upgrade. Keep these
-- idempotent (ALTER ... IF NOT EXISTS): the offline path records the package version once at the end, so a
-- resumed upgrade re-runs this whole file.
-- RULE CHAIN NOTES MIGRATION START
ALTER TABLE rule_chain ADD COLUMN IF NOT EXISTS notes varchar(1000000);
-- RULE CHAIN NOTES MIGRATION END

24
application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java

@ -114,21 +114,23 @@ public class LtsMigrationService {
private List<VersionedMigration> select(String fromVersion, String toVersion) {
LtsVersion from = LtsVersion.parse(fromVersion);
LtsVersion to = LtsVersion.parse(toVersion);
// Select every migration in the (from, to] range, regardless of family. On a cross-family offline
// upgrade (e.g. 4.3.x -> 4.4) this is what makes the real in-range older-family beans run: it picks the
// 4.3.1.x schema/data changes the source has not yet passed AND the new target-family beans, each exactly
// once -- the half-open (from, to] range skips anything the source already applied. One logical migration
// is thus authored once (one bean + one lts/<version>/schema_update.sql) and reused by both the offline
// and no-downtime paths; nothing is reproduced into a newer family.
//
// Load-bearing invariant: no two beans may reproduce the same change within a single supported upgrade
// range. A reproduction-duplicate bean (one that re-does an older bean's work on a newer-family branch)
// must sit STRICTLY BELOW the minimum supported upgrade source, so it can never be selected together with
// the bean it duplicates. The only such pair today is 4.2.2.3 <-> 4.3.1.3, and the supported-source floor
// is 4.3.0.0 (SUPPORTED_VERSIONS_FOR_UPGRADE), well above 4.2.2.3. LtsMigrationServiceTest guards this.
return migrations.stream()
.filter(vm -> isInRangeForTargetFamily(vm.version(), from, to))
.filter(vm -> vm.version().isInRange(from, to))
.toList();
}
// Run only migrations whose family matches the target version. Older-family
// migrations (e.g. 4.2.x) ride onto newer-family branches (e.g. 4.3.x) via the
// release-merge cascade, but each branch's own family of migrations is
// self-contained (newer-family copies reproduce the older schema/data changes),
// so a cross-family upgrade is fully handled by the target-family migrations.
// Excluding the dormant older-family beans avoids double-processing.
static boolean isInRangeForTargetFamily(LtsVersion version, LtsVersion from, LtsVersion to) {
return version.sameFamily(to) && version.isInRange(from, to);
}
private void runSchemaUpdate(String version) {
Path sqlFile = Paths.get(installScripts.getDataDir(), "upgrade", "lts", version, SCHEMA_UPDATE_SQL);
if (!Files.exists(sqlFile)) {

41
application/src/main/java/org/thingsboard/server/service/install/lts/V4_4_0_0Migration.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install.lts;
import org.springframework.stereotype.Component;
import org.thingsboard.server.queue.util.TbCoreComponent;
/**
* Baseline migration for the {@code 4.4.0.0} LTS family. Registration-only: it has no programmatic
* {@link #apply()} work and exists so {@link LtsMigrationService} discovers version {@code 4.4.0.0} and runs
* its {@code data/upgrade/lts/4.4.0.0/schema_update.sql} -- the 4.4 baseline flat DDL. A directory holding a
* {@code schema_update.sql} but lacking a matching bean would be silently skipped (the runner iterates beans,
* not directories), so this bean is what makes the 4.4 baseline DDL run on a {@code 4.3.x -> 4.4} offline
* upgrade. {@code 4.4.0.0} is always in {@code (4.3.x, 4.4.y]}, so it is selected on every cross-family jump
* and correctly excluded ({@code > from}) from within-4.4 no-downtime patches.
* <p>
* PE inherits this bean by merge and appends its own 4.4 baseline DDL to the same
* {@code lts/4.4.0.0/schema_update.sql}, so there is a single {@code 4.4.0.0} bean across both editions.
*/
@Component
@TbCoreComponent
public class V4_4_0_0Migration implements LtsMigration {
@Override
public String getVersion() {
return "4.4.0.0";
}
}

8
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -31,7 +31,6 @@ import org.thingsboard.server.service.component.RuleNodeClassInfo;
import org.thingsboard.server.service.install.DatabaseSchemaSettingsService;
import org.thingsboard.server.service.install.DbUpgradeExecutorService;
import org.thingsboard.server.service.install.lts.LtsMigrationService;
import org.thingsboard.server.service.install.lts.V4_3_1_3Migration;
import org.thingsboard.server.utils.TbNodeUpgradeUtils;
import java.util.ArrayList;
@ -52,18 +51,11 @@ public class DefaultDataUpdateService implements DataUpdateService {
private final DbUpgradeExecutorService executorService;
private final DatabaseSchemaSettingsService schemaSettingsService;
private final LtsMigrationService ltsMigrationService;
private final V4_3_1_3Migration v4313Migration;
@Override
public void updateData() throws Exception {
log.info("Updating data ...");
ltsMigrationService.runDataMigrations(schemaSettingsService.getDbSchemaVersion(), schemaSettingsService.getPackageSchemaVersion());
// 4.4-release data step: the 4.3.1.3 data migration (obsolete widget-bundle cleanup) is dormant on 4.4 because
// LtsMigrationService.select() filters out non-target-family migrations and there are no 4.4-family beans, so the
// 4.3-family V4_3_1_3Migration is never selected. The 4.4 SUPPORTED_VERSIONS_FOR_UPGRADE allows upgrading from
// sources below 4.3.1.3, so we run it explicitly for every supported 4.3.x -> 4.4 upgrade. It is idempotent
// (bundle deletion checks existence first), so re-running it for a source already at 4.3.1.3 is a safe no-op.
v4313Migration.apply();
log.info("Data updated.");
}

21
application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationIntegrationTest.java

@ -189,6 +189,27 @@ public class LtsMigrationIntegrationTest extends AbstractControllerTest {
assertTrue(columnExists("calculated_field", "additional_info"));
}
@Test
public void offlineCrossFamilyUpgradeRunsInRangeOlderFamilyAndBaselineSchema() {
// A 4.3.x -> 4.4 offline upgrade must run BOTH the in-range 4.3.1.x beans and the new 4.4.0.0 baseline
// bean via the loosened selector. Drop the columns/tables they own so we can prove each one re-lands.
jdbcTemplate.execute("ALTER TABLE rule_chain DROP COLUMN IF EXISTS notes"); // owned by lts/4.4.0.0
jdbcTemplate.execute("ALTER TABLE calculated_field DROP COLUMN IF EXISTS additional_info"); // owned by 4.3.1.2
jdbcTemplate.execute("DROP TABLE IF EXISTS iot_hub_installed_item"); // created by 4.3.1.3
assertFalse(columnExists("rule_chain", "notes"));
assertFalse(columnExists("calculated_field", "additional_info"));
assertFalse(tableExists("iot_hub_installed_item"));
// Offline schema phase over the whole supported cross-family range.
ltsMigrationService.runSchemaMigrations("4.3.0.0", "4.4.0.0");
// 4.4.0.0 baseline DDL landed (this is what used to live in basic/schema_update.sql)...
assertTrue(columnExists("rule_chain", "notes"));
// ...and the in-range older-family 4.3.1.x beans ran too.
assertTrue(columnExists("calculated_field", "additional_info"));
assertTrue(tableExists("iot_hub_installed_item"));
}
@Test
public void migrationDirectoriesAndBeansStayInSyncBothWays() {
Path ltsDir = Paths.get(installScripts.getDataDir(), "upgrade", "lts");

75
application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java

@ -116,70 +116,99 @@ class LtsMigrationServiceTest {
}
@Test
void skipsMigrationsOutsideTargetFamilyOnCrossFamilyUpgrade() {
void selectsInRangeOlderFamilyBeansOnCrossFamilyUpgrade() {
List<String> applied = new ArrayList<>();
// 4.2.2.3 is carried onto a 4.3 branch by the release-merge cascade but must stay dormant:
// a cross-family 4.2 -> 4.3 upgrade is handled entirely by the 4.3-family migrations.
// A cross-family 4.3 -> 4.4 offline upgrade now runs the real in-range 4.3.1.x beans (the source has
// not passed them) alongside the new 4.4 baseline bean -- each exactly once. No 4.4-family bean
// reproduces the 4.3.1.x work anymore.
LtsMigrationService service = service(List.of(
migration("4.2.2.3", applied),
migration("4.3.1.2", applied),
migration("4.3.1.3", applied)));
migration("4.3.1.3", applied),
migration("4.4.0.0", applied)));
service.runDataMigrations("4.2.2.2", "4.3.1.3");
service.runDataMigrations("4.3.0.0", "4.4.0.0");
assertEquals(List.of("4.3.1.2", "4.3.1.3"), applied);
// 4.2.2.3 sits below the supported-source floor (4.3.0.0), so it is out of range and never selected.
assertEquals(List.of("4.3.1.2", "4.3.1.3", "4.4.0.0"), applied);
}
@Test
void applyMigrationsSkipsMigrationsOutsideTargetFamilyOnCrossFamilyUpgrade() {
void applyMigrationsRunsAndRecordsEachInRangeVersionOnCrossFamilyUpgrade() {
List<String> applied = new ArrayList<>();
// The dormant 4.2.2.3 bean must not apply or record on a cross-family 4.2 -> 4.3 upgrade.
LtsMigrationService service = service(List.of(
migration("4.2.2.3", applied),
migration("4.3.1.2", applied),
migration("4.3.1.3", applied)));
migration("4.3.1.3", applied),
migration("4.4.0.0", applied)));
service.applyMigrations("4.2.2.2", "4.3.1.3");
service.applyMigrations("4.3.0.0", "4.4.0.0");
assertEquals(List.of("4.3.1.2", "4.3.1.3"), applied);
assertEquals(List.of("4.3.1.2", "4.3.1.3", "4.4.0.0"), applied);
// The below-floor 4.2.2.3 duplicate must not apply or record.
verify(schemaSettingsService, never()).updateSchemaVersion("4.2.2.3");
verify(schemaSettingsService).updateSchemaVersion("4.3.1.2");
verify(schemaSettingsService).updateSchemaVersion("4.3.1.3");
verify(schemaSettingsService).updateSchemaVersion("4.4.0.0");
}
@Test
void runSchemaMigrationsSkipsMigrationsOutsideTargetFamilyOnCrossFamilyUpgrade() throws Exception {
void runSchemaMigrationsRunsEachInRangeSqlOnCrossFamilyUpgrade() throws Exception {
List<String> applied = new ArrayList<>();
writeSql("4.2.2.3", "SELECT 1;");
writeSql("4.3.1.2", "SELECT 2;");
writeSql("4.3.1.3", "SELECT 3;");
writeSql("4.4.0.0", "SELECT 4;");
LtsMigrationService service = service(List.of(
migration("4.2.2.3", applied),
migration("4.3.1.2", applied),
migration("4.3.1.3", applied)));
migration("4.3.1.3", applied),
migration("4.4.0.0", applied)));
service.runSchemaMigrations("4.2.2.2", "4.3.1.3");
service.runSchemaMigrations("4.3.0.0", "4.4.0.0");
// The dormant 4.2.2.3 SQL must not run; the 4.3-family SQL must.
// The below-floor 4.2.2.3 SQL must not run; every in-range bean's SQL must.
verify(jdbcTemplate, never()).execute("SELECT 1;");
verify(jdbcTemplate).execute("SELECT 2;");
verify(jdbcTemplate).execute("SELECT 3;");
verify(jdbcTemplate).execute("SELECT 4;");
}
@Test
void isInRangeForTargetFamilyPredicate() {
void isInRangePredicate() {
LtsVersion from = LtsVersion.parse("4.3.1.1");
LtsVersion to = LtsVersion.parse("4.3.1.3");
// same-family in range
assertTrue(LtsMigrationService.isInRangeForTargetFamily(LtsVersion.parse("4.3.1.2"), from, to));
// older-family bean within the numeric range but wrong family
assertFalse(LtsMigrationService.isInRangeForTargetFamily(LtsVersion.parse("4.2.2.3"), from, to));
// within-family in range
assertTrue(LtsVersion.parse("4.3.1.2").isInRange(from, to));
// the target itself (upper boundary, inclusive)
assertTrue(LtsMigrationService.isInRangeForTargetFamily(to, from, to));
assertTrue(to.isInRange(from, to));
// at from (lower boundary, exclusive)
assertFalse(LtsMigrationService.isInRangeForTargetFamily(from, from, to));
assertFalse(from.isInRange(from, to));
// below from
assertFalse(LtsMigrationService.isInRangeForTargetFamily(LtsVersion.parse("4.3.1.0"), from, to));
assertFalse(LtsVersion.parse("4.3.1.0").isInRange(from, to));
// above to
assertFalse(LtsVersion.parse("4.3.1.4").isInRange(from, to));
// Cross-family: an in-range older-family bean IS now selected (the whole point of the loosened filter),
// as is the target-family baseline; only a bean below `from` stays out of range.
LtsVersion crossFrom = LtsVersion.parse("4.3.0.0");
LtsVersion crossTo = LtsVersion.parse("4.4.0.0");
assertTrue(LtsVersion.parse("4.3.1.3").isInRange(crossFrom, crossTo));
assertTrue(crossTo.isInRange(crossFrom, crossTo));
assertFalse(LtsVersion.parse("4.2.2.3").isInRange(crossFrom, crossTo));
}
@Test
void reproductionDuplicateBeanSitsBelowSupportedSourceFloor() {
// Load-bearing invariant (see LtsMigrationService.select): the only reproduction-duplicate pair is
// 4.2.2.3 <-> 4.3.1.3 (both do the solution-template / widget-bundle work). For the loosened (from, to]
// filter to never select both in a single supported upgrade, the duplicate 4.2.2.3 must sit strictly
// below the minimum supported upgrade source. That floor is 4.3.0.0 (SUPPORTED_VERSIONS_FOR_UPGRADE).
LtsVersion duplicate = LtsVersion.parse("4.2.2.3");
LtsVersion supportedSourceFloor = LtsVersion.parse("4.3.0.0");
assertTrue(duplicate.compareTo(supportedSourceFloor) < 0);
// So on any supported cross-family upgrade (from >= floor), the duplicate is out of range.
assertFalse(duplicate.isInRange(supportedSourceFloor, LtsVersion.parse("4.4.0.0")));
}
@Test

Loading…
Cancel
Save