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}