1use crate::dictionary::Dictionary;
18use crate::header::{
19 Header, Section, SectionKind, FLAG_HAS_QUADS, FLAG_HAS_QUOTED_TRIPLES, FLAG_TILE_SYNOPSIS,
20 HEADER_LEN, MAGIC,
21};
22use crate::index::{GraphIndex, IndexPermutation, Pattern, PermSet, NUM_PERMS};
23use crate::meta::{ClassNode, CommunityDescriptor, LevelLinks, LevelRollup, PyramidMeta};
24use crate::pyramid::{build_dendrogram, project_graph, PyramidAlgo};
25use crate::reader::RangeReader;
26use crate::tiling::{choose_round_for_budget, summarize, SuperEdge};
27use crate::triples::Triple;
28use crate::varint::{read_uvarint, write_uvarint};
29
30pub const DEFAULT_TILE_BUDGET: usize = 64 * 1024;
32
33pub fn build_pyramid_meta(
42 dict: &Dictionary,
43 triples: &[(u32, u32, u32)],
44 budget: usize,
45) -> (Vec<u8>, u16) {
46 build_pyramid_meta_with(dict, triples, budget, None)
47}
48
49pub fn build_pyramid_meta_with(
53 dict: &Dictionary,
54 triples: &[(u32, u32, u32)],
55 budget: usize,
56 type_override: Option<&str>,
57) -> (Vec<u8>, u16) {
58 build_pyramid_meta_algo(dict, triples, budget, type_override, PyramidAlgo::Louvain)
59}
60
61pub fn build_pyramid_meta_algo(
68 dict: &Dictionary,
69 triples: &[(u32, u32, u32)],
70 budget: usize,
71 type_override: Option<&str>,
72 algo: PyramidAlgo,
73) -> (Vec<u8>, u16) {
74 build_pyramid_meta_layout(
75 dict,
76 triples,
77 budget,
78 type_override,
79 algo,
80 PyramidLayout::Combined,
81 )
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum PyramidLayout {
87 #[default]
91 Combined,
92 CommunityOnly,
97}
98
99pub fn build_pyramid_meta_layout(
101 dict: &Dictionary,
102 triples: &[(u32, u32, u32)],
103 budget: usize,
104 type_override: Option<&str>,
105 algo: PyramidAlgo,
106 layout: PyramidLayout,
107) -> (Vec<u8>, u16) {
108 let timing = std::env::var_os("RETE_BUILD_TIMING").is_some();
114 let mut t = timing.then(std::time::Instant::now);
115 let mut lap = |label: &str| {
116 if let Some(t0) = &mut t {
117 eprintln!(
118 " [pyramid] {label}: {:.0} ms",
119 t0.elapsed().as_secs_f64() * 1000.0
120 );
121 *t0 = std::time::Instant::now();
122 }
123 };
124
125 let louvain = |lap: &mut dyn FnMut(&str)| {
127 let g = project_graph(dict, triples);
128 lap("project_graph");
129 let d = build_dendrogram(&g);
130 lap("build_dendrogram (Louvain)");
131 d
132 };
133 let dend = match algo {
134 PyramidAlgo::Louvain => louvain(&mut lap),
135 PyramidAlgo::Types => {
136 match crate::schema_pyramid::build_type_dendrogram(dict, triples, type_override) {
137 Some(d) => {
138 lap("build_type_dendrogram");
139 d
140 }
141 None => {
142 eprintln!(
143 " [pyramid] --pyramid-algo types: no usable rdf:type \
144 predicate — falling back to louvain"
145 );
146 louvain(&mut lap)
147 }
148 }
149 }
150 };
151 let round = choose_round_for_budget(dict, triples, &dend, budget);
152 lap("choose_round_for_budget");
153 let summary = summarize(dict, triples, &dend, round);
154 lap("summarize");
155 if layout == PyramidLayout::CommunityOnly {
160 let descriptors = crate::schema_pyramid::build_community_descriptors(
163 dict,
164 triples,
165 &dend,
166 round,
167 type_override,
168 );
169 lap("build_community_descriptors");
170 let meta = PyramidMeta::new(round as u32, summary, &[]).with_schema(
171 Vec::new(),
172 Vec::new(),
173 Vec::new(),
174 descriptors,
175 Vec::new(),
176 Vec::new(),
177 Vec::new(),
178 );
179 let out = (meta.encode(), dend.rounds() as u16);
180 lap("encode");
181 return out;
182 }
183 let sp = crate::schema_pyramid::build_schema_pyramid_with(
184 dict,
185 triples,
186 &dend,
187 round,
188 type_override,
189 );
190 lap("build_schema_pyramid");
191 let predicate_stats = compute_predicate_stats(triples);
192 lap("compute_predicate_stats");
193 let char_sets = compute_char_sets(triples);
194 lap("compute_char_sets");
195 let label_index = compute_label_index(dict, triples);
196 lap("compute_label_index");
197 let meta = PyramidMeta::new(round as u32, summary, &[])
198 .with_schema(
199 sp.class_hierarchy,
200 sp.level_rollups,
201 sp.level_links,
202 sp.descriptors,
203 sp.subclass_cycles,
204 sp.disjoint_pairs,
205 sp.equivalent_pairs,
206 )
207 .with_predicate_stats(predicate_stats)
208 .with_char_sets(char_sets)
209 .with_label_index(label_index);
210 let out = (meta.encode(), dend.rounds() as u16);
211 lap("encode");
212 out
213}
214
215const LABEL_PREDICATES: &[&str] = crate::label_index::DIRECT_LABEL_PREDICATES;
220
221fn compute_label_index(
229 dict: &Dictionary,
230 triples: &[(u32, u32, u32)],
231) -> Vec<crate::meta::LabelEntry> {
232 use crate::terms::{is_literal, literal_lexical};
233 use std::collections::{HashMap, HashSet};
234 const MAX_LABELS: usize = 8192;
235
236 let label_pids: HashSet<u32> = LABEL_PREDICATES
238 .iter()
239 .filter_map(|p| dict.predicate_id(p))
240 .collect();
241 if label_pids.is_empty() {
242 return Vec::new();
243 }
244 let mut degree: HashMap<u32, u32> = HashMap::new();
246 for &(s, _p, _o) in triples {
247 *degree.entry(s).or_insert(0) += 1;
248 }
249 let mut seen: HashSet<(u32, String)> = HashSet::new();
251 let mut candidates: Vec<(u32, String, u32)> = Vec::new(); for &(s, p, o) in triples {
253 if !label_pids.contains(&p) {
254 continue;
255 }
256 let Some(term) = dict.object_term(o) else {
257 continue;
258 };
259 if !is_literal(&term) {
260 continue;
261 }
262 let Some(label) = literal_lexical(&term) else {
263 continue;
264 };
265 if label.is_empty() {
266 continue;
267 }
268 if seen.insert((s, label.to_lowercase())) {
269 candidates.push((*degree.get(&s).unwrap_or(&0), label, s));
270 }
271 }
272 if candidates.len() > MAX_LABELS {
275 candidates.sort_by(|a, b| {
276 b.0.cmp(&a.0)
277 .then_with(|| a.2.cmp(&b.2))
278 .then_with(|| a.1.cmp(&b.1))
279 });
280 candidates.truncate(MAX_LABELS);
281 }
282 candidates.sort_by(|a, b| {
284 a.1.to_lowercase()
285 .cmp(&b.1.to_lowercase())
286 .then_with(|| a.1.cmp(&b.1))
287 .then_with(|| a.2.cmp(&b.2))
288 });
289 candidates
290 .into_iter()
291 .map(|(_deg, label, subject)| crate::meta::LabelEntry { label, subject })
292 .collect()
293}
294
295pub fn compute_text_index(dict: &Dictionary, triples: &[(u32, u32, u32)]) -> Vec<u8> {
304 crate::text_index::build_section(
305 dict,
306 &crate::text_index::TextIndexScope::default(),
307 triples,
308 std::iter::empty(),
309 writer_codec(),
310 )
311}
312
313fn compute_char_sets(triples: &[(u32, u32, u32)]) -> Vec<crate::meta::CharSet> {
318 use std::collections::{BTreeSet, HashMap};
319 const MAX_CHAR_SETS: usize = 128;
320 let mut by_subject: HashMap<u32, BTreeSet<u32>> = HashMap::new();
321 for &(s, p, _o) in triples {
322 by_subject.entry(s).or_default().insert(p);
323 }
324 let mut shapes: HashMap<Vec<u32>, u64> = HashMap::new();
325 for set in by_subject.into_values() {
326 *shapes.entry(set.into_iter().collect()).or_insert(0) += 1;
327 }
328 let mut v: Vec<crate::meta::CharSet> = shapes
329 .into_iter()
330 .map(|(predicates, subjects)| crate::meta::CharSet {
331 predicates,
332 subjects,
333 })
334 .collect();
335 v.sort_by(|a, b| {
336 b.subjects
337 .cmp(&a.subjects)
338 .then_with(|| a.predicates.cmp(&b.predicates))
339 });
340 v.truncate(MAX_CHAR_SETS);
341 v
342}
343
344fn compute_predicate_stats(triples: &[(u32, u32, u32)]) -> Vec<crate::meta::PredStat> {
350 use std::collections::HashMap;
351 #[allow(clippy::type_complexity)]
352 let mut acc: HashMap<u32, (HashMap<u32, u32>, HashMap<u32, u32>, u64)> = HashMap::new();
353 for &(s, p, o) in triples {
354 let e = acc.entry(p).or_default();
355 *e.0.entry(s).or_insert(0) += 1;
356 *e.1.entry(o).or_insert(0) += 1;
357 e.2 += 1;
358 }
359 let mut stats: Vec<crate::meta::PredStat> = acc
360 .into_iter()
361 .map(|(predicate, (subj, obj, count))| crate::meta::PredStat {
362 predicate,
363 count,
364 distinct_subjects: subj.len() as u64,
365 distinct_objects: obj.len() as u64,
366 max_objects_per_subject: subj.values().copied().max().unwrap_or(0),
367 max_subjects_per_object: obj.values().copied().max().unwrap_or(0),
368 })
369 .collect();
370 stats.sort_by_key(|p| p.predicate);
371 stats
372}
373
374pub const CODEC_NONE: u8 = 0;
376pub const CODEC_ZSTD: u8 = 1;
378
379#[derive(Debug, thiserror::Error)]
380#[non_exhaustive]
381pub enum FileError {
382 #[error("header: {0}")]
383 Header(#[from] crate::header::HeaderError),
384 #[error("malformed container: {0}")]
385 Container(&'static str),
386 #[error("unknown codec: {0}")]
387 UnknownCodec(u8),
388 #[error("decompression failed: {0}")]
389 Decompress(std::io::Error),
390 #[error("io: {0}")]
391 Io(#[from] std::io::Error),
392}
393
394pub(crate) fn writer_codec() -> u8 {
397 if cfg!(feature = "compression") {
398 CODEC_ZSTD
399 } else {
400 CODEC_NONE
401 }
402}
403
404fn intersect_sorted(a: &[u32], b: &[u32]) -> Vec<u32> {
407 let mut out = Vec::with_capacity(a.len().min(b.len()));
408 let (mut i, mut j) = (0, 0);
409 while i < a.len() && j < b.len() {
410 match a[i].cmp(&b[j]) {
411 std::cmp::Ordering::Less => i += 1,
412 std::cmp::Ordering::Greater => j += 1,
413 std::cmp::Ordering::Equal => {
414 out.push(a[i]);
415 i += 1;
416 j += 1;
417 }
418 }
419 }
420 out
421}
422
423pub(crate) fn compress(codec: u8, bytes: &[u8]) -> Vec<u8> {
424 match codec {
425 #[cfg(feature = "compression")]
428 CODEC_ZSTD => {
429 let level = crate::write_opts::zstd_level();
430 match crate::write_opts::frame_shape() {
431 crate::write_opts::FrameShape::Stream => zstd::encode_all(bytes, level),
432 crate::write_opts::FrameShape::Sized => zstd::bulk::compress(bytes, level),
433 }
434 .expect("zstd encode is infallible in-memory")
435 }
436 _ => bytes.to_vec(),
437 }
438}
439
440#[cfg(not(target_arch = "wasm32"))]
458pub(crate) fn compress_to(
459 codec: u8,
460 src_len: u64,
461 src: &mut dyn std::io::Read,
462 dst: &mut dyn std::io::Write,
463) -> std::io::Result<()> {
464 match codec {
465 #[cfg(feature = "compression")]
466 CODEC_ZSTD => zstd_stream(src_len, src, dst),
467 _ => {
468 let _ = src_len;
469 std::io::copy(src, dst).map(|_| ())
470 }
471 }
472}
473
474pub(crate) fn compress_streamed(codec: u8, bytes: &[u8]) -> Vec<u8> {
480 match codec {
481 #[cfg(feature = "compression")]
482 CODEC_ZSTD => {
483 let mut out = Vec::with_capacity(bytes.len() / 4 + 64);
484 zstd_stream(bytes.len() as u64, &mut &bytes[..], &mut out)
485 .expect("zstd encode is infallible in-memory");
486 out
487 }
488 _ => bytes.to_vec(),
489 }
490}
491
492#[cfg(feature = "compression")]
494fn zstd_stream(
495 src_len: u64,
496 src: &mut dyn std::io::Read,
497 dst: &mut dyn std::io::Write,
498) -> std::io::Result<()> {
499 let mut enc = zstd::stream::Encoder::new(dst, crate::write_opts::zstd_level())?;
500 if crate::write_opts::frame_shape() == crate::write_opts::FrameShape::Sized {
501 enc.set_pledged_src_size(Some(src_len))?;
502 enc.include_contentsize(true)?;
503 }
504 let copied = std::io::copy(src, &mut enc)?;
505 if copied != src_len {
506 return Err(std::io::Error::other(format!(
507 "zstd stream: {copied} bytes read, {src_len} announced"
508 )));
509 }
510 enc.finish()?;
511 Ok(())
512}
513
514pub(crate) fn decompress(codec: u8, bytes: &[u8]) -> Result<Vec<u8>, FileError> {
515 match codec {
516 CODEC_NONE => Ok(bytes.to_vec()),
517 #[cfg(feature = "compression")]
526 CODEC_ZSTD => zstd::decode_all(bytes).map_err(FileError::Decompress),
527 #[cfg(not(feature = "compression"))]
532 CODEC_ZSTD => {
533 use std::io::Read;
534 let mut dec = ruzstd::decoding::StreamingDecoder::new(bytes)
535 .map_err(|e| FileError::Decompress(std::io::Error::other(e.to_string())))?;
536 let mut out = Vec::new();
537 dec.read_to_end(&mut out).map_err(FileError::Decompress)?;
538 Ok(out)
539 }
540 other => Err(FileError::UnknownCodec(other)),
541 }
542}
543
544const TILE_COALESCE_GAP: u64 = 4096;
548
549const DICT_COALESCE_GAP: u64 = 64 * 1024;
555
556pub const DEFAULT_EXPORT_BUDGET_MB: u64 = 4096;
563
564pub const DEFAULT_QUERY_BUDGET_MB: u64 = DEFAULT_EXPORT_BUDGET_MB;
576
577pub fn spill_threshold_for(budget: u64) -> u64 {
581 budget / 8
582}
583
584const MIN_DICT_CACHE_CAP: u64 = 64 << 20; const TRANSIENT_RESERVE_MAX: u64 = 128 << 20; #[derive(Debug, Clone, Copy)]
605pub struct MemoryBudget {
606 pub block_cache: u64,
608 pub dict_cache: u64,
610 pub tile_cache: u64,
612}
613
614pub fn split_memory_budget(total: u64) -> MemoryBudget {
625 if total == u64::MAX {
626 return MemoryBudget {
627 block_cache: u64::MAX,
628 dict_cache: u64::MAX,
629 tile_cache: u64::MAX,
630 };
631 }
632 let block_cache = (total / 8).min(crate::block_cache::DEFAULT_CACHE_CAP);
635 let tile_cache = (total / 16).min(256 << 20);
638 let reserve = (total / 32).min(TRANSIENT_RESERVE_MAX);
644 let dict_cache = total
647 .saturating_sub(block_cache)
648 .saturating_sub(tile_cache)
649 .saturating_sub(reserve)
650 .max(MIN_DICT_CACHE_CAP);
651 MemoryBudget {
652 block_cache,
653 dict_cache,
654 tile_cache,
655 }
656}
657
658#[derive(Debug, Clone, Copy, Default)]
661pub struct FixedOverhead {
662 pub dict_directory: u64,
664 pub index_directory: u64,
666 pub named_graphs: u64,
668}
669
670impl FixedOverhead {
671 pub fn total(&self) -> u64 {
673 self.dict_directory + self.index_directory + self.named_graphs
674 }
675}
676
677fn fmt_cap(cap: u64) -> String {
679 if cap == u64::MAX {
680 "unlimited".to_string()
681 } else {
682 format!("{}MiB", cap >> 20)
683 }
684}
685
686fn read_coalesced<R: RangeReader + ?Sized>(
692 reader: &R,
693 ranges: &[ByteRange],
694 gap: u64,
695) -> Option<Vec<Vec<u8>>> {
696 let mut spans: Vec<(u64, u64)> = Vec::new();
699 let mut span_of: Vec<usize> = Vec::with_capacity(ranges.len());
700 let mut i = 0;
701 while i < ranges.len() {
702 let start = ranges[i].offset;
703 let mut end = ranges[i].offset.checked_add(ranges[i].len)?;
704 let mut j = i + 1;
705 while j < ranges.len() {
706 let r = &ranges[j];
707 if r.offset < end || r.offset - end > gap {
708 break;
709 }
710 end = r.offset.checked_add(r.len)?;
711 j += 1;
712 }
713 let si = spans.len();
714 spans.push((start, end - start));
715 for _ in i..j {
716 span_of.push(si);
717 }
718 i = j;
719 }
720 let blobs = reader.read_many(&spans).ok()?;
721 if blobs.len() != spans.len() {
722 return None;
723 }
724 let mut out = Vec::with_capacity(ranges.len());
725 for (k, r) in ranges.iter().enumerate() {
726 let (span_start, _) = spans[span_of[k]];
727 let blob = &blobs[span_of[k]];
728 let lo = (r.offset - span_start) as usize;
729 let hi = lo.checked_add(r.len as usize)?;
730 out.push(blob.get(lo..hi)?.to_vec());
731 }
732 Some(out)
733}
734
735fn content_hash(parts: &[&[u8]]) -> [u8; 16] {
738 let mut h = blake3::Hasher::new();
739 for p in parts {
740 h.update(p);
741 }
742 let mut out = [0u8; 16];
743 out.copy_from_slice(&h.finalize().as_bytes()[..16]);
744 out
745}
746
747pub type TermTriple = (String, String, String);
749
750#[derive(Debug, Clone, Copy, PartialEq, Eq)]
753pub struct DumpPlan {
754 pub scan: Option<crate::index::ScanPlan>,
758 pub dictionary_bytes: u64,
767}
768
769#[derive(Debug, Clone)]
773pub struct LayoutSegment {
774 pub kind: &'static str,
775 pub label: String,
776 pub offset: u64,
777 pub len: u64,
778}
779
780#[derive(Debug, Clone, Copy, PartialEq, Eq)]
782pub struct ByteRange {
783 pub offset: u64,
784 pub len: u64,
785}
786
787impl ByteRange {
788 pub fn end(self) -> u64 {
789 self.offset + self.len
790 }
791}
792
793#[derive(Debug, Clone, PartialEq, Eq)]
795pub struct TripleProvenance {
796 pub terms: TermTriple,
798 pub ids: Triple,
800 pub graph: Option<String>,
802 pub matched_pattern: Pattern,
804 pub index_permutation: IndexPermutation,
806 pub dictionary_range: ByteRange,
808 pub index_range: ByteRange,
810 pub index_section_range: ByteRange,
813 pub pyramid_range: Option<ByteRange>,
815 pub tile: Option<String>,
819 pub tile_range: Option<ByteRange>,
822}
823
824fn encode_container(sections: &[&[u8]], codec: u8) -> Vec<u8> {
828 let mut out = Vec::new();
829 write_uvarint(&mut out, sections.len() as u64);
830 for s in sections {
831 let payload = compress(codec, s);
832 write_uvarint(&mut out, payload.len() as u64);
833 out.extend_from_slice(&payload);
834 }
835 out
836}
837
838fn decode_container(bytes: &[u8], codec: u8) -> Result<Vec<Vec<u8>>, FileError> {
840 let (n, mut pos) = read_uvarint(bytes).ok_or(FileError::Container("truncated count"))?;
841 let mut out = Vec::with_capacity((n as usize).min(bytes.len()));
845 for _ in 0..n {
846 let (len, used) =
847 read_uvarint(&bytes[pos..]).ok_or(FileError::Container("truncated length"))?;
848 pos += used;
849 let end = pos + len as usize;
850 if end > bytes.len() {
851 return Err(FileError::Container("section overruns buffer"));
852 }
853 out.push(decompress(codec, &bytes[pos..end])?);
854 pos = end;
855 }
856 Ok(out)
857}
858
859fn checked_end(off: u64, len: u64) -> Result<u64, FileError> {
860 off.checked_add(len)
861 .ok_or(FileError::Container("section range overflows"))
862}
863
864const DICT_CHUNK_BUDGET: usize = crate::write_opts::DEFAULT_DICT_CHUNK_BYTES;
867
868fn encode_chunked_dict_section(raw: &[u8], codec: u8) -> Vec<u8> {
885 let meta = crate::dict::parse_meta(raw).unwrap_or(crate::dict::SectionMeta {
886 term_count: 0,
887 restart_interval: 1,
888 restart_offsets: Vec::new(),
889 });
890 let body_start = meta
891 .restart_offsets
892 .first()
893 .copied()
894 .unwrap_or(raw.len() as u64);
895 let header = &raw[..(body_start.min(raw.len() as u64)) as usize];
896
897 let chunk_budget = crate::write_opts::dict_chunk_budget() as u64;
899 let n_runs = meta.restart_offsets.len();
900 let mut bounds: Vec<(usize, u64, u64)> = Vec::new(); let mut r = 0;
902 while r < n_runs {
903 let start = meta.restart_offsets[r];
904 let mut r2 = r + 1;
905 while r2 < n_runs && meta.restart_offsets[r2] - start < chunk_budget {
906 r2 += 1;
907 }
908 let end = if r2 < n_runs {
909 meta.restart_offsets[r2]
910 } else {
911 raw.len() as u64
912 };
913 bounds.push((r, start, end));
914 r = r2;
915 }
916
917 #[cfg(feature = "parallel")]
921 let compressed: Vec<Vec<u8>> = {
922 use rayon::prelude::*;
923 bounds
924 .par_iter()
925 .map(|&(_, s, e)| compress(codec, &raw[s as usize..e as usize]))
926 .collect()
927 };
928 #[cfg(not(feature = "parallel"))]
929 let compressed: Vec<Vec<u8>> = bounds
930 .iter()
931 .map(|&(_, s, e)| compress(codec, &raw[s as usize..e as usize]))
932 .collect();
933 let mut out = Vec::new();
934 write_uvarint(&mut out, header.len() as u64);
935 out.extend_from_slice(header);
936 write_uvarint(&mut out, bounds.len() as u64);
937 let mut prev_run = 0usize;
938 let mut prev_last: Option<Vec<u8>> = None;
942 for (i, (&(first_run, start, end), comp)) in bounds.iter().zip(&compressed).enumerate() {
943 let key = if i == 0 {
944 Vec::new()
946 } else {
947 let first = crate::dict::run_first_term(raw, start as usize).unwrap_or_default();
948 match &prev_last {
949 None => first,
952 Some(pl) => crate::dict::shortest_separator(pl, &first),
953 }
954 };
955 if i + 1 < bounds.len() {
956 let last_run_off = meta.restart_offsets[bounds[i + 1].0 - 1] as usize;
958 prev_last = crate::dict::run_last_term(raw, last_run_off, end as usize);
959 }
960 write_uvarint(&mut out, (first_run - prev_run) as u64);
961 write_uvarint(&mut out, key.len() as u64);
962 out.extend_from_slice(&key);
963 write_uvarint(&mut out, comp.len() as u64);
964 prev_run = first_run;
965 }
966 for comp in &compressed {
967 out.extend_from_slice(comp);
968 }
969 out
970}
971
972struct DictChunkEntry {
976 first_run: usize,
977 key: Vec<u8>,
978 body_start: u64,
979 start: u64,
980 end: u64,
981}
982
983fn parse_chunked_dict_dir(
987 bytes: &[u8],
988 total_len: u64,
989) -> Result<(crate::dict::SectionMeta, Vec<DictChunkEntry>), FileError> {
990 let mut pos = 0usize;
991 let take = |pos: &mut usize| -> Result<u64, FileError> {
992 let (v, n) = read_uvarint(bytes.get(*pos..).unwrap_or(&[]))
993 .ok_or(FileError::Container("truncated dict chunk directory"))?;
994 *pos += n;
995 Ok(v)
996 };
997 let header_len = take(&mut pos)? as usize;
998 let header = bytes
999 .get(pos..pos.saturating_add(header_len))
1000 .ok_or(FileError::Container("truncated dict header"))?;
1001 let meta = crate::dict::parse_meta(header)
1002 .map_err(|_| FileError::Container("malformed dict header"))?;
1003 pos += header_len;
1004
1005 let num_chunks = take(&mut pos)? as usize;
1006 let mut entries = Vec::with_capacity(num_chunks.min(bytes.len()));
1007 let mut lens = Vec::with_capacity(num_chunks.min(bytes.len()));
1008 let mut prev_run = 0usize;
1009 for _ in 0..num_chunks {
1010 let drun = take(&mut pos)? as usize;
1011 let klen = take(&mut pos)? as usize;
1012 let key = bytes
1013 .get(pos..pos.saturating_add(klen))
1014 .ok_or(FileError::Container("truncated dict chunk key"))?
1015 .to_vec();
1016 pos += klen;
1017 let clen = take(&mut pos)?;
1018 let first_run = prev_run + drun;
1019 let body_start = meta
1020 .restart_offsets
1021 .get(first_run)
1022 .copied()
1023 .ok_or(FileError::Container("dict chunk run out of range"))?;
1024 entries.push(DictChunkEntry {
1025 first_run,
1026 key,
1027 body_start,
1028 start: 0,
1029 end: 0,
1030 });
1031 lens.push(clen);
1032 prev_run = first_run;
1033 }
1034 let mut start = pos as u64;
1035 for (e, len) in entries.iter_mut().zip(lens) {
1036 let end = start
1037 .checked_add(len)
1038 .filter(|&e| e <= total_len)
1039 .ok_or(FileError::Container("dict chunk overruns section"))?;
1040 e.start = start;
1041 e.end = end;
1042 start = end;
1043 }
1044 Ok((meta, entries))
1045}
1046
1047fn read_dict_dir_ranged<R: RangeReader>(
1059 reader: &R,
1060 section: ByteRange,
1061) -> Result<(crate::dict::SectionMeta, Vec<DictChunkEntry>), FileError> {
1062 let total = section.len;
1063 let init = 8192.min(total); let head = reader.read_at(section.offset, init)?;
1071 let (header_len, n0) =
1072 read_uvarint(&head).ok_or(FileError::Container("truncated dict header len"))?;
1073 let hbase = n0; let (term_count, n1) = read_uvarint(head.get(hbase..).unwrap_or(&[]))
1075 .ok_or(FileError::Container("truncated dict term_count"))?;
1076 let (restart_interval, _n2) = read_uvarint(head.get(hbase + n1..).unwrap_or(&[]))
1077 .ok_or(FileError::Container("truncated dict interval"))?;
1078 if restart_interval == 0 {
1079 return Err(FileError::Container("zero restart interval"));
1080 }
1081 let dir_start = (hbase as u64)
1084 .checked_add(header_len)
1085 .filter(|&d| d <= total)
1086 .ok_or(FileError::Container("dict header overruns section"))?;
1087 let dir_total = total - dir_start;
1088 let meta = crate::dict::SectionMeta {
1089 term_count: term_count as u32,
1090 restart_interval: restart_interval as u32,
1091 restart_offsets: Vec::new(),
1092 };
1093 let finish = |mut entries: Vec<DictChunkEntry>| {
1094 for e in &mut entries {
1095 e.start += dir_start; e.end += dir_start;
1097 }
1098 (meta.clone(), entries)
1099 };
1100 let mut have: Vec<u8> = Vec::new();
1104 if dir_start < head.len() as u64 {
1105 match parse_chunk_dir_only(&head[dir_start as usize..], dir_total)? {
1106 ChunkDirParse::Done(entries) => return Ok(finish(entries)),
1107 ChunkDirParse::Truncated { .. } => have = head[dir_start as usize..].to_vec(),
1108 }
1109 }
1110 let mut want = 4096u64.min(dir_total).max(1);
1123 loop {
1124 let held = have.len() as u64;
1125 if want > held {
1126 let extra = reader.read_at(section.offset + dir_start + held, want - held)?;
1127 if extra.is_empty() {
1128 return Err(FileError::Container("truncated dict chunk directory"));
1129 }
1130 have.extend_from_slice(&extra);
1131 }
1132 let held = have.len() as u64;
1133 match parse_chunk_dir_only(&have, dir_total)? {
1134 ChunkDirParse::Done(entries) => return Ok(finish(entries)),
1135 ChunkDirParse::Truncated { .. } if held >= dir_total => {
1136 return Err(FileError::Container("truncated dict chunk directory"));
1137 }
1138 ChunkDirParse::Truncated {
1139 parsed,
1140 used,
1141 total,
1142 } => {
1143 let est = if parsed >= 16 && used > 0 {
1148 let whole = ((used as u64) / (parsed as u64)).saturating_mul(total as u64);
1149 whole.saturating_add(whole / 8).saturating_add(64)
1150 } else {
1151 held.saturating_mul(2)
1153 };
1154 want = est
1155 .max(held.saturating_add(1))
1156 .min(held.saturating_mul(4))
1157 .min(dir_total);
1158 }
1159 }
1160 }
1161}
1162
1163enum ChunkDirParse {
1168 Done(Vec<DictChunkEntry>),
1169 Truncated {
1170 parsed: usize,
1172 used: usize,
1174 total: usize,
1176 },
1177}
1178
1179fn parse_chunk_dir_only(dir: &[u8], dir_total: u64) -> Result<ChunkDirParse, FileError> {
1189 let mut pos = 0usize;
1190 let take = |pos: &mut usize| -> Option<u64> {
1191 let (v, n) = read_uvarint(dir.get(*pos..).unwrap_or(&[]))?;
1192 *pos += n;
1193 Some(v)
1194 };
1195 let Some(num_chunks) = take(&mut pos) else {
1196 return Ok(ChunkDirParse::Truncated {
1197 parsed: 0,
1198 used: 0,
1199 total: 0,
1200 });
1201 };
1202 let num_chunks = num_chunks as usize;
1203 let mut entries = Vec::with_capacity(num_chunks.min(dir.len()));
1204 let mut lens: Vec<u64> = Vec::with_capacity(num_chunks.min(dir.len()));
1205 let mut prev_run = 0usize;
1206 for _ in 0..num_chunks {
1207 let entry_start = pos;
1208 let short = ChunkDirParse::Truncated {
1209 parsed: entries.len(),
1210 used: entry_start,
1211 total: num_chunks,
1212 };
1213 let (Some(drun), Some(klen)) = (take(&mut pos), take(&mut pos)) else {
1214 return Ok(short);
1215 };
1216 let (drun, klen) = (drun as usize, klen as usize);
1217 let Some(key) = dir.get(pos..pos.saturating_add(klen)) else {
1218 return Ok(short);
1219 };
1220 let key = key.to_vec();
1221 pos += klen;
1222 let Some(clen) = take(&mut pos) else {
1223 return Ok(short);
1224 };
1225 let first_run = prev_run + drun;
1226 entries.push(DictChunkEntry {
1227 first_run,
1228 key,
1229 body_start: 0,
1230 start: 0,
1231 end: 0,
1232 });
1233 lens.push(clen);
1234 prev_run = first_run;
1235 }
1236 let mut start = pos as u64;
1237 for (e, len) in entries.iter_mut().zip(lens) {
1238 let end = start
1239 .checked_add(len)
1240 .filter(|&e| e <= dir_total)
1241 .ok_or(FileError::Container("dict chunk overruns section"))?;
1242 e.start = start;
1243 e.end = end;
1244 start = end;
1245 }
1246 Ok(ChunkDirParse::Done(entries))
1247}
1248
1249fn decode_chunked_dict_section(
1253 payload: &[u8],
1254 codec: u8,
1255 cache: std::sync::Arc<crate::chunk_cache::ChunkCache>,
1256 section_index: u8,
1257) -> Result<crate::dict::ChunkedSection, FileError> {
1258 let (meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64)?;
1259 let chunks = entries
1260 .into_iter()
1261 .map(|e| {
1262 let body = decompress(codec, &payload[e.start as usize..e.end as usize])?;
1263 Ok((
1264 crate::dict::SectionChunk::new(e.first_run, e.key, e.body_start),
1265 body,
1266 ))
1267 })
1268 .collect::<Result<Vec<_>, FileError>>()?;
1269 Ok(crate::dict::ChunkedSection::resident(
1270 meta,
1271 chunks,
1272 cache,
1273 section_index,
1274 ))
1275}
1276
1277fn decode_dictionary_container(bytes: &[u8], codec: u8) -> Result<Dictionary, FileError> {
1278 let dsecs = decode_container(bytes, CODEC_NONE)?;
1279 if dsecs.len() != 4 {
1280 return Err(FileError::Container("expected 4 dictionary sections"));
1281 }
1282 let cache = crate::chunk_cache::ChunkCache::unlimited_arc();
1285 let mut sections = Vec::with_capacity(4);
1286 for (si, sec) in dsecs.iter().enumerate() {
1287 sections.push(decode_chunked_dict_section(
1288 sec,
1289 codec,
1290 cache.clone(),
1291 si as u8,
1292 )?);
1293 }
1294 let arr: [crate::dict::ChunkedSection; 4] = sections
1295 .try_into()
1296 .map_err(|_| FileError::Container("expected 4 dictionary sections"))?;
1297 Ok(Dictionary::from_chunked_sections(arr))
1298}
1299
1300fn encode_tiled_section(index: &GraphIndex, si: usize, codec: u8) -> Vec<u8> {
1306 let tiles = index.tile_sections()[si];
1307 let n = tiles.len();
1308 #[cfg(feature = "parallel")]
1315 let compressed: Vec<Vec<u8>> = {
1316 use rayon::prelude::*;
1317 (0..n)
1318 .into_par_iter()
1319 .map(|ti| compress(codec, &index.tile_body(si, ti)))
1320 .collect()
1321 };
1322 #[cfg(not(feature = "parallel"))]
1323 let compressed: Vec<Vec<u8>> = (0..n)
1324 .map(|ti| compress(codec, &index.tile_body(si, ti)))
1325 .collect();
1326 let mut out = Vec::new();
1327 write_uvarint(&mut out, tiles.len() as u64);
1328 let mut prev_min = 0u32;
1329 for (tile, comp) in tiles.iter().zip(&compressed) {
1330 let (min_a, max_a) = tile.leading_range();
1331 write_uvarint(&mut out, (min_a - prev_min) as u64);
1332 write_uvarint(&mut out, (max_a - min_a) as u64);
1333 write_uvarint(&mut out, comp.len() as u64);
1334 prev_min = min_a;
1335 }
1336 for comp in &compressed {
1337 out.extend_from_slice(comp);
1338 }
1339 for ti in 0..n {
1347 let body = index.tile_body(si, ti);
1348 let (min_b, max_b, min_c, max_c) = match crate::triples::TripleBlock::parse(&body) {
1349 Ok(b) => {
1350 let z = b.zone();
1351 (z.min_b, z.max_b, z.min_c, z.max_c)
1352 }
1353 Err(_) => (0, u32::MAX, 0, u32::MAX),
1354 };
1355 write_uvarint(&mut out, min_b as u64);
1356 write_uvarint(&mut out, (max_b - min_b) as u64);
1357 write_uvarint(&mut out, min_c as u64);
1358 write_uvarint(&mut out, (max_c - min_c) as u64);
1359 }
1360 out
1361}
1362
1363struct TileDirEntry {
1366 min_a: u32,
1367 max_a: u32,
1368 start: u64,
1369 end: u64,
1370}
1371
1372type TileSynopsis = (u32, u32, u32, u32);
1374
1375fn parse_tile_synopsis(
1383 payload: &[u8],
1384 trailer_start: usize,
1385 num_tiles: usize,
1386) -> Option<Vec<TileSynopsis>> {
1387 let mut pos = trailer_start;
1388 let take = |pos: &mut usize| -> Option<u32> {
1389 let (v, n) = read_uvarint(payload.get(*pos..)?)?;
1390 *pos += n;
1391 u32::try_from(v).ok()
1392 };
1393 let mut out = Vec::with_capacity(num_tiles.min(payload.len()));
1394 for _ in 0..num_tiles {
1395 let min_b = take(&mut pos)?;
1396 let max_b = min_b.checked_add(take(&mut pos)?)?;
1397 let min_c = take(&mut pos)?;
1398 let max_c = min_c.checked_add(take(&mut pos)?)?;
1399 out.push((min_b, max_b, min_c, max_c));
1400 }
1401 Some(out)
1402}
1403
1404fn parse_tile_directory(bytes: &[u8], total_len: u64) -> Result<Vec<TileDirEntry>, FileError> {
1409 let mut pos = 0usize;
1410 let take = |pos: &mut usize| -> Result<u64, FileError> {
1411 let (v, n) = read_uvarint(bytes.get(*pos..).unwrap_or(&[]))
1412 .ok_or(FileError::Container("truncated tile directory"))?;
1413 *pos += n;
1414 Ok(v)
1415 };
1416 let num_tiles = take(&mut pos)? as usize;
1417 let mut entries = Vec::with_capacity(num_tiles.min(bytes.len()));
1418 let mut prev_min = 0u32;
1419 let mut lens = Vec::with_capacity(num_tiles.min(bytes.len()));
1420 for _ in 0..num_tiles {
1421 let dmin = take(&mut pos)? as u32;
1422 let span = take(&mut pos)? as u32;
1423 let len = take(&mut pos)?;
1424 let min_a = prev_min.wrapping_add(dmin);
1425 entries.push(TileDirEntry {
1426 min_a,
1427 max_a: min_a.wrapping_add(span),
1428 start: 0,
1429 end: 0,
1430 });
1431 lens.push(len);
1432 prev_min = min_a;
1433 }
1434 let mut start = pos as u64;
1435 for (e, len) in entries.iter_mut().zip(lens) {
1436 let end = start
1437 .checked_add(len)
1438 .filter(|&e| e <= total_len)
1439 .ok_or(FileError::Container("tile overruns section"))?;
1440 e.start = start;
1441 e.end = end;
1442 start = end;
1443 }
1444 Ok(entries)
1445}
1446
1447fn read_tile_directory_ranged<R: RangeReader>(
1451 reader: &R,
1452 section: ByteRange,
1453) -> Result<Vec<TileDirEntry>, FileError> {
1454 let total = section.len;
1455 let mut prefetch = 4096u64.min(total);
1456 loop {
1457 let prefix = reader.read_at(section.offset, prefetch)?;
1458 match parse_tile_directory(&prefix, total) {
1459 Ok(dir) => return Ok(dir),
1460 Err(_) if prefetch < total => prefetch = prefetch.saturating_mul(2).min(total),
1461 Err(e) => return Err(e),
1462 }
1463 }
1464}
1465
1466fn read_tile_synopsis_ranged<R: RangeReader>(
1473 reader: &R,
1474 section: ByteRange,
1475 dir: &[TileDirEntry],
1476) -> Vec<Option<TileSynopsis>> {
1477 let n = dir.len();
1478 let none = vec![None; n];
1479 let trailer_start = dir.iter().map(|e| e.end).max().unwrap_or(0);
1480 let total = section.len;
1481 if n == 0 || trailer_start >= total {
1482 return none; }
1484 let trailer_len = total - trailer_start;
1485 let Ok(bytes) = reader.read_at(section.offset + trailer_start, trailer_len) else {
1486 return none;
1487 };
1488 match parse_tile_synopsis(&bytes, 0, n) {
1489 Some(v) => v.into_iter().map(Some).collect(),
1490 None => none,
1491 }
1492}
1493
1494fn tile_file_ranges(
1498 index_bytes: &[u8],
1499 container_offset: u64,
1500 section_ranges: &[ByteRange; NUM_PERMS],
1501) -> [Vec<(u32, u32, ByteRange)>; NUM_PERMS] {
1502 let mut out: [Vec<(u32, u32, ByteRange)>; NUM_PERMS] = Default::default();
1503 for (section, range) in out.iter_mut().zip(section_ranges) {
1504 if range.len == 0 || range.offset < container_offset {
1508 continue;
1509 }
1510 let start = (range.offset - container_offset) as usize;
1511 let Some(payload) = index_bytes.get(start..start + range.len as usize) else {
1512 continue;
1513 };
1514 if let Ok(dir) = parse_tile_directory(payload, payload.len() as u64) {
1515 *section = dir
1516 .into_iter()
1517 .map(|e| {
1518 (
1519 e.min_a,
1520 e.max_a,
1521 ByteRange {
1522 offset: range.offset + e.start,
1523 len: (e.end - e.start),
1524 },
1525 )
1526 })
1527 .collect();
1528 }
1529 }
1530 out
1531}
1532
1533fn decode_tiled_section(payload: &[u8], codec: u8) -> Result<Vec<(u32, u32, Vec<u8>)>, FileError> {
1536 parse_tile_directory(payload, payload.len() as u64)?
1537 .into_iter()
1538 .map(|e| {
1539 Ok((
1540 e.min_a,
1541 e.max_a,
1542 decompress(codec, &payload[e.start as usize..e.end as usize])?,
1543 ))
1544 })
1545 .collect()
1546}
1547
1548fn decode_index_container(
1558 bytes: &[u8],
1559 codec: u8,
1560 perms: PermSet,
1561) -> Result<GraphIndex, FileError> {
1562 let isecs = decode_container(bytes, CODEC_NONE)?;
1563 if isecs.len() != perms.len() {
1564 return Err(FileError::Container(
1565 "index container section count does not match the header permutation mask",
1566 ));
1567 }
1568 decode_index_sections(isecs, codec, perms)
1569}
1570
1571fn decode_index_sections(
1572 mut isecs: Vec<Vec<u8>>,
1573 codec: u8,
1574 perms: PermSet,
1575) -> Result<GraphIndex, FileError> {
1576 let mut sections: [Vec<(u32, u32, Vec<u8>)>; NUM_PERMS] = Default::default();
1577 for (perm, sec) in perms.iter().zip(isecs.iter_mut()) {
1578 sections[perm.section_index()] = decode_tiled_section(sec, codec)?;
1579 }
1580 Ok(GraphIndex::from_tiles(sections, perms))
1581}
1582
1583fn decode_root_index_container(
1588 bytes: &[u8],
1589 header: &Header,
1590) -> Result<(GraphIndex, Option<[u8; 16]>), FileError> {
1591 if !header.has_quad_index() {
1592 return Ok((
1593 decode_index_container(bytes, header.block_codec, header.perms)?,
1594 None,
1595 ));
1596 }
1597 let mut isecs = decode_container(bytes, CODEC_NONE)?;
1598 if isecs.len() != header.index_container_sections() {
1599 return Err(FileError::Container(
1600 "index container section count does not match the header permutation mask",
1601 ));
1602 }
1603 let seal = isecs.pop().unwrap_or_default();
1604 let digest = crate::quad::parse_seal(&seal)?;
1605 Ok((
1606 decode_index_sections(isecs, header.block_codec, header.perms)?,
1607 Some(digest),
1608 ))
1609}
1610
1611fn check_named_layout(header: &Header) -> Result<(), FileError> {
1616 let quad = header.quad_index_section().is_some();
1617 if header.has_quad_index() {
1618 if !quad {
1619 return Err(FileError::Container(
1620 "quad-index flag set but the file has no quad-index section",
1621 ));
1622 }
1623 if header.named_graphs_len > 0 {
1624 return Err(FileError::Container(
1625 "a file cannot carry both a quad index and a named-graphs section",
1626 ));
1627 }
1628 crate::quad::check_perms(header.perms)?;
1629 } else if quad {
1630 return Err(FileError::Container(
1631 "quad-index section present but the header does not flag it",
1632 ));
1633 }
1634 Ok(())
1635}
1636
1637fn container_section_payload_ranges(
1638 bytes: &[u8],
1639 container_offset: u64,
1640 expected_sections: usize,
1641) -> Result<Vec<ByteRange>, FileError> {
1642 let (section_count, mut pos) =
1643 read_uvarint(bytes).ok_or(FileError::Container("truncated count"))?;
1644 let section_count = usize::try_from(section_count)
1645 .map_err(|_| FileError::Container("section count too large"))?;
1646 if section_count != expected_sections {
1647 return Err(FileError::Container("unexpected section count"));
1648 }
1649
1650 let mut ranges = Vec::with_capacity(section_count);
1651 for _ in 0..section_count {
1652 let remaining = bytes
1653 .get(pos..)
1654 .ok_or(FileError::Container("truncated length"))?;
1655 let (payload_len, used) =
1656 read_uvarint(remaining).ok_or(FileError::Container("truncated length"))?;
1657 pos = pos
1658 .checked_add(used)
1659 .ok_or(FileError::Container("section range overflows"))?;
1660 let payload_len_usize = usize::try_from(payload_len)
1661 .map_err(|_| FileError::Container("section length too large"))?;
1662 let payload_end = pos
1663 .checked_add(payload_len_usize)
1664 .ok_or(FileError::Container("section range overflows"))?;
1665 if payload_end > bytes.len() {
1666 return Err(FileError::Container("section overruns buffer"));
1667 }
1668 ranges.push(ByteRange {
1669 offset: checked_end(container_offset, pos as u64)?,
1670 len: payload_len,
1671 });
1672 pos = payload_end;
1673 }
1674
1675 Ok(ranges)
1676}
1677
1678fn decode_index_section_ranges(
1682 bytes: &[u8],
1683 container_offset: u64,
1684 perms: PermSet,
1685 sections: usize,
1686) -> Result<[ByteRange; NUM_PERMS], FileError> {
1687 let ranges = container_section_payload_ranges(bytes, container_offset, sections)?;
1688 let mut out = [ByteRange { offset: 0, len: 0 }; NUM_PERMS];
1689 for (perm, range) in perms.iter().zip(ranges) {
1690 out[perm.section_index()] = range;
1691 }
1692 Ok(out)
1693}
1694
1695fn read_uvarint_at<R: RangeReader + ?Sized>(
1696 reader: &R,
1697 absolute_offset: u64,
1698 container_end: u64,
1699) -> Result<(u64, u64), FileError> {
1700 if absolute_offset >= container_end {
1701 return Err(FileError::Container("truncated container varint"));
1702 }
1703 let remaining = container_end - absolute_offset;
1704 let probe_len = remaining.min(10);
1705 let bytes = reader.read_at(absolute_offset, probe_len)?;
1706 read_uvarint(&bytes)
1707 .map(|(value, used)| (value, used as u64))
1708 .ok_or(FileError::Container("truncated container varint"))
1709}
1710
1711fn locate_container_section_ranged<R: RangeReader + ?Sized>(
1714 reader: &R,
1715 container_offset: u64,
1716 container_len: u64,
1717 section_index: usize,
1718 expected_sections: u64,
1719) -> Result<ByteRange, FileError> {
1720 let container_end = checked_end(container_offset, container_len)?;
1721 let (section_count, used) = read_uvarint_at(reader, container_offset, container_end)?;
1722 if section_count != expected_sections {
1723 return Err(FileError::Container("unexpected container section count"));
1724 }
1725 if section_index >= section_count as usize {
1726 return Err(FileError::Container(
1727 "container section index out of bounds",
1728 ));
1729 }
1730
1731 let mut pos = checked_end(container_offset, used)?;
1732 for i in 0..section_count as usize {
1733 let (payload_len, len_used) = read_uvarint_at(reader, pos, container_end)?;
1734 pos = checked_end(pos, len_used)?;
1735 let payload_end = checked_end(pos, payload_len)?;
1736 if payload_end > container_end {
1737 return Err(FileError::Container("section overruns buffer"));
1738 }
1739 if i == section_index {
1740 return Ok(ByteRange {
1741 offset: pos,
1742 len: payload_len,
1743 });
1744 }
1745 pos = payload_end;
1746 }
1747 Err(FileError::Container("container section not found"))
1748}
1749
1750#[allow(clippy::type_complexity)]
1760fn open_index_container_lazy(
1761 reader: &std::sync::Arc<dyn RangeReader + Send + Sync>,
1762 container: ByteRange,
1763 block_codec: u8,
1764 has_synopsis: bool,
1765 read_concurrency: usize,
1766 perms: PermSet,
1767 sections: usize,
1768) -> Result<
1769 (
1770 GraphIndex,
1771 [ByteRange; NUM_PERMS],
1772 [Vec<(u32, u32, ByteRange)>; NUM_PERMS],
1773 ),
1774 FileError,
1775> {
1776 let mut index_section_ranges = [ByteRange { offset: 0, len: 0 }; NUM_PERMS];
1777 let mut tile_ranges: [Vec<(u32, u32, ByteRange)>; NUM_PERMS] = Default::default();
1778 #[allow(clippy::type_complexity)]
1779 let mut directories: [Vec<(u32, u32, Option<TileSynopsis>)>; NUM_PERMS] = Default::default();
1780 for (pos, perm) in perms.iter().enumerate() {
1781 let si = perm.section_index();
1782 let section = locate_container_section_ranged(
1783 reader,
1784 container.offset,
1785 container.len,
1786 pos,
1787 sections as u64,
1788 )?;
1789 index_section_ranges[si] = section;
1790 let dir = read_tile_directory_ranged(reader, section)?;
1791 let syn = if has_synopsis {
1794 read_tile_synopsis_ranged(reader, section, &dir)
1795 } else {
1796 vec![None; dir.len()]
1797 };
1798 directories[si] = dir
1799 .iter()
1800 .zip(syn)
1801 .map(|(e, s)| (e.min_a, e.max_a, s))
1802 .collect();
1803 tile_ranges[si] = dir
1804 .into_iter()
1805 .map(|e| {
1806 (
1807 e.min_a,
1808 e.max_a,
1809 ByteRange {
1810 offset: section.offset + e.start,
1811 len: (e.end - e.start),
1812 },
1813 )
1814 })
1815 .collect();
1816 }
1817
1818 let codec = block_codec;
1823 let loader_ranges = tile_ranges.clone();
1824 let loader_reader = reader.clone();
1825 let loader: crate::index::TileLoader = Box::new(move |si, ti| {
1826 let (_, _, range) = loader_ranges.get(si)?.get(ti)?;
1827 let bytes = loader_reader.read_at(range.offset, range.len).ok()?;
1828 decompress(codec, &bytes).ok()
1829 });
1830 let bulk_ranges = tile_ranges.clone();
1831 let bulk_reader = reader.clone();
1832 let bulk: crate::index::TileBulkLoader = Box::new(move |si, tis| {
1833 let section = bulk_ranges.get(si)?;
1834 let want: Option<Vec<ByteRange>> = tis
1835 .iter()
1836 .map(|&ti| section.get(ti).map(|&(_, _, r)| r))
1837 .collect();
1838 let blobs = read_coalesced(bulk_reader.as_ref(), &want?, TILE_COALESCE_GAP)?;
1839 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
1840 });
1841 let mut index =
1842 GraphIndex::from_remote_directories(directories, perms, loader).with_bulk_loader(bulk);
1843 index.set_tile_lens(std::array::from_fn(|si| {
1846 tile_ranges[si]
1847 .iter()
1848 .map(|&(_, _, r)| r.len.min(u32::MAX as u64) as u32)
1849 .collect()
1850 }));
1851 index.set_read_concurrency(read_concurrency);
1855 Ok((index, index_section_ranges, tile_ranges))
1856}
1857
1858pub fn write_file(
1862 dict: &Dictionary,
1863 index: &GraphIndex,
1864 has_quads: bool,
1865 pyramid_meta: &[u8],
1866 pyramid_levels: u16,
1867) -> Vec<u8> {
1868 write_dataset(dict, index, &[], has_quads, pyramid_meta, pyramid_levels)
1869}
1870
1871pub(crate) fn encode_index_container(index: &GraphIndex, codec: u8) -> Vec<u8> {
1877 encode_index_container_sealed(index, codec, None)
1878}
1879
1880fn encode_index_container_sealed(index: &GraphIndex, codec: u8, seal: Option<&[u8]>) -> Vec<u8> {
1885 let payloads: Vec<Vec<u8>> = index
1886 .perms()
1887 .iter()
1888 .map(|perm| encode_tiled_section(index, perm.section_index(), codec))
1889 .collect();
1890 let mut refs: Vec<&[u8]> = payloads.iter().map(|p| p.as_slice()).collect();
1891 if let Some(seal) = seal {
1892 refs.push(seal);
1893 }
1894 encode_container(&refs, CODEC_NONE)
1895}
1896
1897fn encode_named_graphs(named: &[(String, GraphIndex)], codec: u8) -> Vec<u8> {
1899 let mut out = Vec::new();
1900 write_uvarint(&mut out, named.len() as u64);
1901 for (iri, index) in named {
1902 write_uvarint(&mut out, iri.len() as u64);
1903 out.extend_from_slice(iri.as_bytes());
1904 let container = encode_index_container(index, codec);
1905 write_uvarint(&mut out, container.len() as u64);
1906 out.extend_from_slice(&container);
1907 }
1908 out
1909}
1910
1911fn decode_named_graphs(
1912 bytes: &[u8],
1913 codec: u8,
1914 perms: PermSet,
1915) -> Result<Vec<(String, GraphIndex)>, FileError> {
1916 let (n, mut pos) = read_uvarint(bytes).ok_or(FileError::Container("truncated graph count"))?;
1917 let bound = |start: usize, len: u64| -> Result<usize, FileError> {
1920 start
1921 .checked_add(len as usize)
1922 .filter(|&e| e <= bytes.len())
1923 .ok_or(FileError::Container("named-graph field overruns buffer"))
1924 };
1925 let mut out = Vec::with_capacity((n as usize).min(bytes.len()));
1926 for _ in 0..n {
1927 let (ilen, u1) = read_uvarint(bytes.get(pos..).unwrap_or(&[]))
1928 .ok_or(FileError::Container("truncated iri len"))?;
1929 pos += u1;
1930 let iend = bound(pos, ilen)?;
1931 let iri = String::from_utf8_lossy(&bytes[pos..iend]).into_owned();
1932 pos = iend;
1933 let (clen, u2) = read_uvarint(bytes.get(pos..).unwrap_or(&[]))
1934 .ok_or(FileError::Container("truncated container len"))?;
1935 pos += u2;
1936 let cend = bound(pos, clen)?;
1937 let index = decode_index_container(&bytes[pos..cend], codec, perms)?;
1938 out.push((iri, index));
1939 pos = cend;
1940 }
1941 Ok(out)
1942}
1943
1944pub fn write_dataset(
1947 dict: &Dictionary,
1948 default_index: &GraphIndex,
1949 named: &[(String, GraphIndex)],
1950 has_quads: bool,
1951 pyramid_meta: &[u8],
1952 pyramid_levels: u16,
1953) -> Vec<u8> {
1954 write_dataset_with_metadata(
1955 dict,
1956 default_index,
1957 named,
1958 has_quads,
1959 pyramid_meta,
1960 pyramid_levels,
1961 &[],
1962 &[],
1963 )
1964}
1965
1966pub(crate) fn encode_dict_container(dict: &Dictionary, codec: u8) -> Vec<u8> {
1971 let raw_sections = dict.sections();
1972 let dict_payloads: Vec<Vec<u8>> = raw_sections
1973 .iter()
1974 .map(|raw| encode_chunked_dict_section(raw, codec))
1975 .collect();
1976 encode_container(
1977 &[
1978 dict_payloads[0].as_slice(),
1979 dict_payloads[1].as_slice(),
1980 dict_payloads[2].as_slice(),
1981 dict_payloads[3].as_slice(),
1982 ],
1983 CODEC_NONE,
1984 )
1985}
1986
1987#[allow(clippy::too_many_arguments)]
1998pub fn write_dataset_with_metadata(
1999 dict: &Dictionary,
2000 default_index: &GraphIndex,
2001 named: &[(String, GraphIndex)],
2002 has_quads: bool,
2003 pyramid_meta: &[u8],
2004 pyramid_levels: u16,
2005 metadata: &[u8],
2006 text_index: &[u8],
2007) -> Vec<u8> {
2008 let codec = writer_codec();
2009 let dict_container = encode_dict_container(dict, codec);
2010 let scope = (!text_index.is_empty())
2014 .then(|| crate::text_index::TextIndex::from_section(text_index, codec).ok())
2015 .flatten()
2016 .map(|ti| ti.scope().clone());
2017 let text_index_partial = scope
2018 .as_ref()
2019 .is_some_and(crate::text_index::TextIndexScope::restricts_predicates);
2020 let text_index_named = scope
2021 .as_ref()
2022 .is_some_and(crate::text_index::TextIndexScope::covers_named_graphs);
2023 write_dataset_from_parts(
2024 &dict_container,
2025 dict.term_count() as u64,
2026 default_index,
2027 NamedPart::PerGraph(named),
2028 has_quads,
2029 dict.has_quoted_triples(),
2030 pyramid_meta,
2031 pyramid_levels,
2032 metadata,
2033 text_index,
2034 text_index_partial,
2035 text_index_named,
2036 codec,
2037 )
2038}
2039
2040pub(crate) enum NamedPart<'a> {
2043 PerGraph(&'a [(String, GraphIndex)]),
2044 Quad(&'a crate::quad::EncodedQuadIndex),
2045}
2046
2047#[allow(clippy::too_many_arguments)]
2052pub(crate) fn write_dataset_from_parts(
2053 dict_container: &[u8],
2054 term_count: u64,
2055 default_index: &GraphIndex,
2056 named: NamedPart<'_>,
2057 has_quads: bool,
2058 has_quoted_triples: bool,
2059 pyramid_meta: &[u8],
2060 pyramid_levels: u16,
2061 metadata: &[u8],
2062 text_index: &[u8],
2063 text_index_partial: bool,
2064 text_index_named: bool,
2065 codec: u8,
2066) -> Vec<u8> {
2067 let (named_section, quad, named_quads) = match named {
2071 NamedPart::PerGraph(named) => (
2072 if named.is_empty() {
2073 Vec::new()
2074 } else {
2075 encode_named_graphs(named, codec)
2076 },
2077 None,
2078 named
2079 .iter()
2080 .map(|(_, idx)| idx.triple_count() as u64)
2081 .sum::<u64>(),
2082 ),
2083 NamedPart::Quad(q) if q.graph_count == 0 => (Vec::new(), None, 0),
2084 NamedPart::Quad(q) => (Vec::new(), Some(q), q.quad_count),
2085 };
2086 let seal = quad.map(|q| crate::quad::encode_seal(crate::quad::digest(&q.bytes)));
2087 let index_container = encode_index_container_sealed(default_index, codec, seal.as_deref());
2088 let text_index_partial = text_index_partial && !text_index.is_empty();
2093 let digested;
2094 let text_index: &[u8] = if text_index_partial {
2095 digested = crate::text_index::with_partial_digest(text_index);
2096 &digested
2097 } else {
2098 text_index
2099 };
2100
2101 let meta_section_len = metadata.len() as u64;
2104 let dict_offset = HEADER_LEN as u64 + meta_section_len;
2105 let dict_len = dict_container.len() as u64;
2106 let index_offset = dict_offset + dict_len;
2107 let index_len = index_container.len() as u64;
2108 let pyr_offset = index_offset + index_len;
2109 let pyr_len = pyramid_meta.len() as u64;
2110 let text_offset = pyr_offset + pyr_len;
2112 let text_len = text_index.len() as u64;
2113 let named_offset = text_offset + text_len;
2114 let named_len = named_section.len() as u64;
2115 let quad_len = quad.map_or(0, |q| q.bytes.len() as u64);
2118
2119 let mut parts: Vec<&[u8]> = Vec::with_capacity(5);
2125 if meta_section_len > 0 {
2126 parts.push(metadata);
2127 }
2128 parts.push(dict_container);
2129 parts.push(&index_container);
2130 parts.push(pyramid_meta);
2131 if text_len > 0 && !text_index_partial {
2132 parts.push(text_index);
2133 }
2134 if named_len > 0 {
2135 parts.push(&named_section);
2136 }
2137
2138 let schema_meta_len = crate::meta::schema_block_len(pyramid_meta);
2141
2142 let header = Header {
2146 version: crate::header::CURRENT_FORMAT_VERSION,
2147 flags: FLAG_TILE_SYNOPSIS
2148 | if has_quads { FLAG_HAS_QUADS } else { 0 }
2149 | if quad.is_some() {
2150 crate::header::FLAG_QUAD_INDEX
2151 } else {
2152 0
2153 }
2154 | if has_quoted_triples {
2155 FLAG_HAS_QUOTED_TRIPLES
2156 } else {
2157 0
2158 }
2159 | if text_len > 0 && text_index_named {
2160 crate::header::FLAG_TEXT_INDEX_NAMED_GRAPHS
2161 } else {
2162 0
2163 },
2164 metadata_offset: HEADER_LEN as u64,
2165 metadata_len: meta_section_len,
2166 dictionary_offset: dict_offset,
2167 dictionary_len: dict_len,
2168 root_dir_offset: index_offset,
2169 root_dir_len: index_len,
2170 pyramid_meta_offset: if pyr_len > 0 { pyr_offset } else { 0 },
2171 pyramid_meta_len: pyr_len,
2172 dict_codec: codec,
2173 block_codec: codec,
2174 pyramid_levels,
2175 perms: default_index.perms(),
2179 quad_count: default_index.triple_count() as u64 + named_quads,
2180 term_count,
2181 content_hash: content_hash(&parts),
2182 named_graphs_offset: if named_len > 0 { named_offset } else { 0 },
2183 named_graphs_len: named_len,
2184 schema_meta_len,
2185 text_index_offset: if text_len > 0 { text_offset } else { 0 },
2186 text_index_len: text_len,
2187 text_index_partial: text_len > 0 && text_index_partial,
2188 build_info_offset: 0,
2189 build_info_len: 0,
2190 extra_sections: if quad_len > 0 {
2191 vec![crate::header::Section {
2192 kind: crate::header::SectionKind::QuadIndex,
2193 flags: 0,
2194 offset: named_offset,
2195 length: quad_len,
2196 }]
2197 } else {
2198 Vec::new()
2199 },
2200 };
2201
2202 let mut out = Vec::with_capacity(
2203 HEADER_LEN
2204 + metadata.len()
2205 + dict_container.len()
2206 + index_container.len()
2207 + pyramid_meta.len()
2208 + text_index.len()
2209 + named_section.len()
2210 + quad_len as usize
2211 + MAGIC.len(),
2212 );
2213 out.extend_from_slice(&header.to_bytes());
2214 if meta_section_len > 0 {
2215 out.extend_from_slice(metadata);
2216 }
2217 out.extend_from_slice(dict_container);
2218 out.extend_from_slice(&index_container);
2219 out.extend_from_slice(pyramid_meta);
2220 if text_len > 0 {
2221 out.extend_from_slice(text_index);
2222 }
2223 if named_len > 0 {
2224 out.extend_from_slice(&named_section);
2225 }
2226 if let Some(q) = quad {
2227 out.extend_from_slice(&q.bytes);
2228 }
2229 out.extend_from_slice(&MAGIC); out
2231}
2232
2233pub const RDF_TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
2235
2236pub fn schema_summary(rete: &Rete) -> Vec<(String, String, String, u32)> {
2243 use std::collections::{BTreeMap, HashMap};
2244 let triples = rete.dump(None);
2245
2246 let mut class_of: HashMap<&str, &str> = HashMap::new();
2247 for (s, p, o) in &triples {
2248 if p == RDF_TYPE {
2249 class_of.insert(s.as_str(), o.as_str());
2250 }
2251 }
2252 let classify = |t: &str| -> String {
2253 if let Some(c) = class_of.get(t) {
2254 (*c).to_string()
2255 } else if t.starts_with('"') {
2256 "(literal)".to_string()
2257 } else {
2258 "(untyped)".to_string()
2259 }
2260 };
2261
2262 let mut counts: BTreeMap<(String, String, String), u32> = BTreeMap::new();
2263 for (s, p, o) in &triples {
2264 if p == RDF_TYPE {
2265 continue; }
2267 *counts
2268 .entry((classify(s), p.clone(), classify(o)))
2269 .or_default() += 1;
2270 }
2271 counts
2272 .into_iter()
2273 .map(|((a, p, b), c)| (a, p, b, c))
2274 .collect()
2275}
2276
2277pub fn schema_classes(rete: &Rete) -> Vec<(String, u32)> {
2281 use std::collections::BTreeMap;
2282 let mut counts: BTreeMap<String, u32> = BTreeMap::new();
2283 for (_s, p, o) in rete.dump(None) {
2284 if p == RDF_TYPE {
2285 *counts.entry(o).or_default() += 1;
2286 }
2287 }
2288 let mut out: Vec<(String, u32)> = counts.into_iter().collect();
2289 out.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
2290 out
2291}
2292
2293pub fn read_metadata_ranged<R: RangeReader>(reader: &R) -> Result<Option<Vec<u8>>, FileError> {
2303 let head = reader.read_at(0, HEADER_LEN as u64)?;
2304 let header = Header::from_bytes(&head)?;
2305 if header.metadata_len == 0 {
2306 return Ok(None);
2307 }
2308 let bytes = reader.read_at(header.metadata_offset, header.metadata_len)?;
2309 Ok(Some(bytes))
2310}
2311
2312#[allow(clippy::type_complexity)]
2322pub fn read_card_and_build_info_ranged<R: RangeReader>(
2323 reader: &R,
2324) -> Result<(Option<Vec<u8>>, Option<Vec<u8>>), FileError> {
2325 read_card_and_build_info_with_header(reader).map(|(_, m, b)| (m, b))
2326}
2327
2328#[allow(clippy::type_complexity)]
2339pub fn read_card_and_build_info_with_header<R: RangeReader>(
2340 reader: &R,
2341) -> Result<(Header, Option<Vec<u8>>, Option<Vec<u8>>), FileError> {
2342 let head = reader.read_at(0, HEADER_LEN as u64)?;
2343 let header = Header::from_bytes(&head)?;
2344 let meta = (header.metadata_offset, header.metadata_len);
2345 let build = (header.build_info_offset, header.build_info_len);
2346 if meta.1 > 0 && build.1 > 0 && build.0 == meta.0 + meta.1 {
2347 let both = reader.read_at(meta.0, meta.1 + build.1)?;
2349 let (m, b) = both.split_at(meta.1 as usize);
2350 return Ok((header, Some(m.to_vec()), Some(b.to_vec())));
2351 }
2352 let fetch = |off: u64, len: u64| -> Result<Option<Vec<u8>>, FileError> {
2353 if len == 0 {
2354 return Ok(None);
2355 }
2356 Ok(Some(reader.read_at(off, len)?))
2357 };
2358 Ok((header, fetch(meta.0, meta.1)?, fetch(build.0, build.1)?))
2359}
2360
2361pub fn read_build_info(bytes: &[u8]) -> Result<Option<Vec<u8>>, FileError> {
2366 let header = Header::from_bytes(bytes)?;
2367 if header.build_info_len == 0 {
2368 return Ok(None);
2369 }
2370 let start = header.build_info_offset as usize;
2371 let end = start
2372 .checked_add(header.build_info_len as usize)
2373 .filter(|&e| e <= bytes.len())
2374 .ok_or(FileError::Container("build-info section overruns buffer"))?;
2375 Ok(Some(bytes[start..end].to_vec()))
2376}
2377
2378#[derive(Debug, Clone, PartialEq, Eq)]
2387pub struct BuildInfoPlan {
2388 pub header: [u8; HEADER_LEN],
2390 pub insert: u64,
2393 pub tail_start: u64,
2397 pub new_len: u64,
2399}
2400
2401pub fn plan_build_info(
2423 head: &[u8],
2424 file_len: u64,
2425 info_len: u64,
2426) -> Result<BuildInfoPlan, FileError> {
2427 let mut header = Header::from_bytes(head)?;
2428 let insert = if header.metadata_len > 0 {
2433 header
2434 .metadata_offset
2435 .checked_add(header.metadata_len)
2436 .filter(|&end| header.metadata_offset >= HEADER_LEN as u64 && end <= file_len)
2437 .ok_or(FileError::Container(
2438 "the metadata section lies outside the file",
2439 ))?
2440 } else {
2441 HEADER_LEN as u64
2442 };
2443 let old_len = header.build_info_len;
2444 if old_len > 0 && header.build_info_offset != insert {
2445 return Err(FileError::Container(
2446 "existing build-info section is not adjacent to the metadata",
2447 ));
2448 }
2449 let tail_start = insert.saturating_add(old_len);
2450 if tail_start > file_len || insert > file_len {
2451 return Err(FileError::Container("build-info splice out of bounds"));
2452 }
2453
2454 let shift = |off: &mut u64, len: u64| {
2456 if len > 0 && *off >= tail_start {
2457 *off = *off - old_len + info_len;
2458 }
2459 };
2460 shift(&mut header.dictionary_offset, header.dictionary_len);
2461 shift(&mut header.root_dir_offset, header.root_dir_len);
2462 shift(&mut header.pyramid_meta_offset, header.pyramid_meta_len);
2463 shift(&mut header.named_graphs_offset, header.named_graphs_len);
2464 shift(&mut header.text_index_offset, header.text_index_len);
2465 for s in &mut header.extra_sections {
2466 if s.length > 0 && s.offset >= tail_start {
2467 s.offset = s.offset - old_len + info_len;
2468 }
2469 }
2470 if info_len == 0 {
2471 header.build_info_offset = 0;
2472 header.build_info_len = 0;
2473 } else {
2474 header.build_info_offset = insert;
2475 header.build_info_len = info_len;
2476 }
2477
2478 Ok(BuildInfoPlan {
2479 header: header.to_bytes(),
2480 insert,
2481 tail_start,
2482 new_len: file_len - old_len + info_len,
2483 })
2484}
2485
2486pub fn attach_build_info(image: &[u8], info: &[u8]) -> Result<Vec<u8>, FileError> {
2492 let plan = plan_build_info(image, image.len() as u64, info.len() as u64)?;
2493 let mut out = Vec::with_capacity(plan.new_len as usize);
2494 out.extend_from_slice(&plan.header);
2495 out.extend_from_slice(&image[HEADER_LEN..plan.insert as usize]);
2496 out.extend_from_slice(info);
2497 out.extend_from_slice(&image[plan.tail_start as usize..]);
2498 debug_assert_eq!(out.len() as u64, plan.new_len);
2499 Ok(out)
2500}
2501
2502pub fn replace_metadata(image: &[u8], metadata: &[u8]) -> Result<Vec<u8>, FileError> {
2522 let mut header = Header::from_bytes(image)?;
2523 let insert = HEADER_LEN as u64;
2526 if header.metadata_len > 0 && header.metadata_offset != insert {
2527 return Err(FileError::Container(
2528 "metadata section does not sit immediately after the header",
2529 ));
2530 }
2531 let old_len = header.metadata_len;
2532 let tail_start = (insert + old_len) as usize;
2533 if tail_start > image.len() {
2534 return Err(FileError::Container("metadata splice out of bounds"));
2535 }
2536 let new_len = metadata.len() as u64;
2537
2538 let shift = |off: &mut u64, len: u64| {
2539 if len > 0 && *off >= insert + old_len {
2540 *off = *off - old_len + new_len;
2541 }
2542 };
2543 shift(&mut header.dictionary_offset, header.dictionary_len);
2544 shift(&mut header.root_dir_offset, header.root_dir_len);
2545 shift(&mut header.pyramid_meta_offset, header.pyramid_meta_len);
2546 shift(&mut header.named_graphs_offset, header.named_graphs_len);
2547 shift(&mut header.text_index_offset, header.text_index_len);
2548 shift(&mut header.build_info_offset, header.build_info_len);
2549 for s in &mut header.extra_sections {
2550 if s.length > 0 && s.offset >= insert + old_len {
2551 s.offset = s.offset - old_len + new_len;
2552 }
2553 }
2554 header.metadata_offset = insert;
2555 header.metadata_len = new_len;
2556
2557 let mut out = Vec::with_capacity(image.len() - old_len as usize + metadata.len());
2558 out.extend_from_slice(&header.to_bytes());
2559 out.extend_from_slice(metadata);
2560 out.extend_from_slice(&image[tail_start..]);
2561 header.content_hash = hash_of_sections(&header, &out)?;
2565 out[..HEADER_LEN].copy_from_slice(&header.to_bytes());
2566 Ok(out)
2567}
2568
2569fn hash_of_sections(header: &Header, bytes: &[u8]) -> Result<[u8; 16], FileError> {
2575 let slice = |off: u64, len: u64| -> Result<&[u8], FileError> {
2576 let end = off.saturating_add(len);
2583 bytes
2584 .get(off as usize..end as usize)
2585 .ok_or(FileError::Container("section overruns buffer"))
2586 };
2587 let parts = content_hash_parts(header)
2588 .into_iter()
2589 .map(|(off, len)| slice(off, len))
2590 .collect::<Result<Vec<&[u8]>, FileError>>()?;
2591 Ok(content_hash(&parts))
2592}
2593
2594pub fn content_hash_parts(header: &Header) -> Vec<(u64, u64)> {
2607 let mut parts: Vec<(u64, u64)> = Vec::with_capacity(6);
2608 if header.metadata_len > 0 {
2609 parts.push((header.metadata_offset, header.metadata_len));
2610 }
2611 parts.push((header.dictionary_offset, header.dictionary_len));
2612 parts.push((header.root_dir_offset, header.root_dir_len));
2613 if header.pyramid_meta_len > 0 {
2616 parts.push((header.pyramid_meta_offset, header.pyramid_meta_len));
2617 }
2618 if header.text_index_len > 0 && !header.text_index_partial {
2620 parts.push((header.text_index_offset, header.text_index_len));
2621 }
2622 if header.named_graphs_len > 0 {
2623 parts.push((header.named_graphs_offset, header.named_graphs_len));
2624 }
2625 parts
2626}
2627
2628pub fn verify(bytes: &[u8]) -> Result<bool, FileError> {
2636 let header = Header::from_bytes(bytes)?;
2637 if hash_of_sections(&header, bytes)? != header.content_hash {
2638 return Ok(false);
2639 }
2640 if header.has_quad_index() || header.quad_index_section().is_some() {
2643 check_named_layout(&header)?;
2644 let slice = |off: u64, len: u64| -> Result<&[u8], FileError> {
2645 bytes
2646 .get(off as usize..off.saturating_add(len) as usize)
2647 .ok_or(FileError::Container("section overruns buffer"))
2648 };
2649 let root = slice(header.root_dir_offset, header.root_dir_len)?;
2650 let ranges = container_section_payload_ranges(
2651 root,
2652 header.root_dir_offset,
2653 header.index_container_sections(),
2654 )?;
2655 let seal = ranges
2656 .last()
2657 .ok_or(FileError::Container("missing quad-index seal"))?;
2658 let want = crate::quad::parse_seal(slice(seal.offset, seal.len)?)?;
2659 let q = header
2660 .quad_index_section()
2661 .ok_or(FileError::Container("missing quad-index section"))?;
2662 if crate::quad::digest(slice(q.offset, q.length)?) != want {
2663 return Ok(false);
2664 }
2665 }
2666 for s in self_digested_sections(&header) {
2667 let end = s.offset.saturating_add(s.length);
2668 let section = bytes
2669 .get(s.offset as usize..end as usize)
2670 .ok_or(FileError::Container("section overruns buffer"))?;
2671 if !crate::label_index::digest_ok(section) {
2672 return Ok(false);
2673 }
2674 }
2675 Ok(true)
2676}
2677
2678fn self_digested_sections(header: &Header) -> Vec<Section> {
2682 [
2683 header.label_index_section(),
2684 header.schema_section(),
2685 header
2686 .section(crate::header::SectionKind::TextIndexPartial)
2687 .filter(|s| s.length > 0),
2688 ]
2689 .into_iter()
2690 .flatten()
2691 .collect()
2692}
2693
2694pub fn append_sections(
2702 image: &[u8],
2703 sections: &[(SectionKind, &[u8])],
2704) -> Result<Vec<u8>, FileError> {
2705 let lens: Vec<(SectionKind, u64)> =
2706 sections.iter().map(|(k, b)| (*k, b.len() as u64)).collect();
2707 let (header, body_end) = plan_append(image, image.len() as u64, &lens)?;
2708 let extra: usize = sections.iter().map(|(_, b)| b.len()).sum();
2709 let mut out = Vec::with_capacity(image.len() + extra);
2710 out.extend_from_slice(&header.to_bytes());
2711 out.extend_from_slice(&image[HEADER_LEN..body_end as usize]);
2712 for (_, b) in sections {
2713 out.extend_from_slice(b);
2714 }
2715 out.extend_from_slice(&MAGIC);
2716 Ok(out)
2717}
2718
2719fn plan_append(
2721 head: &[u8],
2722 file_len: u64,
2723 sections: &[(SectionKind, u64)],
2724) -> Result<(Header, u64), FileError> {
2725 let mut header = Header::from_bytes(head)?;
2726 if header.expected_file_len() != Some(file_len) {
2727 return Err(FileError::Container(
2728 "cannot append sections: the file length does not match its header",
2729 ));
2730 }
2731 let body_end = file_len - MAGIC.len() as u64;
2732 let mut at = body_end;
2733 for &(kind, len) in sections {
2734 if !matches!(kind, SectionKind::LabelIndex | SectionKind::Schema) {
2735 return Err(FileError::Container(
2736 "only LABEL_INDEX and SCHEMA sections can be appended",
2737 ));
2738 }
2739 if len == 0 {
2740 continue;
2741 }
2742 if header.section(kind).is_some_and(|s| s.length > 0) {
2743 return Err(FileError::Container(
2744 "the file already carries that section",
2745 ));
2746 }
2747 header = header.with_section(kind, at, len);
2748 at += len;
2749 }
2750 Ok((header, body_end))
2751}
2752
2753#[cfg(not(target_arch = "wasm32"))]
2755pub(crate) enum PayloadPart<'a> {
2756 Bytes(&'a [u8]),
2758 File(&'a std::path::Path),
2760}
2761
2762#[cfg(not(target_arch = "wasm32"))]
2767pub(crate) fn append_sections_to_file(
2768 path: &std::path::Path,
2769 sections: &[(SectionKind, Vec<PayloadPart<'_>>)],
2770) -> Result<(), FileError> {
2771 use std::io::{Read, Seek, SeekFrom, Write};
2772 let part_len = |p: &PayloadPart<'_>| -> std::io::Result<u64> {
2773 match p {
2774 PayloadPart::Bytes(b) => Ok(b.len() as u64),
2775 PayloadPart::File(f) => Ok(std::fs::metadata(f)?.len()),
2776 }
2777 };
2778 let mut lens = Vec::with_capacity(sections.len());
2779 for (kind, parts) in sections {
2780 let mut n = 0u64;
2781 for p in parts {
2782 n += part_len(p)?;
2783 }
2784 lens.push((*kind, n));
2785 }
2786 let mut f = std::fs::OpenOptions::new()
2787 .read(true)
2788 .write(true)
2789 .open(path)?;
2790 let len = f.metadata()?.len();
2791 let mut head = vec![0u8; HEADER_LEN];
2792 f.read_exact(&mut head)?;
2793 let (header, body_end) = plan_append(&head, len, &lens)?;
2794 f.seek(SeekFrom::Start(body_end))?;
2795 let mut w = std::io::BufWriter::with_capacity(1 << 20, &mut f);
2796 for (_, parts) in sections {
2797 for p in parts {
2798 match p {
2799 PayloadPart::Bytes(b) => w.write_all(b)?,
2800 PayloadPart::File(src) => {
2801 std::io::copy(&mut std::fs::File::open(src)?, &mut w)?;
2802 }
2803 }
2804 }
2805 }
2806 w.write_all(&MAGIC)?;
2807 w.flush()?;
2808 drop(w);
2809 f.seek(SeekFrom::Start(0))?;
2810 f.write_all(&header.to_bytes())?;
2811 f.flush()?;
2812 Ok(())
2813}
2814
2815#[cfg(not(target_arch = "wasm32"))]
2820pub(crate) fn open_dictionary_lazy<R: RangeReader + Send + Sync + 'static>(
2821 reader: R,
2822) -> Result<Dictionary, FileError> {
2823 let head = reader.read_at(0, HEADER_LEN as u64)?;
2824 let header = Header::from_bytes(&head)?;
2825 ranged_chunked_dictionary(&std::sync::Arc::new(reader), &header, [true; 4])
2826}
2827
2828pub fn verify_ranged(reader: &dyn crate::reader::RangeReader) -> Result<bool, FileError> {
2834 const SLICE: u64 = 8 << 20;
2837 let len = reader.len();
2838 if len < HEADER_LEN as u64 {
2839 return Err(FileError::Container("file shorter than its header"));
2840 }
2841 let header = Header::from_bytes(&reader.read_at(0, HEADER_LEN as u64)?)?;
2842 let mut h = blake3::Hasher::new();
2843 for (off, n) in content_hash_parts(&header) {
2844 if off.checked_add(n).is_none_or(|end| end > len) {
2847 return Err(FileError::Container("section overruns buffer"));
2848 }
2849 let mut at = off;
2850 while at < off + n {
2851 let take = (off + n - at).min(SLICE);
2852 h.update(&reader.read_at(at, take)?);
2853 at += take;
2854 }
2855 }
2856 let mut out = [0u8; 16];
2857 out.copy_from_slice(&h.finalize().as_bytes()[..16]);
2858 if out != header.content_hash {
2859 return Ok(false);
2860 }
2861 if header.has_quad_index() || header.quad_index_section().is_some() {
2864 check_named_layout(&header)?;
2865 let sections = header.index_container_sections();
2866 let seal = locate_container_section_ranged(
2867 reader,
2868 header.root_dir_offset,
2869 header.root_dir_len,
2870 sections - 1,
2871 sections as u64,
2872 )?;
2873 let want = crate::quad::parse_seal(&reader.read_at(seal.offset, seal.len)?)?;
2874 let q = header
2875 .quad_index_section()
2876 .ok_or(FileError::Container("missing quad-index section"))?;
2877 if q.offset.checked_add(q.length).is_none_or(|end| end > len) {
2878 return Err(FileError::Container("section overruns buffer"));
2879 }
2880 let mut h = blake3::Hasher::new();
2881 let mut at = q.offset;
2882 while at < q.offset + q.length {
2883 let take = (q.offset + q.length - at).min(SLICE);
2884 h.update(&reader.read_at(at, take)?);
2885 at += take;
2886 }
2887 let mut got = [0u8; 16];
2888 got.copy_from_slice(&h.finalize().as_bytes()[..16]);
2889 if got != want {
2890 return Ok(false);
2891 }
2892 }
2893 for s in self_digested_sections(&header) {
2895 if s.length < 16 || s.offset.checked_add(s.length).is_none_or(|end| end > len) {
2896 return Err(FileError::Container("section overruns buffer"));
2897 }
2898 let want = reader.read_at(s.offset, 16)?;
2899 let mut h = blake3::Hasher::new();
2900 let mut at = s.offset + 16;
2901 while at < s.offset + s.length {
2902 let take = (s.offset + s.length - at).min(SLICE);
2903 h.update(&reader.read_at(at, take)?);
2904 at += take;
2905 }
2906 if h.finalize().as_bytes()[..16] != want[..] {
2907 return Ok(false);
2908 }
2909 }
2910 Ok(true)
2911}
2912
2913type PyramidLoader = Box<dyn Fn() -> Option<PyramidMeta> + Send + Sync>;
2915
2916enum PyramidSlot {
2922 Resident(Option<PyramidMeta>),
2923 Lazy {
2924 loader: PyramidLoader,
2925 cell: std::sync::OnceLock<Option<PyramidMeta>>,
2926 },
2927}
2928
2929pub(crate) enum SectionSlot<T> {
2934 Resident(Option<T>),
2935 Lazy {
2936 loader: Box<dyn Fn() -> Option<T> + Send + Sync>,
2937 cell: std::sync::OnceLock<Option<T>>,
2938 },
2939}
2940
2941impl<T> SectionSlot<T> {
2942 pub(crate) fn get(&self) -> Option<&T> {
2944 match self {
2945 SectionSlot::Resident(v) => v.as_ref(),
2946 SectionSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
2947 }
2948 }
2949
2950 pub(crate) fn get_if_loaded(&self) -> Option<&T> {
2952 match self {
2953 SectionSlot::Resident(v) => v.as_ref(),
2954 SectionSlot::Lazy { cell, .. } => cell.get().and_then(|o| o.as_ref()),
2955 }
2956 }
2957}
2958
2959fn resident_label_slot<'a>(
2961 header: &Header,
2962 region: &dyn Fn(u64, u64) -> Result<&'a [u8], FileError>,
2963) -> SectionSlot<crate::label_index::LabelIndex> {
2964 SectionSlot::Resident(header.label_index_section().and_then(|s| {
2965 let bytes = region(s.offset, s.length).ok()?;
2966 crate::label_index::LabelIndex::from_section(bytes, header.block_codec).ok()
2967 }))
2968}
2969
2970fn resident_schema_slot<'a>(
2972 header: &Header,
2973 region: &dyn Fn(u64, u64) -> Result<&'a [u8], FileError>,
2974) -> SectionSlot<crate::schema_section::SchemaMeta> {
2975 SectionSlot::Resident(header.schema_section().and_then(|s| {
2976 crate::schema_section::SchemaMeta::decode(region(s.offset, s.length).ok()?).ok()
2977 }))
2978}
2979
2980fn ranged_label_slot<R: RangeReader + Send + Sync + 'static>(
2984 reader: &std::sync::Arc<R>,
2985 header: &Header,
2986) -> SectionSlot<crate::label_index::LabelIndex> {
2987 let Some(s) = header.label_index_section() else {
2988 return SectionSlot::Resident(None);
2989 };
2990 let r = reader.clone();
2991 let codec = header.block_codec;
2992 SectionSlot::Lazy {
2993 loader: Box::new(move || {
2994 let head = r
2995 .read_at(s.offset, crate::label_index::HEAD_READ.min(s.length))
2996 .ok()?;
2997 let rr = r.clone();
2998 let loader = Box::new(move |off: u64, len: u64| rr.read_at(s.offset + off, len).ok());
2999 crate::label_index::LabelIndex::from_head(&head, s.length, codec, loader).ok()
3000 }),
3001 cell: std::sync::OnceLock::new(),
3002 }
3003}
3004
3005fn ranged_schema_slot<R: RangeReader + Send + Sync + 'static>(
3007 reader: &std::sync::Arc<R>,
3008 header: &Header,
3009) -> SectionSlot<crate::schema_section::SchemaMeta> {
3010 let Some(s) = header.schema_section() else {
3011 return SectionSlot::Resident(None);
3012 };
3013 let r = reader.clone();
3014 SectionSlot::Lazy {
3015 loader: Box::new(move || {
3016 let bytes = r.read_at(s.offset, s.length).ok()?;
3017 crate::schema_section::SchemaMeta::decode(&bytes).ok()
3018 }),
3019 cell: std::sync::OnceLock::new(),
3020 }
3021}
3022
3023pub fn read_schema_ranged<R: RangeReader + ?Sized>(
3027 reader: &R,
3028) -> Result<Option<crate::schema_section::SchemaMeta>, FileError> {
3029 let head = reader.read_at(0, HEADER_LEN as u64)?;
3030 let header = Header::from_bytes(&head)?;
3031 Ok(read_schema_with_header(reader, &header))
3032}
3033
3034fn read_schema_with_header<R: RangeReader + ?Sized>(
3035 reader: &R,
3036 header: &Header,
3037) -> Option<crate::schema_section::SchemaMeta> {
3038 let s = header.schema_section()?;
3039 let bytes = reader.read_at(s.offset, s.length).ok()?;
3040 crate::schema_section::SchemaMeta::decode(&bytes).ok()
3041}
3042
3043type TextIndexLoader = Box<dyn Fn() -> Option<crate::text_index::TextIndex> + Send + Sync>;
3046
3047type TokenTableProbe = Box<dyn Fn() -> Option<u64> + Send + Sync>;
3050
3051enum TextIndexSlot {
3061 Resident {
3062 index: Option<crate::text_index::TextIndex>,
3063 token_table_len: Option<u64>,
3066 },
3067 Lazy {
3068 loader: TextIndexLoader,
3069 cell: std::sync::OnceLock<Option<crate::text_index::TextIndex>>,
3070 token_table: TokenTableProbe,
3072 token_table_cell: std::sync::OnceLock<Option<u64>>,
3073 },
3074}
3075
3076impl TextIndexSlot {
3077 fn index(&self) -> Option<&crate::text_index::TextIndex> {
3079 match self {
3080 TextIndexSlot::Resident { index, .. } => index.as_ref(),
3081 TextIndexSlot::Lazy { loader, cell, .. } => cell.get_or_init(loader).as_ref(),
3082 }
3083 }
3084
3085 fn token_table_len(&self) -> Option<u64> {
3088 match self {
3089 TextIndexSlot::Resident {
3090 token_table_len, ..
3091 } => *token_table_len,
3092 TextIndexSlot::Lazy {
3093 token_table,
3094 token_table_cell,
3095 ..
3096 } => *token_table_cell.get_or_init(token_table),
3097 }
3098 }
3099}
3100
3101fn resident_text_index_slot(section: Option<&[u8]>, codec: u8) -> Result<TextIndexSlot, FileError> {
3104 let Some(section) = section else {
3105 return Ok(TextIndexSlot::Resident {
3106 index: None,
3107 token_table_len: None,
3108 });
3109 };
3110 Ok(TextIndexSlot::Resident {
3111 index: Some(
3112 crate::text_index::TextIndex::from_section(section, codec)
3113 .map_err(|_| FileError::Container("malformed text index"))?,
3114 ),
3115 token_table_len: crate::text_index::TextIndex::postings_base(section).map(|b| b as u64),
3116 })
3117}
3118
3119pub fn read_text_index_token_table_len_ranged<R: RangeReader + ?Sized>(
3132 reader: &R,
3133 header: &Header,
3134) -> Option<u64> {
3135 let (ti_off, ti_len) = header.text_index_payload();
3136 if ti_len == 0 {
3137 return None;
3138 }
3139 read_token_table_len(reader, ti_off, ti_len)
3140}
3141
3142fn read_token_table_len<R: RangeReader + ?Sized>(reader: &R, off: u64, len: u64) -> Option<u64> {
3146 let head = reader.read_at(off, 10u64.min(len)).ok()?;
3147 let (ttlen, n) = crate::varint::read_uvarint(&head)?;
3148 Some((n as u64 + ttlen).min(len))
3149}
3150
3151const NAMED_GRAPH_RESIDENT_MAX: u64 = 1 << 20; pub const TRANSIENT_GRAPH_QUADS: u64 = 1024;
3163
3164const NAMED_SLAB: usize = 1024;
3169
3170const NAMED_WALK_CHUNK: u64 = 64 * 1024;
3174
3175const NAMED_WALK_CHUNK_MAX: u64 = 8 * 1024 * 1024;
3188
3189#[derive(Default)]
3191struct NamedEntry {
3192 meta: std::sync::OnceLock<(String, ByteRange)>,
3195 index: std::sync::OnceLock<Box<GraphIndex>>,
3198}
3199
3200#[derive(Default)]
3204struct NamedWalk {
3205 next: usize,
3207 pos: u64,
3211 buf: Vec<u8>,
3218 buf_off: u64,
3219 chunk: u64,
3226 big_seen: bool,
3233}
3234
3235struct LazyNamedGraphs {
3249 reader: std::sync::Arc<dyn RangeReader + Send + Sync>,
3250 section: ByteRange,
3252 codec: u8,
3253 has_synopsis: bool,
3254 read_concurrency: usize,
3255 perms: PermSet,
3258 #[allow(clippy::type_complexity)]
3262 dir: std::sync::OnceLock<(usize, Box<[std::sync::OnceLock<Box<[NamedEntry]>>]>)>,
3263 walk: std::sync::Mutex<NamedWalk>,
3264 failed: std::sync::atomic::AtomicBool,
3265 tile_cap: std::sync::atomic::AtomicU64,
3271}
3272
3273impl LazyNamedGraphs {
3274 fn new(
3275 reader: std::sync::Arc<dyn RangeReader + Send + Sync>,
3276 section: ByteRange,
3277 codec: u8,
3278 has_synopsis: bool,
3279 read_concurrency: usize,
3280 perms: PermSet,
3281 ) -> Self {
3282 LazyNamedGraphs {
3283 reader,
3284 section,
3285 codec,
3286 has_synopsis,
3287 read_concurrency,
3288 perms,
3289 dir: std::sync::OnceLock::new(),
3290 walk: std::sync::Mutex::new(NamedWalk::default()),
3291 failed: std::sync::atomic::AtomicBool::new(false),
3292 tile_cap: std::sync::atomic::AtomicU64::new(u64::MAX),
3293 }
3294 }
3295
3296 fn set_tile_cap(&self, cap: u64) {
3300 self.tile_cap
3301 .store(cap, std::sync::atomic::Ordering::Relaxed);
3302 self.for_each_opened(|g| {
3303 if g.is_lazy() {
3304 g.set_cache_cap(cap);
3305 }
3306 });
3307 }
3308
3309 fn fail(&self) {
3311 self.failed
3312 .store(true, std::sync::atomic::Ordering::Relaxed);
3313 }
3314
3315 #[allow(clippy::type_complexity)]
3317 fn directory(&self) -> Option<&(usize, Box<[std::sync::OnceLock<Box<[NamedEntry]>>]>)> {
3318 if let Some(d) = self.dir.get() {
3319 return Some(d);
3320 }
3321 let end = self.section.offset.checked_add(self.section.len)?;
3322 let (n, used) = match read_uvarint_at(&self.reader, self.section.offset, end) {
3323 Ok(v) => v,
3324 Err(_) => {
3325 self.fail();
3326 return None;
3327 }
3328 };
3329 if n > self.section.len / 2 {
3332 self.fail();
3333 return None;
3334 }
3335 let n = n as usize;
3336 let slabs = n.div_ceil(NAMED_SLAB);
3337 let table: Box<[std::sync::OnceLock<Box<[NamedEntry]>>]> =
3338 (0..slabs).map(|_| std::sync::OnceLock::new()).collect();
3339 let _ = self.dir.set((n, table));
3340 {
3341 let mut w = self.walk.lock().unwrap();
3344 if w.pos == 0 {
3345 w.pos = self.section.offset + used;
3346 }
3347 }
3348 self.dir.get()
3349 }
3350
3351 fn count(&self) -> usize {
3352 self.directory().map(|(n, _)| *n).unwrap_or(0)
3353 }
3354
3355 fn entry(&self, i: usize) -> Option<&NamedEntry> {
3357 let (n, table) = self.directory()?;
3358 if i >= *n {
3359 return None;
3360 }
3361 let slab = table[i / NAMED_SLAB].get_or_init(|| {
3362 let len = NAMED_SLAB.min(n - (i / NAMED_SLAB) * NAMED_SLAB);
3363 (0..len).map(|_| NamedEntry::default()).collect()
3364 });
3365 slab.get(i % NAMED_SLAB)
3366 }
3367
3368 fn next_read_len(w: &mut NamedWalk, pos: u64, end: u64, need: u64) -> u64 {
3376 let cur = w.chunk.max(NAMED_WALK_CHUNK);
3377 w.chunk = (cur * 2).min(NAMED_WALK_CHUNK_MAX);
3378 cur.max(need).min(end - pos)
3379 }
3380
3381 fn ensure_meta(&self, upto: usize, exhaustive: bool) -> Option<()> {
3396 let target = self.entry(upto)?;
3397 if target.meta.get().is_some() {
3398 return Some(());
3399 }
3400 let end = self.section.offset.checked_add(self.section.len)?;
3401 let mut w = self.walk.lock().unwrap();
3402 if exhaustive && !w.big_seen {
3403 w.chunk = NAMED_WALK_CHUNK_MAX;
3404 }
3405 while w.next <= upto {
3406 let pos = w.pos;
3407 if pos >= end {
3408 self.fail();
3410 return None;
3411 }
3412 let have =
3415 |b: &[u8], off: u64, need: u64| pos >= off && pos + need <= off + b.len() as u64;
3416 if !have(&w.buf, w.buf_off, 20.min(end - pos)) {
3417 let len = Self::next_read_len(&mut w, pos, end, 20.min(end - pos));
3418 w.buf = match self.reader.read_at(pos, len) {
3419 Ok(b) => b,
3420 Err(_) => {
3421 self.fail();
3422 return None;
3423 }
3424 };
3425 w.buf_off = pos;
3426 }
3427 let rel = (pos - w.buf_off) as usize;
3428 let Some((ilen, u1)) = read_uvarint(&w.buf[rel..]) else {
3429 self.fail();
3430 return None;
3431 };
3432 let header_need = u1 as u64 + ilen + 10; if pos + u1 as u64 + ilen > end {
3434 self.fail(); return None;
3436 }
3437 if !have(&w.buf, w.buf_off, header_need.min(end - pos)) {
3438 let len = Self::next_read_len(&mut w, pos, end, header_need);
3439 w.buf = match self.reader.read_at(pos, len) {
3440 Ok(b) => b,
3441 Err(_) => {
3442 self.fail();
3443 return None;
3444 }
3445 };
3446 w.buf_off = pos;
3447 }
3448 let rel = (pos - w.buf_off) as usize;
3449 let istart = rel + u1;
3450 let iend = istart + ilen as usize;
3451 let iri = String::from_utf8_lossy(&w.buf[istart..iend]).into_owned();
3452 let Some((clen, u2)) = read_uvarint(&w.buf[iend..]) else {
3453 self.fail();
3454 return None;
3455 };
3456 let cstart = pos + u1 as u64 + ilen + u2 as u64;
3457 let cend = match cstart.checked_add(clen) {
3458 Some(e) if e <= end => e,
3459 _ => {
3460 self.fail(); return None;
3462 }
3463 };
3464 if clen > NAMED_GRAPH_RESIDENT_MAX {
3465 w.big_seen = true;
3470 w.chunk = NAMED_WALK_CHUNK;
3471 }
3472 let range = ByteRange {
3473 offset: cstart,
3474 len: clen,
3475 };
3476 if let Some(e) = self.entry(w.next) {
3477 let _ = e.meta.set((iri, range));
3478 }
3479 w.pos = cend;
3480 w.next += 1;
3481 }
3482 Some(())
3483 }
3484
3485 fn name_at(&self, i: usize, exhaustive: bool) -> Option<&str> {
3488 self.ensure_meta(i, exhaustive)?;
3489 self.entry(i)?.meta.get().map(|(iri, _)| iri.as_str())
3490 }
3491
3492 fn graph_at(&self, i: usize, exhaustive: bool) -> Option<(&str, &GraphIndex)> {
3495 self.ensure_meta(i, exhaustive)?;
3496 let e = self.entry(i)?;
3497 let (iri, range) = e.meta.get()?;
3498 if e.index.get().is_none() {
3499 let opened = self.open_graph(*range)?;
3500 let _ = e.index.set(Box::new(opened));
3501 }
3502 Some((iri.as_str(), e.index.get()?.as_ref()))
3503 }
3504
3505 fn container_bytes(&self, range: ByteRange) -> Option<Vec<u8>> {
3509 {
3510 let w = self.walk.lock().unwrap();
3511 let buf_end = w.buf_off + w.buf.len() as u64;
3512 if range.offset >= w.buf_off && range.offset + range.len <= buf_end {
3513 let a = (range.offset - w.buf_off) as usize;
3514 return Some(w.buf[a..a + range.len as usize].to_vec());
3515 }
3516 }
3517 match self.reader.read_at(range.offset, range.len) {
3518 Ok(b) => Some(b),
3519 Err(_) => {
3520 self.fail();
3521 None
3522 }
3523 }
3524 }
3525
3526 fn open_graph(&self, range: ByteRange) -> Option<GraphIndex> {
3531 if range.len <= NAMED_GRAPH_RESIDENT_MAX {
3532 let bytes = self.container_bytes(range)?;
3533 match decode_index_container(&bytes, self.codec, self.perms) {
3534 Ok(g) => Some(g),
3535 Err(_) => {
3536 self.fail();
3537 None
3538 }
3539 }
3540 } else {
3541 match open_index_container_lazy(
3542 &self.reader,
3543 range,
3544 self.codec,
3545 self.has_synopsis,
3546 self.read_concurrency,
3547 self.perms,
3548 self.perms.len(),
3549 ) {
3550 Ok((g, _, _)) => {
3551 let cap = self.tile_cap.load(std::sync::atomic::Ordering::Relaxed);
3555 if cap != u64::MAX {
3556 g.set_cache_cap(cap);
3557 }
3558 Some(g)
3559 }
3560 Err(_) => {
3561 self.fail();
3562 None
3563 }
3564 }
3565 }
3566 }
3567
3568 fn release(&mut self, iri: &str) {
3574 let Some((_, table)) = self.dir.get_mut() else {
3575 return;
3576 };
3577 for slab in table.iter_mut() {
3578 let Some(entries) = slab.get_mut() else {
3579 continue;
3580 };
3581 for e in entries.iter_mut() {
3582 let matches = e
3583 .meta
3584 .get()
3585 .map(|(name, _)| name.as_str() == iri)
3586 .unwrap_or(false);
3587 if matches {
3588 e.index.take();
3589 return;
3590 }
3591 }
3592 }
3593 }
3594
3595 fn for_each_opened(&self, mut f: impl FnMut(&GraphIndex)) {
3597 if let Some((_, table)) = self.dir.get() {
3598 for slab in table.iter().filter_map(|s| s.get()) {
3599 for e in slab.iter() {
3600 if let Some(g) = e.index.get() {
3601 f(g);
3602 }
3603 }
3604 }
3605 }
3606 }
3607}
3608
3609enum NamedGraphsSlot {
3614 Resident(Vec<(String, GraphIndex)>),
3615 Lazy(LazyNamedGraphs),
3616 Quad(crate::quad::QuadNamed),
3619}
3620
3621impl NamedGraphsSlot {
3622 fn directory_bytes(&self) -> u64 {
3626 match self {
3627 NamedGraphsSlot::Resident(v) => v
3628 .iter()
3629 .map(|(iri, g)| iri.capacity() as u64 + g.directory_bytes())
3630 .sum(),
3631 NamedGraphsSlot::Lazy(l) => {
3632 let mut bytes = l.walk.lock().map(|w| w.buf.capacity() as u64).unwrap_or(0);
3633 if let Some((_, table)) = l.dir.get() {
3634 bytes += table.len() as u64
3635 * std::mem::size_of::<std::sync::OnceLock<Box<[NamedEntry]>>>() as u64;
3636 for slab in table.iter().filter_map(|s| s.get()) {
3637 bytes += slab.len() as u64 * std::mem::size_of::<NamedEntry>() as u64;
3638 for e in slab.iter() {
3639 if let Some((iri, _)) = e.meta.get() {
3640 bytes += iri.capacity() as u64;
3641 }
3642 if let Some(g) = e.index.get() {
3643 bytes += g.directory_bytes();
3644 }
3645 }
3646 }
3647 }
3648 bytes
3649 }
3650 NamedGraphsSlot::Quad(q) => q.directory_bytes(),
3651 }
3652 }
3653
3654 fn count(&self) -> usize {
3655 match self {
3656 NamedGraphsSlot::Resident(v) => v.len(),
3657 NamedGraphsSlot::Lazy(l) => l.count(),
3658 NamedGraphsSlot::Quad(q) => q.count(),
3659 }
3660 }
3661
3662 fn name_at<'a>(&'a self, dict: &Dictionary, i: usize, exhaustive: bool) -> Option<&'a str> {
3666 match self {
3667 NamedGraphsSlot::Resident(v) => v.get(i).map(|(iri, _)| iri.as_str()),
3668 NamedGraphsSlot::Lazy(l) => l.name_at(i, exhaustive),
3669 NamedGraphsSlot::Quad(q) => q.name_at(dict, i, exhaustive),
3670 }
3671 }
3672
3673 fn graph_at<'a>(
3674 &'a self,
3675 dict: &Dictionary,
3676 i: usize,
3677 exhaustive: bool,
3678 ) -> Option<(&'a str, &'a GraphIndex)> {
3679 match self {
3680 NamedGraphsSlot::Resident(v) => v.get(i).map(|(iri, g)| (iri.as_str(), g)),
3681 NamedGraphsSlot::Lazy(l) => l.graph_at(i, exhaustive),
3682 NamedGraphsSlot::Quad(q) => q.graph_at(dict, i, exhaustive),
3683 }
3684 }
3685
3686 fn find<'a>(&'a self, dict: &Dictionary, iri: &str) -> Option<&'a GraphIndex> {
3687 match self {
3688 NamedGraphsSlot::Quad(q) => q.find(dict, iri),
3689 NamedGraphsSlot::Resident(v) => v.iter().find(|(name, _)| name == iri).map(|(_, g)| g),
3690 NamedGraphsSlot::Lazy(l) => {
3691 for i in 0..l.count() {
3696 if l.name_at(i, false)? == iri {
3697 return l.graph_at(i, false).map(|(_, g)| g);
3698 }
3699 }
3700 None
3701 }
3702 }
3703 }
3704
3705 fn load_incomplete(&self) -> bool {
3706 match self {
3707 NamedGraphsSlot::Resident(v) => v.iter().any(|(_, g)| g.load_incomplete()),
3708 NamedGraphsSlot::Lazy(l) => {
3709 if l.failed.load(std::sync::atomic::Ordering::Relaxed) {
3710 return true;
3711 }
3712 let mut bad = false;
3713 l.for_each_opened(|g| bad |= g.load_incomplete());
3714 bad
3715 }
3716 NamedGraphsSlot::Quad(q) => q.load_incomplete(),
3717 }
3718 }
3719
3720 fn reset_load_failures(&self) {
3721 match self {
3722 NamedGraphsSlot::Resident(v) => {
3723 for (_, g) in v {
3724 g.reset_load_failure();
3725 }
3726 }
3727 NamedGraphsSlot::Lazy(l) => {
3728 l.failed.store(false, std::sync::atomic::Ordering::Relaxed);
3729 l.for_each_opened(|g| g.reset_load_failure());
3730 }
3731 NamedGraphsSlot::Quad(q) => q.reset_load_failures(),
3732 }
3733 }
3734
3735 fn resident(
3739 header: &Header,
3740 quad_section: impl FnOnce(u64, u64) -> Result<Vec<u8>, FileError>,
3741 per_graph: impl FnOnce() -> Result<Vec<(String, GraphIndex)>, FileError>,
3742 ) -> Result<Self, FileError> {
3743 check_named_layout(header)?;
3744 if let Some(q) = header.quad_index_section() {
3745 let bytes = quad_section(q.offset, q.length)?;
3746 return Ok(NamedGraphsSlot::Quad(crate::quad::QuadNamed::resident(
3747 bytes,
3748 header.block_codec,
3749 header.perms,
3750 )));
3751 }
3752 Ok(NamedGraphsSlot::Resident(if header.named_graphs_len > 0 {
3753 per_graph()?
3754 } else {
3755 Vec::new()
3756 }))
3757 }
3758}
3759
3760pub struct Rete {
3762 header: Header,
3763 dict: Dictionary,
3764 index: GraphIndex,
3765 index_section_ranges: [ByteRange; NUM_PERMS],
3766 tile_ranges: [Vec<(u32, u32, ByteRange)>; NUM_PERMS],
3770 pyramid: PyramidSlot,
3771 text_index: TextIndexSlot,
3772 labels: SectionSlot<crate::label_index::LabelIndex>,
3774 schema: SectionSlot<crate::schema_section::SchemaMeta>,
3776 named_graphs: NamedGraphsSlot,
3777 metadata: Vec<u8>,
3782 service_client: Option<Box<dyn crate::service::ServiceClient>>,
3787 service_error: std::sync::Mutex<Option<String>>,
3791 spill_threshold: std::sync::atomic::AtomicU64,
3794}
3795
3796impl Rete {
3797 pub fn open(bytes: &[u8]) -> Result<Self, FileError> {
3800 let header = Header::from_bytes(bytes)?;
3801
3802 let region = |off: u64, len: u64| -> Result<&[u8], FileError> {
3806 let start = off as usize;
3807 let end = start
3808 .checked_add(len as usize)
3809 .filter(|&e| e <= bytes.len())
3810 .ok_or(FileError::Container("section range out of bounds"))?;
3811 Ok(&bytes[start..end])
3812 };
3813
3814 let dict = decode_dictionary_container(
3815 region(header.dictionary_offset, header.dictionary_len)?,
3816 header.dict_codec,
3817 )?;
3818
3819 check_named_layout(&header)?;
3820 let index_bytes = region(header.root_dir_offset, header.root_dir_len)?;
3821 let (index, _) = decode_root_index_container(index_bytes, &header)?;
3822 let index_section_ranges = decode_index_section_ranges(
3823 index_bytes,
3824 header.root_dir_offset,
3825 header.perms,
3826 header.index_container_sections(),
3827 )?;
3828
3829 let pyramid = PyramidSlot::Resident(if header.pyramid_meta_len > 0 {
3830 Some(
3831 PyramidMeta::decode(region(header.pyramid_meta_offset, header.pyramid_meta_len)?)
3832 .map_err(|_| FileError::Container("malformed pyramid meta"))?,
3833 )
3834 } else {
3835 None
3836 });
3837
3838 let (ti_off, ti_len) = header.text_index_payload();
3841 let text_index = resident_text_index_slot(
3842 if ti_len > 0 {
3843 Some(region(ti_off, ti_len)?)
3844 } else {
3845 None
3846 },
3847 header.block_codec,
3848 )?;
3849 let labels = resident_label_slot(&header, ®ion);
3850 let schema = resident_schema_slot(&header, ®ion);
3851
3852 let named_graphs = NamedGraphsSlot::resident(
3853 &header,
3854 |off, len| Ok(region(off, len)?.to_vec()),
3855 || {
3856 decode_named_graphs(
3857 region(header.named_graphs_offset, header.named_graphs_len)?,
3858 header.block_codec,
3859 header.perms,
3860 )
3861 },
3862 )?;
3863
3864 let metadata = if header.metadata_len > 0 {
3865 region(header.metadata_offset, header.metadata_len)?.to_vec()
3866 } else {
3867 Vec::new()
3868 };
3869
3870 let tile_ranges =
3871 tile_file_ranges(index_bytes, header.root_dir_offset, &index_section_ranges);
3872 Ok(Self {
3873 header,
3874 dict,
3875 index,
3876 index_section_ranges,
3877 tile_ranges,
3878 pyramid,
3879 text_index,
3880 labels,
3881 schema,
3882 named_graphs,
3883 metadata,
3884 service_client: None,
3885 service_error: std::sync::Mutex::new(None),
3886 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
3887 })
3888 }
3889
3890 pub fn set_service_client(&mut self, client: Box<dyn crate::service::ServiceClient>) {
3895 self.service_client = Some(client);
3896 }
3897
3898 pub(crate) fn service_client(&self) -> Option<&dyn crate::service::ServiceClient> {
3899 self.service_client.as_deref()
3900 }
3901
3902 pub(crate) fn record_service_error(&self, msg: &str) {
3904 let mut e = self.service_error.lock().unwrap();
3905 if e.is_none() {
3906 *e = Some(msg.to_string());
3907 }
3908 }
3909
3910 pub(crate) fn take_service_error(&self) -> Option<String> {
3913 self.service_error.lock().unwrap().take()
3914 }
3915
3916 pub fn header(&self) -> &Header {
3917 &self.header
3918 }
3919
3920 pub fn file_layout(&self) -> Vec<LayoutSegment> {
3926 let h = &self.header;
3927 let seg = |kind: &'static str, label: String, offset: u64, len: u64| LayoutSegment {
3928 kind,
3929 label,
3930 offset,
3931 len,
3932 };
3933 let mut out = vec![seg(
3939 "header",
3940 format!("header (fixed {} bytes)", crate::header::HEADER_LEN),
3941 0,
3942 crate::header::HEADER_LEN as u64,
3943 )];
3944 if h.metadata_len > 0 {
3945 out.push(seg(
3946 "metadata",
3947 "metadata (dataset card)".into(),
3948 h.metadata_offset,
3949 h.metadata_len,
3950 ));
3951 }
3952 out.push(seg(
3953 "dictionary",
3954 "dictionary (4 front-coded term sections)".into(),
3955 h.dictionary_offset,
3956 h.dictionary_len,
3957 ));
3958 for (si, perm) in crate::index::ALL_PERMS.into_iter().enumerate() {
3959 let sec = self.index_section_ranges[si];
3960 if sec.len == 0 {
3961 continue;
3962 }
3963 let first_tile = self.tile_ranges[si]
3964 .first()
3965 .map(|&(_, _, r)| r.offset)
3966 .unwrap_or(sec.offset + sec.len);
3967 if first_tile > sec.offset {
3968 out.push(seg(
3969 "directory",
3970 format!("{} tile directory", perm.name()),
3971 sec.offset,
3972 first_tile - sec.offset,
3973 ));
3974 }
3975 for (ti, &(min_a, max_a, r)) in self.tile_ranges[si].iter().enumerate() {
3976 out.push(seg(
3977 "tile",
3978 format!("{} tile {ti} (leading ids {min_a}..{max_a})", perm.name()),
3979 r.offset,
3980 r.len,
3981 ));
3982 }
3983 }
3984 if h.pyramid_meta_len > 0 {
3985 out.push(seg(
3986 "pyramid",
3987 "pyramid summary (communities + superedges)".into(),
3988 h.pyramid_meta_offset,
3989 h.pyramid_meta_len,
3990 ));
3991 }
3992 if h.named_graphs_len > 0 {
3993 out.push(seg(
3994 "named-graphs",
3995 format!("named graphs ({})", self.named_graphs.count()),
3996 h.named_graphs_offset,
3997 h.named_graphs_len,
3998 ));
3999 }
4000 if let Some(s) = h.label_index_section() {
4001 let label = match self.labels.get_if_loaded() {
4002 Some(li) => format!("label index ({} entries)", li.entry_count()),
4003 None => "label index".to_string(),
4004 };
4005 out.push(seg("label-index", label, s.offset, s.length));
4006 }
4007 if let Some(s) = h.schema_section() {
4008 out.push(seg(
4009 "schema",
4010 "schema (classes, query_stats, shapes)".into(),
4011 s.offset,
4012 s.length,
4013 ));
4014 }
4015 if let Some(q) = h.quad_index_section() {
4016 out.push(seg(
4017 "quad-index",
4018 format!("quad index ({} named graphs)", self.named_graphs.count()),
4019 q.offset,
4020 q.length,
4021 ));
4022 }
4023 out.sort_by_key(|s| s.offset);
4024 out
4025 }
4026
4027 pub fn metadata(&self) -> Option<&[u8]> {
4032 if self.metadata.is_empty() {
4033 None
4034 } else {
4035 Some(&self.metadata)
4036 }
4037 }
4038
4039 pub fn dictionary(&self) -> &Dictionary {
4040 &self.dict
4041 }
4042
4043 pub fn pyramid(&self) -> Option<&PyramidMeta> {
4045 match &self.pyramid {
4046 PyramidSlot::Resident(p) => p.as_ref(),
4047 PyramidSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
4049 }
4050 }
4051
4052 pub fn pyramid_if_loaded(&self) -> Option<&PyramidMeta> {
4057 match &self.pyramid {
4058 PyramidSlot::Resident(p) => p.as_ref(),
4059 PyramidSlot::Lazy { cell, .. } => cell.get().and_then(|o| o.as_ref()),
4060 }
4061 }
4062
4063 pub fn schema(&self) -> Option<&crate::schema_section::SchemaMeta> {
4069 self.schema.get()
4070 }
4071
4072 pub fn schema_or_pyramid(
4076 &self,
4077 ) -> Option<std::borrow::Cow<'_, crate::schema_section::SchemaMeta>> {
4078 if let Some(s) = self.schema() {
4079 return Some(std::borrow::Cow::Borrowed(s));
4080 }
4081 self.pyramid()
4082 .map(|p| std::borrow::Cow::Owned(crate::schema_section::SchemaMeta::from_pyramid(p)))
4083 }
4084
4085 pub fn predicate_stats(&self) -> &[crate::meta::PredStat] {
4090 if let Some(s) = self.schema().filter(|s| !s.predicate_stats.is_empty()) {
4091 return &s.predicate_stats;
4092 }
4093 self.pyramid_if_loaded()
4094 .map(|p| p.predicate_stats.as_slice())
4095 .unwrap_or(&[])
4096 }
4097
4098 pub(crate) fn planner_stats(&self, fault: bool) -> &[crate::meta::PredStat] {
4103 let schema = if fault {
4104 self.schema.get()
4105 } else {
4106 self.schema.get_if_loaded()
4107 };
4108 if let Some(s) = schema.filter(|s| !s.predicate_stats.is_empty()) {
4109 return &s.predicate_stats;
4110 }
4111 self.pyramid_if_loaded()
4112 .map(|p| p.predicate_stats.as_slice())
4113 .unwrap_or(&[])
4114 }
4115
4116 pub fn char_sets(&self) -> &[crate::meta::CharSet] {
4119 if let Some(s) = self.schema().filter(|s| !s.char_sets.is_empty()) {
4120 return &s.char_sets;
4121 }
4122 self.pyramid_if_loaded()
4123 .map(|p| p.char_sets.as_slice())
4124 .unwrap_or(&[])
4125 }
4126
4127 pub fn label_index(&self) -> &[crate::meta::LabelEntry] {
4133 self.pyramid_if_loaded()
4134 .map(|p| p.label_index.as_slice())
4135 .unwrap_or(&[])
4136 }
4137
4138 pub fn has_label_index(&self) -> bool {
4140 self.header.label_index_section().is_some()
4141 }
4142
4143 pub fn label_index_section(&self) -> Option<&crate::label_index::LabelIndex> {
4145 self.labels.get()
4146 }
4147
4148 pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
4155 if let Some(li) = self.labels.get() {
4156 return li.prefix_search(prefix, limit);
4157 }
4158 let Some(pyr) = self.pyramid() else {
4159 return Vec::new();
4160 };
4161 resolve_label_hits(&self.dict, pyr.prefix_search(prefix, limit))
4162 }
4163
4164 pub(crate) fn text_index(&self) -> Option<&crate::text_index::TextIndex> {
4167 self.text_index.index()
4168 }
4169
4170 pub fn has_text_index(&self) -> bool {
4173 self.header.text_index_len > 0
4174 }
4175
4176 pub fn text_index_scope(&self) -> Option<crate::text_index::TextIndexScope> {
4180 self.text_index().map(|ti| ti.scope().clone())
4181 }
4182
4183 pub fn text_index_token_table_len(&self) -> Option<u64> {
4198 self.text_index.token_table_len()
4199 }
4200
4201 pub fn text_search(&self, words: &[&str], prefix: Option<&str>, limit: usize) -> Vec<String> {
4211 text_search_in(self.text_index(), &self.dict, words, prefix, limit)
4212 }
4213
4214 pub fn default_index(&self) -> &GraphIndex {
4216 &self.index
4217 }
4218
4219 pub fn dump(&self, graph: Option<&str>) -> Vec<TermTriple> {
4221 let index = match graph {
4222 None => &self.index,
4223 Some(g) => match self.graph_index(g) {
4224 Some(i) => i,
4225 None => return Vec::new(),
4226 },
4227 };
4228 index
4229 .match_pattern((None, None, None))
4230 .into_iter()
4231 .filter_map(|(s, p, o)| {
4232 Some((
4233 self.dict.subject_term(s)?,
4234 self.dict.predicate_term(p)?,
4235 self.dict.object_term(o)?,
4236 ))
4237 })
4238 .collect()
4239 }
4240
4241 pub fn dump_each<F: FnMut(&str, &str, &str)>(&self, graph: Option<&str>, f: F) {
4257 self.dump_filtered_each(graph, None, None, None, f)
4258 }
4259
4260 pub fn dump_filtered_each<F: FnMut(&str, &str, &str)>(
4331 &self,
4332 graph: Option<&str>,
4333 s: Option<&str>,
4334 p: Option<&str>,
4335 o: Option<&str>,
4336 mut f: F,
4337 ) {
4338 let index = match graph {
4339 None => Some(&self.index),
4340 Some(g) => self.graph_index(g),
4341 };
4342 let Some(index) = index else { return };
4343 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
4346 return;
4347 };
4348 let dict = &self.dict;
4349 const WINDOW: usize = 4096;
4394 let cap = dict.cache_cap();
4405 let prefetch_window =
4406 cap == u64::MAX || cap >= (WINDOW as u64) * 2 * (DICT_CHUNK_BUDGET as u64);
4407 const PREFETCH_BATCH: usize = 64;
4414 let prefetch_batch = if cap == u64::MAX {
4415 usize::MAX
4416 } else {
4417 PREFETCH_BATCH
4418 };
4419 let mut ids: Vec<(u32, u32, u32)> = Vec::with_capacity(WINDOW);
4420 let mut nodes: Vec<u32> = Vec::with_capacity(WINDOW * 2);
4421 let mut preds: Vec<u32> = Vec::with_capacity(WINDOW);
4422 let mut resolver = crate::dictionary::WindowResolver::default();
4423 let mut flush = |ids: &mut Vec<(u32, u32, u32)>,
4424 resolver: &mut crate::dictionary::WindowResolver,
4425 f: &mut F| {
4426 if ids.is_empty() {
4427 return;
4428 }
4429 if prefetch_window {
4433 nodes.clear();
4434 preds.clear();
4435 for &(s, p, o) in ids.iter() {
4436 nodes.push(dict.subject_node(s));
4437 nodes.push(dict.object_node(o));
4438 preds.push(p);
4439 }
4440 dict.prefetch_terms_batched(&nodes, &preds, prefetch_batch);
4441 }
4442 dict.resolve_window(ids, resolver, |s, p, o| f(s, p, o));
4443 ids.clear();
4444 };
4445 for t in index.scan_iter(pattern) {
4446 ids.push(t);
4447 if ids.len() == WINDOW {
4448 flush(&mut ids, &mut resolver, &mut f);
4449 }
4450 }
4451 flush(&mut ids, &mut resolver, &mut f);
4452 }
4453
4454 pub fn dump_plan(
4462 &self,
4463 graph: Option<&str>,
4464 s: Option<&str>,
4465 p: Option<&str>,
4466 o: Option<&str>,
4467 ) -> DumpPlan {
4468 let index = match graph {
4469 None => Some(&self.index),
4470 Some(g) => self.graph_index(g),
4471 };
4472 let scan = index
4473 .zip(self.resolve_query_pattern(s, p, o))
4474 .map(|(ix, pattern)| ix.scan_plan(pattern));
4475 DumpPlan {
4476 scan,
4477 dictionary_bytes: self.header.dictionary_len,
4478 }
4479 }
4480
4481 pub fn dump_iter(&self, graph: Option<&str>) -> impl Iterator<Item = TermTriple> + '_ {
4499 self.query_iter(graph, None, None, None)
4500 }
4501
4502 pub fn query_iter(
4513 &self,
4514 graph: Option<&str>,
4515 s: Option<&str>,
4516 p: Option<&str>,
4517 o: Option<&str>,
4518 ) -> impl Iterator<Item = TermTriple> + '_ {
4519 let index = match graph {
4520 None => Some(&self.index),
4521 Some(g) => self.graph_index(g),
4522 };
4523 index
4524 .zip(self.resolve_query_pattern(s, p, o))
4525 .into_iter()
4526 .flat_map(|(ix, pattern)| ix.scan_iter(pattern))
4527 .filter_map(move |(s, p, o)| {
4528 Some((
4529 self.dict.subject_term(s)?,
4530 self.dict.predicate_term(p)?,
4531 self.dict.object_term(o)?,
4532 ))
4533 })
4534 }
4535
4536 pub fn query_batch(
4555 &self,
4556 graph: Option<&str>,
4557 s: Option<&str>,
4558 p: Option<&str>,
4559 o: Option<&str>,
4560 cursor: u64,
4561 max_quads: usize,
4562 ) -> (Vec<TermTriple>, u64, bool) {
4563 let index = match graph {
4564 None => &self.index,
4565 Some(g) => match self.graph_index(g) {
4568 Some(i) => i,
4569 None => return (Vec::new(), cursor, true),
4570 },
4571 };
4572 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
4575 return (Vec::new(), cursor, true);
4576 };
4577 let (ids, next, done) = index.scan_batch(pattern, cursor, max_quads);
4578 if !ids.is_empty() {
4582 let mut nodes = Vec::with_capacity(ids.len() * 2);
4583 let mut preds = Vec::with_capacity(ids.len());
4584 for &(s, p, o) in &ids {
4585 nodes.push(self.dict.subject_node(s));
4586 nodes.push(self.dict.object_node(o));
4587 preds.push(p);
4588 }
4589 self.dict.prefetch_terms(&nodes, &preds);
4590 }
4591 let triples = ids
4592 .into_iter()
4593 .filter_map(|(s, p, o)| {
4594 Some((
4595 self.dict.subject_term(s)?,
4596 self.dict.predicate_term(p)?,
4597 self.dict.object_term(o)?,
4598 ))
4599 })
4600 .collect();
4601 (triples, next, done)
4602 }
4603
4604 pub fn dump_batch(
4630 &self,
4631 graph: Option<&str>,
4632 cursor: u32,
4633 max_quads: usize,
4634 ) -> (Vec<TermTriple>, u32, bool) {
4635 fn next_tile_start(tiles: &[crate::index::Tile], sid: u32) -> Option<u32> {
4640 let i = tiles.partition_point(|t| t.leading_range().1 < sid);
4641 tiles.get(i).map(|t| t.leading_range().0.max(sid))
4642 }
4643
4644 let index = match graph {
4645 None => self.default_index(),
4646 Some(g) => match self.graph_index(g) {
4649 Some(i) => i,
4650 None => return (Vec::new(), cursor, true),
4651 },
4652 };
4653 let tiles = index.tile_sections()[IndexPermutation::Spo.section_index()];
4657 let (Some(first), Some(last)) = (
4658 tiles.first().map(|t| t.leading_range().0),
4659 tiles.last().map(|t| t.leading_range().1),
4660 ) else {
4661 return (Vec::new(), cursor, true); };
4663
4664 let mut sid = cursor.max(first);
4665 let mut probes = max_quads.saturating_mul(4).max(1 << 16);
4666 let mut ids: Vec<(u32, u32, u32)> = Vec::new();
4667 let mut exhausted = false;
4668 while sid <= last && probes > 0 {
4669 probes -= 1;
4670 let hits = index.match_pattern((Some(sid), None, None));
4671 if hits.is_empty() {
4672 match index.next_leading(IndexPermutation::Spo.section_index(), sid) {
4682 Some(next) if next > sid => {
4683 sid = next;
4684 continue;
4685 }
4686 None if !index.load_incomplete() => {
4687 exhausted = true;
4688 break;
4689 }
4690 _ => {}
4691 }
4692 match next_tile_start(tiles, sid) {
4693 None => {
4694 exhausted = true;
4695 break;
4696 }
4697 Some(next) if next > sid => {
4698 sid = next;
4699 continue;
4700 }
4701 Some(_) => {}
4702 }
4703 } else {
4704 ids.extend(hits);
4705 }
4706 if sid == u32::MAX {
4707 exhausted = true;
4708 break;
4709 }
4710 sid += 1;
4711 if ids.len() >= max_quads {
4712 break;
4713 }
4714 }
4715 let done = exhausted || sid > last;
4716
4717 let dict = self.dictionary();
4724 if !ids.is_empty() {
4725 let mut nodes = Vec::with_capacity(ids.len() * 2);
4726 let mut preds = Vec::with_capacity(ids.len());
4727 for &(s, p, o) in &ids {
4728 nodes.push(dict.subject_node(s));
4729 nodes.push(dict.object_node(o));
4730 preds.push(p);
4731 }
4732 dict.prefetch_terms(&nodes, &preds);
4733 }
4734 let triples = ids
4735 .into_iter()
4736 .filter_map(|(s, p, o)| {
4737 Some((
4738 dict.subject_term(s)?,
4739 dict.predicate_term(p)?,
4740 dict.object_term(o)?,
4741 ))
4742 })
4743 .collect();
4744 (triples, sid, done)
4745 }
4746
4747 pub fn named_graph_count(&self) -> usize {
4750 self.named_graphs.count()
4751 }
4752
4753 pub fn named_graph_quad_count(&self, i: usize) -> Option<u64> {
4758 match &self.named_graphs {
4759 NamedGraphsSlot::Quad(q) => q.quad_count_at(i),
4760 _ => None,
4761 }
4762 }
4763
4764 pub fn named_graph_transient(&self, i: usize) -> Option<GraphIndex> {
4773 match &self.named_graphs {
4774 NamedGraphsSlot::Quad(q) => q.transient_view(i, TRANSIENT_GRAPH_QUADS),
4775 _ => None,
4776 }
4777 }
4778
4779 pub(crate) fn named_graphs_with_subjects(
4786 &self,
4787 subjects: &std::collections::BTreeSet<u32>,
4788 ) -> Option<Vec<usize>> {
4789 match &self.named_graphs {
4790 NamedGraphsSlot::Quad(q) => q.graphs_with_subjects(subjects),
4791 _ => None,
4792 }
4793 }
4794
4795 pub fn named_quad_count(&self) -> Option<u64> {
4800 match &self.named_graphs {
4801 NamedGraphsSlot::Quad(q) => Some(q.quad_count()),
4802 _ => None,
4803 }
4804 }
4805
4806 pub fn named_graph_name_at(&self, i: usize) -> Option<&str> {
4809 self.named_graphs.name_at(&self.dict, i, false)
4810 }
4811
4812 pub fn named_graph_at(&self, i: usize) -> Option<(&str, &GraphIndex)> {
4816 self.named_graphs.graph_at(&self.dict, i, false)
4817 }
4818
4819 pub(crate) fn named_graph_name_at_demand(&self, i: usize, exhaustive: bool) -> Option<&str> {
4825 self.named_graphs.name_at(&self.dict, i, exhaustive)
4826 }
4827
4828 pub(crate) fn named_graph_at_demand(
4831 &self,
4832 i: usize,
4833 exhaustive: bool,
4834 ) -> Option<(&str, &GraphIndex)> {
4835 self.named_graphs.graph_at(&self.dict, i, exhaustive)
4836 }
4837
4838 pub fn graph_names(&self) -> Vec<&str> {
4843 (0..self.named_graphs.count())
4844 .filter_map(|i| self.named_graphs.name_at(&self.dict, i, false))
4845 .collect()
4846 }
4847
4848 pub fn graph_index(&self, iri: &str) -> Option<&GraphIndex> {
4850 self.named_graphs.find(&self.dict, iri)
4851 }
4852
4853 pub fn match_ids(
4856 &self,
4857 pattern: (Option<u32>, Option<u32>, Option<u32>),
4858 ) -> Vec<(u32, u32, u32)> {
4859 self.index.match_pattern(pattern)
4860 }
4861
4862 pub fn predicate_pairs(&self, predicate: &str) -> Vec<(u32, u32)> {
4865 let pid = match self.dict.predicate_id(predicate) {
4866 Some(p) => p,
4867 None => return Vec::new(),
4868 };
4869 self.index
4870 .match_pattern((None, Some(pid), None))
4871 .into_iter()
4872 .map(|(s, _p, o)| (self.dict.subject_node(s), self.dict.object_node(o)))
4873 .collect()
4874 }
4875
4876 pub fn open_ranged<R: RangeReader>(reader: &R) -> Result<Self, FileError> {
4880 let head = reader.read_at(0, HEADER_LEN as u64)?;
4881 let header = Header::from_bytes(&head)?;
4882
4883 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
4884 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
4885
4886 check_named_layout(&header)?;
4887 let index_bytes = reader.read_at(header.root_dir_offset, header.root_dir_len)?;
4888 let (index, _) = decode_root_index_container(&index_bytes, &header)?;
4889 let index_section_ranges = decode_index_section_ranges(
4890 &index_bytes,
4891 header.root_dir_offset,
4892 header.perms,
4893 header.index_container_sections(),
4894 )?;
4895
4896 let pyramid = PyramidSlot::Resident(if header.pyramid_meta_len > 0 {
4897 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
4898 Some(
4899 PyramidMeta::decode(&mb)
4900 .map_err(|_| FileError::Container("malformed pyramid meta"))?,
4901 )
4902 } else {
4903 None
4904 });
4905
4906 let (ti_off, ti_len) = header.text_index_payload();
4909 let text_index_bytes = if ti_len > 0 {
4910 Some(reader.read_at(ti_off, ti_len)?)
4911 } else {
4912 None
4913 };
4914 let text_index = resident_text_index_slot(text_index_bytes.as_deref(), header.block_codec)?;
4915 let labels = SectionSlot::Resident(header.label_index_section().and_then(|s| {
4918 let b = reader.read_at(s.offset, s.length).ok()?;
4919 crate::label_index::LabelIndex::from_section(&b, header.block_codec).ok()
4920 }));
4921 let schema = SectionSlot::Resident(read_schema_with_header(reader, &header));
4922
4923 let named_graphs = NamedGraphsSlot::resident(
4924 &header,
4925 |off, len| Ok(reader.read_at(off, len)?),
4926 || {
4927 let nb = reader.read_at(header.named_graphs_offset, header.named_graphs_len)?;
4928 decode_named_graphs(&nb, header.block_codec, header.perms)
4929 },
4930 )?;
4931
4932 let tile_ranges =
4936 tile_file_ranges(&index_bytes, header.root_dir_offset, &index_section_ranges);
4937 Ok(Self {
4938 header,
4939 dict,
4940 index,
4941 index_section_ranges,
4942 tile_ranges,
4943 pyramid,
4944 text_index,
4945 labels,
4946 schema,
4947 named_graphs,
4948 metadata: Vec::new(),
4949 service_client: None,
4950 service_error: std::sync::Mutex::new(None),
4951 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
4952 })
4953 }
4954
4955 pub fn open_ranged_lazy<R: RangeReader + Send + Sync + 'static>(
4967 reader: R,
4968 ) -> Result<Self, FileError> {
4969 let head = reader.read_at(0, HEADER_LEN as u64)?;
4970 let header = Header::from_bytes(&head)?;
4971 check_named_layout(&header)?;
4972 let reader = std::sync::Arc::new(reader);
4973 let read_concurrency = reader.concurrency();
4976
4977 let open_dict = || ranged_chunked_dictionary(&reader, &header, [true; 4]);
4981
4982 let reader_dyn: std::sync::Arc<dyn RangeReader + Send + Sync> = reader.clone();
4986 let open_index = || {
4987 open_index_container_lazy(
4988 &reader_dyn,
4989 ByteRange {
4990 offset: header.root_dir_offset,
4991 len: header.root_dir_len,
4992 },
4993 header.block_codec,
4994 header.has_tile_synopsis(),
4995 read_concurrency,
4996 header.perms,
4997 header.index_container_sections(),
4998 )
4999 };
5000
5001 let (dict, index) = open_side_by_side(read_concurrency, open_dict, open_index);
5008 let dict = dict?;
5009 let (index, index_section_ranges, tile_ranges) = index?;
5010
5011 let pyramid = ranged_pyramid_slot(&reader, &header);
5015
5016 let text_index = ranged_text_index_slot(&reader, &header);
5021
5022 let labels = ranged_label_slot(&reader, &header);
5026 let schema = ranged_schema_slot(&reader, &header);
5027
5028 let named_graphs = if let Some(q) = header.quad_index_section() {
5035 NamedGraphsSlot::Quad(crate::quad::QuadNamed::new(
5039 reader_dyn,
5040 ByteRange {
5041 offset: q.offset,
5042 len: q.length,
5043 },
5044 header.block_codec,
5045 header.perms,
5046 read_concurrency,
5047 ))
5048 } else if header.named_graphs_len > 0 {
5049 NamedGraphsSlot::Lazy(LazyNamedGraphs::new(
5050 reader_dyn,
5051 ByteRange {
5052 offset: header.named_graphs_offset,
5053 len: header.named_graphs_len,
5054 },
5055 header.block_codec,
5056 header.has_tile_synopsis(),
5057 read_concurrency,
5058 header.perms,
5059 ))
5060 } else {
5061 NamedGraphsSlot::Resident(Vec::new())
5062 };
5063
5064 let rete = Self {
5065 header,
5066 dict,
5067 index,
5068 index_section_ranges,
5069 tile_ranges,
5070 pyramid,
5071 text_index,
5072 labels,
5073 schema,
5074 named_graphs,
5075 metadata: Vec::new(),
5076 service_client: None,
5077 service_error: std::sync::Mutex::new(None),
5078 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
5079 };
5080 #[cfg(not(target_arch = "wasm32"))]
5092 rete.set_memory_budget(match crate::sysmem::default_query_budget_mb() {
5093 0 => None,
5094 mb => Some(mb << 20),
5095 });
5096 Ok(rete)
5097 }
5098
5099 pub fn index_incomplete(&self) -> bool {
5104 self.index.load_incomplete()
5105 || self.dict.load_incomplete()
5106 || self.named_graphs.load_incomplete()
5107 }
5108
5109 pub fn reset_load_failures(&self) {
5117 self.index.reset_load_failure();
5118 self.dict.reset_load_failure();
5119 self.named_graphs.reset_load_failures();
5120 }
5121
5122 pub fn set_memory_budget(&self, budget: Option<u64>) -> MemoryBudget {
5153 let total = budget.unwrap_or(u64::MAX);
5154 self.set_spill_threshold(budget.map(spill_threshold_for));
5155 let mut split = split_memory_budget(total);
5156 let fixed = if total == u64::MAX {
5157 0
5158 } else {
5159 self.fixed_overhead().total()
5160 };
5161 if split.dict_cache != u64::MAX {
5162 split.dict_cache = split
5163 .dict_cache
5164 .saturating_sub(fixed)
5165 .max(MIN_DICT_CACHE_CAP);
5166 }
5167 self.dict.set_cache_cap(split.dict_cache);
5168 self.index.set_cache_cap(split.tile_cache);
5169 match &self.named_graphs {
5174 NamedGraphsSlot::Lazy(lazy) => lazy.set_tile_cap(split.tile_cache),
5175 NamedGraphsSlot::Quad(q) => q.set_tile_cap(split.tile_cache),
5178 NamedGraphsSlot::Resident(_) => {}
5179 }
5180 if std::env::var("RETE_OPEN_DEBUG").is_ok() {
5181 eprintln!(
5182 "[memory-budget] total={} dict_cache={} tile_cache={} block_cache={} fixed={:.1}MiB",
5183 if total == u64::MAX {
5184 "unlimited".to_string()
5185 } else {
5186 format!("{}MiB", total >> 20)
5187 },
5188 fmt_cap(split.dict_cache),
5189 fmt_cap(split.tile_cache),
5190 fmt_cap(split.block_cache),
5191 fixed as f64 / (1u64 << 20) as f64,
5192 );
5193 }
5194 split
5195 }
5196
5197 pub fn spill_threshold(&self) -> u64 {
5204 self.spill_threshold
5205 .load(std::sync::atomic::Ordering::Relaxed)
5206 }
5207
5208 pub fn set_spill_threshold(&self, bytes: Option<u64>) {
5212 self.spill_threshold.store(
5213 bytes.unwrap_or(u64::MAX),
5214 std::sync::atomic::Ordering::Relaxed,
5215 );
5216 }
5217
5218 pub fn dict_cache_stats(&self) -> crate::chunk_cache::CacheStats {
5222 self.dict.cache_stats()
5223 }
5224
5225 pub fn fixed_overhead(&self) -> FixedOverhead {
5229 FixedOverhead {
5230 dict_directory: self.dict.directory_bytes(),
5231 index_directory: self.index.directory_bytes(),
5232 named_graphs: self.named_graphs.directory_bytes(),
5233 }
5234 }
5235
5236 pub fn index_cache_stats(&self) -> crate::chunk_cache::CacheStats {
5238 self.index.cache_stats()
5239 }
5240
5241 pub fn dict_cache_cap(&self) -> u64 {
5243 self.dict.cache_cap()
5244 }
5245
5246 pub fn release_named_graph(&mut self, iri: &str) {
5259 match &mut self.named_graphs {
5260 NamedGraphsSlot::Lazy(lazy) => lazy.release(iri),
5261 NamedGraphsSlot::Quad(q) => q.release(&self.dict, iri),
5262 NamedGraphsSlot::Resident(_) => {}
5263 }
5264 }
5265
5266 fn resolve_query_pattern(
5267 &self,
5268 s: Option<&str>,
5269 p: Option<&str>,
5270 o: Option<&str>,
5271 ) -> Option<Pattern> {
5272 let sid = match s {
5273 Some(t) => match self.dict.subject_id(t) {
5274 Some(id) => Some(id),
5275 None => return None,
5276 },
5277 None => None,
5278 };
5279 let pid = match p {
5280 Some(t) => match self.dict.predicate_id(t) {
5281 Some(id) => Some(id),
5282 None => return None,
5283 },
5284 None => None,
5285 };
5286 let oid = match o {
5287 Some(t) => match self.dict.object_id(t) {
5288 Some(id) => Some(id),
5289 None => return None,
5290 },
5291 None => None,
5292 };
5293 Some((sid, pid, oid))
5294 }
5295
5296 pub fn query_with_provenance(
5300 &self,
5301 s: Option<&str>,
5302 p: Option<&str>,
5303 o: Option<&str>,
5304 ) -> Vec<TripleProvenance> {
5305 let pattern = match self.resolve_query_pattern(s, p, o) {
5306 Some(pattern) => pattern,
5307 None => return Vec::new(),
5308 };
5309
5310 let index_permutation = GraphIndex::best_permutation_in(self.header.perms, pattern);
5311 let dictionary_range = ByteRange {
5312 offset: self.header.dictionary_offset,
5313 len: self.header.dictionary_len,
5314 };
5315 let index_range = ByteRange {
5316 offset: self.header.root_dir_offset,
5317 len: self.header.root_dir_len,
5318 };
5319 let index_section_range = self.index_section_ranges[index_permutation.section_index()];
5320 let pyramid_range = (self.header.pyramid_meta_len > 0).then_some(ByteRange {
5321 offset: self.header.pyramid_meta_offset,
5322 len: self.header.pyramid_meta_len,
5323 });
5324
5325 let tiles = &self.tile_ranges[index_permutation.section_index()];
5326 self.index
5327 .match_pattern(pattern)
5328 .into_iter()
5329 .filter_map(|(s, p, o)| {
5330 let terms = (
5331 self.dict.subject_term(s)?,
5332 self.dict.predicate_term(p)?,
5333 self.dict.object_term(o)?,
5334 );
5335 let a = index_permutation.forward((s, p, o)).0;
5338 let ti = tiles.partition_point(|&(_, max_a, _)| max_a < a);
5339 let (tile, tile_range) = match tiles.get(ti) {
5340 Some(&(min_a, _, range)) if min_a <= a => (
5341 Some(format!("{}/{ti}", index_permutation.name())),
5342 Some(range),
5343 ),
5344 _ => (None, None),
5345 };
5346 Some(TripleProvenance {
5347 terms,
5348 ids: (s, p, o),
5349 graph: None,
5350 matched_pattern: pattern,
5351 index_permutation,
5352 dictionary_range,
5353 index_range,
5354 index_section_range,
5355 pyramid_range,
5356 tile,
5357 tile_range,
5358 })
5359 })
5360 .collect()
5361 }
5362
5363 pub fn query(&self, s: Option<&str>, p: Option<&str>, o: Option<&str>) -> Vec<TermTriple> {
5367 self.query_with_provenance(s, p, o)
5368 .into_iter()
5369 .map(|m| m.terms)
5370 .collect()
5371 }
5372
5373 pub fn query_in_graph(
5381 &self,
5382 graph: Option<&str>,
5383 s: Option<&str>,
5384 p: Option<&str>,
5385 o: Option<&str>,
5386 ) -> Vec<TermTriple> {
5387 let pattern = match self.resolve_query_pattern(s, p, o) {
5388 Some(pattern) => pattern,
5389 None => return Vec::new(),
5390 };
5391 let index = match graph {
5392 None => &self.index,
5393 Some(g) => match self.graph_index(g) {
5394 Some(i) => i,
5395 None => return Vec::new(),
5396 },
5397 };
5398 self.resolve_matches(index.match_pattern(pattern))
5399 }
5400
5401 fn resolve_matches(&self, ids: Vec<Triple>) -> Vec<TermTriple> {
5404 if !ids.is_empty() {
5412 let mut nodes = Vec::with_capacity(ids.len() * 2);
5413 let mut preds = Vec::with_capacity(ids.len());
5414 for &(s, p, o) in &ids {
5415 nodes.push(self.dict.subject_node(s));
5416 nodes.push(self.dict.object_node(o));
5417 preds.push(p);
5418 }
5419 self.dict.prefetch_terms(&nodes, &preds);
5420 }
5421 ids.into_iter()
5422 .filter_map(|(s, p, o)| {
5423 Some((
5424 self.dict.subject_term(s)?,
5425 self.dict.predicate_term(p)?,
5426 self.dict.object_term(o)?,
5427 ))
5428 })
5429 .collect()
5430 }
5431
5432 pub fn query_quads(
5437 &self,
5438 s: Option<&str>,
5439 p: Option<&str>,
5440 o: Option<&str>,
5441 ) -> Vec<(TermTriple, Option<String>)> {
5442 let mut out: Vec<(TermTriple, Option<String>)> = self
5443 .query_in_graph(None, s, p, o)
5444 .into_iter()
5445 .map(|t| (t, None))
5446 .collect();
5447 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
5448 return out;
5449 };
5450 for i in 0..self.named_graphs.count() {
5451 let Some(iri) = self.named_graphs.name_at(&self.dict, i, true) else {
5454 continue;
5455 };
5456 let owned = self.named_graph_transient(i);
5459 let index = match &owned {
5460 Some(view) => view,
5461 None => match self.named_graphs.graph_at(&self.dict, i, true) {
5462 Some((_, g)) => g,
5463 None => continue,
5464 },
5465 };
5466 for triple in self.resolve_matches(index.match_pattern(pattern)) {
5467 out.push((triple, Some(iri.to_string())));
5468 }
5469 }
5470 out
5471 }
5472
5473 pub fn query_ranged<R: RangeReader>(
5480 reader: &R,
5481 s: Option<&str>,
5482 p: Option<&str>,
5483 o: Option<&str>,
5484 ) -> Result<Vec<TermTriple>, FileError> {
5485 let routed = match route_pattern(reader, s, p, o)? {
5486 Some(routed) => routed,
5487 None => return Ok(Vec::new()),
5488 };
5489 let matches = fetch_routed_matches(reader, &routed)?;
5490 Ok(matches
5491 .into_iter()
5492 .filter_map(|(s, p, o)| {
5493 Some((
5494 routed.dict.subject_term(s)?,
5495 routed.dict.predicate_term(p)?,
5496 routed.dict.object_term(o)?,
5497 ))
5498 })
5499 .collect())
5500 }
5501
5502 pub fn route_pattern_ranged<R: RangeReader>(
5506 reader: &R,
5507 s: Option<&str>,
5508 p: Option<&str>,
5509 o: Option<&str>,
5510 ) -> Result<bool, FileError> {
5511 Ok(route_pattern(reader, s, p, o)?.is_some())
5512 }
5513}
5514
5515struct RoutedPattern {
5518 dict: Dictionary,
5519 pattern: Pattern,
5520 permutation: IndexPermutation,
5521 header: Header,
5522 section: ByteRange,
5524}
5525
5526fn route_pattern<R: RangeReader>(
5529 reader: &R,
5530 s: Option<&str>,
5531 p: Option<&str>,
5532 o: Option<&str>,
5533) -> Result<Option<RoutedPattern>, FileError> {
5534 let head = reader.read_at(0, HEADER_LEN as u64)?;
5535 let header = Header::from_bytes(&head)?;
5536
5537 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
5538 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
5539
5540 let Some(pattern) = resolve_query_pattern(&dict, s, p, o) else {
5541 return Ok(None);
5542 };
5543 let permutation = GraphIndex::best_permutation_in(header.perms, pattern);
5544 let section = locate_container_section_ranged(
5545 reader,
5546 header.root_dir_offset,
5547 header.root_dir_len,
5548 header
5549 .perms
5550 .position(permutation)
5551 .ok_or(FileError::Container("routed to an absent permutation"))?,
5552 header.index_container_sections() as u64,
5553 )?;
5554 Ok(Some(RoutedPattern {
5555 dict,
5556 pattern,
5557 permutation,
5558 header,
5559 section,
5560 }))
5561}
5562
5563fn fetch_routed_matches<R: RangeReader>(
5568 reader: &R,
5569 routed: &RoutedPattern,
5570) -> Result<Vec<Triple>, FileError> {
5571 let dir = read_tile_directory_ranged(reader, routed.section)?;
5572 let [pa, _, _] = routed.permutation.order_pattern(routed.pattern);
5573 let codec = routed.header.block_codec;
5574 let mut out = Vec::new();
5575 match pa {
5576 Some(a) => {
5579 for e in dir.iter().filter(|e| e.min_a <= a && a <= e.max_a) {
5580 let bytes = reader.read_at(routed.section.offset + e.start, e.end - e.start)?;
5581 let tile = decompress(codec, &bytes)?;
5582 out.extend(GraphIndex::match_serialized_block(
5583 &tile,
5584 routed.permutation,
5585 routed.pattern,
5586 ));
5587 }
5588 }
5589 None => {
5592 if let (Some(first), Some(last)) = (dir.first(), dir.last()) {
5593 let base = first.start;
5594 let body = reader.read_at(routed.section.offset + base, last.end - base)?;
5595 for e in &dir {
5596 let tile = decompress(
5597 codec,
5598 &body[(e.start - base) as usize..(e.end - base) as usize],
5599 )?;
5600 out.extend(GraphIndex::match_serialized_block(
5601 &tile,
5602 routed.permutation,
5603 routed.pattern,
5604 ));
5605 }
5606 }
5607 }
5608 }
5609 out.sort_unstable();
5610 Ok(out)
5611}
5612
5613fn resolve_query_pattern(
5614 dict: &Dictionary,
5615 s: Option<&str>,
5616 p: Option<&str>,
5617 o: Option<&str>,
5618) -> Option<Pattern> {
5619 let sid = match s {
5620 Some(t) => Some(dict.subject_id(t)?),
5621 None => None,
5622 };
5623 let pid = match p {
5624 Some(t) => Some(dict.predicate_id(t)?),
5625 None => None,
5626 };
5627 let oid = match o {
5628 Some(t) => Some(dict.object_id(t)?),
5629 None => None,
5630 };
5631 Some((sid, pid, oid))
5632}
5633
5634fn ranged_chunked_dictionary<R: RangeReader + Send + Sync + 'static>(
5649 reader: &std::sync::Arc<R>,
5650 header: &Header,
5651 want: [bool; 4],
5652) -> Result<Dictionary, FileError> {
5653 let cache = crate::chunk_cache::ChunkCache::unlimited_arc();
5656 let mut dict_sections: Vec<crate::dict::ChunkedSection> = Vec::with_capacity(4);
5657 let mut section_ranges: [std::sync::Arc<Vec<ByteRange>>; 4] = Default::default();
5658 for si in 0..4 {
5659 if !want[si] {
5660 dict_sections.push(crate::dict::ChunkedSection::from_parts(
5661 crate::dict::SectionMeta {
5662 term_count: 0,
5663 restart_interval: 1,
5664 restart_offsets: Vec::new(),
5665 },
5666 Vec::new(),
5667 None,
5668 cache.clone(),
5669 si as u8,
5670 ));
5671 continue;
5672 }
5673 let section = locate_container_section_ranged(
5674 reader.as_ref(),
5675 header.dictionary_offset,
5676 header.dictionary_len,
5677 si,
5678 4,
5679 )?;
5680 let (meta, entries) = read_dict_dir_ranged(reader.as_ref(), section)?;
5681 let ranges: std::sync::Arc<Vec<ByteRange>> = std::sync::Arc::new(
5684 entries
5685 .iter()
5686 .map(|e| ByteRange {
5687 offset: section.offset + e.start,
5688 len: (e.end - e.start),
5689 })
5690 .collect(),
5691 );
5692 section_ranges[si] = ranges.clone();
5693 let chunks: Vec<crate::dict::SectionChunk> = entries
5694 .into_iter()
5695 .map(|e| crate::dict::SectionChunk::new(e.first_run, e.key, e.body_start))
5696 .collect();
5697 let chunk_reader = reader.clone();
5698 let codec = header.dict_codec;
5699 let loader_ranges = ranges.clone();
5700 let loader: crate::dict::ChunkLoader = Box::new(move |ci| {
5701 let range = loader_ranges.get(ci)?;
5702 let bytes = chunk_reader.read_at(range.offset, range.len).ok()?;
5703 decompress(codec, &bytes).ok()
5704 });
5705 let bulk_reader = reader.clone();
5708 let bulk: crate::dict::ChunkBulkLoader = Box::new(move |cis| {
5709 let want: Option<Vec<ByteRange>> =
5710 cis.iter().map(|&ci| ranges.get(ci).copied()).collect();
5711 let blobs = read_coalesced(bulk_reader.as_ref(), &want?, DICT_COALESCE_GAP)?;
5712 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
5713 });
5714 dict_sections.push(
5715 crate::dict::ChunkedSection::from_parts(
5716 meta,
5717 chunks,
5718 Some(loader),
5719 cache.clone(),
5720 si as u8,
5721 )
5722 .with_bulk_loader(bulk),
5723 );
5724 }
5725 let dict_arr: [crate::dict::ChunkedSection; 4] = dict_sections
5726 .try_into()
5727 .map_err(|_| FileError::Container("expected 4 dictionary sections"))?;
5728 let multi_reader = reader.clone();
5732 let codec = header.dict_codec;
5733 let multi: crate::dict::MultiChunkBulkLoader = Box::new(move |pairs| {
5734 let want: Option<Vec<ByteRange>> = pairs
5735 .iter()
5736 .map(|&(si, ci)| section_ranges.get(si as usize)?.get(ci).copied())
5737 .collect();
5738 let blobs = read_coalesced(multi_reader.as_ref(), &want?, DICT_COALESCE_GAP)?;
5739 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
5740 });
5741 Ok(Dictionary::from_chunked_sections(dict_arr).with_multi_bulk_loader(multi))
5742}
5743
5744#[cfg(not(target_arch = "wasm32"))]
5749fn open_side_by_side<A, B: Send>(
5750 concurrency: usize,
5751 a: impl FnOnce() -> A,
5752 b: impl FnOnce() -> B + Send,
5753) -> (A, B) {
5754 if concurrency <= 1 {
5755 return (a(), b());
5756 }
5757 std::thread::scope(|scope| {
5758 let hb = scope.spawn(b);
5759 let ra = a();
5760 let rb = hb
5761 .join()
5762 .unwrap_or_else(|panic| std::panic::resume_unwind(panic));
5763 (ra, rb)
5764 })
5765}
5766
5767#[cfg(target_arch = "wasm32")]
5769fn open_side_by_side<A, B>(
5770 _concurrency: usize,
5771 a: impl FnOnce() -> A,
5772 b: impl FnOnce() -> B,
5773) -> (A, B) {
5774 (a(), b())
5775}
5776
5777fn ranged_pyramid_slot<R: RangeReader + Send + Sync + 'static>(
5781 reader: &std::sync::Arc<R>,
5782 header: &Header,
5783) -> PyramidSlot {
5784 if header.pyramid_meta_len == 0 {
5785 return PyramidSlot::Resident(None);
5786 }
5787 let pyr_reader = reader.clone();
5788 let pyr_off = header.pyramid_meta_offset;
5789 let pyr_len = header.pyramid_meta_len;
5790 PyramidSlot::Lazy {
5791 loader: Box::new(move || {
5792 let mb = pyr_reader.read_at(pyr_off, pyr_len).ok()?;
5793 PyramidMeta::decode(&mb).ok()
5794 }),
5795 cell: std::sync::OnceLock::new(),
5796 }
5797}
5798
5799fn ranged_text_index_slot<R: RangeReader + Send + Sync + 'static>(
5804 reader: &std::sync::Arc<R>,
5805 header: &Header,
5806) -> TextIndexSlot {
5807 let (ti_off, ti_len) = header.text_index_payload();
5808 if ti_len == 0 {
5809 return TextIndexSlot::Resident {
5810 index: None,
5811 token_table_len: None,
5812 };
5813 }
5814 let ti_reader = reader.clone();
5815 let probe_reader = reader.clone();
5816 let codec = header.block_codec;
5817 TextIndexSlot::Lazy {
5818 loader: Box::new(move || {
5819 let prefix_len = read_token_table_len(&*ti_reader, ti_off, ti_len)?;
5823 let prefix = ti_reader.read_at(ti_off, prefix_len).ok()?;
5824 let postings_base = crate::text_index::TextIndex::postings_base(&prefix)? as u64;
5825 let postings_abs = ti_off + postings_base;
5826 let pr = ti_reader.clone();
5827 let posting_loader =
5828 Box::new(move |off: u64, len: u64| pr.read_at(postings_abs + off, len).ok());
5829 crate::text_index::TextIndex::from_token_table(&prefix, codec, posting_loader).ok()
5830 }),
5831 cell: std::sync::OnceLock::new(),
5832 token_table: Box::new(move || read_token_table_len(&*probe_reader, ti_off, ti_len)),
5835 token_table_cell: std::sync::OnceLock::new(),
5836 }
5837}
5838
5839fn resolve_label_hits(
5846 dict: &Dictionary,
5847 hits: Vec<&crate::meta::LabelEntry>,
5848) -> Vec<(String, String)> {
5849 let ids: Vec<u32> = hits.iter().map(|e| e.subject).collect();
5850 dict.prefetch_subject_terms(&ids);
5851 hits.iter()
5852 .zip(dict.subject_terms(&ids))
5853 .filter_map(|(e, iri)| Some((e.label.clone(), iri?)))
5854 .collect()
5855}
5856
5857fn text_search_in(
5860 ti: Option<&crate::text_index::TextIndex>,
5861 dict: &Dictionary,
5862 words: &[&str],
5863 prefix: Option<&str>,
5864 limit: usize,
5865) -> Vec<String> {
5866 let Some(ti) = ti else {
5867 return Vec::new();
5868 };
5869 let mut acc: Option<Vec<u32>> = None;
5873 if let Some(p) = prefix {
5874 acc = Some(ti.prefix(&p.to_lowercase()));
5875 }
5876 for w in words {
5877 for tok in crate::text_index::tokenize(w) {
5878 let posting = ti.lookup(&tok);
5879 acc = Some(match acc {
5880 Some(a) => intersect_sorted(&a, &posting),
5881 None => posting,
5882 });
5883 if acc.as_ref().is_some_and(|a| a.is_empty()) {
5884 return Vec::new();
5885 }
5886 }
5887 }
5888 let ids = acc.unwrap_or_default();
5889 let mut out = Vec::with_capacity(if limit > 0 {
5890 limit.min(ids.len())
5891 } else {
5892 ids.len()
5893 });
5894 let page = if limit > 0 {
5898 limit.min(ids.len())
5899 } else {
5900 ids.len()
5901 };
5902 dict.prefetch_subject_terms(&ids[..page]);
5903 for id in ids {
5904 if let Some(iri) = dict.subject_term(id) {
5905 out.push(iri);
5906 if limit > 0 && out.len() >= limit {
5907 break;
5908 }
5909 }
5910 }
5911 out
5912}
5913
5914pub struct SearchView {
5937 header: Header,
5938 dict: SectionSlot<Dictionary>,
5939 pyramid: PyramidSlot,
5940 text_index: TextIndexSlot,
5941 labels: SectionSlot<crate::label_index::LabelIndex>,
5942}
5943
5944impl SearchView {
5945 pub fn open_ranged<R: RangeReader + Send + Sync + 'static>(
5950 reader: R,
5951 ) -> Result<Self, FileError> {
5952 let head = reader.read_at(0, HEADER_LEN as u64)?;
5953 let header = Header::from_bytes(&head)?;
5954 let reader = std::sync::Arc::new(reader);
5955 let dict = if header.label_index_section().is_some() {
5959 let (r, h) = (reader.clone(), header.clone());
5960 SectionSlot::Lazy {
5961 loader: Box::new(move || {
5962 ranged_chunked_dictionary(&r, &h, [true, true, false, false]).ok()
5963 }),
5964 cell: std::sync::OnceLock::new(),
5965 }
5966 } else {
5967 SectionSlot::Resident(Some(ranged_chunked_dictionary(
5968 &reader,
5969 &header,
5970 [true, true, false, false],
5971 )?))
5972 };
5973 let pyramid = ranged_pyramid_slot(&reader, &header);
5974 let text_index = ranged_text_index_slot(&reader, &header);
5975 let labels = ranged_label_slot(&reader, &header);
5976 Ok(Self {
5977 header,
5978 dict,
5979 pyramid,
5980 text_index,
5981 labels,
5982 })
5983 }
5984
5985 pub fn has_labels(&self) -> bool {
5989 self.header.label_index_section().is_some() || self.header.pyramid_meta_len > 0
5990 }
5991
5992 pub fn has_label_index(&self) -> bool {
5994 self.header.label_index_section().is_some()
5995 }
5996
5997 pub fn header(&self) -> &Header {
5999 &self.header
6000 }
6001
6002 pub fn has_text_index(&self) -> bool {
6004 self.header.text_index_len > 0
6005 }
6006
6007 pub fn has_pyramid(&self) -> bool {
6010 self.header.pyramid_meta_len > 0
6011 }
6012
6013 pub fn text_search(&self, words: &[&str], prefix: Option<&str>, limit: usize) -> Vec<String> {
6016 let Some(dict) = self.dict.get() else {
6017 return Vec::new();
6018 };
6019 text_search_in(self.text_index.index(), dict, words, prefix, limit)
6020 }
6021
6022 pub fn text_index_token_table_len(&self) -> Option<u64> {
6026 self.text_index.token_table_len()
6027 }
6028
6029 pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
6033 if let Some(li) = self.labels.get() {
6034 return li.prefix_search(prefix, limit);
6035 }
6036 let pyr = match &self.pyramid {
6037 PyramidSlot::Resident(p) => p.as_ref(),
6038 PyramidSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
6039 };
6040 let (Some(pyr), Some(dict)) = (pyr, self.dict.get()) else {
6041 return Vec::new();
6042 };
6043 resolve_label_hits(dict, pyr.prefix_search(prefix, limit))
6044 }
6045}
6046
6047#[must_use]
6052pub struct SummaryView {
6053 pub round: u32,
6054 pub summary: Vec<SuperEdge>,
6055 pub class_hierarchy: Vec<ClassNode>,
6057 pub level_rollups: Vec<LevelRollup>,
6059 pub level_links: Vec<LevelLinks>,
6061 pub descriptors: Vec<CommunityDescriptor>,
6063 pub subclass_cycles: Vec<Vec<String>>,
6065 pub disjoint_pairs: Vec<(String, String)>,
6067 pub equivalent_pairs: Vec<(String, String)>,
6069 dict: Dictionary,
6070}
6071
6072impl SummaryView {
6073 pub fn open_ranged<R: RangeReader>(reader: &R) -> Result<Option<Self>, FileError> {
6075 let head = reader.read_at(0, HEADER_LEN as u64)?;
6076 let header = Header::from_bytes(&head)?;
6077 if header.pyramid_meta_len == 0 {
6078 return Ok(None);
6079 }
6080
6081 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
6082 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
6083
6084 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
6085 let mut meta =
6086 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
6087 if let Some(s) = read_schema_with_header(reader, &header) {
6090 meta.class_hierarchy = s.class_hierarchy;
6091 meta.level_rollups = s.level_rollups;
6092 meta.level_links = s.level_links;
6093 meta.subclass_cycles = s.subclass_cycles;
6094 meta.disjoint_pairs = s.disjoint_pairs;
6095 meta.equivalent_pairs = s.equivalent_pairs;
6096 }
6097
6098 Ok(Some(SummaryView {
6099 round: meta.round,
6100 summary: meta.summary,
6101 class_hierarchy: meta.class_hierarchy,
6102 level_rollups: meta.level_rollups,
6103 level_links: meta.level_links,
6104 descriptors: meta.descriptors,
6105 subclass_cycles: meta.subclass_cycles,
6106 disjoint_pairs: meta.disjoint_pairs,
6107 equivalent_pairs: meta.equivalent_pairs,
6108 dict,
6109 }))
6110 }
6111
6112 pub fn open_schema_ranged<R: RangeReader>(reader: &R) -> Result<Option<Self>, FileError> {
6120 let head = reader.read_at(0, HEADER_LEN as u64)?;
6121 let header = Header::from_bytes(&head)?;
6122 let Some(s) = read_schema_with_header(reader, &header) else {
6123 return Ok(None);
6124 };
6125 let dict = crate::DictionaryBuilder::new().build();
6126 Ok(Some(SummaryView {
6127 round: 0,
6128 summary: Vec::new(),
6129 class_hierarchy: s.class_hierarchy,
6130 level_rollups: s.level_rollups,
6131 level_links: s.level_links,
6132 descriptors: Vec::new(),
6133 subclass_cycles: s.subclass_cycles,
6134 disjoint_pairs: s.disjoint_pairs,
6135 equivalent_pairs: s.equivalent_pairs,
6136 dict,
6137 }))
6138 }
6139
6140 pub fn level_count(&self) -> usize {
6142 self.level_rollups.len()
6143 }
6144
6145 pub fn level_rollup(&self, k: usize) -> Option<&LevelRollup> {
6148 self.level_rollups.get(k)
6149 }
6150
6151 pub fn predicate_term(&self, id: u32) -> Option<String> {
6153 self.dict.predicate_term(id)
6154 }
6155
6156 pub fn predicate_total(&self, predicate: &str) -> u32 {
6159 match self.dict.predicate_id(predicate) {
6160 Some(pid) => self
6161 .summary
6162 .iter()
6163 .filter(|e| e.predicate == pid)
6164 .map(|e| e.count)
6165 .sum(),
6166 None => 0,
6167 }
6168 }
6169
6170 pub fn predicate_totals(&self) -> Vec<(String, u32)> {
6172 let mut by_pred: std::collections::BTreeMap<u32, u32> = std::collections::BTreeMap::new();
6173 for e in &self.summary {
6174 *by_pred.entry(e.predicate).or_default() += e.count;
6175 }
6176 let mut out: Vec<(String, u32)> = by_pred
6177 .into_iter()
6178 .filter_map(|(pid, c)| self.dict.predicate_term(pid).map(|t| (t, c)))
6179 .collect();
6180 out.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
6181 out
6182 }
6183
6184 pub fn community_count(&self) -> usize {
6186 let mut comms = std::collections::BTreeSet::new();
6187 for e in &self.summary {
6188 comms.insert(e.s_comm);
6189 comms.insert(e.o_comm);
6190 }
6191 comms.len()
6192 }
6193
6194 pub fn tbox_coherence(&self) -> Vec<crate::reason::Inconsistency> {
6209 schema_coherence(
6210 &self.class_hierarchy,
6211 &self.subclass_cycles,
6212 &self.disjoint_pairs,
6213 &self.equivalent_pairs,
6214 )
6215 }
6216
6217 pub fn tbox_is_coherent(&self) -> bool {
6220 self.tbox_coherence().is_empty()
6221 }
6222}
6223
6224pub fn schema_coherence(
6230 class_hierarchy: &[ClassNode],
6231 subclass_cycles: &[Vec<String>],
6232 disjoint_pairs: &[(String, String)],
6233 equivalent_pairs: &[(String, String)],
6234) -> Vec<crate::reason::Inconsistency> {
6235 use crate::reason::Inconsistency;
6236 use std::collections::{BTreeMap, BTreeSet, VecDeque};
6237 const MAX_REACH: usize = 100_000;
6238
6239 let mut out: Vec<Inconsistency> = Vec::new();
6240
6241 for cyc in subclass_cycles {
6242 let detail = if cyc.len() == 1 {
6243 format!("{} is rdfs:subClassOf itself (a cycle)", cyc[0])
6244 } else {
6245 format!(
6246 "classes {{{}}} are mutually rdfs:subClassOf (a cycle)",
6247 cyc.join(", ")
6248 )
6249 };
6250 out.push(Inconsistency {
6251 kind: "subclass-cycle",
6252 detail,
6253 });
6254 }
6255
6256 if !disjoint_pairs.is_empty() {
6257 let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
6259 for n in class_hierarchy {
6260 let e = adj.entry(n.class.as_str()).or_default();
6261 for p in &n.parents {
6262 e.push(p.as_str());
6263 }
6264 }
6265 for (a, b) in equivalent_pairs {
6266 adj.entry(a.as_str()).or_default().push(b.as_str());
6267 adj.entry(b.as_str()).or_default().push(a.as_str());
6268 }
6269
6270 let mut focuses: BTreeSet<&str> =
6272 class_hierarchy.iter().map(|n| n.class.as_str()).collect();
6273 for (a, b) in disjoint_pairs.iter().chain(equivalent_pairs) {
6274 focuses.insert(a.as_str());
6275 focuses.insert(b.as_str());
6276 }
6277
6278 let mut seen: BTreeSet<&str> = BTreeSet::new();
6279 for &c in &focuses {
6280 let mut reach: BTreeSet<&str> = BTreeSet::new();
6282 let mut q: VecDeque<&str> = VecDeque::new();
6283 reach.insert(c);
6284 q.push_back(c);
6285 while let Some(x) = q.pop_front() {
6286 if reach.len() > MAX_REACH {
6287 break;
6288 }
6289 if let Some(ns) = adj.get(x) {
6290 for &p in ns {
6291 if reach.insert(p) {
6292 q.push_back(p);
6293 }
6294 }
6295 }
6296 }
6297 for (x, y) in disjoint_pairs {
6298 if reach.contains(x.as_str()) && reach.contains(y.as_str()) && seen.insert(c) {
6299 out.push(Inconsistency {
6300 kind: "unsatisfiable-class",
6301 detail: format!(
6302 "{c} is a subclass of both {x} and {y}, which are \
6303 owl:disjointWith — no individual can be a {c}"
6304 ),
6305 });
6306 break;
6307 }
6308 }
6309 }
6310 }
6311
6312 out.sort_by(|a, b| (a.kind, &a.detail).cmp(&(b.kind, &b.detail)));
6313 out
6314}
6315
6316pub fn read_schema_coherence_ranged<R: RangeReader>(
6324 reader: &R,
6325) -> Result<Option<Vec<crate::reason::Inconsistency>>, FileError> {
6326 let head = reader.read_at(0, HEADER_LEN as u64)?;
6327 let header = Header::from_bytes(&head)?;
6328 if let Some(s) = read_schema_with_header(reader, &header) {
6330 return Ok(Some(schema_coherence(
6331 &s.class_hierarchy,
6332 &s.subclass_cycles,
6333 &s.disjoint_pairs,
6334 &s.equivalent_pairs,
6335 )));
6336 }
6337 if header.pyramid_meta_len == 0 {
6338 return Ok(None);
6339 }
6340 if header.schema_meta_len > 0 && (header.schema_meta_len as u64) <= header.pyramid_meta_len {
6344 let off =
6345 header.pyramid_meta_offset + header.pyramid_meta_len - header.schema_meta_len as u64;
6346 let block = reader.read_at(off, header.schema_meta_len as u64)?;
6347 let (hierarchy, cycles, disjoint, equivalent) = crate::meta::decode_schema_block(&block)
6348 .map_err(|_| FileError::Container("malformed schema block"))?;
6349 return Ok(Some(schema_coherence(
6350 &hierarchy,
6351 &cycles,
6352 &disjoint,
6353 &equivalent,
6354 )));
6355 }
6356 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
6358 let meta =
6359 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
6360 Ok(Some(schema_coherence(
6361 &meta.class_hierarchy,
6362 &meta.subclass_cycles,
6363 &meta.disjoint_pairs,
6364 &meta.equivalent_pairs,
6365 )))
6366}
6367
6368#[allow(clippy::type_complexity)]
6376pub fn read_schema_summary_ranged<R: RangeReader>(
6377 reader: &R,
6378) -> Result<Option<(Vec<(String, u64)>, Vec<(String, String, String, u64)>)>, FileError> {
6379 let head = reader.read_at(0, HEADER_LEN as u64)?;
6380 let header = Header::from_bytes(&head)?;
6381 let meta = if let Some(s) = read_schema_with_header(reader, &header) {
6383 PyramidMeta::new(0, Vec::new(), &[]).with_schema(
6384 Vec::new(),
6385 s.level_rollups,
6386 s.level_links,
6387 Vec::new(),
6388 Vec::new(),
6389 Vec::new(),
6390 Vec::new(),
6391 )
6392 } else {
6393 if header.pyramid_meta_len == 0 {
6394 return Ok(None);
6395 }
6396 if header.schema_meta_len > 0 && (header.schema_meta_len as u64) <= header.pyramid_meta_len
6397 {
6398 let off = header.pyramid_meta_offset + header.pyramid_meta_len
6399 - header.schema_meta_len as u64;
6400 let block = reader.read_at(off, header.schema_meta_len as u64)?;
6401 let summary = crate::meta::decode_schema_block_summary(&block)
6402 .map_err(|_| FileError::Container("malformed schema block"))?;
6403 return Ok(Some(summary));
6404 }
6405 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
6407 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?
6408 };
6409 if meta.level_rollups.is_empty() && meta.level_links.is_empty() {
6410 return Ok(None);
6411 }
6412 let classes = meta
6413 .level_rollups
6414 .iter()
6415 .max_by_key(|r| r.depth)
6416 .map(|r| r.classes.clone())
6417 .unwrap_or_default();
6418 let relations = meta
6419 .level_links
6420 .iter()
6421 .max_by_key(|l| l.depth)
6422 .map(|l| {
6423 l.links
6424 .iter()
6425 .map(|c| {
6426 (
6427 c.s_class.clone(),
6428 c.predicate.clone(),
6429 c.o_class.clone(),
6430 c.count,
6431 )
6432 })
6433 .collect()
6434 })
6435 .unwrap_or_default();
6436 Ok(Some((classes, relations)))
6437}
6438
6439#[cfg(test)]
6444pub(crate) fn dict_chunk_keys_for_test(image: &[u8]) -> Vec<Vec<Vec<u8>>> {
6445 let header = Header::from_bytes(&image[..HEADER_LEN]).unwrap();
6446 let s = header.dictionary_offset as usize;
6447 let e = s + header.dictionary_len as usize;
6448 decode_container(&image[s..e], CODEC_NONE)
6449 .unwrap()
6450 .iter()
6451 .map(|payload| {
6452 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
6453 entries.into_iter().map(|c| c.key).collect()
6454 })
6455 .collect()
6456}
6457
6458#[cfg(test)]
6459mod tests {
6460 use super::*;
6461 use crate::dictionary::DictionaryBuilder;
6462 use crate::index::GraphIndexBuilder;
6463
6464 #[cfg(not(feature = "compression"))]
6467 #[test]
6468 fn ruzstd_decodes_a_zstd_frame() {
6469 let data: Vec<u8> = (0..50_000u32)
6470 .flat_map(|i| (i % 251).to_le_bytes())
6471 .collect();
6472 let frame = ruzstd::encoding::compress_to_vec(
6473 &data[..],
6474 ruzstd::encoding::CompressionLevel::Fastest,
6475 );
6476 assert!(frame.len() < data.len());
6477 assert_eq!(decompress(CODEC_ZSTD, &frame).unwrap(), data);
6478 }
6479
6480 #[test]
6486 fn three_permutation_file_round_trips_every_open_path() {
6487 use crate::index::PermSet;
6488 use crate::reader::SliceReader;
6489
6490 let build = |perms: PermSet| {
6491 let mut db = crate::DictionaryBuilder::new();
6492 let mut triples = Vec::new();
6493 for i in 0..60u32 {
6494 let (s, p, o) = (
6495 format!("<http://ex/s{}>", i % 11),
6496 format!("<http://ex/p{}>", i % 3),
6497 format!("<http://ex/o{}>", i % 7),
6498 );
6499 db.observe(&s, &p, &o);
6500 triples.push((s, p, o));
6501 }
6502 let dict = db.build();
6503 let ids: Vec<(u32, u32, u32)> = triples
6504 .iter()
6505 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
6506 .collect();
6507 let def = GraphIndexBuilder::from_triples(ids.clone())
6508 .with_tile_budget(96)
6509 .with_perms(perms)
6510 .build();
6511 let named = GraphIndexBuilder::from_triples(ids)
6512 .with_tile_budget(96)
6513 .with_perms(perms)
6514 .build();
6515 write_dataset(
6516 &dict,
6517 &def,
6518 &[("<http://ex/g>".to_string(), named)],
6519 true,
6520 &[],
6521 0,
6522 )
6523 };
6524
6525 let six = build(PermSet::ALL);
6526 let three = build(PermSet::CORE);
6527 assert!(three.len() < six.len());
6528 assert_eq!(Header::from_bytes(&three).unwrap().perms, PermSet::CORE);
6529 assert_eq!(Header::from_bytes(&six).unwrap().perms, PermSet::ALL);
6530
6531 let shapes: Vec<(Option<&str>, Option<&str>, Option<&str>)> = vec![
6532 (None, None, None),
6533 (Some("<http://ex/s3>"), None, None),
6534 (None, Some("<http://ex/p1>"), None),
6535 (None, None, Some("<http://ex/o5>")),
6536 (Some("<http://ex/s3>"), Some("<http://ex/p1>"), None),
6537 (Some("<http://ex/s3>"), None, Some("<http://ex/o5>")),
6538 (None, Some("<http://ex/p1>"), Some("<http://ex/o5>")),
6539 (
6540 Some("<http://ex/s3>"),
6541 Some("<http://ex/p1>"),
6542 Some("<http://ex/o5>"),
6543 ),
6544 ];
6545
6546 for (image, tag) in [(&six, "six"), (&three, "three")] {
6547 let resident = Rete::open(image).unwrap();
6548 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
6549 let ranged = Rete::open_ranged(&SliceReader::new(leaked)).unwrap();
6550 let lazy = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
6551 for (s, p, o) in &shapes {
6552 let want: Vec<_> = {
6553 let mut v = Rete::open(&six).unwrap().query(*s, *p, *o);
6554 v.sort();
6555 v
6556 };
6557 for (got, path) in [
6558 (resident.query(*s, *p, *o), "resident"),
6559 (ranged.query(*s, *p, *o), "ranged"),
6560 (lazy.query(*s, *p, *o), "lazy"),
6561 ] {
6562 let mut got = got;
6563 got.sort();
6564 assert_eq!(got, want, "{tag}/{path} disagreed on {:?}", (s, p, o));
6565 }
6566 let mut g = resident.query_in_graph(Some("<http://ex/g>"), *s, *p, *o);
6568 g.sort();
6569 assert_eq!(g, want, "{tag}/named disagreed on {:?}", (s, p, o));
6570 }
6571 }
6572
6573 let leaked: &'static [u8] = Box::leak(three.clone().into_boxed_slice());
6577 let reader = SliceReader::new(leaked);
6578 let routed = Rete::query_ranged(&reader, None, None, Some("<http://ex/o5>")).unwrap();
6579 let mut want = Rete::open(&six)
6580 .unwrap()
6581 .query(None, None, Some("<http://ex/o5>"));
6582 want.sort();
6583 let mut got = routed;
6584 got.sort();
6585 assert_eq!(got, want, "routed single-pattern read on a lean file");
6586 }
6587
6588 #[test]
6589 fn read_coalesced_merges_within_gap_and_splits_beyond() {
6590 use crate::reader::{CountingReader, SliceReader};
6591 let bytes = vec![0u8; 4096];
6592 let ranges = [
6594 ByteRange { offset: 0, len: 16 },
6595 ByteRange {
6596 offset: 48,
6597 len: 16,
6598 },
6599 ByteRange {
6600 offset: 1088,
6601 len: 16,
6602 },
6603 ];
6604 let r = CountingReader::new(SliceReader::new(&bytes));
6606 let out = read_coalesced(&r, &ranges, 16).unwrap();
6607 assert_eq!(out.len(), 3);
6608 assert_eq!(r.requests(), 3);
6609 let r = CountingReader::new(SliceReader::new(&bytes));
6611 read_coalesced(&r, &ranges, 64).unwrap();
6612 assert_eq!(r.requests(), 2);
6613 let r = CountingReader::new(SliceReader::new(&bytes));
6615 read_coalesced(&r, &ranges, 4096).unwrap();
6616 assert_eq!(r.requests(), 1);
6617 }
6618
6619 fn build_image() -> Vec<u8> {
6620 let triples = [
6621 ("Alice", "knows", "Bob"),
6622 ("Bob", "knows", "Carol"),
6623 ("Alice", "age", "30"),
6624 ];
6625 let mut db = DictionaryBuilder::new();
6626 for (s, p, o) in triples {
6627 db.observe(s, p, o);
6628 }
6629 let dict = db.build();
6630
6631 let mut ib = GraphIndexBuilder::new();
6632 for (s, p, o) in triples {
6633 ib.push(dict.encode(s, p, o).unwrap());
6634 }
6635 let index = ib.build();
6636
6637 let (meta, levels) = build_pyramid_meta(&dict, &triples_ids(&dict), DEFAULT_TILE_BUDGET);
6638 write_file(&dict, &index, false, &meta, levels)
6639 }
6640
6641 fn triples_ids(dict: &Dictionary) -> Vec<(u32, u32, u32)> {
6642 [
6643 ("Alice", "knows", "Bob"),
6644 ("Bob", "knows", "Carol"),
6645 ("Alice", "age", "30"),
6646 ]
6647 .iter()
6648 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
6649 .collect()
6650 }
6651
6652 #[test]
6653 fn file_round_trips_header_and_counts() {
6654 let bytes = build_image();
6655 let rete = Rete::open(&bytes).unwrap();
6656 assert_eq!(rete.header().quad_count, 3);
6657 assert!(rete.header().term_count >= 5);
6658 let expected_codec = writer_codec();
6659 assert_eq!(rete.header().dict_codec, expected_codec);
6660 assert_eq!(rete.header().block_codec, expected_codec);
6661 assert_eq!(&bytes[bytes.len() - 4..], &MAGIC); }
6663
6664 #[test]
6669 fn multi_tile_file_round_trips_and_routes() {
6670 let triples: Vec<(String, String, String)> = (0..200)
6671 .map(|i| {
6672 (
6673 format!("<http://ex/s/{i}>"),
6674 format!("<http://ex/p/{}>", i % 5),
6675 format!("<http://ex/o/{}>", i % 23),
6676 )
6677 })
6678 .collect();
6679 let mut db = DictionaryBuilder::new();
6680 for (s, p, o) in &triples {
6681 db.observe(s, p, o);
6682 }
6683 let dict = db.build();
6684 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6685 for (s, p, o) in &triples {
6686 ib.push(dict.encode(s, p, o).unwrap());
6687 }
6688 let index = ib.build();
6689 assert!(
6690 index.tile_sections()[0].len() > 3,
6691 "tiny budget must force many tiles"
6692 );
6693 let bytes = write_file(&dict, &index, false, &[], 0);
6694
6695 let rete = Rete::open(&bytes).unwrap();
6696 assert_eq!(rete.header().version, crate::header::CURRENT_FORMAT_VERSION);
6697 assert_eq!(rete.query(None, None, None).len(), 200);
6698 assert_eq!(rete.query(Some("<http://ex/s/7>"), None, None).len(), 1);
6699 assert_eq!(
6700 rete.query(None, Some("<http://ex/p/3>"), None).len(),
6701 40,
6702 "predicate extent spans tiles"
6703 );
6704 assert_eq!(
6705 rete.query(None, None, Some("<http://ex/o/22>")).len(),
6706 8 );
6708
6709 use crate::reader::SliceReader;
6711 let reader = SliceReader::new(&bytes);
6712 let routed = Rete::query_ranged(&reader, Some("<http://ex/s/7>"), None, None).unwrap();
6713 assert_eq!(routed.len(), 1);
6714 let routed = Rete::query_ranged(&reader, None, Some("<http://ex/p/3>"), None).unwrap();
6715 assert_eq!(routed.len(), 40);
6716 let routed = Rete::query_ranged(&reader, None, None, Some("<http://ex/o/22>")).unwrap();
6717 assert_eq!(routed.len(), 8);
6718 }
6719
6720 #[test]
6729 fn tile_directory_offsets_survive_past_4gib() {
6730 let mut dir = Vec::new();
6731 write_uvarint(&mut dir, 2); write_uvarint(&mut dir, 5); write_uvarint(&mut dir, 0); write_uvarint(&mut dir, 3 << 30); write_uvarint(&mut dir, 1); write_uvarint(&mut dir, 0);
6737 write_uvarint(&mut dir, 2 << 30); let total = dir.len() as u64 + (3u64 << 30) + (2u64 << 30) + 64;
6739 let entries = parse_tile_directory(&dir, total).unwrap();
6740 assert_eq!(entries.len(), 2);
6741 assert_eq!(entries[1].start, dir.len() as u64 + (3u64 << 30));
6742 assert!(
6743 entries[1].end > u32::MAX as u64,
6744 "tail tile sits past 4 GiB"
6745 );
6746 assert!(parse_tile_directory(&dir, 1 << 20).is_err());
6748 }
6749
6750 #[test]
6751 fn tile_synopsis_trailer_round_trips() {
6752 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6753 for i in 0..200u32 {
6754 ib.push((i, i % 7, i % 13));
6755 }
6756 let index = ib.build();
6757 let tile_count = index.tile_sections()[0].len();
6758 assert!(tile_count > 3, "tiny budget forces many tiles");
6759
6760 let payload = encode_tiled_section(&index, 0, CODEC_NONE);
6761 let dir = parse_tile_directory(&payload, payload.len() as u64).unwrap();
6762 assert_eq!(dir.len(), tile_count);
6763 let trailer_start = dir.iter().map(|e| e.end).max().unwrap();
6765 assert!(
6766 trailer_start < payload.len() as u64,
6767 "a trailer follows the tiles"
6768 );
6769 for e in &dir {
6770 assert!(
6771 e.end <= payload.len() as u64,
6772 "tiles still located within the payload"
6773 );
6774 }
6775 let syn = parse_tile_synopsis(&payload, trailer_start as usize, dir.len()).unwrap();
6776 for (e, (min_b, max_b, min_c, max_c)) in dir.iter().zip(syn) {
6777 let block = decompress(CODEC_NONE, &payload[e.start as usize..e.end as usize]).unwrap();
6778 let z = *crate::triples::TripleBlock::parse(&block).unwrap().zone();
6779 assert_eq!(
6780 (min_b, max_b, min_c, max_c),
6781 (z.min_b, z.max_b, z.min_c, z.max_c),
6782 "synopsis equals the tile's own zone"
6783 );
6784 }
6785 }
6786
6787 #[test]
6791 fn tile_synopsis_lazy_matches_reference_every_shape() {
6792 use crate::reader::{CountingReader, SliceReader};
6793 let triples: Vec<(String, String, String)> = (0..200u32)
6794 .map(|i| {
6795 (
6796 format!("<http://ex/s/{i:04}>"),
6797 format!("<http://ex/p/{}>", i % 7),
6798 format!("<http://ex/o/{:04}>", i % 13),
6799 )
6800 })
6801 .collect();
6802 let mut db = DictionaryBuilder::new();
6803 for (s, p, o) in &triples {
6804 db.observe(s, p, o);
6805 }
6806 let dict = db.build();
6807 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6808 for (s, p, o) in &triples {
6809 ib.push(dict.encode(s, p, o).unwrap());
6810 }
6811 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
6812
6813 let eager = Rete::open(&bytes).unwrap();
6814 assert!(
6815 eager.header().has_tile_synopsis(),
6816 "new files set the synopsis flag"
6817 );
6818
6819 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
6822 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
6823 let lazy = Rete::open_ranged_lazy(reader).unwrap();
6824
6825 let brute = |s: Option<&str>, p: Option<&str>, o: Option<&str>| {
6826 let mut v: Vec<(String, String, String)> = triples
6827 .iter()
6828 .filter(|(a, b, c)| {
6829 s.is_none_or(|x| x == a) && p.is_none_or(|x| x == b) && o.is_none_or(|x| x == c)
6830 })
6831 .cloned()
6832 .collect();
6833 v.sort();
6834 v
6835 };
6836 let sv = [
6838 None,
6839 Some("<http://ex/s/0007>"),
6840 Some("<http://ex/s/0130>"),
6841 Some("<http://ex/s/9999>"),
6842 ];
6843 let pv = [
6844 None,
6845 Some("<http://ex/p/3>"),
6846 Some("<http://ex/p/6>"),
6847 Some("<http://ex/p/999>"),
6848 ];
6849 let ov = [
6850 None,
6851 Some("<http://ex/o/0000>"),
6852 Some("<http://ex/o/0012>"),
6853 Some("<http://ex/o/9999>"),
6854 ];
6855 for &s in &sv {
6856 for &p in &pv {
6857 for &o in &ov {
6858 let mut e = eager.query(s, p, o);
6859 e.sort();
6860 let mut l = lazy.query(s, p, o);
6861 l.sort();
6862 let r = brute(s, p, o);
6863 assert_eq!(e, r, "eager {s:?} {p:?} {o:?}");
6864 assert_eq!(l, r, "lazy {s:?} {p:?} {o:?} — synopsis over-pruned");
6865 }
6866 }
6867 }
6868 assert!(!lazy.index_incomplete(), "no lazy fetch failed");
6869 }
6870
6871 #[test]
6875 fn polyglot_offset_reads_lazily() {
6876 use crate::reader::{
6877 detect_polyglot_base, CountingReader, OffsetReader, SliceReader, POLYGLOT_DIGITS,
6878 POLYGLOT_MARKER,
6879 };
6880
6881 let image = build_image();
6885 let mut shell = Vec::new();
6886 shell.extend_from_slice(b"<!DOCTYPE html><html><head><!--");
6887 shell.extend_from_slice(POLYGLOT_MARKER);
6888 let digits_at = shell.len();
6889 shell.extend_from_slice(&[b'0'; POLYGLOT_DIGITS]); shell.extend_from_slice(b"--></head><body>a web page</body></html>\n");
6891 shell.resize(50_000, b' '); let base = shell.len() as u64;
6893 let digits = format!("{base:0width$}", width = POLYGLOT_DIGITS);
6894 shell[digits_at..digits_at + POLYGLOT_DIGITS].copy_from_slice(digits.as_bytes());
6895
6896 let mut poly = shell;
6897 poly.extend_from_slice(&image);
6898
6899 assert_ne!(
6901 &poly[0..4],
6902 b"RETE",
6903 "polyglot must not start with the magic"
6904 );
6905 let detected = detect_polyglot_base(&poly[..crate::header::HEADER_LEN]).unwrap();
6907 assert_eq!(detected, base);
6908
6909 let leaked: &'static [u8] = Box::leak(poly.into_boxed_slice());
6911 let counting = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
6912 let lazy = Rete::open_ranged_lazy(OffsetReader::new(counting.clone(), detected)).unwrap();
6913
6914 let plain = Rete::open(&image).unwrap();
6916 let mut want = plain.query(None, None, None);
6917 want.sort();
6918 let mut got = lazy.query(None, None, None);
6919 got.sort();
6920 assert_eq!(
6921 got, want,
6922 "polyglot lazy query differs from the plain .rete"
6923 );
6924 assert!(!got.is_empty(), "expected some triples");
6925 assert!(!lazy.index_incomplete(), "a lazy fetch failed");
6926
6927 let read = counting.bytes_read();
6930 eprintln!(
6931 "polyglot lazy read: HTML shell {base} B + .rete image {} B; \
6932 the query touched only {read} B (never the shell).",
6933 image.len()
6934 );
6935 assert!(
6936 read < base,
6937 "read {read} bytes; the HTML shell alone is {base} — the reader \
6938 touched the prefix instead of only the embedded .rete"
6939 );
6940 }
6941
6942 #[cfg(test)]
6945 fn build_text_indexed(triples: &[(String, String, String)]) -> Vec<u8> {
6946 let mut db = DictionaryBuilder::new();
6947 for (s, p, o) in triples {
6948 db.observe(s, p, o);
6949 }
6950 let dict = db.build();
6951 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6952 let mut id_triples: Vec<(u32, u32, u32)> = Vec::with_capacity(triples.len());
6953 for (s, p, o) in triples {
6954 let t = dict.encode(s, p, o).unwrap();
6955 ib.push(t);
6956 id_triples.push(t);
6957 }
6958 let index = ib.build();
6959 let text_index = compute_text_index(&dict, &id_triples);
6960 assert!(
6961 !text_index.is_empty(),
6962 "literals should produce a text index"
6963 );
6964 write_dataset_with_metadata(&dict, &index, &[], false, &[], 0, &[], &text_index)
6965 }
6966
6967 #[test]
6971 fn text_index_eager_matches_brute_force() {
6972 let triples: Vec<(String, String, String)> = vec![
6973 (
6974 "<http://ex/s0>",
6975 "<http://ex/label>",
6976 "\"alpha glucose phosphate\"",
6977 ),
6978 ("<http://ex/s1>", "<http://ex/label>", "\"beta Glucose\""),
6979 ("<http://ex/s2>", "<http://ex/label>", "\"gamma fructose\""),
6980 (
6981 "<http://ex/s3>",
6982 "<http://ex/note>",
6983 "\"einstein relativity\"",
6984 ),
6985 (
6986 "<http://ex/s4>",
6987 "<http://ex/ref>",
6988 "<http://ex/not-a-literal>",
6989 ),
6990 ]
6991 .into_iter()
6992 .map(|(s, p, o)| (s.to_string(), p.to_string(), o.to_string()))
6993 .collect();
6994 let bytes = build_text_indexed(&triples);
6995 let rete = Rete::open(&bytes).unwrap();
6996 assert!(rete.has_text_index());
6997
6998 let brute = |words: &[&str]| -> Vec<String> {
7000 let mut v: Vec<String> = triples
7001 .iter()
7002 .filter(|(_, _, o)| {
7003 crate::terms::is_literal(o)
7004 && words.iter().all(|w| {
7005 let wl = w.to_lowercase();
7006 crate::terms::literal_lexical(o)
7007 .unwrap()
7008 .split(|c: char| !c.is_alphanumeric())
7009 .any(|t| t.to_lowercase() == wl)
7010 })
7011 })
7012 .map(|(s, _, _)| s.clone())
7013 .collect();
7014 v.sort();
7015 v.dedup();
7016 v
7017 };
7018
7019 let mut got = rete.text_search(&["glucose"], None, 0);
7020 got.sort();
7021 assert_eq!(got, brute(&["glucose"]), "case-insensitive single word");
7022
7023 let mut got = rete.text_search(&["glucose", "phosphate"], None, 0);
7025 got.sort();
7026 assert_eq!(got, brute(&["glucose", "phosphate"]));
7027
7028 assert!(rete.text_search(&["zzznope"], None, 0).is_empty());
7030
7031 let got = rete.text_search(&[], Some("ein"), 0);
7033 assert_eq!(got, vec!["<http://ex/s3>".to_string()]);
7034
7035 let mut db = DictionaryBuilder::new();
7037 for (s, p, o) in &triples {
7038 db.observe(s, p, o);
7039 }
7040 let dict = db.build();
7041 let mut ib = GraphIndexBuilder::new();
7042 for (s, p, o) in &triples {
7043 ib.push(dict.encode(s, p, o).unwrap());
7044 }
7045 let plain = write_dataset(&dict, &ib.build(), &[], false, &[], 0);
7046 let plain_rete = Rete::open(&plain).unwrap();
7047 assert!(!plain_rete.has_text_index());
7048 assert!(plain_rete.text_search(&["glucose"], None, 0).is_empty());
7049 }
7050
7051 #[test]
7055 fn text_index_lazy_faults_only_queried_postings() {
7056 use crate::reader::{CountingReader, SliceReader};
7057 let mut triples: Vec<(String, String, String)> = (0..300u32)
7060 .map(|i| {
7061 (
7062 format!("<http://ex/s/{i:04}>"),
7063 "<http://ex/label>".to_string(),
7064 format!("\"common word number {i}\""),
7065 )
7066 })
7067 .collect();
7068 for i in [3u32, 77, 250] {
7069 triples.push((
7070 format!("<http://ex/s/{i:04}>"),
7071 "<http://ex/tag>".to_string(),
7072 "\"raretoken\"".to_string(),
7073 ));
7074 }
7075 let bytes = build_text_indexed(&triples);
7076 let eager = Rete::open(&bytes).unwrap();
7077 let mut want = eager.text_search(&["raretoken"], None, 0);
7078 want.sort();
7079 assert_eq!(want.len(), 3);
7080
7081 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
7082 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7083 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
7084 let before = reader.bytes_read();
7087 let mut got = lazy.text_search(&["raretoken"], None, 0);
7088 got.sort();
7089 assert_eq!(got, want, "lazy search matches eager");
7090 let pulled = reader.bytes_read() - before;
7091 let ti_len = eager.header().text_index_len;
7094 assert!(
7095 pulled < ti_len,
7096 "search pulled {pulled} B but the section is {ti_len} B — faulted too much"
7097 );
7098 assert!(!lazy.index_incomplete());
7099 }
7100
7101 #[test]
7106 fn search_view_matches_full_open_for_far_fewer_bytes() {
7107 use crate::reader::{CountingReader, SliceReader};
7108 let filler = "lorem ipsum dolor sit amet consectetur adipiscing elit ".repeat(20);
7111 let mut triples: Vec<(String, String, String)> = (0..300u32)
7112 .map(|i| {
7113 (
7114 format!("<http://ex/s/{i:04}>"),
7115 "<http://ex/abstract>".to_string(),
7116 format!("\"{filler} number {i}\""),
7117 )
7118 })
7119 .collect();
7120 for i in [3u32, 77, 250] {
7121 triples.push((
7122 format!("<http://ex/s/{i:04}>"),
7123 "<http://ex/tag>".to_string(),
7124 "\"raretoken\"".to_string(),
7125 ));
7126 }
7127 let bytes = build_text_indexed(&triples);
7128 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
7129
7130 let full_reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7131 let full = Rete::open_ranged_lazy(full_reader.clone()).unwrap();
7132 let mut want = full.text_search(&["raretoken"], None, 0);
7133 want.sort();
7134 assert_eq!(want.len(), 3);
7135
7136 let search_reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7137 let view = SearchView::open_ranged(search_reader.clone()).unwrap();
7138 assert!(view.has_text_index());
7139 let mut got = view.text_search(&["raretoken"], None, 0);
7140 got.sort();
7141 assert_eq!(got, want, "search view matches the full open");
7142 assert!(
7143 search_reader.bytes_read() < full_reader.bytes_read(),
7144 "search read {} B, full open read {} B — the narrow open bought nothing",
7145 search_reader.bytes_read(),
7146 full_reader.bytes_read()
7147 );
7148
7149 let mut prefix_hits = view.prefix_search("", 5);
7152 prefix_hits.sort();
7153 assert!(!prefix_hits.is_empty() || !view.has_pyramid());
7154 }
7155
7156 #[test]
7166 fn dict_chunk_directory_probe_costs_about_one_directory() {
7167 use crate::reader::{CountingReader, SliceReader};
7168 let head = "x".repeat(3000);
7182 let triples: Vec<(String, String, String)> = (0..2000u32)
7183 .map(|i| {
7184 let tail = char::from(b'a' + (i % 26) as u8).to_string().repeat(1000);
7185 (
7186 format!("<http://ex/s/{i:04}>"),
7187 "<http://ex/abstract>".to_string(),
7188 format!("\"{head}{i:04}{tail}\""),
7189 )
7190 })
7191 .collect();
7192 let mut db = DictionaryBuilder::new();
7193 for (s, p, o) in &triples {
7194 db.observe(s, p, o);
7195 }
7196 let dict = db.build();
7197 let mut ib = GraphIndexBuilder::new();
7198 for (s, p, o) in &triples {
7199 ib.push(dict.encode(s, p, o).unwrap());
7200 }
7201 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7202
7203 let reader = CountingReader::new(SliceReader::new(&bytes));
7204 let header = Header::from_bytes(&reader.read_at(0, HEADER_LEN as u64).unwrap()).unwrap();
7205 let section = locate_container_section_ranged(
7207 &reader,
7208 header.dictionary_offset,
7209 header.dictionary_len,
7210 2,
7211 4,
7212 )
7213 .unwrap();
7214
7215 let before = reader.bytes_read();
7216 let (_meta, entries) = read_dict_dir_ranged(&reader, section).unwrap();
7217 let spent = reader.bytes_read() - before;
7218
7219 let dir_end = entries[0].start;
7221 let head = reader.read_at(section.offset, 64).unwrap();
7222 let (header_len, n0) = read_uvarint(&head).unwrap();
7223 let dir_start = n0 as u64 + header_len;
7224 let dir_len = dir_end - dir_start;
7225 let dir_total = section.len - dir_start;
7226 assert!(
7227 dir_start + dir_len > 8192,
7228 "directory fits the header prefix ({dir_len} B) — the probe never runs"
7229 );
7230
7231 let mut old = 0u64;
7233 let mut p = 4096u64.min(dir_total).max(1);
7234 loop {
7235 let got = p.min(dir_total);
7236 old += got;
7237 if got >= dir_len || p >= dir_total {
7238 break;
7239 }
7240 p = p.saturating_mul(2).min(dir_total);
7241 }
7242 assert!(
7243 spent <= dir_len * 2,
7244 "probe read {spent} B for a {dir_len} B directory — past the append-only bound"
7245 );
7246 assert!(
7247 spent < old,
7248 "probe read {spent} B; the re-reading loop it replaced spent {old} B"
7249 );
7250 }
7251
7252 fn verbatim_keyed_twin(payload: &[u8], codec: u8) -> Vec<u8> {
7262 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
7263 let (header_len, n0) = read_uvarint(payload).unwrap();
7264 let mut out = Vec::new();
7265 write_uvarint(&mut out, header_len);
7266 out.extend_from_slice(&payload[n0..n0 + header_len as usize]);
7267 write_uvarint(&mut out, entries.len() as u64);
7268 let mut prev_run = 0usize;
7269 for e in &entries {
7270 let body = decompress(codec, &payload[e.start as usize..e.end as usize]).unwrap();
7271 let first = crate::dict::run_first_term(&body, 0).unwrap();
7272 write_uvarint(&mut out, (e.first_run - prev_run) as u64);
7273 write_uvarint(&mut out, first.len() as u64);
7274 out.extend_from_slice(&first);
7275 write_uvarint(&mut out, e.end - e.start);
7276 prev_run = e.first_run;
7277 }
7278 for e in &entries {
7279 out.extend_from_slice(&payload[e.start as usize..e.end as usize]);
7280 }
7281 out
7282 }
7283
7284 fn chunk_bounds_of(payload: &[u8], codec: u8) -> Vec<(Vec<u8>, Vec<u8>)> {
7286 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
7287 entries
7288 .iter()
7289 .map(|e| {
7290 let body = decompress(codec, &payload[e.start as usize..e.end as usize]).unwrap();
7291 (
7292 crate::dict::run_first_term(&body, 0).unwrap(),
7293 crate::dict::run_last_term(&body, 0, body.len()).unwrap(),
7294 )
7295 })
7296 .collect()
7297 }
7298
7299 fn adversarial_terms() -> Vec<String> {
7305 let mut terms: Vec<String> = Vec::new();
7306 for i in 0..6_000 {
7307 terms.push(format!("<http://example.org/work/{i:06}>"));
7308 }
7309 let shared = "X".repeat(900);
7310 for i in 0..4_000 {
7311 terms.push(format!("\"{shared}-{i:05}-tail\""));
7312 }
7313 let deep = "Y".repeat(3_000); for i in 0..120 {
7315 terms.push(format!("\"{deep}{i:03}\""));
7316 }
7317 for i in 0..6 {
7318 terms.push(format!("\"BIG-{i:02}-{}\"", "Z".repeat(90_000)));
7320 }
7321 for i in 0..800 {
7322 terms.push(format!(
7323 "\"h\u{e9}llo-{}-\u{1F600}{i:04}\"",
7324 "\u{e9}".repeat(40)
7325 ));
7326 }
7327 for i in 0..2_000 {
7328 terms.push(format!("\"{i:05}-{}\"", "T".repeat(1_800)));
7329 }
7330 terms.sort();
7331 terms.dedup();
7332 terms
7333 }
7334
7335 fn chunked_section_of(terms: &[String]) -> Vec<u8> {
7336 let mut b = crate::dict::DictSectionBuilder::new().with_restart_interval(16);
7337 for t in terms {
7338 b.push(t.clone());
7339 }
7340 b.build()
7341 }
7342
7343 #[test]
7353 fn chunk_directory_keys_are_separators_not_terms() {
7354 let terms = adversarial_terms();
7355 let raw = chunked_section_of(&terms);
7356 let payload = encode_chunked_dict_section(&raw, CODEC_NONE);
7357 let (_meta, entries) = parse_chunked_dict_dir(&payload, payload.len() as u64).unwrap();
7358 let bounds = chunk_bounds_of(&payload, CODEC_NONE);
7359 assert!(
7360 entries.len() > 20,
7361 "need a many-chunk section, got {}",
7362 entries.len()
7363 );
7364
7365 assert!(entries[0].key.is_empty(), "chunk 0 needs no separator");
7366 let mut key_bytes = 0usize;
7367 let mut term_bytes = 0usize;
7368 let mut differ = 0usize;
7369 for (i, e) in entries.iter().enumerate() {
7370 key_bytes += e.key.len();
7371 term_bytes += bounds[i].0.len();
7372 if e.key != bounds[i].0 {
7373 differ += 1;
7374 }
7375 assert!(
7376 e.key <= bounds[i].0,
7377 "chunk {i}: key must be <= its own first term"
7378 );
7379 if i > 0 {
7380 assert!(
7381 e.key.as_slice() > bounds[i - 1].1.as_slice(),
7382 "chunk {i}: key must be > the previous chunk's last term"
7383 );
7384 assert!(
7385 e.key > entries[i - 1].key,
7386 "chunk {i}: keys must ascend for partition_point"
7387 );
7388 assert_eq!(
7390 e.key,
7391 crate::dict::shortest_separator(&bounds[i - 1].1, &bounds[i].0),
7392 "chunk {i}: key is not the SHORTEST separator"
7393 );
7394 }
7395 }
7396 assert!(
7397 differ > 0,
7398 "every key equalled its first term — the writer stores terms again"
7399 );
7400 assert!(
7401 key_bytes * 4 < term_bytes,
7402 "separators bought almost nothing: {key_bytes} B vs {term_bytes} B of first terms"
7403 );
7404 eprintln!(
7405 "chunks={} first-term keys={term_bytes} B separator keys={key_bytes} B ({:.1}x)",
7406 entries.len(),
7407 term_bytes as f64 / key_bytes.max(1) as f64
7408 );
7409 }
7410
7411 #[test]
7422 fn separator_keys_route_identically_to_verbatim_first_terms() {
7423 let terms = adversarial_terms();
7424 let raw = chunked_section_of(&terms);
7425 let sep_payload = encode_chunked_dict_section(&raw, CODEC_NONE);
7426 let ver_payload = verbatim_keyed_twin(&sep_payload, CODEC_NONE);
7427
7428 let (_ms, es) = parse_chunked_dict_dir(&sep_payload, sep_payload.len() as u64).unwrap();
7430 let (_mv, ev) = parse_chunked_dict_dir(&ver_payload, ver_payload.len() as u64).unwrap();
7431 assert_eq!(es.len(), ev.len());
7432 for (a, b) in es.iter().zip(ev.iter()) {
7433 assert_eq!(a.first_run, b.first_run);
7434 assert_eq!(
7435 &sep_payload[a.start as usize..a.end as usize],
7436 &ver_payload[b.start as usize..b.end as usize],
7437 "chunk bodies must be identical"
7438 );
7439 }
7440 assert!(
7441 sep_payload.len() < ver_payload.len(),
7442 "separator payload is not smaller"
7443 );
7444
7445 let sec_s = decode_chunked_dict_section(
7446 &sep_payload,
7447 CODEC_NONE,
7448 crate::chunk_cache::ChunkCache::unlimited_arc(),
7449 0,
7450 )
7451 .unwrap();
7452 let sec_v = decode_chunked_dict_section(
7453 &ver_payload,
7454 CODEC_NONE,
7455 crate::chunk_cache::ChunkCache::unlimited_arc(),
7456 0,
7457 )
7458 .unwrap();
7459
7460 for (i, t) in terms.iter().enumerate() {
7462 let want = Some(i as u32 + 1);
7463 assert_eq!(sec_v.id(t), want, "verbatim id() lost a term");
7464 assert_eq!(sec_s.id(t), want, "separator id() lost a term");
7465 }
7466 for i in 0..terms.len() as u32 {
7468 assert_eq!(sec_s.term(i + 1), sec_v.term(i + 1));
7469 assert_eq!(
7470 sec_s.term(i + 1).as_deref(),
7471 Some(terms[i as usize].as_str())
7472 );
7473 }
7474 let bounds = chunk_bounds_of(&sep_payload, CODEC_NONE);
7476 let mut probes: Vec<String> = Vec::new();
7477 let push = |bytes: &[u8], probes: &mut Vec<String>| {
7478 if let Ok(s) = std::str::from_utf8(bytes) {
7479 probes.push(s.to_string());
7480 }
7481 };
7482 for (first, last) in &bounds {
7483 for base in [first, last] {
7484 push(base, &mut probes); let mut after = base.clone();
7486 after.push(0x01); push(&after, &mut probes);
7488 let mut far = base.clone();
7489 far.push(b'~');
7490 push(&far, &mut probes);
7491 if !base.is_empty() {
7492 push(&base[..base.len() - 1], &mut probes); let mut down = base.clone();
7494 *down.last_mut().unwrap() = down.last().unwrap().wrapping_sub(1);
7495 push(&down, &mut probes);
7496 let mut up = base.clone();
7497 *up.last_mut().unwrap() = up.last().unwrap().wrapping_add(1);
7498 push(&up, &mut probes);
7499 }
7500 }
7501 }
7502 for e in &es {
7504 push(&e.key, &mut probes);
7505 let mut plus = e.key.clone();
7506 plus.push(0);
7507 push(&plus, &mut probes);
7508 }
7509 probes.push(String::new()); probes.push("\u{1}".to_string()); probes.push("\u{10FFFF}\u{10FFFF}".to_string()); probes.push(terms.first().unwrap().clone());
7513 probes.push(terms.last().unwrap().clone());
7514
7515 let mut hits = 0usize;
7516 for p in &probes {
7517 let (a, b) = (sec_v.id(p), sec_s.id(p));
7518 assert_eq!(
7519 a,
7520 b,
7521 "probe diverged: {:?}",
7522 p.get(..p.len().min(48)).unwrap_or(p.as_str())
7523 );
7524 hits += usize::from(a.is_some());
7525 }
7526 assert!(
7527 probes.len() > 500 && hits > 50,
7528 "probe set is too thin: {} probes, {hits} of them present",
7529 probes.len()
7530 );
7531 eprintln!(
7532 "{} boundary probes, {hits} present, all identical",
7533 probes.len()
7534 );
7535 }
7536
7537 mod separator_props {
7543 use super::*;
7544 use proptest::prelude::*;
7545
7546 fn term_set() -> impl Strategy<Value = Vec<String>> {
7553 prop::collection::vec(("[a-c]{1,6}", 0u8..3, 300usize..1200), 60..250).prop_map(
7554 |specs| {
7555 let mut terms: Vec<String> = specs
7556 .into_iter()
7557 .enumerate()
7558 .map(|(i, (core, kind, pad))| {
7559 let block = "z".repeat(pad);
7560 match kind {
7561 0 => format!("<http://ex/{core}/{i:05}>"),
7562 1 => format!("\"{block}{core}{i:05}\""),
7563 _ => format!("\"{core}{i:05}{block}\""),
7564 }
7565 })
7566 .collect();
7567 terms.sort();
7568 terms.dedup();
7569 terms
7570 },
7571 )
7572 }
7573
7574 proptest! {
7575 #[test]
7576 fn prop_separator_keyed_section_answers_like_verbatim(
7577 terms in term_set(),
7578 extra in prop::collection::vec(any::<String>(), 0..8),
7579 ) {
7580 let raw = chunked_section_of(&terms);
7581 let sep = encode_chunked_dict_section(&raw, CODEC_NONE);
7582 let ver = verbatim_keyed_twin(&sep, CODEC_NONE);
7583 prop_assert!(sep.len() <= ver.len());
7584
7585 let (_m, es) = parse_chunked_dict_dir(&sep, sep.len() as u64).unwrap();
7587 let bounds = chunk_bounds_of(&sep, CODEC_NONE);
7588 prop_assert!(es[0].key.is_empty());
7589 for i in 1..es.len() {
7590 prop_assert!(es[i].key.as_slice() > bounds[i - 1].1.as_slice());
7591 prop_assert!(es[i].key <= bounds[i].0);
7592 prop_assert!(es[i].key > es[i - 1].key);
7593 }
7594
7595 let sec_s = decode_chunked_dict_section(
7596 &sep,
7597 CODEC_NONE,
7598 crate::chunk_cache::ChunkCache::unlimited_arc(),
7599 0,
7600 )
7601 .unwrap();
7602 let sec_v = decode_chunked_dict_section(
7603 &ver,
7604 CODEC_NONE,
7605 crate::chunk_cache::ChunkCache::unlimited_arc(),
7606 0,
7607 )
7608 .unwrap();
7609
7610 for (i, t) in terms.iter().enumerate() {
7611 prop_assert_eq!(sec_s.id(t), Some(i as u32 + 1));
7612 prop_assert_eq!(sec_v.id(t), Some(i as u32 + 1));
7613 prop_assert_eq!(sec_s.term(i as u32 + 1), sec_v.term(i as u32 + 1));
7614 }
7615
7616 let mut probes: Vec<String> = extra;
7618 probes.push(String::new());
7619 for (first, last) in &bounds {
7620 for base in [first, last] {
7621 if let Ok(s) = std::str::from_utf8(base) {
7622 probes.push(s.to_string());
7623 probes.push(format!("{s}\u{1}"));
7624 probes.push(s[..s.len() - s.chars().next_back()
7625 .map(char::len_utf8).unwrap_or(0)].to_string());
7626 }
7627 }
7628 }
7629 for e in &es {
7630 if let Ok(s) = std::str::from_utf8(&e.key) {
7631 probes.push(s.to_string());
7632 }
7633 }
7634 for p in &probes {
7635 prop_assert_eq!(sec_s.id(p), sec_v.id(p), "probe diverged: {:?}", p);
7636 }
7637 }
7638 }
7639 }
7640
7641 #[test]
7646 fn text_index_is_tamper_evident_and_verifies() {
7647 let triples: Vec<(String, String, String)> = vec![(
7648 "<http://ex/s0>".to_string(),
7649 "<http://ex/label>".to_string(),
7650 "\"alpha glucose phosphate\"".to_string(),
7651 )];
7652 let bytes = build_text_indexed(&triples);
7653 let header = Rete::open(&bytes).unwrap().header().clone();
7654 assert!(header.text_index_len > 0);
7655 assert!(verify(&bytes).unwrap(), "a text-indexed build must verify");
7656
7657 let mut tampered = bytes.clone();
7658 tampered[header.text_index_offset as usize] ^= 0xff;
7659 assert!(
7660 !verify(&tampered).unwrap(),
7661 "tampering with the text index must break verify()"
7662 );
7663 }
7664
7665 #[test]
7669 fn synopsis_cuts_remote_fetch_bytes() {
7670 use crate::header::FLAG_TILE_SYNOPSIS;
7671 use crate::reader::{CountingReader, SliceReader};
7672
7673 let triples: Vec<(String, String, String)> = (0..400u32)
7677 .map(|i| {
7678 (
7679 format!("<http://ex/s/{i:04}>"),
7680 "<http://ex/p>".to_string(),
7681 format!("<http://ex/o/{i:04}>"),
7682 )
7683 })
7684 .collect();
7685 let mut db = DictionaryBuilder::new();
7686 for (s, p, o) in &triples {
7687 db.observe(s, p, o);
7688 }
7689 let dict = db.build();
7690 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
7691 for (s, p, o) in &triples {
7692 ib.push(dict.encode(s, p, o).unwrap());
7693 }
7694 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7695
7696 let q = (Some("<http://ex/s/0395>"), None, Some("<http://ex/o/0005>"));
7700 let query_bytes = |image: &[u8]| -> (u64, usize) {
7704 let leaked: &'static [u8] = Box::leak(image.to_vec().into_boxed_slice());
7705 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7706 let rete = Rete::open_ranged_lazy(reader.clone()).unwrap();
7707 let before = reader.bytes_read(); let n = rete.query(q.0, q.1, q.2).len();
7709 assert!(!rete.index_incomplete());
7710 (reader.bytes_read() - before, n)
7711 };
7712
7713 let (on_bytes, on_n) = query_bytes(&bytes);
7714 let mut off = bytes.clone();
7716 off[5] &= !FLAG_TILE_SYNOPSIS;
7717 let (off_bytes, off_n) = query_bytes(&off);
7718
7719 assert_eq!(on_n, 0, "the pair never co-occurs");
7720 assert_eq!(off_n, 0, "same answer without the synopsis");
7721 assert!(
7725 on_bytes < off_bytes,
7726 "synopsis skips the routed tile fetch: {on_bytes} < {off_bytes}"
7727 );
7728 }
7729
7730 #[test]
7736 fn double_bound_object_join_eager_matches_lazy() {
7737 use crate::reader::SliceReader;
7738 let occ = "<http://ex/occ>";
7739 let phys = "<http://ex/physicist>";
7740 let phil = "<http://ex/philosopher>";
7741 let label = "<http://www.w3.org/2000/01/rdf-schema#label>";
7742 let mut triples: Vec<(String, String, String)> = Vec::new();
7744 for i in 0..20u32 {
7745 triples.push((format!("<http://ex/p/{i:02}>"), occ.into(), phys.into()));
7746 if i < 10 {
7747 triples.push((format!("<http://ex/p/{i:02}>"), occ.into(), phil.into()));
7748 }
7749 triples.push((
7750 format!("<http://ex/p/{i:02}>"),
7751 label.into(),
7752 format!("\"Name {i:02}\""),
7753 ));
7754 }
7755 let mut db = DictionaryBuilder::new();
7756 for (s, p, o) in &triples {
7757 db.observe(s, p, o);
7758 }
7759 let dict = db.build();
7760 let mut ib = GraphIndexBuilder::new().with_tile_budget(16);
7761 for (s, p, o) in &triples {
7762 ib.push(dict.encode(s, p, o).unwrap());
7763 }
7764 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7765
7766 let q = "SELECT ?l WHERE { \
7767 ?p <http://ex/occ> <http://ex/physicist> ; \
7768 <http://ex/occ> <http://ex/philosopher> ; \
7769 <http://www.w3.org/2000/01/rdf-schema#label> ?l }";
7770 let run = |rete: &Rete| -> Vec<String> {
7771 let (_, sols) = crate::eval_sparql(rete, q).unwrap();
7772 let mut v: Vec<String> = sols.iter().filter_map(|b| b.get("l").cloned()).collect();
7773 v.sort();
7774 v
7775 };
7776
7777 let eager_rows = run(&Rete::open(&bytes).unwrap());
7778 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
7779 let lazy = Rete::open_ranged_lazy(std::sync::Arc::new(SliceReader::new(leaked))).unwrap();
7780 let lazy_rows = run(&lazy);
7781 assert!(!lazy.index_incomplete());
7782
7783 assert_eq!(eager_rows.len(), 10, "the 10 physicist∩philosopher labels");
7784 assert_eq!(eager_rows, lazy_rows, "eager and lazy must agree exactly");
7785 }
7786
7787 #[test]
7791 fn multi_chunk_dictionary_round_trips() {
7792 let mut db = DictionaryBuilder::new();
7793 let term = |i: u32| format!("<http://example.org/some/long/prefix/entity/{i:06}>");
7794 for i in 0..6000u32 {
7795 db.observe(&term(i), "<http://ex/p>", &term(i + 1));
7796 }
7797 let dict = db.build();
7798 let mut ib = GraphIndexBuilder::new();
7799 for i in 0..6000u32 {
7800 ib.push(
7801 dict.encode(&term(i), "<http://ex/p>", &term(i + 1))
7802 .unwrap(),
7803 );
7804 }
7805 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7806 let rete = Rete::open(&bytes).unwrap();
7807 let d = rete.dictionary();
7808 assert_eq!(d.term_count(), dict.term_count());
7809 for i in (0..6000).step_by(97).chain([0, 1, 5999, 6000]) {
7810 let t = term(i);
7811 let sid = dict.subject_id(&t);
7812 assert_eq!(d.subject_id(&t), sid, "subject_id({t})");
7813 if let Some(id) = sid {
7814 assert_eq!(d.subject_term(id).as_deref(), Some(t.as_str()));
7815 }
7816 let oid = dict.object_id(&t);
7817 assert_eq!(d.object_id(&t), oid, "object_id({t})");
7818 }
7819 assert_eq!(d.subject_id("<http://example.org/absent>"), None);
7820 assert_eq!(d.predicate_id("<http://ex/p>"), Some(1));
7821 assert_eq!(d.predicate_term(1).as_deref(), Some("<http://ex/p>"));
7822 }
7823
7824 #[test]
7825 #[cfg(feature = "compression")]
7826 fn compression_shrinks_repetitive_data() {
7827 let mut db = DictionaryBuilder::new();
7831 let triples: Vec<(String, String, String)> = (0..500)
7832 .map(|i| {
7833 (
7834 format!("<http://example.org/entity/{i}>"),
7835 "<http://example.org/p/relatedTo>".to_string(),
7836 format!("<http://example.org/entity/{}>", (i + 1) % 500),
7837 )
7838 })
7839 .collect();
7840 for (s, p, o) in &triples {
7841 db.observe(s, p, o);
7842 }
7843 let dict = db.build();
7844 let mut ib = GraphIndexBuilder::new();
7845 for (s, p, o) in &triples {
7846 ib.push(dict.encode(s, p, o).unwrap());
7847 }
7848 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7849
7850 let raw: usize = triples
7851 .iter()
7852 .map(|(s, p, o)| s.len() + p.len() + o.len())
7853 .sum();
7854 assert!(
7855 bytes.len() < raw / 2,
7856 "expected strong compression: file {} vs raw terms {raw}",
7857 bytes.len()
7858 );
7859
7860 let rete = Rete::open(&bytes).unwrap();
7862 let r = rete.query(Some("<http://example.org/entity/0>"), None, None);
7863 assert_eq!(r.len(), 1);
7864 assert_eq!(r[0].2, "<http://example.org/entity/1>");
7865 }
7866
7867 fn big_file_with_pyramid() -> Vec<u8> {
7868 let triples: Vec<(String, String, String)> = (0..300)
7870 .map(|i| {
7871 (
7872 format!("<http://ex/e{i}>"),
7873 "<http://ex/next>".to_string(),
7874 format!("<http://ex/e{}>", (i + 1) % 300),
7875 )
7876 })
7877 .collect();
7878 let mut db = DictionaryBuilder::new();
7879 for (s, p, o) in &triples {
7880 db.observe(s, p, o);
7881 }
7882 let dict = db.build();
7883 let ids: Vec<_> = triples
7884 .iter()
7885 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
7886 .collect();
7887 let mut ib = GraphIndexBuilder::new();
7888 for &t in &ids {
7889 ib.push(t);
7890 }
7891 let (meta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
7892 write_file(&dict, &ib.build(), false, &meta, levels)
7893 }
7894
7895 #[test]
7896 fn ranged_open_is_minimal_and_correct() {
7897 use crate::reader::{CountingReader, SliceReader};
7898 let bytes = big_file_with_pyramid();
7899
7900 let full = CountingReader::new(SliceReader::new(&bytes));
7901 let rete = Rete::open_ranged(&full).unwrap();
7902 assert!(full.requests() <= 4, "requests = {}", full.requests());
7904 assert_eq!(
7905 rete.query(Some("<http://ex/e0>"), None, None)[0].2,
7906 "<http://ex/e1>"
7907 );
7908
7909 let summ_reader = CountingReader::new(SliceReader::new(&bytes));
7911 let view = SummaryView::open_ranged(&summ_reader).unwrap().unwrap();
7912 assert!(!view.summary.is_empty());
7913 assert!(
7914 summ_reader.bytes_read() < bytes.len() as u64,
7915 "summary read {} of {} bytes",
7916 summ_reader.bytes_read(),
7917 bytes.len()
7918 );
7919 assert!(summ_reader.bytes_read() < full.bytes_read());
7921 }
7922
7923 #[test]
7924 fn content_hash_is_set_and_verifies() {
7925 let bytes = build_image();
7926 let rete = Rete::open(&bytes).unwrap();
7927 assert_ne!(
7928 rete.header().content_hash,
7929 [0u8; 16],
7930 "hash must be populated"
7931 );
7932 assert!(verify(&bytes).unwrap(), "freshly built file verifies");
7933 assert!(
7934 verify_ranged(&crate::reader::SliceReader::new(&bytes)).unwrap(),
7935 "the streaming verify agrees"
7936 );
7937
7938 assert_eq!(
7940 Rete::open(&build_image()).unwrap().header().content_hash,
7941 rete.header().content_hash
7942 );
7943
7944 let mut tampered = bytes.clone();
7946 let last = tampered.len() - 5; tampered[last] ^= 0xff;
7948 assert!(!verify(&tampered).unwrap());
7949 assert!(!verify_ranged(&crate::reader::SliceReader::new(&tampered)).unwrap());
7950 let short = &bytes[..bytes.len() / 2];
7952 assert!(verify_ranged(&crate::reader::SliceReader::new(short)).is_err());
7953 assert!(verify_ranged(&crate::reader::SliceReader::new(&bytes[..10])).is_err());
7954 }
7955
7956 fn build_with_metadata(meta: &[u8]) -> Vec<u8> {
7958 let triples = [
7959 ("Alice", "knows", "Bob"),
7960 ("Bob", "knows", "Carol"),
7961 ("Alice", "age", "30"),
7962 ];
7963 let mut db = DictionaryBuilder::new();
7964 for (s, p, o) in triples {
7965 db.observe(s, p, o);
7966 }
7967 let dict = db.build();
7968 let ids: Vec<_> = triples
7969 .iter()
7970 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
7971 .collect();
7972 let mut ib = GraphIndexBuilder::new();
7973 for &t in &ids {
7974 ib.push(t);
7975 }
7976 let (pmeta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
7977 write_dataset_with_metadata(&dict, &ib.build(), &[], false, &pmeta, levels, meta, &[])
7978 }
7979
7980 #[test]
7981 fn metadata_round_trips_and_shifts_offsets() {
7982 let card = br#"{"title":"My Dataset"}"#;
7983 let bytes = build_with_metadata(card);
7984 let rete = Rete::open(&bytes).unwrap();
7985
7986 assert_eq!(rete.metadata(), Some(card.as_slice()));
7988 let h = rete.header();
7989 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
7990 assert_eq!(h.metadata_len, card.len() as u64);
7991 assert_eq!(h.dictionary_offset, HEADER_LEN as u64 + card.len() as u64);
7993
7994 assert_eq!(
7996 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
7997 1
7998 );
7999 assert!(verify(&bytes).unwrap());
8001 }
8002
8003 #[test]
8004 fn empty_metadata_is_byte_identical_to_plain_writer() {
8005 assert_eq!(
8008 build_with_metadata(&[]),
8009 build_image(),
8010 "empty-metadata output must equal the plain writer byte-for-byte"
8011 );
8012 }
8013
8014 #[test]
8019 fn replace_metadata_matches_a_direct_write() {
8020 let first = br#"{"title":"My Dataset","queries":[1,2,3]}"#;
8021 let bytes = build_with_metadata(first);
8022 for target in [
8023 br#"{"title":"My Dataset","queries":[1,2]}"#.as_slice(), br#"{"title":"My Dataset","queries":[1,2,3,4,5,6,7]}"#.as_slice(), b"".as_slice(), first.as_slice(), ] {
8028 let spliced = replace_metadata(&bytes, target).unwrap();
8029 assert_eq!(
8030 spliced,
8031 build_with_metadata(target),
8032 "splicing {} bytes of metadata must equal a direct write",
8033 target.len()
8034 );
8035 assert!(
8036 verify(&spliced).unwrap(),
8037 "the new hash covers the new card"
8038 );
8039 let rete = Rete::open(&spliced).unwrap();
8040 assert_eq!(
8041 rete.metadata(),
8042 (!target.is_empty()).then_some(target),
8043 "the new payload reads back verbatim"
8044 );
8045 assert_eq!(
8047 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
8048 1
8049 );
8050 }
8051 }
8052
8053 #[test]
8056 fn replace_metadata_keeps_build_info_adjacent() {
8057 let with_info =
8058 attach_build_info(&build_with_metadata(br#"{"a":1}"#), br#"{"builder":"x"}"#).unwrap();
8059 let spliced = replace_metadata(&with_info, br#"{"a":1,"b":2}"#).unwrap();
8060 let h = Header::from_bytes(&spliced).unwrap();
8061 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
8062 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + h.metadata_len);
8063 assert_eq!(
8064 read_build_info(&spliced).unwrap().as_deref(),
8065 Some(br#"{"builder":"x"}"#.as_slice())
8066 );
8067 assert!(verify(&spliced).unwrap());
8068 assert_eq!(
8070 h.content_hash,
8071 Header::from_bytes(&build_with_metadata(br#"{"a":1,"b":2}"#))
8072 .unwrap()
8073 .content_hash
8074 );
8075 }
8076
8077 #[test]
8078 fn metadata_is_tamper_evident() {
8079 let card = br#"{"title":"x"}"#;
8080 let mut bytes = build_with_metadata(card);
8081 assert!(verify(&bytes).unwrap());
8082 bytes[HEADER_LEN + 2] ^= 0xff;
8084 assert!(
8085 !verify(&bytes).unwrap(),
8086 "tampering with the card must break verify()"
8087 );
8088 }
8089
8090 #[test]
8091 fn ranged_opens_do_not_fetch_metadata() {
8092 use crate::reader::{CountingReader, SliceReader};
8093 let card = vec![0xABu8; 512]; let bytes = build_with_metadata(&card);
8095 let total = bytes.len() as u64;
8096
8097 let r = CountingReader::new(SliceReader::new(&bytes));
8099 let rete = Rete::open_ranged(&r).unwrap();
8100 assert!(
8101 rete.metadata().is_none(),
8102 "open_ranged must not load the card"
8103 );
8104 assert!(r.requests() <= 4, "requests = {}", r.requests());
8105 assert!(
8106 r.bytes_read() <= total - card.len() as u64,
8107 "read {} of {} bytes; the {}-byte card must be skipped",
8108 r.bytes_read(),
8109 total,
8110 card.len()
8111 );
8112
8113 let rs = CountingReader::new(SliceReader::new(&bytes));
8115 let view = SummaryView::open_ranged(&rs).unwrap().unwrap();
8116 assert!(!view.summary.is_empty());
8117 assert!(rs.bytes_read() <= total - card.len() as u64);
8118 }
8119
8120 #[test]
8121 fn metadata_ranged_fetches_only_header_and_card() {
8122 use crate::reader::{CountingReader, SliceReader};
8123 let card = vec![0xCDu8; 384];
8126 let bytes = build_with_metadata(&card);
8127
8128 let r = CountingReader::new(SliceReader::new(&bytes));
8129 let got = read_metadata_ranged(&r).unwrap().unwrap();
8130 assert_eq!(got, card, "the card reads back verbatim");
8131 assert_eq!(r.requests(), 2, "exactly header + metadata ranges");
8132 assert_eq!(
8133 r.bytes_read(),
8134 HEADER_LEN as u64 + card.len() as u64,
8135 "no dictionary/index/pyramid bytes are touched"
8136 );
8137
8138 let plain = build_image();
8140 let rp = CountingReader::new(SliceReader::new(&plain));
8141 assert!(read_metadata_ranged(&rp).unwrap().is_none());
8142 assert_eq!(rp.requests(), 1, "header only for a cardless file");
8143 assert_eq!(rp.bytes_read(), HEADER_LEN as u64);
8144 }
8145
8146 #[test]
8147 fn build_info_attaches_outside_the_hash() {
8148 let card = br#"{"title":"My Dataset"}"#;
8149 let base = build_with_metadata(card);
8150 let base_hash = Header::from_bytes(&base).unwrap().content_hash;
8151
8152 let info = br#"{"built_at":"2026-08-04T00:00:00Z","builder":"rete-cli 0.3.2"}"#;
8153 let with = attach_build_info(&base, info).unwrap();
8154
8155 assert_eq!(read_build_info(&with).unwrap().unwrap(), info);
8157 let h = Header::from_bytes(&with).unwrap();
8158 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + card.len() as u64);
8159 assert_eq!(h.build_info_len, info.len() as u64);
8160 assert_eq!(h.expected_file_len(), Some(with.len() as u64));
8161
8162 assert_eq!(h.content_hash, base_hash);
8166 assert!(verify(&with).unwrap());
8167 let other = attach_build_info(&base, br#"{"built_at":"1999-01-01T00:00:00Z"}"#).unwrap();
8168 assert_eq!(Header::from_bytes(&other).unwrap().content_hash, base_hash);
8169
8170 let rete = Rete::open(&with).unwrap();
8172 assert_eq!(
8173 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
8174 1
8175 );
8176 assert_eq!(rete.metadata(), Some(card.as_slice()));
8177
8178 let replaced = attach_build_info(&with, br#"{"builder":"other"}"#).unwrap();
8181 assert_eq!(
8182 read_build_info(&replaced).unwrap().unwrap(),
8183 br#"{"builder":"other"}"#
8184 );
8185 assert!(verify(&replaced).unwrap());
8186 let stripped = attach_build_info(&replaced, &[]).unwrap();
8187 assert_eq!(stripped, base, "stripping build-info restores the image");
8188 }
8189
8190 #[test]
8191 fn build_info_attaches_on_a_metadata_free_file() {
8192 let base = build_image();
8194 let info = b"{\"builder\":\"x\"}";
8195 let with = attach_build_info(&base, info).unwrap();
8196 let h = Header::from_bytes(&with).unwrap();
8197 assert_eq!(h.build_info_offset, HEADER_LEN as u64);
8198 assert_eq!(h.metadata_len, 0);
8199 assert_eq!(read_build_info(&with).unwrap().unwrap(), info);
8200 assert!(verify(&with).unwrap());
8201 assert_eq!(
8202 h.content_hash,
8203 Header::from_bytes(&base).unwrap().content_hash
8204 );
8205 Rete::open(&with).unwrap();
8206 }
8207
8208 #[test]
8215 fn a_plan_rebuilds_exactly_what_the_in_memory_splice_produces() {
8216 let card = br#"{"title":"My Dataset"}"#;
8217 let base = build_with_metadata(card);
8218 let base_hash = Header::from_bytes(&base).unwrap().content_hash;
8219 for start in [
8220 base.clone(),
8221 attach_build_info(&base, b"{\"a\":1}").unwrap(),
8222 ] {
8223 for info in [
8224 br#"{"builder":"rete-cli 0.3.2","query_costs":{}}"#.as_slice(), b"{}".as_slice(), b"".as_slice(), ] {
8228 let plan = plan_build_info(&start, start.len() as u64, info.len() as u64).unwrap();
8229 let mut streamed = Vec::new();
8230 streamed.extend_from_slice(&plan.header);
8231 streamed.extend_from_slice(&start[HEADER_LEN..plan.insert as usize]);
8232 streamed.extend_from_slice(info);
8233 streamed.extend_from_slice(&start[plan.tail_start as usize..]);
8234 assert_eq!(streamed.len() as u64, plan.new_len);
8235 assert_eq!(
8236 streamed,
8237 attach_build_info(&start, info).unwrap(),
8238 "a {}-byte section streamed must equal the same section spliced",
8239 info.len()
8240 );
8241 assert!(verify(&streamed).unwrap());
8243 let h = Header::from_bytes(&streamed).unwrap();
8244 assert_eq!(h.content_hash, base_hash);
8245 assert_eq!(h.expected_file_len(), Some(plan.new_len));
8246 let rete = Rete::open(&streamed).unwrap();
8247 assert_eq!(rete.metadata(), Some(card.as_slice()));
8248 assert_eq!(
8249 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
8250 1
8251 );
8252 }
8253 }
8254 }
8255
8256 #[test]
8259 fn a_plan_refuses_a_header_that_overruns_the_file() {
8260 let base = build_with_metadata(br#"{"title":"x"}"#);
8261 assert!(plan_build_info(&base, 8, 16).is_err());
8262 assert!(plan_build_info(&base[..HEADER_LEN], base.len() as u64, 16).is_ok());
8263 }
8264
8265 #[test]
8266 fn card_and_build_info_ranged_is_one_header_plus_one_range() {
8267 use crate::reader::{CountingReader, SliceReader};
8268 let card = vec![0xCDu8; 384];
8269 let info = vec![0xEFu8; 200];
8270 let bytes = attach_build_info(&build_with_metadata(&card), &info).unwrap();
8271
8272 let r = CountingReader::new(SliceReader::new(&bytes));
8275 let (m, b) = read_card_and_build_info_ranged(&r).unwrap();
8276 assert_eq!(m.unwrap(), card);
8277 assert_eq!(b.unwrap(), info);
8278 assert_eq!(r.requests(), 2, "header + one coalesced range");
8279 assert_eq!(
8280 r.bytes_read(),
8281 HEADER_LEN as u64 + card.len() as u64 + info.len() as u64
8282 );
8283
8284 let plain = build_with_metadata(&card);
8286 let rp = CountingReader::new(SliceReader::new(&plain));
8287 let (m, b) = read_card_and_build_info_ranged(&rp).unwrap();
8288 assert_eq!(m.unwrap(), card);
8289 assert!(b.is_none());
8290 assert_eq!(rp.requests(), 2);
8291 assert_eq!(
8296 r.requests(),
8297 rp.requests(),
8298 "build info must not cost an extra request",
8299 );
8300
8301 let none = build_image();
8303 let rn = CountingReader::new(SliceReader::new(&none));
8304 let (m, b) = read_card_and_build_info_ranged(&rn).unwrap();
8305 assert!(m.is_none() && b.is_none());
8306 assert_eq!(rn.requests(), 1);
8307 }
8308
8309 #[test]
8310 fn schema_summary_groups_by_type() {
8311 let rt = RDF_TYPE;
8312 let bytes = build_from(&[
8313 ("Alice", rt, "Person"),
8314 ("Bob", rt, "Person"),
8315 ("NYC", rt, "City"),
8316 ("Alice", "knows", "Bob"),
8317 ("Alice", "livesIn", "NYC"),
8318 ("Alice", "name", "\"Alice\""),
8319 ]);
8320 let rete = Rete::open(&bytes).unwrap();
8321 let summary = schema_summary(&rete);
8322 assert!(summary.contains(&("Person".into(), "knows".into(), "Person".into(), 1)));
8324 assert!(summary.contains(&("Person".into(), "livesIn".into(), "City".into(), 1)));
8325 assert!(summary.contains(&("Person".into(), "name".into(), "(literal)".into(), 1)));
8326 assert!(!summary.iter().any(|(_, p, _, _)| p == RDF_TYPE));
8328
8329 let classes = schema_classes(&rete);
8331 assert_eq!(
8332 classes,
8333 vec![("Person".into(), 2u32), ("City".into(), 1u32)]
8334 );
8335 }
8336
8337 fn build_from(triples: &[(&str, &str, &str)]) -> Vec<u8> {
8338 let mut db = DictionaryBuilder::new();
8339 for (s, p, o) in triples {
8340 db.observe(s, p, o);
8341 }
8342 let dict = db.build();
8343 let mut ib = GraphIndexBuilder::new();
8344 for (s, p, o) in triples {
8345 ib.push(dict.encode(s, p, o).unwrap());
8346 }
8347 write_file(&dict, &ib.build(), false, &[], 0)
8348 }
8349
8350 fn build_with_pyramid(triples: &[(&str, &str, &str)]) -> Vec<u8> {
8352 let mut db = DictionaryBuilder::new();
8353 for (s, p, o) in triples {
8354 db.observe(s, p, o);
8355 }
8356 let dict = db.build();
8357 let encoded: Vec<_> = triples
8358 .iter()
8359 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
8360 .collect();
8361 let mut ib = GraphIndexBuilder::new();
8362 for t in &encoded {
8363 ib.push(*t);
8364 }
8365 let (meta, levels) = build_pyramid_meta(&dict, &encoded, DEFAULT_TILE_BUDGET);
8366 write_dataset(&dict, &ib.build(), &[], false, &meta, levels)
8367 }
8368
8369 #[test]
8370 fn tbox_coherence_flags_unsatisfiable_class_index_free() {
8371 use crate::reader::{CountingReader, SliceReader};
8372 let rt = RDF_TYPE;
8373 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
8374 let disj = "<http://www.w3.org/2002/07/owl#disjointWith>";
8375 let bytes = build_with_pyramid(&[
8378 ("<http://ex/C>", sub, "<http://ex/D>"),
8379 ("<http://ex/C>", sub, "<http://ex/E>"),
8380 ("<http://ex/D>", disj, "<http://ex/E>"),
8381 ("<http://ex/x>", rt, "<http://ex/C>"),
8382 ]);
8383
8384 let r = CountingReader::new(SliceReader::new(&bytes));
8385 let view = SummaryView::open_ranged(&r).unwrap().unwrap();
8386 let points = view.tbox_coherence();
8387 assert!(
8388 points
8389 .iter()
8390 .any(|i| i.kind == "unsatisfiable-class" && i.detail.contains("http://ex/C>")),
8391 "expected C unsatisfiable from the schema alone, got {points:?}"
8392 );
8393
8394 let header = Header::from_bytes(&bytes[..HEADER_LEN]).unwrap();
8397 assert!(
8398 r.bytes_read() <= bytes.len() as u64 - header.root_dir_len,
8399 "tbox_coherence must not read the triple index"
8400 );
8401 }
8402
8403 #[test]
8404 fn schema_coherence_reads_only_the_schema_block() {
8405 use crate::reader::{CountingReader, SliceReader};
8406 let rt = RDF_TYPE;
8407 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
8408 let disj = "<http://www.w3.org/2002/07/owl#disjointWith>";
8409 let mut triples: Vec<(String, String, String)> = vec![
8413 ("<http://ex/C>".into(), sub.into(), "<http://ex/D>".into()),
8414 ("<http://ex/C>".into(), sub.into(), "<http://ex/E>".into()),
8415 ("<http://ex/D>".into(), disj.into(), "<http://ex/E>".into()),
8416 ];
8417 for i in 0..500 {
8418 let s = format!("<http://ex/x{i}>");
8419 triples.push((s.clone(), rt.into(), "<http://ex/C>".into()));
8420 triples.push((
8421 s,
8422 "<http://ex/label>".into(),
8423 format!("\"unique label {i}\""),
8424 ));
8425 }
8426 let trefs: Vec<(&str, &str, &str)> = triples
8427 .iter()
8428 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
8429 .collect();
8430 let bytes = build_with_pyramid(&trefs);
8431
8432 let header = Header::from_bytes(&bytes[..HEADER_LEN]).unwrap();
8433 assert!(
8434 header.schema_meta_len > 0,
8435 "the writer recorded a schema-block length"
8436 );
8437 assert!(
8438 (header.schema_meta_len as u64) < header.pyramid_meta_len,
8439 "schema block ({}) should be far smaller than the whole pyramid-meta ({})",
8440 header.schema_meta_len,
8441 header.pyramid_meta_len
8442 );
8443
8444 let r = CountingReader::new(SliceReader::new(&bytes));
8445 let points = read_schema_coherence_ranged(&r).unwrap().unwrap();
8446 assert!(points.iter().any(|i| i.kind == "unsatisfiable-class"));
8447 assert!(
8449 r.bytes_read() <= HEADER_LEN as u64 + header.schema_meta_len as u64,
8450 "read {} bytes; expected <= header + schema block ({})",
8451 r.bytes_read(),
8452 HEADER_LEN as u64 + header.schema_meta_len as u64
8453 );
8454 }
8455
8456 #[test]
8457 fn tbox_coherence_clean_schema_is_coherent() {
8458 use crate::reader::SliceReader;
8459 let rt = RDF_TYPE;
8460 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
8461 let bytes = build_with_pyramid(&[
8462 ("<http://ex/Dog>", sub, "<http://ex/Animal>"),
8463 ("<http://ex/x>", rt, "<http://ex/Dog>"),
8464 ]);
8465 let view = SummaryView::open_ranged(&SliceReader::new(&bytes))
8466 .unwrap()
8467 .unwrap();
8468 assert!(view.tbox_is_coherent(), "a plain hierarchy is coherent");
8469 }
8470
8471 #[test]
8479 fn readers_before_the_quad_index_refuse_its_files() {
8480 for perms in [PermSet::ALL, PermSet::CORE] {
8481 let quads: Vec<crate::ingest::RawQuad> = (0..50)
8482 .map(|i| {
8483 (
8484 format!("<http://ex/s{i}>"),
8485 "<http://ex/p>".to_string(),
8486 format!("\"{i}\""),
8487 Some(format!("<http://ex/g{}>", i % 7)),
8488 )
8489 })
8490 .collect();
8491 let (bytes, _) = crate::ingest::assemble_dataset_with_layout(
8492 quads,
8493 false,
8494 false,
8495 None,
8496 crate::PyramidAlgo::Louvain,
8497 perms,
8498 crate::ingest::GraphLayout::Quad,
8499 |_, _| Vec::<u8>::new(),
8500 );
8501 let h = Header::from_bytes(&bytes).unwrap();
8502 assert!(h.has_quad_index());
8503 let root =
8504 &bytes[h.root_dir_offset as usize..(h.root_dir_offset + h.root_dir_len) as usize];
8505 assert!(decode_index_container(root, h.block_codec, h.perms).is_err());
8507 assert!(
8508 container_section_payload_ranges(root, h.root_dir_offset, h.perms.len()).is_err()
8509 );
8510 assert_ne!(decode_container(root, CODEC_NONE).unwrap().len(), NUM_PERMS);
8512 assert_ne!(
8513 decode_container(root, CODEC_NONE).unwrap().len(),
8514 h.perms.len()
8515 );
8516 let r = crate::reader::SliceReader::new(&bytes);
8518 for expect in [h.perms.len(), NUM_PERMS] {
8519 assert!(locate_container_section_ranged(
8520 &r,
8521 h.root_dir_offset,
8522 h.root_dir_len,
8523 0,
8524 expect as u64
8525 )
8526 .is_err());
8527 }
8528 assert_eq!(hash_of_sections(&h, &bytes).unwrap(), h.content_hash);
8530 let rete = Rete::open(&bytes).unwrap();
8532 assert_eq!(rete.named_graph_count(), 7);
8533 assert_eq!(rete.dump(Some("<http://ex/g3>")).len(), 7);
8534 }
8535 }
8536
8537 #[test]
8538 fn named_graphs_round_trip() {
8539 let all = [
8541 ("Alice", "knows", "Bob"), ("Bob", "age", "30"), ];
8544 let mut db = DictionaryBuilder::new();
8545 for (s, p, o) in all {
8546 db.observe(s, p, o);
8547 }
8548 let dict = db.build();
8549
8550 let mut def = GraphIndexBuilder::new();
8551 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
8552 let mut g1 = GraphIndexBuilder::new();
8553 g1.push(dict.encode("Bob", "age", "30").unwrap());
8554
8555 let named = vec![("http://ex/g1".to_string(), g1.build())];
8556 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8557
8558 assert!(verify(&bytes).unwrap());
8559 let rete = Rete::open(&bytes).unwrap();
8560 assert_eq!(rete.graph_names(), vec!["http://ex/g1"]);
8561 let gi = rete.graph_index("http://ex/g1").unwrap();
8563 assert_eq!(gi.triple_count(), 1);
8564 assert!(rete.graph_index("http://ex/missing").is_none());
8565
8566 assert_eq!(rete.header().quad_count, 2);
8569
8570 assert_eq!(rete.query(Some("Alice"), None, None).len(), 1);
8572
8573 assert_eq!(
8575 rete.dump(None),
8576 vec![("Alice".into(), "knows".into(), "Bob".into())]
8577 );
8578 assert_eq!(
8579 rete.dump(Some("http://ex/g1")),
8580 vec![("Bob".into(), "age".into(), "30".into())]
8581 );
8582 }
8583
8584 #[test]
8590 fn named_graph_container_opens_tile_lazily_like_the_root() {
8591 let mut db = DictionaryBuilder::new();
8592 let node = |n: u32| format!("<http://ex/n{n}>");
8593 let p = "<http://ex/p>".to_string();
8594 db.observe(&node(0), &p, &node(1));
8595 for i in 0..500u32 {
8596 db.observe(&node(1000 + i), &p, &node(2000 + i));
8597 }
8598 let dict = db.build();
8599 let mut def = GraphIndexBuilder::new();
8600 def.push(dict.encode(&node(0), &p, &node(1)).unwrap());
8601 let mut g = GraphIndexBuilder::new().with_tile_budget(256); for i in 0..500u32 {
8603 g.push(dict.encode(&node(1000 + i), &p, &node(2000 + i)).unwrap());
8604 }
8605 let named = vec![("<http://ex/g>".to_string(), g.build())];
8606 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8607 let rete = Rete::open(&bytes).unwrap();
8608 let header = rete.header();
8609
8610 let soff = header.named_graphs_offset as usize;
8613 let send = soff + header.named_graphs_len as usize;
8614 let sec = &bytes[soff..send];
8615 let (n, mut pos) = read_uvarint(sec).unwrap();
8616 assert_eq!(n, 1);
8617 let (ilen, u1) = read_uvarint(&sec[pos..]).unwrap();
8618 pos += u1 + ilen as usize;
8619 let (clen, u2) = read_uvarint(&sec[pos..]).unwrap();
8620 pos += u2;
8621 let container = ByteRange {
8622 offset: (soff + pos) as u64,
8623 len: clen,
8624 };
8625
8626 use crate::reader::SliceReader;
8627 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8628 let reader: std::sync::Arc<dyn RangeReader + Send + Sync> =
8629 std::sync::Arc::new(SliceReader::new(leaked));
8630 let (lazy_idx, _, _) = open_index_container_lazy(
8631 &reader,
8632 container,
8633 header.block_codec,
8634 header.has_tile_synopsis(),
8635 1,
8636 header.perms,
8637 header.perms.len(),
8638 )
8639 .unwrap();
8640 let mut want = rete
8641 .graph_index("<http://ex/g>")
8642 .unwrap()
8643 .match_pattern((None, None, None));
8644 let mut got = lazy_idx.match_pattern((None, None, None));
8645 want.sort_unstable();
8646 got.sort_unstable();
8647 assert_eq!(got.len(), 500);
8648 assert_eq!(got, want);
8649 assert!(!lazy_idx.load_incomplete());
8650 }
8651
8652 #[test]
8658 fn dump_iter_matches_dump_and_stops_early() {
8659 use crate::reader::{CountingReader, SliceReader};
8660
8661 let triples: Vec<(String, String, String)> = (0..2000u32)
8663 .map(|i| {
8664 (
8665 format!("<http://ex/s/{i:04}>"),
8666 "<http://ex/p>".to_string(),
8667 format!("<http://ex/o/{i:04}>"),
8668 )
8669 })
8670 .collect();
8671 let mut db = DictionaryBuilder::new();
8672 for (s, p, o) in &triples {
8673 db.observe(s, p, o);
8674 }
8675 let dict = db.build();
8676 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
8677 for (s, p, o) in &triples {
8678 ib.push(dict.encode(s, p, o).unwrap());
8679 }
8680 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8681
8682 let rete = Rete::open(&bytes).unwrap();
8684 assert_eq!(rete.dump_iter(None).collect::<Vec<_>>(), rete.dump(None));
8685 assert_eq!(rete.dump_iter(Some("http://ex/nope")).count(), 0);
8687
8688 let read_bytes = |take: usize| -> u64 {
8690 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8691 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8692 let rete = Rete::open_ranged_lazy(reader.clone()).unwrap();
8693 let before = reader.bytes_read();
8694 let n = rete.dump_iter(None).take(take).count();
8695 assert_eq!(n, take.min(triples.len()));
8696 assert!(!rete.index_incomplete());
8697 reader.bytes_read() - before
8698 };
8699 let first_only = read_bytes(1);
8700 let everything = read_bytes(triples.len());
8701 assert!(
8703 first_only < everything,
8704 "dump_iter is not lazy: one triple read {first_only} B, a full scan {everything} B"
8705 );
8706 #[cfg(feature = "compression")]
8713 assert!(
8714 first_only * 2 < everything,
8715 "dump_iter is not lazy: one triple read {first_only} B of the {everything} B a full scan reads"
8716 );
8717
8718 let fresh = Rete::open(&bytes).unwrap();
8723 let drain = |max_quads: usize| {
8728 let (mut out, mut cursor, mut calls) = (Vec::new(), 0u32, 0);
8729 loop {
8730 let (batch, next, done) = fresh.dump_batch(None, cursor, max_quads);
8731 out.extend(batch);
8732 cursor = next;
8733 calls += 1;
8734 assert!(calls < 10_000, "cursor failed to advance");
8735 if done {
8736 break out;
8737 }
8738 }
8739 };
8740 let eager = fresh.dump(None);
8741 for max in [1usize, 7, 128, 100_000] {
8742 assert_eq!(drain(max), eager, "batch size {max} changed the dump");
8743 }
8744 let (t, _, done) = fresh.dump_batch(Some("http://ex/nope"), 0, 16);
8747 assert!(t.is_empty() && done);
8748 }
8749
8750 #[test]
8758 fn query_batch_matches_query_in_graph_and_stops_early() {
8759 use crate::reader::{CountingReader, SliceReader};
8760
8761 let triples: Vec<(String, String, String)> = (0..2000u32)
8764 .map(|i| {
8765 (
8766 format!("<http://ex/s/{:04}>", i / 2),
8767 format!("<http://ex/p/{}>", i % 2),
8768 format!("<http://ex/o/{i:04}>"),
8769 )
8770 })
8771 .collect();
8772 let mut db = DictionaryBuilder::new();
8773 for (s, p, o) in &triples {
8774 db.observe(s, p, o);
8775 }
8776 let dict = db.build();
8777 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
8778 for (s, p, o) in &triples {
8779 ib.push(dict.encode(s, p, o).unwrap());
8780 }
8781 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8782 let rete = Rete::open(&bytes).unwrap();
8783
8784 let drain = |s: Option<&str>, p: Option<&str>, o: Option<&str>, max: usize| {
8785 let (mut out, mut cursor, mut calls) = (Vec::new(), 0u64, 0);
8786 loop {
8787 let (batch, next, done) = rete.query_batch(None, s, p, o, cursor, max);
8788 assert!(!batch.is_empty() || done, "empty non-final batch");
8791 out.extend(batch);
8792 cursor = next;
8793 calls += 1;
8794 assert!(calls < 20_000, "cursor failed to advance");
8795 if done {
8796 break out;
8797 }
8798 }
8799 };
8800 let shapes: [(Option<&str>, Option<&str>, Option<&str>); 6] = [
8801 (None, None, None),
8802 (Some("<http://ex/s/0007>"), None, None),
8803 (None, Some("<http://ex/p/1>"), None),
8804 (None, None, Some("<http://ex/o/0042>")),
8805 (Some("<http://ex/s/0007>"), Some("<http://ex/p/0>"), None),
8806 (
8807 Some("<http://ex/s/0007>"),
8808 Some("<http://ex/p/0>"),
8809 Some("<http://ex/o/0014>"),
8810 ),
8811 ];
8812 for (s, p, o) in shapes {
8813 let eager = rete.query_in_graph(None, s, p, o);
8814 assert!(
8815 !eager.is_empty(),
8816 "fixture shape {s:?} {p:?} {o:?} matches nothing"
8817 );
8818 assert_eq!(
8820 rete.query_iter(None, s, p, o).collect::<Vec<_>>(),
8821 eager,
8822 "query_iter disagrees for {s:?} {p:?} {o:?}"
8823 );
8824 for max in [1usize, 3, 64, 100_000] {
8825 let mut got = drain(s, p, o, max);
8826 let mut want = eager.clone();
8827 got.sort();
8830 want.sort();
8831 assert_eq!(got, want, "batch size {max} changed {s:?} {p:?} {o:?}");
8832 }
8833 if s.is_none() && p.is_none() && o.is_none() {
8835 assert_eq!(drain(s, p, o, 7), eager, "unbound batching reordered rows");
8836 }
8837 }
8838
8839 let (t, _, done) = rete.query_batch(None, Some("<http://ex/nope>"), None, None, 0, 16);
8842 assert!(t.is_empty() && done);
8843 let (t, _, done) = rete.query_batch(Some("http://ex/nope"), None, None, None, 0, 16);
8844 assert!(t.is_empty() && done);
8845 assert_eq!(
8846 rete.query_iter(None, Some("<http://ex/nope>"), None, None)
8847 .count(),
8848 0
8849 );
8850
8851 let read_bytes = |take: usize| -> u64 {
8854 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8855 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8856 let rete = Rete::open_ranged_lazy(reader.clone()).unwrap();
8857 let before = reader.bytes_read();
8858 let (mut n, mut cursor) = (0usize, 0u64);
8859 loop {
8860 let (batch, next, done) = rete.query_batch(None, None, None, None, cursor, 1);
8861 n += batch.len();
8862 cursor = next;
8863 if done || n >= take {
8864 break;
8865 }
8866 }
8867 assert!(n >= take.min(triples.len()));
8868 assert!(!rete.index_incomplete());
8869 reader.bytes_read() - before
8870 };
8871 let first_only = read_bytes(1);
8872 let everything = read_bytes(triples.len());
8873 assert!(
8874 first_only < everything,
8875 "query_batch is not lazy: one row read {first_only} B, a full scan {everything} B"
8876 );
8877 #[cfg(feature = "compression")]
8878 assert!(
8879 first_only * 2 < everything,
8880 "query_batch is not lazy: one row read {first_only} B of the {everything} B a full scan reads"
8881 );
8882 }
8883
8884 #[test]
8887 fn query_batch_is_graph_scoped() {
8888 let mut db = DictionaryBuilder::new();
8889 for (s, p, o) in [
8890 ("Alice", "knows", "Bob"),
8891 ("Alice", "knows", "Carol"),
8892 ("Alice", "knows", "Dave"),
8893 ("Erin", "knows", "Frank"),
8894 ("Alice", "knows", "Zoe"),
8895 ] {
8896 db.observe(s, p, o);
8897 }
8898 let dict = db.build();
8899 let mut def = GraphIndexBuilder::new();
8900 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
8901 def.push(dict.encode("Alice", "knows", "Carol").unwrap());
8902 let mut g1 = GraphIndexBuilder::new();
8903 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
8904 g1.push(dict.encode("Erin", "knows", "Frank").unwrap());
8905 let mut g2 = GraphIndexBuilder::new();
8906 g2.push(dict.encode("Alice", "knows", "Zoe").unwrap());
8907 let named = vec![
8908 ("http://ex/g1".to_string(), g1.build()),
8909 ("http://ex/g2".to_string(), g2.build()),
8910 ];
8911 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8912 let rete = Rete::open(&bytes).unwrap();
8913 for graph in [None, Some("http://ex/g1"), Some("http://ex/g2")] {
8914 let eager = rete.query_in_graph(graph, None, None, None);
8915 let (mut out, mut cursor) = (Vec::new(), 0u64);
8916 loop {
8917 let (batch, next, done) = rete.query_batch(graph, None, None, None, cursor, 1);
8918 out.extend(batch);
8919 cursor = next;
8920 if done {
8921 break;
8922 }
8923 }
8924 assert_eq!(out, eager, "graph {graph:?} batched differently");
8925 }
8926 }
8927
8928 #[test]
8929 fn query_in_graph_is_graph_scoped() {
8930 let mut db = DictionaryBuilder::new();
8933 for (s, p, o) in [
8934 ("Alice", "knows", "Bob"),
8935 ("Alice", "knows", "Carol"),
8936 ("Alice", "knows", "Dave"),
8937 ] {
8938 db.observe(s, p, o);
8939 }
8940 let dict = db.build();
8941
8942 let mut def = GraphIndexBuilder::new();
8943 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
8944 def.push(dict.encode("Alice", "knows", "Carol").unwrap());
8945 let mut g1 = GraphIndexBuilder::new();
8946 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
8947
8948 let named = vec![("http://ex/g1".to_string(), g1.build())];
8949 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8950 let rete = Rete::open(&bytes).unwrap();
8951
8952 let mut def_objs: Vec<String> = rete
8954 .query_in_graph(None, Some("Alice"), Some("knows"), None)
8955 .into_iter()
8956 .map(|(_, _, o)| o)
8957 .collect();
8958 def_objs.sort();
8959 assert_eq!(def_objs, vec!["Bob".to_string(), "Carol".to_string()]);
8960
8961 assert_eq!(
8963 rete.query_in_graph(Some("http://ex/g1"), Some("Alice"), None, None),
8964 vec![("Alice".into(), "knows".into(), "Dave".into())]
8965 );
8966
8967 assert_eq!(rete.query_in_graph(None, None, None, None).len(), 2);
8969 assert_eq!(
8970 rete.query_in_graph(Some("http://ex/g1"), None, None, None)
8971 .len(),
8972 1
8973 );
8974
8975 assert!(rete
8977 .query_in_graph(Some("http://ex/missing"), None, None, None)
8978 .is_empty());
8979 }
8980
8981 #[test]
8994 fn bounded_query_in_graph_does_not_fault_the_whole_dictionary() {
8995 use crate::reader::{CountingReader, SliceReader};
8996
8997 let payload = |i: u32| -> String {
9003 let mut s = String::with_capacity(800);
9004 let mut x = i as u64 ^ 0x9e37_79b9_7f4a_7c15;
9005 while s.len() < 800 {
9006 x = x
9007 .wrapping_mul(6364136223846793005)
9008 .wrapping_add(1442695040888963407);
9009 s.push_str(&format!("{:016x}", x ^ (x >> 29)));
9010 }
9011 s
9012 };
9013 let quads: Vec<(String, String, String, bool)> = (0..2000u32)
9020 .map(|i| {
9021 let named = i % 2 == 0;
9022 let ns = if named { 'a' } else { 'b' };
9023 (
9024 format!("<http://ex/{ns}/s/{i:04}>"),
9025 "<http://ex/abstract>".to_string(),
9026 format!("\"{ns}{} number {i}\"", payload(i)),
9027 named,
9028 )
9029 })
9030 .collect();
9031 let mut db = DictionaryBuilder::new();
9032 for (s, p, o, _) in &quads {
9033 db.observe(s, p, o);
9034 }
9035 let dict = db.build();
9036 let mut def = GraphIndexBuilder::new().with_tile_budget(256);
9037 let mut g1 = GraphIndexBuilder::new().with_tile_budget(256);
9038 for (s, p, o, named) in &quads {
9039 let t = dict.encode(s, p, o).unwrap();
9040 if *named {
9041 g1.push(t);
9042 } else {
9043 def.push(t);
9044 }
9045 }
9046 let named = vec![("http://ex/g1".to_string(), g1.build())];
9047 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9048
9049 let eager = Rete::open(&bytes).unwrap();
9050 let dict_len = eager.header().dictionary_len;
9051
9052 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
9055 let probe = |f: &dyn Fn(&Rete) -> Vec<TermTriple>| -> (Vec<TermTriple>, u64) {
9056 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9057 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
9058 let before = reader.bytes_read();
9059 let out = f(&lazy);
9060 assert!(!lazy.index_incomplete(), "a chunk fetch failed");
9061 (out, reader.bytes_read() - before)
9062 };
9063
9064 let want = eager.query_in_graph(
9066 Some("http://ex/g1"),
9067 Some("<http://ex/a/s/0100>"),
9068 None,
9069 None,
9070 );
9071 assert_eq!(want.len(), 1, "fixture: expected exactly one match");
9072 let (got, pulled) = probe(&|r| {
9073 r.query_in_graph(
9074 Some("http://ex/g1"),
9075 Some("<http://ex/a/s/0100>"),
9076 None,
9077 None,
9078 )
9079 });
9080 assert_eq!(got, want, "lazy query_in_graph disagrees with eager");
9081 assert!(
9082 pulled * 4 < dict_len,
9083 "a 1-row query_in_graph faulted {pulled} B of a {dict_len} B dictionary — \
9084 it is prefetching the whole thing again"
9085 );
9086
9087 let (empty, q_pulled) =
9095 probe(&|r| r.query_in_graph(Some("http://ex/nope"), None, None, None));
9096 assert!(empty.is_empty());
9097 let (_, d_pulled) = probe(&|r| {
9098 let mut n = 0usize;
9099 r.dump_each(Some("http://ex/nope"), |_, _, _| n += 1);
9100 assert_eq!(n, 0, "an absent graph yielded triples");
9101 Vec::new()
9102 });
9103 assert_eq!(
9104 q_pulled, d_pulled,
9105 "query_in_graph and dump_each disagree on what an absent graph costs"
9106 );
9107 assert!(
9108 q_pulled * 4 < dict_len,
9109 "an absent graph faulted {q_pulled} B of a {dict_len} B dictionary"
9110 );
9111
9112 let want: Vec<TermTriple> = eager.dump(Some("http://ex/g1"));
9115 assert_eq!(want.len(), 1000);
9116 let (got, pulled) = probe(&|r| {
9117 let mut out = Vec::new();
9118 r.dump_each(Some("http://ex/g1"), |s, p, o| {
9119 out.push((s.to_string(), p.to_string(), o.to_string()))
9120 });
9121 out
9122 });
9123 assert_eq!(got, want, "lazy dump_each disagrees with eager dump");
9124 assert!(
9125 pulled * 3 < dict_len * 2,
9126 "dumping half the graph faulted {pulled} B of a {dict_len} B dictionary — \
9127 the other graph's chunks are being pulled in too"
9128 );
9129 }
9130
9131 #[cfg(test)]
9136 fn build_multichunk_image() -> Vec<u8> {
9137 use crate::index::PermSet;
9138 let mut db = DictionaryBuilder::new();
9139 let mut triples: Vec<(String, String, String)> = (0..4000u32)
9140 .map(|i| {
9141 (
9142 format!("<http://ex/s/{i:07}>"),
9143 format!("<http://ex/p/{}>", i % 8),
9144 format!("\"object literal number {i:07} with padding text to widen the term\""),
9145 )
9146 })
9147 .collect();
9148 triples.push((
9149 "<http://ex/s/big>".to_string(),
9150 "<http://ex/p/big>".to_string(),
9151 format!("\"{}\"", "Z".repeat(8000)),
9152 ));
9153 for (s, p, o) in &triples {
9154 db.observe(s, p, o);
9155 }
9156 let dict = db.build();
9157 let mut idx = GraphIndexBuilder::new()
9158 .with_tile_budget(64)
9159 .with_perms(PermSet::ALL);
9160 for (s, p, o) in &triples {
9161 idx.push(dict.encode(s, p, o).unwrap());
9162 }
9163 write_dataset(&dict, &idx.build(), &[], true, &[], 0)
9164 }
9165
9166 #[test]
9175 fn tiny_dict_cache_evicts_yet_dump_is_byte_identical() {
9176 use crate::reader::SliceReader;
9177 let leaked: &'static [u8] = Box::leak(build_multichunk_image().into_boxed_slice());
9178 let dump = |cap: u64| -> (
9179 Vec<(String, String, String)>,
9180 crate::chunk_cache::CacheStats,
9181 ) {
9182 let rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9183 rete.dict.set_cache_cap(cap);
9184 let mut out = Vec::new();
9185 rete.dump_filtered_each(None, None, None, None, |s, p, o| {
9186 out.push((s.to_string(), p.to_string(), o.to_string()))
9187 });
9188 assert!(
9189 !rete.index_incomplete(),
9190 "no lazy fault should fail at cap {cap}"
9191 );
9192 let stats = rete.dict_cache_stats();
9193 (out, stats)
9194 };
9195
9196 let (unlimited, us) = dump(u64::MAX);
9197 assert_eq!(unlimited.len(), 4001, "every triple dumped");
9198 assert_eq!(us.evictions, 0, "unlimited cap evicts nothing");
9199
9200 let (tiny, ts) = dump(128 * 1024);
9203 assert_eq!(tiny, unlimited, "128 KiB-cap dump differs from unlimited");
9204 assert!(ts.evictions > 0, "a tiny cap must evict (stats {ts:?})");
9205
9206 let (starved, ss) = dump(1);
9208 assert_eq!(starved, unlimited, "1-byte-cap dump differs from unlimited");
9209 assert!(
9210 ss.evictions > 0,
9211 "1-byte cap evicts every insert (stats {ss:?})"
9212 );
9213 }
9214
9215 #[test]
9226 fn memory_budget_charges_the_fixed_directories() {
9227 use crate::reader::SliceReader;
9228 let leaked: &'static [u8] = Box::leak(build_multichunk_image().into_boxed_slice());
9229 let rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9230
9231 let fixed = rete.fixed_overhead();
9232 assert!(
9235 fixed.dict_directory > 0 && fixed.index_directory > 0,
9236 "{fixed:?}"
9237 );
9238 assert_eq!(fixed.named_graphs, 0, "no named graphs in this file");
9239 assert_eq!(
9240 fixed.total(),
9241 fixed.dict_directory + fixed.index_directory + fixed.named_graphs
9242 );
9243
9244 let total: u64 = 512 << 20;
9247 let applied = rete.set_memory_budget(Some(total));
9248 let plain = split_memory_budget(total);
9249 assert_eq!(applied.dict_cache, plain.dict_cache - fixed.total());
9250 assert_eq!(rete.dict_cache_cap(), applied.dict_cache);
9251 assert_eq!(applied.tile_cache, plain.tile_cache);
9252 assert_eq!(applied.block_cache, plain.block_cache);
9253 assert!(
9254 applied.dict_cache + applied.tile_cache + applied.block_cache + fixed.total() <= total,
9255 "caps + fixed directories exceed the budget: {applied:?} + {fixed:?} > {total}"
9256 );
9257
9258 let applied = rete.set_memory_budget(Some(1 << 20));
9260 assert_eq!(applied.dict_cache, MIN_DICT_CACHE_CAP);
9261
9262 let applied = rete.set_memory_budget(None);
9264 assert_eq!(applied.dict_cache, u64::MAX);
9265 assert_eq!(rete.dict_cache_cap(), u64::MAX);
9266
9267 let rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9270 rete.dict.set_cache_cap(128 * 1024);
9271 let mut rows = 0usize;
9272 rete.dump_filtered_each(None, None, None, None, |_, _, _| rows += 1);
9273 assert_eq!(rows, 4001);
9274 let s = rete.dict_cache_stats();
9275 assert!(s.evictions > 0, "the cap must bind: {s:?}");
9276 assert!(
9279 s.entries <= 1 || s.resident_bytes <= 128 * 1024,
9280 "dictionary cache over its cap after the dump: {s:?}"
9281 );
9282 }
9283
9284 #[test]
9289 fn releasing_a_named_graph_drops_and_reopens_its_index() {
9290 use crate::index::PermSet;
9291 use crate::reader::SliceReader;
9292 let mut db = DictionaryBuilder::new();
9293 let all: Vec<(String, String, String)> = (0..40u32)
9294 .map(|i| {
9295 (
9296 format!("<http://ex/s{}>", i % 7),
9297 format!("<http://ex/p{}>", i % 3),
9298 format!("<http://ex/o{}>", i % 5),
9299 )
9300 })
9301 .collect();
9302 for (s, p, o) in &all {
9303 db.observe(s, p, o);
9304 }
9305 let dict = db.build();
9306 let mk = |take: fn(u32) -> bool| {
9307 let mut g = GraphIndexBuilder::new()
9308 .with_tile_budget(16)
9309 .with_perms(PermSet::ALL);
9310 for (i, (s, p, o)) in all.iter().enumerate() {
9311 if take(i as u32) {
9312 g.push(dict.encode(s, p, o).unwrap());
9313 }
9314 }
9315 g.build()
9316 };
9317 let def = mk(|_| true);
9318 let g1 = mk(|i| i % 2 == 0);
9319 let g2 = mk(|i| i % 3 == 0);
9320 let image = write_dataset(
9321 &dict,
9322 &def,
9323 &[
9324 ("<http://ex/g1>".to_string(), g1),
9325 ("<http://ex/g2>".to_string(), g2),
9326 ],
9327 true,
9328 &[],
9329 0,
9330 );
9331 let leaked: &'static [u8] = Box::leak(image.into_boxed_slice());
9332 let mut rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9333
9334 let opened = |r: &Rete| -> usize {
9335 match &r.named_graphs {
9336 NamedGraphsSlot::Lazy(l) => {
9337 let mut n = 0;
9338 l.for_each_opened(|_| n += 1);
9339 n
9340 }
9341 _ => 0,
9342 }
9343 };
9344
9345 assert_eq!(opened(&rete), 0, "nothing opened before first touch");
9346 assert!(rete.graph_index("<http://ex/g1>").is_some());
9348 assert_eq!(opened(&rete), 1, "g1 opened");
9349 rete.release_named_graph("<http://ex/g1>");
9351 assert_eq!(opened(&rete), 0, "g1 released");
9352 rete.release_named_graph("<http://ex/absent>");
9354 rete.release_named_graph("<http://ex/g1>");
9355 assert_eq!(opened(&rete), 0);
9356 assert!(rete.graph_index("<http://ex/g1>").is_some());
9358 assert_eq!(opened(&rete), 1, "g1 re-opened after release");
9359 }
9360
9361 #[test]
9379 fn filtered_dump_is_exactly_the_unfiltered_dump_filtered() {
9380 use crate::index::PermSet;
9381 use crate::reader::SliceReader;
9382
9383 let terms = |i: u32| {
9384 (
9385 format!("<http://ex/s{}>", i % 11),
9386 format!("<http://ex/p{}>", i % 3),
9387 if i.is_multiple_of(4) {
9391 format!("\"lit {}\"@en", i % 5)
9392 } else {
9393 format!("<http://ex/o{}>", i % 7)
9394 },
9395 )
9396 };
9397 let build = |perms: PermSet| {
9398 let mut db = DictionaryBuilder::new();
9399 let all: Vec<_> = (0..90u32).map(terms).collect();
9400 for (s, p, o) in &all {
9401 db.observe(s, p, o);
9402 }
9403 let dict = db.build();
9404 let mut def = GraphIndexBuilder::new()
9405 .with_tile_budget(16)
9406 .with_perms(perms);
9407 let mut g1 = GraphIndexBuilder::new()
9408 .with_tile_budget(16)
9409 .with_perms(perms);
9410 for (i, (s, p, o)) in all.iter().enumerate() {
9411 let t = dict.encode(s, p, o).unwrap();
9412 if i % 2 == 0 {
9416 g1.push(t);
9417 }
9418 def.push(t);
9419 }
9420 write_dataset(
9421 &dict,
9422 &def.build(),
9423 &[("<http://ex/g1>".to_string(), g1.build())],
9424 true,
9425 &[],
9426 0,
9427 )
9428 };
9429
9430 let shapes: Vec<(Option<&str>, Option<&str>, Option<&str>)> = vec![
9431 (None, None, None),
9432 (Some("<http://ex/s3>"), None, None),
9433 (None, Some("<http://ex/p1>"), None),
9434 (None, None, Some("<http://ex/o5>")),
9435 (None, None, Some("\"lit 0\"@en")),
9436 (Some("<http://ex/s3>"), Some("<http://ex/p0>"), None),
9437 (Some("<http://ex/s4>"), None, Some("<http://ex/o4>")),
9438 (None, Some("<http://ex/p1>"), Some("<http://ex/o5>")),
9439 (
9440 Some("<http://ex/s3>"),
9441 Some("<http://ex/p0>"),
9442 Some("<http://ex/o3>"),
9443 ),
9444 (Some("<http://ex/nope>"), None, None),
9448 (None, Some("<http://ex/nope>"), None),
9449 (None, None, Some("<http://ex/nope>")),
9450 ];
9451
9452 let mut any_pruned = false;
9453 for perms in [PermSet::ALL, PermSet::CORE] {
9454 let image = build(perms);
9455 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
9456 let opens: Vec<(&str, Rete)> = vec![
9457 ("resident", Rete::open(&image).unwrap()),
9458 (
9459 "ranged",
9460 Rete::open_ranged(&SliceReader::new(leaked)).unwrap(),
9461 ),
9462 (
9463 "lazy",
9464 Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap(),
9465 ),
9466 ];
9467 for graph in [None, Some("<http://ex/g1>"), Some("<http://ex/absent>")] {
9468 let full: Vec<TermTriple> = {
9470 let mut v = Vec::new();
9471 Rete::open(&image).unwrap().dump_each(graph, |s, p, o| {
9472 v.push((s.to_string(), p.to_string(), o.to_string()))
9473 });
9474 v.sort();
9475 v
9476 };
9477 for &(s, p, o) in &shapes {
9478 let mut want = full.clone();
9479 want.retain(|(ts, tp, to)| {
9480 s.is_none_or(|x| x == ts)
9481 && p.is_none_or(|x| x == tp)
9482 && o.is_none_or(|x| x == to)
9483 });
9484 for (path, rete) in &opens {
9485 let mut got = Vec::new();
9486 rete.dump_filtered_each(graph, s, p, o, |s, p, o| {
9487 got.push((s.to_string(), p.to_string(), o.to_string()))
9488 });
9489 got.sort();
9490 assert_eq!(
9491 got,
9492 want,
9493 "{path}/{perms:?} graph={graph:?} pattern={:?} — filtered dump \
9494 is not the unfiltered dump filtered",
9495 (s, p, o)
9496 );
9497 assert!(!rete.index_incomplete(), "{path}: a fetch failed");
9498 }
9499 let plan = opens[2].1.dump_plan(graph, s, p, o);
9503 if graph == Some("<http://ex/absent>")
9504 || [s, p, o].contains(&Some("<http://ex/nope>"))
9505 {
9506 assert!(
9507 plan.scan.is_none(),
9508 "an unmatchable dump still planned a scan: {:?}",
9509 (graph, s, p, o)
9510 );
9511 } else {
9512 let scan = plan.scan.expect("a matchable dump plans a scan");
9513 assert!(
9514 PermSet::CORE.contains(scan.permutation),
9515 "a dump routed to {:?}, outside PermSet::CORE — a \
9516 --permutations 3 file has no such section",
9517 scan.permutation
9518 );
9519 assert!(scan.tiles_admitted <= scan.tiles_routed);
9520 assert!(scan.tiles_routed <= scan.tiles_total);
9521 assert!(scan.tile_bytes <= scan.section_bytes);
9522 if scan.tiles_admitted < scan.tiles_total {
9523 any_pruned = true;
9524 }
9525 }
9526 }
9527 }
9528 }
9529 assert!(
9530 any_pruned,
9531 "no shape pruned a single tile — the fixture is too small to be \
9532 measuring anything"
9533 );
9534 }
9535
9536 #[test]
9546 fn a_predicate_scoped_dump_fetches_less_than_the_graph() {
9547 use crate::reader::{CountingReader, SliceReader};
9548
9549 let mut db = DictionaryBuilder::new();
9555 let mut all = Vec::new();
9556 for s in 0..200u32 {
9557 for p in 0..20u32 {
9558 let t = (
9559 format!("<http://ex/s/{s:04}>"),
9560 format!("<http://ex/p/{p:02}>"),
9561 format!("\"object {s:04}/{p:02}\""),
9562 );
9563 db.observe(&t.0, &t.1, &t.2);
9564 all.push(t);
9565 }
9566 }
9567 let dict = db.build();
9568 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
9569 for (s, p, o) in &all {
9570 ib.push(dict.encode(s, p, o).unwrap());
9571 }
9572 let image = write_dataset(&dict, &ib.build(), &[], false, &[], 0);
9573 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
9574
9575 let probe = |s: Option<&str>, p: Option<&str>, o: Option<&str>| -> (usize, u64) {
9577 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9578 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
9579 let before = reader.bytes_read();
9580 let mut n = 0usize;
9581 lazy.dump_filtered_each(None, s, p, o, |_, _, _| n += 1);
9582 assert!(!lazy.index_incomplete());
9583 (n, reader.bytes_read() - before)
9584 };
9585
9586 let (full_rows, full_bytes) = probe(None, None, None);
9587 let (pred_rows, pred_bytes) = probe(None, Some("<http://ex/p/07>"), None);
9588 let (subj_rows, subj_bytes) = probe(Some("<http://ex/s/0021>"), None, None);
9589 assert_eq!(full_rows, 4000);
9590 assert_eq!(pred_rows, 200, "one predicate covers every subject once");
9591 assert_eq!(subj_rows, 20);
9592 assert!(
9594 pred_bytes < full_bytes && subj_bytes < full_bytes,
9595 "a slice fetched {pred_bytes} / {subj_bytes} B where the whole graph fetched \
9596 {full_bytes} B — the scan is not being pruned at all"
9597 );
9598 #[cfg(feature = "compression")]
9609 assert!(
9610 pred_bytes * 4 < full_bytes && subj_bytes * 4 < full_bytes,
9611 "a 1-in-20 predicate slice fetched {pred_bytes} B and one subject \
9612 {subj_bytes} B where the whole graph fetched {full_bytes} B"
9613 );
9614
9615 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9617 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
9618 let before = reader.bytes_read();
9619 let plan = lazy
9620 .dump_plan(None, None, Some("<http://ex/p/07>"), None)
9621 .scan
9622 .expect("a known predicate plans a scan");
9623 assert!(
9624 reader.bytes_read() - before < 64 * 1024,
9625 "planning fetched {} B — it is supposed to read directories, not tiles",
9626 reader.bytes_read() - before
9627 );
9628 assert!(plan.tiles_admitted < plan.tiles_total);
9629 assert!(
9635 plan.tile_bytes * 5 < plan.section_bytes,
9636 "one predicate of twenty planned {} B of a {} B section",
9637 plan.tile_bytes,
9638 plan.section_bytes
9639 );
9640 }
9641
9642 #[test]
9643 fn query_quads_tags_every_graph() {
9644 let mut db = DictionaryBuilder::new();
9645 for (s, p, o) in [("Alice", "knows", "Bob"), ("Alice", "knows", "Dave")] {
9646 db.observe(s, p, o);
9647 }
9648 let dict = db.build();
9649 let mut def = GraphIndexBuilder::new();
9650 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
9651 let mut g1 = GraphIndexBuilder::new();
9652 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
9653 let named = vec![("http://ex/g1".to_string(), g1.build())];
9654 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9655 let rete = Rete::open(&bytes).unwrap();
9656
9657 let quads = rete.query_quads(Some("Alice"), Some("knows"), None);
9659 assert_eq!(quads.len(), 2);
9660 assert_eq!(
9661 quads[0],
9662 (("Alice".into(), "knows".into(), "Bob".into()), None)
9663 );
9664 assert_eq!(
9665 quads[1],
9666 (
9667 ("Alice".into(), "knows".into(), "Dave".into()),
9668 Some("http://ex/g1".to_string())
9669 )
9670 );
9671
9672 assert!(rete.query_quads(Some("Nobody"), None, None).is_empty());
9674 }
9675
9676 #[test]
9677 fn pyramid_meta_round_trips_in_file() {
9678 let rete = Rete::open(&build_image()).unwrap();
9679 let pyr = rete.pyramid().expect("file has a pyramid");
9680 let total: u32 = pyr.summary.iter().map(|e| e.count).sum();
9682 assert_eq!(total, 3);
9683 assert!(!pyr.summary.is_empty());
9684 assert!(pyr.tiles.is_empty());
9685 }
9686
9687 #[test]
9688 fn schema_pyramid_round_trips_through_file_index_free() {
9689 use crate::reader::{CountingReader, SliceReader};
9690 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
9691 let q = |s: &str, p: &str, o: &str| {
9692 (s.to_string(), p.to_string(), o.to_string(), None::<String>)
9693 };
9694 let quads = vec![
9696 q("<a>", RDF_TYPE, "<Astronomer>"),
9697 q("<b>", RDF_TYPE, "<Astronomer>"),
9698 q("<c>", RDF_TYPE, "<Person>"),
9699 q("<Astronomer>", sub, "<Scientist>"),
9700 q("<Scientist>", sub, "<Person>"),
9701 q("<Person>", sub, "<Agent>"),
9702 q("<a>", "<knows>", "<b>"),
9703 q("<b>", "<knows>", "<c>"),
9704 ];
9705 let (bytes, _) =
9706 crate::ingest::assemble_dataset_with_opts(quads, true, false, None, |_, _| Vec::new());
9707
9708 let rete = Rete::open(&bytes).unwrap();
9710 let pyr = rete.pyramid().expect("pyramid present");
9711 assert!(!pyr.level_rollups.is_empty(), "schema pyramid shipped");
9712 assert!(pyr
9713 .class_hierarchy
9714 .iter()
9715 .any(|n| n.class == "<Agent>" && n.depth == 0));
9716
9717 let r = CountingReader::new(SliceReader::new(&bytes));
9719 let view = SummaryView::open_ranged(&r).unwrap().unwrap();
9720 assert!(view.level_count() >= 2, "multi-level pyramid");
9721 let coarse = view.level_rollup(0).unwrap();
9722 assert!(
9723 coarse.classes.iter().any(|(c, _)| c == "<Agent>"),
9724 "coarsest level rolls up to the root Agent"
9725 );
9726 let h = Header::from_bytes(&bytes).unwrap();
9727 assert!(
9728 r.bytes_read() <= bytes.len() as u64 - h.root_dir_len,
9729 "summary read {} bytes; the {}-byte index section must be skipped",
9730 r.bytes_read(),
9731 h.root_dir_len
9732 );
9733 }
9734
9735 #[test]
9736 fn predicate_totals_from_summary_only() {
9737 use crate::reader::SliceReader;
9738 let bytes = build_image();
9740 let reader = SliceReader::new(&bytes);
9741 let view = SummaryView::open_ranged(&reader).unwrap().unwrap();
9742 assert_eq!(view.predicate_total("knows"), 2);
9743 assert_eq!(view.predicate_total("age"), 1);
9744 assert_eq!(view.predicate_total("missing"), 0);
9745 let totals = view.predicate_totals();
9746 assert_eq!(totals[0], ("knows".to_string(), 2)); }
9748
9749 #[test]
9750 fn query_patterns_resolve_to_terms() {
9751 let rete = Rete::open(&build_image()).unwrap();
9752
9753 assert_eq!(rete.query(None, None, None).len(), 3);
9755
9756 let mut alice = rete.query(Some("Alice"), None, None);
9758 alice.sort();
9759 assert_eq!(
9760 alice,
9761 vec![
9762 ("Alice".into(), "age".into(), "30".into()),
9763 ("Alice".into(), "knows".into(), "Bob".into()),
9764 ]
9765 );
9766
9767 assert_eq!(rete.query(None, Some("knows"), None).len(), 2);
9769
9770 assert_eq!(
9772 rete.query(Some("Bob"), Some("knows"), Some("Carol")),
9773 vec![("Bob".into(), "knows".into(), "Carol".into())]
9774 );
9775 assert!(rete.query(Some("Nobody"), None, None).is_empty());
9776 assert!(rete.query(None, Some("likes"), None).is_empty());
9777 }
9778
9779 #[test]
9780 fn query_provenance_reports_terms_ids_sections_and_index_choice() {
9781 let bytes = build_image();
9782 let rete = Rete::open(&bytes).unwrap();
9783
9784 let mut matches = rete.query_with_provenance(None, Some("knows"), None);
9785 matches.sort_by(|a, b| a.terms.cmp(&b.terms));
9786
9787 assert_eq!(matches.len(), 2);
9788 assert_eq!(
9789 matches[0].terms,
9790 ("Alice".into(), "knows".into(), "Bob".into())
9791 );
9792 assert_eq!(
9793 matches[0].ids,
9794 rete.dictionary().encode("Alice", "knows", "Bob").unwrap()
9795 );
9796 assert_eq!(matches[0].graph.as_deref(), None);
9797 assert_eq!(
9798 matches[0].matched_pattern,
9799 (None, Some(matches[0].ids.1), None)
9800 );
9801 assert_eq!(
9802 matches[0].index_permutation,
9803 crate::index::IndexPermutation::Pos
9804 );
9805
9806 let h = rete.header();
9807 assert_eq!(matches[0].dictionary_range.offset, h.dictionary_offset);
9808 assert_eq!(matches[0].dictionary_range.len, h.dictionary_len);
9809 assert_eq!(matches[0].index_range.offset, h.root_dir_offset);
9810 assert_eq!(matches[0].index_range.len, h.root_dir_len);
9811 assert!(
9812 matches[0].index_section_range.offset > h.root_dir_offset,
9813 "POS is section 1, so its payload starts after the container header and SPO payload"
9814 );
9815 assert!(matches[0].index_section_range.len > 0);
9816 assert!(matches[0].index_section_range.end() <= matches[0].index_range.end());
9817 assert!(matches[0].index_section_range.len < matches[0].index_range.len);
9818 assert_eq!(
9819 matches[0].pyramid_range.as_ref().map(|r| (r.offset, r.len)),
9820 Some((h.pyramid_meta_offset, h.pyramid_meta_len))
9821 );
9822 let tile_range = matches[0].tile_range.expect("tiled file reports a tile");
9825 assert!(matches[0]
9826 .tile
9827 .as_deref()
9828 .unwrap()
9829 .starts_with(matches[0].index_permutation.name()));
9830 assert!(matches[0].index_section_range.offset <= tile_range.offset);
9831 assert!(tile_range.end() <= matches[0].index_section_range.end());
9832 }
9833
9834 fn build_labeled(n: usize) -> Vec<u8> {
9838 const LABEL: &str = "<http://www.w3.org/2000/01/rdf-schema#label>";
9839 const WORDS: &[&str] = &[
9840 "alanine",
9841 "benzene",
9842 "glucose",
9843 "dextrose",
9844 "ethanol",
9845 "formate",
9846 "heptane",
9847 "isoleucine",
9848 ];
9849 let triples: Vec<(String, String, String)> = (0..n)
9850 .flat_map(|i| {
9851 let s = format!("<http://ex/e{i}>");
9852 let w = WORDS[i % WORDS.len()];
9853 [
9854 (s.clone(), LABEL.to_string(), format!("\"{w}-{i:06}\"")),
9855 (
9856 s,
9857 "<http://ex/p>".to_string(),
9858 format!("<http://ex/c{}>", i % 64),
9859 ),
9860 ]
9861 })
9862 .collect();
9863 let mut db = DictionaryBuilder::new();
9864 for (s, p, o) in &triples {
9865 db.observe(s, p, o);
9866 }
9867 let dict = db.build();
9868 let ids: Vec<(u32, u32, u32)> = triples
9869 .iter()
9870 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
9871 .collect();
9872 let mut ib = GraphIndexBuilder::new();
9873 for &t in &ids {
9874 ib.push(t);
9875 }
9876 let (meta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
9877 write_file(&dict, &ib.build(), false, &meta, levels)
9878 }
9879
9880 #[test]
9881 fn prefix_search_matches_a_filter_scan() {
9882 let bytes = build_labeled(800);
9885 let rete = Rete::open(&bytes).unwrap();
9886 let idx_subjects: std::collections::BTreeSet<String> = rete
9887 .prefix_search("glucose", 10_000)
9888 .into_iter()
9889 .map(|(_label, subject)| subject)
9890 .collect();
9891 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
9893 FILTER(STRSTARTS(LCASE(?l), \"glucose\")) }";
9894 let crate::QueryOutput::Select(_, rows) = crate::eval_query(&rete, q).unwrap() else {
9895 panic!("expected SELECT");
9896 };
9897 let scan_subjects: std::collections::BTreeSet<String> =
9898 rows.iter().map(|r| r.get("s").cloned().unwrap()).collect();
9899 assert_eq!(idx_subjects, scan_subjects, "index agrees with the scan");
9900 assert_eq!(
9901 idx_subjects.len(),
9902 100,
9903 "800/8 words = 100 glucose-* labels"
9904 );
9905 }
9906
9907 #[test]
9911 fn legacy_label_hits_resolve_in_one_batched_read() {
9912 use std::sync::{Arc, Mutex};
9913 type CallLog = Arc<Mutex<Vec<Vec<(u64, u64)>>>>;
9914 #[derive(Clone)]
9915 struct Calls(Arc<Vec<u8>>, CallLog);
9916 impl crate::RangeReader for Calls {
9917 fn len(&self) -> u64 {
9918 self.0.len() as u64
9919 }
9920 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
9921 self.1.lock().unwrap().push(vec![(offset, len)]);
9922 crate::SliceReader::new(&self.0).read_at(offset, len)
9923 }
9924 fn read_many(&self, ranges: &[(u64, u64)]) -> std::io::Result<Vec<Vec<u8>>> {
9925 self.1.lock().unwrap().push(ranges.to_vec());
9926 let r = crate::SliceReader::new(&self.0);
9927 ranges.iter().map(|&(o, l)| r.read_at(o, l)).collect()
9928 }
9929 }
9930 let bytes = build_labeled(20_000);
9931 let h = Header::from_bytes(&bytes).unwrap();
9932 let (d0, dl) = (h.dictionary_offset, h.dictionary_len);
9933 let want = Rete::open(&bytes).unwrap().prefix_search("glucose", 1000);
9934 assert_eq!(want.len(), 1000);
9935 for search_view in [false, true] {
9936 let calls = Calls(Arc::new(bytes.clone()), Arc::new(Mutex::new(Vec::new())));
9937 let got = if search_view {
9938 let view = SearchView::open_ranged(calls.clone()).unwrap();
9939 calls.1.lock().unwrap().clear();
9940 view.prefix_search("glucose", 1000)
9941 } else {
9942 let rete = Rete::open_ranged_lazy(calls.clone()).unwrap();
9943 calls.1.lock().unwrap().clear();
9944 rete.prefix_search("glucose", 1000)
9945 };
9946 assert_eq!(got, want, "search view = {search_view}");
9947 let dict_reads = calls
9948 .1
9949 .lock()
9950 .unwrap()
9951 .iter()
9952 .filter(|c| c.iter().any(|&(o, l)| o < d0 + dl && d0 < o + l))
9953 .count();
9954 assert_eq!(dict_reads, 1, "search view = {search_view}");
9955 }
9956 }
9957
9958 #[test]
9962 #[ignore]
9963 fn bench_prefix_search_vs_filter_scan() {
9964 use std::time::Instant;
9965 let n = 6000; let bytes = build_labeled(n);
9967 let rete = Rete::open(&bytes).unwrap();
9968 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
9969 FILTER(STRSTARTS(LCASE(?l), \"glucose\")) }";
9970 let reps = 200;
9971 let idx_n = rete.prefix_search("glucose", 100_000).len();
9972 let t = Instant::now();
9973 for _ in 0..reps {
9974 std::hint::black_box(rete.prefix_search("glucose", 100_000));
9975 }
9976 let idx_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
9977 let t = Instant::now();
9978 for _ in 0..reps {
9979 let _ = std::hint::black_box(crate::eval_query(&rete, q).unwrap());
9980 }
9981 let scan_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
9982 println!(
9983 "label prefix search over {n} labeled subjects ({idx_n} matches): \
9984 index {idx_ms:.4} ms vs FILTER scan {scan_ms:.3} ms ({:.0}× faster)",
9985 scan_ms / idx_ms
9986 );
9987 }
9988
9989 #[test]
9993 #[ignore]
9994 fn bench_text_search_vs_contains_scan() {
9995 use std::time::Instant;
9996 const LABEL: &str = "<http://www.w3.org/2000/01/rdf-schema#label>";
9997 const WORDS: &[&str] = &[
9998 "alanine",
9999 "benzene",
10000 "glucose",
10001 "dextrose",
10002 "ethanol",
10003 "formate",
10004 "heptane",
10005 "isoleucine",
10006 ];
10007 let n = 6000;
10008 let triples: Vec<(String, String, String)> = (0..n)
10009 .map(|i| {
10010 (
10011 format!("<http://ex/e{i}>"),
10012 LABEL.to_string(),
10013 format!("\"{} sample number {i:06}\"", WORDS[i % WORDS.len()]),
10014 )
10015 })
10016 .collect();
10017 let mut db = DictionaryBuilder::new();
10018 for (s, p, o) in &triples {
10019 db.observe(s, p, o);
10020 }
10021 let dict = db.build();
10022 let ids: Vec<(u32, u32, u32)> = triples
10023 .iter()
10024 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
10025 .collect();
10026 let mut ib = GraphIndexBuilder::new();
10027 for &t in &ids {
10028 ib.push(t);
10029 }
10030 let ti = compute_text_index(&dict, &ids);
10031 let bytes = write_dataset_with_metadata(&dict, &ib.build(), &[], false, &[], 0, &[], &ti);
10032 let rete = Rete::open(&bytes).unwrap();
10033
10034 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
10035 FILTER(CONTAINS(LCASE(?l), \"glucose\")) }";
10036 let reps = 200;
10037 let idx_n = rete.text_search(&["glucose"], None, 100_000).len();
10038 let t = Instant::now();
10039 for _ in 0..reps {
10040 std::hint::black_box(rete.text_search(&["glucose"], None, 100_000));
10041 }
10042 let idx_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
10043 let t = Instant::now();
10044 for _ in 0..reps {
10045 let _ = std::hint::black_box(crate::eval_query(&rete, q).unwrap());
10046 }
10047 let scan_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
10048 println!(
10049 "text search over {n} literals ({idx_n} matches): \
10050 index {idx_ms:.4} ms vs FILTER(CONTAINS) scan {scan_ms:.3} ms ({:.0}× faster)",
10051 scan_ms / idx_ms
10052 );
10053 }
10054
10055 #[test]
10059 #[ignore = "operational tool, driven by RETE_DEBUG_* env vars"]
10060 fn debug_bound_po_routing() {
10061 struct FR(std::fs::File);
10062 impl crate::RangeReader for FR {
10063 fn len(&self) -> u64 {
10064 self.0.metadata().map(|m| m.len()).unwrap_or(0)
10065 }
10066 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
10067 use std::os::unix::fs::FileExt;
10068 let mut buf = vec![0u8; len as usize];
10069 self.0.read_exact_at(&mut buf, offset)?;
10070 Ok(buf)
10071 }
10072 }
10073 let path = std::env::var("RETE_DEBUG_FILE").expect("RETE_DEBUG_FILE");
10074 let p_iri = std::env::var("RETE_DEBUG_P").expect("RETE_DEBUG_P");
10075 let o_iri = std::env::var("RETE_DEBUG_O").expect("RETE_DEBUG_O");
10076 let rete =
10077 Rete::open_ranged_lazy(std::sync::Arc::new(FR(std::fs::File::open(&path).unwrap())))
10078 .unwrap();
10079 let pid = rete.dict.predicate_id(&p_iri).expect("p resolves");
10080 let oid = rete.dict.object_id(&o_iri).expect("o resolves");
10081 eprintln!("pid={pid} oid={oid}");
10082 let pattern = (None, Some(pid), Some(oid));
10083 let perm = GraphIndex::best_permutation(pattern);
10084 eprintln!("best_permutation = {}", perm.name());
10085 let si = perm.section_index();
10086 let tiles = &rete.index.sections[si];
10087 eprintln!("section {} tiles = {}", perm.name(), tiles.len());
10088 let [pa, pb, pc] = perm.order_pattern(pattern);
10089 eprintln!("permuted pattern pa={pa:?} pb={pb:?} pc={pc:?}");
10090 let (start, end) = rete.index.tile_span(si, pa);
10091 eprintln!("tile_span = [{start}, {end}) -> {} tiles", end - start);
10092 let mut admitted = 0usize;
10093 for (ti, t) in tiles.iter().enumerate().take(end).skip(start) {
10094 if t.syn_admits(pb, pc) {
10095 admitted += 1;
10096 if admitted <= 10 {
10097 let (lo, hi) = t.leading_range();
10098 eprintln!(" admit tile {ti}: a=[{lo},{hi}] syn={:?}", t.syn);
10099 }
10100 }
10101 }
10102 eprintln!("admitted {admitted} tile(s) by synopsis");
10103 let n = rete.index.scan_iter(pattern).count();
10104 eprintln!("scan_iter matches = {n}");
10105 let hi_res = rete.query(None, Some(&p_iri), Some(&o_iri));
10106 eprintln!("high-level query matches = {}", hi_res.len());
10107 }
10108
10109 #[test]
10117 fn layout_header_label_matches_its_span() {
10118 let mut db = DictionaryBuilder::new();
10119 let mut triples = Vec::new();
10120 for i in 0..24u32 {
10121 let (s, p, o) = (
10122 format!("<http://ex/s{}>", i % 5),
10123 format!("<http://ex/p{}>", i % 3),
10124 format!("<http://ex/o{i}>"),
10125 );
10126 db.observe(&s, &p, &o);
10127 triples.push((s, p, o));
10128 }
10129 let dict = db.build();
10130 let ids: Vec<(u32, u32, u32)> = triples
10131 .iter()
10132 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
10133 .collect();
10134 let index = GraphIndexBuilder::from_triples(ids).build();
10135 let image = write_dataset_with_metadata(
10136 &dict,
10137 &index,
10138 &[],
10139 false,
10140 &[],
10141 0,
10142 br#"{"name":"layout-label-fixture"}"#,
10143 &[],
10144 );
10145 let rete = Rete::open(&image).unwrap();
10146
10147 let layout = rete.file_layout();
10148 let header = layout
10149 .iter()
10150 .find(|s| s.kind == "header")
10151 .expect("every layout starts with a header segment");
10152
10153 assert_eq!(
10154 header.offset, 0,
10155 "the header is the first thing in the file"
10156 );
10157 assert_eq!(
10158 header.len, HEADER_LEN as u64,
10159 "the header segment must span exactly HEADER_LEN"
10160 );
10161
10162 let digits: String = header
10165 .label
10166 .chars()
10167 .skip_while(|c| !c.is_ascii_digit())
10168 .take_while(|c| c.is_ascii_digit())
10169 .collect();
10170 assert!(
10171 !digits.is_empty(),
10172 "header label {:?} states no size at all",
10173 header.label
10174 );
10175 assert_eq!(
10176 digits.parse::<u64>().unwrap(),
10177 header.len,
10178 "header label {:?} disagrees with the {}-byte span it describes",
10179 header.label,
10180 header.len
10181 );
10182
10183 assert!(
10186 layout.windows(2).all(|w| w[0].offset <= w[1].offset),
10187 "file_layout must return segments sorted by offset"
10188 );
10189 }
10190}