Skip to content

Commit 2c637c9

Browse files
committed
refactor: wip
1 parent 661b70f commit 2c637c9

11 files changed

Lines changed: 260 additions & 60 deletions

File tree

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/response/data/ChangeResult.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ public boolean isAlreadyApplied() {
113113
return status == ChangeStatus.ALREADY_APPLIED;
114114
}
115115

116+
public boolean isNotReached() {
117+
return status == ChangeStatus.NOT_REACHED;
118+
}
119+
116120
public static Builder builder() {
117121
return new Builder();
118122
}

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/response/data/ChangeStatus.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,33 @@
2020
*/
2121
public enum ChangeStatus {
2222
/**
23-
* Change was successfully applied.
23+
* Change was successfully applied during this run by the operation.
2424
*/
2525
APPLIED,
2626

2727
/**
28-
* Change was already applied in a previous execution.
28+
* Change was found already applied — either by the executor (SKIP action during this run)
29+
* or by the planner (audit-confirmed without invoking the executor).
2930
*/
3031
ALREADY_APPLIED,
3132

3233
/**
33-
* Change failed during execution.
34+
* Change failed during execution this run.
3435
*/
3536
FAILED,
3637

3738
/**
38-
* Change failed but was successfully rolled back.
39+
* Change failed during execution this run and was successfully rolled back (transactional
40+
* auto-rollback).
3941
*/
4042
ROLLED_BACK,
4143

4244
/**
43-
* Change was not reached due to a prior failure.
45+
* No positive information about this change in this run. Either the operation didn't process
46+
* it (executor stopped on a prior failure, the stage was unreached, etc.) or the planner
47+
* found no audit entry confirming it was applied. Default initial status assigned to every
48+
* loaded change at {@code PipelineRun} construction; writers transition records forward as
49+
* they learn facts.
4450
*/
4551
NOT_REACHED
4652
}

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/response/data/ExecuteResponseData.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public class ExecuteResponseData {
4242
private int appliedChanges;
4343
private int skippedChanges;
4444
private int failedChanges;
45+
private int notReachedChanges;
4546

4647
// Per-stage breakdown
4748
private List<StageResult> stages;
@@ -64,6 +65,7 @@ private ExecuteResponseData(Builder builder) {
6465
this.appliedChanges = builder.appliedChanges;
6566
this.skippedChanges = builder.skippedChanges;
6667
this.failedChanges = builder.failedChanges;
68+
this.notReachedChanges = builder.notReachedChanges;
6769
this.stages = builder.stages != null ? builder.stages : new ArrayList<>();
6870
}
6971

@@ -171,6 +173,14 @@ public void setFailedChanges(int failedChanges) {
171173
this.failedChanges = failedChanges;
172174
}
173175

176+
public int getNotReachedChanges() {
177+
return notReachedChanges;
178+
}
179+
180+
public void setNotReachedChanges(int notReachedChanges) {
181+
this.notReachedChanges = notReachedChanges;
182+
}
183+
174184
public List<StageResult> getStages() {
175185
return stages;
176186
}
@@ -205,6 +215,7 @@ public static class Builder {
205215
private int appliedChanges;
206216
private int skippedChanges;
207217
private int failedChanges;
218+
private int notReachedChanges;
208219
private List<StageResult> stages = new ArrayList<>();
209220

210221
public Builder status(ExecutionStatus status) {
@@ -272,6 +283,11 @@ public Builder failedChanges(int failedChanges) {
272283
return this;
273284
}
274285

286+
public Builder notReachedChanges(int notReachedChanges) {
287+
this.notReachedChanges = notReachedChanges;
288+
return this;
289+
}
290+
275291
public Builder stages(List<StageResult> stages) {
276292
this.stages = stages;
277293
return this;

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/response/data/ExecutionReportFormatter.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,11 @@ public static String summary(ExecuteResponseData result) {
7373
}
7474

7575
String counts = String.format(
76-
"; changes applied=%d, failed=%d, skipped=%d; duration=%dms",
76+
"; changes newly_applied=%d, already_applied=%d, failed=%d, not_reached=%d; duration=%dms",
7777
result.getAppliedChanges(),
78-
result.getFailedChanges(),
7978
result.getSkippedChanges(),
79+
result.getFailedChanges(),
80+
result.getNotReachedChanges(),
8081
result.getTotalDurationMs());
8182

8283
StringBuilder sb = new StringBuilder(headline).append(counts);
@@ -110,9 +111,10 @@ public static String report(ExecuteResponseData result) {
110111
.append(NEWLINE);
111112
sb.append(" Changes: ")
112113
.append(result.getTotalChanges()).append(" total — ")
113-
.append(result.getAppliedChanges()).append(" applied, ")
114-
.append(result.getSkippedChanges()).append(" already at target state, ")
115-
.append(result.getFailedChanges()).append(" failed")
114+
.append(result.getAppliedChanges()).append(" newly applied, ")
115+
.append(result.getSkippedChanges()).append(" already applied, ")
116+
.append(result.getFailedChanges()).append(" failed, ")
117+
.append(result.getNotReachedChanges()).append(" not reached")
116118
.append(NEWLINE);
117119

118120
// Per-stage breakdown: include any stage the operation touched (state != NOT_STARTED) OR
@@ -181,7 +183,7 @@ private static void appendStageBlock(StringBuilder sb, StageResult stage) {
181183
: 0;
182184
int reportableCount = alreadyAppliedCount > 0 ? alreadyAppliedCount : stage.getTotalChanges();
183185
sb.append(" ").append(label).append(' ').append(name)
184-
.append(" (").append(reportableCount).append(" changes already at target state)")
186+
.append(" (").append(reportableCount).append(" changes already applied)")
185187
.append(NEWLINE);
186188
return;
187189
}
@@ -195,10 +197,12 @@ private static void appendStageBlock(StringBuilder sb, StageResult stage) {
195197
// ROLLED_BACK is widened into the failed bucket — the change did not succeed even though
196198
// the system was left clean. See PipelineRun.toResponse() for the matching aggregate logic.
197199
int failed = (int) changes.stream().filter(c -> c != null && (c.isFailed() || c.isRolledBack())).count();
200+
int notReached = (int) changes.stream().filter(c -> c != null && c.isNotReached()).count();
198201
sb.append(" changes: ")
199-
.append(applied).append(" applied, ")
200-
.append(skipped).append(" already at target state, ")
201-
.append(failed).append(" failed")
202+
.append(applied).append(" newly applied, ")
203+
.append(skipped).append(" already applied, ")
204+
.append(failed).append(" failed, ")
205+
.append(notReached).append(" not reached")
202206
.append(NEWLINE);
203207

204208
StageState state = stage.getState();

core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/response/data/ExecutionReportFormatterTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,10 @@ void reportSuccessHeadlineAndPerStageBlock() {
122122
String out = ExecutionReportFormatter.report(result);
123123
assertTrue(out.contains("Flamingock execution report — SUCCESS"), out);
124124
assertTrue(out.contains("Stages: 1 total — 1 completed, 0 failed, 0 up to date, 0 not reached"), out);
125-
assertTrue(out.contains("Changes: 1 total — 1 applied, 0 already at target state, 0 failed"), out);
125+
assertTrue(out.contains("Changes: 1 total — 1 newly applied, 0 already applied, 0 failed, 0 not reached"), out);
126126
assertTrue(out.contains("[COMPLETED]"), out);
127127
assertTrue(out.contains("only-stage"), out);
128-
assertTrue(out.contains("1 applied, 0 already at target state, 0 failed"), out);
128+
assertTrue(out.contains("1 newly applied, 0 already applied, 0 failed, 0 not reached"), out);
129129
assertFalse(out.contains("Not reached"), "no Not-reached section when all stages reached: " + out);
130130
}
131131

@@ -243,7 +243,7 @@ void reportCountsRolledBackChangeAsFailedAndListsItsId() {
243243
.build();
244244

245245
String out = ExecutionReportFormatter.report(result);
246-
assertTrue(out.contains("0 applied, 5 already at target state, 1 failed"),
246+
assertTrue(out.contains("0 newly applied, 5 already applied, 1 failed"),
247247
"per-stage block must count ROLLED_BACK as failed: " + out);
248248
assertTrue(out.contains("failed change(s): InsertEmployeeSeedData"),
249249
"failed change(s) line must include the rolled-back change ID: " + out);
@@ -292,12 +292,12 @@ void reportAllUpToDateShowsNoChangesHeadlineAndUpToDateRowsInBreakdown() {
292292
assertTrue(out.contains("Flamingock execution report — NO CHANGES"),
293293
"headline must render NO_CHANGES with a space: " + out);
294294
assertTrue(out.contains("Stages: 1 total — 0 completed, 0 failed, 1 up to date, 0 not reached"), out);
295-
assertTrue(out.contains("Changes: 6 total — 0 applied, 6 already at target state, 0 failed"), out);
295+
assertTrue(out.contains("Changes: 6 total — 0 newly applied, 6 already applied, 0 failed, 0 not reached"), out);
296296
assertTrue(out.contains("Per-stage breakdown:"),
297297
"per-stage section should include [UP TO DATE] rows: " + out);
298298
assertTrue(out.contains("[UP TO DATE]"), out);
299299
assertTrue(out.contains("database-init"), out);
300-
assertTrue(out.contains("(6 changes already at target state)"), out);
300+
assertTrue(out.contains("(6 changes already applied)"), out);
301301
assertFalse(out.contains("Not reached"),
302302
"Not-reached section must be omitted when nothing reached and nothing not-evaluated: " + out);
303303
}
@@ -321,7 +321,7 @@ void reportAllUpToDateForCloudCaseRendersCountOnlyWhenChangesEmpty() {
321321

322322
String out = ExecutionReportFormatter.report(result);
323323
assertTrue(out.contains("[UP TO DATE]"), out);
324-
assertTrue(out.contains("(6 changes already at target state)"),
324+
assertTrue(out.contains("(6 changes already applied)"),
325325
"cloud-style up-to-date row should use totalChanges when per-change list is empty: " + out);
326326
}
327327

@@ -359,14 +359,14 @@ void reportPartialCoverageMixesReachedUpToDateAndNotReached() {
359359
ExecuteResponseData result = ExecuteResponseData.builder()
360360
.status(ExecutionStatus.FAILED)
361361
.totalStages(4).completedStages(1).upToDateStages(1).notReachedStages(1).failedStages(1)
362-
.totalChanges(10).appliedChanges(3).skippedChanges(3).failedChanges(1)
362+
.totalChanges(10).appliedChanges(3).skippedChanges(3).failedChanges(1).notReachedChanges(3)
363363
.totalDurationMs(170)
364364
.stages(Arrays.asList(block1, block2, block3, block4))
365365
.build();
366366

367367
String out = ExecutionReportFormatter.report(result);
368368
assertTrue(out.contains("Stages: 4 total — 1 completed, 1 failed, 1 up to date, 1 not reached"), out);
369-
assertTrue(out.contains("Changes: 10 total — 3 applied, 3 already at target state, 1 failed"), out);
369+
assertTrue(out.contains("Changes: 10 total — 3 newly applied, 3 already applied, 1 failed, 3 not reached"), out);
370370
assertTrue(out.contains("Per-stage breakdown:"), out);
371371
assertTrue(out.contains("[COMPLETED] block-1"), out);
372372
assertTrue(out.contains("[UP TO DATE]") && out.contains("block-2"), out);

core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/run/PipelineRun.java

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,10 @@
3434
import java.util.ArrayList;
3535
import java.util.Arrays;
3636
import java.util.Collections;
37-
import java.util.HashSet;
37+
import java.util.HashMap;
3838
import java.util.LinkedHashMap;
3939
import java.util.List;
4040
import java.util.Map;
41-
import java.util.Set;
4241
import java.util.stream.Collectors;
4342

4443
public class PipelineRun {
@@ -165,13 +164,16 @@ public void markStageStarted(String stageName) {
165164
}
166165

167166
public void markStageCompleted(String stageName, StageResult result) {
168-
// Incoming result from the executor already carries state=COMPLETED. Preserve the
169-
// totalChanges / plannerVerdict fields set on the in-memory StageResult before
170-
// execution started — the executor doesn't know about them and would otherwise reset
171-
// them to defaults.
167+
// Incoming result from the executor already carries state=COMPLETED. Merge per-change
168+
// records by ID: the executor's records win for the IDs it processed; records for
169+
// changes the executor didn't process (i.e. NOT_REACHED records from PipelineRun
170+
// construction) survive. Also preserve totalChanges / plannerVerdict — the executor
171+
// doesn't know about them.
172172
StageRun run = lookupOrThrow(stageName);
173173
StageResult current = run.getResult();
174+
List<ChangeResult> merged = mergeChangeRecords(current.getChanges(), result.getChanges());
174175
run.setResult(StageResult.builder(result)
176+
.changes(merged)
175177
.totalChanges(current.getTotalChanges())
176178
.plannerVerdict(current.getPlannerVerdict())
177179
.build());
@@ -186,13 +188,45 @@ public void markStageFailed(String stageName, StageExecutionException exception)
186188
);
187189
StageRun run = lookupOrThrow(stageName);
188190
StageResult current = run.getResult();
191+
// Same merge semantics as markStageCompleted: executor's records win for processed
192+
// IDs; NOT_REACHED records for changes the executor never reached (stage stopped on
193+
// a failure) stay intact.
194+
List<ChangeResult> merged = mergeChangeRecords(current.getChanges(), resultFromExecutor.getChanges());
189195
run.setResult(StageResult.builder(resultFromExecutor)
190196
.state(StageState.failed(errorInfo))
197+
.changes(merged)
191198
.totalChanges(current.getTotalChanges())
192199
.plannerVerdict(current.getPlannerVerdict())
193200
.build());
194201
}
195202

203+
/**
204+
* Merges two per-change record lists keyed by change ID. Incoming records (typically from
205+
* the executor) take precedence for the IDs they carry; existing records (e.g. initial
206+
* NOT_REACHED entries from {@code StageRun} construction) for any other ID survive. Order
207+
* follows the first occurrence — existing entries keep their position; new IDs from
208+
* {@code incoming} are appended.
209+
*/
210+
private static List<ChangeResult> mergeChangeRecords(List<ChangeResult> existing,
211+
List<ChangeResult> incoming) {
212+
LinkedHashMap<String, ChangeResult> byId = new LinkedHashMap<>();
213+
if (existing != null) {
214+
for (ChangeResult r : existing) {
215+
if (r != null && r.getChangeId() != null) {
216+
byId.put(r.getChangeId(), r);
217+
}
218+
}
219+
}
220+
if (incoming != null) {
221+
for (ChangeResult r : incoming) {
222+
if (r != null && r.getChangeId() != null) {
223+
byId.put(r.getChangeId(), r);
224+
}
225+
}
226+
}
227+
return new ArrayList<>(byId.values());
228+
}
229+
196230
/**
197231
* Stage-scope failure for non-{@link StageExecutionException} Throwables (no enriched
198232
* {@link StageResult} from the executor; we rebuild the existing one with a Failed state).
@@ -256,11 +290,15 @@ public void markStageVerdict(String stageName, PlannerVerdict verdict) {
256290
}
257291

258292
/**
259-
* Adds {@link ChangeStatus#ALREADY_APPLIED} {@link ChangeResult}s for the given change IDs.
260-
* Defensive merge: for any change ID the operation has already recorded a result for, the
261-
* planner's would-be record is dropped — the operation's observation is more authoritative.
262-
* This keeps the iterative planner→operation→planner cycle safe when audit re-reads see
263-
* APPLIED entries the operation just wrote.
293+
* Upgrades {@link ChangeStatus#NOT_REACHED} records to {@link ChangeStatus#ALREADY_APPLIED}
294+
* for the given change IDs. Defensive merge: only upgrades records currently at NOT_REACHED;
295+
* any record the operation (or a prior writer) has already populated with positive info
296+
* (APPLIED / FAILED / ROLLED_BACK / ALREADY_APPLIED) is left untouched.
297+
*
298+
* <p>This codifies "operation wins, planner fills gaps" against the fully-populated record
299+
* set that {@code StageRun} initializes with NOT_REACHED entries. As a fallback, IDs without
300+
* an existing record (shouldn't happen — every loaded change starts with one — but defensive)
301+
* get appended as fresh ALREADY_APPLIED.
264302
*/
265303
public void markStageAlreadyAppliedFromAudit(String stageName, List<String> changeIds) {
266304
if (changeIds == null || changeIds.isEmpty()) {
@@ -271,21 +309,34 @@ public void markStageAlreadyAppliedFromAudit(String stageName, List<String> chan
271309
List<ChangeResult> merged = current.getChanges() != null
272310
? new ArrayList<>(current.getChanges())
273311
: new ArrayList<>();
274-
Set<String> existing = new HashSet<>();
275-
for (ChangeResult c : merged) {
312+
Map<String, Integer> indexById = new HashMap<>();
313+
for (int i = 0; i < merged.size(); i++) {
314+
ChangeResult c = merged.get(i);
276315
if (c != null && c.getChangeId() != null) {
277-
existing.add(c.getChangeId());
316+
indexById.put(c.getChangeId(), i);
278317
}
279318
}
280319
for (String id : changeIds) {
281-
if (id == null || existing.contains(id)) {
320+
if (id == null) {
321+
continue;
322+
}
323+
Integer idx = indexById.get(id);
324+
if (idx == null) {
325+
// No existing record (defensive): append.
326+
merged.add(ChangeResult.builder()
327+
.changeId(id)
328+
.status(ChangeStatus.ALREADY_APPLIED)
329+
.build());
330+
indexById.put(id, merged.size() - 1);
282331
continue;
283332
}
284-
merged.add(ChangeResult.builder()
285-
.changeId(id)
286-
.status(ChangeStatus.ALREADY_APPLIED)
287-
.build());
288-
existing.add(id);
333+
if (merged.get(idx).isNotReached()) {
334+
merged.set(idx, ChangeResult.builder()
335+
.changeId(id)
336+
.status(ChangeStatus.ALREADY_APPLIED)
337+
.build());
338+
}
339+
// else: operation (or prior planner write) has positive info — respect it.
289340
}
290341
run.setResult(StageResult.builder(current)
291342
.changes(merged)
@@ -327,6 +378,7 @@ public ExecuteResponseData toResponse() {
327378
int appliedChanges = 0;
328379
int skippedChanges = 0;
329380
int failedChanges = 0;
381+
int notReachedChanges = 0;
330382
boolean anyFailed = false;
331383

332384
for (StageRun stageRun : stageRuns) {
@@ -369,6 +421,8 @@ public ExecuteResponseData toResponse() {
369421
// ROLLED_BACK counts as a failed attempt for the user-facing aggregate.
370422
// The per-change record retains the precise status for downstream consumers.
371423
failedChanges++;
424+
} else if (change.getStatus() == ChangeStatus.NOT_REACHED) {
425+
notReachedChanges++;
372426
}
373427
}
374428
}
@@ -403,6 +457,7 @@ public ExecuteResponseData toResponse() {
403457
.appliedChanges(appliedChanges)
404458
.skippedChanges(skippedChanges)
405459
.failedChanges(failedChanges)
460+
.notReachedChanges(notReachedChanges)
406461
.stages(stages)
407462
.build();
408463
}

0 commit comments

Comments
 (0)