use crate::commands::buildinfo;
use crate::commands::card::{self, CardArgs};
use rete_core::card::{claim_of, Claim};
use rete_core::ingest;
fn drop_measured_empty_queries(
bytes: Vec<u8>,
mut card: card::DatasetCard,
) -> anyhow::Result<(Vec<u8>, buildinfo::QueryCosts, Vec<buildinfo::DroppedQuery>)> {
let image = std::sync::Arc::new(bytes);
let measured = buildinfo::measure_query_costs(image.clone(), &card.queries);
let mut bytes = std::sync::Arc::try_unwrap(image).unwrap_or_else(|a| (*a).clone());
eprintln!(
"measured {} starter quer{} against the built file",
measured.len(),
if measured.len() == 1 { "y" } else { "ies" }
);
let mut kept = Vec::with_capacity(measured.len());
let mut dropped = Vec::new();
for m in measured {
match m.useless {
None => kept.push(m.cost),
Some(why) => {
let contradicts_claim = !m.vacuous && claim_of(&m.cost.id) == Claim::CannotBeEmpty;
dropped.push(buildinfo::DroppedQuery {
id: m.cost.id,
why,
contradicts_claim,
});
}
}
}
if !dropped.is_empty() {
eprintln!(
"dropping {} starter quer{} the build measured as useless on this file:",
dropped.len(),
if dropped.len() == 1 { "y" } else { "ies" }
);
for d in &dropped {
eprintln!(" {:<14} {}", d.id, d.why);
if d.contradicts_claim {
eprintln!(
" {:<14} ^ GENERATOR DEFECT: its template claims this query cannot come back \
empty. Fix the rule in crates/rete-cli/src/commands/queries.rs.",
""
);
}
}
let dropped_ids: std::collections::BTreeSet<&str> =
dropped.iter().map(|d| d.id.as_str()).collect();
card.queries
.retain(|q| !dropped_ids.contains(q.id.as_str()));
bytes = rete_core::replace_metadata(&bytes, &card.to_json_bytes())?;
eprintln!(
"re-carded with the {} starter quer{} that answer (content hash follows the card)",
card.queries.len(),
if card.queries.len() == 1 { "y" } else { "ies" }
);
}
Ok((bytes, buildinfo::query_costs(kept), dropped))
}
#[allow(clippy::too_many_arguments)]
fn attach_build_record(
bytes: Vec<u8>,
command: &str,
format: Option<&str>,
no_pyramid: bool,
text_index: bool,
materialize: bool,
reason: bool,
pyramid_algo: rete_core::PyramidAlgo,
measure_costs: bool,
) -> anyhow::Result<Vec<u8>> {
let header = rete_core::Header::from_bytes(&bytes)?;
let mut info = buildinfo::new_build_info(buildinfo::BuildParams {
command: Some(command.to_string()),
format: format.map(str::to_string),
no_pyramid,
text_index,
materialize,
reason,
pyramid_algo: if no_pyramid {
None
} else {
Some(
match pyramid_algo {
rete_core::PyramidAlgo::Types => "types",
_ => "louvain",
}
.to_string(),
)
},
memory_budget_mb: None,
codec: Some(buildinfo::codec_name(header.dict_codec).to_string()),
card_top_n: Some(card::CARD_TOP_N as u32),
});
if !measure_costs {
eprintln!(
"--no-card-costs: the starter queries were NOT run, so nothing checked that they \
answer on this file"
);
}
let card = card::load_card(&bytes)?.filter(|c| measure_costs && !c.queries.is_empty());
let bytes = match card {
Some(card) => {
let (bytes, costs, dropped) = drop_measured_empty_queries(bytes, card)?;
info.query_costs = Some(costs);
info.dropped_queries = dropped;
bytes
}
None => bytes,
};
let blob = info.to_json_bytes();
let out = rete_core::attach_build_info(&bytes, &blob)?;
eprintln!(
"embedded build info ({} bytes, outside the content hash)",
blob.len()
);
Ok(out)
}
fn open_reader(path: &str) -> anyhow::Result<Box<dyn std::io::BufRead>> {
Ok(open_reader_counted(path, None)?.0)
}
struct CountedRead<R> {
inner: R,
counter: std::rc::Rc<std::cell::Cell<u64>>,
limit: Option<u64>,
}
impl<R: std::io::Read> std::io::Read for CountedRead<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.limit.is_some_and(|l| self.counter.get() >= l) {
return Ok(0);
}
let n = self.inner.read(buf)?;
self.counter.set(self.counter.get() + n as u64);
Ok(n)
}
}
pub(crate) type ByteCounter = std::rc::Rc<std::cell::Cell<u64>>;
pub(crate) fn open_reader_counted(
path: &str,
limit: Option<u64>,
) -> anyhow::Result<(Box<dyn std::io::BufRead>, ByteCounter)> {
use std::io::BufRead;
let inner: Box<dyn std::io::Read> = if path == "-" {
Box::new(std::io::stdin())
} else {
Box::new(std::fs::File::open(path).map_err(|e| anyhow::anyhow!("{path}: {e}"))?)
};
let counter = std::rc::Rc::new(std::cell::Cell::new(0u64));
let counted = CountedRead {
inner,
counter: counter.clone(),
limit,
};
let mut buf = std::io::BufReader::with_capacity(1 << 20, counted);
let gzipped = {
let head = buf.fill_buf().map_err(|e| anyhow::anyhow!("{path}: {e}"))?;
head.len() >= 2 && head[0] == 0x1f && head[1] == 0x8b
};
if gzipped {
Ok((
Box::new(std::io::BufReader::with_capacity(
1 << 20,
flate2::read::MultiGzDecoder::new(buf),
)),
counter,
))
} else {
Ok((Box::new(buf), counter))
}
}
fn read_input(path: &str) -> anyhow::Result<String> {
use std::io::Read;
let mut s = String::new();
open_reader(path)?
.read_to_string(&mut s)
.map_err(|e| anyhow::anyhow!("{path}: {e}"))?;
Ok(s)
}
pub(crate) fn input_format(path: &str, override_fmt: Option<&str>) -> &'static str {
if let Some(f) = override_fmt {
return match f {
"nq" => "nq",
"ttl" => "ttl",
"trig" => "trig",
"rdfxml" | "rdf" | "owl" | "xml" => "rdfxml",
_ => "nt",
};
}
let lower = path.to_ascii_lowercase();
let p = lower
.strip_suffix(".gz")
.or_else(|| lower.strip_suffix(".gzip"))
.unwrap_or(lower.as_str());
if p.ends_with(".nq") || p.ends_with(".nquads") {
"nq"
} else if p.ends_with(".ttl") || p.ends_with(".turtle") {
"ttl"
} else if p.ends_with(".trig") {
"trig"
} else if p.ends_with(".rdf") || p.ends_with(".owl") || p.ends_with(".rdfxml") {
"rdfxml"
} else {
"nt"
}
}
fn looks_like_non_rdf_owl(text: &str) -> bool {
let head = &text[..text.len().min(4096)];
(!head.contains("rdf:RDF") && head.contains("<Ontology"))
|| (!head.trim_start().starts_with('<') && head.contains("Ontology("))
}
const STRICT_HINT: &str = "hint: `--strict` refuses input carrying IRIs the N-Triples/N-Quads \
IRIREF grammar (RFC 3987) disallows.\n Build without it to store them verbatim — the \
build then reports how many it ingested — or repair the source.\n `rete export \
--sanitize-iris` percent-encodes the repairable classes on the way out.";
fn iri_audit(strict: bool) -> ingest::IriAudit {
if strict {
ingest::IriAudit::refusing()
} else {
ingest::IriAudit::counting()
}
}
fn ingest_error(input: Option<&str>, e: ingest::IngestError) -> anyhow::Error {
let where_ = input.map(|i| format!("{i}: ")).unwrap_or_default();
match e {
ingest::IngestError::InvalidIri { .. } => anyhow::anyhow!("{where_}{e}\n{STRICT_HINT}"),
_ => anyhow::anyhow!("{where_}{e}"),
}
}
fn parse_inputs(
inputs: &[String],
format: Option<&str>,
audit: &mut ingest::IriAudit,
surface: ingest::QuotedTripleSurface,
) -> anyhow::Result<Vec<ingest::RawQuad>> {
let mut quads: Vec<ingest::RawQuad> = Vec::new();
for input in inputs {
let fmt = input_format(input, format);
let parsed = if input != "-" && (fmt == "nt" || fmt == "nq") {
let cap = std::fs::metadata(input)
.map(|m| (m.len() / 64) as usize)
.unwrap_or(0);
ingest::parse_reader_audited(open_reader(input)?, fmt, cap, Some(audit))
.map_err(|e| ingest_error(Some(input), e))?
} else {
let text = read_input(input)?;
ingest::parse_statements_audited_surface(&text, fmt, Some(audit), surface).map_err(
|e| {
if fmt == "rdfxml" && looks_like_non_rdf_owl(&text) {
anyhow::anyhow!(
"{input}: {e}\n\
hint: this looks like OWL/XML or OWL Functional Syntax, which \
are not RDF. Convert to RDF/XML or Turtle first (e.g. owlready2, \
`robot convert`, or Protégé → Save as → RDF/XML), then build that."
)
} else {
ingest_error(Some(input), e)
}
},
)?
};
if quads.is_empty() {
quads = parsed;
} else {
quads.extend(parsed);
}
}
Ok(quads)
}
pub(crate) fn validate(
inputs: &[String],
format: Option<&str>,
strict: bool,
surface: ingest::QuotedTripleSurface,
) -> anyhow::Result<()> {
let mut audit = iri_audit(strict);
let quads = parse_inputs(inputs, format, &mut audit, surface)?;
let named: std::collections::BTreeSet<&String> =
quads.iter().filter_map(|(_, _, _, g)| g.as_ref()).collect();
let in_default = quads.iter().filter(|(_, _, _, g)| g.is_none()).count();
println!(
"valid: {} statement(s) — {} in the default graph, {} named graph(s)",
quads.len(),
in_default,
named.len()
);
crate::commands::iri_report::warn_after_build(&audit.report);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn build(
inputs: &[String],
output: &str,
format: Option<&str>,
materialize: bool,
no_pyramid: bool,
reason: bool,
pyramid_algo: rete_core::PyramidAlgo,
text_index: bool,
type_predicate: Option<&str>,
collapse_graphs: bool,
card_args: CardArgs,
no_card_costs: bool,
perms: rete_core::PermSet,
strict: bool,
surface: ingest::QuotedTripleSurface,
) -> anyhow::Result<()> {
let streamable = !materialize
&& !reason
&& inputs
.iter()
.all(|i| i != "-" && matches!(input_format(i, format), "nt" | "nq"));
if streamable {
let curated = if card_args.requested() {
Some(card::load_curated(&card_args)?)
} else {
None
};
let card_requested = curated.is_some();
let inputs_fmt: Vec<(&str, &'static str)> = inputs
.iter()
.map(|i| (i.as_str(), input_format(i, format)))
.collect();
let audit = std::cell::RefCell::new(iri_audit(strict));
let pass = std::cell::Cell::new(0u32);
let stream = |visit: &mut dyn FnMut(ingest::RawQuad)| -> Result<(), ingest::IngestError> {
let first = pass.replace(pass.get() + 1) == 0;
let mut guard = audit.borrow_mut();
let mut sink = if first { Some(&mut *guard) } else { None };
for (path, fmt) in &inputs_fmt {
let rd = open_reader(path)
.map_err(|e| ingest::IngestError::Io(format!("{path}: {e}")))?;
if collapse_graphs {
let mut flatten = |q: ingest::RawQuad| visit((q.0, q.1, q.2, None));
ingest::stream_reader_audited(rd, fmt, sink.as_deref_mut(), &mut flatten)?;
} else {
ingest::stream_reader_audited(rd, fmt, sink.as_deref_mut(), visit)?;
}
}
Ok(())
};
let (bytes, stats) = ingest::assemble_dataset_streaming_with_perms(
stream,
!no_pyramid,
text_index,
type_predicate,
pyramid_algo,
perms,
|stats, dict, triples| match curated {
Some(curated) => {
let card = card::derive_card_encoded(
dict,
triples,
stats.statements as u64,
stats.terms as u64,
stats.named_graphs as u64,
curated,
);
rete_core::ingest::DeferredMetadata::new(move |counts| {
let blob = card.with_final_counts(counts).to_json_bytes();
eprintln!("embedded dataset card ({} bytes of metadata)", blob.len());
blob
})
}
None => rete_core::ingest::DeferredMetadata::none(),
},
)
.map_err(|e| ingest_error(None, e))?;
let bytes = if card_requested {
attach_build_record(
bytes,
"build",
format,
no_pyramid,
text_index,
false,
false,
pyramid_algo,
!no_card_costs,
)?
} else {
bytes
};
std::fs::write(output, &bytes)?;
print_build_summary(output, &stats, bytes.len());
crate::commands::iri_report::warn_after_build(&audit.into_inner().report);
return Ok(());
}
let mut audit = iri_audit(strict);
let mut quads = parse_inputs(inputs, format, &mut audit, surface)?;
if collapse_graphs {
for q in quads.iter_mut() {
q.3 = None;
}
}
let reasoning = if materialize || reason {
let base: Vec<(String, String, String)> = quads
.iter()
.filter(|(_, _, _, g)| g.is_none())
.map(|(s, p, o, _)| (s.clone(), p.clone(), o.clone()))
.collect();
Some(rete_core::reason(&base))
} else {
None
};
if materialize {
let r = reasoning
.as_ref()
.expect("reasoning computed when materialize");
if !r.inconsistencies.is_empty() {
for inc in &r.inconsistencies {
eprintln!(" incoherent [{}] {}", inc.kind, inc.detail);
}
anyhow::bail!(
"{} inconsistency(ies) — refusing to materialize an incoherent graph",
r.inconsistencies.len()
);
}
let inferred = r.inferred.len();
quads.extend(r.inferred.iter().cloned().map(|(s, p, o)| (s, p, o, None)));
eprintln!("materialized {inferred} inferred triple(s) into the default graph");
}
let curated = if card_args.requested() || reason {
Some(card::load_curated(&card_args)?)
} else {
None
};
let card_requested = curated.is_some();
let (bytes, stats) = ingest::assemble_dataset_with_perms(
quads,
!no_pyramid,
text_index,
type_predicate,
pyramid_algo,
perms,
|stats, quads| match curated {
Some(curated) => {
let mut dataset_card = card::derive_card(
quads,
stats.terms as u64,
stats.named_graphs as u64,
curated,
);
if let Some(r) = reasoning.as_ref() {
dataset_card = dataset_card.with_coherence(r, materialize);
}
rete_core::ingest::DeferredMetadata::new(move |counts| {
let blob = dataset_card.with_final_counts(counts).to_json_bytes();
eprintln!("embedded dataset card ({} bytes of metadata)", blob.len());
blob
})
}
None => rete_core::ingest::DeferredMetadata::none(),
},
);
let bytes = if card_requested {
attach_build_record(
bytes,
"build",
format,
no_pyramid,
text_index,
materialize,
reason,
pyramid_algo,
!no_card_costs,
)?
} else {
bytes
};
std::fs::write(output, &bytes)?;
print_build_summary(output, &stats, bytes.len());
crate::commands::iri_report::warn_after_build(&audit.report);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_external_cmd(
inputs: &[String],
output: &str,
format: Option<&str>,
memory_budget_mb: u64,
tmp_dir: Option<&str>,
materialize: bool,
reason: bool,
text_index: bool,
collapse_graphs: bool,
card_args: CardArgs,
perms: rete_core::PermSet,
strict: bool,
surface: ingest::QuotedTripleSurface,
) -> anyhow::Result<()> {
if materialize || reason {
anyhow::bail!(
"--memory-budget-mb is incompatible with --materialize/--reason \
(they need the whole graph resident); run them via the standard build"
);
}
if text_index {
anyhow::bail!(
"--memory-budget-mb does not support --text-index (the inverted index is \
a separate external sort over literal tokens, not part of the triple \
spill); build with the standard path, or drop --text-index"
);
}
let inputs_fmt: Vec<(&str, &'static str)> = inputs
.iter()
.map(|i| (i.as_str(), input_format(i, format)))
.collect();
let uses_stdin = inputs.iter().any(|i| i == "-");
if uses_stdin && (inputs.len() != 1 || format.is_none()) {
anyhow::bail!(
"--memory-budget-mb with stdin: `-` must be the only input and \
requires an explicit --format nt|nq|ttl|trig"
);
}
if let Some((bad, fmt)) = inputs_fmt
.iter()
.find(|(i, f)| *i != "-" && !matches!(*f, "nt" | "nq" | "ttl" | "trig"))
{
anyhow::bail!(
"--memory-budget-mb streams N-Triples/N-Quads/Turtle/TriG only \
({bad} is {fmt}); convert RDF/XML inputs to .ttl or .nt first"
);
}
let curated = if card_args.requested() {
Some(card::load_curated(&card_args)?)
} else {
None
};
let build_info = if curated.is_some() {
buildinfo::new_build_info(buildinfo::BuildParams {
command: Some("build --memory-budget-mb".to_string()),
format: format.map(str::to_string),
no_pyramid: true,
memory_budget_mb: Some(memory_budget_mb),
..Default::default()
})
.to_json_bytes()
} else {
Vec::new()
};
let out_path = std::path::Path::new(output).to_path_buf();
let audit = std::cell::RefCell::new(iri_audit(strict));
let stats = rete_core::extbuild::build_external(
|visit| {
let mut guard = audit.borrow_mut();
for (path, fmt) in &inputs_fmt {
let mut err: Option<rete_core::extbuild::ExtBuildError> = None;
let mut on_quad = |q: ingest::RawQuad| {
if err.is_none() {
let q = if collapse_graphs {
(q.0, q.1, q.2, None)
} else {
q
};
if let Err(e) = visit(q) {
err = Some(e);
}
}
};
let rd = open_reader(path).map_err(|e| {
rete_core::extbuild::ExtBuildError::Ingest(ingest::IngestError::Io(
e.to_string(),
))
})?;
let res = ingest::stream_reader_audited_surface(
rd,
fmt,
Some(&mut *guard),
surface,
&mut on_quad,
);
if let Some(e) = err {
return Err(e);
}
res.map_err(rete_core::extbuild::ExtBuildError::Ingest)?;
}
Ok(())
},
&out_path,
rete_core::extbuild::ExternalBuildOptions {
memory_budget: memory_budget_mb.saturating_mul(1 << 20),
tmp_dir: tmp_dir.map(std::path::PathBuf::from),
metadata: Box::new(move |stats| match curated {
Some(curated) => {
let blob = card::curated_counts_card(
stats.statements as u64,
stats.terms as u64,
curated,
)
.to_json_bytes();
eprintln!("embedded dataset card ({} bytes of metadata)", blob.len());
blob
}
None => Vec::new(),
}),
build_info,
perms,
},
)
.map_err(|e| match e {
rete_core::extbuild::ExtBuildError::Ingest(inner) => ingest_error(None, inner),
other => anyhow::anyhow!("{other}"),
})?;
let byte_len = std::fs::metadata(output).map(|m| m.len()).unwrap_or(0) as usize;
print_build_summary(output, &stats, byte_len);
crate::commands::iri_report::warn_after_build(&audit.into_inner().report);
Ok(())
}
fn print_build_summary(output: &str, stats: &ingest::BuildStats, byte_len: usize) {
if stats.named_graphs > 0 {
println!(
"wrote {output}: {} quads ({} default + {} named graph(s)), {} terms, {} bytes",
stats.statements, stats.default_triples, stats.named_graphs, stats.terms, byte_len
);
} else {
println!(
"wrote {output}: {} triples, {} terms, {} pyramid level(s), {} bytes",
stats.default_triples, stats.terms, stats.pyramid_levels, byte_len
);
}
}
pub(crate) fn repyramid(
input: &str,
output: &str,
text_index: bool,
type_predicate: Option<&str>,
pyramid_algo: rete_core::PyramidAlgo,
card_args: CardArgs,
) -> anyhow::Result<()> {
let bytes = std::fs::read(input)?;
let rete = rete_core::Rete::open(&bytes)?;
let perms = rete.header().perms;
if perms != rete_core::PermSet::ALL {
eprintln!(
"preserving the input's {} permutation(s): {}",
perms.len(),
perms.names().join(", ")
);
}
drop(bytes);
let mut quads: Vec<ingest::RawQuad> = rete
.dump(None)
.into_iter()
.map(|(s, p, o)| (s, p, o, None))
.collect();
let named: Vec<String> = rete.graph_names().iter().map(|g| g.to_string()).collect();
for g in &named {
for (s, p, o) in rete.dump(Some(g)) {
quads.push((s, p, o, Some(g.clone())));
}
}
drop(rete);
let curated = if card_args.requested() {
Some(card::load_curated(&card_args)?)
} else {
None
};
let card_requested = curated.is_some();
let (out_bytes, stats) = ingest::assemble_dataset_with_perms(
quads,
true,
text_index,
type_predicate,
pyramid_algo,
perms,
|stats, quads| match curated {
Some(curated) => {
let card = card::derive_card(
quads,
stats.terms as u64,
stats.named_graphs as u64,
curated,
);
rete_core::ingest::DeferredMetadata::new(move |counts| {
let blob = card.with_final_counts(counts).to_json_bytes();
eprintln!("embedded dataset card ({} bytes of metadata)", blob.len());
blob
})
}
None => rete_core::ingest::DeferredMetadata::none(),
},
);
let out_bytes = if card_requested {
attach_build_record(
out_bytes,
"repyramid",
None,
false,
text_index,
false,
false,
pyramid_algo,
true,
)?
} else {
out_bytes
};
std::fs::write(output, &out_bytes)?;
if stats.named_graphs > 0 {
println!(
"repyramid: wrote {output} — {} quads ({} default + {} named), {} terms, {} bytes",
stats.statements,
stats.default_triples,
stats.named_graphs,
stats.terms,
out_bytes.len()
);
} else {
println!(
"repyramid: wrote {output} — {} triples, {} terms, {} pyramid level(s), {} bytes",
stats.default_triples,
stats.terms,
stats.pyramid_levels,
out_bytes.len()
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rete_core::Rete;
fn build_carded(tag: &str, nt: &str, measure: bool) -> Vec<u8> {
let pid = std::process::id();
let dir = std::env::temp_dir();
let inp = dir.join(format!("rete_{tag}_{pid}.nt"));
let out = dir.join(format!("rete_{tag}_{pid}.rete"));
std::fs::write(&inp, nt).unwrap();
build(
&[inp.to_str().unwrap().to_string()],
out.to_str().unwrap(),
None,
false,
false,
false,
rete_core::PyramidAlgo::Louvain,
false,
None,
false,
CardArgs {
enabled: true,
title: Some("measured".into()),
..Default::default()
},
!measure,
rete_core::PermSet::ALL,
false,
ingest::QuotedTripleSurface::default(),
)
.unwrap();
let bytes = std::fs::read(&out).unwrap();
std::fs::remove_file(&inp).ok();
std::fs::remove_file(&out).ok();
bytes
}
fn card_of(bytes: &[u8]) -> card::DatasetCard {
card::load_card(bytes).unwrap().expect("card embedded")
}
fn build_info_of(bytes: &[u8]) -> buildinfo::BuildInfo {
buildinfo::BuildInfo::from_json_bytes(&rete_core::read_build_info(bytes).unwrap().unwrap())
.unwrap()
}
const NO_DANGLING_IRI: &str = "\
<http://x/a> <http://x/p> <http://x/b> .
<http://x/b> <http://x/p> <http://x/a> .
<http://x/a> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://x/C> .
<http://x/b> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://x/C> .
<http://x/C> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://x/C> .
";
#[test]
fn a_starter_query_measured_at_zero_rows_is_not_shipped() {
let bytes = build_carded("drop_empty", NO_DANGLING_IRI, true);
let card = card_of(&bytes);
let info = build_info_of(&bytes);
assert!(
!card.queries.iter().any(|q| q.id == "top-dangling"),
"the card must not ship a query the build measured at zero rows"
);
let dropped = &info.dropped_queries;
assert_eq!(
dropped.len(),
1,
"exactly one query was useless: {dropped:?}"
);
assert_eq!(dropped[0].id, "top-dangling");
assert!(dropped[0].why.contains("0 rows"), "{:?}", dropped[0].why);
assert!(
!dropped[0].contradicts_claim,
"its template declared itself undecidable, so this is news, not a defect"
);
let costs = info.query_costs.expect("costs measured");
let ids: Vec<&str> = costs.queries.iter().map(|c| c.id.as_str()).collect();
assert_eq!(
ids,
card.queries
.iter()
.map(|q| q.id.as_str())
.collect::<Vec<_>>(),
"the cost table stays aligned with the queries the card ships"
);
for c in &costs.queries {
assert!(c.rows > 0, "{} shipped with {} rows", c.id, c.rows);
}
assert!(rete_core::verify(&bytes).unwrap());
}
#[test]
fn a_starter_query_measured_as_a_row_binding_nothing_is_not_shipped() {
const LAT_AND_LONG_APART: &str = "\
<http://x/a> <http://www.w3.org/2003/01/geo/wgs84_pos#lat> \"46.5\" .
<http://x/b> <http://www.w3.org/2003/01/geo/wgs84_pos#long> \"6.6\" .
<http://x/a> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://x/C> .
<http://x/b> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://x/C> .
";
let bytes = build_carded("drop_vacuous", LAT_AND_LONG_APART, true);
let card = card_of(&bytes);
let info = build_info_of(&bytes);
assert!(
!card.queries.iter().any(|q| q.id == "sp-bbox"),
"a one-row answer that binds nothing is not an answer"
);
let d = info
.dropped_queries
.iter()
.find(|d| d.id == "sp-bbox")
.expect("sp-bbox dropped");
assert!(d.why.contains("binding no variable"), "{}", d.why);
assert!(
!d.contradicts_claim,
"NonEmpty::Aggregate promises a row, not a bound one — no claim is broken"
);
}
#[test]
fn measuring_does_not_touch_a_file_whose_queries_all_answer() {
let nt = "<http://x/a> <http://x/p> <http://x/b> .\n\
<http://x/a> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://x/C> .\n";
let measured = build_carded("nochurn_yes", nt, true);
let unmeasured = build_carded("nochurn_no", nt, false);
assert!(
build_info_of(&measured).dropped_queries.is_empty(),
"nothing to drop on this graph"
);
assert_eq!(
rete_core::attach_build_info(&measured, &[]).unwrap(),
rete_core::attach_build_info(&unmeasured, &[]).unwrap(),
"measuring must not change a single byte when nothing is dropped"
);
let costs = build_info_of(&measured).query_costs.expect("measured");
assert!(costs.queries.iter().all(|c| c.rows > 0));
assert!(build_info_of(&unmeasured).query_costs.is_none());
}
#[test]
fn no_card_costs_opts_out_of_the_emptiness_check_too() {
let bytes = build_carded("optout", NO_DANGLING_IRI, false);
assert!(
card_of(&bytes)
.queries
.iter()
.any(|q| q.id == "top-dangling"),
"without the measurement the generator's static reasoning is all there is"
);
assert!(build_info_of(&bytes).dropped_queries.is_empty());
}
#[test]
fn card_build_is_hash_deterministic_with_build_info_outside() {
let nt = "<http://x/a> <http://x/p> <http://x/b> .\n\
<http://x/a> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://x/C> .\n";
let pid = std::process::id();
let inp = std::env::temp_dir().join(format!("rete_bi_in_{pid}.nt"));
let out1 = std::env::temp_dir().join(format!("rete_bi_out1_{pid}.rete"));
let out2 = std::env::temp_dir().join(format!("rete_bi_out2_{pid}.rete"));
std::fs::write(&inp, nt).unwrap();
let run = |out: &std::path::Path| {
build(
&[inp.to_str().unwrap().to_string()],
out.to_str().unwrap(),
None,
false,
false,
false,
rete_core::PyramidAlgo::Louvain,
false,
None,
false,
CardArgs {
enabled: true,
title: Some("BI test".into()),
..Default::default()
},
false,
rete_core::PermSet::ALL,
false,
ingest::QuotedTripleSurface::default(),
)
.unwrap();
std::fs::read(out).unwrap()
};
let a = run(&out1);
let b = run(&out2);
std::fs::remove_file(&inp).ok();
std::fs::remove_file(&out1).ok();
std::fs::remove_file(&out2).ok();
let info = buildinfo::BuildInfo::from_json_bytes(
&rete_core::read_build_info(&a).unwrap().unwrap(),
)
.unwrap();
assert!(info.built_at.is_some(), "timestamp stamped");
assert!(
info.builder.as_deref().unwrap().starts_with("rete-cli "),
"builder version stamped"
);
assert_eq!(info.params.command.as_deref(), Some("build"));
assert_eq!(info.params.card_top_n, Some(card::CARD_TOP_N as u32));
let costs = info.query_costs.clone().expect("costs measured by default");
assert!(costs.context.engine.is_some() && costs.context.note.is_some());
let smoke = costs
.queries
.iter()
.find(|c| c.id == "ov-one-row")
.expect("one-row smoke query measured");
assert_eq!(
smoke.rows, 1,
"the smoke query answers with exactly one row"
);
assert!(smoke.bytes > 0 && smoke.requests > 0);
let ha = rete_core::Header::from_bytes(&a).unwrap();
let hb = rete_core::Header::from_bytes(&b).unwrap();
assert_eq!(
ha.content_hash, hb.content_hash,
"content hash reproducible"
);
assert!(rete_core::verify(&a).unwrap() && rete_core::verify(&b).unwrap());
assert_eq!(
rete_core::attach_build_info(&a, &[]).unwrap(),
rete_core::attach_build_info(&b, &[]).unwrap(),
"everything except build-info is reproducible byte-for-byte"
);
let info_b = buildinfo::BuildInfo::from_json_bytes(
&rete_core::read_build_info(&b).unwrap().unwrap(),
)
.unwrap();
let strip_ms = |c: &buildinfo::QueryCosts| {
c.queries
.iter()
.map(|q| (q.id.clone(), q.bytes, q.requests, q.rows))
.collect::<Vec<_>>()
};
assert_eq!(
strip_ms(&costs),
strip_ms(info_b.query_costs.as_ref().unwrap()),
);
}
#[test]
fn cardless_build_gets_no_build_info() {
let nt = "<http://x/a> <http://x/p> <http://x/b> .\n";
let pid = std::process::id();
let inp = std::env::temp_dir().join(format!("rete_nobi_in_{pid}.nt"));
let out = std::env::temp_dir().join(format!("rete_nobi_out_{pid}.rete"));
std::fs::write(&inp, nt).unwrap();
build(
&[inp.to_str().unwrap().to_string()],
out.to_str().unwrap(),
None,
false,
false,
false,
rete_core::PyramidAlgo::Louvain,
false,
None,
false,
CardArgs::default(),
false,
rete_core::PermSet::ALL,
false,
ingest::QuotedTripleSurface::default(),
)
.unwrap();
let bytes = std::fs::read(&out).unwrap();
std::fs::remove_file(&inp).ok();
std::fs::remove_file(&out).ok();
assert!(rete_core::read_build_info(&bytes).unwrap().is_none());
let h = rete_core::Header::from_bytes(&bytes).unwrap();
assert_eq!(h.metadata_len, 0);
assert_eq!(h.build_info_len, 0);
}
#[test]
fn build_materialize_bakes_in_entailments() {
const TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
const SUBCLASS: &str = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
let nt = format!("<http://x> {TYPE} <http://C> .\n<http://C> {SUBCLASS} <http://D> .\n");
let pid = std::process::id();
let inp = std::env::temp_dir().join(format!("rete_mat_in_{pid}.nt"));
let out = std::env::temp_dir().join(format!("rete_mat_out_{pid}.rete"));
std::fs::write(&inp, nt).unwrap();
build(
&[inp.to_str().unwrap().to_string()],
out.to_str().unwrap(),
None,
true,
false,
false,
rete_core::PyramidAlgo::Louvain,
false,
None,
false,
CardArgs::default(),
false,
rete_core::PermSet::ALL,
false,
ingest::QuotedTripleSurface::default(),
)
.unwrap();
let bytes = std::fs::read(&out).unwrap();
let rete = Rete::open(&bytes).unwrap();
let inferred = rete.query(Some("<http://x>"), Some(TYPE), Some("<http://D>"));
assert_eq!(inferred.len(), 1, "x a D should be materialized");
build(
&[inp.to_str().unwrap().to_string()],
out.to_str().unwrap(),
None,
false,
false,
false,
rete_core::PyramidAlgo::Louvain,
false,
None,
false,
CardArgs::default(),
false,
rete_core::PermSet::ALL,
false,
ingest::QuotedTripleSurface::default(),
)
.unwrap();
let plain = Rete::open(&std::fs::read(&out).unwrap()).unwrap();
assert!(plain
.query(Some("<http://x>"), Some(TYPE), Some("<http://D>"))
.is_empty());
std::fs::remove_file(&inp).ok();
std::fs::remove_file(&out).ok();
}
}