2016-01-31 00:18:21 -06:00
|
|
|
## DB Migrate
|
|
|
|
## ==========
|
|
|
|
##
|
|
|
|
## Simple tool to manage database migrations.
|
|
|
|
|
2016-01-31 12:27:37 -06:00
|
|
|
import json, times, os, strutils, docopt, db_postgres, db_mysql, db_sqlite
|
2016-01-31 00:18:21 -06:00
|
|
|
|
|
|
|
type DbMigrateConfig* = tuple[ driver, sqlDir, connectionString: string ]
|
|
|
|
|
|
|
|
proc loadConfig*(filename: string): DbMigrateConfig =
|
2016-01-31 11:54:37 -06:00
|
|
|
## Load DbMigrateConfig from a file.
|
2016-01-31 00:18:21 -06:00
|
|
|
let cfg = json.parseFile(filename)
|
2016-01-31 11:54:37 -06:00
|
|
|
|
|
|
|
return (
|
2016-01-31 00:18:21 -06:00
|
|
|
driver: if cfg.hasKey("driver"): cfg["driver"].getStr else: "postres",
|
|
|
|
sqlDir: if cfg.hasKey("sqlDir"): cfg["sqlDir"].getStr else: "migrations",
|
|
|
|
connectionString: cfg["connectionString"].getStr)
|
|
|
|
|
2016-01-31 12:27:37 -06:00
|
|
|
proc getDbConnection*(config: DbMigrateConfig): DbConn =
|
|
|
|
case config.driver
|
|
|
|
of "postgres": discard
|
|
|
|
of "sqlite": discard
|
|
|
|
of "mysql": discard
|
|
|
|
else: dbError "Unsupported database driver: " & config.driver
|
|
|
|
|
|
|
|
proc createMigration*(config: DbMigrateConfig, migrationName: string): seq[string] =
|
2016-01-31 11:54:37 -06:00
|
|
|
## Create a new set of database migration files.
|
2016-01-31 00:18:21 -06:00
|
|
|
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]
|
|
|
|
|
|
|
|
when isMainModule:
|
|
|
|
let doc = """
|
|
|
|
Usage:
|
|
|
|
db-migrate [options] create <migration-name>
|
2016-01-31 12:27:37 -06:00
|
|
|
db-migrate [options] up [<count>]
|
|
|
|
db-migrate [options] down [<count>]
|
|
|
|
db-migrate [options] init <schema-name>
|
2016-01-31 00:18:21 -06:00
|
|
|
|
|
|
|
Options:
|
|
|
|
-c --config <config-file> Use the given configuration file (defaults to
|
|
|
|
"database.json").
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Parse arguments
|
|
|
|
let args = docopt(doc, version = "db-migrate 0.1.0")
|
|
|
|
|
2016-01-31 11:54:37 -06:00
|
|
|
let exitErr = proc(msg: string): void =
|
|
|
|
stderr.writeLine("db_migrate: " & msg)
|
|
|
|
quit(QuitFailure)
|
|
|
|
|
2016-01-31 00:18:21 -06:00
|
|
|
# Load configuration file
|
|
|
|
let configFilename =
|
|
|
|
if args["--config"]: $args["--config"]
|
|
|
|
else: "database.json"
|
|
|
|
|
2016-01-31 11:54:37 -06:00
|
|
|
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()
|
|
|
|
|
|
|
|
# Check for migrations directory
|
|
|
|
if not existsDir config.sqlDir:
|
|
|
|
try:
|
|
|
|
echo "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()
|
2016-01-31 00:18:21 -06:00
|
|
|
|
|
|
|
# Execute commands
|
|
|
|
if args["create"]:
|
2016-01-31 11:54:37 -06:00
|
|
|
try:
|
|
|
|
let filesCreated = createMigration(config, $args["<migration-name>"])
|
|
|
|
echo "Created new migration files:"
|
|
|
|
for filename in filesCreated: echo "\t" & filename
|
|
|
|
except IOError:
|
|
|
|
exitErr "Unable to create migration scripts: " & getCurrentExceptionMsg()
|
|
|
|
|
2016-01-31 12:27:37 -06:00
|
|
|
elif args["up"]: discard
|
|
|
|
# Query the database to find out what migrations have been run.
|
|
|
|
|
|
|
|
# Diff with the list of migrations that we have in our migrations
|
|
|
|
# directory.
|
|
|
|
|
|
|
|
# Make sure we have no gaps (database is in an unknown state)
|
|
|
|
|
|
|
|
# Find the subset of migrations we need to apply (consider the count
|
|
|
|
# parameter if passed)
|
|
|
|
|
|
|
|
# If none: "Up to date."
|
|
|
|
|
|
|
|
# Begin a transaction.
|
|
|
|
|
|
|
|
# Apply each of the migrations.
|
|
|
|
# If any fail, roll back the transaction
|
|
|
|
# Otherwise report success
|
|
|
|
|
|
|
|
elif args["down"]: discard
|
|
|
|
# Query the database to find out what migrations have been run.
|
|
|
|
|
|
|
|
# Find how many we need to go down (default to 1 if the count parameter was
|
|
|
|
# not passed.
|
|
|
|
|
|
|
|
# Begin transaction
|
|
|
|
|
|
|
|
# Apply each down script
|
|
|
|
# If any fail, roll back the transaction
|
|
|
|
# Otherwise report success
|
|
|
|
|
|
|
|
elif args["init"]: discard
|
|
|
|
|