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