Added UP command (not working).

This commit is contained in:
Jonathan Bernard 2016-02-06 23:28:35 -06:00
parent 3963a27a66
commit efc5dd2ed9

View File

@ -1,130 +1,207 @@
## DB Migrate ## DB Migrate
## ========== ## ==========
## ##
## Simple tool to manage database migrations. ## Simple tool to manage database migrations.
import json, times, os, strutils, docopt, db_postgres, db_mysql, db_sqlite import algorithm, json, times, os, strutils, docopt, db_postgres, sets,
sequtils
type DbMigrateConfig* = tuple[ driver, sqlDir, connectionString: string ]
type
proc loadConfig*(filename: string): DbMigrateConfig = LogLevel = enum quiet, normal, verbose, very_verbose
## Load DbMigrateConfig from a file. DbMigrateConfig* = tuple[ driver, sqlDir, connectionString: string, logLevel: LogLevel ]
let cfg = json.parseFile(filename)
proc createMigrationsTable(conn: DbConn): void =
return ( conn.exec(sql("""
driver: if cfg.hasKey("driver"): cfg["driver"].getStr else: "postres", CREATE TABLE IF NOT EXISTS migrations (
sqlDir: if cfg.hasKey("sqlDir"): cfg["sqlDir"].getStr else: "migrations", id SERIAL PRIMARY KEY,
connectionString: cfg["connectionString"].getStr) name VARCHAR NOT NULL,
run_at TIMESTAMP NOT NULL DEFAULT NOW())"""))
proc getDbConnection*(config: DbMigrateConfig): DbConn =
case config.driver proc loadConfig*(filename: string): DbMigrateConfig =
of "postgres": discard ## Load DbMigrateConfig from a file.
of "sqlite": discard let cfg = json.parseFile(filename)
of "mysql": discard
else: dbError "Unsupported database driver: " & config.driver return (
driver: if cfg.hasKey("driver"): cfg["driver"].getStr else: "postres",
proc createMigration*(config: DbMigrateConfig, migrationName: string): seq[string] = sqlDir: if cfg.hasKey("sqlDir"): cfg["sqlDir"].getStr else: "migrations",
## Create a new set of database migration files. connectionString: cfg["connectionString"].getStr,
let timestamp = getTime().getLocalTime().format("yyyyMMddHHmmss") logLevel: if cfg.hasKey("logLevel"): parseEnum[LogLevel](cfg["logLevel"].getStr) else: normal)
let filenamePrefix = timestamp & "-" & migrationName
proc createMigration*(config: DbMigrateConfig, migrationName: string): seq[string] =
let upFilename = joinPath(config.sqlDir, filenamePrefix & "-up.sql") ## Create a new set of database migration files.
let downFilename = joinPath(config.sqlDir, filenamePrefix & "-down.sql") let timestamp = getTime().getLocalTime().format("yyyyMMddHHmmss")
let filenamePrefix = timestamp & "-" & migrationName
let scriptDesc = migrationName & " (" & timestamp & ")"
let upFilename = joinPath(config.sqlDir, filenamePrefix & "-up.sql")
let upFile = open(upFilename, fmWrite) let downFilename = joinPath(config.sqlDir, filenamePrefix & "-down.sql")
let downFile = open(downFilename, fmWrite)
let scriptDesc = migrationName & " (" & timestamp & ")"
upFile.writeLine "-- UP script for " & scriptDesc
downFile.writeLine "-- DOWN script for " & scriptDesc let upFile = open(upFilename, fmWrite)
let downFile = open(downFilename, fmWrite)
upFile.close()
downFile.close() upFile.writeLine "-- UP script for " & scriptDesc
downFile.writeLine "-- DOWN script for " & scriptDesc
return @[upFilename, downFilename]
upFile.close()
when isMainModule: downFile.close()
let doc = """
Usage: return @[upFilename, downFilename]
db-migrate [options] create <migration-name>
db-migrate [options] up [<count>] proc diffMigrations*(pgConn: DbConn, config: DbMigrateConfig):
db-migrate [options] down [<count>] tuple[ run, notRun, missing: seq[string] ] =
db-migrate [options] init <schema-name>
# Query the database to find out what migrations have been run.
Options: var migrationsRun = initSet[string]()
-c --config <config-file> Use the given configuration file (defaults to
"database.json"). for row in pgConn.fastRows(sql"SELECT * FROM migrations ORDER BY name", @[]):
""" migrationsRun.incl(row[1])
# Parse arguments # Inspect the filesystem to see what migrations are available.
let args = docopt(doc, version = "db-migrate 0.2.0") var migrationsAvailable = initSet[string]()
for filePath in walkFiles(joinPath(config.sqlDir, "*.sql")):
let exitErr = proc(msg: string): void = var migrationName = filePath.extractFilename
stderr.writeLine("db_migrate: " & msg) migrationName.removeSuffix("-up.sql")
quit(QuitFailure) migrationName.removeSuffix("-down.sql")
migrationsAvailable.incl(migrationName)
# Load configuration file
let configFilename = # Diff with the list of migrations that we have in our migrations
if args["--config"]: $args["--config"] # directory.
else: "database.json" let migrationsInOrder =
toSeq(migrationsAvailable.items).sorted(system.cmp)
var config: DbMigrateConfig
try: var migrationsNotRun: seq[string] = @[]
config = loadConfig(configFilename) var missingMigrations: seq[string] = @[]
except IOError:
exitErr "Cannot open config file: " & configFilename for migration in migrationsInOrder:
except: if not migrationsRun.contains(migration):
exitErr "Error parsing config file: " & configFilename & "\L\t" & getCurrentExceptionMsg() migrationsNotRun.add(migration)
# Check for migrations directory # if we've already seen some migrations that have not been run, but this
if not existsDir config.sqlDir: # one has been, that means we have a gap and are missing migrations
try: elif migrationsNotRun.len > 0:
echo "SQL directory '" & config.sqlDir & missingMigrations.add(migrationsNotRun)
"' does not exist and will be created." migrationsNotRun = @[]
createDir config.sqlDir
except IOError: return (run: toSeq(migrationsRun.items), notRun: migrationsNotRun, missing: missingMigrations)
exitErr "Unable to create directory: " & config.sqlDir & ":\L\T" & getCurrentExceptionMsg()
proc up*(config: DbMigrateConfig, count: int): seq[string] =
# Execute commands let pgConn = open("", "", "", config.connectionString)
if args["create"]: pgConn.createMigrationsTable
try:
let filesCreated = createMigration(config, $args["<migration-name>"]) let (run, toRun, missing) = diffMigrations(pgConn, config)
echo "Created new migration files:"
for filename in filesCreated: echo "\t" & filename # Make sure we have no gaps (database is in an unknown state)
except IOError: if missing.len > 0:
exitErr "Unable to create migration scripts: " & getCurrentExceptionMsg() dbError("Database is in an inconsistent state. Migrations have been " &
"run that are not sequential.")
elif args["up"]: discard
# Query the database to find out what migrations have been run. var migrationsRun: seq[string] = @[]
if toRun.len > 0:
# Diff with the list of migrations that we have in our migrations # Begin a transaction.
# directory. pgConn.exec(sql"BEGIN")
# Make sure we have no gaps (database is in an unknown state) let rollbackWithErr = proc (errMsg: string): void =
pgConn.exec(sql"ROLLBACK")
# Find the subset of migrations we need to apply (consider the count dbError(errMsg)
# parameter if passed)
# Apply each of the migrations.
# If none: "Up to date." for migration in toRun:
let filename = joinPath(config.sqlDir, migration & "-up.sql")
# Begin a transaction.
if not filename.fileExists:
# Apply each of the migrations. rollbackWithErr "Can not find up file for " & migration &
# If any fail, roll back the transaction ". Expected '" & filename & "'."
# Otherwise report success
let migrationSql = filename.readFile
elif args["down"]: discard try:
# Query the database to find out what migrations have been run. pgConn.exec(sql(migrationSql))
migrationsRun.add(migration)
# Find how many we need to go down (default to 1 if the count parameter was except DbError:
# not passed. rollbackWithErr "Migration '" & migration & "'failed:\n\t" &
getCurrentExceptionMsg()
# Begin transaction
# Apply each down script return migrationsRun
# If any fail, roll back the transaction
# Otherwise report success when isMainModule:
let doc = """
elif args["init"]: discard 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").
-v --verbose Print detailed log information (use -vv to
print even more details).
-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 =
stderr.writeLine("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()
# 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()
# Execute commands
if args["create"]:
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()
elif args["up"]:
try:
let migrationsRun = up(config, if args["<count>"]: parseInt($args["<count>"]) else: 1)
for migration in migrationsRun:
echo migration
echo "Up to date."
except DbError:
exitErr "Unable to migrate database: " & getCurrentExceptionMsg()
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