refactor(db): migrate CV extraction data

Move upload artifacts and extraction runs into an additive provider-aware migration. Preserve parsed results, indexes, and nullable artifact retention semantics.
This commit is contained in:
cesnimda
2026-08-30 16:59:05 +02:00
parent 6c50e406b5
commit e28cc0ad47
9 changed files with 193 additions and 91 deletions
@@ -108,6 +108,8 @@ public sealed class MigrationChainTests
Assert.Contains("CREATE TABLE IF NOT EXISTS `GmailConnections`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `MicrosoftGraphConnections`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `ImapConnections`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `CvUploadArtifacts`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `CvExtractionRuns`", script, StringComparison.Ordinal);
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
Assert.All(
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
@@ -387,6 +389,68 @@ public sealed class MigrationChainTests
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
[Fact]
public async Task Cv_extraction_adoption_preserves_artifacts_results_and_relationship()
{
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
await using var db = Context(connection);
var migrator = db.GetService<IMigrator>();
await migrator.MigrateAsync("20260830124000_AdoptEmailConnectionSchema");
await ExecuteAsync(connection, """
CREATE TABLE "CvUploadArtifacts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CvUploadArtifacts" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL, "OriginalFileName" TEXT NOT NULL,
"StoredFileName" TEXT NOT NULL, "MimeType" TEXT NOT NULL, "ByteSize" INTEGER NOT NULL,
"Sha256" TEXT NOT NULL, "StoragePath" TEXT NOT NULL, "UploadedAtUtc" TEXT NOT NULL
);
CREATE TABLE "CvExtractionRuns" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CvExtractionRuns" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL, "ArtifactId" INTEGER NULL, "Trigger" TEXT NOT NULL,
"ParserVersion" TEXT NOT NULL, "NormalizerVersion" TEXT NOT NULL,
"LlmPromptVersion" TEXT NOT NULL, "Status" TEXT NOT NULL,
"RawExtractedText" TEXT NULL, "NormalizedText" TEXT NULL,
"StructuredProfileJson" TEXT NULL, "ErrorMessage" TEXT NULL,
"StartedAtUtc" TEXT NOT NULL, "CompletedAtUtc" TEXT NULL, "AppliedAtUtc" TEXT NULL,
CONSTRAINT "FK_CvExtractionRuns_CvUploadArtifacts_ArtifactId"
FOREIGN KEY ("ArtifactId") REFERENCES "CvUploadArtifacts" ("Id") ON DELETE SET NULL
);
INSERT INTO "CvUploadArtifacts"
("OwnerUserId", "OriginalFileName", "StoredFileName", "MimeType", "ByteSize",
"Sha256", "StoragePath", "UploadedAtUtc")
VALUES ('owner-fixture', 'resume.pdf', 'fixture.bin', 'application/pdf', 4096,
'fixture-sha', 'uploads/fixture.bin', '2026-08-30T09:00:00+00:00');
INSERT INTO "CvExtractionRuns"
("OwnerUserId", "ArtifactId", "Trigger", "ParserVersion", "NormalizerVersion",
"LlmPromptVersion", "Status", "StructuredProfileJson", "StartedAtUtc")
VALUES ('owner-fixture', 1, 'upload', 'parser-v1', 'normalizer-v1', 'prompt-v1',
'completed', '{"summary":"preserve me"}', '2026-08-30T09:01:00+00:00');
""");
await migrator.MigrateAsync();
Assert.Equal("fixture-sha", await ScalarAsync<string>(connection,
"SELECT Sha256 FROM CvUploadArtifacts WHERE Id = 1;"));
Assert.Equal("{\"summary\":\"preserve me\"}", await ScalarAsync<string>(connection,
"SELECT StructuredProfileJson FROM CvExtractionRuns WHERE ArtifactId = 1;"));
await migrator.MigrateAsync("20260830124000_AdoptEmailConnectionSchema");
Assert.Equal("fixture-sha", await ScalarAsync<string>(connection,
"SELECT Sha256 FROM CvUploadArtifacts WHERE Id = 1;"));
await migrator.MigrateAsync();
Assert.Equal(3L, await ScalarAsync<long>(connection, """
SELECT COUNT(*) FROM sqlite_master
WHERE type = 'index' AND name IN (
'IX_CvUploadArtifacts_OwnerUserId_UploadedAtUtc',
'IX_CvExtractionRuns_OwnerUserId_StartedAtUtc',
'IX_CvExtractionRuns_ArtifactId');
"""));
await ExecuteAsync(connection, "DELETE FROM CvUploadArtifacts WHERE Id = 1;");
Assert.Equal(1L, await ScalarAsync<long>(connection,
"SELECT COUNT(*) FROM CvExtractionRuns WHERE ArtifactId IS NULL;"));
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
private static JobTrackerContext Context(SqliteConnection connection)
{
var currentUser = new Mock<ICurrentUserService>();