use rete_core::ingest::RawQuad;
use rete_core::Rete;
use std::path::Path;
use crate::commands::card::{self, CardArgs};
const BATCH: usize = 10_000;
fn stream_file<E>(
rete: &Rete,
label: &str,
visit: &mut dyn FnMut(RawQuad) -> Result<(), E>,
) -> Result<u64, E> {
let mut slots: Vec<Option<String>> = vec![None];
slots.extend(rete.graph_names().iter().map(|g| Some((*g).to_string())));
let mut total: u64 = 0;
for slot in slots {
let mut cursor = 0u32;
loop {
let (triples, next, done) = rete.dump_batch(slot.as_deref(), cursor, BATCH);
for (s, p, o) in triples {
visit((s, p, o, slot.clone()))?;
total += 1;
}
cursor = next;
if done {
break;
}
}
}
eprintln!("merge: {label} contributed {total} quad(s)");
Ok(total)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn merge_cmd(
inputs: &[String],
output: &str,
memory_budget_mb: u64,
tmp_dir: Option<&str>,
card_args: CardArgs,
) -> anyhow::Result<()> {
if inputs.is_empty() {
anyhow::bail!("merge needs at least one input .rete file");
}
for i in inputs {
if i == "-" {
anyhow::bail!(
"merge reads .rete files, not stdin — a .rete is read by range, not streamed"
);
}
if !Path::new(i).exists() {
anyhow::bail!("no such file: {i}");
}
}
if inputs.iter().any(|i| i == output) {
anyhow::bail!("refusing to overwrite an input ({output} is also an input)");
}
let mut perms_bits = rete_core::PermSet::CORE.bits();
for path in inputs {
let rete = crate::commands::range_source::open_local(path)?;
perms_bits |= rete.header().perms.bits();
}
let curated = if card_args.requested() {
Some(card::load_curated(&card_args)?)
} else {
None
};
let build_info = if curated.is_some() {
crate::commands::buildinfo::new_build_info(crate::commands::buildinfo::BuildParams {
command: Some("merge".to_string()),
no_pyramid: true,
memory_budget_mb: Some(memory_budget_mb),
..Default::default()
})
.to_json_bytes()
} else {
Vec::new()
};
let perms = rete_core::PermSet::from_bits(perms_bits).map_err(|e| anyhow::anyhow!("{e}"))?;
eprintln!("merge: {} input file(s) -> {output}", inputs.len());
if perms != rete_core::PermSet::ALL {
eprintln!(
"merge: every input is {}-permutation; the merged file will be too",
perms.len()
);
}
let out_path = Path::new(output).to_path_buf();
let stats = rete_core::extbuild::build_external(
|visit| {
for path in inputs {
let rete = crate::commands::range_source::open_local(path).map_err(|e| {
rete_core::extbuild::ExtBuildError::Ingest(rete_core::ingest::IngestError::Io(
format!("{path}: {e}"),
))
})?;
stream_file(&rete, path, visit)?;
if rete.index_incomplete() {
return Err(rete_core::extbuild::ExtBuildError::Ingest(
rete_core::ingest::IngestError::Io(format!(
"{path}: reads failed during the walk — the merge would be short"
)),
));
}
}
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| anyhow::anyhow!("{e}"))?;
let size = std::fs::metadata(&out_path).map(|m| m.len()).unwrap_or(0);
eprintln!(
"merge: wrote {output}: {} statement(s), {} term(s), {:.2} MB",
stats.statements,
stats.terms,
size as f64 / 1e6
);
Ok(())
}