package com.jdblabs.dbmigrate import groovy.sql.Sql import com.jdbernard.util.AnsiEscapeCodeSequence as ANSI import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import java.text.SimpleDateFormat import org.docopt.Docopt import org.slf4j.Logger import org.slf4j.LoggerFactory public class DbMigrate { public static final VERSION = "ALPHA" public static final def DOC = """\ db-migrate (groovy) v${VERSION} Usage: db_migrate [options] create db_migrate [options] up [] db_migrate [options] down [] db_migrate [options] init db_migrate (-V | --version) db_migrate (-h | --help) Options: -c --config Use the given configuration file (defaults to "database.properties"). -q --quiet Suppress log information. -v --verbose Print detailed log information. --very-verbose Print very detailed log information. -V --version Print the tools version information. -h --help Print this usage information. """ private static sdf = new SimpleDateFormat('yyyyMMddHHmmss') private static Logger LOGGER = LoggerFactory.getLogger(DbMigrate) Sql sql File migrationsDir public static void main(String[] args) { // Parse arguments def opts = new Docopt(DOC).withVersion("wdiwtlt v$VERSION").parse(args) if (opts['--version']) { println "db-migrate.groovy v$VERSION" System.exit(0) } if (opts['--help']) { println DOC; System.exit(0) } // TODO: Setup logging & output levels Logger clilog = LoggerFactory.getLogger("db-migrate.cli") // Load the configuration file def givenCfg = new Properties() File cfgFile if (opts["--config"]) cfgFile = new File(opts["--config"]) if (!cfgFile || !cfgFile.exists() || !cfgFile.isFile()) cfgFile = new File("database.properties") if (!cfgFile.exists() || !cfgFile.isFile()) { clilog.warn("Config file '{}' does not exist or is not a regular file.", cfgFile.canonicalPath) } if (cfgFile.exists() && cfgFile.isFile()) { try { cfgFile.withInputStream { givenCfg.load(it) } } catch (Exception e) { clilog.error("Could not read configuration file.", e) givenCfg.clear() } } // Check for migrations directory File migrationsDir = new File(givenCfg["migrations.dir"]) if (!migrationsDir.exists() || !migrationsDir.isDirectory()) { clilog.error("'{}' does not exist or is not a directory.", migrationsDir.canonicalPath) System.exit(1) } // Create the datasource HikariConfig hcfg = new HikariConfig(givenCfg) HikariDataSource hds = new HikariDataSource(hcfg) // Instantiate the DbMigrate instance DbMigrate dbmigrate = new DbMigrate(new Sql(hds)) // Execute the appropriate command. if (opts['create']) { try { List files = dbmigrate.createMigration(opts['']) clilog.info("Creted new migration files:\n\t${files.name.join('\n\t')}") } catch (Exception e) { clilog.error('Unable to create migration scripts.', e) } } else if (opts['up']) dbmigrate.up(opts['']) else if (opts['down']) dbmigrate.down(opts[''] ?: 1) } public File createMigration(String migrationName) { String timestamp = sdf.format(new Date()) File upFile = new File(migrationsDir, "$timestamp-$migrationName-up.sql") File downFiles = new File(migrationsDir, "$timestamp-$migrationName-down.sql") upFile.text = "-- UP script for $migrationName ($timestamp)" downFile.text = "-- DOWN script for $migrationName ($timestamp)" return [upFile, downFile] } public static def createMigrationsTable() { sql.execute(''' CREATE TABLE IF NOT EXISTS migrations ( id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, run_at TIMESTAMP NOT NULL DEFAULT NOW())''') } public def diffMigrations() { def results = [notRun: [], missing: []] results.run = sql.rows('SELECT name FROM migrations ORDER BY name') .collect { it.name }.sort() SortedSet available = new TreeSet<>() migrationsDir.eachFile { f -> available << f.name.replaceAll(/-(up|down).sql$/, '') } available.each { migrationName -> if (!results.run.contains(migrationName)) results.notRun << migrationName // If we've already seen some migrations that have not been run but this // one has been run, that means we have a gap and are missing migrations. else if (results.notRun.size() > 0) { results.missing += reults.notRun results.notRun = [] } } return results } public List up(Integer count = null) { createMigrationsTable() def diff = diffMigrations() List toRun = count < diff.notRun.size() ? notRun[0.. down(Integer count = 1) { createMigrationsTable() def diff = diffMigrations() List toRun = count < diff.notRun.size() ? notRun.reverse()[0.. runMigrations(List toRun, boolean up = true) { List migrationsRun = [] try { sql.execute('BEGIN') toRun.each { migrationName -> File migrationFile = new File(migrationsDir, "$migrationName-${up ? 'up' : 'down'}.sql") if (!migrationFile.exists() || !migrationFile.isFile()) throw new FileNotFoundException(migrationFile.canonicalPath + "does not exist or is not a regular file.") List statements = migrationFile.text.split(/;/) .findAll { it.trim().length() > 0 && !it.startsWith('--') } .collect { "$it;" } statements.each { sql.execute(it) } if (up) sql.executeInsert( 'INSERT INTO migrations (name) VALUES (?);', migrationName) else sql.execute( 'DELETE FROM migrations WHERE name = ?;', migrationName) migrationsRun << migrationName } sql.execute('COMMIT') } catch (Exception e) { sql.execute('ROLLBACK'); } return migrationsRun } }