nwaku/waku/common/sqlite/migrations.nim

139 lines
4.2 KiB
Nim

{.push raises: [].}
import
std/[strutils, sequtils, os, algorithm],
stew/results,
chronicles
import
../sqlite
logScope:
topics = "sqlite"
## Migration scripts
proc getMigrationScriptVersion(path: string): DatabaseResult[int64] =
let name = extractFilename(path)
let parts = name.split("_", 1)
try:
let version = parseInt(parts[0])
return ok(version)
except ValueError:
return err("failed to parse file version: " & name)
proc isSqlScript(path: string): bool =
path.toLower().endsWith(".sql")
proc listSqlScripts(path: string): DatabaseResult[seq[string]] =
var scripts = newSeq[string]()
try:
for scriptPath in walkDirRec(path):
if isSqlScript(scriptPath):
scripts.add(scriptPath)
else:
debug "invalid migration script", file=scriptPath
except OSError:
return err("failed to list migration scripts: " & getCurrentExceptionMsg())
ok(scripts)
proc filterMigrationScripts(paths: seq[string], lowVersion, highVersion: int64, direction: string = "up"): seq[string] =
## Returns migration scripts whose version fall between lowVersion and highVersion (inclusive)
let filterPredicate = proc(script: string): bool =
if not isSqlScript(script):
return false
if direction != "" and not script.toLower().endsWith("." & direction & ".sql"):
return false
let scriptVersionRes = getMigrationScriptVersion(script)
if scriptVersionRes.isErr():
return false
let scriptVersion = scriptVersionRes.value
return lowVersion < scriptVersion and scriptVersion <= highVersion
paths.filter(filterPredicate)
proc sortMigrationScripts(paths: seq[string]): seq[string] =
## Sort migration scripts paths alphabetically
paths.sorted(system.cmp[string])
proc loadMigrationScripts(paths: seq[string]): DatabaseResult[seq[string]] =
var loadedScripts = newSeq[string]()
for script in paths:
try:
loadedScripts.add(readFile(script))
except OSError, IOError:
return err("failed to load script '" & script & "': " & getCurrentExceptionMsg())
ok(loadedScripts)
proc breakIntoStatements(script: string): seq[string] =
var statements = newSeq[string]()
for chunk in script.split(';'):
if chunk.strip().isEmptyOrWhitespace():
continue
let statement = chunk.strip() & ";"
statements.add(statement)
statements
proc migrate*(db: SqliteDatabase, targetVersion: int64, migrationsScriptsDir: string): DatabaseResult[void] =
## Compares the `user_version` of the sqlite database with the provided `targetVersion`, then
## it runs migration scripts if the `user_version` is outdated. The `migrationScriptsDir` path
## points to the directory holding the migrations scripts once the db is updated, it sets the
## `user_version` to the `tragetVersion`.
##
## NOTE: Down migration it is not currently supported
let userVersion = ?db.getUserVersion()
if userVersion == targetVersion:
debug "database schema is up to date", userVersion=userVersion, targetVersion=targetVersion
return ok()
info "database schema is outdated", userVersion=userVersion, targetVersion=targetVersion
# Load migration scripts
var migrationScriptsPaths = ?listSqlScripts(migrationsScriptsDir)
migrationScriptsPaths = filterMigrationScripts(migrationScriptsPaths, lowVersion=userVersion, highVersion=targetVersion, direction="up")
migrationScriptsPaths = sortMigrationScripts(migrationScriptsPaths)
if migrationScriptsPaths.len <= 0:
debug "no scripts to be run"
return ok()
let scripts = ?loadMigrationScripts(migrationScriptsPaths)
# Run the migration scripts
for script in scripts:
for statement in script.breakIntoStatements():
debug "executing migration statement", statement=statement
let execRes = db.query(statement, NoopRowHandler)
if execRes.isErr():
error "failed to execute migration statement", statement=statement, error=execRes.error
return err("failed to execute migration statement")
debug "migration statement executed succesfully", statement=statement
# Update user_version
?db.setUserVersion(targetVersion)
debug "database user_version updated", userVersion=targetVersion
ok()