diff --git a/schema/schema.sql b/schema/schema.sql deleted file mode 100644 index 1e75c85..0000000 --- a/schema/schema.sql +++ /dev/null @@ -1,100 +0,0 @@ --- ClaudeDo SQLite schema (single source of truth, 3NF) --- Applied by Worker on first startup. WAL mode is set via PRAGMA after open. - -PRAGMA foreign_keys = ON; - -CREATE TABLE IF NOT EXISTS lists ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - created_at TIMESTAMP NOT NULL, - working_dir TEXT NULL, - default_commit_type TEXT NOT NULL DEFAULT 'chore' -); - -CREATE TABLE IF NOT EXISTS tasks ( - id TEXT PRIMARY KEY, - list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE, - title TEXT NOT NULL, - description TEXT NULL, - status TEXT NOT NULL CHECK (status IN ('manual','queued','running','done','failed')), - scheduled_for TIMESTAMP NULL, - result TEXT NULL, - log_path TEXT NULL, - created_at TIMESTAMP NOT NULL, - started_at TIMESTAMP NULL, - finished_at TIMESTAMP NULL, - commit_type TEXT NOT NULL DEFAULT 'chore' -); - -CREATE INDEX IF NOT EXISTS idx_tasks_list_id ON tasks(list_id); -CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); - -CREATE TABLE IF NOT EXISTS tags ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE -); - -CREATE TABLE IF NOT EXISTS list_tags ( - list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, - PRIMARY KEY (list_id, tag_id) -); - -CREATE TABLE IF NOT EXISTS task_tags ( - task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, - PRIMARY KEY (task_id, tag_id) -); - -CREATE TABLE IF NOT EXISTS list_config ( - list_id TEXT PRIMARY KEY REFERENCES lists(id) ON DELETE CASCADE, - model TEXT NULL, - system_prompt TEXT NULL, - agent_path TEXT NULL -); - -CREATE TABLE IF NOT EXISTS worktrees ( - task_id TEXT PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE, - path TEXT NOT NULL, - branch_name TEXT NOT NULL, - base_commit TEXT NOT NULL, - head_commit TEXT NULL, - diff_stat TEXT NULL, - state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active','merged','discarded','kept')), - created_at TIMESTAMP NOT NULL -); - -CREATE TABLE IF NOT EXISTS task_runs ( - id TEXT PRIMARY KEY, - task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, - run_number INTEGER NOT NULL, - session_id TEXT NULL, - is_retry INTEGER NOT NULL DEFAULT 0, - prompt TEXT NOT NULL, - result_markdown TEXT NULL, - structured_output TEXT NULL, - error_markdown TEXT NULL, - exit_code INTEGER NULL, - turn_count INTEGER NULL, - tokens_in INTEGER NULL, - tokens_out INTEGER NULL, - log_path TEXT NULL, - started_at TIMESTAMP NULL, - finished_at TIMESTAMP NULL -); - -CREATE INDEX IF NOT EXISTS idx_task_runs_task_id ON task_runs(task_id); - -CREATE TABLE IF NOT EXISTS subtasks ( - id TEXT PRIMARY KEY, - task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, - title TEXT NOT NULL, - completed INTEGER NOT NULL DEFAULT 0, - order_num INTEGER NOT NULL, - created_at TIMESTAMP NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_subtasks_task_id ON subtasks(task_id); - --- Seed: minimal tag set (ignored if already present) -INSERT OR IGNORE INTO tags (name) VALUES ('agent'); -INSERT OR IGNORE INTO tags (name) VALUES ('manual'); diff --git a/src/ClaudeDo.Data/ClaudeDoDbContextFactory.cs b/src/ClaudeDo.Data/ClaudeDoDbContextFactory.cs new file mode 100644 index 0000000..2c83c7e --- /dev/null +++ b/src/ClaudeDo.Data/ClaudeDoDbContextFactory.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace ClaudeDo.Data; + +public sealed class ClaudeDoDbContextFactory : IDesignTimeDbContextFactory +{ + public ClaudeDoDbContext CreateDbContext(string[] args) + { + var options = new DbContextOptionsBuilder() + .UseSqlite("Data Source=design-time.db") + .Options; + return new ClaudeDoDbContext(options); + } +} diff --git a/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs index 204ae9d..5774c2a 100644 --- a/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs +++ b/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs @@ -1,12 +1,32 @@ using ClaudeDo.Data.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Data.Configuration; public class TaskEntityConfiguration : IEntityTypeConfiguration { + private static string StatusToString(TaskStatus v) + => v == TaskStatus.Manual ? "manual" + : v == TaskStatus.Queued ? "queued" + : v == TaskStatus.Running ? "running" + : v == TaskStatus.Done ? "done" + : v == TaskStatus.Failed ? "failed" + : throw new ArgumentOutOfRangeException(nameof(v)); + + private static TaskStatus StatusFromString(string v) + => v == "manual" ? TaskStatus.Manual + : v == "queued" ? TaskStatus.Queued + : v == "running" ? TaskStatus.Running + : v == "done" ? TaskStatus.Done + : v == "failed" ? TaskStatus.Failed + : throw new ArgumentOutOfRangeException(nameof(v)); + + private static readonly ValueConverter StatusConverter = + new(v => StatusToString(v), v => StatusFromString(v)); + public void Configure(EntityTypeBuilder builder) { builder.ToTable("tasks"); @@ -17,25 +37,7 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration builder.Property(t => t.Title).HasColumnName("title").IsRequired(); builder.Property(t => t.Description).HasColumnName("description"); builder.Property(t => t.Status).HasColumnName("status").IsRequired() - .HasConversion( - v => v switch - { - TaskStatus.Manual => "manual", - TaskStatus.Queued => "queued", - TaskStatus.Running => "running", - TaskStatus.Done => "done", - TaskStatus.Failed => "failed", - _ => throw new ArgumentOutOfRangeException() - }, - v => v switch - { - "manual" => TaskStatus.Manual, - "queued" => TaskStatus.Queued, - "running" => TaskStatus.Running, - "done" => TaskStatus.Done, - "failed" => TaskStatus.Failed, - _ => throw new ArgumentOutOfRangeException() - }); + .HasConversion(StatusConverter); builder.Property(t => t.ScheduledFor).HasColumnName("scheduled_for"); builder.Property(t => t.Result).HasColumnName("result"); builder.Property(t => t.LogPath).HasColumnName("log_path"); diff --git a/src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs index e463f0f..2917f47 100644 --- a/src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs +++ b/src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs @@ -1,11 +1,29 @@ using ClaudeDo.Data.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace ClaudeDo.Data.Configuration; public class WorktreeEntityConfiguration : IEntityTypeConfiguration { + private static string StateToString(WorktreeState v) + => v == WorktreeState.Active ? "active" + : v == WorktreeState.Merged ? "merged" + : v == WorktreeState.Discarded ? "discarded" + : v == WorktreeState.Kept ? "kept" + : throw new ArgumentOutOfRangeException(nameof(v)); + + private static WorktreeState StateFromString(string v) + => v == "active" ? WorktreeState.Active + : v == "merged" ? WorktreeState.Merged + : v == "discarded" ? WorktreeState.Discarded + : v == "kept" ? WorktreeState.Kept + : throw new ArgumentOutOfRangeException(nameof(v)); + + private static readonly ValueConverter StateConverter = + new(v => StateToString(v), v => StateFromString(v)); + public void Configure(EntityTypeBuilder builder) { builder.ToTable("worktrees"); @@ -19,23 +37,7 @@ public class WorktreeEntityConfiguration : IEntityTypeConfiguration w.DiffStat).HasColumnName("diff_stat"); builder.Property(w => w.State).HasColumnName("state").IsRequired() .HasDefaultValue(WorktreeState.Active) - .HasConversion( - v => v switch - { - WorktreeState.Active => "active", - WorktreeState.Merged => "merged", - WorktreeState.Discarded => "discarded", - WorktreeState.Kept => "kept", - _ => throw new ArgumentOutOfRangeException() - }, - v => v switch - { - "active" => WorktreeState.Active, - "merged" => WorktreeState.Merged, - "discarded" => WorktreeState.Discarded, - "kept" => WorktreeState.Kept, - _ => throw new ArgumentOutOfRangeException() - }); + .HasConversion(StateConverter); builder.Property(w => w.CreatedAt).HasColumnName("created_at").IsRequired(); } } diff --git a/src/ClaudeDo.Data/Migrations/20260416064948_InitialCreate.cs b/src/ClaudeDo.Data/Migrations/20260416064948_InitialCreate.cs new file mode 100644 index 0000000..301b931 --- /dev/null +++ b/src/ClaudeDo.Data/Migrations/20260416064948_InitialCreate.cs @@ -0,0 +1,298 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace ClaudeDo.Data.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "lists", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "TEXT", nullable: false), + created_at = table.Column(type: "TEXT", nullable: false), + working_dir = table.Column(type: "TEXT", nullable: true), + default_commit_type = table.Column(type: "TEXT", nullable: false, defaultValue: "chore") + }, + constraints: table => + { + table.PrimaryKey("PK_lists", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "tags", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + name = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tags", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "list_config", + columns: table => new + { + list_id = table.Column(type: "TEXT", nullable: false), + model = table.Column(type: "TEXT", nullable: true), + system_prompt = table.Column(type: "TEXT", nullable: true), + agent_path = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_list_config", x => x.list_id); + table.ForeignKey( + name: "FK_list_config_lists_list_id", + column: x => x.list_id, + principalTable: "lists", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tasks", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + list_id = table.Column(type: "TEXT", nullable: false), + title = table.Column(type: "TEXT", nullable: false), + description = table.Column(type: "TEXT", nullable: true), + status = table.Column(type: "TEXT", nullable: false), + scheduled_for = table.Column(type: "TEXT", nullable: true), + result = table.Column(type: "TEXT", nullable: true), + log_path = table.Column(type: "TEXT", nullable: true), + created_at = table.Column(type: "TEXT", nullable: false), + started_at = table.Column(type: "TEXT", nullable: true), + finished_at = table.Column(type: "TEXT", nullable: true), + commit_type = table.Column(type: "TEXT", nullable: false, defaultValue: "chore"), + model = table.Column(type: "TEXT", nullable: true), + system_prompt = table.Column(type: "TEXT", nullable: true), + agent_path = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_tasks", x => x.id); + table.ForeignKey( + name: "FK_tasks_lists_list_id", + column: x => x.list_id, + principalTable: "lists", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "list_tags", + columns: table => new + { + list_id = table.Column(type: "TEXT", nullable: false), + tag_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_list_tags", x => new { x.list_id, x.tag_id }); + table.ForeignKey( + name: "FK_list_tags_lists_list_id", + column: x => x.list_id, + principalTable: "lists", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_list_tags_tags_tag_id", + column: x => x.tag_id, + principalTable: "tags", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "subtasks", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + task_id = table.Column(type: "TEXT", nullable: false), + title = table.Column(type: "TEXT", nullable: false), + completed = table.Column(type: "INTEGER", nullable: false, defaultValue: false), + order_num = table.Column(type: "INTEGER", nullable: false), + created_at = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_subtasks", x => x.id); + table.ForeignKey( + name: "FK_subtasks_tasks_task_id", + column: x => x.task_id, + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "task_runs", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + task_id = table.Column(type: "TEXT", nullable: false), + run_number = table.Column(type: "INTEGER", nullable: false), + session_id = table.Column(type: "TEXT", nullable: true), + is_retry = table.Column(type: "INTEGER", nullable: false, defaultValue: false), + prompt = table.Column(type: "TEXT", nullable: false), + result_markdown = table.Column(type: "TEXT", nullable: true), + structured_output = table.Column(type: "TEXT", nullable: true), + error_markdown = table.Column(type: "TEXT", nullable: true), + exit_code = table.Column(type: "INTEGER", nullable: true), + turn_count = table.Column(type: "INTEGER", nullable: true), + tokens_in = table.Column(type: "INTEGER", nullable: true), + tokens_out = table.Column(type: "INTEGER", nullable: true), + log_path = table.Column(type: "TEXT", nullable: true), + started_at = table.Column(type: "TEXT", nullable: true), + finished_at = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_task_runs", x => x.id); + table.ForeignKey( + name: "FK_task_runs_tasks_task_id", + column: x => x.task_id, + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "task_tags", + columns: table => new + { + task_id = table.Column(type: "TEXT", nullable: false), + tag_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_task_tags", x => new { x.task_id, x.tag_id }); + table.ForeignKey( + name: "FK_task_tags_tags_tag_id", + column: x => x.tag_id, + principalTable: "tags", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_task_tags_tasks_task_id", + column: x => x.task_id, + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "worktrees", + columns: table => new + { + task_id = table.Column(type: "TEXT", nullable: false), + path = table.Column(type: "TEXT", nullable: false), + branch_name = table.Column(type: "TEXT", nullable: false), + base_commit = table.Column(type: "TEXT", nullable: false), + head_commit = table.Column(type: "TEXT", nullable: true), + diff_stat = table.Column(type: "TEXT", nullable: true), + state = table.Column(type: "TEXT", nullable: false, defaultValue: "active"), + created_at = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_worktrees", x => x.task_id); + table.ForeignKey( + name: "FK_worktrees_tasks_task_id", + column: x => x.task_id, + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "tags", + columns: new[] { "id", "name" }, + values: new object[,] + { + { 1L, "agent" }, + { 2L, "manual" } + }); + + migrationBuilder.CreateIndex( + name: "IX_list_tags_tag_id", + table: "list_tags", + column: "tag_id"); + + migrationBuilder.CreateIndex( + name: "idx_subtasks_task_id", + table: "subtasks", + column: "task_id"); + + migrationBuilder.CreateIndex( + name: "IX_tags_name", + table: "tags", + column: "name", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx_task_runs_task_id", + table: "task_runs", + column: "task_id"); + + migrationBuilder.CreateIndex( + name: "IX_task_tags_tag_id", + table: "task_tags", + column: "tag_id"); + + migrationBuilder.CreateIndex( + name: "idx_tasks_list_id", + table: "tasks", + column: "list_id"); + + migrationBuilder.CreateIndex( + name: "idx_tasks_status", + table: "tasks", + column: "status"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "list_config"); + + migrationBuilder.DropTable( + name: "list_tags"); + + migrationBuilder.DropTable( + name: "subtasks"); + + migrationBuilder.DropTable( + name: "task_runs"); + + migrationBuilder.DropTable( + name: "task_tags"); + + migrationBuilder.DropTable( + name: "worktrees"); + + migrationBuilder.DropTable( + name: "tags"); + + migrationBuilder.DropTable( + name: "tasks"); + + migrationBuilder.DropTable( + name: "lists"); + } + } +} diff --git a/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs b/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs new file mode 100644 index 0000000..f5b3351 --- /dev/null +++ b/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs @@ -0,0 +1,479 @@ +// +using System; +using ClaudeDo.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ClaudeDo.Data.Migrations +{ + [DbContext(typeof(ClaudeDoDbContext))] + partial class ClaudeDoDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.11"); + + modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b => + { + b.Property("ListId") + .HasColumnType("TEXT") + .HasColumnName("list_id"); + + b.Property("AgentPath") + .HasColumnType("TEXT") + .HasColumnName("agent_path"); + + b.Property("Model") + .HasColumnType("TEXT") + .HasColumnName("model"); + + b.Property("SystemPrompt") + .HasColumnType("TEXT") + .HasColumnName("system_prompt"); + + b.HasKey("ListId"); + + b.ToTable("list_config", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DefaultCommitType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("chore") + .HasColumnName("default_commit_type"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("WorkingDir") + .HasColumnType("TEXT") + .HasColumnName("working_dir"); + + b.HasKey("Id"); + + b.ToTable("lists", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Completed") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("completed"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("OrderNum") + .HasColumnType("INTEGER") + .HasColumnName("order_num"); + + b.Property("TaskId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id"); + + b.HasIndex("TaskId") + .HasDatabaseName("idx_subtasks_task_id"); + + b.ToTable("subtasks", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("tags", (string)null); + + b.HasData( + new + { + Id = 1L, + Name = "agent" + }, + new + { + Id = 2L, + Name = "manual" + }); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AgentPath") + .HasColumnType("TEXT") + .HasColumnName("agent_path"); + + b.Property("CommitType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("chore") + .HasColumnName("commit_type"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("FinishedAt") + .HasColumnType("TEXT") + .HasColumnName("finished_at"); + + b.Property("ListId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("list_id"); + + b.Property("LogPath") + .HasColumnType("TEXT") + .HasColumnName("log_path"); + + b.Property("Model") + .HasColumnType("TEXT") + .HasColumnName("model"); + + b.Property("Result") + .HasColumnType("TEXT") + .HasColumnName("result"); + + b.Property("ScheduledFor") + .HasColumnType("TEXT") + .HasColumnName("scheduled_for"); + + b.Property("StartedAt") + .HasColumnType("TEXT") + .HasColumnName("started_at"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("SystemPrompt") + .HasColumnType("TEXT") + .HasColumnName("system_prompt"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id"); + + b.HasIndex("ListId") + .HasDatabaseName("idx_tasks_list_id"); + + b.HasIndex("Status") + .HasDatabaseName("idx_tasks_status"); + + b.ToTable("tasks", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ErrorMarkdown") + .HasColumnType("TEXT") + .HasColumnName("error_markdown"); + + b.Property("ExitCode") + .HasColumnType("INTEGER") + .HasColumnName("exit_code"); + + b.Property("FinishedAt") + .HasColumnType("TEXT") + .HasColumnName("finished_at"); + + b.Property("IsRetry") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("is_retry"); + + b.Property("LogPath") + .HasColumnType("TEXT") + .HasColumnName("log_path"); + + b.Property("Prompt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("prompt"); + + b.Property("ResultMarkdown") + .HasColumnType("TEXT") + .HasColumnName("result_markdown"); + + b.Property("RunNumber") + .HasColumnType("INTEGER") + .HasColumnName("run_number"); + + b.Property("SessionId") + .HasColumnType("TEXT") + .HasColumnName("session_id"); + + b.Property("StartedAt") + .HasColumnType("TEXT") + .HasColumnName("started_at"); + + b.Property("StructuredOutputJson") + .HasColumnType("TEXT") + .HasColumnName("structured_output"); + + b.Property("TaskId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.Property("TokensIn") + .HasColumnType("INTEGER") + .HasColumnName("tokens_in"); + + b.Property("TokensOut") + .HasColumnType("INTEGER") + .HasColumnName("tokens_out"); + + b.Property("TurnCount") + .HasColumnType("INTEGER") + .HasColumnName("turn_count"); + + b.HasKey("Id"); + + b.HasIndex("TaskId") + .HasDatabaseName("idx_task_runs_task_id"); + + b.ToTable("task_runs", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b => + { + b.Property("TaskId") + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.Property("BaseCommit") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("base_commit"); + + b.Property("BranchName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("branch_name"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DiffStat") + .HasColumnType("TEXT") + .HasColumnName("diff_stat"); + + b.Property("HeadCommit") + .HasColumnType("TEXT") + .HasColumnName("head_commit"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("State") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("active") + .HasColumnName("state"); + + b.HasKey("TaskId"); + + b.ToTable("worktrees", (string)null); + }); + + modelBuilder.Entity("list_tags", b => + { + b.Property("list_id") + .HasColumnType("TEXT"); + + b.Property("tag_id") + .HasColumnType("INTEGER"); + + b.HasKey("list_id", "tag_id"); + + b.HasIndex("tag_id"); + + b.ToTable("list_tags", (string)null); + }); + + modelBuilder.Entity("task_tags", b => + { + b.Property("task_id") + .HasColumnType("TEXT"); + + b.Property("tag_id") + .HasColumnType("INTEGER"); + + b.HasKey("task_id", "tag_id"); + + b.HasIndex("tag_id"); + + b.ToTable("task_tags", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b => + { + b.HasOne("ClaudeDo.Data.Models.ListEntity", "List") + .WithOne("Config") + .HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("List"); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b => + { + b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task") + .WithMany("Subtasks") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b => + { + b.HasOne("ClaudeDo.Data.Models.ListEntity", "List") + .WithMany("Tasks") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("List"); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b => + { + b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task") + .WithMany("Runs") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b => + { + b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task") + .WithOne("Worktree") + .HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("list_tags", b => + { + b.HasOne("ClaudeDo.Data.Models.ListEntity", null) + .WithMany() + .HasForeignKey("list_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ClaudeDo.Data.Models.TagEntity", null) + .WithMany() + .HasForeignKey("tag_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("task_tags", b => + { + b.HasOne("ClaudeDo.Data.Models.TagEntity", null) + .WithMany() + .HasForeignKey("tag_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ClaudeDo.Data.Models.TaskEntity", null) + .WithMany() + .HasForeignKey("task_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b => + { + b.Navigation("Config"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b => + { + b.Navigation("Runs"); + + b.Navigation("Subtasks"); + + b.Navigation("Worktree"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ClaudeDo.Data/SchemaInitializer.cs b/src/ClaudeDo.Data/SchemaInitializer.cs deleted file mode 100644 index 65e4806..0000000 --- a/src/ClaudeDo.Data/SchemaInitializer.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Reflection; -using Microsoft.Data.Sqlite; - -namespace ClaudeDo.Data; - -/// -/// Applies the embedded schema.sql script. Safe to call on every start — the script uses -/// IF NOT EXISTS / INSERT OR IGNORE. -/// -public static class SchemaInitializer -{ - private const string ResourceName = "ClaudeDo.Data.schema.sql"; - - public static void Apply(SqliteConnectionFactory factory) - { - using var conn = factory.Open(); - ApplyTo(conn); - } - - public static void ApplyTo(SqliteConnection conn) - { - var sql = LoadScript(); - using var tx = conn.BeginTransaction(); - using var cmd = conn.CreateCommand(); - cmd.Transaction = tx; - cmd.CommandText = sql; - cmd.ExecuteNonQuery(); - tx.Commit(); - - ApplyMigrations(conn); - } - - private static void ApplyMigrations(SqliteConnection conn) - { - string[] alterStatements = - [ - "ALTER TABLE tasks ADD COLUMN model TEXT NULL", - "ALTER TABLE tasks ADD COLUMN system_prompt TEXT NULL", - "ALTER TABLE tasks ADD COLUMN agent_path TEXT NULL", - ]; - - foreach (var sql in alterStatements) - { - try - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = sql; - cmd.ExecuteNonQuery(); - } - catch (SqliteException ex) when (ex.SqliteErrorCode == 1) - { - // Column already exists — safe to ignore. - } - } - } - - private static string LoadScript() - { - var asm = typeof(SchemaInitializer).Assembly; - using var stream = asm.GetManifestResourceStream(ResourceName) - ?? throw new InvalidOperationException( - $"Embedded resource '{ResourceName}' not found in {asm.GetName().Name}. " + - $"Available: {string.Join(", ", asm.GetManifestResourceNames())}"); - using var reader = new StreamReader(stream); - return reader.ReadToEnd(); - } -} diff --git a/src/ClaudeDo.Data/SqliteConnectionFactory.cs b/src/ClaudeDo.Data/SqliteConnectionFactory.cs deleted file mode 100644 index 43da6fa..0000000 --- a/src/ClaudeDo.Data/SqliteConnectionFactory.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Microsoft.Data.Sqlite; - -namespace ClaudeDo.Data; - -/// -/// Opens instances pointed at . -/// First call ensures the parent directory exists, enables WAL and foreign keys. -/// -public sealed class SqliteConnectionFactory -{ - public string DbPath { get; } - private readonly string _connectionString; - private int _walApplied; - - public SqliteConnectionFactory(string dbPath) - { - DbPath = Paths.Expand(dbPath); - Directory.CreateDirectory(Path.GetDirectoryName(DbPath)!); - - _connectionString = new SqliteConnectionStringBuilder - { - DataSource = DbPath, - Mode = SqliteOpenMode.ReadWriteCreate, - Cache = SqliteCacheMode.Shared, - }.ToString(); - } - - public SqliteConnection Open() - { - var conn = new SqliteConnection(_connectionString); - conn.Open(); - - // WAL is a persistent DB-level setting; applying it once per process is enough, - // but idempotent so we do it defensively on the first connection we hand out. - if (Interlocked.Exchange(ref _walApplied, 1) == 0) - { - using var pragma = conn.CreateCommand(); - pragma.CommandText = "PRAGMA journal_mode=WAL;"; - pragma.ExecuteNonQuery(); - } - - using var fk = conn.CreateCommand(); - fk.CommandText = "PRAGMA foreign_keys=ON;"; - fk.ExecuteNonQuery(); - - return conn; - } -}