use std::rc::Rc;
use super::*;
use crate::index::GraphIndex;
use crate::row::{Ctx, Row};
impl FExpr {
pub(super) fn value(&self, ctx: &Ctx, b: &Row) -> Option<Rc<str>> {
match self {
FExpr::Var(v) => {
let slot = ctx.slots.slot(v)?;
b[slot].as_ref().and_then(|val| ctx.resolver.str_of(val))
}
FExpr::Const(c) => Some(Rc::from(c.as_str())),
FExpr::Arith(op, l, r) => {
let a = arith_number(&l.value(ctx, b)?)?;
let c = arith_number(&r.value(ctx, b)?)?;
let v = match op {
ArithOp::Add => a + c,
ArithOp::Sub => a - c,
ArithOp::Mul => a * c,
ArithOp::Div if c == 0.0 => return None,
ArithOp::Div => a / c,
};
Some(Rc::from(fmt_num_typed(v)))
}
FExpr::Func(f, args) => func_value(*f, args, ctx, b),
FExpr::Coalesce(args) => args.iter().find_map(|e| e.value(ctx, b)),
FExpr::If(c, t, e) => match c.ebv_opt(ctx, b) {
Some(true) => t.value(ctx, b),
Some(false) => e.value(ctx, b),
None => None,
},
FExpr::In(..)
| FExpr::SameTerm(..)
| FExpr::Compare(..)
| FExpr::And(..)
| FExpr::Or(..)
| FExpr::Not(..)
| FExpr::Bound(..) => Some(Rc::from(bool_literal(self.ebv(ctx, b)))),
_ => None,
}
}
fn ebv_opt(&self, ctx: &Ctx, b: &Row) -> Option<bool> {
term_ebv(&self.value(ctx, b)?)
}
fn ebv(&self, ctx: &Ctx, b: &Row) -> bool {
match self {
FExpr::Bound(v) => ctx.slots.slot(v).is_some_and(|slot| b[slot].is_some()),
FExpr::Not(e) => !e.ebv(ctx, b),
FExpr::And(l, r) => l.ebv(ctx, b) && r.ebv(ctx, b),
FExpr::Or(l, r) => l.ebv(ctx, b) || r.ebv(ctx, b),
FExpr::Compare(op, l, r) => match (l.value(ctx, b), r.value(ctx, b)) {
(Some(a), Some(c)) => compare(*op, &a, &c),
_ => false,
},
FExpr::In(e, list) => match e.value(ctx, b) {
Some(v) => list
.iter()
.any(|x| x.value(ctx, b).is_some_and(|m| compare(Op::Eq, &v, &m))),
None => false,
},
FExpr::SameTerm(l, r) => match (l.value(ctx, b), r.value(ctx, b)) {
(Some(a), Some(c)) => a == c,
_ => false,
},
FExpr::If(c, t, e) => {
if c.ebv(ctx, b) {
t.ebv(ctx, b)
} else {
e.ebv(ctx, b)
}
}
FExpr::Func(f, args) => func_bool(*f, args, ctx, b),
_ => self.ebv_opt(ctx, b).unwrap_or(false),
}
}
pub(super) fn boolean(
&self,
ctx: &Ctx,
index: &GraphIndex,
b: &Row,
cache: &mut ExistsCache,
) -> bool {
match self {
FExpr::Not(e) => !e.boolean(ctx, index, b, cache),
FExpr::And(l, r) => l.boolean(ctx, index, b, cache) && r.boolean(ctx, index, b, cache),
FExpr::Or(l, r) => l.boolean(ctx, index, b, cache) || r.boolean(ctx, index, b, cache),
FExpr::If(c, t, e) => {
if c.boolean(ctx, index, b, cache) {
t.boolean(ctx, index, b, cache)
} else {
e.boolean(ctx, index, b, cache)
}
}
FExpr::Exists(plan) => {
let key = plan.as_ref() as *const Plan;
let entry = cache.entry(key).or_insert_with(|| ExistsEntry {
sols: eval_plan_in(ctx, index, None, plan),
probe: None,
});
if entry.sols.is_empty() {
return false;
}
if entry.probe.is_none() {
entry.probe = Some(build_exists_probe(b, &entry.sols));
}
exists_matches(b, entry)
}
_ => self.ebv(ctx, b),
}
}
}
fn func_value(f: Builtin, args: &[FExpr], ctx: &Ctx, b: &Row) -> Option<Rc<str>> {
let a0 = || args.first().and_then(|e| e.value(ctx, b));
let num = |x: f64| -> Option<Rc<str>> { Some(Rc::from(fmt_num_typed(x))) };
let lex = |t: &str| crate::terms::lexical(t).into_owned();
let s = |x: String| -> Option<Rc<str>> { Some(Rc::from(make_literal(&x, None, None))) };
let sl = |x: String, lang: Option<&str>| -> Option<Rc<str>> {
Some(Rc::from(make_literal(&x, lang, None)))
};
match f {
Builtin::Str => s(lex(&a0()?)),
Builtin::Subject => {
crate::ingest::quoted_triple_parts(&a0()?).map(|(x, _, _)| Rc::from(x.as_str()))
}
Builtin::Predicate => {
crate::ingest::quoted_triple_parts(&a0()?).map(|(_, x, _)| Rc::from(x.as_str()))
}
Builtin::Object => {
crate::ingest::quoted_triple_parts(&a0()?).map(|(_, _, x)| Rc::from(x.as_str()))
}
Builtin::TripleTerm => {
let sub = a0()?;
let pred = args.get(1).and_then(|e| e.value(ctx, b))?;
let obj = args.get(2).and_then(|e| e.value(ctx, b))?;
Some(Rc::from(format!("<<{sub} {pred} {obj}>>")))
}
Builtin::StrLen => {
let a = a0()?;
string_arg(&a)?;
num(lex(&a).chars().count() as f64)
}
Builtin::UCase => {
let a = a0()?;
let lang = string_arg(&a)?;
sl(lex(&a).to_uppercase(), lang.as_deref())
}
Builtin::LCase => {
let a = a0()?;
let lang = string_arg(&a)?;
sl(lex(&a).to_lowercase(), lang.as_deref())
}
Builtin::Abs => num(as_number(&a0()?)?.abs()),
Builtin::Ceil => num(as_number(&a0()?)?.ceil()),
Builtin::Floor => num(as_number(&a0()?)?.floor()),
Builtin::Round => num((as_number(&a0()?)? + 0.5).floor()),
Builtin::Concat => {
let mut out = String::new();
let mut lang: Option<Option<String>> = None;
for a in args {
let v = a.value(ctx, b)?;
let this = string_arg(&v)?;
out.push_str(&lex(&v));
lang = Some(match lang {
None => this,
Some(prev) if prev == this => prev,
Some(_) => None,
});
}
sl(out, lang.flatten().as_deref())
}
Builtin::SubStr => {
let a = a0()?;
let lang = string_arg(&a)?;
let chars: Vec<char> = lex(&a).chars().collect();
let start = as_number(&args.get(1)?.value(ctx, b)?)?.max(1.0) as usize - 1;
let it = chars.iter().skip(start);
let out: String = match args.get(2) {
Some(lenarg) => it
.take(as_number(&lenarg.value(ctx, b)?)?.max(0.0) as usize)
.collect(),
None => it.collect(),
};
sl(out, lang.as_deref())
}
Builtin::StrBefore => {
let a = a0()?;
let lang = before_after_lang(&a, &args.get(1)?.value(ctx, b)?)?;
let (t, needle) = (lex(&a), lex(&args.get(1)?.value(ctx, b)?));
match (needle.is_empty(), t.find(&needle)) {
(true, _) => sl(String::new(), lang.as_deref()),
(false, Some(i)) => sl(t[..i].to_string(), lang.as_deref()),
(false, None) => s(String::new()),
}
}
Builtin::StrAfter => {
let a = a0()?;
let lang = before_after_lang(&a, &args.get(1)?.value(ctx, b)?)?;
let (t, needle) = (lex(&a), lex(&args.get(1)?.value(ctx, b)?));
match (needle.is_empty(), t.find(&needle)) {
(true, _) => sl(t, lang.as_deref()),
(false, Some(i)) => sl(t[i + needle.len()..].to_string(), lang.as_deref()),
(false, None) => s(String::new()),
}
}
Builtin::StrDt => {
let v = simple_string(&a0()?)?;
let dt = iri_content(&args.get(1)?.value(ctx, b)?)?.to_string();
Some(Rc::from(make_literal(&v, None, Some(&dt))))
}
Builtin::StrLang => {
let v = simple_string(&a0()?)?;
let lang = lex(&args.get(1)?.value(ctx, b)?);
Some(Rc::from(make_literal(&v, Some(&lang), None)))
}
Builtin::Iri => {
let a = a0()?;
if is_iri(&a) {
Some(a)
} else {
Some(Rc::from(format!("<{}>", lex(&a))))
}
}
Builtin::EncodeForUri => {
let a = a0()?;
string_arg(&a)?;
s(encode_for_uri(&lex(&a)))
}
Builtin::Replace => {
let a = a0()?;
let lang = string_arg(&a)?; let text = lex(&a);
let pat = lex(&args.get(1)?.value(ctx, b)?);
let rep = lex(&args.get(2)?.value(ctx, b)?);
let flags = match args.get(3) {
Some(e) => lex(&e.value(ctx, b)?),
None => String::new(),
};
let out = ctx.resolver.regex_replace(&pat, &flags, &text, &rep)?;
sl(out, lang.as_deref())
}
Builtin::Md5 | Builtin::Sha1 | Builtin::Sha256 | Builtin::Sha384 | Builtin::Sha512 => {
let a = a0()?;
string_arg(&a)?;
s(hash_hex(f, &lex(&a)))
}
Builtin::Year => num(parse_datetime(&lex(&a0()?))?.0 as f64),
Builtin::Month => num(parse_datetime(&lex(&a0()?))?.1 as f64),
Builtin::Day => num(parse_datetime(&lex(&a0()?))?.2 as f64),
Builtin::Hours => num(parse_datetime(&lex(&a0()?))?.3 as f64),
Builtin::Minutes => num(parse_datetime(&lex(&a0()?))?.4 as f64),
Builtin::Seconds => num(parse_datetime(&lex(&a0()?))?.5),
Builtin::Tz => s(parse_datetime(&lex(&a0()?))?.6),
Builtin::Timezone => {
let dur = tz_to_duration(&parse_datetime(&lex(&a0()?))?.6)?;
Some(Rc::from(make_literal(
&dur,
None,
Some("http://www.w3.org/2001/XMLSchema#dayTimeDuration"),
)))
}
Builtin::CastInteger
| Builtin::CastDecimal
| Builtin::CastFloat
| Builtin::CastDouble
| Builtin::CastBoolean
| Builtin::CastString => cast_to(&a0()?, f),
Builtin::Rand => {
let r = (random_u64() >> 11) as f64 / (1u64 << 53) as f64;
Some(Rc::from(make_literal(
&format!("{r}"),
None,
Some("http://www.w3.org/2001/XMLSchema#double"),
)))
}
Builtin::Uuid => Some(Rc::from(format!("<urn:uuid:{}>", uuid_v4()))),
Builtin::StrUuid => s(uuid_v4()),
Builtin::BNode => match args.first().and_then(|e| e.value(ctx, b)) {
Some(v) => {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
lex(&v).hash(&mut h);
Some(Rc::from(format!("_:b{:016x}", h.finish())))
}
None => Some(Rc::from(format!("_:b{:016x}", random_u64()))),
},
Builtin::Datatype => datatype_iri(&a0()?).map(|iri| Rc::from(format!("<{iri}>"))),
Builtin::Lang => lang_of(&a0()?).map(|l| Rc::from(format!("\"{l}\""))),
Builtin::GeoSfContains
| Builtin::GeoSfWithin
| Builtin::GeoSfIntersects
| Builtin::GeoSfDisjoint
| Builtin::GeoSfEquals => {
let r = geo_relation(f, &a0()?, &args.get(1)?.value(ctx, b)?)?;
Some(Rc::from(bool_literal(r)))
}
Builtin::GeoDistance => {
let unit = geo_unit(&args.get(2)?.value(ctx, b)?)?;
let d = crate::geo::distance(
&wkt_arg(&a0()?)?,
&wkt_arg(&args.get(1)?.value(ctx, b)?)?,
unit,
)?;
Some(Rc::from(make_literal(
&fmt_plain(d),
None,
Some(XSD_DOUBLE),
)))
}
Builtin::GeoEnvelope => {
let wkt = crate::geo::envelope_wkt(&wkt_arg(&a0()?)?)?;
Some(Rc::from(make_literal(
&wkt,
None,
Some(crate::geo::GEO_WKT),
)))
}
Builtin::Geo3Contains | Builtin::Geo3Within | Builtin::Geo3Adjacent => {
let gap = args.get(2).and_then(|e| e.value(ctx, b));
let r = geo3_relation(f, &a0()?, &args.get(1)?.value(ctx, b)?, gap)?;
Some(Rc::from(bool_literal(r)))
}
Builtin::Geo3Distance => {
let d =
crate::geo3::distance(&geo3_arg(&a0()?)?, &geo3_arg(&args.get(1)?.value(ctx, b)?)?);
Some(Rc::from(make_literal(
&fmt_plain(d),
None,
Some(XSD_DOUBLE),
)))
}
_ => None,
}
}
const XSD_DOUBLE: &str = "http://www.w3.org/2001/XMLSchema#double";
fn wkt_arg(token: &str) -> Option<crate::geo::Geometry> {
if !token.starts_with('"') {
return None;
}
match datatype_iri(token).as_deref() {
Some(crate::geo::GEO_WKT) | Some(XSD_STRING) | None => {}
Some(_) => return None,
}
crate::geo::parse_wkt(&crate::terms::lexical(token))
}
fn geo_relation(f: Builtin, t1: &str, t2: &str) -> Option<bool> {
let rel = match f {
Builtin::GeoSfContains => crate::geo::Rel::Contains,
Builtin::GeoSfWithin => crate::geo::Rel::Within,
Builtin::GeoSfIntersects => crate::geo::Rel::Intersects,
Builtin::GeoSfDisjoint => crate::geo::Rel::Disjoint,
Builtin::GeoSfEquals => crate::geo::Rel::Equals,
_ => return None,
};
crate::geo::relate(rel, &wkt_arg(t1)?, &wkt_arg(t2)?)
}
fn geo3_arg(token: &str) -> Option<crate::geo3::Aabb> {
if !token.starts_with('"') {
return None;
}
match datatype_iri(token).as_deref() {
Some(crate::geo3::WKT3)
| Some(crate::geo3::BOX3D)
| Some(crate::geo::GEO_WKT)
| Some(XSD_STRING)
| None => {}
Some(_) => return None,
}
crate::geo3::parse(&crate::terms::lexical(token))
}
fn geo3_relation(f: Builtin, t1: &str, t2: &str, gap: Option<Rc<str>>) -> Option<bool> {
let rel = match f {
Builtin::Geo3Contains => crate::geo3::Rel3::Contains,
Builtin::Geo3Within => crate::geo3::Rel3::Within,
Builtin::Geo3Adjacent => crate::geo3::Rel3::Adjacent,
_ => return None,
};
let g = gap.and_then(|t| as_number(&t)).unwrap_or(0.0);
Some(crate::geo3::relate(rel, &geo3_arg(t1)?, &geo3_arg(t2)?, g))
}
fn geo_unit(token: &str) -> Option<crate::geo::Unit> {
match iri_content(token)? {
crate::geo::UOM_METRE => Some(crate::geo::Unit::Metre),
crate::geo::UOM_DEGREE => Some(crate::geo::Unit::Degree),
_ => None,
}
}
use crate::terms::{
iri_content, is_iri, lang_tag as lang_of, literal_datatype as datatype_iri, make_literal,
};
const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
fn string_arg(token: &str) -> Option<Option<String>> {
if !is_iri(token) && token.starts_with('"') {
if let Some(l) = lang_of(token).filter(|l| !l.is_empty()) {
return Some(Some(l));
}
if datatype_iri(token).as_deref() == Some(XSD_STRING) {
return Some(None);
}
}
None
}
fn simple_string(token: &str) -> Option<String> {
match string_arg(token) {
Some(None) => Some(crate::terms::lexical(token).into_owned()),
_ => None,
}
}
fn before_after_lang(arg1: &str, arg2: &str) -> Option<Option<String>> {
let l1 = string_arg(arg1)?;
let l2 = string_arg(arg2)?;
match l2 {
None => Some(l1),
Some(_) if l2 == l1 => Some(l1),
Some(_) => None,
}
}
#[allow(clippy::type_complexity)]
fn parse_datetime(lex: &str) -> Option<(i64, u32, u32, u32, u32, f64, String)> {
let (neg, rest) = match lex.strip_prefix('-') {
Some(r) => (true, r),
None => (false, lex),
};
let (date, timetz) = rest.split_once('T')?;
let mut dp = date.split('-');
let year: i64 = dp.next()?.parse().ok()?;
let month: u32 = dp.next()?.parse().ok()?;
let day: u32 = dp.next()?.parse().ok()?;
let (time, tz) = if let Some(i) = timetz.find('Z') {
(&timetz[..i], &timetz[i..])
} else if let Some(i) = timetz.rfind(['+', '-']) {
(&timetz[..i], &timetz[i..])
} else {
(timetz, "")
};
let mut tp = time.split(':');
let hour: u32 = tp.next()?.parse().ok()?;
let minute: u32 = tp.next()?.parse().ok()?;
let second: f64 = tp.next()?.parse().ok()?;
let year = if neg { -year } else { year };
Some((year, month, day, hour, minute, second, tz.to_string()))
}
fn tz_to_duration(tz: &str) -> Option<String> {
if tz.is_empty() {
return None;
}
if tz == "Z" {
return Some("PT0S".to_string());
}
let sign = tz.starts_with('-');
let h: u32 = tz.get(1..3)?.parse().ok()?;
let m: u32 = tz.get(4..6)?.parse().ok()?;
if h == 0 && m == 0 {
return Some("PT0S".to_string());
}
let mut out = String::new();
if sign {
out.push('-');
}
out.push_str("PT");
if h > 0 {
out.push_str(&format!("{h}H"));
}
if m > 0 {
out.push_str(&format!("{m}M"));
}
Some(out)
}
fn term_ebv(token: &str) -> Option<bool> {
match datatype_iri(token).as_deref() {
Some("http://www.w3.org/2001/XMLSchema#boolean") => {
match crate::terms::lexical(token).as_ref() {
"true" | "1" => Some(true),
"false" | "0" => Some(false),
_ => None,
}
}
Some(dt) if is_numeric_dt(Some(dt)) => as_number(token).map(|n| n != 0.0 && !n.is_nan()),
Some(XSD_STRING) => Some(!crate::terms::lexical(token).is_empty()),
_ => None,
}
}
fn random_u64() -> u64 {
let mut buf = [0u8; 8];
let _ = getrandom::getrandom(&mut buf);
u64::from_le_bytes(buf)
}
fn uuid_v4() -> String {
let mut b = [0u8; 16];
let _ = getrandom::getrandom(&mut b);
b[6] = (b[6] & 0x0f) | 0x40; b[8] = (b[8] & 0x3f) | 0x80; let h: String = b.iter().map(|x| format!("{x:02x}")).collect();
format!(
"{}-{}-{}-{}-{}",
&h[0..8],
&h[8..12],
&h[12..16],
&h[16..20],
&h[20..32]
)
}
fn arith_number(token: &str) -> Option<f64> {
match datatype_iri(token) {
Some(dt) => is_numeric_dt(Some(&dt)).then(|| as_number(token)).flatten(),
None => as_number(token),
}
}
fn is_numeric_dt(dt: Option<&str>) -> bool {
matches!(
dt,
Some(
"http://www.w3.org/2001/XMLSchema#integer"
| "http://www.w3.org/2001/XMLSchema#decimal"
| "http://www.w3.org/2001/XMLSchema#float"
| "http://www.w3.org/2001/XMLSchema#double"
)
)
}
fn is_int_lexical(s: &str) -> bool {
let body = s.strip_prefix(['+', '-']).unwrap_or(s);
!body.is_empty() && body.bytes().all(|b| b.is_ascii_digit())
}
fn is_decimal_lexical(s: &str) -> bool {
let body = s.strip_prefix(['+', '-']).unwrap_or(s);
let (int, frac) = body.split_once('.').unwrap_or((body, ""));
let digits = |x: &str| x.bytes().all(|b| b.is_ascii_digit());
(!int.is_empty() || !frac.is_empty()) && digits(int) && digits(frac)
}
fn parse_xsd_double(s: &str) -> Option<f64> {
match s {
"INF" | "+INF" => Some(f64::INFINITY),
"-INF" => Some(f64::NEG_INFINITY),
"NaN" => Some(f64::NAN),
_ if s.eq_ignore_ascii_case("inf")
|| s.eq_ignore_ascii_case("nan")
|| s.eq_ignore_ascii_case("infinity") =>
{
None
}
_ => s.parse::<f64>().ok(),
}
}
fn cast_to(token: &str, target: Builtin) -> Option<Rc<str>> {
const NS: &str = "http://www.w3.org/2001/XMLSchema#";
let dt = datatype_iri(token);
let is_string_src = dt.as_deref() == Some(XSD_STRING);
let is_numeric_src = is_numeric_dt(dt.as_deref());
let is_bool_src = dt.as_deref() == Some("http://www.w3.org/2001/XMLSchema#boolean");
let raw = crate::terms::lexical(token);
let src = raw.trim();
let typed = |lex: String, ty: &str| {
Some(Rc::from(make_literal(
&lex,
None,
Some(&format!("{NS}{ty}")),
)))
};
let bool_true = src == "true" || src == "1";
let numeric_src = || -> Option<f64> {
if is_numeric_src {
as_number(token)
} else if is_bool_src {
Some(if bool_true { 1.0 } else { 0.0 })
} else {
None
}
};
match target {
Builtin::CastString if is_numeric_src => typed(fmt_plain(as_number(token)?), "string"),
Builtin::CastString if is_bool_src => {
typed(if bool_true { "true" } else { "false" }.into(), "string")
}
Builtin::CastString if dt.is_some() || is_iri(token) => typed(raw.into_owned(), "string"),
Builtin::CastBoolean if is_bool_src => {
typed(if bool_true { "true" } else { "false" }.into(), "boolean")
}
Builtin::CastBoolean if is_string_src => match src {
"true" | "1" => typed("true".into(), "boolean"),
"false" | "0" => typed("false".into(), "boolean"),
_ => None,
},
Builtin::CastBoolean if is_numeric_src => typed(
if as_number(token)? != 0.0 {
"true"
} else {
"false"
}
.into(),
"boolean",
),
Builtin::CastInteger => {
let n: i64 = if is_string_src {
is_int_lexical(src)
.then(|| src.trim_start_matches('+').parse().ok())
.flatten()?
} else {
numeric_src()?.trunc() as i64
};
typed(n.to_string(), "integer")
}
Builtin::CastDecimal => {
let v: f64 = if is_string_src {
is_decimal_lexical(src)
.then(|| src.parse().ok())
.flatten()?
} else {
numeric_src()?
};
typed(fmt_plain(v), "decimal")
}
Builtin::CastFloat | Builtin::CastDouble => {
let v: f64 = if is_string_src {
parse_xsd_double(src)?
} else {
numeric_src()?
};
let ty = if matches!(target, Builtin::CastFloat) {
"float"
} else {
"double"
};
typed(fmt_plain(v), ty)
}
_ => None,
}
}
fn fmt_plain(v: f64) -> String {
if v == v.trunc() && v.is_finite() {
format!("{}", v as i64)
} else {
format!("{v}")
}
}
fn bool_literal(v: bool) -> String {
let lex = if v { "true" } else { "false" };
format!("\"{lex}\"^^<http://www.w3.org/2001/XMLSchema#boolean>")
}
fn encode_for_uri(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for &byte in s.as_bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
out.push(byte as char)
}
_ => out.push_str(&format!("%{byte:02X}")),
}
}
out
}
fn hash_hex(f: Builtin, s: &str) -> String {
use sha2::Digest;
let bytes = s.as_bytes();
match f {
Builtin::Md5 => format!("{:x}", md5::Md5::digest(bytes)),
Builtin::Sha1 => format!("{:x}", sha1::Sha1::digest(bytes)),
Builtin::Sha256 => format!("{:x}", sha2::Sha256::digest(bytes)),
Builtin::Sha384 => format!("{:x}", sha2::Sha384::digest(bytes)),
Builtin::Sha512 => format!("{:x}", sha2::Sha512::digest(bytes)),
_ => unreachable!("hash_hex called with non-hash builtin"),
}
}
fn func_bool(f: Builtin, args: &[FExpr], ctx: &Ctx, b: &Row) -> bool {
let val = |i: usize| args.get(i).and_then(|e| e.value(ctx, b));
let two = |g: fn(&str, &str) -> bool| match (val(0), val(1)) {
(Some(a), Some(c)) if string_arg(&a).is_some() && string_arg(&c).is_some() => {
g(&lexical(&a), &lexical(&c))
}
_ => false,
};
match f {
Builtin::IsIri => val(0).is_some_and(|t| t.starts_with('<')),
Builtin::IsBlank => val(0).is_some_and(|t| t.starts_with("_:")),
Builtin::IsLiteral => val(0).is_some_and(|t| t.starts_with('"')),
Builtin::IsNumeric => val(0).and_then(|t| as_number(&t)).is_some(),
Builtin::IsTriple => val(0).is_some_and(|t| crate::terms::is_quoted_triple(&t)),
Builtin::Contains => two(|a, c| a.contains(c)),
Builtin::StrStarts => two(|a, c| a.starts_with(c)),
Builtin::StrEnds => two(|a, c| a.ends_with(c)),
Builtin::Regex => match (val(0), val(1)) {
(Some(text), Some(pat)) if string_arg(&text).is_some() => {
let flags = val(2).map(|t| lexical(&t)).unwrap_or_default();
ctx.resolver
.regex_match(&lexical(&pat), &flags, &lexical(&text))
}
_ => false,
},
Builtin::LangMatches => match (val(0), val(1)) {
(Some(tag), Some(range)) => lang_matches(&lexical(&tag), &lexical(&range)),
_ => false,
},
Builtin::GeoSfContains
| Builtin::GeoSfWithin
| Builtin::GeoSfIntersects
| Builtin::GeoSfDisjoint
| Builtin::GeoSfEquals => match (val(0), val(1)) {
(Some(a), Some(c)) => geo_relation(f, &a, &c).unwrap_or(false),
_ => false,
},
Builtin::Geo3Contains | Builtin::Geo3Within | Builtin::Geo3Adjacent => {
match (val(0), val(1)) {
(Some(a), Some(c)) => geo3_relation(f, &a, &c, val(2)).unwrap_or(false),
_ => false,
}
}
_ => false,
}
}
fn lang_matches(tag: &str, range: &str) -> bool {
if range == "*" {
return !tag.is_empty();
}
let (tag, range) = (tag.to_ascii_lowercase(), range.to_ascii_lowercase());
tag == range || tag.starts_with(&format!("{range}-"))
}
pub(super) enum SortKey {
Unbound,
Bound(Option<f64>, Rc<str>),
}
impl SortKey {
pub(super) fn of(v: Option<Rc<str>>) -> Self {
match v {
None => SortKey::Unbound,
Some(s) => SortKey::Bound(as_number(&s), s),
}
}
pub(super) fn cmp(&self, other: &SortKey) -> std::cmp::Ordering {
use std::cmp::Ordering;
match (self, other) {
(SortKey::Unbound, SortKey::Unbound) => Ordering::Equal,
(SortKey::Unbound, _) => Ordering::Less,
(_, SortKey::Unbound) => Ordering::Greater,
(SortKey::Bound(na, sa), SortKey::Bound(nb, sb)) => match (na, nb) {
(Some(x), Some(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
_ => sa.cmp(sb),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dictionary::DictionaryBuilder;
use crate::file::{write_file, Rete};
use crate::index::GraphIndexBuilder;
use crate::row::{Ctx, Slots, Val};
const XSD: &str = "http://www.w3.org/2001/XMLSchema#";
const GEO: &str = "http://www.opengis.net/ont/geosparql#";
fn lit(value: &str, datatype: &str) -> String {
format!("\"{value}\"^^<{datatype}>")
}
fn fixture() -> Rete {
let triples = [("<s>", "<p>", "\"value\"")];
let mut builder = DictionaryBuilder::new();
for (s, p, o) in triples {
builder.observe(s, p, o);
}
let dict = builder.build();
let mut index = GraphIndexBuilder::new();
for (s, p, o) in triples {
index.push(dict.encode(s, p, o).unwrap());
}
Rete::open(&write_file(&dict, &index.build(), false, &[], 0)).unwrap()
}
fn call(ctx: &Ctx, builtin: Builtin, args: &[&str]) -> Option<String> {
let args = args
.iter()
.map(|v| FExpr::Const((*v).to_string()))
.collect::<Vec<_>>();
func_value(builtin, &args, ctx, &ctx.slots.empty_row()).map(|v| v.to_string())
}
#[test]
fn expression_values_boolean_errors_and_sort_keys() {
let rete = fixture();
let mut slots = Slots::new();
let x = slots.add("x");
slots.add("unset");
let ctx = Ctx::new(&rete, slots);
let mut row = ctx.slots.empty_row();
row[x] = Some(Val::Str(Rc::from(lit("2", &format!("{XSD}integer")))));
let var = FExpr::Var("x".into());
assert!(var.value(&ctx, &row).unwrap().contains("2"));
assert_eq!(FExpr::Var("missing".into()).value(&ctx, &row), None);
for (op, expected) in [
(ArithOp::Add, 5.0),
(ArithOp::Sub, -1.0),
(ArithOp::Mul, 6.0),
(ArithOp::Div, 2.0 / 3.0),
] {
let expr = FExpr::Arith(
op,
Box::new(var.clone()),
Box::new(FExpr::Const(lit("3", &format!("{XSD}integer")))),
);
let actual = as_number(&expr.value(&ctx, &row).unwrap()).unwrap();
assert!((actual - expected).abs() < 1e-12);
}
assert_eq!(
FExpr::Arith(
ArithOp::Div,
Box::new(var.clone()),
Box::new(FExpr::Const("0".into()))
)
.value(&ctx, &row),
None
);
assert_eq!(
FExpr::Coalesce(vec![
FExpr::Var("missing".into()),
FExpr::Const("ok".into())
])
.value(&ctx, &row)
.as_deref(),
Some("ok")
);
let yes = FExpr::Const(lit("true", &format!("{XSD}boolean")));
let no = FExpr::Const(lit("false", &format!("{XSD}boolean")));
assert_eq!(
FExpr::If(
Box::new(yes.clone()),
Box::new(FExpr::Const("then".into())),
Box::new(FExpr::Const("else".into()))
)
.value(&ctx, &row)
.as_deref(),
Some("then")
);
assert_eq!(
FExpr::If(
Box::new(FExpr::Const("<iri-has-no-ebv>".into())),
Box::new(FExpr::Const("then".into())),
Box::new(FExpr::Const("else".into()))
)
.value(&ctx, &row),
None
);
assert!(FExpr::Bound("x".into()).ebv(&ctx, &row));
assert!(!FExpr::Bound("unset".into()).ebv(&ctx, &row));
assert!(FExpr::Not(Box::new(no.clone())).ebv(&ctx, &row));
assert!(FExpr::And(Box::new(yes.clone()), Box::new(yes.clone())).ebv(&ctx, &row));
assert!(FExpr::Or(Box::new(no.clone()), Box::new(yes.clone())).ebv(&ctx, &row));
assert!(FExpr::Compare(
Op::Lt,
Box::new(FExpr::Const("2".into())),
Box::new(FExpr::Const("3".into()))
)
.ebv(&ctx, &row));
assert!(FExpr::In(
Box::new(FExpr::Const("2".into())),
vec![FExpr::Const("1".into()), FExpr::Const("2".into())]
)
.ebv(&ctx, &row));
assert!(FExpr::SameTerm(
Box::new(FExpr::Const("\"x\"".into())),
Box::new(FExpr::Const("\"x\"".into()))
)
.ebv(&ctx, &row));
assert!(FExpr::If(
Box::new(no),
Box::new(FExpr::Const("0".into())),
Box::new(yes)
)
.ebv(&ctx, &row));
let unbound = SortKey::of(None);
let one = SortKey::of(Some(Rc::from("1")));
let two = SortKey::of(Some(Rc::from("2")));
let text = SortKey::of(Some(Rc::from("z")));
assert_eq!(unbound.cmp(&unbound), std::cmp::Ordering::Equal);
assert_eq!(unbound.cmp(&one), std::cmp::Ordering::Less);
assert_eq!(one.cmp(&unbound), std::cmp::Ordering::Greater);
assert_eq!(one.cmp(&two), std::cmp::Ordering::Less);
assert_eq!(text.cmp(&two), std::cmp::Ordering::Greater);
}
#[test]
fn value_builtins_cover_strings_dates_hashes_casts_and_rdf_star() {
let rete = fixture();
let ctx = Ctx::new(&rete, Slots::new());
let string = lit("Straße", &format!("{XSD}string"));
let lang = "\"Hello World\"@en";
let int = lit("-3", &format!("{XSD}integer"));
let decimal = lit("2.4", &format!("{XSD}decimal"));
for (builtin, args) in [
(Builtin::Str, vec!["<http://ex/a>"]),
(Builtin::StrLen, vec![string.as_str()]),
(Builtin::UCase, vec![lang]),
(Builtin::LCase, vec![lang]),
(Builtin::Abs, vec![int.as_str()]),
(Builtin::Ceil, vec![decimal.as_str()]),
(Builtin::Floor, vec![decimal.as_str()]),
(Builtin::Round, vec![decimal.as_str()]),
(Builtin::Concat, vec!["\"a\"@en", "\"b\"@en"]),
(Builtin::SubStr, vec![lang, "2", "3"]),
(Builtin::StrBefore, vec![lang, "\" World\""]),
(Builtin::StrAfter, vec![lang, "\"Hello \""]),
(Builtin::StrDt, vec!["\"7\"", "<http://ex/type>"]),
(Builtin::StrLang, vec!["\"hello\"", "\"de\""]),
(Builtin::Iri, vec!["\"http://ex/a\""]),
(Builtin::EncodeForUri, vec!["\"a b/ä\""]),
(
Builtin::Replace,
vec!["\"Abba\"", "\"b+\"", "\"X\"", "\"i\""],
),
(Builtin::Md5, vec!["\"abc\""]),
(Builtin::Sha1, vec!["\"abc\""]),
(Builtin::Sha256, vec!["\"abc\""]),
(Builtin::Sha384, vec!["\"abc\""]),
(Builtin::Sha512, vec!["\"abc\""]),
] {
assert!(call(&ctx, builtin, &args).is_some(), "{builtin:?}");
}
assert!(call(&ctx, Builtin::StrBefore, &[lang, "\"missing\""])
.unwrap()
.starts_with("\"\""));
assert!(call(&ctx, Builtin::StrAfter, &[lang, "\"\""])
.unwrap()
.contains("Hello World"));
assert_eq!(call(&ctx, Builtin::StrLen, &["<iri>"]), None);
assert_eq!(call(&ctx, Builtin::Concat, &["\"ok\"", "<iri>"]), None);
assert_eq!(
call(&ctx, Builtin::Replace, &["\"x\"", "\"[\"", "\"y\""]),
None
);
let dt = lit("-2024-07-14T12:34:56.5-08:30", &format!("{XSD}dateTime"));
for builtin in [
Builtin::Year,
Builtin::Month,
Builtin::Day,
Builtin::Hours,
Builtin::Minutes,
Builtin::Seconds,
Builtin::Timezone,
Builtin::Tz,
] {
assert!(call(&ctx, builtin, &[&dt]).is_some(), "{builtin:?}");
}
assert!(call(
&ctx,
Builtin::Timezone,
&[&lit("2024-01-01T00:00:00Z", &format!("{XSD}dateTime"))]
)
.is_some());
assert_eq!(
call(
&ctx,
Builtin::Timezone,
&[&lit("2024-01-01T00:00:00", &format!("{XSD}dateTime"))]
),
None
);
let string_true = lit("true", &format!("{XSD}string"));
let bool_true = lit("true", &format!("{XSD}boolean"));
for builtin in [
Builtin::CastInteger,
Builtin::CastDecimal,
Builtin::CastFloat,
Builtin::CastDouble,
Builtin::CastBoolean,
Builtin::CastString,
] {
let source = if builtin == Builtin::CastBoolean {
&string_true
} else {
&bool_true
};
assert!(call(&ctx, builtin, &[source]).is_some(), "{builtin:?}");
}
assert_eq!(
call(
&ctx,
Builtin::CastInteger,
&[&lit("1.2", &format!("{XSD}string"))]
),
None
);
assert!(call(&ctx, Builtin::Rand, &[]).is_some());
assert!(call(&ctx, Builtin::Uuid, &[])
.unwrap()
.starts_with("<urn:uuid:"));
assert!(call(&ctx, Builtin::StrUuid, &[]).is_some());
assert!(call(&ctx, Builtin::BNode, &["\"stable\""])
.unwrap()
.starts_with("_:b"));
assert!(call(&ctx, Builtin::BNode, &[]).unwrap().starts_with("_:b"));
assert_eq!(
call(&ctx, Builtin::Datatype, &[lang]).unwrap(),
"<http://www.w3.org/1999/02/22-rdf-syntax-ns#langString>"
);
assert_eq!(call(&ctx, Builtin::Lang, &[lang]).unwrap(), "\"en\"");
assert_eq!(call(&ctx, Builtin::Datatype, &["<iri>"]), None);
let triple = "<<<s> <p> \"o\">>";
assert_eq!(
call(&ctx, Builtin::Subject, &[triple]).as_deref(),
Some("<s>")
);
assert_eq!(
call(&ctx, Builtin::Predicate, &[triple]).as_deref(),
Some("<p>")
);
assert_eq!(
call(&ctx, Builtin::Object, &[triple]).as_deref(),
Some("\"o\"")
);
assert_eq!(call(&ctx, Builtin::Subject, &["<s>"]), None);
assert_eq!(
call(&ctx, Builtin::TripleTerm, &["<s>", "<p>", "\"o\""]).as_deref(),
Some(triple)
);
}
#[test]
fn boolean_and_geo_builtins_cover_success_and_type_errors() {
let rete = fixture();
let ctx = Ctx::new(&rete, Slots::new());
let row = ctx.slots.empty_row();
let boolean = |builtin, args: &[&str]| {
func_bool(
builtin,
&args
.iter()
.map(|v| FExpr::Const((*v).to_string()))
.collect::<Vec<_>>(),
&ctx,
&row,
)
};
assert!(boolean(Builtin::IsIri, &["<iri>"]));
assert!(boolean(Builtin::IsBlank, &["_:b"]));
assert!(boolean(Builtin::IsLiteral, &["\"x\""]));
assert!(boolean(Builtin::IsNumeric, &["42"]));
assert!(boolean(Builtin::IsTriple, &["<<<s> <p> <o>>>"]));
assert!(boolean(Builtin::Contains, &["\"abc\"", "\"b\""]));
assert!(boolean(Builtin::StrStarts, &["\"abc\"", "\"a\""]));
assert!(boolean(Builtin::StrEnds, &["\"abc\"", "\"c\""]));
assert!(!boolean(Builtin::Contains, &["<iri>", "\"i\""]));
assert!(boolean(Builtin::Regex, &["\"Abc\"", "\"^a\"", "\"i\""]));
assert!(!boolean(Builtin::Regex, &["<iri>", "\"i\""]));
assert!(boolean(Builtin::LangMatches, &["\"en-GB\"", "\"EN\""]));
assert!(boolean(Builtin::LangMatches, &["\"de\"", "\"*\""]));
assert!(!boolean(Builtin::LangMatches, &["\"\"", "\"*\""]));
let wkt = |s: &str| lit(s, &format!("{GEO}wktLiteral"));
let point = wkt("POINT(1 1)");
let same = wkt("POINT(1 1)");
let far = wkt("POINT(5 5)");
assert!(boolean(Builtin::GeoSfEquals, &[&point, &same]));
assert!(boolean(Builtin::GeoSfDisjoint, &[&point, &far]));
assert!(!boolean(Builtin::GeoSfWithin, &["<iri>", &point]));
assert!(call(&ctx, Builtin::GeoSfEquals, &[&point, &same]).is_some());
assert!(call(
&ctx,
Builtin::GeoDistance,
&[
&point,
&far,
"<http://www.opengis.net/def/uom/OGC/1.0/metre>"
]
)
.is_some());
assert!(call(&ctx, Builtin::GeoEnvelope, &[&point]).is_some());
assert_eq!(call(&ctx, Builtin::GeoEnvelope, &["<iri>"]), None);
assert_eq!(
call(&ctx, Builtin::GeoDistance, &[&point, &far, "<bad-unit>"]),
None
);
let wkt3 = |s: &str| lit(s, "https://w3id.org/rete/geo3#wktLiteral3D");
let box3d = |s: &str| lit(s, "https://w3id.org/rete/geo3#box3dLiteral");
let big = box3d("BOX3D(0 0 0, 10 10 10)");
let inner = box3d("BOX3D(2 2 2, 4 4 4)");
let p000 = wkt3("POINT Z(0 0 0)");
let p_far = wkt3("POINT Z(3 4 12)"); assert!(boolean(Builtin::Geo3Contains, &[&big, &inner]));
assert!(!boolean(Builtin::Geo3Contains, &[&inner, &big]));
assert!(boolean(Builtin::Geo3Within, &[&inner, &big]));
let a = box3d("BOX3D(0 0 0, 1 1 1)");
let b = box3d("BOX3D(0 0 3, 1 1 4)");
assert!(!boolean(Builtin::Geo3Adjacent, &[&a, &b]));
assert!(boolean(Builtin::Geo3Adjacent, &[&a, &b, "3"]));
assert!(!boolean(Builtin::Geo3Adjacent, &["<iri>", &b]));
let d = call(&ctx, Builtin::Geo3Distance, &[&p000, &p_far]).unwrap();
assert!(d.contains("13"), "distance3D result: {d}");
assert_eq!(call(&ctx, Builtin::Geo3Distance, &[&p000, "<iri>"]), None);
}
#[test]
fn lexical_numeric_cast_datetime_and_comparison_helpers_reject_bad_inputs() {
assert_eq!(arith_number("\"2\""), None);
assert_eq!(arith_number("2"), Some(2.0));
assert!(is_numeric_dt(Some(&format!("{XSD}double"))));
assert!(!is_numeric_dt(Some(&format!("{XSD}string"))));
assert!(is_int_lexical("-12"));
assert!(!is_int_lexical("+"));
assert!(is_decimal_lexical(".5"));
assert!(!is_decimal_lexical("1.2.3"));
assert_eq!(parse_xsd_double("INF"), Some(f64::INFINITY));
assert_eq!(parse_xsd_double("-INF"), Some(f64::NEG_INFINITY));
assert!(parse_xsd_double("NaN").unwrap().is_nan());
assert_eq!(parse_xsd_double("inf"), None);
assert_eq!(parse_xsd_double("nope"), None);
assert_eq!(fmt_plain(2.0), "2");
assert_eq!(fmt_plain(2.5), "2.5");
assert_eq!(bool_literal(true), lit("true", &format!("{XSD}boolean")));
assert_eq!(encode_for_uri("a b/ä"), "a%20b%2F%C3%A4");
assert_eq!(hash_hex(Builtin::Md5, "abc").len(), 32);
assert_eq!(parse_datetime("invalid"), None);
assert_eq!(tz_to_duration(""), None);
assert_eq!(tz_to_duration("+00:00").as_deref(), Some("PT0S"));
assert_eq!(tz_to_duration("+01:30").as_deref(), Some("PT1H30M"));
assert_eq!(term_ebv(&lit("true", &format!("{XSD}boolean"))), Some(true));
assert_eq!(term_ebv(&lit("0", &format!("{XSD}integer"))), Some(false));
assert_eq!(term_ebv("<iri>"), None);
assert!(compare(Op::Eq, "2", "2.0"));
assert!(compare(Op::Ne, "a", "b"));
assert!(compare(Op::Le, "a", "b"));
assert!(compare(Op::Gt, "3", "2"));
assert!(compare(Op::Ge, "3", "3"));
assert!(lang_matches("en-US", "en"));
assert!(!lang_matches("english", "en"));
}
}