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