Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
2dbe3ea07c | |||
9acbc27710 | |||
7cf53a4702 | |||
6837e5448b | |||
daf3a8dad0 | |||
|
4e771345ea | ||
e26b6eb01c | |||
|
ad1540ca20 | ||
|
e35a5177ef | ||
|
9f38da0043 | ||
|
c933d6ac2b | ||
|
b49b648358 | ||
|
37c1c01ffc | ||
|
6bf2b86592 | ||
|
9b3e7b4d26 | ||
|
5b9de9b10f | ||
|
109a667fd5 | ||
|
462da00dd3 | ||
|
7242a13e51 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ nimcache/
|
|||||||
*.sw?
|
*.sw?
|
||||||
build/
|
build/
|
||||||
db_migrate
|
db_migrate
|
||||||
|
.gradle/
|
||||||
|
38
README.md
38
README.md
@ -1,4 +1,36 @@
|
|||||||
Nim DB Migrate
|
# DB Migrate
|
||||||
==============
|
|
||||||
|
|
||||||
Small tool to manage database migrations in Nim.
|
Small tool(s) to manage database migrations in various languages.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage:
|
||||||
|
db_migrate [options] create <migration-name>
|
||||||
|
db_migrate [options] up [<count>]
|
||||||
|
db_migrate [options] down [<count>]
|
||||||
|
db_migrate [options] init <schema-name>
|
||||||
|
db_migrate (-V | --version)
|
||||||
|
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.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Database Config Format
|
||||||
|
|
||||||
|
The database config is formatted as JSON. The following keys are supported by
|
||||||
|
all of the implementations:
|
||||||
|
|
||||||
|
* `sqlDir` -- Directory to store SQL files.
|
||||||
|
|
||||||
|
The following keys are supported by the Nim implementation:
|
||||||
|
|
||||||
|
* `connectionString` --
|
||||||
|
28
build.gradle
Normal file
28
build.gradle
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
apply plugin: 'groovy'
|
||||||
|
apply plugin: 'application'
|
||||||
|
apply plugin: 'maven'
|
||||||
|
|
||||||
|
group = 'com.jdblabs'
|
||||||
|
version = '0.2.5'
|
||||||
|
|
||||||
|
mainClassName = 'com.jdblabs.dbmigrate.DbMigrate'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url "http://mvn.jdb-labs.com/repo" }
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile localGroovy()
|
||||||
|
compile 'ch.qos.logback:logback-classic:1.1.3'
|
||||||
|
compile 'ch.qos.logback:logback-core:1.1.3'
|
||||||
|
compile 'com.jdbernard:jdb-util:4.4'
|
||||||
|
compile 'com.offbytwo:docopt:0.6.0.20150202'
|
||||||
|
compile 'com.zaxxer:HikariCP:2.4.3'
|
||||||
|
|
||||||
|
testCompile 'junit:junit:4.12'
|
||||||
|
|
||||||
|
runtime 'com.h2database:h2:1.4.185'
|
||||||
|
runtime 'org.postgresql:postgresql:9.4.1207.jre7'
|
||||||
|
}
|
268
db_migrate.nim
268
db_migrate.nim
@ -1,268 +0,0 @@
|
|||||||
## DB Migrate
|
|
||||||
## ==========
|
|
||||||
##
|
|
||||||
## Simple tool to manage database migrations.
|
|
||||||
|
|
||||||
import algorithm, json, times, os, strutils, docopt, db_postgres, sets,
|
|
||||||
sequtils, logging
|
|
||||||
|
|
||||||
type
|
|
||||||
DbMigrateConfig* = tuple[ driver, sqlDir, connectionString: string, logLevel: Level ]
|
|
||||||
|
|
||||||
proc createMigrationsTable(conn: DbConn): void =
|
|
||||||
conn.exec(sql("""
|
|
||||||
CREATE TABLE IF NOT EXISTS migrations (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR NOT NULL,
|
|
||||||
run_at TIMESTAMP NOT NULL DEFAULT NOW())"""))
|
|
||||||
|
|
||||||
proc rollbackWithErr (pgConn: DbConn, errMsg: string): void =
|
|
||||||
pgConn.exec(sql"ROLLBACK")
|
|
||||||
dbError(errMsg)
|
|
||||||
|
|
||||||
proc loadConfig*(filename: string): DbMigrateConfig =
|
|
||||||
## Load DbMigrateConfig from a file.
|
|
||||||
let cfg = json.parseFile(filename)
|
|
||||||
|
|
||||||
var logLevel: Level
|
|
||||||
if cfg.hasKey("logLevel"):
|
|
||||||
let idx = find(LevelNames, cfg["logLevel"].getStr.toUpper)
|
|
||||||
logLevel = if idx == -1: lvlInfo else: (Level)(idx)
|
|
||||||
|
|
||||||
return (
|
|
||||||
driver: if cfg.hasKey("driver"): cfg["driver"].getStr else: "postres",
|
|
||||||
sqlDir: if cfg.hasKey("sqlDir"): cfg["sqlDir"].getStr else: "migrations",
|
|
||||||
connectionString: cfg["connectionString"].getStr,
|
|
||||||
logLevel: logLevel)
|
|
||||||
|
|
||||||
proc createMigration*(config: DbMigrateConfig, migrationName: string): seq[string] =
|
|
||||||
## Create a new set of database migration files.
|
|
||||||
let timestamp = getTime().getLocalTime().format("yyyyMMddHHmmss")
|
|
||||||
let filenamePrefix = timestamp & "-" & migrationName
|
|
||||||
|
|
||||||
let upFilename = joinPath(config.sqlDir, filenamePrefix & "-up.sql")
|
|
||||||
let downFilename = joinPath(config.sqlDir, filenamePrefix & "-down.sql")
|
|
||||||
|
|
||||||
let scriptDesc = migrationName & " (" & timestamp & ")"
|
|
||||||
|
|
||||||
let upFile = open(upFilename, fmWrite)
|
|
||||||
let downFile = open(downFilename, fmWrite)
|
|
||||||
|
|
||||||
upFile.writeLine "-- UP script for " & scriptDesc
|
|
||||||
downFile.writeLine "-- DOWN script for " & scriptDesc
|
|
||||||
|
|
||||||
upFile.close()
|
|
||||||
downFile.close()
|
|
||||||
|
|
||||||
return @[upFilename, downFilename]
|
|
||||||
|
|
||||||
proc diffMigrations*(pgConn: DbConn, config: DbMigrateConfig):
|
|
||||||
tuple[ run, notRun, missing: seq[string] ] =
|
|
||||||
|
|
||||||
# Query the database to find out what migrations have been run.
|
|
||||||
var migrationsRun = initSet[string]()
|
|
||||||
|
|
||||||
for row in pgConn.fastRows(sql"SELECT * FROM migrations ORDER BY name", @[]):
|
|
||||||
migrationsRun.incl(row[1])
|
|
||||||
|
|
||||||
# Inspect the filesystem to see what migrations are available.
|
|
||||||
var migrationsAvailable = initSet[string]()
|
|
||||||
for filePath in walkFiles(joinPath(config.sqlDir, "*.sql")):
|
|
||||||
var migrationName = filePath.extractFilename
|
|
||||||
migrationName.removeSuffix("-up.sql")
|
|
||||||
migrationName.removeSuffix("-down.sql")
|
|
||||||
migrationsAvailable.incl(migrationName)
|
|
||||||
|
|
||||||
# Diff with the list of migrations that we have in our migrations
|
|
||||||
# directory.
|
|
||||||
let migrationsInOrder =
|
|
||||||
toSeq(migrationsAvailable.items).sorted(system.cmp)
|
|
||||||
|
|
||||||
var migrationsNotRun = newSeq[string]()
|
|
||||||
var missingMigrations = newSeq[string]()
|
|
||||||
|
|
||||||
for migration in migrationsInOrder:
|
|
||||||
if not migrationsRun.contains(migration):
|
|
||||||
migrationsNotRun.add(migration)
|
|
||||||
|
|
||||||
# if we've already seen some migrations that have not been run, but this
|
|
||||||
# one has been, that means we have a gap and are missing migrations
|
|
||||||
elif migrationsNotRun.len > 0:
|
|
||||||
missingMigrations.add(migrationsNotRun)
|
|
||||||
migrationsNotRun = newSeq[string]()
|
|
||||||
|
|
||||||
return (run: toSeq(migrationsRun.items).sorted(system.cmp),
|
|
||||||
notRun: migrationsNotRun,
|
|
||||||
missing: missingMigrations)
|
|
||||||
|
|
||||||
proc readStatements*(filename: string): seq[SqlQuery] =
|
|
||||||
let migrationSql = filename.readFile
|
|
||||||
result = migrationSql.split(';').
|
|
||||||
filter(proc(st: string): bool = st.strip.len > 0 and not st.startsWith("--")).
|
|
||||||
map(proc(st: string): SqlQuery = sql(st & ";"))
|
|
||||||
|
|
||||||
proc up*(pgConn: DbConn, config: DbMigrateConfig, toRun: seq[string]): seq[string] =
|
|
||||||
var migrationsRun = newSeq[string]()
|
|
||||||
# Begin a transaction.
|
|
||||||
pgConn.exec(sql"BEGIN")
|
|
||||||
|
|
||||||
# Apply each of the migrations.
|
|
||||||
for migration in toRun:
|
|
||||||
info migration
|
|
||||||
let filename = joinPath(config.sqlDir, migration & "-up.sql")
|
|
||||||
|
|
||||||
if not filename.fileExists:
|
|
||||||
pgConn.rollbackWithErr "Can not find UP file for " & migration &
|
|
||||||
". Expected '" & filename & "'."
|
|
||||||
|
|
||||||
let statements = filename.readStatements
|
|
||||||
|
|
||||||
try:
|
|
||||||
for statement in statements: pgConn.exec(statement)
|
|
||||||
pgConn.exec(sql"INSERT INTO migrations (name) VALUES (?);", migration)
|
|
||||||
except DbError:
|
|
||||||
pgConn.rollbackWithErr "Migration '" & migration & "' failed:\n\t" &
|
|
||||||
getCurrentExceptionMsg()
|
|
||||||
|
|
||||||
migrationsRun.add(migration)
|
|
||||||
|
|
||||||
pgConn.exec(sql"COMMIT")
|
|
||||||
|
|
||||||
return migrationsRun
|
|
||||||
|
|
||||||
proc down*(pgConn: DbConn, config: DbMigrateConfig, migrationsToDown: seq[string]): seq[string] =
|
|
||||||
var migrationsDowned = newSeq[string]()
|
|
||||||
|
|
||||||
pgConn.exec(sql"BEGIN")
|
|
||||||
|
|
||||||
for migration in migrationsToDown:
|
|
||||||
info migration
|
|
||||||
|
|
||||||
let filename = joinPath(config.sqlDir, migration & "-down.sql")
|
|
||||||
|
|
||||||
if not filename.fileExists:
|
|
||||||
pgConn.rollbackWithErr "Can not find DOWN file for " & migration &
|
|
||||||
". Expected '" & filename & "'."
|
|
||||||
|
|
||||||
let statements = filename.readStatements
|
|
||||||
|
|
||||||
try:
|
|
||||||
for statement in statements: pgConn.exec(statement)
|
|
||||||
pgConn.exec(sql"DELETE FROM migrations WHERE name = ?;", migration)
|
|
||||||
except DbError:
|
|
||||||
pgConn.rollbackWithErr "Migration '" & migration & "' failed:\n\t" &
|
|
||||||
getCurrentExceptionMsg()
|
|
||||||
|
|
||||||
migrationsDowned.add(migration)
|
|
||||||
|
|
||||||
pgConn.exec(sql"COMMIT")
|
|
||||||
return migrationsDowned
|
|
||||||
|
|
||||||
when isMainModule:
|
|
||||||
let doc = """
|
|
||||||
Usage:
|
|
||||||
db_migrate [options] create <migration-name>
|
|
||||||
db_migrate [options] up [<count>]
|
|
||||||
db_migrate [options] down [<count>]
|
|
||||||
db_migrate [options] init <schema-name>
|
|
||||||
db_migrate (-V | --version)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
-c --config <config-file> Use the given configuration file (defaults to
|
|
||||||
"database.json").
|
|
||||||
|
|
||||||
-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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
let args = docopt(doc, version = "db-migrate 0.2.0")
|
|
||||||
|
|
||||||
let exitErr = proc(msg: string): void =
|
|
||||||
fatal("db_migrate: " & msg)
|
|
||||||
quit(QuitFailure)
|
|
||||||
|
|
||||||
# Load configuration file
|
|
||||||
let configFilename =
|
|
||||||
if args["--config"]: $args["<config-file>"]
|
|
||||||
else: "database.json"
|
|
||||||
|
|
||||||
var config: DbMigrateConfig
|
|
||||||
try:
|
|
||||||
config = loadConfig(configFilename)
|
|
||||||
except IOError:
|
|
||||||
exitErr "Cannot open config file: " & configFilename
|
|
||||||
except:
|
|
||||||
exitErr "Error parsing config file: " & configFilename & "\L\t" & getCurrentExceptionMsg()
|
|
||||||
|
|
||||||
|
|
||||||
logging.addHandler(newConsoleLogger())
|
|
||||||
if args["--quiet"]: logging.setLogFilter(lvlError)
|
|
||||||
elif args["--very-verbose"]: logging.setLogFilter(lvlAll)
|
|
||||||
elif args["--verbose"]: logging.setlogFilter(lvlDebug)
|
|
||||||
else: logging.setLogFilter(config.logLevel)
|
|
||||||
|
|
||||||
# Check for migrations directory
|
|
||||||
if not existsDir config.sqlDir:
|
|
||||||
try:
|
|
||||||
warn "SQL directory '" & config.sqlDir &
|
|
||||||
"' does not exist and will be created."
|
|
||||||
createDir config.sqlDir
|
|
||||||
except IOError:
|
|
||||||
exitErr "Unable to create directory: " & config.sqlDir & ":\L\T" & getCurrentExceptionMsg()
|
|
||||||
|
|
||||||
# Execute commands
|
|
||||||
if args["create"]:
|
|
||||||
try:
|
|
||||||
let filesCreated = createMigration(config, $args["<migration-name>"])
|
|
||||||
info "Created new migration files:"
|
|
||||||
for filename in filesCreated: info "\t" & filename
|
|
||||||
except IOError:
|
|
||||||
exitErr "Unable to create migration scripts: " & getCurrentExceptionMsg()
|
|
||||||
|
|
||||||
else:
|
|
||||||
let pgConn: DbConn =
|
|
||||||
try: open("", "", "", config.connectionString)
|
|
||||||
except DbError:
|
|
||||||
exitErr "Unable to connect to the database."
|
|
||||||
(DbConn)(nil)
|
|
||||||
|
|
||||||
pgConn.createMigrationsTable
|
|
||||||
|
|
||||||
let (run, notRun, missing) = diffMigrations(pgConn, config)
|
|
||||||
|
|
||||||
# Make sure we have no gaps (database is in an unknown state)
|
|
||||||
if missing.len > 0:
|
|
||||||
exitErr "Database is in an inconsistent state. Migrations have been " &
|
|
||||||
"run that are not sequential."
|
|
||||||
|
|
||||||
if args["up"]:
|
|
||||||
try:
|
|
||||||
let count = if args["<count>"]: parseInt($args["<count>"]) else: high(int)
|
|
||||||
let toRun = if count < notRun.len: notRun[0..<count] else: notRun
|
|
||||||
let migrationsRun = pgConn.up(config, toRun)
|
|
||||||
if count < high(int): info "Went up " & $(migrationsRun.len) & "."
|
|
||||||
except DbError:
|
|
||||||
exitErr "Unable to migrate database: " & getCurrentExceptionMsg()
|
|
||||||
|
|
||||||
elif args["down"]:
|
|
||||||
try:
|
|
||||||
let count = if args["<count>"]: parseInt($args["<count>"]) else: 1
|
|
||||||
let toRun = if count < run.len: run.reversed[0..<count] else: run.reversed
|
|
||||||
let migrationsRun = pgConn.down(config, toRun)
|
|
||||||
info "Went down " & $(migrationsRun.len) & "."
|
|
||||||
except DbError:
|
|
||||||
exitErr "Unable to migrate database: " & getCurrentExceptionMsg()
|
|
||||||
|
|
||||||
elif args["init"]: discard
|
|
||||||
|
|
||||||
let newResults = diffMigrations(pgConn, config)
|
|
||||||
if newResults.notRun.len > 0:
|
|
||||||
info "Database is behind by " & $(newResults.notRun.len) & " migrations."
|
|
||||||
else: info "Database is up to date."
|
|
@ -1,12 +1,12 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
bin = @["db_migrate"]
|
bin = @["db_migrate"]
|
||||||
version = "0.2.0"
|
version = "0.3.2"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Simple tool to handle database migrations."
|
description = "Simple tool to handle database migrations."
|
||||||
license = "BSD"
|
license = "BSD"
|
||||||
|
srcDir = "src/main/nim"
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
requires: @["nim >= 0.13.0", "docopt >= 0.1.0"]
|
requires: @["nim >= 2.0.0", "docopt >= 0.1.0", "db_connector"]
|
||||||
|
|
||||||
|
1
settings.gradle
Normal file
1
settings.gradle
Normal file
@ -0,0 +1 @@
|
|||||||
|
rootProject.name = "db-migrate.groovy"
|
254
src/main/groovy/com/jdblabs/dbmigrate/DbMigrate.groovy
Normal file
254
src/main/groovy/com/jdblabs/dbmigrate/DbMigrate.groovy
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
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
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
public class DbMigrate {
|
||||||
|
|
||||||
|
public static final VERSION = "0.2.5"
|
||||||
|
|
||||||
|
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>]
|
||||||
|
db_migrate [options] init <schema-name>
|
||||||
|
db_migrate (-V | --version)
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
private static sdf = new SimpleDateFormat('yyyyMMddHHmmss')
|
||||||
|
private static Logger LOGGER = LoggerFactory.getLogger(DbMigrate)
|
||||||
|
|
||||||
|
Sql sql
|
||||||
|
File sqlDir
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
|
||||||
|
// Parse arguments
|
||||||
|
def opts = new Docopt(DOC).withVersion("db-migrate.groovy 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")
|
||||||
|
|
||||||
|
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
|
||||||
|
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 sqlDir = new File(givenCfg["sqlDir"] ?: 'migrations')
|
||||||
|
if (!sqlDir.exists() || !sqlDir.isDirectory()) {
|
||||||
|
clilog.error("'{}' does not exist or is not a directory.",
|
||||||
|
sqlDir.canonicalPath)
|
||||||
|
System.exit(1) }
|
||||||
|
|
||||||
|
// Instantiate the DbMigrate instance
|
||||||
|
DbMigrate dbmigrate = new DbMigrate(sqlDir: sqlDir)
|
||||||
|
|
||||||
|
// 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("Created new migration files:\n\t${files.name.join('\n\t')}")
|
||||||
|
return }
|
||||||
|
catch (Exception e) {
|
||||||
|
clilog.error('Unable to create migration scripts.', e)
|
||||||
|
System.exit(1) } }
|
||||||
|
|
||||||
|
// Create the datasource.
|
||||||
|
Properties dsProps = new Properties()
|
||||||
|
dsProps.putAll(givenCfg.findAll { it.key != 'sqlDir' })
|
||||||
|
|
||||||
|
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 List<File> createMigration(String migrationName) {
|
||||||
|
String timestamp = sdf.format(new Date())
|
||||||
|
|
||||||
|
File upFile = new File(sqlDir, "$timestamp-$migrationName-up.sql")
|
||||||
|
File downFile = new File(sqlDir, "$timestamp-$migrationName-down.sql")
|
||||||
|
|
||||||
|
upFile.text = "-- UP script for $migrationName ($timestamp)"
|
||||||
|
downFile.text = "-- DOWN script for $migrationName ($timestamp)"
|
||||||
|
|
||||||
|
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,
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
run_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW())''') }
|
||||||
|
|
||||||
|
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<>()
|
||||||
|
available.addAll(sqlDir
|
||||||
|
.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))
|
||||||
|
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 = [] } }
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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<String> down(Integer count = 1) {
|
||||||
|
createMigrationsTable()
|
||||||
|
def diff = diffMigrations()
|
||||||
|
|
||||||
|
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) }
|
||||||
|
|
||||||
|
private List<String> runMigrations(List<String> toRun, boolean up = true) {
|
||||||
|
List<String> migrationsRun = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
LOGGER.trace("Beginning transaction.")
|
||||||
|
sql.execute('BEGIN')
|
||||||
|
|
||||||
|
toRun.each { migrationName ->
|
||||||
|
LOGGER.info(migrationName)
|
||||||
|
File migrationFile = new File(sqlDir,
|
||||||
|
"$migrationName-${up ? 'up' : 'down'}.sql")
|
||||||
|
|
||||||
|
if (!migrationFile.exists() || !migrationFile.isFile())
|
||||||
|
throw new FileNotFoundException(migrationFile.canonicalPath +
|
||||||
|
"does not exist or is not a regular file.")
|
||||||
|
|
||||||
|
runFile(migrationFile)
|
||||||
|
|
||||||
|
if (up) sql.execute(
|
||||||
|
'INSERT INTO migrations (name) VALUES (?)', migrationName)
|
||||||
|
else sql.execute(
|
||||||
|
'DELETE FROM migrations WHERE name = ?', migrationName)
|
||||||
|
|
||||||
|
migrationsRun << migrationName }
|
||||||
|
|
||||||
|
sql.execute('COMMIT')
|
||||||
|
LOGGER.info('Went {} {} migrations.',
|
||||||
|
up ? 'up' : 'down', migrationsRun.size()) }
|
||||||
|
|
||||||
|
catch (Exception e) { sql.execute('ROLLBACK'); }
|
||||||
|
|
||||||
|
return migrationsRun }
|
||||||
|
|
||||||
|
public void runFile(File file) {
|
||||||
|
LOGGER.trace('Raw statements:\n\n{}\n', file.text.split(/;/).join('\n'))
|
||||||
|
|
||||||
|
List<String> statements = file.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) } }
|
||||||
|
}
|
339
src/main/nim/db_migrate.nim
Normal file
339
src/main/nim/db_migrate.nim
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
## DB Migrate
|
||||||
|
## ==========
|
||||||
|
##
|
||||||
|
## Simple tool to manage database migrations.
|
||||||
|
|
||||||
|
import std/[algorithm, json, logging, os, sequtils, sets, strutils, tables,
|
||||||
|
times]
|
||||||
|
import db_connector/db_postgres
|
||||||
|
import docopt
|
||||||
|
|
||||||
|
type
|
||||||
|
DbMigrateConfig* = object
|
||||||
|
driver, connectionString: string
|
||||||
|
sqlDirs: seq[string]
|
||||||
|
logLevel: Level
|
||||||
|
|
||||||
|
MigrationEntry* = tuple[name, upPath, downPath: string]
|
||||||
|
|
||||||
|
proc ensureMigrationsTableExists(conn: DbConn): void =
|
||||||
|
let tableCount = conn.getValue(sql"""
|
||||||
|
SELECT COUNT(*) FROM information_schema.tables
|
||||||
|
WHERE table_name = 'migrations';""")
|
||||||
|
|
||||||
|
if tableCount.strip == "0":
|
||||||
|
info "Creating the migrations table as it does not already exist."
|
||||||
|
conn.exec(sql("""
|
||||||
|
CREATE TABLE IF NOT EXISTS migrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
run_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW())"""))
|
||||||
|
|
||||||
|
proc rollbackWithErr (pgConn: DbConn, errMsg: string): void =
|
||||||
|
pgConn.exec(sql"ROLLBACK")
|
||||||
|
dbError(errMsg)
|
||||||
|
|
||||||
|
proc loadConfig*(filename: string): DbMigrateConfig =
|
||||||
|
## Load DbMigrateConfig from a file.
|
||||||
|
let cfg = json.parseFile(filename)
|
||||||
|
|
||||||
|
var logLevel: Level = lvlInfo
|
||||||
|
if existsEnv("DBM_LOG_LEVEL"):
|
||||||
|
let idx = find(LevelNames, $getEnv("DBM_LOG_LEVEL").toUpper)
|
||||||
|
logLevel = if idx == -1: lvlInfo else: (Level)(idx)
|
||||||
|
elif cfg.hasKey("logLevel"):
|
||||||
|
let idx = find(LevelNames, cfg["logLevel"].getStr.toUpper)
|
||||||
|
logLevel = if idx == -1: lvlInfo else: (Level)(idx)
|
||||||
|
|
||||||
|
return DbMigrateConfig(
|
||||||
|
driver:
|
||||||
|
if existsEnv("DATABASE_DRIVER"): $getEnv("DATABASE_DRIVER")
|
||||||
|
elif cfg.hasKey("driver"): cfg["driver"].getStr
|
||||||
|
else: "postres",
|
||||||
|
connectionString:
|
||||||
|
if existsEnv("DATABASE_URL"): $getEnv("DATABASE_URL")
|
||||||
|
elif cfg.hasKey("connectionString"): cfg["connectionString"].getStr
|
||||||
|
else: "",
|
||||||
|
sqlDirs:
|
||||||
|
if existsEnv("MIGRATIONS_DIRS"): getEnv("MIGRATIONS_DIRS").split(';')
|
||||||
|
elif cfg.hasKey("sqlDirs"): cfg["sqlDirs"].getElems.mapIt(it.getStr)
|
||||||
|
else: @["migrations"],
|
||||||
|
logLevel: logLevel)
|
||||||
|
|
||||||
|
proc createMigration*(config: DbMigrateConfig, migrationName: string): MigrationEntry =
|
||||||
|
## Create a new set of database migration files.
|
||||||
|
let timestamp = now().format("yyyyMMddHHmmss")
|
||||||
|
let filenamePrefix = timestamp & "-" & migrationName
|
||||||
|
|
||||||
|
let migration = (
|
||||||
|
name: filenamePrefix & "-up.sql",
|
||||||
|
upPath: joinPath(config.sqlDirs[0], filenamePrefix & "-up.sql"),
|
||||||
|
downPath: joinPath(config.sqlDirs[0], filenamePrefix & "-down.sql"))
|
||||||
|
|
||||||
|
let scriptDesc = migrationName & " (" & timestamp & ")"
|
||||||
|
|
||||||
|
let upFile = open(migration.upPath, fmWrite)
|
||||||
|
let downFile = open(migration.downPath, fmWrite)
|
||||||
|
|
||||||
|
upFile.writeLine "-- UP script for " & scriptDesc
|
||||||
|
downFile.writeLine "-- DOWN script for " & scriptDesc
|
||||||
|
|
||||||
|
upFile.close()
|
||||||
|
downFile.close()
|
||||||
|
|
||||||
|
return migration
|
||||||
|
|
||||||
|
proc diffMigrations*(
|
||||||
|
pgConn: DbConn,
|
||||||
|
config: DbMigrateConfig
|
||||||
|
): tuple[
|
||||||
|
available: TableRef[string, MigrationEntry],
|
||||||
|
run: seq[string],
|
||||||
|
notRun, missing: seq[MigrationEntry] ] =
|
||||||
|
|
||||||
|
debug "diffMigrations: inspecting database and configured directories " &
|
||||||
|
"for migrations"
|
||||||
|
|
||||||
|
# Query the database to find out what migrations have been run.
|
||||||
|
var migrationsRun = initHashSet[string]()
|
||||||
|
|
||||||
|
for row in pgConn.fastRows(sql"SELECT * FROM migrations ORDER BY name", @[]):
|
||||||
|
migrationsRun.incl(row[1])
|
||||||
|
|
||||||
|
# Inspect the filesystem to see what migrations are available.
|
||||||
|
var migrationsAvailable = newTable[string, MigrationEntry]()
|
||||||
|
for sqlDir in config.sqlDirs:
|
||||||
|
debug "Looking in " & sqlDir
|
||||||
|
for filePath in walkFiles(joinPath(sqlDir, "*.sql")):
|
||||||
|
debug "Saw migration file: " & filePath
|
||||||
|
var migrationName = filePath.extractFilename
|
||||||
|
migrationName.removeSuffix("-up.sql")
|
||||||
|
migrationName.removeSuffix("-down.sql")
|
||||||
|
migrationsAvailable[migrationName] = (
|
||||||
|
name: migrationName,
|
||||||
|
upPath: joinPath(sqlDir, migrationName) & "-up.sql",
|
||||||
|
downPath: joinPath(sqlDir, migrationName) & "-down.sql")
|
||||||
|
|
||||||
|
# Diff with the list of migrations that we have in our migrations
|
||||||
|
# directory.
|
||||||
|
let migrationsInOrder =
|
||||||
|
toSeq(migrationsAvailable.keys).sorted(system.cmp)
|
||||||
|
|
||||||
|
var migrationsNotRun = newSeq[MigrationEntry]()
|
||||||
|
var missingMigrations = newSeq[MigrationEntry]()
|
||||||
|
|
||||||
|
for migName in migrationsInOrder:
|
||||||
|
if not migrationsRun.contains(migName):
|
||||||
|
migrationsNotRun.add(migrationsAvailable[migName])
|
||||||
|
|
||||||
|
# if we've already seen some migrations that have not been run, but this
|
||||||
|
# one has been, that means we have a gap and are missing migrations
|
||||||
|
elif migrationsNotRun.len > 0:
|
||||||
|
missingMigrations.add(migrationsNotRun)
|
||||||
|
migrationsNotRun = newSeq[MigrationEntry]()
|
||||||
|
|
||||||
|
result = (available: migrationsAvailable,
|
||||||
|
run: toSeq(migrationsRun.items).sorted(system.cmp),
|
||||||
|
notRun: migrationsNotRun,
|
||||||
|
missing: missingMigrations)
|
||||||
|
|
||||||
|
debug "diffMigration: Results" &
|
||||||
|
"\n\tavailable: " & $toSeq(result[0].keys) &
|
||||||
|
"\n\trun: " & $result[1] &
|
||||||
|
"\n\tnotRun: " & $(result[2].mapIt(it.name)) &
|
||||||
|
"\n\tmissing: " & $(result[3].mapIt(it.name))
|
||||||
|
|
||||||
|
|
||||||
|
proc readStatements*(filename: string): seq[SqlQuery] =
|
||||||
|
result = @[]
|
||||||
|
var stmt: string = ""
|
||||||
|
|
||||||
|
for line in filename.lines:
|
||||||
|
let l = line.strip
|
||||||
|
if l.len == 0 or l.startsWith("--"): continue
|
||||||
|
|
||||||
|
let parts = line.split(';')
|
||||||
|
stmt &= "\n" & parts[0]
|
||||||
|
|
||||||
|
if parts.len > 1:
|
||||||
|
result.add(sql(stmt & ";"))
|
||||||
|
stmt = parts[1] & "";
|
||||||
|
|
||||||
|
if stmt.strip.len > 0: result.add(sql(stmt))
|
||||||
|
|
||||||
|
proc up*(
|
||||||
|
pgConn: DbConn,
|
||||||
|
config: DbMigrateConfig,
|
||||||
|
toRun: seq[MigrationEntry]): seq[MigrationEntry] =
|
||||||
|
var migrationsRun = newSeq[MigrationEntry]()
|
||||||
|
# Begin a transaction.
|
||||||
|
pgConn.exec(sql"BEGIN")
|
||||||
|
|
||||||
|
# Apply each of the migrations.
|
||||||
|
for migration in toRun:
|
||||||
|
info migration.name
|
||||||
|
|
||||||
|
if not migration.upPath.fileExists:
|
||||||
|
pgConn.rollbackWithErr "Can not find UP file for " & migration.name &
|
||||||
|
". Expected '" & migration.upPath & "'."
|
||||||
|
|
||||||
|
let statements = migration.upPath.readStatements
|
||||||
|
|
||||||
|
try:
|
||||||
|
for statement in statements:
|
||||||
|
pgConn.exec(statement)
|
||||||
|
pgConn.exec(sql"INSERT INTO migrations (name) VALUES (?);", migration.name)
|
||||||
|
except DbError:
|
||||||
|
pgConn.rollbackWithErr "Migration '" & migration.name & "' failed:\n\t" &
|
||||||
|
getCurrentExceptionMsg()
|
||||||
|
|
||||||
|
migrationsRun.add(migration)
|
||||||
|
|
||||||
|
pgConn.exec(sql"COMMIT")
|
||||||
|
|
||||||
|
return migrationsRun
|
||||||
|
|
||||||
|
proc down*(
|
||||||
|
pgConn: DbConn,
|
||||||
|
config: DbMigrateConfig,
|
||||||
|
migrationsToDown: seq[MigrationEntry]): seq[MigrationEntry] =
|
||||||
|
var migrationsDowned = newSeq[MigrationEntry]()
|
||||||
|
|
||||||
|
pgConn.exec(sql"BEGIN")
|
||||||
|
|
||||||
|
for migration in migrationsToDown:
|
||||||
|
info migration.name
|
||||||
|
|
||||||
|
if not migration.downPath.fileExists:
|
||||||
|
pgConn.rollbackWithErr "Can not find DOWN file for " & migration.name &
|
||||||
|
". Expected '" & migration.downPath & "'."
|
||||||
|
|
||||||
|
let statements = migration.downPath.readStatements
|
||||||
|
|
||||||
|
try:
|
||||||
|
for statement in statements: pgConn.exec(statement)
|
||||||
|
pgConn.exec(sql"DELETE FROM migrations WHERE name = ?;", migration.name)
|
||||||
|
except DbError:
|
||||||
|
pgConn.rollbackWithErr "Migration '" & migration.name & "' failed:\n\t" &
|
||||||
|
getCurrentExceptionMsg()
|
||||||
|
|
||||||
|
migrationsDowned.add(migration)
|
||||||
|
|
||||||
|
pgConn.exec(sql"COMMIT")
|
||||||
|
return migrationsDowned
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
let doc = """
|
||||||
|
Usage:
|
||||||
|
db_migrate [options] create <migration-name>
|
||||||
|
db_migrate [options] up [<count>]
|
||||||
|
db_migrate [options] down [<count>]
|
||||||
|
db_migrate [options] init <schema-name>
|
||||||
|
db_migrate (-V | --version)
|
||||||
|
db_migrate (-h | --help)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-c --config <config-file> Use the given configuration file (defaults to
|
||||||
|
"database.json").
|
||||||
|
|
||||||
|
-h --help Show this usage information.
|
||||||
|
|
||||||
|
-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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
let args = docopt(doc, version = "db-migrate (Nim) 0.3.2\nhttps://git.jdb-software.com/jdb/db-migrate")
|
||||||
|
|
||||||
|
let exitErr = proc(msg: string): void =
|
||||||
|
fatal("db_migrate: " & msg)
|
||||||
|
quit(QuitFailure)
|
||||||
|
|
||||||
|
# Load configuration file
|
||||||
|
let configFilename =
|
||||||
|
if args["--config"]: $args["--config"]
|
||||||
|
else: "database.json"
|
||||||
|
|
||||||
|
var config: DbMigrateConfig
|
||||||
|
try:
|
||||||
|
config = loadConfig(configFilename)
|
||||||
|
except IOError:
|
||||||
|
writeLine(stderr, "db_migrate: Cannot open config file: " & configFilename)
|
||||||
|
except:
|
||||||
|
writeLine(stderr, "db_migrate: Error parsing config file: " &
|
||||||
|
configFilename & "\L\t" & getCurrentExceptionMsg())
|
||||||
|
|
||||||
|
|
||||||
|
logging.addHandler(newConsoleLogger())
|
||||||
|
if args["--quiet"]: logging.setLogFilter(lvlError)
|
||||||
|
elif args["--very-verbose"]: logging.setLogFilter(lvlAll)
|
||||||
|
elif args["--verbose"]: logging.setlogFilter(lvlDebug)
|
||||||
|
else: logging.setLogFilter(config.logLevel)
|
||||||
|
|
||||||
|
# Check for migrations directory
|
||||||
|
for sqlDir in config.sqlDirs:
|
||||||
|
if not dirExists sqlDir:
|
||||||
|
try:
|
||||||
|
warn "SQL directory '" & sqlDir &
|
||||||
|
"' does not exist and will be created."
|
||||||
|
createDir sqlDir
|
||||||
|
except IOError:
|
||||||
|
exitErr "Unable to create directory: " & sqlDir & ":\L\T" & getCurrentExceptionMsg()
|
||||||
|
|
||||||
|
# Execute commands
|
||||||
|
if args["create"]:
|
||||||
|
try:
|
||||||
|
let newMigration = createMigration(config, $args["<migration-name>"])
|
||||||
|
info "Created new migration files:"
|
||||||
|
info "\t" & newMigration.upPath
|
||||||
|
info "\t" & newMigration.downPath
|
||||||
|
except IOError:
|
||||||
|
exitErr "Unable to create migration scripts: " & getCurrentExceptionMsg()
|
||||||
|
|
||||||
|
else:
|
||||||
|
let pgConn: DbConn =
|
||||||
|
try: open("", "", "", config.connectionString)
|
||||||
|
except DbError:
|
||||||
|
exitErr "Unable to connect to the database: " & getCurrentExceptionMsg()
|
||||||
|
(DbConn)(nil)
|
||||||
|
|
||||||
|
pgConn.ensureMigrationsTableExists
|
||||||
|
|
||||||
|
let (available, run, notRun, missing) = diffMigrations(pgConn, config)
|
||||||
|
|
||||||
|
# Make sure we have no gaps (database is in an unknown state)
|
||||||
|
if missing.len > 0:
|
||||||
|
exitErr "Database is in an inconsistent state. Migrations have been " &
|
||||||
|
"run that are not sequential."
|
||||||
|
|
||||||
|
if args["up"]:
|
||||||
|
try:
|
||||||
|
let count = if args["<count>"]: parseInt($args["<count>"]) else: high(int)
|
||||||
|
let toRun = if count < notRun.len: notRun[0..<count] else: notRun
|
||||||
|
let migrationsRun = pgConn.up(config, toRun)
|
||||||
|
if count < high(int): info "Went up " & $(migrationsRun.len) & "."
|
||||||
|
except DbError:
|
||||||
|
exitErr "Unable to migrate database: " & getCurrentExceptionMsg()
|
||||||
|
|
||||||
|
elif args["down"]:
|
||||||
|
try:
|
||||||
|
let count = if args["<count>"]: parseInt($args["<count>"]) else: 1
|
||||||
|
let toRunNames = if count < run.len: run.reversed[0..<count] else: run.reversed
|
||||||
|
let toRun = toRunNames.mapIt(available[it])
|
||||||
|
let migrationsRun = pgConn.down(config, toRun)
|
||||||
|
info "Went down " & $(migrationsRun.len) & "."
|
||||||
|
except DbError:
|
||||||
|
exitErr "Unable to migrate database: " & getCurrentExceptionMsg()
|
||||||
|
|
||||||
|
elif args["init"]: discard
|
||||||
|
|
||||||
|
let newResults = diffMigrations(pgConn, config)
|
||||||
|
if newResults.notRun.len > 0:
|
||||||
|
info "Database is behind by " & $(newResults.notRun.len) & " migrations."
|
||||||
|
else: info "Database is up to date."
|
17
src/main/resources/logback.groovy
Normal file
17
src/main/resources/logback.groovy
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user