## DB Migrate ## ========== ## ## Simple tool to manage database migrations. import json, times, os, strutils, docopt, db_postgres, db_mysql, db_sqlite type DbMigrateConfig* = tuple[ driver, sqlDir, connectionString: string ] proc loadConfig*(filename: string): DbMigrateConfig = ## Load DbMigrateConfig from a file. let cfg = json.parseFile(filename) return ( driver: if cfg.hasKey("driver"): cfg["driver"].getStr else: "postres", sqlDir: if cfg.hasKey("sqlDir"): cfg["sqlDir"].getStr else: "migrations", connectionString: cfg["connectionString"].getStr) 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] = ## Create a new set of database migration files. 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 db-migrate [options] up [] db-migrate [options] down [] db-migrate [options] init Options: -c --config Use the given configuration file (defaults to "database.json"). """ # 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"] 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[""]) echo "Created new migration files:" for filename in filesCreated: echo "\t" & filename except IOError: exitErr "Unable to create migration scripts: " & getCurrentExceptionMsg() 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