diff --git a/src/Purrse.Api/Services/SimpleFinConnectionMigration.cs b/src/Purrse.Api/Services/SimpleFinConnectionMigration.cs
index f9d03ce..fd52edc 100644
--- a/src/Purrse.Api/Services/SimpleFinConnectionMigration.cs
+++ b/src/Purrse.Api/Services/SimpleFinConnectionMigration.cs
@@ -10,13 +10,28 @@ namespace Purrse.Api.Services;
///
/// One-time data migration that splits SimpleFIN connections containing accounts
/// from multiple institutions into separate connections per institution.
+/// Tracks completion in a `data_migrations` table so it only runs once.
///
public static class SimpleFinConnectionMigration
{
+ private const string MigrationName = "SplitSimpleFinConnectionsByInstitution";
+
public static async Task RunAsync(IServiceProvider services, ILogger logger)
{
using var scope = services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
+
+ // Create tracking table if it doesn't exist, then check if already applied
+ await db.Database.ExecuteSqlRawAsync(
+ "CREATE TABLE IF NOT EXISTS data_migrations (name TEXT PRIMARY KEY, applied_at TIMESTAMP NOT NULL)");
+
+ var alreadyApplied = await db.Database
+ .SqlQueryRaw($"SELECT 1 AS \"Value\" FROM data_migrations WHERE name = '{MigrationName}'")
+ .AnyAsync();
+
+ if (alreadyApplied)
+ return;
+
var encryption = scope.ServiceProvider.GetRequiredService();
var simpleFin = scope.ServiceProvider.GetRequiredService();
@@ -26,7 +41,10 @@ public static class SimpleFinConnectionMigration
.ToListAsync();
if (connections.Count == 0)
+ {
+ await MarkCompleteAsync(db);
return;
+ }
var splitCount = 0;
@@ -113,5 +131,13 @@ public static class SimpleFinConnectionMigration
await db.SaveChangesAsync();
logger.LogInformation("SimpleFIN connection migration complete: created {Count} new connections", splitCount);
}
+
+ await MarkCompleteAsync(db);
+ }
+
+ private static async Task MarkCompleteAsync(PurrseDbContext db)
+ {
+ await db.Database.ExecuteSqlRawAsync(
+ $"INSERT INTO data_migrations (name, applied_at) VALUES ('{MigrationName}', NOW()) ON CONFLICT DO NOTHING");
}
}