001package io.ebeaninternal.dbmigration; 002 003import io.ebean.DB; 004import io.ebean.Database; 005import io.ebean.EbeanServer; 006import io.ebean.annotation.Platform; 007import io.ebean.config.DatabaseConfig; 008import io.ebean.config.DbConstraintNaming; 009import io.ebean.config.PlatformConfig; 010import io.ebean.config.PropertiesWrapper; 011import io.ebean.config.dbplatform.DatabasePlatform; 012import io.ebean.config.dbplatform.clickhouse.ClickHousePlatform; 013import io.ebean.config.dbplatform.cockroach.CockroachPlatform; 014import io.ebean.config.dbplatform.db2.DB2Platform; 015import io.ebean.config.dbplatform.h2.H2Platform; 016import io.ebean.config.dbplatform.hana.HanaPlatform; 017import io.ebean.config.dbplatform.hsqldb.HsqldbPlatform; 018import io.ebean.config.dbplatform.mariadb.MariaDbPlatform; 019import io.ebean.config.dbplatform.mysql.MySql55Platform; 020import io.ebean.config.dbplatform.mysql.MySqlPlatform; 021import io.ebean.config.dbplatform.nuodb.NuoDbPlatform; 022import io.ebean.config.dbplatform.oracle.Oracle11Platform; 023import io.ebean.config.dbplatform.oracle.OraclePlatform; 024import io.ebean.config.dbplatform.postgres.Postgres9Platform; 025import io.ebean.config.dbplatform.postgres.PostgresPlatform; 026import io.ebean.config.dbplatform.sqlanywhere.SqlAnywherePlatform; 027import io.ebean.config.dbplatform.sqlite.SQLitePlatform; 028import io.ebean.config.dbplatform.sqlserver.SqlServer16Platform; 029import io.ebean.config.dbplatform.sqlserver.SqlServer17Platform; 030import io.ebean.dbmigration.DbMigration; 031import io.ebeaninternal.api.DbOffline; 032import io.ebeaninternal.api.SpiEbeanServer; 033import io.ebeaninternal.dbmigration.ddlgeneration.DdlOptions; 034import io.ebeaninternal.dbmigration.ddlgeneration.DdlWrite; 035import io.ebeaninternal.dbmigration.migration.Migration; 036import io.ebeaninternal.dbmigration.migrationreader.MigrationXmlWriter; 037import io.ebeaninternal.dbmigration.model.CurrentModel; 038import io.ebeaninternal.dbmigration.model.MConfiguration; 039import io.ebeaninternal.dbmigration.model.MigrationModel; 040import io.ebeaninternal.dbmigration.model.ModelContainer; 041import io.ebeaninternal.dbmigration.model.ModelDiff; 042import io.ebeaninternal.dbmigration.model.PlatformDdlWriter; 043import io.ebeaninternal.extraddl.model.DdlScript; 044import io.ebeaninternal.extraddl.model.ExtraDdl; 045import io.ebeaninternal.extraddl.model.ExtraDdlXmlReader; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048 049import java.io.File; 050import java.io.FileWriter; 051import java.io.IOException; 052import java.util.ArrayList; 053import java.util.List; 054import java.util.Properties; 055 056import static io.ebeaninternal.api.PlatformMatch.matchPlatform; 057 058/** 059 * Generates DB Migration xml and sql scripts. 060 * <p> 061 * Reads the prior migrations and compares with the current model of the EbeanServer 062 * and generates a migration 'diff' in the form of xml document with the logical schema 063 * changes and a series of sql scripts to apply, rollback the applied changes if necessary 064 * and drop objects (drop tables, drop columns). 065 * </p> 066 * <p> 067 * This does not run the migration or ddl scripts but just generates them. 068 * </p> 069 * <pre>{@code 070 * 071 * DbMigration migration = DbMigration.create(); 072 * migration.setPathToResources("src/main/resources"); 073 * migration.setPlatform(Platform.POSTGRES); 074 * 075 * migration.generateMigration(); 076 * 077 * }</pre> 078 */ 079public class DefaultDbMigration implements DbMigration { 080 081 protected static final Logger logger = LoggerFactory.getLogger("io.ebean.GenerateMigration"); 082 083 private static final String initialVersion = "1.0"; 084 085 private static final String GENERATED_COMMENT = "THIS IS A GENERATED FILE - DO NOT MODIFY"; 086 087 private boolean logToSystemOut = true; 088 089 /** 090 * Set to true if DefaultDbMigration run with online EbeanServer instance. 091 */ 092 protected final boolean online; 093 094 protected SpiEbeanServer server; 095 096 protected String pathToResources = "src/main/resources"; 097 098 protected String migrationPath = "dbmigration"; 099 protected String migrationInitPath = "dbinit"; 100 protected String modelPath = "model"; 101 protected String modelSuffix = ".model.xml"; 102 103 protected DatabasePlatform databasePlatform; 104 105 private boolean vanillaPlatform; 106 107 protected List<Pair> platforms = new ArrayList<>(); 108 109 protected DatabaseConfig databaseConfig; 110 111 protected DbConstraintNaming constraintNaming; 112 113 protected Boolean strictMode; 114 protected Boolean includeGeneratedFileComment; 115 protected String header; 116 protected String applyPrefix = ""; 117 protected String version; 118 protected String name; 119 protected String generatePendingDrop; 120 private boolean addForeignKeySkipCheck; 121 private int lockTimeoutSeconds; 122 123 protected boolean includeBuiltInPartitioning = true; 124 125 /** 126 * Create for offline migration generation. 127 */ 128 public DefaultDbMigration() { 129 this.online = false; 130 } 131 132 /** 133 * Create using online EbeanServer. 134 */ 135 public DefaultDbMigration(EbeanServer server) { 136 this.online = true; 137 setServer(server); 138 } 139 140 /** 141 * Set the path from the current working directory to the application resources. 142 * <p> 143 * This defaults to maven style 'src/main/resources'. 144 */ 145 @Override 146 public void setPathToResources(String pathToResources) { 147 this.pathToResources = pathToResources; 148 } 149 150 @Override 151 public void setMigrationPath(String migrationPath) { 152 this.migrationPath = migrationPath; 153 } 154 155 /** 156 * Set the server to use to determine the current model. 157 * Typically this is not called explicitly. 158 */ 159 @Override 160 public void setServer(Database database) { 161 this.server = (SpiEbeanServer) database; 162 setServerConfig(server.getServerConfig()); 163 } 164 165 /** 166 * Set the DatabaseConfig to use. Typically this is not called explicitly. 167 */ 168 @Override 169 public void setServerConfig(DatabaseConfig config) { 170 if (this.databaseConfig == null) { 171 this.databaseConfig = config; 172 } 173 if (constraintNaming == null) { 174 this.constraintNaming = databaseConfig.getConstraintNaming(); 175 } 176 177 Properties properties = config.getProperties(); 178 if (properties != null) { 179 PropertiesWrapper props = new PropertiesWrapper("ebean", config.getName(), properties, null); 180 migrationPath = props.get("migration.migrationPath", migrationPath); 181 migrationInitPath = props.get("migration.migrationInitPath", migrationInitPath); 182 pathToResources = props.get("migration.pathToResources", pathToResources); 183 } 184 } 185 186 @Override 187 public void setStrictMode(boolean strictMode) { 188 this.strictMode = strictMode; 189 } 190 191 @Override 192 public void setApplyPrefix(String applyPrefix) { 193 this.applyPrefix = applyPrefix; 194 } 195 196 @Override 197 public void setVersion(String version) { 198 this.version = version; 199 } 200 201 @Override 202 public void setName(String name) { 203 this.name = name; 204 } 205 206 @Override 207 public void setAddForeignKeySkipCheck(boolean addForeignKeySkipCheck) { 208 this.addForeignKeySkipCheck = addForeignKeySkipCheck; 209 } 210 211 @Override 212 public void setLockTimeout(int seconds) { 213 this.lockTimeoutSeconds = seconds; 214 } 215 216 @Override 217 public void setGeneratePendingDrop(String generatePendingDrop) { 218 this.generatePendingDrop = generatePendingDrop; 219 } 220 221 @Override 222 public void setIncludeGeneratedFileComment(boolean includeGeneratedFileComment) { 223 this.includeGeneratedFileComment = includeGeneratedFileComment; 224 } 225 226 @Override 227 public void setIncludeBuiltInPartitioning(boolean includeBuiltInPartitioning) { 228 this.includeBuiltInPartitioning = includeBuiltInPartitioning; 229 } 230 231 @Override 232 public void setHeader(String header) { 233 this.header = header; 234 } 235 236 /** 237 * Set the specific platform to generate DDL for. 238 * <p> 239 * If not set this defaults to the platform of the default server. 240 * </p> 241 */ 242 @Override 243 public void setPlatform(Platform platform) { 244 vanillaPlatform = true; 245 setPlatform(getPlatform(platform)); 246 } 247 248 /** 249 * Set the specific platform to generate DDL for. 250 * <p> 251 * If not set this defaults to the platform of the default server. 252 * </p> 253 */ 254 @Override 255 public void setPlatform(DatabasePlatform databasePlatform) { 256 this.databasePlatform = databasePlatform; 257 if (!online) { 258 DbOffline.setPlatform(databasePlatform.getPlatform()); 259 } 260 } 261 262 /** 263 * Add an additional platform to write the migration DDL. 264 * <p> 265 * Use this when you want to generate sql scripts for multiple database platforms 266 * from the migration (e.g. generate migration sql for MySql, Postgres and Oracle). 267 * </p> 268 */ 269 @Override 270 public void addPlatform(Platform platform, String prefix) { 271 platforms.add(new Pair(getPlatform(platform), prefix)); 272 } 273 274 @Override 275 public void addDatabasePlatform(DatabasePlatform databasePlatform, String prefix) { 276 platforms.add(new Pair(databasePlatform, prefix)); 277 } 278 279 /** 280 * Generate the next migration xml file and associated apply and rollback sql scripts. 281 * <p> 282 * This does not run the migration or ddl scripts but just generates them. 283 * </p> 284 * <h3>Example: Run for a single specific platform</h3> 285 * <pre>{@code 286 * 287 * DbMigration migration = DbMigration.create(); 288 * migration.setPathToResources("src/main/resources"); 289 * migration.setPlatform(DbPlatformName.ORACLE); 290 * 291 * migration.generateMigration(); 292 * 293 * }</pre> 294 * <p> 295 * <h3>Example: Run migration generating DDL for multiple platforms</h3> 296 * <pre>{@code 297 * 298 * DbMigration migration = DbMigration.create(); 299 * migration.setPathToResources("src/main/resources"); 300 * 301 * migration.addPlatform(DbPlatformName.POSTGRES, "pg"); 302 * migration.addPlatform(DbPlatformName.MYSQL, "mysql"); 303 * migration.addPlatform(DbPlatformName.ORACLE, "mysql"); 304 * 305 * migration.generateMigration(); 306 * 307 * }</pre> 308 * 309 * @return the generated migration or null 310 */ 311 @Override 312 public String generateMigration() throws IOException { 313 return generateMigrationFor(false); 314 } 315 316 @Override 317 public String generateInitMigration() throws IOException { 318 return generateMigrationFor(true); 319 } 320 321 private String generateMigrationFor(boolean dbinitMigration) throws IOException { 322 323 // use this flag to stop other plugins like full DDL generation 324 if (!online) { 325 DbOffline.setGenerateMigration(); 326 if (databasePlatform == null && !platforms.isEmpty()) { 327 // for multiple platform generation the first platform 328 // is used to generate the "logical" model diff 329 setPlatform(platforms.get(0).platform); 330 } 331 } 332 setDefaults(); 333 if (!platforms.isEmpty()) { 334 configurePlatforms(); 335 } 336 try { 337 Request request = createRequest(dbinitMigration); 338 if (!dbinitMigration) { 339 // repeatable migrations 340 if (platforms.isEmpty()) { 341 generateExtraDdl(request.migrationDir, databasePlatform, request.isTablePartitioning()); 342 } else { 343 for (Pair pair : platforms) { 344 PlatformDdlWriter platformWriter = createDdlWriter(pair.platform); 345 File subPath = platformWriter.subPath(request.migrationDir, pair.prefix); 346 generateExtraDdl(subPath, pair.platform, request.isTablePartitioning()); 347 } 348 } 349 } 350 351 String pendingVersion = generatePendingDrop(); 352 if (pendingVersion != null) { 353 return generatePendingDrop(request, pendingVersion); 354 } else { 355 return generateDiff(request); 356 } 357 358 } catch (UnknownResourcePathException e) { 359 logError("ERROR - " + e.getMessage()); 360 logError("Check the working directory or change dbMigration.setPathToResources() value?"); 361 return null; 362 363 } finally { 364 if (!online) { 365 DbOffline.reset(); 366 } 367 } 368 } 369 370 /** 371 * Return the versions containing pending drops. 372 */ 373 @Override 374 public List<String> getPendingDrops() { 375 if (!online) { 376 DbOffline.setGenerateMigration(); 377 } 378 setDefaults(); 379 try { 380 return createRequest(false).getPendingDrops(); 381 } finally { 382 if (!online) { 383 DbOffline.reset(); 384 } 385 } 386 } 387 388 /** 389 * Load the configuration for each of the target platforms. 390 */ 391 private void configurePlatforms() { 392 for (Pair pair : platforms) { 393 PlatformConfig config = databaseConfig.newPlatformConfig("dbmigration.platform", pair.prefix); 394 pair.platform.configure(config); 395 } 396 } 397 398 /** 399 * Generate "repeatable" migration scripts. 400 * <p> 401 * These take scrips from extra-ddl.xml (typically views) and outputs "repeatable" 402 * migration scripts (starting with "R__") to be run by FlywayDb or Ebean's own 403 * migration runner. 404 * </p> 405 */ 406 private void generateExtraDdl(File migrationDir, DatabasePlatform dbPlatform, boolean tablePartitioning) throws IOException { 407 408 if (dbPlatform != null) { 409 if (tablePartitioning && includeBuiltInPartitioning) { 410 generateExtraDdlFor(migrationDir, dbPlatform, ExtraDdlXmlReader.readBuiltinTablePartitioning()); 411 } 412 generateExtraDdlFor(migrationDir, dbPlatform, ExtraDdlXmlReader.readBuiltin()); 413 generateExtraDdlFor(migrationDir, dbPlatform, ExtraDdlXmlReader.read()); 414 } 415 } 416 417 private void generateExtraDdlFor(File migrationDir, DatabasePlatform dbPlatform, ExtraDdl extraDdl) throws IOException { 418 if (extraDdl != null) { 419 List<DdlScript> ddlScript = extraDdl.getDdlScript(); 420 for (DdlScript script : ddlScript) { 421 if (!script.isDrop() && matchPlatform(dbPlatform.getPlatform(), script.getPlatforms())) { 422 writeExtraDdl(migrationDir, script); 423 } 424 } 425 } 426 } 427 428 /** 429 * Write (or override) the "repeatable" migration script. 430 */ 431 private void writeExtraDdl(File migrationDir, DdlScript script) throws IOException { 432 433 String fullName = repeatableMigrationName(script.isInit(), script.getName()); 434 logger.debug("writing repeatable script {}", fullName); 435 436 File file = new File(migrationDir, fullName); 437 try (FileWriter writer = new FileWriter(file)) { 438 writer.write(script.getValue()); 439 writer.flush(); 440 } 441 } 442 443 @Override 444 public void setLogToSystemOut(boolean logToSystemOut) { 445 this.logToSystemOut = logToSystemOut; 446 } 447 448 private void logError(String message) { 449 if (logToSystemOut) { 450 System.out.println("DbMigration> " + message); 451 } else { 452 logger.error(message); 453 } 454 } 455 456 private void logInfo(String message, Object value) { 457 if (value != null) { 458 message = String.format(message, value); 459 } 460 if (logToSystemOut) { 461 System.out.println("DbMigration> " + message); 462 } else { 463 logger.info(message); 464 } 465 } 466 467 private String repeatableMigrationName(boolean init, String scriptName) { 468 StringBuilder sb = new StringBuilder(); 469 if (init) { 470 sb.append("I__"); 471 } else { 472 sb.append("R__"); 473 } 474 sb.append(scriptName.replace(' ', '_')); 475 sb.append(".sql"); 476 return sb.toString(); 477 } 478 479 /** 480 * Generate the diff migration. 481 */ 482 private String generateDiff(Request request) throws IOException { 483 484 List<String> pendingDrops = request.getPendingDrops(); 485 if (!pendingDrops.isEmpty()) { 486 logInfo("Pending un-applied drops in versions %s", pendingDrops); 487 } 488 489 Migration migration = request.createDiffMigration(); 490 if (migration == null) { 491 logInfo("no changes detected - no migration written", null); 492 return null; 493 } else { 494 // there were actually changes to write 495 return generateMigration(request, migration, null); 496 } 497 } 498 499 /** 500 * Generate the migration based on the pendingDrops from a prior version. 501 */ 502 private String generatePendingDrop(Request request, String pendingVersion) throws IOException { 503 504 Migration migration = request.migrationForPendingDrop(pendingVersion); 505 506 String version = generateMigration(request, migration, pendingVersion); 507 508 List<String> pendingDrops = request.getPendingDrops(); 509 if (!pendingDrops.isEmpty()) { 510 logInfo("... remaining pending un-applied drops in versions %s", pendingDrops); 511 } 512 return version; 513 } 514 515 private Request createRequest(boolean dbinitMigration) { 516 return new Request(dbinitMigration); 517 } 518 519 private class Request { 520 521 final boolean dbinitMigration; 522 final File migrationDir; 523 final File modelDir; 524 final CurrentModel currentModel; 525 final ModelContainer migrated; 526 final ModelContainer current; 527 528 private Request(boolean dbinitMigration) { 529 this.dbinitMigration = dbinitMigration; 530 this.currentModel = new CurrentModel(server, constraintNaming); 531 this.current = currentModel.read(); 532 this.migrationDir = getMigrationDirectory(dbinitMigration); 533 if (dbinitMigration) { 534 this.modelDir = null; 535 this.migrated = new ModelContainer(); 536 } else { 537 this.modelDir = getModelDirectory(migrationDir); 538 MigrationModel migrationModel = new MigrationModel(modelDir, modelSuffix); 539 this.migrated = migrationModel.read(dbinitMigration); 540 } 541 } 542 543 boolean isTablePartitioning() { 544 return current.isTablePartitioning(); 545 } 546 547 /** 548 * Return the next migration version (based on existing migration versions). 549 */ 550 String nextVersion() { 551 // always read the next version using the main migration directory (not dbinit) 552 File migDirectory = getMigrationDirectory(false); 553 File modelDir = getModelDirectory(migDirectory); 554 return LastMigration.nextVersion(migDirectory, modelDir, dbinitMigration); 555 } 556 557 /** 558 * Return the migration for the pending drops for a given version. 559 */ 560 Migration migrationForPendingDrop(String pendingVersion) { 561 562 Migration migration = migrated.migrationForPendingDrop(pendingVersion); 563 564 // register any remaining pending drops 565 migrated.registerPendingHistoryDropColumns(current); 566 return migration; 567 } 568 569 /** 570 * Return the list of versions that have pending un-applied drops. 571 */ 572 public List<String> getPendingDrops() { 573 return migrated.getPendingDrops(); 574 } 575 576 /** 577 * Create and return the diff of the current model to the migration model. 578 */ 579 Migration createDiffMigration() { 580 ModelDiff diff = new ModelDiff(migrated); 581 diff.compareTo(current); 582 return diff.isEmpty() ? null : diff.getMigration(); 583 } 584 } 585 586 private String generateMigration(Request request, Migration dbMigration, String dropsFor) throws IOException { 587 588 String fullVersion = getFullVersion(request.nextVersion(), dropsFor); 589 590 logInfo("generating migration:%s", fullVersion); 591 if (!request.dbinitMigration && !writeMigrationXml(dbMigration, request.modelDir, fullVersion)) { 592 logError("migration already exists, not generating DDL"); 593 return null; 594 } else { 595 if (!platforms.isEmpty()) { 596 writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir); 597 598 } else if (databasePlatform != null) { 599 // writer needs the current model to provide table/column details for 600 // history ddl generation (triggers, history tables etc) 601 DdlOptions options = new DdlOptions(addForeignKeySkipCheck); 602 DdlWrite write = new DdlWrite(new MConfiguration(), request.current, options); 603 PlatformDdlWriter writer = createDdlWriter(databasePlatform); 604 writer.processMigration(dbMigration, write, request.migrationDir, fullVersion); 605 } 606 return fullVersion; 607 } 608 } 609 610 /** 611 * Return true if the next pending drop changeSet should be generated as the next migration. 612 */ 613 private String generatePendingDrop() { 614 String nextDrop = System.getProperty("ddl.migration.pendingDropsFor"); 615 if (nextDrop != null) { 616 return nextDrop; 617 } 618 return generatePendingDrop; 619 } 620 621 /** 622 * Return the full version for the migration being generated. 623 * <p> 624 * The full version can contain a comment suffix after a "__" double underscore. 625 */ 626 private String getFullVersion(String nextVersion, String dropsFor) { 627 628 String version = getVersion(); 629 if (version == null) { 630 version = (nextVersion != null) ? nextVersion : initialVersion; 631 } 632 633 String fullVersion = applyPrefix + version; 634 String name = getName(); 635 if (name != null) { 636 fullVersion += "__" + toUnderScore(name); 637 638 } else if (dropsFor != null) { 639 fullVersion += "__" + toUnderScore("dropsFor_" + trimDropsFor(dropsFor)); 640 641 } else if (version.equals(initialVersion)) { 642 fullVersion += "__initial"; 643 } 644 return fullVersion; 645 } 646 647 String trimDropsFor(String dropsFor) { 648 if (dropsFor.startsWith("V") || dropsFor.startsWith("v")) { 649 dropsFor = dropsFor.substring(1); 650 } 651 int commentStart = dropsFor.indexOf("__"); 652 if (commentStart > -1) { 653 // trim off the trailing comment 654 dropsFor = dropsFor.substring(0, commentStart); 655 } 656 return dropsFor; 657 } 658 659 /** 660 * Replace spaces with underscores. 661 */ 662 private String toUnderScore(String name) { 663 return name.replace(' ', '_'); 664 } 665 666 /** 667 * Write any extra platform ddl. 668 */ 669 private void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException { 670 DdlOptions options = new DdlOptions(addForeignKeySkipCheck); 671 for (Pair pair : platforms) { 672 DdlWrite platformBuffer = new DdlWrite(new MConfiguration(), currentModel.read(), options); 673 PlatformDdlWriter platformWriter = createDdlWriter(pair.platform); 674 File subPath = platformWriter.subPath(writePath, pair.prefix); 675 platformWriter.processMigration(dbMigration, platformBuffer, subPath, fullVersion); 676 } 677 } 678 679 private PlatformDdlWriter createDdlWriter(DatabasePlatform platform) { 680 return new PlatformDdlWriter(platform, databaseConfig, lockTimeoutSeconds); 681 } 682 683 /** 684 * Write the migration xml. 685 */ 686 private boolean writeMigrationXml(Migration dbMigration, File resourcePath, String fullVersion) { 687 String modelFile = fullVersion + modelSuffix; 688 File file = new File(resourcePath, modelFile); 689 if (file.exists()) { 690 return false; 691 } 692 String comment = Boolean.TRUE.equals(includeGeneratedFileComment) ? GENERATED_COMMENT : null; 693 MigrationXmlWriter xmlWriter = new MigrationXmlWriter(comment); 694 xmlWriter.write(dbMigration, file); 695 return true; 696 } 697 698 /** 699 * Set default server and platform if necessary. 700 */ 701 private void setDefaults() { 702 if (server == null) { 703 setServer(DB.getDefault()); 704 } 705 if (vanillaPlatform || databasePlatform == null) { 706 // not explicitly set so use the platform of the server 707 databasePlatform = server.getDatabasePlatform(); 708 } 709 if (databaseConfig != null) { 710 if (strictMode != null) { 711 databaseConfig.setDdlStrictMode(strictMode); 712 } 713 if (header != null) { 714 databaseConfig.setDdlHeader(header); 715 } 716 } 717 } 718 719 /** 720 * Return the migration version (typically FlywayDb compatible). 721 * <p> 722 * Example: 1.1.1_2 723 * <p> 724 * The version is expected to be the combination of the current pom version plus 725 * a 'feature' id. The combined version must be unique and ordered to work with 726 * FlywayDb so each developer sets a unique version so that the migration script 727 * generated is unique (typically just prior to being submitted as a merge request). 728 */ 729 private String getVersion() { 730 String envVersion = readEnvironment("ddl.migration.version"); 731 if (!isEmpty(envVersion)) { 732 return envVersion.trim(); 733 } 734 return version; 735 } 736 737 /** 738 * Return the migration name which is short description text that can be appended to 739 * the migration version to become the ddl script file name. 740 * <p> 741 * So if the name is "a foo table" then the ddl script file could be: 742 * "1.1.1_2__a-foo-table.sql" 743 * </p> 744 * <p> 745 * When the DB migration relates to a git feature (merge request) then this description text 746 * is a short description of the feature. 747 * </p> 748 */ 749 private String getName() { 750 String envName = readEnvironment("ddl.migration.name"); 751 if (!isEmpty(envName)) { 752 return envName.trim(); 753 } 754 return name; 755 } 756 757 /** 758 * Return true if the string is null or empty. 759 */ 760 private boolean isEmpty(String val) { 761 return val == null || val.trim().isEmpty(); 762 } 763 764 /** 765 * Return the system or environment property. 766 */ 767 private String readEnvironment(String key) { 768 String val = System.getProperty(key); 769 if (val == null) { 770 val = System.getenv(key); 771 } 772 return val; 773 } 774 775 /** 776 * Return the main migration directory. 777 */ 778 File getMigrationDirectory() { 779 return getMigrationDirectory(false); 780 } 781 782 /** 783 * Return the file path to write the xml and sql to. 784 */ 785 File getMigrationDirectory(boolean dbinitMigration) { 786 787 // path to src/main/resources in typical maven project 788 File resourceRootDir = new File(pathToResources); 789 if (!resourceRootDir.exists()) { 790 String msg = String.format("Error - path to resources %s does not exist. Absolute path is %s", pathToResources, resourceRootDir.getAbsolutePath()); 791 throw new UnknownResourcePathException(msg); 792 } 793 String resourcePath = getMigrationPath(dbinitMigration); 794 795 // expect to be a path to something like - src/main/resources/dbmigration/model 796 File path = new File(resourceRootDir, resourcePath); 797 if (!path.exists()) { 798 if (!path.mkdirs()) { 799 logInfo("Warning - Unable to ensure migration directory exists at %s", path.getAbsolutePath()); 800 } 801 } 802 return path; 803 } 804 805 private String getMigrationPath(boolean dbinitMigration) { 806 return dbinitMigration ? migrationInitPath : migrationPath; 807 } 808 809 /** 810 * Return the model directory (relative to the migration directory). 811 */ 812 private File getModelDirectory(File migrationDirectory) { 813 if (modelPath == null || modelPath.isEmpty()) { 814 return migrationDirectory; 815 } 816 File modelDir = new File(migrationDirectory, modelPath); 817 if (!modelDir.exists() && !modelDir.mkdirs()) { 818 logInfo("Warning - Unable to ensure migration model directory exists at %s", modelDir.getAbsolutePath()); 819 } 820 return modelDir; 821 } 822 823 /** 824 * Return the DatabasePlatform given the platform key. 825 */ 826 protected DatabasePlatform getPlatform(Platform platform) { 827 switch (platform) { 828 case H2: 829 return new H2Platform(); 830 case HSQLDB: 831 return new HsqldbPlatform(); 832 case POSTGRES9: 833 return new Postgres9Platform(); 834 case POSTGRES: 835 return new PostgresPlatform(); 836 case MARIADB: 837 return new MariaDbPlatform(); 838 case MYSQL55: 839 return new MySql55Platform(); 840 case MYSQL: 841 return new MySqlPlatform(); 842 case ORACLE: 843 return new OraclePlatform(); 844 case ORACLE11: 845 return new Oracle11Platform(); 846 case SQLANYWHERE: 847 return new SqlAnywherePlatform(); 848 case SQLSERVER16: 849 return new SqlServer16Platform(); 850 case SQLSERVER17: 851 return new SqlServer17Platform(); 852 case SQLSERVER: 853 throw new IllegalArgumentException("Please choose the more specific SQLSERVER16 or SQLSERVER17 platform. Refer to issue #1340 for details"); 854 case DB2: 855 return new DB2Platform(); 856 case SQLITE: 857 return new SQLitePlatform(); 858 case HANA: 859 return new HanaPlatform(); 860 case NUODB: 861 return new NuoDbPlatform(); 862 case COCKROACH: 863 return new CockroachPlatform(); 864 case CLICKHOUSE: 865 return new ClickHousePlatform(); 866 867 case GENERIC: 868 return new DatabasePlatform(); 869 870 default: 871 throw new IllegalArgumentException("Platform missing? " + platform); 872 } 873 } 874 875 /** 876 * Holds a platform and prefix. Used to generate multiple platform specific DDL 877 * for a single migration. 878 */ 879 static class Pair { 880 881 /** 882 * The platform to generate the DDL for. 883 */ 884 final DatabasePlatform platform; 885 886 /** 887 * A prefix included into the file/resource names indicating the platform. 888 */ 889 final String prefix; 890 891 Pair(DatabasePlatform platform, String prefix) { 892 this.platform = platform; 893 this.prefix = prefix; 894 } 895 } 896 897}