Author: cbegin Date: Thu Aug 21 22:06:29 2008 New Revision: 687946 URL: http://svn.apache.org/viewvc?rev=687946&view=rev Log: Introduced migration utilities.
Added: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/CommandLine.java ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/MigrationException.java ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/MigrationReader.java ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/Migrator.java ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_changelog.sql ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_environment.properties ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_migration.sql ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/drivers/ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/migrate (with props) ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/migrate.cmd Modified: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/ScriptRunner.java Added: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/CommandLine.java URL: http://svn.apache.org/viewvc/ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/CommandLine.java?rev=687946&view=auto ============================================================================== --- ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/CommandLine.java (added) +++ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/CommandLine.java Thu Aug 21 22:06:29 2008 @@ -0,0 +1,175 @@ +package org.apache.ibatis.migration; + +import java.util.Arrays; +import java.util.Set; +import java.util.Collections; +import java.util.HashSet; +import java.io.File; +import java.io.PrintStream; +import java.math.BigInteger; + +public class CommandLine { + + private static final String PATH_PREFIX = "--path="; + private static final String ENV_PREFIX = "--env="; + private static final String FORCE = "--force"; + private static final String INIT = "init"; + private static final String NEW = "new"; + private static final String RUN = "run"; + private static final String VERSION = "version"; + private static final String UNDO = "undo"; + private static final Set<String> KNOWN_COMMANDS = Collections.unmodifiableSet( + new HashSet<String>(Arrays.asList(INIT, NEW, RUN, VERSION, UNDO))); + + private String repository; + private String environment; + private String command; + private String params; + private String parseError; + private boolean help; + private boolean force; + + public CommandLine(String[] args) { + parse(args); + validate(); + } + + public String getRepository() { + return repository; + } + + public String getEnvironment() { + return environment; + } + + public String getCommand() { + return command; + } + + public String getParams() { + return params; + } + + public boolean isHelp() { + return help; + } + + public boolean isValid() { + return parseError == null; + } + + public String toString() { + return repository + " " + environment + " " + command + " " + (params == null ? "" : params); + } + + public void execute() { + if (isHelp()) { + printUsage(); + } else if (!isValid()) { + printError(); + printUsage(); + } else { + try { + runCommand(); + } catch (MigrationException e) { + System.err.println(e.getMessage()); + printUsage(); + } + } + } + + private void runCommand() { + Migrator migrator = new Migrator(repository, environment, force); + if (INIT.equals(command)) { + migrator.initialize(); + } else if (NEW.equals(command)) { + migrator.newMigration(params); + } else if (RUN.equals(command)) { + migrator.runPendingMigrations(); + } else if (VERSION.equals(command)) { + BigInteger version = null; + try { + version = new BigInteger(params); + } catch(Exception e) { + System.err.println("Invalid version number specified: " + params); + printUsage(); + } + migrator.migrateToVersion(version); + } else if (UNDO.equals(command)) { + migrator.undoLastMigration(); + } else { + throw new RuntimeException("Attempt to execute unkown command."); + } + } + + public void printError() { + PrintStream out = System.err; + out.println(parseError); + out.flush(); + } + + public void printUsage() { + PrintStream out = System.out; + out.println(); + out.println("Usage: migrate command [parameter] [--path=<directory>] [--env=<environment>]"); + out.println(); + out.println("--path=<directory> Path to repository. Default current working directory."); + out.println("--env=<environment> Environment to configure. Default environment is 'development'."); + out.println("--env=<environment> Environment to configure. Default environment is 'development'."); + out.println("--force Forces script to continue even if SQL errors are encountered."); + out.println("--help Displays this usage message."); + out.println(); + out.println("Commands:"); + out.println(" init Creates (if necessary) and initializes a migration path."); + out.println(" new <description> Creates a new migration with the provided description."); + out.println(" run Run all unapplied migrations."); + out.println(" version <version> Migrates the database up or down to the specified version."); + out.println(" undo Undoes the last migration applied to the database."); + out.println(); + out.flush(); + } + + private void parse(String[] args) { + for (String arg : args) { + if (arg.startsWith(PATH_PREFIX) && arg.length() > PATH_PREFIX.length()) { + repository = arg.split("=")[1]; + } else if (arg.startsWith(ENV_PREFIX) && arg.length() > ENV_PREFIX.length()) { + environment = arg.split("=")[1]; + } else if (arg.startsWith("--force")) { + force = true; + } else if (arg.startsWith("--help")) { + help = true; + } else if (command == null) { + command = arg; + } else if (params == null){ + params = arg; + } else { + params += " "; + params += arg; + } + } + } + + private void validate() { + if (repository == null) { + repository = "./"; + } + if (environment == null) { + environment = "development"; + } + File f = new File(repository); + if (f.exists() && !f.isDirectory()) { + parseError = ("Migrations path must be a directory: " + f.getAbsolutePath()); + } else { + repository = f.getAbsolutePath(); + if (command == null) { + parseError = "No command specified."; + } else { + if (!KNOWN_COMMANDS.contains(command)) { + parseError = "Unknown command: " + command; + } + } + } + } + +} \ No newline at end of file Added: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/MigrationException.java URL: http://svn.apache.org/viewvc/ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/MigrationException.java?rev=687946&view=auto ============================================================================== --- ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/MigrationException.java (added) +++ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/MigrationException.java Thu Aug 21 22:06:29 2008 @@ -0,0 +1,20 @@ +package org.apache.ibatis.migration; + +public class MigrationException extends RuntimeException { + + public MigrationException() { + super(); + } + + public MigrationException(String message) { + super(message); + } + + public MigrationException(String message, Throwable cause) { + super(message, cause); + } + + public MigrationException(Throwable cause) { + super(cause); + } +} Added: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/MigrationReader.java URL: http://svn.apache.org/viewvc/ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/MigrationReader.java?rev=687946&view=auto ============================================================================== --- ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/MigrationReader.java (added) +++ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/MigrationReader.java Thu Aug 21 22:06:29 2008 @@ -0,0 +1,46 @@ +package org.apache.ibatis.migration; + +import java.io.Reader; +import java.io.IOException; +import java.io.BufferedReader; +import java.io.StringReader; + +public class MigrationReader extends Reader { + + private Reader target; + + public MigrationReader(Reader source, boolean undo) throws IOException { + try { + BufferedReader reader = new BufferedReader(source); + StringBuilder doBuilder = new StringBuilder(); + StringBuilder undoBuilder = new StringBuilder(); + StringBuilder currentBuilder = doBuilder; + String line; + while ((line = reader.readLine()) != null) { + if (line != null) { + if (line.trim().startsWith("--//@UNDO")) { + currentBuilder = undoBuilder; + } + currentBuilder.append(line); + currentBuilder.append("\n"); + } + } + if (undo) { + target = new StringReader(undoBuilder.toString()); + } else { + target = new StringReader(doBuilder.toString()); + } + } finally { + source.close(); + } + } + + public int read(char[] cbuf, int off, int len) throws IOException { + return target.read(cbuf, off, len); + } + + public void close() throws IOException { + target.close(); + } + +} Added: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/Migrator.java URL: http://svn.apache.org/viewvc/ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/Migrator.java?rev=687946&view=auto ============================================================================== --- ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/Migrator.java (added) +++ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/Migrator.java Thu Aug 21 22:06:29 2008 @@ -0,0 +1,217 @@ +package org.apache.ibatis.migration; + +import org.apache.ibatis.io.Resources; + +import java.math.BigInteger; +import java.io.*; +import java.text.SimpleDateFormat; +import java.sql.Date; +import java.util.*; + +public class Migrator { + + private File repository; + private String environment; + private boolean force; + + public Migrator(String repository, String environment, boolean force) { + this.repository = new File(repository); + this.environment = environment; + this.force = force; + } + + public static void main(String[] args) throws Exception { + new CommandLine(args).execute(); + } + + public void initialize() { + createIfNecessary(repository); + ensureEmpty(repository); + System.out.println("Initializing: " + repository); + copyResourceTo("org/apache/ibatis/migration/template_environment.properties", environmentFile(environment)); + copyResourceTo("org/apache/ibatis/migration/template_changelog.sql", repositoryFile(getTimestamp() + "_create_changelog.sql")); + copyResourceTo("org/apache/ibatis/migration/template_migration.sql", repositoryFile(getTimestamp() + "_first_migration.sql")); + System.out.println("Done!"); + } + + public void newMigration(String description) { + if (description == null) { + throw new MigrationException("No description specified for new migration."); + } + Map<String,String> variables = new HashMap<String,String>(); + variables.put("description",description); + ensureEnvironment(environment); + String filename = getTimestamp() + "_" + description.replace(' ', '_') + ".sql"; + copyResourceTo("org/apache/ibatis/migration/template_migration.sql", repositoryFile(filename), variables); + System.out.println("Done!"); + } + + public void runPendingMigrations() { + try { + String[] filenames = repository.list(); + Arrays.sort(filenames); + for(String filename : filenames) { + if (filename.endsWith(".sql")) { + System.out.println(horizontalLine("Applying: " + filename,80)); + ScriptRunner runner = getScriptRunner(); + runner.runScript(new MigrationReader(new FileReader(repositoryFile(filename)),false)); + } + } + } catch (Exception e) { + throw new RuntimeException("<Description>. Cause: " + e, e); + } + } + + public void migrateToVersion(BigInteger version) { + System.out.println("Migrating to: " + version); + } + + public void undoLastMigration() { + try { + String[] filenames = repository.list(); + Arrays.sort(filenames,new Comparator(){ + public int compare(Object o1, Object o2) { + return ((Comparable)o2).compareTo(o1); + } + }); + for(String filename : filenames) { + if (filename.endsWith(".sql")) { + System.out.println(horizontalLine("Undoing: " + filename,80)); + ScriptRunner runner = getScriptRunner(); + runner.runScript(new MigrationReader(new FileReader(repositoryFile(filename)),true)); + } + } + } catch (Exception e) { + throw new RuntimeException("<Description>. Cause: " + e, e); + } + } + + private String horizontalLine(String caption, int length) { + StringBuilder builder = new StringBuilder(); + builder.append("========== "); + builder.append(caption); + builder.append(" "); + for (int i=0; i < length - caption.length() - 2; i++) { + builder.append("="); + } + return builder.toString(); + } + + private String getTimestamp() { + try { + // Ensure that two subsequent calls are less likely to return the same value. + Thread.sleep(1000); + } catch (InterruptedException e) { + //ignore + } + return new SimpleDateFormat("yyyyMMddHHmmss").format(new Date(System.currentTimeMillis())); + } + + private File repositoryFile(String fileName) { + return new File(repository.getAbsolutePath()+File.separator+fileName); + } + + private void copyResourceTo(String resource, File toFile) { + copyResourceTo(resource,toFile,null); + } + + private void copyResourceTo(String resource, File toFile, Map<String,String> variables) { + System.out.println("Creating: " + toFile.getName()); + try { + LineNumberReader reader = new LineNumberReader(Resources.getResourceAsReader(this.getClass().getClassLoader(), resource)); + try { + PrintWriter writer = new PrintWriter(new FileWriter(toFile)); + try { + String line; + while ((line = reader.readLine()) != null) { + line = parsePlaceholders(line,variables); + writer.println(line); + } + } finally { + writer.close(); + } + } finally { + reader.close(); + } + } catch (IOException e) { + throw new MigrationException("Error copying " + resource + " to " + toFile.getAbsolutePath() + ". Cause: " + e, e); + } + } + + private void ensureEmpty(File path) { + String[] list = path.list(); + if (list.length != 0) { + for (String entry : list) { + if (!entry.startsWith(".")) { + throw new MigrationException("Directory must be empty (.svn etc allowed): " + path.getAbsolutePath()); + } + } + } + } + + private void ensureEnvironment(String environment) { + File envFile = environmentFile(environment); + if (!envFile.exists()) { + throw new MigrationException("Environment file missing: " + envFile.getAbsolutePath()); + } + } + + private File environmentFile(String environment) { + return repositoryFile(environment+".properties"); + } + + private void createIfNecessary(File path) { + if (!path.exists()) { + File parent = new File(path.getParent()); + createIfNecessary(parent); + if (!path.mkdir()) { + throw new MigrationException("Could not create directory path for an unknown reason. Make sure you have access to the directory."); + } + } + } + + private String parsePlaceholders(String string, Map<String,String> variables) { + final String OPEN = "${"; + final String CLOSE = "}"; + String newString = string; + if (newString != null && variables != null) { + int start = newString.indexOf(OPEN); + int end = newString.indexOf(CLOSE); + + while (start > -1 && end > start) { + String prepend = newString.substring(0, start); + String append = newString.substring(end + CLOSE.length()); + String propName = newString.substring(start + OPEN.length(), end); + String propValue = variables.get(propName); + if (propValue == null) { + newString = prepend + append; + } else { + newString = prepend + propValue + append; + } + start = newString.indexOf(OPEN); + end = newString.indexOf(CLOSE); + } + } + return newString; + } + + private ScriptRunner getScriptRunner() { + try { + Properties props = new Properties(); + String filename = repository + File.separator + environment + ".properties"; + File file = new File(filename); + if (!file.exists()) { + throw new MigrationException("Could not find environment properties file: " + filename); + } + props.load(new FileInputStream(file)); + String driver = props.getProperty("driver"); + String url = props.getProperty("url"); + String username = props.getProperty("username"); + String password = props.getProperty("password"); + return new ScriptRunner(driver,url,username,password,false,!force); + } catch (Exception e) { + throw new MigrationException("Error creating ScriptRunner. Cause: " + e, e); + } + } + +} Modified: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/ScriptRunner.java URL: http://svn.apache.org/viewvc/ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/ScriptRunner.java?rev=687946&r1=687945&r2=687946&view=diff ============================================================================== --- ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/ScriptRunner.java (original) +++ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/ScriptRunner.java Thu Aug 21 22:06:29 2008 @@ -69,7 +69,11 @@ try { configureAutoCommitAndRun(reader); } finally { - connection.close(); + try { + connection.close(); + } finally { + connection = null; + } } } else if (connection != null) { configureAutoCommitAndRun(reader); @@ -93,6 +97,8 @@ throw e; } catch (Exception e) { throw new RuntimeException("Error running script. Cause: " + e, e); + } finally { + flush(); } } @@ -120,19 +126,17 @@ throws IOException, SQLException { StringBuffer command = null; try { - LineNumberReader lineReader = new LineNumberReader(reader); + BufferedReader lineReader = new BufferedReader(reader); String line; while ((line = lineReader.readLine()) != null) { if (command == null) { command = new StringBuffer(); } String trimmedLine = line.trim(); - if (trimmedLine.startsWith("--")) { + if (trimmedLine.length() < 1) { + // do nothing + } else if (trimmedLine.startsWith("//") || trimmedLine.startsWith("--")) { println(trimmedLine); - } else if (trimmedLine.length() < 1 || trimmedLine.startsWith("//")) { - //Do nothing - } else if (trimmedLine.length() < 1 || trimmedLine.startsWith("--")) { - //Do nothing } else if (!fullLineDelimiter && trimmedLine.endsWith(getDelimiter()) || fullLineDelimiter && trimmedLine.equals(getDelimiter())) { command.append(line.substring(0, line.lastIndexOf(getDelimiter()))); @@ -158,21 +162,23 @@ conn.commit(); } - ResultSet rs = statement.getResultSet(); - if (hasResults && rs != null) { - ResultSetMetaData md = rs.getMetaData(); - int cols = md.getColumnCount(); - for (int i = 0; i < cols; i++) { - String name = md.getColumnLabel(i + 1); - print(name + "\t"); - } - println(""); - while (rs.next()) { + if (hasResults) { + ResultSet rs = statement.getResultSet(); + if (rs != null) { + ResultSetMetaData md = rs.getMetaData(); + int cols = md.getColumnCount(); for (int i = 0; i < cols; i++) { - String value = rs.getString(i + 1); - print(value + "\t"); + String name = md.getColumnLabel(i + 1); + print(name + "\t"); } println(""); + while (rs.next()) { + for (int i = 0; i < cols; i++) { + String value = rs.getString(i + 1); + print(value + "\t"); + } + println(""); + } } } @@ -213,7 +219,7 @@ private void print(Object o) { if (logWriter != null) { - System.out.print(o); + logWriter.print(o); } } Added: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_changelog.sql URL: http://svn.apache.org/viewvc/ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_changelog.sql?rev=687946&view=auto ============================================================================== --- ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_changelog.sql (added) +++ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_changelog.sql Thu Aug 21 22:06:29 2008 @@ -0,0 +1,23 @@ +--// Create Changelog + +-- Default DDL for changelog table that will keep +-- a record of the migrations that have been run. + +-- You can modify this to suit your database before +-- running your first migration. + +-- Be sure that ID and DESCRIPTION fields exist in +-- BigInteger and String compatible fields respectively. + +CREATE TABLE CHANGELOG ( + ID NUMERIC(20,0) NOT NULL, + DESCRIPTION VARCHAR(255) NOT NULL +); + +ALTER TABLE CHANGELOG + ADD CONSTRAINT PK_CHANGELOG + PRIMARY KEY (id); + +--//@UNDO + +DROP TABLE CHANGELOG; \ No newline at end of file Added: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_environment.properties URL: http://svn.apache.org/viewvc/ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_environment.properties?rev=687946&view=auto ============================================================================== --- ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_environment.properties (added) +++ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_environment.properties Thu Aug 21 22:06:29 2008 @@ -0,0 +1,15 @@ +## JDBC connection properties. +driver= +url= +username= +password= + +## The table containing the changelog. Defaults to 'changelog' +#changelog.table= + +## The schema the changelog can be found in. Default is null. +#changelog.schema= + +## The catalog the changelog can be found in. Default is null. +#changelog.catalog= + Added: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_migration.sql URL: http://svn.apache.org/viewvc/ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_migration.sql?rev=687946&view=auto ============================================================================== --- ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_migration.sql (added) +++ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/java/org/apache/ibatis/migration/template_migration.sql Thu Aug 21 22:06:29 2008 @@ -0,0 +1,9 @@ +--// ${description} +-- Migration SQL that makes the change goes here. + + + +--//@UNDO +-- SQL to undo the change goes here. + + Added: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/migrate URL: http://svn.apache.org/viewvc/ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/migrate?rev=687946&view=auto ============================================================================== --- ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/migrate (added) +++ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/migrate Thu Aug 21 22:06:29 2008 @@ -0,0 +1,9 @@ +#!/bin/sh + +IBATIS_LIB=/home/clinton/Development/ibatis-3/ibatis-3-core/target/classes +CURDIR=$(dirname $0) +JARS=$CURDIR/drivers/ +MYCP=$(ls $JARS/ | sed "s#^#$JARS&#" | sed "s/$/:/" | xargs echo | sed 's/: */:/') +MYCP="$MYCP:$IBATIS_LIB" + +java -cp $MYCP org.apache.ibatis.migration.Migrator $* Propchange: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/migrate ------------------------------------------------------------------------------ svn:executable = * Added: ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/migrate.cmd URL: http://svn.apache.org/viewvc/ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/migrate.cmd?rev=687946&view=auto ============================================================================== --- ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/migrate.cmd (added) +++ ibatis/trunk/java/ibatis-3/ibatis-3-core/src/main/scripts/migrate.cmd Thu Aug 21 22:06:29 2008 @@ -0,0 +1,5 @@ [EMAIL PROTECTED] off + +set IBATIS_LIB=/home/clinton/Development/ibatis-3/ibatis-3-core/target/classes + +java -cp %IBATIS_LIB% org.apache.ibatis.migration.Migrator %*