# EF Core Migration Design

Replace the raw ADO.NET / Microsoft.Data.Sqlite data layer with Entity Framework Core and LINQ queries.

## Motivation

- Developer ergonomics: raw SQL is tedious to write and maintain; LINQ enables faster iteration.
- Maintainability: the ad-hoc migration approach (ALTER TABLE with error-code catching) and manual DBNull/enum mapping are a liability as the schema grows. EF Core provides proper migration versioning, value converters, and change tracking.

## Decision Summary

| Decision | Choice |
|---|---|
| Approach | Big bang — rewrite all 6 repositories at once |
| Migration strategy | Fresh start — EF Core owns the schema, drop schema.sql |
| DbContext sharing | Single shared `ClaudeDoDbContext` in ClaudeDo.Data |
| Configuration style | Fluent API only, clean POCO models |
| Atomic queue claim | Kept as `FromSqlRaw` — not expressible in LINQ |

---

## 1. DbContext and Entity Configuration

### ClaudeDoDbContext

A single `ClaudeDoDbContext` in `ClaudeDo.Data` with DbSets for all entities:

```csharp
public class ClaudeDoDbContext : DbContext
{
    public DbSet<TaskEntity> Tasks => Set<TaskEntity>();
    public DbSet<ListEntity> Lists => Set<ListEntity>();
    public DbSet<TagEntity> Tags => Set<TagEntity>();
    public DbSet<ListConfigEntity> ListConfigs => Set<ListConfigEntity>();
    public DbSet<WorktreeEntity> Worktrees => Set<WorktreeEntity>();
    public DbSet<TaskRunEntity> TaskRuns => Set<TaskRunEntity>();
    public DbSet<SubtaskEntity> Subtasks => Set<SubtaskEntity>();
}
```

### Entity-to-Table Mapping

| Entity | Table | Key | Notes |
|---|---|---|---|
| `TaskEntity` | `tasks` | `Id` (TEXT) | Nav to List, Tags, Worktree, Runs, Subtasks |
| `ListEntity` | `lists` | `Id` (TEXT) | Nav to Tasks, Tags, Config |
| `TagEntity` | `tags` | `Id` (INTEGER auto) | Nav to Lists, Tasks (both M:N) |
| `ListConfigEntity` | `list_config` | `ListId` (TEXT) | 1:1 owned by List |
| `WorktreeEntity` | `worktrees` | `TaskId` (TEXT) | 1:1 owned by Task |
| `TaskRunEntity` | `task_runs` | `Id` (TEXT) | FK to Task |
| `SubtaskEntity` | `subtasks` | `Id` (TEXT) | FK to Task |

### Navigation Properties Added to Models

```csharp
// TaskEntity gains:
public ListEntity List { get; set; }
public WorktreeEntity? Worktree { get; set; }
public ICollection<TagEntity> Tags { get; set; }
public ICollection<TaskRunEntity> Runs { get; set; }
public ICollection<SubtaskEntity> Subtasks { get; set; }

// ListEntity gains:
public ListConfigEntity? Config { get; set; }
public ICollection<TaskEntity> Tasks { get; set; }
public ICollection<TagEntity> Tags { get; set; }

// TagEntity gains:
public ICollection<ListEntity> Lists { get; set; }
public ICollection<TaskEntity> Tasks { get; set; }
```

### Enum Handling

EF Core `ValueConverter<TEnum, string>` for `TaskStatus` and `WorktreeState`, storing the same lowercase strings (`"manual"`, `"active"`, etc.) for database compatibility. The `ToDb`/`FromDb` methods in repositories are removed.

### Junction Tables

`list_tags` and `task_tags` are configured as implicit join tables via `.UsingEntity()` in Fluent API — no explicit junction entity classes needed.

### Fluent Configuration

Each entity gets its own `IEntityTypeConfiguration<T>` class in a `Configuration/` folder within `ClaudeDo.Data`.

---

## 2. Migration Strategy

### Fresh Start

- `schema.sql` and `SchemaInitializer` are deleted.
- An initial EF Core migration (`InitialCreate`) is generated from the DbContext model, producing the full schema (all 8 tables, indexes, foreign keys, check constraints).
- EF's `__EFMigrationsHistory` table tracks applied migrations.

### Startup

Both App and Worker call `context.Database.Migrate()` at startup instead of `SchemaInitializer.Apply()`. This is idempotent.

### Existing Database Compatibility

For users who already have a database created by `schema.sql`, the initial migration must handle the schema already existing. On startup, if the `lists` table exists but `__EFMigrationsHistory` does not, insert the initial migration record into `__EFMigrationsHistory` so EF skips it.

### Seed Data

The `"agent"` and `"manual"` tags move into `OnModelCreating` via `HasData()`:

```csharp
modelBuilder.Entity<TagEntity>().HasData(
    new TagEntity { Id = 1, Name = "agent" },
    new TagEntity { Id = 2, Name = "manual" });
```

### Ad-hoc Migrations Removed

The 3 manual `ALTER TABLE` statements (model, system_prompt, agent_path on tasks) become part of the initial migration since they're already in the model. The manual `ApplyMigrations()` method is deleted.

---

## 3. Repository Rewrite

All 6 repositories are rewritten to use `ClaudeDoDbContext` and LINQ.

### Per-Repository Changes

| Repository | After EF Core |
|---|---|
| `TagRepository` | LINQ queries. `GetOrCreateAsync` uses `FirstOrDefaultAsync` + `Add` + `SaveChangesAsync`. Static `SqliteConnection` overload removed. |
| `SubtaskRepository` | Straightforward LINQ CRUD, `.OrderBy(s => s.OrderNum)`. |
| `WorktreeRepository` | LINQ CRUD. State update becomes property set + `SaveChangesAsync`. |
| `ListRepository` | LINQ CRUD. Tag management via `.Tags` navigation property. Config upsert via `List.Config` navigation. |
| `TaskRunRepository` | LINQ CRUD. Latest = `.OrderByDescending(r => r.RunNumber).FirstOrDefaultAsync()`. |
| `TaskRepository` | See special cases below. |

### TaskRepository Special Cases

**Atomic queue claim** (`GetNextQueuedAgentTaskAsync`): kept as `FromSqlRaw` / `ExecuteSqlRawAsync`. The `UPDATE ... WHERE id = (SELECT ...) RETURNING` is not expressible in LINQ and the atomicity guarantee matters.

**Effective tags** (`GetEffectiveTagsAsync`): LINQ via navigation properties:

```csharp
var taskTags = context.Tasks
    .Where(t => t.Id == taskId)
    .SelectMany(t => t.Tags);
var listTags = context.Tasks
    .Where(t => t.Id == taskId)
    .SelectMany(t => t.List.Tags);
return await taskTags.Union(listTags).Distinct().ToListAsync(ct);
```

**FlipAllRunningToFailed**: EF Core 7+ bulk update:

```csharp
await context.Tasks
    .Where(t => t.Status == TaskStatus.Running)
    .ExecuteUpdateAsync(s => s.SetProperty(t => t.Status, TaskStatus.Failed), ct);
```

**Status transitions** (`MarkRunningAsync`, `MarkDoneAsync`, `MarkFailedAsync`): property updates + `SaveChangesAsync`.

### Removed Code

- `SqliteConnectionFactory.cs`
- `SchemaInitializer.cs`
- `schema/schema.sql`
- All `ToDb`/`FromDb` enum mapping methods
- All manual `DBNull.Value` handling
- `BindTask` helper methods

---

## 4. Package Changes and DI Registration

### ClaudeDo.Data.csproj

- Remove: `Microsoft.Data.Sqlite`
- Remove: embedded resource for `schema.sql`
- Add: `Microsoft.EntityFrameworkCore.Sqlite`
- Add: `Microsoft.EntityFrameworkCore.Design` (`PrivateAssets="all"`)

### ClaudeDo.Worker.Tests.csproj

- Remove: `Microsoft.Data.Sqlite`
- Add: `Microsoft.EntityFrameworkCore.Sqlite`

### App DI (Program.cs)

```csharp
// Replace SqliteConnectionFactory + singleton repos with:
sc.AddDbContextFactory<ClaudeDoDbContext>(opt =>
    opt.UseSqlite($"Data Source={dbPath}"));
sc.AddScoped<ClaudeDoDbContext>(sp =>
    sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
sc.AddScoped<ListRepository>();
sc.AddScoped<TaskRepository>();
sc.AddScoped<SubtaskRepository>();
sc.AddScoped<TagRepository>();
sc.AddScoped<WorktreeRepository>();
sc.AddScoped<TaskRunRepository>();

// Migrate at startup:
using var initScope = services.CreateScope();
initScope.ServiceProvider.GetRequiredService<ClaudeDoDbContext>().Database.Migrate();
```

ViewModels are singletons that currently take repositories as constructor parameters. Since repositories become scoped, ViewModels switch to taking `IDbContextFactory<ClaudeDoDbContext>` and create a fresh context (+ repositories) per operation. Each ViewModel method that touches data does: `using var context = _factory.CreateDbContext();` then constructs or resolves the needed repository with that context. This mirrors the current connection-per-call pattern.

### Worker DI (Program.cs)

```csharp
builder.Services.AddDbContext<ClaudeDoDbContext>(opt =>
    opt.UseSqlite($"Data Source={cfg.DbPath}"));
builder.Services.AddScoped<ListRepository>();
builder.Services.AddScoped<TaskRepository>();
builder.Services.AddScoped<SubtaskRepository>();
builder.Services.AddScoped<TagRepository>();
builder.Services.AddScoped<WorktreeRepository>();
builder.Services.AddScoped<TaskRunRepository>();

// Migrate at startup after build:
using var scope = app.Services.CreateScope();
scope.ServiceProvider.GetRequiredService<ClaudeDoDbContext>().Database.Migrate();
```

Worker has request scopes via SignalR hub invocations, so scoped registration works naturally.

---

## 5. Test Infrastructure

### DbFixture

`DbFixture` is rewritten as an EF Core fixture:

- Creates a temp SQLite file per test class.
- Builds `DbContextOptions<ClaudeDoDbContext>` with `UseSqlite`.
- Calls `context.Database.Migrate()` to apply the schema (also tests that migrations work).
- Exposes a `CreateContext()` method so each test gets a fresh context instance (avoids change-tracker bleed).

Tests construct repositories by passing in a fresh context from the fixture.

No mocking — tests keep hitting real SQLite, same philosophy as today.

---

## 6. Risk and Mitigation

| Risk | Mitigation |
|---|---|
| Big-bang rewrite touches nearly every file in ClaudeDo.Data | Existing tests are the safety net — all must pass after migration |
| Existing databases with schema from schema.sql | Compatibility shim: detect existing tables, mark initial migration as applied |
| Atomic queue claim semantics change | Kept as raw SQL via `FromSqlRaw` |
| Scoped lifetime vs. singleton ViewModels | `IDbContextFactory` provides on-demand contexts |
| EF change tracker overhead vs. raw ADO.NET | Negligible for this workload size; use `AsNoTracking()` for read-only queries |
