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/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/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()); + } +}