Files
ClaudeDo/src/ClaudeDo.Data/ClaudeDoDbContext.cs
mika kuns 2a36998ac7 chore(claude-do): Fix inconsistent timezone on timestamps
Timestamps are serialized inconsistently across tools. add_task returns createdAt with a trailing 'Z' (e.g. "2026-06-01T13:03:56.1636946Z"), but get_task and list_runs return the same value WITHOUT the 'Z'. This is a timezone-ambiguity bug.

Fix: serialize all DateTime values as UTC with the 'Z' suffix consistently (use a single shared JSON serializer setting / DateTimeKind=Utc). Audit every tool

ClaudeDo-Task: 4bbc759e-ff05-45e3-a57f-b290c7e16264
2026-06-01 15:16:25 +02:00

104 lines
4.4 KiB
C#

using ClaudeDo.Data.Models;
using ClaudeDo.Data.Seeding;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace ClaudeDo.Data;
public class ClaudeDoDbContext : DbContext
{
public ClaudeDoDbContext(DbContextOptions<ClaudeDoDbContext> options) : base(options) { }
public DbSet<TaskEntity> Tasks => Set<TaskEntity>();
public DbSet<ListEntity> Lists => Set<ListEntity>();
public DbSet<ListConfigEntity> ListConfigs => Set<ListConfigEntity>();
public DbSet<WorktreeEntity> Worktrees => Set<WorktreeEntity>();
public DbSet<TaskRunEntity> TaskRuns => Set<TaskRunEntity>();
public DbSet<SubtaskEntity> Subtasks => Set<SubtaskEntity>();
public DbSet<AppSettingsEntity> AppSettings => Set<AppSettingsEntity>();
public DbSet<PrimeScheduleEntity> PrimeSchedules => Set<PrimeScheduleEntity>();
private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
new(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
private static readonly ValueConverter<DateTime?, DateTime?> UtcNullableConverter =
new(v => v, v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : null);
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ClaudeDoDbContext).Assembly);
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
foreach (var property in entityType.GetProperties())
{
if (property.ClrType == typeof(DateTime) && property.GetValueConverter() == null)
property.SetValueConverter(UtcConverter);
else if (property.ClrType == typeof(DateTime?) && property.GetValueConverter() == null)
property.SetValueConverter(UtcNullableConverter);
}
}
/// <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)
{
var conn = db.Database.GetDbConnection();
try
{
conn.Open();
// Set WAL FIRST, before migrations — prevents write-lock contention
// when UI and Worker start simultaneously.
using (var walCmd = conn.CreateCommand())
{
walCmd.CommandText = "PRAGMA journal_mode=wal;";
walCmd.ExecuteNonQuery();
}
// Enable FK enforcement — SQLite defaults to OFF per connection.
using (var fkCmd = conn.CreateCommand())
{
fkCmd.CommandText = "PRAGMA foreign_keys=ON;";
fkCmd.ExecuteNonQuery();
}
// If the 'lists' table exists but __EFMigrationsHistory does not,
// this is a pre-EF database. Baseline the InitialCreate migration.
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)
{
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();
}
}
}
finally
{
conn.Close();
}
db.Database.Migrate();
DefaultListsSeeder.SeedAsync(db).GetAwaiter().GetResult();
}
}