#636 - ENH: Add DB Migration runner (as alternative to running via FlywayDB etc)

This commit is contained in:
Robin Bygrave
2016-04-06 14:31:57 +12:00
parent a7b239a1b7
commit abaa1ff934
22 changed files with 716 additions and 151 deletions
+1 -7
View File
@@ -64,7 +64,7 @@
<dependency>
<groupId>org.avaje</groupId>
<artifactId>avaje-datasource</artifactId>
<version>1.1.1</version>
<version>1.1.2</version>
</dependency>
<!-- bring in cluster support by default -->
@@ -92,12 +92,6 @@
<version>[1.7.1,1.7.99)</version>
</dependency>
<dependency>
<groupId>org.avaje</groupId>
<artifactId>avaje-classpath-scanner</artifactId>
<version>1.1.1</version>
</dependency>
<!-- provided scope for Spring boot loader classpath scanning support -->
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -74,6 +74,9 @@ public class ClassLoadConfig {
}
}
/**
* Return the resources for the given name.
*/
public Enumeration<URL> getResources(String name) throws IOException {
return context.getResources(name);
}
@@ -6,6 +6,8 @@ import com.avaje.ebean.dbmigration.DbMigration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
/**
* Configuration for the DB migration processing.
*/
@@ -67,6 +69,36 @@ public class DbMigrationConfig {
*/
protected String generatePendingDrop;
/**
* For running migration the DB table that holds migration execution status.
*/
protected String metaTable = "db_migration";
/**
* Flag set to true means to run any outstanding migrations on startup.
*/
protected boolean runMigration;
/**
* Comma and equals delimited key/value placeholders to replace in DDL scripts.
*/
protected String runPlaceholders;
/**
* Map of key/value placeholders to replace in DDL scripts.
*/
protected Map<String,String> runPlaceholderMap;
/**
* DB user used to run the DB migration.
*/
protected String dbUser;
/**
* DB password used to run the DB migration.
*/
protected String dbPassword;
/**
* Return the DB platform to generate migration DDL for.
*
@@ -199,10 +231,119 @@ public class DbMigrationConfig {
this.modelPath = "";
}
/**
* Return the table name that holds the migration run details
* (used by DB Migration runner only).
*/
public String getMetaTable() {
return metaTable;
}
/**
* Set the table name that holds the migration run details
* (used by DB Migration runner only).
*/
public void setMetaTable(String metaTable) {
this.metaTable = metaTable;
}
/**
* Return a comma and equals delimited placeholders that are substituted in SQL scripts when running migration
* (used by DB Migration runner only).
*/
public String getRunPlaceholders() {
// environment properties take precedence
String placeholders = readEnvironment("ddl.migration.placeholders");
if (placeholders != null) {
return placeholders;
}
return runPlaceholders;
}
/**
* Set a comma and equals delimited placeholders that are substituted in SQL scripts when running migration
* (used by DB Migration runner only).
*/
public void setRunPlaceholders(String runPlaceholders) {
this.runPlaceholders = runPlaceholders;
}
/**
* Return a map of placeholder values that are substituted in SQL scripts when running migration
* (used by DB Migration runner only).
*/
public Map<String, String> getRunPlaceholderMap() {
return runPlaceholderMap;
}
/**
* Set a map of placeholder values that are substituted when running migration
* (used by DB Migration runner only).
*/
public void setRunPlaceholderMap(Map<String, String> runPlaceholderMap) {
this.runPlaceholderMap = runPlaceholderMap;
}
/**
* Return true if the DB migration should be run on startup.
*/
public boolean isRunMigration() {
// environment properties take precedence
String run = readEnvironment("ddl.migration.run");
if (run != null) {
return "true".equalsIgnoreCase(run.trim());
}
return runMigration;
}
/**
* Set to true to run the DB migration on startup.
*/
public void setRunMigration(boolean runMigration) {
this.runMigration = runMigration;
}
/**
* Return the DB user to use for running DB migrations.
*/
public String getDbUser() {
// environment properties take precedence
String user = readEnvironment("ddl.migration.user");
if (user != null) {
return user;
}
return dbUser;
}
/**
* Set the DB user to use for running DB migrations.
*/
public void setDbUser(String dbUser) {
this.dbUser = dbUser;
}
/**
* Return the DB password to use for running DB migrations.
*/
public String getDbPassword() {
String user = readEnvironment("ddl.migration.password");
if (user != null) {
return user;
}
return dbPassword;
}
/**
* Set the DB password to use for running DB migrations.
*/
public void setDbPassword(String dbPassword) {
this.dbPassword = dbPassword;
}
/**
* Load the settings from the PropertiesWrapper.
*/
public void loadSettings(PropertiesWrapper properties) {
public void loadSettings(PropertiesWrapper properties, String serverName) {
migrationPath = properties.get("migration.migrationPath", migrationPath);
if (properties.getBoolean("migration.singleDirectory", false)) {
@@ -219,7 +360,19 @@ public class DbMigrationConfig {
generate = properties.getBoolean("migration.generate", generate);
version = properties.get("migration.version", version);
name = properties.get("migration.name", name);
this.name = properties.get("migration.name", this.name);
runMigration = properties.getBoolean("migration.run", runMigration);
metaTable = properties.get("migration.metaTable", metaTable);
runPlaceholders = properties.get("migration.placeholders", runPlaceholders);
String adminUser = properties.get("datasource."+serverName+".user", dbUser);
adminUser = properties.get("datasource."+serverName+".adminuser", adminUser);
dbUser = properties.get("migration.dbuser", adminUser);
String adminPwd = properties.get("datasource."+serverName+".password", dbPassword);
adminPwd = properties.get("datasource."+serverName+".adminpassword", adminPwd);
dbPassword = properties.get("migration.dbpassword", adminPwd);
}
/**
@@ -2267,7 +2267,7 @@ public class ServerConfig {
*/
protected void loadSettings(PropertiesWrapper p) {
migrationConfig.loadSettings(p);
migrationConfig.loadSettings(p, name);
namingConvention = createNamingConvention(p, namingConvention);
if (namingConvention != null) {
@@ -540,6 +540,9 @@ public class DatabasePlatform {
}
}
/**
* Close the resultSet.
*/
protected void close(ResultSet resultSet) {
try {
resultSet.close();
@@ -1,7 +1,6 @@
package com.avaje.ebean.dbmigration;
import com.avaje.ebean.EbeanServer;
import com.avaje.ebean.Transaction;
import com.avaje.ebean.config.DbMigrationConfig;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.dbmigration.runner.LocalMigrationResource;
@@ -11,62 +10,91 @@ import com.avaje.ebeaninternal.util.JdbcClose;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
/**
*
* Runs the DB migration typically on application start.
*/
public class MigrationRunner {
private static final Logger logger = LoggerFactory.getLogger(MigrationRunner.class);
final EbeanServer server;
final ServerConfig config;
private final EbeanServer server;
private final ServerConfig config;
private final DbMigrationConfig migrationConfig;
public MigrationRunner(EbeanServer server) {
public MigrationRunner(EbeanServer server, DbMigrationConfig migrationConfig) {
this.server = server;
this.config = server.getPluginApi().getServerConfig();
migrationConfig = this.config.getMigrationConfig();
this.migrationConfig = migrationConfig;
}
/**
* Run the migrations if there are any that need running.
*/
public void run() {
LocalMigrationResources resources = new LocalMigrationResources(config);
LocalMigrationResources resources = new LocalMigrationResources(config, migrationConfig);
if (!resources.readResources()) {
logger.debug("no migrations to check");
return;
}
Transaction transaction = server.createTransaction();
Connection connection = transaction.getConnection();
String migrationUser = migrationConfig.getDbUser();
String migrationPwd = migrationConfig.getDbPassword();
DataSource dataSource = server.getPluginApi().getDataSource();
Connection connection;
try {
MigrationTable table = new MigrationTable(server, migrationConfig, connection);
table.createIfNeeded();
connection = dataSource.getConnection(migrationUser, migrationPwd);
} catch (SQLException e) {
throw new IllegalArgumentException("Error trying to connect to database using DB Migration user [" + migrationUser + "]", e);
}
LocalMigrationResource priorVersion = null;
List<LocalMigrationResource> localVersions = resources.getVersions();
for (int i = 0; i < localVersions.size(); i++) {
LocalMigrationResource localVersion = localVersions.get(i);
if (i > 0) {
priorVersion = localVersions.get(i-1);
}
if (!table.shouldRun(localVersion, priorVersion)) {
break;
}
}
try {
connection.setAutoCommit(false);
table.commit();
runMigrations(resources, connection);
connection.commit();
} catch (Exception e) {
JdbcClose.rollback(connection);
throw new RuntimeException(e);
} finally {
JdbcClose.close(connection);
}
}
/**
* Run all the migrations as needed.
*/
private void runMigrations(LocalMigrationResources resources, Connection connection) throws SQLException, IOException {
MigrationTable table = new MigrationTable(server, migrationConfig, connection);
table.createIfNeeded();
// get the migrations in version order
List<LocalMigrationResource> localVersions = resources.getVersions();
LocalMigrationResource priorVersion = null;
// run migrations in order
for (int i = 0; i < localVersions.size(); i++) {
LocalMigrationResource localVersion = localVersions.get(i);
if (!table.shouldRun(localVersion, priorVersion)) {
break;
}
priorVersion = localVersion;
}
}
}
@@ -4,7 +4,7 @@ import com.avaje.ebean.dbmigration.model.MigrationVersion;
import org.avaje.classpath.scanner.Resource;
/**
* A DB migration resource.
* A DB migration resource (DDL script with version).
*/
public class LocalMigrationResource implements Comparable<LocalMigrationResource> {
@@ -14,6 +14,9 @@ public class LocalMigrationResource implements Comparable<LocalMigrationResource
private final Resource resource;
/**
* Construct with version and resource.
*/
public LocalMigrationResource(MigrationVersion version, String location, Resource resource) {
this.version = version;
this.location = location;
@@ -3,9 +3,8 @@ package com.avaje.ebean.dbmigration.runner;
import com.avaje.ebean.config.DbMigrationConfig;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.dbmigration.model.MigrationVersion;
import org.avaje.classpath.scanner.Location;
import org.avaje.classpath.scanner.MatchResource;
import org.avaje.classpath.scanner.Resource;
import org.avaje.classpath.scanner.ResourceFilter;
import org.avaje.classpath.scanner.Scanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,7 +14,7 @@ import java.util.Collections;
import java.util.List;
/**
*
* Loads the DB migration resources and sorts them into execution order.
*/
public class LocalMigrationResources {
@@ -27,11 +26,17 @@ public class LocalMigrationResources {
private final List<LocalMigrationResource> versions = new ArrayList<LocalMigrationResource>();
public LocalMigrationResources(ServerConfig serverConfig) {
/**
* Construct with configuration options.
*/
public LocalMigrationResources(ServerConfig serverConfig, DbMigrationConfig migrationConfig) {
this.serverConfig = serverConfig;
this.migrationConfig = serverConfig.getMigrationConfig();
this.migrationConfig = migrationConfig;
}
/**
* Read all the migration resources (SQL scripts) returning true if there are versions.
*/
public boolean readResources() {
String migrationPath = migrationConfig.getMigrationPath();
@@ -59,14 +64,20 @@ public class LocalMigrationResources {
return !versions.isEmpty();
}
/**
* Return the list of migration resources in version order.
*/
public List<LocalMigrationResource> getVersions() {
return versions;
}
static class Match implements MatchResource {
/**
* Filter used to find the migration scripts.
*/
private static class Match implements ResourceFilter {
final DbMigrationConfig migrationConfig;
private final DbMigrationConfig migrationConfig;
Match(DbMigrationConfig migrationConfig) {
this.migrationConfig = migrationConfig;
@@ -74,12 +85,7 @@ public class LocalMigrationResources {
@Override
public boolean isMatch(String name) {
return name.endsWith(migrationConfig.getApplySuffix())
|| name.endsWith(migrationConfig.getModelSuffix())
|| name.endsWith(migrationConfig.getDropSuffix())
|| name.endsWith(migrationConfig.getRollbackSuffix());
return name.endsWith(migrationConfig.getApplySuffix());
}
}
}
@@ -1,27 +1,100 @@
package com.avaje.ebean.dbmigration.runner;
import com.avaje.ebean.SqlRow;
import com.avaje.ebean.SqlUpdate;
import java.sql.Timestamp;
/**
*
* Bean holding migration execution details stored in the migration table.
*/
public class MigrationMetaRow implements Comparable<MigrationMetaRow> {
class MigrationMetaRow {
int id;
String status;
String runVersion;
String depVersion;
String comment;
int checksum;
Timestamp runOn;
String runBy;
private int id;
@Override
public int compareTo(MigrationMetaRow o) {
return (id < o.id) ? -1 : ((id == o.id) ? 0 : 1);
private String status;
private String runVersion;
private String depVersion;
private String comment;
private int checksum;
private Timestamp runOn;
private String runBy;
/**
* Construct for inserting into table.
*/
MigrationMetaRow(int id, String runVersion, String priorVersion, String comment, int checksum, String runBy) {
this.id = id;
this.runVersion = runVersion;
this.depVersion = priorVersion;
this.checksum = checksum;
this.comment = comment;
this.runBy = runBy;
this.runOn = new Timestamp(System.currentTimeMillis());
}
/**
* Construct from the SqlRow (read from table).
*/
MigrationMetaRow(SqlRow row) {
id = row.getInteger("id");
status = row.getString("status");
runVersion = row.getString("row_version");
depVersion = row.getString("dep_version");
comment = row.getString("comment");
checksum = row.getInteger("checksum");
runOn = row.getTimestamp("run_on");
runBy = row.getString("run_by");
}
//String sql = "insert into ebean_migration (id,status,run_version,dep_version,comment,checksum,run_on,run_by,run_ip) "+
// "values (?,?,?,?,?,?,?,?,?)";
/**
* Return the id for this migration.
*/
int getId() {
return id;
}
/**
* Return the normalised version for this migration.
*/
String getRunVersion() {
return runVersion;
}
/**
* Return the checksum for this migration.
*/
int getChecksum() {
return checksum;
}
/**
* Bind to the insert statement.
*/
void bindInsert(SqlUpdate insert) {
insert.setParameter(1, id);
insert.setParameter(2, "success");
insert.setParameter(3, runVersion);
insert.setParameter(4, depVersion);
insert.setParameter(5, comment);
insert.setParameter(6, checksum);
insert.setParameter(7, runOn);
insert.setParameter(8, runBy);
insert.setParameter(9, "ip");
}
/**
* Return the SQL insert given the table migration meta data is stored in.
*/
static String insertSql(String table) {
return "insert into " + table
+ " (id, status, run_version, dep_version, comment, checksum, run_on, run_by, run_ip)"
+ " values (?,?,?,?,?,?,?,?,?)";
}
}
@@ -5,12 +5,15 @@ import com.avaje.ebean.dbmigration.DdlRunner;
import java.sql.Connection;
/**
* Created by rob on 5/02/16.
* Runs the DDL migration scripts.
*/
public class MigrationScriptRunner {
final Connection connection;
private final Connection connection;
/**
* Construct with a given connection.
*/
public MigrationScriptRunner(Connection connection) {
this.connection = connection;
}
@@ -17,12 +17,13 @@ import java.io.IOException;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Created by rob on 5/02/16.
* Manages the migration table.
*/
public class MigrationTable {
@@ -34,34 +35,53 @@ public class MigrationTable {
private final DatabasePlatform databasePlatform;
private final DbMigrationConfig migrationConfig;
private final String catalog;
private final String schema;
private final String table;
private final ServerConfig serverConfig;
private final String envUserName;
int currentSearchPosition;
private final ScriptTransform scriptTransform;
List<SqlRow> metaRows;
private final String insertSql;
private final LinkedHashMap<String,MigrationMetaRow> migrations;
private MigrationMetaRow lastMigration;
/**
* Construct with server, configuration and jdbc connection (DB admin user).
*/
public MigrationTable(EbeanServer server, DbMigrationConfig migrationConfig, Connection connection) {
this.connection = connection;
this.server = server;
this.migrations = new LinkedHashMap<String, MigrationMetaRow>();
SpiServer pluginApi = server.getPluginApi();
this.serverConfig = pluginApi.getServerConfig();
this.databasePlatform = pluginApi.getDatabasePlatform();
this.migrationConfig = migrationConfig;
this.catalog = null;
this.schema = null;
this.table = "ebean_migration";
this.table = migrationConfig.getMetaTable();
this.insertSql = MigrationMetaRow.insertSql(table);
this.scriptTransform = createScriptTransform(migrationConfig);
this.envUserName = System.getProperty("user.name");
}
/**
* Create the ScriptTransform for placeholder key/value replacement.
*/
private ScriptTransform createScriptTransform(DbMigrationConfig config) {
Map<String, String> map = PlaceholderBuilder.build(config.getRunPlaceholders(), config.getRunPlaceholderMap());
return new ScriptTransform(map);
}
/**
* Create the table is it does not exist.
*/
public void createIfNeeded() throws SQLException, IOException {
if (!tableExists(connection)) {
@@ -69,20 +89,39 @@ public class MigrationTable {
}
ExternalJdbcTransaction t = new ExternalJdbcTransaction(connection);
SqlQuery sqlQuery = server.createSqlQuery("select * from ebean_migration order by id for update");
metaRows = server.findList(sqlQuery, t);
SqlQuery sqlQuery = server.createSqlQuery("select * from "+table+" order by id for update");
List<SqlRow> metaRows = server.findList(sqlQuery, t);
for (SqlRow row : metaRows) {
addMigration(new MigrationMetaRow(row));
}
}
private void createTable(Connection connection) throws IOException {
String script = getCreateTableScript();
String script = ScriptTransform.table(table, getCreateTableScript());
MigrationScriptRunner run = new MigrationScriptRunner(connection);
run.runScript(false, script, "create migration tables");
run.runScript(false, script, "create migration table");
}
/**
* Return the create table script.
*/
private String getCreateTableScript() throws IOException {
// supply a script to override the default table create script
String script = readResource("migration-support/create-table.sql");
if (script == null) {
// no, just use the default script
script = readResource("migration-support/default-create-table.sql");
}
return script;
}
Enumeration<URL> resources = serverConfig.getClassLoadConfig().getResources("migration-support/create.sql");
private String readResource(String location) throws IOException {
Enumeration<URL> resources = serverConfig.getClassLoadConfig().getResources(location);
if (resources.hasMoreElements()) {
URL url = resources.nextElement();
return IOUtils.readUtf8(url.openStream());
@@ -90,75 +129,106 @@ public class MigrationTable {
return null;
}
public boolean tableExists(Connection connection) throws SQLException {
/**
* Return true if the table exists.
*/
private boolean tableExists(Connection connection) throws SQLException {
return databasePlatform.tableExists(connection, catalog, schema, table);
}
/**
* Return true if the migration ran successfully and false if the migration failed.
*/
public boolean shouldRun(LocalMigrationResource localVersion, LocalMigrationResource priorVersion) {
// if prior != null check previous version installed
// if previous version not installed ... error - missing version (prior version)
// if (!priorVersionNotInstalled(priorVersion)) {
//
// }
//
// // if localVersion installed
// // check checksum and return ok, or re-installable?
// // else
// // install version and continue
//
// if (runPosition >= metaRows.size()) {
// logger.debug("No matching row");
// runMigration(runPosition, localVersion);
// return true;
// }
return false;
}
private void checkInstalled(LocalMigrationResource priorVersion) {
if (priorVersion != null) {
searchFor(priorVersion);
// check priorVersion is installed
MigrationMetaRow existing = migrations.get(priorVersion.getVersion().normalised());
if (existing == null) {
logger.warn("Migration {} requires prior migration {} which has not been run", localVersion.getVersion(), priorVersion.getVersion());
return false;
}
}
MigrationMetaRow existing = migrations.get(localVersion.getVersion().normalised());
if (existing == null) {
runMigration(localVersion, priorVersion);
return true;
} else {
// check checksum and return ok, or re-run if repeatable script?
existing.getChecksum();
return true;
}
}
private void searchFor(LocalMigrationResource priorVersion) {
}
public void commit() throws SQLException {
connection.commit();
}
private void runMigration(int runPosition, LocalMigrationResource localVersion) {
/**
* Run the migration script.
*/
private void runMigration(LocalMigrationResource localVersion, LocalMigrationResource prior) {
logger.debug("run migration "+localVersion.getLocation());
String script = localVersion.getContent();
String script = convertScript(localVersion.getContent());
MigrationScriptRunner run = new MigrationScriptRunner(connection);
run.runScript(false, script, "run migration version: "+localVersion.getVersion());
String sql = "insert into ebean_migration (id,status,run_version,dep_version,comment,checksum,run_on,run_by,run_ip) "+
"values (?,?,?,?,?,?,?,?,?)";
SqlUpdate sqlUpdate = server.createSqlUpdate(sql);
sqlUpdate.setParameter(1, runPosition+1);
sqlUpdate.setParameter(2, "success");
sqlUpdate.setParameter(3, localVersion.getVersion().getFull());
sqlUpdate.setParameter(4, "na");
sqlUpdate.setParameter(5, "comm");
sqlUpdate.setParameter(6, 0);
sqlUpdate.setParameter(7, new Timestamp(System.currentTimeMillis()));
sqlUpdate.setParameter(8, envUserName);
sqlUpdate.setParameter(9, "userIp");
int checksum = Checksum.calculate(script);
MigrationMetaRow metaRow = createMetaRow(localVersion, prior, checksum);
ExternalJdbcTransaction t = new ExternalJdbcTransaction(connection);
server.execute(sqlUpdate, t);
SqlUpdate insert = server.createSqlUpdate(insertSql);
metaRow.bindInsert(insert);
server.execute(insert, new ExternalJdbcTransaction(connection));
addMigration(metaRow);
}
/**
* Create the MigrationMetaRow for this migration.
*/
private MigrationMetaRow createMetaRow(LocalMigrationResource localVersion, LocalMigrationResource prior, int checksum) {
int nextId = 1;
if (lastMigration != null) {
nextId = lastMigration.getId() + 1;
}
String runVersion = localVersion.getVersion().normalised();
String comment = getMigrationComment(localVersion);
String priorVersion = getMigrationPriorVersion(prior);
return new MigrationMetaRow(nextId, runVersion, priorVersion, comment, checksum, envUserName);
}
/**
* Return the prior migration normalised version.
*/
private String getMigrationPriorVersion(LocalMigrationResource prior) {
return prior != null ? prior.getVersion().normalised() : "-";
}
/**
* Return the migration comment.
*/
private String getMigrationComment(LocalMigrationResource localVersion) {
String comment = localVersion.getVersion().getComment();
return comment == null || comment.isEmpty() ? "-" : comment;
}
/**
* Apply the placeholder key/value replacement on the script.
*/
private String convertScript(String script) {
return scriptTransform.transform(script);
}
/**
* Register the successfully executed migration (to allow dependant scripts to run).
*/
private void addMigration(MigrationMetaRow metaRow) {
lastMigration = metaRow;
migrations.put(metaRow.getRunVersion(), metaRow);
}
}
@@ -1,7 +1,53 @@
package com.avaje.ebean.dbmigration.runner;
import java.util.HashMap;
import java.util.Map;
/**
* Created by rob on 1/04/16.
* Joins placeholder map and comma/equals delimited string.
*/
public class PlaceholderBuilder {
class PlaceholderBuilder {
private final Map<String,String> map = new HashMap<String,String>();
/**
* Create with raw comma and equals delimited pairs plus map of key value pairs.
*/
public static Map<String,String> build(String commaDelimited, Map<String,String> placeholders) {
PlaceholderBuilder builder = new PlaceholderBuilder();
builder.add(commaDelimited);
builder.add(placeholders);
return builder.map;
}
private PlaceholderBuilder() {
}
/**
* Add a comma and equals delimited string to parse for key value pairs.
*/
public void add(String commaDelimited) {
if (commaDelimited != null) {
String[] split = commaDelimited.split("[,;]");
for (String keyValue : split) {
String[] pair = keyValue.split("=");
if (pair.length == 2) {
map.put(pair[0].trim(), pair[1].trim());
}
}
}
}
/**
* Add a map of key value placeholder pairs.
*/
public void add(Map<String,String> placeholders) {
if (placeholders != null) {
map.putAll(placeholders);
}
}
}
@@ -1,7 +1,47 @@
package com.avaje.ebean.dbmigration.runner;
import java.util.HashMap;
import java.util.Map;
/**
* Created by rob on 1/04/16.
* Transforms a SQL script given a map of key/value substitutions.
*/
public class ScriptTransform {
class ScriptTransform {
/**
* Transform just ${table} with the table name.
*/
public static String table(String tableName, String script) {
return script.replace("${table}", tableName);
}
private final Map<String,String> placeholders = new HashMap<String, String>();
ScriptTransform(Map<String,String> map) {
for (Map.Entry<String, String> entry : map.entrySet()) {
placeholders.put(wrapKey(entry.getKey()), entry.getValue());
}
}
private String wrapKey(String key) {
return "${"+key+"}";
}
/**
* Return true if this contains no placeholders.
*/
boolean isEmpty() {
return placeholders.isEmpty();
}
/**
* Transform the script replacing placeholders in the form <code>${key}</code> with <code>value</code>.
*/
String transform(String source) {
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
source = source.replace(entry.getKey(), entry.getValue());
}
return source;
}
}
@@ -4,6 +4,7 @@ import com.avaje.ebean.EbeanServer;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
import javax.sql.DataSource;
import java.util.List;
/**
@@ -40,4 +41,9 @@ public interface SpiServer extends EbeanServer {
* Return the bean type for a given doc store queueId.
*/
BeanType<?> getBeanTypeForQueueId(String queueId);
/**
* Return the associated DataSource for this EbeanServer instance.
*/
DataSource getDataSource();
}
@@ -20,8 +20,8 @@ import com.avaje.ebean.event.readaudit.ReadAuditLogger;
import com.avaje.ebean.event.readaudit.ReadAuditPrepare;
import com.avaje.ebean.meta.MetaInfoManager;
import com.avaje.ebean.plugin.BeanType;
import com.avaje.ebean.plugin.SpiServer;
import com.avaje.ebean.plugin.Plugin;
import com.avaje.ebean.plugin.SpiServer;
import com.avaje.ebean.text.csv.CsvReader;
import com.avaje.ebean.text.json.JsonContext;
import com.avaje.ebeaninternal.api.LoadBeanRequest;
@@ -73,6 +73,7 @@ import org.slf4j.LoggerFactory;
import javax.persistence.NonUniqueResultException;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -80,7 +81,6 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.FutureTask;
@@ -308,6 +308,11 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
return autoTuneService;
}
@Override
public DataSource getDataSource() {
return transactionManager.getDataSource();
}
@Override
public ReadAuditPrepare getReadAuditPrepare() {
return readAuditPrepare;
@@ -341,8 +346,9 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
migrationConfig.generateOnStart(this);
}
MigrationRunner migrationRunner = new MigrationRunner(this);
migrationRunner.run();
if (migrationConfig.isRunMigration()) {
new MigrationRunner(this, migrationConfig).run();
}
}
/**
@@ -15,7 +15,6 @@ public final class EncodeUtil {
/* no instances */
}
/**
* URL-encodes the specified UTF-8 string.
*/
@@ -7,9 +7,10 @@ import java.io.OutputStream;
import java.io.Reader;
/**
* Created by rob on 5/02/16.
* Utilities for IO.
*/
public class IOUtils {
/**
* Reads the entire contents of the specified input stream and returns them
* as a byte array.
@@ -44,6 +45,9 @@ public class IOUtils {
return EncodeUtil.decodeBytes(read(in), encoding);
}
/**
* Read the entire contents from the reader returning as a String.
*/
public static String read(Reader reader) throws IOException {
StringBuilder sb = new StringBuilder();
@@ -23,4 +23,15 @@ public class JdbcClose {
logger.warn("Error closing connection", e);
}
}
/**
* Rollback the connection logging if an error occurs.
*/
public static void rollback(Connection connection) {
try {
connection.rollback();
} catch (SQLException e) {
logger.warn("Error on connection rollback", e);
}
}
}
@@ -0,0 +1,13 @@
create table ${table} (
id integer not null,
status varchar(10) not null,
run_version varchar(150) not null,
dep_version varchar(150) not null,
comment varchar(150),
checksum integer not null,
run_on timestamp not null,
run_by varchar(30) not null,
run_ip varchar(30),
constraint pk_${table} primary key (id)
);
@@ -1,10 +1,27 @@
package com.avaje.ebean.dbmigration;
import static org.junit.Assert.*;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.EbeanServer;
import com.avaje.ebean.config.DbMigrationConfig;
import org.junit.Test;
/**
* Created by rob on 1/04/16.
*/
public class MigrationRunnerTest {
@Test
public void test() {
EbeanServer server = Ebean.getDefaultServer();
DbMigrationConfig config = new DbMigrationConfig();
config.setMigrationPath("test-dbmigration");
config.setRunMigration(true);
config.setDbUser("sa");
config.setDbPassword("");
MigrationRunner runner = new MigrationRunner(server, config);
runner.run();
}
}
@@ -1,16 +1,87 @@
package com.avaje.ebean.dbmigration.runner;
import org.assertj.core.data.MapEntry;
import org.junit.Test;
import static org.junit.Assert.*;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Created by rob on 1/04/16.
*/
public class PlaceholderBuilderTest {
@Test
public void build() throws Exception {
@Test
public void empty() throws Exception {
assertThat(PlaceholderBuilder.build(null, null)).isEmpty();
}
@Test
@SuppressWarnings("unchecked")
public void comma() throws Exception {
assertThat(PlaceholderBuilder.build("a=1", null))
.containsExactly(MapEntry.entry("a","1"));
}
@Test
@SuppressWarnings("unchecked")
public void comma_withSpace() throws Exception {
assertThat(PlaceholderBuilder.build(" a=1 ", null))
.containsExactly(MapEntry.entry("a","1"));
}
@Test
@SuppressWarnings("unchecked")
public void comma_withSpace_withSemi() throws Exception {
assertThat(PlaceholderBuilder.build(" a=1 ; ", null))
.containsExactly(MapEntry.entry("a","1"));
}
@Test
@SuppressWarnings("unchecked")
public void commaPair() throws Exception {
assertThat(PlaceholderBuilder.build("a=1;b=2", null))
.containsExactly(MapEntry.entry("a","1"),MapEntry.entry("b","2"));
}
@Test
@SuppressWarnings("unchecked")
public void mapPair() throws Exception {
Map<String,String> in = new HashMap<String,String>();
in.put("a", "1");
in.put("b", "2");
assertThat(PlaceholderBuilder.build(null, in))
.containsExactly(MapEntry.entry("a","1"),MapEntry.entry("b","2"));
}
@Test
@SuppressWarnings("unchecked")
public void mapPair_override() throws Exception {
Map<String,String> in = new HashMap<String,String>();
in.put("a", "1");
in.put("b", "2");
assertThat(PlaceholderBuilder.build("a=11;b=12", in))
.containsExactly(MapEntry.entry("a","1"),MapEntry.entry("b","2"));
}
@Test
@SuppressWarnings("unchecked")
public void mapPair_join() throws Exception {
Map<String,String> in = new HashMap<String,String>();
in.put("c", "3");
assertThat(PlaceholderBuilder.build("a=1;b=2", in))
.containsExactly(MapEntry.entry("a","1"),MapEntry.entry("b","2"), MapEntry.entry("c","3"));
}
}
@@ -2,15 +2,31 @@ package com.avaje.ebean.dbmigration.runner;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.*;
/**
* Created by rob on 1/04/16.
*/
public class ScriptTransformTest {
@Test
public void transform() throws Exception {
Map<String,String> map = new HashMap<String,String>();
map.put("one", "PLACE1");
map.put("two", "PLACE2");
ScriptTransform transform = new ScriptTransform(map);
assertEquals(transform.transform("${one}"), "PLACE1");
assertEquals(transform.transform("${two}"), "PLACE2");
assertEquals(transform.transform("${one}${two}"), "PLACE1PLACE2");
assertEquals(transform.transform("${two}${one}"), "PLACE2PLACE1");
assertEquals(transform.transform("A${one}B${two}C"), "APLACE1BPLACE2C");
assertEquals(transform.transform(" ${one} ${two} "), " PLACE1 PLACE2 ");
assertEquals(transform.transform("$${one}"), "$PLACE1");
assertEquals(transform.transform("$${one}}"), "$PLACE1}");
}
}