use std::collections::{BTreeMap, HashMap, HashSet};
use crate::error::{Error, Result};
use crate::model::domain::Domain;
use super::inference::support_indices;
use super::types::{JoinMatch, JoinOptions, JoinPredicate, JoinResult, Tri};
use super::Query;
const MAX_PAIRS: usize = 3_000_000;
struct Prep {
id: String,
post: Vec<f64>,
support: Vec<usize>,
}
impl Query<'_> {
pub fn join(&self, pred: &JoinPredicate, opts: &JoinOptions) -> Result<JoinResult> {
let (ls, rs) = pred.slots();
let ld = self.db.domain(ls)?.clone();
let rd = self.db.domain(rs)?.clone();
if pred.is_numeric() {
if !matches!(ld, Domain::Continuous { .. }) || !matches!(rd, Domain::Continuous { .. })
{
return Err(Error::Invalid(
"gt/lt/approx joins require continuous slots on both sides".into(),
));
}
} else if !matches!(ld, Domain::Categorical { .. })
|| !matches!(rd, Domain::Categorical { .. })
{
return Err(Error::Invalid(
"same joins require categorical slots on both sides".into(),
));
}
let left = self.prepare(ls, opts.left_prefix.as_deref(), opts.require_evidence)?;
let symmetric = ls == rs && opts.left_prefix == opts.right_prefix;
let right_owned = if symmetric {
Vec::new()
} else {
self.prepare(rs, opts.right_prefix.as_deref(), opts.require_evidence)?
};
let right: &[Prep] = if symmetric { &left } else { &right_owned };
let mut matches = Vec::new();
let mut examined = 0usize;
let capped = if pred.is_numeric() {
self.join_numeric(
pred,
opts,
&ld,
&rd,
&left,
right,
symmetric,
&mut matches,
&mut examined,
)
} else {
self.join_same(
pred,
opts,
&ld,
&rd,
&left,
right,
symmetric,
&mut matches,
&mut examined,
)
};
matches.sort_by(|a, b| {
b.probability
.total_cmp(&a.probability)
.then(a.left.cmp(&b.left))
.then(a.right.cmp(&b.right))
});
let truncated = capped || matches.len() > opts.limit;
matches.truncate(opts.limit);
Ok(JoinResult {
matches,
pairs_examined: examined,
truncated,
})
}
fn prepare(
&self,
slot: &str,
prefix: Option<&str>,
require_evidence: bool,
) -> Result<Vec<Prep>> {
let mut out = Vec::new();
for e in self.db.entities() {
if let Some(p) = prefix {
if !e.starts_with(p) {
continue;
}
}
if require_evidence && self.db.evidence_for(e, slot).is_empty() {
continue;
}
let post = self.marginal(e, slot, &BTreeMap::new(), &[])?;
let support = support_indices(&post);
out.push(Prep {
id: e.to_string(),
post,
support,
});
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn join_numeric(
&self,
pred: &JoinPredicate,
opts: &JoinOptions,
ld: &Domain,
rd: &Domain,
left: &[Prep],
right: &[Prep],
symmetric: bool,
matches: &mut Vec<JoinMatch>,
examined: &mut usize,
) -> bool {
let lmid: Vec<f64> = (0..ld.n()).map(|i| ld.midpoint(i)).collect();
let rmid: Vec<f64> = (0..rd.n()).map(|i| rd.midpoint(i)).collect();
let rmin: Vec<f64> = right.iter().map(|p| rmid[p.support[0]]).collect();
let rmax: Vec<f64> = right
.iter()
.map(|p| rmid[*p.support.last().unwrap()])
.collect();
let tol = match pred {
JoinPredicate::Approx { tol, .. } => *tol,
_ => 0.0,
};
let mut order: Vec<usize> = (0..right.len()).collect();
match pred {
JoinPredicate::Lt { .. } => order.sort_by(|&a, &b| rmax[b].total_cmp(&rmax[a])),
_ => order.sort_by(|&a, &b| rmin[a].total_cmp(&rmin[b])),
}
let dedup = symmetric && pred.is_symmetric();
for a in left {
let a_min = lmid[a.support[0]];
let a_max = lmid[*a.support.last().unwrap()];
for &ri in &order {
let (b_min, b_max) = (rmin[ri], rmax[ri]);
match pred {
JoinPredicate::Gt { .. } => {
if b_min >= a_max {
break; }
}
JoinPredicate::Lt { .. } => {
if b_max <= a_min {
break; }
}
JoinPredicate::Approx { .. } => {
if b_min > a_max + tol {
break;
}
if b_max < a_min - tol {
continue;
}
}
JoinPredicate::Same { .. } => unreachable!(),
}
let b = &right[ri];
if a.id == b.id {
continue;
}
if dedup && a.id >= b.id {
continue;
}
*examined += 1;
if *examined > MAX_PAIRS {
return true;
}
let tri = match pred {
JoinPredicate::Gt { .. } => tri_gt(a_min, a_max, b_min, b_max),
JoinPredicate::Lt { .. } => tri_gt(b_min, b_max, a_min, a_max),
JoinPredicate::Approx { .. } => tri_approx(a_min, a_max, b_min, b_max, tol),
JoinPredicate::Same { .. } => unreachable!(),
};
if tri == Tri::False {
continue;
}
if opts.certain_only && tri != Tri::True {
continue;
}
let p = match pred {
JoinPredicate::Gt { .. } => p_gt(&a.post, &lmid, &b.post, &rmid),
JoinPredicate::Lt { .. } => p_gt(&b.post, &rmid, &a.post, &lmid),
JoinPredicate::Approx { .. } => p_approx(&a.post, &lmid, &b.post, &rmid, tol),
JoinPredicate::Same { .. } => unreachable!(),
};
if p + 1e-12 < opts.min_probability {
continue;
}
matches.push(JoinMatch {
left: a.id.clone(),
right: b.id.clone(),
probability: p,
certainty: tri,
});
}
}
false
}
#[allow(clippy::too_many_arguments)]
fn join_same(
&self,
pred: &JoinPredicate,
opts: &JoinOptions,
ld: &Domain,
rd: &Domain,
left: &[Prep],
right: &[Prep],
symmetric: bool,
matches: &mut Vec<JoinMatch>,
examined: &mut usize,
) -> bool {
let lvals = match ld {
Domain::Categorical { values } => values,
_ => unreachable!(),
};
let rvals = match rd {
Domain::Categorical { values } => values,
_ => unreachable!(),
};
let r_labels: Vec<HashSet<&str>> = right
.iter()
.map(|p| p.support.iter().map(|&i| rvals[i].as_str()).collect())
.collect();
let mut buckets: HashMap<&str, Vec<usize>> = HashMap::new();
for (ri, labs) in r_labels.iter().enumerate() {
for l in labs {
buckets.entry(l).or_default().push(ri);
}
}
let dedup = symmetric && pred.is_symmetric();
for a in left {
let a_labels: HashSet<&str> = a.support.iter().map(|&i| lvals[i].as_str()).collect();
let mut seen: HashSet<usize> = HashSet::new();
for l in &a_labels {
let Some(cands) = buckets.get(l) else {
continue;
};
for &ri in cands {
if !seen.insert(ri) {
continue;
}
let b = &right[ri];
if a.id == b.id {
continue;
}
if dedup && a.id >= b.id {
continue;
}
*examined += 1;
if *examined > MAX_PAIRS {
return true;
}
let tri = tri_same(&a_labels, &r_labels[ri]);
if opts.certain_only && tri != Tri::True {
continue;
}
let p = p_same(&a.post, lvals, &b.post, rvals);
if p + 1e-12 < opts.min_probability {
continue;
}
matches.push(JoinMatch {
left: a.id.clone(),
right: b.id.clone(),
probability: p,
certainty: tri,
});
}
}
}
false
}
}
fn p_gt(a: &[f64], amid: &[f64], b: &[f64], bmid: &[f64]) -> f64 {
let mut j = 0usize;
let mut cum = 0.0;
let mut p = 0.0;
for i in 0..a.len() {
while j < b.len() && bmid[j] < amid[i] {
cum += b[j];
j += 1;
}
p += a[i] * cum;
}
p
}
fn p_approx(a: &[f64], amid: &[f64], b: &[f64], bmid: &[f64], tol: f64) -> f64 {
let mut lo = 0usize;
let mut hi = 0usize;
let mut win = 0.0;
let mut p = 0.0;
for i in 0..a.len() {
let ub = amid[i] + tol;
let lb = amid[i] - tol;
while hi < b.len() && bmid[hi] <= ub {
win += b[hi];
hi += 1;
}
while lo < hi && bmid[lo] < lb {
win -= b[lo];
lo += 1;
}
p += a[i] * win;
}
p
}
fn p_same(a: &[f64], avals: &[String], b: &[f64], bvals: &[String]) -> f64 {
let bmap: HashMap<&str, f64> = bvals
.iter()
.enumerate()
.map(|(j, l)| (l.as_str(), b[j]))
.collect();
let mut p = 0.0;
for (i, l) in avals.iter().enumerate() {
if let Some(bp) = bmap.get(l.as_str()) {
p += a[i] * bp;
}
}
p
}
fn tri_gt(a_min: f64, a_max: f64, b_min: f64, b_max: f64) -> Tri {
if a_min > b_max {
Tri::True
} else if a_max > b_min {
Tri::Possible
} else {
Tri::False
}
}
fn tri_approx(a_min: f64, a_max: f64, b_min: f64, b_max: f64, tol: f64) -> Tri {
let farthest = (a_max - b_min).max(b_max - a_min);
let gap = 0f64.max(a_min - b_max).max(b_min - a_max);
if farthest <= tol {
Tri::True
} else if gap <= tol {
Tri::Possible
} else {
Tri::False
}
}
fn tri_same(a: &HashSet<&str>, b: &HashSet<&str>) -> Tri {
let intersect = a.iter().any(|l| b.contains(l));
if a.len() == 1 && b.len() == 1 && intersect {
Tri::True
} else if intersect {
Tri::Possible
} else {
Tri::False
}
}