Files
elwig/Elwig/Helpers/AppDbUpdater.cs
T
2026-09-23 19:42:43 +02:00

160 lines
7.0 KiB
C#

using Elwig.Services;
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 {
// Don't forget to update value in Tests/fetch-resources.bat!
public static readonly int RequiredSchemaVersion = 42;
private static int VersionOffset = 0;
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;
using (var cnx = await AppDbContext.ConnectAsync()) {
await cnx.QuickCheck();
applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
if (applId != 0x454C5747)
throw new FileFormatException($"Invalid application_id in database (0x{applId:X08})");
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((int)(schemaVers / 100), RequiredSchemaVersion);
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(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)];
List<(int ToVersion, string File)> toExecute = [];
var vers = fromVersion;
while (vers < toVersion) {
var (_, to, name) = scripts.Last(s => s.From == vers);
toExecute.Add((to, name));
vers = to;
}
if (toExecute.Count == 0)
return;
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");
await cnx.IntegrityCheck();
await cnx.ForeignKeyCheck();
foreach (var (to, script) in toExecute) {
await cnx.ExecuteEmbeddedScript(asm, script);
if (to == 42) {
await UpdateDbSchema_41_To_42(cnx);
}
}
await cnx.IntegrityCheck();
await cnx.ForeignKeyCheck();
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);
}
}
private static async Task UpdateDbSchema_41_To_42(SqliteConnection cnx) {
List<(int MgNr, string LfbisNr, string Name, string Address, int Plz)> members = [];
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = """
SELECT m.mgnr, m.lfbis_nr,
COALESCE(a.name, (COALESCE(m.prefix || ' ', '') || COALESCE(m.given_name || ' ', '') || m.name || COALESCE(' ' || m.middle_names, '') || COALESCE(' ' || m.suffix, ''))),
COALESCE(a.address, m.address),
COALESCE(aplz.plz, mplz.plz)
FROM member m
JOIN postal_dest mp ON (mp.country, mp.id) = (m.country, m.postal_dest)
JOIN AT_plz_dest ma ON (ma.country, ma.id) = (mp.country, mp.id)
JOIN AT_plz mplz ON mplz.plz = ma.plz
LEFT JOIN member_billing_address a ON a.mgnr = m.mgnr
LEFT JOIN postal_dest ap ON (ap.country, ap.id) = (a.country, a.postal_dest)
LEFT JOIN AT_plz_dest aa ON (aa.country, aa.id) = (ap.country, mp.id)
LEFT JOIN AT_plz aplz ON aplz.plz = aa.plz
WHERE organic
""";
using var reader = await cmd.ExecuteReaderAsync();
var header = await reader.GetColumnSchemaAsync();
while (await reader.ReadAsync()) {
members.Add((reader.GetInt32(0), reader.GetString(1), reader.GetString(2), reader.GetString(3), reader.GetInt32(4)));
}
}
if (members.Count > 0)
InteractionService.ShowInformation("Datenbank-Update", "Es wird versucht, die Bio-Kontrollstellen-Informationen der Mitglieder automatisch zu laden. Dies könnte einen kurzen Moment in Anspruch nehmen.");
foreach (var m in members) {
string? oocId = null;
string? authCode = null;
var certs = await OrganicService.FetchTracesCertificates(null, m.LfbisNr, (m.Name, m.Address, m.Plz.ToString()));
if (certs.Length > 0) {
oocId = certs[0].OperatorId;
authCode = certs[0].AuthorityCode;
}
if (oocId != null || authCode != null) {
oocId = oocId == null ? "NULL" : $"'{oocId}'";
authCode = authCode == null ? "NULL" : $"'{authCode}'";
await cnx.ExecuteBatch($"UPDATE member SET ooc_id = {oocId}, org_auth_code = {authCode} WHERE mgnr = {m.MgNr}");
}
}
}
}
}