Files
kandev/apps/backend/internal/task/repository/sqlite/base.go
T
Carlos Florêncio 3717d6982f feat: review ux (#210)
feat: review dialog
2026-02-08 11:14:15 +00:00

403 lines
13 KiB
Go

// Package sqlite provides SQLite-based repository implementations.
package sqlite
import (
"database/sql"
"fmt"
)
// Repository provides SQLite-based task storage operations.
type Repository struct {
db *sql.DB
ownsDB bool
}
// NewWithDB creates a new SQLite repository with an existing database connection (shared ownership).
func NewWithDB(dbConn *sql.DB) (*Repository, error) {
return newRepository(dbConn, false)
}
func newRepository(dbConn *sql.DB, ownsDB bool) (*Repository, error) {
repo := &Repository{db: dbConn, ownsDB: ownsDB}
if err := repo.initSchema(); err != nil {
if ownsDB {
if closeErr := dbConn.Close(); closeErr != nil {
return nil, fmt.Errorf("failed to close database after schema error: %w", closeErr)
}
}
return nil, fmt.Errorf("failed to initialize schema: %w", err)
}
return repo, nil
}
// Close closes the database connection
func (r *Repository) Close() error {
if !r.ownsDB {
return nil
}
return r.db.Close()
}
// DB returns the underlying sql.DB instance for shared access
func (r *Repository) DB() *sql.DB {
return r.db
}
// 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_boards_workspace_id ON boards(workspace_id)`); err != nil {
return err
}
return nil
}
// initSchema creates the database tables if they don't exist
func (r *Repository) initSchema() error {
schema := `
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 '',
created_at DATETIME NOT NULL,
updated_at DATETIME 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 DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
deleted_at DATETIME
);
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 DATETIME,
error_message TEXT DEFAULT '',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
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 DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
deleted_at DATETIME
);
CREATE TABLE IF NOT EXISTS boards (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL DEFAULT '',
workflow_template_id TEXT DEFAULT '',
name TEXT NOT NULL,
description TEXT DEFAULT '',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL DEFAULT '',
board_id TEXT NOT NULL,
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 '{}',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (board_id) REFERENCES boards(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 '',
position INTEGER DEFAULT 0,
metadata TEXT DEFAULT '{}',
created_at DATETIME NOT NULL,
updated_at DATETIME 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)
);
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 '',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
deleted_at DATETIME,
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
);
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 DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_tasks_board_id ON tasks(board_id);
CREATE INDEX IF NOT EXISTS idx_tasks_workflow_step_id ON tasks(workflow_step_id);
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);
`
if _, err := r.db.Exec(schema); err != nil {
return err
}
// Task plans table
plansSchema := `
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 DATETIME NOT NULL,
updated_at DATETIME 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);
`
if _, err := r.db.Exec(plansSchema); err != nil {
return err
}
// Session and message tables
sessionSchema := `
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 DATETIME NOT NULL,
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);
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 DATETIME NOT NULL,
completed_at DATETIME,
metadata TEXT DEFAULT '{}',
created_at DATETIME NOT NULL,
updated_at DATETIME 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_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 NOT NULL,
executor_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 DATETIME NOT NULL,
completed_at DATETIME,
updated_at DATETIME NOT NULL,
is_primary INTEGER DEFAULT 0,
workflow_step_id TEXT DEFAULT '',
review_status TEXT DEFAULT '',
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_session_worktrees (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
worktree_id TEXT NOT NULL,
repository_id TEXT NOT NULL,
position INTEGER DEFAULT 0,
worktree_path TEXT DEFAULT '',
worktree_branch TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
merged_at DATETIME,
deleted_at DATETIME,
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);
`
if _, err := r.db.Exec(sessionSchema); err != nil {
return err
}
// Git tracking tables
gitSchema := `
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 DATETIME 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 DATETIME 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 DATETIME 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);
`
if _, err := r.db.Exec(gitSchema); err != nil {
return err
}
reviewSchema := `
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 DATETIME,
created_at DATETIME NOT NULL,
updated_at DATETIME 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);
`
if _, err := r.db.Exec(reviewSchema); err != nil {
return err
}
if err := r.ensureDefaultWorkspace(); err != nil {
return err
}
if err := r.ensureDefaultExecutorsAndEnvironments(); err != nil {
return err
}
if err := r.ensureWorkspaceIndexes(); err != nil {
return err
}
return nil
}