From ed07472dadb378ab74bf0809866955196cd455ed Mon Sep 17 00:00:00 2001 From: vsxd Date: Thu, 19 Mar 2026 09:50:07 +0800 Subject: [PATCH] fix: resolve duplicate flyway migration versions --- docs/16-backend-time-inventory.md | 6 +- ...ptz.sql => V24__api_token_timestamptz.sql} | 0 ...25__account_merge_request_timestamptz.sql} | 0 ...sql => V26__skill_version_timestamptz.sql} | 0 .../db/FlywayMigrationGuardrailTest.java | 95 +++++++++++++++++++ 5 files changed, 98 insertions(+), 3 deletions(-) rename server/skillhub-app/src/main/resources/db/migration/{V13__api_token_timestamptz.sql => V24__api_token_timestamptz.sql} (100%) rename server/skillhub-app/src/main/resources/db/migration/{V14__account_merge_request_timestamptz.sql => V25__account_merge_request_timestamptz.sql} (100%) rename server/skillhub-app/src/main/resources/db/migration/{V15__skill_version_timestamptz.sql => V26__skill_version_timestamptz.sql} (100%) create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/db/FlywayMigrationGuardrailTest.java diff --git a/docs/16-backend-time-inventory.md b/docs/16-backend-time-inventory.md index 0f46e93b..565b444f 100644 --- a/docs/16-backend-time-inventory.md +++ b/docs/16-backend-time-inventory.md @@ -99,11 +99,11 @@ - `V12__governance_notifications.sql` - `user_notification.created_at / read_at` -- `V13__api_token_timestamptz.sql` +- `V24__api_token_timestamptz.sql` - `api_token.expires_at / last_used_at / revoked_at / created_at` -- `V14__account_merge_request_timestamptz.sql` +- `V25__account_merge_request_timestamptz.sql` - `account_merge_request.token_expires_at / completed_at / created_at` -- `V15__skill_version_timestamptz.sql` +- `V26__skill_version_timestamptz.sql` - `skill_version.published_at / created_at / yanked_at` - `V16__skill_hidden_at_timestamptz.sql` - `skill.hidden_at` diff --git a/server/skillhub-app/src/main/resources/db/migration/V13__api_token_timestamptz.sql b/server/skillhub-app/src/main/resources/db/migration/V24__api_token_timestamptz.sql similarity index 100% rename from server/skillhub-app/src/main/resources/db/migration/V13__api_token_timestamptz.sql rename to server/skillhub-app/src/main/resources/db/migration/V24__api_token_timestamptz.sql diff --git a/server/skillhub-app/src/main/resources/db/migration/V14__account_merge_request_timestamptz.sql b/server/skillhub-app/src/main/resources/db/migration/V25__account_merge_request_timestamptz.sql similarity index 100% rename from server/skillhub-app/src/main/resources/db/migration/V14__account_merge_request_timestamptz.sql rename to server/skillhub-app/src/main/resources/db/migration/V25__account_merge_request_timestamptz.sql diff --git a/server/skillhub-app/src/main/resources/db/migration/V15__skill_version_timestamptz.sql b/server/skillhub-app/src/main/resources/db/migration/V26__skill_version_timestamptz.sql similarity index 100% rename from server/skillhub-app/src/main/resources/db/migration/V15__skill_version_timestamptz.sql rename to server/skillhub-app/src/main/resources/db/migration/V26__skill_version_timestamptz.sql diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/db/FlywayMigrationGuardrailTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/db/FlywayMigrationGuardrailTest.java new file mode 100644 index 00000000..ee23aee6 --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/db/FlywayMigrationGuardrailTest.java @@ -0,0 +1,95 @@ +package com.iflytek.skillhub.db; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + +class FlywayMigrationGuardrailTest { + + private static final Pattern VERSIONED_MIGRATION_PATTERN = + Pattern.compile("^V(?\\d+)__(?.+)\\.sql$"); + + @Test + void versionedMigrations_mustUseUniqueVersions() throws IOException { + Map> versions = new LinkedHashMap<>(); + + for (Path file : migrationFiles()) { + Matcher matcher = VERSIONED_MIGRATION_PATTERN.matcher(file.getFileName().toString()); + if (!matcher.matches()) { + continue; + } + int version = Integer.parseInt(matcher.group("version")); + versions.computeIfAbsent(version, ignored -> new ArrayList<>()) + .add(relativeToRepo(file)); + } + + List duplicates = versions.entrySet().stream() + .filter(entry -> entry.getValue().size() > 1) + .map(entry -> "V" + entry.getKey() + " -> " + entry.getValue()) + .toList(); + + assertThat(duplicates).isEmpty(); + } + + @Test + void versionedMigrations_mustRemainContiguous() throws IOException { + List versions = migrationFiles().stream() + .map(path -> VERSIONED_MIGRATION_PATTERN.matcher(path.getFileName().toString())) + .filter(Matcher::matches) + .map(matcher -> Integer.parseInt(matcher.group("version"))) + .sorted() + .toList(); + + List gaps = new ArrayList<>(); + for (int expected = 1; expected <= versions.size(); expected++) { + int actual = versions.get(expected - 1); + if (actual != expected) { + gaps.add("expected V" + expected + " but found V" + actual); + } + } + + assertThat(gaps).isEmpty(); + } + + @Test + void migrationFiles_mustMatchFlywayVersionedNaming() throws IOException { + List invalidFiles = migrationFiles().stream() + .map(path -> path.getFileName().toString()) + .filter(name -> !VERSIONED_MIGRATION_PATTERN.matcher(name).matches()) + .sorted() + .toList(); + + assertThat(invalidFiles).isEmpty(); + } + + private List migrationFiles() throws IOException { + Path root = repoRoot() + .resolve("server") + .resolve("skillhub-app") + .resolve("src/main/resources/db/migration"); + try (var stream = Files.list(root)) { + return stream + .filter(path -> path.getFileName().toString().endsWith(".sql")) + .sorted(Comparator.comparing(path -> path.getFileName().toString())) + .toList(); + } + } + + private Path repoRoot() { + return Path.of("").toAbsolutePath().getParent().getParent(); + } + + private String relativeToRepo(Path file) { + return repoRoot().relativize(file).toString(); + } +}