use std::collections::BTreeMap;
use cozo::{DataValue, DbInstance, NamedRows, ScriptMutability};
use super::graph_facts::GraphFacts;
use crate::domain::model::tag_descriptor::TagDescriptors;
use crate::domain::usecases::decision_record::DecisionRecordRepository;
use crate::domain::usecases::issue::IssueRepository;
pub struct CozoAdapter {
db: DbInstance,
}
impl CozoAdapter {
pub fn from_workspace(
issue_repo: &dyn IssueRepository,
dr_repos: &[(String, &dyn DecisionRecordRepository)],
tag_descriptors: &TagDescriptors,
) -> anyhow::Result<Self> {
Self::from_facts(GraphFacts::from_workspace(
issue_repo,
dr_repos,
tag_descriptors,
))
}
pub fn from_facts(facts: GraphFacts) -> anyhow::Result<Self> {
let db = DbInstance::new("mem", "", Default::default())
.map_err(|e| anyhow::anyhow!("cozo: {e}"))?;
let create_schema = r#"
:create issue {id: String => title: String, status: String}
:create decision {id: String => kind: String, title: String, status: String}
:create link {source: String, kind: String, target: String}
:create relates {source: String, target: String}
:create assignee {issue_id: String, name: String}
:create tag {record_id: String, key: String, value: String}
:create event {record_id: String, action: String, ts: String, ts_unix: Int, from_status: String?, to_status: String?}
:create rollup {issue_id: String => queued: Int, active: Int, stalled: Int, resolved: Int, cancelled: Int, category: String}
:create tag_rollup {issue_id: String, key: String => value: String, count: Int}
"#;
for stmt in create_schema
.split(":create")
.filter(|s| !s.trim().is_empty())
{
let stmt = format!(":create {stmt}");
db.run_script(&stmt, BTreeMap::new(), ScriptMutability::Mutable)
.map_err(|e| anyhow::anyhow!("cozo schema: {e}"))?;
}
insert_rows(
&db,
"?[id, title, status]",
":put issue {id => title, status}",
&["id", "title", "status"],
facts
.issues
.iter()
.map(|(a, b, c)| vec![string(a), string(b), string(c)])
.collect(),
)?;
insert_rows(
&db,
"?[id, kind, title, status]",
":put decision {id => kind, title, status}",
&["id", "kind", "title", "status"],
facts
.decisions
.iter()
.map(|(a, b, c, d)| vec![string(a), string(b), string(c), string(d)])
.collect(),
)?;
insert_rows(
&db,
"?[source, kind, target]",
":put link {source, kind, target}",
&["source", "kind", "target"],
facts
.links
.iter()
.map(|(a, b, c)| vec![string(a), string(b), string(c)])
.collect(),
)?;
insert_rows(
&db,
"?[source, target]",
":put relates {source, target}",
&["source", "target"],
facts
.relates
.iter()
.map(|(a, b)| vec![string(a), string(b)])
.collect(),
)?;
insert_rows(
&db,
"?[issue_id, name]",
":put assignee {issue_id, name}",
&["issue_id", "name"],
facts
.assignees
.iter()
.map(|(a, b)| vec![string(a), string(b)])
.collect(),
)?;
insert_rows(
&db,
"?[record_id, key, value]",
":put tag {record_id, key, value}",
&["record_id", "key", "value"],
facts
.tags
.iter()
.map(|(a, b, c)| vec![string(a), string(b), string(c)])
.collect(),
)?;
insert_rows(
&db,
"?[record_id, action, ts, ts_unix, from_status, to_status]",
":put event {record_id, action, ts, ts_unix, from_status, to_status}",
&[
"record_id",
"action",
"ts",
"ts_unix",
"from_status",
"to_status",
],
facts
.events
.iter()
.map(|e| {
vec![
string(&e.record_id),
string(&e.action),
string(&e.ts),
DataValue::from(e.ts_unix),
opt_string(e.from_status.as_deref()),
opt_string(e.to_status.as_deref()),
]
})
.collect(),
)?;
insert_rows(
&db,
"?[issue_id, queued, active, stalled, resolved, cancelled, category]",
":put rollup {issue_id => queued, active, stalled, resolved, cancelled, category}",
&[
"issue_id",
"queued",
"active",
"stalled",
"resolved",
"cancelled",
"category",
],
facts
.rollups
.iter()
.map(|r| {
vec![
string(&r.issue_id),
DataValue::from(r.queued as i64),
DataValue::from(r.active as i64),
DataValue::from(r.stalled as i64),
DataValue::from(r.resolved as i64),
DataValue::from(r.cancelled as i64),
string(&r.category),
]
})
.collect(),
)?;
insert_rows(
&db,
"?[issue_id, key, value, count]",
":put tag_rollup {issue_id, key => value, count}",
&["issue_id", "key", "value", "count"],
facts
.tag_rollups
.iter()
.map(|r| {
vec![
string(&r.issue_id),
string(&r.key),
string(&r.value),
DataValue::from(r.count as i64),
]
})
.collect(),
)?;
Ok(Self { db })
}
pub fn run(&self, script: &str) -> anyhow::Result<NamedRows> {
self.db
.run_script(script, BTreeMap::new(), ScriptMutability::Immutable)
.map_err(|e| anyhow::anyhow!("cozo: {e}"))
}
}
fn insert_rows(
db: &DbInstance,
head: &str,
put_clause: &str,
columns: &[&str],
rows: Vec<Vec<DataValue>>,
) -> anyhow::Result<()> {
if rows.is_empty() {
return Ok(());
}
let mut script = format!("{head} <- [");
for (i, _) in rows.iter().enumerate() {
if i > 0 {
script.push(',');
}
script.push('[');
for (j, col) in columns.iter().enumerate() {
if j > 0 {
script.push(',');
}
script.push_str(&format!("${col}{i}"));
}
script.push(']');
}
script.push_str(&format!("] {put_clause}"));
let mut params: BTreeMap<String, DataValue> = BTreeMap::new();
for (i, row) in rows.into_iter().enumerate() {
for (col, val) in columns.iter().zip(row) {
params.insert(format!("{col}{i}"), val);
}
}
db.run_script(&script, params, ScriptMutability::Mutable)
.map_err(|e| anyhow::anyhow!("cozo insert: {e}"))?;
Ok(())
}
fn string(s: &str) -> DataValue {
DataValue::Str(s.to_owned().into())
}
fn opt_string(s: Option<&str>) -> DataValue {
match s {
Some(v) => DataValue::Str(v.to_owned().into()),
None => DataValue::Null,
}
}