using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Dapper;
using JetBrains.Annotations;
using Microsoft.Data.Sqlite;
using Robust.Shared.Log;
namespace Content.Server.Preferences.Migrations
{
///
/// Ensures database schemas are up to date.
///
public static class MigrationManager
{
///
/// Ensures the database schema for the given connection string is up to date.
///
public static void PerformUpgrade(string connectionString)
{
using (var connection = new SqliteConnection(connectionString))
{
EnsureSchemaVersionTableExists(connection);
foreach (var migrationToRun in MigrationsToRun(connection))
{
Logger.InfoS("db", "Running migration {0}", migrationToRun.Id);
migrationToRun.Run(connection);
}
}
}
///
/// Generated for each SQL file found.
///
private class Migration
{
public readonly string Id;
private readonly string _sql;
public Migration(string id, string sql)
{
Id = id;
_sql = sql;
}
///
/// Executes the query in and logs this in the SchemaVersion table.
///
public void Run(IDbConnection connection)
{
connection.Execute(_sql);
InsertMigrationLog(connection, Id);
}
}
private const string InsertMigrationLogQuery =
@"INSERT INTO SchemaVersion (Id) VALUES (@Id)";
///
/// Inserts a in the SchemaVersion table.
///
private static void InsertMigrationLog(IDbConnection connection, string id)
{
Logger.InfoS("db", "Completing migration {0}", id);
connection.Execute(InsertMigrationLogQuery, new {Id = id});
}
///
/// An entry in the SchemaVersion table.
///
[UsedImplicitly]
private class MigrationLog
{
public string Id;
public string Timestamp;
}
private const string GetRanMigrationsQuery =
@"SELECT Id, Timestamp FROM SchemaVersion ORDER BY Id COLLATE NOCASE";
///
/// Fetches a collection of from the SchemaVersion table and returns it.
///
private static IEnumerable RanMigrations(IDbConnection connection)
{
return connection.Query(GetRanMigrationsQuery);
}
///
/// Finds all available migrations, returns those that haven't been run yet.
///
private static List MigrationsToRun(IDbConnection connection)
{
var discoveredMigrations = DiscoverMigrations(connection);
if (discoveredMigrations.Count == 0)
{
// No migrations found.
return null;
}
var ranMigrations = RanMigrations(connection);
// Filter out migrations that have already been executed
discoveredMigrations
.RemoveAll(migration => ranMigrations.Any(ranMigration => migration.Id == ranMigration.Id));
return discoveredMigrations;
}
///
/// Given an embedded resource's full path returns its contents as a string.
///
private static string ResourceAssemblyToString(string resourceName)
{
using (var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream(resourceName))
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
///
/// Searches the current assembly for SQL migration files.
/// TODO: Filter by subfolder so that different databases use different sets of migrations.
///
[NotNull]
private static List DiscoverMigrations(IDbConnection connection)
{
var results = new List();
var assembly = Assembly.GetExecutingAssembly();
foreach (var sqlResourceName in assembly
.GetManifestResourceNames()
.Where(IsValidMigrationFileName))
{
var splitName = sqlResourceName.Split('.');
// The second to last string in the list is the actual file name without the final ".sql"
var migrationId = splitName[splitName.Length - 2];
var sqlContents = ResourceAssemblyToString(sqlResourceName);
results.Add(new Migration(migrationId, sqlContents));
}
return results;
}
///
/// A valid file name is "000_Initial.sql". A dot (from the path, not to be included in the filename itself),
/// three digits, a mandatory underscore, any number of characters, a mandatory ".sql".
///
private static bool IsValidMigrationFileName(string name)
{
return Regex.IsMatch(name, @"\.\d\d\d_[a-zA-Z]+\.sql$");
}
private const string EnsureSchemaVersionTableExistsQuery =
@"CREATE TABLE IF NOT EXISTS SchemaVersion (
Id TEXT NOT NULL UNIQUE,
Timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)";
///
/// Creates the SchemaVersion table if it doesn't exist.
///
private static void EnsureSchemaVersionTableExists(IDbConnection connection)
{
connection.Execute(EnsureSchemaVersionTableExistsQuery);
}
}
}