using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.ValueGeneration; using SVSim.Database.Models; namespace SVSim.UnitTests.Infrastructure; /// /// Replaces the default in tests. After the normal /// OnModelCreating runs, strips the Postgres sequence the production model declares /// for Viewer.ShortUdid so EnsureCreated can build the schema against SQLite (which /// has no sequence support). /// internal class SqliteFriendlyModelCustomizer : ModelCustomizer { public SqliteFriendlyModelCustomizer(ModelCustomizerDependencies dependencies) : base(dependencies) { } public override void Customize(ModelBuilder modelBuilder, DbContext context) { base.Customize(modelBuilder, context); modelBuilder.Model.RemoveSequence("ShortUdidSequence"); var shortUdidProperty = modelBuilder.Entity().Property(v => v.ShortUdid).Metadata; shortUdidProperty.RemoveAnnotation("Relational:DefaultValueSql"); shortUdidProperty.ValueGenerated = ValueGenerated.Never; AssignClientSideKeyGenerators(modelBuilder.Model); } /// /// Owned-collection shadow PKs are ValueGenerated.OnAdd with the production model /// expecting the database to auto-fill (Postgres IDENTITY). On SQLite a composite-PK column /// is not a ROWID alias, so the DB can't auto-fill it and we get NOT NULL violations. Walk /// every owned entity and swap any auto-add primary-key property to use an in-process /// counter instead. /// private static void AssignClientSideKeyGenerators(IMutableModel model) { foreach (var entityType in model.GetEntityTypes()) { if (!entityType.IsOwned()) continue; foreach (var key in entityType.GetKeys()) { foreach (var property in key.Properties) { if (property.ValueGenerated != ValueGenerated.OnAdd) continue; if (property.ClrType != typeof(int) && property.ClrType != typeof(long)) continue; property.SetValueGeneratorFactory((_, _) => property.ClrType == typeof(int) ? (ValueGenerator)new MonotonicIntValueGenerator() : new MonotonicLongValueGenerator()); } } } } } internal sealed class MonotonicIntValueGenerator : ValueGenerator { private static int _current; public override bool GeneratesTemporaryValues => false; public override int Next(EntityEntry entry) => Interlocked.Increment(ref _current); } internal sealed class MonotonicLongValueGenerator : ValueGenerator { private static long _current; public override bool GeneratesTemporaryValues => false; public override long Next(EntityEntry entry) => Interlocked.Increment(ref _current); }