using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using System.Text.Json; using RobotNet10.NavigationTune.Shared.Models; namespace RobotNet10.NavigationTune.Data; /// /// Database context for tuning system /// public class TuningDbContext(DbContextOptions options) : DbContext(options) { public DbSet ParameterSets { get; set; } public DbSet TestScenarios { get; set; } public DbSet TestRuns { get; set; } public DbSet TestMetrics { get; set; } public DbSet SafetyViolations { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // JSON converter for complex types var jsonOptions = new JsonSerializerOptions { WriteIndented = false }; // ParameterSets - Configure complex types as JSON modelBuilder.Entity(entity => { entity.ToTable("parameter_sets"); entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired().HasMaxLength(200); entity.Property(e => e.Description).HasMaxLength(500); entity.Property(e => e.CreatedAt).IsRequired(); entity.Property(e => e.UpdatedAt); entity.Property(e => e.IsDefault); entity.Property(e => e.Version); entity.Property(e => e.ControllerType).HasConversion(); // Store enum as int // Store complex types as JSON entity.Property(e => e.MovePidConfig) .HasConversion( v => JsonSerializer.Serialize(v, jsonOptions), v => JsonSerializer.Deserialize(v, jsonOptions) ?? new PIDConfig()) .HasColumnType("TEXT"); // SQLite uses TEXT, PostgreSQL will use jsonb entity.Property(e => e.RotatePidConfig) .HasConversion( v => JsonSerializer.Serialize(v, jsonOptions), v => JsonSerializer.Deserialize(v, jsonOptions) ?? new PIDConfig()) .HasColumnType("TEXT"); entity.Property(e => e.PurePursuitConfig) .HasConversion( v => JsonSerializer.Serialize(v, jsonOptions), v => JsonSerializer.Deserialize(v, jsonOptions) ?? new PurePursuitConfig()) .HasColumnType("TEXT"); entity.Property(e => e.StanleyConfig) .HasConversion( v => JsonSerializer.Serialize(v, jsonOptions), v => JsonSerializer.Deserialize(v, jsonOptions) ?? new StanleyConfig()) .HasColumnType("TEXT"); entity.Property(e => e.EstimatorConfig) .HasConversion( v => JsonSerializer.Serialize(v, jsonOptions), v => JsonSerializer.Deserialize(v, jsonOptions) ?? new VelocityEstimatorConfig()) .HasColumnType("TEXT"); entity.Property(e => e.SignalConfig) .HasConversion( v => JsonSerializer.Serialize(v, jsonOptions), v => JsonSerializer.Deserialize(v, jsonOptions) ?? new VelocitySignalProcessingConfig()) .HasColumnType("TEXT"); entity.Property(e => e.MotorDynamicsConfig) .HasConversion( v => JsonSerializer.Serialize(v, jsonOptions), v => JsonSerializer.Deserialize(v, jsonOptions) ?? new MotorDynamicsConfig()) .HasColumnType("TEXT"); entity.Property(e => e.NavigationConfig) .HasConversion( v => JsonSerializer.Serialize(v, jsonOptions), v => JsonSerializer.Deserialize(v, jsonOptions) ?? new NavigationConfig()) .HasColumnType("TEXT"); entity.HasIndex(e => e.Name); entity.HasIndex(e => e.IsDefault); entity.HasIndex(e => e.CreatedAt); }); // TestScenarios - Store config as JSON since TestScenario is abstract modelBuilder.Entity(entity => { entity.ToTable("test_scenarios"); entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired().HasMaxLength(200); entity.Property(e => e.Description).HasMaxLength(500); entity.Property(e => e.Type).IsRequired(); entity.Property(e => e.ConfigJson).IsRequired().HasColumnType("TEXT"); entity.Property(e => e.CreatedAt).IsRequired(); entity.Property(e => e.IsDefault); entity.HasIndex(e => e.Type); entity.HasIndex(e => e.Name); entity.HasIndex(e => e.CreatedAt); }); // TestRuns modelBuilder.Entity(entity => { entity.ToTable("test_runs"); entity.HasKey(e => e.Id); // Foreign keys entity.Property(e => e.ScenarioId).IsRequired(); entity.Property(e => e.ParameterSetId).IsRequired(); // Navigation properties - Ignore since we load separately to avoid circular dependencies entity.Ignore(e => e.Scenario); entity.Ignore(e => e.ParameterSet); // Properties entity.Property(e => e.StartTime).IsRequired(); entity.Property(e => e.EndTime); entity.Property(e => e.Status).IsRequired().HasConversion(); entity.Property(e => e.Duration); entity.Property(e => e.Notes).HasMaxLength(1000); entity.Property(e => e.ErrorMessage).HasMaxLength(1000); // Relationships entity.HasOne(e => e.Metrics) .WithOne() .HasForeignKey(m => m.TestRunId) .OnDelete(DeleteBehavior.Cascade); entity.HasMany(e => e.SafetyViolations) .WithOne() .HasForeignKey(v => v.TestRunId) .OnDelete(DeleteBehavior.Cascade); // Indexes entity.HasIndex(e => e.ScenarioId); entity.HasIndex(e => e.ParameterSetId); entity.HasIndex(e => e.StartTime); entity.HasIndex(e => e.Status); entity.HasIndex(e => new { e.ScenarioId, e.ParameterSetId }); }); // TestMetrics modelBuilder.Entity(entity => { entity.ToTable("test_metrics"); entity.HasKey(e => e.Id); // Foreign key entity.Property(e => e.TestRunId).IsRequired(); // All metrics are double/double entity.Property(e => e.CrossTrackErrorRMS); entity.Property(e => e.CrossTrackErrorPeak); entity.Property(e => e.CrossTrackErrorMean); entity.Property(e => e.CrossTrackErrorStdDev); entity.Property(e => e.HeadingErrorRMS); entity.Property(e => e.HeadingErrorPeak); entity.Property(e => e.GoalPositionError); entity.Property(e => e.GoalHeadingError); entity.Property(e => e.VelocityStdDev); entity.Property(e => e.AccelerationStdDev); entity.Property(e => e.PathLengthRatio); entity.Property(e => e.CompletionTime); entity.Property(e => e.AverageSpeed); entity.Property(e => e.MaxSpeed); entity.Property(e => e.OverallScore); entity.Property(e => e.TrackingScore); entity.Property(e => e.SmoothnessScore); entity.Property(e => e.EfficiencyScore); entity.Property(e => e.PassedCriteria); // Unique index on TestRunId (one-to-one relationship) entity.HasIndex(e => e.TestRunId).IsUnique(); }); // SafetyViolations modelBuilder.Entity(entity => { entity.ToTable("safety_violations"); entity.HasKey(e => e.Id); // Foreign key entity.Property(e => e.TestRunId).IsRequired(); // Properties entity.Property(e => e.Timestamp).IsRequired(); entity.Property(e => e.Type).IsRequired().HasConversion(); entity.Property(e => e.Severity).IsRequired().HasConversion(); entity.Property(e => e.Value); entity.Property(e => e.Threshold); entity.Property(e => e.Message).IsRequired().HasMaxLength(500); // Indexes entity.HasIndex(e => e.TestRunId); entity.HasIndex(e => e.Timestamp); entity.HasIndex(e => new { e.TestRunId, e.Timestamp }); }); } }