[jOOQ/jOOQ#9506] More work on Migrations API:

- Snapshots will be a commercial only feature
- CommitImpl should reject duplicate paths
- Add the ContentType.SCRIPT implementation
- Files.writeString() wasn't available in JDK 8 yet
This commit is contained in:
Lukas Eder 2024-11-27 07:57:57 +01:00
parent 64107cd10c
commit dfb607c1ff
9 changed files with 150 additions and 103 deletions

View File

@ -38,14 +38,14 @@
package org.jooq.migrations.maven;
import static java.lang.Boolean.TRUE;
import static java.util.Arrays.asList;
import static org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_SOURCES;
import static org.apache.maven.plugins.annotations.ResolutionScope.TEST;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.EnumSet;
import org.jooq.DDLExportConfiguration;
@ -55,7 +55,6 @@ import org.jooq.HistoryVersion;
import org.jooq.Meta;
import org.jooq.Migration;
import org.jooq.Queries;
import org.jooq.exception.DataMigrationVerificationException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
@ -106,6 +105,6 @@ public class AddSnapshotMojo extends AbstractMigrateMojo {
if (getLog().isInfoEnabled())
getLog().info("Writing snapshot to: " + file + "\n" + export);
Files.writeString(file.toPath(), export, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
Files.write(file.toPath(), asList(export), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
}

View File

@ -37,23 +37,17 @@
*/
package org.jooq.migrations.maven;
import static java.lang.Boolean.TRUE;
import static java.util.Arrays.asList;
import static org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_SOURCES;
import static org.apache.maven.plugins.annotations.ResolutionScope.TEST;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;
import java.util.Arrays;
import org.jooq.Commit;
import org.jooq.DDLExportConfiguration;
import org.jooq.DDLFlag;
import org.jooq.History;
import org.jooq.HistoryVersion;
import org.jooq.Meta;
import org.jooq.Migration;
import org.jooq.Queries;
@ -103,7 +97,7 @@ public class AddUntrackedMojo extends AbstractMigrateMojo {
sb.append(untracked);
sb.append("\n");
Files.writeString(file.toPath(), sb.toString(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
Files.write(file.toPath(), asList(sb.toString()), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
}

View File

@ -46,8 +46,18 @@ import org.jetbrains.annotations.ApiStatus.Experimental;
* contained in {@link Commit#delta()} will be processed in the following order:
* <p>
* <ul>
* <li>{@link ContentType#INCREMENT}</li>
* <li>{@link ContentType#SCHEMA}</li>
* <li>{@link #SNAPSHOT} (replacing all other contents of this {@link Commit} as
* well as previous commits, if migrating from {@link Commit#root()})</li>
* <li>{@link #INCREMENT}</li>
* <li>{@link #SCRIPT}</li>
* <li>{@link #SCHEMA}</li>
* </ul>
* <p>
* When undoing a migration, the order is:
* <ul>
* <li>{@link #SCHEMA}</li>
* <li>{@link #DECREMENT}</li>
* </ul>
* </ul>
*/
@Experimental
@ -102,6 +112,9 @@ public enum ContentType {
* <p>
* In order to restore a database, or install a new one, we don't have to go
* back any further than the snapshot.
* <p>
* This API is part of a commercial only feature. To use this feature,
* please use the jOOQ Professional Edition or the jOOQ Enterprise Edition.
*/
SNAPSHOT,

View File

@ -69,6 +69,9 @@ public interface Files extends Iterable<File> {
* <p>
* This is EXPERIMENTAL functionality and subject to change in future jOOQ
* versions.
* <p>
* This API is part of a commercial only feature. To use this feature,
* please use the jOOQ Professional Edition or the jOOQ Enterprise Edition.
*/
@Nullable
@Experimental

View File

@ -68,6 +68,9 @@ public interface Migration extends Scope {
/**
* The last {@link ContentType#SNAPSHOT} commit that is being migrated from,
* or <code>null</code> if there's no snapshot.
* <p>
* This API is part of a commercial only feature. To use this feature,
* please use the jOOQ Professional Edition or the jOOQ Enterprise Edition.
*/
@Nullable
Commit fromSnapshot();

View File

@ -40,10 +40,12 @@ 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.SCRIPT;
import static org.jooq.ContentType.SNAPSHOT;
import static org.jooq.impl.Tools.EMPTY_SOURCE;
import static org.jooq.impl.Tools.anyMatch;
import static org.jooq.impl.Tools.filter;
import static org.jooq.impl.Tools.iterable;
import static org.jooq.tools.StringUtils.isBlank;
import java.util.ArrayDeque;
@ -74,6 +76,7 @@ import org.jooq.Source;
import org.jooq.Tag;
import org.jooq.Version;
import org.jooq.exception.DataMigrationVerificationException;
import org.jooq.impl.DefaultParseContext.IgnoreQuery;
import org.jooq.tools.StringUtils;
/**
@ -108,6 +111,20 @@ final class CommitImpl extends AbstractNode<Commit> implements Commit {
this.tags = new ArrayList<>();
this.delta = map(delta, false);
this.valid = valid;
if (delta.size() > this.delta.size()) {
throw new DataMigrationVerificationException("Path is ambiguous within commit: " + duplicatePath(delta));
}
}
private static final String duplicatePath(Collection<? extends File> files) {
Set<String> paths = new HashSet<>();
for (File file : files)
if (!paths.add(file.path()))
return file.path();
return null;
}
private CommitImpl(CommitImpl copy, boolean newValid) {
@ -397,21 +414,6 @@ final class CommitImpl extends AbstractNode<Commit> implements Commit {
@ -490,74 +492,70 @@ final class CommitImpl extends AbstractNode<Commit> implements Commit {
for (Commit commit : commitHistory) {
List<File> commitFiles = new ArrayList<>(commit.delta());
// [#9506] Migrations from root to a snapshot can be skipped
// TODO: effectively skip all intermediary steps
if (isRoot && anyMatch(commitFiles, f -> f.type() == SNAPSHOT)) {
result.clear();
configuration().requireCommercial(() -> "Snapshots are a commercial only feature. Please upgrade to the jOOQ Professional Edition or jOOQ Enterprise Edition.");
// [#9506] TODO: Are there impacts on other maps?
for (File f : commitFiles)
if (f.type() == SNAPSHOT)
result.put(f.path(), f);
fromSnapshotCommit = commit;
continue commitLoop;
}
// Deletions
Iterator<File> deletions = commitFiles.iterator();
while (deletions.hasNext()) {
File file = deletions.next();
Iterator<File> deletions = filter(commitFiles.iterator(), f -> f.content() == null);
for (File file : iterable(deletions)) {
hasDeletions |= true;
String path = file.path();
String tempKey = tempHistoryKeys.remove(path);
String tempRemove = tempKey != null ? tempKey : path;
String key = historyKeys.remove(path);
String remove = key != null ? key : path;
if (file.content() == null) {
hasDeletions |= true;
String path = file.path();
String tempKey = tempHistoryKeys.remove(path);
String tempRemove = tempKey != null ? tempKey : path;
String key = historyKeys.remove(path);
String remove = key != null ? key : path;
if (recordingResult && result.remove(tempRemove) == null && file.type() == INCREMENT && history.containsKey(tempRemove))
result.put(tempRemove, file);
if (recordingResult && result.remove(tempRemove) == null && file.type() == INCREMENT && history.containsKey(tempRemove))
result.put(tempRemove, file);
else if (recordingResult && result.remove(remove) == null && file.type() == SCHEMA && history.containsKey(remove))
result.put(remove, file);
else
history.remove(tempRemove);
// TODO: Support deletions of scripts
else if (recordingResult && result.remove(remove) == null && file.type() == SCHEMA && history.containsKey(remove))
result.put(remove, file);
else
history.remove(tempRemove);
tempHistory.remove(path);
deletions.remove();
tempHistory.remove(path);
deletions.remove();
}
}
// Increments
Iterator<File> increments = commitFiles.iterator();
while (increments.hasNext()) {
File file = increments.next();
Iterator<File> increments = filter(commitFiles.iterator(), f -> f.type() == INCREMENT);
for (File file : iterable(increments)) {
String path = file.path();
File oldFile = recordingResult ? history.get(path) : history.put(path, file);
if (file.type() == INCREMENT) {
String path = file.path();
File oldFile = recordingResult ? history.get(path) : history.put(path, file);
if (oldFile == null && !tempHistory.isEmpty() && !result.containsKey(path))
move(tempHistory, result, tempHistoryKeys);
if (oldFile == null && !tempHistory.isEmpty() && !result.containsKey(path))
move(tempHistory, result, tempHistoryKeys);
if (recordingResult)
result.put(path, file);
if (recordingResult)
result.put(path, file);
increments.remove();
increments.remove();
}
}
@ -572,34 +570,45 @@ final class CommitImpl extends AbstractNode<Commit> implements Commit {
// Script files
Iterator<File> scripts = filter(commitFiles.iterator(), f -> f.type() == SCRIPT);
for (File file : iterable(scripts)) {
String path = file.path();
File oldFile = recordingResult ? history.get(path) : history.put(path, file);
if (oldFile == null && !tempHistory.isEmpty() && !result.containsKey(path))
move(tempHistory, result, tempHistoryKeys);
if (recordingResult)
result.put(path, file);
scripts.remove();
}
// Schema files
Iterator<File> schemas = commitFiles.iterator();
while (schemas.hasNext()) {
File file = schemas.next();
if (file.type() == SCHEMA) {
String path = file.path();
String key = commit.id() + "-" + path;
if (recordingResult) {
tempHistory.put(path, file);
tempHistoryKeys.put(path, key);
}
else {
history.put(key, file);
historyKeys.put(path, key);
}
schemas.remove();
Iterator<File> schemas = filter(commitFiles.iterator(), f -> f.type() == SCHEMA);
for (File file : iterable(schemas)) {
String path = file.path();
String key = commit.id() + "-" + path;
if (recordingResult) {
tempHistory.put(path, file);
tempHistoryKeys.put(path, key);
}
else {
history.put(key, file);
historyKeys.put(path, key);
}
schemas.remove();
}
recordingResult |= id().equals(commit.id());
@ -649,9 +658,13 @@ final class CommitImpl extends AbstractNode<Commit> implements Commit {
Map<String, File> versionFiles = new HashMap<>();
Version from = version(ctx.migrations().version(ROOT), id(), versionFiles, history.values());
Version fromSnapshot = fromSnapshotCommit != null
? version(from, fromSnapshotCommit.id(), versionFiles, filter(fromSnapshotCommit.delta(), f -> f.type() == SNAPSHOT))
: null;
Version fromSnapshot = null;
Version to = version(from, resultCommit.id(), versionFiles, result.values());
return new MigrationHistory(
pathHistory,
@ -682,7 +695,7 @@ final class CommitImpl extends AbstractNode<Commit> implements Commit {
}
}
private static final Version version(Version from, String newId, Map<String, File> files, Iterable<File> result) {
private final Version version(Version from, String newId, Map<String, File> files, Iterable<File> result) {
Version to = from;
for (File file : result) {
@ -693,6 +706,10 @@ final class CommitImpl extends AbstractNode<Commit> implements Commit {
if (file.type() == SCHEMA)
to = to.commit(newId, sources(apply(files, file, true).values()).toArray(EMPTY_SOURCE));
// [#9506] Scripts must be ignored by the interpreter
else if (file.type() == SCRIPT)
to = to.apply(newId, new IgnoreQuery(file.content(), ctx.configuration()));
else
to = to.apply(newId, file.content());
}

View File

@ -131,8 +131,13 @@ final class MigrationImpl extends AbstractScope implements Migration {
@Override
public final Commit fromSnapshot() {
Version version = from().migrateTo(to()).fromSnapshot();
return version != null ? commits().get(version.id()) : null;
if (!configuration().commercial())
return null;
}
@Override

View File

@ -15828,7 +15828,11 @@ final class DefaultParseContext extends AbstractScope implements ParseContext {
}
IgnoreQuery(String sql) {
super(CONFIG.get());
this(sql, CONFIG.get());
}
IgnoreQuery(String sql, Configuration configuration) {
super(configuration);
this.sql = sql;
}

View File

@ -2744,6 +2744,10 @@ final class Tools {
return array;
}
static final <T> Iterable<T> iterable(Iterator<T> iterator) {
return () -> iterator;
}
static final <T, U> Iterator<U> iterator(Iterator<? extends T> iterator, Function<? super T, ? extends U> mapper) {
return new Iterator<U>() {
@Override
@ -6871,6 +6875,11 @@ final class Tools {
uptodate = false;
return next;
}
@Override
public void remove() {
iterator.remove();
}
};
}