Update SQLX stuff

This commit is contained in:
Louis Knight-Webb
2025-06-20 21:32:00 +01:00
parent 3e279cdef9
commit 37099032ee
8 changed files with 55 additions and 428 deletions

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT \n t.id as \"id!: Uuid\", \n t.project_id as \"project_id!: Uuid\", \n t.title, \n t.description, \n t.status as \"status!: TaskStatus\", \n t.created_at as \"created_at!: DateTime<Utc>\", \n t.updated_at as \"updated_at!: DateTime<Utc>\",\n CASE WHEN in_progress_attempts.task_id IS NOT NULL THEN true ELSE false END as \"has_in_progress_attempt!\",\n CASE WHEN merged_attempts.task_id IS NOT NULL THEN true ELSE false END as \"has_merged_attempt!\"\n FROM tasks t\n LEFT JOIN (\n SELECT DISTINCT ta.task_id \n FROM task_attempts ta\n INNER JOIN (\n SELECT task_attempt_id, MAX(created_at) as latest_created_at\n FROM task_attempt_activities\n GROUP BY task_attempt_id\n ) latest_activity ON ta.id = latest_activity.task_attempt_id\n INNER JOIN task_attempt_activities taa ON ta.id = taa.task_attempt_id \n AND taa.created_at = latest_activity.latest_created_at\n WHERE taa.status IN ('setuprunning', 'executorrunning')\n ) in_progress_attempts ON t.id = in_progress_attempts.task_id\n LEFT JOIN (\n SELECT DISTINCT ta.task_id \n FROM task_attempts ta\n WHERE ta.merge_commit IS NOT NULL\n ) merged_attempts ON t.id = merged_attempts.task_id\n WHERE t.project_id = $1 \n ORDER BY t.created_at DESC", "query": "SELECT \n t.id as \"id!: Uuid\", \n t.project_id as \"project_id!: Uuid\", \n t.title, \n t.description, \n t.status as \"status!: TaskStatus\", \n t.created_at as \"created_at!: DateTime<Utc>\", \n t.updated_at as \"updated_at!: DateTime<Utc>\",\n CASE WHEN in_progress_attempts.task_id IS NOT NULL THEN true ELSE false END as \"has_in_progress_attempt!: i64\",\n CASE WHEN merged_attempts.task_id IS NOT NULL THEN true ELSE false END as \"has_merged_attempt!\"\n FROM tasks t\n LEFT JOIN (\n SELECT DISTINCT ta.task_id \n FROM task_attempts ta\n INNER JOIN (\n SELECT task_attempt_id, MAX(created_at) as latest_created_at\n FROM task_attempt_activities\n GROUP BY task_attempt_id\n ) latest_activity ON ta.id = latest_activity.task_attempt_id\n INNER JOIN task_attempt_activities taa ON ta.id = taa.task_attempt_id \n AND taa.created_at = latest_activity.latest_created_at\n WHERE taa.status IN ('setuprunning', 'executorrunning')\n ) in_progress_attempts ON t.id = in_progress_attempts.task_id\n LEFT JOIN (\n SELECT DISTINCT ta.task_id \n FROM task_attempts ta\n WHERE ta.merge_commit IS NOT NULL\n ) merged_attempts ON t.id = merged_attempts.task_id\n WHERE t.project_id = $1 \n ORDER BY t.created_at DESC",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -39,14 +39,14 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "has_in_progress_attempt!", "name": "has_in_progress_attempt!: i64",
"ordinal": 7, "ordinal": 7,
"type_info": "Int" "type_info": "Integer"
}, },
{ {
"name": "has_merged_attempt!", "name": "has_merged_attempt!",
"ordinal": 8, "ordinal": 8,
"type_info": "Int" "type_info": "Integer"
} }
], ],
"parameters": { "parameters": {
@@ -64,5 +64,5 @@
false false
] ]
}, },
"hash": "ffd6ac9bbe361cdb172198be10ecfbcc10970bba5d9f41fd5018c858fd784cb2" "hash": "35c2c111465a13b4a9d45ef505512796b6e6970ffd35ee4095d9c5caf28502e4"
} }

View File

@@ -1,20 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT COUNT(*) as count FROM projects WHERE id = $1",
"describe": {
"columns": [
{
"name": "count",
"ordinal": 0,
"type_info": "Int"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "b4efb71365b75edd3dfc843f774f7cdb06dc756328ed5929d6b13ef3a956be0e"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "\n SELECT COUNT(*) as \"count!: i64\"\n FROM projects\n WHERE id = $1\n ",
"describe": {
"columns": [
{
"name": "count!: i64",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2"
}

View File

@@ -18,7 +18,7 @@ serde_json = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid"] } sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4", "serde"] } uuid = { version = "1.0", features = ["v4", "serde"] }
bcrypt = "0.15" bcrypt = "0.15"

View File

@@ -12,9 +12,17 @@ use crate::models::{
task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity}, task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity},
}; };
// #[derive(Debug)]
// pub enum ExecutionType {
// SetupScript,
// CodingAgent,
// DevServer,
// }
#[derive(Debug)] #[derive(Debug)]
pub struct RunningExecution { pub struct RunningExecution {
pub task_attempt_id: Uuid, pub task_attempt_id: Uuid,
// pub execution_type: ExecutionType,
pub child: tokio::process::Child, pub child: tokio::process::Child,
pub started_at: DateTime<Utc>, pub started_at: DateTime<Utc>,
} }

View File

@@ -143,9 +143,17 @@ impl Project {
} }
pub async fn exists(pool: &SqlitePool, id: Uuid) -> Result<bool, sqlx::Error> { pub async fn exists(pool: &SqlitePool, id: Uuid) -> Result<bool, sqlx::Error> {
let result = sqlx::query!("SELECT COUNT(*) as count FROM projects WHERE id = $1", id) let result = sqlx::query!(
.fetch_one(pool) r#"
.await?; SELECT COUNT(*) as "count!: i64"
FROM projects
WHERE id = $1
"#,
id
)
.fetch_one(pool)
.await?;
Ok(result.count > 0) Ok(result.count > 0)
} }
} }

View File

@@ -88,7 +88,7 @@ impl Task {
t.status as "status!: TaskStatus", t.status as "status!: TaskStatus",
t.created_at as "created_at!: DateTime<Utc>", t.created_at as "created_at!: DateTime<Utc>",
t.updated_at as "updated_at!: DateTime<Utc>", t.updated_at as "updated_at!: DateTime<Utc>",
CASE WHEN in_progress_attempts.task_id IS NOT NULL THEN true ELSE false END as "has_in_progress_attempt!", CASE WHEN in_progress_attempts.task_id IS NOT NULL THEN true ELSE false END as "has_in_progress_attempt!: i64",
CASE WHEN merged_attempts.task_id IS NOT NULL THEN true ELSE false END as "has_merged_attempt!" CASE WHEN merged_attempts.task_id IS NOT NULL THEN true ELSE false END as "has_merged_attempt!"
FROM tasks t FROM tasks t
LEFT JOIN ( LEFT JOIN (

View File

@@ -135,8 +135,15 @@ pub async fn create_task_and_start(
// Create task attempt // Create task attempt
let attempt_id = Uuid::new_v4(); let attempt_id = Uuid::new_v4();
let worktree_path = format!("/tmp/task-{}-attempt-{}", task_id, std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis()); let worktree_path = format!(
"/tmp/task-{}-attempt-{}",
task_id,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
);
let attempt_payload = CreateTaskAttempt { let attempt_payload = CreateTaskAttempt {
task_id, task_id,
worktree_path, worktree_path,
@@ -756,399 +763,3 @@ pub fn tasks_router() -> Router {
post(delete_task_attempt_file), post(delete_task_attempt_file),
) )
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::hash_password;
use crate::models::{
project::Project,
task::{CreateTask, TaskStatus, UpdateTask},
user::User,
};
use axum::extract::Extension;
use chrono::Utc;
use sqlx::SqlitePool;
use uuid::Uuid;
async fn create_test_user(
pool: &SqlitePool,
email: &str,
password: &str,
is_admin: bool,
) -> User {
let id = Uuid::new_v4();
let now = Utc::now();
let password_hash = hash_password(password).unwrap();
sqlx::query_as!(
User,
"INSERT INTO users (id, email, password_hash, is_admin, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, email, password_hash, is_admin, created_at, updated_at",
id,
email,
password_hash,
is_admin,
now,
now
)
.fetch_one(pool)
.await
.unwrap()
}
async fn create_test_project(pool: &SqlitePool, name: &str, owner_id: Uuid) -> Project {
let id = Uuid::new_v4();
let now = Utc::now();
let git_repo_path = format!("/tmp/test-repo-{}", id);
sqlx::query_as!(
Project,
"INSERT INTO projects (id, name, git_repo_path, owner_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, git_repo_path, owner_id, created_at, updated_at",
id,
name,
git_repo_path,
owner_id,
now,
now
)
.fetch_one(pool)
.await
.unwrap()
}
async fn create_test_task(
pool: &SqlitePool,
project_id: Uuid,
title: &str,
description: Option<String>,
status: TaskStatus,
) -> Task {
let id = Uuid::new_v4();
let now = Utc::now();
sqlx::query_as!(
Task,
r#"INSERT INTO tasks (id, project_id, title, description, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, project_id, title, description, status as "status!: TaskStatus", created_at, updated_at"#,
id,
project_id,
title,
description,
status as TaskStatus,
now,
now
)
.fetch_one(pool)
.await
.unwrap()
}
#[sqlx::test]
async fn test_get_project_tasks_success(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
// Create multiple tasks
create_test_task(
&pool,
project.id,
"Task 1",
Some("Description 1".to_string()),
TaskStatus::Todo,
)
.await;
create_test_task(&pool, project.id, "Task 2", None, TaskStatus::InProgress).await;
create_test_task(
&pool,
project.id,
"Task 3",
Some("Description 3".to_string()),
TaskStatus::Done,
)
.await;
let result = get_project_tasks(Path(project.id), Extension(pool)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
assert_eq!(response.data.unwrap().len(), 3);
}
#[sqlx::test]
async fn test_get_project_tasks_empty_project(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Empty Project", user.id).await;
let result = get_project_tasks(Path(project.id), Extension(pool)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
assert_eq!(response.data.unwrap().len(), 0);
}
#[sqlx::test]
async fn test_get_task_success(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let task = create_test_task(
&pool,
project.id,
"Test Task",
Some("Test Description".to_string()),
TaskStatus::Todo,
)
.await;
let result = get_task(Path((project.id, task.id)), Extension(pool)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let returned_task = response.data.unwrap();
assert_eq!(returned_task.id, task.id);
assert_eq!(returned_task.title, task.title);
assert_eq!(returned_task.description, task.description);
assert_eq!(returned_task.status, task.status);
}
#[sqlx::test]
async fn test_get_task_not_found(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let nonexistent_task_id = Uuid::new_v4();
let result = get_task(Path((project.id, nonexistent_task_id)), Extension(pool)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_get_task_wrong_project(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project1 = create_test_project(&pool, "Project 1", user.id).await;
let project2 = create_test_project(&pool, "Project 2", user.id).await;
let task = create_test_task(&pool, project1.id, "Test Task", None, TaskStatus::Todo).await;
// Try to get task from wrong project
let result = get_task(Path((project2.id, task.id)), Extension(pool)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_create_task_success(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let create_request = CreateTask {
project_id: project.id, // This will be overridden by the path parameter
title: "New Task".to_string(),
description: Some("Task description".to_string()),
};
let result = create_task(Path(project.id), Extension(pool), Json(create_request)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let created_task = response.data.unwrap();
assert_eq!(created_task.title, "New Task");
assert_eq!(
created_task.description,
Some("Task description".to_string())
);
assert_eq!(created_task.status, TaskStatus::Todo);
assert_eq!(created_task.project_id, project.id);
}
#[sqlx::test]
async fn test_create_task_project_not_found(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let nonexistent_project_id = Uuid::new_v4();
let create_request = CreateTask {
project_id: nonexistent_project_id,
title: "New Task".to_string(),
description: None,
};
let result = create_task(
Path(nonexistent_project_id),
Extension(pool),
Json(create_request),
)
.await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_update_task_success(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let task = create_test_task(
&pool,
project.id,
"Original Title",
Some("Original Description".to_string()),
TaskStatus::Todo,
)
.await;
let update_request = UpdateTask {
title: Some("Updated Title".to_string()),
description: Some("Updated Description".to_string()),
status: Some(TaskStatus::InProgress),
};
let result = update_task(
Path((project.id, task.id)),
Extension(pool),
Json(update_request),
)
.await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let updated_task = response.data.unwrap();
assert_eq!(updated_task.title, "Updated Title");
assert_eq!(
updated_task.description,
Some("Updated Description".to_string())
);
assert_eq!(updated_task.status, TaskStatus::InProgress);
}
#[sqlx::test]
async fn test_update_task_partial(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let task = create_test_task(
&pool,
project.id,
"Original Title",
Some("Original Description".to_string()),
TaskStatus::Todo,
)
.await;
// Only update status
let update_request = UpdateTask {
title: None,
description: None,
status: Some(TaskStatus::Done),
};
let result = update_task(
Path((project.id, task.id)),
Extension(pool),
Json(update_request),
)
.await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let updated_task = response.data.unwrap();
assert_eq!(updated_task.title, "Original Title"); // Should remain unchanged
assert_eq!(
updated_task.description,
Some("Original Description".to_string())
); // Should remain unchanged
assert_eq!(updated_task.status, TaskStatus::Done); // Should be updated
}
#[sqlx::test]
async fn test_update_task_not_found(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let nonexistent_task_id = Uuid::new_v4();
let update_request = UpdateTask {
title: Some("Updated Title".to_string()),
description: None,
status: None,
};
let result = update_task(
Path((project.id, nonexistent_task_id)),
Extension(pool),
Json(update_request),
)
.await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_update_task_wrong_project(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project1 = create_test_project(&pool, "Project 1", user.id).await;
let project2 = create_test_project(&pool, "Project 2", user.id).await;
let task = create_test_task(&pool, project1.id, "Test Task", None, TaskStatus::Todo).await;
let update_request = UpdateTask {
title: Some("Updated Title".to_string()),
description: None,
status: None,
};
// Try to update task in wrong project
let result = update_task(
Path((project2.id, task.id)),
Extension(pool),
Json(update_request),
)
.await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_delete_task_success(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let task =
create_test_task(&pool, project.id, "Task to Delete", None, TaskStatus::Todo).await;
let result = delete_task(Path((project.id, task.id)), Extension(pool)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert_eq!(response.message.unwrap(), "Task deleted successfully");
}
#[sqlx::test]
async fn test_delete_task_not_found(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let nonexistent_task_id = Uuid::new_v4();
let result = delete_task(Path((project.id, nonexistent_task_id)), Extension(pool)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_delete_task_wrong_project(pool: SqlitePool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project1 = create_test_project(&pool, "Project 1", user.id).await;
let project2 = create_test_project(&pool, "Project 2", user.id).await;
let task =
create_test_task(&pool, project1.id, "Task to Delete", None, TaskStatus::Todo).await;
// Try to delete task from wrong project
let result = delete_task(Path((project2.id, task.id)), Extension(pool)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
}