use chrono::{DateTime, Utc};
use forge_core_executors::executors::BaseCodingAgent;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
use thiserror::Error;
use ts_rs_forge::TS;
use uuid::Uuid;
use super::project::Project;
#[derive(Debug, Error)]
pub enum ExecutionRunError {
#[error(transparent)]
Database(#[from] sqlx::Error),
#[error("Execution run not found")]
ExecutionRunNotFound,
#[error("Project not found")]
ProjectNotFound,
#[error("Validation error: {0}")]
ValidationError(String),
}
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)]
pub struct ExecutionRun {
pub id: Uuid,
pub project_id: Uuid,
pub branch: String,
pub target_branch: String,
pub executor: String,
pub variant: Option<String>,
pub container_ref: Option<String>,
pub prompt: String,
pub worktree_deleted: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Deserialize, TS)]
pub struct CreateExecutionRun {
pub executor: BaseCodingAgent,
pub variant: Option<String>,
pub base_branch: String,
pub prompt: String,
}
#[derive(Debug)]
pub struct ExecutionRunContext {
pub execution_run: ExecutionRun,
pub project: Project,
}
impl ExecutionRun {
pub async fn find_by_id(pool: &SqlitePool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
sqlx::query_as!(
ExecutionRun,
r#"SELECT id AS "id!: Uuid",
project_id AS "project_id!: Uuid",
branch,
target_branch,
executor AS "executor!",
variant,
container_ref,
prompt,
worktree_deleted AS "worktree_deleted!: bool",
created_at AS "created_at!: DateTime<Utc>",
updated_at AS "updated_at!: DateTime<Utc>"
FROM execution_runs
WHERE id = $1"#,
id
)
.fetch_optional(pool)
.await
}
pub async fn fetch_all(
pool: &SqlitePool,
project_id: Option<Uuid>,
) -> Result<Vec<Self>, ExecutionRunError> {
let runs = match project_id {
Some(pid) => sqlx::query_as!(
ExecutionRun,
r#"SELECT id AS "id!: Uuid",
project_id AS "project_id!: Uuid",
branch,
target_branch,
executor AS "executor!",
variant,
container_ref,
prompt,
worktree_deleted AS "worktree_deleted!: bool",
created_at AS "created_at!: DateTime<Utc>",
updated_at AS "updated_at!: DateTime<Utc>"
FROM execution_runs
WHERE project_id = $1
ORDER BY created_at DESC"#,
pid
)
.fetch_all(pool)
.await
.map_err(ExecutionRunError::Database)?,
None => sqlx::query_as!(
ExecutionRun,
r#"SELECT id AS "id!: Uuid",
project_id AS "project_id!: Uuid",
branch,
target_branch,
executor AS "executor!",
variant,
container_ref,
prompt,
worktree_deleted AS "worktree_deleted!: bool",
created_at AS "created_at!: DateTime<Utc>",
updated_at AS "updated_at!: DateTime<Utc>"
FROM execution_runs
ORDER BY created_at DESC"#
)
.fetch_all(pool)
.await
.map_err(ExecutionRunError::Database)?,
};
Ok(runs)
}
pub async fn load_context(
pool: &SqlitePool,
run_id: Uuid,
project_id: Uuid,
) -> Result<ExecutionRunContext, ExecutionRunError> {
let execution_run = sqlx::query_as!(
ExecutionRun,
r#"SELECT er.id AS "id!: Uuid",
er.project_id AS "project_id!: Uuid",
er.branch,
er.target_branch,
er.executor AS "executor!",
er.variant,
er.container_ref,
er.prompt,
er.worktree_deleted AS "worktree_deleted!: bool",
er.created_at AS "created_at!: DateTime<Utc>",
er.updated_at AS "updated_at!: DateTime<Utc>"
FROM execution_runs er
JOIN projects p ON er.project_id = p.id
WHERE er.id = $1 AND p.id = $2"#,
run_id,
project_id
)
.fetch_optional(pool)
.await?
.ok_or(ExecutionRunError::ExecutionRunNotFound)?;
let project = Project::find_by_id(pool, project_id)
.await?
.ok_or(ExecutionRunError::ProjectNotFound)?;
Ok(ExecutionRunContext {
execution_run,
project,
})
}
pub async fn create(
pool: &SqlitePool,
data: &CreateExecutionRun,
id: Uuid,
project_id: Uuid,
branch: &str,
) -> Result<Self, ExecutionRunError> {
Ok(sqlx::query_as!(
ExecutionRun,
r#"INSERT INTO execution_runs (id, project_id, branch, target_branch, executor, variant, container_ref, prompt, worktree_deleted)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id AS "id!: Uuid",
project_id AS "project_id!: Uuid",
branch,
target_branch,
executor AS "executor!",
variant,
container_ref,
prompt,
worktree_deleted AS "worktree_deleted!: bool",
created_at AS "created_at!: DateTime<Utc>",
updated_at AS "updated_at!: DateTime<Utc>""#,
id,
project_id,
branch,
data.base_branch,
data.executor,
data.variant,
Option::<String>::None,
data.prompt,
false
)
.fetch_one(pool)
.await?)
}
pub async fn update_container_ref(
pool: &SqlitePool,
run_id: Uuid,
container_ref: &str,
) -> Result<(), sqlx::Error> {
let now = Utc::now();
sqlx::query!(
"UPDATE execution_runs SET container_ref = $1, updated_at = $2 WHERE id = $3",
container_ref,
now,
run_id
)
.execute(pool)
.await?;
Ok(())
}
pub async fn mark_worktree_deleted(pool: &SqlitePool, run_id: Uuid) -> Result<(), sqlx::Error> {
sqlx::query!(
"UPDATE execution_runs SET worktree_deleted = TRUE, updated_at = datetime('now', 'subsec') WHERE id = ?",
run_id
)
.execute(pool)
.await?;
Ok(())
}
pub async fn container_ref_exists(
pool: &SqlitePool,
container_ref: &str,
) -> Result<bool, sqlx::Error> {
let result = sqlx::query!(
r#"SELECT EXISTS(SELECT 1 FROM execution_runs WHERE container_ref = ?) as "exists!: bool""#,
container_ref
)
.fetch_one(pool)
.await?;
Ok(result.exists)
}
pub async fn resolve_container_ref(
pool: &SqlitePool,
container_ref: &str,
) -> Result<(Uuid, Uuid), sqlx::Error> {
let result = sqlx::query!(
r#"SELECT er.id AS "run_id!: Uuid",
er.project_id AS "project_id!: Uuid"
FROM execution_runs er
WHERE er.container_ref = ?"#,
container_ref
)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
Ok((result.run_id, result.project_id))
}
}