using Data.Data.Configurations; using DeepDrftModels.Entities; using DeepDrftModels.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace DeepDrftData.Data.Configurations; public class ReleaseConfiguration : BaseEntityConfiguration { public override void Configure(EntityTypeBuilder builder) { // Wires up Id PK + audit columns (CreatedAt, UpdatedAt, IsDeleted) and the IsDeleted index. base.Configure(builder); builder.ToTable("release"); // Map the base audit columns to the snake_case naming the rest of the schema uses. builder.Property(e => e.Id).HasColumnName("id"); builder.Property(e => e.CreatedAt).HasColumnName("created_at"); builder.Property(e => e.UpdatedAt).HasColumnName("updated_at"); builder.Property(e => e.IsDeleted).HasColumnName("is_deleted"); // App-minted GUID-string public handle, configured exactly like TrackConfiguration's // entry_key: required, max 100, snake_case column. The unique index guarantees a release // resolves to one row by its public key. builder.Property(e => e.EntryKey) .IsRequired() .HasMaxLength(100) .HasColumnName("entry_key"); builder.HasIndex(e => e.EntryKey) .IsUnique() .HasDatabaseName("IX_release_entry_key"); builder.Property(e => e.Title) .IsRequired() .HasMaxLength(200) .HasColumnName("title"); builder.Property(e => e.Artist) .IsRequired() .HasMaxLength(200) .HasColumnName("artist"); builder.Property(e => e.Genre) .HasMaxLength(100) .HasColumnName("genre"); // Plain-text prose blurb. Generous ceiling for a paragraph; nullable (no data migration). builder.Property(e => e.Description) .HasMaxLength(4000) .HasColumnName("description"); builder.Property(e => e.ReleaseDate) .HasColumnName("release_date"); builder.Property(e => e.ImagePath) .HasMaxLength(500) .HasColumnName("image_path"); // ReleaseType is meaningful ONLY when Medium == Cut. It is the Cut medium's discriminator // data and lives on the base table by deliberate, named exception: // A CutMetadata satellite (mirroring SessionMetadata/MixMetadata) was considered and // rejected. ReleaseType is read on every card of the /cuts browse — the highest-traffic // read in the system. Moving it to a satellite would put a join on that hot path. So it // stays here. Future media MUST NOT copy this pattern: the default is a satellite metadata // table; this is the one allowed exception, justified solely by the /cuts read volume. // // The "ReleaseType only for Cut" invariant is advisory — enforced at the service layer and // surfaced via the nullable ReleaseDto.ReleaseType (nulled for non-Cut at the converter). // It is NOT a DB check constraint by choice, not necessity: EF supports HasCheckConstraint, // but the invariant is advisory and we keep the schema free of it. builder.Property(e => e.ReleaseType) .IsRequired() .HasConversion() // Store as readable string, not int ordinal .HasMaxLength(20) .HasColumnName("release_type") .HasDefaultValue(ReleaseType.Single); builder.Property(e => e.Medium) .IsRequired() .HasConversion() // Store as readable string, not int ordinal .HasMaxLength(20) .HasColumnName("medium") .HasDefaultValue(ReleaseMedium.Cut); // Existing rows migrate to Cut with no data migration. builder.Property(e => e.CreatedByUserId) .HasColumnName("created_by_user_id"); // Names the is_deleted index explicitly. BaseEntityConfiguration.Configure already // calls HasIndex(e => e.IsDeleted); this adds HasDatabaseName so EF always uses // "IX_release_is_deleted" regardless of auto-naming conventions. builder.HasIndex(e => e.IsDeleted).HasDatabaseName("IX_release_is_deleted"); // Unique constraint on the natural key (title + artist). Prevents duplicate release rows // from concurrent uploads of the same album. The FindOrCreateRelease path catches the // resulting UniqueViolation and re-queries for the winning row. // Partial filter excludes soft-deleted rows so re-uploading a deleted release does not // hit a uniqueness conflict when FindOrCreateRelease creates a fresh row. builder.HasIndex(e => new { e.Title, e.Artist }) .IsUnique() .HasDatabaseName("IX_release_title_artist") .HasFilter("\"is_deleted\" = false"); } }