Event-Strings beim Upsert kappen (Marktsync brach komplett ab)

Beim Event-Upsert wurden nur Slug und Title gekappt. Description,
ImageUrl und Tags haben in AppDbContext ebenfalls ein HasMaxLength,
wurden aber ungekuerzt geschrieben. Ein Polymarket-Event mit einer
Beschreibung ueber 4096 Zeichen liess MySQL deshalb den gesamten Batch
mit "Data too long for column 'Description'" ablehnen - damit brach
nicht nur dieses eine Event weg, sondern der komplette Marktsync.

Sichtbar in logs/error/error-20260804.log; in der Dev-DB liegt die
laengste gespeicherte Beschreibung bei 3605 Zeichen, weil alles
Laengere nie durchkam.

Ersetzt die zwei Inline-Kuerzungen durch TruncateEventStrings, analog
zum bereits vorhandenen TruncateMarketStrings.

Regressionstest deckt beide Zweige ab (neues und bestehendes Event).
SQLite erzwingt keine varchar-Laengen, der Test prueft daher die
gespeicherten Laengen statt auf eine ausbleibende Exception.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-04 12:29:56 +02:00
co-authored by Claude Fable 5
parent aa19a89301
commit 8a8a35384a
2 changed files with 80 additions and 2 deletions
@@ -90,4 +90,67 @@ public class MarketRepositoryTests
Assert.Equal("Elections", market.Subcategory);
}
}
/// <summary>
/// Regression (2026-08-04): only Slug and Title were capped on the event upsert,
/// so a Polymarket event with an over-long Description made MySQL reject the whole
/// batch with "Data too long for column 'Description'" — aborting the entire market
/// sync, not just that one event. Every length-constrained Event field must be capped.
///
/// SQLite ignores varchar limits, so asserting "no exception" would prove nothing:
/// the assertions check the stored lengths instead.
/// </summary>
[Theory]
[InlineData(true)] // event already exists → update branch
[InlineData(false)] // new event → insert branch
public async Task AddOrUpdateEventsAsync_CapsAllLengthConstrainedFields(bool eventAlreadyExists)
{
using var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.Options;
using (var setup = new AppDbContext(options))
{
setup.Database.EnsureCreated();
if (eventAlreadyExists)
{
setup.Set<Event>().Add(new Event
{
Id = 1, Platform = PlatformType.Polymarket, PlatformEventId = 99L,
Slug = "existing", Title = "Existing", Description = "short"
});
setup.SaveChanges();
}
}
using (var ctx = new AppDbContext(options))
{
var repo = new MarketRepository(ctx);
await repo.AddOrUpdateEventsAsync(new[]
{
new Event
{
Platform = PlatformType.Polymarket,
PlatformEventId = 99L,
Slug = new string('s', 900),
Title = new string('t', 1500),
Description = new string('d', 6000),
ImageUrl = "https://x/" + new string('i', 2000),
Tags = new string('g', 2000)
}
});
}
using (var assertCtx = new AppDbContext(options))
{
var stored = await assertCtx.Set<Event>().SingleAsync(e => e.PlatformEventId == 99L);
Assert.Equal(512, stored.Slug.Length);
Assert.Equal(1024, stored.Title.Length);
Assert.Equal(4096, stored.Description!.Length);
Assert.Equal(1024, stored.ImageUrl!.Length);
Assert.Equal(1024, stored.Tags.Length);
}
}
}
@@ -136,8 +136,7 @@ public class MarketRepository : IMarketRepository
foreach (var ev in currentBatch)
{
if (ev.Slug != null && ev.Slug.Length > 512) ev.Slug = ev.Slug[..512];
if (ev.Title != null && ev.Title.Length > 1024) ev.Title = ev.Title[..1024];
TruncateEventStrings(ev);
if (existingEventsMap.TryGetValue(ev.PlatformEventId, out var existing))
{
@@ -231,6 +230,22 @@ public class MarketRepository : IMarketRepository
}
}
/// <summary>
/// Caps every length-constrained Event string to its column width.
/// Must cover ALL such fields: MySQL rejects the whole batch with
/// "Data too long for column ..." if a single one overflows, so one long
/// description aborts the entire market sync — not just that one event.
/// Keep in sync with the HasMaxLength calls in AppDbContext.
/// </summary>
private void TruncateEventStrings(Event ev)
{
ev.Slug = StringHelper.Truncate(ev.Slug, 512) ?? "";
ev.Title = StringHelper.Truncate(ev.Title, 1024) ?? "";
ev.Description = StringHelper.Truncate(ev.Description, 4096);
ev.ImageUrl = StringHelper.Truncate(ev.ImageUrl, 1024);
ev.Tags = StringHelper.Truncate(ev.Tags, 1024) ?? "";
}
private void TruncateMarketStrings(Market market)
{
market.Question = StringHelper.Truncate(market.Question, 1024) ?? "";