|
|
|
@@ -3,8 +3,10 @@
|
|
|
|
|
##
|
|
|
|
|
## Simple tool to manage database migrations.
|
|
|
|
|
|
|
|
|
|
import algorithm, db_postgres, docopt, json, logging, os, sequtils, sets,
|
|
|
|
|
strutils, tables, times
|
|
|
|
|
import std/[algorithm, json, logging, os, sequtils, sets, strutils, tables,
|
|
|
|
|
times]
|
|
|
|
|
import db_connector/db_postgres
|
|
|
|
|
import docopt
|
|
|
|
|
|
|
|
|
|
type
|
|
|
|
|
DbMigrateConfig* = object
|
|
|
|
@@ -89,6 +91,9 @@ proc diffMigrations*(
|
|
|
|
|
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]()
|
|
|
|
|
|
|
|
|
@@ -98,7 +103,9 @@ proc diffMigrations*(
|
|
|
|
|
# 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")
|
|
|
|
@@ -125,11 +132,18 @@ proc diffMigrations*(
|
|
|
|
|
missingMigrations.add(migrationsNotRun)
|
|
|
|
|
migrationsNotRun = newSeq[MigrationEntry]()
|
|
|
|
|
|
|
|
|
|
return (available: migrationsAvailable,
|
|
|
|
|
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 = ""
|
|
|
|
@@ -157,7 +171,7 @@ proc up*(
|
|
|
|
|
|
|
|
|
|
# Apply each of the migrations.
|
|
|
|
|
for migration in toRun:
|
|
|
|
|
info migration.name
|
|
|
|
|
info "Applying up script for " & migration.name
|
|
|
|
|
|
|
|
|
|
if not migration.upPath.fileExists:
|
|
|
|
|
pgConn.rollbackWithErr "Can not find UP file for " & migration.name &
|
|
|
|
@@ -188,7 +202,7 @@ proc down*(
|
|
|
|
|
pgConn.exec(sql"BEGIN")
|
|
|
|
|
|
|
|
|
|
for migration in migrationsToDown:
|
|
|
|
|
info migration.name
|
|
|
|
|
info "Applying down script for " & migration.name
|
|
|
|
|
|
|
|
|
|
if not migration.downPath.fileExists:
|
|
|
|
|
pgConn.rollbackWithErr "Can not find DOWN file for " & migration.name &
|
|
|
|
@@ -215,6 +229,7 @@ Usage:
|
|
|
|
|
db_migrate [options] up [<count>]
|
|
|
|
|
db_migrate [options] down [<count>]
|
|
|
|
|
db_migrate [options] init <schema-name>
|
|
|
|
|
db_migrate [options] status
|
|
|
|
|
db_migrate (-V | --version)
|
|
|
|
|
db_migrate (-h | --help)
|
|
|
|
|
|
|
|
|
@@ -235,7 +250,7 @@ Options:
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# Parse arguments
|
|
|
|
|
let args = docopt(doc, version = "db-migrate (Nim) 0.3.0\nhttps://git.jdb-software.com/jdb/db-migrate")
|
|
|
|
|
let args = docopt(doc, version = "db-migrate (Nim) 0.4.1\nhttps://git.jdb-software.com/jdb/db-migrate")
|
|
|
|
|
|
|
|
|
|
let exitErr = proc(msg: string): void =
|
|
|
|
|
fatal("db_migrate: " & msg)
|
|
|
|
@@ -293,12 +308,12 @@ Options:
|
|
|
|
|
|
|
|
|
|
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"]:
|
|
|
|
|
# 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."
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let count = if args["<count>"]: parseInt($args["<count>"]) else: high(int)
|
|
|
|
|
let toRun = if count < notRun.len: notRun[0..<count] else: notRun
|
|
|
|
@@ -317,9 +332,29 @@ Options:
|
|
|
|
|
except DbError:
|
|
|
|
|
exitErr "Unable to migrate database: " & getCurrentExceptionMsg()
|
|
|
|
|
|
|
|
|
|
elif args["status"]:
|
|
|
|
|
|
|
|
|
|
info "Database Migration Status" &
|
|
|
|
|
"\n\nSQL Migration Folders: " & join(config.sqlDirs.mapIt("\n " & it), "") &
|
|
|
|
|
"\n\nMigrations: " &
|
|
|
|
|
"\n available: " & join(
|
|
|
|
|
toSeq(available.keys)
|
|
|
|
|
.sorted(system.cmp)
|
|
|
|
|
.mapIt("\n " & it),
|
|
|
|
|
"") &
|
|
|
|
|
"\n run: " & join(run.mapIt("\n " & it), "") &
|
|
|
|
|
"\n notRun: " & join(notRun.mapIt("\n " & it.name), "") &
|
|
|
|
|
"\n missing: " & join(missing.mapIt("\n " & it.name), "") &
|
|
|
|
|
"\n"
|
|
|
|
|
|
|
|
|
|
elif args["init"]: discard
|
|
|
|
|
|
|
|
|
|
let newResults = diffMigrations(pgConn, config)
|
|
|
|
|
|
|
|
|
|
if newResults.missing.len > 0:
|
|
|
|
|
exitErr "Database is in an inconsistent state. Migrations have been " &
|
|
|
|
|
"run that are not sequential."
|
|
|
|
|
|
|
|
|
|
if newResults.notRun.len > 0:
|
|
|
|
|
info "Database is behind by " & $(newResults.notRun.len) & " migrations."
|
|
|
|
|
else: info "Database is up to date."
|
|
|
|
|