package io.ebeaninternal.dbmigration.model;
import io.ebeaninternal.dbmigration.ddlgeneration.platform.DdlHelp;
import io.ebeaninternal.dbmigration.ddlgeneration.platform.SplitColumns;
import io.ebeaninternal.dbmigration.migration.AddColumn;
import io.ebeaninternal.dbmigration.migration.AddHistoryTable;
import io.ebeaninternal.dbmigration.migration.AddTableComment;
import io.ebeaninternal.dbmigration.migration.AlterColumn;
import io.ebeaninternal.dbmigration.migration.Column;
import io.ebeaninternal.dbmigration.migration.CreateTable;
import io.ebeaninternal.dbmigration.migration.DropColumn;
import io.ebeaninternal.dbmigration.migration.DropHistoryTable;
import io.ebeaninternal.dbmigration.migration.DropTable;
import io.ebeaninternal.dbmigration.migration.ForeignKey;
import io.ebeaninternal.dbmigration.migration.IdentityType;
import io.ebeaninternal.dbmigration.migration.UniqueConstraint;
import io.ebeaninternal.server.deploy.PartitionMeta;
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.
This effectively represents a table, its columns and all associated
constraints, foreign keys and indexes.
Migrations can be applied to this such that it represents the state
of a given table after various migrations have been applied.
This table model can also be derived from the EbeanServer bean descriptor
and associated properties.
/**
* 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.
/**
* Table name.
*/
private final String name;
The associated draft table.
/**
* 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.
/**
* Marked true for draft tables. These need to have their FK references adjusted
* after all the draft tables have been identified.
*/
private boolean draft;
private PartitionMeta partitionMeta;
Primary key name.
/**
* Primary key name.
*/
private String pkName;
Table comment.
/**
* Table comment.
*/
private String comment;
Tablespace to use.
/**
* Tablespace to use.
*/
private String tablespace;
private String storageEngine;
Tablespace to use for indexes on this table.
/**
* 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.
/**
* 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.
/**
* DB sequence name.
*/
private String sequenceName;
private int sequenceInitial;
private int sequenceAllocate;
If set to true this table should has history support.
/**
* If set to true this table should has history support.
*/
private boolean withHistory;
The columns on the table.
/**
* The columns on the table.
*/
private Map<String, MColumn> columns = new LinkedHashMap<>();
Compound unique constraints.
/**
* Compound unique constraints.
*/
private List<MCompoundUniqueConstraint> uniqueConstraints = new ArrayList<>();
Compound foreign keys.
/**
* 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.
/**
* 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.
/**
* Temporary - holds addColumn settings.
*/
private AddColumn addColumn;
private List<String> droppedColumns = new ArrayList<>();
Create a copy of this table structure as a 'draft' table.
Note that both tables contain @DraftOnly MColumns and these are filtered out
later when creating the CreateTable object.
/**
* 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.
/**
* Construct for migration.
*/
public MTable(CreateTable createTable) {
this.name = createTable.getName();
this.pkName = createTable.getPkName();
this.comment = createTable.getComment();
this.storageEngine = createTable.getStorageEngine();
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);
}
List<UniqueConstraint> uqConstraints = createTable.getUniqueConstraint();
for (UniqueConstraint uq : uqConstraints) {
MCompoundUniqueConstraint mUq = new MCompoundUniqueConstraint(SplitColumns.split(uq.getColumnNames()), uq.isOneToOne(), uq.getName());
mUq.setNullableColumns(SplitColumns.split(uq.getNullableColumns()));
uniqueConstraints.add(mUq);
}
for (ForeignKey fk : createTable.getForeignKey()) {
if (DdlHelp.isDropForeignKey(fk.getColumnNames())) {
removeForeignKey(fk.getName());
} else {
addForeignKey(fk.getName(), fk.getRefTableName(), fk.getIndexName(), fk.getColumnNames(), fk.getRefColumnNames());
}
}
}
public void addForeignKey(String name, String refTableName, String indexName, String columnNames, String refColumnNames) {
MCompoundForeignKey foreignKey = new MCompoundForeignKey(name, refTableName, indexName);
String[] cols = SplitColumns.split(columnNames);
String[] refCols = SplitColumns.split(refColumnNames);
for (int i = 0; i < cols.length && i < refCols.length; i++) {
foreignKey.addColumnPair(cols[i], refCols[i]);
}
addForeignKey(foreignKey);
}
Construct typically from EbeanServer meta data.
/**
* Construct typically from EbeanServer meta data.
*/
public MTable(String name) {
this.name = name;
}
Return the DropTable migration for this table.
/**
* Return the DropTable migration for this table.
*/
public DropTable dropTable() {
DropTable dropTable = new DropTable();
dropTable.setName(name);
// we must add pk col name & sequence name, as we have to delete the sequence also.
if (identityType != IdentityType.GENERATOR && identityType != IdentityType.EXTERNAL) {
String pkCol = null;
for (MColumn column : columns.values()) {
if (column.isPrimaryKey()) {
if (pkCol == null) {
pkCol = column.getName();
} else { // multiple pk cols -> no sequence
pkCol = null;
break;
}
}
}
if (pkCol != null) {
dropTable.setSequenceCol(pkCol);
dropTable.setSequenceName(sequenceName);
}
}
return dropTable;
}
Return the CreateTable migration for this table.
/**
* Return the CreateTable migration for this table.
*/
public CreateTable createTable() {
CreateTable createTable = new CreateTable();
createTable.setName(name);
createTable.setPkName(pkName);
createTable.setComment(comment);
if (partitionMeta != null) {
createTable.setPartitionMode(partitionMeta.getMode().name());
createTable.setPartitionColumn(partitionMeta.getProperty());
}
createTable.setStorageEngine(storageEngine);
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 = constraint.getUniqueConstraint();
createTable.getUniqueConstraint().add(uq);
}
return createTable;
}
Compare to another version of the same table to perform a diff.
/**
* 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);
}
}
compareColumns(modelDiff, newTable);
if (MColumn.different(comment, newTable.comment)) {
AddTableComment addTableComment = new AddTableComment();
addTableComment.setName(name);
if (newTable.comment == null) {
addTableComment.setComment(DdlHelp.DROP_COMMENT);
} else {
addTableComment.setComment(newTable.comment);
}
modelDiff.addTableComment(addTableComment);
}
compareCompoundKeys(modelDiff, newTable);
compareUniqueKeys(modelDiff, newTable);
}
private void compareColumns(ModelDiff modelDiff, MTable newTable) {
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);
}
}
private void compareCompoundKeys(ModelDiff modelDiff, MTable newTable) {
List<MCompoundForeignKey> newKeys = new ArrayList<>(newTable.getCompoundKeys());
List<MCompoundForeignKey> currentKeys = new ArrayList<>(getCompoundKeys());
// remove keys that have not changed
currentKeys.removeAll(newTable.getCompoundKeys());
newKeys.removeAll(getCompoundKeys());
for (MCompoundForeignKey currentKey : currentKeys) {
modelDiff.addAlterForeignKey(currentKey.dropForeignKey(name));
}
for (MCompoundForeignKey newKey : newKeys) {
modelDiff.addAlterForeignKey(newKey.addForeignKey(name));
}
}
private void compareUniqueKeys(ModelDiff modelDiff, MTable newTable) {
List<MCompoundUniqueConstraint> newKeys = new ArrayList<>(newTable.getUniqueConstraints());
List<MCompoundUniqueConstraint> currentKeys = new ArrayList<>(getUniqueConstraints());
// remove keys that have not changed
currentKeys.removeAll(newTable.getUniqueConstraints());
newKeys.removeAll(getUniqueConstraints());
for (MCompoundUniqueConstraint currentKey: currentKeys) {
modelDiff.addUniqueConstraint(currentKey.dropUniqueConstraint(name));
}
for (MCompoundUniqueConstraint newKey: newKeys) {
modelDiff.addUniqueConstraint(newKey.addUniqueConstraint(name));
}
}
Apply AddColumn migration.
/**
* Apply AddColumn migration.
*/
public void apply(AddColumn addColumn) {
checkTableName(addColumn.getTableName());
for (Column column : addColumn.getColumn()) {
addColumn(column);
}
}
Apply AddColumn migration.
/**
* 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.
/**
* 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.
/**
* Return true if this table is a 'Draft' table.
*/
public boolean isDraft() {
return draft;
}
Return true if this table is partitioned.
/**
* Return true if this table is partitioned.
*/
public boolean isPartitioned() {
return partitionMeta != null;
}
Return the partition meta for this table.
/**
* Return the partition meta for this table.
*/
public PartitionMeta getPartitionMeta() {
return partitionMeta;
}
public void setPkName(String pkName) {
this.pkName = pkName;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public void setStorageEngine(String storageEngine) {
this.storageEngine = storageEngine;
}
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).
/**
* Return all the columns (excluding columns marked as dropped).
*/
public Collection<MColumn> allColumns() {
return columns.values();
}
Return the column by name.
/**
* 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.
If set then this overrides the platform default so for UUID generated values
or DB's supporting both sequences and autoincrement.
/**
* 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.
/**
* 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;
}
Return the primary key column if it is a simple primary key.
/**
* Return the primary key column if it is a simple primary key.
*/
public String singlePrimaryKey() {
List<MColumn> columns = primaryKeyColumns();
if (columns.size() == 1) {
return columns.get(0).getName();
}
return null;
}
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.
/**
* 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).
/**
* Add a model column (typically from EbeanServer meta data).
*/
public void addColumn(MColumn column) {
columns.put(column.getName(), column);
}
Add a unique constraint.
/**
* Add a unique constraint.
*/
public void addUniqueConstraint(String[] columns, boolean oneToOne, String constraintName) {
uniqueConstraints.add(new MCompoundUniqueConstraint(columns, oneToOne, constraintName));
}
Add a unique constraint.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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) {
// 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.
This means this column still needs to be included in history views/triggers etc even
though it is not part of the current model.
/**
* 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.
This can occur when an ManyToMany relates back to itself.
/**
* 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.
/**
* 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.
/**
* 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).
/**
* 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).
/**
* 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);
}
This method adds information which columns are nullable or not to the compound indices.
/**
* This method adds information which columns are nullable or not to the compound indices.
*/
public void updateCompoundIndices() {
for (MCompoundUniqueConstraint uniq : uniqueConstraints) {
List<String> nullableColumns = new ArrayList<>();
for (String columnName : uniq.getColumns()) {
MColumn col = getColumn(columnName);
if (col == null) {
throw new IllegalStateException("Column '" + columnName + "' not found in table " + getName());
}
if (!col.isNotnull()) {
nullableColumns.add(columnName);
}
}
uniq.setNullableColumns(nullableColumns.toArray(new String[nullableColumns.size()]));
}
}
public void removeForeignKey(String name) {
compoundKeys.removeIf(fk -> fk.getName().equals(name));
}
Clear the indexes on the foreign keys as they are covered by unique constraints.
/**
* Clear the indexes on the foreign keys as they are covered by unique constraints.
*/
public void clearForeignKeyIndexes() {
for (MCompoundForeignKey compoundKey : compoundKeys) {
compoundKey.setIndexName(null);
}
}
public void setPartitionMeta(PartitionMeta partitionMeta) {
this.partitionMeta = partitionMeta;
}
}