pub(crate) mod codec;
pub(crate) mod dict;
pub(crate) mod triples;
use std::collections::HashMap;
use std::rc::Rc;
use codec::{
control_info, CiType, DICTIONARY_FORMAT, GLOBAL_FORMAT, HEADER_FORMAT, ORDER_SPO,
TRIPLES_FORMAT,
};
use dict::{write_pfc_section, TermArena, BLOCK_SIZE};
use triples::BitmapTriples;
pub(crate) const MAX_OBJECT_IDS: u64 = 4_000_000_000;
const _: () = assert!(MAX_OBJECT_IDS < (1u64 << 32) - 1);
const _: () = assert!(MAX_OBJECT_IDS > 3_000_000_000);
pub(crate) const MEMORY_PER_TERM: u64 = 512;
pub(crate) const MEMORY_PER_TRIPLE: u64 = 16;
pub(crate) struct Budget {
pub(crate) estimated_bytes: u64,
pub(crate) object_ids: u64,
pub(crate) terms: u64,
pub(crate) quads: u64,
pub(crate) dict_bytes: u64,
}
pub(crate) fn check_limits(rete: &rete_core::Rete, limit_bytes: u64) -> anyhow::Result<Budget> {
let d = rete.dictionary();
let object_ids = d.shared_count() as u64 + d.object_only_count() as u64;
if object_ids >= MAX_OBJECT_IDS {
anyhow::bail!(
"this file has {object_ids} distinct objects, and HDT cannot represent more than \
{MAX_OBJECT_IDS}.\n\
The reference implementation truncates object dictionary ids to 32 bits while \
building its query index, so a larger file would be written successfully and then \
answer queries incorrectly.\n\
hint: `--format trig --compress zstd` is lossless, compact, and has no such limit."
);
}
let terms = rete.header().term_count;
let quads = rete.header().quad_count;
let dict_bytes = rete.header().dictionary_len;
let estimated_bytes =
terms.saturating_mul(MEMORY_PER_TERM) + quads.saturating_mul(MEMORY_PER_TRIPLE);
if estimated_bytes > limit_bytes {
anyhow::bail!(
"writing this file as HDT needs roughly {} of memory ({terms} terms, {quads} \
triples), above the {} limit.\n\
HDT cannot be written in a single streaming pass: rete stores IRIs with angle \
brackets and HDT stores them bare, which reorders the dictionary, so every id has \
to be remapped and the triples re-sorted in the new order.\n\
hint: raise `--memory-budget-mb`, export one graph with `--graph`, or use \
`--format trig --compress zstd`, which streams and has no ceiling.",
human_bytes(estimated_bytes),
human_bytes(limit_bytes),
);
}
Ok(Budget {
estimated_bytes,
object_ids,
terms,
quads,
dict_bytes,
})
}
pub(crate) fn human_bytes(n: u64) -> String {
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
let mut v = n as f64;
let mut u = 0;
while v >= 1024.0 && u < UNITS.len() - 1 {
v /= 1024.0;
u += 1;
}
format!("{v:.1} {}", UNITS[u])
}
#[derive(Default)]
struct Interner {
ids: HashMap<Rc<[u8]>, u32>,
text: Vec<Rc<[u8]>>,
roles: Vec<u8>,
}
const ROLE_SUBJECT: u8 = 1;
const ROLE_OBJECT: u8 = 2;
impl Interner {
fn intern(&mut self, token: &str, role: u8) -> u32 {
let owned = dict::hdt_form(token);
let text: &[u8] = &owned;
if let Some(&i) = self.ids.get(text) {
self.roles[i as usize] |= role;
return i;
}
let rc: Rc<[u8]> = Rc::from(text);
let i = self.text.len() as u32;
self.ids.insert(Rc::clone(&rc), i);
self.text.push(rc);
self.roles.push(role);
i
}
fn sections(&self) -> (Vec<u32>, Vec<u32>, Vec<u32>) {
let mut shared = Vec::new();
let mut subjects = Vec::new();
let mut objects = Vec::new();
for (i, &r) in self.roles.iter().enumerate() {
match r {
x if x == ROLE_SUBJECT | ROLE_OBJECT => shared.push(i as u32),
ROLE_SUBJECT => subjects.push(i as u32),
_ => objects.push(i as u32),
}
}
let by_text = |v: &mut Vec<u32>| {
v.sort_unstable_by(|&a, &b| self.text[a as usize].cmp(&self.text[b as usize]))
};
by_text(&mut shared);
by_text(&mut subjects);
by_text(&mut objects);
(shared, subjects, objects)
}
}
pub(crate) struct Built {
pub(crate) bytes: Vec<u8>,
pub(crate) triples: u64,
pub(crate) shared: u64,
pub(crate) subjects: u64,
pub(crate) predicates: u64,
pub(crate) objects: u64,
}
pub(crate) fn build(
rete: &mut rete_core::Rete,
graph: Option<&str>,
s: Option<&str>,
p: Option<&str>,
o: Option<&str>,
iris: &mut Option<rete_core::iri::IriReport>,
source: &str,
) -> Built {
let mut nodes = Interner::default();
let mut preds: HashMap<Rc<[u8]>, u32> = HashMap::new();
let mut pred_text: Vec<Rc<[u8]>> = Vec::new();
let mut raw: Vec<(u32, u32, u32)> = Vec::new();
rete.dump_filtered_each(graph, s, p, o, |ts, tp, to| {
let (ts, tp, to) = crate::commands::export::clean(iris, ts, tp, to);
let si = nodes.intern(&ts, ROLE_SUBJECT);
let oi = nodes.intern(&to, ROLE_OBJECT);
let powned = dict::hdt_form(&tp);
let ptext: &[u8] = &powned;
let pi = match preds.get(ptext) {
Some(&i) => i,
None => {
let rc: Rc<[u8]> = Rc::from(ptext);
let i = pred_text.len() as u32;
preds.insert(Rc::clone(&rc), i);
pred_text.push(rc);
i
}
};
raw.push((si, pi, oi));
});
if let Some(g) = graph {
rete.release_named_graph(g);
}
let (shared, subj_only, obj_only) = nodes.sections();
let mut pred_order: Vec<u32> = (0..pred_text.len() as u32).collect();
pred_order.sort_unstable_by(|&a, &b| pred_text[a as usize].cmp(&pred_text[b as usize]));
let n = nodes.text.len();
let mut subject_id = vec![0u32; n];
let mut object_id = vec![0u32; n];
for (rank, &idx) in shared.iter().enumerate() {
let id = rank as u32 + 1;
subject_id[idx as usize] = id;
object_id[idx as usize] = id;
}
let base = shared.len() as u32;
for (rank, &idx) in subj_only.iter().enumerate() {
subject_id[idx as usize] = base + rank as u32 + 1;
}
for (rank, &idx) in obj_only.iter().enumerate() {
object_id[idx as usize] = base + rank as u32 + 1;
}
let mut predicate_id = vec![0u32; pred_text.len()];
for (rank, &idx) in pred_order.iter().enumerate() {
predicate_id[idx as usize] = rank as u32 + 1;
}
for t in raw.iter_mut() {
*t = (
subject_id[t.0 as usize],
predicate_id[t.1 as usize],
object_id[t.2 as usize],
);
}
raw.sort_unstable();
raw.dedup();
let bt = BitmapTriples::build(&raw);
let triple_count = bt.triple_count();
let mut arena = TermArena::with_capacity(n, 0);
let section = |idx: &[u32], arena: &mut TermArena| -> Vec<u32> {
idx.iter()
.map(|&i| arena.push_bytes(&nodes.text[i as usize]) as u32)
.collect()
};
let shared_ord = section(&shared, &mut arena);
let subjects_ord = section(&subj_only, &mut arena);
let objects_ord = section(&obj_only, &mut arena);
let predicates_ord: Vec<u32> = pred_order
.iter()
.map(|&i| arena.push_bytes(&pred_text[i as usize]) as u32)
.collect();
let size_strings = arena.text_len();
let mut out = Vec::new();
control_info(&mut out, CiType::Global, GLOBAL_FORMAT, &[]);
let header = header_ntriples(
source,
triple_count,
shared.len() as u64,
subj_only.len() as u64,
pred_order.len() as u64,
obj_only.len() as u64,
size_strings,
);
control_info(
&mut out,
CiType::Header,
HEADER_FORMAT,
&[("length", header.len().to_string())],
);
out.extend_from_slice(header.as_bytes());
control_info(
&mut out,
CiType::Dictionary,
DICTIONARY_FORMAT,
&[
("mapping", "1".to_string()),
("sizeStrings", size_strings.to_string()),
],
);
write_pfc_section(&mut out, &arena, &shared_ord);
write_pfc_section(&mut out, &arena, &subjects_ord);
write_pfc_section(&mut out, &arena, &predicates_ord);
write_pfc_section(&mut out, &arena, &objects_ord);
control_info(
&mut out,
CiType::Triples,
TRIPLES_FORMAT,
&[("order", ORDER_SPO.to_string())],
);
bt.write(&mut out);
Built {
bytes: out,
triples: triple_count,
shared: shared.len() as u64,
subjects: subj_only.len() as u64,
predicates: pred_order.len() as u64,
objects: obj_only.len() as u64,
}
}
#[allow(clippy::too_many_arguments)]
fn header_ntriples(
source: &str,
triples: u64,
shared: u64,
subjects: u64,
predicates: u64,
objects: u64,
size_strings: u64,
) -> String {
const HDT: &str = "http://purl.org/HDT/hdt#";
const VOID: &str = "http://rdfs.org/ns/void#";
let base = format!("<{source}>");
let mut h = String::new();
let mut t = |s: &str, p: &str, o: &str| {
h.push_str(s);
h.push(' ');
h.push_str(p);
h.push(' ');
h.push_str(o);
h.push_str(" .\n");
};
t(
&base,
"<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>",
&format!("<{HDT}Dataset>"),
);
t(
&base,
"<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>",
&format!("<{VOID}Dataset>"),
);
t(
&base,
&format!("<{VOID}triples>"),
&format!("\"{triples}\""),
);
t(
&base,
&format!("<{VOID}properties>"),
&format!("\"{predicates}\""),
);
t(
&base,
&format!("<{VOID}distinctSubjects>"),
&format!("\"{}\"", shared + subjects),
);
t(
&base,
&format!("<{VOID}distinctObjects>"),
&format!("\"{}\"", shared + objects),
);
t(&base, &format!("<{HDT}formatInformation>"), "_:format");
t("_:format", &format!("<{HDT}dictionary>"), "_:dictionary");
t("_:format", &format!("<{HDT}triples>"), "_:triples");
t(
"_:dictionary",
"<http://purl.org/dc/terms/format>",
&format!("<{HDT}dictionaryFour>"),
);
t(
"_:dictionary",
&format!("<{HDT}dictionarynumSharedSubjectObject>"),
&format!("\"{shared}\""),
);
t(
"_:dictionary",
&format!("<{HDT}dictionarymapping>"),
"\"1\"",
);
t(
"_:dictionary",
&format!("<{HDT}dictionarysizeStrings>"),
&format!("\"{size_strings}\""),
);
t(
"_:dictionary",
&format!("<{HDT}dictionaryblockSize>"),
&format!("\"{BLOCK_SIZE}\""),
);
t(
"_:triples",
"<http://purl.org/dc/terms/format>",
&format!("<{HDT}triplesBitmap>"),
);
t(
"_:triples",
&format!("<{HDT}triplesnumTriples>"),
&format!("\"{triples}\""),
);
t("_:triples", &format!("<{HDT}triplesOrder>"), "\"SPO\"");
h
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn human_bytes_reads_like_a_size() {
assert_eq!(human_bytes(0), "0.0 B");
assert_eq!(human_bytes(1536), "1.5 KiB");
assert_eq!(human_bytes(6_000_000_000), "5.6 GiB");
}
#[test]
fn the_header_is_well_formed_ntriples() {
let h = header_ntriples("file://x.rete", 5, 1, 2, 3, 4, 100);
for line in h.lines() {
assert!(line.ends_with(" ."), "not a statement: {line}");
assert!(line.split(' ').count() >= 4, "too few terms: {line}");
}
assert!(h.contains("\"SPO\""));
assert!(h.contains("dictionarynumSharedSubjectObject> \"1\""));
assert!(h.contains("distinctSubjects> \"3\""), "{h}");
assert!(h.contains("distinctObjects> \"5\""), "{h}");
}
#[test]
fn interning_records_both_roles_of_a_shared_term() {
let mut i = Interner::default();
let a = i.intern("<http://ex/a>", ROLE_SUBJECT);
let b = i.intern("<http://ex/b>", ROLE_OBJECT);
let a2 = i.intern("<http://ex/a>", ROLE_OBJECT);
assert_eq!(a, a2, "the same term interns once");
assert_eq!(i.roles[a as usize], ROLE_SUBJECT | ROLE_OBJECT);
assert_eq!(i.roles[b as usize], ROLE_OBJECT);
let (shared, subjects, objects) = i.sections();
assert_eq!(shared.len(), 1);
assert_eq!(subjects.len(), 0);
assert_eq!(objects.len(), 1);
}
#[test]
fn sections_are_sorted_by_the_stripped_form() {
let mut i = Interner::default();
i.intern("<http://ex/b>", ROLE_SUBJECT);
i.intern("<http://ex/a>", ROLE_SUBJECT);
i.intern("_:z", ROLE_SUBJECT);
let (_, subjects, _) = i.sections();
let texts: Vec<&[u8]> = subjects.iter().map(|&x| &*i.text[x as usize]).collect();
assert_eq!(
texts,
vec![b"_:z".as_slice(), b"http://ex/a", b"http://ex/b"]
);
}
}