From d158994b11ce2121f079b3f73abd16e325dfddf2 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 26 Aug 2026 18:32:16 -0400 Subject: [PATCH 1/2] fix(events): use EF inserts instead of bulk copy on SQLite EventRepository.CreateManyAsync wrote event batches through linq2db's BulkCopyAsync, which resolves a SQLite provider adapter before emitting any SQL. On self-hosted SQLite deployments that resolution can select the Classic System.Data.SQLite adapter, an assembly this repository does not reference at any version, and the resulting InvalidOperationException propagates out of BaseRequestValidator.BuildSuccessResultAsync and fails the login. Batches on this path are one event for the user plus one per events-enabled organization or provider, so bulk copy buys nothing here. Branch on Database.IsSqlite() and use AddRangeAsync with a single SaveChangesAsync, matching the provider-branching precedent in CipherRepository and CollectionRepository. MySQL and Postgres keep the bulk copy path. [PM-35184] --- .../Dirt/Repositories/EventRepository.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Infrastructure.EntityFramework/Dirt/Repositories/EventRepository.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/EventRepository.cs index 57884478a07a..070cb5cf3902 100644 --- a/src/Infrastructure.EntityFramework/Dirt/Repositories/EventRepository.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/EventRepository.cs @@ -48,6 +48,15 @@ public async Task CreateManyAsync(IEnumerable entities) var tableEvents = entities.Select(e => e as Core.Entities.Event ?? new Core.Entities.Event(e)); var entityEvents = Mapper.Map>(tableEvents); entityEvents.ForEach(e => e.SetNewId()); + + // SQLite deployments can fail to resolve the linq2db bulk copy provider adapter + if (dbContext.Database.IsSqlite()) + { + await dbContext.Events.AddRangeAsync(entityEvents); + await dbContext.SaveChangesAsync(); + return; + } + await dbContext.BulkCopyAsync(entityEvents); } } From b219186589b5a4f01f5fbd805e9d38ab668620bd Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 26 Aug 2026 18:33:05 -0400 Subject: [PATCH 2/2] test(events): cover CreateManyAsync across database providers CreateManyAsync had no test asserting it persists what it was given. The only coverage was incidental, through EventRepositoryDeleteManyTests, where the writes are setup for a delete assertion. Add multi-provider coverage for the batch path, the single-event short circuit, and the empty guard, so the SQLite branch and the bulk copy branch are both exercised on every configured provider. [PM-35184] --- .../EventRepositoryCreateManyTests.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 test/Infrastructure.IntegrationTest/Dirt/Repositories/EventRepositoryCreateManyTests.cs diff --git a/test/Infrastructure.IntegrationTest/Dirt/Repositories/EventRepositoryCreateManyTests.cs b/test/Infrastructure.IntegrationTest/Dirt/Repositories/EventRepositoryCreateManyTests.cs new file mode 100644 index 000000000000..b03856c3f77a --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Dirt/Repositories/EventRepositoryCreateManyTests.cs @@ -0,0 +1,64 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Xunit; +using Event = Bit.Core.Entities.Event; +using IEventRepository = Bit.Core.Repositories.IEventRepository; + +namespace Bit.Infrastructure.IntegrationTest.Dirt.Repositories; + +/// +/// Covers CreateManyAsync on every configured provider. IEventRepository is only +/// registered for self-hosted deployments, hence SelfHosted = true; that resolves to the +/// Dapper implementation on SQL Server and the EF implementation on the other providers. +/// +public class EventRepositoryCreateManyTests +{ + [Theory, DatabaseData(SelfHosted = true)] + public async Task CreateManyAsync_MultipleEvents_PersistsEveryEvent(IEventRepository sut) + { + var organizationId = Guid.NewGuid(); + + await sut.CreateManyAsync(BuildEvents(organizationId, 3)); + + Assert.Equal(3, (await ReadEventsAsync(sut, organizationId)).Count); + } + + [Theory, DatabaseData(SelfHosted = true)] + public async Task CreateManyAsync_SingleEvent_PersistsThatEvent(IEventRepository sut) + { + var organizationId = Guid.NewGuid(); + + await sut.CreateManyAsync(BuildEvents(organizationId, 1)); + + Assert.Single(await ReadEventsAsync(sut, organizationId)); + } + + [Theory, DatabaseData(SelfHosted = true)] + public async Task CreateManyAsync_NoEvents_DoesNotThrow(IEventRepository sut) + { + var organizationId = Guid.NewGuid(); + + await sut.CreateManyAsync([]); + + Assert.Empty(await ReadEventsAsync(sut, organizationId)); + } + + private static List BuildEvents(Guid organizationId, int count) => + Enumerable.Range(0, count) + .Select(i => new Event + { + Type = EventType.Organization_Updated, + OrganizationId = organizationId, + Date = DateTime.UtcNow.AddMinutes(-i), + }) + .ToList(); + + private static async Task> ReadEventsAsync( + IEventRepository sut, Guid organizationId) + { + var result = await sut.GetManyByOrganizationAsync( + organizationId, DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddDays(1), + new PageOptions { PageSize = 100 }); + return result.Data; + } +}