001package io.ebeaninternal.dbmigration.model; 002 003import io.ebean.migration.MigrationVersion; 004import io.ebeaninternal.dbmigration.migration.ChangeSet; 005import io.ebeaninternal.dbmigration.migration.ChangeSetType; 006import io.ebeaninternal.dbmigration.migration.DropColumn; 007import io.ebeaninternal.dbmigration.migration.DropHistoryTable; 008import io.ebeaninternal.dbmigration.migration.DropTable; 009import io.ebeaninternal.dbmigration.migration.Migration; 010 011import java.util.ArrayList; 012import java.util.Iterator; 013import java.util.LinkedHashMap; 014import java.util.List; 015 016/** 017 * The migrations with pending un-applied drops. 018 */ 019public class PendingDrops { 020 021 private final LinkedHashMap<String, Entry> map = new LinkedHashMap<>(); 022 023 /** 024 * Add a 'pending drops' changeSet for the given version. 025 */ 026 public void add(MigrationVersion version, ChangeSet changeSet) { 027 Entry entry = map.computeIfAbsent(version.normalised(), k -> new Entry(version)); 028 entry.add(changeSet); 029 } 030 031 /** 032 * Return the list of versions with pending drops. 033 */ 034 public List<String> pendingDrops() { 035 List<String> versions = new ArrayList<>(); 036 for (Entry value : map.values()) { 037 if (value.hasPendingDrops()) { 038 versions.add(value.version.asString()); 039 } 040 } 041 return versions; 042 } 043 044 /** 045 * All the pending drops for this migration version have been applied so we need 046 * to remove the (unsuppressed) pending drops for this version. 047 */ 048 public boolean appliedDropsFor(ChangeSet changeSet) { 049 MigrationVersion version = MigrationVersion.parse(changeSet.getDropsFor()); 050 051 Entry entry = map.get(version.normalised()); 052 if (entry.removeDrops(changeSet)) { 053 // it had no suppressForever changeSets so remove completely 054 map.remove(version.normalised()); 055 return true; 056 } 057 058 return false; 059 } 060 061 /** 062 * Return the migration for the pending drops from a version. 063 * <p> 064 * The value of version can be "next" to find the first un-applied pending drops. 065 * </p> 066 */ 067 public Migration migrationForVersion(String pendingVersion) { 068 Entry entry = getEntry(pendingVersion); 069 070 Migration migration = new Migration(); 071 Iterator<ChangeSet> it = entry.list.iterator(); 072 while (it.hasNext()) { 073 ChangeSet changeSet = it.next(); 074 if (!isSuppressForever(changeSet)) { 075 it.remove(); 076 changeSet.setType(ChangeSetType.APPLY); 077 changeSet.setDropsFor(entry.version.asString()); 078 migration.getChangeSet().add(changeSet); 079 } 080 } 081 082 if (migration.getChangeSet().isEmpty()) { 083 throw new IllegalArgumentException("The remaining pendingDrops changeSets in migration [" + pendingVersion + "] are suppressDropsForever=true and can't be applied"); 084 } 085 086 if (!entry.containsSuppressForever()) { 087 // we can remove it completely as it has no suppressForever changes 088 map.remove(entry.version.normalised()); 089 } 090 091 return migration; 092 } 093 094 private Entry getEntry(String pendingVersion) { 095 if ("next".equalsIgnoreCase(pendingVersion)) { 096 Iterator<Entry> it = map.values().iterator(); 097 if (it.hasNext()) { 098 return it.next(); 099 } 100 } else { 101 Entry remove = map.get(MigrationVersion.parse(pendingVersion).normalised()); 102 if (remove != null) { 103 return remove; 104 } 105 } 106 throw new IllegalArgumentException("No 'pendingDrops' changeSets for migration version [" + pendingVersion + "] found"); 107 } 108 109 /** 110 * Register pending drop columns on history tables to the new model. 111 */ 112 public void registerPendingHistoryDropColumns(ModelContainer newModel) { 113 for (Entry entry : map.values()) { 114 for (ChangeSet changeSet : entry.list) { 115 newModel.registerPendingHistoryDropColumns(changeSet); 116 } 117 } 118 } 119 120 /** 121 * Return true if there is an Entry for the given version. 122 */ 123 boolean testContainsEntryFor(MigrationVersion version) { 124 return map.containsKey(version.normalised()); 125 } 126 127 /** 128 * Return the Entry for the given version. 129 */ 130 Entry testGetEntryFor(MigrationVersion version) { 131 return map.get(version.normalised()); 132 } 133 134 static class Entry { 135 136 final MigrationVersion version; 137 final List<ChangeSet> list = new ArrayList<>(); 138 139 Entry(MigrationVersion version) { 140 this.version = version; 141 } 142 143 void add(ChangeSet changeSet) { 144 list.add(changeSet); 145 } 146 147 /** 148 * Return true if this contains suppressForever changeSets. 149 */ 150 boolean containsSuppressForever() { 151 for (ChangeSet changeSet : list) { 152 if (isSuppressForever(changeSet)) { 153 return true; 154 } 155 } 156 return false; 157 } 158 159 /** 160 * Return true if this contains drops that can be applied / migrated. 161 */ 162 boolean hasPendingDrops() { 163 for (ChangeSet changeSet : list) { 164 if (!isSuppressForever(changeSet)) { 165 return true; 166 } 167 } 168 return false; 169 } 170 171 /** 172 * Remove the drops that are not suppressForever and return true if that 173 * removed all the changeSets (and there are no suppressForever ones). 174 */ 175 boolean removeDrops(ChangeSet appliedDrops) { 176 Iterator<ChangeSet> iterator = list.iterator(); 177 while (iterator.hasNext()) { 178 ChangeSet next = iterator.next(); 179 if (!isSuppressForever(next)) { 180 removeMatchingChanges(next, appliedDrops); 181 if (next.getChangeSetChildren().isEmpty()) { 182 iterator.remove(); 183 } 184 } 185 } 186 187 return list.isEmpty(); 188 } 189 190 /** 191 * Remove the applied drops from the pending ones matching by table name and column name. 192 */ 193 private void removeMatchingChanges(ChangeSet pendingDrops, ChangeSet appliedDrops) { 194 List<Object> pending = pendingDrops.getChangeSetChildren(); 195 Iterator<Object> iterator = pending.iterator(); 196 while (iterator.hasNext()) { 197 Object pendingDrop = iterator.next(); 198 if (pendingDrop instanceof DropColumn && dropColumnIn((DropColumn) pendingDrop, appliedDrops)) { 199 iterator.remove(); 200 } else if (pendingDrop instanceof DropTable && dropTableIn((DropTable) pendingDrop, appliedDrops)) { 201 iterator.remove(); 202 } else if (pendingDrop instanceof DropHistoryTable && dropHistoryTableIn((DropHistoryTable) pendingDrop, appliedDrops)) { 203 iterator.remove(); 204 } 205 } 206 } 207 208 /** 209 * Return true if the pendingDrop is contained in the appliedDrops. 210 */ 211 private boolean dropHistoryTableIn(DropHistoryTable pendingDrop, ChangeSet appliedDrops) { 212 for (Object o : appliedDrops.getChangeSetChildren()) { 213 if (o instanceof DropHistoryTable && sameHistoryTable(pendingDrop, (DropHistoryTable) o)) { 214 return true; 215 } 216 } 217 return false; 218 } 219 220 /** 221 * Return true if the pendingDrop is contained in the appliedDrops. 222 */ 223 private boolean dropTableIn(DropTable pendingDrop, ChangeSet appliedDrops) { 224 for (Object o : appliedDrops.getChangeSetChildren()) { 225 if (o instanceof DropTable && sameTable(pendingDrop, (DropTable) o)) { 226 return true; 227 } 228 } 229 return false; 230 } 231 232 /** 233 * Return true if the pendingDrop is contained in the appliedDrops. 234 */ 235 private boolean dropColumnIn(DropColumn pendingDrop, ChangeSet appliedDrops) { 236 for (Object o : appliedDrops.getChangeSetChildren()) { 237 if (o instanceof DropColumn && sameColumn(pendingDrop, (DropColumn) o)) { 238 return true; 239 } 240 } 241 return false; 242 } 243 244 /** 245 * Return true if the DropHistoryTable match by base-table name. 246 */ 247 private boolean sameHistoryTable(DropHistoryTable pendingDrop, DropHistoryTable o) { 248 return pendingDrop.getBaseTable().equals(o.getBaseTable()); 249 } 250 251 /** 252 * Return true if the DropTable match by table name. 253 */ 254 private boolean sameTable(DropTable pendingDrop, DropTable o) { 255 return pendingDrop.getName().equals(o.getName()); 256 } 257 258 /** 259 * Return true if the DropColumns match by table and column name. 260 */ 261 private boolean sameColumn(DropColumn pending, DropColumn o) { 262 return pending.getColumnName().equals(o.getColumnName()) 263 && pending.getTableName().equals(o.getTableName()); 264 } 265 266 } 267 268 private static boolean isSuppressForever(ChangeSet next) { 269 return Boolean.TRUE.equals(next.isSuppressDropsForever()); 270 } 271 272}