#557 - DDL - DB Migration refactor - remove drop.ddl and use pendingDrops.

This commit is contained in:
Robin Bygrave
2016-02-11 20:15:49 +13:00
parent 097552b8d3
commit 1bee6fcae5
22 changed files with 608 additions and 332 deletions
@@ -61,11 +61,6 @@ public class DbMigrationConfig {
*/
protected String modelPath = "model";
/**
* Subdirectory the drop ddl scripts go into.
*/
protected String dropPath = "drop";
/**
* Subdirectory the rollback ddl scripts go into.
*/
@@ -76,11 +71,6 @@ public class DbMigrationConfig {
*/
protected String applySuffix = ".sql";
/**
* Default drop script suffix to ddl so that it isn't picked up by FlywayDb.
*/
protected String dropSuffix = ".drop.ddl";
/**
* Default rollback script suffix to ddl so that it isn't picked up by FlywayDb.
*/
@@ -90,6 +80,11 @@ public class DbMigrationConfig {
protected boolean includeGeneratedFileComment;
/**
* The version of a pending drop that should be generated as the next migration.
*/
protected String generatePendingDrop;
/**
* Return the DB platform to generate migration DDL for.
*
@@ -140,20 +135,6 @@ public class DbMigrationConfig {
this.modelPath = modelPath;
}
/**
* Return the relative path for the drop ddl scripts (defaults to drop).
*/
public String getDropPath() {
return dropPath;
}
/**
* Set the relative path for the drop ddl scripts (defaults to drop).
*/
public void setDropPath(String dropPath) {
this.dropPath = dropPath;
}
/**
* Return the relative path for the rollback ddl scripts (defaults to rollback).
*/
@@ -210,20 +191,6 @@ public class DbMigrationConfig {
this.applySuffix = applySuffix;
}
/**
* Return the drop script suffix (defaults to ddl so that it isn't picked up by FlywayDb).
*/
public String getDropSuffix() {
return dropSuffix;
}
/**
* Set the drop script suffix (defaults to ddl so that it isn't picked up by FlywayDb).
*/
public void setDropSuffix(String dropSuffix) {
this.dropSuffix = dropSuffix;
}
/**
* Return the rollback script suffix (defaults to ddl so that it isn't picked up by FlywayDb).
*/
@@ -252,6 +219,20 @@ public class DbMigrationConfig {
this.includeGeneratedFileComment = includeGeneratedFileComment;
}
/**
* Return the migration version (or "next") to generate pending drops for.
*/
public String getGeneratePendingDrop() {
return generatePendingDrop;
}
/**
* Set the migration version (or "next") to generate pending drops for.
*/
public void setGeneratePendingDrop(String generatePendingDrop) {
this.generatePendingDrop = generatePendingDrop;
}
/**
* Set the migration version.
* <p>
@@ -275,7 +256,6 @@ public class DbMigrationConfig {
* into a single directory.
*/
public void singleDirectory() {
this.dropPath = "";
this.rollbackPath = "";
this.modelPath = "";
}
@@ -291,13 +271,12 @@ public class DbMigrationConfig {
} else {
modelPath = properties.get("migration.modelPath", modelPath);
rollbackPath = properties.get("migration.rollbackPath", rollbackPath);
dropPath = properties.get("migration.dropPath", dropPath);
}
applySuffix = properties.get("migration.applySuffix", applySuffix);
dropSuffix = properties.get("migration.dropSuffix", dropSuffix);
rollbackSuffix = properties.get("migration.rollbackSuffix", rollbackSuffix);
modelSuffix = properties.get("migration.modelSuffix", modelSuffix);
includeGeneratedFileComment = properties.getBoolean("migration.includeGeneratedFileComment", includeGeneratedFileComment);
generatePendingDrop = properties.get("migration.generatePendingDrop", generatePendingDrop);
platform = properties.getEnum(DbPlatformName.class, "migration.platform", platform);
suppressRollback = properties.getBoolean("migration.suppressRollback", suppressRollback);
@@ -324,7 +303,6 @@ public class DbMigrationConfig {
return generate;
}
/**
* Called by EbeanServer on start.
*
@@ -20,6 +20,7 @@ import com.avaje.ebean.dbmigration.migrationreader.MigrationXmlWriter;
import com.avaje.ebean.dbmigration.model.CurrentModel;
import com.avaje.ebean.dbmigration.model.MConfiguration;
import com.avaje.ebean.dbmigration.model.MigrationModel;
import com.avaje.ebean.dbmigration.model.MigrationVersion;
import com.avaje.ebean.dbmigration.model.ModelContainer;
import com.avaje.ebean.dbmigration.model.ModelDiff;
import com.avaje.ebean.dbmigration.model.PlatformDdlWriter;
@@ -201,46 +202,15 @@ public class DbMigration {
if (!online) {
DbOffline.setRunningMigration();
}
setDefaults();
try {
Request request = createRequest();
File migrationDir = getMigrationDirectory();
File modelDir = getModelDirectory(migrationDir);
MigrationModel migrationModel = new MigrationModel(modelDir, migrationConfig.getModelSuffix());
ModelContainer migrated = migrationModel.read();
CurrentModel currentModel = new CurrentModel(server, constraintNaming);
ModelContainer current = currentModel.read();
ModelDiff diff = new ModelDiff(migrated);
diff.compareTo(current);
if (diff.isEmpty()) {
logger.info("no changes detected - no migration written");
return;
}
// there were actually changes to write
Migration dbMigration = diff.getMigration();
String fullVersion = getFullVersion(migrationModel);
logger.info("generating migration:{}", fullVersion);
if (!writeMigrationXml(dbMigration, modelDir, fullVersion)) {
logger.warn("migration already exists, not generating DDL");
String pendingVersion = generatePendingDrop();
if (pendingVersion != null) {
generatePendingDrop(request, pendingVersion);
} else {
if (databasePlatform != null) {
// writer needs the current model to provide table/column details for
// history ddl generation (triggers, history tables etc)
DdlWrite write = new DdlWrite(new MConfiguration(), current);
PlatformDdlWriter writer = createDdlWriter(databasePlatform, "");
writer.processMigration(dbMigration, write, migrationDir , fullVersion);
}
writeExtraPlatformDdl(fullVersion, currentModel, dbMigration, migrationDir);
generateDiff(request);
}
} finally {
@@ -250,10 +220,127 @@ public class DbMigration {
}
}
private void generateDiff(Request request) throws IOException {
if (request.hasPendingDrops()) {
logger.info("Pending un-applied drops in versions {}", request.getPendingDrops());
}
ModelDiff diff = request.createDiff();
if (diff.isEmpty()) {
logger.info("no changes detected - no migration written");
} else {
// there were actually changes to write
generateMigration(request, diff.getMigration(), null);
}
}
private void generatePendingDrop(Request request, String pendingVersion) throws IOException {
Migration migration = request.migrationForPendingDrop(pendingVersion);
generateMigration(request, migration, pendingVersion);
if (request.hasPendingDrops()) {
logger.info("... remaining pending un-applied drops in versions {}", request.getPendingDrops());
}
}
private Request createRequest() {
return new Request();
}
private class Request {
final File migrationDir;
final File modelDir;
final MigrationModel migrationModel;
final CurrentModel currentModel;
final ModelContainer migrated;
final ModelContainer current;
private Request() {
this.migrationDir = getMigrationDirectory();
this.modelDir = getModelDirectory(migrationDir);
this.migrationModel = new MigrationModel(modelDir, migrationConfig.getModelSuffix());
this.migrated = migrationModel.read();
this.currentModel = new CurrentModel(server, constraintNaming);
this.current = currentModel.read();
}
/**
* Return true if there are pending un-applied drops.
*/
public boolean hasPendingDrops() {
return migrated.hasPendingDrops();
}
/**
* Return the migration for the pending drops for a given version.
*/
public Migration migrationForPendingDrop(String pendingVersion) {
Migration migration = migrated.migrationForPendingDrop(pendingVersion);
// register any remaining pending drops
migrated.registerPendingHistoryDropColumns(current);
return migration;
}
/**
* Return the list of versions that have pending un-applied drops.
*/
public List<String> getPendingDrops() {
return migrated.getPendingDrops();
}
/**
* Create an return the diff of the current model to the migration model.
*/
public ModelDiff createDiff() {
ModelDiff diff = new ModelDiff(migrated);
diff.compareTo(current);
return diff;
}
}
private void generateMigration(Request request, Migration dbMigration, String dropsFor) throws IOException {
String fullVersion = getFullVersion(request.migrationModel, dropsFor);
logger.info("generating migration:{}", fullVersion);
if (!writeMigrationXml(dbMigration, request.modelDir, fullVersion)) {
logger.warn("migration already exists, not generating DDL");
} else {
if (databasePlatform != null) {
// writer needs the current model to provide table/column details for
// history ddl generation (triggers, history tables etc)
DdlWrite write = new DdlWrite(new MConfiguration(), request.current);
PlatformDdlWriter writer = createDdlWriter(databasePlatform, "");
writer.processMigration(dbMigration, write, request.migrationDir , fullVersion);
}
writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir);
}
}
/**
* Return true if the next pending drop changeSet should be generated as the next migration.
*/
private String generatePendingDrop() {
String nextDrop = System.getProperty("ddl.migration.pendingDrop");
if (nextDrop != null) {
return nextDrop;
}
return migrationConfig.getGeneratePendingDrop();
}
/**
* Return the full version for the migration being generated.
*
* The full version can contain a comment suffix after a "__" double underscore.
*/
private String getFullVersion(MigrationModel migrationModel) {
private String getFullVersion(MigrationModel migrationModel, String dropsFor) {
String version = migrationConfig.getVersion();
if (version == null) {
@@ -261,10 +348,14 @@ public class DbMigration {
}
String fullVersion = version;
if (migrationConfig.getName() != null) {
fullVersion += "__" + toUnderScore(migrationConfig.getName());
String name = migrationConfig.getName();
if (name != null) {
fullVersion += "__" + toUnderScore(name);
} else if (dropsFor != null) {
fullVersion += "__" + toUnderScore("dropsFor_" + MigrationVersion.trim(dropsFor));
} else if (version.equals(initialVersion)) {
fullVersion += "__initial";
}
return fullVersion;
}
@@ -12,8 +12,7 @@ public class DdlWrite {
public enum Mode {
APPLY,
ROLLBACK,
DROP
ROLLBACK
}
private final ModelContainer currentModel;
@@ -32,28 +31,6 @@ public class DdlWrite {
private final DdlBuffer rollback;
/**
* For DDL that drops tables and columns etc.
*
* This DDL typically can not run automatically in production as there is most commonly
* existing servers running the application using these tables and columns. Typically
* these drop statements may be executed AFTER all the servers in the application have
* migrated onto new code.
*/
private final DdlBuffer drop;
/**
* For use when History is turned off for a base table or history is no longer
* desired on specific columns. This DDL should typically execute manually after review
* by DBA's.
*/
private final DdlBuffer dropHistory;
/**
* Buffer used to drop dependencies early in the 'drop script'.
*/
private final DdlBuffer dropDropDependencies;
/**
* Create without any configuration or current model (no history support).
*/
@@ -73,9 +50,6 @@ public class DdlWrite {
this.rollbackDropDependencies = new BaseDdlBuffer(configuration);
this.rollbackForeignKeys = new BaseDdlBuffer(configuration);
this.rollback = new BaseDdlBuffer(configuration);
this.drop = new BaseDdlBuffer(configuration);
this.dropHistory = new BaseDdlBuffer(configuration);
this.dropDropDependencies = new BaseDdlBuffer(configuration);
}
/**
@@ -115,7 +89,6 @@ public class DdlWrite {
switch (mode) {
case APPLY: return apply();
case ROLLBACK: return rollback();
case DROP: return drop();
default:
throw new IllegalStateException("Invalid mode" + mode);
}
@@ -128,7 +101,6 @@ public class DdlWrite {
switch (mode) {
case APPLY: return applyHistory();
case ROLLBACK: return rollback();
case DROP: return dropHistory();
default:
throw new IllegalStateException("Invalid mode" + mode);
}
@@ -142,20 +114,11 @@ public class DdlWrite {
switch (mode) {
case APPLY: return applyDropDependencies();
case ROLLBACK: return rollbackDropDependencies();
case DROP: return dropDropDependencies();
default:
throw new IllegalStateException("Invalid mode" + mode);
}
}
/**
* Return true the drop buffers are empty.
*/
public boolean isDropEmpty() {
return drop.getBuffer().isEmpty() && dropHistory.getBuffer().isEmpty();
}
/**
* Return the buffer that APPLY DDL is written to.
*/
@@ -216,26 +179,8 @@ public class DdlWrite {
return rollback;
}
/**
* Return the buffer that destructive changes are written to. This is typically drop table and
* drop column.
*/
public DdlBuffer drop() {
return drop;
}
/**
* Return the buffer that is used when history is no longer required on a table or specific columns.
*/
public DdlBuffer dropHistory() {
return dropHistory;
}
/**
* Return the buffer that executes early for 'drop' script.
*/
public DdlBuffer dropDropDependencies() {
return dropDropDependencies;
return apply;
}
}
@@ -71,37 +71,23 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
DbTriggerUpdate triggerUpdate = createDbTriggerUpdate(writer, table);
if (update.hasApplyChanges()) {
// includes add, include and exclude column changes
String description = update.description();
List<String> includedColumns = columnNamesForApply(table);
String applyChangeDescription = update.descriptionForApply();
List<String> includedColumns = columnNamesForApply(table);
DdlBuffer apply = writer.applyHistory();
apply.append("-- changes: ").append(description).newLine();
DdlBuffer apply = writer.applyHistory();
apply.append("-- changes: ").append(applyChangeDescription).newLine();
triggerUpdate.prepare(DdlWrite.Mode.APPLY, includedColumns);
updateHistoryTriggers(triggerUpdate);
triggerUpdate.prepare(DdlWrite.Mode.APPLY, includedColumns);
updateHistoryTriggers(triggerUpdate);
// put a reverted version into the rollback buffer
update.toRevertedColumns(includedColumns);
// put a reverted version into the rollback buffer
update.toRevertedColumns(includedColumns);
DdlBuffer rollback = writer.rollback();
rollback.append("-- revert changes: ").append(description).newLine();
DdlBuffer rollback = writer.rollback();
rollback.append("-- revert changes: ").append(applyChangeDescription).newLine();
triggerUpdate.prepare(DdlWrite.Mode.ROLLBACK, includedColumns);
updateHistoryTriggers(triggerUpdate);//writer, DdlWrite.Mode.ROLLBACK, baseTableName, historyTableName, includedColumns);
}
if (update.hasDropChanges()) {
// effectively applies the dropped columns changes to history triggers
DdlBuffer drop = writer.dropHistory();
drop.append("-- changes: ").append(update.descriptionForDrop()).newLine();
triggerUpdate.prepare(DdlWrite.Mode.DROP, columnNamesForDrop(table));
updateHistoryTriggers(triggerUpdate);//writer, DdlWrite.Mode.DROP, baseTableName, historyTableName, columnNamesForDrop(table));
}
triggerUpdate.prepare(DdlWrite.Mode.ROLLBACK, includedColumns);
updateHistoryTriggers(triggerUpdate);
}
protected DbTriggerUpdate createDbTriggerUpdate(DdlWrite writer, MTable table) {
@@ -116,11 +102,10 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
String baseTable = dropHistoryTable.getBaseTable();
// drop in appropriate order
dropTriggers(writer.dropHistory(), baseTable);
dropHistoryTableEtc(writer.dropHistory(), baseTable);
dropTriggers(writer.applyDropDependencies(), baseTable);
dropHistoryTableEtc(writer.applyDropDependencies(), baseTable);
}
@Override
public void addHistoryTable(DdlWrite writer, AddHistoryTable addHistoryTable) throws IOException {
@@ -315,11 +300,4 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
return table.allHistoryColumns(true);
}
/**
* Return the column names included in history for the drop script.
*/
protected List<String> columnNamesForDrop(MTable table) throws IOException {
return table.allHistoryColumns(false);
}
}
@@ -10,7 +10,6 @@ import java.util.List;
*/
public class HistoryTableUpdate {
/**
* Column change type.
*/
@@ -32,12 +31,12 @@ public class HistoryTableUpdate {
this.column = column;
}
public String description() {
return change.name().toLowerCase()+" "+column;
public String toString() {
return description();
}
private boolean isChangeFor(boolean apply) {
return apply ? change != Change.DROP : change == Change.DROP;
public String description() {
return change.name().toLowerCase()+" "+column;
}
private void revert(List<String> includedColumns) {
@@ -47,12 +46,11 @@ public class HistoryTableUpdate {
includedColumns.remove(column);
break;
}
case EXCLUDE: {
case EXCLUDE:
case DROP: {
includedColumns.add(column);
break;
}
case DROP:
break;
default:
throw new IllegalStateException("Unexpected change "+change);
}
@@ -70,60 +68,12 @@ public class HistoryTableUpdate {
this.baseTable = baseTable;
}
private boolean isChangeFor(boolean apply) {
for (Column columnChange : columnChanges) {
if (columnChange.isChangeFor(apply)) {
return true;
}
}
return false;
}
/**
* Return true if the change includes apply changes (ADD, INCLUDE, EXCLUDE).
*/
public boolean hasApplyChanges() {
return isChangeFor(true);
}
/**
* Return true if the change includes DROP column.
*/
public boolean hasDropChanges() {
return isChangeFor(false);
}
/**
* Return a description of the changes that cause the history trigger/function
* to be regenerated (added, included or excluded columns).
* to be regenerated (added, included, excluded and dropped columns).
*/
public String descriptionForApply() {
return descriptionFor(true);
}
/**
* Return a description of the changes that cause the history trigger/function
* to be regenerated in the drop script (dropped columns only).
*/
public String descriptionForDrop() {
return descriptionFor(false);
}
private String descriptionFor(boolean apply) {
StringBuilder sb = new StringBuilder(90);
boolean first = true;
for (Column column : columnChanges) {
if (column.isChangeFor(apply)) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(column.description());
}
}
return sb.toString();
public String description() {
return columnChanges.toString();
}
/**
@@ -27,6 +27,7 @@ import javax.xml.bind.annotation.XmlType;
* &lt;/choice>
* &lt;/sequence>
* &lt;attribute name="type" use="required" type="{http://ebean-orm.github.io/xml/ns/dbmigration}changeSetType" />
* &lt;attribute name="dropsFor" type="{http://www.w3.org/2001/XMLSchema}string" />
* &lt;attribute name="generated" type="{http://www.w3.org/2001/XMLSchema}boolean" />
* &lt;attribute name="author" type="{http://www.w3.org/2001/XMLSchema}string" />
* &lt;attribute name="comment" type="{http://www.w3.org/2001/XMLSchema}string" />
@@ -62,6 +63,8 @@ public class ChangeSet {
protected List<Object> changeSetChildren;
@XmlAttribute(name = "type", required = true)
protected ChangeSetType type;
@XmlAttribute(name = "dropsFor")
protected String dropsFor;
@XmlAttribute(name = "generated")
protected Boolean generated;
@XmlAttribute(name = "author")
@@ -134,6 +137,30 @@ public class ChangeSet {
this.type = value;
}
/**
* Gets the value of the dropsFor property.
*
* @return
* possible object is
* {@link String }
*
*/
public String getDropsFor() {
return dropsFor;
}
/**
* Sets the value of the dropsFor property.
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setDropsFor(String value) {
this.dropsFor = value;
}
/**
* Gets the value of the generated property.
*
@@ -15,7 +15,7 @@ import javax.xml.bind.annotation.XmlType;
* &lt;simpleType name="changeSetType">
* &lt;restriction base="{http://www.w3.org/2001/XMLSchema}string">
* &lt;enumeration value="apply"/>
* &lt;enumeration value="drop"/>
* &lt;enumeration value="pendingDrops"/>
* &lt;enumeration value="baseline"/>
* &lt;/restriction>
* &lt;/simpleType>
@@ -28,8 +28,8 @@ public enum ChangeSetType {
@XmlEnumValue("apply")
APPLY("apply"),
@XmlEnumValue("drop")
DROP("drop"),
@XmlEnumValue("pendingDrops")
PENDING_DROPS("pendingDrops"),
@XmlEnumValue("baseline")
BASELINE("baseline");
private final String value;
@@ -132,14 +132,14 @@ public class MTable {
*/
public MTable createDraftTable() {
draftTable = new MTable(name+"_draft");
draftTable = new MTable(name + "_draft");
draftTable.draft = true;
draftTable.whenCreatedColumn = whenCreatedColumn;
// compoundKeys
// compoundUniqueConstraints
draftTable.identityType = identityType;
for (MColumn col: allColumns()) {
for (MColumn col : allColumns()) {
draftTable.addColumn(col.copyForDraft());
}
@@ -546,20 +546,19 @@ public class MTable {
// These dropColumns should occur on the history
// table as well as the base table
dropColumn.setWithHistory(Boolean.TRUE);
newTable.registerDroppedColumn(existingColumn.getName(), columnPosition);
}
modelDiff.addDropColumn(dropColumn);
}
/**
* Register a dropped column with it's previous column position.
* We need this for history triggers and views as we don't actually drop the
* column until the 'drop script' is run so the 'apply script' for history changes
* still needs to include the columns that are going to be dropped.
* Register a pending un-applied drop column.
* <p>
* This means this column still needs to be included in history views/triggers etc even
* though it is not part of the current model.
*/
protected void registerDroppedColumn(String name, int columnPosition) {
droppedColumns.add(new DroppedColumn(name, columnPosition));
public void registerPendingDropColumn(String columnName) {
droppedColumns.add(new DroppedColumn(columnName, 100));
}
private static class DroppedColumn implements Comparable<DroppedColumn> {
@@ -573,7 +572,7 @@ public class MTable {
@Override
public int compareTo(DroppedColumn o) {
return Integer.compare(o.columnPosition, columnPosition);
return (o.columnPosition < columnPosition) ? -1 : ((o.columnPosition == columnPosition) ? 0 : 1);
}
}
@@ -588,7 +587,7 @@ public class MTable {
/**
* Check if there are duplicate foreign keys.
* <p>
* This can occur when an ManyToMany relates back to itself.
* This can occur when an ManyToMany relates back to itself.
* </p>
*/
public void checkDuplicateForeignKeys() {
@@ -639,7 +638,7 @@ public class MTable {
*/
private String extractBaseTable(String references) {
int lastDot = references.lastIndexOf('.');
return references.substring(0,lastDot);
return references.substring(0, lastDot);
}
/**
@@ -648,7 +647,7 @@ public class MTable {
*/
private String deriveReferences(String references, String draftTableName) {
int lastDot = references.lastIndexOf('.');
return draftTableName+"."+references.substring(lastDot+1);
return draftTableName + "." + references.substring(lastDot + 1);
}
}
@@ -52,7 +52,7 @@ public class MigrationModel {
List<MigrationResource> resources = new ArrayList<MigrationResource>();
for (File xmlFile: xmlFiles) {
resources.add(new MigrationResource(xmlFile));
resources.add(new MigrationResource(xmlFile, createVersion(xmlFile)));
}
// sort into version order before applying
@@ -60,7 +60,7 @@ public class MigrationModel {
for (MigrationResource migrationResource: resources) {
logger.debug("read {}", migrationResource);
model.apply(migrationResource.read());
model.apply(migrationResource.read(), migrationResource.getVersion());
}
// remember the last version
@@ -69,6 +69,12 @@ public class MigrationModel {
}
}
private MigrationVersion createVersion(File xmlFile) {
String fileName = xmlFile.getName();
String versionName = fileName.substring(0, fileName.length() - modelSuffix.length());
return MigrationVersion.parse(versionName);
}
public String getNextVersion(String initialVersion) {
return lastVersion == null ? initialVersion : lastVersion.nextVersion();
@@ -17,9 +17,9 @@ public class MigrationResource implements Comparable<MigrationResource> {
/**
* Construct with a migration xml file.
*/
public MigrationResource(File migrationFile) {
public MigrationResource(File migrationFile, MigrationVersion version) {
this.migrationFile = migrationFile;
this.version = MigrationVersion.parse(migrationFile.getName());
this.version = version;
}
public String toString() {
@@ -1,5 +1,7 @@
package com.avaje.ebean.dbmigration.model;
import java.util.Arrays;
/**
* The version of a migration used so that migrations are processed in order.
*/
@@ -15,23 +17,74 @@ public class MigrationVersion implements Comparable<MigrationVersion> {
*/
private final int[] ordering;
private MigrationVersion(String raw, int[] ordering) {
private final boolean[] underscores;
private final String comment;
private MigrationVersion(String raw, int[] ordering, boolean[] underscores, String comment) {
this.raw = raw;
this.ordering = ordering;
this.underscores = underscores;
this.comment = comment;
}
public String toString() {
return raw;
}
/**
* Return the version comment.
*/
public String getComment() {
return comment;
}
/**
* Return the version in raw form.
*/
public String getRaw() {
return raw;
}
/**
* Return the trimmed version excluding version comment and un-parsable string.
*/
public String asString() {
return formattedVersion(false, false);
}
/**
* Return the trimmed version with any underscores replaced with '.'
*/
public String normalised() {
return formattedVersion(true, false);
}
/**
* Return the next version based on this version.
*/
public String nextVersion() {
return formattedVersion(false, true);
}
/**
* Returns the version part of the string.
*
* Normalised means always use '.' delimiters (no underscores).
* NextVersion means bump/increase the last version number by 1.
*/
private String formattedVersion(boolean normalised, boolean nextVersion) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ordering.length; i++) {
if (i < ordering.length -1 ) {
sb.append(ordering[i]).append(".");
if (i < ordering.length - 1) {
sb.append(ordering[i]);
if (normalised) {
sb.append('.');
} else {
sb.append(underscores[i] ? '_' : '.');
}
} else {
sb.append(ordering[i]+1);
sb.append((nextVersion) ? ordering[i] + 1 : ordering[i]);
}
}
return sb.toString();
@@ -54,33 +107,54 @@ public class MigrationVersion implements Comparable<MigrationVersion> {
return 0;
}
/**
* Parse the raw version string and just return the leading version number;
*/
public static String trim(String raw) {
return parse(raw).asString();
}
/**
* Parse the raw version string into a MigrationVersion.
*/
public static MigrationVersion parse(String raw) {
String value = raw.replace("__",".");
value = value.replace('_','.');
String comment = "";
String value = raw;
int commentStart = raw.indexOf("__");
if (commentStart > -1) {
// trim off the trailing comment
comment = raw.substring(commentStart + 2);
value = value.substring(0, commentStart);
}
value = value.replace('_', '.');
String[] sections = value.split("\\.");
boolean[] underscores = new boolean[sections.length];
int[] ordering = new int[sections.length];
int delimiterPos = 0;
int stopIndex = 0;
for (int i = 0; i < sections.length; i++) {
try {
ordering[i] = Integer.parseInt(sections[i]);
stopIndex++;
delimiterPos += sections[i].length();
underscores[i] = (delimiterPos < raw.length() - 1 && raw.charAt(delimiterPos) == '_');
delimiterPos++;
} catch (NumberFormatException e) {
// stop parsing
break;
}
}
int[] actualOrder = new int[stopIndex];
System.arraycopy(ordering, 0, actualOrder, 0, stopIndex);
int[] actualOrder = Arrays.copyOf(ordering, stopIndex);
boolean[] actualUnderscores = Arrays.copyOf(underscores, stopIndex);
return new MigrationVersion(raw, actualOrder);
return new MigrationVersion(raw, actualOrder, actualUnderscores, comment);
}
}
@@ -4,6 +4,7 @@ import com.avaje.ebean.dbmigration.migration.AddColumn;
import com.avaje.ebean.dbmigration.migration.AddHistoryTable;
import com.avaje.ebean.dbmigration.migration.AlterColumn;
import com.avaje.ebean.dbmigration.migration.ChangeSet;
import com.avaje.ebean.dbmigration.migration.ChangeSetType;
import com.avaje.ebean.dbmigration.migration.CreateIndex;
import com.avaje.ebean.dbmigration.migration.CreateTable;
import com.avaje.ebean.dbmigration.migration.DropColumn;
@@ -28,15 +29,16 @@ public class ModelContainer {
/**
* All the tables in the model.
*/
private Map<String, MTable> tables = new LinkedHashMap<String, MTable>();
private final Map<String, MTable> tables = new LinkedHashMap<String, MTable>();
/**
* All the non unique non foreign key indexes.
*/
private Map<String, MIndex> indexes = new LinkedHashMap<String, MIndex>();
private final Map<String, MIndex> indexes = new LinkedHashMap<String, MIndex>();
private final PendingDrops pendingDrops = new PendingDrops();
public ModelContainer() {
}
/**
@@ -82,14 +84,31 @@ public class ModelContainer {
/**
* Apply a migration with associated changeSets to the model.
*/
public void apply(Migration migration) {
public void apply(Migration migration, MigrationVersion version) {
List<ChangeSet> changeSets = migration.getChangeSet();
for (ChangeSet changeSet : changeSets) {
applyChangeSet(changeSet);
boolean pending = changeSet.getType() == ChangeSetType.PENDING_DROPS;
if (pending) {
// un-applied drop columns etc
pendingDrops.add(version, changeSet);
} else if (isDropsFor(changeSet)) {
// applied drops (so no longer pending)
pendingDrops.remove(MigrationVersion.parse(changeSet.getDropsFor()));
}
if (!isDropsFor(changeSet)) {
applyChangeSet(changeSet);
}
}
}
/**
* Return true if the changeSet contains drops for a previous PENDING_DROPS changeSet.
*/
private boolean isDropsFor(ChangeSet changeSet) {
return changeSet.getDropsFor() != null;
}
/**
* Apply a changeSet to the model.
*/
@@ -245,4 +264,44 @@ public class ModelContainer {
indexes.put(indexName, new MIndex(indexName, tableName, columnNames));
}
/**
* Return true if there are pending drops.
*/
public boolean hasPendingDrops() {
return !pendingDrops.isEmpty();
}
/**
* Return the list of versions containing un-applied pending drops.
*/
public List<String> getPendingDrops() {
return pendingDrops.pendingDrops();
}
/**
* Return the migration for the pending drops for a given version.
*/
public Migration migrationForPendingDrop(String pendingVersion) {
return pendingDrops.migrationForVersion(pendingVersion);
}
/**
* Register the drop columns on history tables that have not been applied yet.
*/
public void registerPendingHistoryDropColumns(ModelContainer newModel) {
pendingDrops.registerPendingHistoryDropColumns(newModel);
}
/**
* Register a drop column on a history tables that has not been applied yet.
*/
public void registerPendingDropColumn(DropColumn dropColumn) {
MTable table = getTable(dropColumn.getTableName());
if (table == null) {
throw new IllegalArgumentException("Table ["+dropColumn.getTableName()+"] not found?");
}
table.registerPendingDropColumn(dropColumn.getColumnName());
}
}
@@ -111,7 +111,7 @@ public class ModelDiff {
public ChangeSet getDropChangeSet() {
// put the changes into a ChangeSet
ChangeSet createChangeSet = new ChangeSet();
createChangeSet.setType(ChangeSetType.DROP);
createChangeSet.setType(ChangeSetType.PENDING_DROPS);
createChangeSet.getChangeSetChildren().addAll(dropChanges);
return createChangeSet;
}
@@ -156,6 +156,7 @@ public class ModelDiff {
}
}
baseModel.registerPendingHistoryDropColumns(newModel);
}
protected void addDropTable(MTable existingTable) {
@@ -0,0 +1,131 @@
package com.avaje.ebean.dbmigration.model;
import com.avaje.ebean.dbmigration.migration.ChangeSet;
import com.avaje.ebean.dbmigration.migration.ChangeSetType;
import com.avaje.ebean.dbmigration.migration.DropColumn;
import com.avaje.ebean.dbmigration.migration.Migration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
/**
* The migrations with pending un-applied drops.
*/
public class PendingDrops {
private final LinkedHashMap<String, Entry> map = new LinkedHashMap<String, Entry>();
/**
* Add a 'pending drops' changeSet for the given version.
*/
public void add(MigrationVersion version, ChangeSet changeSet) {
Entry entry = map.get(version.normalised());
if (entry == null) {
entry = new Entry(version);
map.put(version.normalised(), entry);
}
entry.add(changeSet);
}
/**
* Return the list of versions with pending drops.
*/
public List<String> pendingDrops() {
List<String> versions = new ArrayList<String>();
for (Entry value : map.values()) {
versions.add(value.version.asString());
}
return versions;
}
/**
* Remove the pending drops for a version (as they have been applied).
*/
public void remove(MigrationVersion version) {
map.remove(version.normalised());
}
/**
* Return true if there are no pending drops.
*/
public boolean isEmpty() {
return map.isEmpty();
}
/**
* Return the migration for the pending drops from a version.
* <p>
* The value of version can be "next" to find the first un-applied pending drops.
* </p>
*/
public Migration migrationForVersion(String pendingVersion) {
Entry entry = getChangeSets(pendingVersion);
Migration migration = new Migration();
for (ChangeSet changeSet : entry.list) {
changeSet.setType(ChangeSetType.APPLY);
changeSet.setDropsFor(entry.version.asString());
migration.getChangeSet().add(changeSet);
}
return migration;
}
private Entry getChangeSets(String pendingVersion) {
if ("next".equalsIgnoreCase(pendingVersion)) {
Iterator<Entry> it = map.values().iterator();
if (it.hasNext()) {
Entry first = it.next();
it.remove();
return first;
}
} else {
Entry remove = map.remove(MigrationVersion.parse(pendingVersion).normalised());
if (remove != null) {
return remove;
}
}
throw new IllegalArgumentException("No pending changeSets for version [" + pendingVersion + "] found");
}
/**
* Register pending drop columns on history tables to the new model.
*/
public void registerPendingHistoryDropColumns(ModelContainer newModel) {
for (Entry entry : map.values()) {
for (ChangeSet changeSet : entry.list) {
for (Object change : changeSet.getChangeSetChildren()) {
if (change instanceof DropColumn) {
DropColumn dropColumn = (DropColumn) change;
if (Boolean.TRUE.equals(dropColumn.isWithHistory())) {
newModel.registerPendingDropColumn(dropColumn);
}
}
}
}
}
}
static class Entry {
final MigrationVersion version;
final List<ChangeSet> list = new ArrayList<ChangeSet>();
Entry(MigrationVersion version) {
this.version = version;
}
void add(ChangeSet changeSet) {
list.add(changeSet);
}
}
}
@@ -7,6 +7,7 @@ import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlHandler;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
import com.avaje.ebean.dbmigration.migration.ChangeSet;
import com.avaje.ebean.dbmigration.migration.ChangeSetType;
import com.avaje.ebean.dbmigration.migration.Migration;
import java.io.File;
@@ -44,7 +45,7 @@ public class PlatformDdlWriter {
List<ChangeSet> changeSets = dbMigration.getChangeSet();
for (ChangeSet changeSet : changeSets) {
if (!changeSet.getChangeSetChildren().isEmpty()) {
if (isApply(changeSet)) {
handler.generate(write, changeSet);
}
}
@@ -53,6 +54,13 @@ public class PlatformDdlWriter {
writePlatformDdl(write, writePath, fullVersion);
}
/**
* Return true if the changeSet is APPLY and not empty.
*/
private boolean isApply(ChangeSet changeSet) {
return changeSet.getType() == ChangeSetType.APPLY && !changeSet.getChangeSetChildren().isEmpty();
}
/**
* Write the ddl files.
*/
@@ -78,15 +86,6 @@ public class PlatformDdlWriter {
}
}
if (!write.isDropEmpty()) {
FileWriter dropWriter = createWriter(resourcePath, fullVersion, config.getDropPath(), config.getDropSuffix());
try {
writeDropDdl(dropWriter, write);
dropWriter.flush();
} finally {
dropWriter.close();
}
}
}
protected FileWriter createWriter(File path, String fullVersion, String subPath, String suffix) throws IOException {
@@ -136,17 +135,6 @@ public class PlatformDdlWriter {
writer.append(write.rollback().getBuffer());
}
/**
* Write the 'Drop' DDL buffers to the writer.
*/
protected void writeDropDdl(Writer writer, DdlWrite write) throws IOException {
// merge the rollback buffers in the appropriate order
prependDropDependencies(writer, write.dropDropDependencies());
writer.append(write.dropHistory().getBuffer());
writer.append(write.drop().getBuffer());
}
private void prependDropDependencies(Writer writer, DdlBuffer buffer) throws IOException {
if (!buffer.isEmpty()) {
writer.append("-- drop dependencies\n");