/** SQLite-specific table. */ public class SQLiteTable extends Table { private static final Log LOG = LogFactory.getLog(SQLiteTable.class); /** * Creates a new SQLite table. * * @param jdbcTemplate The Jdbc Template for communicating with the DB. * @param dbSupport The database-specific support. * @param schema The schema this table lives in. * @param name The name of the table. */ public SQLiteTable(JdbcTemplate jdbcTemplate, DbSupport dbSupport, Schema schema, String name) { super(jdbcTemplate, dbSupport, schema, name); } @Override protected void doDrop() throws SQLException { jdbcTemplate.execute("DROP TABLE " + dbSupport.quote(schema.getName(), name)); } @Override protected boolean doExists() throws SQLException { return jdbcTemplate.queryForInt( "SELECT count(tbl_name) FROM " + dbSupport.quote(schema.getName()) + ".sqlite_master WHERE type='table' AND tbl_name='" + name + "'") > 0; } @Override protected void doLock() throws SQLException { LOG.debug( "Unable to lock " + this + " as SQLite does not support locking. No concurrent migration supported."); } }
/** Main workflow for cleaning the database. */ public class DbClean { private static final Log LOG = LogFactory.getLog(DbClean.class); /** The connection to use. */ private final Connection connection; /** The metadata table. */ private final MetaDataTable metaDataTable; /** The schemas to clean. */ private final Schema[] schemas; /** * The list of callbacks that fire before or after the clean task is executed. You can add as many * callbacks as you want. These should be set on the Flyway class by the end user as Flyway will * set them automatically for you here. */ private final FlywayCallback[] callbacks; /** * Whether to disable clean. * * <p>This is especially useful for production environments where running clean can be quite a * career limiting move. */ private boolean cleanDisabled; /** The DB support for the connection. */ private final DbSupport dbSupport; /** * Creates a new database cleaner. * * @param connection The connection to use. * @param dbSupport The DB support for the connection. * @param metaDataTable The metadata table. * @param schemas The schemas to clean. * @param callbacks The list of callbacks that fire before or after the clean task is executed. * @param cleanDisabled Whether to disable clean. */ public DbClean( Connection connection, DbSupport dbSupport, MetaDataTable metaDataTable, Schema[] schemas, FlywayCallback[] callbacks, boolean cleanDisabled) { this.connection = connection; this.dbSupport = dbSupport; this.metaDataTable = metaDataTable; this.schemas = schemas; this.callbacks = callbacks; this.cleanDisabled = cleanDisabled; } /** * Cleans the schemas of all objects. * * @throws FlywayException when clean failed. */ public void clean() throws FlywayException { if (cleanDisabled) { throw new FlywayException( "Unable to execute clean as it has been disabled with the \"flyway.cleanDisabled\" property."); } try { for (final FlywayCallback callback : callbacks) { new TransactionTemplate(connection) .execute( new TransactionCallback<Object>() { @Override public Object doInTransaction() throws SQLException { dbSupport.changeCurrentSchemaTo(schemas[0]); callback.beforeClean(connection); return null; } }); } dbSupport.changeCurrentSchemaTo(schemas[0]); boolean dropSchemas = false; try { dropSchemas = metaDataTable.hasSchemasMarker(); } catch (Exception e) { LOG.error("Error while checking whether the schemas should be dropped", e); } for (Schema schema : schemas) { if (!schema.exists()) { LOG.warn("Unable to clean unknown schema: " + schema); continue; } if (dropSchemas) { dropSchema(schema); } else { cleanSchema(schema); } } for (final FlywayCallback callback : callbacks) { new TransactionTemplate(connection) .execute( new TransactionCallback<Object>() { @Override public Object doInTransaction() throws SQLException { dbSupport.changeCurrentSchemaTo(schemas[0]); callback.afterClean(connection); return null; } }); } } finally { dbSupport.restoreCurrentSchema(); } } /** * Drops this schema. * * @param schema The schema to drop. * @throws FlywayException when the drop failed. */ private void dropSchema(final Schema schema) { LOG.debug("Dropping schema " + schema + " ..."); StopWatch stopWatch = new StopWatch(); stopWatch.start(); new TransactionTemplate(connection) .execute( new TransactionCallback<Void>() { public Void doInTransaction() { schema.drop(); return null; } }); stopWatch.stop(); LOG.info( String.format( "Dropped schema %s (execution time %s)", schema, TimeFormat.format(stopWatch.getTotalTimeMillis()))); } /** * Cleans this schema of all objects. * * @param schema The schema to clean. * @throws FlywayException when clean failed. */ private void cleanSchema(final Schema schema) { LOG.debug("Cleaning schema " + schema + " ..."); StopWatch stopWatch = new StopWatch(); stopWatch.start(); new TransactionTemplate(connection) .execute( new TransactionCallback<Void>() { public Void doInTransaction() { schema.clean(); return null; } }); stopWatch.stop(); LOG.info( String.format( "Cleaned schema %s (execution time %s)", schema, TimeFormat.format(stopWatch.getTotalTimeMillis()))); } }
/** * Initializes the logging. * * @param level The minimum level to log at. */ private static void initLogging(final Level level) { LogFactory.setLogCreator(new ConsoleLogCreator(level)); LOG = LogFactory.getLog(Migrator.class); }
/** Test to demonstrate the migration functionality using Phoenix. */ @Category(DbCategory.Phoenix.class) public class PhoenixMigrationMediumTest extends MigrationTestCase { private static final Log LOG = LogFactory.getLog(PhoenixMigrationMediumTest.class); protected static HBaseTestingUtility testUtility = null; protected static DriverDataSource dataSource = null; @Override protected String getBasedir() { return "migration/dbsupport/phoenix/sql/sql"; } @Override protected String getMigrationDir() { return "migration/dbsupport/phoenix/sql"; } @Override protected String getQuoteLocation() { return "migration/dbsupport/phoenix/sql/quote"; } @Override protected String getFutureFailedLocation() { return "migration/dbsupport/phoenix/sql/future_failed"; } @Override protected String getValidateLocation() { return "migration/dbsupport/phoenix/sql/validate"; } @Override protected String getSemiColonLocation() { return "migration/dbsupport/phoenix/sql/semicolon"; } @Override protected String getCommentLocation() { return "migration/dbsupport/phoenix/sql/comment"; } @BeforeClass public static void beforeClassSetUp() throws Exception { // Startup HBase in-memory cluster LOG.info("Starting mini-cluster"); testUtility = new HBaseTestingUtility(); testUtility.startMiniCluster(); // Set up Phoenix schema String server = testUtility.getConfiguration().get("hbase.zookeeper.quorum"); String port = testUtility.getConfiguration().get("hbase.zookeeper.property.clientPort"); String zkServer = server + ":" + port; dataSource = new DriverDataSource( Thread.currentThread().getContextClassLoader(), null, "jdbc:phoenix:" + zkServer, "", ""); } @Override protected DataSource createDataSource(Properties customProperties) throws Exception { return dataSource; } @After @Override public void tearDown() throws Exception { // Don't close the connection after each test } @AfterClass public static void afterClassTearDown() throws Exception { LOG.info("Shutting down mini-cluster"); dataSource.close(); testUtility.shutdownMiniCluster(); } @Override public void createTestTable() throws SQLException { jdbcTemplate.execute( "CREATE TABLE t1 (\n" + " name VARCHAR(25) NOT NULL PRIMARY KEY\n" + " )"); } // The default schema doesn't exist until something has been // created in the schema @Test @Override public void schemaExists() throws SQLException { assertFalse(dbSupport.getOriginalSchema().exists()); assertFalse(dbSupport.getSchema("InVaLidScHeMa").exists()); jdbcTemplate.execute( "CREATE TABLE tc1 (\n" + " name VARCHAR(25) NOT NULL PRIMARY KEY\n" + " )"); assertTrue(dbSupport.getOriginalSchema().exists()); } // Custom query, Phoenix has a LIKE with newline issue: // https://issues.apache.org/jira/browse/PHOENIX-1351 @Test @Override public void semicolonWithinStringLiteral() throws Exception { flyway.setLocations("migration/dbsupport/phoenix/sql/semicolon"); flyway.migrate(); assertEquals("1.1", flyway.info().current().getVersion().toString()); assertEquals("Populate table", flyway.info().current().getDescription()); assertEquals( "Mr. Semicolon+Linebreak;\nanother line", jdbcTemplate.queryForString( "SELECT name FROM test_user ORDER BY LENGTH(NAME) DESC LIMIT 1")); } // Needs its own file for syntax, and a custom sql migration prefix to prevent other unit tests // from failing @Test public void validateClean() throws Exception { flyway.setLocations(getValidateLocation()); flyway.migrate(); assertEquals("1", flyway.info().current().getVersion().toString()); flyway.setValidateOnMigrate(true); flyway.setCleanOnValidationError(true); flyway.setSqlMigrationPrefix("PhoenixCheckValidate"); assertEquals(1, flyway.migrate()); } // Phoenix doesn't support setting an explicit schema @Ignore @Override public void setCurrentSchema() throws Exception {} }
/** FileSystem scanner. */ public class FileSystemScanner { private static final Log LOG = LogFactory.getLog(FileSystemScanner.class); /** * Scans the FileSystem for resources under the specified location, starting with the specified * prefix and ending with the specified suffix. * * @param location The location in the filesystem to start searching. Subdirectories are also * searched. * @param prefix The prefix of the resource names to match. * @param suffix The suffix of the resource names to match. * @return The resources that were found. * @throws java.io.IOException when the location could not be scanned. */ public Resource[] scanForResources(Location location, String prefix, String suffix) throws IOException { String path = location.getPath(); LOG.debug( "Scanning for filesystem resources at '" + path + "' (Prefix: '" + prefix + "', Suffix: '" + suffix + "')"); File dir = new File(path); if (!dir.isDirectory() || !dir.canRead()) { LOG.warn("Unable to resolve location filesystem:" + path); return new Resource[0]; } Set<Resource> resources = new TreeSet<Resource>(); Set<String> resourceNames = findResourceNames(path, prefix, suffix); for (String resourceName : resourceNames) { resources.add(new FileSystemResource(resourceName)); LOG.debug("Found filesystem resource: " + resourceName); } return resources.toArray(new Resource[resources.size()]); } /** * Finds the resources names present at this location and below on the classpath starting with * this prefix and ending with this suffix. * * @param path The path on the classpath to scan. * @param prefix The filename prefix to match. * @param suffix The filename suffix to match. * @return The resource names. * @throws java.io.IOException when scanning this location failed. */ private Set<String> findResourceNames(String path, String prefix, String suffix) throws IOException { Set<String> resourceNames = findResourceNamesFromFileSystem(path, new File(path)); return filterResourceNames(resourceNames, prefix, suffix); } /** * Finds all the resource names contained in this file system folder. * * @param scanRootLocation The root location of the scan on disk. * @param folder The folder to look for resources under on disk. * @return The resource names; * @throws IOException when the folder could not be read. */ @SuppressWarnings("ConstantConditions") private Set<String> findResourceNamesFromFileSystem(String scanRootLocation, File folder) throws IOException { LOG.debug( "Scanning for resources in path: " + folder.getPath() + " (" + scanRootLocation + ")"); Set<String> resourceNames = new TreeSet<String>(); File[] files = folder.listFiles(); for (File file : files) { if (file.canRead()) { if (file.isDirectory()) { resourceNames.addAll(findResourceNamesFromFileSystem(scanRootLocation, file)); } else { resourceNames.add(file.getPath()); } } } return resourceNames; } /** * Filters this list of resource names to only include the ones whose filename matches this prefix * and this suffix. * * @param resourceNames The names to filter. * @param prefix The prefix to match. * @param suffix The suffix to match. * @return The filtered names set. */ private Set<String> filterResourceNames(Set<String> resourceNames, String prefix, String suffix) { Set<String> filteredResourceNames = new TreeSet<String>(); for (String resourceName : resourceNames) { String fileName = resourceName.substring(resourceName.lastIndexOf(File.separator) + 1); if (fileName.startsWith(prefix) && fileName.endsWith(suffix) && (fileName.length() > (prefix + suffix).length())) { filteredResourceNames.add(resourceName); } else { LOG.debug("Filtering out resource: " + resourceName + " (filename: " + fileName + ")"); } } return filteredResourceNames; } }