use rete_core::{eval_query, query_predicates, QueryOutput, Rete, SliceReader, SummaryView};
use crate::commands::range_source::open_local;
use crate::commands::render::print_query_output;
use crate::http::HttpRangeReader;
fn is_url(source: &str) -> bool {
source.starts_with("http://") || source.starts_with("https://")
}
pub(crate) fn source_predicates(
source: &str,
) -> anyhow::Result<Option<std::collections::BTreeSet<String>>> {
let view = if is_url(source) {
let reader = HttpRangeReader::open(source)?;
SummaryView::open_ranged(&reader)?
} else {
let bytes = std::fs::read(source)?;
let reader = SliceReader::new(&bytes);
SummaryView::open_ranged(&reader)?
};
Ok(view.map(|v| {
v.predicate_totals()
.into_iter()
.map(|(p, _)| p)
.collect::<std::collections::BTreeSet<String>>()
}))
}
fn eval_source(source: &str, query: &str) -> anyhow::Result<QueryOutput> {
let mut rete = if is_url(source) {
let reader = HttpRangeReader::open(source)?;
Rete::open_ranged(&reader)?
} else {
open_local(source)?
};
rete.set_service_client(Box::new(super::service_http::HttpServiceClient));
eval_query(&rete, query).map_err(|e| anyhow::anyhow!("{source}: {e}"))
}
pub(crate) fn federate(
sources: &[String],
query: &str,
json: bool,
route: bool,
) -> anyhow::Result<()> {
use std::collections::BTreeSet;
use std::time::Instant;
let query_preds: BTreeSet<String> =
query_predicates(query).map_err(|e| anyhow::anyhow!("{e}"))?;
let mut queried: Vec<&String> = Vec::new();
let mut skipped: Vec<&String> = Vec::new();
for source in sources {
let mut prune = false;
if route && !query_preds.is_empty() {
match source_predicates(source) {
Ok(Some(preds)) => {
prune = query_preds.is_disjoint(&preds);
}
Ok(None) => {} Err(e) => eprintln!("warning: {source}: routing skipped ({e})"),
}
}
if prune {
skipped.push(source);
} else {
queried.push(source);
}
}
let mut acc = MergeAcc::default();
for source in &queried {
let start = Instant::now();
let out = eval_source(source, query)?;
let contributed = acc.absorb(out);
eprintln!(
" {source}: {contributed} row(s) in {:.1?}",
start.elapsed()
);
}
let result = acc.into_output();
print_query_output(&result, json);
eprintln!(
"federated {} source(s): {} queried, {} pruned (routing {}); {} merged result(s)",
sources.len(),
queried.len(),
skipped.len(),
if route { "on" } else { "off" },
match &result {
QueryOutput::Select(_, rows) => rows.len(),
QueryOutput::Ask(_) => 1,
QueryOutput::Construct(ts) => ts.len(),
_ => 0,
}
);
if !skipped.is_empty() {
eprintln!(" pruned (predicate-disjoint): {}", join_sources(&skipped));
}
Ok(())
}
#[derive(Default)]
pub(crate) struct MergeAcc {
kind: Option<OutKind>,
select_vars: Vec<String>,
select_rows: Vec<rete_core::Binding>,
select_seen: std::collections::BTreeSet<String>,
ask_any: bool,
construct: Vec<(String, String, String)>,
construct_seen: std::collections::BTreeSet<(String, String, String)>,
}
#[derive(Clone, Copy, PartialEq)]
enum OutKind {
Select,
Ask,
Construct,
}
impl MergeAcc {
pub(crate) fn absorb(&mut self, out: QueryOutput) -> usize {
match out {
QueryOutput::Select(vars, rows) => {
self.kind.get_or_insert(OutKind::Select);
if self.select_vars.is_empty() {
self.select_vars = vars;
}
let before = self.select_rows.len();
for row in rows {
if self.select_seen.insert(row_key(&self.select_vars, &row)) {
self.select_rows.push(row);
}
}
self.select_rows.len() - before
}
QueryOutput::Ask(b) => {
self.kind.get_or_insert(OutKind::Ask);
self.ask_any |= b;
usize::from(b)
}
QueryOutput::Construct(triples) => {
self.kind.get_or_insert(OutKind::Construct);
let before = self.construct.len();
for t in triples {
if self.construct_seen.insert(t.clone()) {
self.construct.push(t);
}
}
self.construct.len() - before
}
_ => {
eprintln!("warning: query result kind is not supported by this CLI build");
0
}
}
}
pub(crate) fn into_output(self) -> QueryOutput {
match self.kind {
Some(OutKind::Ask) => QueryOutput::Ask(self.ask_any),
Some(OutKind::Construct) => QueryOutput::Construct(self.construct),
Some(OutKind::Select) | None => QueryOutput::Select(self.select_vars, self.select_rows),
}
}
}
fn row_key(vars: &[String], row: &rete_core::Binding) -> String {
if vars.is_empty() {
row.iter()
.map(|(k, v)| format!("{k}\u{1}{v}"))
.collect::<Vec<_>>()
.join("\u{2}")
} else {
vars.iter()
.map(|v| row.get(v).map(String::as_str).unwrap_or("").to_string())
.collect::<Vec<_>>()
.join("\u{2}")
}
}
fn join_sources(sources: &[&String]) -> String {
sources
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
}