using Microsoft.Data.Sqlite; 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 { public static readonly int RequiredSchemaVersion = 12; private static int VersionOffset = 0; private static async Task ExNonQuery(SqliteConnection cnx, string sql) { using var cmd = cnx.CreateCommand(); cmd.CommandText = sql; await (await cmd.ExecuteReaderAsync()).CloseAsync(); } private static async Task ExScalar(SqliteConnection cnx, string sql) { using var cmd = cnx.CreateCommand(); cmd.CommandText = sql; return await cmd.ExecuteScalarAsync(); } public static async Task CheckDb() { using var cnx = AppDbContext.Connect(); var applId = (long?)await ExScalar(cnx, "PRAGMA application_id") ?? 0; if (applId != 0x454C5747) throw new Exception("Invalid application_id of database"); var schemaVers = (long?)await ExScalar(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 ExScalar(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 ExNonQuery(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 ExNonQuery(cnx, """ PRAGMA locking_mode = EXCLUSIVE; PRAGMA foreign_keys = OFF; BEGIN EXCLUSIVE; """); foreach (var script in toExecute) { using var stream = asm.GetManifestResourceStream(script) ?? throw new Exception("Unable to load embedded resource"); using var reader = new StreamReader(stream); await ExNonQuery(cnx, await reader.ReadToEndAsync()); } await ExNonQuery(cnx, $""" PRAGMA foreign_key_check; COMMIT; PRAGMA foreign_keys = ON; VACUUM; PRAGMA schema_version = {toVersion * 100 + VersionOffset}; """); } } }