fix(data): harden FK pragma per-connection and seed concurrency
- Add SqliteForeignKeyInterceptor (DbConnectionInterceptor) registered via OnConfiguring so every IDbContextFactory-created context runs PRAGMA foreign_keys=ON, not only the MigrateAndConfigure context. - DefaultListsSeeder: replace TOCTOU read-then-insert with atomic INSERT … SELECT … WHERE NOT EXISTS — one SQLite writer lock, no race. - AppSettingsRepository.GetAsync: catch DbUpdateException on the get-or-create path and re-read so concurrent startup cannot throw. - Migration 20260609000000_UniqueListName: de-duplicates empty list rows (startup-race leftovers) then adds a UNIQUE index on lists.name. - ForeignKeyTests: verifies ON DELETE SET NULL (blocked_by_task_id) is enforced on a fresh DbContext with no manual PRAGMA call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
763732a9b3
commit
7f1a14ab80
@@ -1,6 +1,8 @@
|
||||
using System.Data.Common;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Seeding;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -9,8 +11,35 @@ namespace ClaudeDo.Data;
|
||||
|
||||
public class ClaudeDoDbContext : DbContext
|
||||
{
|
||||
// Runs PRAGMA foreign_keys=ON on every EF-managed connection open so FK
|
||||
// enforcement is active for all IDbContextFactory-created contexts, not
|
||||
// just the single context used in MigrateAndConfigure.
|
||||
private sealed class SqliteForeignKeyInterceptor : DbConnectionInterceptor
|
||||
{
|
||||
internal static readonly SqliteForeignKeyInterceptor Instance = new();
|
||||
|
||||
public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData)
|
||||
=> Apply(connection);
|
||||
|
||||
public override Task ConnectionOpenedAsync(DbConnection connection, ConnectionEndEventData eventData, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Apply(connection);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void Apply(DbConnection connection)
|
||||
{
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA foreign_keys=ON;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public ClaudeDoDbContext(DbContextOptions<ClaudeDoDbContext> options) : base(options) { }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
=> optionsBuilder.AddInterceptors(SqliteForeignKeyInterceptor.Instance);
|
||||
|
||||
public DbSet<TaskEntity> Tasks => Set<TaskEntity>();
|
||||
public DbSet<ListEntity> Lists => Set<ListEntity>();
|
||||
public DbSet<ListConfigEntity> ListConfigs => Set<ListConfigEntity>();
|
||||
|
||||
Reference in New Issue
Block a user