package io.ebean.dbmigration.model; import io.ebean.dbmigration.migration.ChangeSet; import io.ebean.dbmigration.migration.ChangeSetType; import io.ebean.dbmigration.migration.DropColumn; import io.ebean.dbmigration.migration.DropTable; import io.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<>(); /** * Add a 'pending drops' changeSet for the given version. */ public void add(MigrationVersion version, ChangeSet changeSet) { Entry entry = map.computeIfAbsent(version.normalised(), k -> new Entry(version)); entry.add(changeSet); } /** * Return the list of versions with pending drops. */ public List<String> pendingDrops() { List<String> versions = new ArrayList<>(); for (Entry value : map.values()) { if (value.hasPendingDrops()) { versions.add(value.version.asString()); } } return versions; } /** * All the pending drops for this migration version have been applied so we need * to remove the (unsuppressed) pending drops for this version. */ public boolean appliedDropsFor(ChangeSet changeSet) { MigrationVersion version = MigrationVersion.parse(changeSet.getDropsFor()); Entry entry = map.get(version.normalised()); if (entry.removeDrops(changeSet)) { // it had no suppressForever changeSets so remove completely map.remove(version.normalised()); return true; } return false; } /** * 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 = getEntry(pendingVersion); Migration migration = new Migration(); Iterator<ChangeSet> it = entry.list.iterator(); while (it.hasNext()) { ChangeSet changeSet = it.next(); if (!isSuppressForever(changeSet)) { it.remove(); changeSet.setType(ChangeSetType.APPLY); changeSet.setDropsFor(entry.version.asString()); migration.getChangeSet().add(changeSet); } } if (migration.getChangeSet().isEmpty()) { throw new IllegalArgumentException("The remaining pendingDrops changeSets in migration [" + pendingVersion + "] are suppressDropsForever=true and can't be applied"); } if (!entry.containsSuppressForever()) { // we can remove it completely as it has no suppressForever changes map.remove(entry.version.normalised()); } return migration; } private Entry getEntry(String pendingVersion) { if ("next".equalsIgnoreCase(pendingVersion)) { Iterator<Entry> it = map.values().iterator(); if (it.hasNext()) { return it.next(); } } else { Entry remove = map.get(MigrationVersion.parse(pendingVersion).normalised()); if (remove != null) { return remove; } } throw new IllegalArgumentException("No 'pendingDrops' changeSets for migration 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) { newModel.registerPendingHistoryDropColumns(changeSet); } } } /** * Return true if there is an Entry for the given version. */ boolean testContainsEntryFor(MigrationVersion version) { return map.containsKey(version.normalised()); } /** * Return the Entry for the given version. */ Entry testGetEntryFor(MigrationVersion version) { return map.get(version.normalised()); } static class Entry { final MigrationVersion version; final List<ChangeSet> list = new ArrayList<>(); Entry(MigrationVersion version) { this.version = version; } void add(ChangeSet changeSet) { list.add(changeSet); } /** * Return true if this contains suppressForever changeSets. */ boolean containsSuppressForever() { for (ChangeSet changeSet : list) { if (isSuppressForever(changeSet)) { return true; } } return false; } /** * Return true if this contains drops that can be applied / migrated. */ boolean hasPendingDrops() { for (ChangeSet changeSet : list) { if (!isSuppressForever(changeSet)) { return true; } } return false; } /** * Remove the drops that are not suppressForever and return true if that * removed all the changeSets (and there are no suppressForever ones). */ boolean removeDrops(ChangeSet appliedDrops) { Iterator<ChangeSet> iterator = list.iterator(); while (iterator.hasNext()) { ChangeSet next = iterator.next(); if (!isSuppressForever(next)) { removeMatchingChanges(next, appliedDrops); if (next.getChangeSetChildren().isEmpty()) { iterator.remove(); } } } return list.isEmpty(); } /** * Remove the applied drops from the pending ones matching by table name and column name. */ private void removeMatchingChanges(ChangeSet pendingDrops, ChangeSet appliedDrops) { List<Object> pending = pendingDrops.getChangeSetChildren(); Iterator<Object> iterator = pending.iterator(); while (iterator.hasNext()) { Object pendingDrop = iterator.next(); if (pendingDrop instanceof DropColumn && dropColumnIn((DropColumn) pendingDrop, appliedDrops)) { iterator.remove(); } else if (pendingDrop instanceof DropTable && dropTableIn((DropTable) pendingDrop, appliedDrops)) { iterator.remove(); } } } /** * Return true if the pendingDrop is contained in the appliedDrops. */ private boolean dropTableIn(DropTable pendingDrop, ChangeSet appliedDrops) { for (Object o : appliedDrops.getChangeSetChildren()) { if (o instanceof DropTable && sameTable(pendingDrop, (DropTable) o)) { return true; } } return false; } /** * Return true if the pendingDrop is contained in the appliedDrops. */ private boolean dropColumnIn(DropColumn pendingDrop, ChangeSet appliedDrops) { for (Object o : appliedDrops.getChangeSetChildren()) { if (o instanceof DropColumn && sameColumn(pendingDrop, (DropColumn) o)) { return true; } } return false; } /** * Return true if the DropTable match by table name. */ private boolean sameTable(DropTable pendingDrop, DropTable o) { return pendingDrop.getName().equals(o.getName()); } /** * Return true if the DropColumns match by table and column name. */ private boolean sameColumn(DropColumn pending, DropColumn o) { return pending.getColumnName().equals(o.getColumnName()) && pending.getTableName().equals(o.getTableName()); } } private static boolean isSuppressForever(ChangeSet next) { return Boolean.TRUE.equals(next.isSuppressDropsForever()); } }