use crate::error::{AgentDbError, Result};
use crate::schema::now_ms;
use rusqlite::params;
use rusqlite::Connection;
use serde_json::Value;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct Workflow {
pub id: String,
pub name: String,
pub status: String,
pub input: Option<Value>,
pub output: Option<Value>,
pub metadata: Option<Value>,
pub created_at: i64,
pub updated_at: i64,
pub steps: Vec<WorkflowStep>,
}
#[derive(Debug, Clone)]
pub struct WorkflowStep {
pub id: String,
pub workflow_id: String,
pub step_index: i64,
pub name: String,
pub status: String,
pub input: Option<Value>,
pub output: Option<Value>,
pub error: Option<String>,
pub started_at: Option<i64>,
pub completed_at: Option<i64>,
}
pub struct WorkflowStore {
conn: Arc<Mutex<Connection>>,
}
impl WorkflowStore {
pub(crate) fn new(conn: Arc<Mutex<Connection>>) -> Self {
Self { conn }
}
pub fn create_workflow(&self, id: &str, name: &str, input: Option<Value>) -> Result<()> {
let conn = self.conn.lock().unwrap();
let input_str = input.as_ref().map(|v| v.to_string());
let now = now_ms();
conn.execute(
"INSERT INTO _adb_workflows (id, name, status, input, created_at, updated_at)
VALUES (?1, ?2, 'pending', ?3, ?4, ?5)",
params![id, name, input_str, now, now],
)?;
Ok(())
}
pub fn add_step(&self, workflow_id: &str, name: &str, input: Option<Value>) -> Result<String> {
let step_id = Uuid::new_v4().to_string();
let conn = self.conn.lock().unwrap();
let input_str = input.as_ref().map(|v| v.to_string());
let step_index: i64 = conn
.query_row(
"SELECT COALESCE(MAX(step_index) + 1, 0)
FROM _adb_workflow_steps
WHERE workflow_id = ?1",
params![workflow_id],
|r| r.get(0),
)
.unwrap_or(0);
conn.execute(
"INSERT INTO _adb_workflow_steps
(id, workflow_id, step_index, name, status, input)
VALUES (?1, ?2, ?3, ?4, 'pending', ?5)",
params![step_id, workflow_id, step_index, name, input_str],
)?;
Ok(step_id)
}
pub fn update_step(
&self,
step_id: &str,
status: &str,
output: Option<Value>,
error: Option<&str>,
) -> Result<()> {
let conn = self.conn.lock().unwrap();
let output_str = output.as_ref().map(|v| v.to_string());
let now = now_ms();
let started_at: Option<i64> = if status == "running" { Some(now) } else { None };
let completed_at: Option<i64> = if status == "completed" || status == "failed" {
Some(now)
} else {
None
};
let changed = conn.execute(
"UPDATE _adb_workflow_steps
SET status = ?2,
output = COALESCE(?3, output),
error = COALESCE(?4, error),
started_at = COALESCE(?5, started_at),
completed_at = COALESCE(?6, completed_at)
WHERE id = ?1",
params![step_id, status, output_str, error, started_at, completed_at],
)?;
if changed == 0 {
return Err(AgentDbError::InvalidArgument(format!(
"step not found: {step_id}"
)));
}
Ok(())
}
pub fn complete_workflow(&self, id: &str, output: Option<Value>) -> Result<()> {
let conn = self.conn.lock().unwrap();
let output_str = output.as_ref().map(|v| v.to_string());
let now = now_ms();
let changed = conn.execute(
"UPDATE _adb_workflows
SET status = 'completed', output = ?2, updated_at = ?3
WHERE id = ?1",
params![id, output_str, now],
)?;
if changed == 0 {
return Err(AgentDbError::InvalidArgument(format!(
"workflow not found: {id}"
)));
}
Ok(())
}
pub fn get_workflow(&self, id: &str) -> Result<Workflow> {
let workflow = {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT id, name, status, input, output, metadata, created_at, updated_at
FROM _adb_workflows
WHERE id = ?1",
params![id],
parse_workflow_row,
)
.map_err(|_| AgentDbError::InvalidArgument(format!("workflow not found: {id}")))?
};
let steps = self.steps_for_workflow(id)?;
Ok(Workflow { steps, ..workflow })
}
pub fn list_workflows(&self, status_filter: Option<&str>) -> Result<Vec<Workflow>> {
let conn = self.conn.lock().unwrap();
let workflows: Vec<Workflow> = match status_filter {
Some(s) => {
let mut stmt = conn.prepare(
"SELECT id, name, status, input, output, metadata, created_at, updated_at
FROM _adb_workflows
WHERE status = ?1
ORDER BY created_at DESC",
)?;
let rows = stmt.query_map(params![s], parse_workflow_row)?;
rows.map(|r| r.map_err(AgentDbError::Sqlite))
.collect::<Result<Vec<_>>>()?
}
None => {
let mut stmt = conn.prepare(
"SELECT id, name, status, input, output, metadata, created_at, updated_at
FROM _adb_workflows
ORDER BY created_at DESC",
)?;
let rows = stmt.query_map([], parse_workflow_row)?;
rows.map(|r| r.map_err(AgentDbError::Sqlite))
.collect::<Result<Vec<_>>>()?
}
};
Ok(workflows)
}
fn steps_for_workflow(&self, workflow_id: &str) -> Result<Vec<WorkflowStep>> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, workflow_id, step_index, name, status,
input, output, error, started_at, completed_at
FROM _adb_workflow_steps
WHERE workflow_id = ?1
ORDER BY step_index ASC",
)?;
let rows = stmt.query_map(params![workflow_id], parse_step_row)?;
rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
}
}
fn parse_workflow_row(row: &rusqlite::Row) -> rusqlite::Result<Workflow> {
let input_str: Option<String> = row.get(3)?;
let output_str: Option<String> = row.get(4)?;
let meta_str: Option<String> = row.get(5)?;
Ok(Workflow {
id: row.get(0)?,
name: row.get(1)?,
status: row.get(2)?,
input: input_str
.as_deref()
.and_then(|s| serde_json::from_str(s).ok()),
output: output_str
.as_deref()
.and_then(|s| serde_json::from_str(s).ok()),
metadata: meta_str
.as_deref()
.and_then(|s| serde_json::from_str(s).ok()),
created_at: row.get(6)?,
updated_at: row.get(7)?,
steps: vec![],
})
}
fn parse_step_row(row: &rusqlite::Row) -> rusqlite::Result<WorkflowStep> {
let input_str: Option<String> = row.get(5)?;
let output_str: Option<String> = row.get(6)?;
Ok(WorkflowStep {
id: row.get(0)?,
workflow_id: row.get(1)?,
step_index: row.get(2)?,
name: row.get(3)?,
status: row.get(4)?,
input: input_str
.as_deref()
.and_then(|s| serde_json::from_str(s).ok()),
output: output_str
.as_deref()
.and_then(|s| serde_json::from_str(s).ok()),
error: row.get(7)?,
started_at: row.get(8)?,
completed_at: row.get(9)?,
})
}