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