using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; namespace Elwig.Helpers { public static class AppDbUpdater { // Don't forget to update value in Tests/fetch-resources.bat! public static readonly int RequiredSchemaVersion = 40; private static int VersionOffset = 0; public static async Task CheckDb() { long? applId, schemaVers; using (var cnx = await AppDbContext.ConnectAsync()) { applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0; if (applId != 0x454C5747) throw new Exception($"Invalid application_id in database (0x{applId:X08})"); schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0; VersionOffset = (int)(schemaVers % 100); if (VersionOffset != 0) { // schema was modified manually/externally // TODO issue warning } } await UpdateDbSchema((int)(schemaVers / 100), RequiredSchemaVersion); Version v; using (var cnx = await AppDbContext.ConnectAsync()) { var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version") ?? 0; v = new Version((int)(userVers >> 24), (int)((userVers >> 16) & 0xFF), (int)((userVers >> 8) & 0xFF), (int)(userVers & 0xFF)); if (App.Version > v) { long vers = (App.Version.Major << 24) | (App.Version.Minor << 16) | (App.Version.Build << 8) | App.Version.Revision; await cnx.ExecuteBatch($"PRAGMA user_version = {vers}"); } } return v; } private static async Task UpdateDbSchema(int fromVersion, int toVersion) { if (fromVersion == toVersion) { return; } else if (fromVersion > toVersion) { throw new Exception("schema_version of database is too new"); } else if (fromVersion <= 0) { throw new Exception("schema_version of database is invalid"); } var asm = Assembly.GetExecutingAssembly(); (int From, int To, string Name)[] scripts = [.. asm.GetManifestResourceNames() .Where(n => n.StartsWith("Elwig.Resources.Sql.")) .Select(n => { var p = n.Split(".")[^2].Split("-"); return (int.Parse(p[0]), int.Parse(p[1]), n); }) .OrderBy(s => s.Item1).ThenBy(s => s.Item2)]; List toExecute = []; var vers = fromVersion; while (vers < toVersion) { var (_, to, name) = scripts.Last(s => s.From == vers); toExecute.Add(name); vers = to; } if (toExecute.Count == 0) return; var backup = Path.ChangeExtension(App.Config.DatabaseFile, $".v{fromVersion}.sqlite3"); File.Copy(App.Config.DatabaseFile, backup, true); try { using var cnx = await AppDbContext.ConnectAsync(); await cnx.ExecuteBatch("PRAGMA locking_mode = EXCLUSIVE"); foreach (var script in toExecute) { await cnx.ExecuteEmbeddedScript(asm, script); } var violations = await cnx.ForeignKeyCheck(); if (violations.Length > 0) { throw new Exception($"Foreign key violations ({violations.Length}):\n" + string.Join("\n", violations .Take(50) .Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}"))); } await cnx.ExecuteBatch("VACUUM"); await cnx.ExecuteBatch($"PRAGMA schema_version = {toVersion * 100 + VersionOffset}"); } catch (Exception) { File.Move(backup, App.Config.DatabaseFile, true); throw; } finally { File.Delete(backup); } } } }