package io.ebeaninternal.dbmigration; import static io.ebeaninternal.api.PlatformMatch.matchPlatform; import java.io.File; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.StringJoiner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.ebean.DB; import io.ebean.Database; import io.ebean.annotation.Platform; import io.ebean.config.ClassLoadConfig; import io.ebean.config.DatabaseConfig; import io.ebean.config.DbConstraintNaming; import io.ebean.config.PlatformConfig; import io.ebean.config.PropertiesWrapper; import io.ebean.config.dbplatform.DatabasePlatform; import io.ebean.config.dbplatform.clickhouse.ClickHousePlatform; import io.ebean.config.dbplatform.cockroach.CockroachPlatform; import io.ebean.config.dbplatform.db2.DB2Platform; import io.ebean.config.dbplatform.h2.H2Platform; import io.ebean.config.dbplatform.hana.HanaPlatform; import io.ebean.config.dbplatform.hsqldb.HsqldbPlatform; import io.ebean.config.dbplatform.mariadb.MariaDbPlatform; import io.ebean.config.dbplatform.mysql.MySql55Platform; import io.ebean.config.dbplatform.mysql.MySqlPlatform; import io.ebean.config.dbplatform.nuodb.NuoDbPlatform; import io.ebean.config.dbplatform.oracle.Oracle11Platform; import io.ebean.config.dbplatform.oracle.OraclePlatform; import io.ebean.config.dbplatform.postgres.Postgres9Platform; import io.ebean.config.dbplatform.postgres.PostgresPlatform; import io.ebean.config.dbplatform.sqlanywhere.SqlAnywherePlatform; import io.ebean.config.dbplatform.sqlite.SQLitePlatform; import io.ebean.config.dbplatform.sqlserver.SqlServer16Platform; import io.ebean.config.dbplatform.sqlserver.SqlServer17Platform; import io.ebean.dbmigration.DbMigration; import io.ebean.util.IOUtils; import io.ebean.util.StringHelper; import io.ebeaninternal.api.DbOffline; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.dbmigration.ddlgeneration.DdlOptions; import io.ebeaninternal.dbmigration.ddlgeneration.DdlWrite; import io.ebeaninternal.dbmigration.migration.Migration; import io.ebeaninternal.dbmigration.migrationreader.MigrationXmlWriter; import io.ebeaninternal.dbmigration.model.CurrentModel; import io.ebeaninternal.dbmigration.model.MConfiguration; import io.ebeaninternal.dbmigration.model.MigrationModel; import io.ebeaninternal.dbmigration.model.ModelContainer; import io.ebeaninternal.dbmigration.model.ModelDiff; import io.ebeaninternal.dbmigration.model.PlatformDdlWriter; import io.ebeaninternal.extraddl.model.DdlScript; import io.ebeaninternal.extraddl.model.ExtraDdl; import io.ebeaninternal.extraddl.model.ExtraDdlXmlReader; /** * Generates DB Migration xml and sql scripts. *
* Reads the prior migrations and compares with the current model of the EbeanServer * and generates a migration 'diff' in the form of xml document with the logical schema * changes and a series of sql scripts to apply, rollback the applied changes if necessary * and drop objects (drop tables, drop columns). *
** This does not run the migration or ddl scripts but just generates them. *
*{@code
*
* DbMigration migration = DbMigration.create();
* migration.setPathToResources("src/main/resources");
* migration.setPlatform(Platform.POSTGRES);
*
* migration.generateMigration();
*
* }
*/
public class DefaultDbMigration implements DbMigration {
protected static final Logger logger = LoggerFactory.getLogger("io.ebean.GenerateMigration");
private static final String initialVersion = "1.0";
private static final String GENERATED_COMMENT = "THIS IS A GENERATED FILE - DO NOT MODIFY";
protected final boolean online;
private boolean logToSystemOut = true;
protected SpiEbeanServer server;
protected String pathToResources = "src/main/resources";
protected String migrationPath = "dbmigration";
protected String migrationInitPath = "dbinit";
protected String modelPath = "model";
protected String modelSuffix = ".model.xml";
protected DatabasePlatform databasePlatform;
private boolean vanillaPlatform;
protected List* If not set this defaults to the platform of the default server. *
*/ @Override public void setPlatform(Platform platform) { vanillaPlatform = true; setPlatform(platform(platform)); } /** * Set the specific platform to generate DDL for. ** If not set this defaults to the platform of the default server. *
*/ @Override public void setPlatform(DatabasePlatform databasePlatform) { this.databasePlatform = databasePlatform; if (!online) { DbOffline.setPlatform(databasePlatform.getPlatform()); } } @Override public void addPlatform(Platform platform) { String prefix = platform.base().name().toLowerCase(); addPlatform(platform, prefix); } @Override public void addPlatform(Platform platform, String prefix) { platforms.add(new Pair(platform(platform), prefix)); } @Override public void addDatabasePlatform(DatabasePlatform databasePlatform, String prefix) { platforms.add(new Pair(databasePlatform, prefix)); } /** * Generate the next migration xml file and associated apply and rollback sql scripts. ** This does not run the migration or ddl scripts but just generates them. *
*{@code
*
* DbMigration migration = DbMigration.create();
* migration.setPathToResources("src/main/resources");
* migration.setPlatform(Platform.ORACLE);
*
* migration.generateMigration();
*
* }
* *
{@code
*
* DbMigration migration = DbMigration.create();
* migration.setPathToResources("src/main/resources");
*
* migration.addPlatform(Platform.POSTGRES);
* migration.addPlatform(Platform.MYSQL);
* migration.addPlatform(Platform.ORACLE);
*
* migration.generateMigration();
*
* }
*
* @return the generated migration or null
*/
@Override
public String generateMigration() throws IOException {
final String version = generateMigrationFor(false);
if (includeIndex) {
generateIndex();
}
return version;
}
/**
* Generate the {@code idx_platform.migrations} file.
*/
private void generateIndex() throws IOException {
final File topDir = migrationDirectory(false);
if (!platforms.isEmpty()) {
for (Pair pair : platforms) {
new IndexMigration(topDir, pair).generate();
}
} else {
new IndexMigration(topDir, databasePlatform).generate();
}
}
@Override
public String generateInitMigration() throws IOException {
return generateMigrationFor(true);
}
private String generateMigrationFor(boolean initMigration) throws IOException {
if (!online) {
// use this flag to stop other plugins like full DDL generation
DbOffline.setGenerateMigration();
if (databasePlatform == null && !platforms.isEmpty()) {
// for multiple platform generation the first platform
// is used to generate the "logical" model diff
setPlatform(platforms.get(0).platform);
}
}
setDefaults();
if (!platforms.isEmpty()) {
configurePlatforms();
}
try {
Request request = createRequest(initMigration);
if (!initMigration) {
// repeatable migrations
if (platforms.isEmpty()) {
generateExtraDdl(request.migrationDir, databasePlatform, request.isTablePartitioning());
} else {
for (Pair pair : platforms) {
PlatformDdlWriter platformWriter = createDdlWriter(pair.platform);
File subPath = platformWriter.subPath(request.migrationDir, pair.prefix);
generateExtraDdl(subPath, pair.platform, request.isTablePartitioning());
}
}
}
String pendingVersion = generatePendingDrop();
if (pendingVersion != null) {
return generatePendingDrop(request, pendingVersion);
} else {
return generateDiff(request);
}
} catch (UnknownResourcePathException e) {
logError("ERROR - " + e.getMessage());
logError("Check the working directory or change dbMigration.setPathToResources() value?");
return null;
} finally {
if (!online) {
DbOffline.reset();
}
}
}
/**
* Return the versions containing pending drops.
*/
@Override
public List* These take scrips from extra-ddl.xml (typically views) and outputs "repeatable" * migration scripts (starting with "R__") to be run by FlywayDb or Ebean's own * migration runner. *
*/ private void generateExtraDdl(File migrationDir, DatabasePlatform dbPlatform, boolean tablePartitioning) throws IOException { if (dbPlatform != null) { if (tablePartitioning && includeBuiltInPartitioning) { generateExtraDdlFor(migrationDir, dbPlatform, ExtraDdlXmlReader.readBuiltinTablePartitioning(), true); } generateExtraDdlFor(migrationDir, dbPlatform, ExtraDdlXmlReader.readBuiltin(), true); generateExtraDdlFor(migrationDir, dbPlatform, ExtraDdlXmlReader.read(), false); } } private void generateExtraDdlFor(File migrationDir, DatabasePlatform dbPlatform, ExtraDdl extraDdl, boolean isBuiltin) throws IOException { if (extraDdl != null) { List* The full version can contain a comment suffix after a "__" double underscore. */ private String fullVersion(String nextVersion, String dropsFor) { String version = version(); if (version == null) { version = (nextVersion != null) ? nextVersion : initialVersion; } String fullVersion = applyPrefix + version; String name = name(); if (name != null) { fullVersion += "__" + toUnderScore(name); } else if (dropsFor != null) { fullVersion += "__" + toUnderScore("dropsFor_" + trimDropsFor(dropsFor)); } else if (version.equals(initialVersion)) { fullVersion += "__initial"; } return fullVersion; } String trimDropsFor(String dropsFor) { if (dropsFor.startsWith("V") || dropsFor.startsWith("v")) { dropsFor = dropsFor.substring(1); } int commentStart = dropsFor.indexOf("__"); if (commentStart > -1) { // trim off the trailing comment dropsFor = dropsFor.substring(0, commentStart); } return dropsFor; } /** * Replace spaces with underscores. */ private String toUnderScore(String name) { return name.replace(' ', '_'); } /** * Write any extra platform ddl. */ private void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException { DdlOptions options = new DdlOptions(addForeignKeySkipCheck); for (Pair pair : platforms) { DdlWrite platformBuffer = new DdlWrite(new MConfiguration(), currentModel.read(), options); PlatformDdlWriter platformWriter = createDdlWriter(pair.platform); File subPath = platformWriter.subPath(writePath, pair.prefix); platformWriter.processMigration(dbMigration, platformBuffer, subPath, fullVersion); } } private PlatformDdlWriter createDdlWriter(DatabasePlatform platform) { return new PlatformDdlWriter(platform, databaseConfig, lockTimeoutSeconds); } /** * Write the migration xml. */ private boolean writeMigrationXml(Migration dbMigration, File resourcePath, String fullVersion) { String modelFile = fullVersion + modelSuffix; File file = new File(resourcePath, modelFile); if (file.exists()) { return false; } String comment = Boolean.TRUE.equals(includeGeneratedFileComment) ? GENERATED_COMMENT : null; MigrationXmlWriter xmlWriter = new MigrationXmlWriter(comment); xmlWriter.write(dbMigration, file); return true; } /** * Set default server and platform if necessary. */ private void setDefaults() { if (server == null) { setServer(DB.getDefault()); } if (vanillaPlatform || databasePlatform == null) { // not explicitly set so use the platform of the server databasePlatform = server.databasePlatform(); } if (databaseConfig != null) { if (strictMode != null) { databaseConfig.setDdlStrictMode(strictMode); } if (header != null) { databaseConfig.setDdlHeader(header); } } } /** * Return the migration version (typically FlywayDb compatible). *
* Example: 1.1.1_2 *
* The version is expected to be the combination of the current pom version plus * a 'feature' id. The combined version must be unique and ordered to work with * FlywayDb so each developer sets a unique version so that the migration script * generated is unique (typically just prior to being submitted as a merge request). */ private String version() { String envVersion = readEnvironment("ddl.migration.version"); if (!isEmpty(envVersion)) { return envVersion.trim(); } return version; } /** * Return the migration name which is short description text that can be appended to * the migration version to become the ddl script file name. *
* So if the name is "a foo table" then the ddl script file could be: * "1.1.1_2__a-foo-table.sql" *
** When the DB migration relates to a git feature (merge request) then this description text * is a short description of the feature. *
*/ private String name() { String envName = readEnvironment("ddl.migration.name"); if (!isEmpty(envName)) { return envName.trim(); } return name; } /** * Return true if the string is null or empty. */ private boolean isEmpty(String val) { return val == null || val.trim().isEmpty(); } /** * Return the system or environment property. */ private String readEnvironment(String key) { String val = System.getProperty(key); if (val == null) { val = System.getenv(key); } return val; } /** * Return the main migration directory. */ File migrationDirectory() { return migrationDirectory(false); } /** * Return the file path to write the xml and sql to. */ File migrationDirectory(boolean initMigration) { // path to src/main/resources in typical maven project File resourceRootDir = new File(pathToResources); if (!resourceRootDir.exists()) { String msg = String.format("Error - path to resources %s does not exist. Absolute path is %s", pathToResources, resourceRootDir.getAbsolutePath()); throw new UnknownResourcePathException(msg); } String resourcePath = migrationPath(initMigration); // expect to be a path to something like - src/main/resources/dbmigration File path = new File(resourceRootDir, resourcePath); if (!path.exists()) { if (!path.mkdirs()) { logInfo("Warning - Unable to ensure migration directory exists at %s", path.getAbsolutePath()); } } return path; } private String migrationPath(boolean initMigration) { return initMigration ? migrationInitPath : migrationPath; } /** * Return the model directory (relative to the migration directory). */ private File modelDirectory(File migrationDirectory) { if (modelPath == null || modelPath.isEmpty()) { return migrationDirectory; } File modelDir = new File(migrationDirectory, modelPath); if (!modelDir.exists() && !modelDir.mkdirs()) { logInfo("Warning - Unable to ensure migration model directory exists at %s", modelDir.getAbsolutePath()); } return modelDir; } /** * Return the DatabasePlatform given the platform key. */ protected DatabasePlatform platform(Platform platform) { switch (platform) { case H2: return new H2Platform(); case HSQLDB: return new HsqldbPlatform(); case POSTGRES9: return new Postgres9Platform(); case POSTGRES: return new PostgresPlatform(); case MARIADB: return new MariaDbPlatform(); case MYSQL55: return new MySql55Platform(); case MYSQL: return new MySqlPlatform(); case ORACLE: return new OraclePlatform(); case ORACLE11: return new Oracle11Platform(); case SQLANYWHERE: return new SqlAnywherePlatform(); case SQLSERVER16: return new SqlServer16Platform(); case SQLSERVER17: return new SqlServer17Platform(); case SQLSERVER: throw new IllegalArgumentException("Please choose the more specific SQLSERVER16 or SQLSERVER17 platform. Refer to issue #1340 for details"); case DB2: return new DB2Platform(); case SQLITE: return new SQLitePlatform(); case HANA: return new HanaPlatform(); case NUODB: return new NuoDbPlatform(); case COCKROACH: return new CockroachPlatform(); case CLICKHOUSE: return new ClickHousePlatform(); case GENERIC: return new DatabasePlatform(); default: throw new IllegalArgumentException("Platform missing? " + platform); } } /** * Holds a platform and prefix. Used to generate multiple platform specific DDL * for a single migration. */ static class Pair { /** * The platform to generate the DDL for. */ final DatabasePlatform platform; /** * A prefix included into the file/resource names indicating the platform. */ final String prefix; Pair(DatabasePlatform platform, String prefix) { this.platform = platform; this.prefix = prefix; } } }