using InABox.Configuration; using InABox.Core; using Microsoft.CodeAnalysis.Scripting; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace InABox.Database; public class DatabaseVersion : BaseObject, IGlobalConfigurationSettings { public string Version { get; set; } public DatabaseVersion() { Version = "0.00"; } } public class VersionNumber { public int MajorVersion { get; set; } public int MinorVersion { get; set; } public string Release { get; set; } public bool IsDevelopmentVersion { get; set; } public VersionNumber(int majorVersion, int minorVersion, string release = "", bool isDevelopmentVersion = false) { MajorVersion = majorVersion; MinorVersion = minorVersion; Release = release; IsDevelopmentVersion = isDevelopmentVersion; } private static Regex _format = new(@"^(\d+)\.(\d+)([a-zA-Z]*)$"); public static VersionNumber Parse(string versionStr) { if(versionStr == "???") { return new(0, 0, "", true); } var match = _format.Match(versionStr); if (!match.Success) throw new FormatException($"'{versionStr}' is not a valid version!"); return new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false); } public static bool TryParse(string versionStr, [NotNullWhen(true)] out VersionNumber? version) { if (versionStr == "???") { version = new(0, 0, "", true); return true; } var match = _format.Match(versionStr); if (!match.Success) { version = null; return false; } version = new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false); return true; } public static bool operator <(VersionNumber a, VersionNumber b) { if (a.IsDevelopmentVersion) { return false; } else if (b.IsDevelopmentVersion) { return true; } return a.MajorVersion < b.MajorVersion || (a.MajorVersion == b.MajorVersion && (a.MinorVersion < b.MinorVersion || (a.MinorVersion == b.MinorVersion && string.Compare(a.Release, b.Release, StringComparison.Ordinal) < 0))); } public static bool operator >(VersionNumber a, VersionNumber b) { return b < a; } public static bool operator <=(VersionNumber a, VersionNumber b) { return !(b < a); } public static bool operator >=(VersionNumber a, VersionNumber b) { return !(a < b); } public override bool Equals(object? obj) { if(obj is VersionNumber v) { return this == v; } return false; } public override int GetHashCode() { if (IsDevelopmentVersion) return 0; return MajorVersion ^ MinorVersion ^ Release.GetHashCode(); } public static bool operator ==(VersionNumber a, VersionNumber b) { if (a.IsDevelopmentVersion) return b.IsDevelopmentVersion; if (b.IsDevelopmentVersion) return false; return a.MajorVersion == b.MajorVersion && a.MinorVersion == b.MinorVersion && a.Release == b.Release; } public static bool operator !=(VersionNumber a, VersionNumber b) { if (a.IsDevelopmentVersion) return !b.IsDevelopmentVersion; if (b.IsDevelopmentVersion) return true; return a.MajorVersion != b.MajorVersion || a.MinorVersion != b.MinorVersion || a.Release != b.Release; } public override string ToString() { return IsDevelopmentVersion ? "???" : $"{MajorVersion}.{MinorVersion:D2}{Release}"; } } public static class DataUpdater { private static Dictionary> updateScripts = new(); /// /// Register a migration script to run when updating to this version. /// /// The should probably be repeatable; /// that is, if you run it a second time, it only updates data that needed updating. This way if it accidentally somehow gets run twice, there is no issue. /// /// /// The version to update to. /// The action to be run. public static void RegisterUpdateScript() where TUpdater : DatabaseUpdateScript, new() { var updater = new TUpdater(); if(!updateScripts.TryGetValue(updater.Version, out var list)) { list = new(); updateScripts[updater.Version] = list; } list.Add(updater); } private static bool MigrateDatabase(VersionNumber fromVersion, VersionNumber toVersion, out VersionNumber newVersion) { var versionNumbers = updateScripts.Keys.ToList(); versionNumbers.Sort((x, y) => x == y ? 0 : x < y ? -1 : 1); newVersion = fromVersion; int? index = null; foreach (var (i, number) in versionNumbers.Select((x, i) => new Tuple(i, x))) { if (number > fromVersion) { index = i; break; } } if(index != null && fromVersion < toVersion) { Logger.Send(LogType.Information, "", $"Updating database from {fromVersion} to {toVersion}"); for (int i = (int)index; i < versionNumbers.Count; i++) { var version = versionNumbers[i]; if (toVersion < version) { break; } Logger.Send(LogType.Information, "", $"Executing update to {version}"); foreach(var updater in updateScripts[version]) { if (!updater.Update()) { Logger.Send(LogType.Error, "", $"Script failed, cancelling migration"); return false; } } newVersion = version; } Logger.Send(LogType.Information, "", $"Data migration complete!"); } newVersion = toVersion; return true; } private static void UpdateVersionNumber(VersionNumber version) { if (version.IsDevelopmentVersion) { return; } var dbVersion = DbFactory.GetVersionSettings(); dbVersion.Version = version.ToString(); var result = DbFactory.NewProvider(Logger.Main).Query(new Filter(x => x.Section).IsEqualTo(nameof(DatabaseVersion))) .Rows.FirstOrDefault()?.ToObject() ?? new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" }; result.OriginalValueList["Contents"] = result.Contents; result.Contents = Serialization.Serialize(dbVersion); DbFactory.NewProvider(Logger.Main).Save(result); } /// /// Migrates the database to the current version. /// /// false if the migration fails. public static bool MigrateDatabase() { try { var from = DbFactory.GetDatabaseVersion(); var to = VersionNumber.Parse(CoreUtils.GetVersion()); var success = MigrateDatabase(from, to, out var newVersion); if (newVersion != from) { UpdateVersionNumber(newVersion); } return success; } catch(Exception e) { Logger.Send(LogType.Error, "", $"Error while migrating database: {CoreUtils.FormatException(e)}"); return false; } } public static bool DoSpecificMigration(VersionNumber from, VersionNumber to) { try { var _success = MigrateDatabase(from, to, out var _); return _success; } catch(Exception e) { Logger.Send(LogType.Error, "", $"Error while migrating database: {CoreUtils.FormatException(e)}"); return false; } } }