Compare commits

..

No commits in common. "main" and "0.2.8" have entirely different histories.
main ... 0.2.8

2 changed files with 65 additions and 102 deletions

View File

@ -1,7 +1,7 @@
# Package
bin = @["db_migrate"]
version = "0.3.2"
version = "0.2.8"
author = "Jonathan Bernard"
description = "Simple tool to handle database migrations."
license = "BSD"
@ -9,4 +9,4 @@ srcDir = "src/main/nim"
# Dependencies
requires: @["nim >= 2.0.0", "docopt >= 0.1.0", "db_connector"]
requires: @["nim >= 1.4.0", "docopt >= 0.1.0"]

View File

@ -3,18 +3,11 @@
##
## Simple tool to manage database migrations.
import std/[algorithm, json, logging, os, sequtils, sets, strutils, tables,
times]
import db_connector/db_postgres
import docopt
import algorithm, db_postgres, docopt, json, logging, os, sequtils, sets,
strutils, times
type
DbMigrateConfig* = object
driver, connectionString: string
sqlDirs: seq[string]
logLevel: Level
MigrationEntry* = tuple[name, upPath, downPath: string]
DbMigrateConfig* = tuple[ driver, sqlDir, connectionString: string, logLevel: Level ]
proc ensureMigrationsTableExists(conn: DbConn): void =
let tableCount = conn.getValue(sql"""
@ -45,35 +38,33 @@ proc loadConfig*(filename: string): DbMigrateConfig =
let idx = find(LevelNames, cfg["logLevel"].getStr.toUpper)
logLevel = if idx == -1: lvlInfo else: (Level)(idx)
return DbMigrateConfig(
return (
driver:
if existsEnv("DATABASE_DRIVER"): $getEnv("DATABASE_DRIVER")
elif cfg.hasKey("driver"): cfg["driver"].getStr
else: "postres",
sqlDir:
if existsEnv("MIGRATIONS_DIR"): $getEnv("MIGRATIONS_DIR")
elif cfg.hasKey("sqlDir"): cfg["sqlDir"].getStr
else: "migrations",
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 =
proc createMigration*(config: DbMigrateConfig, migrationName: string): seq[string] =
## 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 upFilename = joinPath(config.sqlDir, filenamePrefix & "-up.sql")
let downFilename = joinPath(config.sqlDir, filenamePrefix & "-down.sql")
let scriptDesc = migrationName & " (" & timestamp & ")"
let upFile = open(migration.upPath, fmWrite)
let downFile = open(migration.downPath, fmWrite)
let upFile = open(upFilename, fmWrite)
let downFile = open(downFilename, fmWrite)
upFile.writeLine "-- UP script for " & scriptDesc
downFile.writeLine "-- DOWN script for " & scriptDesc
@ -81,18 +72,10 @@ proc createMigration*(config: DbMigrateConfig, migrationName: string): Migration
upFile.close()
downFile.close()
return migration
return @[upFilename, downFilename]
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"
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 = initHashSet[string]()
@ -101,49 +84,35 @@ proc diffMigrations*(
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 migrationsAvailable = initHashSet[string]()
for filePath in walkFiles(joinPath(config.sqlDir, "*.sql")):
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")
migrationsAvailable.incl(migrationName)
# Diff with the list of migrations that we have in our migrations
# directory.
let migrationsInOrder =
toSeq(migrationsAvailable.keys).sorted(system.cmp)
toSeq(migrationsAvailable.items).sorted(system.cmp)
var migrationsNotRun = newSeq[MigrationEntry]()
var missingMigrations = newSeq[MigrationEntry]()
var migrationsNotRun = newSeq[string]()
var missingMigrations = newSeq[string]()
for migName in migrationsInOrder:
if not migrationsRun.contains(migName):
migrationsNotRun.add(migrationsAvailable[migName])
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[MigrationEntry]()
migrationsNotRun = newSeq[string]()
result = (available: migrationsAvailable,
run: toSeq(migrationsRun.items).sorted(system.cmp),
return (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 = ""
@ -161,30 +130,28 @@ proc readStatements*(filename: string): seq[SqlQuery] =
if stmt.strip.len > 0: result.add(sql(stmt))
proc up*(
pgConn: DbConn,
config: DbMigrateConfig,
toRun: seq[MigrationEntry]): seq[MigrationEntry] =
var migrationsRun = newSeq[MigrationEntry]()
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.name
info migration
let filename = joinPath(config.sqlDir, migration & "-up.sql")
if not migration.upPath.fileExists:
pgConn.rollbackWithErr "Can not find UP file for " & migration.name &
". Expected '" & migration.upPath & "'."
if not filename.fileExists:
pgConn.rollbackWithErr "Can not find UP file for " & migration &
". Expected '" & filename & "'."
let statements = migration.upPath.readStatements
let statements = filename.readStatements
try:
for statement in statements:
pgConn.exec(statement)
pgConn.exec(sql"INSERT INTO migrations (name) VALUES (?);", migration.name)
pgConn.exec(sql"INSERT INTO migrations (name) VALUES (?);", migration)
except DbError:
pgConn.rollbackWithErr "Migration '" & migration.name & "' failed:\n\t" &
pgConn.rollbackWithErr "Migration '" & migration & "' failed:\n\t" &
getCurrentExceptionMsg()
migrationsRun.add(migration)
@ -193,28 +160,27 @@ proc up*(
return migrationsRun
proc down*(
pgConn: DbConn,
config: DbMigrateConfig,
migrationsToDown: seq[MigrationEntry]): seq[MigrationEntry] =
var migrationsDowned = newSeq[MigrationEntry]()
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.name
info migration
if not migration.downPath.fileExists:
pgConn.rollbackWithErr "Can not find DOWN file for " & migration.name &
". Expected '" & migration.downPath & "'."
let filename = joinPath(config.sqlDir, migration & "-down.sql")
let statements = migration.downPath.readStatements
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.name)
pgConn.exec(sql"DELETE FROM migrations WHERE name = ?;", migration)
except DbError:
pgConn.rollbackWithErr "Migration '" & migration.name & "' failed:\n\t" &
pgConn.rollbackWithErr "Migration '" & migration & "' failed:\n\t" &
getCurrentExceptionMsg()
migrationsDowned.add(migration)
@ -249,7 +215,7 @@ Options:
"""
# Parse arguments
let args = docopt(doc, version = "db-migrate (Nim) 0.3.2\nhttps://git.jdb-software.com/jdb/db-migrate")
let args = docopt(doc, version = "db-migrate (Nim) 0.2.8\nhttps://git.jdb-software.com/jdb/db-migrate")
let exitErr = proc(msg: string): void =
fatal("db_migrate: " & msg)
@ -277,22 +243,20 @@ Options:
else: logging.setLogFilter(config.logLevel)
# Check for migrations directory
for sqlDir in config.sqlDirs:
if not dirExists sqlDir:
if not dirExists config.sqlDir:
try:
warn "SQL directory '" & sqlDir &
warn "SQL directory '" & config.sqlDir &
"' does not exist and will be created."
createDir sqlDir
createDir config.sqlDir
except IOError:
exitErr "Unable to create directory: " & sqlDir & ":\L\T" & getCurrentExceptionMsg()
exitErr "Unable to create directory: " & config.sqlDir & ":\L\T" & getCurrentExceptionMsg()
# Execute commands
if args["create"]:
try:
let newMigration = createMigration(config, $args["<migration-name>"])
let filesCreated = createMigration(config, $args["<migration-name>"])
info "Created new migration files:"
info "\t" & newMigration.upPath
info "\t" & newMigration.downPath
for filename in filesCreated: info "\t" & filename
except IOError:
exitErr "Unable to create migration scripts: " & getCurrentExceptionMsg()
@ -305,7 +269,7 @@ Options:
pgConn.ensureMigrationsTableExists
let (available, run, notRun, missing) = diffMigrations(pgConn, config)
let (run, notRun, missing) = diffMigrations(pgConn, config)
# Make sure we have no gaps (database is in an unknown state)
if missing.len > 0:
@ -324,8 +288,7 @@ Options:
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 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: