//! Runtime and Worker repository for database operations //! //! This module provides CRUD operations and queries for Runtime and Worker entities. use crate::models::{ enums::{WorkerStatus, WorkerType}, runtime::*, Id, JsonDict, }; use crate::Result; use sqlx::{Executor, Postgres, QueryBuilder}; use super::{Create, Delete, FindById, FindByRef, List, Patch, Repository, Update}; /// Repository for Runtime operations pub struct RuntimeRepository; impl Repository for RuntimeRepository { type Entity = Runtime; fn table_name() -> &'static str { "runtime" } } /// Columns selected for all Runtime queries. Centralised here so that /// schema changes only need one update. pub const SELECT_COLUMNS: &str = "id, ref, pack, pack_ref, description, name, \ distributions, installation, installers, execution_config, \ auto_detected, detection_config, \ created, updated"; /// Input for creating a new runtime #[derive(Debug, Clone)] pub struct CreateRuntimeInput { pub r#ref: String, pub pack: Option, pub pack_ref: Option, pub description: Option, pub name: String, pub distributions: JsonDict, pub installation: Option, pub execution_config: JsonDict, pub auto_detected: bool, pub detection_config: JsonDict, } /// Input for updating a runtime #[derive(Debug, Clone, Default)] pub struct UpdateRuntimeInput { pub description: Option>, pub name: Option, pub distributions: Option, pub installation: Option>, pub execution_config: Option, pub auto_detected: Option, pub detection_config: Option, } #[async_trait::async_trait] impl FindById for RuntimeRepository { async fn find_by_id<'e, E>(executor: E, id: i64) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let query = format!("SELECT {} FROM runtime WHERE id = $1", SELECT_COLUMNS); let runtime = sqlx::query_as::<_, Runtime>(&query) .bind(id) .fetch_optional(executor) .await?; Ok(runtime) } } #[async_trait::async_trait] impl FindByRef for RuntimeRepository { async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let query = format!("SELECT {} FROM runtime WHERE ref = $1", SELECT_COLUMNS); let runtime = sqlx::query_as::<_, Runtime>(&query) .bind(ref_str) .fetch_optional(executor) .await?; Ok(runtime) } } #[async_trait::async_trait] impl List for RuntimeRepository { async fn list<'e, E>(executor: E) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let query = format!("SELECT {} FROM runtime ORDER BY ref ASC", SELECT_COLUMNS); let runtimes = sqlx::query_as::<_, Runtime>(&query) .fetch_all(executor) .await?; Ok(runtimes) } } #[async_trait::async_trait] impl Create for RuntimeRepository { type CreateInput = CreateRuntimeInput; async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let query = format!( "INSERT INTO runtime (ref, pack, pack_ref, description, name, \ distributions, installation, installers, execution_config, \ auto_detected, detection_config) \ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) \ RETURNING {}", SELECT_COLUMNS ); let runtime = sqlx::query_as::<_, Runtime>(&query) .bind(&input.r#ref) .bind(input.pack) .bind(&input.pack_ref) .bind(&input.description) .bind(&input.name) .bind(&input.distributions) .bind(&input.installation) .bind(serde_json::json!({})) .bind(&input.execution_config) .bind(input.auto_detected) .bind(&input.detection_config) .fetch_one(executor) .await?; Ok(runtime) } } #[async_trait::async_trait] impl Update for RuntimeRepository { type UpdateInput = UpdateRuntimeInput; async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result where E: Executor<'e, Database = Postgres> + 'e, { // Build update query let mut query = QueryBuilder::new("UPDATE runtime SET "); let mut has_updates = false; if let Some(description) = &input.description { query.push("description = "); match description { Patch::Set(description) => query.push_bind(description), Patch::Clear => query.push_bind(Option::::None), }; has_updates = true; } if let Some(name) = &input.name { if has_updates { query.push(", "); } query.push("name = "); query.push_bind(name); has_updates = true; } if let Some(distributions) = &input.distributions { if has_updates { query.push(", "); } query.push("distributions = "); query.push_bind(distributions); has_updates = true; } if let Some(installation) = &input.installation { if has_updates { query.push(", "); } query.push("installation = "); match installation { Patch::Set(installation) => query.push_bind(installation), Patch::Clear => query.push_bind(Option::::None), }; has_updates = true; } if let Some(execution_config) = &input.execution_config { if has_updates { query.push(", "); } query.push("execution_config = "); query.push_bind(execution_config); has_updates = true; } if let Some(auto_detected) = input.auto_detected { if has_updates { query.push(", "); } query.push("auto_detected = "); query.push_bind(auto_detected); has_updates = true; } if let Some(detection_config) = &input.detection_config { if has_updates { query.push(", "); } query.push("detection_config = "); query.push_bind(detection_config); has_updates = true; } if !has_updates { // No updates requested, fetch and return existing entity return Self::get_by_id(executor, id).await; } query.push(", updated = NOW() WHERE id = "); query.push_bind(id); query.push(&format!(" RETURNING {}", SELECT_COLUMNS)); let runtime = query .build_query_as::() .fetch_one(executor) .await?; Ok(runtime) } } #[async_trait::async_trait] impl Delete for RuntimeRepository { async fn delete<'e, E>(executor: E, id: i64) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let result = sqlx::query("DELETE FROM runtime WHERE id = $1") .bind(id) .execute(executor) .await?; Ok(result.rows_affected() > 0) } } impl RuntimeRepository { /// Find runtimes by pack pub async fn find_by_pack<'e, E>(executor: E, pack_id: Id) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let query = format!( "SELECT {} FROM runtime WHERE pack = $1 ORDER BY ref ASC", SELECT_COLUMNS ); let runtimes = sqlx::query_as::<_, Runtime>(&query) .bind(pack_id) .fetch_all(executor) .await?; Ok(runtimes) } /// Find a runtime by name (case-insensitive) pub async fn find_by_name<'e, E>(executor: E, name: &str) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let query = format!( "SELECT {} FROM runtime WHERE LOWER(name) = LOWER($1) LIMIT 1", SELECT_COLUMNS ); let runtime = sqlx::query_as::<_, Runtime>(&query) .bind(name) .fetch_optional(executor) .await?; Ok(runtime) } /// Delete runtimes belonging to a pack whose refs are NOT in the given set. /// /// Used during pack reinstallation to clean up runtimes that were removed /// from the pack's YAML files. Associated runtime_version rows are /// cascade-deleted by the FK constraint. pub async fn delete_by_pack_excluding<'e, E>( executor: E, pack_id: Id, keep_refs: &[String], ) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let result = if keep_refs.is_empty() { sqlx::query("DELETE FROM runtime WHERE pack = $1") .bind(pack_id) .execute(executor) .await? } else { sqlx::query("DELETE FROM runtime WHERE pack = $1 AND ref != ALL($2)") .bind(pack_id) .bind(keep_refs) .execute(executor) .await? }; Ok(result.rows_affected()) } } // ============================================================================ // Worker Repository // ============================================================================ /// Repository for Worker operations pub struct WorkerRepository; impl Repository for WorkerRepository { type Entity = Worker; fn table_name() -> &'static str { "worker" } } /// Input for creating a new worker #[derive(Debug, Clone)] pub struct CreateWorkerInput { pub name: String, pub worker_type: WorkerType, pub runtime: Option, pub host: Option, pub port: Option, pub status: Option, pub capabilities: Option, pub meta: Option, } /// Input for updating a worker #[derive(Debug, Clone, Default)] pub struct UpdateWorkerInput { pub name: Option, pub status: Option, pub capabilities: Option, pub meta: Option, pub host: Option, pub port: Option, } #[async_trait::async_trait] impl FindById for WorkerRepository { async fn find_by_id<'e, E>(executor: E, id: i64) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let worker = sqlx::query_as::<_, Worker>( r#" SELECT id, name, worker_type, worker_role, runtime, host, port, status, capabilities, meta, last_heartbeat, created, updated FROM worker WHERE id = $1 "#, ) .bind(id) .fetch_optional(executor) .await?; Ok(worker) } } #[async_trait::async_trait] impl List for WorkerRepository { async fn list<'e, E>(executor: E) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let workers = sqlx::query_as::<_, Worker>( r#" SELECT id, name, worker_type, worker_role, runtime, host, port, status, capabilities, meta, last_heartbeat, created, updated FROM worker ORDER BY name ASC "#, ) .fetch_all(executor) .await?; Ok(workers) } } #[async_trait::async_trait] impl Create for WorkerRepository { type CreateInput = CreateWorkerInput; async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let worker = sqlx::query_as::<_, Worker>( r#" INSERT INTO worker (name, worker_type, runtime, host, port, status, capabilities, meta) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, name, worker_type, worker_role, runtime, host, port, status, capabilities, meta, last_heartbeat, created, updated "#, ) .bind(&input.name) .bind(input.worker_type) .bind(input.runtime) .bind(&input.host) .bind(input.port) .bind(input.status) .bind(&input.capabilities) .bind(&input.meta) .fetch_one(executor) .await?; Ok(worker) } } #[async_trait::async_trait] impl Update for WorkerRepository { type UpdateInput = UpdateWorkerInput; async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result where E: Executor<'e, Database = Postgres> + 'e, { // Build update query let mut query = QueryBuilder::new("UPDATE worker SET "); let mut has_updates = false; if let Some(name) = &input.name { query.push("name = "); query.push_bind(name); has_updates = true; } if let Some(status) = input.status { if has_updates { query.push(", "); } query.push("status = "); query.push_bind(status); has_updates = true; } if let Some(capabilities) = &input.capabilities { if has_updates { query.push(", "); } query.push("capabilities = "); query.push_bind(capabilities); has_updates = true; } if let Some(meta) = &input.meta { if has_updates { query.push(", "); } query.push("meta = "); query.push_bind(meta); has_updates = true; } if let Some(host) = &input.host { if has_updates { query.push(", "); } query.push("host = "); query.push_bind(host); has_updates = true; } if let Some(port) = input.port { if has_updates { query.push(", "); } query.push("port = "); query.push_bind(port); has_updates = true; } if !has_updates { // No updates requested, fetch and return existing entity return Self::get_by_id(executor, id).await; } query.push(", updated = NOW() WHERE id = "); query.push_bind(id); query.push( " RETURNING id, name, worker_type, worker_role, runtime, host, port, status, \ capabilities, meta, last_heartbeat, created, updated", ); let worker = query.build_query_as::().fetch_one(executor).await?; Ok(worker) } } #[async_trait::async_trait] impl Delete for WorkerRepository { async fn delete<'e, E>(executor: E, id: i64) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let result = sqlx::query("DELETE FROM worker WHERE id = $1") .bind(id) .execute(executor) .await?; Ok(result.rows_affected() > 0) } } impl WorkerRepository { /// Find workers by status pub async fn find_by_status<'e, E>(executor: E, status: WorkerStatus) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let workers = sqlx::query_as::<_, Worker>( r#" SELECT id, name, worker_type, worker_role, runtime, host, port, status, capabilities, meta, last_heartbeat, created, updated FROM worker WHERE status = $1 ORDER BY name ASC "#, ) .bind(status) .fetch_all(executor) .await?; Ok(workers) } /// Find workers by type pub async fn find_by_type<'e, E>(executor: E, worker_type: WorkerType) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let workers = sqlx::query_as::<_, Worker>( r#" SELECT id, name, worker_type, worker_role, runtime, host, port, status, capabilities, meta, last_heartbeat, created, updated FROM worker WHERE worker_type = $1 ORDER BY name ASC "#, ) .bind(worker_type) .fetch_all(executor) .await?; Ok(workers) } /// Update worker heartbeat pub async fn update_heartbeat<'e, E>(executor: E, id: i64) -> Result<()> where E: Executor<'e, Database = Postgres> + 'e, { sqlx::query("UPDATE worker SET last_heartbeat = NOW() WHERE id = $1") .bind(id) .execute(executor) .await?; Ok(()) } /// Find workers by name pub async fn find_by_name<'e, E>(executor: E, name: &str) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let worker = sqlx::query_as::<_, Worker>( r#" SELECT id, name, worker_type, worker_role, runtime, host, port, status, capabilities, meta, last_heartbeat, created, updated FROM worker WHERE name = $1 "#, ) .bind(name) .fetch_optional(executor) .await?; Ok(worker) } /// Find workers that can execute actions (role = 'action') pub async fn find_action_workers<'e, E>(executor: E) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let workers = sqlx::query_as::<_, Worker>( r#" SELECT id, name, worker_type, worker_role, runtime, host, port, status, capabilities, meta, last_heartbeat, created, updated FROM worker WHERE worker_role = 'action' ORDER BY name ASC "#, ) .fetch_all(executor) .await?; Ok(workers) } }