fix(data): address code review findings

- Fix sort order regression in GetByListIdAsync (was descending, should be ascending)
- Restore WAL mode pragma (was silently dropped in EF migration)
- Add existing-DB compatibility shim in MigrateAndConfigure (baselines InitialCreate
  migration for databases created by the old schema.sql)
- Remove dead AddDbContext/AddScoped registrations from Worker (only IDbContextFactory
  is used by singleton consumers)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-16 09:10:35 +02:00
parent 8d61b05179
commit 611454df1e
5 changed files with 48 additions and 12 deletions

View File

@@ -21,7 +21,8 @@ sealed class Program
using (var scope = services.CreateScope()) using (var scope = services.CreateScope())
{ {
scope.ServiceProvider.GetRequiredService<ClaudeDoDbContext>().Database.Migrate(); ClaudeDoDbContext.MigrateAndConfigure(
scope.ServiceProvider.GetRequiredService<ClaudeDoDbContext>());
} }
try try

View File

@@ -1,5 +1,7 @@
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
namespace ClaudeDo.Data; namespace ClaudeDo.Data;
@@ -19,4 +21,44 @@ public class ClaudeDoDbContext : DbContext
{ {
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ClaudeDoDbContext).Assembly); modelBuilder.ApplyConfigurationsFromAssembly(typeof(ClaudeDoDbContext).Assembly);
} }
/// <summary>
/// Applies EF Core migrations and sets WAL mode. Safe for both fresh and existing databases.
/// Existing databases (created by the old schema.sql) have their tables but no
/// __EFMigrationsHistory — this method detects that case and baselines the initial
/// migration so EF skips re-creating tables that already exist.
/// </summary>
public static void MigrateAndConfigure(ClaudeDoDbContext db)
{
// If the 'lists' table exists but __EFMigrationsHistory does not,
// this is a pre-EF database. Baseline the InitialCreate migration.
var conn = db.Database.GetDbConnection();
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='lists'";
var hasLists = Convert.ToInt64(cmd.ExecuteScalar()) > 0;
cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='__EFMigrationsHistory'";
var hasHistory = Convert.ToInt64(cmd.ExecuteScalar()) > 0;
if (hasLists && !hasHistory)
{
// Create the history table and mark InitialCreate as applied.
cmd.CommandText = """
CREATE TABLE "__EFMigrationsHistory" (
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
"ProductVersion" TEXT NOT NULL
);
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260416064948_InitialCreate', '8.0.11');
""";
cmd.ExecuteNonQuery();
}
}
conn.Close();
db.Database.Migrate();
db.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL");
}
} }

View File

@@ -38,7 +38,7 @@ public sealed class TaskRepository
{ {
return await _context.Tasks return await _context.Tasks
.Where(t => t.ListId == listId) .Where(t => t.ListId == listId)
.OrderByDescending(t => t.CreatedAt) .OrderBy(t => t.CreatedAt)
.ToListAsync(ct); .ToListAsync(ct);
} }

View File

@@ -19,7 +19,7 @@ public sealed class InitDatabaseStep : IInstallStep
.UseSqlite($"Data Source={expandedPath}") .UseSqlite($"Data Source={expandedPath}")
.Options; .Options;
using var context = new ClaudeDoDbContext(options); using var context = new ClaudeDoDbContext(options);
context.Database.Migrate(); ClaudeDoDbContext.MigrateAndConfigure(context);
progress.Report("Schema applied successfully"); progress.Report("Schema applied successfully");
return Task.FromResult(StepResult.Ok()); return Task.FromResult(StepResult.Ok());

View File

@@ -17,16 +17,8 @@ builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker");
builder.Services.AddDbContextFactory<ClaudeDoDbContext>(opt => builder.Services.AddDbContextFactory<ClaudeDoDbContext>(opt =>
opt.UseSqlite($"Data Source={cfg.DbPath}")); opt.UseSqlite($"Data Source={cfg.DbPath}"));
builder.Services.AddDbContext<ClaudeDoDbContext>(opt =>
opt.UseSqlite($"Data Source={cfg.DbPath}"));
builder.Services.AddSingleton(cfg); builder.Services.AddSingleton(cfg);
builder.Services.AddScoped<TagRepository>();
builder.Services.AddScoped<ListRepository>();
builder.Services.AddScoped<TaskRepository>();
builder.Services.AddScoped<SubtaskRepository>();
builder.Services.AddScoped<WorktreeRepository>();
builder.Services.AddScoped<TaskRunRepository>();
builder.Services.AddHostedService<StaleTaskRecovery>(); builder.Services.AddHostedService<StaleTaskRecovery>();
builder.Services.AddSignalR(); builder.Services.AddSignalR();
@@ -54,7 +46,8 @@ var app = builder.Build();
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
scope.ServiceProvider.GetRequiredService<ClaudeDoDbContext>().Database.Migrate(); ClaudeDoDbContext.MigrateAndConfigure(
scope.ServiceProvider.GetRequiredService<ClaudeDoDbContext>());
} }
app.MapHub<WorkerHub>("/hub"); app.MapHub<WorkerHub>("/hub");