using Microsoft.Data.Sqlite; using System; using System.Collections.Generic; 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 = 27; private static int VersionOffset = 0; public static async Task CheckDb() { using var cnx = AppDbContext.Connect(); var applId = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA application_id") ?? 0; if (applId != 0x454C5747) throw new Exception($"Invalid application_id in database (0x{applId:X08})"); var schemaVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA schema_version") ?? 0; VersionOffset = (int)(schemaVers % 100); if (VersionOffset != 0) { // schema was modified manually/externally // TODO issue warning } await UpdateDbSchema(cnx, (int)(schemaVers / 100), RequiredSchemaVersion); var userVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA user_version") ?? 0; var v = new Version((int)(userVers >> 24), (int)((userVers >> 16) & 0xFF), (int)(userVers & 0xFFFF)); if (App.Version > v) { long vers = (App.Version.Major << 24) | (App.Version.Minor << 16) | App.Version.Build; await AppDbContext.ExecuteBatch(cnx, $"PRAGMA user_version = {vers}"); } return v; } private static async Task UpdateDbSchema(SqliteConnection cnx, 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) .ToArray(); List toExecute = []; var vers = fromVersion; while (vers < toVersion) { var (_, to, name) = scripts.Where(s => s.From == vers).Last(); toExecute.Add(name); vers = to; } if (toExecute.Count == 0) return; await AppDbContext.ExecuteBatch(cnx, """ PRAGMA locking_mode = EXCLUSIVE; BEGIN EXCLUSIVE; """); foreach (var script in toExecute) { await AppDbContext.ExecuteEmbeddedScript(cnx, asm, script); } var violations = await AppDbContext.ForeignKeyCheck(cnx); if (violations.Length > 0) { throw new Exception($"Foreign key violations ({violations.Length}):\n" + string.Join("\n", violations .Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}"))); } await AppDbContext.ExecuteBatch(cnx, $""" COMMIT; VACUUM; PRAGMA schema_version = {toVersion * 100 + VersionOffset}; """); } } }