Groovy impl: added verbosity levels. Tested and fixed create/up/down.

This commit is contained in:
Jonathan Bernard 2016-04-11 21:08:16 -05:00
parent 9b3e7b4d26
commit 6bf2b86592
2 changed files with 107 additions and 35 deletions

View File

@ -2,9 +2,12 @@ package com.jdblabs.dbmigrate
import groovy.sql.Sql
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger as LBLogger
import com.jdbernard.util.AnsiEscapeCodeSequence as ANSI
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import java.io.FilenameFilter
import java.text.SimpleDateFormat
import org.docopt.Docopt
import org.slf4j.Logger
@ -12,13 +15,12 @@ import org.slf4j.LoggerFactory
public class DbMigrate {
public static final VERSION = "0.2.1"
public static final VERSION = "0.2.2"
public static final def DOC = """\
db-migrate.groovy v${VERSION}
Usage:
db_migrate [options] create <migration-name>
db_migrate [options] up [<count>]
db_migrate [options] down [<count>]
@ -27,18 +29,12 @@ Usage:
db_migrate (-h | --help)
Options:
-c --config <config-file> 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.
"""
@ -62,6 +58,19 @@ Options:
// TODO: Setup logging & output levels
Logger clilog = LoggerFactory.getLogger("db-migrate.cli")
if (opts['--quiet']) {
((LBLogger) LOGGER).level = Level.ERROR
((LBLogger) LoggerFactory.getLogger(LBLogger.ROOT_LOGGER_NAME))
.level = Level.ERROR }
if (opts['--verbose']) {
((LBLogger) LOGGER).level = Level.DEBUG
((LBLogger) LoggerFactory.getLogger(LBLogger.ROOT_LOGGER_NAME))
.level = Level.INFO }
if (opts['--very-verbose']) {
((LBLogger) LOGGER).level = Level.TRACE
((LBLogger) LoggerFactory.getLogger(LBLogger.ROOT_LOGGER_NAME))
.level = Level.DEBUG }
// Load the configuration file
def givenCfg = new Properties()
File cfgFile
@ -81,35 +90,43 @@ Options:
givenCfg.clear() } }
// Check for migrations directory
File migrationsDir = new File(givenCfg["migrations.dir"])
File migrationsDir = new File(givenCfg["migrations.dir"] ?: 'migrations')
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))
DbMigrate dbmigrate = new DbMigrate(migrationsDir: migrationsDir)
// Execute the appropriate command.
// If we've only been asked to create a new migration, we don't need to
// setup the DB connection.
if (opts['create']) {
try {
List<File> files = dbmigrate.createMigration(opts['<migration-name>'])
clilog.info("Creted new migration files:\n\t${files.name.join('\n\t')}") }
clilog.info("Created new migration files:\n\t${files.name.join('\n\t')}")
return }
catch (Exception e) {
clilog.error('Unable to create migration scripts.', e) } }
clilog.error('Unable to create migration scripts.', e)
System.exit(1) } }
else if (opts['up']) dbmigrate.up(opts['<count>'])
// Create the datasource.
Properties dsProps = new Properties()
dsProps.putAll(givenCfg.findAll { it.key != 'migrations.dir' })
HikariDataSource hds = new HikariDataSource(new HikariConfig(dsProps))
dbmigrate.sql = new Sql(hds)
// Execute the appropriate command.
if (opts['up']) dbmigrate.up(opts['<count>'])
else if (opts['down']) dbmigrate.down(opts['<count>'] ?: 1) }
public File createMigration(String migrationName) {
public List<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")
File downFile = new File(migrationsDir, "$timestamp-$migrationName-down.sql")
upFile.text = "-- UP script for $migrationName ($timestamp)"
downFile.text = "-- DOWN script for $migrationName ($timestamp)"
@ -117,6 +134,8 @@ Options:
return [upFile, downFile] }
public def createMigrationsTable() {
LOGGER.trace('Checking for the existence of the migrations table and ' +
'creating it if it does not exist.')
sql.execute('''
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
@ -126,12 +145,14 @@ CREATE TABLE IF NOT EXISTS migrations (
public def diffMigrations() {
def results = [notRun: [], missing: []]
LOGGER.trace('Diffing migrations...')
results.run = sql.rows('SELECT name FROM migrations ORDER BY name')
.collect { it.name }.sort()
SortedSet<String> available = new TreeSet<>()
migrationsDir.eachFile { f ->
available << f.name.replaceAll(/-(up|down).sql$/, '') }
available.addAll(migrationsDir
.listFiles({ d, n -> n ==~ /.+-(up|down).sql$/ } as FilenameFilter)
.collect { f -> f.name.replaceAll(/-(up|down).sql$/, '') })
available.each { migrationName ->
if (!results.run.contains(migrationName))
@ -143,23 +164,45 @@ CREATE TABLE IF NOT EXISTS migrations (
results.missing += reults.notRun
results.notRun = [] } }
LOGGER.trace('Migrations diff:\n\trun: {}\n\tnot run: {}\n\tmissing: {}',
results.run, results.notRun, results.missing)
return results }
public List<String> up(Integer count = null) {
createMigrationsTable()
def diff = diffMigrations()
List<String> toRun = count < diff.notRun.size() ?
notRun[0..<count] : notRun
if (diff.missing) {
LOGGER.error('Missing migrations:\n\t{}', diff.missing)
throw new Exception('Database is in an inconsistent state.') }
LOGGER.debug('Migrating up.')
List<String> toRun
if (!count || count >= diff.notRun.size()) toRun = diff.notRun
else toRun = diff.notRun[0..<count]
LOGGER.debug('{} migrations to run.', toRun.size())
LOGGER.trace('Migrations: {}.', toRun)
return runMigrations(toRun, true) }
public List<File> down(Integer count = 1) {
public List<String> down(Integer count = 1) {
createMigrationsTable()
def diff = diffMigrations()
List<String> toRun = count < diff.notRun.size() ?
notRun.reverse()[0..<count] : notRun.reverse()
if (diff.missing) {
LOGGER.error('Missing migrations:\n\t{}', diff.missing)
throw new Exception('Database is in an inconsistent state.') }
LOGGER.debug('Migrating down.')
List<String> toRun = count < diff.run.size() ?
diff.run.reverse()[0..<count] : diff.run.reverse()
LOGGER.debug('{} migrations to run.', toRun.size())
LOGGER.trace('Migrations: {}.', toRun)
return runMigrations(toRun, false) }
@ -167,9 +210,11 @@ CREATE TABLE IF NOT EXISTS migrations (
List<String> migrationsRun = []
try {
LOGGER.trace("Beginning transaction.")
sql.execute('BEGIN')
toRun.each { migrationName ->
LOGGER.info(migrationName)
File migrationFile = new File(migrationsDir,
"$migrationName-${up ? 'up' : 'down'}.sql")
@ -177,19 +222,29 @@ CREATE TABLE IF NOT EXISTS migrations (
throw new FileNotFoundException(migrationFile.canonicalPath +
"does not exist or is not a regular file.")
List<String> statements = migrationFile.text.split(/;/)
.findAll { it.trim().length() > 0 && !it.startsWith('--') }
.collect { "$it;" }
LOGGER.trace('Raw statements:\n\n{}\n', migrationFile.text.split(/;/).join('\n'))
statements.each { sql.execute(it) }
if (up) sql.executeInsert(
'INSERT INTO migrations (name) VALUES (?);', migrationName)
List<String> statements = migrationFile.text.split(/;/)
.collect { it.replaceAll(/--.*$/, '').trim() }
.findAll { it.length() > 0 }
LOGGER.trace('Statements:\n\n{}\n', statements.join('\n'))
statements.each {
LOGGER.trace('Executing SQL: {}', it)
sql.execute(it) }
if (up) sql.execute(
'INSERT INTO migrations (name) VALUES (?)', migrationName)
else sql.execute(
'DELETE FROM migrations WHERE name = ?;', migrationName)
'DELETE FROM migrations WHERE name = ?', migrationName)
migrationsRun << migrationName }
sql.execute('COMMIT') }
sql.execute('COMMIT')
LOGGER.info('Went {} {} migrations.',
up ? 'up' : 'down', migrationsRun.size()) }
catch (Exception e) { sql.execute('ROLLBACK'); }
return migrationsRun }

View File

@ -0,0 +1,17 @@
import ch.qos.logback.core.*;
import ch.qos.logback.core.encoder.*;
import ch.qos.logback.core.read.*;
import ch.qos.logback.core.rolling.*;
import ch.qos.logback.core.status.*;
import ch.qos.logback.classic.net.*;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
appender("STDOUT", ConsoleAppender) {
encoder(PatternLayoutEncoder) {
pattern = "db-migrate.groovy: %level - %msg%n"
}
}
root(WARN, ["STDOUT"])
logger('com.jdblabs.dbmigrate', INFO)