REVIEW-FIXES-2: DB-4 (Deutsche Collation ä/ö/ü) + DB-5 (Litter.ExternalRef)

DB-4 (medium): ICU German collation de-x-icu auf deutschen Textspalten
  Gerbil.Name, Gerbil.NameSearch, Gerbil.OriginBreeder, ColorVariety.Name,
  Contact.Name. Korrekte ä/ö/ü-Sortierung + locale-aware lower() in Postgres.
  Nur bei Npgsql (Database.ProviderName check) — SQLite-Testhost kennt keine
  eigenen Collation-Namen und wuerde EnsureCreated fail lassen.
  Migration AddGermanCollation (ALTER COLUMN ... TYPE text COLLATE de-x-icu).

DB-5 (low): Litter.ExternalRef + filtered unique index + Importer-Anbindung
  Litter.ExternalRef (string?, nullable) = stable import source id (sl.Id).
  Filtered unique index WHERE ExternalRef IS NOT NULL (wie DB-1 fuer Gerbil).
  Migration AddLitterExternalRef.
  Importer (ImportService.cs): setzt ExternalRef = sl.Id beim Litter-Insert;
  prueft existingLitterExtRefSet als PRIMAEREN Idempotenz-Key (ExternalRef),
  Name+Date-Key als Fallback fuer Wuerfe ohne ExternalRef aus frueheren Laeufen.
  +Test DB5_Litter_ExternalRef_set_and_used_for_idempotency (mutiert den Namen
  nach erstem Import, prueft dass Re-Import via ExternalRef erkennt).

GATE: 140/140 C#, has-pending=No.
This commit is contained in:
2026-06-06 19:49:50 +02:00
parent e6b513ed5d
commit ea3596ca0d
9 changed files with 3047 additions and 14 deletions

View File

@@ -49,6 +49,13 @@ public class ApplicationContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// DB-4: ICU German collation on user-visible / searched text columns.
// "de-x-icu" gives correct German sort order (ä between a and b) and locale-aware
// case-folding (lower('Ä')='ä'). Applied only on Npgsql (Postgres); SQLite does not
// support custom collation names and would fail EnsureCreated on the test host.
const string DeIcu = "de-x-icu";
bool isNpgsql = Database.ProviderName?.Contains("Npgsql", StringComparison.OrdinalIgnoreCase) ?? false;
modelBuilder.Entity<Gerbil>(e =>
{
// Enums persisted as their string names (readable, Gridify-friendly).
@@ -58,6 +65,14 @@ public class ApplicationContext : DbContext
// Residency defaults to true (own stock unless explicitly marked external).
e.Property(g => g.IsResident).HasDefaultValue(true);
// DB-4: German collation on searched/sorted name fields (Npgsql-only).
if (isNpgsql)
{
e.Property(g => g.Name).UseCollation(DeIcu);
e.Property(g => g.NameSearch).UseCollation(DeIcu);
e.Property(g => g.OriginBreeder).UseCollation(DeIcu);
}
// FEAT-14: character traits stored as a JSON text column (works on both
// Npgsql and the SQLite test host; opaque labels, no backend vocabulary).
var traitsConverter = new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<List<string>, string>(
@@ -95,6 +110,12 @@ public class ApplicationContext : DbContext
.HasForeignKey(l => l.FatherId).OnDelete(DeleteBehavior.Restrict);
e.HasOne(l => l.Mother).WithMany()
.HasForeignKey(l => l.MotherId).OnDelete(DeleteBehavior.Restrict);
// DB-5: ExternalRef = source litter id from extract.py.
// Unique (filtered, nulls allowed for manually-entered litters).
e.HasIndex(l => l.ExternalRef)
.IsUnique()
.HasFilter("\"ExternalRef\" IS NOT NULL");
});
modelBuilder.Entity<HealthRecord>(e =>
@@ -176,6 +197,15 @@ public class ApplicationContext : DbContext
modelBuilder.Entity<MailSettings>()
.HasData(new MailSettings { Id = GerbilManagerWebAPI.Models.MailSettings.SingletonId });
// DB-4: German collation on remaining searched/sorted text columns (Npgsql-only).
if (isNpgsql)
{
modelBuilder.Entity<ColorVariety>()
.Property(v => v.Name).UseCollation(DeIcu);
modelBuilder.Entity<Contact>()
.Property(c => c.Name).UseCollation(DeIcu);
}
SeedColorVarieties(modelBuilder);
}