diff --git a/application/src/main/data/upgrade/lts/schema_update.sql b/application/src/main/data/upgrade/lts/4.2.2.3/schema_update.sql similarity index 77% rename from application/src/main/data/upgrade/lts/schema_update.sql rename to application/src/main/data/upgrade/lts/4.2.2.3/schema_update.sql index e23405e637..c1c83da44f 100644 --- a/application/src/main/data/upgrade/lts/schema_update.sql +++ b/application/src/main/data/upgrade/lts/4.2.2.3/schema_update.sql @@ -14,16 +14,6 @@ -- limitations under the License. -- --- LTS cumulative schema update file. --- All statements must be idempotent (use IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, DO $$ ... END $$ guards, etc.). --- This file is executed by SystemPatchApplier on every version increase within the LTS family. - --- CALCULATED FIELD ADDITIONAL INFO ADDITION START - -ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar; - --- CALCULATED FIELD ADDITIONAL INFO ADDITION END - -- IOT HUB INSTALLED ITEM START CREATE TABLE IF NOT EXISTS iot_hub_installed_item ( diff --git a/application/src/main/data/upgrade/lts/4.3.1.2/schema_update.sql b/application/src/main/data/upgrade/lts/4.3.1.2/schema_update.sql new file mode 100644 index 0000000000..8b53eda2bf --- /dev/null +++ b/application/src/main/data/upgrade/lts/4.3.1.2/schema_update.sql @@ -0,0 +1,21 @@ +-- +-- 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. +-- + +-- CALCULATED FIELD ADDITIONAL INFO ADDITION START + +ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar; + +-- CALCULATED FIELD ADDITIONAL INFO ADDITION END diff --git a/application/src/main/data/upgrade/lts/4.3.1.3/schema_update.sql b/application/src/main/data/upgrade/lts/4.3.1.3/schema_update.sql new file mode 100644 index 0000000000..c1c83da44f --- /dev/null +++ b/application/src/main/data/upgrade/lts/4.3.1.3/schema_update.sql @@ -0,0 +1,37 @@ +-- +-- 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. +-- + +-- IOT HUB INSTALLED ITEM START + +CREATE TABLE IF NOT EXISTS iot_hub_installed_item ( + id UUID NOT NULL PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + item_id UUID NOT NULL, + item_version_id UUID NOT NULL, + item_name VARCHAR NOT NULL, + item_type VARCHAR NOT NULL, + version VARCHAR NOT NULL, + descriptor JSONB NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_tenant_id ON iot_hub_installed_item(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_item_type ON iot_hub_installed_item(tenant_id, item_type); + +CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_item_id ON iot_hub_installed_item(tenant_id, item_id); + +-- IOT HUB INSTALLED ITEM END diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaSettingsService.java index c4930115f9..39fba7dd64 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaSettingsService.java @@ -23,6 +23,8 @@ public interface DatabaseSchemaSettingsService { void updateSchemaVersion(); + void updateSchemaVersion(String version); + String getPackageSchemaVersion(); String getDbSchemaVersion(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index 72307b2159..d71e7e81b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -75,7 +75,12 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti @Override public void updateSchemaVersion() { - jdbcTemplate.execute("UPDATE tb_schema_settings SET schema_version = " + getPackageSchemaVersionForDb()); + updateSchemaVersion(getPackageSchemaVersion()); + } + + @Override + public void updateSchemaVersion(String version) { + jdbcTemplate.execute("UPDATE tb_schema_settings SET schema_version = " + toDbVersion(version)); } @Override @@ -124,13 +129,15 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti } private long getPackageSchemaVersionForDb() { - String[] versionParts = getPackageSchemaVersion().split("\\."); - - long major = Integer.parseInt(versionParts[0]); - long minor = Integer.parseInt(versionParts[1]); - long maintenance = Integer.parseInt(versionParts[2]); - long patch = Integer.parseInt(versionParts[3]); + return toDbVersion(getPackageSchemaVersion()); + } + private long toDbVersion(String version) { + String[] versionParts = version.split("\\."); + long major = Long.parseLong(versionParts[0]); + long minor = versionParts.length > 1 ? Long.parseLong(versionParts[1]) : 0; + long maintenance = versionParts.length > 2 ? Long.parseLong(versionParts[2]) : 0; + long patch = versionParts.length > 3 ? Long.parseLong(versionParts[3]) : 0; return major * 1_000_000_000L + minor * 1_000_000L + maintenance * 1000L + patch; } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 3932aa9dcf..82efdd2cee 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -23,6 +23,7 @@ import org.springframework.jdbc.core.StatementCallback; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; +import org.thingsboard.server.service.install.lts.LtsMigrationService; import java.io.IOException; import java.io.UncheckedIOException; @@ -42,19 +43,26 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService private final InstallScripts installScripts; private final JdbcTemplate jdbcTemplate; private final TransactionTemplate transactionTemplate; + private final DatabaseSchemaSettingsService schemaSettingsService; + private final LtsMigrationService ltsMigrationService; - public SqlDatabaseUpgradeService(InstallScripts installScripts, JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) { + public SqlDatabaseUpgradeService(InstallScripts installScripts, JdbcTemplate jdbcTemplate, + PlatformTransactionManager transactionManager, + DatabaseSchemaSettingsService schemaSettingsService, + LtsMigrationService ltsMigrationService) { this.installScripts = installScripts; this.jdbcTemplate = jdbcTemplate; this.transactionTemplate = new TransactionTemplate(transactionManager); this.transactionTemplate.setTimeout((int) TimeUnit.MINUTES.toSeconds(120)); + this.schemaSettingsService = schemaSettingsService; + this.ltsMigrationService = ltsMigrationService; } @Override public void upgradeDatabase() { log.info("Updating schema..."); loadSql(getSchemaUpdateFile("basic")); - loadSql(getSchemaUpdateFile("lts")); + ltsMigrationService.runSchemaMigrations(schemaSettingsService.getDbSchemaVersion(), schemaSettingsService.getPackageSchemaVersion()); log.info("Schema updated."); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigration.java b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigration.java new file mode 100644 index 0000000000..4822022aab --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigration.java @@ -0,0 +1,30 @@ +/** + * 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; + +/** + * One LTS migration, keyed by the version it ships in (e.g. "4.2.2.3"). + * The runner runs the version's data/upgrade/lts/<version>/schema_update.sql by convention + * (if present), then calls {@link #apply()} for any programmatic work beyond SQL. + * Most migrations are SQL-only and only override {@link #getVersion()}. + */ +public interface LtsMigration { + + String getVersion(); + + default void apply() { + } +} 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 new file mode 100644 index 0000000000..a21d89cc53 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java @@ -0,0 +1,139 @@ +/** + * 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 lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; +import org.thingsboard.server.service.install.InstallScripts; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Slf4j +@Component +@TbCoreComponent +public class LtsMigrationService { + + private static final String SCHEMA_UPDATE_SQL = "schema_update.sql"; + + private final JdbcTemplate jdbcTemplate; + private final InstallScripts installScripts; + private final DatabaseSchemaSettingsService schemaSettingsService; + private final TransactionTemplate transactionTemplate; + private final List migrations; + + public LtsMigrationService(JdbcTemplate jdbcTemplate, + InstallScripts installScripts, + DatabaseSchemaSettingsService schemaSettingsService, + PlatformTransactionManager transactionManager, + List migrations) { + this.jdbcTemplate = jdbcTemplate; + this.installScripts = installScripts; + this.schemaSettingsService = schemaSettingsService; + this.transactionTemplate = new TransactionTemplate(transactionManager); + this.migrations = validateAndSort(migrations); + log.info("Discovered {} LTS migration(s): {}", this.migrations.size(), + this.migrations.stream().map(LtsMigration::getVersion).toList()); + } + + private static List validateAndSort(List migrations) { + Set seen = new HashSet<>(); + for (LtsMigration m : migrations) { + LtsVersion.parse(m.getVersion()); // fail loud on unparseable version + if (!seen.add(m.getVersion())) { + throw new IllegalStateException("Duplicate LTS migration version: " + m.getVersion()); + } + } + return migrations.stream() + .sorted(Comparator.comparing(m -> LtsVersion.parse(m.getVersion()))) + .toList(); + } + + /** No-downtime path: per migration in (from, to] run SQL, then apply(), then record the version. */ + public void applyMigrations(String fromVersion, String toVersion) { + for (LtsMigration migration : select(fromVersion, toVersion)) { + String version = migration.getVersion(); + transactionTemplate.executeWithoutResult(status -> { + runSchemaUpdate(version); + migration.apply(); + schemaSettingsService.updateSchemaVersion(version); + }); + log.info("Applied LTS migration {}", version); + } + } + + /** Offline major-upgrade schema phase: per migration in (from, to] run SQL only. No version record. */ + public void runSchemaMigrations(String fromVersion, String toVersion) { + for (LtsMigration migration : select(fromVersion, toVersion)) { + String version = migration.getVersion(); + transactionTemplate.executeWithoutResult(status -> runSchemaUpdate(version)); + log.info("Applied LTS schema migration {}", version); + } + } + + /** Offline major-upgrade data phase: per migration in (from, to] run apply() only. No SQL, no version record. */ + public void runDataMigrations(String fromVersion, String toVersion) { + for (LtsMigration migration : select(fromVersion, toVersion)) { + migration.apply(); + log.info("Applied LTS data migration {}", migration.getVersion()); + } + } + + private List select(String fromVersion, String toVersion) { + LtsVersion from = LtsVersion.parse(fromVersion); + LtsVersion to = LtsVersion.parse(toVersion); + return migrations.stream() + .filter(migration -> { + LtsVersion version = LtsVersion.parse(migration.getVersion()); + // 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. + return version.sameFamily(to) + && version.compareTo(from) > 0 + && version.compareTo(to) <= 0; + }) + .toList(); + } + + private void runSchemaUpdate(String version) { + Path sqlFile = Paths.get(installScripts.getDataDir(), "upgrade", "lts", version, SCHEMA_UPDATE_SQL); + if (!Files.exists(sqlFile)) { + log.trace("No LTS schema update file for version {} at {}", version, sqlFile); + return; + } + try { + jdbcTemplate.execute(Files.readString(sqlFile)); + log.info("Applied LTS SQL schema update from {}", sqlFile); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read LTS schema update file: " + sqlFile, e); + } + } +} 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 new file mode 100644 index 0000000000..cb55e61b3e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsVersion.java @@ -0,0 +1,61 @@ +/** + * 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; + +public record LtsVersion(int major, int minor, int maintenance, int patch) implements Comparable { + + public static LtsVersion parse(String version) { + if (version == null) { + throw new IllegalArgumentException("Version is null"); + } + String[] parts = version.split("\\."); + try { + int major = Integer.parseInt(parts[0]); + int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + int maintenance = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + int patch = parts.length > 3 ? Integer.parseInt(parts[3]) : 0; + return new LtsVersion(major, minor, maintenance, patch); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid version: " + version, e); + } + } + + public boolean sameFamily(LtsVersion other) { + return major == other.major && minor == other.minor; + } + + @Override + public int compareTo(LtsVersion o) { + int c = Integer.compare(major, o.major); + if (c != 0) { + return c; + } + c = Integer.compare(minor, o.minor); + if (c != 0) { + return c; + } + c = Integer.compare(maintenance, o.maintenance); + if (c != 0) { + return c; + } + return Integer.compare(patch, o.patch); + } + + @Override + public String toString() { + return major + "." + minor + "." + maintenance + "." + patch; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/lts/V4_2_2_3Migration.java b/application/src/main/java/org/thingsboard/server/service/install/lts/V4_2_2_3Migration.java new file mode 100644 index 0000000000..98701cf8e0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/lts/V4_2_2_3Migration.java @@ -0,0 +1,73 @@ +/** + * 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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.List; + +@Slf4j +@Component +@TbCoreComponent +@RequiredArgsConstructor +public class V4_2_2_3Migration implements LtsMigration { + + // Match on the bundle ALIAS, not the JSON filename stem. + // The "industrial_widgets" bundle shipped in a misspelled file (industial_widgets.json), + // but the alias inside it is correctly spelled. + private static final List OBSOLETE_BUNDLE_ALIASES = List.of( + "air_quality", "indoor_environment", "industrial_widgets", "outdoor_environment"); + + private final WidgetsBundleService widgetsBundleService; + private final WidgetTypeService widgetTypeService; + + @Override + public String getVersion() { + return "4.2.2.3"; + } + + @Override + public void apply() { + for (String alias : OBSOLETE_BUNDLE_ALIASES) { + deprecateTypesAndDeleteBundle(alias); + } + } + + // Marks the bundle's widget types as deprecated (keeping the type entities) and deletes ONLY the bundle entity. + private void deprecateTypesAndDeleteBundle(String alias) { + WidgetsBundle bundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, alias); + if (bundle == null) { + return; // already removed — idempotent + } + List types = widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(TenantId.SYS_TENANT_ID, bundle.getId()); + for (WidgetTypeDetails type : types) { + if (!type.isDeprecated()) { + type.setDeprecated(true); + widgetTypeService.saveWidgetType(type); + } + } + widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, bundle.getId()); + log.info("Deprecated {} widget type(s) and removed obsolete system widget bundle: {}", types.size(), alias); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/lts/V4_3_1_2Migration.java b/application/src/main/java/org/thingsboard/server/service/install/lts/V4_3_1_2Migration.java new file mode 100644 index 0000000000..cc76184c04 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/lts/V4_3_1_2Migration.java @@ -0,0 +1,29 @@ +/** + * 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; + +@Component +@TbCoreComponent +public class V4_3_1_2Migration implements LtsMigration { + + @Override + public String getVersion() { + return "4.3.1.2"; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/lts/V4_3_1_3Migration.java b/application/src/main/java/org/thingsboard/server/service/install/lts/V4_3_1_3Migration.java new file mode 100644 index 0000000000..16e03771a7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/lts/V4_3_1_3Migration.java @@ -0,0 +1,73 @@ +/** + * 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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.List; + +@Slf4j +@Component +@TbCoreComponent +@RequiredArgsConstructor +public class V4_3_1_3Migration implements LtsMigration { + + // Match on the bundle ALIAS, not the JSON filename stem. + // The "industrial_widgets" bundle shipped in a misspelled file (industial_widgets.json), + // but the alias inside it is correctly spelled. + private static final List OBSOLETE_BUNDLE_ALIASES = List.of( + "air_quality", "indoor_environment", "industrial_widgets", "outdoor_environment"); + + private final WidgetsBundleService widgetsBundleService; + private final WidgetTypeService widgetTypeService; + + @Override + public String getVersion() { + return "4.3.1.3"; + } + + @Override + public void apply() { + for (String alias : OBSOLETE_BUNDLE_ALIASES) { + deprecateTypesAndDeleteBundle(alias); + } + } + + // Marks the bundle's widget types as deprecated (keeping the type entities) and deletes ONLY the bundle entity. + private void deprecateTypesAndDeleteBundle(String alias) { + WidgetsBundle bundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, alias); + if (bundle == null) { + return; // already removed — idempotent + } + List types = widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(TenantId.SYS_TENANT_ID, bundle.getId()); + for (WidgetTypeDetails type : types) { + if (!type.isDeprecated()) { + type.setDeprecated(true); + widgetTypeService.saveWidgetType(type); + } + } + widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, bundle.getId()); + log.info("Deprecated {} widget type(s) and removed obsolete system widget bundle: {}", types.size(), alias); + } +} 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 2eb8df92fe..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 @@ -28,7 +28,9 @@ import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.service.component.ComponentDiscoveryService; 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.utils.TbNodeUpgradeUtils; import java.util.ArrayList; @@ -47,12 +49,13 @@ public class DefaultDataUpdateService implements DataUpdateService { private final RuleChainService ruleChainService; private final ComponentDiscoveryService componentDiscoveryService; private final DbUpgradeExecutorService executorService; + private final DatabaseSchemaSettingsService schemaSettingsService; + private final LtsMigrationService ltsMigrationService; @Override public void updateData() throws Exception { log.info("Updating data ..."); - //TODO: should be cleaned after each release - + ltsMigrationService.runDataMigrations(schemaSettingsService.getDbSchemaVersion(), schemaSettingsService.getPackageSchemaVersion()); log.info("Data updated."); } diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java index a256888107..80d857c296 100644 --- a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java +++ b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java @@ -35,6 +35,8 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; import org.thingsboard.server.service.install.InstallScripts; +import org.thingsboard.server.service.install.lts.LtsMigrationService; +import org.thingsboard.server.service.install.lts.LtsVersion; import org.thingsboard.server.service.install.update.DefaultDataUpdateService; import java.io.IOException; @@ -74,6 +76,7 @@ public class SystemPatchApplier { private final WidgetTypeService widgetTypeService; private final WidgetsBundleService widgetsBundleService; private final ImageService imageService; + private final LtsMigrationService ltsMigrationService; @PostConstruct private void init() { @@ -101,7 +104,9 @@ public class SystemPatchApplier { } try { - updateLtsSqlSchema(); + String dbVersion = schemaSettingsService.getDbSchemaVersion(); + String packageVersion = schemaSettingsService.getPackageSchemaVersion(); + ltsMigrationService.applyMigrations(dbVersion, packageVersion); updateSqlViews(); log.info("Updated sql database views"); @@ -129,15 +134,17 @@ public class SystemPatchApplier { log.trace("Package version: {}, DB schema version: {}", packageVersion, dbVersion); - VersionInfo packageVersionInfo = parseVersion(packageVersion); - VersionInfo dbVersionInfo = parseVersion(dbVersion); - - if (packageVersionInfo == null || dbVersionInfo == null) { + LtsVersion packageVersionInfo; + LtsVersion dbVersionInfo; + try { + packageVersionInfo = LtsVersion.parse(packageVersion); + dbVersionInfo = LtsVersion.parse(dbVersion); + } catch (IllegalArgumentException e) { log.warn("Unable to parse versions. Package: {}, DB: {}", packageVersion, dbVersion); return false; } - if (!isVersionIncreased(packageVersionInfo, dbVersionInfo)) { + if (!packageVersionInfo.sameFamily(dbVersionInfo) || packageVersionInfo.compareTo(dbVersionInfo) <= 0) { return false; } @@ -145,31 +152,6 @@ public class SystemPatchApplier { return true; } - private boolean isVersionIncreased(VersionInfo packageVersion, VersionInfo dbVersion) { - if (packageVersion.major != dbVersion.major || packageVersion.minor != dbVersion.minor) { - return false; - } - if (packageVersion.maintenance != dbVersion.maintenance) { - return packageVersion.maintenance > dbVersion.maintenance; - } - return packageVersion.patch > dbVersion.patch; - } - - private void updateLtsSqlSchema() { - Path sqlFile = Paths.get(installScripts.getDataDir(), "upgrade", "lts", "schema_update.sql"); - if (!Files.exists(sqlFile)) { - log.trace("LTS schema update file does not exist: {}", sqlFile); - return; - } - try { - String sql = Files.readString(sqlFile); - jdbcTemplate.execute(sql); - log.info("Applied LTS SQL schema update from {}", sqlFile); - } catch (IOException e) { - throw new RuntimeException("Failed to read LTS schema update file: " + sqlFile, e); - } - } - private void updateSqlViews() { try { URL schemaViewsUrl = Resources.getResource(SCHEMA_VIEWS_SQL); @@ -428,20 +410,6 @@ public class SystemPatchApplier { } } - private VersionInfo parseVersion(String version) { - try { - String[] parts = version.split("\\."); - int major = Integer.parseInt(parts[0]); - int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; - int maintenance = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; - int patch = parts.length > 3 ? Integer.parseInt(parts[3]) : 0; - return new VersionInfo(major, minor, maintenance, patch); - } catch (Exception e) { - log.error("Failed to parse version: {}", version, e); - return null; - } - } - private Stream listDir(Path dir) { try { return Files.list(dir); @@ -452,8 +420,6 @@ public class SystemPatchApplier { } } - public record VersionInfo(int major, int minor, int maintenance, int patch) {} - public record WidgetTypeStats(int created, int updated) {} private enum WidgetTypeChange { CREATED, UPDATED, UNCHANGED } diff --git a/application/src/test/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsServiceTest.java b/application/src/test/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsServiceTest.java new file mode 100644 index 0000000000..91c455c292 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsServiceTest.java @@ -0,0 +1,50 @@ +/** + * 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; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class DefaultDatabaseSchemaSettingsServiceTest { + + @Mock + private ProjectInfo projectInfo; + + @Mock + private JdbcTemplate jdbcTemplate; + + @InjectMocks + private DefaultDatabaseSchemaSettingsService service; + + @Test + void updateSchemaVersionWithExplicitVersionEncodesAsLong() { + service.updateSchemaVersion("4.2.2.3"); + verify(jdbcTemplate).execute("UPDATE tb_schema_settings SET schema_version = 4002002003"); + } + + @Test + void updateSchemaVersionWithShortVersionPadsMissingComponents() { + service.updateSchemaVersion("4.3"); + verify(jdbcTemplate).execute("UPDATE tb_schema_settings SET schema_version = 4003000000"); + } +} 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 new file mode 100644 index 0000000000..1018ec6f2b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationIntegrationTest.java @@ -0,0 +1,144 @@ +/** + * 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 com.fasterxml.jackson.databind.JsonNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; + +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class LtsMigrationIntegrationTest extends AbstractControllerTest { + + private static final long V_4_2_2_2 = 4_002_002_002L; + private static final long V_4_2_2_3 = 4_002_002_003L; + private static final String OBSOLETE_ALIAS = "air_quality"; + + @Autowired + private LtsMigrationService ltsMigrationService; + @Autowired + private WidgetsBundleService widgetsBundleService; + @Autowired + private WidgetTypeService widgetTypeService; + @Autowired + private JdbcTemplate jdbcTemplate; + + private Long originalSchemaVersion; + private WidgetsBundleId bundleId; + private WidgetTypeId widgetTypeId; + + @Before + public void setUp() { + // The test-context DB (DaoSqlTest) creates tb_schema_settings empty, so ensure the baseline + // version row a real installed DB always has exists before driving the migration. + originalSchemaVersion = jdbcTemplate.query("SELECT schema_version FROM tb_schema_settings", + rs -> rs.next() ? rs.getLong(1) : null); + if (originalSchemaVersion == null) { + jdbcTemplate.execute("INSERT INTO tb_schema_settings (schema_version, product) VALUES (" + V_4_2_2_2 + ", 'CE')"); + } + + // Seed an obsolete system widget bundle with one (non-deprecated) widget type linked to it. + WidgetsBundle bundle = new WidgetsBundle(); + bundle.setTenantId(TenantId.SYS_TENANT_ID); + bundle.setAlias(OBSOLETE_ALIAS); + bundle.setTitle("Air quality"); + bundleId = widgetsBundleService.saveWidgetsBundle(bundle).getId(); + + WidgetTypeDetails type = new WidgetTypeDetails(); + type.setTenantId(TenantId.SYS_TENANT_ID); + type.setFqn("air_quality_sample_" + UUID.randomUUID()); + type.setName("Air quality sample"); + type.setDescriptor(JacksonUtil.fromString("{ \"type\": \"latest\" }", JsonNode.class)); + WidgetTypeDetails saved = widgetTypeService.saveWidgetType(type); + widgetTypeId = saved.getId(); + widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, bundleId, List.of(saved.getFqn())); + + // Pretend the DB is at 4.2.2.2 so the 4.2.2.3 migration is in range (4.2.2.2, 4.2.2.3]. + jdbcTemplate.execute("UPDATE tb_schema_settings SET schema_version = " + V_4_2_2_2); + } + + @After + public void tearDown() { + // Restore the schema version (or drop the row we seeded) and clean up whatever the migration left behind. + if (originalSchemaVersion != null) { + jdbcTemplate.execute("UPDATE tb_schema_settings SET schema_version = " + originalSchemaVersion); + } else { + jdbcTemplate.execute("DELETE FROM tb_schema_settings"); + } + if (widgetTypeId != null && widgetTypeService.findWidgetTypeDetailsById(TenantId.SYS_TENANT_ID, widgetTypeId) != null) { + widgetTypeService.deleteWidgetType(TenantId.SYS_TENANT_ID, widgetTypeId); + } + WidgetsBundle bundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, OBSOLETE_ALIAS); + if (bundle != null) { + widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, bundle.getId()); + } + } + + @Test + public void appliesSqlDeprecatesTypesDeletesBundleAndRecordsVersion() { + ltsMigrationService.applyMigrations("4.2.2.2", "4.2.2.3"); + + // 1. The version's SQL ran: iot_hub_installed_item table now exists. + assertTrue(tableExists("iot_hub_installed_item")); + // 2. The version was recorded. + assertEquals(Long.valueOf(V_4_2_2_3), + jdbcTemplate.queryForObject("SELECT schema_version FROM tb_schema_settings", Long.class)); + // 3. The obsolete bundle entity was deleted. + assertNull(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, OBSOLETE_ALIAS)); + // 4. The widget type was KEPT and marked deprecated. + WidgetTypeDetails type = widgetTypeService.findWidgetTypeDetailsById(TenantId.SYS_TENANT_ID, widgetTypeId); + assertNotNull(type); + assertTrue(type.isDeprecated()); + } + + @Test + public void reRunFromCurrentVersionIsNoOp() { + ltsMigrationService.applyMigrations("4.2.2.2", "4.2.2.3"); + // Re-running from the now-current version selects an empty range — nothing changes, nothing throws. + ltsMigrationService.applyMigrations("4.2.2.3", "4.2.2.3"); + + assertEquals(Long.valueOf(V_4_2_2_3), + jdbcTemplate.queryForObject("SELECT schema_version FROM tb_schema_settings", Long.class)); + assertNull(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, OBSOLETE_ALIAS)); + assertTrue(widgetTypeService.findWidgetTypeDetailsById(TenantId.SYS_TENANT_ID, widgetTypeId).isDeprecated()); + } + + private boolean tableExists(String table) { + Boolean exists = jdbcTemplate.queryForObject( + "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = ?)", Boolean.class, table); + return Boolean.TRUE.equals(exists); + } +} 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 new file mode 100644 index 0000000000..f2f3e93896 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java @@ -0,0 +1,194 @@ +/** + * 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.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.SimpleTransactionStatus; +import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; +import org.thingsboard.server.service.install.InstallScripts; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class LtsMigrationServiceTest { + + private JdbcTemplate jdbcTemplate; + private InstallScripts installScripts; + private DatabaseSchemaSettingsService schemaSettingsService; + private PlatformTransactionManager txManager; + + @TempDir + Path dataDir; + + @BeforeEach + void setUp() { + jdbcTemplate = Mockito.mock(JdbcTemplate.class); + installScripts = Mockito.mock(InstallScripts.class); + schemaSettingsService = Mockito.mock(DatabaseSchemaSettingsService.class); + txManager = Mockito.mock(PlatformTransactionManager.class); + when(txManager.getTransaction(any())).thenReturn(new SimpleTransactionStatus()); + when(installScripts.getDataDir()).thenReturn(dataDir.toString()); + } + + private void writeSql(String version, String sql) throws Exception { + Path dir = dataDir.resolve("upgrade").resolve("lts").resolve(version); + Files.createDirectories(dir); + Files.writeString(dir.resolve("schema_update.sql"), sql); + } + + /** Records which apply() hooks fired, in order. */ + private LtsMigration migration(String version, List applied) { + return new LtsMigration() { + @Override public String getVersion() { return version; } + @Override public void apply() { applied.add(version); } + }; + } + + private LtsMigrationService service(List migrations) { + return new LtsMigrationService(jdbcTemplate, installScripts, schemaSettingsService, txManager, migrations); + } + + @Test + void selectsOnlyInRangeMigrationsInAscendingOrder() throws Exception { + List applied = new ArrayList<>(); + writeSql("4.2.2.3", "SELECT 1;"); + writeSql("4.2.2.4", "SELECT 2;"); + // intentionally unsorted input; service must sort ascending + LtsMigrationService service = service(List.of( + migration("4.2.2.4", applied), + migration("4.2.2.2", applied), + migration("4.2.2.3", applied))); + + service.applyMigrations("4.2.2.2", "4.2.2.3"); + + // only 4.2.2.3 is in (4.2.2.2, 4.2.2.3] + assertEquals(List.of("4.2.2.3"), applied); + verify(jdbcTemplate).execute("SELECT 1;"); + verify(jdbcTemplate, never()).execute("SELECT 2;"); + verify(schemaSettingsService).updateSchemaVersion("4.2.2.3"); + } + + @Test + void appliesAllInRangeAndRecordsEachVersion() throws Exception { + List applied = new ArrayList<>(); + writeSql("4.2.2.3", "SELECT 1;"); + writeSql("4.2.2.4", "SELECT 2;"); + LtsMigrationService service = service(List.of( + migration("4.2.2.3", applied), migration("4.2.2.4", applied))); + + service.applyMigrations("4.2.2.2", "4.2.2.4"); + + assertEquals(List.of("4.2.2.3", "4.2.2.4"), applied); + verify(jdbcTemplate).execute("SELECT 1;"); + verify(jdbcTemplate).execute("SELECT 2;"); + verify(schemaSettingsService).updateSchemaVersion("4.2.2.3"); + verify(schemaSettingsService).updateSchemaVersion("4.2.2.4"); + } + + @Test + void skipsMigrationsOutsideTargetFamilyOnCrossFamilyUpgrade() { + 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. + LtsMigrationService service = service(List.of( + migration("4.2.2.3", applied), + migration("4.3.1.2", applied), + migration("4.3.1.3", applied))); + + service.runDataMigrations("4.2.2.2", "4.3.1.3"); + + assertEquals(List.of("4.3.1.2", "4.3.1.3"), applied); + } + + @Test + void reRunAtCurrentVersionIsNoOp() throws Exception { + List applied = new ArrayList<>(); + writeSql("4.2.2.3", "SELECT 1;"); + LtsMigrationService service = service(List.of(migration("4.2.2.3", applied))); + + service.applyMigrations("4.2.2.3", "4.2.2.3"); + + assertEquals(List.of(), applied); + verify(jdbcTemplate, never()).execute(anyString()); + verify(schemaSettingsService, never()).updateSchemaVersion(anyString()); + } + + @Test + void runSchemaMigrationsRunsSqlButNeverAppliesOrRecords() throws Exception { + List applied = new ArrayList<>(); + writeSql("4.2.2.3", "SELECT 1;"); + LtsMigrationService service = service(List.of(migration("4.2.2.3", applied))); + + service.runSchemaMigrations("4.2.2.2", "4.2.2.3"); + + verify(jdbcTemplate).execute("SELECT 1;"); + assertEquals(List.of(), applied); + verify(schemaSettingsService, never()).updateSchemaVersion(anyString()); + } + + @Test + void runDataMigrationsAppliesButNeverRunsSqlOrRecords() throws Exception { + List applied = new ArrayList<>(); + writeSql("4.2.2.3", "SELECT 1;"); + LtsMigrationService service = service(List.of(migration("4.2.2.3", applied))); + + service.runDataMigrations("4.2.2.2", "4.2.2.3"); + + assertEquals(List.of("4.2.2.3"), applied); + verify(jdbcTemplate, never()).execute(anyString()); + verify(schemaSettingsService, never()).updateSchemaVersion(anyString()); + } + + @Test + void migrationWithoutSqlFileStillAppliesAndRecords() { + List applied = new ArrayList<>(); + LtsMigrationService service = service(List.of(migration("4.2.2.3", applied))); + + service.applyMigrations("4.2.2.2", "4.2.2.3"); + + assertEquals(List.of("4.2.2.3"), applied); + verify(jdbcTemplate, never()).execute(anyString()); + verify(schemaSettingsService).updateSchemaVersion("4.2.2.3"); + } + + @Test + void failsLoudOnDuplicateVersion() { + List applied = new ArrayList<>(); + assertThrows(IllegalStateException.class, () -> service(List.of( + migration("4.2.2.3", applied), migration("4.2.2.3", applied)))); + } + + @Test + void failsLoudOnUnparseableVersion() { + List applied = new ArrayList<>(); + assertThrows(IllegalArgumentException.class, () -> service(List.of(migration("nope", applied)))); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/install/lts/LtsVersionTest.java b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsVersionTest.java new file mode 100644 index 0000000000..dc5d3a2fee --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsVersionTest.java @@ -0,0 +1,78 @@ +/** + * 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.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LtsVersionTest { + + @ParameterizedTest + @CsvSource({ + "4.2.1, 4, 2, 1, 0", + "4.2.0, 4, 2, 0, 0", + "4.2, 4, 2, 0, 0", + "4.0.1.2, 4, 0, 1, 2", + "4, 4, 0, 0, 0", + "10.20.30.40, 10, 20, 30, 40" + }) + void parsesValidVersions(String input, int major, int minor, int maintenance, int patch) { + LtsVersion v = LtsVersion.parse(input); + assertEquals(major, v.major()); + assertEquals(minor, v.minor()); + assertEquals(maintenance, v.maintenance()); + assertEquals(patch, v.patch()); + } + + @ParameterizedTest + @ValueSource(strings = {"invalid", "a.b.c", "1.2.y.x", "1.x.3", ""}) + void throwsOnInvalidVersions(String input) { + assertThrows(IllegalArgumentException.class, () -> LtsVersion.parse(input)); + } + + @Test + void throwsOnNull() { + assertThrows(IllegalArgumentException.class, () -> LtsVersion.parse(null)); + } + + @Test + void comparesByEachComponent() { + assertTrue(LtsVersion.parse("4.2.2.3").compareTo(LtsVersion.parse("4.2.2.2")) > 0); + assertTrue(LtsVersion.parse("4.2.2.0").compareTo(LtsVersion.parse("4.2.1.9")) > 0); + assertTrue(LtsVersion.parse("4.3.0.0").compareTo(LtsVersion.parse("4.2.9.9")) > 0); + assertEquals(0, LtsVersion.parse("4.2.2.3").compareTo(LtsVersion.parse("4.2.2.3"))); + } + + @Test + void sameFamilyComparesMajorAndMinorOnly() { + assertTrue(LtsVersion.parse("4.2.2.3").sameFamily(LtsVersion.parse("4.2.0.0"))); + assertFalse(LtsVersion.parse("4.3.0.0").sameFamily(LtsVersion.parse("4.2.9.9"))); + assertFalse(LtsVersion.parse("5.2.0.0").sameFamily(LtsVersion.parse("4.2.0.0"))); + } + + @Test + void toStringIsFourComponentDotForm() { + assertEquals("4.2.2.3", LtsVersion.parse("4.2.2.3").toString()); + assertEquals("4.2.0.0", LtsVersion.parse("4.2").toString()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/install/lts/V4_2_2_3MigrationTest.java b/application/src/test/java/org/thingsboard/server/service/install/lts/V4_2_2_3MigrationTest.java new file mode 100644 index 0000000000..083a61c974 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/install/lts/V4_2_2_3MigrationTest.java @@ -0,0 +1,96 @@ +/** + * 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.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class V4_2_2_3MigrationTest { + + @Mock + private WidgetsBundleService widgetsBundleService; + + @Mock + private WidgetTypeService widgetTypeService; + + @InjectMocks + private V4_2_2_3Migration migration; + + @Test + void versionIs4223() { + assertEquals("4.2.2.3", migration.getVersion()); + } + + @Test + void deprecatesTypesThenDeletesBundleEntityOnly() { + WidgetsBundle bundle = new WidgetsBundle(); + bundle.setId(new WidgetsBundleId(UUID.randomUUID())); + bundle.setAlias("air_quality"); + + WidgetTypeDetails fresh = new WidgetTypeDetails(); + fresh.setId(new WidgetTypeId(UUID.randomUUID())); + fresh.setDeprecated(false); + WidgetTypeDetails already = new WidgetTypeDetails(); + already.setId(new WidgetTypeId(UUID.randomUUID())); + already.setDeprecated(true); + + when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "air_quality")).thenReturn(bundle); + when(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(TenantId.SYS_TENANT_ID, bundle.getId())).thenReturn(List.of(fresh, already)); + when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "indoor_environment")).thenReturn(null); + when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "industrial_widgets")).thenReturn(null); + when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "outdoor_environment")).thenReturn(null); + + migration.apply(); + + // only the previously non-deprecated type is re-saved, now deprecated + verify(widgetTypeService).saveWidgetType(argThat(WidgetTypeDetails::isDeprecated)); + // widget types are NOT deleted + verify(widgetTypeService, never()).deleteWidgetTypesByBundleId(any(), any()); + // only the bundle entity is removed + verify(widgetsBundleService).deleteWidgetsBundle(TenantId.SYS_TENANT_ID, bundle.getId()); + } + + @Test + void absentBundleIsNoOp() { + when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(any(), any())).thenReturn(null); + + migration.apply(); + + verify(widgetTypeService, never()).saveWidgetType(any()); + verify(widgetsBundleService, never()).deleteWidgetsBundle(any(), any()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/install/lts/V4_3_1_3MigrationTest.java b/application/src/test/java/org/thingsboard/server/service/install/lts/V4_3_1_3MigrationTest.java new file mode 100644 index 0000000000..8d5f1a0190 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/install/lts/V4_3_1_3MigrationTest.java @@ -0,0 +1,96 @@ +/** + * 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.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class V4_3_1_3MigrationTest { + + @Mock + private WidgetsBundleService widgetsBundleService; + + @Mock + private WidgetTypeService widgetTypeService; + + @InjectMocks + private V4_3_1_3Migration migration; + + @Test + void versionIs4313() { + assertEquals("4.3.1.3", migration.getVersion()); + } + + @Test + void deprecatesTypesThenDeletesBundleEntityOnly() { + WidgetsBundle bundle = new WidgetsBundle(); + bundle.setId(new WidgetsBundleId(UUID.randomUUID())); + bundle.setAlias("air_quality"); + + WidgetTypeDetails fresh = new WidgetTypeDetails(); + fresh.setId(new WidgetTypeId(UUID.randomUUID())); + fresh.setDeprecated(false); + WidgetTypeDetails already = new WidgetTypeDetails(); + already.setId(new WidgetTypeId(UUID.randomUUID())); + already.setDeprecated(true); + + when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "air_quality")).thenReturn(bundle); + when(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(TenantId.SYS_TENANT_ID, bundle.getId())).thenReturn(List.of(fresh, already)); + when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "indoor_environment")).thenReturn(null); + when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "industrial_widgets")).thenReturn(null); + when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "outdoor_environment")).thenReturn(null); + + migration.apply(); + + // only the previously non-deprecated type is re-saved, now deprecated + verify(widgetTypeService).saveWidgetType(argThat(WidgetTypeDetails::isDeprecated)); + // widget types are NOT deleted + verify(widgetTypeService, never()).deleteWidgetTypesByBundleId(any(), any()); + // only the bundle entity is removed + verify(widgetsBundleService).deleteWidgetsBundle(TenantId.SYS_TENANT_ID, bundle.getId()); + } + + @Test + void absentBundleIsNoOp() { + when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(any(), any())).thenReturn(null); + + migration.apply(); + + verify(widgetTypeService, never()).saveWidgetType(any()); + verify(widgetsBundleService, never()).deleteWidgetsBundle(any(), any()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java index f2485dc7d5..9de10e09bb 100644 --- a/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java +++ b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java @@ -21,7 +21,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -39,6 +38,7 @@ import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; import org.thingsboard.server.service.install.InstallScripts; +import org.thingsboard.server.service.install.lts.LtsMigrationService; import org.thingsboard.server.service.system.SystemPatchApplier; import java.nio.file.Files; @@ -56,7 +56,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -91,46 +90,15 @@ public class SystemPatchApplierTest { @Mock private ImageService imageService; + @Mock + private LtsMigrationService ltsMigrationService; + @InjectMocks private SystemPatchApplier reconciler; @TempDir Path tempDir; - @ParameterizedTest(name = "Parse version {0} should return major={1}, minor={2}, patch={3}") - @CsvSource({ - "4.2.1, 4, 2, 1, 0", - "4.2.0, 4, 2, 0, 0", - "4.2, 4, 2, 0, 0", - "4.0.1.2, 4, 0, 1, 2", - "4, 4, 0, 0, 0", - "1.0.5.7, 1, 0, 5, 7", - "10.20.30.40, 10, 20, 30, 40", - "0.0.1, 0, 0, 1, 0" - }) - void testParseVersion(String versionString, int expectedMajor, int expectedMinor, int expectedMaintenance, int expectedPatch) { - SystemPatchApplier.VersionInfo version = ReflectionTestUtils.invokeMethod(reconciler, "parseVersion", versionString); - - assertNotNull(version, "Version should not be null for: " + versionString); - assertEquals(expectedMajor, version.major(), "Major version mismatch"); - assertEquals(expectedMinor, version.minor(), "Minor version mismatch"); - assertEquals(expectedMaintenance, version.maintenance(), "Maintenance version mismatch"); - assertEquals(expectedPatch, version.patch(), "Patch version mismatch"); - } - - @ParameterizedTest(name = "Parse invalid version: {0}") - @CsvSource({ - "invalid", - "a.b.c", - "1.2.y.x", - "''", - "1.x.3" - }) - void testParseInvalidVersion(String invalidVersion) { - SystemPatchApplier.VersionInfo version = ReflectionTestUtils.invokeMethod(reconciler, "parseVersion", invalidVersion); - assertNull(version, "Version should be null for invalid input: " + invalidVersion); - } - @Test void whenLockIsNotAcquired_thenAcquiredIsSuccess() { when(jdbcTemplate.queryForObject(anyString(), eq(Boolean.class), anyLong())).thenReturn(true); @@ -449,78 +417,6 @@ public class SystemPatchApplierTest { verify(widgetTypeService, times(1)).saveWidgetType(any()); } - // --- isVersionIncreased tests --- - - @ParameterizedTest(name = "isVersionIncreased: {0} (package={1}, db={2}) -> {3}") - @MethodSource("provideVersionComparisonTestCases") - void testIsVersionIncreased(String testName, SystemPatchApplier.VersionInfo packageVersion, - SystemPatchApplier.VersionInfo dbVersion, boolean expected) { - Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isVersionIncreased", packageVersion, dbVersion); - assertEquals(expected, result, testName); - } - - private static Stream provideVersionComparisonTestCases() { - return Stream.of( - // Maintenance digit increases within same LTS family - Arguments.of("maintenance increased", - new SystemPatchApplier.VersionInfo(4, 3, 1, 0), - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true), - Arguments.of("maintenance increased by more than one", - new SystemPatchApplier.VersionInfo(4, 3, 3, 0), - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true), - - // Patch digit increases within same maintenance - Arguments.of("patch increased", - new SystemPatchApplier.VersionInfo(4, 3, 0, 1), - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true), - Arguments.of("patch increased by more than one", - new SystemPatchApplier.VersionInfo(4, 3, 0, 5), - new SystemPatchApplier.VersionInfo(4, 3, 0, 2), true), - - // Both maintenance and patch increased - Arguments.of("maintenance and patch both increased", - new SystemPatchApplier.VersionInfo(4, 3, 1, 1), - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true), - - // Maintenance increased, patch value is lower (irrelevant — maintenance wins) - Arguments.of("maintenance increased, patch is lower", - new SystemPatchApplier.VersionInfo(4, 3, 2, 0), - new SystemPatchApplier.VersionInfo(4, 3, 1, 5), true), - - // Same version — no increase - Arguments.of("same version", - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false), - Arguments.of("same version with non-zero parts", - new SystemPatchApplier.VersionInfo(4, 3, 1, 2), - new SystemPatchApplier.VersionInfo(4, 3, 1, 2), false), - - // Decreased versions — no increase - Arguments.of("maintenance decreased", - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), - new SystemPatchApplier.VersionInfo(4, 3, 1, 0), false), - Arguments.of("patch decreased", - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), - new SystemPatchApplier.VersionInfo(4, 3, 0, 1), false), - - // Different major — different family, skip - Arguments.of("different major", - new SystemPatchApplier.VersionInfo(5, 3, 0, 0), - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false), - Arguments.of("major decreased", - new SystemPatchApplier.VersionInfo(3, 3, 0, 0), - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false), - - // Different minor — different LTS family, skip - Arguments.of("minor increased (different LTS family)", - new SystemPatchApplier.VersionInfo(4, 4, 0, 0), - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false), - Arguments.of("minor decreased", - new SystemPatchApplier.VersionInfo(4, 2, 0, 0), - new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false) - ); - } - // --- isVersionChanged tests --- @Test @@ -563,76 +459,22 @@ public class SystemPatchApplierTest { assertFalse(result); } - // --- updateLtsSqlSchema tests --- - - @Test - void whenLtsSqlFileExists_thenExecutesSql() throws Exception { - Path dataDir = tempDir.resolve("data"); - Path ltsDir = dataDir.resolve("upgrade").resolve("lts"); - Files.createDirectories(ltsDir); - Files.writeString(ltsDir.resolve("schema_update.sql"), "ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);"); - when(installScripts.getDataDir()).thenReturn(dataDir.toString()); - - ReflectionTestUtils.invokeMethod(reconciler, "updateLtsSqlSchema"); - - verify(jdbcTemplate).execute("ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);"); - } - - @Test - void whenLtsSqlFileDoesNotExist_thenSkips() { - Path dataDir = tempDir.resolve("data"); - // Don't create the file - when(installScripts.getDataDir()).thenReturn(dataDir.toString()); - - ReflectionTestUtils.invokeMethod(reconciler, "updateLtsSqlSchema"); - - verify(jdbcTemplate, never()).execute(anyString()); - } - - @Test - void whenLtsSqlFileHasMultipleStatements_thenExecutesAll() throws Exception { - Path dataDir = tempDir.resolve("data"); - Path ltsDir = dataDir.resolve("upgrade").resolve("lts"); - Files.createDirectories(ltsDir); - String sql = "DO $$ BEGIN\n" + - " IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'test_type') THEN\n" + - " CREATE TYPE test_type AS ENUM ('A', 'B');\n" + - " END IF;\n" + - "END $$;\n" + - "ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);"; - Files.writeString(ltsDir.resolve("schema_update.sql"), sql); - when(installScripts.getDataDir()).thenReturn(dataDir.toString()); - - ReflectionTestUtils.invokeMethod(reconciler, "updateLtsSqlSchema"); - - verify(jdbcTemplate).execute(sql); - } - // --- applyPatchIfNeeded flow tests --- @Test - void whenVersionIncreased_thenAppliesLtsSqlBeforeViewsAndWidgets() throws Exception { + void whenVersionIncreased_thenAppliesMigrationsBeforeViewsAndWidgets() { when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.1.0"); when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0"); when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())).thenReturn(true); when(jdbcTemplate.queryForObject(contains("pg_advisory_unlock"), eq(Boolean.class), anyLong())).thenReturn(true); - Path dataDir = tempDir.resolve("data"); - Path ltsDir = dataDir.resolve("upgrade").resolve("lts"); - Files.createDirectories(ltsDir); - Files.writeString(ltsDir.resolve("schema_update.sql"), "SELECT 1;"); - when(installScripts.getDataDir()).thenReturn(dataDir.toString()); - - Path widgetTypesDir = tempDir.resolve("widget_types"); - Files.createDirectories(widgetTypesDir); - when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(tempDir.resolve("widget_types")); when(installScripts.getWidgetBundlesDir()).thenReturn(tempDir.resolve("widget_bundles_missing")); + when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString()); ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded"); - // LTS SQL was executed - verify(jdbcTemplate).execute("SELECT 1;"); - // Schema version was updated + verify(ltsMigrationService).applyMigrations("4.3.0.0", "4.3.1.0"); verify(schemaSettingsService).updateSchemaVersion(); } @@ -668,17 +510,16 @@ public class SystemPatchApplierTest { when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())).thenReturn(true); when(jdbcTemplate.queryForObject(contains("pg_advisory_unlock"), eq(Boolean.class), anyLong())).thenReturn(true); - Path dataDir = tempDir.resolve("data"); - when(installScripts.getDataDir()).thenReturn(dataDir.toString()); - Path widgetTypesDir = tempDir.resolve("widget_types"); Files.createDirectories(widgetTypesDir); when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); when(installScripts.getWidgetBundlesDir()).thenReturn(tempDir.resolve("widget_bundles_missing")); + when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString()); ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded"); verify(schemaSettingsService).updateSchemaVersion(); + verify(ltsMigrationService).applyMigrations("4.3.1.0", "4.3.2.0"); } @Test