diff --git a/src/Predictalytics.Application.Tests/Services/MarketRepositoryTests.cs b/src/Predictalytics.Application.Tests/Services/MarketRepositoryTests.cs index 5fc434c..783ec2a 100644 --- a/src/Predictalytics.Application.Tests/Services/MarketRepositoryTests.cs +++ b/src/Predictalytics.Application.Tests/Services/MarketRepositoryTests.cs @@ -90,4 +90,67 @@ public class MarketRepositoryTests Assert.Equal("Elections", market.Subcategory); } } + + /// + /// 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. + /// + [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() + .UseSqlite(connection) + .Options; + + using (var setup = new AppDbContext(options)) + { + setup.Database.EnsureCreated(); + if (eventAlreadyExists) + { + setup.Set().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().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); + } + } } diff --git a/src/Predictalytics.Infrastructure/Data/Repositories/MarketRepository.cs b/src/Predictalytics.Infrastructure/Data/Repositories/MarketRepository.cs index bd46b22..e130545 100644 --- a/src/Predictalytics.Infrastructure/Data/Repositories/MarketRepository.cs +++ b/src/Predictalytics.Infrastructure/Data/Repositories/MarketRepository.cs @@ -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 } } + /// + /// 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. + /// + 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) ?? "";