From dd0037f8479e357428d0983c2d90259eb2f86800 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 13 Jul 2026 14:31:39 +0300 Subject: [PATCH 1/2] Support cross-family LTS upgrades via a family-agnostic migration selector Loosen LtsMigrationService.select() to pick every bean in the (from, to] range regardless of family, so a 4.3.x -> 4.4 offline upgrade runs the real in-range 4.3.1.x beans plus a new 4.4.0.0 baseline bean, each exactly once. This replaces the ad-hoc explicit V4_3_1_3Migration.apply() call in DefaultDataUpdateService and moves the 4.4 baseline flat DDL out of basic/schema_update.sql into V4_4_0_0Migration / lts/4.4.0.0/schema_update.sql. --- .../main/data/upgrade/basic/schema_update.sql | 14 +--- .../upgrade/lts/4.4.0.0/schema_update.sql | 25 +++++++ .../install/lts/LtsMigrationService.java | 25 ++++--- .../install/lts/V4_4_0_0Migration.java | 41 ++++++++++ .../update/DefaultDataUpdateService.java | 8 -- .../lts/LtsMigrationIntegrationTest.java | 21 ++++++ .../install/lts/LtsMigrationServiceTest.java | 75 +++++++++++++------ 7 files changed, 157 insertions(+), 52 deletions(-) create mode 100644 application/src/main/data/upgrade/lts/4.4.0.0/schema_update.sql create mode 100644 application/src/main/java/org/thingsboard/server/service/install/lts/V4_4_0_0Migration.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 2f9c0cd302..e68dde316f 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/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//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. diff --git a/application/src/main/data/upgrade/lts/4.4.0.0/schema_update.sql b/application/src/main/data/upgrade/lts/4.4.0.0/schema_update.sql new file mode 100644 index 0000000000..5c3c071853 --- /dev/null +++ b/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 diff --git a/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java index ccafa88234..d4181cdade 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java @@ -115,19 +115,24 @@ public class LtsMigrationService { LtsVersion from = LtsVersion.parse(fromVersion); LtsVersion to = LtsVersion.parse(toVersion); return migrations.stream() - .filter(vm -> isInRangeForTargetFamily(vm.version(), from, to)) + .filter(vm -> isInRange(vm.version(), 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.compareTo(from) > 0 + // 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//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. + static boolean isInRange(LtsVersion version, LtsVersion from, LtsVersion to) { + return version.compareTo(from) > 0 && version.compareTo(to) <= 0; } diff --git a/application/src/main/java/org/thingsboard/server/service/install/lts/V4_4_0_0Migration.java b/application/src/main/java/org/thingsboard/server/service/install/lts/V4_4_0_0Migration.java new file mode 100644 index 0000000000..748dc56c27 --- /dev/null +++ b/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. + *

+ * 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"; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 086d76a25c..464714fde7 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/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."); } diff --git a/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationIntegrationTest.java b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationIntegrationTest.java index b6cf3178fc..7390f5393b 100644 --- a/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationIntegrationTest.java +++ b/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"); diff --git a/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java index 0ff8e64260..af044cf35f 100644 --- a/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java @@ -116,70 +116,99 @@ class LtsMigrationServiceTest { } @Test - void skipsMigrationsOutsideTargetFamilyOnCrossFamilyUpgrade() { + void selectsInRangeOlderFamilyBeansOnCrossFamilyUpgrade() { List 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 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 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(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.2"), from, to)); // the target itself (upper boundary, inclusive) - assertTrue(LtsMigrationService.isInRangeForTargetFamily(to, from, to)); + assertTrue(LtsMigrationService.isInRange(to, from, to)); // at from (lower boundary, exclusive) - assertFalse(LtsMigrationService.isInRangeForTargetFamily(from, from, to)); + assertFalse(LtsMigrationService.isInRange(from, from, to)); // below from - assertFalse(LtsMigrationService.isInRangeForTargetFamily(LtsVersion.parse("4.3.1.0"), from, to)); + assertFalse(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.0"), from, to)); + // above to + assertFalse(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.4"), 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(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.3"), crossFrom, crossTo)); + assertTrue(LtsMigrationService.isInRange(crossTo, crossFrom, crossTo)); + assertFalse(LtsMigrationService.isInRange(LtsVersion.parse("4.2.2.3"), crossFrom, crossTo)); + } + + @Test + void reproductionDuplicateBeanSitsBelowSupportedSourceFloor() { + // Load-bearing invariant (see LtsMigrationService.isInRange): 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(LtsMigrationService.isInRange(duplicate, supportedSourceFloor, LtsVersion.parse("4.4.0.0"))); } @Test From 8ec2a949e6eaecaeb2784b04eb54d6ad0da583f6 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 13 Jul 2026 15:49:57 +0300 Subject: [PATCH 2/2] Move LTS version range check into LtsVersion --- .../install/lts/LtsMigrationService.java | 31 ++++++++----------- .../service/install/lts/LtsVersion.java | 8 +++++ .../install/lts/LtsMigrationServiceTest.java | 20 ++++++------ 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java index d4181cdade..9c5907a3fe 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java @@ -114,28 +114,23 @@ public class LtsMigrationService { private List 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//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 -> isInRange(vm.version(), from, to)) + .filter(vm -> vm.version().isInRange(from, to)) .toList(); } - // 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//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. - static boolean isInRange(LtsVersion version, LtsVersion from, LtsVersion to) { - return version.compareTo(from) > 0 - && version.compareTo(to) <= 0; - } - private void runSchemaUpdate(String version) { Path sqlFile = Paths.get(installScripts.getDataDir(), "upgrade", "lts", version, SCHEMA_UPDATE_SQL); if (!Files.exists(sqlFile)) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/lts/LtsVersion.java b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsVersion.java index cb55e61b3e..9a0fa5be34 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/lts/LtsVersion.java +++ b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsVersion.java @@ -37,6 +37,14 @@ public record LtsVersion(int major, int minor, int maintenance, int patch) imple return major == other.major && minor == other.minor; } + /** + * Half-open range check: {@code from < this <= to}. The lower bound is exclusive (a source already at + * {@code from} has applied that version) and the upper bound inclusive (the target version's own migration runs). + */ + public boolean isInRange(LtsVersion from, LtsVersion to) { + return compareTo(from) > 0 && compareTo(to) <= 0; + } + @Override public int compareTo(LtsVersion o) { int c = Integer.compare(major, o.major); diff --git a/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java index af044cf35f..8fc02c5629 100644 --- a/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java @@ -179,28 +179,28 @@ class LtsMigrationServiceTest { LtsVersion from = LtsVersion.parse("4.3.1.1"); LtsVersion to = LtsVersion.parse("4.3.1.3"); // within-family in range - assertTrue(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.2"), from, to)); + assertTrue(LtsVersion.parse("4.3.1.2").isInRange(from, to)); // the target itself (upper boundary, inclusive) - assertTrue(LtsMigrationService.isInRange(to, from, to)); + assertTrue(to.isInRange(from, to)); // at from (lower boundary, exclusive) - assertFalse(LtsMigrationService.isInRange(from, from, to)); + assertFalse(from.isInRange(from, to)); // below from - assertFalse(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.0"), from, to)); + assertFalse(LtsVersion.parse("4.3.1.0").isInRange(from, to)); // above to - assertFalse(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.4"), from, 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(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.3"), crossFrom, crossTo)); - assertTrue(LtsMigrationService.isInRange(crossTo, crossFrom, crossTo)); - assertFalse(LtsMigrationService.isInRange(LtsVersion.parse("4.2.2.3"), crossFrom, crossTo)); + 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.isInRange): the only reproduction-duplicate pair is + // 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). @@ -208,7 +208,7 @@ class LtsMigrationServiceTest { 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(LtsMigrationService.isInRange(duplicate, supportedSourceFloor, LtsVersion.parse("4.4.0.0"))); + assertFalse(duplicate.isInRange(supportedSourceFloor, LtsVersion.parse("4.4.0.0"))); } @Test