feat(import): RennmausPro-III-Backup-Importer (analyze+execute, Dedup, Fotos)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
477
GerbilManagerWebAPI/Import/Rpro3/Rpro3Reader.cs
Normal file
477
GerbilManagerWebAPI/Import/Rpro3/Rpro3Reader.cs
Normal file
@@ -0,0 +1,477 @@
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using GerbilManagerWebAPI.Models;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace GerbilManagerWebAPI.Import.Rpro3
|
||||
{
|
||||
/// <summary>
|
||||
/// Liest eine RennmausPro-III-`.backup` (ZIP mit `_rpro3.db` + `.mxp`) bzw. eine entpackte
|
||||
/// `_rpro3.db` direkt und überführt sie in <see cref="Rpro3Data"/>.
|
||||
///
|
||||
/// KRITISCHE PARSING-DETAILS (Port von tools/import/compare_rpro3.py):
|
||||
/// 1. DATUM: _BIRTH/_AM/_DATE/_TODAM/_ABAM sind astronomische Julianische Tageszahlen (REAL).
|
||||
/// 0/None = unbekannt. date = fromordinal(round(jdn) - 1721425).
|
||||
/// 2. ENCODING (gemischt!): TEXT teils UTF-8 (neuer), teils CP1252 (älter). Wir lesen jede
|
||||
/// Spalte als BLOB (Bytes) und dekodieren erst strikt UTF-8, bei Fehler CP1252
|
||||
/// (siehe smart_decode). Microsoft.Data.Sqlite würde TEXT sonst als UTF-8 erzwingen → Mojibake.
|
||||
/// 3. ID-NAMENSRÄUME: "N"=stamm_tb, "uN"=fremd_tb, "XjY"=wurftier_tb.
|
||||
/// </summary>
|
||||
public sealed class Rpro3Reader
|
||||
{
|
||||
private static bool _cp1252Registered;
|
||||
|
||||
public Rpro3Reader()
|
||||
{
|
||||
// CP1252 ist auf .NET Core nicht ohne CodePagesEncodingProvider verfügbar.
|
||||
if (!_cp1252Registered)
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
_cp1252Registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Findet/extrahiert die `_rpro3.db` aus einem Upload-Stream (ZIP/.backup) und
|
||||
/// parst sie. Wirft <see cref="Rpro3FormatException"/>, wenn keine DB gefunden wurde.
|
||||
/// Gibt den temporären Arbeitsordner zurück (Caller löscht ihn).</summary>
|
||||
public Rpro3Data ReadFromBackup(Stream backupStream, out string workDir)
|
||||
{
|
||||
workDir = Path.Combine(Path.GetTempPath(), "rpro3-import-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(workDir);
|
||||
try
|
||||
{
|
||||
using var archive = new ZipArchive(backupStream, ZipArchiveMode.Read, leaveOpen: true);
|
||||
var dbEntry = archive.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals("_rpro3.db", StringComparison.OrdinalIgnoreCase))
|
||||
?? archive.Entries.FirstOrDefault(e =>
|
||||
e.Name.EndsWith(".db", StringComparison.OrdinalIgnoreCase));
|
||||
if (dbEntry is null)
|
||||
throw new Rpro3FormatException(
|
||||
"In der hochgeladenen Datei wurde keine RennmausPro-Datenbank (_rpro3.db) gefunden.");
|
||||
|
||||
var dbPath = Path.Combine(workDir, "_rpro3.db");
|
||||
dbEntry.ExtractToFile(dbPath, overwrite: true);
|
||||
return ReadFromDbFile(dbPath);
|
||||
}
|
||||
catch (InvalidDataException ex)
|
||||
{
|
||||
throw new Rpro3FormatException(
|
||||
"Die Datei ist kein gültiges RennmausPro-Backup (kein lesbares ZIP-Archiv).", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Parst eine bereits entpackte `_rpro3.db` direkt (für Tests/Schnellpfad).</summary>
|
||||
public Rpro3Data ReadFromDbFile(string dbPath)
|
||||
{
|
||||
var connectionString = new SqliteConnectionStringBuilder
|
||||
{
|
||||
DataSource = dbPath,
|
||||
Mode = SqliteOpenMode.ReadOnly,
|
||||
}.ToString();
|
||||
|
||||
using var conn = new SqliteConnection(connectionString);
|
||||
conn.Open();
|
||||
return Parse(conn);
|
||||
}
|
||||
|
||||
// ---------- Decoding helpers (Port von smart_decode / jdn_to_date) ----------
|
||||
|
||||
/// <summary>RPRO3-DB ist gemischt kodiert. Erst UTF-8 strikt versuchen, sonst CP1252.</summary>
|
||||
internal static string? SmartDecode(byte[]? bytes)
|
||||
{
|
||||
if (bytes is null || bytes.Length == 0) return bytes is null ? null : "";
|
||||
try
|
||||
{
|
||||
return new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)
|
||||
.GetString(bytes);
|
||||
}
|
||||
catch (DecoderFallbackException)
|
||||
{
|
||||
return Encoding.GetEncoding(1252).GetString(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>JDN (REAL) → DateOnly. 0/None/ungültig = null.</summary>
|
||||
internal static DateOnly? JdnToDate(double? jdn)
|
||||
{
|
||||
if (jdn is null || jdn.Value <= 0) return null;
|
||||
// Python: date.fromordinal(round(jdn) - 1721425). .NET-Ordinaltage zählen ab 0001-01-01
|
||||
// mit DateOnly.FromDayNumber(n) wobei DayNumber 0 = 0001-01-01 (= Python-ordinal 1).
|
||||
// → dayNumber = round(jdn) - 1721425 - 1.
|
||||
long dayNumber = (long)Math.Round(jdn.Value) - 1721425 - 1;
|
||||
if (dayNumber < 0 || dayNumber > DateOnly.MaxValue.DayNumber) return null;
|
||||
return DateOnly.FromDayNumber((int)dayNumber);
|
||||
}
|
||||
|
||||
private static Gender ParseGender(string? sex) => (sex ?? "").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"männlich" or "maennlich" or "m" or "male" => Gender.male,
|
||||
"weiblich" or "w" or "f" or "female" => Gender.female,
|
||||
_ => Gender.unknown,
|
||||
};
|
||||
|
||||
// ---------- BLOB-aware accessors ----------
|
||||
|
||||
private static string? Str(SqliteDataReader r, int ord)
|
||||
{
|
||||
if (r.IsDBNull(ord)) return null;
|
||||
// Als Bytes lesen → smart decode (UTF-8/CP1252).
|
||||
using var stream = r.GetStream(ord);
|
||||
using var ms = new MemoryStream();
|
||||
stream.CopyTo(ms);
|
||||
return SmartDecode(ms.ToArray());
|
||||
}
|
||||
|
||||
private static double? Real(SqliteDataReader r, int ord)
|
||||
{
|
||||
if (r.IsDBNull(ord)) return null;
|
||||
try { return r.GetDouble(ord); }
|
||||
catch { return double.TryParse(Str(r, ord), NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : null; }
|
||||
}
|
||||
|
||||
private static int? IntOrNull(SqliteDataReader r, int ord)
|
||||
{
|
||||
if (r.IsDBNull(ord)) return null;
|
||||
try { return (int)r.GetInt64(ord); }
|
||||
catch
|
||||
{
|
||||
var s = Str(r, ord);
|
||||
return int.TryParse(s, out var v) ? v : null;
|
||||
}
|
||||
}
|
||||
|
||||
private static decimal? DecimalOrNull(SqliteDataReader r, int ord)
|
||||
{
|
||||
if (r.IsDBNull(ord)) return null;
|
||||
try { return (decimal)r.GetDouble(ord); }
|
||||
catch
|
||||
{
|
||||
var s = Str(r, ord);
|
||||
return decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var v) ? v : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Map column name → ordinal once per query (RPRO3 column order is stable but we stay safe).
|
||||
private static Func<SqliteDataReader, string, int> OrdinalLookup(SqliteDataReader r)
|
||||
{
|
||||
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
for (int i = 0; i < r.FieldCount; i++) map[r.GetName(i)] = i;
|
||||
return (rr, name) => map.TryGetValue(name, out var o) ? o : -1;
|
||||
}
|
||||
|
||||
// ---------- Parse ----------
|
||||
|
||||
private Rpro3Data Parse(SqliteConnection conn)
|
||||
{
|
||||
var data = new Rpro3Data();
|
||||
|
||||
// herk_tb → Anzeigename + voller Kontakt
|
||||
var herkName = new Dictionary<int, string>();
|
||||
foreach (var (id, c) in ReadContacts(conn, "herk_tb", "herk", "_HERK_IGNORED"))
|
||||
{
|
||||
data.HerkContacts[c.Id.ToString()] = c;
|
||||
herkName[id] = c.DisplayName;
|
||||
}
|
||||
foreach (var (_, c) in ReadContacts(conn, "abn_tb", "abn", "_ABN_IGNORED"))
|
||||
data.AbnContacts[c.Id.ToString()] = c;
|
||||
|
||||
// baum_tb: master parent table (id → Mutter/Vater im Namensraum)
|
||||
var baum = new Dictionary<string, (string? mid, string? pid)>();
|
||||
Query(conn, "SELECT id, _MID, _PID FROM baum_tb", (r, ord) =>
|
||||
{
|
||||
var id = Str(r, ord(r, "id"));
|
||||
if (id is not null) baum[id] = (Str(r, ord(r, "_MID")), Str(r, ord(r, "_PID")));
|
||||
});
|
||||
|
||||
// Farben: color_tb (stamm), wurfcolor_tb (pup), fremdcolor_tb (extern)
|
||||
var colorStamm = ReadColors(conn, "color_tb");
|
||||
var colorPup = ReadColors(conn, "wurfcolor_tb");
|
||||
var colorExt = ReadColors(conn, "fremdcolor_tb");
|
||||
data.ColorStammCount = colorStamm.Count;
|
||||
data.ColorExtCount = colorExt.Count;
|
||||
|
||||
(string farbe, string fcode) ColorFor(string rid)
|
||||
{
|
||||
Dictionary<string, (string, string)> src =
|
||||
rid.StartsWith('u') ? colorExt : rid.Contains('j') ? colorPup : colorStamm;
|
||||
return src.TryGetValue(rid, out var v) ? v : ("", "");
|
||||
}
|
||||
|
||||
// Tod (stamm/fremd nutzen tod_tb via tid = stamm-id; fremd hat eigenes _TODAM)
|
||||
var tod = new Dictionary<string, (DateOnly? am, string? warum)>();
|
||||
Query(conn, "SELECT tid, _AM, _WARUM FROM tod_tb", (r, ord) =>
|
||||
{
|
||||
var tid = Str(r, ord(r, "tid"));
|
||||
if (tid is not null) tod[tid] = (JdnToDate(Real(r, ord(r, "_AM"))), Str(r, ord(r, "_WARUM")));
|
||||
});
|
||||
|
||||
// stamm_tb: Namen für Eltern-Auflösung
|
||||
var stammName = new Dictionary<int, string?>();
|
||||
var fremdName = new Dictionary<int, string?>();
|
||||
var pupName = new Dictionary<string, string?>();
|
||||
|
||||
// stamm_tb (eigene Tiere)
|
||||
Query(conn,
|
||||
"SELECT id,_NAME,_SEX,_BIRTH,_HERKUNFT,_ZB,_STATUS,_FEHLER,_KASTRAT_DATE FROM stamm_tb",
|
||||
(r, ord) =>
|
||||
{
|
||||
var sid = IntOrNull(r, ord(r, "id"));
|
||||
if (sid is null) return;
|
||||
var rid = sid.Value.ToString();
|
||||
var name = Str(r, ord(r, "_NAME"));
|
||||
stammName[sid.Value] = name;
|
||||
var (farbe, fcode) = ColorFor(rid);
|
||||
baum.TryGetValue(rid, out var par);
|
||||
var herkId = IntOrNull(r, ord(r, "_HERKUNFT"));
|
||||
tod.TryGetValue(rid, out var death);
|
||||
data.Animals.Add(new Rpro3Animal
|
||||
{
|
||||
Rid = rid,
|
||||
Src = Rpro3Src.Stamm,
|
||||
Name = name,
|
||||
Gender = ParseGender(Str(r, ord(r, "_SEX"))),
|
||||
Dob = JdnToDate(Real(r, ord(r, "_BIRTH"))),
|
||||
Farbe = farbe,
|
||||
Fcode = fcode,
|
||||
Origin = herkId is not null && herkName.TryGetValue(herkId.Value, out var hn) ? hn : "",
|
||||
OriginHerkId = herkId,
|
||||
Zb = Str(r, ord(r, "_ZB")),
|
||||
MidRaw = par.mid,
|
||||
PidRaw = par.pid,
|
||||
DateOfDeath = death.am,
|
||||
CauseOfDeath = death.warum,
|
||||
Defects = Str(r, ord(r, "_FEHLER")),
|
||||
IsCastrated = JdnToDate(Real(r, ord(r, "_KASTRAT_DATE"))) is not null,
|
||||
StatusCode = IntOrNull(r, ord(r, "_STATUS")),
|
||||
});
|
||||
});
|
||||
|
||||
// fremd_tb (externe Ahnen)
|
||||
Query(conn,
|
||||
"SELECT id,_NAME,_SEX,_BIRTH,_ZB,_HERK,_MID,_PID,_TODAM,_KASTRAT_DATE FROM fremd_tb",
|
||||
(r, ord) =>
|
||||
{
|
||||
var fid = IntOrNull(r, ord(r, "id"));
|
||||
if (fid is null) return;
|
||||
var rid = "u" + fid.Value;
|
||||
var name = Str(r, ord(r, "_NAME"));
|
||||
fremdName[fid.Value] = name;
|
||||
var (farbe, fcode) = ColorFor(rid);
|
||||
var herkId = IntOrNull(r, ord(r, "_HERK"));
|
||||
data.Animals.Add(new Rpro3Animal
|
||||
{
|
||||
Rid = rid,
|
||||
Src = Rpro3Src.Fremd,
|
||||
Name = name,
|
||||
Gender = ParseGender(Str(r, ord(r, "_SEX"))),
|
||||
Dob = JdnToDate(Real(r, ord(r, "_BIRTH"))),
|
||||
Farbe = farbe,
|
||||
Fcode = fcode,
|
||||
Origin = herkId is not null && herkName.TryGetValue(herkId.Value, out var hn) ? hn : "",
|
||||
OriginHerkId = herkId,
|
||||
Zb = Str(r, ord(r, "_ZB")),
|
||||
MidRaw = Str(r, ord(r, "_MID")),
|
||||
PidRaw = Str(r, ord(r, "_PID")),
|
||||
DateOfDeath = JdnToDate(Real(r, ord(r, "_TODAM"))),
|
||||
IsCastrated = JdnToDate(Real(r, ord(r, "_KASTRAT_DATE"))) is not null,
|
||||
});
|
||||
});
|
||||
|
||||
// wurf_tb (Würfe)
|
||||
Query(conn, "SELECT id,_MID,_PID,_AM,_BEZ,_BEM FROM wurf_tb", (r, ord) =>
|
||||
{
|
||||
var wid = IntOrNull(r, ord(r, "id"));
|
||||
if (wid is null) return;
|
||||
data.Litters[wid.Value] = new Rpro3Litter
|
||||
{
|
||||
Id = wid.Value,
|
||||
Name = Str(r, ord(r, "_BEZ")),
|
||||
Date = JdnToDate(Real(r, ord(r, "_AM"))),
|
||||
MotherRid = Str(r, ord(r, "_MID")),
|
||||
FatherRid = Str(r, ord(r, "_PID")),
|
||||
Notes = Str(r, ord(r, "_BEM")),
|
||||
};
|
||||
});
|
||||
|
||||
// wurftier_tb (Welpen)
|
||||
Query(conn,
|
||||
"SELECT id,_WID,_NAME,_SEX,_BIRTH,_SID,_TODAM,_WARUM,_ABAM,_ABN,_PRICE,_ZB,_FEHLER FROM wurftier_tb",
|
||||
(r, ord) =>
|
||||
{
|
||||
var id = Str(r, ord(r, "id"));
|
||||
if (id is null) return;
|
||||
var name = Str(r, ord(r, "_NAME"));
|
||||
pupName[id] = name;
|
||||
data.Pups[id] = new Rpro3Pup
|
||||
{
|
||||
Id = id,
|
||||
WurfId = IntOrNull(r, ord(r, "_WID")) ?? 0,
|
||||
Name = name,
|
||||
Gender = ParseGender(Str(r, ord(r, "_SEX"))),
|
||||
Dob = JdnToDate(Real(r, ord(r, "_BIRTH"))),
|
||||
Sid = Str(r, ord(r, "_SID")),
|
||||
DateOfDeath = JdnToDate(Real(r, ord(r, "_TODAM"))),
|
||||
CauseOfDeath = Str(r, ord(r, "_WARUM")),
|
||||
AbgabeDate = JdnToDate(Real(r, ord(r, "_ABAM"))),
|
||||
AbnId = IntOrNull(r, ord(r, "_ABN")),
|
||||
Price = DecimalOrNull(r, ord(r, "_PRICE")),
|
||||
Zb = Str(r, ord(r, "_ZB")),
|
||||
Defects = Str(r, ord(r, "_FEHLER")),
|
||||
};
|
||||
});
|
||||
|
||||
// Eltern-Namen auflösen (für Report / Provenance)
|
||||
string? NameFor(string? rid)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rid) || rid is "n" or "NULL") return null;
|
||||
if (rid.StartsWith('u'))
|
||||
return int.TryParse(rid[1..], out var fid) && fremdName.TryGetValue(fid, out var n) ? n : null;
|
||||
if (rid.Contains('j'))
|
||||
return pupName.TryGetValue(rid, out var n) ? n : null;
|
||||
return int.TryParse(rid, out var sid) && stammName.TryGetValue(sid, out var sn) ? sn : null;
|
||||
}
|
||||
data.ResolveName = NameFor;
|
||||
|
||||
// Gewichte: waage_tb (_TID = stamm-id) + jungwaage_tb (_TID = wurftier-id "XjY" oder via _WID)
|
||||
Query(conn, "SELECT _TID,_GRAMM,_DATE,_BEM FROM waage_tb", (r, ord) =>
|
||||
{
|
||||
var tid = Str(r, ord(r, "_TID"));
|
||||
if (tid is null) return;
|
||||
data.Weights.Add(new Rpro3Weight
|
||||
{
|
||||
Tid = tid,
|
||||
Grams = (int)Math.Round(Real(r, ord(r, "_GRAMM")) ?? 0),
|
||||
Date = JdnToDate(Real(r, ord(r, "_DATE"))),
|
||||
Notes = Str(r, ord(r, "_BEM")),
|
||||
});
|
||||
});
|
||||
Query(conn, "SELECT _TID,_WID,_GRAMM,_DATE,_BEM FROM jungwaage_tb", (r, ord) =>
|
||||
{
|
||||
// _TID ist hier i. d. R. die laufende Pup-Nr im Wurf; _WID der Wurf → "WIDjTID".
|
||||
var tidRaw = Str(r, ord(r, "_TID"));
|
||||
var widRaw = IntOrNull(r, ord(r, "_WID"));
|
||||
string? tid = tidRaw is not null && tidRaw.Contains('j') ? tidRaw
|
||||
: widRaw is not null && tidRaw is not null ? $"{widRaw}j{tidRaw}" : null;
|
||||
if (tid is null) return;
|
||||
data.Weights.Add(new Rpro3Weight
|
||||
{
|
||||
Tid = tid,
|
||||
Grams = (int)Math.Round(Real(r, ord(r, "_GRAMM")) ?? 0),
|
||||
Date = JdnToDate(Real(r, ord(r, "_DATE"))),
|
||||
Notes = Str(r, ord(r, "_BEM")),
|
||||
});
|
||||
});
|
||||
|
||||
// Krankheiten: krank_tb (_TID = stamm-id)
|
||||
Query(conn, "SELECT _TID,_DATE,_BEM,_MED FROM krank_tb", (r, ord) =>
|
||||
{
|
||||
var tid = Str(r, ord(r, "_TID"));
|
||||
if (tid is null) return;
|
||||
data.Health.Add(new Rpro3Health
|
||||
{
|
||||
Tid = tid,
|
||||
Date = JdnToDate(Real(r, ord(r, "_DATE"))),
|
||||
Description = Str(r, ord(r, "_BEM")),
|
||||
Medication = Str(r, ord(r, "_MED")),
|
||||
});
|
||||
});
|
||||
|
||||
// Tagebuch: diary_tb (_TID = stamm-id)
|
||||
Query(conn, "SELECT _TID,_BEZ,_DATE,_DESC FROM diary_tb", (r, ord) =>
|
||||
{
|
||||
var tid = Str(r, ord(r, "_TID"));
|
||||
if (tid is null) return;
|
||||
data.Diary.Add(new Rpro3Diary
|
||||
{
|
||||
Tid = tid,
|
||||
Title = Str(r, ord(r, "_BEZ")),
|
||||
Date = JdnToDate(Real(r, ord(r, "_DATE"))),
|
||||
Description = Str(r, ord(r, "_DESC")),
|
||||
});
|
||||
});
|
||||
|
||||
// Fotos: photo_tb (id = stamm-id, _P1/_P2/_P3 = Bilddateinamen)
|
||||
Query(conn, "SELECT id,_P1,_P2,_P3 FROM photo_tb", (r, ord) =>
|
||||
{
|
||||
var pid = IntOrNull(r, ord(r, "id"));
|
||||
if (pid is null) return;
|
||||
var set = new Rpro3PhotoSet { Tid = pid.Value.ToString() };
|
||||
foreach (var col in new[] { "_P1", "_P2", "_P3" })
|
||||
{
|
||||
var fn = Str(r, ord(r, col));
|
||||
if (!string.IsNullOrWhiteSpace(fn) && fn != "---")
|
||||
set.FileNames.Add(Path.GetFileName(fn.Replace('\\', '/')));
|
||||
}
|
||||
if (set.FileNames.Count > 0) data.Photos[set.Tid] = set;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private static Dictionary<string, (string farbe, string fcode)> ReadColors(SqliteConnection conn, string table)
|
||||
{
|
||||
var dict = new Dictionary<string, (string, string)>();
|
||||
Query(conn, $"SELECT id,_FARBE,_FCODE FROM {table}", (r, ord) =>
|
||||
{
|
||||
var id = Str(r, ord(r, "id"));
|
||||
if (id is not null)
|
||||
dict[id] = (Str(r, ord(r, "_FARBE")) ?? "", Str(r, ord(r, "_FCODE")) ?? "");
|
||||
});
|
||||
return dict;
|
||||
}
|
||||
|
||||
private static IEnumerable<(int id, Rpro3Contact c)> ReadContacts(
|
||||
SqliteConnection conn, string table, string ns, string _)
|
||||
{
|
||||
var list = new List<(int, Rpro3Contact)>();
|
||||
Query(conn,
|
||||
$"SELECT id,_BEZ,_ANREDE,_VNAME,_NNAME,_CLAN,_STR,_ORT,_PLZ,_TELP,_TELD,_MAIL,_HTTP,_MEMO,_BEM FROM {table}",
|
||||
(r, ord) =>
|
||||
{
|
||||
var id = IntOrNull(r, ord(r, "id"));
|
||||
if (id is null) return;
|
||||
list.Add((id.Value, new Rpro3Contact
|
||||
{
|
||||
Id = id.Value,
|
||||
Namespace = ns,
|
||||
Bez = Str(r, ord(r, "_BEZ")),
|
||||
Anrede = Str(r, ord(r, "_ANREDE")),
|
||||
Vname = Str(r, ord(r, "_VNAME")),
|
||||
Nname = Str(r, ord(r, "_NNAME")),
|
||||
Clan = Str(r, ord(r, "_CLAN")),
|
||||
Str = Str(r, ord(r, "_STR")),
|
||||
Ort = Str(r, ord(r, "_ORT")),
|
||||
Plz = Str(r, ord(r, "_PLZ")),
|
||||
Telp = Str(r, ord(r, "_TELP")),
|
||||
Teld = Str(r, ord(r, "_TELD")),
|
||||
Mail = Str(r, ord(r, "_MAIL")),
|
||||
Http = Str(r, ord(r, "_HTTP")),
|
||||
Memo = Str(r, ord(r, "_MEMO")),
|
||||
Bem = Str(r, ord(r, "_BEM")),
|
||||
}));
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
private static void Query(
|
||||
SqliteConnection conn, string sql,
|
||||
Action<SqliteDataReader, Func<SqliteDataReader, string, int>> onRow)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
using var r = cmd.ExecuteReader();
|
||||
Func<SqliteDataReader, string, int>? ord = null;
|
||||
while (r.Read())
|
||||
{
|
||||
ord ??= OrdinalLookup(r);
|
||||
onRow(r, ord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class Rpro3FormatException : Exception
|
||||
{
|
||||
public Rpro3FormatException(string message, Exception? inner = null) : base(message, inner) { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user