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