From f8ddfaa8b7da3f32a1646ef675c51d9979645c6e Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Tue, 30 Jun 2026 15:55:59 +0200 Subject: [PATCH] AppDbUpdater: Fix handling of foreign key errors --- Elwig/Helpers/AppDbUpdater.cs | 86 +++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/Elwig/Helpers/AppDbUpdater.cs b/Elwig/Helpers/AppDbUpdater.cs index 5dc5a6a..6251407 100644 --- a/Elwig/Helpers/AppDbUpdater.cs +++ b/Elwig/Helpers/AppDbUpdater.cs @@ -1,6 +1,6 @@ -using Microsoft.Data.Sqlite; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -14,31 +14,36 @@ namespace Elwig.Helpers { private static int VersionOffset = 0; public static async Task CheckDb() { - using var cnx = AppDbContext.Connect(); + 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})"); - var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0; - if (applId != 0x454C5747) throw new Exception($"Invalid application_id in database (0x{applId:X08})"); - - var schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0; - VersionOffset = (int)(schemaVers % 100); - if (VersionOffset != 0) { - // schema was modified manually/externally - // TODO issue warning + 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(cnx, (int)(schemaVers / 100), RequiredSchemaVersion); - var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version") ?? 0; - var v = new Version((int)(userVers >> 24), (int)((userVers >> 16) & 0xFF), (int)((userVers >> 8) & 0xFF), (int)(userVers & 0xFF)); + await UpdateDbSchema((int)(schemaVers / 100), RequiredSchemaVersion); - 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}"); + 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(SqliteConnection cnx, int fromVersion, int toVersion) { + private static async Task UpdateDbSchema(int fromVersion, int toVersion) { if (fromVersion == toVersion) { return; } else if (fromVersion > toVersion) { @@ -48,43 +53,48 @@ namespace Elwig.Helpers { } var asm = Assembly.GetExecutingAssembly(); - (int From, int To, string Name)[] scripts = asm.GetManifestResourceNames() + (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(); + .OrderBy(s => s.Item1).ThenBy(s => s.Item2)]; List toExecute = []; var vers = fromVersion; while (vers < toVersion) { - var (_, to, name) = scripts.Where(s => s.From == vers).Last(); + var (_, to, name) = scripts.Last(s => s.From == vers); toExecute.Add(name); vers = to; } if (toExecute.Count == 0) return; - await cnx.ExecuteBatch(""" - PRAGMA locking_mode = EXCLUSIVE; - BEGIN 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 - .Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}"))); - } + 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); + } - await cnx.ExecuteBatch($""" - COMMIT; - VACUUM; - PRAGMA schema_version = {toVersion * 100 + VersionOffset}; - """); + 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); + } } } }