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 = 13; 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 of database"); 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 major = userVers >> 24; var minor = (userVers >> 16) & 0xFF; var patch = userVers & 0xFFFF; if (App.VersionMajor > major || (App.VersionMajor == major && App.VersionMinor > minor) || (App.VersionMajor == major && App.VersionMinor == minor && App.VersionPatch > patch)) { long vers = (App.VersionMajor << 24) | (App.VersionMinor << 16) | App.VersionPatch; await AppDbContext.ExecuteBatch(cnx, $"PRAGMA user_version = {vers}"); } return $"{major}.{minor}.{patch}"; } 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; PRAGMA foreign_keys = OFF; BEGIN EXCLUSIVE; """); foreach (var script in toExecute) { await AppDbContext.ExecuteEmbeddedScript(cnx, asm, script); } await AppDbContext.ExecuteBatch(cnx, $""" PRAGMA foreign_key_check; COMMIT; PRAGMA foreign_keys = ON; VACUUM; PRAGMA schema_version = {toVersion * 100 + VersionOffset}; """); } } }