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}