feat(data): rewrite all repositories to use EF Core ClaudeDoDbContext

Replace raw ADO.NET implementations with EF Core LINQ queries and
ExecuteUpdate/ExecuteDelete for bulk operations. TaskRepository preserves
FlipAllRunningToFailedAsync(reason) signature and keeps raw SQL for the
atomic queue claim (UPDATE...RETURNING). GetByListAsync alias kept for
backwards compat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-16 08:58:57 +02:00
parent 51a5dcbb73
commit 34ca1b018f
6 changed files with 227 additions and 641 deletions

View File

@@ -1,102 +1,43 @@
using ClaudeDo.Data.Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Data.Repositories;
public sealed class WorktreeRepository
{
private readonly SqliteConnectionFactory _factory;
private readonly ClaudeDoDbContext _context;
public WorktreeRepository(SqliteConnectionFactory factory) => _factory = factory;
private static string ToDb(WorktreeState s) => s switch
{
WorktreeState.Active => "active",
WorktreeState.Merged => "merged",
WorktreeState.Discarded => "discarded",
WorktreeState.Kept => "kept",
_ => throw new ArgumentOutOfRangeException(nameof(s)),
};
private static WorktreeState FromDb(string s) => s switch
{
"active" => WorktreeState.Active,
"merged" => WorktreeState.Merged,
"discarded" => WorktreeState.Discarded,
"kept" => WorktreeState.Kept,
_ => throw new ArgumentOutOfRangeException(nameof(s)),
};
public WorktreeRepository(ClaudeDoDbContext context) => _context = context;
public async Task AddAsync(WorktreeEntity entity, CancellationToken ct = default)
{
await using var conn = _factory.Open();
await using var cmd = conn.CreateCommand();
cmd.CommandText = """
INSERT INTO worktrees (task_id, path, branch_name, base_commit, head_commit, diff_stat, state, created_at)
VALUES (@task_id, @path, @branch_name, @base_commit, @head_commit, @diff_stat, @state, @created_at)
""";
cmd.Parameters.AddWithValue("@task_id", entity.TaskId);
cmd.Parameters.AddWithValue("@path", entity.Path);
cmd.Parameters.AddWithValue("@branch_name", entity.BranchName);
cmd.Parameters.AddWithValue("@base_commit", entity.BaseCommit);
cmd.Parameters.AddWithValue("@head_commit", (object?)entity.HeadCommit ?? DBNull.Value);
cmd.Parameters.AddWithValue("@diff_stat", (object?)entity.DiffStat ?? DBNull.Value);
cmd.Parameters.AddWithValue("@state", ToDb(entity.State));
cmd.Parameters.AddWithValue("@created_at", entity.CreatedAt.ToString("o"));
await cmd.ExecuteNonQueryAsync(ct);
_context.Worktrees.Add(entity);
await _context.SaveChangesAsync(ct);
}
public async Task<WorktreeEntity?> GetByTaskIdAsync(string taskId, CancellationToken ct = default)
{
await using var conn = _factory.Open();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT task_id, path, branch_name, base_commit, head_commit, diff_stat, state, created_at FROM worktrees WHERE task_id = @task_id";
cmd.Parameters.AddWithValue("@task_id", taskId);
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (!await reader.ReadAsync(ct)) return null;
return ReadWorktree(reader);
return await _context.Worktrees.FirstOrDefaultAsync(w => w.TaskId == taskId, ct);
}
public async Task UpdateHeadAsync(string taskId, string headCommit, string? diffStat, CancellationToken ct = default)
{
await using var conn = _factory.Open();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "UPDATE worktrees SET head_commit = @head_commit, diff_stat = @diff_stat WHERE task_id = @task_id";
cmd.Parameters.AddWithValue("@task_id", taskId);
cmd.Parameters.AddWithValue("@head_commit", headCommit);
cmd.Parameters.AddWithValue("@diff_stat", (object?)diffStat ?? DBNull.Value);
await cmd.ExecuteNonQueryAsync(ct);
await _context.Worktrees
.Where(w => w.TaskId == taskId)
.ExecuteUpdateAsync(s => s
.SetProperty(w => w.HeadCommit, headCommit)
.SetProperty(w => w.DiffStat, diffStat), ct);
}
public async Task SetStateAsync(string taskId, WorktreeState state, CancellationToken ct = default)
{
await using var conn = _factory.Open();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "UPDATE worktrees SET state = @state WHERE task_id = @task_id";
cmd.Parameters.AddWithValue("@task_id", taskId);
cmd.Parameters.AddWithValue("@state", ToDb(state));
await cmd.ExecuteNonQueryAsync(ct);
await _context.Worktrees
.Where(w => w.TaskId == taskId)
.ExecuteUpdateAsync(s => s.SetProperty(w => w.State, state), ct);
}
public async Task DeleteAsync(string taskId, CancellationToken ct = default)
{
await using var conn = _factory.Open();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM worktrees WHERE task_id = @task_id";
cmd.Parameters.AddWithValue("@task_id", taskId);
await cmd.ExecuteNonQueryAsync(ct);
await _context.Worktrees.Where(w => w.TaskId == taskId).ExecuteDeleteAsync(ct);
}
private static WorktreeEntity ReadWorktree(SqliteDataReader r) => new()
{
TaskId = r.GetString(0),
Path = r.GetString(1),
BranchName = r.GetString(2),
BaseCommit = r.GetString(3),
HeadCommit = r.IsDBNull(4) ? null : r.GetString(4),
DiffStat = r.IsDBNull(5) ? null : r.GetString(5),
State = FromDb(r.GetString(6)),
CreatedAt = DateTime.Parse(r.GetString(7)),
};
}