This is an automated email from the ASF dual-hosted git repository.

SvenO3 pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new 49bc094e97 feat(#4367): Warn on incompatible dataset schema changes 
affecting measurements in pipelines (#4449)
49bc094e97 is described below

commit 49bc094e9747582a6b8fe754d2c4193539a09ed6
Author: Sven Oehler <[email protected]>
AuthorDate: Fri May 8 09:23:13 2026 +0200

    feat(#4367): Warn on incompatible dataset schema changes affecting 
measurements in pipelines (#4449)
---
 .../streampipes/client/http/HttpRequest.java       |  37 ++++-
 .../dataexplorer/DataExplorerSchemaManagement.java |  22 +++
 .../DataExplorerSchemaManagementTest.java          |  18 +--
 .../model/pipeline/PipelineHealthStatus.java       |   1 +
 .../matching/PipelineVerificationHandlerV2.java    |  13 +-
 .../pipeline/CriticalMeasurementFieldChange.java   |  10 +-
 .../v2/pipeline/MeasurementChangeDetector.java     | 110 +++++++++++++++
 .../pipeline/MeasurementChangeValidationStep.java  | 119 ++++++++++++++++
 .../v2/pipeline/PipelineValidationSteps.java       |   1 +
 .../manager/matching/v2/pipeline/StorageType.java  |  12 +-
 .../pipeline/update/PipelineUpdateCoordinator.java |  21 ++-
 .../v2/pipeline/MeasurementChangeDetectorTest.java | 121 +++++++++++++++++
 .../MeasurementChangeValidationStepTest.java       | 132 ++++++++++++++++++
 .../update/PipelineUpdateCoordinatorTest.java      | 124 ++++++++++++++++-
 .../impl/datalake/DataLakeMeasureResource.java     |  17 ++-
 .../src/lib/model/gen/streampipes-model.ts         |   8 +-
 .../pipeline-details/pipeline-details.component.ts |  40 ++++++
 .../pipeline-preview-meta.component.ts             |   5 +-
 .../pipeline-overview.component.html               |   4 +-
 .../measurement-update-dialog.component.html       | 150 +++++++++++++++++++++
 .../measurement-update-dialog.component.scss       |  20 ++-
 .../measurement-update-dialog.component.ts         |  60 +++++++++
 22 files changed, 1004 insertions(+), 41 deletions(-)

diff --git 
a/streampipes-client/src/main/java/org/apache/streampipes/client/http/HttpRequest.java
 
b/streampipes-client/src/main/java/org/apache/streampipes/client/http/HttpRequest.java
index cbcc8550e5..69e324c5b6 100644
--- 
a/streampipes-client/src/main/java/org/apache/streampipes/client/http/HttpRequest.java
+++ 
b/streampipes-client/src/main/java/org/apache/streampipes/client/http/HttpRequest.java
@@ -24,7 +24,10 @@ import org.apache.streampipes.client.serializer.Serializer;
 import org.apache.streampipes.client.util.StreamPipesApiPath;
 import org.apache.streampipes.commons.exceptions.SpHttpErrorStatusCode;
 import org.apache.streampipes.commons.exceptions.SpRuntimeException;
+import org.apache.streampipes.serializers.json.JacksonSerializer;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
 import org.apache.http.Header;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
@@ -109,7 +112,7 @@ public abstract class HttpRequest<K, V, T> {
           case HttpStatus.SC_NOT_FOUND ->
               throw new SpHttpErrorStatusCode(" 404 - The requested resource 
could not be found.",
                       HttpStatus.SC_NOT_FOUND);
-          default -> throw new SpHttpErrorStatusCode(status.getStatusCode() + 
" - " + status.getReasonPhrase(),
+          default -> throw new SpHttpErrorStatusCode(makeErrorMessage(status, 
response.getEntity()),
               status.getStatusCode());
         }
       }
@@ -119,6 +122,38 @@ public abstract class HttpRequest<K, V, T> {
     }
   }
 
+  private String makeErrorMessage(StatusLine status,
+                                  HttpEntity entity) throws IOException {
+    String message;
+    if (!status.getReasonPhrase().isBlank()) {
+      message = status.getReasonPhrase();
+    } else {
+      String body = entity != null ? entityAsString(entity) : "";
+      message = extractErrorMessage(body);
+    }
+    return status.getStatusCode() + " - " + message;
+  }
+
+  private String extractErrorMessage(String responseBody) {
+    try {
+      JsonNode jsonNode = 
JacksonSerializer.getObjectMapper().readTree(responseBody);
+
+      if (jsonNode.isTextual()) {
+        return jsonNode.asText();
+      } else if (jsonNode.hasNonNull("cause")) {
+        return jsonNode.get("cause").asText();
+      } else if (jsonNode.hasNonNull("title")) {
+        return jsonNode.get("title").asText();
+      } else if (jsonNode.hasNonNull("detail")) {
+        return jsonNode.get("detail").asText();
+      }
+    } catch (JsonProcessingException e) {
+      return responseBody;
+    }
+
+    return responseBody;
+  }
+
   public void writeToFile(String fileLocation) throws SpRuntimeException {
 
     String urlString = makeUrl();
diff --git 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
 
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
index cb3ad997b9..7a2076a879 100644
--- 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
+++ 
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
@@ -19,10 +19,12 @@
 package org.apache.streampipes.dataexplorer;
 
 import org.apache.streampipes.dataexplorer.api.IDataExplorerSchemaManagement;
+import 
org.apache.streampipes.manager.matching.v2.pipeline.MeasurementChangeDetector;
 import org.apache.streampipes.manager.permission.DataLakePermissionManager;
 import org.apache.streampipes.model.datalake.DataLakeMeasure;
 import 
org.apache.streampipes.model.datalake.DataLakeMeasureSchemaUpdateStrategy;
 import org.apache.streampipes.model.schema.EventProperty;
+import org.apache.streampipes.model.schema.EventSchema;
 import org.apache.streampipes.storage.api.core.CRUDStorage;
 
 import java.util.ArrayList;
@@ -86,6 +88,7 @@ public class DataExplorerSchemaManagement implements 
IDataExplorerSchemaManageme
       DataLakeMeasure measure,
       DataLakeMeasure existingMeasure) {
     measure.setElementId(existingMeasure.getElementId());
+    checkFieldChanges(existingMeasure.getEventSchema(), 
measure.getEventSchema());
     if 
(DataLakeMeasureSchemaUpdateStrategy.UPDATE_SCHEMA.equals(measure.getSchemaUpdateStrategy()))
 {
       // For the update schema strategy the old schema is overwritten with the 
new one
       updateMeasurement(measure);
@@ -197,4 +200,23 @@ public class DataExplorerSchemaManagement implements 
IDataExplorerSchemaManageme
         .values();
     return new ArrayList<>(unifiedEventProperties);
   }
+
+  private void checkFieldChanges(EventSchema existingSchema, EventSchema 
schema) {
+    var criticalFieldChanges = 
MeasurementChangeDetector.findCriticalMeasurementFieldChanges(
+        existingSchema,
+        schema
+    );
+    if (!criticalFieldChanges.isEmpty()) {
+      throw new RuntimeException(
+          "Can't save measurement with critical field changes: " + 
criticalFieldChanges
+              .stream()
+              .map(change -> "%s (%s -> %s)".formatted(
+                  change.runtimeName(),
+                  change.existingType(),
+                  change.updatedType()
+              ))
+              .collect(Collectors.joining(", "))
+      );
+    }
+  }
 }
diff --git 
a/streampipes-data-explorer/src/test/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagementTest.java
 
b/streampipes-data-explorer/src/test/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagementTest.java
index 95ae273e62..0dbecba6f5 100644
--- 
a/streampipes-data-explorer/src/test/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagementTest.java
+++ 
b/streampipes-data-explorer/src/test/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagementTest.java
@@ -37,6 +37,7 @@ import java.util.List;
 
 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;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
@@ -145,17 +146,16 @@ public class DataExplorerSchemaManagementTest {
 
     var newMeasure = 
getNewMeasure(DataLakeMeasureSchemaUpdateStrategy.EXTEND_EXISTING_SCHEMA);
 
-    var resultMeasure = 
schemaManagement.createOrUpdateMeasurement(newMeasure,null);
-    assertEquals(newMeasure.getMeasureName(), resultMeasure.getMeasureName());
-    verify(dataLakeStorageMock, Mockito.times(1)).updateElement(any());
+    var exception = assertThrows(
+        RuntimeException.class,
+        () -> schemaManagement.createOrUpdateMeasurement(newMeasure, null)
+    );
+
     assertEquals(
-        2,
-        resultMeasure.getEventSchema()
-                     .getEventProperties()
-                     .size()
+        "Can't save measurement with critical field changes: newProperty ("
+            + XSD.INTEGER + " -> " + XSD.STRING + ")",
+        exception.getMessage()
     );
-    assertTrue(containsPropertyWithName(resultMeasure, OLD_PROPERTY));
-    assertTrue(containsPropertyWithName(resultMeasure, NEW_PROPERTY));
   }
 
   private EventProperty getEventProperty(
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
 
b/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
index 6fac07a465..b863e47ad7 100644
--- 
a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
+++ 
b/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
@@ -20,5 +20,6 @@ package org.apache.streampipes.model.pipeline;
 public enum PipelineHealthStatus {
   OK,
   REQUIRES_ATTENTION,
+  HANDLE_MEASUREMENT_UPDATE,
   FAILURE
 }
diff --git 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/PipelineVerificationHandlerV2.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/PipelineVerificationHandlerV2.java
index d87f7a8139..d57c4af51f 100644
--- 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/PipelineVerificationHandlerV2.java
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/PipelineVerificationHandlerV2.java
@@ -57,7 +57,11 @@ public class PipelineVerificationHandlerV2 {
   }
 
   public PipelineModificationResult makeModifiedPipeline() {
-    var result = verifyAndBuildGraphs(false);
+    return makeModifiedPipeline(verifyPipeline());
+  }
+
+  public PipelineModificationResult 
makeModifiedPipeline(PipelineModificationMessage modificationMessage) {
+    var result = verifyAndBuildGraphs(modificationMessage, false);
     var allElements = result.modifiedPipelineElements();
     pipeline.setSepas(filterAndConvert(allElements, 
DataProcessorInvocation.class));
     pipeline.setActions(filterAndConvert(allElements, 
DataSinkInvocation.class));
@@ -74,7 +78,12 @@ public class PipelineVerificationHandlerV2 {
   }
 
   public PipelineVerificationResult verifyAndBuildGraphs(boolean 
ignoreUnconfigured) {
-    var pipelineModifications = verifyPipeline().getPipelineModifications();
+    return verifyAndBuildGraphs(verifyPipeline(), ignoreUnconfigured);
+  }
+
+  public PipelineVerificationResult 
verifyAndBuildGraphs(PipelineModificationMessage modificationMessage,
+                                                        boolean 
ignoreUnconfigured) {
+    var pipelineModifications = modificationMessage.getPipelineModifications();
     var allElements = new AllElementsProvider(pipeline).getAllElements();
     var validationInfos = new 
ArrayList<ExtendedPipelineElementValidationInfo>();
     var modifiedPipelineElements = new ArrayList<NamedStreamPipesEntity>();
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/CriticalMeasurementFieldChange.java
similarity index 75%
copy from 
streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
copy to 
streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/CriticalMeasurementFieldChange.java
index 6fac07a465..250b55ab52 100644
--- 
a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/CriticalMeasurementFieldChange.java
@@ -15,10 +15,10 @@
  * limitations under the License.
  *
  */
-package org.apache.streampipes.model.pipeline;
 
-public enum PipelineHealthStatus {
-  OK,
-  REQUIRES_ATTENTION,
-  FAILURE
+package org.apache.streampipes.manager.matching.v2.pipeline;
+
+public record CriticalMeasurementFieldChange(String runtimeName,
+                                             String existingType,
+                                             String updatedType) {
 }
diff --git 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeDetector.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeDetector.java
new file mode 100644
index 0000000000..a7026fb7a6
--- /dev/null
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeDetector.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.manager.matching.v2.pipeline;
+
+import org.apache.streampipes.model.schema.EventProperty;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+import org.apache.streampipes.model.schema.EventSchema;
+import org.apache.streampipes.model.schema.PropertyScope;
+import org.apache.streampipes.vocabulary.SO;
+import org.apache.streampipes.vocabulary.XSD;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public final class MeasurementChangeDetector {
+
+  private MeasurementChangeDetector() {
+  }
+
+  public static List<CriticalMeasurementFieldChange> 
findCriticalMeasurementFieldChanges(
+      EventSchema existingEventSchema,
+      EventSchema updatedEventSchema) {
+    var existingMeasurementFields = existingEventSchema
+        .getEventProperties()
+        .stream()
+        .filter(MeasurementChangeDetector::isMeasurementField)
+        .collect(Collectors.toMap(
+            EventProperty::getRuntimeName,
+            Function.identity(),
+            (existingProperty, duplicateProperty) -> existingProperty
+        ));
+
+    return updatedEventSchema
+        .getEventProperties()
+        .stream()
+        .filter(MeasurementChangeDetector::isMeasurementField)
+        .map(updatedProperty -> {
+          var existingProperty = 
existingMeasurementFields.get(updatedProperty.getRuntimeName());
+          if (existingProperty != null && 
hasCriticalFieldTypeChange(existingProperty, updatedProperty)) {
+            return Optional.of(new CriticalMeasurementFieldChange(
+                updatedProperty.getRuntimeName(),
+                toDisplayType(existingProperty),
+                toDisplayType(updatedProperty)
+            ));
+          } else {
+            return Optional.<CriticalMeasurementFieldChange>empty();
+          }
+        })
+        .flatMap(Optional::stream)
+        .toList();
+  }
+
+  private static boolean isMeasurementField(EventProperty eventProperty) {
+    return 
!PropertyScope.DIMENSION_PROPERTY.name().equals(eventProperty.getPropertyScope());
+  }
+
+  private static boolean hasCriticalFieldTypeChange(EventProperty 
existingProperty,
+                                                    EventProperty 
updatedProperty) {
+    return toStorageType(existingProperty) != toStorageType(updatedProperty);
+  }
+
+  private static StorageType toStorageType(EventProperty eventProperty) {
+    if (eventProperty instanceof EventPropertyPrimitive primitiveProperty) {
+      return toPrimitiveStorageType(primitiveProperty.getRuntimeType());
+    } else {
+      return StorageType.STRING;
+    }
+  }
+
+  private static StorageType toPrimitiveStorageType(String runtimeType) {
+    if (XSD.INTEGER.toString().equals(runtimeType)
+        || XSD.LONG.toString().equals(runtimeType)) {
+      return StorageType.INTEGER;
+    } else if (XSD.FLOAT.toString().equals(runtimeType)
+        || XSD.DOUBLE.toString().equals(runtimeType)
+        || SO.NUMBER.equals(runtimeType)) {
+      return StorageType.FLOAT;
+    } else if (XSD.BOOLEAN.toString().equals(runtimeType)) {
+      return StorageType.BOOLEAN;
+    } else {
+      return StorageType.STRING;
+    }
+  }
+
+  private static String toDisplayType(EventProperty eventProperty) {
+    if (eventProperty instanceof EventPropertyPrimitive primitiveProperty) {
+      return primitiveProperty.getRuntimeType();
+    } else {
+      return eventProperty.getClass().getSimpleName();
+    }
+  }
+}
diff --git 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeValidationStep.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeValidationStep.java
new file mode 100644
index 0000000000..4d53c0d49c
--- /dev/null
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeValidationStep.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.manager.matching.v2.pipeline;
+
+import org.apache.streampipes.model.DataSinkType;
+import org.apache.streampipes.model.SpDataStream;
+import org.apache.streampipes.model.base.InvocableStreamPipesEntity;
+import org.apache.streampipes.model.base.NamedStreamPipesEntity;
+import org.apache.streampipes.model.graph.DataProcessorInvocation;
+import org.apache.streampipes.model.graph.DataSinkInvocation;
+import org.apache.streampipes.model.pipeline.PipelineElementValidationInfo;
+import org.apache.streampipes.model.schema.EventSchema;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+public class MeasurementChangeValidationStep extends 
AbstractPipelineValidationStep {
+
+  public static final String MEASUREMENT_UPDATE_REQUIRED =
+      "Measurement field storage type changed. Manual measurement update 
handling required";
+  private static final String DATA_LAKE_SINK_APP_ID = 
"org.apache.streampipes.sinks.internal.jvm.datalake";
+
+  @Override
+  public void apply(NamedStreamPipesEntity source,
+                    InvocableStreamPipesEntity target,
+                    Set<InvocableStreamPipesEntity> allTargets,
+                    List<PipelineElementValidationInfo> validationInfos) {
+    if (target instanceof DataSinkInvocation dataSink && 
isDatabaseSink(dataSink)) {
+      var criticalFieldChanges = findCriticalMeasurementFieldChanges(source, 
dataSink);
+      if (!criticalFieldChanges.isEmpty()) {
+        
validationInfos.add(PipelineElementValidationInfo.error(makeValidationMessage(criticalFieldChanges)));
+      }
+    }
+
+    if (target.getInputStreams() != null && target.getInputStreams().size() > 
1) {
+      this.visitorHistory.put(target.getDom(), 1);
+    }
+  }
+
+  private boolean isDatabaseSink(DataSinkInvocation dataSink) {
+    return DATA_LAKE_SINK_APP_ID.equals(dataSink.getAppId())
+        || 
streamOf(dataSink.getCategory()).anyMatch(DataSinkType.DATABASE.name()::equals);
+  }
+
+  private List<CriticalMeasurementFieldChange> 
findCriticalMeasurementFieldChanges(
+      NamedStreamPipesEntity source,
+      DataSinkInvocation dataSink) {
+    var existingEventSchema = getExistingDataSinkSchema(dataSink);
+    var updatedEventSchema = getUpdatedSourceSchema(source);
+
+    if (existingEventSchema.isPresent() && updatedEventSchema.isPresent()) {
+      return MeasurementChangeDetector.findCriticalMeasurementFieldChanges(
+          existingEventSchema.get(),
+          updatedEventSchema.get()
+      );
+    } else {
+      return List.of();
+    }
+  }
+
+  private String makeValidationMessage(
+      List<CriticalMeasurementFieldChange> criticalFieldChanges) {
+    return MEASUREMENT_UPDATE_REQUIRED + ": " + criticalFieldChanges
+        .stream()
+        .map(change -> "%s (%s -> %s)".formatted(
+            change.runtimeName(),
+            change.existingType(),
+            change.updatedType()
+        ))
+        .collect(Collectors.joining(", "));
+  }
+
+  private Optional<EventSchema> getExistingDataSinkSchema(DataSinkInvocation 
dataSink) {
+    if (dataSink.getInputStreams() == null || 
dataSink.getInputStreams().size() <= getIndex(dataSink)) {
+      return Optional.empty();
+    }
+
+    return 
Optional.ofNullable(dataSink.getInputStreams().get(getIndex(dataSink)).getEventSchema());
+  }
+
+  private Optional<EventSchema> getUpdatedSourceSchema(NamedStreamPipesEntity 
source) {
+    if (source instanceof SpDataStream dataStream) {
+      return Optional.ofNullable(dataStream.getEventSchema());
+    } else if (source instanceof DataProcessorInvocation dataProcessor
+        && dataProcessor.getOutputStream() != null) {
+      return 
Optional.ofNullable(dataProcessor.getOutputStream().getEventSchema());
+    } else {
+      return Optional.empty();
+    }
+  }
+
+  private <T> Stream<T> streamOf(Iterable<T> iterable) {
+    if (iterable == null) {
+      return Stream.empty();
+    }
+
+    return StreamSupport.stream(iterable.spliterator(), false);
+  }
+}
diff --git 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/PipelineValidationSteps.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/PipelineValidationSteps.java
index 495a79c418..fb8e1c1681 100644
--- 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/PipelineValidationSteps.java
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/PipelineValidationSteps.java
@@ -27,6 +27,7 @@ public class PipelineValidationSteps {
 
   public List<IPipelineValidationStep> collect(ExtensionServiceRequestManager 
requestManager) {
     return Arrays.asList(
+        new MeasurementChangeValidationStep(),
         new PrepareStep(),
         new ApplyGroundingStep(),
         new SchemaValidationStep(),
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/StorageType.java
similarity index 86%
copy from 
streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
copy to 
streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/StorageType.java
index 6fac07a465..07f10abcbf 100644
--- 
a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/v2/pipeline/StorageType.java
@@ -15,10 +15,12 @@
  * limitations under the License.
  *
  */
-package org.apache.streampipes.model.pipeline;
 
-public enum PipelineHealthStatus {
-  OK,
-  REQUIRES_ATTENTION,
-  FAILURE
+package org.apache.streampipes.manager.matching.v2.pipeline;
+
+enum StorageType {
+  INTEGER,
+  FLOAT,
+  BOOLEAN,
+  STRING
 }
diff --git 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/PipelineUpdateCoordinator.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/PipelineUpdateCoordinator.java
index 6de0a17945..228477dd2d 100644
--- 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/PipelineUpdateCoordinator.java
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/PipelineUpdateCoordinator.java
@@ -22,6 +22,7 @@ import 
org.apache.streampipes.commons.prometheus.pipelines.PipelinesStats;
 import 
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
 import org.apache.streampipes.manager.execution.PipelineExecutor;
 import org.apache.streampipes.manager.matching.PipelineVerificationHandlerV2;
+import 
org.apache.streampipes.manager.matching.v2.pipeline.MeasurementChangeValidationStep;
 import org.apache.streampipes.manager.pipeline.PipelineManager;
 import org.apache.streampipes.model.SpDataStream;
 import org.apache.streampipes.model.base.NamedStreamPipesEntity;
@@ -107,11 +108,15 @@ public class PipelineUpdateCoordinator {
         var verificationHandler = new 
PipelineVerificationHandlerV2(updatedPipeline, requestManager);
         var modificationMessage = verificationHandler.verifyPipeline();
         var updateInfo = makeUpdateInfo(modificationMessage, updatedPipeline);
-        var modifiedPipeline = 
verificationHandler.makeModifiedPipeline().pipeline();
+        var modifiedPipeline = 
verificationHandler.makeModifiedPipeline(modificationMessage).pipeline();
         var canAutoMigrate = canAutoMigrate(modificationMessage);
 
         if (!canAutoMigrate) {
-          
modifiedPipeline.setHealthStatus(PipelineHealthStatus.REQUIRES_ATTENTION);
+          modifiedPipeline.setHealthStatus(
+              requiresMeasurementUpdate(modificationMessage)
+                  ? PipelineHealthStatus.HANDLE_MEASUREMENT_UPDATE
+                  : PipelineHealthStatus.REQUIRES_ATTENTION
+          );
           PIPELINES_STATS.updatePipelineHealthState(
               modifiedPipeline.getElementId(),
               modifiedPipeline.getName(),
@@ -142,7 +147,8 @@ public class PipelineUpdateCoordinator {
       var updatedPipeline = updatePipeline(pipeline, affectedElementId, 
updatedStreamName, updatedEventSchema);
       try {
         var modificationMessage = new 
PipelineVerificationHandlerV2(updatedPipeline, requestManager).verifyPipeline();
-        updateInfos.add(makeUpdateInfo(modificationMessage, updatedPipeline));
+        var updateInfo = makeUpdateInfo(modificationMessage, updatedPipeline);
+        updateInfos.add(updateInfo);
       } catch (Exception e) {
         throw new RuntimeException(e);
       }
@@ -188,6 +194,15 @@ public class PipelineUpdateCoordinator {
             && modification.getValidationInfos().isEmpty());
   }
 
+  private boolean requiresMeasurementUpdate(PipelineModificationMessage 
modificationMessage) {
+    return modificationMessage
+        .getPipelineModifications()
+        .stream()
+        .flatMap(modification -> modification.getValidationInfos().stream())
+        .anyMatch(validationInfo -> validationInfo.getMessage() != null
+            && 
validationInfo.getMessage().startsWith(MeasurementChangeValidationStep.MEASUREMENT_UPDATE_REQUIRED));
+  }
+
   private List<String> toNotification(PipelineUpdateInfo updateInfo,
                                       String updateType) {
     var notifications = new ArrayList<String>();
diff --git 
a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeDetectorTest.java
 
b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeDetectorTest.java
new file mode 100644
index 0000000000..d5b99a2956
--- /dev/null
+++ 
b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeDetectorTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.manager.matching.v2.pipeline;
+
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+import org.apache.streampipes.model.schema.EventSchema;
+import org.apache.streampipes.model.schema.PropertyScope;
+import org.apache.streampipes.vocabulary.XSD;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MeasurementChangeDetectorTest {
+
+  @Test
+  void findCriticalMeasurementFieldChanges_ShouldReturnChangedNamesAndTypes() {
+    var existingSchema = makeSchema(
+        makeMeasurementProperty("temperature", XSD.INTEGER),
+        makeMeasurementProperty("pressure", XSD.FLOAT)
+    );
+    var updatedSchema = makeSchema(
+        makeMeasurementProperty("temperature", XSD.STRING),
+        makeMeasurementProperty("pressure", XSD.BOOLEAN)
+    );
+
+    var result = 
MeasurementChangeDetector.findCriticalMeasurementFieldChanges(existingSchema, 
updatedSchema);
+
+    assertEquals(2, result.size());
+    assertEquals("temperature", result.get(0).runtimeName());
+    assertEquals(XSD.INTEGER.toString(), result.get(0).existingType());
+    assertEquals(XSD.STRING.toString(), result.get(0).updatedType());
+    assertEquals("pressure", result.get(1).runtimeName());
+    assertEquals(XSD.FLOAT.toString(), result.get(1).existingType());
+    assertEquals(XSD.BOOLEAN.toString(), result.get(1).updatedType());
+  }
+
+  @Test
+  void findCriticalMeasurementFieldChanges_ShouldIgnoreIntegerToLongChange() {
+    var existingSchema = makeSchema(makeMeasurementProperty("temperature", 
XSD.INTEGER));
+    var updatedSchema = makeSchema(makeMeasurementProperty("temperature", 
XSD.LONG));
+
+    var result = 
MeasurementChangeDetector.findCriticalMeasurementFieldChanges(existingSchema, 
updatedSchema);
+
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void findCriticalMeasurementFieldChanges_ShouldIgnoreFloatToDoubleChange() {
+    var existingSchema = makeSchema(makeMeasurementProperty("temperature", 
XSD.FLOAT));
+    var updatedSchema = makeSchema(makeMeasurementProperty("temperature", 
XSD.DOUBLE));
+
+    var result = 
MeasurementChangeDetector.findCriticalMeasurementFieldChanges(existingSchema, 
updatedSchema);
+
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void findCriticalMeasurementFieldChanges_ShouldIgnoreAddedAndRemovedFields() 
{
+    var existingSchema = makeSchema(
+        makeMeasurementProperty("temperature", XSD.INTEGER),
+        makeMeasurementProperty("pressure", XSD.DOUBLE)
+    );
+    var updatedSchema = makeSchema(
+        makeMeasurementProperty("temperature", XSD.INTEGER),
+        makeMeasurementProperty("humidity", XSD.DOUBLE)
+    );
+
+    var result = 
MeasurementChangeDetector.findCriticalMeasurementFieldChanges(existingSchema, 
updatedSchema);
+
+    assertTrue(result.isEmpty());
+  }
+
+  @Test
+  void findCriticalMeasurementFieldChanges_ShouldIgnoreDimensionFieldChanges() 
{
+    var existingSchema = makeSchema(makeDimensionProperty("machineId", 
XSD.INTEGER));
+    var updatedSchema = makeSchema(makeDimensionProperty("machineId", 
XSD.STRING));
+
+    var result = 
MeasurementChangeDetector.findCriticalMeasurementFieldChanges(existingSchema, 
updatedSchema);
+
+    assertTrue(result.isEmpty());
+  }
+
+  private EventSchema makeSchema(EventPropertyPrimitive... properties) {
+    return new EventSchema(List.of(properties));
+  }
+
+  private EventPropertyPrimitive makeMeasurementProperty(String runtimeName,
+                                                        URI runtimeType) {
+    var property = new EventPropertyPrimitive(runtimeType.toString(), 
runtimeName, "", "");
+    property.setPropertyScope(PropertyScope.MEASUREMENT_PROPERTY.name());
+    return property;
+  }
+
+  private EventPropertyPrimitive makeDimensionProperty(String runtimeName,
+                                                      URI runtimeType) {
+    var property = new EventPropertyPrimitive(runtimeType.toString(), 
runtimeName, "", "");
+    property.setPropertyScope(PropertyScope.DIMENSION_PROPERTY.name());
+    return property;
+  }
+}
diff --git 
a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeValidationStepTest.java
 
b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeValidationStepTest.java
new file mode 100644
index 0000000000..559e209fdc
--- /dev/null
+++ 
b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/matching/v2/pipeline/MeasurementChangeValidationStepTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.manager.matching.v2.pipeline;
+
+import org.apache.streampipes.model.DataSinkType;
+import org.apache.streampipes.model.SpDataStream;
+import org.apache.streampipes.model.graph.DataSinkInvocation;
+import org.apache.streampipes.model.pipeline.PipelineElementValidationInfo;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+import org.apache.streampipes.model.schema.EventSchema;
+import org.apache.streampipes.model.schema.PropertyScope;
+import org.apache.streampipes.vocabulary.XSD;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MeasurementChangeValidationStepTest {
+
+  private static final String DATA_LAKE_SINK_APP_ID = 
"org.apache.streampipes.sinks.internal.jvm.datalake";
+
+  private final MeasurementChangeValidationStep step = new 
MeasurementChangeValidationStep();
+
+  @Test
+  void apply_ShouldAddValidationInfoForCriticalDatabaseMeasurementChange() {
+    var source = makeStream(makeSchema(makeMeasurementProperty("temperature", 
XSD.STRING)));
+    var target = 
makeDatabaseSink(makeSchema(makeMeasurementProperty("temperature", 
XSD.INTEGER)));
+    var validationInfos = new ArrayList<PipelineElementValidationInfo>();
+
+    step.apply(source, target, Set.of(target), validationInfos);
+
+    assertEquals(1, validationInfos.size());
+    assertEquals(
+        MeasurementChangeValidationStep.MEASUREMENT_UPDATE_REQUIRED
+            + ": temperature (" + XSD.INTEGER + " -> " + XSD.STRING + ")",
+        validationInfos.get(0).getMessage()
+    );
+  }
+
+  @Test
+  void apply_ShouldAddValidationInfoForDataLakeSinkMeasurementChange() {
+    var source = makeStream(makeSchema(makeMeasurementProperty("temperature", 
XSD.STRING)));
+    var target = 
makeDataLakeSink(makeSchema(makeMeasurementProperty("temperature", 
XSD.INTEGER)));
+    var validationInfos = new ArrayList<PipelineElementValidationInfo>();
+
+    step.apply(source, target, Set.of(target), validationInfos);
+
+    assertEquals(1, validationInfos.size());
+  }
+
+  @Test
+  void apply_ShouldIgnoreNonDatabaseSinks() {
+    var source = makeStream(makeSchema(makeMeasurementProperty("temperature", 
XSD.STRING)));
+    var target = makeSink(makeSchema(makeMeasurementProperty("temperature", 
XSD.INTEGER)));
+    var validationInfos = new ArrayList<PipelineElementValidationInfo>();
+
+    step.apply(source, target, Set.of(target), validationInfos);
+
+    assertTrue(validationInfos.isEmpty());
+  }
+
+  @Test
+  void apply_ShouldIgnoreNonCriticalStorageTypeChanges() {
+    var source = makeStream(makeSchema(makeMeasurementProperty("temperature", 
XSD.LONG)));
+    var target = 
makeDatabaseSink(makeSchema(makeMeasurementProperty("temperature", 
XSD.INTEGER)));
+    var validationInfos = new ArrayList<PipelineElementValidationInfo>();
+
+    step.apply(source, target, Set.of(target), validationInfos);
+
+    assertTrue(validationInfos.isEmpty());
+  }
+
+  private SpDataStream makeStream(EventSchema eventSchema) {
+    var stream = new SpDataStream();
+    stream.setEventSchema(eventSchema);
+    return stream;
+  }
+
+  private DataSinkInvocation makeDatabaseSink(EventSchema inputSchema) {
+    var sink = makeSink(inputSchema);
+    sink.setCategory(List.of(DataSinkType.DATABASE.name()));
+    return sink;
+  }
+
+  private DataSinkInvocation makeSink(EventSchema inputSchema) {
+    var inputStream = new SpDataStream();
+    inputStream.setEventSchema(inputSchema);
+
+    var sink = new DataSinkInvocation();
+    sink.setInputStreams(List.of(inputStream));
+    return sink;
+  }
+
+  private DataSinkInvocation makeDataLakeSink(EventSchema inputSchema) {
+    var sink = makeSink(inputSchema);
+    sink.setAppId(DATA_LAKE_SINK_APP_ID);
+    return sink;
+  }
+
+  private EventSchema makeSchema(EventPropertyPrimitive... properties) {
+    return new EventSchema(List.of(properties));
+  }
+
+  private EventPropertyPrimitive makeMeasurementProperty(String runtimeName,
+                                                        URI runtimeType) {
+    var property = new EventPropertyPrimitive(runtimeType.toString(), 
runtimeName, "", "");
+    property.setPropertyScope(PropertyScope.MEASUREMENT_PROPERTY.name());
+    return property;
+  }
+}
diff --git 
a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/update/PipelineUpdateCoordinatorTest.java
 
b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/update/PipelineUpdateCoordinatorTest.java
index 638a64fc24..e15511347e 100644
--- 
a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/update/PipelineUpdateCoordinatorTest.java
+++ 
b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/update/PipelineUpdateCoordinatorTest.java
@@ -21,27 +21,33 @@ package org.apache.streampipes.manager.pipeline.update;
 import 
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
 import org.apache.streampipes.manager.execution.PipelineExecutor;
 import org.apache.streampipes.manager.matching.PipelineVerificationHandlerV2;
+import 
org.apache.streampipes.manager.matching.v2.pipeline.MeasurementChangeValidationStep;
 import org.apache.streampipes.manager.pipeline.PipelineManager;
 import org.apache.streampipes.model.SpDataStream;
 import org.apache.streampipes.model.connect.adapter.AdapterDescription;
 import org.apache.streampipes.model.connect.adapter.PipelineUpdateInfo;
 import org.apache.streampipes.model.graph.DataProcessorInvocation;
+import org.apache.streampipes.model.graph.DataSinkInvocation;
 import org.apache.streampipes.model.message.PipelineModificationMessage;
 import org.apache.streampipes.model.pipeline.Pipeline;
 import org.apache.streampipes.model.pipeline.PipelineElementValidationInfo;
 import org.apache.streampipes.model.pipeline.PipelineHealthStatus;
 import org.apache.streampipes.model.pipeline.PipelineModification;
 import org.apache.streampipes.model.pipeline.PipelineModificationResult;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
 import org.apache.streampipes.model.schema.EventSchema;
+import org.apache.streampipes.model.schema.PropertyScope;
 import org.apache.streampipes.storage.api.core.INoSqlStorage;
 import org.apache.streampipes.storage.api.pipeline.IPipelineStorage;
 import org.apache.streampipes.storage.couchdb.CouchDbStorageManager;
+import org.apache.streampipes.vocabulary.XSD;
 
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.MockedConstruction;
 import org.mockito.MockedStatic;
 
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -57,6 +63,8 @@ import static org.mockito.Mockito.when;
 
 class PipelineUpdateCoordinatorTest {
 
+  private static final String DATA_LAKE_SINK_APP_ID = 
"org.apache.streampipes.sinks.internal.jvm.datalake";
+
   @Test
   void updatePipelines_ShouldRestartRunningPipelinesForDataStreamUpdates() {
     var requestManager = mock(ExtensionServiceRequestManager.class);
@@ -77,7 +85,8 @@ class PipelineUpdateCoordinatorTest {
              mockConstruction(PipelineVerificationHandlerV2.class, (mock, 
context) -> {
                verifiedPipelines.add((Pipeline) context.arguments().get(0));
                when(mock.verifyPipeline()).thenReturn(modificationMessage);
-               when(mock.makeModifiedPipeline()).thenReturn(new 
PipelineModificationResult(modifiedPipeline, List.of()));
+               when(mock.makeModifiedPipeline(modificationMessage))
+                   .thenReturn(new 
PipelineModificationResult(modifiedPipeline, List.of()));
              });
          MockedConstruction<CouchDbStorageManager> storageManagerConstruction =
              mockConstruction(CouchDbStorageManager.class, (mock, context) ->
@@ -126,7 +135,8 @@ class PipelineUpdateCoordinatorTest {
              mockConstruction(PipelineVerificationHandlerV2.class, (mock, 
context) -> {
                verifiedPipelines.add((Pipeline) context.arguments().get(0));
                when(mock.verifyPipeline()).thenReturn(modificationMessage);
-               when(mock.makeModifiedPipeline()).thenReturn(new 
PipelineModificationResult(modifiedPipeline, List.of()));
+               when(mock.makeModifiedPipeline(modificationMessage))
+                   .thenReturn(new 
PipelineModificationResult(modifiedPipeline, List.of()));
              });
          MockedConstruction<CouchDbStorageManager> storageManagerConstruction =
              mockConstruction(CouchDbStorageManager.class, (mock, context) ->
@@ -157,6 +167,56 @@ class PipelineUpdateCoordinatorTest {
     }
   }
 
+  @Test
+  void 
updatePipelines_ShouldMarkPipelineRequiringAttentionForCriticalMeasurementFieldChange()
 {
+    var requestManager = mock(ExtensionServiceRequestManager.class);
+    var coordinator = new PipelineUpdateCoordinator(requestManager);
+    var adapterDescription = makeAdapter("stream-1", "Updated adapter");
+    
adapterDescription.getDataStream().setEventSchema(makeSchema(makeMeasurementProperty("temperature",
 XSD.STRING)));
+
+    var storedPipeline = makePipeline("pipeline-1", "Pipeline", true, 
"stream-1", "Old stream");
+    
storedPipeline.getStreams().get(0).setEventSchema(makeSchema(makeMeasurementProperty("temperature",
 XSD.INTEGER)));
+    storedPipeline.setActions(List.of(makeDataLakeSink()));
+
+    var modifiedPipeline = makePipeline("pipeline-1", "Pipeline", true, 
"stream-1", "Updated adapter");
+    var measurementUpdateInfo = PipelineElementValidationInfo.info(
+        measurementUpdateRequiredMessage());
+    var modificationMessage = new 
PipelineModificationMessage(List.of(validModification("sepa-1", 
measurementUpdateInfo)));
+    var pipelineStorage = mock(IPipelineStorage.class);
+
+    try (MockedStatic<PipelineManager> pipelineManager = 
mockStatic(PipelineManager.class);
+         MockedConstruction<PipelineVerificationHandlerV2> 
verificationHandlerConstruction =
+             mockConstruction(PipelineVerificationHandlerV2.class, (mock, 
context) -> {
+               when(mock.verifyPipeline()).thenReturn(modificationMessage);
+               when(mock.makeModifiedPipeline(modificationMessage))
+                   .thenReturn(new 
PipelineModificationResult(modifiedPipeline, List.of()));
+             });
+         MockedConstruction<CouchDbStorageManager> storageManagerConstruction =
+             mockConstruction(CouchDbStorageManager.class, (mock, context) ->
+                 
when(mock.getPipelineStorageAPI()).thenReturn(pipelineStorage));
+         MockedConstruction<PipelineExecutor> executorConstruction =
+             mockConstruction(PipelineExecutor.class)) {
+
+      pipelineManager.when(() -> 
PipelineManager.getPipelinesContainingElements("stream-1"))
+          .thenReturn(List.of(storedPipeline));
+      pipelineManager.when(() -> PipelineManager.getPipeline("pipeline-1"))
+          .thenReturn(storedPipeline);
+
+      coordinator.updatePipelines(adapterDescription);
+
+      assertEquals(1, verificationHandlerConstruction.constructed().size());
+      assertEquals(1, storageManagerConstruction.constructed().size());
+
+      var pipelineCaptor = ArgumentCaptor.forClass(Pipeline.class);
+      verify(pipelineStorage).updateElement(pipelineCaptor.capture());
+      assertEquals(PipelineHealthStatus.HANDLE_MEASUREMENT_UPDATE, 
pipelineCaptor.getValue().getHealthStatus());
+      assertFalse(pipelineCaptor.getValue().isValid());
+
+      assertEquals(1, executorConstruction.constructed().size());
+      verify(executorConstruction.constructed().get(0)).stopPipeline(true);
+    }
+  }
+
   @Test
   void checkPipelineMigrations_ShouldUseUpdatedDataStreamValues() {
     var requestManager = mock(ExtensionServiceRequestManager.class);
@@ -226,6 +286,37 @@ class PipelineUpdateCoordinatorTest {
     }
   }
 
+  @Test
+  void 
checkPipelineMigrations_ShouldDisableAutoMigrationForCriticalMeasurementFieldChange()
 {
+    var requestManager = mock(ExtensionServiceRequestManager.class);
+    var coordinator = new PipelineUpdateCoordinator(requestManager);
+    var adapterDescription = makeAdapter("stream-1", "Updated adapter");
+    
adapterDescription.getDataStream().setEventSchema(makeSchema(makeMeasurementProperty("temperature",
 XSD.STRING)));
+
+    var pipeline = makePipeline("pipeline-1", "Pipeline", false, "stream-1", 
"Old stream");
+    
pipeline.getStreams().get(0).setEventSchema(makeSchema(makeMeasurementProperty("temperature",
 XSD.INTEGER)));
+    pipeline.setActions(List.of(makeDataLakeSink()));
+
+    var measurementUpdateInfo = PipelineElementValidationInfo.info(
+        measurementUpdateRequiredMessage());
+    var modificationMessage = new 
PipelineModificationMessage(List.of(validModification("sepa-1", 
measurementUpdateInfo)));
+
+    try (MockedStatic<PipelineManager> pipelineManager = 
mockStatic(PipelineManager.class);
+         MockedConstruction<PipelineVerificationHandlerV2> 
verificationHandlerConstruction =
+             mockConstruction(PipelineVerificationHandlerV2.class, (mock, 
context) ->
+                 when(mock.verifyPipeline()).thenReturn(modificationMessage))) 
{
+
+      pipelineManager.when(() -> 
PipelineManager.getPipelinesContainingElements("stream-1"))
+          .thenReturn(List.of(pipeline));
+
+      var result = coordinator.checkPipelineMigrations(adapterDescription);
+
+      assertEquals(1, verificationHandlerConstruction.constructed().size());
+      assertEquals(1, result.size());
+      assertFalse(result.get(0).isCanAutoMigrate());
+    }
+  }
+
   private SpDataStream makeDataStream(String elementId, String name) {
     var dataStream = new SpDataStream();
     dataStream.setElementId(elementId);
@@ -256,6 +347,28 @@ class PipelineUpdateCoordinatorTest {
     return pipeline;
   }
 
+  private DataSinkInvocation makeDataLakeSink() {
+    var sink = new DataSinkInvocation();
+    sink.setAppId(DATA_LAKE_SINK_APP_ID);
+    return sink;
+  }
+
+  private EventSchema makeSchema(EventPropertyPrimitive... eventProperties) {
+    return new EventSchema(List.of(eventProperties));
+  }
+
+  private EventPropertyPrimitive makeMeasurementProperty(String runtimeName,
+                                                        URI runtimeType) {
+    var property = new EventPropertyPrimitive(runtimeType.toString(), 
runtimeName, "", "");
+    property.setPropertyScope(PropertyScope.MEASUREMENT_PROPERTY.name());
+    return property;
+  }
+
+  private String measurementUpdateRequiredMessage() {
+    return MeasurementChangeValidationStep.MEASUREMENT_UPDATE_REQUIRED
+        + ": temperature (" + XSD.INTEGER + " -> " + XSD.STRING + ")";
+  }
+
   private DataProcessorInvocation makeSepa(String elementId, String name) {
     var sepa = new DataProcessorInvocation();
     sepa.setElementId(elementId);
@@ -270,6 +383,13 @@ class PipelineUpdateCoordinatorTest {
     return modification;
   }
 
+  private PipelineModification validModification(String elementId,
+                                                 PipelineElementValidationInfo 
validationInfo) {
+    var modification = validModification(elementId);
+    modification.setValidationInfos(List.of(validationInfo));
+    return modification;
+  }
+
   private PipelineModification invalidModification(String elementId,
                                                    
PipelineElementValidationInfo validationInfo) {
     var modification = new PipelineModification();
diff --git 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
index 5637d37692..28e12fc92e 100644
--- 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
@@ -20,6 +20,7 @@ package org.apache.streampipes.rest.impl.datalake;
 
 import org.apache.streampipes.dataexplorer.management.DataExplorerDispatcher;
 import org.apache.streampipes.model.datalake.DataLakeMeasure;
+import org.apache.streampipes.model.monitoring.SpLogMessage;
 
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -51,12 +52,16 @@ public class DataLakeMeasureResource extends 
AbstractDataLakeResource {
 
   @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = 
MediaType.APPLICATION_JSON_VALUE)
   @PreAuthorize("this.hasWriteAuthority()")
-  public ResponseEntity<DataLakeMeasure> addDataLake(@RequestBody 
DataLakeMeasure dataLakeMeasure) {
-    DataLakeMeasure result = 
this.dataLakeMeasureManagement.createOrUpdateMeasurement(
-        dataLakeMeasure,
-        getAuthenticatedUserSid()
-    );
-    return ok(result);
+  public ResponseEntity<?> addDataLake(@RequestBody DataLakeMeasure 
dataLakeMeasure) {
+    try {
+      DataLakeMeasure result = 
this.dataLakeMeasureManagement.createOrUpdateMeasurement(
+          dataLakeMeasure,
+          getAuthenticatedUserSid()
+      );
+      return ok(result);
+    } catch (RuntimeException e) {
+      return badRequest(SpLogMessage.from(e));
+    }
   }
 
   /**
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
index 397faf0f6b..1c09e3e189 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
@@ -4632,7 +4632,13 @@ export type OutputStrategyUnion =
     | TransformOutputStrategy
     | UserDefinedOutputStrategy;
 
-export type PipelineHealthStatus = 'OK' | 'REQUIRES_ATTENTION' | 'FAILURE';
+export type PipelineHealthStatus =
+    | 'OK'
+    | 'REQUIRES_ATTENTION'
+    | 'HANDLE_MEASUREMENT_UPDATE'
+    | 'FAILURE';
+
+export type MeasurementUpdateAction = 'edit-pipeline' | 'manage-datasets';
 
 export type PropertyScope =
     | 'HEADER_PROPERTY'
diff --git a/ui/src/app/pipeline-details/pipeline-details.component.ts 
b/ui/src/app/pipeline-details/pipeline-details.component.ts
index 7a2448ede8..eb63b3e913 100644
--- a/ui/src/app/pipeline-details/pipeline-details.component.ts
+++ b/ui/src/app/pipeline-details/pipeline-details.component.ts
@@ -20,6 +20,7 @@ import { Component, OnDestroy, OnInit, ViewChild, inject } 
from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 import { AuthService } from '../services/auth.service';
 import {
+    MeasurementUpdateAction,
     Pipeline,
     PipelineCanvasMetadata,
     PipelineCanvasMetadataService,
@@ -55,6 +56,7 @@ import { PipelineDetailsToolbarComponent } from 
'./components/pipeline-details-t
 import { PipelineDetailsExpansionPanelComponent } from 
'./components/pipeline-details-expansion-panel/pipeline-details-expansion-panel.component';
 import { TranslatePipe } from '@ngx-translate/core';
 import { PipelineOperationsService } from 
'../pipelines/services/pipeline-operations.service';
+import { MeasurementUpdateDialogComponent } from 
'../pipelines/dialog/measurement-update/measurement-update-dialog.component';
 
 @Component({
     selector: 'sp-pipeline-details-overview-component',
@@ -102,6 +104,7 @@ export class SpPipelineDetailsComponent implements OnInit, 
OnDestroy {
     currentUser$: Subscription;
     autoRefresh$: Subscription;
     private shortcutReg: ShortcutRegistration;
+    private measurementUpdateDialogOpened = false;
 
     @ViewChild('pipelinePreviewComponent')
     pipelinePreviewComponent: PipelinePreviewComponent;
@@ -171,6 +174,43 @@ export class SpPipelineDetailsComponent implements OnInit, 
OnDestroy {
             { label: this.pipeline.name },
             { label: 'Overview' },
         ]);
+        this.openMeasurementUpdateDialogIfRequired();
+    }
+
+    openMeasurementUpdateDialogIfRequired(): void {
+        if (
+            this.measurementUpdateDialogOpened ||
+            this.pipeline.healthStatus !== 'HANDLE_MEASUREMENT_UPDATE'
+        ) {
+            return;
+        }
+
+        this.measurementUpdateDialogOpened = true;
+        const dialogRef = this.dialogService.open(
+            MeasurementUpdateDialogComponent,
+            {
+                panelType: PanelType.STANDARD_PANEL,
+                title: 'Measurement update required',
+                width: '50vw',
+                data: {
+                    pipeline: this.pipeline,
+                },
+            },
+        );
+
+        dialogRef.afterClosed().subscribe(action => {
+            this.handleMeasurementUpdateAction(action);
+        });
+    }
+
+    handleMeasurementUpdateAction(action?: MeasurementUpdateAction): void {
+        if (action === 'edit-pipeline') {
+            this.pipelineOperationsService.showPipelineInEditor(
+                this.pipeline._id,
+            );
+        } else if (action === 'manage-datasets') {
+            this.router.navigate(['datasets']);
+        }
     }
 
     setupAutoRefresh(): void {
diff --git 
a/ui/src/app/pipelines/components/pipeline-feature-card/pipeline-preview-meta/pipeline-preview-meta.component.ts
 
b/ui/src/app/pipelines/components/pipeline-feature-card/pipeline-preview-meta/pipeline-preview-meta.component.ts
index 77ea3b5929..1649099ab2 100644
--- 
a/ui/src/app/pipelines/components/pipeline-feature-card/pipeline-preview-meta/pipeline-preview-meta.component.ts
+++ 
b/ui/src/app/pipelines/components/pipeline-feature-card/pipeline-preview-meta/pipeline-preview-meta.component.ts
@@ -73,7 +73,10 @@ export class PipelinePreviewMetaComponent implements OnInit {
 
         if (this.healthStatus === 'OK') {
             this.healthStatusTone = 'success';
-        } else if (this.healthStatus === 'REQUIRES_ATTENTION') {
+        } else if (
+            this.healthStatus === 'REQUIRES_ATTENTION' ||
+            this.healthStatus === 'HANDLE_MEASUREMENT_UPDATE'
+        ) {
             this.healthStatusTone = 'warning';
         } else if (this.healthStatus === 'FAILURE') {
             this.healthStatusTone = 'error';
diff --git 
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
 
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
index cb78aaa817..cee5e86ca3 100644
--- 
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
+++ 
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
@@ -51,7 +51,9 @@
                     }
                     @if (
                         pipeline.running &&
-                        pipeline.healthStatus === 'REQUIRES_ATTENTION'
+                        (pipeline.healthStatus === 'REQUIRES_ATTENTION' ||
+                            pipeline.healthStatus ===
+                                'HANDLE_MEASUREMENT_UPDATE')
                     ) {
                         <div class="light light-yellow"></div>
                     }
diff --git 
a/ui/src/app/pipelines/dialog/measurement-update/measurement-update-dialog.component.html
 
b/ui/src/app/pipelines/dialog/measurement-update/measurement-update-dialog.component.html
new file mode 100644
index 0000000000..cc9064c75f
--- /dev/null
+++ 
b/ui/src/app/pipelines/dialog/measurement-update/measurement-update-dialog.component.html
@@ -0,0 +1,150 @@
+<!--
+~ Licensed to the Apache Software Foundation (ASF) under one or more
+~ contributor license agreements.  See the NOTICE file distributed with
+~ this work for additional information regarding copyright ownership.
+~ The ASF licenses this file to You 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.
+~
+-->
+
+<div class="sp-dialog-container">
+    <div
+        class="sp-dialog-content p-md"
+        data-cy="measurement-update-dialog"
+        fxLayout="column"
+        fxLayoutGap="var(--space-lg)"
+    >
+        <div fxLayout="column" fxLayoutGap="var(--space-xs)">
+            @if (pipeline?.pipelineNotifications?.length > 0) {
+                <div
+                    class="measurement-update-notifications mt-sm"
+                    fxLayout="column"
+                    fxLayoutGap="0.75rem"
+                >
+                    @for (
+                        notification of pipeline.pipelineNotifications;
+                        track notification
+                    ) {
+                        <sp-alert-banner
+                            type="warning"
+                            title="Pipeline Notification"
+                            [description]="notification"
+                        ></sp-alert-banner>
+                    }
+                </div>
+            }
+            <p class="measurement-update-description">
+                {{
+                    'A measurement field changed to a data type that conflicts 
with existing data. This needs manual adjustments. You can handle this error by 
following one of the given instructions:'
+                        | translate
+                }}
+            </p>
+        </div>
+
+        <div
+            class="measurement-update-options"
+            fxLayout="column"
+            fxLayoutGap="0.75rem"
+        >
+            <div
+                class="measurement-update-option"
+                fxLayout="row"
+                fxLayoutAlign="start center"
+                fxLayoutGap="0.75rem"
+            >
+                <mat-icon>add_chart</mat-icon>
+                <span
+                    class="measurement-update-option-text"
+                    fxLayout="column"
+                    fxLayoutGap="0.5rem"
+                >
+                    <span class="font-bold">
+                        {{ 'Save in a new data lake' | translate }}
+                    </span>
+                    <span class="text-small">
+                        {{
+                            'Change the identifier of the "Data Lake" by 
opening the pipeline element configuration to save the data in a new "Data 
Lake".'
+                                | translate
+                        }}
+                    </span>
+                </span>
+            </div>
+            <div
+                class="measurement-update-option"
+                fxLayout="row"
+                fxLayoutAlign="start center"
+                fxLayoutGap="0.75rem"
+            >
+                <mat-icon>drive_file_rename_outline</mat-icon>
+                <span
+                    class="measurement-update-option-text"
+                    fxLayout="column"
+                    fxLayoutGap="0.5rem"
+                >
+                    <span class="font-bold">
+                        {{ 'Rename the changed fields' | translate }}
+                    </span>
+                    <span class="text-small">
+                        {{
+                            'Rename the fields with changed data types by 
using the "Field Renamer" data processor'
+                                | translate
+                        }}
+                    </span>
+                </span>
+            </div>
+            <div
+                class="measurement-update-option"
+                fxLayout="row"
+                fxLayoutAlign="start center"
+                fxLayoutGap="0.75rem"
+            >
+                <mat-icon>delete_forever</mat-icon>
+                <span
+                    class="measurement-update-option-text"
+                    fxLayout="column"
+                    fxLayoutGap="0.5rem"
+                >
+                    <span class="font-bold">
+                        {{ 'Delete historical data' | translate }}
+                    </span>
+                    <span class="text-small">
+                        {{
+                            'Truncate the data of the dataset or delete it 
completely. All historical data is lost!'
+                                | translate
+                        }}
+                    </span>
+                </span>
+            </div>
+        </div>
+    </div>
+    <mat-divider></mat-divider>
+    <div
+        class="sp-dialog-actions actions-align-right"
+        fxLayout="row"
+        fxLayoutAlign="end center"
+        fxLayoutGap="0.5rem"
+    >
+        <button mat-flat-button class="mat-basic" (click)="close()">
+            {{ 'Close' | translate }}
+        </button>
+        <button mat-flat-button color="accent" 
(click)="close('edit-pipeline')">
+            {{ 'Edit pipeline' | translate }}
+        </button>
+        <button
+            mat-flat-button
+            color="accent"
+            (click)="close('manage-datasets')"
+        >
+            {{ 'Manage datasets' | translate }}
+        </button>
+    </div>
+</div>
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
 
b/ui/src/app/pipelines/dialog/measurement-update/measurement-update-dialog.component.scss
similarity index 69%
copy from 
streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
copy to 
ui/src/app/pipelines/dialog/measurement-update/measurement-update-dialog.component.scss
index 6fac07a465..6370d9f4e0 100644
--- 
a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineHealthStatus.java
+++ 
b/ui/src/app/pipelines/dialog/measurement-update/measurement-update-dialog.component.scss
@@ -15,10 +15,20 @@
  * limitations under the License.
  *
  */
-package org.apache.streampipes.model.pipeline;
 
-public enum PipelineHealthStatus {
-  OK,
-  REQUIRES_ATTENTION,
-  FAILURE
+.measurement-update-description {
+    margin: 0;
+}
+
+.measurement-update-option {
+    height: auto;
+    min-height: calc(var(--space-xl) * 3.5);
+    padding: var(--space-md);
+    text-align: left;
+    border: 1px solid var(--color-border-subtle);
+    border-radius: var(--button-border-radius);
+}
+
+.measurement-update-option-text {
+    white-space: normal;
 }
diff --git 
a/ui/src/app/pipelines/dialog/measurement-update/measurement-update-dialog.component.ts
 
b/ui/src/app/pipelines/dialog/measurement-update/measurement-update-dialog.component.ts
new file mode 100644
index 0000000000..8201e57699
--- /dev/null
+++ 
b/ui/src/app/pipelines/dialog/measurement-update/measurement-update-dialog.component.ts
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ *
+ */
+
+import { Component, Input, inject } from '@angular/core';
+import {
+    MeasurementUpdateAction,
+    Pipeline,
+} from '@streampipes/platform-services';
+import { DialogRef, SpAlertBannerComponent } from '@streampipes/shared-ui';
+import { MatButton } from '@angular/material/button';
+import { MatDivider } from '@angular/material/divider';
+import { MatIcon } from '@angular/material/icon';
+import { TranslatePipe } from '@ngx-translate/core';
+import {
+    LayoutAlignDirective,
+    LayoutDirective,
+    LayoutGapDirective,
+} from '@ngbracket/ngx-layout/flex';
+
+@Component({
+    selector: 'sp-measurement-update-dialog',
+    templateUrl: './measurement-update-dialog.component.html',
+    styleUrls: ['./measurement-update-dialog.component.scss'],
+    imports: [
+        LayoutAlignDirective,
+        LayoutDirective,
+        LayoutGapDirective,
+        MatButton,
+        MatDivider,
+        MatIcon,
+        SpAlertBannerComponent,
+        TranslatePipe,
+    ],
+})
+export class MeasurementUpdateDialogComponent {
+    private dialogRef =
+        inject<DialogRef<MeasurementUpdateDialogComponent>>(DialogRef);
+
+    @Input()
+    pipeline: Pipeline;
+
+    close(action?: MeasurementUpdateAction): void {
+        this.dialogRef.close(action);
+    }
+}

Reply via email to