diff --git a/jOOQ/src/main/java/org/jooq/ContentType.java b/jOOQ/src/main/java/org/jooq/ContentType.java index 39934e942d..a3fb1d647f 100644 --- a/jOOQ/src/main/java/org/jooq/ContentType.java +++ b/jOOQ/src/main/java/org/jooq/ContentType.java @@ -70,10 +70,23 @@ public enum ContentType { * similar statements, which are applied as increments in a migration using * {@link Meta#apply(Queries)}. *

- * Within the same {@link Commit}. + * Within the same {@link Commit}, increments are sorted according to their + * {@link File#path()}. */ INCREMENT, + /** + * The file contains decrement information. + *

+ * Decrements work like {@link #INCREMENT} typed files, but are applied only + * when downgrading to a previous version, decrements are sorted in reverse + * order according to their {@link File#path()}. + *

+ * This API is part of a commercial only feature. To use this feature, + * please use the jOOQ Professional Edition or the jOOQ Enterprise Edition. + */ + DECREMENT, + /** * The file contains a script. *

diff --git a/jOOQ/src/main/java/org/jooq/impl/CommitImpl.java b/jOOQ/src/main/java/org/jooq/impl/CommitImpl.java index 1417631ee3..967b53a1d0 100644 --- a/jOOQ/src/main/java/org/jooq/impl/CommitImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/CommitImpl.java @@ -37,6 +37,7 @@ */ package org.jooq.impl; +import static org.jooq.ContentType.DECREMENT; import static org.jooq.ContentType.INCREMENT; import static org.jooq.ContentType.SCHEMA; import static org.jooq.ContentType.SNAPSHOT; @@ -63,16 +64,15 @@ import java.util.Set; import org.jooq.Commit; import org.jooq.Configuration; -import org.jooq.ContentType; import org.jooq.DSLContext; import org.jooq.File; import org.jooq.Files; import org.jooq.Meta; import org.jooq.Node; +// ... import org.jooq.Source; import org.jooq.Tag; import org.jooq.Version; -import org.jooq.exception.DataMigrationException; import org.jooq.exception.DataMigrationVerificationException; import org.jooq.tools.StringUtils; @@ -277,13 +277,197 @@ final class CommitImpl extends AbstractNode implements Commit { @Override public final Files migrateTo(Commit resultCommit) { + if (equals(resultCommit)) + if (equals(root())) + return FilesImpl.empty(ctx.migrations().version(ROOT)); + else + return FilesImpl.empty(version()); // TODO: Implement reverting a branch up to the common ancestor Commit ancestor = commonAncestor(resultCommit); + + // TODO: The reverse check doesn't take into account branching + if (ancestor.equals(resultCommit)) { + configuration().requireCommercial(() -> "Reverse migrations are a commercial only feature. Please upgrade to the jOOQ Professional Edition or jOOQ Enterprise Edition."); + + + + + } + return migrateTo0(resultCommit); } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + private static final record MigrationHistory( + Map> pathHistory, + Files result + ) { + + + + + + + + + + + + + + + + + + + + + + + + } + private final Files migrateTo0(Commit resultCommit) { + return migrateTo1(resultCommit, false).result(); + } + + private final MigrationHistory migrateTo1(Commit resultCommit, boolean recordPathHistory) { + Map> pathHistory = recordPathHistory ? new HashMap<>() : null; // History are all the files that have been applied before this commit Map history = new LinkedHashMap<>(); @@ -297,18 +481,20 @@ final class CommitImpl extends AbstractNode implements Commit { Map tempHistoryKeys = new HashMap<>(); Deque commitHistory = new ArrayDeque<>(); - history(commitHistory, new HashSet<>(), Arrays.asList(resultCommit)); - boolean recordingResult = false; boolean hasDeletions = false; boolean isRoot = equals(root()); Commit fromSnapshotCommit = null; + history(commitHistory, new HashSet<>(), Arrays.asList(resultCommit)); + commitLoop: for (Commit commit : commitHistory) { List commitFiles = new ArrayList<>(commit.delta()); // [#9506] Migrations from root to a snapshot can be skipped + // TODO: effectively skip all intermediary steps + // TODO: Support pathHistory if (isRoot && anyMatch(commitFiles, f -> f.type() == SNAPSHOT)) { result.clear(); @@ -343,6 +529,8 @@ final class CommitImpl extends AbstractNode implements Commit { tempHistory.remove(path); deletions.remove(); + + // TODO: Support pathHistory } } @@ -362,6 +550,9 @@ final class CommitImpl extends AbstractNode implements Commit { result.put(path, file); increments.remove(); + + if (pathHistory != null) + pathHistory.computeIfAbsent(path, p -> new LinkedHashMap<>()).put(commit.id(), file); } } @@ -384,6 +575,9 @@ final class CommitImpl extends AbstractNode implements Commit { } schemas.remove(); + + if (pathHistory != null) + pathHistory.computeIfAbsent(path, p -> new LinkedHashMap<>()).put(commit.id(), file); } } @@ -405,7 +599,7 @@ final class CommitImpl extends AbstractNode implements Commit { // Altering history is not allowed if (!StringUtils.equals(historicFile.content(), file.content())) - throw new DataMigrationException("Cannot edit increment file that has already been applied: " + file); + throw new DataMigrationVerificationException("Cannot edit increment file that has already been applied: " + file); // History was altered, but the alteration was reverted else @@ -438,13 +632,18 @@ final class CommitImpl extends AbstractNode implements Commit { ? version(from, fromSnapshotCommit.id(), versionFiles, filter(fromSnapshotCommit.delta(), f -> f.type() == SNAPSHOT)) : null; Version to = version(from, resultCommit.id(), versionFiles, result.values()); - return new FilesImpl(from, fromSnapshot, to, result.values()); + return new MigrationHistory( + pathHistory, + new FilesImpl(from, fromSnapshot, to, result.values()) + ); } /** * Breadth first recursion over commit graph. */ private static final void history(Deque commitHistory, Set set, List commits) { + + // TODO: When encountering a snapshot on a single path (no branches), we can abort recursion for (Commit commit : commits) if (set.add(commit)) commitHistory.push(commit); @@ -456,6 +655,8 @@ final class CommitImpl extends AbstractNode implements Commit { if (!p.isEmpty()) { List l = new ArrayList<>(p); Collections.reverse(l); + + // TODO: Use iteration instead of depending on tail recursion optimisation. history(commitHistory, set, l); } } diff --git a/jOOQ/src/main/java/org/jooq/impl/FilesImpl.java b/jOOQ/src/main/java/org/jooq/impl/FilesImpl.java index 914f3309ce..79d8bd4828 100644 --- a/jOOQ/src/main/java/org/jooq/impl/FilesImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/FilesImpl.java @@ -37,6 +37,8 @@ */ package org.jooq.impl; +import static java.util.Collections.emptyList; + import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; @@ -63,6 +65,10 @@ final class FilesImpl implements Files { this.files = new ArrayList<>(f); } + static final Files empty(Version version) { + return new FilesImpl(version, null, version, emptyList()); + } + @Override public final Iterator iterator() { return files.iterator(); diff --git a/jOOQ/src/main/java/org/jooq/impl/VersionImpl.java b/jOOQ/src/main/java/org/jooq/impl/VersionImpl.java index b6ec3d227a..390b936222 100644 --- a/jOOQ/src/main/java/org/jooq/impl/VersionImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/VersionImpl.java @@ -195,6 +195,7 @@ final class VersionImpl extends AbstractNode implements Version { if (!target.forceApply()) return meta().migrateTo(target.meta()); + // TODO: Avoid recursion here, and iterate, instead for (Parent parent : target.parents) { result = migrateTo(parent.version, result);