pub const DB_FN_IDENT: &str = ":db/fn";
pub const DEFAULT_MAX_DEPTH: usize = 16;
type Res<T> = Result<T, String>;
pub fn expand<L, E>(
value: edn::Value,
max_depth: usize,
mut lookup: L,
mut eval: E,
) -> Res<edn::Value>
where
L: FnMut(&str) -> Res<Option<String>>,
E: FnMut(&str) -> Res<String>,
{
let edn::Value::Vector(items) = value else {
return Ok(value);
};
if !items.iter().any(|it| invocation_head(it).is_some()) {
return Ok(edn::Value::Vector(items));
}
let mut out = Vec::with_capacity(items.len());
for item in items {
expand_into(item, 0, max_depth.max(1), &mut lookup, &mut eval, &mut out)?;
}
Ok(edn::Value::Vector(out))
}
pub fn expand_str<L, E>(edn_tx: &str, max_depth: usize, lookup: L, eval: E) -> Res<String>
where
L: FnMut(&str) -> Res<Option<String>>,
E: FnMut(&str) -> Res<String>,
{
if !edn_tx.contains("[:") {
return Ok(edn_tx.to_string());
}
let value = edn::parse::value(edn_tx)
.map_err(|e| e.to_string())?
.without_spans();
let edn::Value::Vector(ref items) = value else {
return Ok(edn_tx.to_string());
};
if !items.iter().any(|it| invocation_head(it).is_some()) {
return Ok(edn_tx.to_string());
}
Ok(format!("{}", expand(value, max_depth, lookup, eval)?))
}
pub fn invocation_head(v: &edn::Value) -> Option<&edn::symbols::Keyword> {
let edn::Value::Vector(vec) = v else {
return None;
};
let edn::Value::Keyword(kw) = vec.first()? else {
return None;
};
let reserved = matches!(
(kw.namespace(), kw.name()),
(Some("db"), "add")
| (Some("db"), "retract")
| (Some("db"), "cas")
| (Some("db"), "retractEntity")
| (Some("db.fn"), _)
| (None, "retractEntity")
);
(!reserved && kw.namespace().is_some()).then_some(kw)
}
fn expand_into<L, E>(
item: edn::Value,
depth: usize,
max_depth: usize,
lookup: &mut L,
eval: &mut E,
out: &mut Vec<edn::Value>,
) -> Res<()>
where
L: FnMut(&str) -> Res<Option<String>>,
E: FnMut(&str) -> Res<String>,
{
let Some(kw) = invocation_head(&item) else {
out.push(item);
return Ok(());
};
let ident = format!("{kw}");
let Some(src) = lookup(&ident).map_err(|e| format!(":db.error/txfn-lookup {ident}: {e}"))?
else {
out.push(item);
return Ok(());
};
if depth >= max_depth {
return Err(format!(
":db.error/txfn-depth transaction function {ident} exceeded the tx-fn \
depth limit ({max_depth}; mentat.txfn_max_depth on PostgreSQL, \
MENTAT_TXFN_MAX_DEPTH embedded). A tx-fn that returns calls to itself \
(directly or indirectly) must terminate."
));
}
let edn::Value::Vector(call) = item else {
unreachable!("invocation_head only matches vectors")
};
let args: Vec<String> = call[1..].iter().map(|a| format!("(quote {a})")).collect();
let form = format!(
"(let [f {src} db (mentat.store/db (mentat.store/open))] (f db {}))",
args.join(" ")
);
let printed = eval(&form)
.map_err(|e| format!(":db.error/txfn-error transaction function {ident} failed: {e}"))?;
let produced = edn::parse::value(&printed)
.map_err(|e| {
format!(
":db.error/txfn-result transaction function {ident} returned \
non-EDN tx-data ({e}): {printed}"
)
})?
.without_spans();
let edn::Value::Vector(entities) = normalize(produced) else {
return Err(format!(
":db.error/txfn-result transaction function {ident} must return a vector \
of tx-data (e.g. [[:db/add e a v]]), got {printed}"
));
};
for e in entities {
expand_into(e, depth + 1, max_depth, lookup, eval, out)?;
}
Ok(())
}
fn normalize(v: edn::Value) -> edn::Value {
match v {
edn::Value::List(l) => edn::Value::Vector(l.into_iter().map(normalize).collect()),
edn::Value::Vector(vs) => edn::Value::Vector(vs.into_iter().map(normalize).collect()),
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lookup_none(_: &str) -> Res<Option<String>> {
Ok(None)
}
#[test]
fn untouched_without_invocations() {
let s = "[{:a/b 1} [:db/add 1 :a/b 2]]";
let out = expand_str(s, 4, lookup_none, |_| unreachable!("no eval")).unwrap();
assert_eq!(out, s);
}
#[test]
fn expands_recursively_and_bounds_depth() {
let lookup = |id: &str| -> Res<Option<String>> {
Ok(match id {
":t/outer" => Some("OUTER".into()),
":t/inner" => Some("INNER".into()),
":t/loop" => Some("LOOP".into()),
_ => None,
})
};
let eval = |form: &str| -> Res<String> {
Ok(if form.contains("OUTER") {
"[[:t/inner] {:x/y 1}]".into()
} else if form.contains("INNER") {
"([:db/add 1 :x/y 2])".into() } else {
"[[:t/loop]]".into()
})
};
let out = expand_str("[[:t/outer] [:t/none 1]]", 4, lookup, eval).unwrap();
let parsed = |s: &str| edn::parse::value(s).unwrap().without_spans();
assert_eq!(
parsed(&out),
parsed("[[:db/add 1 :x/y 2] {:x/y 1} [:t/none 1]]")
);
let err = expand_str("[[:t/loop]]", 3, lookup, eval).unwrap_err();
assert!(err.contains("txfn-depth"), "{err}");
}
#[test]
fn rejects_non_vector_results() {
let err =
expand_str("[[:t/f]]", 4, |_| Ok(Some("F".into())), |_| Ok("42".into())).unwrap_err();
assert!(err.contains("txfn-result"), "{err}");
}
}