Added UP command (not working).
This commit is contained in:
parent
3963a27a66
commit
efc5dd2ed9
145
db_migrate.nim
145
db_migrate.nim
@ -3,9 +3,19 @@
|
|||||||
##
|
##
|
||||||
## 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
|
||||||
|
LogLevel = enum quiet, normal, verbose, very_verbose
|
||||||
|
DbMigrateConfig* = tuple[ driver, sqlDir, connectionString: string, logLevel: LogLevel ]
|
||||||
|
|
||||||
|
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 loadConfig*(filename: string): DbMigrateConfig =
|
proc loadConfig*(filename: string): DbMigrateConfig =
|
||||||
## Load DbMigrateConfig from a file.
|
## Load DbMigrateConfig from a file.
|
||||||
@ -14,14 +24,8 @@ proc loadConfig*(filename: string): DbMigrateConfig =
|
|||||||
return (
|
return (
|
||||||
driver: if cfg.hasKey("driver"): cfg["driver"].getStr else: "postres",
|
driver: if cfg.hasKey("driver"): cfg["driver"].getStr else: "postres",
|
||||||
sqlDir: if cfg.hasKey("sqlDir"): cfg["sqlDir"].getStr else: "migrations",
|
sqlDir: if cfg.hasKey("sqlDir"): cfg["sqlDir"].getStr else: "migrations",
|
||||||
connectionString: cfg["connectionString"].getStr)
|
connectionString: cfg["connectionString"].getStr,
|
||||||
|
logLevel: if cfg.hasKey("logLevel"): parseEnum[LogLevel](cfg["logLevel"].getStr) else: normal)
|
||||||
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] =
|
proc createMigration*(config: DbMigrateConfig, migrationName: string): seq[string] =
|
||||||
## Create a new set of database migration files.
|
## Create a new set of database migration files.
|
||||||
@ -44,17 +48,100 @@ proc createMigration*(config: DbMigrateConfig, migrationName: string): seq[strin
|
|||||||
|
|
||||||
return @[upFilename, downFilename]
|
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: seq[string] = @[]
|
||||||
|
var missingMigrations: seq[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 = @[]
|
||||||
|
|
||||||
|
return (run: toSeq(migrationsRun.items), notRun: migrationsNotRun, missing: missingMigrations)
|
||||||
|
|
||||||
|
proc up*(config: DbMigrateConfig, count: int): seq[string] =
|
||||||
|
let pgConn = open("", "", "", config.connectionString)
|
||||||
|
pgConn.createMigrationsTable
|
||||||
|
|
||||||
|
let (run, toRun, missing) = diffMigrations(pgConn, config)
|
||||||
|
|
||||||
|
# Make sure we have no gaps (database is in an unknown state)
|
||||||
|
if missing.len > 0:
|
||||||
|
dbError("Database is in an inconsistent state. Migrations have been " &
|
||||||
|
"run that are not sequential.")
|
||||||
|
|
||||||
|
var migrationsRun: seq[string] = @[]
|
||||||
|
if toRun.len > 0:
|
||||||
|
# Begin a transaction.
|
||||||
|
pgConn.exec(sql"BEGIN")
|
||||||
|
|
||||||
|
let rollbackWithErr = proc (errMsg: string): void =
|
||||||
|
pgConn.exec(sql"ROLLBACK")
|
||||||
|
dbError(errMsg)
|
||||||
|
|
||||||
|
# Apply each of the migrations.
|
||||||
|
for migration in toRun:
|
||||||
|
let filename = joinPath(config.sqlDir, migration & "-up.sql")
|
||||||
|
|
||||||
|
if not filename.fileExists:
|
||||||
|
rollbackWithErr "Can not find up file for " & migration &
|
||||||
|
". Expected '" & filename & "'."
|
||||||
|
|
||||||
|
let migrationSql = filename.readFile
|
||||||
|
try:
|
||||||
|
pgConn.exec(sql(migrationSql))
|
||||||
|
migrationsRun.add(migration)
|
||||||
|
except DbError:
|
||||||
|
rollbackWithErr "Migration '" & migration & "'failed:\n\t" &
|
||||||
|
getCurrentExceptionMsg()
|
||||||
|
|
||||||
|
|
||||||
|
return migrationsRun
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
let doc = """
|
let doc = """
|
||||||
Usage:
|
Usage:
|
||||||
db-migrate [options] create <migration-name>
|
db_migrate [options] create <migration-name>
|
||||||
db-migrate [options] up [<count>]
|
db_migrate [options] up [<count>]
|
||||||
db-migrate [options] down [<count>]
|
db_migrate [options] down [<count>]
|
||||||
db-migrate [options] init <schema-name>
|
db_migrate [options] init <schema-name>
|
||||||
|
db_migrate (-V | --version)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-c --config <config-file> Use the given configuration file (defaults to
|
-c --config <config-file> Use the given configuration file (defaults to
|
||||||
"database.json").
|
"database.json").
|
||||||
|
|
||||||
|
-v --verbose Print detailed log information (use -vv to
|
||||||
|
print even more details).
|
||||||
|
|
||||||
|
-V --version Print the tools version information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
@ -66,7 +153,7 @@ Options:
|
|||||||
|
|
||||||
# Load configuration file
|
# Load configuration file
|
||||||
let configFilename =
|
let configFilename =
|
||||||
if args["--config"]: $args["--config"]
|
if args["--config"]: $args["<config-file>"]
|
||||||
else: "database.json"
|
else: "database.json"
|
||||||
|
|
||||||
var config: DbMigrateConfig
|
var config: DbMigrateConfig
|
||||||
@ -81,7 +168,7 @@ Options:
|
|||||||
if not existsDir config.sqlDir:
|
if not existsDir config.sqlDir:
|
||||||
try:
|
try:
|
||||||
echo "SQL directory '" & config.sqlDir &
|
echo "SQL directory '" & config.sqlDir &
|
||||||
"' does not exist and will be created."
|
"' does not exist and will be created."
|
||||||
createDir config.sqlDir
|
createDir config.sqlDir
|
||||||
except IOError:
|
except IOError:
|
||||||
exitErr "Unable to create directory: " & config.sqlDir & ":\L\T" & getCurrentExceptionMsg()
|
exitErr "Unable to create directory: " & config.sqlDir & ":\L\T" & getCurrentExceptionMsg()
|
||||||
@ -95,24 +182,14 @@ Options:
|
|||||||
except IOError:
|
except IOError:
|
||||||
exitErr "Unable to create migration scripts: " & getCurrentExceptionMsg()
|
exitErr "Unable to create migration scripts: " & getCurrentExceptionMsg()
|
||||||
|
|
||||||
elif args["up"]: discard
|
elif args["up"]:
|
||||||
# Query the database to find out what migrations have been run.
|
try:
|
||||||
|
let migrationsRun = up(config, if args["<count>"]: parseInt($args["<count>"]) else: 1)
|
||||||
# Diff with the list of migrations that we have in our migrations
|
for migration in migrationsRun:
|
||||||
# directory.
|
echo migration
|
||||||
|
echo "Up to date."
|
||||||
# Make sure we have no gaps (database is in an unknown state)
|
except DbError:
|
||||||
|
exitErr "Unable to migrate database: " & getCurrentExceptionMsg()
|
||||||
# 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
|
elif args["down"]: discard
|
||||||
# Query the database to find out what migrations have been run.
|
# Query the database to find out what migrations have been run.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user