001package io.ebeaninternal.dbmigration.model;
002
003import io.ebean.migration.MigrationVersion;
004import io.ebeaninternal.dbmigration.ddlgeneration.platform.DdlHelp;
005import io.ebeaninternal.dbmigration.migration.AddColumn;
006import io.ebeaninternal.dbmigration.migration.AddHistoryTable;
007import io.ebeaninternal.dbmigration.migration.AddTableComment;
008import io.ebeaninternal.dbmigration.migration.AddUniqueConstraint;
009import io.ebeaninternal.dbmigration.migration.AlterColumn;
010import io.ebeaninternal.dbmigration.migration.AlterForeignKey;
011import io.ebeaninternal.dbmigration.migration.ChangeSet;
012import io.ebeaninternal.dbmigration.migration.ChangeSetType;
013import io.ebeaninternal.dbmigration.migration.CreateIndex;
014import io.ebeaninternal.dbmigration.migration.CreateTable;
015import io.ebeaninternal.dbmigration.migration.DropColumn;
016import io.ebeaninternal.dbmigration.migration.DropHistoryTable;
017import io.ebeaninternal.dbmigration.migration.DropIndex;
018import io.ebeaninternal.dbmigration.migration.DropTable;
019import io.ebeaninternal.dbmigration.migration.Migration;
020import io.ebeaninternal.dbmigration.migration.RenameColumn;
021import io.ebeaninternal.dbmigration.migration.Sql;
022
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.TreeSet;
030
031/**
032 * Holds all the tables, views, indexes etc that represent the model.
033 * <p>
034 * Migration changeSets can be applied to the model.
035 * </p>
036 */
037public class ModelContainer {
038
039  private final Set<String> schemas = new TreeSet<>();
040  /**
041   * All the tables in the model.
042   */
043  private final Map<String, MTable> tables = new LinkedHashMap<>();
044
045  /**
046   * All the non-unique non-foreign key indexes.
047   */
048  private final Map<String, MIndex> indexes = new LinkedHashMap<>();
049  private final PendingDrops pendingDrops = new PendingDrops();
050  private final List<MTable> partitionedTables = new ArrayList<>();
051
052  public ModelContainer() {
053  }
054
055  /**
056   * Return the schemas.
057   */
058  public Set<String> getSchemas() {
059    return schemas;
060  }
061
062  /**
063   * Return true if the model contains tables that are partitioned.
064   */
065  public boolean isTablePartitioning() {
066    return !partitionedTables.isEmpty();
067  }
068
069  /**
070   * Return the list of partitioned tables.
071   */
072  public List<MTable> getPartitionedTables() {
073    return partitionedTables;
074  }
075
076  /**
077   * Adjust the FK references on all the draft tables.
078   */
079  public void adjustDraftReferences() {
080    for (MTable table : this.tables.values()) {
081      if (table.isDraft()) {
082        table.adjustReferences(this);
083      }
084    }
085  }
086
087  /**
088   * Return the map of all the tables.
089   */
090  public Map<String, MTable> getTables() {
091    return tables;
092  }
093
094  /**
095   * Return the table by name.
096   */
097  public MTable getTable(String tableName) {
098    return tables.get(tableName);
099  }
100
101  /**
102   * Lookup the matching index during DIFF migration processing.
103   */
104  public MIndex getIndex(MIndex newIndex) {
105    return indexes.get(newIndex.getKey());
106  }
107
108  public Collection<MIndex> allIndexes() {
109    return indexes.values();
110  }
111
112  /**
113   * Return true if the index does not exist and should be dropped.
114   */
115  public boolean dropIndex(MIndex existingIndex) {
116    return !indexes.containsKey(existingIndex.getKey());
117  }
118
119  /**
120   * Apply a migration with associated changeSets to the model.
121   */
122  public void apply(Migration migration, MigrationVersion version) {
123
124    List<ChangeSet> changeSets = migration.getChangeSet();
125    for (ChangeSet changeSet : changeSets) {
126      boolean pending = changeSet.getType() == ChangeSetType.PENDING_DROPS;
127      if (pending) {
128        // un-applied drop columns etc
129        pendingDrops.add(version, changeSet);
130
131      } else if (isDropsFor(changeSet)) {
132        pendingDrops.appliedDropsFor(changeSet);
133      }
134      if (!isDropsFor(changeSet)) {
135        applyChangeSet(changeSet);
136      }
137    }
138  }
139
140  /**
141   * Return true if the changeSet contains drops for a previous PENDING_DROPS changeSet.
142   */
143  private boolean isDropsFor(ChangeSet changeSet) {
144    return changeSet.getDropsFor() != null;
145  }
146
147  /**
148   * Apply a changeSet to the model.
149   */
150  protected void applyChangeSet(ChangeSet changeSet) {
151
152    List<Object> changeSetChildren = changeSet.getChangeSetChildren();
153    for (Object change : changeSetChildren) {
154      if (change instanceof CreateTable) {
155        applyChange((CreateTable) change);
156      } else if (change instanceof DropTable) {
157        applyChange((DropTable) change);
158      } else if (change instanceof AlterColumn) {
159        applyChange((AlterColumn) change);
160      } else if (change instanceof AddColumn) {
161        applyChange((AddColumn) change);
162      } else if (change instanceof DropColumn) {
163        applyChange((DropColumn) change);
164      } else if (change instanceof RenameColumn) {
165        applyChange((RenameColumn) change);
166      } else if (change instanceof CreateIndex) {
167        applyChange((CreateIndex) change);
168      } else if (change instanceof DropIndex) {
169        applyChange((DropIndex) change);
170      } else if (change instanceof AddHistoryTable) {
171        applyChange((AddHistoryTable) change);
172      } else if (change instanceof DropHistoryTable) {
173        applyChange((DropHistoryTable) change);
174      } else if (change instanceof AddUniqueConstraint) {
175        applyChange((AddUniqueConstraint) change);
176      } else if (change instanceof AlterForeignKey) {
177        applyChange((AlterForeignKey) change);
178      } else if (change instanceof AddTableComment) {
179        applyChange((AddTableComment) change);
180      } else if (change instanceof Sql) {
181        // do nothing
182      } else {
183        throw new IllegalArgumentException("No rule for " + change);
184      }
185    }
186  }
187
188  /**
189   * Set the withHistory flag on the associated base table.
190   */
191  private void applyChange(AddHistoryTable change) {
192
193    MTable table = tables.get(change.getBaseTable());
194    if (table == null) {
195      throw new IllegalStateException("Table [" + change.getBaseTable() + "] does not exist in model?");
196    }
197    table.setWithHistory(true);
198  }
199
200  /**
201   * Unset the withHistory flag on the associated base table.
202   */
203  protected void applyChange(DropHistoryTable change) {
204
205    MTable table = tables.get(change.getBaseTable());
206    if (table != null) {
207      table.setWithHistory(false);
208    }
209  }
210
211  private void applyChange(AddUniqueConstraint change) {
212    MTable table = tables.get(change.getTableName());
213    if (table == null) {
214      throw new IllegalStateException("Table [" + change.getTableName() + "] does not exist in model?");
215    }
216    if (DdlHelp.isDropConstraint(change.getColumnNames())) {
217      table.getUniqueConstraints().removeIf(constraint -> constraint.getName().equals(change.getConstraintName()));
218    } else {
219      table.getUniqueConstraints().add(new MCompoundUniqueConstraint(change));
220    }
221  }
222
223  private void applyChange(AlterForeignKey change) {
224    MTable table = tables.get(change.getTableName());
225    if (table == null) {
226      throw new IllegalStateException("Table [" + change.getName() + "] does not exist in model?");
227    }
228    if (DdlHelp.isDropForeignKey(change.getColumnNames())) {
229      table.removeForeignKey(change.getName());
230    } else {
231      table.addForeignKey(change.getName(), change.getRefTableName(), change.getIndexName(), change.getColumnNames(), change.getRefColumnNames());
232    }
233  }
234
235  private void applyChange(AddTableComment change) {
236    MTable table = tables.get(change.getName());
237    if (table == null) {
238      throw new IllegalStateException("Table [" + change.getName() + "] does not exist in model?");
239    }
240    if (DdlHelp.isDropComment(change.getComment())) {
241      table.setComment(null);
242    } else {
243      table.setComment(change.getComment());
244    }
245  }
246
247  /**
248   * Apply a CreateTable change to the model.
249   */
250  protected void applyChange(CreateTable createTable) {
251    String tableName = createTable.getName();
252    if (tables.containsKey(tableName)) {
253      throw new IllegalStateException("Table [" + tableName + "] already exists in model?");
254    }
255    tables.put(tableName, new MTable(createTable));
256  }
257
258  /**
259   * Apply a DropTable change to the model.
260   */
261  protected void applyChange(DropTable dropTable) {
262    tables.remove(dropTable.getName());
263  }
264
265  /**
266   * Apply a CreateTable change to the model.
267   */
268  protected void applyChange(CreateIndex createIndex) {
269    String indexName = createIndex.getIndexName();
270    if (indexes.containsKey(indexName)) {
271      throw new IllegalStateException("Index [" + indexName + "] already exists in model?");
272    }
273    MIndex index = new MIndex(createIndex);
274    indexes.put(index.getKey(), index);
275  }
276
277  /**
278   * Apply a DropTable change to the model.
279   */
280  protected void applyChange(DropIndex dropIndex) {
281    indexes.remove(dropIndex.getIndexName());
282  }
283
284  /**
285   * Apply a AddColumn change to the model.
286   */
287  protected void applyChange(AddColumn addColumn) {
288    MTable table = tables.get(addColumn.getTableName());
289    if (table == null) {
290      throw new IllegalStateException("Table [" + addColumn.getTableName() + "] does not exist in model?");
291    }
292    table.apply(addColumn);
293  }
294
295  /**
296   * Apply a AddColumn change to the model.
297   */
298  protected void applyChange(AlterColumn alterColumn) {
299    MTable table = tables.get(alterColumn.getTableName());
300    if (table == null) {
301      throw new IllegalStateException("Table [" + alterColumn.getTableName() + "] does not exist in model?");
302    }
303    table.apply(alterColumn);
304  }
305
306  /**
307   * Apply a DropColumn change to the model.
308   */
309  protected void applyChange(DropColumn dropColumn) {
310    MTable table = tables.get(dropColumn.getTableName());
311    if (table == null) {
312      throw new IllegalStateException("Table [" + dropColumn.getTableName() + "] does not exist in model?");
313    }
314    table.apply(dropColumn);
315  }
316
317  protected void applyChange(RenameColumn renameColumn) {
318    MTable table = tables.get(renameColumn.getTableName());
319    if (table == null) {
320      throw new IllegalStateException("Table [" + renameColumn.getTableName() + "] does not exist in model?");
321    }
322    table.apply(renameColumn);
323  }
324
325  /**
326   * Add a table (typically from reading EbeanServer meta data).
327   */
328  public MTable addTable(MTable table) {
329    if (table.isPartitioned()) {
330      partitionedTables.add(table);
331    }
332    String schema = table.getSchema();
333    if (schema != null) {
334      schemas.add(schema);
335    }
336    return tables.put(table.getName(), table);
337  }
338
339  /**
340   * Add an element table taking into account if it is reused/references back
341   * to multiple bean types (and so can't have foreign key).
342   */
343  public void addTableElementCollection(MTable table) {
344    final MTable reusedElementCollection = tables.get(table.getName());
345    if (reusedElementCollection != null) {
346      final MIndex index = reusedElementCollection.setReusedElementCollection();
347      if (index != null) {
348        indexes.put(index.getKey(), index);
349      }
350    } else {
351      if (table.isPartitioned()) {
352        partitionedTables.add(table);
353      }
354      tables.put(table.getName(), table);
355    }
356  }
357
358  /**
359   * Add an index.
360   */
361  public void addIndex(MIndex index) {
362    indexes.put(index.getKey(), index);
363  }
364
365  /**
366   * Return the list of versions containing un-applied pending drops.
367   */
368  public List<String> getPendingDrops() {
369    return pendingDrops.pendingDrops();
370  }
371
372  /**
373   * Return the migration for the pending drops for a given version.
374   */
375  public Migration migrationForPendingDrop(String pendingVersion) {
376    return pendingDrops.migrationForVersion(pendingVersion);
377  }
378
379  /**
380   * Register the drop columns on history tables that have not been applied yet.
381   */
382  public void registerPendingHistoryDropColumns(ModelContainer newModel) {
383    pendingDrops.registerPendingHistoryDropColumns(newModel);
384  }
385
386  /**
387   * Register any pending drop columns on history tables.  These columns are now not in the current
388   * logical model but we still need to include them in the history views and triggers until they
389   * are actually dropped.
390   */
391  public void registerPendingHistoryDropColumns(ChangeSet changeSet) {
392    for (Object change : changeSet.getChangeSetChildren()) {
393      if (change instanceof DropColumn) {
394        DropColumn dropColumn = (DropColumn) change;
395        if (Boolean.TRUE.equals(dropColumn.isWithHistory())) {
396          registerPendingDropColumn(dropColumn);
397        }
398      }
399    }
400  }
401
402  /**
403   * Register a drop column on a history tables that has not been applied yet.
404   */
405  private void registerPendingDropColumn(DropColumn dropColumn) {
406
407    MTable table = getTable(dropColumn.getTableName());
408    if (table == null) {
409      throw new IllegalArgumentException("Table [" + dropColumn.getTableName() + "] not found?");
410    }
411    table.registerPendingDropColumn(dropColumn.getColumnName());
412  }
413}