App: Add option to restore database when a DB error occurs at startup
Test / Run tests (push) Successful in 2m12s

This commit is contained in:
2026-08-26 12:01:03 +02:00
parent c1690bf104
commit cadac4302d
5 changed files with 81 additions and 30 deletions
+8 -2
View File
@@ -93,11 +93,15 @@ namespace Elwig {
try { try {
await AppDbUpdater.CheckDb(); await AppDbUpdater.CheckDb();
} catch (FileNotFoundException exc) {
InteractionService.ShowException("Datenbank nicht gefunden", "Die Datenbank konnte nicht gefunden werden", exc);
} catch (Exception exc) { } catch (Exception exc) {
if (Config.UpdateUrl != null && Utils.HasInternetConnectivity()) { if (Config.UpdateUrl != null && Utils.HasInternetConnectivity()) {
await CheckForUpdates(); await CheckForUpdates();
} }
InteractionService.ShowException("Fehlerhafte Datenbank", "Fehlerhafte Datenbank", exc); InteractionService.ShowException("Fehlerhafte Datenbank", "Schwerwiegender Fehler in der Datenbank, unbedingt den Programmierern melden!", exc);
if (InteractionService.AskQuestion("Backup wiederherstellen", "Soll die Datenbank aus einem Backup wiederhergestellt werden?\n\nAchtung: Das kann zu Datenverlust führen!", false))
await BackupService.RestoreDatabase();
Shutdown(); Shutdown();
return; return;
} }
@@ -112,7 +116,9 @@ namespace Elwig {
try { try {
Client = new(ctx); Client = new(ctx);
} catch (Exception exc) { } catch (Exception exc) {
InteractionService.ShowException("Fehler", "Fehler beim Laden der Mandantendaten", exc); InteractionService.ShowException("Fehler", "Fehler beim Laden der Mandantendaten, unbedingt den Programmierern melden!", exc);
if (InteractionService.AskQuestion("Backup wiederherstellen", "Soll die Datenbank aus einem Backup wiederhergestellt werden?\n\nAchtung: Das kann zu Datenverlust führen!", false))
await BackupService.RestoreDatabase();
Shutdown(); Shutdown();
return; return;
} }
+14 -7
View File
@@ -14,10 +14,17 @@ 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() {
if (!File.Exists(App.Config.DatabaseFile))
throw new FileNotFoundException($"Die Datei \"{App.Config.DatabaseFile}\" exisitiert nicht");
long? applId, schemaVers; long? applId, schemaVers;
using (var cnx = await AppDbContext.ConnectAsync()) { using (var cnx = await AppDbContext.ConnectAsync()) {
await cnx.QuickCheck();
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 FileFormatException($"Invalid application_id in database (0x{applId:X08})");
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);
@@ -76,16 +83,16 @@ namespace Elwig.Helpers {
try { try {
using var cnx = await AppDbContext.ConnectAsync(); using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch("PRAGMA locking_mode = EXCLUSIVE"); await cnx.ExecuteBatch("PRAGMA locking_mode = EXCLUSIVE");
await cnx.IntegrityCheck();
await cnx.ForeignKeyCheck();
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(); await cnx.IntegrityCheck();
if (violations.Length > 0) { await cnx.ForeignKeyCheck();
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("VACUUM");
await cnx.ExecuteBatch($"PRAGMA schema_version = {toVersion * 100 + VersionOffset}"); await cnx.ExecuteBatch($"PRAGMA schema_version = {toVersion * 100 + VersionOffset}");
+35 -3
View File
@@ -135,11 +135,39 @@ namespace Elwig.Helpers {
return await cmd.ExecuteScalarAsync(); return await cmd.ExecuteScalarAsync();
} }
public static async Task<(string Table, long RowId, string Parent, long FkId)[]> ForeignKeyCheck(this SqliteConnection cnx) { public static async Task QuickCheck(this SqliteConnection cnx) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = "PRAGMA quick_check";
using var reader = await cmd.ExecuteReaderAsync();
var list = new List<string>();
while (await reader.ReadAsync()) {
list.Add(reader.GetString(0));
}
if (list.Count != 1 || list[0] != "ok") {
throw new FileFormatException($"Integrity problems ({list.Count}):\n" + string.Join("\n", list.Take(50)));
}
}
public static async Task IntegrityCheck(this SqliteConnection cnx) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = "PRAGMA integrity_check";
using var reader = await cmd.ExecuteReaderAsync();
var list = new List<string>();
while (await reader.ReadAsync()) {
list.Add(reader.GetString(0));
}
if (list.Count != 1 || list[0] != "ok") {
throw new FileFormatException($"Integrity problems ({list.Count}):\n" + string.Join("\n", list.Take(50)));
}
}
public static async Task ForeignKeyCheck(this SqliteConnection cnx) {
using var cmd = cnx.CreateCommand(); using var cmd = cnx.CreateCommand();
cmd.CommandText = "PRAGMA foreign_key_check"; cmd.CommandText = "PRAGMA foreign_key_check";
using var reader = await cmd.ExecuteReaderAsync(); using var reader = await cmd.ExecuteReaderAsync();
var list = new List<(string, long, string, long)>(); var list = new List<(string Table, long RowId, string Parent, long FkId)>();
while (await reader.ReadAsync()) { while (await reader.ReadAsync()) {
var table = reader.GetString(0); var table = reader.GetString(0);
var rowid = reader.GetInt64(1); var rowid = reader.GetInt64(1);
@@ -147,7 +175,11 @@ namespace Elwig.Helpers {
var fkid = reader.GetInt64(3); var fkid = reader.GetInt64(3);
list.Add((table, rowid, parent, fkid)); list.Add((table, rowid, parent, fkid));
} }
return [.. list];
if (list.Count > 0) {
throw new InvalidDataException($"Foreign key violations ({list.Count}):\n" + string.Join("\n", list.Take(50)
.Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}")));
}
} }
public static IEnumerable<T> Join<T>(this IEnumerable<T> src, Func<T> separatorFactory) { public static IEnumerable<T> Join<T>(this IEnumerable<T> src, Func<T> separatorFactory) {
+23
View File
@@ -1,11 +1,13 @@
using Elwig.Helpers; using Elwig.Helpers;
using Elwig.Helpers.Export; using Elwig.Helpers.Export;
using Microsoft.Win32;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
namespace Elwig.Services { namespace Elwig.Services {
public static class BackupService { public static class BackupService {
@@ -106,5 +108,26 @@ namespace Elwig.Services {
} catch { } } catch { }
} }
} }
public static async Task RestoreDatabase() {
try {
var d = new OpenFileDialog() {
Title = "Datenbank wiederherstellen - Elwig",
DefaultDirectory = App.Config.BackupPath,
InitialDirectory = App.Config.BackupPath,
DefaultExt = "sql.zip",
Filter = "SQLite-Datenbank (*.sqlite3, *.sqlite3.zip, *.sql, *.sql.zip)|*.sqlite3;*.sqlite3.zip;*.sql;*.sql.zip",
};
if (d.ShowDialog() == true) {
if (!InteractionService.AskContinue("Datenbank wiederherstellen", "Soll die Datenbank wirklich unwiederruflich durch die wiederhergestellte Version ersetzt werden?"))
return;
Mouse.OverrideCursor = Cursors.Wait;
await App.ReplaceDatabase(d.FileName);
}
} catch (Exception exc) {
InteractionService.ShowException(exc);
}
Mouse.OverrideCursor = null;
}
} }
} }
+1 -18
View File
@@ -182,24 +182,7 @@ namespace Elwig.Windows {
} }
private async void Menu_Database_Restore_Click(object sender, RoutedEventArgs evt) { private async void Menu_Database_Restore_Click(object sender, RoutedEventArgs evt) {
try { await BackupService.RestoreDatabase();
var d = new OpenFileDialog() {
Title = "Datenbank wiederherstellen - Elwig",
DefaultDirectory = App.Config.BackupPath,
InitialDirectory = App.Config.BackupPath,
DefaultExt = "sql.zip",
Filter = "SQLite-Datenbank (*.sqlite3, *.sqlite3.zip, *.sql, *.sql.zip)|*.sqlite3;*.sqlite3.zip;*.sql;*.sql.zip",
};
if (d.ShowDialog() == true) {
if (!InteractionService.AskContinue("Datenbank wiederherstellen", "Soll die Datenbank wirklich unwiederruflich durch die wiederhergestellte Version ersetzt werden?"))
return;
Mouse.OverrideCursor = Cursors.Wait;
await App.ReplaceDatabase(d.FileName);
}
} catch (Exception exc) {
InteractionService.ShowException(exc);
}
Mouse.OverrideCursor = null;
} }
private async void SyncButton_Click(object sender, RoutedEventArgs evt) { private async void SyncButton_Click(object sender, RoutedEventArgs evt) {