From c738080ad3004f15f8530be1c0bbbc3e9db58aaf Mon Sep 17 00:00:00 2001 From: Oliver Santana Date: Wed, 14 Oct 2026 23:39:19 +0100 Subject: [PATCH 1/2] fix: sql test parallelism --- community/flamingock-sql-auditstore/build.gradle.kts | 4 ++-- .../store/sql/SqlAuditCompactionConformanceTest.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/community/flamingock-sql-auditstore/build.gradle.kts b/community/flamingock-sql-auditstore/build.gradle.kts index de887193b..de6a18f43 100644 --- a/community/flamingock-sql-auditstore/build.gradle.kts +++ b/community/flamingock-sql-auditstore/build.gradle.kts @@ -52,8 +52,8 @@ tasks.test { systemProperty("sql.test.dialects", enabledDialects.get()) - // Parallel execution control - maxParallelForks = if (isCI) 1 else (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) + // SQL integration tests share container-backed databases and fixed table names across test classes. + maxParallelForks = 1 // Timeout for long-running database tests if (isCI) { diff --git a/community/flamingock-sql-auditstore/src/test/java/io/flamingock/store/sql/SqlAuditCompactionConformanceTest.java b/community/flamingock-sql-auditstore/src/test/java/io/flamingock/store/sql/SqlAuditCompactionConformanceTest.java index ed17a607e..91e081ceb 100644 --- a/community/flamingock-sql-auditstore/src/test/java/io/flamingock/store/sql/SqlAuditCompactionConformanceTest.java +++ b/community/flamingock-sql-auditstore/src/test/java/io/flamingock/store/sql/SqlAuditCompactionConformanceTest.java @@ -232,8 +232,9 @@ private TestContext setupTest(SqlDialect sqlDialect, String dialectName) throws try { dataSource = SqlAuditTestHelper.createDataSource(container); testContext = new TestContext(dataSource, container, sqlDialect); + SqlAuditTestHelper.createTables(dataSource, sqlDialect); return testContext; - } catch (RuntimeException exception) { + } catch (SQLException | RuntimeException exception) { TestContext cleanupContext = testContext != null ? testContext : new TestContext(dataSource, container, sqlDialect); From 72232d94e8dac3744c3f9c8bb85ac49e82de9f5b Mon Sep 17 00:00:00 2001 From: Oliver Santana Date: Mon, 19 Oct 2026 23:09:03 +0100 Subject: [PATCH 2/2] feat(core): add stage-level runAlways support - Add StageMode.STANDARD and StageMode.RUN_ALWAYS to native stages and YAML pipelines. - Derive runAlways for native code and template changes from the stage mode. - Preserve Mongock ChangeUnit and ChangeSet runAlways metadata in the LEGACY stage. - Reapply the stage policy after metadata merge and support changes from nested packages. - Update Community and Cloud planning so runAlways changes are executed again after prior application. - Reject user-defined StageType values in YAML and keep SYSTEM and LEGACY stages internal. - Add planner, processor, metadata, Cloud, and legacy compatibility tests. --- build.gradle.kts | 2 +- cloud/flamingock-cloud-api/build.gradle.kts | 1 + .../cloud/api/request/ChangeRequest.java | 31 ++-- ...entSubmissionRequestSerializationTest.java | 39 +++-- .../planner/CloudExecutionPlanMapper.java | 8 +- .../planner/CloudExecutionPlanMapperTest.java | 93 +++++++++-- .../planner/CloudExecutionPlannerTest.java | 41 ++--- .../store/couchbase/PipelineTestHelper.java | 2 + .../store/dynamodb/PipelineTestHelper.java | 2 + .../common/core/metadata/MetadataLoader.java | 2 +- .../common/core/preview/PreviewStage.java | 31 +++- .../core/preview/SystemPreviewStage.java | 3 +- .../builder/CodePreviewChangeBuilder.java | 2 +- .../common/core/preview/PreviewStageTest.java | 91 +++++++++++ .../TemplatePreviewChangeBuilderTest.java | 16 ++ .../loaded/CodeLoadedChangeBuilder.java | 1 + .../loaded/stage/AbstractLoadedStage.java | 29 ++-- .../loaded/stage/DefaultLoadedStage.java | 17 +- .../loaded/stage/LegacyLoadedStage.java | 8 +- .../loaded/stage/SystemLoadedStage.java | 8 +- .../CommunityChangeActionBuilder.java | 9 ++ .../community/CommunityExecutionPlanner.java | 3 + .../loaded/stage/DefaultLoadedStageTest.java | 98 ++++++++++++ .../run/PipelineRunOfLoadedPipelineTest.java | 3 +- .../CommunityChangeActionBuilderTest.java | 104 ++++++++++++ .../CommunityExecutionPlannerTest.java | 26 +++ .../FlamingockAnnotationProcessor.java | 5 +- .../util/FlamingockMetadataMerger.java | 25 ++- .../util/PipelineStructureBuilder.java | 33 ++-- .../processor/MultiModuleProcessingIT.java | 2 + .../processor/PipelinePreProcessorTest.java | 118 +++++++------- .../util/FlamingockMetadataMergerTest.java | 42 ++++- .../util/FlamingockMetadataStoreTest.java | 29 +++- .../couchbase/PipelineTestHelper.java | 2 + .../dynamodb/PipelineTestHelper.java | 2 + .../mongodb/reactive/PipelineTestHelper.java | 2 + .../reactive/PipelineTestHelper.java | 2 + .../springdata/PipelineTestHelper.java | 2 + .../mongodb/sync/PipelineTestHelper.java | 2 + .../targetsystem/sql/PipelineTestHelper.java | 2 + .../MongockCodePreviewChangeHelper.java | 10 -- .../MongockCodePreviewChangeHelperTest.java | 150 ++++++++++++++++++ .../cloud/deprecated/MockRunnerServerOld.java | 4 +- .../test/cloud/prototype/PrototypeChange.java | 4 +- .../test/pipeline/PipelineTestHelper.java | 2 + 45 files changed, 926 insertions(+), 182 deletions(-) create mode 100644 core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/PreviewStageTest.java create mode 100644 core/flamingock-core/src/test/java/io/flamingock/internal/core/pipeline/loaded/stage/DefaultLoadedStageTest.java create mode 100644 core/flamingock-core/src/test/java/io/flamingock/internal/core/plan/community/CommunityChangeActionBuilderTest.java create mode 100644 legacy/mongock-support/src/test/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelperTest.java diff --git a/build.gradle.kts b/build.gradle.kts index f5e411b56..36cff7081 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,7 @@ allprojects { extra["generalUtilVersion"] = "1.6.0" extra["templateApiVersion"] = "1.3.4" - extra["coreApiVersion"] = "1.3.3" + extra["coreApiVersion"] = "1.4.0-SNAPSHOT" extra["sqlVersion"] = "1.3.2" extra["mongodbTemplateVersion"] = "1.3.2" diff --git a/cloud/flamingock-cloud-api/build.gradle.kts b/cloud/flamingock-cloud-api/build.gradle.kts index 6770e4e7c..16297fc50 100644 --- a/cloud/flamingock-cloud-api/build.gradle.kts +++ b/cloud/flamingock-cloud-api/build.gradle.kts @@ -7,6 +7,7 @@ val coreApiVersion: String by extra val jacksonVersion = "2.14.1" dependencies { api("io.flamingock:flamingock-core-api:${coreApiVersion}") + implementation("com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}") testImplementation("com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}") } diff --git a/cloud/flamingock-cloud-api/src/main/java/io/flamingock/cloud/api/request/ChangeRequest.java b/cloud/flamingock-cloud-api/src/main/java/io/flamingock/cloud/api/request/ChangeRequest.java index 95bb30ccc..33f939304 100644 --- a/cloud/flamingock-cloud-api/src/main/java/io/flamingock/cloud/api/request/ChangeRequest.java +++ b/cloud/flamingock-cloud-api/src/main/java/io/flamingock/cloud/api/request/ChangeRequest.java @@ -44,35 +44,33 @@ public class ChangeRequest { private boolean transactional; + private boolean runAlways; + public ChangeRequest() { } public static ChangeRequest change(String id, boolean transactional) { - return new ChangeRequest(id, CloudTargetSystemAuditMarkType.NONE, null, transactional); + return new ChangeRequest(id, CloudTargetSystemAuditMarkType.NONE, null, transactional, false); } public static ChangeRequest ongoingExecution(String id, boolean transactional) { - return new ChangeRequest(id, CloudTargetSystemAuditMarkType.APPLIED, null, transactional); + return new ChangeRequest(id, CloudTargetSystemAuditMarkType.APPLIED, null, transactional, false); } public static ChangeRequest ongoingRollback(String id, boolean transactional) { - return new ChangeRequest(id, CloudTargetSystemAuditMarkType.ROLLED_BACK, null, transactional); - } - - public ChangeRequest(String id, - CloudTargetSystemAuditMarkType ongoingStatus, - boolean transactional) { - this(id, ongoingStatus, null, transactional); + return new ChangeRequest(id, CloudTargetSystemAuditMarkType.ROLLED_BACK, null, transactional, false); } public ChangeRequest(String id, CloudTargetSystemAuditMarkType ongoingStatus, CloudChangeStatus currentStatus, - boolean transactional) { + boolean transactional, + boolean runAlways) { this.id = id; this.ongoingStatus = ongoingStatus; this.currentStatus = currentStatus; this.transactional = transactional; + this.runAlways = runAlways; } public String getId() { @@ -91,6 +89,10 @@ public boolean isTransactional() { return transactional; } + public boolean isRunAlways() { + return runAlways; + } + public void setId(String id) { this.id = id; } @@ -107,12 +109,17 @@ public void setTransactional(boolean transactional) { this.transactional = transactional; } + public void setRunAlways(boolean runAlways) { + this.runAlways = runAlways; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ChangeRequest that = (ChangeRequest) o; return transactional == that.transactional + && runAlways == that.runAlways && java.util.Objects.equals(id, that.id) && java.util.Objects.equals(ongoingStatus, that.ongoingStatus) && java.util.Objects.equals(currentStatus, that.currentStatus); @@ -120,6 +127,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return java.util.Objects.hash(id, ongoingStatus, currentStatus, transactional); + return java.util.Objects.hash(id, ongoingStatus, currentStatus, transactional, runAlways); } -} \ No newline at end of file +} diff --git a/cloud/flamingock-cloud-api/src/test/java/io/flamingock/cloud/api/request/ClientSubmissionRequestSerializationTest.java b/cloud/flamingock-cloud-api/src/test/java/io/flamingock/cloud/api/request/ClientSubmissionRequestSerializationTest.java index 672cdfa0d..4de2d9ee5 100644 --- a/cloud/flamingock-cloud-api/src/test/java/io/flamingock/cloud/api/request/ClientSubmissionRequestSerializationTest.java +++ b/cloud/flamingock-cloud-api/src/test/java/io/flamingock/cloud/api/request/ClientSubmissionRequestSerializationTest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.flamingock.api.StageType; +import io.flamingock.cloud.api.vo.CloudChangeStatus; import io.flamingock.cloud.api.vo.CloudStageStatus; import io.flamingock.cloud.api.vo.CloudTargetSystemAuditMarkType; import org.junit.jupiter.api.DisplayName; @@ -49,12 +50,10 @@ class ClientSubmissionRequestSerializationTest { void serializesBlocksWithTypeAndStages() throws Exception { StageRequest sysStage = new StageRequest( "system-stage", 0, CloudStageStatus.NOT_STARTED, - Collections.singletonList(new ChangeRequest("sys-c1", - CloudTargetSystemAuditMarkType.NONE, false))); + Collections.singletonList(new ChangeRequest("sys-c1", CloudTargetSystemAuditMarkType.NONE, null, false, false))); StageRequest userStage = new StageRequest( "user-stage", 1, CloudStageStatus.NOT_STARTED, - Collections.singletonList(new ChangeRequest("user-c1", - CloudTargetSystemAuditMarkType.NONE, false))); + Collections.singletonList(new ChangeRequest("user-c1", CloudTargetSystemAuditMarkType.NONE, null, false, false))); ClientSubmissionRequest request = new ClientSubmissionRequest(Arrays.asList( new StageBlockRequest(StageType.SYSTEM, Collections.singletonList(sysStage)), @@ -82,15 +81,12 @@ void roundTripsPreservingBlockOrderAndContents() throws Exception { ClientSubmissionRequest original = new ClientSubmissionRequest(Arrays.asList( new StageBlockRequest(StageType.LEGACY, Collections.singletonList( new StageRequest("legacy", 0, CloudStageStatus.COMPLETED, - Collections.singletonList(new ChangeRequest("legacy-c1", - CloudTargetSystemAuditMarkType.APPLIED, true))))), + Collections.singletonList(new ChangeRequest("legacy-c1", CloudTargetSystemAuditMarkType.APPLIED, null, true, false))))), new StageBlockRequest(StageType.DEFAULT, Arrays.asList( new StageRequest("user-a", 1, CloudStageStatus.STARTED, - Collections.singletonList(new ChangeRequest("user-a-c1", - CloudTargetSystemAuditMarkType.NONE, false))), + Collections.singletonList(new ChangeRequest("user-a-c1", CloudTargetSystemAuditMarkType.NONE, null, false, false))), new StageRequest("user-b", 2, CloudStageStatus.NOT_STARTED, - Collections.singletonList(new ChangeRequest("user-b-c1", - CloudTargetSystemAuditMarkType.NONE, false))))))); + Collections.singletonList(new ChangeRequest("user-b-c1", CloudTargetSystemAuditMarkType.NONE, null, false, false))))))); String json = MAPPER.writeValueAsString(original); ClientSubmissionRequest deserialized = MAPPER.readValue(json, ClientSubmissionRequest.class); @@ -109,11 +105,9 @@ void roundTripsPreservingBlockOrderAndContents() throws Exception { @DisplayName("Same StageType repeated across multiple blocks is preserved on the wire (multi-block-same-type lock-in)") void sameStageTypeAcrossMultipleBlocksIsPreserved() throws Exception { StageRequest a = new StageRequest("user-a", 0, CloudStageStatus.NOT_STARTED, - Collections.singletonList(new ChangeRequest("a-c1", - CloudTargetSystemAuditMarkType.NONE, false))); + Collections.singletonList(new ChangeRequest("a-c1", CloudTargetSystemAuditMarkType.NONE, null, false, false))); StageRequest b = new StageRequest("user-b", 1, CloudStageStatus.NOT_STARTED, - Collections.singletonList(new ChangeRequest("b-c1", - CloudTargetSystemAuditMarkType.NONE, false))); + Collections.singletonList(new ChangeRequest("b-c1", CloudTargetSystemAuditMarkType.NONE, null, false, false))); ClientSubmissionRequest request = new ClientSubmissionRequest(Arrays.asList( new StageBlockRequest(StageType.DEFAULT, Collections.singletonList(a)), @@ -142,6 +136,23 @@ void emptyBlocksListRoundTrips() throws Exception { assertTrue(deserialized.getBlocks().isEmpty()); } + @Test + @DisplayName("ChangeRequest round-trips runAlways flag") + void changeRequestRoundTripsRunAlwaysFlag() throws Exception { + ChangeRequest change = new ChangeRequest( + "repeatable-c1", + CloudTargetSystemAuditMarkType.APPLIED, + CloudChangeStatus.ALREADY_APPLIED, + false, + true); + + String json = MAPPER.writeValueAsString(change); + ChangeRequest deserialized = MAPPER.readValue(json, ChangeRequest.class); + + assertTrue(deserialized.isRunAlways()); + assertEquals(change, deserialized); + } + @Test @DisplayName("Old flat 'stages' field at top level is silently ignored (clean cut — no fallback)") void oldFlatStagesFieldIsIgnored() throws Exception { diff --git a/cloud/flamingock-cloud/src/main/java/io/flamingock/cloud/planner/CloudExecutionPlanMapper.java b/cloud/flamingock-cloud/src/main/java/io/flamingock/cloud/planner/CloudExecutionPlanMapper.java index 0c42139be..2934f8e8d 100644 --- a/cloud/flamingock-cloud/src/main/java/io/flamingock/cloud/planner/CloudExecutionPlanMapper.java +++ b/cloud/flamingock-cloud/src/main/java/io/flamingock/cloud/planner/CloudExecutionPlanMapper.java @@ -115,7 +115,7 @@ private static ChangeRequest mapToChangeRequest(AbstractLoadedChange descriptor, : CloudTargetSystemAuditMarkType.NONE; CloudChangeStatus currentStatus = CloudApiMapper.toCloud( currentStatusByChangeId.get(descriptor.getId())); - return new ChangeRequest(descriptor.getId(), cloudStatus, currentStatus, descriptor.isTransactional()); + return new ChangeRequest(descriptor.getId(), cloudStatus, currentStatus, descriptor.isTransactional(), descriptor.isRunAlways()); } static List getExecutableStages(ExecutionPlanResponse response, List loadedStages) { @@ -153,8 +153,10 @@ private static ChangeActionMap getChangeActionMap(AbstractLoadedStage loadedStag String changeId = change.getId(); CloudChangeAction cloudAction = actionsMapByChangeId.get(changeId); - // If change not in response, assume it's already applied (cloud orchestrator decision) - if (cloudAction == null) { + if (change.isRunAlways() && (cloudAction == null || cloudAction == CloudChangeAction.SKIP)) { + actionMap.put(changeId, ChangeAction.APPLY); + } else if (cloudAction == null) { + // If change not in response, assume it's already applied (cloud orchestrator decision) actionMap.put(changeId, ChangeAction.SKIP); } else { // Use anti-corruption layer to map cloud domain to internal domain diff --git a/cloud/flamingock-cloud/src/test/java/io/flamingock/cloud/planner/CloudExecutionPlanMapperTest.java b/cloud/flamingock-cloud/src/test/java/io/flamingock/cloud/planner/CloudExecutionPlanMapperTest.java index 1e3363a86..ad18fd22f 100644 --- a/cloud/flamingock-cloud/src/test/java/io/flamingock/cloud/planner/CloudExecutionPlanMapperTest.java +++ b/cloud/flamingock-cloud/src/test/java/io/flamingock/cloud/planner/CloudExecutionPlanMapperTest.java @@ -16,6 +16,7 @@ package io.flamingock.cloud.planner; import io.flamingock.api.StageType; +import io.flamingock.api.StageMode; import io.flamingock.cloud.api.response.ChangeResponse; import io.flamingock.cloud.api.response.ExecutionPlanResponse; import io.flamingock.cloud.api.response.StageResponse; @@ -57,6 +58,8 @@ import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class CloudExecutionPlanMapperTest { @@ -130,6 +133,31 @@ void shouldDefaultToSkipWhenChangeNotInResponse() { assertEquals(ChangeAction.SKIP, actions.get(change2.getId())); } + @Test + @DisplayName("Should apply runAlways change when cloud response omits it") + void shouldApplyRunAlwaysChangeWhenCloudResponseOmitsIt() { + AbstractLoadedChange runAlwaysChange = mockChange("run-always", true); + CapturingStage stage = new CapturingStage("stage-1", runAlwaysChange); + ExecutionPlanResponse response = buildResponse(buildStageResponse("stage-1", 0)); + + List result = CloudExecutionPlanMapper.getExecutableStages(response, Collections.singletonList(stage)); + + assertEquals(ChangeAction.APPLY, result.get(0).getChanges().get(0).getAction()); + } + + @Test + @DisplayName("Should apply runAlways change when cloud response asks to skip it") + void shouldApplyRunAlwaysChangeWhenCloudResponseSkipsIt() { + AbstractLoadedChange runAlwaysChange = mockChange("run-always", true); + CapturingStage stage = new CapturingStage("stage-1", runAlwaysChange); + ExecutionPlanResponse response = buildResponse( + buildStageResponse("stage-1", 0, changeResponse("run-always", CloudChangeAction.SKIP))); + + List result = CloudExecutionPlanMapper.getExecutableStages(response, Collections.singletonList(stage)); + + assertEquals(ChangeAction.APPLY, result.get(0).getChanges().get(0).getAction()); + } + @Test @DisplayName("Should correctly map mixed actions in a single stage") void shouldMapMixedActions() { @@ -220,6 +248,19 @@ void shouldMapOngoingStatusFromAuditMarksToChangeRequests() { assertEquals(CloudTargetSystemAuditMarkType.NONE, marksByChangeId.get(change2.getId())); } + @Test + @DisplayName("toRequest() maps runAlways from loaded changes to ChangeRequest") + void shouldMapRunAlwaysToChangeRequest() { + AbstractLoadedChange runAlwaysChange = mockChange("run-always", true); + AbstractLoadedStage stage = new CapturingStage("stage-1", runAlwaysChange); + + ExecutionPlanRequest request = CloudExecutionPlanMapper.toRequest( + PipelineRun.of(Collections.singletonList(stage)), 60000L, Collections.emptyMap()); + + ChangeRequest changeRequest = request.getClientSubmission().getBlocks().get(0).getStages().get(0).getChanges().get(0); + assertTrue(changeRequest.isRunAlways()); + } + @Test @DisplayName("toRequest() populates ChangeRequest.currentStatus from the operation's recorded ChangeResult statuses") void shouldMapCurrentStatusFromPipelineRun() { @@ -303,12 +344,9 @@ void shouldMapPerStageStatusFromPipelineRun() { void toRequestEmitsOneBlockPerStageRunBlock() { // Three stages of three different types — PipelineRun.of(...) groups them into // SYSTEM -> LEGACY -> DEFAULT blocks (one stage each). - AbstractLoadedStage systemStage = new DefaultLoadedStage("system-stage", StageType.SYSTEM, - Collections.singletonList(change1)); - AbstractLoadedStage legacyStage = new DefaultLoadedStage("legacy-stage", StageType.LEGACY, - Collections.singletonList(change1)); - AbstractLoadedStage userStage = new DefaultLoadedStage("user-stage", StageType.DEFAULT, - Collections.singletonList(change1)); + AbstractLoadedStage systemStage = new io.flamingock.internal.core.pipeline.loaded.stage.SystemLoadedStage("system-stage", StageType.SYSTEM, Collections.singletonList(change1)); + AbstractLoadedStage legacyStage = new io.flamingock.internal.core.pipeline.loaded.stage.LegacyLoadedStage("legacy-stage", StageType.LEGACY, Collections.singletonList(change1)); + AbstractLoadedStage userStage = new DefaultLoadedStage("user-stage", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change1)); PipelineRun pipelineRun = PipelineRun.of(Arrays.asList(userStage, systemStage, legacyStage)); @@ -333,12 +371,9 @@ void toRequestEmitsOneBlockPerStageRunBlock() { void toRequestPreservesGlobalStageOrderAcrossBlocks() { // Two stages in the SYSTEM block, then one in DEFAULT — verify the per-stage `order` // field increments globally across the flattened block sequence. - AbstractLoadedStage system1 = new DefaultLoadedStage("system-1", StageType.SYSTEM, - Collections.singletonList(change1)); - AbstractLoadedStage system2 = new DefaultLoadedStage("system-2", StageType.SYSTEM, - Collections.singletonList(change2)); - AbstractLoadedStage user1 = new DefaultLoadedStage("user-1", StageType.DEFAULT, - Collections.singletonList(change1)); + AbstractLoadedStage system1 = new io.flamingock.internal.core.pipeline.loaded.stage.SystemLoadedStage("system-1", StageType.SYSTEM, Collections.singletonList(change1)); + AbstractLoadedStage system2 = new io.flamingock.internal.core.pipeline.loaded.stage.SystemLoadedStage("system-2", StageType.SYSTEM, Collections.singletonList(change2)); + AbstractLoadedStage user1 = new DefaultLoadedStage("user-1", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change1)); PipelineRun pipelineRun = PipelineRun.of(Arrays.asList(system1, system2, user1)); @@ -368,7 +403,39 @@ void toRequestEmptyPipelineRunYieldsEmptyBlocks() { } private static DefaultLoadedStage buildStage(String name, AbstractLoadedChange... changes) { - return new DefaultLoadedStage(name, StageType.DEFAULT, Arrays.asList(changes)); + return new DefaultLoadedStage(name, StageType.DEFAULT, StageMode.STANDARD, Arrays.asList(changes)); + } + + private static AbstractLoadedChange mockChange(String id, boolean runAlways) { + AbstractLoadedChange change = mock(AbstractLoadedChange.class); + when(change.getId()).thenReturn(id); + when(change.isRunAlways()).thenReturn(runAlways); + when(change.isTransactional()).thenReturn(false); + when(change.isStandard()).thenReturn(true); + return change; + } + + private static class CapturingStage extends DefaultLoadedStage { + + CapturingStage(String name, AbstractLoadedChange... changes) { + super(name, StageType.DEFAULT, StageMode.STANDARD, Arrays.asList(changes)); + } + + @Override + public ExecutableStage applyActions(io.flamingock.internal.common.core.recovery.action.ChangeActionMap actionPlan) { + List executableChanges = getChanges().stream() + .map(change -> executableChange(change.getId(), actionPlan.getActionFor(change.getId()))) + .collect(Collectors.toList()); + return new ExecutableStage(getName(), executableChanges); + } + } + + private static ExecutableChange executableChange(String id, ChangeAction action) { + ExecutableChange executableChange = mock(ExecutableChange.class); + when(executableChange.getId()).thenReturn(id); + when(executableChange.getAction()).thenReturn(action); + when(executableChange.isAlreadyApplied()).thenReturn(action == ChangeAction.SKIP); + return executableChange; } private static ExecutionPlanResponse buildResponse(StageResponse... stages) { diff --git a/cloud/flamingock-cloud/src/test/java/io/flamingock/cloud/planner/CloudExecutionPlannerTest.java b/cloud/flamingock-cloud/src/test/java/io/flamingock/cloud/planner/CloudExecutionPlannerTest.java index 853c9d829..4d0dc2976 100644 --- a/cloud/flamingock-cloud/src/test/java/io/flamingock/cloud/planner/CloudExecutionPlannerTest.java +++ b/cloud/flamingock-cloud/src/test/java/io/flamingock/cloud/planner/CloudExecutionPlannerTest.java @@ -16,6 +16,7 @@ package io.flamingock.cloud.planner; import io.flamingock.api.StageType; +import io.flamingock.api.StageMode; import io.flamingock.cloud.api.request.ExecutionPlanRequest; import io.flamingock.cloud.api.request.ChangeRequest; import io.flamingock.cloud.api.response.ChangeResponse; @@ -115,14 +116,10 @@ void shouldSendMultiBlockPipelineRunOnTheWire() { when(client.createExecution(any(), any(), anyLong())).thenReturn(continueResponse); // Build a PipelineRun with three blocks: SYSTEM (1 stage), LEGACY (1 stage), DEFAULT (2 stages). - AbstractLoadedStage systemStage = new DefaultLoadedStage("system-stage", StageType.SYSTEM, - Collections.singletonList(change1)); - AbstractLoadedStage legacyStage = new DefaultLoadedStage("legacy-stage", StageType.LEGACY, - Collections.singletonList(change1)); - AbstractLoadedStage userStageA = new DefaultLoadedStage("user-a", StageType.DEFAULT, - Collections.singletonList(change1)); - AbstractLoadedStage userStageB = new DefaultLoadedStage("user-b", StageType.DEFAULT, - Collections.singletonList(change2)); + AbstractLoadedStage systemStage = new io.flamingock.internal.core.pipeline.loaded.stage.SystemLoadedStage("system-stage", StageType.SYSTEM, Collections.singletonList(change1)); + AbstractLoadedStage legacyStage = new io.flamingock.internal.core.pipeline.loaded.stage.LegacyLoadedStage("legacy-stage", StageType.LEGACY, Collections.singletonList(change1)); + AbstractLoadedStage userStageA = new DefaultLoadedStage("user-a", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change1)); + AbstractLoadedStage userStageB = new DefaultLoadedStage("user-b", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change2)); planner.getNextExecution(PipelineRun.of(Arrays.asList(systemStage, legacyStage, userStageA, userStageB))); @@ -162,7 +159,7 @@ void shouldReturnAbortPlanWhenServerReturnsAbort() { when(client.createExecution(any(), any(), anyLong())).thenReturn(response); List stages = Collections.singletonList( - new DefaultLoadedStage("stage-1", StageType.DEFAULT, Collections.singletonList(change1))); + new DefaultLoadedStage("stage-1", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change1))); ExecutionPlan plan = planner.getNextExecution(PipelineRun.of(stages)); @@ -185,7 +182,7 @@ void shouldReturnAbortPlanWhenServerReturnsAbortWithNoMIChanges() { when(client.createExecution(any(), any(), anyLong())).thenReturn(response); List stages = Collections.singletonList( - new DefaultLoadedStage("stage-1", StageType.DEFAULT, Collections.singletonList(change1))); + new DefaultLoadedStage("stage-1", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change1))); ExecutionPlan plan = planner.getNextExecution(PipelineRun.of(stages)); @@ -217,7 +214,7 @@ void shouldIncludeAuditMarksInExecutionRequest() { when(client.createExecution(any(), any(), anyLong())).thenReturn(response); List stages = Collections.singletonList( - new DefaultLoadedStage("stage-1", StageType.DEFAULT, Arrays.asList(change1, change2))); + new DefaultLoadedStage("stage-1", StageType.DEFAULT, StageMode.STANDARD, Arrays.asList(change1, change2))); planner.getNextExecution(PipelineRun.of(stages)); @@ -246,7 +243,7 @@ void shouldSendNoneStatusWhenNoMarks() { when(client.createExecution(any(), any(), anyLong())).thenReturn(response); List stages = Collections.singletonList( - new DefaultLoadedStage("stage-1", StageType.DEFAULT, Collections.singletonList(change1))); + new DefaultLoadedStage("stage-1", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change1))); planner.getNextExecution(PipelineRun.of(stages)); @@ -276,7 +273,7 @@ void shouldClearMarksWhenResponseHasSynchronizedMarksTrue() { when(client.createExecution(any(), any(), anyLong())).thenReturn(response); List stages = Collections.singletonList( - new DefaultLoadedStage("stage-1", StageType.DEFAULT, Arrays.asList(change1, change2))); + new DefaultLoadedStage("stage-1", StageType.DEFAULT, StageMode.STANDARD, Arrays.asList(change1, change2))); planner.getNextExecution(PipelineRun.of(stages)); @@ -298,7 +295,7 @@ void shouldNotClearMarksWhenResponseHasSynchronizedMarksFalse() { when(client.createExecution(any(), any(), anyLong())).thenReturn(response); List stages = Collections.singletonList( - new DefaultLoadedStage("stage-1", StageType.DEFAULT, Collections.singletonList(change1))); + new DefaultLoadedStage("stage-1", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change1))); planner.getNextExecution(PipelineRun.of(stages)); @@ -319,7 +316,7 @@ void shouldClearMarksRegardlessOfResponseAction() { when(client.createExecution(any(), any(), anyLong())).thenReturn(response); List stages = Collections.singletonList( - new DefaultLoadedStage("stage-1", StageType.DEFAULT, Collections.singletonList(change1))); + new DefaultLoadedStage("stage-1", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change1))); planner.getNextExecution(PipelineRun.of(stages)); @@ -341,7 +338,7 @@ void shouldNotClearNewMarksWrittenAfterRequest() { when(client.createExecution(any(), any(), anyLong())).thenReturn(response); List stages = Collections.singletonList( - new DefaultLoadedStage("stage-1", StageType.DEFAULT, Arrays.asList(change1, change2))); + new DefaultLoadedStage("stage-1", StageType.DEFAULT, StageMode.STANDARD, Arrays.asList(change1, change2))); planner.getNextExecution(PipelineRun.of(stages)); @@ -401,10 +398,8 @@ void continueWithPipelineResultWritesVerdict() { when(client.createExecution(any(), any(), anyLong())).thenReturn(response); List stages = Arrays.asList( - new DefaultLoadedStage("stage-a", StageType.DEFAULT, - Collections.singletonList(change1)), - new DefaultLoadedStage("stage-b", StageType.DEFAULT, - Collections.singletonList(change2))); + new DefaultLoadedStage("stage-a", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change1)), + new DefaultLoadedStage("stage-b", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change2))); PipelineRun run = PipelineRun.of(stages); planner.getNextExecution(run); @@ -448,8 +443,7 @@ void executeWithPipelineResultUpgradesAlreadyAppliedRecords() { when(client.createExecution(any(), any(), anyLong())).thenReturn(response); List stages = Collections.singletonList( - new DefaultLoadedStage("stage-1", StageType.DEFAULT, - Arrays.asList(change1, change2))); + new DefaultLoadedStage("stage-1", StageType.DEFAULT, StageMode.STANDARD, Arrays.asList(change1, change2))); PipelineRun run = PipelineRun.of(stages); planner.getNextExecution(run); @@ -485,8 +479,7 @@ void abortLeavesPipelineRunUntouched() { when(client.createExecution(any(), any(), anyLong())).thenReturn(response); List stages = Collections.singletonList( - new DefaultLoadedStage("stage-1", StageType.DEFAULT, - Collections.singletonList(change1))); + new DefaultLoadedStage("stage-1", StageType.DEFAULT, StageMode.STANDARD, Collections.singletonList(change1))); PipelineRun run = PipelineRun.of(stages); ExecutionPlan plan = planner.getNextExecution(run); diff --git a/community/flamingock-couchbase-auditstore/src/test/java/io/flamingock/store/couchbase/PipelineTestHelper.java b/community/flamingock-couchbase-auditstore/src/test/java/io/flamingock/store/couchbase/PipelineTestHelper.java index c85ae4d37..32c1232fa 100644 --- a/community/flamingock-couchbase-auditstore/src/test/java/io/flamingock/store/couchbase/PipelineTestHelper.java +++ b/community/flamingock-couchbase-auditstore/src/test/java/io/flamingock/store/couchbase/PipelineTestHelper.java @@ -16,6 +16,7 @@ package io.flamingock.store.couchbase; import io.flamingock.api.StageType; +import io.flamingock.api.StageMode; import io.flamingock.api.annotations.TargetSystem; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; import io.flamingock.internal.common.core.preview.PreviewConstructor; @@ -106,6 +107,7 @@ public static FlamingockMetadata getPreviewPipeline(String stageName, Trio PreviewStage stage = new PreviewStage( stageName, StageType.DEFAULT, + StageMode.STANDARD, "some description", null, null, diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/MetadataLoader.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/MetadataLoader.java index 6f4aaded6..1fb53c383 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/MetadataLoader.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/MetadataLoader.java @@ -314,7 +314,7 @@ private static PreviewStage mergeSameNameLegacyStages(List sameNam } } } - return new PreviewStage(first.getName(), first.getType(), first.getDescription(), + return new PreviewStage(first.getName(), first.getType(), first.getMode(), first.getDescription(), first.getSourcesPackage(), first.getResourcesDir(), merged); } } diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/PreviewStage.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/PreviewStage.java index 3db098bc8..b0824f9c0 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/PreviewStage.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/PreviewStage.java @@ -15,6 +15,7 @@ */ package io.flamingock.internal.common.core.preview; +import io.flamingock.api.StageMode; import io.flamingock.api.StageType; import io.flamingock.internal.common.core.preview.builder.PreviewChangeBuilder; import io.flamingock.internal.common.core.template.ChangeTemplateFileContent; @@ -41,6 +42,8 @@ public class PreviewStage { private StageType type; + private StageMode mode = StageMode.STANDARD; + private String description; private String sourcesPackage; @@ -56,12 +59,14 @@ public PreviewStage() { //TODO it shouldn't be public public PreviewStage(String name, StageType type, + StageMode mode, String description, String sourcesPackage, String resourcesDir, Collection changes) { this.name = name; this.type = type; + this.mode = mode; this.description = description; this.sourcesPackage = sourcesPackage; this.resourcesDir = resourcesDir; @@ -95,6 +100,14 @@ public void setType(StageType type) { this.type = type; } + public StageMode getMode() { + return mode; + } + + public void setMode(StageMode mode) { + this.mode = mode; + } + public String getDescription() { return description; } @@ -145,6 +158,7 @@ public int hashCode() { public String toString() { return "PreviewStage{" + "name='" + name + '\'' + ", type=" + type + + ", mode=" + getMode() + ", description='" + description + '\'' + ", sourcesPackage='" + sourcesPackage + '\'' + ", resourcesDir='" + resourcesDir + '\'' + @@ -169,7 +183,11 @@ protected PreviewStage buildInstance(String name, String sourcesPackage, String resourcesDir, Collection allDescriptors) { - return new PreviewStage(name, type, description, sourcesPackage, resourcesDir, allDescriptors); + StageMode effectiveMode = type == StageType.DEFAULT ? getMode() : StageMode.STANDARD; + if (effectiveMode == StageMode.RUN_ALWAYS && allDescriptors != null) { + allDescriptors.forEach(change -> change.setRunAlways(true)); + } + return new PreviewStage(name, type, effectiveMode, description, sourcesPackage, resourcesDir, allDescriptors); } } @@ -199,6 +217,7 @@ public abstract static class AbstractBuilder { private String resourcesRoot; private Collection sourcesRoots; private Collection changes; + private StageMode mode = StageMode.STANDARD; private boolean parallel = false; protected AbstractBuilder() { @@ -227,6 +246,15 @@ public AbstractBuilder setDescription(String description) { return this; } + public AbstractBuilder setMode(StageMode mode) { + this.mode = mode; + return this; + } + + protected StageMode getMode() { + return mode; + } + /** * Sets the collection of source root directories (e.g., {@code src/main/java}, {@code src/main/kotlin}). * Used in conjunction with {@link #setSourcesPackage(String)} to locate YAML changes. @@ -337,7 +365,6 @@ public T build() { Collection allDescriptors = Stream.concat(templatedChangesDescriptors.stream(), changeClassesList.stream()) .collect(Collectors.toList()); - return buildInstance(name, description, sourcesPackage, resourcesDir, allDescriptors); } diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/SystemPreviewStage.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/SystemPreviewStage.java index 5eccb7916..a72098fa3 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/SystemPreviewStage.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/SystemPreviewStage.java @@ -15,6 +15,7 @@ */ package io.flamingock.internal.common.core.preview; +import io.flamingock.api.StageMode; import io.flamingock.api.StageType; import org.jetbrains.annotations.NotNull; @@ -37,7 +38,7 @@ public SystemPreviewStage(String name, String sourcesPackage, String resourcesDir, Collection changes) { - super(name, StageType.SYSTEM, description, sourcesPackage, resourcesDir, changes); + super(name, StageType.SYSTEM, StageMode.STANDARD, description, sourcesPackage, resourcesDir, changes); } diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java index 7ee167856..6ce4af79f 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java @@ -144,7 +144,7 @@ CodePreviewChangeBuilder setTypeElement(TypeElement typeElement) { setConstructor(getPreviewConstructor(typeElement)); setApplyMethod(getAnnotatedMethodInfo(typeElement, Apply.class).orElse(null)); setRollbackMethod(getAnnotatedMethodInfo(typeElement, Rollback.class).orElse(null)); - setRunAlways(false); //TODO: how to set runAlways + setRunAlways(false); setTransactionalFlag(changeAnnotation.transactional()); setSystem(false); } diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/PreviewStageTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/PreviewStageTest.java new file mode 100644 index 000000000..8e82683a7 --- /dev/null +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/PreviewStageTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * 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 io.flamingock.internal.common.core.preview; + +import io.flamingock.api.StageMode; +import io.flamingock.api.StageType; +import io.flamingock.internal.common.core.change.RecoveryDescriptor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PreviewStageTest { + + @Test + @DisplayName("RUN_ALWAYS default stage marks all preview changes as runAlways") + void runAlwaysDefaultStageMarksAllChanges() { + AbstractPreviewChange change = previewChange("change-1", false); + + PreviewStage stage = PreviewStage.defaultBuilder(StageType.DEFAULT) + .setName("repeatable") + .setMode(StageMode.RUN_ALWAYS) + .setChanges(Collections.singletonList(change)) + .build(); + + assertEquals(StageMode.RUN_ALWAYS, stage.getMode()); + assertTrue(stage.getChanges().iterator().next().isRunAlways()); + } + + @Test + @DisplayName("STANDARD default stage keeps preview changes non-runAlways") + void standardDefaultStageKeepsChangesNonRunAlways() { + AbstractPreviewChange change = previewChange("change-1", false); + + PreviewStage stage = PreviewStage.defaultBuilder(StageType.DEFAULT) + .setName("standard") + .setChanges(Collections.singletonList(change)) + .build(); + + assertEquals(StageMode.STANDARD, stage.getMode()); + assertFalse(stage.getChanges().iterator().next().isRunAlways()); + } + + @Test + @DisplayName("LEGACY stage ignores stage mode runAlways") + void legacyStageIgnoresStageModeRunAlways() { + AbstractPreviewChange change = previewChange("change-1", false); + + PreviewStage stage = PreviewStage.defaultBuilder(StageType.LEGACY) + .setName("legacy") + .setMode(StageMode.RUN_ALWAYS) + .setChanges(Collections.singletonList(change)) + .build(); + + assertEquals(StageMode.STANDARD, stage.getMode()); + assertFalse(stage.getChanges().iterator().next().isRunAlways()); + } + + private static AbstractPreviewChange previewChange(String id, boolean runAlways) { + return new AbstractPreviewChange( + id, + "001", + "author", + "source", + "source-file", + runAlways, + true, + false, + null, + RecoveryDescriptor.getDefault(), + false) { + }; + } +} diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewChangeBuilderTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewChangeBuilderTest.java index 4fd76702c..41a142115 100644 --- a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewChangeBuilderTest.java +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewChangeBuilderTest.java @@ -75,4 +75,20 @@ void shouldPreserveExplicitFalseTransactional() { assertEquals(Optional.of(false), preview.getTransactionalFlag()); assertEquals("_0001__test.yaml", preview.getSourceFile()); } + + @Test + @DisplayName("Should default runAlways to false when omitted") + void shouldDefaultRunAlwaysToFalseWhenOmitted() { + ChangeTemplateFileContent content = new ChangeTemplateFileContent( + "test-id", "author", "SqlTemplate", null, + null, null, "CREATE TABLE", null, + TargetSystemDescriptor.fromId("postgresql"), null); + + TemplatePreviewChange preview = TemplatePreviewChangeBuilder.builder(content) + .setFileName("_0001__test.yaml") + .build(); + + assertFalse(preview.isRunAlways()); + } + } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java index ef2dea64f..ad5eb7da6 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java @@ -217,6 +217,7 @@ private void setFromFlamingockChangeAnnotation(Class sourceClass, Change anno setConstructor(getConstructor(sourceClass)); setApplyMethod(getApplyMethodFromAnnotation(sourceClass)); setRollbackMethod(getRollbackMethodFromAnnotation(sourceClass)); + setRunAlways(false); setTransactionalFlag(annotation.transactional()); setSystem(false); setRecoveryFromClass(sourceClass); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/AbstractLoadedStage.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/AbstractLoadedStage.java index 66c4f5a53..52f2cb93e 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/AbstractLoadedStage.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/AbstractLoadedStage.java @@ -19,6 +19,7 @@ import io.flamingock.internal.common.core.error.validation.Validatable; import io.flamingock.internal.common.core.error.validation.ValidationError; +import io.flamingock.api.StageMode; import io.flamingock.api.StageType; import io.flamingock.internal.common.core.preview.PreviewStage; import io.flamingock.internal.core.pipeline.execution.ExecutableStage; @@ -56,14 +57,18 @@ public static Builder builder() { private final StageType type; + private final StageMode mode; + private final Collection changes; public AbstractLoadedStage(String name, StageType type, + StageMode mode, Collection changes, StageValidationContext validationContext) { this.name = name; this.type = type; + this.mode = mode != null ? mode : StageMode.STANDARD; this.changes = changes; this.validationContext = validationContext; } @@ -99,6 +104,10 @@ public StageType getType() { return type; } + public StageMode getMode() { + return mode; + } + public Collection getChanges() { return changes; } @@ -111,23 +120,14 @@ public Collection getChanges() { *

Used at runtime construction time to materialize a stage with a filtered subset * of changes without mutating the original {@code AbstractLoadedStage} (whose * {@code changes} collection is final and immutable post-construction). The copy is - * produced by reflecting on the concrete subclass to find its public - * {@code (String, StageType, Collection)} constructor — every concrete subclass - * ({@link DefaultLoadedStage}, {@link LegacyLoadedStage}, {@link SystemLoadedStage}) - * exposes one. + * produced by the concrete subclass so each stage type can preserve exactly the state it owns. */ public AbstractLoadedStage withChanges(Collection newChanges) { - try { - return getClass() - .getConstructor(String.class, StageType.class, Collection.class) - .newInstance(getName(), getType(), newChanges); - } catch (ReflectiveOperationException e) { - throw new IllegalStateException( - "Cannot copy stage [" + getName() + "] of type " + getClass().getName() - + " with filtered changes", e); - } + return copyWithChanges(newChanges); } + protected abstract AbstractLoadedStage copyWithChanges(Collection newChanges); + /** @@ -177,6 +177,7 @@ public List getValidationErrors(PipelineValidationContext conte @Override public String toString() { return "LoadedStage{" + "name='" + name + '\'' + + ", mode=" + getMode() + ", loadedChanges=" + changes + '}'; } @@ -226,7 +227,7 @@ public AbstractLoadedStage build() { return new SystemLoadedStage(previewStage.getName(), previewStage.getType(), loadedChanges); case DEFAULT: default: - return new DefaultLoadedStage(previewStage.getName(), previewStage.getType(), loadedChanges); + return new DefaultLoadedStage(previewStage.getName(), previewStage.getType(), previewStage.getMode(), loadedChanges); } } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/DefaultLoadedStage.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/DefaultLoadedStage.java index 9960d13db..df8e0574f 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/DefaultLoadedStage.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/DefaultLoadedStage.java @@ -16,6 +16,7 @@ package io.flamingock.internal.core.pipeline.loaded.stage; +import io.flamingock.api.StageMode; import io.flamingock.api.StageType; import io.flamingock.internal.common.core.error.validation.ValidationError; import io.flamingock.internal.core.pipeline.loaded.PipelineValidationContext; @@ -32,6 +33,8 @@ public class DefaultLoadedStage extends AbstractLoadedStage { private final static String INVALID_CHANGE_TYPE_MSG = "Invalid change detected: a non-standard change was found while processing a standard stage"; + private final static String RUN_ALWAYS_NOT_ALLOWED_MSG = "Invalid change detected: runAlways changes are only allowed in RUN_ALWAYS stages"; + private final static String RUN_ALWAYS_REQUIRED_MSG = "Invalid change detected: RUN_ALWAYS stages can only contain runAlways changes"; private static final StageValidationContext validationContext = StageValidationContext.builder() .setSorted(SEQUENTIAL_FORMATTED) @@ -39,8 +42,14 @@ public class DefaultLoadedStage extends AbstractLoadedStage { public DefaultLoadedStage(String name, StageType type, + StageMode mode, Collection loadedChanges) { - super(name, type, loadedChanges, validationContext); + super(name, type, mode, loadedChanges, validationContext); + } + + @Override + protected AbstractLoadedStage copyWithChanges(Collection newChanges) { + return new DefaultLoadedStage(getName(), getType(), getMode(), newChanges); } @Override @@ -52,6 +61,12 @@ public List getValidationErrors(PipelineValidationContext conte if (!change.isStandard()) { errors.add(new ValidationError(INVALID_CHANGE_TYPE_MSG, change.getId(), "change")); } + if (getMode() == StageMode.STANDARD && change.isRunAlways()) { + errors.add(new ValidationError(RUN_ALWAYS_NOT_ALLOWED_MSG, change.getId(), "change")); + } + if (getMode() == StageMode.RUN_ALWAYS && !change.isRunAlways()) { + errors.add(new ValidationError(RUN_ALWAYS_REQUIRED_MSG, change.getId(), "change")); + } } else { errors.add(new ValidationError("Null changes are not allowed in default stage", "unknown", "change")); } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/LegacyLoadedStage.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/LegacyLoadedStage.java index 57f3b6ee2..ede0da36e 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/LegacyLoadedStage.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/LegacyLoadedStage.java @@ -16,6 +16,7 @@ package io.flamingock.internal.core.pipeline.loaded.stage; +import io.flamingock.api.StageMode; import io.flamingock.api.StageType; import io.flamingock.internal.common.core.error.validation.ValidationError; import io.flamingock.internal.core.pipeline.loaded.PipelineValidationContext; @@ -40,10 +41,15 @@ public class LegacyLoadedStage extends AbstractLoadedStage { public LegacyLoadedStage(String name, StageType type, Collection loadedChanges) { - super(name, type, loadedChanges, validationContext); + super(name, type, StageMode.STANDARD, loadedChanges, validationContext); } + @Override + protected AbstractLoadedStage copyWithChanges(Collection newChanges) { + return new LegacyLoadedStage(getName(), getType(), newChanges); + } + @Override public List getValidationErrors(PipelineValidationContext context) { // Legacy stages may legitimately be empty (e.g. no Mongock audit history to import) — diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/SystemLoadedStage.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/SystemLoadedStage.java index 1418fa530..7f4fbb09d 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/SystemLoadedStage.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/loaded/stage/SystemLoadedStage.java @@ -16,6 +16,7 @@ package io.flamingock.internal.core.pipeline.loaded.stage; +import io.flamingock.api.StageMode; import io.flamingock.api.StageType; import io.flamingock.internal.common.core.error.validation.ValidationError; import io.flamingock.internal.core.pipeline.loaded.PipelineValidationContext; @@ -41,10 +42,15 @@ public class SystemLoadedStage extends AbstractLoadedStage { public SystemLoadedStage(String name, StageType type, Collection loadedChanges) { - super(name, type, loadedChanges, validationContext); + super(name, type, StageMode.STANDARD, loadedChanges, validationContext); } + @Override + protected AbstractLoadedStage copyWithChanges(Collection newChanges) { + return new SystemLoadedStage(getName(), getType(), newChanges); + } + @Override public List getValidationErrors(PipelineValidationContext context) { diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/plan/community/CommunityChangeActionBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/plan/community/CommunityChangeActionBuilder.java index af60136e0..502b18b02 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/plan/community/CommunityChangeActionBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/plan/community/CommunityChangeActionBuilder.java @@ -59,6 +59,10 @@ public static ChangeActionMap build(Collection changes, Ma log.debug("Change[{}] action={} | Reason: No previous audit entry (first execution)", change.getId(), ChangeAction.APPLY); actionMap.put(change.getId(), ChangeAction.APPLY); + } else if (change.isRunAlways() && isSuccessfullyApplied(auditEntry)) { + log.debug("Change[{}] action={} | Reason: runAlways change with previous successful audit entry", + change.getId(), ChangeAction.APPLY); + actionMap.put(change.getId(), ChangeAction.APPLY); } else { ChangeAction action = ChangeActionResolver.resolve(auditEntry); actionMap.put(change.getId(), action); @@ -69,5 +73,10 @@ public static ChangeActionMap build(Collection changes, Ma return new ChangeActionMap(actionMap); } + private static boolean isSuccessfullyApplied(AuditEntry auditEntry) { + return auditEntry.getState() == AuditEntry.Status.APPLIED + || auditEntry.getState() == AuditEntry.Status.MANUAL_MARKED_AS_APPLIED; + } + } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/plan/community/CommunityExecutionPlanner.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/plan/community/CommunityExecutionPlanner.java index b1e8d300e..4b2f2ff9f 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/plan/community/CommunityExecutionPlanner.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/plan/community/CommunityExecutionPlanner.java @@ -281,6 +281,9 @@ private static void stampSnapshotFacts(PipelineRun pipelineRun, Map alreadyAppliedChangeIds(StageRun stageRun, Map auditSnapshot) { List ids = new ArrayList<>(); for (AbstractLoadedChange change : stageRun.getLoadedStage().getChanges()) { + if (change.isRunAlways()) { + continue; + } AuditEntry entry = auditSnapshot.get(change.getId()); if (entry == null) { continue; diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/pipeline/loaded/stage/DefaultLoadedStageTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/pipeline/loaded/stage/DefaultLoadedStageTest.java new file mode 100644 index 000000000..8079c9238 --- /dev/null +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/pipeline/loaded/stage/DefaultLoadedStageTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * 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 io.flamingock.internal.core.pipeline.loaded.stage; + +import io.flamingock.api.StageMode; +import io.flamingock.api.StageType; +import io.flamingock.internal.common.core.error.validation.ValidationError; +import io.flamingock.internal.core.change.loaded.AbstractLoadedChange; +import io.flamingock.internal.core.pipeline.loaded.PipelineValidationContext; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class DefaultLoadedStageTest { + + @Test + @DisplayName("STANDARD native stage rejects runAlways changes") + void standardStageRejectsRunAlwaysChanges() { + DefaultLoadedStage stage = new DefaultLoadedStage( + "standard", + StageType.DEFAULT, + StageMode.STANDARD, + Collections.singletonList(change("change-1", true))); + + List errors = stage.getValidationErrors(mock(PipelineValidationContext.class)); + + assertTrue(hasMessage(errors, "runAlways changes are only allowed in RUN_ALWAYS stages")); + } + + @Test + @DisplayName("RUN_ALWAYS native stage rejects non-runAlways changes") + void runAlwaysStageRejectsNonRunAlwaysChanges() { + DefaultLoadedStage stage = new DefaultLoadedStage( + "repeatable", + StageType.DEFAULT, + StageMode.RUN_ALWAYS, + Collections.singletonList(change("change-1", false))); + + List errors = stage.getValidationErrors(mock(PipelineValidationContext.class)); + + assertTrue(hasMessage(errors, "RUN_ALWAYS stages can only contain runAlways changes")); + } + + @Test + @DisplayName("LEGACY stage allows mixed runAlways values") + void legacyStageAllowsMixedRunAlwaysValues() { + LegacyLoadedStage stage = new LegacyLoadedStage( + "legacy", + StageType.LEGACY, + Arrays.asList(legacyChange("change-1", true), legacyChange("change-2", false))); + + List errors = stage.getValidationErrors(mock(PipelineValidationContext.class)); + + assertFalse(hasMessage(errors, "runAlways")); + } + + private static boolean hasMessage(List errors, String text) { + return errors.stream().anyMatch(error -> error.getMessage().contains(text)); + } + + private static AbstractLoadedChange change(String id, boolean runAlways) { + AbstractLoadedChange change = mock(AbstractLoadedChange.class); + when(change.getId()).thenReturn(id); + when(change.isStandard()).thenReturn(true); + when(change.isRunAlways()).thenReturn(runAlways); + when(change.getValidationErrors(any(StageValidationContext.class))).thenReturn(Collections.emptyList()); + return change; + } + + private static AbstractLoadedChange legacyChange(String id, boolean runAlways) { + AbstractLoadedChange change = change(id, runAlways); + when(change.isStandard()).thenReturn(false); + when(change.isLegacy()).thenReturn(true); + return change; + } +} diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/pipeline/run/PipelineRunOfLoadedPipelineTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/pipeline/run/PipelineRunOfLoadedPipelineTest.java index 51f1a7a1e..5f3794c37 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/pipeline/run/PipelineRunOfLoadedPipelineTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/pipeline/run/PipelineRunOfLoadedPipelineTest.java @@ -16,6 +16,7 @@ package io.flamingock.internal.core.pipeline.run; import io.flamingock.api.StageType; +import io.flamingock.api.StageMode; import io.flamingock.internal.core.change.filter.ChangeFilter; import io.flamingock.internal.core.change.loaded.AbstractLoadedChange; import io.flamingock.internal.core.pipeline.loaded.LoadedPipeline; @@ -300,7 +301,7 @@ private static DefaultLoadedStage newStage(String name, StageType type, String.. for (String id : changeIds) { changes.add(mockChange(id)); } - return new DefaultLoadedStage(name, type, changes); + return new DefaultLoadedStage(name, type, StageMode.STANDARD, changes); } private static AbstractLoadedChange mockChange(String id) { diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/plan/community/CommunityChangeActionBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/plan/community/CommunityChangeActionBuilderTest.java new file mode 100644 index 000000000..39a97d3c7 --- /dev/null +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/plan/community/CommunityChangeActionBuilderTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * 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 io.flamingock.internal.core.plan.community; + +import io.flamingock.api.RecoveryStrategy; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.core.recovery.action.ChangeAction; +import io.flamingock.internal.common.core.recovery.action.ChangeActionMap; +import io.flamingock.internal.core.change.loaded.AbstractLoadedChange; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CommunityChangeActionBuilderTest { + + @Test + @DisplayName("Normal already-applied changes are skipped") + void normalAlreadyAppliedChangeIsSkipped() { + AbstractLoadedChange change = change("change-1", false); + Map audit = new HashMap<>(); + audit.put("change-1", auditEntry("change-1", AuditEntry.Status.APPLIED)); + + ChangeActionMap actions = CommunityChangeActionBuilder.build(Collections.singletonList(change), audit); + + assertEquals(ChangeAction.SKIP, actions.getActionFor("change-1")); + } + + @Test + @DisplayName("runAlways already-applied changes are applied again") + void runAlwaysAlreadyAppliedChangeIsAppliedAgain() { + AbstractLoadedChange change = change("change-1", true); + Map audit = new HashMap<>(); + audit.put("change-1", auditEntry("change-1", AuditEntry.Status.APPLIED)); + + ChangeActionMap actions = CommunityChangeActionBuilder.build(Collections.singletonList(change), audit); + + assertEquals(ChangeAction.APPLY, actions.getActionFor("change-1")); + } + + @Test + @DisplayName("runAlways manual-marked-as-applied changes are applied again") + void runAlwaysManualMarkedAsAppliedChangeIsAppliedAgain() { + AbstractLoadedChange change = change("change-1", true); + Map audit = new HashMap<>(); + audit.put("change-1", auditEntry("change-1", AuditEntry.Status.MANUAL_MARKED_AS_APPLIED)); + + ChangeActionMap actions = CommunityChangeActionBuilder.build(Collections.singletonList(change), audit); + + assertEquals(ChangeAction.APPLY, actions.getActionFor("change-1")); + } + + private static AbstractLoadedChange change(String id, boolean runAlways) { + AbstractLoadedChange change = mock(AbstractLoadedChange.class); + when(change.getId()).thenReturn(id); + when(change.isRunAlways()).thenReturn(runAlways); + return change; + } + + private static AuditEntry auditEntry(String changeId, AuditEntry.Status status) { + return new AuditEntry( + "exec-1", + "stage-1", + changeId, + "test-author", + LocalDateTime.now(), + status, + AuditEntry.ChangeType.STANDARD_CODE, + "io.flamingock.test.TestChange", + "apply", + null, + 100L, + "localhost", + null, + false, + null, + null, + null, + null, + RecoveryStrategy.MANUAL_INTERVENTION, + null + ); + } +} diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/plan/community/CommunityExecutionPlannerTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/plan/community/CommunityExecutionPlannerTest.java index 127cbf295..db20f42b7 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/plan/community/CommunityExecutionPlannerTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/plan/community/CommunityExecutionPlannerTest.java @@ -194,6 +194,32 @@ void shouldReturnContinueWithoutLockWhenAllChangesApplied() { verify(lockService, never()).upsert(any(), any(), anyLong()); } + @Test + @DisplayName("Should execute runAlways change even when audit says already applied") + void shouldExecuteRunAlwaysChangeWhenAlreadyApplied() { + AbstractLoadedChange change = mockLoadedChange("change-1"); + when(change.isRunAlways()).thenReturn(true); + AbstractLoadedStage stage = mockStage("stage-1", change); + + Map snapshot = new HashMap<>(); + snapshot.put("change-1", buildAuditEntry("change-1", AuditEntry.Status.APPLIED, null)); + when(auditReader.getAuditSnapshotByChangeId()).thenReturn(snapshot); + when(lockService.upsert(any(), any(), anyLong())) + .thenReturn(new LockAcquisition(RunnerId.fromString("test-runner"), 60000L)); + + PipelineRun pipelineRun = PipelineRun.of(Collections.singletonList(stage)); + ExecutionPlan plan = planner.getNextExecution(pipelineRun); + + assertFalse(plan.isAborted()); + assertTrue(plan.isExecutionRequired()); + assertEquals(io.flamingock.internal.common.core.recovery.action.ChangeAction.APPLY, + plan.getExecutableStages().get(0).getChanges().get(0).getAction()); + assertEquals(io.flamingock.internal.common.core.response.data.ChangeStatus.NOT_REACHED, + pipelineRun.getStageRun("stage-1").getResult().getChanges().get(0).getStatus(), + "runAlways changes must not be stamped as ALREADY_APPLIED from audit"); + verify(lockService).upsert(any(), any(), anyLong()); + } + @Test @DisplayName("Should return CONTINUE when every block is terminal+successful (multi-block, all done)") void shouldReturnContinueWhenAllBlocksAreSuccessful() { diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java index e57699165..0f0fceefc 100644 --- a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java @@ -101,9 +101,8 @@ *

    *
  • @EnableFlamingock annotation is mandatory
  • *
  • Must specify either {@code configFile} OR {@code stages} (mutually exclusive)
  • - *
  • Maximum of 1 stage with type {@code StageType.SYSTEM} is allowed
  • - *
  • Maximum of 1 stage with type {@code StageType.LEGACY} is allowed
  • - *
  • Unlimited stages with type {@code StageType.DEFAULT} are allowed
  • + *
  • User-declared stages are standard Flamingock stages
  • + *
  • System and legacy stages are generated internally by Flamingock
  • *
* *

Supported Annotations

diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/FlamingockMetadataMerger.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/FlamingockMetadataMerger.java index 91232c00f..0a5ea4558 100644 --- a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/FlamingockMetadataMerger.java +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/FlamingockMetadataMerger.java @@ -15,6 +15,7 @@ */ package io.flamingock.core.processor.util; +import io.flamingock.api.StageMode; import io.flamingock.api.StageType; import io.flamingock.internal.common.core.metadata.BuilderProviderInfo; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; @@ -27,7 +28,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -139,6 +139,12 @@ public static void applyRound(FlamingockMetadata target, orphans.add(c); } } + + // 7. Apply the repeatability policy after every code change has been routed to its + // definitive stage. Orphans must not inherit a stage policy. + normalizeRunAlwaysChanges(effective); + + // 8. Persist candidates that could not be assigned to any stage as orphans. if (!orphans.isEmpty()) { addOrphansInternal(target, orphans); } @@ -309,6 +315,7 @@ private static void ensureLegacyStageIfNeeded(PreviewPipeline pipeline, if (c.isLegacy()) { PreviewStage legacy = new PreviewStage( PipelineHelper.LEGACY_STAGE_ID, StageType.LEGACY, + StageMode.STANDARD, "Flamingock legacy stage", null, null, new ArrayList<>()); List mutable = (stages == null) ? new ArrayList<>() : new ArrayList<>(stages); @@ -383,6 +390,22 @@ private static void appendToStage(PreviewStage stage, CodePreviewChange change) stage.setChanges(next); } + /** Apply the native stage repeatability policy after all code changes have been routed. */ + private static void normalizeRunAlwaysChanges(PreviewPipeline pipeline) { + if (pipeline == null || pipeline.getStages() == null) { + return; + } + for (PreviewStage stage : pipeline.getStages()) { + if (stage.getType() == StageType.DEFAULT + && stage.getMode() == StageMode.RUN_ALWAYS + && stage.getChanges() != null) { + for (AbstractPreviewChange change : stage.getChanges()) { + change.setRunAlways(true); + } + } + } + } + private static boolean pruneStage(PreviewStage stage, java.util.function.Predicate typeExists) { Collection current = stage.getChanges(); if (current == null || current.isEmpty()) return false; diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/PipelineStructureBuilder.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/PipelineStructureBuilder.java index 368514bde..51023e4e5 100644 --- a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/PipelineStructureBuilder.java +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/PipelineStructureBuilder.java @@ -15,6 +15,7 @@ */ package io.flamingock.core.processor.util; +import io.flamingock.api.StageMode; import io.flamingock.api.StageType; import io.flamingock.api.annotations.EnableFlamingock; import io.flamingock.api.annotations.Stage; @@ -58,8 +59,7 @@ * not declarable in YAML or annotations. *
  • Legacy stage: built from changes flagged {@link CodePreviewChange#isLegacy()}; * inserted before all DEFAULT stages.
  • - *
  • DEFAULT stages: declared inline (annotation) or in YAML; YAML may also declare - * LEGACY stages (max 1).
  • + *
  • DEFAULT stages: declared inline (annotation) or in YAML.
  • * */ public final class PipelineStructureBuilder { @@ -132,7 +132,7 @@ private PreviewStage stageFromAnnotation(Stage stageAnnotation, : stageAnnotation.name(); String description = stageAnnotation.description().isEmpty() ? null : stageAnnotation.description(); - return buildStage(name, description, StageType.DEFAULT, location, changesByPackage); + return buildStage(name, description, StageType.DEFAULT, stageAnnotation.mode(), location, changesByPackage); } // ---------------------------------------------------------------- YAML source @@ -149,7 +149,7 @@ private PreviewPipeline buildFromYaml(File yamlFile, @SuppressWarnings("unchecked") List> stageList = (List>) pipelineMap.get("stages"); - validateYamlStageTypes(stageList); + validateYamlDoesNotDeclareStageType(stageList); List stages = new ArrayList<>(); for (Map stageMap : stageList) { @@ -171,7 +171,10 @@ private PreviewStage stageFromYaml(Map stageMap, ? PathResolver.deriveNameFromLocation(location) : rawName; return buildStage(name, stageMap.get("description"), - StageType.from(stageMap.get("type")), location, changesByPackage); + StageType.DEFAULT, + StageMode.from(stageMap.get("mode")), + location, + changesByPackage); } // ---------------------------------------------------------------- shared stage build @@ -184,6 +187,7 @@ private PreviewStage stageFromYaml(Map stageMap, private PreviewStage buildStage(String name, String description, StageType type, + StageMode mode, String location, Map> changesByPackage) { logger.verbose("Building stage: " + name); @@ -207,6 +211,7 @@ private PreviewStage buildStage(String name, return PreviewStage.defaultBuilder(type) .setName(name) .setDescription(description) + .setMode(mode) .setSourcesRoots(sourceRoots) .setSourcesPackage(sourcesPackage) .setResourcesRoot(resourcesRoot) @@ -286,20 +291,14 @@ private static void validateExclusive(boolean hasFile, boolean hasStages) { } } - private static void validateYamlStageTypes(List> stageList) { - int system = 0; - int legacy = 0; + private static void validateYamlDoesNotDeclareStageType(List> stageList) { for (Map stageMap : stageList) { - StageType type = StageType.from(stageMap.get("type")); - if (type == StageType.SYSTEM && ++system > 1) { + String type = stageMap.get("type"); + if (type != null && !type.trim().isEmpty()) { throw new RuntimeException( - "Multiple SYSTEM stages are not allowed in YAML pipeline. " - + "Only one stage with type 'system' is permitted."); - } - if (type == StageType.LEGACY && ++legacy > 1) { - throw new RuntimeException( - "Multiple LEGACY stages are not allowed in YAML pipeline. " - + "Only one stage with type 'legacy' is permitted."); + "Stage field 'type' is not supported in YAML pipeline configuration. " + + "StageType is internal; declared YAML stages are always DEFAULT. " + + "Use 'mode' to configure stage repeatability."); } } } diff --git a/core/flamingock-processor/src/test/java/io/flamingock/core/processor/MultiModuleProcessingIT.java b/core/flamingock-processor/src/test/java/io/flamingock/core/processor/MultiModuleProcessingIT.java index 907a393f8..489cac88d 100644 --- a/core/flamingock-processor/src/test/java/io/flamingock/core/processor/MultiModuleProcessingIT.java +++ b/core/flamingock-processor/src/test/java/io/flamingock/core/processor/MultiModuleProcessingIT.java @@ -16,6 +16,7 @@ package io.flamingock.core.processor; import com.fasterxml.jackson.databind.SerializationFeature; +import io.flamingock.api.StageMode; import io.flamingock.core.processor.util.MetadataModuleIdentity; import io.flamingock.internal.common.core.change.RecoveryDescriptor; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; @@ -434,6 +435,7 @@ private static void injectLegacyStage(Path classOutput, String stageName, List> changes = createMockChangesMap(); + + PreviewPipeline pipeline = buildPipelineFromAnnotation(newBuilder(), annotation, changes); + + PreviewStage stage = pipeline.getStages().iterator().next(); + assertEquals(StageMode.RUN_ALWAYS, stage.getMode()); + assertTrue(stage.getChanges().stream().allMatch(AbstractPreviewChange::isRunAlways)); + } + + @Test + @DisplayName("Should apply RUN_ALWAYS mode from YAML stages") + void shouldApplyRunAlwaysModeFromYamlStages() throws Exception { + Path configFile = tempDir.resolve("pipeline.yaml"); + String yamlContent = "pipeline:\n" + + " stages:\n" + + " - location: com.example.migrations\n" + + " mode: RUN_ALWAYS\n"; + Files.write(configFile, yamlContent.getBytes()); + EnableFlamingock annotation = createMockAnnotationWithFile("pipeline.yaml"); + Map> changes = createMockChangesMap(); + + PreviewPipeline pipeline = buildPipelineFromFile(newBuilder(), annotation, changes); + + PreviewStage stage = pipeline.getStages().iterator().next(); + assertEquals(StageMode.RUN_ALWAYS, stage.getMode()); + assertTrue(stage.getChanges().stream().allMatch(AbstractPreviewChange::isRunAlways)); } /** @@ -199,83 +236,49 @@ void shouldAllowMultipleDefaultStagesInAnnotation() throws Exception { assertNull(pipeline.getSystemStage(), "Should not have system stage"); } - /** - * Test that YAML stages are ordered correctly regardless of declaration order. - */ @Test - @DisplayName("Should order YAML stages by type priority: LEGACY before DEFAULT") - void shouldOrderYamlStagesByTypePriorityLegacyBeforeDefault() throws Exception { - // Given - create YAML file with stages in reverse order (DEFAULT first, LEGACY second) + @DisplayName("Should reject stage type in YAML configuration") + void shouldRejectStageTypeInYamlConfiguration() throws Exception { Path configFile = tempDir.resolve("pipeline.yaml"); String yamlContent = "pipeline:\n" + " stages:\n" + " - location: com.example.migrations\n" + - " - location: com.example.init\n" + - " type: legacy\n" + - " - location: com.example.cleanup\n"; + " type: legacy\n"; Files.write(configFile, yamlContent.getBytes()); - + EnableFlamingock annotation = createMockAnnotationWithFile("pipeline.yaml"); Map> changes = createMockChangesMap(); PipelineStructureBuilder builder = newBuilder(); - // When - build pipeline from file - PreviewPipeline pipeline = buildPipelineFromFile(builder, annotation, changes); + Exception exception = assertThrows(Exception.class, () -> + buildPipelineFromFile(builder, annotation, changes)); - // Then - verify stages are sorted by type priority - assertNotNull(pipeline, "Pipeline should be created"); - assertEquals(3, pipeline.getStages().size(), "Should have 3 stages"); - - PreviewStage[] stages = pipeline.getStages().toArray(new PreviewStage[0]); - - // First stage should be LEGACY (highest priority) - assertEquals(StageType.LEGACY, stages[0].getType()); - assertEquals("init", stages[0].getName()); - assertEquals("com.example.init", stages[0].getSourcesPackage()); - - // Second and third stages should be DEFAULT (lower priority) - assertEquals(StageType.DEFAULT, stages[1].getType()); - assertEquals("migrations", stages[1].getName()); - assertEquals("com.example.migrations", stages[1].getSourcesPackage()); - - assertEquals(StageType.DEFAULT, stages[2].getType()); - assertEquals("cleanup", stages[2].getName()); - assertEquals("com.example.cleanup", stages[2].getSourcesPackage()); + Throwable cause = exception.getCause(); + String message = cause instanceof RuntimeException ? cause.getMessage() : exception.getMessage(); + assertTrue(message.contains("Stage field 'type' is not supported in YAML pipeline configuration"), + "Should reject YAML stage type explicitly"); } - /** - * Test validation for YAML pipeline with multiple SYSTEM stages. - */ @Test - @DisplayName("Should throw error for multiple SYSTEM stages in YAML configuration") - void shouldThrowErrorForMultipleSystemStagesInYaml() throws Exception { - // Given - create YAML file with multiple system stages + @DisplayName("Should treat all YAML stages as DEFAULT when type is omitted") + void shouldTreatAllYamlStagesAsDefaultWhenTypeIsOmitted() throws Exception { Path configFile = tempDir.resolve("pipeline.yaml"); String yamlContent = "pipeline:\n" + " stages:\n" + - " - location: com.example.system1\n" + - " type: importer\n" + - " - location: com.example.system2\n" + - " type: importer\n" + - " - location: com.example.changes\n"; + " - location: com.example.migrations\n" + + " - location: com.example.cleanup\n"; Files.write(configFile, yamlContent.getBytes()); - + EnableFlamingock annotation = createMockAnnotationWithFile("pipeline.yaml"); Map> changes = createMockChangesMap(); PipelineStructureBuilder builder = newBuilder(); - // When & Then - should throw RuntimeException - Exception exception = assertThrows(Exception.class, () -> - buildPipelineFromFile(builder, annotation, changes)); - - Throwable cause = exception.getCause(); - if (cause instanceof RuntimeException) { - assertTrue(cause.getMessage().contains("Multiple SYSTEM stages are not allowed"), - "Should have error about multiple SYSTEM stages in YAML"); - } else { - assertTrue(exception.getMessage().contains("Multiple SYSTEM stages are not allowed"), - "Should have error about multiple SYSTEM stages in YAML"); - } + PreviewPipeline pipeline = buildPipelineFromFile(builder, annotation, changes); + + assertNotNull(pipeline, "Pipeline should be created"); + assertEquals(2, pipeline.getStages().size(), "Should have 2 YAML stages"); + assertTrue(pipeline.getStages().stream().allMatch(stage -> stage.getType() == StageType.DEFAULT), + "All user-declared YAML stages should be DEFAULT"); } // The validateAllChangesAreMappedToStages helper was removed in Phase 3 — its job is now @@ -439,10 +442,15 @@ private EnableFlamingock createMockAnnotationWithNeitherFileNorStages() { } private Stage createMockStage(String name, String location) { + return createMockStage(name, location, StageMode.STANDARD); + } + + private Stage createMockStage(String name, String location, StageMode mode) { return new Stage() { @Override public String name() { return name; } @Override public String description() { return ""; } @Override public String location() { return location; } + @Override public StageMode mode() { return mode; } @Override public Class annotationType() { return Stage.class; } }; } diff --git a/core/flamingock-processor/src/test/java/io/flamingock/core/processor/util/FlamingockMetadataMergerTest.java b/core/flamingock-processor/src/test/java/io/flamingock/core/processor/util/FlamingockMetadataMergerTest.java index 940391ae8..cb215769a 100644 --- a/core/flamingock-processor/src/test/java/io/flamingock/core/processor/util/FlamingockMetadataMergerTest.java +++ b/core/flamingock-processor/src/test/java/io/flamingock/core/processor/util/FlamingockMetadataMergerTest.java @@ -16,6 +16,7 @@ package io.flamingock.core.processor.util; import io.flamingock.api.StageType; +import io.flamingock.api.StageMode; import io.flamingock.internal.common.core.metadata.BuilderProviderInfo; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; import io.flamingock.internal.common.core.preview.AbstractPreviewChange; @@ -23,6 +24,7 @@ import io.flamingock.internal.common.core.preview.PreviewPipeline; import io.flamingock.internal.common.core.preview.PreviewStage; import io.flamingock.internal.common.core.preview.SystemPreviewStage; +import io.flamingock.internal.common.core.preview.TemplatePreviewChange; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -322,6 +324,40 @@ void mergePipelinePlacesInRoundChangeInSubPackageOfStage() { "no orphans expected — covers semantics rescues the sub-package change"); } + @Test + @DisplayName("RUN_ALWAYS mode is applied after routing a sub-package change") + void runAlwaysModeIsAppliedAfterRoutingSubPackageChange() { + FlamingockMetadata target = new FlamingockMetadata(); + PreviewPipeline freshStructure = pipeline(stage("changes", "com.example.changes", + StageMode.RUN_ALWAYS)); + CodePreviewChange subPackageChange = change("id-sub", "com.example.changes.sub.MyChange"); + + FlamingockMetadataMerger.applyRound(target, freshStructure, + Collections.singletonList(subPackageChange), false); + + PreviewStage placed = first(target.getPipeline().getStages()); + assertTrue(first(placed.getChanges()).isRunAlways()); + } + + @Test + @DisplayName("RUN_ALWAYS mode is preserved for template changes kept during merge") + void runAlwaysModeIsAppliedToTemplateChanges() { + FlamingockMetadata target = new FlamingockMetadata(); + TemplatePreviewChange template = new TemplatePreviewChange( + "change.yaml", "template-1", "001", "author", "template", + Collections.emptyList(), null, false, false, + null, null, null, null, null, null); + PreviewPipeline freshStructure = pipeline(new PreviewStage( + "templates", StageType.DEFAULT, StageMode.RUN_ALWAYS, null, + null, "resources/migrations", Collections.singletonList(template))); + + FlamingockMetadataMerger.applyRound(target, freshStructure, + Collections.emptyList(), false); + + AbstractPreviewChange placed = first(first(target.getPipeline().getStages()).getChanges()); + assertTrue(placed.isRunAlways()); + } + @Test @DisplayName("F3: longest-prefix wins when multiple stages cover a change's package") void mergePipelineLongestPrefixWins() { @@ -436,7 +472,11 @@ private static CodePreviewChange systemChange(String id, String fqcn) { } private static PreviewStage stage(String name, String sourcesPackage, AbstractPreviewChange... changes) { - return new PreviewStage(name, StageType.DEFAULT, null, sourcesPackage, null, Arrays.asList(changes)); + return new PreviewStage(name, StageType.DEFAULT, StageMode.STANDARD, null, sourcesPackage, null, Arrays.asList(changes)); + } + + private static PreviewStage stage(String name, String sourcesPackage, StageMode mode) { + return new PreviewStage(name, StageType.DEFAULT, mode, null, sourcesPackage, null, new ArrayList<>()); } private static PreviewPipeline pipeline(PreviewStage... stages) { diff --git a/core/flamingock-processor/src/test/java/io/flamingock/core/processor/util/FlamingockMetadataStoreTest.java b/core/flamingock-processor/src/test/java/io/flamingock/core/processor/util/FlamingockMetadataStoreTest.java index 4f8ddcf63..044b0f490 100644 --- a/core/flamingock-processor/src/test/java/io/flamingock/core/processor/util/FlamingockMetadataStoreTest.java +++ b/core/flamingock-processor/src/test/java/io/flamingock/core/processor/util/FlamingockMetadataStoreTest.java @@ -16,6 +16,7 @@ package io.flamingock.core.processor.util; import io.flamingock.api.StageType; +import io.flamingock.api.StageMode; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; import io.flamingock.internal.common.core.preview.CodePreviewChange; import io.flamingock.internal.common.core.preview.PreviewPipeline; @@ -122,6 +123,23 @@ void commitWritesOnlyWhenDirty() throws IOException { assertEquals(1, read.getPipeline().getStages().size()); } + @Test + void commitPreservesRunAlwaysModeAndChangeFlag() throws IOException { + Path readPath = tempDir.resolve("does-not-exist.json"); + FakeFiler filer = new FakeFiler(readPath, tempDir); + ProcessingEnvironment env = mockEnv(filer); + FlamingockMetadataStore store = new FlamingockMetadataStore(env, new LoggerPreProcessor(env), METADATA_PATH, REFLECT_PATH); + + store.update(metadata -> metadata.setPipeline(runAlwaysPipeline())); + store.commit(); + + Path metadataOut = tempDir.resolve(METADATA_PATH); + FlamingockMetadata read = JsonObjectMapper.DEFAULT_INSTANCE.readValue(metadataOut.toFile(), FlamingockMetadata.class); + PreviewStage stage = read.getPipeline().getStages().iterator().next(); + assertEquals(StageMode.RUN_ALWAYS, stage.getMode()); + assertTrue(stage.getChanges().iterator().next().isRunAlways()); + } + @Test void peekDoesNotMarkDirty() { FakeFiler filer = new FakeFiler(tempDir.resolve("does-not-exist.json"), tempDir); @@ -151,7 +169,16 @@ private static PreviewPipeline samplePipeline() { CodePreviewChange c = new CodePreviewChange("id-1", null, "author", "com.example.changes.A", "com.example.changes.A", null, null, null, false, null, false, null, null, false); - PreviewStage stage = new PreviewStage("default", StageType.DEFAULT, null, + PreviewStage stage = new PreviewStage("default", StageType.DEFAULT, StageMode.STANDARD, null, + "com.example.changes", null, java.util.Collections.singletonList(c)); + return new PreviewPipeline(Arrays.asList(stage)); + } + + private static PreviewPipeline runAlwaysPipeline() { + CodePreviewChange c = new CodePreviewChange("id-run-always", null, "author", + "com.example.changes.A", "com.example.changes.A", + null, null, null, true, null, false, null, null, false); + PreviewStage stage = new PreviewStage("repeatable", StageType.DEFAULT, StageMode.RUN_ALWAYS, null, "com.example.changes", null, java.util.Collections.singletonList(c)); return new PreviewPipeline(Arrays.asList(stage)); } diff --git a/core/target-systems/flamingock-couchbase-targetsystem/src/test/java/io/flamingock/targetsystem/couchbase/PipelineTestHelper.java b/core/target-systems/flamingock-couchbase-targetsystem/src/test/java/io/flamingock/targetsystem/couchbase/PipelineTestHelper.java index 0b2931ff6..7f71e2d30 100644 --- a/core/target-systems/flamingock-couchbase-targetsystem/src/test/java/io/flamingock/targetsystem/couchbase/PipelineTestHelper.java +++ b/core/target-systems/flamingock-couchbase-targetsystem/src/test/java/io/flamingock/targetsystem/couchbase/PipelineTestHelper.java @@ -16,6 +16,7 @@ package io.flamingock.targetsystem.couchbase; import io.flamingock.api.StageType; +import io.flamingock.api.StageMode; import io.flamingock.api.annotations.Change; import io.flamingock.api.annotations.TargetSystem; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; @@ -108,6 +109,7 @@ public static FlamingockMetadata getPreviewPipeline(String stageName, Trio changes = processor.changes.stream() + .collect(Collectors.toMap(CodePreviewChange::getId, Function.identity())); + assertEquals(3, changes.size()); + assertTrue(changes.get("unit").isRunAlways()); + assertTrue(changes.get("always").isRunAlways()); + assertTrue(!changes.get("normal").isRunAlways()); + } + + private static final class CapturingProcessor extends AbstractProcessor { + private final List changes = new ArrayList<>(); + + @Override + public Set getSupportedAnnotationTypes() { + return new java.util.HashSet<>(Arrays.asList( + ChangeUnit.class.getCanonicalName(), + "com.github.cloudyrock.mongock.ChangeLog")); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnvironment) { + if (roundEnvironment.processingOver()) { + return false; + } + MongockCodePreviewChangeHelper helper = new MongockCodePreviewChangeHelper(); + for (javax.lang.model.element.Element element : roundEnvironment.getRootElements()) { + if (element instanceof TypeElement) { + TypeElement type = (TypeElement) element; + if (type.getAnnotation(ChangeUnit.class) != null + || type.getAnnotation(com.github.cloudyrock.mongock.ChangeLog.class) != null) { + changes.addAll(helper.getCodePreviewChanges(type, "test-target")); + } + } + } + return false; + } + } + + private static final class StringJavaFileObject extends javax.tools.SimpleJavaFileObject { + private final String source; + + private StringJavaFileObject(String className, String source) { + super(URI.create("string:///" + className + JavaFileObject.Kind.SOURCE.extension), + JavaFileObject.Kind.SOURCE); + this.source = source; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return source; + } + } +} diff --git a/utils/test-util/src/main/java/io/flamingock/common/test/cloud/deprecated/MockRunnerServerOld.java b/utils/test-util/src/main/java/io/flamingock/common/test/cloud/deprecated/MockRunnerServerOld.java index 3a0754a96..89f1c842a 100644 --- a/utils/test-util/src/main/java/io/flamingock/common/test/cloud/deprecated/MockRunnerServerOld.java +++ b/utils/test-util/src/main/java/io/flamingock/common/test/cloud/deprecated/MockRunnerServerOld.java @@ -190,7 +190,7 @@ public MockRunnerServerOld addSimpleStageExecutionPlan(Long executionId, String CloudTargetSystemAuditMarkType cloudStatus = operation != null ? CloudTargetSystemAuditMarkType.valueOf(operation.name()) : CloudTargetSystemAuditMarkType.NONE; - return new ChangeRequest(auditEntryExpectation.getChangeId(), cloudStatus, auditEntryExpectation.isTransactional()); + return new ChangeRequest(auditEntryExpectation.getChangeId(), cloudStatus, null, auditEntryExpectation.isTransactional(), false); }) .collect(Collectors.toList()); @@ -218,7 +218,7 @@ public MockRunnerServerOld addMultipleStageExecutionPlan(Long executionId, List< CloudTargetSystemAuditMarkType cloudStatus = operation != null ? CloudTargetSystemAuditMarkType.valueOf(operation.name()) : CloudTargetSystemAuditMarkType.NONE; - return new ChangeRequest(auditEntryExpectation.getChangeId(), cloudStatus, auditEntryExpectation.isTransactional()); + return new ChangeRequest(auditEntryExpectation.getChangeId(), cloudStatus, null, auditEntryExpectation.isTransactional(), false); }) .collect(Collectors.toList()); diff --git a/utils/test-util/src/main/java/io/flamingock/common/test/cloud/prototype/PrototypeChange.java b/utils/test-util/src/main/java/io/flamingock/common/test/cloud/prototype/PrototypeChange.java index dbd888019..15d3c97b0 100644 --- a/utils/test-util/src/main/java/io/flamingock/common/test/cloud/prototype/PrototypeChange.java +++ b/utils/test-util/src/main/java/io/flamingock/common/test/cloud/prototype/PrototypeChange.java @@ -52,11 +52,11 @@ public ChangeRequest toExecutionPlanChangeRequest(TargetSystemAuditMarkType ongo CloudTargetSystemAuditMarkType cloudStatus = ongoingStatus != null ? CloudTargetSystemAuditMarkType.valueOf(ongoingStatus.name()) : CloudTargetSystemAuditMarkType.NONE; - return new ChangeRequest(changeId, cloudStatus, transactional); + return new ChangeRequest(changeId, cloudStatus, null, transactional, false); } public ChangeRequest toExecutionPlanChangeRequest() { - return new ChangeRequest(changeId, CloudTargetSystemAuditMarkType.NONE, transactional); + return new ChangeRequest(changeId, CloudTargetSystemAuditMarkType.NONE, null, transactional, false); } public ChangeResponse toExecutionPlanChangeResponse(CloudChangeAction state) { diff --git a/utils/test-util/src/main/java/io/flamingock/common/test/pipeline/PipelineTestHelper.java b/utils/test-util/src/main/java/io/flamingock/common/test/pipeline/PipelineTestHelper.java index 144626565..6abf56e20 100644 --- a/utils/test-util/src/main/java/io/flamingock/common/test/pipeline/PipelineTestHelper.java +++ b/utils/test-util/src/main/java/io/flamingock/common/test/pipeline/PipelineTestHelper.java @@ -16,6 +16,7 @@ package io.flamingock.common.test.pipeline; import io.flamingock.api.StageType; +import io.flamingock.api.StageMode; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; import io.flamingock.internal.common.core.metadata.MetadataLoader; import io.flamingock.internal.common.core.preview.AbstractPreviewChange; @@ -61,6 +62,7 @@ public static FlamingockMetadata getPreviewPipeline(String stageName, List