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 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.jdbernard.util.AnsiEscapeCodeSequence as ANSI
import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import java.io.FilenameFilter
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import org.docopt.Docopt import org.docopt.Docopt
import org.slf4j.Logger import org.slf4j.Logger
@ -12,13 +15,12 @@ import org.slf4j.LoggerFactory
public class DbMigrate { public class DbMigrate {
public static final VERSION = "0.2.1" public static final VERSION = "0.2.2"
public static final def DOC = """\ public static final def DOC = """\
db-migrate.groovy v${VERSION} db-migrate.groovy v${VERSION}
Usage: Usage:
db_migrate [options] create <migration-name> db_migrate [options] create <migration-name>
db_migrate [options] up [<count>] db_migrate [options] up [<count>]
db_migrate [options] down [<count>] db_migrate [options] down [<count>]
@ -27,18 +29,12 @@ Usage:
db_migrate (-h | --help) db_migrate (-h | --help)
Options: Options:
-c --config <config-file> Use the given configuration file (defaults to -c --config <config-file> Use the given configuration file (defaults to
"database.properties"). "database.properties").
-q --quiet Suppress log information. -q --quiet Suppress log information.
-v --verbose Print detailed log information. -v --verbose Print detailed log information.
--very-verbose Print very detailed log information. --very-verbose Print very detailed log information.
-V --version Print the tools version information. -V --version Print the tools version information.
-h --help Print this usage information. -h --help Print this usage information.
""" """
@ -62,6 +58,19 @@ Options:
// TODO: Setup logging & output levels // TODO: Setup logging & output levels
Logger clilog = LoggerFactory.getLogger("db-migrate.cli") 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 // Load the configuration file
def givenCfg = new Properties() def givenCfg = new Properties()
File cfgFile File cfgFile
@ -81,35 +90,43 @@ Options:
givenCfg.clear() } } givenCfg.clear() } }
// Check for migrations directory // Check for migrations directory
File migrationsDir = new File(givenCfg["migrations.dir"]) File migrationsDir = new File(givenCfg["migrations.dir"] ?: 'migrations')
if (!migrationsDir.exists() || !migrationsDir.isDirectory()) { if (!migrationsDir.exists() || !migrationsDir.isDirectory()) {
clilog.error("'{}' does not exist or is not a directory.", clilog.error("'{}' does not exist or is not a directory.",
migrationsDir.canonicalPath) migrationsDir.canonicalPath)
System.exit(1) } System.exit(1) }
// Create the datasource
HikariConfig hcfg = new HikariConfig(givenCfg)
HikariDataSource hds = new HikariDataSource(hcfg)
// Instantiate the DbMigrate instance // 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']) { if (opts['create']) {
try { try {
List<File> files = dbmigrate.createMigration(opts['<migration-name>']) 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) { 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) } 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()) String timestamp = sdf.format(new Date())
File upFile = new File(migrationsDir, "$timestamp-$migrationName-up.sql") 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)" upFile.text = "-- UP script for $migrationName ($timestamp)"
downFile.text = "-- DOWN script for $migrationName ($timestamp)" downFile.text = "-- DOWN script for $migrationName ($timestamp)"
@ -117,6 +134,8 @@ Options:
return [upFile, downFile] } return [upFile, downFile] }
public def createMigrationsTable() { public def createMigrationsTable() {
LOGGER.trace('Checking for the existence of the migrations table and ' +
'creating it if it does not exist.')
sql.execute(''' sql.execute('''
CREATE TABLE IF NOT EXISTS migrations ( CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
@ -126,12 +145,14 @@ CREATE TABLE IF NOT EXISTS migrations (
public def diffMigrations() { public def diffMigrations() {
def results = [notRun: [], missing: []] def results = [notRun: [], missing: []]
LOGGER.trace('Diffing migrations...')
results.run = sql.rows('SELECT name FROM migrations ORDER BY name') results.run = sql.rows('SELECT name FROM migrations ORDER BY name')
.collect { it.name }.sort() .collect { it.name }.sort()
SortedSet<String> available = new TreeSet<>() SortedSet<String> available = new TreeSet<>()
migrationsDir.eachFile { f -> available.addAll(migrationsDir
available << f.name.replaceAll(/-(up|down).sql$/, '') } .listFiles({ d, n -> n ==~ /.+-(up|down).sql$/ } as FilenameFilter)
.collect { f -> f.name.replaceAll(/-(up|down).sql$/, '') })
available.each { migrationName -> available.each { migrationName ->
if (!results.run.contains(migrationName)) if (!results.run.contains(migrationName))
@ -143,23 +164,45 @@ CREATE TABLE IF NOT EXISTS migrations (
results.missing += reults.notRun results.missing += reults.notRun
results.notRun = [] } } results.notRun = [] } }
LOGGER.trace('Migrations diff:\n\trun: {}\n\tnot run: {}\n\tmissing: {}',
results.run, results.notRun, results.missing)
return results } return results }
public List<String> up(Integer count = null) { public List<String> up(Integer count = null) {
createMigrationsTable() createMigrationsTable()
def diff = diffMigrations() def diff = diffMigrations()
List<String> toRun = count < diff.notRun.size() ? if (diff.missing) {
notRun[0..<count] : notRun 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) } return runMigrations(toRun, true) }
public List<File> down(Integer count = 1) { public List<String> down(Integer count = 1) {
createMigrationsTable() createMigrationsTable()
def diff = diffMigrations() def diff = diffMigrations()
List<String> toRun = count < diff.notRun.size() ? if (diff.missing) {
notRun.reverse()[0..<count] : notRun.reverse() 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) } return runMigrations(toRun, false) }
@ -167,9 +210,11 @@ CREATE TABLE IF NOT EXISTS migrations (
List<String> migrationsRun = [] List<String> migrationsRun = []
try { try {
LOGGER.trace("Beginning transaction.")
sql.execute('BEGIN') sql.execute('BEGIN')
toRun.each { migrationName -> toRun.each { migrationName ->
LOGGER.info(migrationName)
File migrationFile = new File(migrationsDir, File migrationFile = new File(migrationsDir,
"$migrationName-${up ? 'up' : 'down'}.sql") "$migrationName-${up ? 'up' : 'down'}.sql")
@ -177,19 +222,29 @@ CREATE TABLE IF NOT EXISTS migrations (
throw new FileNotFoundException(migrationFile.canonicalPath + throw new FileNotFoundException(migrationFile.canonicalPath +
"does not exist or is not a regular file.") "does not exist or is not a regular file.")
List<String> statements = migrationFile.text.split(/;/) LOGGER.trace('Raw statements:\n\n{}\n', migrationFile.text.split(/;/).join('\n'))
.findAll { it.trim().length() > 0 && !it.startsWith('--') }
.collect { "$it;" }
statements.each { sql.execute(it) } List<String> statements = migrationFile.text.split(/;/)
if (up) sql.executeInsert( .collect { it.replaceAll(/--.*$/, '').trim() }
'INSERT INTO migrations (name) VALUES (?);', migrationName) .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( else sql.execute(
'DELETE FROM migrations WHERE name = ?;', migrationName) 'DELETE FROM migrations WHERE name = ?', migrationName)
migrationsRun << migrationName } migrationsRun << migrationName }
sql.execute('COMMIT') } sql.execute('COMMIT')
LOGGER.info('Went {} {} migrations.',
up ? 'up' : 'down', migrationsRun.size()) }
catch (Exception e) { sql.execute('ROLLBACK'); } catch (Exception e) { sql.execute('ROLLBACK'); }
return migrationsRun } 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)