use std::cmp::Reverse;
use std::collections::BinaryHeap;
use super::eval::RowIter;
use crate::bgp::{PatternTerm, TriplePattern};
use crate::index::{GraphIndex, Pattern};
use crate::row::{Ctx, Row, Val};
use crate::triples::Triple;
struct KeyCol {
col: usize,
same: Vec<usize>,
}
struct Lowered {
pattern: Pattern,
keys: Vec<KeyCol>,
anonymous: usize,
}
fn lower(ctx: &Ctx, vars: &[String], src: &TriplePattern) -> Option<Lowered> {
let dict = ctx.rete.dictionary();
let terms = [&src.s, &src.p, &src.o];
let mut pattern: [Option<u32>; 3] = [None, None, None];
let mut keys: Vec<KeyCol> = vars
.iter()
.map(|_| KeyCol {
col: usize::MAX,
same: Vec::new(),
})
.collect();
let mut anonymous = 0;
for (col, t) in terms.into_iter().enumerate() {
match t {
PatternTerm::Const(c) => {
pattern[col] = Some(match col {
0 => dict.subject_id(c)?,
1 => dict.predicate_id(c)?,
_ => dict.object_id(c)?,
});
}
PatternTerm::Var(v) => match vars.iter().position(|x| x == v) {
Some(i) if keys[i].col == usize::MAX => keys[i].col = col,
Some(i) => keys[i].same.push(col),
None => anonymous += 1,
},
}
}
if keys.iter().any(|k| k.col == usize::MAX) {
return None;
}
Some(Lowered {
pattern: (pattern[0], pattern[1], pattern[2]),
keys,
anonymous,
})
}
fn column_value(ctx: &Ctx, t: Triple, col: usize) -> i64 {
let dict = ctx.rete.dictionary();
match col {
0 => dict.subject_node(t.0) as i64,
1 => ctx.resolver.canon_id(-(t.1 as i64) - 1),
_ => dict.object_node(t.2) as i64,
}
}
fn key_of(ctx: &Ctx, l: &Lowered, t: Triple) -> Option<Vec<i64>> {
let mut key = Vec::with_capacity(l.keys.len());
for k in &l.keys {
let v = column_value(ctx, t, k.col);
if k.same.iter().any(|&c| column_value(ctx, t, c) != v) {
return None;
}
key.push(v);
}
Some(key)
}
type KeyIter<'q> = Box<dyn Iterator<Item = Vec<i64>> + 'q>;
fn key_stream<'q>(ctx: &'q Ctx<'q>, index: &'q GraphIndex, l: Lowered) -> KeyIter<'q> {
let sortable = match l.keys.len() {
1 => true,
2 => l.anonymous == 0,
_ => false,
};
if sortable {
let lead = l.keys[0].col;
if lead != 1 && l.keys.get(1).is_none_or(|k| k.col != 1) {
if let Some(it) = index.scan_iter_sorted_on(l.pattern, lead) {
return Box::new(it.filter_map(move |t| key_of(ctx, &l, t)));
}
}
}
let mut keys: Vec<Vec<i64>> = index
.scan_iter(l.pattern)
.filter_map(|t| key_of(ctx, &l, t))
.collect();
keys.sort_unstable();
keys.dedup();
Box::new(keys.into_iter())
}
struct MergeDistinct<'q> {
streams: Vec<KeyIter<'q>>,
heads: BinaryHeap<Reverse<(Vec<i64>, usize)>>,
last: Option<Vec<i64>>,
}
impl<'q> MergeDistinct<'q> {
fn new(mut streams: Vec<KeyIter<'q>>) -> Self {
let mut heads = BinaryHeap::with_capacity(streams.len());
for (i, s) in streams.iter_mut().enumerate() {
if let Some(k) = s.next() {
heads.push(Reverse((k, i)));
}
}
MergeDistinct {
streams,
heads,
last: None,
}
}
}
impl Iterator for MergeDistinct<'_> {
type Item = Vec<i64>;
fn next(&mut self) -> Option<Vec<i64>> {
loop {
let Reverse((key, i)) = self.heads.pop()?;
if let Some(k) = self.streams[i].next() {
self.heads.push(Reverse((k, i)));
}
if self.last.as_ref() != Some(&key) {
self.last = Some(key.clone());
return Some(key);
}
}
}
}
const GROUP_SCAN_MIN: usize = 8;
fn groupable(l: &Lowered) -> Option<(u32, usize, usize, u32)> {
let [k] = l.keys.as_slice() else {
return None;
};
if !k.same.is_empty() || l.anonymous != 0 {
return None;
}
let p = l.pattern.1?;
let (other, id) = match k.col {
0 => (2, l.pattern.2?),
2 => (0, l.pattern.0?),
_ => return None,
};
Some((p, k.col, other, id))
}
fn component(t: Triple, col: usize) -> u32 {
match col {
0 => t.0,
1 => t.1,
_ => t.2,
}
}
fn rows_per_tile(ctx: &Ctx, index: &GraphIndex, si: usize) -> f64 {
let tiles = index.tile_sections()[si].len().max(1);
ctx.rete.header().quad_count as f64 / tiles as f64
}
fn union_tile_rows(ctx: &Ctx, index: &GraphIndex, patterns: impl Iterator<Item = Pattern>) -> f64 {
let mut tiles: std::collections::BTreeSet<(usize, usize)> = std::collections::BTreeSet::new();
for pattern in patterns {
let mut best: Option<(f64, usize, usize, usize)> = None;
for perm in index.perms().iter() {
let si = perm.section_index();
let (start, end) = index.tile_span(si, perm.order_pattern(pattern)[0]);
let rows = (end - start) as f64 * rows_per_tile(ctx, index, si);
if best.is_none_or(|(r, ..)| rows < r) {
best = Some((rows, si, start, end));
}
}
if let Some((_, si, start, end)) = best {
tiles.extend((start..end).map(|i| (si, i)));
}
}
tiles
.into_iter()
.map(|(si, _)| rows_per_tile(ctx, index, si))
.sum()
}
fn group_streams<'q>(
ctx: &'q Ctx<'q>,
index: &'q GraphIndex,
p: u32,
key_col: usize,
other_col: usize,
members: Vec<(Lowered, u32)>,
) -> Vec<KeyIter<'q>> {
if members.len() >= GROUP_SCAN_MIN {
let whole: Pattern = (None, Some(p), None);
let per_source = union_tile_rows(ctx, index, members.iter().map(|(l, _)| l.pattern));
let one_scan = index.estimate_matches(whole, ctx.rete.header().quad_count);
let slack = if ctx.limit_hint.get().is_some_and(|h| h <= 4096) {
4.0
} else {
1.0
};
if one_scan <= per_source * slack {
if let Some(it) = index.scan_iter_sorted_on(whole, key_col) {
let wanted: std::collections::HashSet<u32> =
members.iter().map(|&(_, id)| id).collect();
return vec![Box::new(
it.filter(move |&t| wanted.contains(&component(t, other_col)))
.map(move |t| vec![column_value(ctx, t, key_col)]),
)];
}
}
}
members
.into_iter()
.map(|(l, _)| key_stream(ctx, index, l))
.collect()
}
pub(super) fn iter<'q>(
ctx: &'q Ctx<'q>,
index: &'q GraphIndex,
vars: &'q [String],
sources: &'q [TriplePattern],
) -> RowIter<'q> {
let lowered: Vec<Lowered> = sources.iter().filter_map(|s| lower(ctx, vars, s)).collect();
let Some(slots) = vars
.iter()
.map(|v| ctx.slots.slot(v))
.collect::<Option<Vec<usize>>>()
else {
return Box::new(std::iter::empty());
};
if vars.is_empty() {
let hit = lowered
.iter()
.any(|l| index.scan_iter(l.pattern).next().is_some());
return if hit {
Box::new(std::iter::once(ctx.slots.empty_row()))
} else {
Box::new(std::iter::empty())
};
}
let mut streams: Vec<KeyIter<'q>> = Vec::new();
let mut groups: std::collections::BTreeMap<(u32, usize, usize), Vec<(Lowered, u32)>> =
std::collections::BTreeMap::new();
for l in lowered {
match groupable(&l) {
Some((p, kc, oc, id)) => groups.entry((p, kc, oc)).or_default().push((l, id)),
None => streams.push(key_stream(ctx, index, l)),
}
}
for ((p, kc, oc), members) in groups {
streams.extend(group_streams(ctx, index, p, kc, oc, members));
}
Box::new(MergeDistinct::new(streams).map(move |key| {
let mut row = ctx.slots.empty_row();
for (&slot, v) in slots.iter().zip(key) {
row[slot] = Some(Val::Id(v));
}
row
}))
}
fn role_id(ctx: &Ctx, col: usize, node: i64) -> Option<u32> {
let dict = ctx.rete.dictionary();
match col {
0 if node >= 0 => dict.node_as_subject_id(node as u32),
2 if node >= 0 => dict.node_as_object_id(node as u32),
1 if node < 0 => Some((-node - 1) as u32),
1 => dict
.node_term(node as u32)
.and_then(|t| dict.predicate_id(&t)),
_ => None,
}
}
pub(super) fn matches(
ctx: &Ctx,
index: &GraphIndex,
vars: &[String],
sources: &[TriplePattern],
row: &Row,
) -> bool {
let mut values: Vec<i64> = Vec::with_capacity(vars.len());
for v in vars {
match ctx.slots.slot(v).and_then(|s| row[s].as_ref()) {
Some(Val::Id(id)) => values.push(*id),
Some(Val::Str(_)) | None => return false,
}
}
let mut objects: Vec<((u32, u32), Vec<u32>)> = Vec::new();
sources.iter().any(|src| {
let Some(l) = lower(ctx, vars, src) else {
return false;
};
let mut pattern = [l.pattern.0, l.pattern.1, l.pattern.2];
for (k, &node) in l.keys.iter().zip(&values) {
for col in std::iter::once(k.col).chain(k.same.iter().copied()) {
match role_id(ctx, col, node) {
Some(id) => pattern[col] = Some(id),
None => return false,
}
}
}
let object_is_key = l.keys.iter().any(|k| k.col == 2 || k.same.contains(&2));
if let ([Some(s), Some(p), Some(o)], false) = (pattern, object_is_key) {
let at = match objects.iter().position(|(sp, _)| *sp == (s, p)) {
Some(i) => i,
None => {
let mut os: Vec<u32> = index
.scan_iter((Some(s), Some(p), None))
.map(|t| t.2)
.collect();
os.sort_unstable();
objects.push(((s, p), os));
objects.len() - 1
}
};
return objects[at].1.binary_search(&o).is_ok();
}
index
.scan_iter((pattern[0], pattern[1], pattern[2]))
.next()
.is_some()
})
}
pub(super) fn scan_rows(
ctx: &Ctx,
index: &GraphIndex,
vars: &[String],
sources: &[TriplePattern],
) -> f64 {
let total = ctx.rete.header().quad_count;
sources
.iter()
.filter_map(|s| lower(ctx, vars, s))
.map(|l| index.estimate_matches(l.pattern, total))
.sum()
}