diff --git a/Apis/cv-matcher-data/CvMatcherDbContext.cs b/Apis/cv-matcher-data/CvMatcherDbContext.cs index ab51c35..d2cde82 100644 --- a/Apis/cv-matcher-data/CvMatcherDbContext.cs +++ b/Apis/cv-matcher-data/CvMatcherDbContext.cs @@ -36,7 +36,7 @@ public sealed class CvMatcherDbContext : DbContext entity.Property(x => x.JobDocumentId).HasMaxLength(64).IsRequired(); entity.Property(x => x.ResultJson).IsRequired(); entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()"); - entity.HasIndex(x => new { x.CvDocumentId, x.JobDocumentId }).IsUnique(); + entity.HasIndex(x => new { x.CvDocumentId, x.JobDocumentId, x.Language }).IsUnique(); }); modelBuilder.Entity(entity => diff --git a/Apis/cv-matcher-data/Migrations/20260529111000_UpdateResultsUniqueConstraintToIncludeLanguage.Designer.cs b/Apis/cv-matcher-data/Migrations/20260601131043_UpdateResultsUniqueConstraintToIncludeLanguage.Designer.cs similarity index 96% rename from Apis/cv-matcher-data/Migrations/20260529111000_UpdateResultsUniqueConstraintToIncludeLanguage.Designer.cs rename to Apis/cv-matcher-data/Migrations/20260601131043_UpdateResultsUniqueConstraintToIncludeLanguage.Designer.cs index 560b81f..a50831d 100644 --- a/Apis/cv-matcher-data/Migrations/20260529111000_UpdateResultsUniqueConstraintToIncludeLanguage.Designer.cs +++ b/Apis/cv-matcher-data/Migrations/20260601131043_UpdateResultsUniqueConstraintToIncludeLanguage.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using CvMatcher.Data; using Microsoft.EntityFrameworkCore; @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace CvMatcher.Data.Migrations { [DbContext(typeof(CvMatcherDbContext))] - [Migration("20260529111000_UpdateResultsUniqueConstraintToIncludeLanguage")] + [Migration("20260601131043_UpdateResultsUniqueConstraintToIncludeLanguage")] partial class UpdateResultsUniqueConstraintToIncludeLanguage { /// @@ -80,7 +80,7 @@ namespace CvMatcher.Data.Migrations b.Property("Language") .IsRequired() - .HasColumnType("nvarchar(max)"); + .HasColumnType("nvarchar(450)"); b.Property("ResultJson") .IsRequired() diff --git a/Apis/cv-matcher-data/Migrations/20260529111000_UpdateResultsUniqueConstraintToIncludeLanguage.cs b/Apis/cv-matcher-data/Migrations/20260601131043_UpdateResultsUniqueConstraintToIncludeLanguage.cs similarity index 59% rename from Apis/cv-matcher-data/Migrations/20260529111000_UpdateResultsUniqueConstraintToIncludeLanguage.cs rename to Apis/cv-matcher-data/Migrations/20260601131043_UpdateResultsUniqueConstraintToIncludeLanguage.cs index a2447f0..cf4c4ce 100644 --- a/Apis/cv-matcher-data/Migrations/20260529111000_UpdateResultsUniqueConstraintToIncludeLanguage.cs +++ b/Apis/cv-matcher-data/Migrations/20260601131043_UpdateResultsUniqueConstraintToIncludeLanguage.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using CvMatcher.Data; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -11,18 +10,23 @@ namespace CvMatcher.Data.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - // The Language column was added in migration 20260524140335, but the unique constraint - // was never updated from (CvDocumentId, JobDocumentId) to include Language. - // This caused duplicate key violations when matching the same CV+Job in different languages. - migrationBuilder.DropIndex( name: "IX_Results_CvDocumentId_JobDocumentId", - schema: MigrationConstants.SchemaName, + schema: "cvMatcher", table: "Results"); + migrationBuilder.AlterColumn( + name: "Language", + schema: "cvMatcher", + table: "Results", + type: "nvarchar(450)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + migrationBuilder.CreateIndex( name: "IX_Results_CvDocumentId_JobDocumentId_Language", - schema: MigrationConstants.SchemaName, + schema: "cvMatcher", table: "Results", columns: new[] { "CvDocumentId", "JobDocumentId", "Language" }, unique: true); @@ -33,12 +37,21 @@ namespace CvMatcher.Data.Migrations { migrationBuilder.DropIndex( name: "IX_Results_CvDocumentId_JobDocumentId_Language", - schema: MigrationConstants.SchemaName, + schema: "cvMatcher", table: "Results"); + migrationBuilder.AlterColumn( + name: "Language", + schema: "cvMatcher", + table: "Results", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + migrationBuilder.CreateIndex( name: "IX_Results_CvDocumentId_JobDocumentId", - schema: MigrationConstants.SchemaName, + schema: "cvMatcher", table: "Results", columns: new[] { "CvDocumentId", "JobDocumentId" }, unique: true); diff --git a/Apis/cv-matcher-data/Migrations/CvMatcherDbContextModelSnapshot.cs b/Apis/cv-matcher-data/Migrations/CvMatcherDbContextModelSnapshot.cs index 33937c3..64a6262 100644 --- a/Apis/cv-matcher-data/Migrations/CvMatcherDbContextModelSnapshot.cs +++ b/Apis/cv-matcher-data/Migrations/CvMatcherDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using CvMatcher.Data; using Microsoft.EntityFrameworkCore; @@ -77,7 +77,7 @@ namespace CvMatcher.Data.Migrations b.Property("Language") .IsRequired() - .HasColumnType("nvarchar(max)"); + .HasColumnType("nvarchar(450)"); b.Property("ResultJson") .IsRequired() @@ -88,7 +88,7 @@ namespace CvMatcher.Data.Migrations b.HasKey("Id"); - b.HasIndex("CvDocumentId", "JobDocumentId") + b.HasIndex("CvDocumentId", "JobDocumentId", "Language") .IsUnique(); b.ToTable("Results", "cvMatcher"); diff --git a/Apis/cv-matcher-data/Repositories/EfMatcherRepository.cs b/Apis/cv-matcher-data/Repositories/EfMatcherRepository.cs index 8965fc2..504fd0c 100644 --- a/Apis/cv-matcher-data/Repositories/EfMatcherRepository.cs +++ b/Apis/cv-matcher-data/Repositories/EfMatcherRepository.cs @@ -48,18 +48,27 @@ public sealed class EfMatcherRepository : IMatcherRepository if (exists) return; - _db.CvMatchResults.Add(new CvMatchResultEntity + try { - Id = Guid.NewGuid().ToString("N"), - CvDocumentId = cvDocumentId, - JobDocumentId = jobDocumentId, - Language = language, - ResultJson = JsonSerializer.Serialize(response, new JsonSerializerOptions(JsonSerializerDefaults.Web)), - Score = response.Score, - CreatedAt = DateTime.UtcNow - }); + _db.CvMatchResults.Add(new CvMatchResultEntity + { + Id = Guid.NewGuid().ToString("N"), + CvDocumentId = cvDocumentId, + JobDocumentId = jobDocumentId, + Language = language, + ResultJson = JsonSerializer.Serialize(response, new JsonSerializerOptions(JsonSerializerDefaults.Web)), + Score = response.Score, + CreatedAt = DateTime.UtcNow + }); - await _db.SaveChangesAsync(ct); + await _db.SaveChangesAsync(ct); + } + catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("IX_Results_CvDocumentId_JobDocumentId_Language") == true + || ex.InnerException?.Message.Contains("unique") == true) + { + // Duplicate key violation: record was inserted between the AnyAsync check and SaveChangesAsync. + // This is safe to ignore — the match result already exists in the database. + } } public async Task GetChatCompletionAsync(string cacheKey, CancellationToken ct)