AppDbUpdater: Fix handling of foreign key errors
Test / Run tests (push) Successful in 3m4s

This commit is contained in:
2026-06-30 15:55:59 +02:00
parent beacba6bd9
commit f8ddfaa8b7
+31 -21
View File
@@ -1,6 +1,6 @@
using Microsoft.Data.Sqlite;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -14,31 +14,36 @@ namespace Elwig.Helpers {
private static int VersionOffset = 0; private static int VersionOffset = 0;
public static async Task<Version> CheckDb() { public static async Task<Version> CheckDb() {
using var cnx = AppDbContext.Connect(); long? applId, schemaVers;
using (var cnx = await AppDbContext.ConnectAsync()) {
var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0; applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
if (applId != 0x454C5747) throw new Exception($"Invalid application_id in database (0x{applId:X08})"); if (applId != 0x454C5747) throw new Exception($"Invalid application_id in database (0x{applId:X08})");
var schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0; schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;
VersionOffset = (int)(schemaVers % 100); VersionOffset = (int)(schemaVers % 100);
if (VersionOffset != 0) { if (VersionOffset != 0) {
// schema was modified manually/externally // schema was modified manually/externally
// TODO issue warning // TODO issue warning
} }
await UpdateDbSchema(cnx, (int)(schemaVers / 100), RequiredSchemaVersion); }
await UpdateDbSchema((int)(schemaVers / 100), RequiredSchemaVersion);
Version v;
using (var cnx = await AppDbContext.ConnectAsync()) {
var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version") ?? 0; 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)); v = new Version((int)(userVers >> 24), (int)((userVers >> 16) & 0xFF), (int)((userVers >> 8) & 0xFF), (int)(userVers & 0xFF));
if (App.Version > v) { if (App.Version > v) {
long vers = (App.Version.Major << 24) | (App.Version.Minor << 16) | (App.Version.Build << 8) | App.Version.Revision; long vers = (App.Version.Major << 24) | (App.Version.Minor << 16) | (App.Version.Build << 8) | App.Version.Revision;
await cnx.ExecuteBatch($"PRAGMA user_version = {vers}"); await cnx.ExecuteBatch($"PRAGMA user_version = {vers}");
} }
}
return v; 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) { if (fromVersion == toVersion) {
return; return;
} else if (fromVersion > toVersion) { } else if (fromVersion > toVersion) {
@@ -48,43 +53,48 @@ namespace Elwig.Helpers {
} }
var asm = Assembly.GetExecutingAssembly(); 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.")) .Where(n => n.StartsWith("Elwig.Resources.Sql."))
.Select(n => { .Select(n => {
var p = n.Split(".")[^2].Split("-"); var p = n.Split(".")[^2].Split("-");
return (int.Parse(p[0]), int.Parse(p[1]), n); return (int.Parse(p[0]), int.Parse(p[1]), n);
}) })
.OrderBy(s => s.Item1).ThenBy(s => s.Item2) .OrderBy(s => s.Item1).ThenBy(s => s.Item2)];
.ToArray();
List<string> toExecute = []; List<string> toExecute = [];
var vers = fromVersion; var vers = fromVersion;
while (vers < toVersion) { 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); toExecute.Add(name);
vers = to; vers = to;
} }
if (toExecute.Count == 0) if (toExecute.Count == 0)
return; return;
await cnx.ExecuteBatch(""" var backup = Path.ChangeExtension(App.Config.DatabaseFile, $".v{fromVersion}.sqlite3");
PRAGMA locking_mode = EXCLUSIVE; File.Copy(App.Config.DatabaseFile, backup, true);
BEGIN EXCLUSIVE; try {
"""); using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch("PRAGMA locking_mode = EXCLUSIVE");
foreach (var script in toExecute) { foreach (var script in toExecute) {
await cnx.ExecuteEmbeddedScript(asm, script); await cnx.ExecuteEmbeddedScript(asm, script);
} }
var violations = await cnx.ForeignKeyCheck(); var violations = await cnx.ForeignKeyCheck();
if (violations.Length > 0) { if (violations.Length > 0) {
throw new Exception($"Foreign key violations ({violations.Length}):\n" + string.Join("\n", violations 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}"))); .Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}")));
} }
await cnx.ExecuteBatch($""" await cnx.ExecuteBatch("VACUUM");
COMMIT; await cnx.ExecuteBatch($"PRAGMA schema_version = {toVersion * 100 + VersionOffset}");
VACUUM; } catch (Exception) {
PRAGMA schema_version = {toVersion * 100 + VersionOffset}; File.Move(backup, App.Config.DatabaseFile, true);
"""); throw;
} finally {
File.Delete(backup);
}
} }
} }
} }