Files
kandev/apps/backend/internal/task/repository/sqlite/base_schema.go
T
2026-06-27 15:00:38 +01:00

689 lines
24 KiB
Go

// Package sqlite provides SQLite-based repository implementations.
package sqlite
import (
"fmt"
"github.com/google/uuid"
"github.com/kandev/kandev/internal/db/dialect"
)
// initSchema creates the database tables if they don't exist and applies
// pending migrations in order. Steps are sequenced through an explicit
// table so the call list stays readable as new schema/migration steps are
// added without growing the function's cyclomatic complexity.
func (r *Repository) initSchema() error {
steps := []func() error{
r.initCoreSchema,
r.initPlansSchema,
r.initDocumentsSchema,
r.initSessionSchema,
r.initGitSchema,
r.initReviewSchema,
r.migrateExecutorProfiles,
r.migrateTaskSessions,
r.ensureDefaultWorkspace,
r.ensureDefaultExecutorsAndEnvironments,
r.runMigrations,
r.backfillTaskEnvironments,
r.backfillTaskEnvironmentRepos,
r.healTaskEnvironmentWorkspacePaths,
r.healDuplicateTaskEnvironments,
r.ensureTaskEnvironmentTaskUniqueIndex,
r.healSessionTaskEnvironmentIDs,
r.ensureWorkspaceIndexes,
r.ensureMessageMetadataIndexes,
}
for _, step := range steps {
if err := step(); err != nil {
return err
}
}
return nil
}
// ensureWorkspaceIndexes creates workspace-related indexes
func (r *Repository) ensureWorkspaceIndexes() error {
if _, err := r.db.Exec(`CREATE INDEX IF NOT EXISTS idx_tasks_workspace_id ON tasks(workspace_id)`); err != nil {
return err
}
if _, err := r.db.Exec(`CREATE INDEX IF NOT EXISTS idx_workflows_workspace_id ON workflows(workspace_id)`); err != nil {
return err
}
if _, err := r.db.Exec(`CREATE INDEX IF NOT EXISTS idx_tasks_workspace_archived ON tasks(workspace_id, archived_at)`); err != nil {
return err
}
return nil
}
// ensureMessageMetadataIndexes creates indexes on JSON metadata fields for fast lookups.
func (r *Repository) ensureMessageMetadataIndexes() error {
driver := r.db.DriverName()
toolCallIndex := fmt.Sprintf(
`CREATE INDEX IF NOT EXISTS idx_messages_metadata_tool_call_id ON task_session_messages(task_session_id, (%s))`,
dialect.JSONExtract(driver, "metadata", "tool_call_id"),
)
if _, err := r.db.Exec(toolCallIndex); err != nil {
return err
}
pendingIndex := fmt.Sprintf(
`CREATE INDEX IF NOT EXISTS idx_messages_metadata_pending_id ON task_session_messages(task_session_id, (%s))`,
dialect.JSONExtract(driver, "metadata", "pending_id"),
)
if _, err := r.db.Exec(pendingIndex); err != nil {
return err
}
return nil
}
// ensureRunnerProjectionTables creates stub workflow_steps and
// workflow_step_participants tables if they're not yet present. The
// task repo's task SELECT projection includes a correlated subquery
// against both tables to resolve the per-task runner (ADR 0005 Wave F);
// when only the task repo is initialised (e.g. unit tests), the
// workflow repo hasn't created the canonical tables and the queries
// would error with "no such table". Stubs created here are minimal —
// the workflow repo's init still runs and adds the rest of its columns
// via idempotent ALTER and CREATE statements.
func (r *Repository) ensureRunnerProjectionTables() {
// workflow_steps: matches the full schema declared in the workflow
// repo so workflow.NewWithDB's later ALTER ADD COLUMNs become no-ops
// (column-already-exists errors are swallowed). Mirrors
// internal/workflow/repository/sqlite.go (the canonical owner).
_, _ = r.db.Exec(`
CREATE TABLE IF NOT EXISTS workflow_steps (
id TEXT PRIMARY KEY,
workflow_id TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
position INTEGER NOT NULL DEFAULT 0,
color TEXT,
prompt TEXT,
events TEXT,
allow_manual_move INTEGER DEFAULT 1,
is_start_step INTEGER DEFAULT 0,
show_in_command_panel INTEGER DEFAULT 1,
auto_archive_after_hours INTEGER DEFAULT 0,
agent_profile_id TEXT NOT NULL DEFAULT '',
stage_type TEXT NOT NULL DEFAULT 'custom',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)`)
_, _ = r.db.Exec(`
CREATE TABLE IF NOT EXISTS workflow_step_participants (
id TEXT PRIMARY KEY,
step_id TEXT NOT NULL DEFAULT '',
task_id TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT '',
agent_profile_id TEXT NOT NULL DEFAULT '',
decision_required INTEGER NOT NULL DEFAULT 0,
position INTEGER NOT NULL DEFAULT 0
)`)
}
func (r *Repository) initCoreSchema() error {
if err := r.initInfraSchema(); err != nil {
return err
}
if err := r.initTaskSchema(); err != nil {
return err
}
return r.initCoreIndexes()
}
// infraSchemaDDL is the concatenated CREATE TABLE block for infrastructure
// tables (workspaces, executors, environments, workflows). Kept as a single
// DDL string so the table layout reads top-to-bottom; lives in a const so the
// owning function stays within the funlen limit.
const infraSchemaDDL = `
CREATE TABLE IF NOT EXISTS workspaces (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT DEFAULT '',
owner_id TEXT DEFAULT '',
default_executor_id TEXT DEFAULT '',
default_environment_id TEXT DEFAULT '',
default_agent_profile_id TEXT DEFAULT '',
default_config_agent_profile_id TEXT DEFAULT '',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS executors (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
is_system INTEGER NOT NULL DEFAULT 0,
resumable INTEGER NOT NULL DEFAULT 1,
config TEXT DEFAULT '{}',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
deleted_at TIMESTAMP
);
CREATE TABLE IF NOT EXISTS executors_running (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL UNIQUE,
task_id TEXT NOT NULL,
executor_id TEXT NOT NULL,
runtime TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'starting',
resumable INTEGER NOT NULL DEFAULT 0,
resume_token TEXT DEFAULT '',
agent_execution_id TEXT DEFAULT '',
container_id TEXT DEFAULT '',
agentctl_url TEXT DEFAULT '',
agentctl_port INTEGER DEFAULT 0,
pid INTEGER DEFAULT 0,
worktree_id TEXT DEFAULT '',
worktree_path TEXT DEFAULT '',
worktree_branch TEXT DEFAULT '',
last_seen_at TIMESTAMP,
error_message TEXT DEFAULT '',
metadata TEXT DEFAULT '{}',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS executor_profiles (
id TEXT PRIMARY KEY,
executor_id TEXT NOT NULL,
name TEXT NOT NULL,
mcp_policy TEXT DEFAULT '',
config TEXT DEFAULT '{}',
prepare_script TEXT DEFAULT '',
cleanup_script TEXT DEFAULT '',
env_vars TEXT DEFAULT '[]',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (executor_id) REFERENCES executors(id)
);
CREATE TABLE IF NOT EXISTS environments (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
kind TEXT NOT NULL,
is_system INTEGER NOT NULL DEFAULT 0,
worktree_root TEXT DEFAULT '',
image_tag TEXT DEFAULT '',
dockerfile TEXT DEFAULT '',
build_config TEXT DEFAULT '{}',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
deleted_at TIMESTAMP
);
CREATE TABLE IF NOT EXISTS workflows (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL DEFAULT '',
workflow_template_id TEXT DEFAULT '',
name TEXT NOT NULL,
description TEXT DEFAULT '',
hidden INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
`
func (r *Repository) initInfraSchema() error {
_, err := r.db.Exec(infraSchemaDDL)
return err
}
func (r *Repository) initTaskSchema() error {
_, err := r.db.Exec(`
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL DEFAULT '',
workflow_id TEXT NOT NULL DEFAULT '',
workflow_step_id TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL,
description TEXT DEFAULT '',
state TEXT DEFAULT 'TODO',
priority INTEGER DEFAULT 0,
position INTEGER DEFAULT 0,
metadata TEXT DEFAULT '{}',
archived_at TIMESTAMP,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS repositories (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
name TEXT NOT NULL,
source_type TEXT NOT NULL DEFAULT 'local',
local_path TEXT DEFAULT '',
provider TEXT DEFAULT '',
provider_repo_id TEXT DEFAULT '',
provider_owner TEXT DEFAULT '',
provider_name TEXT DEFAULT '',
default_branch TEXT DEFAULT '',
worktree_branch_prefix TEXT DEFAULT 'feature/',
pull_before_worktree INTEGER NOT NULL DEFAULT 1,
setup_script TEXT DEFAULT '',
cleanup_script TEXT DEFAULT '',
dev_script TEXT DEFAULT '',
copy_files TEXT DEFAULT '',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
deleted_at TIMESTAMP,
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS task_repositories (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
repository_id TEXT NOT NULL,
base_branch TEXT DEFAULT '',
checkout_branch TEXT DEFAULT '',
position INTEGER DEFAULT 0,
metadata TEXT DEFAULT '{}',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE,
UNIQUE(task_id, repository_id, base_branch, checkout_branch)
);
CREATE TABLE IF NOT EXISTS repository_scripts (
id TEXT PRIMARY KEY,
repository_id TEXT NOT NULL,
name TEXT NOT NULL,
command TEXT NOT NULL,
position INTEGER DEFAULT 0,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE
);
`)
return err
}
func (r *Repository) initCoreIndexes() error {
_, err := r.db.Exec(`
CREATE INDEX IF NOT EXISTS idx_tasks_workflow_id ON tasks(workflow_id);
CREATE INDEX IF NOT EXISTS idx_tasks_workflow_step_id ON tasks(workflow_step_id);
CREATE INDEX IF NOT EXISTS idx_tasks_archived_at ON tasks(archived_at);
CREATE INDEX IF NOT EXISTS idx_task_repositories_task_id ON task_repositories(task_id);
CREATE INDEX IF NOT EXISTS idx_task_repositories_repository_id ON task_repositories(repository_id);
CREATE INDEX IF NOT EXISTS idx_repositories_workspace_id ON repositories(workspace_id);
CREATE INDEX IF NOT EXISTS idx_repository_scripts_repo_id ON repository_scripts(repository_id);
`)
return err
}
func (r *Repository) initPlansSchema() error {
if _, err := r.db.Exec(`
CREATE TABLE IF NOT EXISTS task_plans (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL UNIQUE,
title TEXT NOT NULL DEFAULT 'Plan',
content TEXT NOT NULL DEFAULT '',
created_by TEXT NOT NULL DEFAULT 'agent',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_task_plans_task_id ON task_plans(task_id);
`); err != nil {
return err
}
if _, err := r.db.Exec(`
CREATE TABLE IF NOT EXISTS task_plan_revisions (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
revision_number INTEGER NOT NULL,
title TEXT NOT NULL DEFAULT 'Plan',
content TEXT NOT NULL DEFAULT '',
author_kind TEXT NOT NULL DEFAULT 'agent',
author_name TEXT NOT NULL DEFAULT '',
revert_of_revision_id TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
UNIQUE (task_id, revision_number)
);
CREATE INDEX IF NOT EXISTS idx_task_plan_revisions_task_created
ON task_plan_revisions(task_id, created_at DESC);
-- Hot-path index: GetLatestTaskPlanRevision (called on every plan write
-- as part of the coalesce check), ListTaskPlanRevisions, and the
-- MAX(revision_number) lookup in WritePlanRevision all order/scan by
-- (task_id, revision_number DESC). With this index the latest-row lookup
-- is O(1) instead of an O(N) scan + sort per task.
CREATE INDEX IF NOT EXISTS idx_task_plan_revisions_task_number
ON task_plan_revisions(task_id, revision_number DESC);
`); err != nil {
return err
}
return r.backfillInitialPlanRevisions()
}
// backfillInitialPlanRevisions ensures every existing task_plans row has at least
// one corresponding revision. Runs once at startup and is idempotent.
func (r *Repository) backfillInitialPlanRevisions() error {
rows, err := r.db.Query(`
SELECT p.id, p.task_id, p.title, p.content, p.created_by, p.created_at, p.updated_at
FROM task_plans p
WHERE NOT EXISTS (
SELECT 1 FROM task_plan_revisions r WHERE r.task_id = p.task_id
)`)
if err != nil {
return fmt.Errorf("query plans missing revisions: %w", err)
}
defer func() { _ = rows.Close() }()
type row struct {
id, taskID, title, content, createdBy string
createdAt, updatedAt interface{}
}
var pending []row
for rows.Next() {
var x row
if err := rows.Scan(&x.id, &x.taskID, &x.title, &x.content, &x.createdBy, &x.createdAt, &x.updatedAt); err != nil {
return fmt.Errorf("scan plan for backfill: %w", err)
}
pending = append(pending, x)
}
if err := rows.Err(); err != nil {
return fmt.Errorf("iterate plans for backfill: %w", err)
}
for _, x := range pending {
authorKind := x.createdBy
// Match CreateTaskPlan (plan.go) and the task_plan_revisions column DEFAULT 'agent'.
if authorKind != "user" && authorKind != authorKindAgent {
authorKind = authorKindAgent
}
_, err := r.db.Exec(r.db.Rebind(`
INSERT INTO task_plan_revisions
(id, task_id, revision_number, title, content, author_kind, author_name, revert_of_revision_id, created_at, updated_at)
VALUES (?, ?, 1, ?, ?, ?, 'legacy', NULL, ?, ?)
`), uuid.New().String(), x.taskID, x.title, x.content, authorKind, x.createdAt, x.updatedAt)
if err != nil {
return fmt.Errorf("backfill revision for task %s: %w", x.taskID, err)
}
}
return nil
}
// initDocumentsSchema creates the task_documents and task_document_revisions tables.
// These tables generalize task_plans: documents have a key (e.g., "plan", "spec") and type.
func (r *Repository) initDocumentsSchema() error {
if _, err := r.db.Exec(`
CREATE TABLE IF NOT EXISTS task_documents (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
key TEXT NOT NULL DEFAULT 'plan',
type TEXT NOT NULL DEFAULT 'plan',
title TEXT NOT NULL DEFAULT 'Plan',
content TEXT NOT NULL DEFAULT '',
author_kind TEXT NOT NULL DEFAULT 'agent',
author_name TEXT NOT NULL DEFAULT '',
filename TEXT NOT NULL DEFAULT '',
mime_type TEXT NOT NULL DEFAULT '',
size_bytes INTEGER NOT NULL DEFAULT 0,
disk_path TEXT NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
UNIQUE(task_id, key)
);
CREATE INDEX IF NOT EXISTS idx_task_documents_task_id ON task_documents(task_id);
CREATE TABLE IF NOT EXISTS task_document_revisions (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
document_key TEXT NOT NULL DEFAULT 'plan',
revision_number INTEGER NOT NULL,
title TEXT NOT NULL DEFAULT 'Plan',
content TEXT NOT NULL DEFAULT '',
author_kind TEXT NOT NULL DEFAULT 'agent',
author_name TEXT NOT NULL DEFAULT '',
revert_of_revision_id TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
UNIQUE (task_id, document_key, revision_number)
);
CREATE INDEX IF NOT EXISTS idx_task_document_revisions_task_key
ON task_document_revisions(task_id, document_key, revision_number DESC);
`); err != nil {
return fmt.Errorf("init documents schema: %w", err)
}
return nil
}
func (r *Repository) initSessionSchema() error {
if err := r.initSessionWorktreeSchema(); err != nil {
return err
}
return r.initMessageTurnSchema()
}
func (r *Repository) initMessageTurnSchema() error {
_, err := r.db.Exec(`
CREATE TABLE IF NOT EXISTS task_session_turns (
id TEXT PRIMARY KEY,
task_session_id TEXT NOT NULL,
task_id TEXT NOT NULL,
started_at TIMESTAMP NOT NULL,
completed_at TIMESTAMP,
metadata TEXT DEFAULT '{}',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (task_session_id) REFERENCES task_sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_turns_session_id ON task_session_turns(task_session_id);
CREATE INDEX IF NOT EXISTS idx_turns_session_started ON task_session_turns(task_session_id, started_at);
CREATE INDEX IF NOT EXISTS idx_turns_task_id ON task_session_turns(task_id);
CREATE TABLE IF NOT EXISTS task_session_messages (
id TEXT PRIMARY KEY,
task_session_id TEXT NOT NULL,
task_id TEXT DEFAULT '',
turn_id TEXT NOT NULL,
author_type TEXT NOT NULL DEFAULT 'user',
author_id TEXT DEFAULT '',
content TEXT NOT NULL,
requests_input INTEGER DEFAULT 0,
type TEXT NOT NULL DEFAULT 'message',
metadata TEXT DEFAULT '{}',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_session_id) REFERENCES task_sessions(id) ON DELETE CASCADE,
FOREIGN KEY (turn_id) REFERENCES task_session_turns(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON task_session_messages(task_session_id);
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON task_session_messages(created_at);
CREATE INDEX IF NOT EXISTS idx_messages_session_created ON task_session_messages(task_session_id, created_at);
CREATE INDEX IF NOT EXISTS idx_messages_turn_id ON task_session_messages(turn_id);
-- idx_messages_session_updated is created in runMigrations() after the
-- updated_at ADD COLUMN + backfill. Creating it here would fail on existing
-- DBs where CREATE TABLE IF NOT EXISTS is a no-op and the column does not
-- yet exist (schema init runs before migrations).
`)
return err
}
// sessionWorktreeSchemaDDL groups task_sessions, task_environments,
// task_environment_repos, and task_session_worktrees DDL so the owning
// function stays within the funlen limit.
const sessionWorktreeSchemaDDL = `
CREATE TABLE IF NOT EXISTS task_sessions (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
agent_execution_id TEXT NOT NULL DEFAULT '',
container_id TEXT NOT NULL DEFAULT '',
agent_profile_id TEXT,
executor_id TEXT DEFAULT '',
executor_profile_id TEXT DEFAULT '',
environment_id TEXT DEFAULT '',
repository_id TEXT DEFAULT '',
base_branch TEXT DEFAULT '',
agent_profile_snapshot TEXT DEFAULT '{}',
executor_snapshot TEXT DEFAULT '{}',
environment_snapshot TEXT DEFAULT '{}',
repository_snapshot TEXT DEFAULT '{}',
state TEXT NOT NULL DEFAULT 'CREATED',
error_message TEXT DEFAULT '',
metadata TEXT DEFAULT '{}',
started_at TIMESTAMP NOT NULL,
completed_at TIMESTAMP,
updated_at TIMESTAMP NOT NULL,
is_primary INTEGER DEFAULT 0,
is_passthrough INTEGER DEFAULT 0,
review_status TEXT DEFAULT '',
base_commit_sha TEXT DEFAULT '',
task_environment_id TEXT DEFAULT '',
cost_subcents INTEGER NOT NULL DEFAULT 0,
tokens_in INTEGER NOT NULL DEFAULT 0,
tokens_out INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_task_sessions_task_id ON task_sessions(task_id);
CREATE INDEX IF NOT EXISTS idx_task_sessions_state ON task_sessions(state);
CREATE INDEX IF NOT EXISTS idx_task_sessions_task_state ON task_sessions(task_id, state);
CREATE TABLE IF NOT EXISTS task_environments (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
repository_id TEXT DEFAULT '',
executor_type TEXT NOT NULL DEFAULT '',
executor_id TEXT DEFAULT '',
executor_profile_id TEXT DEFAULT '',
agent_execution_id TEXT DEFAULT '',
control_port INTEGER DEFAULT 0,
status TEXT NOT NULL DEFAULT 'creating',
worktree_id TEXT DEFAULT '',
worktree_path TEXT DEFAULT '',
worktree_branch TEXT DEFAULT '',
workspace_path TEXT DEFAULT '',
container_id TEXT DEFAULT '',
sandbox_id TEXT DEFAULT '',
task_dir_name TEXT DEFAULT '',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_task_environments_task_id ON task_environments(task_id);
CREATE INDEX IF NOT EXISTS idx_task_environments_status ON task_environments(status);
CREATE TABLE IF NOT EXISTS task_environment_repos (
id TEXT PRIMARY KEY,
task_environment_id TEXT NOT NULL,
repository_id TEXT NOT NULL,
branch_slug TEXT NOT NULL DEFAULT '',
worktree_id TEXT DEFAULT '',
worktree_path TEXT DEFAULT '',
worktree_branch TEXT DEFAULT '',
position INTEGER DEFAULT 0,
error_message TEXT DEFAULT '',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (task_environment_id) REFERENCES task_environments(id) ON DELETE CASCADE,
UNIQUE(task_environment_id, repository_id, branch_slug)
);
CREATE INDEX IF NOT EXISTS idx_task_environment_repos_env_id ON task_environment_repos(task_environment_id);
CREATE INDEX IF NOT EXISTS idx_task_environment_repos_repository_id ON task_environment_repos(repository_id);
CREATE TABLE IF NOT EXISTS task_session_worktrees (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
worktree_id TEXT NOT NULL,
repository_id TEXT NOT NULL,
branch_slug TEXT NOT NULL DEFAULT '',
position INTEGER DEFAULT 0,
worktree_path TEXT DEFAULT '',
worktree_branch TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
merged_at TIMESTAMP,
deleted_at TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES task_sessions(id) ON DELETE CASCADE,
UNIQUE(session_id, worktree_id)
);
CREATE INDEX IF NOT EXISTS idx_task_session_worktrees_session_id ON task_session_worktrees(session_id);
CREATE INDEX IF NOT EXISTS idx_task_session_worktrees_worktree_id ON task_session_worktrees(worktree_id);
CREATE INDEX IF NOT EXISTS idx_task_session_worktrees_repository_id ON task_session_worktrees(repository_id);
CREATE INDEX IF NOT EXISTS idx_task_session_worktrees_status ON task_session_worktrees(status);
`
func (r *Repository) initSessionWorktreeSchema() error {
_, err := r.db.Exec(sessionWorktreeSchemaDDL)
return err
}
func (r *Repository) initGitSchema() error {
_, err := r.db.Exec(`
CREATE TABLE IF NOT EXISTS task_session_git_snapshots (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
snapshot_type TEXT NOT NULL,
branch TEXT NOT NULL,
remote_branch TEXT DEFAULT '',
head_commit TEXT DEFAULT '',
base_commit TEXT DEFAULT '',
ahead INTEGER DEFAULT 0,
behind INTEGER DEFAULT 0,
files TEXT DEFAULT '{}',
triggered_by TEXT DEFAULT '',
metadata TEXT DEFAULT '{}',
created_at TIMESTAMP NOT NULL,
FOREIGN KEY (session_id) REFERENCES task_sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_git_snapshots_session ON task_session_git_snapshots(session_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_git_snapshots_type ON task_session_git_snapshots(session_id, snapshot_type);
CREATE TABLE IF NOT EXISTS task_session_commits (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
commit_sha TEXT NOT NULL,
parent_sha TEXT DEFAULT '',
author_name TEXT DEFAULT '',
author_email TEXT DEFAULT '',
commit_message TEXT DEFAULT '',
committed_at TIMESTAMP NOT NULL,
pre_commit_snapshot_id TEXT DEFAULT '',
post_commit_snapshot_id TEXT DEFAULT '',
files_changed INTEGER DEFAULT 0,
insertions INTEGER DEFAULT 0,
deletions INTEGER DEFAULT 0,
created_at TIMESTAMP NOT NULL,
FOREIGN KEY (session_id) REFERENCES task_sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_session_commits_session ON task_session_commits(session_id, committed_at DESC);
CREATE INDEX IF NOT EXISTS idx_session_commits_sha ON task_session_commits(commit_sha);
`)
return err
}
func (r *Repository) initReviewSchema() error {
_, err := r.db.Exec(`
CREATE TABLE IF NOT EXISTS session_file_reviews (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
file_path TEXT NOT NULL,
reviewed INTEGER NOT NULL DEFAULT 0,
diff_hash TEXT NOT NULL DEFAULT '',
reviewed_at TIMESTAMP,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (session_id) REFERENCES task_sessions(id) ON DELETE CASCADE,
UNIQUE(session_id, file_path)
);
CREATE INDEX IF NOT EXISTS idx_session_file_reviews_session ON session_file_reviews(session_id);
`)
return err
}