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 `GmailConnections`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `MicrosoftGraphConnections`", 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 `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.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
Assert.All( Assert.All(
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value), Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
@@ -387,6 +389,68 @@ public sealed class MigrationChainTests
Assert.Empty(await db.Database.GetPendingMigrationsAsync()); 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) private static JobTrackerContext Context(SqliteConnection connection)
{ {
var currentUser = new Mock<ICurrentUserService>(); var currentUser = new Mock<ICurrentUserService>();
@@ -0,0 +1,111 @@
using System;
using JobTrackerApi.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace JobTrackerApi.Migrations;
/// <summary>
/// Moves CV upload and extraction-run creation into the migration chain while preserving imported
/// artifacts, structured results, and their nullable relationship.
/// </summary>
[DbContext(typeof(JobTrackerContext))]
[Migration("20260830125000_AdoptCvExtractionSchema")]
public sealed class AdoptCvExtractionSchema : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase))
{
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS `CvUploadArtifacts` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`OriginalFileName` longtext NOT NULL,
`StoredFileName` longtext NOT NULL,
`MimeType` longtext NOT NULL,
`ByteSize` bigint NOT NULL,
`Sha256` longtext NOT NULL,
`StoragePath` longtext NOT NULL,
`UploadedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`)
) CHARACTER SET=utf8mb4;
CREATE INDEX IF NOT EXISTS `IX_CvUploadArtifacts_OwnerUserId_UploadedAtUtc`
ON `CvUploadArtifacts` (`OwnerUserId`(191), `UploadedAtUtc`);
CREATE TABLE IF NOT EXISTS `CvExtractionRuns` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`ArtifactId` int NULL,
`Trigger` longtext NOT NULL,
`ParserVersion` longtext NOT NULL,
`NormalizerVersion` longtext NOT NULL,
`LlmPromptVersion` longtext NOT NULL,
`Status` longtext NOT NULL,
`RawExtractedText` longtext NULL,
`NormalizedText` longtext NULL,
`StructuredProfileJson` longtext NULL,
`ErrorMessage` longtext NULL,
`StartedAtUtc` datetime(6) NOT NULL,
`CompletedAtUtc` datetime(6) NULL,
`AppliedAtUtc` datetime(6) NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_CvExtractionRuns_CvUploadArtifacts_ArtifactId`
FOREIGN KEY (`ArtifactId`) REFERENCES `CvUploadArtifacts` (`Id`) ON DELETE SET NULL
) CHARACTER SET=utf8mb4;
CREATE INDEX IF NOT EXISTS `IX_CvExtractionRuns_OwnerUserId_StartedAtUtc`
ON `CvExtractionRuns` (`OwnerUserId`(191), `StartedAtUtc`);
CREATE INDEX IF NOT EXISTS `IX_CvExtractionRuns_ArtifactId`
ON `CvExtractionRuns` (`ArtifactId`);
""");
return;
}
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS "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 INDEX IF NOT EXISTS "IX_CvUploadArtifacts_OwnerUserId_UploadedAtUtc"
ON "CvUploadArtifacts" ("OwnerUserId", "UploadedAtUtc");
CREATE TABLE IF NOT EXISTS "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
);
CREATE INDEX IF NOT EXISTS "IX_CvExtractionRuns_OwnerUserId_StartedAtUtc"
ON "CvExtractionRuns" ("OwnerUserId", "StartedAtUtc");
CREATE INDEX IF NOT EXISTS "IX_CvExtractionRuns_ArtifactId"
ON "CvExtractionRuns" ("ArtifactId");
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Preserve uploaded-artifact metadata and extraction history from pre-migration installs.
}
}
@@ -548,45 +548,6 @@ public static class StartupInitializationExtensions
static void EnsureCvTables(DbConnection c) static void EnsureCvTables(DbConnection c)
{ {
Exec(c, """
CREATE TABLE IF NOT EXISTS "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
);
""");
Exec(c, """
CREATE TABLE IF NOT EXISTS "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
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CvUploadArtifacts_OwnerUserId_UploadedAtUtc" ON "CvUploadArtifacts" ("OwnerUserId", "UploadedAtUtc");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CvExtractionRuns_OwnerUserId_StartedAtUtc" ON "CvExtractionRuns" ("OwnerUserId", "StartedAtUtc");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CvExtractionRuns_ArtifactId" ON "CvExtractionRuns" ("ArtifactId");""");
Exec(c, """ Exec(c, """
CREATE TABLE IF NOT EXISTS "TailoredCvDrafts" ( CREATE TABLE IF NOT EXISTS "TailoredCvDrafts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_TailoredCvDrafts" PRIMARY KEY AUTOINCREMENT, "Id" INTEGER NOT NULL CONSTRAINT "PK_TailoredCvDrafts" PRIMARY KEY AUTOINCREMENT,
@@ -1293,49 +1254,6 @@ public static class StartupInitializationExtensions
seedRuleSettings.ExecuteNonQuery(); seedRuleSettings.ExecuteNonQuery();
} }
if (!HasMySqlTable(conn, "CvUploadArtifacts"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `CvUploadArtifacts` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`OriginalFileName` longtext NOT NULL,
`StoredFileName` longtext NOT NULL,
`MimeType` longtext NOT NULL,
`ByteSize` bigint NOT NULL,
`Sha256` longtext NOT NULL,
`StoragePath` longtext NOT NULL,
`UploadedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`)
);";
cmd.ExecuteNonQuery();
}
if (!HasMySqlTable(conn, "CvExtractionRuns") && HasMySqlTable(conn, "CvUploadArtifacts"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `CvExtractionRuns` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`ArtifactId` int NULL,
`Trigger` longtext NOT NULL,
`ParserVersion` longtext NOT NULL,
`NormalizerVersion` longtext NOT NULL,
`LlmPromptVersion` longtext NOT NULL,
`Status` longtext NOT NULL,
`RawExtractedText` longtext NULL,
`NormalizedText` longtext NULL,
`StructuredProfileJson` longtext NULL,
`ErrorMessage` longtext NULL,
`StartedAtUtc` datetime(6) NOT NULL,
`CompletedAtUtc` datetime(6) NULL,
`AppliedAtUtc` datetime(6) NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_CvExtractionRuns_CvUploadArtifacts_ArtifactId` FOREIGN KEY (`ArtifactId`) REFERENCES `CvUploadArtifacts` (`Id`) ON DELETE SET NULL
);";
cmd.ExecuteNonQuery();
}
EnsureMySqlColumn(conn, "GmailConnections", "LastSyncAttemptedAt", "ALTER TABLE `GmailConnections` ADD COLUMN `LastSyncAttemptedAt` datetime(6) NULL;"); EnsureMySqlColumn(conn, "GmailConnections", "LastSyncAttemptedAt", "ALTER TABLE `GmailConnections` ADD COLUMN `LastSyncAttemptedAt` datetime(6) NULL;");
EnsureMySqlColumn(conn, "GmailConnections", "LastSyncSucceededAt", "ALTER TABLE `GmailConnections` ADD COLUMN `LastSyncSucceededAt` datetime(6) NULL;"); EnsureMySqlColumn(conn, "GmailConnections", "LastSyncSucceededAt", "ALTER TABLE `GmailConnections` ADD COLUMN `LastSyncSucceededAt` datetime(6) NULL;");
EnsureMySqlColumn(conn, "GmailConnections", "LastSyncMode", "ALTER TABLE `GmailConnections` ADD COLUMN `LastSyncMode` varchar(255) NULL;"); EnsureMySqlColumn(conn, "GmailConnections", "LastSyncMode", "ALTER TABLE `GmailConnections` ADD COLUMN `LastSyncMode` varchar(255) NULL;");
@@ -15,6 +15,8 @@ internal static class StartupSchemaOwnership
"Attachments", "Attachments",
"Companies", "Companies",
"Correspondences", "Correspondences",
"CvExtractionRuns",
"CvUploadArtifacts",
"EmailDrafts", "EmailDrafts",
"EmailSendAttempts", "EmailSendAttempts",
"GmailConnections", "GmailConnections",
@@ -55,8 +57,6 @@ internal static class StartupSchemaOwnership
"CareerProjects", "CareerProjects",
"CareerSkills", "CareerSkills",
"CoverLetterVersions", "CoverLetterVersions",
"CvExtractionRuns",
"CvUploadArtifacts",
"CvVariants", "CvVariants",
"CvVariantVersions", "CvVariantVersions",
"InterviewPrepItems", "InterviewPrepItems",
+1 -1
View File
@@ -558,7 +558,7 @@ SEC-008 implements the same durable state machine with `<final>.uploading` and `
### P3-1 — Reduce dual schema ownership incrementally ### P3-1 — Reduce dual schema ownership incrementally
**Status (2026-08-30): in progress.** All 49 model tables now have an executable, disjoint creation-owner classification. Nine formerly reconciler-created tables—including authentication support and all three email-provider connections—have moved through additive provider-aware migrations with legacy-row preservation, downgrade/retry safety, startup-DDL removal, fresh SQLite runtime proof and generated MariaDB SQL. The Gmail decision migration also closes its missing MariaDB creation path. Twenty-six startup-created tables remain; see `docs/verification/jt-019-schema-ownership.md` and V-194V-198. **Status (2026-08-30): in progress.** All 49 model tables now have an executable, disjoint creation-owner classification. Eleven formerly reconciler-created tables—including authentication support, email-provider connections, and CV extraction persistence—have moved through additive provider-aware migrations with legacy-row preservation, downgrade/retry safety, startup-DDL removal, fresh SQLite runtime proof and generated MariaDB SQL. The Gmail decision migration also closes its missing MariaDB creation path. Twenty-four startup-created tables remain; see `docs/verification/jt-019-schema-ownership.md` and V-194V-199.
- **Findings/scope:** JT-019; inventory reconciler operations and move stable schema ownership to EF migrations in small steps. - **Findings/scope:** JT-019; inventory reconciler operations and move stable schema ownership to EF migrations in small steps.
- **Dependencies:** provider upgrade fixtures and P2-2 restore safety. - **Dependencies:** provider upgrade fixtures and P2-2 restore safety.
+1
View File
@@ -230,3 +230,4 @@ Output was reduced to filenames and commit counts. The token artifact appears un
| V-196 | Ownership/migration chain; populated adoption/downgrade/retry; MariaDB script; full backend | Repository root / disposable local SQLite | Move GmailReviewDecisions to migration ownership and close the missing MariaDB table path | PASS — focused ownership/migration 8/8; representative decision survives adoption, downgrade and re-upgrade; generated MariaDB SQL contains provider-safe DDL; SQLite startup create is removed; full backend 724/724 | MariaDB SQL generated only; no provider account or production migration. Down preserves ambiguous legacy data by design. Thirty-two reconciler-owned tables remain | JT-019 third leaf transfer complete; provider runtime and dependency groups continue incrementally | | V-196 | Ownership/migration chain; populated adoption/downgrade/retry; MariaDB script; full backend | Repository root / disposable local SQLite | Move GmailReviewDecisions to migration ownership and close the missing MariaDB table path | PASS — focused ownership/migration 8/8; representative decision survives adoption, downgrade and re-upgrade; generated MariaDB SQL contains provider-safe DDL; SQLite startup create is removed; full backend 724/724 | MariaDB SQL generated only; no provider account or production migration. Down preserves ambiguous legacy data by design. Thirty-two reconciler-owned tables remain | JT-019 third leaf transfer complete; provider runtime and dependency groups continue incrementally |
| V-197 | Ownership/migration chain; populated adoption/downgrade/retry; index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move the authentication-support group to migration ownership without invalidating recovery codes, trusted devices, or active sessions | PASS — focused ownership/migration 9/9; representative rows survive adoption, downgrade and re-upgrade; expected indexes exist; generated MariaDB SQL contains all three provider-safe definitions; both startup-create paths are removed; full backend 725/725 | MariaDB SQL generated only; no provider account or production migration. Guarded MariaDB index/auto-increment repairs remain temporarily. Twenty-nine reconciler-owned tables remain | JT-019 authentication-support transfer complete; dependent feature groups continue incrementally | | V-197 | Ownership/migration chain; populated adoption/downgrade/retry; index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move the authentication-support group to migration ownership without invalidating recovery codes, trusted devices, or active sessions | PASS — focused ownership/migration 9/9; representative rows survive adoption, downgrade and re-upgrade; expected indexes exist; generated MariaDB SQL contains all three provider-safe definitions; both startup-create paths are removed; full backend 725/725 | MariaDB SQL generated only; no provider account or production migration. Guarded MariaDB index/auto-increment repairs remain temporarily. Twenty-nine reconciler-owned tables remain | JT-019 authentication-support transfer complete; dependent feature groups continue incrementally |
| V-198 | Ownership/migration chain; populated adoption/downgrade/retry; encrypted-value and index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move Gmail, Microsoft Graph, and IMAP connection records to migration ownership without losing credentials or sync state | PASS — focused ownership/migration 10/10; representative encrypted values survive adoption, downgrade and re-upgrade; all five expected indexes exist; generated MariaDB SQL contains all three provider-safe definitions; both startup-create paths are removed; full backend 726/726 | MariaDB SQL generated only; no provider account or production migration. Gmail column repair and guarded MariaDB index/auto-increment repairs remain temporarily. Twenty-six reconciler-owned tables remain | JT-019 email-provider transfer complete; dependent CV/career groups continue incrementally | | V-198 | Ownership/migration chain; populated adoption/downgrade/retry; encrypted-value and index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move Gmail, Microsoft Graph, and IMAP connection records to migration ownership without losing credentials or sync state | PASS — focused ownership/migration 10/10; representative encrypted values survive adoption, downgrade and re-upgrade; all five expected indexes exist; generated MariaDB SQL contains all three provider-safe definitions; both startup-create paths are removed; full backend 726/726 | MariaDB SQL generated only; no provider account or production migration. Gmail column repair and guarded MariaDB index/auto-increment repairs remain temporarily. Twenty-six reconciler-owned tables remain | JT-019 email-provider transfer complete; dependent CV/career groups continue incrementally |
| V-199 | Ownership/migration chain; populated adoption/downgrade/retry; FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move CV upload artifacts and extraction runs to migration ownership without losing import history | PASS — focused ownership/migration 11/11; artifact hash and structured result survive adoption, downgrade and re-upgrade; all three indexes exist; artifact deletion sets the run link null; generated MariaDB SQL contains provider-safe parent/child DDL; startup creates neither table; full backend 727/727 | MariaDB SQL generated only; no provider account or production migration. Guarded MariaDB index/auto-increment repairs remain temporarily. Twenty-four reconciler-owned tables remain | JT-019 CV extraction persistence transfer complete; tailored CV and career groups continue incrementally |
+6 -3
View File
@@ -67,7 +67,7 @@ already-correct database. Two consequences worth knowing:
Created by EF migrations, never by the reconciler: Created by EF migrations, never by the reconciler:
`AccountDeletionFiles`, `AccountDeletionRequests`, `AiUsageRecords`, `Attachments`, `Companies`, `AccountDeletionFiles`, `AccountDeletionRequests`, `AiUsageRecords`, `Attachments`, `Companies`,
`Correspondences`, `EmailDrafts`, `EmailSendAttempts`, `GmailConnections`, `Correspondences`, `CvExtractionRuns`, `CvUploadArtifacts`, `EmailDrafts`, `EmailSendAttempts`, `GmailConnections`,
`GmailReviewDecisions`, `ImapConnections`, `JobApplications`, `JobEvents`, `Jobs`, `GmailReviewDecisions`, `ImapConnections`, `JobApplications`, `JobEvents`, `Jobs`,
`MicrosoftGraphConnections`, `RuleSettings`, `SystemEmailSettings`, `TrustedDevices`, `MicrosoftGraphConnections`, `RuleSettings`, `SystemEmailSettings`, `TrustedDevices`,
`TwoFactorRecoveryCodes`, `UserNotifications`, `UserOperations`, `UserRuleSettings`, and `UserSessions`. `TwoFactorRecoveryCodes`, `UserNotifications`, `UserOperations`, `UserRuleSettings`, and `UserSessions`.
@@ -92,6 +92,10 @@ The email-provider connection group (`GmailConnections`, `MicrosoftGraphConnecti
additive Gmail sync-column repair plus guarded MariaDB identity/index repair; it does not create additive Gmail sync-column repair plus guarded MariaDB identity/index repair; it does not create
these tables. these tables.
The CV import persistence group (`CvUploadArtifacts` and `CvExtractionRuns`) moved in
`20260830125000_AdoptCvExtractionSchema`. The extraction run's optional artifact foreign key remains
`ON DELETE SET NULL`, so retention cleanup cannot erase the extraction/review record.
The reconciler may **repair** these (add a missing column, add an index, fix a non-`AUTO_INCREMENT` The reconciler may **repair** these (add a missing column, add an index, fix a non-`AUTO_INCREMENT`
primary key) and may seed the default `RuleSettings` row — but it must never `CREATE TABLE` them. primary key) and may seed the default `RuleSettings` row — but it must never `CREATE TABLE` them.
It used to create `RuleSettings`, which is precisely why a clean install failed with It used to create `RuleSettings`, which is precisely why a clean install failed with
@@ -101,8 +105,7 @@ It used to create `RuleSettings`, which is precisely why a clean install failed
Created by `StartupInitializationExtensions`, with a **no-op migration** holding the model snapshot: Created by `StartupInitializationExtensions`, with a **no-op migration** holding the model snapshot:
`CvUploadArtifacts`, `CvExtractionRuns`, `TailoredCvDrafts`, `TailoredCvDrafts`, `CareerProfiles`, `CareerProfileVersions`, the six CareerProfile children (`CareerExperiences`,
`CareerProfiles`, `CareerProfileVersions`, the six CareerProfile children (`CareerExperiences`,
`CareerEducations`, `CareerSkills`, `CareerProjects`, `CareerCertifications`, `CareerLanguages`), `CareerEducations`, `CareerSkills`, `CareerProjects`, `CareerCertifications`, `CareerLanguages`),
`InterviewPrepNotes`, `AiWorkspaceNotes`, `CvVariants`, `CvVariantVersions`, `AiInteractions`, `InterviewPrepNotes`, `AiWorkspaceNotes`, `CvVariants`, `CvVariantVersions`, `AiInteractions`,
`ApplicationChecklistItems`, `CoverLetterVersions`, and `InterviewPrepItems`. `ApplicationChecklistItems`, `CoverLetterVersions`, and `InterviewPrepItems`.
+6 -2
View File
@@ -25,6 +25,8 @@ cohesive table groups. It does not attempt to delete the legacy reconciler whole
- Added `20260830124000_AdoptEmailConnectionSchema` for Gmail, Microsoft Graph, and IMAP connection - Added `20260830124000_AdoptEmailConnectionSchema` for Gmail, Microsoft Graph, and IMAP connection
records. Encrypted credentials and sync state are retained; scoped Gmail column repair and guarded records. Encrypted credentials and sync state are retained; scoped Gmail column repair and guarded
MariaDB index/identity repair remain for historical installations. MariaDB index/identity repair remain for historical installations.
- Added `20260830125000_AdoptCvExtractionSchema` for upload artifacts and extraction runs, retaining
parsed output and the nullable `ON DELETE SET NULL` artifact relationship.
- Corrected the ownership runbook: new tables now default to migrations; the reconciler is legacy - Corrected the ownership runbook: new tables now default to migrations; the reconciler is legacy
compatibility code to retire one dependency group at a time. compatibility code to retire one dependency group at a time.
@@ -48,14 +50,16 @@ migration.
and re-upgrade, and their indexes are present afterwards. and re-upgrade, and their indexes are present afterwards.
- Representative encrypted credentials for all three email providers survive adoption, downgrade, - Representative encrypted credentials for all three email providers survive adoption, downgrade,
and re-upgrade, and all five expected provider indexes are present afterwards. and re-upgrade, and all five expected provider indexes are present afterwards.
- Representative CV artifact metadata and structured extraction output survive adoption, downgrade,
and re-upgrade; deleting the artifact preserves the run and clears its nullable relationship.
- Generated MariaDB SQL contains the provider-correct `SystemEmailSettings` DDL. - Generated MariaDB SQL contains the provider-correct `SystemEmailSettings` DDL.
- Full backend: 726/726 passed after the email-provider connection transfer. - Full backend: 727/727 passed after the CV extraction persistence transfer.
- Fresh application startup over a new disposable SQLite database applied - Fresh application startup over a new disposable SQLite database applied
`20260830120000_AdoptSystemEmailSettingsSchema` and reached the healthy listening state. `20260830120000_AdoptSystemEmailSettingsSchema` and reached the healthy listening state.
## Remaining JT-019 work ## Remaining JT-019 work
Twenty-six model tables remain startup-created, including the Identity group and several tables Twenty-four model tables remain startup-created, including the Identity group and several tables
with parent dependencies. Transfer them in small dependency-aware migrations with blank, populated, with parent dependencies. Transfer them in small dependency-aware migrations with blank, populated,
retry and MariaDB runtime proof. Column/index repairs must remain until historical upgrade fixtures retry and MariaDB runtime proof. Column/index repairs must remain until historical upgrade fixtures
prove each one redundant. prove each one redundant.
+2 -1
View File
@@ -48,6 +48,7 @@ Updated: 2026-08-30
- Moved `GmailReviewDecisions` into a provider-aware migration, preserving existing SQLite decisions and closing the previously missing MariaDB table path. - Moved `GmailReviewDecisions` into a provider-aware migration, preserving existing SQLite decisions and closing the previously missing MariaDB table path.
- Moved recovery codes, trusted devices, and revocable user sessions into one provider-aware authentication-support migration; populated legacy rows and indexes survive adoption, downgrade, and retry. - Moved recovery codes, trusted devices, and revocable user sessions into one provider-aware authentication-support migration; populated legacy rows and indexes survive adoption, downgrade, and retry.
- Moved Gmail, Microsoft Graph, and IMAP connection records into one provider-aware migration; encrypted credentials, sync state, uniqueness constraints, and historical repair paths are preserved. - Moved Gmail, Microsoft Graph, and IMAP connection records into one provider-aware migration; encrypted credentials, sync state, uniqueness constraints, and historical repair paths are preserved.
- Moved CV upload artifacts and extraction runs into a provider-aware migration; existing hashes, structured results, indexes, and nullable artifact retention semantics are preserved.
### In progress ### In progress
@@ -78,7 +79,7 @@ Updated: 2026-08-30
- Focused frontend: 2 suites, 6 tests passed. - Focused frontend: 2 suites, 6 tests passed.
- Full frontend: 64 suites, 272 tests passed. - Full frontend: 64 suites, 272 tests passed.
- Next production build and TypeScript: passed. - Next production build and TypeScript: passed.
- Full backend: 726/726 tests passed after the email-provider JT-019 transfer. - Full backend: 727/727 tests passed after the CV extraction JT-019 transfer.
- Portable Playwright launcher: resolved the user-local .NET 9 SDK; backend Release build passed with 0 warnings/errors. - Portable Playwright launcher: resolved the user-local .NET 9 SDK; backend Release build passed with 0 warnings/errors.
- Playwright: initial full run 9/10 exposed the intentional mobile Settings control change; updated focused rerun passed 1/1. A final complete browser rerun remains in the end-of-batch gate. - Playwright: initial full run 9/10 exposed the intentional mobile Settings control change; updated focused rerun passed 1/1. A final complete browser rerun remains in the end-of-batch gate.
- Focused backend match/intelligence verification: 34/34 passed. - Focused backend match/intelligence verification: 34/34 passed.