diff --git a/.gitignore b/.gitignore index 995ad168..4a8757b5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ ibderby atlassian-ide-plugin.xml .classpath .project -.settings \ No newline at end of file +.settings diff --git a/README.md b/README.md index d0d25e2c..d4960a7e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Install MyBatis Migrations ${project.version} (${implementation.build}) See the reference documentation here http://mybatis.github.io/migrations/ -* Windows +### Windows [1] Unzip the distribution archive, i.e. mybatis-${project.version}-migrations.zip to the directory you wish to install MyBatis Migrations. @@ -33,7 +33,7 @@ Install MyBatis Migrations ${project.version} (${implementation.build}) [4] In the same dialog, update/create the Path environment variable in the user variables and prepend the value %MIGRATIONS% to add MyBatis Migrations available in the command line. -* Unix-based Operating Systems (Linux, Solaris and Mac OS X) +### Unix-based Operating Systems (Linux, Solaris and Mac OS X) [1] Extract the distribution archive, i.e. mybatis-${project.version}-migrations.zip to the directory you wish to install MyBatis Migrations. These instructions assume you chose diff --git a/src/main/java/org/apache/ibatis/migration/Change.java b/src/main/java/org/apache/ibatis/migration/Change.java index 4025cb1b..09832aca 100644 --- a/src/main/java/org/apache/ibatis/migration/Change.java +++ b/src/main/java/org/apache/ibatis/migration/Change.java @@ -70,7 +70,9 @@ public void setFilename(String filename) { } public String toString() { - return id + " " + (appliedTimestamp == null ? " ...pending... " : appliedTimestamp) + " " + description; + String status = appliedTimestamp == null ? " ...pending... " : appliedTimestamp; + String message = filename == null ? " ...missing script... " : " "; + return id + " " + status + message + description; } public boolean equals(Object o) { @@ -83,14 +85,16 @@ public boolean equals(Object o) { Change change = (Change) o; - return (id.equals(change.getId())); + return (id.equals(change.getId())) && (description == null ? change.getDescription() == null : description.equals(change.getDescription())); } public int hashCode() { - return id.hashCode(); + return id.hashCode() ^ (description == null ? 0 : description.hashCode()); } public int compareTo(Change change) { - return id.compareTo(change.getId()); + return id.compareTo(change.getId()) != 0 ? id.compareTo(change.getId()) + : (description == null ? (change.getDescription() == null ? 0 : 1) + : description.compareTo(change.getDescription())); } } diff --git a/src/main/java/org/apache/ibatis/migration/ChangeValidator.java b/src/main/java/org/apache/ibatis/migration/ChangeValidator.java new file mode 100644 index 00000000..2f1bbb2c --- /dev/null +++ b/src/main/java/org/apache/ibatis/migration/ChangeValidator.java @@ -0,0 +1,57 @@ +package org.apache.ibatis.migration; + +import java.math.BigDecimal; +import java.util.Properties; +import java.util.regex.Pattern; + +public class ChangeValidator { + private static final String CUSTOM_FILE_NAME_FILTER_PROPERTY = "filename_filter"; + + /** + * @param change to validate + * @param properties Environmental configuration + * @throws MigrationException + */ + public static void validateChangeForConfiguration(Change change, Properties properties) + throws MigrationException { + String filename = change.getFilename(); + String filenameFilter = properties.getProperty(CUSTOM_FILE_NAME_FILTER_PROPERTY); + if (filenameFilter != null) { + Pattern p = null; + try { + p = Pattern.compile(filenameFilter, Pattern.CASE_INSENSITIVE); + } catch (Exception ex) { + throw new MigrationException("Exception parsing the value in your environmental configuration for the filename filter of " + filenameFilter, ex); + } + if (!p.matcher(filename).find()) { + throw new MigrationException("The change filename " + filename + " does not match the required filename filter of " + filenameFilter); + } + } + } + + /** + * @param filename + * @param properties Environmental configuration + * @return Change + */ + public static Change parseChangeFromFilename(String filename, Properties properties) { + try { + Change change = new Change(); + int lastIndexOfDot = filename.lastIndexOf("."); + String[] parts = filename.substring(0, lastIndexOfDot).split("_"); + change.setId(new BigDecimal(parts[0])); + StringBuilder builder = new StringBuilder(); + for (int i = 1; i < parts.length; i++) { + if (i > 1) { + builder.append(" "); + } + builder.append(parts[i]); + } + change.setDescription(builder.toString()); + change.setFilename(filename); + return change; + } catch (Exception e) { + throw new MigrationException("Error parsing change from filename. Cause: " + e, e); + } + } +} diff --git a/src/main/java/org/apache/ibatis/migration/CommandLine.java b/src/main/java/org/apache/ibatis/migration/CommandLine.java index e5d35462..53cd69c8 100644 --- a/src/main/java/org/apache/ibatis/migration/CommandLine.java +++ b/src/main/java/org/apache/ibatis/migration/CommandLine.java @@ -122,7 +122,7 @@ private void printUsage() { console.printf(" info Display build version informations.%n"); console.printf(" init Creates (if necessary) and initializes a migration path.%n"); console.printf(" bootstrap Runs the bootstrap SQL script (see scripts/bootstrap.sql for more).%n"); - console.printf(" new Creates a new migration with the provided description.%n"); + console.printf(" new [n] Creates a new migration with the provided description and 'n' version.%n"); console.printf(" up [n] Run unapplied migrations, ALL by default, or 'n' specified.%n"); console.printf( " down [n] Undoes migrations applied to the database. ONE by default or 'n' specified.%n"); diff --git a/src/main/java/org/apache/ibatis/migration/FileMigrationLoader.java b/src/main/java/org/apache/ibatis/migration/FileMigrationLoader.java index db238065..83cf30b4 100644 --- a/src/main/java/org/apache/ibatis/migration/FileMigrationLoader.java +++ b/src/main/java/org/apache/ibatis/migration/FileMigrationLoader.java @@ -18,7 +18,6 @@ import java.io.File; import java.io.IOException; import java.io.Reader; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -27,6 +26,7 @@ import org.apache.ibatis.migration.utils.Util; public class FileMigrationLoader implements MigrationLoader { + private final File scriptsDir; private final String charset; @@ -51,7 +51,8 @@ public List getMigrations() { Arrays.sort(filenames); for (String filename : filenames) { if (filename.endsWith(".sql") && !"bootstrap.sql".equals(filename)) { - Change change = parseChangeFromFilename(filename); + Change change = ChangeValidator.parseChangeFromFilename(filename, properties); + ChangeValidator.validateChangeForConfiguration (change, properties); migrations.add(change); } } @@ -59,27 +60,6 @@ public List getMigrations() { return migrations; } - private Change parseChangeFromFilename(String filename) { - try { - Change change = new Change(); - int lastIndexOfDot = filename.lastIndexOf("."); - String[] parts = filename.substring(0, lastIndexOfDot).split("_"); - change.setId(new BigDecimal(parts[0])); - StringBuilder builder = new StringBuilder(); - for (int i = 1; i < parts.length; i++) { - if (i > 1) { - builder.append(" "); - } - builder.append(parts[i]); - } - change.setDescription(builder.toString()); - change.setFilename(filename); - return change; - } catch (Exception e) { - throw new MigrationException("Error parsing change from file. Cause: " + e, e); - } - } - @Override public Reader getScriptReader(Change change, boolean undo) { try { @@ -101,4 +81,5 @@ public Reader getBootstrapReader() { throw new MigrationException("Error reading bootstrap.sql", e); } } + } diff --git a/src/main/java/org/apache/ibatis/migration/MigrationReader.java b/src/main/java/org/apache/ibatis/migration/MigrationReader.java index e1e97aff..1273839f 100644 --- a/src/main/java/org/apache/ibatis/migration/MigrationReader.java +++ b/src/main/java/org/apache/ibatis/migration/MigrationReader.java @@ -33,6 +33,8 @@ import java.util.Set; public class MigrationReader extends Reader { + // FEFF because this is the Unicode char represented by the UTF-8 byte order mark (EF BB BF). + public static final String UTF8_BOM = "\uFEFF"; private static final String LINE_SEPARATOR = System.getProperty("line.separator", "\n"); @@ -51,7 +53,15 @@ public MigrationReader(InputStream inputStream, String charset, boolean undo, Pr StringBuilder undoBuilder = new StringBuilder(); StringBuilder currentBuilder = doBuilder; String line; + boolean firstLine = true; + while ((line = reader.readLine()) != null) { + if (firstLine) { + if ("UTF-8".equalsIgnoreCase(charset) && line.startsWith(UTF8_BOM)) { + line = line.substring(1); + } + firstLine = false; + } if (line.trim().matches("^--\\s*//.*$")) { if (line.contains("@UNDO")) { currentBuilder = undoBuilder; diff --git a/src/main/java/org/apache/ibatis/migration/commands/BaseCommand.java b/src/main/java/org/apache/ibatis/migration/commands/BaseCommand.java index 1b81e5f6..b61df142 100644 --- a/src/main/java/org/apache/ibatis/migration/commands/BaseCommand.java +++ b/src/main/java/org/apache/ibatis/migration/commands/BaseCommand.java @@ -15,39 +15,28 @@ */ package org.apache.ibatis.migration.commands; -import static org.apache.ibatis.migration.utils.Util.*; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.LineNumberReader; -import java.io.PrintStream; -import java.io.PrintWriter; -import java.net.URL; -import java.net.URLClassLoader; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Properties; -import java.util.TimeZone; - import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; import org.apache.ibatis.io.ExternalResources; import org.apache.ibatis.io.Resources; -import org.apache.ibatis.migration.ConnectionProvider; -import org.apache.ibatis.migration.DataSourceConnectionProvider; -import org.apache.ibatis.migration.FileMigrationLoader; -import org.apache.ibatis.migration.MigrationException; -import org.apache.ibatis.migration.MigrationLoader; +import org.apache.ibatis.migration.*; import org.apache.ibatis.migration.options.DatabaseOperationOption; import org.apache.ibatis.migration.options.SelectedOptions; import org.apache.ibatis.migration.options.SelectedPaths; import org.apache.ibatis.parsing.PropertyParser; +import java.io.*; +import java.net.URL; +import java.net.URLClassLoader; +import java.text.SimpleDateFormat; +import java.util.*; + +import static org.apache.ibatis.migration.utils.Util.file; + public abstract class BaseCommand implements Command { private static final String DATE_FORMAT = "yyyyMMddHHmmss"; + private static final String FILENAME_SEQUENCE_NUMBER_PADDING = "sequence_number_padding"; + + private Properties envProperties; private ClassLoader driverClassLoader; @@ -83,6 +72,48 @@ protected String changelogTable() { } protected String getNextIDAsString() { + if (getDatabaseOperationOption().useSequenceNumber()) { + return getNextSequenceNumberAsIdString(); + } + + return getNextTimestampIDAsString(); + } + + protected String getNextSequenceNumberAsIdString() { + // if script directory is empty, use the initial sequence + if (paths.getScriptPath().list().length==0) { + return getDatabaseOperationOption().getInitialSequence().toString(); + } + + // else, initialise with the largest seq number from scripts dir and increment by 1 + File[] sqlFiles = paths.getScriptPath().listFiles(); + + Arrays.sort( + sqlFiles, + new Comparator() { + public int compare(File a, File b) { + return sequenceNumberOfFile(a.getName()).compareTo(sequenceNumberOfFile(b.getName())); + } + }); + + File lastFile = sqlFiles[sqlFiles.length - 1]; + Integer nextSeqNumber = sequenceNumberOfFile(lastFile.getName()) + 1; + Integer zerosToPadd = Integer.valueOf(environmentProperties().getProperty(FILENAME_SEQUENCE_NUMBER_PADDING, "1")); + + return String.format("%0" + zerosToPadd + "d", nextSeqNumber); + } + + private Integer sequenceNumberOfFile(String fileName) { + try { + return Integer.valueOf(fileName.substring(0, fileName.indexOf("_"))); + } catch (StringIndexOutOfBoundsException e) { + } catch (NumberFormatException e) {} + + // File does not have any numbers. Will ignore this and make this the same as the initial number + return getDatabaseOperationOption().getInitialSequence() - 1; + } + + protected String getNextTimestampIDAsString() { try { // Ensure that two subsequent calls are less likely to return the same value. Thread.sleep(1000); @@ -149,24 +180,27 @@ protected File existingEnvironmentFile() { } protected Properties environmentProperties() { - FileInputStream fileInputStream = null; - try { - File file = existingEnvironmentFile(); - Properties props = new Properties(); - fileInputStream = new FileInputStream(file); - props.load(fileInputStream); - return props; - } catch (IOException e) { - throw new MigrationException("Error loading environment properties. Cause: " + e, e); - } finally { - if (fileInputStream != null) { - try { - fileInputStream.close(); - } catch (IOException e) { - // Nothing to do here + if (envProperties == null) { + FileInputStream fileInputStream = null; + try { + File file = existingEnvironmentFile(); + Properties props = new Properties(); + fileInputStream = new FileInputStream(file); + props.load(fileInputStream); + envProperties = props; + } catch (IOException e) { + throw new MigrationException("Error loading environment properties. Cause: " + e, e); + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) { + // Nothing to do here + } } } } + return envProperties; } protected int getStepCountParameter(int defaultSteps, String... params) { @@ -246,6 +280,10 @@ protected DatabaseOperationOption getDatabaseOperationOption() { option.setRemoveCRs(Boolean.valueOf(props.getProperty("remove_crs"))); String delimiterString = props.getProperty("delimiter"); option.setDelimiter(delimiterString == null ? ";" : delimiterString); + option.setUseSequenceNumber(Boolean.valueOf(props.getProperty("use_sequence_number"))); + if (option.useSequenceNumber()) { + option.setInitialSequence(Integer.valueOf(props.getProperty("initial_sequence_number"))); + } return option; } } diff --git a/src/main/java/org/apache/ibatis/migration/commands/NewCommand.java b/src/main/java/org/apache/ibatis/migration/commands/NewCommand.java index 6151695d..543634bf 100644 --- a/src/main/java/org/apache/ibatis/migration/commands/NewCommand.java +++ b/src/main/java/org/apache/ibatis/migration/commands/NewCommand.java @@ -16,18 +16,22 @@ package org.apache.ibatis.migration.commands; import org.apache.ibatis.io.ExternalResources; +import org.apache.ibatis.migration.Change; +import org.apache.ibatis.migration.ChangeValidator; import org.apache.ibatis.migration.MigrationException; import org.apache.ibatis.migration.options.SelectedOptions; import org.apache.ibatis.migration.utils.Util; import java.io.FileNotFoundException; import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public final class NewCommand extends BaseCommand { private static final String MIGRATIONS_HOME = "MIGRATIONS_HOME"; private static final String MIGRATIONS_HOME_PROPERTY = "migrationHome"; - private static final String CUSTOM_NEW_COMMAND_TEMPATE_PROPERTY = "new_command.template"; + private static final String CUSTOM_NEW_COMMAND_TEMPLATE_PROPERTY = "new_command.template"; private static final String MIGRATIONS_PROPERTIES = "migration.properties"; public NewCommand(SelectedOptions options) { @@ -39,13 +43,24 @@ public void execute(String... params) { throw new MigrationException("No description specified for new migration."); } String description = params[0]; + String changeIdOverride = null; + + Pattern p = Pattern.compile("(.+) (\\d+$)"); + Matcher m = p.matcher(description); + if (m.find()) { + changeIdOverride = m.group(2); + description = m.group(1); + } + Properties variables = new Properties(); variables.setProperty("description", description); - existingEnvironmentFile(); - String filename = getNextIDAsString() + "_" + description.replace(' ', '_') + ".sql"; + String filename = (changeIdOverride == null ? getNextIDAsString() : changeIdOverride) + "_" + description.replace(' ', '_') + ".sql"; String migrationsHome = ""; migrationsHome = System.getenv(MIGRATIONS_HOME); + Change change = ChangeValidator.parseChangeFromFilename(filename, environmentProperties()); + ChangeValidator.validateChangeForConfiguration (change, environmentProperties()); + // Check if there is a system property if (migrationsHome == null) { migrationsHome = System.getProperty(MIGRATIONS_HOME_PROPERTY); @@ -58,7 +73,7 @@ public void execute(String... params) { //get template name from properties file final String customConfiguredTemplate = ExternalResources.getConfiguredTemplate(migrationsHome + "/" + MIGRATIONS_PROPERTIES, - CUSTOM_NEW_COMMAND_TEMPATE_PROPERTY); + CUSTOM_NEW_COMMAND_TEMPLATE_PROPERTY); copyExternalResourceTo(migrationsHome + "/" + customConfiguredTemplate, Util.file(paths.getScriptPath(), filename)); } catch (FileNotFoundException e) { @@ -74,6 +89,7 @@ public void execute(String... params) { printStream.println(); } + private void copyDefaultTemplate(Properties variables, String filename) { copyResourceTo("org/apache/ibatis/migration/template_migration.sql", Util.file(paths.getScriptPath(), filename), diff --git a/src/main/java/org/apache/ibatis/migration/operations/StatusOperation.java b/src/main/java/org/apache/ibatis/migration/operations/StatusOperation.java index bc2409dc..47ade5b9 100644 --- a/src/main/java/org/apache/ibatis/migration/operations/StatusOperation.java +++ b/src/main/java/org/apache/ibatis/migration/operations/StatusOperation.java @@ -28,6 +28,8 @@ public final class StatusOperation extends DatabaseOperation { private int applied; + private int missingScript; + private int pending; private List changes; @@ -39,22 +41,11 @@ public StatusOperation operate(ConnectionProvider connectionProvider, MigrationL } println(printStream, "ID Applied At Description"); println(printStream, horizontalLine("", 80)); - changes = new ArrayList(); List migrations = migrationsLoader.getMigrations(); if (changelogExists(connectionProvider, option)) { - List changelog = getChangelog(connectionProvider, option); - for (Change change : migrations) { - int index = changelog.indexOf(change); - if (index > -1) { - changes.add(changelog.get(index)); - applied++; - } else { - changes.add(change); - pending++; - } - } + changes = mergeWithChangelog(migrations, getChangelog(connectionProvider, option)); } else { - changes.addAll(migrations); + changes = new ArrayList(migrations); pending = migrations.size(); } Collections.sort(changes); @@ -64,11 +55,39 @@ public StatusOperation operate(ConnectionProvider connectionProvider, MigrationL println(printStream); return this; } + + private List mergeWithChangelog(List migrations, List changelog) { + List merged = new ArrayList(); + for (Change migration : migrations) { + int index = changelog.indexOf(migration); + if (index > -1) { + Change change = changelog.get(index); + change.setFilename(migration.getFilename()); + merged.add(change); + applied++; + } else { + merged.add(migration); + pending++; + } + } + for (Change change : changelog) { + if (migrations.indexOf(change) < 0) { + missingScript++; + applied++; + merged.add(change); + } + } + return merged; + } public int getAppliedCount() { return applied; } + public int getMissingScriptCount() { + return missingScript; + } + public int getPendingCount() { return pending; } diff --git a/src/main/java/org/apache/ibatis/migration/operations/UpOperation.java b/src/main/java/org/apache/ibatis/migration/operations/UpOperation.java index 9a9397d6..8d629dd7 100644 --- a/src/main/java/org/apache/ibatis/migration/operations/UpOperation.java +++ b/src/main/java/org/apache/ibatis/migration/operations/UpOperation.java @@ -58,6 +58,11 @@ public UpOperation operate(ConnectionProvider connectionProvider, MigrationLoade Collections.sort(migrations); int stepCount = 0; for (Change change : migrations) { + if (lastChange != null && change.getId().compareTo(lastChange.getId()) == 0 && !change.equals(lastChange)) { + String errorMessage = String.format("Version conflict between changes [%s] and [%s], aborting and skipping: %s", lastChange.getId(), change.getId(), change.getFilename()); + println(printStream, horizontalLine(errorMessage, 80)); + throw new MigrationException(errorMessage); + } if (lastChange == null || change.getId().compareTo(lastChange.getId()) > 0) { println(printStream, horizontalLine("Applying: " + change.getFilename(), 80)); ScriptRunner runner = getScriptRunner(connectionProvider, option, printStream); @@ -67,6 +72,7 @@ public UpOperation operate(ConnectionProvider connectionProvider, MigrationLoade runner.closeConnection(); } insertChangelog(change, connectionProvider, option); + lastChange = change; println(printStream); stepCount++; if (steps != null && stepCount >= steps) { diff --git a/src/main/java/org/apache/ibatis/migration/operations/VersionOperation.java b/src/main/java/org/apache/ibatis/migration/operations/VersionOperation.java index 19d17f3f..583339ed 100644 --- a/src/main/java/org/apache/ibatis/migration/operations/VersionOperation.java +++ b/src/main/java/org/apache/ibatis/migration/operations/VersionOperation.java @@ -66,8 +66,10 @@ public VersionOperation operate(ConnectionProvider connectionProvider, Migration private void ensureVersionExists(MigrationLoader migrationsLoader) { List migrations = migrationsLoader.getMigrations(); - if (!migrations.contains(new Change(version))) { - throw new MigrationException("A migration for the specified version number does not exist."); - } + for (Change change : migrations) + if (change.getId().compareTo(version) == 0) + return; + + throw new MigrationException("A migration for the specified version number does not exist."); } } diff --git a/src/main/java/org/apache/ibatis/migration/options/DatabaseOperationOption.java b/src/main/java/org/apache/ibatis/migration/options/DatabaseOperationOption.java index e7efaccf..48d75454 100644 --- a/src/main/java/org/apache/ibatis/migration/options/DatabaseOperationOption.java +++ b/src/main/java/org/apache/ibatis/migration/options/DatabaseOperationOption.java @@ -36,6 +36,10 @@ public class DatabaseOperationOption { private String delimiter; + private boolean useSequenceNumber = false; + + private Integer initialSequence; + public String getChangelogTable() { return changelogTable == null ? DEFAULT_CHANGELOG_TABLE : changelogTable; } @@ -99,4 +103,20 @@ public String getDelimiter() { public void setDelimiter(String delimiter) { this.delimiter = delimiter; } + + public void setUseSequenceNumber(boolean useSequenceNumber) { + this.useSequenceNumber = useSequenceNumber; + } + + public boolean useSequenceNumber() { + return useSequenceNumber; + } + + public void setInitialSequence(Integer initialSequence) { + this.initialSequence = initialSequence; + } + + public Integer getInitialSequence() { + return initialSequence; + } } diff --git a/src/main/java/org/apache/ibatis/migration/options/SelectedOptions.java b/src/main/java/org/apache/ibatis/migration/options/SelectedOptions.java index 41cf2f5d..7cbebeeb 100644 --- a/src/main/java/org/apache/ibatis/migration/options/SelectedOptions.java +++ b/src/main/java/org/apache/ibatis/migration/options/SelectedOptions.java @@ -29,6 +29,10 @@ public SelectedPaths getPaths() { return paths; } + public void setPaths(SelectedPaths paths) { + this.paths = paths; + } + public String getEnvironment() { return environment; } diff --git a/src/main/java/org/apache/ibatis/migration/template_environment.properties b/src/main/java/org/apache/ibatis/migration/template_environment.properties index dd1d7617..333f08f5 100644 --- a/src/main/java/org/apache/ibatis/migration/template_environment.properties +++ b/src/main/java/org/apache/ibatis/migration/template_environment.properties @@ -1,6 +1,16 @@ ## Base time zone to ensure times are consistent across machines time_zone=GMT+0:00 +# Use number sequence instead of timezone +# If set to true, it will start with initial_sequence_number. Each new command will increment sequence by 1 +#use_sequence_number=true +#initial_sequence_number=1 +#sequence_number_padding=3 + +## Change file name filter +# A regular expression which all change files must match to be applied +#filename_filter=APP-\\d+ + ## The character set that scripts are encoded with # script_char_set=UTF-8 @@ -61,4 +71,3 @@ changelog=CHANGELOG # with the exception of changelog. # Example: The following would be referenced in a migration file as ${ip_address} # ip_address=192.168.0.1 - diff --git a/src/test/java/org/apache/ibatis/migration/MigratorTest.java b/src/test/java/org/apache/ibatis/migration/MigratorTest.java index 8b7b83c0..b5c5898f 100644 --- a/src/test/java/org/apache/ibatis/migration/MigratorTest.java +++ b/src/test/java/org/apache/ibatis/migration/MigratorTest.java @@ -50,22 +50,7 @@ public static void setup() throws Exception { buffer = new StringOutputStream(); System.setOut(new PrintStream(buffer)); - DataSource ds = createUnpooledDataSource(BLOG_PROPERTIES); - Connection conn = ds.getConnection(); - SqlRunner executor = new SqlRunner(conn); - safeRun(executor, "DROP TABLE bootstrap"); - safeRun(executor, "DROP TABLE comment"); - safeRun(executor, "DROP TABLE post_tag"); - safeRun(executor, "DROP TABLE tag"); - safeRun(executor, "DROP TABLE post"); - safeRun(executor, "DROP TABLE blog"); - safeRun(executor, "DROP TABLE author"); - safeRun(executor, "DROP PROCEDURE selectTwoSetsOfAuthors"); - safeRun(executor, "DROP PROCEDURE insertAuthor"); - safeRun(executor, "DROP PROCEDURE selectAuthorViaOutParams"); - safeRun(executor, "DROP TABLE changelog"); - conn.commit(); - conn.close(); + ensureCleanTestDB(); System.setSecurityManager(new SecurityManager() { @@ -85,9 +70,11 @@ public void checkExit(int status) { } @AfterClass - public static void teardown() { + public static void teardown() throws Exception { System.setOut(out); System.setSecurityManager(null); + + ensureCleanTestDB(); } @Test @@ -385,5 +372,22 @@ public static UnpooledDataSource createUnpooledDataSource(String resource) throw return ds; } - + public static void ensureCleanTestDB() throws Exception { + DataSource ds = createUnpooledDataSource(BLOG_PROPERTIES); + Connection conn = ds.getConnection(); + SqlRunner executor = new SqlRunner(conn); + safeRun(executor, "DROP TABLE bootstrap"); + safeRun(executor, "DROP TABLE comment"); + safeRun(executor, "DROP TABLE post_tag"); + safeRun(executor, "DROP TABLE tag"); + safeRun(executor, "DROP TABLE post"); + safeRun(executor, "DROP TABLE blog"); + safeRun(executor, "DROP TABLE author"); + safeRun(executor, "DROP PROCEDURE selectTwoSetsOfAuthors"); + safeRun(executor, "DROP PROCEDURE insertAuthor"); + safeRun(executor, "DROP PROCEDURE selectAuthorViaOutParams"); + safeRun(executor, "DROP TABLE changelog"); + conn.commit(); + conn.close(); + } } diff --git a/src/test/java/org/apache/ibatis/migration/commands/ChangeVersionConflictsTest.java b/src/test/java/org/apache/ibatis/migration/commands/ChangeVersionConflictsTest.java new file mode 100644 index 00000000..34f7c553 --- /dev/null +++ b/src/test/java/org/apache/ibatis/migration/commands/ChangeVersionConflictsTest.java @@ -0,0 +1,238 @@ +/** + * Copyright 2010-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.migration.commands; + +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.jdbc.SqlRunner; +import org.apache.ibatis.migration.MigrationException; +import org.apache.ibatis.migration.options.SelectedOptions; +import org.apache.ibatis.migration.options.SelectedPaths; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.sql.DataSource; + +import static org.junit.Assert.*; + +public class ChangeVersionConflictsTest { + private SelectedOptions newSelectedOption; + private SelectedPaths selectedPaths; + + public static final String BLOG_PROPERTIES = "databases/blog/blog-derby.properties"; + + private static PrintStream out; + private static StringOutputStream buffer; + + @Before + public void setup() throws Exception { + selectedPaths = new SelectedPaths(); + selectedPaths.setBasePath(new File("src/test/java/org/apache/ibatis/migration/commands")); + + newSelectedOption = new SelectedOptions(); + newSelectedOption.setCommand("New"); + newSelectedOption.setPaths(selectedPaths); + + out = System.out; + buffer = new StringOutputStream(); + System.setOut(new PrintStream(buffer)); + ensureCleanTestDB(); + setupTestDB(); + } + + @After + public void teardown() throws Exception { + System.setOut(out); + ensureCleanTestDB(); + } + + @Test + public void sequenceNumberOverrideTest() { + newSelectedOption.setEnvironment("development_useSeqNum"); + + NewCommand newCommand = new NewCommand(newSelectedOption); + newCommand.execute("should start with one"); + newCommand.execute("should start with two 1"); + + File[] files = selectedPaths.getScriptPath().listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.indexOf("1")==0; + } + }); + + assertTrue(files.length > 0); + files[0].delete(); + assertTrue(files.length > 1); + files[1].delete(); + } + + @Test + public void testUpCommandDisplaysVersionConflict() { + newSelectedOption.setEnvironment("development_useSeqNum"); + + + buffer.clear(); + StatusCommand statusCommand = new StatusCommand(newSelectedOption); + statusCommand.execute(); + assertFalse(buffer.toString().contains("...pending...")); + + //Add Duplcate + NewCommand newCommand = new NewCommand(newSelectedOption); + newCommand.execute("should start with one first 1"); + newCommand.execute("should start with two first 2"); + newCommand.execute("should start with two duplicate 2"); + + try { + buffer.clear(); + //Migrate up to just before the conflict + UpCommand upCommand = new UpCommand(newSelectedOption); + upCommand.execute("2"); + upCommand = new UpCommand(newSelectedOption); + upCommand.execute(); + } catch (MigrationException mx) { + assertTrue(mx.getMessage().contains("Version conflict between changes [2] and [2]")); + assertTrue(buffer.toString().contains("Version conflict between changes [2] and [2]")); + + buffer.clear(); + statusCommand = new StatusCommand(newSelectedOption); + statusCommand.execute(); + assertTrue(buffer.toString().contains("should start with one first")); + assertTrue(buffer.toString().contains("should start with two first")); + assertTrue(buffer.toString().contains("should start with two duplicate")); + assertTrue(buffer.toString().contains("...pending...")); + + try { + buffer.clear(); + //Migrate down to test a conflict mid migration + DownCommand downCommand = new DownCommand(newSelectedOption); + downCommand.execute("2"); + UpCommand upCommand = new UpCommand(newSelectedOption); + upCommand.execute(); + } catch (MigrationException mx2) { + assertTrue(mx2.getMessage().contains("Version conflict between changes [2] and [2]")); + assertTrue(buffer.toString().contains("Version conflict between changes [2] and [2]")); + + buffer.clear(); + statusCommand = new StatusCommand(newSelectedOption); + statusCommand.execute(); + assertTrue(buffer.toString().contains("should start with one first")); + assertTrue(buffer.toString().contains("should start with two first")); + assertTrue(buffer.toString().contains("should start with two duplicate")); + assertTrue(buffer.toString().contains("...pending...")); + } + } finally { + //Cleanup + File[] files = selectedPaths.getScriptPath().listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.indexOf("2")==0 || name.indexOf("1")==0; + } + }); + + assertTrue(files.length > 0); + files[0].delete(); + assertTrue(files.length > 1); + files[1].delete(); + assertTrue(files.length > 2); + files[2].delete(); + } + } + + private static class StringOutputStream extends OutputStream { + + private StringBuilder builder = new StringBuilder(); + + public void write(int b) throws IOException { + builder.append((char) b); +// out.write(b); + } + + @Override + public String toString() { + return builder.toString(); + } + + public void clear() { + builder.setLength(0); + } + } + + /** + * @throws IOException + * @throws SQLException + */ + public static void ensureCleanTestDB() throws IOException, SQLException { + DataSource ds = createUnpooledDataSource(BLOG_PROPERTIES); + Connection conn = ds.getConnection(); + SqlRunner executor = new SqlRunner(conn); + safeRun(executor, "DROP TABLE changelog"); + conn.commit(); + conn.close(); + } + /** + * @throws IOException + * @throws SQLException + */ + public static void setupTestDB() throws Exception { + DataSource ds = createUnpooledDataSource(BLOG_PROPERTIES); + Connection conn = ds.getConnection(); + SqlRunner executor = new SqlRunner(conn); + safeRun(executor, "CREATE TABLE changelog (" + + "ID NUMERIC(20,0) NOT NULL, " + + "APPLIED_AT VARCHAR(25) NOT NULL, " + + "DESCRIPTION VARCHAR(255) NOT NULL" + + ")"); + conn.commit(); + try { + final List> change = executor.selectAll("select * from changelog "); + assertTrue(change.size() == 0); + } catch (Exception ex) { + throw ex; + } + conn.close(); + } + + public static UnpooledDataSource createUnpooledDataSource(String resource) throws IOException { + Properties props = Resources.getResourceAsProperties(resource); + UnpooledDataSource ds = new UnpooledDataSource(); + ds.setDriver(props.getProperty("driver")); + ds.setUrl(props.getProperty("url")); + ds.setUsername(props.getProperty("username")); + ds.setPassword(props.getProperty("password")); + return ds; + } + + private static void safeRun(SqlRunner executor, String sql) { + try { + executor.run(sql); + } catch (Exception e) { + //ignore + } + } +} \ No newline at end of file diff --git a/src/test/java/org/apache/ibatis/migration/commands/NewAndUpCommandWithFilenameFilterTest.java b/src/test/java/org/apache/ibatis/migration/commands/NewAndUpCommandWithFilenameFilterTest.java new file mode 100644 index 00000000..ec35a2c5 --- /dev/null +++ b/src/test/java/org/apache/ibatis/migration/commands/NewAndUpCommandWithFilenameFilterTest.java @@ -0,0 +1,207 @@ +/** + * Copyright 2010-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.migration.commands; + +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.jdbc.SqlRunner; +import org.apache.ibatis.migration.MigrationException; +import org.apache.ibatis.migration.options.SelectedOptions; +import org.apache.ibatis.migration.options.SelectedPaths; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.sql.DataSource; + +import static org.junit.Assert.*; + +public class NewAndUpCommandWithFilenameFilterTest { + private SelectedOptions newSelectedOption; + private SelectedPaths selectedPaths; + + public static final String BLOG_PROPERTIES = "databases/blog/blog-derby.properties"; + + private static PrintStream out; + private static StringOutputStream buffer; + + @Before + public void setup() throws Exception { + selectedPaths = new SelectedPaths(); + selectedPaths.setBasePath(new File("src/test/java/org/apache/ibatis/migration/commands")); + + newSelectedOption = new SelectedOptions(); + newSelectedOption.setCommand("New"); + newSelectedOption.setPaths(selectedPaths); + + out = System.out; + buffer = new StringOutputStream(); + System.setOut(new PrintStream(buffer)); + ensureCleanTestDB(); + setupTestDB(); + } + + @After + public void teardown() throws Exception { + System.setOut(out); + ensureCleanTestDB(); + } + + @Test + public void filenameFilterNewCommandTest() { + newSelectedOption.setEnvironment("development_useFilenameFilter"); + + NewCommand newCommand = new NewCommand(newSelectedOption); + newCommand.execute("should start with one APP-8987 1"); + try { + newCommand.execute("should start with two 1"); + fail(); + } catch (MigrationException me) { + assertTrue(me.getMessage().contains("filename filter")); + } + + File[] files = selectedPaths.getScriptPath().listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.indexOf("1")==0; + } + }); + + assertTrue(files.length > 0); + files[0].delete(); + if(files.length > 1) + files[1].delete(); + } + + @Test + public void filenameFilterUpCommandTest() { + newSelectedOption.setEnvironment("development_useFilenameFilter"); + + buffer.clear(); + StatusCommand statusCommand = new StatusCommand(newSelectedOption); + statusCommand.execute(); + assertFalse(buffer.toString().contains("...pending...")); + + NewCommand newCommand = new NewCommand(newSelectedOption); + newCommand.execute("APP-34 should start with one first"); + newCommand.execute("app-38 should start with two first"); + + try { + buffer.clear(); + newCommand.execute("APP-nomatch should start with two duplicate"); + } catch (MigrationException me) { + assertTrue(me.getMessage().contains("filename filter")); + } finally { + //Cleanup + File[] files = selectedPaths.getScriptPath().listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.indexOf("2")==0 || name.indexOf("1")==0; + } + }); + + assertTrue(files.length > 0); + files[0].delete(); + assertTrue(files.length > 1); + files[1].delete(); + if(files.length > 2) + files[2].delete(); + } + } + + private static class StringOutputStream extends OutputStream { + + private StringBuilder builder = new StringBuilder(); + + public void write(int b) throws IOException { + builder.append((char) b); +// out.write(b); + } + + @Override + public String toString() { + return builder.toString(); + } + + public void clear() { + builder.setLength(0); + } + } + + /** + * @throws IOException + * @throws SQLException + */ + public static void ensureCleanTestDB() throws IOException, SQLException { + DataSource ds = createUnpooledDataSource(BLOG_PROPERTIES); + Connection conn = ds.getConnection(); + SqlRunner executor = new SqlRunner(conn); + safeRun(executor, "DROP TABLE changelog"); + conn.commit(); + conn.close(); + } + /** + * @throws IOException + * @throws SQLException + */ + public static void setupTestDB() throws Exception { + DataSource ds = createUnpooledDataSource(BLOG_PROPERTIES); + Connection conn = ds.getConnection(); + SqlRunner executor = new SqlRunner(conn); + safeRun(executor, "CREATE TABLE changelog (" + + "ID NUMERIC(20,0) NOT NULL, " + + "APPLIED_AT VARCHAR(25) NOT NULL, " + + "DESCRIPTION VARCHAR(255) NOT NULL" + + ")"); + conn.commit(); + try { + final List> change = executor.selectAll("select * from changelog "); + assertTrue(change.size() == 0); + } catch (Exception ex) { + throw ex; + } + conn.close(); + } + + public static UnpooledDataSource createUnpooledDataSource(String resource) throws IOException { + Properties props = Resources.getResourceAsProperties(resource); + UnpooledDataSource ds = new UnpooledDataSource(); + ds.setDriver(props.getProperty("driver")); + ds.setUrl(props.getProperty("url")); + ds.setUsername(props.getProperty("username")); + ds.setPassword(props.getProperty("password")); + return ds; + } + + private static void safeRun(SqlRunner executor, String sql) { + try { + executor.run(sql); + } catch (Exception e) { + //ignore + } + } +} \ No newline at end of file diff --git a/src/test/java/org/apache/ibatis/migration/commands/NewCommandTest.java b/src/test/java/org/apache/ibatis/migration/commands/NewCommandTest.java new file mode 100644 index 00000000..dc5f5552 --- /dev/null +++ b/src/test/java/org/apache/ibatis/migration/commands/NewCommandTest.java @@ -0,0 +1,107 @@ +package org.apache.ibatis.migration.commands; + +import org.apache.ibatis.migration.MigrationException; +import org.apache.ibatis.migration.options.SelectedOptions; +import org.apache.ibatis.migration.options.SelectedPaths; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.net.URL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class NewCommandTest { + private SelectedOptions newSelectedOption; + private SelectedPaths selectedPaths; + + @Before + public void setup() { + URL url = getClass().getClassLoader().getResource("org/apache/ibatis/migration/commands"); + + selectedPaths = new SelectedPaths(); + selectedPaths.setBasePath(new File(url.getFile())); + + newSelectedOption = new SelectedOptions(); + newSelectedOption.setCommand("New"); + newSelectedOption.setPaths(selectedPaths); + } + + @Test + (expected = MigrationException.class) + public void testThrowsExceptionWhenDescriptionIsNotSupplied() { + NewCommand newCommand = new NewCommand(newSelectedOption); + newCommand.execute(""); + } + + @Test + public void testSpacesInDescriptionIsReplacedWithUnderscores() { + NewCommand newCommand = new NewCommand(newSelectedOption); + newCommand.execute("must be underscores instead of spaces between words"); + + File[] files = selectedPaths.getScriptPath().listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.contains("must_be_underscores_instead_of_spaces_between_words.sql"); + } + }); + + assertEquals(1, files.length); + files[0].delete(); + } + + @Test + public void testNewFileStartsWithNumberSeqence() { + newSelectedOption.setEnvironment("development_useSeqNum"); + + NewCommand newCommand = new NewCommand(newSelectedOption); + newCommand.execute("should start with one"); + + File[] files = selectedPaths.getScriptPath().listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.indexOf("1")==0; + } + }); + + assertEquals(1, files.length); + files[0].delete(); + } + + @Test + public void testSequenceNumberIncrementedOnEachNewCommand() { + newSelectedOption.setEnvironment("development_useSeqNum"); + + NewCommand newCommand = new NewCommand(newSelectedOption); + + newCommand.execute("one"); + newCommand.execute("two"); + newCommand.execute("three"); + + assertEquals("4", newCommand.getNextIDAsString()); + + // cleaning up after the test + for (File file : selectedPaths.getScriptPath().listFiles()) { + if (!file.getName().equals(".gitignore")) { + file.delete(); + } + } + } + + @Test + public void testScriptsDirWithOnlyBootstrapFileWhichHasNoSequenceNumber() throws IOException { + File bootStrapFile = new File(selectedPaths.getScriptPath() + "/bootstrap.sql"); + bootStrapFile.createNewFile(); + assertTrue(bootStrapFile.exists()); + + newSelectedOption.setEnvironment("development_useSeqNum"); + NewCommand newCommand = new NewCommand(newSelectedOption); + + assertEquals("1", newCommand.getNextIDAsString()); + + bootStrapFile.delete(); + } +} \ No newline at end of file diff --git a/src/test/java/org/apache/ibatis/migration/commands/environments/development.properties b/src/test/java/org/apache/ibatis/migration/commands/environments/development.properties new file mode 100644 index 00000000..b56a9d9a --- /dev/null +++ b/src/test/java/org/apache/ibatis/migration/commands/environments/development.properties @@ -0,0 +1,70 @@ +## Base time zone to ensure times are consistent across machines +time_zone=GMT+0:00 + +## The character set that scripts are encoded with +# script_char_set=UTF-8 + +# Use number sequence instead of timezone +# If set to true, it will start with initial_sequence_number. Each new command will increment sequence by 1 +use_sequence_number=false +#initial_sequence_number=1 +#sequence_number_padding=3 + +## JDBC connection properties. +driver=org.apache.derby.jdbc.EmbeddedDriver +url=jdbc:derby:ibderby;create=true +username= +password= + +# +# A NOTE ON STORED PROCEDURES AND DELIMITERS +# +# Stored procedures and +# functions commonly have nested delimiters that conflict +# with the schema migration parsing. If you tend to use +# procs, functions, triggers or anything that could create +# this situation, then you may want to experiment with +# send_full_script=true (preferred), or if you can't use +# send_full_script, then you may have to resort to a full +# line delimiter such as "GO" or "/" or "!RUN!". +# +# Also play with the autocommit settings, as some drivers +# or databases don't support creating procs, functions or +# even tables in a transaction, and others require it. +# + +# This ignores the line delimiters and +# simply sends the entire script at once. +# Use with JDBC drivers that can accept large +# blocks of delimited text at once. +send_full_script=false + +# This controls how statements are delimited. +# By default statements are delimited by an +# end of line semicolon. Some databases may +# (e.g. MS SQL Server) may require a full line +# delimiter such as GO. +# These are ignored if send_full_script is true. +delimiter=; +full_line_delimiter=false + +# If set to true, each statement is isolated +# in its own transaction. Otherwise the entire +# script is executed in one transaction. +# Few databases should need this set to true, +# but some do. +auto_commit=false + +# Custom driver path to allow you to centralize your driver files +# Default requires the drivers to be in the drivers directory of your +# initialized migration directory (created with "migrate init") +# driver_path= + +# Name of the table that tracks changes to the database +changelog=CHANGELOG + +# Migrations support variable substitutions in the form of ${variable} +# in the migration scripts. All of the above properties will be ignored though, +# with the exception of changelog. +# Example: The following would be referenced in a migration file as ${ip_address} +# ip_address=192.168.0.1 \ No newline at end of file diff --git a/src/test/java/org/apache/ibatis/migration/commands/environments/development_useFilenameFilter.properties b/src/test/java/org/apache/ibatis/migration/commands/environments/development_useFilenameFilter.properties new file mode 100644 index 00000000..39bcab46 --- /dev/null +++ b/src/test/java/org/apache/ibatis/migration/commands/environments/development_useFilenameFilter.properties @@ -0,0 +1,74 @@ +## Base time zone to ensure times are consistent across machines +time_zone=GMT+0:00 + +## The character set that scripts are encoded with +# script_char_set=UTF-8 + +# Use number sequence instead of timezone +# If set to true, it will start with initial_sequence_number. Each new command will increment sequence by 1 +use_sequence_number=true +initial_sequence_number=1 +#sequence_number_padding=3 + +## Change file name filter +# A regular expression which all change files must match to be applied +filename_filter=APP-\\d+ + +## JDBC connection properties. +driver=org.apache.derby.jdbc.EmbeddedDriver +url=jdbc:derby:ibderby;create=true +username= +password= + +# +# A NOTE ON STORED PROCEDURES AND DELIMITERS +# +# Stored procedures and +# functions commonly have nested delimiters that conflict +# with the schema migration parsing. If you tend to use +# procs, functions, triggers or anything that could create +# this situation, then you may want to experiment with +# send_full_script=true (preferred), or if you can't use +# send_full_script, then you may have to resort to a full +# line delimiter such as "GO" or "/" or "!RUN!". +# +# Also play with the autocommit settings, as some drivers +# or databases don't support creating procs, functions or +# even tables in a transaction, and others require it. +# + +# This ignores the line delimiters and +# simply sends the entire script at once. +# Use with JDBC drivers that can accept large +# blocks of delimited text at once. +send_full_script=false + +# This controls how statements are delimited. +# By default statements are delimited by an +# end of line semicolon. Some databases may +# (e.g. MS SQL Server) may require a full line +# delimiter such as GO. +# These are ignored if send_full_script is true. +delimiter=; +full_line_delimiter=false + +# If set to true, each statement is isolated +# in its own transaction. Otherwise the entire +# script is executed in one transaction. +# Few databases should need this set to true, +# but some do. +auto_commit=false + +# Custom driver path to allow you to centralize your driver files +# Default requires the drivers to be in the drivers directory of your +# initialized migration directory (created with "migrate init") +# driver_path= + +# Name of the table that tracks changes to the database +changelog=CHANGELOG + +# Migrations support variable substitutions in the form of ${variable} +# in the migration scripts. All of the above properties will be ignored though, +# with the exception of changelog. +# Example: The following would be referenced in a migration file as ${ip_address} +# ip_address=192.168.0.1 \ No newline at end of file diff --git a/src/test/java/org/apache/ibatis/migration/commands/environments/development_useSeqNum.properties b/src/test/java/org/apache/ibatis/migration/commands/environments/development_useSeqNum.properties new file mode 100644 index 00000000..b0cbaced --- /dev/null +++ b/src/test/java/org/apache/ibatis/migration/commands/environments/development_useSeqNum.properties @@ -0,0 +1,70 @@ +## Base time zone to ensure times are consistent across machines +time_zone=GMT+0:00 + +# Use number sequence instead of timezone +# If set to true, it will start with initial_sequence_number. Each new command will increment sequence by 1 +use_sequence_number=true +initial_sequence_number=1 +sequence_number_padding=1 + +## The character set that scripts are encoded with +# script_char_set=UTF-8 + +## JDBC connection properties. +driver=org.apache.derby.jdbc.EmbeddedDriver +url=jdbc:derby:ibderby;create=true +username= +password= + +# +# A NOTE ON STORED PROCEDURES AND DELIMITERS +# +# Stored procedures and +# functions commonly have nested delimiters that conflict +# with the schema migration parsing. If you tend to use +# procs, functions, triggers or anything that could create +# this situation, then you may want to experiment with +# send_full_script=true (preferred), or if you can't use +# send_full_script, then you may have to resort to a full +# line delimiter such as "GO" or "/" or "!RUN!". +# +# Also play with the autocommit settings, as some drivers +# or databases don't support creating procs, functions or +# even tables in a transaction, and others require it. +# + +# This ignores the line delimiters and +# simply sends the entire script at once. +# Use with JDBC drivers that can accept large +# blocks of delimited text at once. +send_full_script=false + +# This controls how statements are delimited. +# By default statements are delimited by an +# end of line semicolon. Some databases may +# (e.g. MS SQL Server) may require a full line +# delimiter such as GO. +# These are ignored if send_full_script is true. +delimiter=; +full_line_delimiter=false + +# If set to true, each statement is isolated +# in its own transaction. Otherwise the entire +# script is executed in one transaction. +# Few databases should need this set to true, +# but some do. +auto_commit=false + +# Custom driver path to allow you to centralize your driver files +# Default requires the drivers to be in the drivers directory of your +# initialized migration directory (created with "migrate init") +# driver_path= + +# Name of the table that tracks changes to the database +changelog=CHANGELOG + +# Migrations support variable substitutions in the form of ${variable} +# in the migration scripts. All of the above properties will be ignored though, +# with the exception of changelog. +# Example: The following would be referenced in a migration file as ${ip_address} +# ip_address=192.168.0.1 diff --git a/src/test/java/org/apache/ibatis/migration/commands/scripts/placeholder.txt b/src/test/java/org/apache/ibatis/migration/commands/scripts/placeholder.txt new file mode 100644 index 00000000..4f9f2c0e --- /dev/null +++ b/src/test/java/org/apache/ibatis/migration/commands/scripts/placeholder.txt @@ -0,0 +1 @@ +#this is a placeholder file for script tests diff --git a/src/test/java/org/apache/ibatis/migration/example/environments/development.properties b/src/test/java/org/apache/ibatis/migration/example/environments/development.properties index a40bd69d..c2bcbceb 100644 --- a/src/test/java/org/apache/ibatis/migration/example/environments/development.properties +++ b/src/test/java/org/apache/ibatis/migration/example/environments/development.properties @@ -4,6 +4,12 @@ time_zone=GMT+0:00 ## The character set that scripts are encoded with # script_char_set=UTF-8 +# Use number sequence instead of timezone +# If set to true, it will start with initial_sequence_number. Each new command will increment sequence by 1 +#use_sequence_number=true +#initial_sequence_number=1 +#sequence_number_padding=3 + ## JDBC connection properties. driver=org.apache.derby.jdbc.EmbeddedDriver url=jdbc:derby:ibderby;create=true diff --git a/src/test/java/org/apache/ibatis/migration/runtime_migration/RuntimeMigrationTest.java b/src/test/java/org/apache/ibatis/migration/runtime_migration/RuntimeMigrationTest.java index 7318d4b3..b052b632 100644 --- a/src/test/java/org/apache/ibatis/migration/runtime_migration/RuntimeMigrationTest.java +++ b/src/test/java/org/apache/ibatis/migration/runtime_migration/RuntimeMigrationTest.java @@ -70,6 +70,7 @@ public void tearDown() throws Exception { public void testInitialStatus() throws Exception { StatusOperation status = new StatusOperation().operate(connectionProvider, migrationsLoader, dbOption, new PrintStream(out)); assertEquals(0, status.getAppliedCount()); + assertEquals(0, status.getMissingScriptCount()); assertEquals(3, status.getPendingCount()); assertEquals(3, status.getCurrentStatus().size()); } @@ -97,6 +98,7 @@ public void testUp() throws Exception { StatusOperation status = new StatusOperation().operate(connectionProvider, migrationsLoader, dbOption, new PrintStream(out)); assertEquals(3, status.getAppliedCount()); + assertEquals(0, status.getMissingScriptCount()); assertEquals(0, status.getPendingCount()); assertEquals(3, status.getCurrentStatus().size()); } @@ -128,6 +130,26 @@ public void testDownWithStep() throws Exception { assertTableDoesNotExist(connectionProvider, "first_table"); assertTableDoesNotExist(connectionProvider, "second_table"); } + + @Test + public void testMissingScript() throws Exception { + new UpOperation().operate(connectionProvider, migrationsLoader, dbOption, new PrintStream(out)); + + StatusOperation status = new StatusOperation().operate(connectionProvider, migrationsLoader, dbOption, new PrintStream(out)); + assertEquals(3, status.getAppliedCount()); + assertEquals(0, status.getMissingScriptCount()); + assertEquals(0, status.getPendingCount()); + assertEquals(3, status.getCurrentStatus().size()); + + runSql(connectionProvider, "insert into changelog (ID, APPLIED_AT, DESCRIPTION) " + + "values (20150903185011, '2015-09-03 18:56:11', 'Test entry')"); + + status = new StatusOperation().operate(connectionProvider, migrationsLoader, dbOption, new PrintStream(out)); + assertEquals(4, status.getAppliedCount()); + assertEquals(1, status.getMissingScriptCount()); + assertEquals(0, status.getPendingCount()); + assertEquals(4, status.getCurrentStatus().size()); + } @Test public void testPending() throws Exception { diff --git a/src/test/java/org/apache/ibatis/migration/runtime_migration/scripts/20130707120739_create_second_table.sql b/src/test/java/org/apache/ibatis/migration/runtime_migration/scripts/20130707120739_create_second_table.sql index b9e57675..c644c83d 100644 --- a/src/test/java/org/apache/ibatis/migration/runtime_migration/scripts/20130707120739_create_second_table.sql +++ b/src/test/java/org/apache/ibatis/migration/runtime_migration/scripts/20130707120739_create_second_table.sql @@ -1,4 +1,5 @@ --- // Second migration. +-- // Second migration. +-- Unicode test data جراءات امنية, استنفار امني -- Migration SQL that makes the change goes here. CREATE TABLE second_table (