001package io.ebeaninternal.dbmigration.model;
002
003import io.ebeaninternal.dbmigration.ddlgeneration.platform.DdlHelp;
004import io.ebeaninternal.dbmigration.migration.AddColumn;
005import io.ebeaninternal.dbmigration.migration.AddHistoryTable;
006import io.ebeaninternal.dbmigration.migration.AddTableComment;
007import io.ebeaninternal.dbmigration.migration.AlterColumn;
008import io.ebeaninternal.dbmigration.migration.Column;
009import io.ebeaninternal.dbmigration.migration.CreateTable;
010import io.ebeaninternal.dbmigration.migration.DropColumn;
011import io.ebeaninternal.dbmigration.migration.DropHistoryTable;
012import io.ebeaninternal.dbmigration.migration.DropTable;
013import io.ebeaninternal.dbmigration.migration.ForeignKey;
014import io.ebeaninternal.dbmigration.migration.RenameColumn;
015import io.ebeaninternal.dbmigration.migration.UniqueConstraint;
016import io.ebeaninternal.server.deploy.BeanDescriptor;
017import io.ebeaninternal.server.deploy.BeanProperty;
018import io.ebeaninternal.server.deploy.IdentityMode;
019import io.ebeaninternal.server.deploy.PartitionMeta;
020import org.slf4j.Logger;
021import org.slf4j.LoggerFactory;
022
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.HashSet;
026import java.util.LinkedHashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import static io.ebeaninternal.dbmigration.ddlgeneration.platform.SplitColumns.split;
032import static io.ebeaninternal.dbmigration.model.MTableIdentity.fromCreateTable;
033import static io.ebeaninternal.dbmigration.model.MTableIdentity.toCreateTable;
034
035/**
036 * Holds the logical model for a given Table and everything associated to it.
037 * <p>
038 * This effectively represents a table, its columns and all associated
039 * constraints, foreign keys and indexes.
040 * <p>
041 * Migrations can be applied to this such that it represents the state
042 * of a given table after various migrations have been applied.
043 * <p>
044 * This table model can also be derived from the EbeanServer bean descriptor
045 * and associated properties.
046 */
047public class MTable {
048
049  private static final Logger logger = LoggerFactory.getLogger(MTable.class);
050
051  private final String name;
052  private MTable draftTable;
053  /**
054   * Marked true for draft tables. These need to have their FK references adjusted
055   * after all the draft tables have been identified.
056   */
057  private boolean draft;
058  private PartitionMeta partitionMeta;
059  private String pkName;
060  private String comment;
061  private String tablespace;
062  private String storageEngine;
063  private String indexTablespace;
064  private IdentityMode identityMode;
065  private boolean withHistory;
066  private final Map<String, MColumn> columns = new LinkedHashMap<>();
067
068  /**
069   * Compound unique constraints.
070   */
071  private final List<MCompoundUniqueConstraint> uniqueConstraints = new ArrayList<>();
072
073  /**
074   * Compound foreign keys.
075   */
076  private final List<MCompoundForeignKey> compoundKeys = new ArrayList<>();
077
078  /**
079   * Column name for the 'When created' column. This can be used for the initial effective start date when adding
080   * history to an existing table and maps to a @WhenCreated or @CreatedTimestamp column.
081   */
082  private String whenCreatedColumn;
083
084  /**
085   * Temporary - holds addColumn settings.
086   */
087  private AddColumn addColumn;
088
089  private final List<String> droppedColumns = new ArrayList<>();
090
091  public MTable(BeanDescriptor<?> descriptor) {
092    this.name = descriptor.baseTable();
093    this.identityMode = descriptor.identityMode();
094    this.storageEngine = descriptor.storageEngine();
095    this.partitionMeta = descriptor.partitionMeta();
096    this.comment = descriptor.dbComment();
097    if (descriptor.isHistorySupport()) {
098      withHistory = true;
099      BeanProperty whenCreated = descriptor.whenCreatedProperty();
100      if (whenCreated != null) {
101        whenCreatedColumn = whenCreated.dbColumn();
102      }
103    }
104  }
105
106  /**
107   * Construct for element collection or intersection table.
108   */
109  public MTable(String name) {
110    this.name = name;
111    this.identityMode = IdentityMode.NONE;
112  }
113
114  /**
115   * Create a copy of this table structure as a 'draft' table.
116   * <p>
117   * Note that both tables contain @DraftOnly MColumns and these are filtered out
118   * later when creating the CreateTable object.
119   */
120  public MTable createDraftTable() {
121    draftTable = new MTable(name + "_draft");
122    draftTable.draft = true;
123    draftTable.whenCreatedColumn = whenCreatedColumn;
124    // compoundKeys
125    // compoundUniqueConstraints
126    draftTable.identityMode = identityMode;
127    for (MColumn col : allColumns()) {
128      draftTable.addColumn(col.copyForDraft());
129    }
130    return draftTable;
131  }
132
133  /**
134   * Construct for migration.
135   */
136  public MTable(CreateTable createTable) {
137    this.name = createTable.getName();
138    this.pkName = createTable.getPkName();
139    this.comment = createTable.getComment();
140    this.storageEngine = createTable.getStorageEngine();
141    this.tablespace = createTable.getTablespace();
142    this.indexTablespace = createTable.getIndexTablespace();
143    this.withHistory = Boolean.TRUE.equals(createTable.isWithHistory());
144    this.draft = Boolean.TRUE.equals(createTable.isDraft());
145    this.identityMode = fromCreateTable(createTable);
146    List<Column> cols = createTable.getColumn();
147    for (Column column : cols) {
148      addColumn(column);
149    }
150    for (UniqueConstraint uq : createTable.getUniqueConstraint()) {
151      uniqueConstraints.add(new MCompoundUniqueConstraint(uq));
152    }
153
154    for (ForeignKey fk : createTable.getForeignKey()) {
155      if (DdlHelp.isDropForeignKey(fk.getColumnNames())) {
156        removeForeignKey(fk.getName());
157      } else {
158        addForeignKey(fk.getName(), fk.getRefTableName(), fk.getIndexName(), fk.getColumnNames(), fk.getRefColumnNames());
159      }
160    }
161  }
162
163  public void addForeignKey(String name, String refTableName, String indexName, String columnNames, String refColumnNames) {
164    MCompoundForeignKey foreignKey = new MCompoundForeignKey(name, refTableName, indexName);
165    String[] cols = split(columnNames);
166    String[] refCols = split(refColumnNames);
167    for (int i = 0; i < cols.length && i < refCols.length; i++) {
168      foreignKey.addColumnPair(cols[i], refCols[i]);
169    }
170    addForeignKey(foreignKey);
171  }
172
173  /**
174   * Return the DropTable migration for this table.
175   */
176  public DropTable dropTable() {
177    DropTable dropTable = new DropTable();
178    dropTable.setName(name);
179    // we must add pk col name & sequence name, as we have to delete the sequence also.
180    if (identityMode.isDatabaseIdentity()) {
181      String pkCol = null;
182      for (MColumn column : columns.values()) {
183        if (column.isPrimaryKey()) {
184          if (pkCol == null) {
185            pkCol = column.getName();
186          } else { // multiple pk cols -> no sequence
187            pkCol = null;
188            break;
189          }
190        }
191      }
192      if (pkCol != null) {
193        dropTable.setSequenceCol(pkCol);
194        dropTable.setSequenceName(identityMode.getSequenceName());
195      }
196    }
197    return dropTable;
198  }
199
200  /**
201   * Return the CreateTable migration for this table.
202   */
203  public CreateTable createTable() {
204    CreateTable createTable = new CreateTable();
205    createTable.setName(name);
206    createTable.setPkName(pkName);
207    createTable.setComment(comment);
208    if (partitionMeta != null) {
209      createTable.setPartitionMode(partitionMeta.getMode().name());
210      createTable.setPartitionColumn(partitionMeta.getProperty());
211    }
212    createTable.setStorageEngine(storageEngine);
213    createTable.setTablespace(tablespace);
214    createTable.setIndexTablespace(indexTablespace);
215    toCreateTable(identityMode, createTable);
216    if (withHistory) {
217      createTable.setWithHistory(Boolean.TRUE);
218    }
219    if (draft) {
220      createTable.setDraft(Boolean.TRUE);
221    }
222    for (MColumn column : allColumns()) {
223      // filter out draftOnly columns from the base table
224      if (draft || !column.isDraftOnly()) {
225        createTable.getColumn().add(column.createColumn());
226      }
227    }
228    for (MCompoundForeignKey compoundKey : compoundKeys) {
229      createTable.getForeignKey().add(compoundKey.createForeignKey());
230    }
231    for (MCompoundUniqueConstraint constraint : uniqueConstraints) {
232      createTable.getUniqueConstraint().add(constraint.getUniqueConstraint());
233    }
234    return createTable;
235  }
236
237  /**
238   * Compare to another version of the same table to perform a diff.
239   */
240  public void compare(ModelDiff modelDiff, MTable newTable) {
241    if (withHistory != newTable.withHistory) {
242      if (withHistory) {
243        DropHistoryTable dropHistoryTable = new DropHistoryTable();
244        dropHistoryTable.setBaseTable(name);
245        modelDiff.addDropHistoryTable(dropHistoryTable);
246
247      } else {
248        AddHistoryTable addHistoryTable = new AddHistoryTable();
249        addHistoryTable.setBaseTable(name);
250        modelDiff.addAddHistoryTable(addHistoryTable);
251      }
252    }
253
254    compareColumns(modelDiff, newTable);
255
256    if (MColumn.different(comment, newTable.comment)) {
257      AddTableComment addTableComment = new AddTableComment();
258      addTableComment.setName(name);
259      if (newTable.comment == null) {
260        addTableComment.setComment(DdlHelp.DROP_COMMENT);
261      } else {
262        addTableComment.setComment(newTable.comment);
263      }
264      modelDiff.addTableComment(addTableComment);
265    }
266
267    compareCompoundKeys(modelDiff, newTable);
268    compareUniqueKeys(modelDiff, newTable);
269  }
270
271  private void compareColumns(ModelDiff modelDiff, MTable newTable) {
272    addColumn = null;
273    Map<String, MColumn> newColumnMap = newTable.getColumns();
274
275    // compare newColumns to existing columns (look for new and diff columns)
276    for (MColumn newColumn : newColumnMap.values()) {
277      MColumn localColumn = getColumn(newColumn.getName());
278      if (localColumn == null) {
279        // can ignore if draftOnly column and non-draft table
280        if (!newColumn.isDraftOnly() || draft) {
281          diffNewColumn(newColumn);
282        }
283      } else {
284        localColumn.compare(modelDiff, this, newColumn);
285      }
286    }
287
288    // compare existing columns (look for dropped columns)
289    for (MColumn existingColumn : allColumns()) {
290      MColumn newColumn = newColumnMap.get(existingColumn.getName());
291      if (newColumn == null) {
292        diffDropColumn(modelDiff, existingColumn);
293      } else if (newColumn.isDraftOnly() && !draft) {
294        // effectively a drop column (draft only column on a non-draft table)
295        logger.trace("... drop column {} from table {} as now draftOnly", newColumn.getName(), name);
296        diffDropColumn(modelDiff, existingColumn);
297      }
298    }
299
300    if (addColumn != null) {
301      modelDiff.addAddColumn(addColumn);
302    }
303  }
304
305
306  private void compareCompoundKeys(ModelDiff modelDiff, MTable newTable) {
307    List<MCompoundForeignKey> newKeys = new ArrayList<>(newTable.getCompoundKeys());
308    List<MCompoundForeignKey> currentKeys = new ArrayList<>(getCompoundKeys());
309
310    // remove keys that have not changed
311    currentKeys.removeAll(newTable.getCompoundKeys());
312    newKeys.removeAll(getCompoundKeys());
313
314    for (MCompoundForeignKey currentKey : currentKeys) {
315      modelDiff.addAlterForeignKey(currentKey.dropForeignKey(name));
316    }
317
318    for (MCompoundForeignKey newKey : newKeys) {
319      modelDiff.addAlterForeignKey(newKey.addForeignKey(name));
320    }
321  }
322
323  private void compareUniqueKeys(ModelDiff modelDiff, MTable newTable) {
324    List<MCompoundUniqueConstraint> newKeys = new ArrayList<>(newTable.getUniqueConstraints());
325    List<MCompoundUniqueConstraint> currentKeys = new ArrayList<>(getUniqueConstraints());
326
327    // remove keys that have not changed
328    currentKeys.removeAll(newTable.getUniqueConstraints());
329    newKeys.removeAll(getUniqueConstraints());
330
331    for (MCompoundUniqueConstraint currentKey : currentKeys) {
332      modelDiff.addUniqueConstraint(currentKey.dropUniqueConstraint(name));
333    }
334    for (MCompoundUniqueConstraint newKey : newKeys) {
335      modelDiff.addUniqueConstraint(newKey.addUniqueConstraint(name));
336    }
337  }
338
339  /**
340   * Apply AddColumn migration.
341   */
342  public void apply(AddColumn addColumn) {
343    checkTableName(addColumn.getTableName());
344    for (Column column : addColumn.getColumn()) {
345      addColumn(column);
346    }
347  }
348
349  /**
350   * Apply AddColumn migration.
351   */
352  public void apply(AlterColumn alterColumn) {
353    checkTableName(alterColumn.getTableName());
354    String columnName = alterColumn.getColumnName();
355    MColumn existingColumn = getColumn(columnName);
356    if (existingColumn == null) {
357      throw new IllegalStateException("Column [" + columnName + "] does not exist for AlterColumn change?");
358    }
359    existingColumn.apply(alterColumn);
360  }
361
362  /**
363   * Apply DropColumn migration.
364   */
365  public void apply(DropColumn dropColumn) {
366    checkTableName(dropColumn.getTableName());
367    MColumn removed = columns.remove(dropColumn.getColumnName());
368    if (removed == null) {
369      throw new IllegalStateException("Column [" + dropColumn.getColumnName() + "] does not exist for DropColumn change on table [" + dropColumn.getTableName() + "]?");
370    }
371  }
372
373  public void apply(RenameColumn renameColumn) {
374    checkTableName(renameColumn.getTableName());
375    MColumn column = columns.remove(renameColumn.getOldName());
376    if (column == null) {
377      throw new IllegalStateException("Column [" + renameColumn.getOldName() + "] does not exist for RenameColumn change on table [" + renameColumn.getTableName() + "]?");
378    }
379    addColumn(column.rename(renameColumn.getNewName()));
380  }
381
382  public String getName() {
383    return name;
384  }
385
386  public String getSchema() {
387    int pos = name.indexOf('.');
388    return pos == -1 ? null : name.substring(0, pos);
389  }
390
391  /**
392   * Return true if this table is a 'Draft' table.
393   */
394  public boolean isDraft() {
395    return draft;
396  }
397
398  /**
399   * Return true if this table is partitioned.
400   */
401  public boolean isPartitioned() {
402    return partitionMeta != null;
403  }
404
405  /**
406   * Return the partition meta for this table.
407   */
408  public PartitionMeta getPartitionMeta() {
409    return partitionMeta;
410  }
411
412  public String getPkName() {
413    return pkName;
414  }
415
416  public void setPkName(String pkName) {
417    this.pkName = pkName;
418  }
419
420  public String getComment() {
421    return comment;
422  }
423
424  public void setComment(String comment) {
425    this.comment = comment;
426  }
427
428  public String getTablespace() {
429    return tablespace;
430  }
431
432  public String getIndexTablespace() {
433    return indexTablespace;
434  }
435
436  public boolean isWithHistory() {
437    return withHistory;
438  }
439
440  public MTable setWithHistory(boolean withHistory) {
441    this.withHistory = withHistory;
442    return this;
443  }
444
445  public List<String> allHistoryColumns(boolean includeDropped) {
446    List<String> columnNames = new ArrayList<>(columns.size());
447    for (MColumn column : columns.values()) {
448      if (column.isIncludeInHistory()) {
449        columnNames.add(column.getName());
450      }
451    }
452    if (includeDropped && !droppedColumns.isEmpty()) {
453      columnNames.addAll(droppedColumns);
454    }
455    return columnNames;
456  }
457
458  /**
459   * Return all the columns (excluding columns marked as dropped).
460   */
461  public Collection<MColumn> allColumns() {
462    return columns.values();
463  }
464
465  /**
466   * Return the column by name.
467   */
468  public MColumn getColumn(String name) {
469    return columns.get(name);
470  }
471
472  private Map<String, MColumn> getColumns() {
473    return columns;
474  }
475
476  public List<MCompoundUniqueConstraint> getUniqueConstraints() {
477    return uniqueConstraints;
478  }
479
480  public List<MCompoundForeignKey> getCompoundKeys() {
481    return compoundKeys;
482  }
483
484  public String getWhenCreatedColumn() {
485    return whenCreatedColumn;
486  }
487
488  /**
489   * Return the list of columns that make the primary key.
490   */
491  public List<MColumn> primaryKeyColumns() {
492    List<MColumn> pk = new ArrayList<>(3);
493    for (MColumn column : allColumns()) {
494      if (column.isPrimaryKey()) {
495        pk.add(column);
496      }
497    }
498    return pk;
499  }
500
501  /**
502   * Return the primary key column if it is a simple primary key.
503   */
504  public String singlePrimaryKey() {
505    List<MColumn> columns = primaryKeyColumns();
506    if (columns.size() == 1) {
507      return columns.get(0).getName();
508    }
509    return null;
510  }
511
512  private void checkTableName(String tableName) {
513    if (!name.equals(tableName)) {
514      throw new IllegalArgumentException("addColumn tableName [" + tableName + "] does not match [" + name + "]");
515    }
516  }
517
518  /**
519   * Add a column via migration data.
520   */
521  private void addColumn(Column column) {
522    columns.put(column.getName(), new MColumn(column));
523  }
524
525  /**
526   * Add a model column (typically from EbeanServer meta data).
527   */
528  public void addColumn(MColumn column) {
529    columns.put(column.getName(), column);
530  }
531
532  /**
533   * Add a unique constraint.
534   */
535  public void addUniqueConstraint(MCompoundUniqueConstraint uniqueConstraint) {
536    uniqueConstraints.add(uniqueConstraint);
537  }
538
539  /**
540   * Add a compound foreign key.
541   */
542  public void addForeignKey(MCompoundForeignKey compoundKey) {
543    compoundKeys.add(compoundKey);
544  }
545
546  /**
547   * Add a column checking if it already exists and if so return the existing column.
548   * Sometimes the case for a primaryKey that is also a foreign key.
549   */
550  public MColumn addColumn(String dbCol, String columnDefn, boolean notnull) {
551    MColumn existingColumn = getColumn(dbCol);
552    if (existingColumn != null) {
553      if (notnull) {
554        existingColumn.setNotnull(true);
555      }
556      return existingColumn;
557    }
558
559    MColumn newCol = new MColumn(dbCol, columnDefn, notnull);
560    addColumn(newCol);
561    return newCol;
562  }
563
564  /**
565   * Add a 'new column' to the AddColumn migration object.
566   */
567  private void diffNewColumn(MColumn newColumn) {
568    if (addColumn == null) {
569      addColumn = new AddColumn();
570      addColumn.setTableName(name);
571      if (withHistory) {
572        // These addColumns need to occur on the history
573        // table as well as the base table
574        addColumn.setWithHistory(Boolean.TRUE);
575      }
576    }
577
578    addColumn.getColumn().add(newColumn.createColumn());
579  }
580
581  /**
582   * Add a 'drop column' to the diff.
583   */
584  private void diffDropColumn(ModelDiff modelDiff, MColumn existingColumn) {
585    DropColumn dropColumn = new DropColumn();
586    dropColumn.setTableName(name);
587    dropColumn.setColumnName(existingColumn.getName());
588    if (withHistory) {
589      // These dropColumns should occur on the history
590      // table as well as the base table
591      dropColumn.setWithHistory(Boolean.TRUE);
592    }
593    modelDiff.addDropColumn(dropColumn);
594  }
595
596  /**
597   * Register a pending un-applied drop column.
598   * <p>
599   * This means this column still needs to be included in history views/triggers etc even
600   * though it is not part of the current model.
601   */
602  public void registerPendingDropColumn(String columnName) {
603    droppedColumns.add(columnName);
604  }
605
606  /**
607   * Check if there are duplicate foreign keys.
608   * <p>
609   * This can occur when an ManyToMany relates back to itself.
610   * </p>
611   */
612  public void checkDuplicateForeignKeys() {
613    if (hasDuplicateForeignKeys()) {
614      int counter = 1;
615      for (MCompoundForeignKey fk : compoundKeys) {
616        fk.addNameSuffix(counter++);
617      }
618    }
619  }
620
621  /**
622   * Return true if the foreign key names are not unique.
623   */
624  private boolean hasDuplicateForeignKeys() {
625    Set<String> fkNames = new HashSet<>();
626    for (MCompoundForeignKey fk : compoundKeys) {
627      if (!fkNames.add(fk.getName())) {
628        return true;
629      }
630    }
631    return false;
632  }
633
634  /**
635   * Adjust the references (FK) if it should relate to a draft table.
636   */
637  public void adjustReferences(ModelContainer modelContainer) {
638    Collection<MColumn> cols = allColumns();
639    for (MColumn col : cols) {
640      String references = col.getReferences();
641      if (references != null) {
642        String baseTable = extractBaseTable(references);
643        MTable refBaseTable = modelContainer.getTable(baseTable);
644        if (refBaseTable.draftTable != null) {
645          // change references to another associated 'draft' table
646          String newReferences = deriveReferences(references, refBaseTable.draftTable.getName());
647          col.setReferences(newReferences);
648        }
649      }
650    }
651  }
652
653  /**
654   * Return the base table name from references (table.column).
655   */
656  private String extractBaseTable(String references) {
657    int lastDot = references.lastIndexOf('.');
658    return references.substring(0, lastDot);
659  }
660
661  /**
662   * Return the new references using the given draftTableName.
663   * (The referenced column is the same as before).
664   */
665  private String deriveReferences(String references, String draftTableName) {
666    int lastDot = references.lastIndexOf('.');
667    return draftTableName + "." + references.substring(lastDot + 1);
668  }
669
670  /**
671   * This method adds information which columns are nullable or not to the compound indices.
672   */
673  public void updateCompoundIndices() {
674    for (MCompoundUniqueConstraint uniq : uniqueConstraints) {
675      List<String> nullableColumns = new ArrayList<>();
676      for (String columnName : uniq.getColumns()) {
677        MColumn col = getColumn(columnName);
678        if (col != null && !col.isNotnull()) {
679          nullableColumns.add(columnName);
680        }
681      }
682      uniq.setNullableColumns(nullableColumns.toArray(new String[0]));
683    }
684  }
685
686  public void removeForeignKey(String name) {
687    compoundKeys.removeIf(fk -> fk.getName().equals(name));
688  }
689
690  /**
691   * Clear the indexes on the foreign keys as they are covered by unique constraints.
692   */
693  public void clearForeignKeyIndexes() {
694    for (MCompoundForeignKey compoundKey : compoundKeys) {
695      compoundKey.setIndexName(null);
696    }
697  }
698
699  /**
700   * Clear foreign key as this element collection table logically references
701   * back to multiple tables.
702   */
703  public MIndex setReusedElementCollection() {
704    MIndex index = null;
705    for (MColumn column : columns.values()) {
706      final String references = column.getReferences();
707      if (references != null) {
708        index = new MIndex(column.getForeignKeyIndex(), name, column.getName());
709        column.clearForeignKey();
710      }
711    }
712    return index;
713  }
714}