package io.ebean.dbmigration.model; import io.ebean.dbmigration.migration.AddColumn; import io.ebean.dbmigration.migration.AddHistoryTable; import io.ebean.dbmigration.migration.AddTableComment; import io.ebean.dbmigration.migration.AlterColumn; import io.ebean.dbmigration.migration.Column; import io.ebean.dbmigration.migration.CreateTable; import io.ebean.dbmigration.migration.DropColumn; import io.ebean.dbmigration.migration.DropHistoryTable; import io.ebean.dbmigration.migration.DropTable; import io.ebean.dbmigration.migration.IdentityType; import io.ebean.dbmigration.migration.UniqueConstraint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * Holds the logical model for a given Table and everything associated to it. * <p> * This effectively represents a table, its columns and all associated * constraints, foreign keys and indexes. * </p> * <p> * Migrations can be applied to this such that it represents the state * of a given table after various migrations have been applied. * </p> * <p> * This table model can also be derived from the EbeanServer bean descriptor * and associated properties. * </p> */ public class MTable { private static final Logger logger = LoggerFactory.getLogger(MTable.class); /** * Table name. */ private final String name; /** * The associated draft table. */ private MTable draftTable; /** * Marked true for draft tables. These need to have their FK references adjusted * after all the draft tables have been identified. */ private boolean draft; /** * Primary key name. */ private String pkName; /** * Table comment. */ private String comment; /** * Tablespace to use. */ private String tablespace; /** * Tablespace to use for indexes on this table. */ private String indexTablespace; /** * If set then this overrides the platform default so for UUID generated values * or DB's supporting both sequences and autoincrement. */ private IdentityType identityType; /** * DB sequence name. */ private String sequenceName; private int sequenceInitial; private int sequenceAllocate; /** * If set to true this table should has history support. */ private boolean withHistory; /** * The columns on the table. */ private Map<String, MColumn> columns = new LinkedHashMap<>(); /** * Compound unique constraints. */ private List<MCompoundUniqueConstraint> uniqueConstraints = new ArrayList<>(); /** * Compound foreign keys. */ private List<MCompoundForeignKey> compoundKeys = new ArrayList<>(); /** * Column name for the 'When created' column. This can be used for the initial effective start date when adding * history to an existing table and maps to a @WhenCreated or @CreatedTimestamp column. */ private String whenCreatedColumn; /** * Temporary - holds addColumn settings. */ private AddColumn addColumn; private List<String> droppedColumns = new ArrayList<>(); /** * Create a copy of this table structure as a 'draft' table. * <p> * Note that both tables contain @DraftOnly MColumns and these are filtered out * later when creating the CreateTable object. */ public MTable createDraftTable() { draftTable = new MTable(name + "_draft"); draftTable.draft = true; draftTable.whenCreatedColumn = whenCreatedColumn; // compoundKeys // compoundUniqueConstraints draftTable.identityType = identityType; for (MColumn col : allColumns()) { draftTable.addColumn(col.copyForDraft()); } return draftTable; } /** * Construct for migration. */ public MTable(CreateTable createTable) { this.name = createTable.getName(); this.pkName = createTable.getPkName(); this.comment = createTable.getComment(); this.tablespace = createTable.getTablespace(); this.indexTablespace = createTable.getIndexTablespace(); this.withHistory = Boolean.TRUE.equals(createTable.isWithHistory()); this.draft = Boolean.TRUE.equals(createTable.isDraft()); this.sequenceName = createTable.getSequenceName(); this.sequenceInitial = toInt(createTable.getSequenceInitial()); this.sequenceAllocate = toInt(createTable.getSequenceAllocate()); List<Column> cols = createTable.getColumn(); for (Column column : cols) { addColumn(column); } } /** * Construct typically from EbeanServer meta data. */ public MTable(String name) { this.name = name; } /** * Return the DropTable migration for this table. */ public DropTable dropTable() { DropTable dropTable = new DropTable(); dropTable.setName(name); return dropTable; } /** * Return the CreateTable migration for this table. */ public CreateTable createTable() { CreateTable createTable = new CreateTable(); createTable.setName(name); createTable.setPkName(pkName); createTable.setComment(comment); createTable.setTablespace(tablespace); createTable.setIndexTablespace(indexTablespace); createTable.setSequenceName(sequenceName); createTable.setSequenceInitial(toBigInteger(sequenceInitial)); createTable.setSequenceAllocate(toBigInteger(sequenceAllocate)); createTable.setIdentityType(identityType); if (withHistory) { createTable.setWithHistory(Boolean.TRUE); } if (draft) { createTable.setDraft(Boolean.TRUE); } for (MColumn column : allColumns()) { // filter out draftOnly columns from the base table if (draft || !column.isDraftOnly()) { createTable.getColumn().add(column.createColumn()); } } for (MCompoundForeignKey compoundKey : compoundKeys) { createTable.getForeignKey().add(compoundKey.createForeignKey()); } for (MCompoundUniqueConstraint constraint : uniqueConstraints) { UniqueConstraint uq = new UniqueConstraint(); uq.setName(constraint.getName()); String[] columns = constraint.getColumns(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < columns.length; i++) { if (i > 0) { sb.append(","); } sb.append(columns[i]); } uq.setColumnNames(sb.toString()); createTable.getUniqueConstraint().add(uq); } return createTable; } /** * Compare to another version of the same table to perform a diff. */ public void compare(ModelDiff modelDiff, MTable newTable) { if (withHistory != newTable.withHistory) { if (withHistory) { DropHistoryTable dropHistoryTable = new DropHistoryTable(); dropHistoryTable.setBaseTable(name); modelDiff.addDropHistoryTable(dropHistoryTable); } else { AddHistoryTable addHistoryTable = new AddHistoryTable(); addHistoryTable.setBaseTable(name); modelDiff.addAddHistoryTable(addHistoryTable); } } addColumn = null; Map<String, MColumn> newColumnMap = newTable.getColumns(); // compare newColumns to existing columns (look for new and diff columns) for (MColumn newColumn : newColumnMap.values()) { MColumn localColumn = getColumn(newColumn.getName()); if (localColumn == null) { // can ignore if draftOnly column and non-draft table if (!newColumn.isDraftOnly() || draft) { diffNewColumn(newColumn); } } else { localColumn.compare(modelDiff, this, newColumn); } } // compare existing columns (look for dropped columns) for (MColumn existingColumn : allColumns()) { MColumn newColumn = newColumnMap.get(existingColumn.getName()); if (newColumn == null) { diffDropColumn(modelDiff, existingColumn); } else if (newColumn.isDraftOnly() && !draft) { // effectively a drop column (draft only column on a non-draft table) logger.trace("... drop column {} from table {} as now draftOnly", newColumn.getName(), name); diffDropColumn(modelDiff, existingColumn); } } if (addColumn != null) { modelDiff.addAddColumn(addColumn); } if (MColumn.different(comment, newTable.comment)) { AddTableComment addTableComment = new AddTableComment(); addTableComment.setName(name); addTableComment.setComment(newTable.comment); modelDiff.addTableComment(addTableComment); } } /** * Apply AddColumn migration. */ public void apply(AddColumn addColumn) { checkTableName(addColumn.getTableName()); for (Column column : addColumn.getColumn()) { addColumn(column); } } /** * Apply AddColumn migration. */ public void apply(AlterColumn alterColumn) { checkTableName(alterColumn.getTableName()); String columnName = alterColumn.getColumnName(); MColumn existingColumn = getColumn(columnName); if (existingColumn == null) { throw new IllegalStateException("Column [" + columnName + "] does not exist for AlterColumn change?"); } existingColumn.apply(alterColumn); } /** * Apply DropColumn migration. */ public void apply(DropColumn dropColumn) { checkTableName(dropColumn.getTableName()); MColumn removed = columns.remove(dropColumn.getColumnName()); if (removed == null) { throw new IllegalStateException("Column [" + dropColumn.getColumnName() + "] does not exist for DropColumn change on table [" + dropColumn.getTableName() + "]?"); } } public String getName() { return name; } /** * Return true if this table is a 'Draft' table. */ public boolean isDraft() { return draft; } public void setPkName(String pkName) { this.pkName = pkName; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public String getTablespace() { return tablespace; } public String getIndexTablespace() { return indexTablespace; } public boolean isWithHistory() { return withHistory; } public MTable setWithHistory(boolean withHistory) { this.withHistory = withHistory; return this; } public List<String> allHistoryColumns(boolean includeDropped) { List<String> columnNames = new ArrayList<>(columns.size()); for (MColumn column : columns.values()) { if (column.isIncludeInHistory()) { columnNames.add(column.getName()); } } if (includeDropped && !droppedColumns.isEmpty()) { columnNames.addAll(droppedColumns); } return columnNames; } /** * Return all the columns (excluding columns marked as dropped). */ public Collection<MColumn> allColumns() { return columns.values(); } /** * Return the column by name. */ public MColumn getColumn(String name) { return columns.get(name); } private Map<String, MColumn> getColumns() { return columns; } public List<MCompoundUniqueConstraint> getUniqueConstraints() { return uniqueConstraints; } public List<MCompoundForeignKey> getCompoundKeys() { return compoundKeys; } public void setSequenceName(String sequenceName) { this.sequenceName = sequenceName; } public void setSequenceInitial(int sequenceInitial) { this.sequenceInitial = sequenceInitial; } public void setSequenceAllocate(int sequenceAllocate) { this.sequenceAllocate = sequenceAllocate; } public void setWhenCreatedColumn(String whenCreatedColumn) { this.whenCreatedColumn = whenCreatedColumn; } public String getWhenCreatedColumn() { return whenCreatedColumn; } /** * Set the identity type to use for this table. * <p> * If set then this overrides the platform default so for UUID generated values * or DB's supporting both sequences and autoincrement. */ public void setIdentityType(IdentityType identityType) { this.identityType = identityType; } /** * Return the list of columns that make the primary key. */ public List<MColumn> primaryKeyColumns() { List<MColumn> pk = new ArrayList<>(3); for (MColumn column : allColumns()) { if (column.isPrimaryKey()) { pk.add(column); } } return pk; } private void checkTableName(String tableName) { if (!name.equals(tableName)) { throw new IllegalArgumentException("addColumn tableName [" + tableName + "] does not match [" + name + "]"); } } /** * Add a column via migration data. */ private void addColumn(Column column) { columns.put(column.getName(), new MColumn(column)); } /** * Add a model column (typically from EbeanServer meta data). */ public void addColumn(MColumn column) { columns.put(column.getName(), column); } /** * Add a unique constraint. */ public void addUniqueConstraint(String[] columns, boolean oneToOne, String constraintName) { uniqueConstraints.add(new MCompoundUniqueConstraint(columns, oneToOne, constraintName)); } /** * Add a unique constraint. */ public void addUniqueConstraint(List<MColumn> columns, boolean oneToOne, String constraintName) { String[] cols = new String[columns.size()]; for (int i = 0; i < columns.size(); i++) { cols[i] = columns.get(i).getName(); } addUniqueConstraint(cols, oneToOne, constraintName); } /** * Add a compound foreign key. */ public void addForeignKey(MCompoundForeignKey compoundKey) { compoundKeys.add(compoundKey); } /** * Add a column checking if it already exists and if so return the existing column. * Sometimes the case for a primaryKey that is also a foreign key. */ public MColumn addColumn(String dbCol, String columnDefn, boolean notnull) { MColumn existingColumn = getColumn(dbCol); if (existingColumn != null) { if (notnull) { existingColumn.setNotnull(true); } return existingColumn; } MColumn newCol = new MColumn(dbCol, columnDefn, notnull); addColumn(newCol); return newCol; } /** * Add a 'new column' to the AddColumn migration object. */ private void diffNewColumn(MColumn newColumn) { if (addColumn == null) { addColumn = new AddColumn(); addColumn.setTableName(name); if (withHistory) { // These addColumns need to occur on the history // table as well as the base table addColumn.setWithHistory(Boolean.TRUE); } } addColumn.getColumn().add(newColumn.createColumn()); } /** * Add a 'drop column' to the diff. */ private void diffDropColumn(ModelDiff modelDiff, MColumn existingColumn) { DropColumn dropColumn = new DropColumn(); dropColumn.setTableName(name); dropColumn.setColumnName(existingColumn.getName()); if (withHistory && !existingColumn.isHistoryExclude()) { // These dropColumns should occur on the history // table as well as the base table dropColumn.setWithHistory(Boolean.TRUE); } modelDiff.addDropColumn(dropColumn); } /** * Register a pending un-applied drop column. * <p> * This means this column still needs to be included in history views/triggers etc even * though it is not part of the current model. */ public void registerPendingDropColumn(String columnName) { droppedColumns.add(columnName); } private int toInt(BigInteger value) { return (value == null) ? 0 : value.intValue(); } private BigInteger toBigInteger(int value) { return (value == 0) ? null : BigInteger.valueOf(value); } /** * Check if there are duplicate foreign keys. * <p> * This can occur when an ManyToMany relates back to itself. * </p> */ public void checkDuplicateForeignKeys() { if (hasDuplicateForeignKeys()) { int counter = 1; for (MCompoundForeignKey fk : compoundKeys) { fk.addNameSuffix(counter++); } } } /** * Return true if the foreign key names are not unique. */ private boolean hasDuplicateForeignKeys() { Set<String> fkNames = new HashSet<>(); for (MCompoundForeignKey fk : compoundKeys) { if (!fkNames.add(fk.getName())) { return true; } } return false; } /** * Adjust the references (FK) if it should relate to a draft table. */ public void adjustReferences(ModelContainer modelContainer) { Collection<MColumn> cols = allColumns(); for (MColumn col : cols) { String references = col.getReferences(); if (references != null) { String baseTable = extractBaseTable(references); MTable refBaseTable = modelContainer.getTable(baseTable); if (refBaseTable.draftTable != null) { // change references to another associated 'draft' table String newReferences = deriveReferences(references, refBaseTable.draftTable.getName()); col.setReferences(newReferences); } } } } /** * Return the base table name from references (table.column). */ private String extractBaseTable(String references) { int lastDot = references.lastIndexOf('.'); return references.substring(0, lastDot); } /** * Return the new references using the given draftTableName. * (The referenced column is the same as before). */ private String deriveReferences(String references, String draftTableName) { int lastDot = references.lastIndexOf('.'); return draftTableName + "." + references.substring(lastDot + 1); } }