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