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, serde::Serialize, serde::Deserialize)]
pub struct Workflow {
pub id: String,
pub name: String,
pub status: String,
pub input: Option<Value>,
pub output: Option<Value>,
pub error: Option<String>,
pub metadata: Option<Value>,
pub created_at: i64,
pub updated_at: i64,
pub step_count: i64,
pub steps: Vec<WorkflowStep>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
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>,
metadata: Option<Value>,
) -> Result<()> {
let conn = self.conn.lock().unwrap();
let input_str = input.as_ref().map(|v| v.to_string());
let meta_str = metadata.as_ref().map(|v| v.to_string());
let now = now_ms();
conn.execute(
"INSERT INTO _adb_workflows
(id, name, status, input, metadata, created_at, updated_at)
VALUES (?1, ?2, 'pending', ?3, ?4, ?5, ?6)",
params![id, name, input_str, meta_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 fail_workflow(&self, id: &str, error: Option<&str>) -> Result<()> {
let conn = self.conn.lock().unwrap();
let now = now_ms();
let changed = conn.execute(
"UPDATE _adb_workflows
SET status = 'failed', error = COALESCE(?2, error), updated_at = ?3
WHERE id = ?1",
params![id, error, 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, error, 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)?;
let step_count = steps.len() as i64;
Ok(Workflow {
steps,
step_count,
..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 w.id, w.name, w.status, w.input, w.output, w.error,
w.metadata, w.created_at, w.updated_at,
COUNT(s.id) AS step_count
FROM _adb_workflows w
LEFT JOIN _adb_workflow_steps s ON s.workflow_id = w.id
WHERE w.status = ?1
GROUP BY w.id
ORDER BY w.created_at DESC",
)?;
let rows = stmt.query_map(params![s], parse_workflow_row_with_count)?;
rows.map(|r| r.map_err(AgentDbError::Sqlite))
.collect::<Result<Vec<_>>>()?
}
None => {
let mut stmt = conn.prepare(
"SELECT w.id, w.name, w.status, w.input, w.output, w.error,
w.metadata, w.created_at, w.updated_at,
COUNT(s.id) AS step_count
FROM _adb_workflows w
LEFT JOIN _adb_workflow_steps s ON s.workflow_id = w.id
GROUP BY w.id
ORDER BY w.created_at DESC",
)?;
let rows = stmt.query_map([], parse_workflow_row_with_count)?;
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(6)?;
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()),
error: row.get(5)?,
metadata: meta_str
.as_deref()
.and_then(|s| serde_json::from_str(s).ok()),
created_at: row.get(7)?,
updated_at: row.get(8)?,
step_count: 0,
steps: vec![],
})
}
fn parse_workflow_row_with_count(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(6)?;
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()),
error: row.get(5)?,
metadata: meta_str
.as_deref()
.and_then(|s| serde_json::from_str(s).ok()),
created_at: row.get(7)?,
updated_at: row.get(8)?,
step_count: row.get(9)?,
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)?,
})
}