use cozo::{DataValue, NamedRows, Num};
use crate::domain::model::query::{
QueryColumn, QueryRelation, QueryResult, QuerySchema, QueryScript, QueryValue,
};
use crate::domain::usecases::query::QueryRunner;
use super::adapter::CozoAdapter;
impl QueryRunner for CozoAdapter {
fn run(&self, script: &QueryScript) -> anyhow::Result<QueryResult> {
let rows = CozoAdapter::run(self, script.as_str())?;
Ok(into_query_result(rows))
}
fn schema(&self) -> anyhow::Result<QuerySchema> {
let relations = CozoAdapter::run(self, "::relations")?;
let name_idx = relations
.headers
.iter()
.position(|h| h == "name")
.unwrap_or(0);
let mut out: Vec<QueryRelation> = Vec::new();
for row in &relations.rows {
let Some(DataValue::Str(name)) = row.get(name_idx) else {
continue;
};
let name = name.to_string();
let columns = CozoAdapter::run(self, &format!("::columns {name}"))?;
out.push(QueryRelation {
columns: parse_columns(&columns),
name,
});
}
Ok(QuerySchema::new(out))
}
}
fn parse_columns(rows: &NamedRows) -> Vec<QueryColumn> {
let col_idx = rows.headers.iter().position(|h| h == "column");
let type_idx = rows.headers.iter().position(|h| h == "type");
let key_idx = rows.headers.iter().position(|h| h == "is_key");
rows.rows
.iter()
.filter_map(|c| {
let name = match c.get(col_idx?) {
Some(DataValue::Str(s)) => s.to_string(),
_ => return None,
};
let type_ = match c.get(type_idx?) {
Some(DataValue::Str(s)) => s.to_string(),
_ => return None,
};
let is_key = matches!(c.get(key_idx?), Some(DataValue::Bool(true)));
Some(QueryColumn {
name,
type_,
is_key,
})
})
.collect()
}
fn into_query_result(rows: NamedRows) -> QueryResult {
let mapped: Vec<Vec<QueryValue>> = rows
.rows
.into_iter()
.map(|row| row.iter().map(value_from_cozo).collect())
.collect();
QueryResult::new(rows.headers, mapped)
}
fn value_from_cozo(v: &DataValue) -> QueryValue {
match v {
DataValue::Null => QueryValue::Null,
DataValue::Bool(b) => QueryValue::Bool(*b),
DataValue::Str(s) => QueryValue::Str(s.to_string()),
DataValue::Num(Num::Int(i)) => QueryValue::Int(*i),
DataValue::Num(Num::Float(f)) => QueryValue::Float(*f),
other => QueryValue::Other(format!("{other:?}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::infra::driven::cozo::graph_facts::GraphFacts;
fn runner() -> CozoAdapter {
CozoAdapter::from_facts(GraphFacts::default()).expect("cozo init")
}
fn run(script: &str) -> QueryResult {
let a = runner();
let r: &dyn QueryRunner = &a;
r.run(&QueryScript::new(script)).unwrap()
}
#[test]
fn primitive_scalars_map_to_their_neutral_variants() {
let r = run("?[n] := n = 42");
assert_eq!(r.headers, vec!["n".to_owned()]);
assert_eq!(r.rows, vec![vec![QueryValue::Int(42)]]);
}
#[test]
fn string_values_round_trip_into_str() {
let r = run("?[s] := s = 'hello'");
assert_eq!(r.rows, vec![vec![QueryValue::Str("hello".to_owned())]]);
}
#[test]
fn empty_result_keeps_headers_and_no_rows() {
let r = run("?[n] := n = 1, n = 2");
assert_eq!(r.headers, vec!["n".to_owned()]);
assert!(r.is_empty());
}
#[test]
fn schema_exposes_workspace_relations_with_their_columns() {
let a = runner();
let r: &dyn QueryRunner = &a;
let s = r.schema().unwrap();
let issue = s
.relations
.iter()
.find(|r| r.name == "issue")
.expect("issue relation present");
let id = issue
.columns
.iter()
.find(|c| c.name == "id")
.expect("id column present");
assert!(id.is_key);
assert_eq!(id.type_, "String");
}
}