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;
573
574pub fn spill_threshold_for(budget: u64) -> u64 {
578 budget / 8
579}
580
581const MIN_DICT_CACHE_CAP: u64 = 64 << 20; const TRANSIENT_RESERVE_MAX: u64 = 128 << 20; #[derive(Debug, Clone, Copy)]
602pub struct MemoryBudget {
603 pub block_cache: u64,
605 pub dict_cache: u64,
607 pub tile_cache: u64,
609}
610
611pub fn split_memory_budget(total: u64) -> MemoryBudget {
622 if total == u64::MAX {
623 return MemoryBudget {
624 block_cache: u64::MAX,
625 dict_cache: u64::MAX,
626 tile_cache: u64::MAX,
627 };
628 }
629 let block_cache = (total / 8).min(crate::block_cache::DEFAULT_CACHE_CAP);
632 let tile_cache = (total / 16).min(256 << 20);
635 let reserve = (total / 32).min(TRANSIENT_RESERVE_MAX);
641 let dict_cache = total
644 .saturating_sub(block_cache)
645 .saturating_sub(tile_cache)
646 .saturating_sub(reserve)
647 .max(MIN_DICT_CACHE_CAP);
648 MemoryBudget {
649 block_cache,
650 dict_cache,
651 tile_cache,
652 }
653}
654
655#[derive(Debug, Clone, Copy, Default)]
658pub struct FixedOverhead {
659 pub dict_directory: u64,
661 pub index_directory: u64,
663 pub named_graphs: u64,
665}
666
667impl FixedOverhead {
668 pub fn total(&self) -> u64 {
670 self.dict_directory + self.index_directory + self.named_graphs
671 }
672}
673
674fn fmt_cap(cap: u64) -> String {
676 if cap == u64::MAX {
677 "unlimited".to_string()
678 } else {
679 format!("{}MiB", cap >> 20)
680 }
681}
682
683fn read_coalesced<R: RangeReader + ?Sized>(
689 reader: &R,
690 ranges: &[ByteRange],
691 gap: u64,
692) -> Option<Vec<Vec<u8>>> {
693 let mut spans: Vec<(u64, u64)> = Vec::new();
696 let mut span_of: Vec<usize> = Vec::with_capacity(ranges.len());
697 let mut i = 0;
698 while i < ranges.len() {
699 let start = ranges[i].offset;
700 let mut end = ranges[i].offset.checked_add(ranges[i].len)?;
701 let mut j = i + 1;
702 while j < ranges.len() {
703 let r = &ranges[j];
704 if r.offset < end || r.offset - end > gap {
705 break;
706 }
707 end = r.offset.checked_add(r.len)?;
708 j += 1;
709 }
710 let si = spans.len();
711 spans.push((start, end - start));
712 for _ in i..j {
713 span_of.push(si);
714 }
715 i = j;
716 }
717 let blobs = reader.read_many(&spans).ok()?;
718 if blobs.len() != spans.len() {
719 return None;
720 }
721 let mut out = Vec::with_capacity(ranges.len());
722 for (k, r) in ranges.iter().enumerate() {
723 let (span_start, _) = spans[span_of[k]];
724 let blob = &blobs[span_of[k]];
725 let lo = (r.offset - span_start) as usize;
726 let hi = lo.checked_add(r.len as usize)?;
727 out.push(blob.get(lo..hi)?.to_vec());
728 }
729 Some(out)
730}
731
732fn content_hash(parts: &[&[u8]]) -> [u8; 16] {
735 let mut h = blake3::Hasher::new();
736 for p in parts {
737 h.update(p);
738 }
739 let mut out = [0u8; 16];
740 out.copy_from_slice(&h.finalize().as_bytes()[..16]);
741 out
742}
743
744pub type TermTriple = (String, String, String);
746
747#[derive(Debug, Clone, Copy, PartialEq, Eq)]
750pub struct DumpPlan {
751 pub scan: Option<crate::index::ScanPlan>,
755 pub dictionary_bytes: u64,
764}
765
766#[derive(Debug, Clone)]
770pub struct LayoutSegment {
771 pub kind: &'static str,
772 pub label: String,
773 pub offset: u64,
774 pub len: u64,
775}
776
777#[derive(Debug, Clone, Copy, PartialEq, Eq)]
779pub struct ByteRange {
780 pub offset: u64,
781 pub len: u64,
782}
783
784impl ByteRange {
785 pub fn end(self) -> u64 {
786 self.offset + self.len
787 }
788}
789
790#[derive(Debug, Clone, PartialEq, Eq)]
792pub struct TripleProvenance {
793 pub terms: TermTriple,
795 pub ids: Triple,
797 pub graph: Option<String>,
799 pub matched_pattern: Pattern,
801 pub index_permutation: IndexPermutation,
803 pub dictionary_range: ByteRange,
805 pub index_range: ByteRange,
807 pub index_section_range: ByteRange,
810 pub pyramid_range: Option<ByteRange>,
812 pub tile: Option<String>,
816 pub tile_range: Option<ByteRange>,
819}
820
821fn encode_container(sections: &[&[u8]], codec: u8) -> Vec<u8> {
825 let mut out = Vec::new();
826 write_uvarint(&mut out, sections.len() as u64);
827 for s in sections {
828 let payload = compress(codec, s);
829 write_uvarint(&mut out, payload.len() as u64);
830 out.extend_from_slice(&payload);
831 }
832 out
833}
834
835fn decode_container(bytes: &[u8], codec: u8) -> Result<Vec<Vec<u8>>, FileError> {
837 let (n, mut pos) = read_uvarint(bytes).ok_or(FileError::Container("truncated count"))?;
838 let mut out = Vec::with_capacity((n as usize).min(bytes.len()));
842 for _ in 0..n {
843 let (len, used) =
844 read_uvarint(&bytes[pos..]).ok_or(FileError::Container("truncated length"))?;
845 pos += used;
846 let end = pos + len as usize;
847 if end > bytes.len() {
848 return Err(FileError::Container("section overruns buffer"));
849 }
850 out.push(decompress(codec, &bytes[pos..end])?);
851 pos = end;
852 }
853 Ok(out)
854}
855
856fn checked_end(off: u64, len: u64) -> Result<u64, FileError> {
857 off.checked_add(len)
858 .ok_or(FileError::Container("section range overflows"))
859}
860
861const DICT_CHUNK_BUDGET: usize = crate::write_opts::DEFAULT_DICT_CHUNK_BYTES;
864
865fn encode_chunked_dict_section(raw: &[u8], codec: u8) -> Vec<u8> {
882 let meta = crate::dict::parse_meta(raw).unwrap_or(crate::dict::SectionMeta {
883 term_count: 0,
884 restart_interval: 1,
885 restart_offsets: Vec::new(),
886 });
887 let body_start = meta
888 .restart_offsets
889 .first()
890 .copied()
891 .unwrap_or(raw.len() as u64);
892 let header = &raw[..(body_start.min(raw.len() as u64)) as usize];
893
894 let chunk_budget = crate::write_opts::dict_chunk_budget() as u64;
896 let n_runs = meta.restart_offsets.len();
897 let mut bounds: Vec<(usize, u64, u64)> = Vec::new(); let mut r = 0;
899 while r < n_runs {
900 let start = meta.restart_offsets[r];
901 let mut r2 = r + 1;
902 while r2 < n_runs && meta.restart_offsets[r2] - start < chunk_budget {
903 r2 += 1;
904 }
905 let end = if r2 < n_runs {
906 meta.restart_offsets[r2]
907 } else {
908 raw.len() as u64
909 };
910 bounds.push((r, start, end));
911 r = r2;
912 }
913
914 #[cfg(feature = "parallel")]
918 let compressed: Vec<Vec<u8>> = {
919 use rayon::prelude::*;
920 bounds
921 .par_iter()
922 .map(|&(_, s, e)| compress(codec, &raw[s as usize..e as usize]))
923 .collect()
924 };
925 #[cfg(not(feature = "parallel"))]
926 let compressed: Vec<Vec<u8>> = bounds
927 .iter()
928 .map(|&(_, s, e)| compress(codec, &raw[s as usize..e as usize]))
929 .collect();
930 let mut out = Vec::new();
931 write_uvarint(&mut out, header.len() as u64);
932 out.extend_from_slice(header);
933 write_uvarint(&mut out, bounds.len() as u64);
934 let mut prev_run = 0usize;
935 let mut prev_last: Option<Vec<u8>> = None;
939 for (i, (&(first_run, start, end), comp)) in bounds.iter().zip(&compressed).enumerate() {
940 let key = if i == 0 {
941 Vec::new()
943 } else {
944 let first = crate::dict::run_first_term(raw, start as usize).unwrap_or_default();
945 match &prev_last {
946 None => first,
949 Some(pl) => crate::dict::shortest_separator(pl, &first),
950 }
951 };
952 if i + 1 < bounds.len() {
953 let last_run_off = meta.restart_offsets[bounds[i + 1].0 - 1] as usize;
955 prev_last = crate::dict::run_last_term(raw, last_run_off, end as usize);
956 }
957 write_uvarint(&mut out, (first_run - prev_run) as u64);
958 write_uvarint(&mut out, key.len() as u64);
959 out.extend_from_slice(&key);
960 write_uvarint(&mut out, comp.len() as u64);
961 prev_run = first_run;
962 }
963 for comp in &compressed {
964 out.extend_from_slice(comp);
965 }
966 out
967}
968
969struct DictChunkEntry {
973 first_run: usize,
974 key: Vec<u8>,
975 body_start: u64,
976 start: u64,
977 end: u64,
978}
979
980fn parse_chunked_dict_dir(
984 bytes: &[u8],
985 total_len: u64,
986) -> Result<(crate::dict::SectionMeta, Vec<DictChunkEntry>), FileError> {
987 let mut pos = 0usize;
988 let take = |pos: &mut usize| -> Result<u64, FileError> {
989 let (v, n) = read_uvarint(bytes.get(*pos..).unwrap_or(&[]))
990 .ok_or(FileError::Container("truncated dict chunk directory"))?;
991 *pos += n;
992 Ok(v)
993 };
994 let header_len = take(&mut pos)? as usize;
995 let header = bytes
996 .get(pos..pos.saturating_add(header_len))
997 .ok_or(FileError::Container("truncated dict header"))?;
998 let meta = crate::dict::parse_meta(header)
999 .map_err(|_| FileError::Container("malformed dict header"))?;
1000 pos += header_len;
1001
1002 let num_chunks = take(&mut pos)? as usize;
1003 let mut entries = Vec::with_capacity(num_chunks.min(bytes.len()));
1004 let mut lens = Vec::with_capacity(num_chunks.min(bytes.len()));
1005 let mut prev_run = 0usize;
1006 for _ in 0..num_chunks {
1007 let drun = take(&mut pos)? as usize;
1008 let klen = take(&mut pos)? as usize;
1009 let key = bytes
1010 .get(pos..pos.saturating_add(klen))
1011 .ok_or(FileError::Container("truncated dict chunk key"))?
1012 .to_vec();
1013 pos += klen;
1014 let clen = take(&mut pos)?;
1015 let first_run = prev_run + drun;
1016 let body_start = meta
1017 .restart_offsets
1018 .get(first_run)
1019 .copied()
1020 .ok_or(FileError::Container("dict chunk run out of range"))?;
1021 entries.push(DictChunkEntry {
1022 first_run,
1023 key,
1024 body_start,
1025 start: 0,
1026 end: 0,
1027 });
1028 lens.push(clen);
1029 prev_run = first_run;
1030 }
1031 let mut start = pos as u64;
1032 for (e, len) in entries.iter_mut().zip(lens) {
1033 let end = start
1034 .checked_add(len)
1035 .filter(|&e| e <= total_len)
1036 .ok_or(FileError::Container("dict chunk overruns section"))?;
1037 e.start = start;
1038 e.end = end;
1039 start = end;
1040 }
1041 Ok((meta, entries))
1042}
1043
1044fn read_dict_dir_ranged<R: RangeReader>(
1056 reader: &R,
1057 section: ByteRange,
1058) -> Result<(crate::dict::SectionMeta, Vec<DictChunkEntry>), FileError> {
1059 let total = section.len;
1060 let init = 8192.min(total); let head = reader.read_at(section.offset, init)?;
1068 let (header_len, n0) =
1069 read_uvarint(&head).ok_or(FileError::Container("truncated dict header len"))?;
1070 let hbase = n0; let (term_count, n1) = read_uvarint(head.get(hbase..).unwrap_or(&[]))
1072 .ok_or(FileError::Container("truncated dict term_count"))?;
1073 let (restart_interval, _n2) = read_uvarint(head.get(hbase + n1..).unwrap_or(&[]))
1074 .ok_or(FileError::Container("truncated dict interval"))?;
1075 if restart_interval == 0 {
1076 return Err(FileError::Container("zero restart interval"));
1077 }
1078 let dir_start = (hbase as u64)
1081 .checked_add(header_len)
1082 .filter(|&d| d <= total)
1083 .ok_or(FileError::Container("dict header overruns section"))?;
1084 let dir_total = total - dir_start;
1085 let meta = crate::dict::SectionMeta {
1086 term_count: term_count as u32,
1087 restart_interval: restart_interval as u32,
1088 restart_offsets: Vec::new(),
1089 };
1090 let finish = |mut entries: Vec<DictChunkEntry>| {
1091 for e in &mut entries {
1092 e.start += dir_start; e.end += dir_start;
1094 }
1095 (meta.clone(), entries)
1096 };
1097 let mut have: Vec<u8> = Vec::new();
1101 if dir_start < head.len() as u64 {
1102 match parse_chunk_dir_only(&head[dir_start as usize..], dir_total)? {
1103 ChunkDirParse::Done(entries) => return Ok(finish(entries)),
1104 ChunkDirParse::Truncated { .. } => have = head[dir_start as usize..].to_vec(),
1105 }
1106 }
1107 let mut want = 4096u64.min(dir_total).max(1);
1120 loop {
1121 let held = have.len() as u64;
1122 if want > held {
1123 let extra = reader.read_at(section.offset + dir_start + held, want - held)?;
1124 if extra.is_empty() {
1125 return Err(FileError::Container("truncated dict chunk directory"));
1126 }
1127 have.extend_from_slice(&extra);
1128 }
1129 let held = have.len() as u64;
1130 match parse_chunk_dir_only(&have, dir_total)? {
1131 ChunkDirParse::Done(entries) => return Ok(finish(entries)),
1132 ChunkDirParse::Truncated { .. } if held >= dir_total => {
1133 return Err(FileError::Container("truncated dict chunk directory"));
1134 }
1135 ChunkDirParse::Truncated {
1136 parsed,
1137 used,
1138 total,
1139 } => {
1140 let est = if parsed >= 16 && used > 0 {
1145 let whole = ((used as u64) / (parsed as u64)).saturating_mul(total as u64);
1146 whole.saturating_add(whole / 8).saturating_add(64)
1147 } else {
1148 held.saturating_mul(2)
1150 };
1151 want = est
1152 .max(held.saturating_add(1))
1153 .min(held.saturating_mul(4))
1154 .min(dir_total);
1155 }
1156 }
1157 }
1158}
1159
1160enum ChunkDirParse {
1165 Done(Vec<DictChunkEntry>),
1166 Truncated {
1167 parsed: usize,
1169 used: usize,
1171 total: usize,
1173 },
1174}
1175
1176fn parse_chunk_dir_only(dir: &[u8], dir_total: u64) -> Result<ChunkDirParse, FileError> {
1186 let mut pos = 0usize;
1187 let take = |pos: &mut usize| -> Option<u64> {
1188 let (v, n) = read_uvarint(dir.get(*pos..).unwrap_or(&[]))?;
1189 *pos += n;
1190 Some(v)
1191 };
1192 let Some(num_chunks) = take(&mut pos) else {
1193 return Ok(ChunkDirParse::Truncated {
1194 parsed: 0,
1195 used: 0,
1196 total: 0,
1197 });
1198 };
1199 let num_chunks = num_chunks as usize;
1200 let mut entries = Vec::with_capacity(num_chunks.min(dir.len()));
1201 let mut lens: Vec<u64> = Vec::with_capacity(num_chunks.min(dir.len()));
1202 let mut prev_run = 0usize;
1203 for _ in 0..num_chunks {
1204 let entry_start = pos;
1205 let short = ChunkDirParse::Truncated {
1206 parsed: entries.len(),
1207 used: entry_start,
1208 total: num_chunks,
1209 };
1210 let (Some(drun), Some(klen)) = (take(&mut pos), take(&mut pos)) else {
1211 return Ok(short);
1212 };
1213 let (drun, klen) = (drun as usize, klen as usize);
1214 let Some(key) = dir.get(pos..pos.saturating_add(klen)) else {
1215 return Ok(short);
1216 };
1217 let key = key.to_vec();
1218 pos += klen;
1219 let Some(clen) = take(&mut pos) else {
1220 return Ok(short);
1221 };
1222 let first_run = prev_run + drun;
1223 entries.push(DictChunkEntry {
1224 first_run,
1225 key,
1226 body_start: 0,
1227 start: 0,
1228 end: 0,
1229 });
1230 lens.push(clen);
1231 prev_run = first_run;
1232 }
1233 let mut start = pos as u64;
1234 for (e, len) in entries.iter_mut().zip(lens) {
1235 let end = start
1236 .checked_add(len)
1237 .filter(|&e| e <= dir_total)
1238 .ok_or(FileError::Container("dict chunk overruns section"))?;
1239 e.start = start;
1240 e.end = end;
1241 start = end;
1242 }
1243 Ok(ChunkDirParse::Done(entries))
1244}
1245
1246fn decode_chunked_dict_section(
1250 payload: &[u8],
1251 codec: u8,
1252 cache: std::sync::Arc<crate::chunk_cache::ChunkCache>,
1253 section_index: u8,
1254) -> Result<crate::dict::ChunkedSection, FileError> {
1255 let (meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64)?;
1256 let chunks = entries
1257 .into_iter()
1258 .map(|e| {
1259 let body = decompress(codec, &payload[e.start as usize..e.end as usize])?;
1260 Ok((
1261 crate::dict::SectionChunk::new(e.first_run, e.key, e.body_start),
1262 body,
1263 ))
1264 })
1265 .collect::<Result<Vec<_>, FileError>>()?;
1266 Ok(crate::dict::ChunkedSection::resident(
1267 meta,
1268 chunks,
1269 cache,
1270 section_index,
1271 ))
1272}
1273
1274fn decode_dictionary_container(bytes: &[u8], codec: u8) -> Result<Dictionary, FileError> {
1275 let dsecs = decode_container(bytes, CODEC_NONE)?;
1276 if dsecs.len() != 4 {
1277 return Err(FileError::Container("expected 4 dictionary sections"));
1278 }
1279 let cache = crate::chunk_cache::ChunkCache::unlimited_arc();
1282 let mut sections = Vec::with_capacity(4);
1283 for (si, sec) in dsecs.iter().enumerate() {
1284 sections.push(decode_chunked_dict_section(
1285 sec,
1286 codec,
1287 cache.clone(),
1288 si as u8,
1289 )?);
1290 }
1291 let arr: [crate::dict::ChunkedSection; 4] = sections
1292 .try_into()
1293 .map_err(|_| FileError::Container("expected 4 dictionary sections"))?;
1294 Ok(Dictionary::from_chunked_sections(arr))
1295}
1296
1297fn encode_tiled_section(index: &GraphIndex, si: usize, codec: u8) -> Vec<u8> {
1303 let tiles = index.tile_sections()[si];
1304 let n = tiles.len();
1305 #[cfg(feature = "parallel")]
1312 let compressed: Vec<Vec<u8>> = {
1313 use rayon::prelude::*;
1314 (0..n)
1315 .into_par_iter()
1316 .map(|ti| compress(codec, &index.tile_body(si, ti)))
1317 .collect()
1318 };
1319 #[cfg(not(feature = "parallel"))]
1320 let compressed: Vec<Vec<u8>> = (0..n)
1321 .map(|ti| compress(codec, &index.tile_body(si, ti)))
1322 .collect();
1323 let mut out = Vec::new();
1324 write_uvarint(&mut out, tiles.len() as u64);
1325 let mut prev_min = 0u32;
1326 for (tile, comp) in tiles.iter().zip(&compressed) {
1327 let (min_a, max_a) = tile.leading_range();
1328 write_uvarint(&mut out, (min_a - prev_min) as u64);
1329 write_uvarint(&mut out, (max_a - min_a) as u64);
1330 write_uvarint(&mut out, comp.len() as u64);
1331 prev_min = min_a;
1332 }
1333 for comp in &compressed {
1334 out.extend_from_slice(comp);
1335 }
1336 for ti in 0..n {
1344 let body = index.tile_body(si, ti);
1345 let (min_b, max_b, min_c, max_c) = match crate::triples::TripleBlock::parse(&body) {
1346 Ok(b) => {
1347 let z = b.zone();
1348 (z.min_b, z.max_b, z.min_c, z.max_c)
1349 }
1350 Err(_) => (0, u32::MAX, 0, u32::MAX),
1351 };
1352 write_uvarint(&mut out, min_b as u64);
1353 write_uvarint(&mut out, (max_b - min_b) as u64);
1354 write_uvarint(&mut out, min_c as u64);
1355 write_uvarint(&mut out, (max_c - min_c) as u64);
1356 }
1357 out
1358}
1359
1360struct TileDirEntry {
1363 min_a: u32,
1364 max_a: u32,
1365 start: u64,
1366 end: u64,
1367}
1368
1369type TileSynopsis = (u32, u32, u32, u32);
1371
1372fn parse_tile_synopsis(
1380 payload: &[u8],
1381 trailer_start: usize,
1382 num_tiles: usize,
1383) -> Option<Vec<TileSynopsis>> {
1384 let mut pos = trailer_start;
1385 let take = |pos: &mut usize| -> Option<u32> {
1386 let (v, n) = read_uvarint(payload.get(*pos..)?)?;
1387 *pos += n;
1388 u32::try_from(v).ok()
1389 };
1390 let mut out = Vec::with_capacity(num_tiles.min(payload.len()));
1391 for _ in 0..num_tiles {
1392 let min_b = take(&mut pos)?;
1393 let max_b = min_b.checked_add(take(&mut pos)?)?;
1394 let min_c = take(&mut pos)?;
1395 let max_c = min_c.checked_add(take(&mut pos)?)?;
1396 out.push((min_b, max_b, min_c, max_c));
1397 }
1398 Some(out)
1399}
1400
1401fn parse_tile_directory(bytes: &[u8], total_len: u64) -> Result<Vec<TileDirEntry>, FileError> {
1406 let mut pos = 0usize;
1407 let take = |pos: &mut usize| -> Result<u64, FileError> {
1408 let (v, n) = read_uvarint(bytes.get(*pos..).unwrap_or(&[]))
1409 .ok_or(FileError::Container("truncated tile directory"))?;
1410 *pos += n;
1411 Ok(v)
1412 };
1413 let num_tiles = take(&mut pos)? as usize;
1414 let mut entries = Vec::with_capacity(num_tiles.min(bytes.len()));
1415 let mut prev_min = 0u32;
1416 let mut lens = Vec::with_capacity(num_tiles.min(bytes.len()));
1417 for _ in 0..num_tiles {
1418 let dmin = take(&mut pos)? as u32;
1419 let span = take(&mut pos)? as u32;
1420 let len = take(&mut pos)?;
1421 let min_a = prev_min.wrapping_add(dmin);
1422 entries.push(TileDirEntry {
1423 min_a,
1424 max_a: min_a.wrapping_add(span),
1425 start: 0,
1426 end: 0,
1427 });
1428 lens.push(len);
1429 prev_min = min_a;
1430 }
1431 let mut start = pos as u64;
1432 for (e, len) in entries.iter_mut().zip(lens) {
1433 let end = start
1434 .checked_add(len)
1435 .filter(|&e| e <= total_len)
1436 .ok_or(FileError::Container("tile overruns section"))?;
1437 e.start = start;
1438 e.end = end;
1439 start = end;
1440 }
1441 Ok(entries)
1442}
1443
1444fn read_tile_directory_ranged<R: RangeReader>(
1448 reader: &R,
1449 section: ByteRange,
1450) -> Result<Vec<TileDirEntry>, FileError> {
1451 let total = section.len;
1452 let mut prefetch = 4096u64.min(total);
1453 loop {
1454 let prefix = reader.read_at(section.offset, prefetch)?;
1455 match parse_tile_directory(&prefix, total) {
1456 Ok(dir) => return Ok(dir),
1457 Err(_) if prefetch < total => prefetch = prefetch.saturating_mul(2).min(total),
1458 Err(e) => return Err(e),
1459 }
1460 }
1461}
1462
1463fn read_tile_synopsis_ranged<R: RangeReader>(
1470 reader: &R,
1471 section: ByteRange,
1472 dir: &[TileDirEntry],
1473) -> Vec<Option<TileSynopsis>> {
1474 let n = dir.len();
1475 let none = vec![None; n];
1476 let trailer_start = dir.iter().map(|e| e.end).max().unwrap_or(0);
1477 let total = section.len;
1478 if n == 0 || trailer_start >= total {
1479 return none; }
1481 let trailer_len = total - trailer_start;
1482 let Ok(bytes) = reader.read_at(section.offset + trailer_start, trailer_len) else {
1483 return none;
1484 };
1485 match parse_tile_synopsis(&bytes, 0, n) {
1486 Some(v) => v.into_iter().map(Some).collect(),
1487 None => none,
1488 }
1489}
1490
1491fn tile_file_ranges(
1495 index_bytes: &[u8],
1496 container_offset: u64,
1497 section_ranges: &[ByteRange; NUM_PERMS],
1498) -> [Vec<(u32, u32, ByteRange)>; NUM_PERMS] {
1499 let mut out: [Vec<(u32, u32, ByteRange)>; NUM_PERMS] = Default::default();
1500 for (section, range) in out.iter_mut().zip(section_ranges) {
1501 if range.len == 0 || range.offset < container_offset {
1505 continue;
1506 }
1507 let start = (range.offset - container_offset) as usize;
1508 let Some(payload) = index_bytes.get(start..start + range.len as usize) else {
1509 continue;
1510 };
1511 if let Ok(dir) = parse_tile_directory(payload, payload.len() as u64) {
1512 *section = dir
1513 .into_iter()
1514 .map(|e| {
1515 (
1516 e.min_a,
1517 e.max_a,
1518 ByteRange {
1519 offset: range.offset + e.start,
1520 len: (e.end - e.start),
1521 },
1522 )
1523 })
1524 .collect();
1525 }
1526 }
1527 out
1528}
1529
1530fn decode_tiled_section(payload: &[u8], codec: u8) -> Result<Vec<(u32, u32, Vec<u8>)>, FileError> {
1533 parse_tile_directory(payload, payload.len() as u64)?
1534 .into_iter()
1535 .map(|e| {
1536 Ok((
1537 e.min_a,
1538 e.max_a,
1539 decompress(codec, &payload[e.start as usize..e.end as usize])?,
1540 ))
1541 })
1542 .collect()
1543}
1544
1545fn decode_index_container(
1555 bytes: &[u8],
1556 codec: u8,
1557 perms: PermSet,
1558) -> Result<GraphIndex, FileError> {
1559 let isecs = decode_container(bytes, CODEC_NONE)?;
1560 if isecs.len() != perms.len() {
1561 return Err(FileError::Container(
1562 "index container section count does not match the header permutation mask",
1563 ));
1564 }
1565 decode_index_sections(isecs, codec, perms)
1566}
1567
1568fn decode_index_sections(
1569 mut isecs: Vec<Vec<u8>>,
1570 codec: u8,
1571 perms: PermSet,
1572) -> Result<GraphIndex, FileError> {
1573 let mut sections: [Vec<(u32, u32, Vec<u8>)>; NUM_PERMS] = Default::default();
1574 for (perm, sec) in perms.iter().zip(isecs.iter_mut()) {
1575 sections[perm.section_index()] = decode_tiled_section(sec, codec)?;
1576 }
1577 Ok(GraphIndex::from_tiles(sections, perms))
1578}
1579
1580fn decode_root_index_container(
1585 bytes: &[u8],
1586 header: &Header,
1587) -> Result<(GraphIndex, Option<[u8; 16]>), FileError> {
1588 if !header.has_quad_index() {
1589 return Ok((
1590 decode_index_container(bytes, header.block_codec, header.perms)?,
1591 None,
1592 ));
1593 }
1594 let mut isecs = decode_container(bytes, CODEC_NONE)?;
1595 if isecs.len() != header.index_container_sections() {
1596 return Err(FileError::Container(
1597 "index container section count does not match the header permutation mask",
1598 ));
1599 }
1600 let seal = isecs.pop().unwrap_or_default();
1601 let digest = crate::quad::parse_seal(&seal)?;
1602 Ok((
1603 decode_index_sections(isecs, header.block_codec, header.perms)?,
1604 Some(digest),
1605 ))
1606}
1607
1608fn check_named_layout(header: &Header) -> Result<(), FileError> {
1613 let quad = header.quad_index_section().is_some();
1614 if header.has_quad_index() {
1615 if !quad {
1616 return Err(FileError::Container(
1617 "quad-index flag set but the file has no quad-index section",
1618 ));
1619 }
1620 if header.named_graphs_len > 0 {
1621 return Err(FileError::Container(
1622 "a file cannot carry both a quad index and a named-graphs section",
1623 ));
1624 }
1625 crate::quad::check_perms(header.perms)?;
1626 } else if quad {
1627 return Err(FileError::Container(
1628 "quad-index section present but the header does not flag it",
1629 ));
1630 }
1631 Ok(())
1632}
1633
1634fn container_section_payload_ranges(
1635 bytes: &[u8],
1636 container_offset: u64,
1637 expected_sections: usize,
1638) -> Result<Vec<ByteRange>, FileError> {
1639 let (section_count, mut pos) =
1640 read_uvarint(bytes).ok_or(FileError::Container("truncated count"))?;
1641 let section_count = usize::try_from(section_count)
1642 .map_err(|_| FileError::Container("section count too large"))?;
1643 if section_count != expected_sections {
1644 return Err(FileError::Container("unexpected section count"));
1645 }
1646
1647 let mut ranges = Vec::with_capacity(section_count);
1648 for _ in 0..section_count {
1649 let remaining = bytes
1650 .get(pos..)
1651 .ok_or(FileError::Container("truncated length"))?;
1652 let (payload_len, used) =
1653 read_uvarint(remaining).ok_or(FileError::Container("truncated length"))?;
1654 pos = pos
1655 .checked_add(used)
1656 .ok_or(FileError::Container("section range overflows"))?;
1657 let payload_len_usize = usize::try_from(payload_len)
1658 .map_err(|_| FileError::Container("section length too large"))?;
1659 let payload_end = pos
1660 .checked_add(payload_len_usize)
1661 .ok_or(FileError::Container("section range overflows"))?;
1662 if payload_end > bytes.len() {
1663 return Err(FileError::Container("section overruns buffer"));
1664 }
1665 ranges.push(ByteRange {
1666 offset: checked_end(container_offset, pos as u64)?,
1667 len: payload_len,
1668 });
1669 pos = payload_end;
1670 }
1671
1672 Ok(ranges)
1673}
1674
1675fn decode_index_section_ranges(
1679 bytes: &[u8],
1680 container_offset: u64,
1681 perms: PermSet,
1682 sections: usize,
1683) -> Result<[ByteRange; NUM_PERMS], FileError> {
1684 let ranges = container_section_payload_ranges(bytes, container_offset, sections)?;
1685 let mut out = [ByteRange { offset: 0, len: 0 }; NUM_PERMS];
1686 for (perm, range) in perms.iter().zip(ranges) {
1687 out[perm.section_index()] = range;
1688 }
1689 Ok(out)
1690}
1691
1692fn read_uvarint_at<R: RangeReader + ?Sized>(
1693 reader: &R,
1694 absolute_offset: u64,
1695 container_end: u64,
1696) -> Result<(u64, u64), FileError> {
1697 if absolute_offset >= container_end {
1698 return Err(FileError::Container("truncated container varint"));
1699 }
1700 let remaining = container_end - absolute_offset;
1701 let probe_len = remaining.min(10);
1702 let bytes = reader.read_at(absolute_offset, probe_len)?;
1703 read_uvarint(&bytes)
1704 .map(|(value, used)| (value, used as u64))
1705 .ok_or(FileError::Container("truncated container varint"))
1706}
1707
1708fn locate_container_section_ranged<R: RangeReader + ?Sized>(
1711 reader: &R,
1712 container_offset: u64,
1713 container_len: u64,
1714 section_index: usize,
1715 expected_sections: u64,
1716) -> Result<ByteRange, FileError> {
1717 let container_end = checked_end(container_offset, container_len)?;
1718 let (section_count, used) = read_uvarint_at(reader, container_offset, container_end)?;
1719 if section_count != expected_sections {
1720 return Err(FileError::Container("unexpected container section count"));
1721 }
1722 if section_index >= section_count as usize {
1723 return Err(FileError::Container(
1724 "container section index out of bounds",
1725 ));
1726 }
1727
1728 let mut pos = checked_end(container_offset, used)?;
1729 for i in 0..section_count as usize {
1730 let (payload_len, len_used) = read_uvarint_at(reader, pos, container_end)?;
1731 pos = checked_end(pos, len_used)?;
1732 let payload_end = checked_end(pos, payload_len)?;
1733 if payload_end > container_end {
1734 return Err(FileError::Container("section overruns buffer"));
1735 }
1736 if i == section_index {
1737 return Ok(ByteRange {
1738 offset: pos,
1739 len: payload_len,
1740 });
1741 }
1742 pos = payload_end;
1743 }
1744 Err(FileError::Container("container section not found"))
1745}
1746
1747#[allow(clippy::type_complexity)]
1757fn open_index_container_lazy(
1758 reader: &std::sync::Arc<dyn RangeReader + Send + Sync>,
1759 container: ByteRange,
1760 block_codec: u8,
1761 has_synopsis: bool,
1762 read_concurrency: usize,
1763 perms: PermSet,
1764 sections: usize,
1765) -> Result<
1766 (
1767 GraphIndex,
1768 [ByteRange; NUM_PERMS],
1769 [Vec<(u32, u32, ByteRange)>; NUM_PERMS],
1770 ),
1771 FileError,
1772> {
1773 let mut index_section_ranges = [ByteRange { offset: 0, len: 0 }; NUM_PERMS];
1774 let mut tile_ranges: [Vec<(u32, u32, ByteRange)>; NUM_PERMS] = Default::default();
1775 #[allow(clippy::type_complexity)]
1776 let mut directories: [Vec<(u32, u32, Option<TileSynopsis>)>; NUM_PERMS] = Default::default();
1777 for (pos, perm) in perms.iter().enumerate() {
1778 let si = perm.section_index();
1779 let section = locate_container_section_ranged(
1780 reader,
1781 container.offset,
1782 container.len,
1783 pos,
1784 sections as u64,
1785 )?;
1786 index_section_ranges[si] = section;
1787 let dir = read_tile_directory_ranged(reader, section)?;
1788 let syn = if has_synopsis {
1791 read_tile_synopsis_ranged(reader, section, &dir)
1792 } else {
1793 vec![None; dir.len()]
1794 };
1795 directories[si] = dir
1796 .iter()
1797 .zip(syn)
1798 .map(|(e, s)| (e.min_a, e.max_a, s))
1799 .collect();
1800 tile_ranges[si] = dir
1801 .into_iter()
1802 .map(|e| {
1803 (
1804 e.min_a,
1805 e.max_a,
1806 ByteRange {
1807 offset: section.offset + e.start,
1808 len: (e.end - e.start),
1809 },
1810 )
1811 })
1812 .collect();
1813 }
1814
1815 let codec = block_codec;
1820 let loader_ranges = tile_ranges.clone();
1821 let loader_reader = reader.clone();
1822 let loader: crate::index::TileLoader = Box::new(move |si, ti| {
1823 let (_, _, range) = loader_ranges.get(si)?.get(ti)?;
1824 let bytes = loader_reader.read_at(range.offset, range.len).ok()?;
1825 decompress(codec, &bytes).ok()
1826 });
1827 let bulk_ranges = tile_ranges.clone();
1828 let bulk_reader = reader.clone();
1829 let bulk: crate::index::TileBulkLoader = Box::new(move |si, tis| {
1830 let section = bulk_ranges.get(si)?;
1831 let want: Option<Vec<ByteRange>> = tis
1832 .iter()
1833 .map(|&ti| section.get(ti).map(|&(_, _, r)| r))
1834 .collect();
1835 let blobs = read_coalesced(bulk_reader.as_ref(), &want?, TILE_COALESCE_GAP)?;
1836 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
1837 });
1838 let mut index =
1839 GraphIndex::from_remote_directories(directories, perms, loader).with_bulk_loader(bulk);
1840 index.set_tile_lens(std::array::from_fn(|si| {
1843 tile_ranges[si]
1844 .iter()
1845 .map(|&(_, _, r)| r.len.min(u32::MAX as u64) as u32)
1846 .collect()
1847 }));
1848 index.set_read_concurrency(read_concurrency);
1852 Ok((index, index_section_ranges, tile_ranges))
1853}
1854
1855pub fn write_file(
1859 dict: &Dictionary,
1860 index: &GraphIndex,
1861 has_quads: bool,
1862 pyramid_meta: &[u8],
1863 pyramid_levels: u16,
1864) -> Vec<u8> {
1865 write_dataset(dict, index, &[], has_quads, pyramid_meta, pyramid_levels)
1866}
1867
1868pub(crate) fn encode_index_container(index: &GraphIndex, codec: u8) -> Vec<u8> {
1874 encode_index_container_sealed(index, codec, None)
1875}
1876
1877fn encode_index_container_sealed(index: &GraphIndex, codec: u8, seal: Option<&[u8]>) -> Vec<u8> {
1882 let payloads: Vec<Vec<u8>> = index
1883 .perms()
1884 .iter()
1885 .map(|perm| encode_tiled_section(index, perm.section_index(), codec))
1886 .collect();
1887 let mut refs: Vec<&[u8]> = payloads.iter().map(|p| p.as_slice()).collect();
1888 if let Some(seal) = seal {
1889 refs.push(seal);
1890 }
1891 encode_container(&refs, CODEC_NONE)
1892}
1893
1894fn encode_named_graphs(named: &[(String, GraphIndex)], codec: u8) -> Vec<u8> {
1896 let mut out = Vec::new();
1897 write_uvarint(&mut out, named.len() as u64);
1898 for (iri, index) in named {
1899 write_uvarint(&mut out, iri.len() as u64);
1900 out.extend_from_slice(iri.as_bytes());
1901 let container = encode_index_container(index, codec);
1902 write_uvarint(&mut out, container.len() as u64);
1903 out.extend_from_slice(&container);
1904 }
1905 out
1906}
1907
1908fn decode_named_graphs(
1909 bytes: &[u8],
1910 codec: u8,
1911 perms: PermSet,
1912) -> Result<Vec<(String, GraphIndex)>, FileError> {
1913 let (n, mut pos) = read_uvarint(bytes).ok_or(FileError::Container("truncated graph count"))?;
1914 let bound = |start: usize, len: u64| -> Result<usize, FileError> {
1917 start
1918 .checked_add(len as usize)
1919 .filter(|&e| e <= bytes.len())
1920 .ok_or(FileError::Container("named-graph field overruns buffer"))
1921 };
1922 let mut out = Vec::with_capacity((n as usize).min(bytes.len()));
1923 for _ in 0..n {
1924 let (ilen, u1) = read_uvarint(bytes.get(pos..).unwrap_or(&[]))
1925 .ok_or(FileError::Container("truncated iri len"))?;
1926 pos += u1;
1927 let iend = bound(pos, ilen)?;
1928 let iri = String::from_utf8_lossy(&bytes[pos..iend]).into_owned();
1929 pos = iend;
1930 let (clen, u2) = read_uvarint(bytes.get(pos..).unwrap_or(&[]))
1931 .ok_or(FileError::Container("truncated container len"))?;
1932 pos += u2;
1933 let cend = bound(pos, clen)?;
1934 let index = decode_index_container(&bytes[pos..cend], codec, perms)?;
1935 out.push((iri, index));
1936 pos = cend;
1937 }
1938 Ok(out)
1939}
1940
1941pub fn write_dataset(
1944 dict: &Dictionary,
1945 default_index: &GraphIndex,
1946 named: &[(String, GraphIndex)],
1947 has_quads: bool,
1948 pyramid_meta: &[u8],
1949 pyramid_levels: u16,
1950) -> Vec<u8> {
1951 write_dataset_with_metadata(
1952 dict,
1953 default_index,
1954 named,
1955 has_quads,
1956 pyramid_meta,
1957 pyramid_levels,
1958 &[],
1959 &[],
1960 )
1961}
1962
1963pub(crate) fn encode_dict_container(dict: &Dictionary, codec: u8) -> Vec<u8> {
1968 let raw_sections = dict.sections();
1969 let dict_payloads: Vec<Vec<u8>> = raw_sections
1970 .iter()
1971 .map(|raw| encode_chunked_dict_section(raw, codec))
1972 .collect();
1973 encode_container(
1974 &[
1975 dict_payloads[0].as_slice(),
1976 dict_payloads[1].as_slice(),
1977 dict_payloads[2].as_slice(),
1978 dict_payloads[3].as_slice(),
1979 ],
1980 CODEC_NONE,
1981 )
1982}
1983
1984#[allow(clippy::too_many_arguments)]
1995pub fn write_dataset_with_metadata(
1996 dict: &Dictionary,
1997 default_index: &GraphIndex,
1998 named: &[(String, GraphIndex)],
1999 has_quads: bool,
2000 pyramid_meta: &[u8],
2001 pyramid_levels: u16,
2002 metadata: &[u8],
2003 text_index: &[u8],
2004) -> Vec<u8> {
2005 let codec = writer_codec();
2006 let dict_container = encode_dict_container(dict, codec);
2007 let scope = (!text_index.is_empty())
2011 .then(|| crate::text_index::TextIndex::from_section(text_index, codec).ok())
2012 .flatten()
2013 .map(|ti| ti.scope().clone());
2014 let text_index_partial = scope
2015 .as_ref()
2016 .is_some_and(crate::text_index::TextIndexScope::restricts_predicates);
2017 let text_index_named = scope
2018 .as_ref()
2019 .is_some_and(crate::text_index::TextIndexScope::covers_named_graphs);
2020 write_dataset_from_parts(
2021 &dict_container,
2022 dict.term_count() as u64,
2023 default_index,
2024 NamedPart::PerGraph(named),
2025 has_quads,
2026 dict.has_quoted_triples(),
2027 pyramid_meta,
2028 pyramid_levels,
2029 metadata,
2030 text_index,
2031 text_index_partial,
2032 text_index_named,
2033 codec,
2034 )
2035}
2036
2037pub(crate) enum NamedPart<'a> {
2040 PerGraph(&'a [(String, GraphIndex)]),
2041 Quad(&'a crate::quad::EncodedQuadIndex),
2042}
2043
2044#[allow(clippy::too_many_arguments)]
2049pub(crate) fn write_dataset_from_parts(
2050 dict_container: &[u8],
2051 term_count: u64,
2052 default_index: &GraphIndex,
2053 named: NamedPart<'_>,
2054 has_quads: bool,
2055 has_quoted_triples: bool,
2056 pyramid_meta: &[u8],
2057 pyramid_levels: u16,
2058 metadata: &[u8],
2059 text_index: &[u8],
2060 text_index_partial: bool,
2061 text_index_named: bool,
2062 codec: u8,
2063) -> Vec<u8> {
2064 let (named_section, quad, named_quads) = match named {
2068 NamedPart::PerGraph(named) => (
2069 if named.is_empty() {
2070 Vec::new()
2071 } else {
2072 encode_named_graphs(named, codec)
2073 },
2074 None,
2075 named
2076 .iter()
2077 .map(|(_, idx)| idx.triple_count() as u64)
2078 .sum::<u64>(),
2079 ),
2080 NamedPart::Quad(q) if q.graph_count == 0 => (Vec::new(), None, 0),
2081 NamedPart::Quad(q) => (Vec::new(), Some(q), q.quad_count),
2082 };
2083 let seal = quad.map(|q| crate::quad::encode_seal(crate::quad::digest(&q.bytes)));
2084 let index_container = encode_index_container_sealed(default_index, codec, seal.as_deref());
2085 let text_index_partial = text_index_partial && !text_index.is_empty();
2090 let digested;
2091 let text_index: &[u8] = if text_index_partial {
2092 digested = crate::text_index::with_partial_digest(text_index);
2093 &digested
2094 } else {
2095 text_index
2096 };
2097
2098 let meta_section_len = metadata.len() as u64;
2101 let dict_offset = HEADER_LEN as u64 + meta_section_len;
2102 let dict_len = dict_container.len() as u64;
2103 let index_offset = dict_offset + dict_len;
2104 let index_len = index_container.len() as u64;
2105 let pyr_offset = index_offset + index_len;
2106 let pyr_len = pyramid_meta.len() as u64;
2107 let text_offset = pyr_offset + pyr_len;
2109 let text_len = text_index.len() as u64;
2110 let named_offset = text_offset + text_len;
2111 let named_len = named_section.len() as u64;
2112 let quad_len = quad.map_or(0, |q| q.bytes.len() as u64);
2115
2116 let mut parts: Vec<&[u8]> = Vec::with_capacity(5);
2122 if meta_section_len > 0 {
2123 parts.push(metadata);
2124 }
2125 parts.push(dict_container);
2126 parts.push(&index_container);
2127 parts.push(pyramid_meta);
2128 if text_len > 0 && !text_index_partial {
2129 parts.push(text_index);
2130 }
2131 if named_len > 0 {
2132 parts.push(&named_section);
2133 }
2134
2135 let schema_meta_len = crate::meta::schema_block_len(pyramid_meta);
2138
2139 let header = Header {
2143 version: crate::header::CURRENT_FORMAT_VERSION,
2144 flags: FLAG_TILE_SYNOPSIS
2145 | if has_quads { FLAG_HAS_QUADS } else { 0 }
2146 | if quad.is_some() {
2147 crate::header::FLAG_QUAD_INDEX
2148 } else {
2149 0
2150 }
2151 | if has_quoted_triples {
2152 FLAG_HAS_QUOTED_TRIPLES
2153 } else {
2154 0
2155 }
2156 | if text_len > 0 && text_index_named {
2157 crate::header::FLAG_TEXT_INDEX_NAMED_GRAPHS
2158 } else {
2159 0
2160 },
2161 metadata_offset: HEADER_LEN as u64,
2162 metadata_len: meta_section_len,
2163 dictionary_offset: dict_offset,
2164 dictionary_len: dict_len,
2165 root_dir_offset: index_offset,
2166 root_dir_len: index_len,
2167 pyramid_meta_offset: if pyr_len > 0 { pyr_offset } else { 0 },
2168 pyramid_meta_len: pyr_len,
2169 dict_codec: codec,
2170 block_codec: codec,
2171 pyramid_levels,
2172 perms: default_index.perms(),
2176 quad_count: default_index.triple_count() as u64 + named_quads,
2177 term_count,
2178 content_hash: content_hash(&parts),
2179 named_graphs_offset: if named_len > 0 { named_offset } else { 0 },
2180 named_graphs_len: named_len,
2181 schema_meta_len,
2182 text_index_offset: if text_len > 0 { text_offset } else { 0 },
2183 text_index_len: text_len,
2184 text_index_partial: text_len > 0 && text_index_partial,
2185 build_info_offset: 0,
2186 build_info_len: 0,
2187 extra_sections: if quad_len > 0 {
2188 vec![crate::header::Section {
2189 kind: crate::header::SectionKind::QuadIndex,
2190 flags: 0,
2191 offset: named_offset,
2192 length: quad_len,
2193 }]
2194 } else {
2195 Vec::new()
2196 },
2197 };
2198
2199 let mut out = Vec::with_capacity(
2200 HEADER_LEN
2201 + metadata.len()
2202 + dict_container.len()
2203 + index_container.len()
2204 + pyramid_meta.len()
2205 + text_index.len()
2206 + named_section.len()
2207 + quad_len as usize
2208 + MAGIC.len(),
2209 );
2210 out.extend_from_slice(&header.to_bytes());
2211 if meta_section_len > 0 {
2212 out.extend_from_slice(metadata);
2213 }
2214 out.extend_from_slice(dict_container);
2215 out.extend_from_slice(&index_container);
2216 out.extend_from_slice(pyramid_meta);
2217 if text_len > 0 {
2218 out.extend_from_slice(text_index);
2219 }
2220 if named_len > 0 {
2221 out.extend_from_slice(&named_section);
2222 }
2223 if let Some(q) = quad {
2224 out.extend_from_slice(&q.bytes);
2225 }
2226 out.extend_from_slice(&MAGIC); out
2228}
2229
2230pub const RDF_TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
2232
2233pub fn schema_summary(rete: &Rete) -> Vec<(String, String, String, u32)> {
2240 use std::collections::{BTreeMap, HashMap};
2241 let triples = rete.dump(None);
2242
2243 let mut class_of: HashMap<&str, &str> = HashMap::new();
2244 for (s, p, o) in &triples {
2245 if p == RDF_TYPE {
2246 class_of.insert(s.as_str(), o.as_str());
2247 }
2248 }
2249 let classify = |t: &str| -> String {
2250 if let Some(c) = class_of.get(t) {
2251 (*c).to_string()
2252 } else if t.starts_with('"') {
2253 "(literal)".to_string()
2254 } else {
2255 "(untyped)".to_string()
2256 }
2257 };
2258
2259 let mut counts: BTreeMap<(String, String, String), u32> = BTreeMap::new();
2260 for (s, p, o) in &triples {
2261 if p == RDF_TYPE {
2262 continue; }
2264 *counts
2265 .entry((classify(s), p.clone(), classify(o)))
2266 .or_default() += 1;
2267 }
2268 counts
2269 .into_iter()
2270 .map(|((a, p, b), c)| (a, p, b, c))
2271 .collect()
2272}
2273
2274pub fn schema_classes(rete: &Rete) -> Vec<(String, u32)> {
2278 use std::collections::BTreeMap;
2279 let mut counts: BTreeMap<String, u32> = BTreeMap::new();
2280 for (_s, p, o) in rete.dump(None) {
2281 if p == RDF_TYPE {
2282 *counts.entry(o).or_default() += 1;
2283 }
2284 }
2285 let mut out: Vec<(String, u32)> = counts.into_iter().collect();
2286 out.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
2287 out
2288}
2289
2290pub fn read_metadata_ranged<R: RangeReader>(reader: &R) -> Result<Option<Vec<u8>>, FileError> {
2300 let head = reader.read_at(0, HEADER_LEN as u64)?;
2301 let header = Header::from_bytes(&head)?;
2302 if header.metadata_len == 0 {
2303 return Ok(None);
2304 }
2305 let bytes = reader.read_at(header.metadata_offset, header.metadata_len)?;
2306 Ok(Some(bytes))
2307}
2308
2309#[allow(clippy::type_complexity)]
2319pub fn read_card_and_build_info_ranged<R: RangeReader>(
2320 reader: &R,
2321) -> Result<(Option<Vec<u8>>, Option<Vec<u8>>), FileError> {
2322 read_card_and_build_info_with_header(reader).map(|(_, m, b)| (m, b))
2323}
2324
2325#[allow(clippy::type_complexity)]
2336pub fn read_card_and_build_info_with_header<R: RangeReader>(
2337 reader: &R,
2338) -> Result<(Header, Option<Vec<u8>>, Option<Vec<u8>>), FileError> {
2339 let head = reader.read_at(0, HEADER_LEN as u64)?;
2340 let header = Header::from_bytes(&head)?;
2341 let meta = (header.metadata_offset, header.metadata_len);
2342 let build = (header.build_info_offset, header.build_info_len);
2343 if meta.1 > 0 && build.1 > 0 && build.0 == meta.0 + meta.1 {
2344 let both = reader.read_at(meta.0, meta.1 + build.1)?;
2346 let (m, b) = both.split_at(meta.1 as usize);
2347 return Ok((header, Some(m.to_vec()), Some(b.to_vec())));
2348 }
2349 let fetch = |off: u64, len: u64| -> Result<Option<Vec<u8>>, FileError> {
2350 if len == 0 {
2351 return Ok(None);
2352 }
2353 Ok(Some(reader.read_at(off, len)?))
2354 };
2355 Ok((header, fetch(meta.0, meta.1)?, fetch(build.0, build.1)?))
2356}
2357
2358pub fn read_build_info(bytes: &[u8]) -> Result<Option<Vec<u8>>, FileError> {
2363 let header = Header::from_bytes(bytes)?;
2364 if header.build_info_len == 0 {
2365 return Ok(None);
2366 }
2367 let start = header.build_info_offset as usize;
2368 let end = start
2369 .checked_add(header.build_info_len as usize)
2370 .filter(|&e| e <= bytes.len())
2371 .ok_or(FileError::Container("build-info section overruns buffer"))?;
2372 Ok(Some(bytes[start..end].to_vec()))
2373}
2374
2375#[derive(Debug, Clone, PartialEq, Eq)]
2384pub struct BuildInfoPlan {
2385 pub header: [u8; HEADER_LEN],
2387 pub insert: u64,
2390 pub tail_start: u64,
2394 pub new_len: u64,
2396}
2397
2398pub fn plan_build_info(
2420 head: &[u8],
2421 file_len: u64,
2422 info_len: u64,
2423) -> Result<BuildInfoPlan, FileError> {
2424 let mut header = Header::from_bytes(head)?;
2425 let insert = if header.metadata_len > 0 {
2430 header
2431 .metadata_offset
2432 .checked_add(header.metadata_len)
2433 .filter(|&end| header.metadata_offset >= HEADER_LEN as u64 && end <= file_len)
2434 .ok_or(FileError::Container(
2435 "the metadata section lies outside the file",
2436 ))?
2437 } else {
2438 HEADER_LEN as u64
2439 };
2440 let old_len = header.build_info_len;
2441 if old_len > 0 && header.build_info_offset != insert {
2442 return Err(FileError::Container(
2443 "existing build-info section is not adjacent to the metadata",
2444 ));
2445 }
2446 let tail_start = insert.saturating_add(old_len);
2447 if tail_start > file_len || insert > file_len {
2448 return Err(FileError::Container("build-info splice out of bounds"));
2449 }
2450
2451 let shift = |off: &mut u64, len: u64| {
2453 if len > 0 && *off >= tail_start {
2454 *off = *off - old_len + info_len;
2455 }
2456 };
2457 shift(&mut header.dictionary_offset, header.dictionary_len);
2458 shift(&mut header.root_dir_offset, header.root_dir_len);
2459 shift(&mut header.pyramid_meta_offset, header.pyramid_meta_len);
2460 shift(&mut header.named_graphs_offset, header.named_graphs_len);
2461 shift(&mut header.text_index_offset, header.text_index_len);
2462 for s in &mut header.extra_sections {
2463 if s.length > 0 && s.offset >= tail_start {
2464 s.offset = s.offset - old_len + info_len;
2465 }
2466 }
2467 if info_len == 0 {
2468 header.build_info_offset = 0;
2469 header.build_info_len = 0;
2470 } else {
2471 header.build_info_offset = insert;
2472 header.build_info_len = info_len;
2473 }
2474
2475 Ok(BuildInfoPlan {
2476 header: header.to_bytes(),
2477 insert,
2478 tail_start,
2479 new_len: file_len - old_len + info_len,
2480 })
2481}
2482
2483pub fn attach_build_info(image: &[u8], info: &[u8]) -> Result<Vec<u8>, FileError> {
2489 let plan = plan_build_info(image, image.len() as u64, info.len() as u64)?;
2490 let mut out = Vec::with_capacity(plan.new_len as usize);
2491 out.extend_from_slice(&plan.header);
2492 out.extend_from_slice(&image[HEADER_LEN..plan.insert as usize]);
2493 out.extend_from_slice(info);
2494 out.extend_from_slice(&image[plan.tail_start as usize..]);
2495 debug_assert_eq!(out.len() as u64, plan.new_len);
2496 Ok(out)
2497}
2498
2499pub fn replace_metadata(image: &[u8], metadata: &[u8]) -> Result<Vec<u8>, FileError> {
2519 let mut header = Header::from_bytes(image)?;
2520 let insert = HEADER_LEN as u64;
2523 if header.metadata_len > 0 && header.metadata_offset != insert {
2524 return Err(FileError::Container(
2525 "metadata section does not sit immediately after the header",
2526 ));
2527 }
2528 let old_len = header.metadata_len;
2529 let tail_start = (insert + old_len) as usize;
2530 if tail_start > image.len() {
2531 return Err(FileError::Container("metadata splice out of bounds"));
2532 }
2533 let new_len = metadata.len() as u64;
2534
2535 let shift = |off: &mut u64, len: u64| {
2536 if len > 0 && *off >= insert + old_len {
2537 *off = *off - old_len + new_len;
2538 }
2539 };
2540 shift(&mut header.dictionary_offset, header.dictionary_len);
2541 shift(&mut header.root_dir_offset, header.root_dir_len);
2542 shift(&mut header.pyramid_meta_offset, header.pyramid_meta_len);
2543 shift(&mut header.named_graphs_offset, header.named_graphs_len);
2544 shift(&mut header.text_index_offset, header.text_index_len);
2545 shift(&mut header.build_info_offset, header.build_info_len);
2546 for s in &mut header.extra_sections {
2547 if s.length > 0 && s.offset >= insert + old_len {
2548 s.offset = s.offset - old_len + new_len;
2549 }
2550 }
2551 header.metadata_offset = insert;
2552 header.metadata_len = new_len;
2553
2554 let mut out = Vec::with_capacity(image.len() - old_len as usize + metadata.len());
2555 out.extend_from_slice(&header.to_bytes());
2556 out.extend_from_slice(metadata);
2557 out.extend_from_slice(&image[tail_start..]);
2558 header.content_hash = hash_of_sections(&header, &out)?;
2562 out[..HEADER_LEN].copy_from_slice(&header.to_bytes());
2563 Ok(out)
2564}
2565
2566fn hash_of_sections(header: &Header, bytes: &[u8]) -> Result<[u8; 16], FileError> {
2572 let slice = |off: u64, len: u64| -> Result<&[u8], FileError> {
2573 let end = off.saturating_add(len);
2580 bytes
2581 .get(off as usize..end as usize)
2582 .ok_or(FileError::Container("section overruns buffer"))
2583 };
2584 let parts = content_hash_parts(header)
2585 .into_iter()
2586 .map(|(off, len)| slice(off, len))
2587 .collect::<Result<Vec<&[u8]>, FileError>>()?;
2588 Ok(content_hash(&parts))
2589}
2590
2591pub fn content_hash_parts(header: &Header) -> Vec<(u64, u64)> {
2604 let mut parts: Vec<(u64, u64)> = Vec::with_capacity(6);
2605 if header.metadata_len > 0 {
2606 parts.push((header.metadata_offset, header.metadata_len));
2607 }
2608 parts.push((header.dictionary_offset, header.dictionary_len));
2609 parts.push((header.root_dir_offset, header.root_dir_len));
2610 if header.pyramid_meta_len > 0 {
2613 parts.push((header.pyramid_meta_offset, header.pyramid_meta_len));
2614 }
2615 if header.text_index_len > 0 && !header.text_index_partial {
2617 parts.push((header.text_index_offset, header.text_index_len));
2618 }
2619 if header.named_graphs_len > 0 {
2620 parts.push((header.named_graphs_offset, header.named_graphs_len));
2621 }
2622 parts
2623}
2624
2625pub fn verify(bytes: &[u8]) -> Result<bool, FileError> {
2633 let header = Header::from_bytes(bytes)?;
2634 if hash_of_sections(&header, bytes)? != header.content_hash {
2635 return Ok(false);
2636 }
2637 if header.has_quad_index() || header.quad_index_section().is_some() {
2640 check_named_layout(&header)?;
2641 let slice = |off: u64, len: u64| -> Result<&[u8], FileError> {
2642 bytes
2643 .get(off as usize..off.saturating_add(len) as usize)
2644 .ok_or(FileError::Container("section overruns buffer"))
2645 };
2646 let root = slice(header.root_dir_offset, header.root_dir_len)?;
2647 let ranges = container_section_payload_ranges(
2648 root,
2649 header.root_dir_offset,
2650 header.index_container_sections(),
2651 )?;
2652 let seal = ranges
2653 .last()
2654 .ok_or(FileError::Container("missing quad-index seal"))?;
2655 let want = crate::quad::parse_seal(slice(seal.offset, seal.len)?)?;
2656 let q = header
2657 .quad_index_section()
2658 .ok_or(FileError::Container("missing quad-index section"))?;
2659 if crate::quad::digest(slice(q.offset, q.length)?) != want {
2660 return Ok(false);
2661 }
2662 }
2663 for s in self_digested_sections(&header) {
2664 let end = s.offset.saturating_add(s.length);
2665 let section = bytes
2666 .get(s.offset as usize..end as usize)
2667 .ok_or(FileError::Container("section overruns buffer"))?;
2668 if !crate::label_index::digest_ok(section) {
2669 return Ok(false);
2670 }
2671 }
2672 Ok(true)
2673}
2674
2675fn self_digested_sections(header: &Header) -> Vec<Section> {
2679 [
2680 header.label_index_section(),
2681 header.schema_section(),
2682 header
2683 .section(crate::header::SectionKind::TextIndexPartial)
2684 .filter(|s| s.length > 0),
2685 ]
2686 .into_iter()
2687 .flatten()
2688 .collect()
2689}
2690
2691pub fn append_sections(
2699 image: &[u8],
2700 sections: &[(SectionKind, &[u8])],
2701) -> Result<Vec<u8>, FileError> {
2702 let lens: Vec<(SectionKind, u64)> =
2703 sections.iter().map(|(k, b)| (*k, b.len() as u64)).collect();
2704 let (header, body_end) = plan_append(image, image.len() as u64, &lens)?;
2705 let extra: usize = sections.iter().map(|(_, b)| b.len()).sum();
2706 let mut out = Vec::with_capacity(image.len() + extra);
2707 out.extend_from_slice(&header.to_bytes());
2708 out.extend_from_slice(&image[HEADER_LEN..body_end as usize]);
2709 for (_, b) in sections {
2710 out.extend_from_slice(b);
2711 }
2712 out.extend_from_slice(&MAGIC);
2713 Ok(out)
2714}
2715
2716fn plan_append(
2718 head: &[u8],
2719 file_len: u64,
2720 sections: &[(SectionKind, u64)],
2721) -> Result<(Header, u64), FileError> {
2722 let mut header = Header::from_bytes(head)?;
2723 if header.expected_file_len() != Some(file_len) {
2724 return Err(FileError::Container(
2725 "cannot append sections: the file length does not match its header",
2726 ));
2727 }
2728 let body_end = file_len - MAGIC.len() as u64;
2729 let mut at = body_end;
2730 for &(kind, len) in sections {
2731 if !matches!(kind, SectionKind::LabelIndex | SectionKind::Schema) {
2732 return Err(FileError::Container(
2733 "only LABEL_INDEX and SCHEMA sections can be appended",
2734 ));
2735 }
2736 if len == 0 {
2737 continue;
2738 }
2739 if header.section(kind).is_some_and(|s| s.length > 0) {
2740 return Err(FileError::Container(
2741 "the file already carries that section",
2742 ));
2743 }
2744 header = header.with_section(kind, at, len);
2745 at += len;
2746 }
2747 Ok((header, body_end))
2748}
2749
2750#[cfg(not(target_arch = "wasm32"))]
2752pub(crate) enum PayloadPart<'a> {
2753 Bytes(&'a [u8]),
2755 File(&'a std::path::Path),
2757}
2758
2759#[cfg(not(target_arch = "wasm32"))]
2764pub(crate) fn append_sections_to_file(
2765 path: &std::path::Path,
2766 sections: &[(SectionKind, Vec<PayloadPart<'_>>)],
2767) -> Result<(), FileError> {
2768 use std::io::{Read, Seek, SeekFrom, Write};
2769 let part_len = |p: &PayloadPart<'_>| -> std::io::Result<u64> {
2770 match p {
2771 PayloadPart::Bytes(b) => Ok(b.len() as u64),
2772 PayloadPart::File(f) => Ok(std::fs::metadata(f)?.len()),
2773 }
2774 };
2775 let mut lens = Vec::with_capacity(sections.len());
2776 for (kind, parts) in sections {
2777 let mut n = 0u64;
2778 for p in parts {
2779 n += part_len(p)?;
2780 }
2781 lens.push((*kind, n));
2782 }
2783 let mut f = std::fs::OpenOptions::new()
2784 .read(true)
2785 .write(true)
2786 .open(path)?;
2787 let len = f.metadata()?.len();
2788 let mut head = vec![0u8; HEADER_LEN];
2789 f.read_exact(&mut head)?;
2790 let (header, body_end) = plan_append(&head, len, &lens)?;
2791 f.seek(SeekFrom::Start(body_end))?;
2792 let mut w = std::io::BufWriter::with_capacity(1 << 20, &mut f);
2793 for (_, parts) in sections {
2794 for p in parts {
2795 match p {
2796 PayloadPart::Bytes(b) => w.write_all(b)?,
2797 PayloadPart::File(src) => {
2798 std::io::copy(&mut std::fs::File::open(src)?, &mut w)?;
2799 }
2800 }
2801 }
2802 }
2803 w.write_all(&MAGIC)?;
2804 w.flush()?;
2805 drop(w);
2806 f.seek(SeekFrom::Start(0))?;
2807 f.write_all(&header.to_bytes())?;
2808 f.flush()?;
2809 Ok(())
2810}
2811
2812#[cfg(not(target_arch = "wasm32"))]
2817pub(crate) fn open_dictionary_lazy<R: RangeReader + Send + Sync + 'static>(
2818 reader: R,
2819) -> Result<Dictionary, FileError> {
2820 let head = reader.read_at(0, HEADER_LEN as u64)?;
2821 let header = Header::from_bytes(&head)?;
2822 ranged_chunked_dictionary(&std::sync::Arc::new(reader), &header, [true; 4])
2823}
2824
2825pub fn verify_ranged(reader: &dyn crate::reader::RangeReader) -> Result<bool, FileError> {
2831 const SLICE: u64 = 8 << 20;
2834 let len = reader.len();
2835 if len < HEADER_LEN as u64 {
2836 return Err(FileError::Container("file shorter than its header"));
2837 }
2838 let header = Header::from_bytes(&reader.read_at(0, HEADER_LEN as u64)?)?;
2839 let mut h = blake3::Hasher::new();
2840 for (off, n) in content_hash_parts(&header) {
2841 if off.checked_add(n).is_none_or(|end| end > len) {
2844 return Err(FileError::Container("section overruns buffer"));
2845 }
2846 let mut at = off;
2847 while at < off + n {
2848 let take = (off + n - at).min(SLICE);
2849 h.update(&reader.read_at(at, take)?);
2850 at += take;
2851 }
2852 }
2853 let mut out = [0u8; 16];
2854 out.copy_from_slice(&h.finalize().as_bytes()[..16]);
2855 if out != header.content_hash {
2856 return Ok(false);
2857 }
2858 if header.has_quad_index() || header.quad_index_section().is_some() {
2861 check_named_layout(&header)?;
2862 let sections = header.index_container_sections();
2863 let seal = locate_container_section_ranged(
2864 reader,
2865 header.root_dir_offset,
2866 header.root_dir_len,
2867 sections - 1,
2868 sections as u64,
2869 )?;
2870 let want = crate::quad::parse_seal(&reader.read_at(seal.offset, seal.len)?)?;
2871 let q = header
2872 .quad_index_section()
2873 .ok_or(FileError::Container("missing quad-index section"))?;
2874 if q.offset.checked_add(q.length).is_none_or(|end| end > len) {
2875 return Err(FileError::Container("section overruns buffer"));
2876 }
2877 let mut h = blake3::Hasher::new();
2878 let mut at = q.offset;
2879 while at < q.offset + q.length {
2880 let take = (q.offset + q.length - at).min(SLICE);
2881 h.update(&reader.read_at(at, take)?);
2882 at += take;
2883 }
2884 let mut got = [0u8; 16];
2885 got.copy_from_slice(&h.finalize().as_bytes()[..16]);
2886 if got != want {
2887 return Ok(false);
2888 }
2889 }
2890 for s in self_digested_sections(&header) {
2892 if s.length < 16 || s.offset.checked_add(s.length).is_none_or(|end| end > len) {
2893 return Err(FileError::Container("section overruns buffer"));
2894 }
2895 let want = reader.read_at(s.offset, 16)?;
2896 let mut h = blake3::Hasher::new();
2897 let mut at = s.offset + 16;
2898 while at < s.offset + s.length {
2899 let take = (s.offset + s.length - at).min(SLICE);
2900 h.update(&reader.read_at(at, take)?);
2901 at += take;
2902 }
2903 if h.finalize().as_bytes()[..16] != want[..] {
2904 return Ok(false);
2905 }
2906 }
2907 Ok(true)
2908}
2909
2910type PyramidLoader = Box<dyn Fn() -> Option<PyramidMeta> + Send + Sync>;
2912
2913enum PyramidSlot {
2919 Resident(Option<PyramidMeta>),
2920 Lazy {
2921 loader: PyramidLoader,
2922 cell: std::sync::OnceLock<Option<PyramidMeta>>,
2923 },
2924}
2925
2926pub(crate) enum SectionSlot<T> {
2931 Resident(Option<T>),
2932 Lazy {
2933 loader: Box<dyn Fn() -> Option<T> + Send + Sync>,
2934 cell: std::sync::OnceLock<Option<T>>,
2935 },
2936}
2937
2938impl<T> SectionSlot<T> {
2939 pub(crate) fn get(&self) -> Option<&T> {
2941 match self {
2942 SectionSlot::Resident(v) => v.as_ref(),
2943 SectionSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
2944 }
2945 }
2946
2947 pub(crate) fn get_if_loaded(&self) -> Option<&T> {
2949 match self {
2950 SectionSlot::Resident(v) => v.as_ref(),
2951 SectionSlot::Lazy { cell, .. } => cell.get().and_then(|o| o.as_ref()),
2952 }
2953 }
2954}
2955
2956fn resident_label_slot<'a>(
2958 header: &Header,
2959 region: &dyn Fn(u64, u64) -> Result<&'a [u8], FileError>,
2960) -> SectionSlot<crate::label_index::LabelIndex> {
2961 SectionSlot::Resident(header.label_index_section().and_then(|s| {
2962 let bytes = region(s.offset, s.length).ok()?;
2963 crate::label_index::LabelIndex::from_section(bytes, header.block_codec).ok()
2964 }))
2965}
2966
2967fn resident_schema_slot<'a>(
2969 header: &Header,
2970 region: &dyn Fn(u64, u64) -> Result<&'a [u8], FileError>,
2971) -> SectionSlot<crate::schema_section::SchemaMeta> {
2972 SectionSlot::Resident(header.schema_section().and_then(|s| {
2973 crate::schema_section::SchemaMeta::decode(region(s.offset, s.length).ok()?).ok()
2974 }))
2975}
2976
2977fn ranged_label_slot<R: RangeReader + Send + Sync + 'static>(
2981 reader: &std::sync::Arc<R>,
2982 header: &Header,
2983) -> SectionSlot<crate::label_index::LabelIndex> {
2984 let Some(s) = header.label_index_section() else {
2985 return SectionSlot::Resident(None);
2986 };
2987 let r = reader.clone();
2988 let codec = header.block_codec;
2989 SectionSlot::Lazy {
2990 loader: Box::new(move || {
2991 let head = r
2992 .read_at(s.offset, crate::label_index::HEAD_READ.min(s.length))
2993 .ok()?;
2994 let rr = r.clone();
2995 let loader = Box::new(move |off: u64, len: u64| rr.read_at(s.offset + off, len).ok());
2996 crate::label_index::LabelIndex::from_head(&head, s.length, codec, loader).ok()
2997 }),
2998 cell: std::sync::OnceLock::new(),
2999 }
3000}
3001
3002fn ranged_schema_slot<R: RangeReader + Send + Sync + 'static>(
3004 reader: &std::sync::Arc<R>,
3005 header: &Header,
3006) -> SectionSlot<crate::schema_section::SchemaMeta> {
3007 let Some(s) = header.schema_section() else {
3008 return SectionSlot::Resident(None);
3009 };
3010 let r = reader.clone();
3011 SectionSlot::Lazy {
3012 loader: Box::new(move || {
3013 let bytes = r.read_at(s.offset, s.length).ok()?;
3014 crate::schema_section::SchemaMeta::decode(&bytes).ok()
3015 }),
3016 cell: std::sync::OnceLock::new(),
3017 }
3018}
3019
3020pub fn read_schema_ranged<R: RangeReader + ?Sized>(
3024 reader: &R,
3025) -> Result<Option<crate::schema_section::SchemaMeta>, FileError> {
3026 let head = reader.read_at(0, HEADER_LEN as u64)?;
3027 let header = Header::from_bytes(&head)?;
3028 Ok(read_schema_with_header(reader, &header))
3029}
3030
3031fn read_schema_with_header<R: RangeReader + ?Sized>(
3032 reader: &R,
3033 header: &Header,
3034) -> Option<crate::schema_section::SchemaMeta> {
3035 let s = header.schema_section()?;
3036 let bytes = reader.read_at(s.offset, s.length).ok()?;
3037 crate::schema_section::SchemaMeta::decode(&bytes).ok()
3038}
3039
3040type TextIndexLoader = Box<dyn Fn() -> Option<crate::text_index::TextIndex> + Send + Sync>;
3043
3044type TokenTableProbe = Box<dyn Fn() -> Option<u64> + Send + Sync>;
3047
3048enum TextIndexSlot {
3058 Resident {
3059 index: Option<crate::text_index::TextIndex>,
3060 token_table_len: Option<u64>,
3063 },
3064 Lazy {
3065 loader: TextIndexLoader,
3066 cell: std::sync::OnceLock<Option<crate::text_index::TextIndex>>,
3067 token_table: TokenTableProbe,
3069 token_table_cell: std::sync::OnceLock<Option<u64>>,
3070 },
3071}
3072
3073impl TextIndexSlot {
3074 fn index(&self) -> Option<&crate::text_index::TextIndex> {
3076 match self {
3077 TextIndexSlot::Resident { index, .. } => index.as_ref(),
3078 TextIndexSlot::Lazy { loader, cell, .. } => cell.get_or_init(loader).as_ref(),
3079 }
3080 }
3081
3082 fn token_table_len(&self) -> Option<u64> {
3085 match self {
3086 TextIndexSlot::Resident {
3087 token_table_len, ..
3088 } => *token_table_len,
3089 TextIndexSlot::Lazy {
3090 token_table,
3091 token_table_cell,
3092 ..
3093 } => *token_table_cell.get_or_init(token_table),
3094 }
3095 }
3096}
3097
3098fn resident_text_index_slot(section: Option<&[u8]>, codec: u8) -> Result<TextIndexSlot, FileError> {
3101 let Some(section) = section else {
3102 return Ok(TextIndexSlot::Resident {
3103 index: None,
3104 token_table_len: None,
3105 });
3106 };
3107 Ok(TextIndexSlot::Resident {
3108 index: Some(
3109 crate::text_index::TextIndex::from_section(section, codec)
3110 .map_err(|_| FileError::Container("malformed text index"))?,
3111 ),
3112 token_table_len: crate::text_index::TextIndex::postings_base(section).map(|b| b as u64),
3113 })
3114}
3115
3116pub fn read_text_index_token_table_len_ranged<R: RangeReader + ?Sized>(
3129 reader: &R,
3130 header: &Header,
3131) -> Option<u64> {
3132 let (ti_off, ti_len) = header.text_index_payload();
3133 if ti_len == 0 {
3134 return None;
3135 }
3136 read_token_table_len(reader, ti_off, ti_len)
3137}
3138
3139fn read_token_table_len<R: RangeReader + ?Sized>(reader: &R, off: u64, len: u64) -> Option<u64> {
3143 let head = reader.read_at(off, 10u64.min(len)).ok()?;
3144 let (ttlen, n) = crate::varint::read_uvarint(&head)?;
3145 Some((n as u64 + ttlen).min(len))
3146}
3147
3148const NAMED_GRAPH_RESIDENT_MAX: u64 = 1 << 20; pub const TRANSIENT_GRAPH_QUADS: u64 = 1024;
3160
3161const NAMED_SLAB: usize = 1024;
3166
3167const NAMED_WALK_CHUNK: u64 = 64 * 1024;
3171
3172const NAMED_WALK_CHUNK_MAX: u64 = 8 * 1024 * 1024;
3185
3186#[derive(Default)]
3188struct NamedEntry {
3189 meta: std::sync::OnceLock<(String, ByteRange)>,
3192 index: std::sync::OnceLock<Box<GraphIndex>>,
3195}
3196
3197#[derive(Default)]
3201struct NamedWalk {
3202 next: usize,
3204 pos: u64,
3208 buf: Vec<u8>,
3215 buf_off: u64,
3216 chunk: u64,
3223 big_seen: bool,
3230}
3231
3232struct LazyNamedGraphs {
3246 reader: std::sync::Arc<dyn RangeReader + Send + Sync>,
3247 section: ByteRange,
3249 codec: u8,
3250 has_synopsis: bool,
3251 read_concurrency: usize,
3252 perms: PermSet,
3255 #[allow(clippy::type_complexity)]
3259 dir: std::sync::OnceLock<(usize, Box<[std::sync::OnceLock<Box<[NamedEntry]>>]>)>,
3260 walk: std::sync::Mutex<NamedWalk>,
3261 failed: std::sync::atomic::AtomicBool,
3262 tile_cap: std::sync::atomic::AtomicU64,
3268}
3269
3270impl LazyNamedGraphs {
3271 fn new(
3272 reader: std::sync::Arc<dyn RangeReader + Send + Sync>,
3273 section: ByteRange,
3274 codec: u8,
3275 has_synopsis: bool,
3276 read_concurrency: usize,
3277 perms: PermSet,
3278 ) -> Self {
3279 LazyNamedGraphs {
3280 reader,
3281 section,
3282 codec,
3283 has_synopsis,
3284 read_concurrency,
3285 perms,
3286 dir: std::sync::OnceLock::new(),
3287 walk: std::sync::Mutex::new(NamedWalk::default()),
3288 failed: std::sync::atomic::AtomicBool::new(false),
3289 tile_cap: std::sync::atomic::AtomicU64::new(u64::MAX),
3290 }
3291 }
3292
3293 fn set_tile_cap(&self, cap: u64) {
3297 self.tile_cap
3298 .store(cap, std::sync::atomic::Ordering::Relaxed);
3299 self.for_each_opened(|g| {
3300 if g.is_lazy() {
3301 g.set_cache_cap(cap);
3302 }
3303 });
3304 }
3305
3306 fn fail(&self) {
3308 self.failed
3309 .store(true, std::sync::atomic::Ordering::Relaxed);
3310 }
3311
3312 #[allow(clippy::type_complexity)]
3314 fn directory(&self) -> Option<&(usize, Box<[std::sync::OnceLock<Box<[NamedEntry]>>]>)> {
3315 if let Some(d) = self.dir.get() {
3316 return Some(d);
3317 }
3318 let end = self.section.offset.checked_add(self.section.len)?;
3319 let (n, used) = match read_uvarint_at(&self.reader, self.section.offset, end) {
3320 Ok(v) => v,
3321 Err(_) => {
3322 self.fail();
3323 return None;
3324 }
3325 };
3326 if n > self.section.len / 2 {
3329 self.fail();
3330 return None;
3331 }
3332 let n = n as usize;
3333 let slabs = n.div_ceil(NAMED_SLAB);
3334 let table: Box<[std::sync::OnceLock<Box<[NamedEntry]>>]> =
3335 (0..slabs).map(|_| std::sync::OnceLock::new()).collect();
3336 let _ = self.dir.set((n, table));
3337 {
3338 let mut w = self.walk.lock().unwrap();
3341 if w.pos == 0 {
3342 w.pos = self.section.offset + used;
3343 }
3344 }
3345 self.dir.get()
3346 }
3347
3348 fn count(&self) -> usize {
3349 self.directory().map(|(n, _)| *n).unwrap_or(0)
3350 }
3351
3352 fn entry(&self, i: usize) -> Option<&NamedEntry> {
3354 let (n, table) = self.directory()?;
3355 if i >= *n {
3356 return None;
3357 }
3358 let slab = table[i / NAMED_SLAB].get_or_init(|| {
3359 let len = NAMED_SLAB.min(n - (i / NAMED_SLAB) * NAMED_SLAB);
3360 (0..len).map(|_| NamedEntry::default()).collect()
3361 });
3362 slab.get(i % NAMED_SLAB)
3363 }
3364
3365 fn next_read_len(w: &mut NamedWalk, pos: u64, end: u64, need: u64) -> u64 {
3373 let cur = w.chunk.max(NAMED_WALK_CHUNK);
3374 w.chunk = (cur * 2).min(NAMED_WALK_CHUNK_MAX);
3375 cur.max(need).min(end - pos)
3376 }
3377
3378 fn ensure_meta(&self, upto: usize, exhaustive: bool) -> Option<()> {
3393 let target = self.entry(upto)?;
3394 if target.meta.get().is_some() {
3395 return Some(());
3396 }
3397 let end = self.section.offset.checked_add(self.section.len)?;
3398 let mut w = self.walk.lock().unwrap();
3399 if exhaustive && !w.big_seen {
3400 w.chunk = NAMED_WALK_CHUNK_MAX;
3401 }
3402 while w.next <= upto {
3403 let pos = w.pos;
3404 if pos >= end {
3405 self.fail();
3407 return None;
3408 }
3409 let have =
3412 |b: &[u8], off: u64, need: u64| pos >= off && pos + need <= off + b.len() as u64;
3413 if !have(&w.buf, w.buf_off, 20.min(end - pos)) {
3414 let len = Self::next_read_len(&mut w, pos, end, 20.min(end - pos));
3415 w.buf = match self.reader.read_at(pos, len) {
3416 Ok(b) => b,
3417 Err(_) => {
3418 self.fail();
3419 return None;
3420 }
3421 };
3422 w.buf_off = pos;
3423 }
3424 let rel = (pos - w.buf_off) as usize;
3425 let Some((ilen, u1)) = read_uvarint(&w.buf[rel..]) else {
3426 self.fail();
3427 return None;
3428 };
3429 let header_need = u1 as u64 + ilen + 10; if pos + u1 as u64 + ilen > end {
3431 self.fail(); return None;
3433 }
3434 if !have(&w.buf, w.buf_off, header_need.min(end - pos)) {
3435 let len = Self::next_read_len(&mut w, pos, end, header_need);
3436 w.buf = match self.reader.read_at(pos, len) {
3437 Ok(b) => b,
3438 Err(_) => {
3439 self.fail();
3440 return None;
3441 }
3442 };
3443 w.buf_off = pos;
3444 }
3445 let rel = (pos - w.buf_off) as usize;
3446 let istart = rel + u1;
3447 let iend = istart + ilen as usize;
3448 let iri = String::from_utf8_lossy(&w.buf[istart..iend]).into_owned();
3449 let Some((clen, u2)) = read_uvarint(&w.buf[iend..]) else {
3450 self.fail();
3451 return None;
3452 };
3453 let cstart = pos + u1 as u64 + ilen + u2 as u64;
3454 let cend = match cstart.checked_add(clen) {
3455 Some(e) if e <= end => e,
3456 _ => {
3457 self.fail(); return None;
3459 }
3460 };
3461 if clen > NAMED_GRAPH_RESIDENT_MAX {
3462 w.big_seen = true;
3467 w.chunk = NAMED_WALK_CHUNK;
3468 }
3469 let range = ByteRange {
3470 offset: cstart,
3471 len: clen,
3472 };
3473 if let Some(e) = self.entry(w.next) {
3474 let _ = e.meta.set((iri, range));
3475 }
3476 w.pos = cend;
3477 w.next += 1;
3478 }
3479 Some(())
3480 }
3481
3482 fn name_at(&self, i: usize, exhaustive: bool) -> Option<&str> {
3485 self.ensure_meta(i, exhaustive)?;
3486 self.entry(i)?.meta.get().map(|(iri, _)| iri.as_str())
3487 }
3488
3489 fn graph_at(&self, i: usize, exhaustive: bool) -> Option<(&str, &GraphIndex)> {
3492 self.ensure_meta(i, exhaustive)?;
3493 let e = self.entry(i)?;
3494 let (iri, range) = e.meta.get()?;
3495 if e.index.get().is_none() {
3496 let opened = self.open_graph(*range)?;
3497 let _ = e.index.set(Box::new(opened));
3498 }
3499 Some((iri.as_str(), e.index.get()?.as_ref()))
3500 }
3501
3502 fn container_bytes(&self, range: ByteRange) -> Option<Vec<u8>> {
3506 {
3507 let w = self.walk.lock().unwrap();
3508 let buf_end = w.buf_off + w.buf.len() as u64;
3509 if range.offset >= w.buf_off && range.offset + range.len <= buf_end {
3510 let a = (range.offset - w.buf_off) as usize;
3511 return Some(w.buf[a..a + range.len as usize].to_vec());
3512 }
3513 }
3514 match self.reader.read_at(range.offset, range.len) {
3515 Ok(b) => Some(b),
3516 Err(_) => {
3517 self.fail();
3518 None
3519 }
3520 }
3521 }
3522
3523 fn open_graph(&self, range: ByteRange) -> Option<GraphIndex> {
3528 if range.len <= NAMED_GRAPH_RESIDENT_MAX {
3529 let bytes = self.container_bytes(range)?;
3530 match decode_index_container(&bytes, self.codec, self.perms) {
3531 Ok(g) => Some(g),
3532 Err(_) => {
3533 self.fail();
3534 None
3535 }
3536 }
3537 } else {
3538 match open_index_container_lazy(
3539 &self.reader,
3540 range,
3541 self.codec,
3542 self.has_synopsis,
3543 self.read_concurrency,
3544 self.perms,
3545 self.perms.len(),
3546 ) {
3547 Ok((g, _, _)) => {
3548 let cap = self.tile_cap.load(std::sync::atomic::Ordering::Relaxed);
3552 if cap != u64::MAX {
3553 g.set_cache_cap(cap);
3554 }
3555 Some(g)
3556 }
3557 Err(_) => {
3558 self.fail();
3559 None
3560 }
3561 }
3562 }
3563 }
3564
3565 fn release(&mut self, iri: &str) {
3571 let Some((_, table)) = self.dir.get_mut() else {
3572 return;
3573 };
3574 for slab in table.iter_mut() {
3575 let Some(entries) = slab.get_mut() else {
3576 continue;
3577 };
3578 for e in entries.iter_mut() {
3579 let matches = e
3580 .meta
3581 .get()
3582 .map(|(name, _)| name.as_str() == iri)
3583 .unwrap_or(false);
3584 if matches {
3585 e.index.take();
3586 return;
3587 }
3588 }
3589 }
3590 }
3591
3592 fn for_each_opened(&self, mut f: impl FnMut(&GraphIndex)) {
3594 if let Some((_, table)) = self.dir.get() {
3595 for slab in table.iter().filter_map(|s| s.get()) {
3596 for e in slab.iter() {
3597 if let Some(g) = e.index.get() {
3598 f(g);
3599 }
3600 }
3601 }
3602 }
3603 }
3604}
3605
3606enum NamedGraphsSlot {
3611 Resident(Vec<(String, GraphIndex)>),
3612 Lazy(LazyNamedGraphs),
3613 Quad(crate::quad::QuadNamed),
3616}
3617
3618impl NamedGraphsSlot {
3619 fn directory_bytes(&self) -> u64 {
3623 match self {
3624 NamedGraphsSlot::Resident(v) => v
3625 .iter()
3626 .map(|(iri, g)| iri.capacity() as u64 + g.directory_bytes())
3627 .sum(),
3628 NamedGraphsSlot::Lazy(l) => {
3629 let mut bytes = l.walk.lock().map(|w| w.buf.capacity() as u64).unwrap_or(0);
3630 if let Some((_, table)) = l.dir.get() {
3631 bytes += table.len() as u64
3632 * std::mem::size_of::<std::sync::OnceLock<Box<[NamedEntry]>>>() as u64;
3633 for slab in table.iter().filter_map(|s| s.get()) {
3634 bytes += slab.len() as u64 * std::mem::size_of::<NamedEntry>() as u64;
3635 for e in slab.iter() {
3636 if let Some((iri, _)) = e.meta.get() {
3637 bytes += iri.capacity() as u64;
3638 }
3639 if let Some(g) = e.index.get() {
3640 bytes += g.directory_bytes();
3641 }
3642 }
3643 }
3644 }
3645 bytes
3646 }
3647 NamedGraphsSlot::Quad(q) => q.directory_bytes(),
3648 }
3649 }
3650
3651 fn count(&self) -> usize {
3652 match self {
3653 NamedGraphsSlot::Resident(v) => v.len(),
3654 NamedGraphsSlot::Lazy(l) => l.count(),
3655 NamedGraphsSlot::Quad(q) => q.count(),
3656 }
3657 }
3658
3659 fn name_at<'a>(&'a self, dict: &Dictionary, i: usize, exhaustive: bool) -> Option<&'a str> {
3663 match self {
3664 NamedGraphsSlot::Resident(v) => v.get(i).map(|(iri, _)| iri.as_str()),
3665 NamedGraphsSlot::Lazy(l) => l.name_at(i, exhaustive),
3666 NamedGraphsSlot::Quad(q) => q.name_at(dict, i, exhaustive),
3667 }
3668 }
3669
3670 fn graph_at<'a>(
3671 &'a self,
3672 dict: &Dictionary,
3673 i: usize,
3674 exhaustive: bool,
3675 ) -> Option<(&'a str, &'a GraphIndex)> {
3676 match self {
3677 NamedGraphsSlot::Resident(v) => v.get(i).map(|(iri, g)| (iri.as_str(), g)),
3678 NamedGraphsSlot::Lazy(l) => l.graph_at(i, exhaustive),
3679 NamedGraphsSlot::Quad(q) => q.graph_at(dict, i, exhaustive),
3680 }
3681 }
3682
3683 fn find<'a>(&'a self, dict: &Dictionary, iri: &str) -> Option<&'a GraphIndex> {
3684 match self {
3685 NamedGraphsSlot::Quad(q) => q.find(dict, iri),
3686 NamedGraphsSlot::Resident(v) => v.iter().find(|(name, _)| name == iri).map(|(_, g)| g),
3687 NamedGraphsSlot::Lazy(l) => {
3688 for i in 0..l.count() {
3693 if l.name_at(i, false)? == iri {
3694 return l.graph_at(i, false).map(|(_, g)| g);
3695 }
3696 }
3697 None
3698 }
3699 }
3700 }
3701
3702 fn load_incomplete(&self) -> bool {
3703 match self {
3704 NamedGraphsSlot::Resident(v) => v.iter().any(|(_, g)| g.load_incomplete()),
3705 NamedGraphsSlot::Lazy(l) => {
3706 if l.failed.load(std::sync::atomic::Ordering::Relaxed) {
3707 return true;
3708 }
3709 let mut bad = false;
3710 l.for_each_opened(|g| bad |= g.load_incomplete());
3711 bad
3712 }
3713 NamedGraphsSlot::Quad(q) => q.load_incomplete(),
3714 }
3715 }
3716
3717 fn reset_load_failures(&self) {
3718 match self {
3719 NamedGraphsSlot::Resident(v) => {
3720 for (_, g) in v {
3721 g.reset_load_failure();
3722 }
3723 }
3724 NamedGraphsSlot::Lazy(l) => {
3725 l.failed.store(false, std::sync::atomic::Ordering::Relaxed);
3726 l.for_each_opened(|g| g.reset_load_failure());
3727 }
3728 NamedGraphsSlot::Quad(q) => q.reset_load_failures(),
3729 }
3730 }
3731
3732 fn resident(
3736 header: &Header,
3737 quad_section: impl FnOnce(u64, u64) -> Result<Vec<u8>, FileError>,
3738 per_graph: impl FnOnce() -> Result<Vec<(String, GraphIndex)>, FileError>,
3739 ) -> Result<Self, FileError> {
3740 check_named_layout(header)?;
3741 if let Some(q) = header.quad_index_section() {
3742 let bytes = quad_section(q.offset, q.length)?;
3743 return Ok(NamedGraphsSlot::Quad(crate::quad::QuadNamed::resident(
3744 bytes,
3745 header.block_codec,
3746 header.perms,
3747 )));
3748 }
3749 Ok(NamedGraphsSlot::Resident(if header.named_graphs_len > 0 {
3750 per_graph()?
3751 } else {
3752 Vec::new()
3753 }))
3754 }
3755}
3756
3757pub struct Rete {
3759 header: Header,
3760 dict: Dictionary,
3761 index: GraphIndex,
3762 index_section_ranges: [ByteRange; NUM_PERMS],
3763 tile_ranges: [Vec<(u32, u32, ByteRange)>; NUM_PERMS],
3767 pyramid: PyramidSlot,
3768 text_index: TextIndexSlot,
3769 labels: SectionSlot<crate::label_index::LabelIndex>,
3771 schema: SectionSlot<crate::schema_section::SchemaMeta>,
3773 named_graphs: NamedGraphsSlot,
3774 metadata: Vec<u8>,
3779 service_client: Option<Box<dyn crate::service::ServiceClient>>,
3784 service_error: std::sync::Mutex<Option<String>>,
3788 spill_threshold: std::sync::atomic::AtomicU64,
3791}
3792
3793impl Rete {
3794 pub fn open(bytes: &[u8]) -> Result<Self, FileError> {
3797 let header = Header::from_bytes(bytes)?;
3798
3799 let region = |off: u64, len: u64| -> Result<&[u8], FileError> {
3803 let start = off as usize;
3804 let end = start
3805 .checked_add(len as usize)
3806 .filter(|&e| e <= bytes.len())
3807 .ok_or(FileError::Container("section range out of bounds"))?;
3808 Ok(&bytes[start..end])
3809 };
3810
3811 let dict = decode_dictionary_container(
3812 region(header.dictionary_offset, header.dictionary_len)?,
3813 header.dict_codec,
3814 )?;
3815
3816 check_named_layout(&header)?;
3817 let index_bytes = region(header.root_dir_offset, header.root_dir_len)?;
3818 let (index, _) = decode_root_index_container(index_bytes, &header)?;
3819 let index_section_ranges = decode_index_section_ranges(
3820 index_bytes,
3821 header.root_dir_offset,
3822 header.perms,
3823 header.index_container_sections(),
3824 )?;
3825
3826 let pyramid = PyramidSlot::Resident(if header.pyramid_meta_len > 0 {
3827 Some(
3828 PyramidMeta::decode(region(header.pyramid_meta_offset, header.pyramid_meta_len)?)
3829 .map_err(|_| FileError::Container("malformed pyramid meta"))?,
3830 )
3831 } else {
3832 None
3833 });
3834
3835 let (ti_off, ti_len) = header.text_index_payload();
3838 let text_index = resident_text_index_slot(
3839 if ti_len > 0 {
3840 Some(region(ti_off, ti_len)?)
3841 } else {
3842 None
3843 },
3844 header.block_codec,
3845 )?;
3846 let labels = resident_label_slot(&header, ®ion);
3847 let schema = resident_schema_slot(&header, ®ion);
3848
3849 let named_graphs = NamedGraphsSlot::resident(
3850 &header,
3851 |off, len| Ok(region(off, len)?.to_vec()),
3852 || {
3853 decode_named_graphs(
3854 region(header.named_graphs_offset, header.named_graphs_len)?,
3855 header.block_codec,
3856 header.perms,
3857 )
3858 },
3859 )?;
3860
3861 let metadata = if header.metadata_len > 0 {
3862 region(header.metadata_offset, header.metadata_len)?.to_vec()
3863 } else {
3864 Vec::new()
3865 };
3866
3867 let tile_ranges =
3868 tile_file_ranges(index_bytes, header.root_dir_offset, &index_section_ranges);
3869 Ok(Self {
3870 header,
3871 dict,
3872 index,
3873 index_section_ranges,
3874 tile_ranges,
3875 pyramid,
3876 text_index,
3877 labels,
3878 schema,
3879 named_graphs,
3880 metadata,
3881 service_client: None,
3882 service_error: std::sync::Mutex::new(None),
3883 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
3884 })
3885 }
3886
3887 pub fn set_service_client(&mut self, client: Box<dyn crate::service::ServiceClient>) {
3892 self.service_client = Some(client);
3893 }
3894
3895 pub(crate) fn service_client(&self) -> Option<&dyn crate::service::ServiceClient> {
3896 self.service_client.as_deref()
3897 }
3898
3899 pub(crate) fn record_service_error(&self, msg: &str) {
3901 let mut e = self.service_error.lock().unwrap();
3902 if e.is_none() {
3903 *e = Some(msg.to_string());
3904 }
3905 }
3906
3907 pub(crate) fn take_service_error(&self) -> Option<String> {
3910 self.service_error.lock().unwrap().take()
3911 }
3912
3913 pub fn header(&self) -> &Header {
3914 &self.header
3915 }
3916
3917 pub fn file_layout(&self) -> Vec<LayoutSegment> {
3923 let h = &self.header;
3924 let seg = |kind: &'static str, label: String, offset: u64, len: u64| LayoutSegment {
3925 kind,
3926 label,
3927 offset,
3928 len,
3929 };
3930 let mut out = vec![seg(
3936 "header",
3937 format!("header (fixed {} bytes)", crate::header::HEADER_LEN),
3938 0,
3939 crate::header::HEADER_LEN as u64,
3940 )];
3941 if h.metadata_len > 0 {
3942 out.push(seg(
3943 "metadata",
3944 "metadata (dataset card)".into(),
3945 h.metadata_offset,
3946 h.metadata_len,
3947 ));
3948 }
3949 out.push(seg(
3950 "dictionary",
3951 "dictionary (4 front-coded term sections)".into(),
3952 h.dictionary_offset,
3953 h.dictionary_len,
3954 ));
3955 for (si, perm) in crate::index::ALL_PERMS.into_iter().enumerate() {
3956 let sec = self.index_section_ranges[si];
3957 if sec.len == 0 {
3958 continue;
3959 }
3960 let first_tile = self.tile_ranges[si]
3961 .first()
3962 .map(|&(_, _, r)| r.offset)
3963 .unwrap_or(sec.offset + sec.len);
3964 if first_tile > sec.offset {
3965 out.push(seg(
3966 "directory",
3967 format!("{} tile directory", perm.name()),
3968 sec.offset,
3969 first_tile - sec.offset,
3970 ));
3971 }
3972 for (ti, &(min_a, max_a, r)) in self.tile_ranges[si].iter().enumerate() {
3973 out.push(seg(
3974 "tile",
3975 format!("{} tile {ti} (leading ids {min_a}..{max_a})", perm.name()),
3976 r.offset,
3977 r.len,
3978 ));
3979 }
3980 }
3981 if h.pyramid_meta_len > 0 {
3982 out.push(seg(
3983 "pyramid",
3984 "pyramid summary (communities + superedges)".into(),
3985 h.pyramid_meta_offset,
3986 h.pyramid_meta_len,
3987 ));
3988 }
3989 if h.named_graphs_len > 0 {
3990 out.push(seg(
3991 "named-graphs",
3992 format!("named graphs ({})", self.named_graphs.count()),
3993 h.named_graphs_offset,
3994 h.named_graphs_len,
3995 ));
3996 }
3997 if let Some(s) = h.label_index_section() {
3998 let label = match self.labels.get_if_loaded() {
3999 Some(li) => format!("label index ({} entries)", li.entry_count()),
4000 None => "label index".to_string(),
4001 };
4002 out.push(seg("label-index", label, s.offset, s.length));
4003 }
4004 if let Some(s) = h.schema_section() {
4005 out.push(seg(
4006 "schema",
4007 "schema (classes, query_stats, shapes)".into(),
4008 s.offset,
4009 s.length,
4010 ));
4011 }
4012 if let Some(q) = h.quad_index_section() {
4013 out.push(seg(
4014 "quad-index",
4015 format!("quad index ({} named graphs)", self.named_graphs.count()),
4016 q.offset,
4017 q.length,
4018 ));
4019 }
4020 out.sort_by_key(|s| s.offset);
4021 out
4022 }
4023
4024 pub fn metadata(&self) -> Option<&[u8]> {
4029 if self.metadata.is_empty() {
4030 None
4031 } else {
4032 Some(&self.metadata)
4033 }
4034 }
4035
4036 pub fn dictionary(&self) -> &Dictionary {
4037 &self.dict
4038 }
4039
4040 pub fn pyramid(&self) -> Option<&PyramidMeta> {
4042 match &self.pyramid {
4043 PyramidSlot::Resident(p) => p.as_ref(),
4044 PyramidSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
4046 }
4047 }
4048
4049 pub fn pyramid_if_loaded(&self) -> Option<&PyramidMeta> {
4054 match &self.pyramid {
4055 PyramidSlot::Resident(p) => p.as_ref(),
4056 PyramidSlot::Lazy { cell, .. } => cell.get().and_then(|o| o.as_ref()),
4057 }
4058 }
4059
4060 pub fn schema(&self) -> Option<&crate::schema_section::SchemaMeta> {
4066 self.schema.get()
4067 }
4068
4069 pub fn schema_or_pyramid(
4073 &self,
4074 ) -> Option<std::borrow::Cow<'_, crate::schema_section::SchemaMeta>> {
4075 if let Some(s) = self.schema() {
4076 return Some(std::borrow::Cow::Borrowed(s));
4077 }
4078 self.pyramid()
4079 .map(|p| std::borrow::Cow::Owned(crate::schema_section::SchemaMeta::from_pyramid(p)))
4080 }
4081
4082 pub fn predicate_stats(&self) -> &[crate::meta::PredStat] {
4087 if let Some(s) = self.schema().filter(|s| !s.predicate_stats.is_empty()) {
4088 return &s.predicate_stats;
4089 }
4090 self.pyramid_if_loaded()
4091 .map(|p| p.predicate_stats.as_slice())
4092 .unwrap_or(&[])
4093 }
4094
4095 pub(crate) fn planner_stats(&self, fault: bool) -> &[crate::meta::PredStat] {
4100 let schema = if fault {
4101 self.schema.get()
4102 } else {
4103 self.schema.get_if_loaded()
4104 };
4105 if let Some(s) = schema.filter(|s| !s.predicate_stats.is_empty()) {
4106 return &s.predicate_stats;
4107 }
4108 self.pyramid_if_loaded()
4109 .map(|p| p.predicate_stats.as_slice())
4110 .unwrap_or(&[])
4111 }
4112
4113 pub fn char_sets(&self) -> &[crate::meta::CharSet] {
4116 if let Some(s) = self.schema().filter(|s| !s.char_sets.is_empty()) {
4117 return &s.char_sets;
4118 }
4119 self.pyramid_if_loaded()
4120 .map(|p| p.char_sets.as_slice())
4121 .unwrap_or(&[])
4122 }
4123
4124 pub fn label_index(&self) -> &[crate::meta::LabelEntry] {
4130 self.pyramid_if_loaded()
4131 .map(|p| p.label_index.as_slice())
4132 .unwrap_or(&[])
4133 }
4134
4135 pub fn has_label_index(&self) -> bool {
4137 self.header.label_index_section().is_some()
4138 }
4139
4140 pub fn label_index_section(&self) -> Option<&crate::label_index::LabelIndex> {
4142 self.labels.get()
4143 }
4144
4145 pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
4152 if let Some(li) = self.labels.get() {
4153 return li.prefix_search(prefix, limit);
4154 }
4155 let Some(pyr) = self.pyramid() else {
4156 return Vec::new();
4157 };
4158 pyr.prefix_search(prefix, limit)
4159 .into_iter()
4160 .filter_map(|e| {
4161 self.dict
4162 .subject_term(e.subject)
4163 .map(|iri| (e.label.clone(), iri))
4164 })
4165 .collect()
4166 }
4167
4168 pub(crate) fn text_index(&self) -> Option<&crate::text_index::TextIndex> {
4171 self.text_index.index()
4172 }
4173
4174 pub fn has_text_index(&self) -> bool {
4177 self.header.text_index_len > 0
4178 }
4179
4180 pub fn text_index_scope(&self) -> Option<crate::text_index::TextIndexScope> {
4184 self.text_index().map(|ti| ti.scope().clone())
4185 }
4186
4187 pub fn text_index_token_table_len(&self) -> Option<u64> {
4202 self.text_index.token_table_len()
4203 }
4204
4205 pub fn text_search(&self, words: &[&str], prefix: Option<&str>, limit: usize) -> Vec<String> {
4215 text_search_in(self.text_index(), &self.dict, words, prefix, limit)
4216 }
4217
4218 pub fn default_index(&self) -> &GraphIndex {
4220 &self.index
4221 }
4222
4223 pub fn dump(&self, graph: Option<&str>) -> Vec<TermTriple> {
4225 let index = match graph {
4226 None => &self.index,
4227 Some(g) => match self.graph_index(g) {
4228 Some(i) => i,
4229 None => return Vec::new(),
4230 },
4231 };
4232 index
4233 .match_pattern((None, None, None))
4234 .into_iter()
4235 .filter_map(|(s, p, o)| {
4236 Some((
4237 self.dict.subject_term(s)?,
4238 self.dict.predicate_term(p)?,
4239 self.dict.object_term(o)?,
4240 ))
4241 })
4242 .collect()
4243 }
4244
4245 pub fn dump_each<F: FnMut(&str, &str, &str)>(&self, graph: Option<&str>, f: F) {
4261 self.dump_filtered_each(graph, None, None, None, f)
4262 }
4263
4264 pub fn dump_filtered_each<F: FnMut(&str, &str, &str)>(
4335 &self,
4336 graph: Option<&str>,
4337 s: Option<&str>,
4338 p: Option<&str>,
4339 o: Option<&str>,
4340 mut f: F,
4341 ) {
4342 let index = match graph {
4343 None => Some(&self.index),
4344 Some(g) => self.graph_index(g),
4345 };
4346 let Some(index) = index else { return };
4347 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
4350 return;
4351 };
4352 let dict = &self.dict;
4353 const WINDOW: usize = 4096;
4398 let cap = dict.cache_cap();
4409 let prefetch_window =
4410 cap == u64::MAX || cap >= (WINDOW as u64) * 2 * (DICT_CHUNK_BUDGET as u64);
4411 const PREFETCH_BATCH: usize = 64;
4418 let prefetch_batch = if cap == u64::MAX {
4419 usize::MAX
4420 } else {
4421 PREFETCH_BATCH
4422 };
4423 let mut ids: Vec<(u32, u32, u32)> = Vec::with_capacity(WINDOW);
4424 let mut nodes: Vec<u32> = Vec::with_capacity(WINDOW * 2);
4425 let mut preds: Vec<u32> = Vec::with_capacity(WINDOW);
4426 let mut resolver = crate::dictionary::WindowResolver::default();
4427 let mut flush = |ids: &mut Vec<(u32, u32, u32)>,
4428 resolver: &mut crate::dictionary::WindowResolver,
4429 f: &mut F| {
4430 if ids.is_empty() {
4431 return;
4432 }
4433 if prefetch_window {
4437 nodes.clear();
4438 preds.clear();
4439 for &(s, p, o) in ids.iter() {
4440 nodes.push(dict.subject_node(s));
4441 nodes.push(dict.object_node(o));
4442 preds.push(p);
4443 }
4444 dict.prefetch_terms_batched(&nodes, &preds, prefetch_batch);
4445 }
4446 dict.resolve_window(ids, resolver, |s, p, o| f(s, p, o));
4447 ids.clear();
4448 };
4449 for t in index.scan_iter(pattern) {
4450 ids.push(t);
4451 if ids.len() == WINDOW {
4452 flush(&mut ids, &mut resolver, &mut f);
4453 }
4454 }
4455 flush(&mut ids, &mut resolver, &mut f);
4456 }
4457
4458 pub fn dump_plan(
4466 &self,
4467 graph: Option<&str>,
4468 s: Option<&str>,
4469 p: Option<&str>,
4470 o: Option<&str>,
4471 ) -> DumpPlan {
4472 let index = match graph {
4473 None => Some(&self.index),
4474 Some(g) => self.graph_index(g),
4475 };
4476 let scan = index
4477 .zip(self.resolve_query_pattern(s, p, o))
4478 .map(|(ix, pattern)| ix.scan_plan(pattern));
4479 DumpPlan {
4480 scan,
4481 dictionary_bytes: self.header.dictionary_len,
4482 }
4483 }
4484
4485 pub fn dump_iter(&self, graph: Option<&str>) -> impl Iterator<Item = TermTriple> + '_ {
4503 self.query_iter(graph, None, None, None)
4504 }
4505
4506 pub fn query_iter(
4517 &self,
4518 graph: Option<&str>,
4519 s: Option<&str>,
4520 p: Option<&str>,
4521 o: Option<&str>,
4522 ) -> impl Iterator<Item = TermTriple> + '_ {
4523 let index = match graph {
4524 None => Some(&self.index),
4525 Some(g) => self.graph_index(g),
4526 };
4527 index
4528 .zip(self.resolve_query_pattern(s, p, o))
4529 .into_iter()
4530 .flat_map(|(ix, pattern)| ix.scan_iter(pattern))
4531 .filter_map(move |(s, p, o)| {
4532 Some((
4533 self.dict.subject_term(s)?,
4534 self.dict.predicate_term(p)?,
4535 self.dict.object_term(o)?,
4536 ))
4537 })
4538 }
4539
4540 pub fn query_batch(
4559 &self,
4560 graph: Option<&str>,
4561 s: Option<&str>,
4562 p: Option<&str>,
4563 o: Option<&str>,
4564 cursor: u64,
4565 max_quads: usize,
4566 ) -> (Vec<TermTriple>, u64, bool) {
4567 let index = match graph {
4568 None => &self.index,
4569 Some(g) => match self.graph_index(g) {
4572 Some(i) => i,
4573 None => return (Vec::new(), cursor, true),
4574 },
4575 };
4576 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
4579 return (Vec::new(), cursor, true);
4580 };
4581 let (ids, next, done) = index.scan_batch(pattern, cursor, max_quads);
4582 if !ids.is_empty() {
4586 let mut nodes = Vec::with_capacity(ids.len() * 2);
4587 let mut preds = Vec::with_capacity(ids.len());
4588 for &(s, p, o) in &ids {
4589 nodes.push(self.dict.subject_node(s));
4590 nodes.push(self.dict.object_node(o));
4591 preds.push(p);
4592 }
4593 self.dict.prefetch_terms(&nodes, &preds);
4594 }
4595 let triples = ids
4596 .into_iter()
4597 .filter_map(|(s, p, o)| {
4598 Some((
4599 self.dict.subject_term(s)?,
4600 self.dict.predicate_term(p)?,
4601 self.dict.object_term(o)?,
4602 ))
4603 })
4604 .collect();
4605 (triples, next, done)
4606 }
4607
4608 pub fn dump_batch(
4634 &self,
4635 graph: Option<&str>,
4636 cursor: u32,
4637 max_quads: usize,
4638 ) -> (Vec<TermTriple>, u32, bool) {
4639 fn next_tile_start(tiles: &[crate::index::Tile], sid: u32) -> Option<u32> {
4644 let i = tiles.partition_point(|t| t.leading_range().1 < sid);
4645 tiles.get(i).map(|t| t.leading_range().0.max(sid))
4646 }
4647
4648 let index = match graph {
4649 None => self.default_index(),
4650 Some(g) => match self.graph_index(g) {
4653 Some(i) => i,
4654 None => return (Vec::new(), cursor, true),
4655 },
4656 };
4657 let tiles = index.tile_sections()[IndexPermutation::Spo.section_index()];
4661 let (Some(first), Some(last)) = (
4662 tiles.first().map(|t| t.leading_range().0),
4663 tiles.last().map(|t| t.leading_range().1),
4664 ) else {
4665 return (Vec::new(), cursor, true); };
4667
4668 let mut sid = cursor.max(first);
4669 let mut probes = max_quads.saturating_mul(4).max(1 << 16);
4670 let mut ids: Vec<(u32, u32, u32)> = Vec::new();
4671 let mut exhausted = false;
4672 while sid <= last && probes > 0 {
4673 probes -= 1;
4674 let hits = index.match_pattern((Some(sid), None, None));
4675 if hits.is_empty() {
4676 match index.next_leading(IndexPermutation::Spo.section_index(), sid) {
4686 Some(next) if next > sid => {
4687 sid = next;
4688 continue;
4689 }
4690 None if !index.load_incomplete() => {
4691 exhausted = true;
4692 break;
4693 }
4694 _ => {}
4695 }
4696 match next_tile_start(tiles, sid) {
4697 None => {
4698 exhausted = true;
4699 break;
4700 }
4701 Some(next) if next > sid => {
4702 sid = next;
4703 continue;
4704 }
4705 Some(_) => {}
4706 }
4707 } else {
4708 ids.extend(hits);
4709 }
4710 if sid == u32::MAX {
4711 exhausted = true;
4712 break;
4713 }
4714 sid += 1;
4715 if ids.len() >= max_quads {
4716 break;
4717 }
4718 }
4719 let done = exhausted || sid > last;
4720
4721 let dict = self.dictionary();
4728 if !ids.is_empty() {
4729 let mut nodes = Vec::with_capacity(ids.len() * 2);
4730 let mut preds = Vec::with_capacity(ids.len());
4731 for &(s, p, o) in &ids {
4732 nodes.push(dict.subject_node(s));
4733 nodes.push(dict.object_node(o));
4734 preds.push(p);
4735 }
4736 dict.prefetch_terms(&nodes, &preds);
4737 }
4738 let triples = ids
4739 .into_iter()
4740 .filter_map(|(s, p, o)| {
4741 Some((
4742 dict.subject_term(s)?,
4743 dict.predicate_term(p)?,
4744 dict.object_term(o)?,
4745 ))
4746 })
4747 .collect();
4748 (triples, sid, done)
4749 }
4750
4751 pub fn named_graph_count(&self) -> usize {
4754 self.named_graphs.count()
4755 }
4756
4757 pub fn named_graph_quad_count(&self, i: usize) -> Option<u64> {
4762 match &self.named_graphs {
4763 NamedGraphsSlot::Quad(q) => q.quad_count_at(i),
4764 _ => None,
4765 }
4766 }
4767
4768 pub fn named_graph_transient(&self, i: usize) -> Option<GraphIndex> {
4777 match &self.named_graphs {
4778 NamedGraphsSlot::Quad(q) => q.transient_view(i, TRANSIENT_GRAPH_QUADS),
4779 _ => None,
4780 }
4781 }
4782
4783 pub fn named_quad_count(&self) -> Option<u64> {
4788 match &self.named_graphs {
4789 NamedGraphsSlot::Quad(q) => Some(q.quad_count()),
4790 _ => None,
4791 }
4792 }
4793
4794 pub fn named_graph_name_at(&self, i: usize) -> Option<&str> {
4797 self.named_graphs.name_at(&self.dict, i, false)
4798 }
4799
4800 pub fn named_graph_at(&self, i: usize) -> Option<(&str, &GraphIndex)> {
4804 self.named_graphs.graph_at(&self.dict, i, false)
4805 }
4806
4807 pub(crate) fn named_graph_name_at_demand(&self, i: usize, exhaustive: bool) -> Option<&str> {
4813 self.named_graphs.name_at(&self.dict, i, exhaustive)
4814 }
4815
4816 pub(crate) fn named_graph_at_demand(
4819 &self,
4820 i: usize,
4821 exhaustive: bool,
4822 ) -> Option<(&str, &GraphIndex)> {
4823 self.named_graphs.graph_at(&self.dict, i, exhaustive)
4824 }
4825
4826 pub fn graph_names(&self) -> Vec<&str> {
4831 (0..self.named_graphs.count())
4832 .filter_map(|i| self.named_graphs.name_at(&self.dict, i, false))
4833 .collect()
4834 }
4835
4836 pub fn graph_index(&self, iri: &str) -> Option<&GraphIndex> {
4838 self.named_graphs.find(&self.dict, iri)
4839 }
4840
4841 pub fn match_ids(
4844 &self,
4845 pattern: (Option<u32>, Option<u32>, Option<u32>),
4846 ) -> Vec<(u32, u32, u32)> {
4847 self.index.match_pattern(pattern)
4848 }
4849
4850 pub fn predicate_pairs(&self, predicate: &str) -> Vec<(u32, u32)> {
4853 let pid = match self.dict.predicate_id(predicate) {
4854 Some(p) => p,
4855 None => return Vec::new(),
4856 };
4857 self.index
4858 .match_pattern((None, Some(pid), None))
4859 .into_iter()
4860 .map(|(s, _p, o)| (self.dict.subject_node(s), self.dict.object_node(o)))
4861 .collect()
4862 }
4863
4864 pub fn open_ranged<R: RangeReader>(reader: &R) -> Result<Self, FileError> {
4868 let head = reader.read_at(0, HEADER_LEN as u64)?;
4869 let header = Header::from_bytes(&head)?;
4870
4871 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
4872 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
4873
4874 check_named_layout(&header)?;
4875 let index_bytes = reader.read_at(header.root_dir_offset, header.root_dir_len)?;
4876 let (index, _) = decode_root_index_container(&index_bytes, &header)?;
4877 let index_section_ranges = decode_index_section_ranges(
4878 &index_bytes,
4879 header.root_dir_offset,
4880 header.perms,
4881 header.index_container_sections(),
4882 )?;
4883
4884 let pyramid = PyramidSlot::Resident(if header.pyramid_meta_len > 0 {
4885 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
4886 Some(
4887 PyramidMeta::decode(&mb)
4888 .map_err(|_| FileError::Container("malformed pyramid meta"))?,
4889 )
4890 } else {
4891 None
4892 });
4893
4894 let (ti_off, ti_len) = header.text_index_payload();
4897 let text_index_bytes = if ti_len > 0 {
4898 Some(reader.read_at(ti_off, ti_len)?)
4899 } else {
4900 None
4901 };
4902 let text_index = resident_text_index_slot(text_index_bytes.as_deref(), header.block_codec)?;
4903 let labels = SectionSlot::Resident(header.label_index_section().and_then(|s| {
4906 let b = reader.read_at(s.offset, s.length).ok()?;
4907 crate::label_index::LabelIndex::from_section(&b, header.block_codec).ok()
4908 }));
4909 let schema = SectionSlot::Resident(read_schema_with_header(reader, &header));
4910
4911 let named_graphs = NamedGraphsSlot::resident(
4912 &header,
4913 |off, len| Ok(reader.read_at(off, len)?),
4914 || {
4915 let nb = reader.read_at(header.named_graphs_offset, header.named_graphs_len)?;
4916 decode_named_graphs(&nb, header.block_codec, header.perms)
4917 },
4918 )?;
4919
4920 let tile_ranges =
4924 tile_file_ranges(&index_bytes, header.root_dir_offset, &index_section_ranges);
4925 Ok(Self {
4926 header,
4927 dict,
4928 index,
4929 index_section_ranges,
4930 tile_ranges,
4931 pyramid,
4932 text_index,
4933 labels,
4934 schema,
4935 named_graphs,
4936 metadata: Vec::new(),
4937 service_client: None,
4938 service_error: std::sync::Mutex::new(None),
4939 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
4940 })
4941 }
4942
4943 pub fn open_ranged_lazy<R: RangeReader + Send + Sync + 'static>(
4955 reader: R,
4956 ) -> Result<Self, FileError> {
4957 let head = reader.read_at(0, HEADER_LEN as u64)?;
4958 let header = Header::from_bytes(&head)?;
4959 check_named_layout(&header)?;
4960 let reader = std::sync::Arc::new(reader);
4961 let read_concurrency = reader.concurrency();
4964
4965 let open_dict = || ranged_chunked_dictionary(&reader, &header, [true; 4]);
4969
4970 let reader_dyn: std::sync::Arc<dyn RangeReader + Send + Sync> = reader.clone();
4974 let open_index = || {
4975 open_index_container_lazy(
4976 &reader_dyn,
4977 ByteRange {
4978 offset: header.root_dir_offset,
4979 len: header.root_dir_len,
4980 },
4981 header.block_codec,
4982 header.has_tile_synopsis(),
4983 read_concurrency,
4984 header.perms,
4985 header.index_container_sections(),
4986 )
4987 };
4988
4989 let (dict, index) = open_side_by_side(read_concurrency, open_dict, open_index);
4996 let dict = dict?;
4997 let (index, index_section_ranges, tile_ranges) = index?;
4998
4999 let pyramid = ranged_pyramid_slot(&reader, &header);
5003
5004 let text_index = ranged_text_index_slot(&reader, &header);
5009
5010 let labels = ranged_label_slot(&reader, &header);
5014 let schema = ranged_schema_slot(&reader, &header);
5015
5016 let named_graphs = if let Some(q) = header.quad_index_section() {
5023 NamedGraphsSlot::Quad(crate::quad::QuadNamed::new(
5027 reader_dyn,
5028 ByteRange {
5029 offset: q.offset,
5030 len: q.length,
5031 },
5032 header.block_codec,
5033 header.perms,
5034 read_concurrency,
5035 ))
5036 } else if header.named_graphs_len > 0 {
5037 NamedGraphsSlot::Lazy(LazyNamedGraphs::new(
5038 reader_dyn,
5039 ByteRange {
5040 offset: header.named_graphs_offset,
5041 len: header.named_graphs_len,
5042 },
5043 header.block_codec,
5044 header.has_tile_synopsis(),
5045 read_concurrency,
5046 header.perms,
5047 ))
5048 } else {
5049 NamedGraphsSlot::Resident(Vec::new())
5050 };
5051
5052 let rete = Self {
5053 header,
5054 dict,
5055 index,
5056 index_section_ranges,
5057 tile_ranges,
5058 pyramid,
5059 text_index,
5060 labels,
5061 schema,
5062 named_graphs,
5063 metadata: Vec::new(),
5064 service_client: None,
5065 service_error: std::sync::Mutex::new(None),
5066 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
5067 };
5068 #[cfg(not(target_arch = "wasm32"))]
5078 rete.set_memory_budget(Some(DEFAULT_QUERY_BUDGET_MB << 20));
5079 Ok(rete)
5080 }
5081
5082 pub fn index_incomplete(&self) -> bool {
5087 self.index.load_incomplete()
5088 || self.dict.load_incomplete()
5089 || self.named_graphs.load_incomplete()
5090 }
5091
5092 pub fn reset_load_failures(&self) {
5100 self.index.reset_load_failure();
5101 self.dict.reset_load_failure();
5102 self.named_graphs.reset_load_failures();
5103 }
5104
5105 pub fn set_memory_budget(&self, budget: Option<u64>) -> MemoryBudget {
5136 let total = budget.unwrap_or(u64::MAX);
5137 self.set_spill_threshold(budget.map(spill_threshold_for));
5138 let mut split = split_memory_budget(total);
5139 let fixed = if total == u64::MAX {
5140 0
5141 } else {
5142 self.fixed_overhead().total()
5143 };
5144 if split.dict_cache != u64::MAX {
5145 split.dict_cache = split
5146 .dict_cache
5147 .saturating_sub(fixed)
5148 .max(MIN_DICT_CACHE_CAP);
5149 }
5150 self.dict.set_cache_cap(split.dict_cache);
5151 self.index.set_cache_cap(split.tile_cache);
5152 match &self.named_graphs {
5157 NamedGraphsSlot::Lazy(lazy) => lazy.set_tile_cap(split.tile_cache),
5158 NamedGraphsSlot::Quad(q) => q.set_tile_cap(split.tile_cache),
5161 NamedGraphsSlot::Resident(_) => {}
5162 }
5163 if std::env::var("RETE_OPEN_DEBUG").is_ok() {
5164 eprintln!(
5165 "[memory-budget] total={} dict_cache={} tile_cache={} block_cache={} fixed={:.1}MiB",
5166 if total == u64::MAX {
5167 "unlimited".to_string()
5168 } else {
5169 format!("{}MiB", total >> 20)
5170 },
5171 fmt_cap(split.dict_cache),
5172 fmt_cap(split.tile_cache),
5173 fmt_cap(split.block_cache),
5174 fixed as f64 / (1u64 << 20) as f64,
5175 );
5176 }
5177 split
5178 }
5179
5180 pub fn spill_threshold(&self) -> u64 {
5187 self.spill_threshold
5188 .load(std::sync::atomic::Ordering::Relaxed)
5189 }
5190
5191 pub fn set_spill_threshold(&self, bytes: Option<u64>) {
5195 self.spill_threshold.store(
5196 bytes.unwrap_or(u64::MAX),
5197 std::sync::atomic::Ordering::Relaxed,
5198 );
5199 }
5200
5201 pub fn dict_cache_stats(&self) -> crate::chunk_cache::CacheStats {
5205 self.dict.cache_stats()
5206 }
5207
5208 pub fn fixed_overhead(&self) -> FixedOverhead {
5212 FixedOverhead {
5213 dict_directory: self.dict.directory_bytes(),
5214 index_directory: self.index.directory_bytes(),
5215 named_graphs: self.named_graphs.directory_bytes(),
5216 }
5217 }
5218
5219 pub fn index_cache_stats(&self) -> crate::chunk_cache::CacheStats {
5221 self.index.cache_stats()
5222 }
5223
5224 pub fn dict_cache_cap(&self) -> u64 {
5226 self.dict.cache_cap()
5227 }
5228
5229 pub fn release_named_graph(&mut self, iri: &str) {
5242 match &mut self.named_graphs {
5243 NamedGraphsSlot::Lazy(lazy) => lazy.release(iri),
5244 NamedGraphsSlot::Quad(q) => q.release(&self.dict, iri),
5245 NamedGraphsSlot::Resident(_) => {}
5246 }
5247 }
5248
5249 fn resolve_query_pattern(
5250 &self,
5251 s: Option<&str>,
5252 p: Option<&str>,
5253 o: Option<&str>,
5254 ) -> Option<Pattern> {
5255 let sid = match s {
5256 Some(t) => match self.dict.subject_id(t) {
5257 Some(id) => Some(id),
5258 None => return None,
5259 },
5260 None => None,
5261 };
5262 let pid = match p {
5263 Some(t) => match self.dict.predicate_id(t) {
5264 Some(id) => Some(id),
5265 None => return None,
5266 },
5267 None => None,
5268 };
5269 let oid = match o {
5270 Some(t) => match self.dict.object_id(t) {
5271 Some(id) => Some(id),
5272 None => return None,
5273 },
5274 None => None,
5275 };
5276 Some((sid, pid, oid))
5277 }
5278
5279 pub fn query_with_provenance(
5283 &self,
5284 s: Option<&str>,
5285 p: Option<&str>,
5286 o: Option<&str>,
5287 ) -> Vec<TripleProvenance> {
5288 let pattern = match self.resolve_query_pattern(s, p, o) {
5289 Some(pattern) => pattern,
5290 None => return Vec::new(),
5291 };
5292
5293 let index_permutation = GraphIndex::best_permutation_in(self.header.perms, pattern);
5294 let dictionary_range = ByteRange {
5295 offset: self.header.dictionary_offset,
5296 len: self.header.dictionary_len,
5297 };
5298 let index_range = ByteRange {
5299 offset: self.header.root_dir_offset,
5300 len: self.header.root_dir_len,
5301 };
5302 let index_section_range = self.index_section_ranges[index_permutation.section_index()];
5303 let pyramid_range = (self.header.pyramid_meta_len > 0).then_some(ByteRange {
5304 offset: self.header.pyramid_meta_offset,
5305 len: self.header.pyramid_meta_len,
5306 });
5307
5308 let tiles = &self.tile_ranges[index_permutation.section_index()];
5309 self.index
5310 .match_pattern(pattern)
5311 .into_iter()
5312 .filter_map(|(s, p, o)| {
5313 let terms = (
5314 self.dict.subject_term(s)?,
5315 self.dict.predicate_term(p)?,
5316 self.dict.object_term(o)?,
5317 );
5318 let a = index_permutation.forward((s, p, o)).0;
5321 let ti = tiles.partition_point(|&(_, max_a, _)| max_a < a);
5322 let (tile, tile_range) = match tiles.get(ti) {
5323 Some(&(min_a, _, range)) if min_a <= a => (
5324 Some(format!("{}/{ti}", index_permutation.name())),
5325 Some(range),
5326 ),
5327 _ => (None, None),
5328 };
5329 Some(TripleProvenance {
5330 terms,
5331 ids: (s, p, o),
5332 graph: None,
5333 matched_pattern: pattern,
5334 index_permutation,
5335 dictionary_range,
5336 index_range,
5337 index_section_range,
5338 pyramid_range,
5339 tile,
5340 tile_range,
5341 })
5342 })
5343 .collect()
5344 }
5345
5346 pub fn query(&self, s: Option<&str>, p: Option<&str>, o: Option<&str>) -> Vec<TermTriple> {
5350 self.query_with_provenance(s, p, o)
5351 .into_iter()
5352 .map(|m| m.terms)
5353 .collect()
5354 }
5355
5356 pub fn query_in_graph(
5364 &self,
5365 graph: Option<&str>,
5366 s: Option<&str>,
5367 p: Option<&str>,
5368 o: Option<&str>,
5369 ) -> Vec<TermTriple> {
5370 let pattern = match self.resolve_query_pattern(s, p, o) {
5371 Some(pattern) => pattern,
5372 None => return Vec::new(),
5373 };
5374 let index = match graph {
5375 None => &self.index,
5376 Some(g) => match self.graph_index(g) {
5377 Some(i) => i,
5378 None => return Vec::new(),
5379 },
5380 };
5381 self.resolve_matches(index.match_pattern(pattern))
5382 }
5383
5384 fn resolve_matches(&self, ids: Vec<Triple>) -> Vec<TermTriple> {
5387 if !ids.is_empty() {
5395 let mut nodes = Vec::with_capacity(ids.len() * 2);
5396 let mut preds = Vec::with_capacity(ids.len());
5397 for &(s, p, o) in &ids {
5398 nodes.push(self.dict.subject_node(s));
5399 nodes.push(self.dict.object_node(o));
5400 preds.push(p);
5401 }
5402 self.dict.prefetch_terms(&nodes, &preds);
5403 }
5404 ids.into_iter()
5405 .filter_map(|(s, p, o)| {
5406 Some((
5407 self.dict.subject_term(s)?,
5408 self.dict.predicate_term(p)?,
5409 self.dict.object_term(o)?,
5410 ))
5411 })
5412 .collect()
5413 }
5414
5415 pub fn query_quads(
5420 &self,
5421 s: Option<&str>,
5422 p: Option<&str>,
5423 o: Option<&str>,
5424 ) -> Vec<(TermTriple, Option<String>)> {
5425 let mut out: Vec<(TermTriple, Option<String>)> = self
5426 .query_in_graph(None, s, p, o)
5427 .into_iter()
5428 .map(|t| (t, None))
5429 .collect();
5430 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
5431 return out;
5432 };
5433 for i in 0..self.named_graphs.count() {
5434 let Some(iri) = self.named_graphs.name_at(&self.dict, i, true) else {
5437 continue;
5438 };
5439 let owned = self.named_graph_transient(i);
5442 let index = match &owned {
5443 Some(view) => view,
5444 None => match self.named_graphs.graph_at(&self.dict, i, true) {
5445 Some((_, g)) => g,
5446 None => continue,
5447 },
5448 };
5449 for triple in self.resolve_matches(index.match_pattern(pattern)) {
5450 out.push((triple, Some(iri.to_string())));
5451 }
5452 }
5453 out
5454 }
5455
5456 pub fn query_ranged<R: RangeReader>(
5463 reader: &R,
5464 s: Option<&str>,
5465 p: Option<&str>,
5466 o: Option<&str>,
5467 ) -> Result<Vec<TermTriple>, FileError> {
5468 let routed = match route_pattern(reader, s, p, o)? {
5469 Some(routed) => routed,
5470 None => return Ok(Vec::new()),
5471 };
5472 let matches = fetch_routed_matches(reader, &routed)?;
5473 Ok(matches
5474 .into_iter()
5475 .filter_map(|(s, p, o)| {
5476 Some((
5477 routed.dict.subject_term(s)?,
5478 routed.dict.predicate_term(p)?,
5479 routed.dict.object_term(o)?,
5480 ))
5481 })
5482 .collect())
5483 }
5484
5485 pub fn route_pattern_ranged<R: RangeReader>(
5489 reader: &R,
5490 s: Option<&str>,
5491 p: Option<&str>,
5492 o: Option<&str>,
5493 ) -> Result<bool, FileError> {
5494 Ok(route_pattern(reader, s, p, o)?.is_some())
5495 }
5496}
5497
5498struct RoutedPattern {
5501 dict: Dictionary,
5502 pattern: Pattern,
5503 permutation: IndexPermutation,
5504 header: Header,
5505 section: ByteRange,
5507}
5508
5509fn route_pattern<R: RangeReader>(
5512 reader: &R,
5513 s: Option<&str>,
5514 p: Option<&str>,
5515 o: Option<&str>,
5516) -> Result<Option<RoutedPattern>, FileError> {
5517 let head = reader.read_at(0, HEADER_LEN as u64)?;
5518 let header = Header::from_bytes(&head)?;
5519
5520 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
5521 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
5522
5523 let Some(pattern) = resolve_query_pattern(&dict, s, p, o) else {
5524 return Ok(None);
5525 };
5526 let permutation = GraphIndex::best_permutation_in(header.perms, pattern);
5527 let section = locate_container_section_ranged(
5528 reader,
5529 header.root_dir_offset,
5530 header.root_dir_len,
5531 header
5532 .perms
5533 .position(permutation)
5534 .ok_or(FileError::Container("routed to an absent permutation"))?,
5535 header.index_container_sections() as u64,
5536 )?;
5537 Ok(Some(RoutedPattern {
5538 dict,
5539 pattern,
5540 permutation,
5541 header,
5542 section,
5543 }))
5544}
5545
5546fn fetch_routed_matches<R: RangeReader>(
5551 reader: &R,
5552 routed: &RoutedPattern,
5553) -> Result<Vec<Triple>, FileError> {
5554 let dir = read_tile_directory_ranged(reader, routed.section)?;
5555 let [pa, _, _] = routed.permutation.order_pattern(routed.pattern);
5556 let codec = routed.header.block_codec;
5557 let mut out = Vec::new();
5558 match pa {
5559 Some(a) => {
5562 for e in dir.iter().filter(|e| e.min_a <= a && a <= e.max_a) {
5563 let bytes = reader.read_at(routed.section.offset + e.start, e.end - e.start)?;
5564 let tile = decompress(codec, &bytes)?;
5565 out.extend(GraphIndex::match_serialized_block(
5566 &tile,
5567 routed.permutation,
5568 routed.pattern,
5569 ));
5570 }
5571 }
5572 None => {
5575 if let (Some(first), Some(last)) = (dir.first(), dir.last()) {
5576 let base = first.start;
5577 let body = reader.read_at(routed.section.offset + base, last.end - base)?;
5578 for e in &dir {
5579 let tile = decompress(
5580 codec,
5581 &body[(e.start - base) as usize..(e.end - base) as usize],
5582 )?;
5583 out.extend(GraphIndex::match_serialized_block(
5584 &tile,
5585 routed.permutation,
5586 routed.pattern,
5587 ));
5588 }
5589 }
5590 }
5591 }
5592 out.sort_unstable();
5593 Ok(out)
5594}
5595
5596fn resolve_query_pattern(
5597 dict: &Dictionary,
5598 s: Option<&str>,
5599 p: Option<&str>,
5600 o: Option<&str>,
5601) -> Option<Pattern> {
5602 let sid = match s {
5603 Some(t) => Some(dict.subject_id(t)?),
5604 None => None,
5605 };
5606 let pid = match p {
5607 Some(t) => Some(dict.predicate_id(t)?),
5608 None => None,
5609 };
5610 let oid = match o {
5611 Some(t) => Some(dict.object_id(t)?),
5612 None => None,
5613 };
5614 Some((sid, pid, oid))
5615}
5616
5617fn ranged_chunked_dictionary<R: RangeReader + Send + Sync + 'static>(
5632 reader: &std::sync::Arc<R>,
5633 header: &Header,
5634 want: [bool; 4],
5635) -> Result<Dictionary, FileError> {
5636 let cache = crate::chunk_cache::ChunkCache::unlimited_arc();
5639 let mut dict_sections: Vec<crate::dict::ChunkedSection> = Vec::with_capacity(4);
5640 let mut section_ranges: [std::sync::Arc<Vec<ByteRange>>; 4] = Default::default();
5641 for si in 0..4 {
5642 if !want[si] {
5643 dict_sections.push(crate::dict::ChunkedSection::from_parts(
5644 crate::dict::SectionMeta {
5645 term_count: 0,
5646 restart_interval: 1,
5647 restart_offsets: Vec::new(),
5648 },
5649 Vec::new(),
5650 None,
5651 cache.clone(),
5652 si as u8,
5653 ));
5654 continue;
5655 }
5656 let section = locate_container_section_ranged(
5657 reader.as_ref(),
5658 header.dictionary_offset,
5659 header.dictionary_len,
5660 si,
5661 4,
5662 )?;
5663 let (meta, entries) = read_dict_dir_ranged(reader.as_ref(), section)?;
5664 let ranges: std::sync::Arc<Vec<ByteRange>> = std::sync::Arc::new(
5667 entries
5668 .iter()
5669 .map(|e| ByteRange {
5670 offset: section.offset + e.start,
5671 len: (e.end - e.start),
5672 })
5673 .collect(),
5674 );
5675 section_ranges[si] = ranges.clone();
5676 let chunks: Vec<crate::dict::SectionChunk> = entries
5677 .into_iter()
5678 .map(|e| crate::dict::SectionChunk::new(e.first_run, e.key, e.body_start))
5679 .collect();
5680 let chunk_reader = reader.clone();
5681 let codec = header.dict_codec;
5682 let loader_ranges = ranges.clone();
5683 let loader: crate::dict::ChunkLoader = Box::new(move |ci| {
5684 let range = loader_ranges.get(ci)?;
5685 let bytes = chunk_reader.read_at(range.offset, range.len).ok()?;
5686 decompress(codec, &bytes).ok()
5687 });
5688 let bulk_reader = reader.clone();
5691 let bulk: crate::dict::ChunkBulkLoader = Box::new(move |cis| {
5692 let want: Option<Vec<ByteRange>> =
5693 cis.iter().map(|&ci| ranges.get(ci).copied()).collect();
5694 let blobs = read_coalesced(bulk_reader.as_ref(), &want?, DICT_COALESCE_GAP)?;
5695 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
5696 });
5697 dict_sections.push(
5698 crate::dict::ChunkedSection::from_parts(
5699 meta,
5700 chunks,
5701 Some(loader),
5702 cache.clone(),
5703 si as u8,
5704 )
5705 .with_bulk_loader(bulk),
5706 );
5707 }
5708 let dict_arr: [crate::dict::ChunkedSection; 4] = dict_sections
5709 .try_into()
5710 .map_err(|_| FileError::Container("expected 4 dictionary sections"))?;
5711 let multi_reader = reader.clone();
5715 let codec = header.dict_codec;
5716 let multi: crate::dict::MultiChunkBulkLoader = Box::new(move |pairs| {
5717 let want: Option<Vec<ByteRange>> = pairs
5718 .iter()
5719 .map(|&(si, ci)| section_ranges.get(si as usize)?.get(ci).copied())
5720 .collect();
5721 let blobs = read_coalesced(multi_reader.as_ref(), &want?, DICT_COALESCE_GAP)?;
5722 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
5723 });
5724 Ok(Dictionary::from_chunked_sections(dict_arr).with_multi_bulk_loader(multi))
5725}
5726
5727#[cfg(not(target_arch = "wasm32"))]
5732fn open_side_by_side<A, B: Send>(
5733 concurrency: usize,
5734 a: impl FnOnce() -> A,
5735 b: impl FnOnce() -> B + Send,
5736) -> (A, B) {
5737 if concurrency <= 1 {
5738 return (a(), b());
5739 }
5740 std::thread::scope(|scope| {
5741 let hb = scope.spawn(b);
5742 let ra = a();
5743 let rb = hb
5744 .join()
5745 .unwrap_or_else(|panic| std::panic::resume_unwind(panic));
5746 (ra, rb)
5747 })
5748}
5749
5750#[cfg(target_arch = "wasm32")]
5752fn open_side_by_side<A, B>(
5753 _concurrency: usize,
5754 a: impl FnOnce() -> A,
5755 b: impl FnOnce() -> B,
5756) -> (A, B) {
5757 (a(), b())
5758}
5759
5760fn ranged_pyramid_slot<R: RangeReader + Send + Sync + 'static>(
5764 reader: &std::sync::Arc<R>,
5765 header: &Header,
5766) -> PyramidSlot {
5767 if header.pyramid_meta_len == 0 {
5768 return PyramidSlot::Resident(None);
5769 }
5770 let pyr_reader = reader.clone();
5771 let pyr_off = header.pyramid_meta_offset;
5772 let pyr_len = header.pyramid_meta_len;
5773 PyramidSlot::Lazy {
5774 loader: Box::new(move || {
5775 let mb = pyr_reader.read_at(pyr_off, pyr_len).ok()?;
5776 PyramidMeta::decode(&mb).ok()
5777 }),
5778 cell: std::sync::OnceLock::new(),
5779 }
5780}
5781
5782fn ranged_text_index_slot<R: RangeReader + Send + Sync + 'static>(
5787 reader: &std::sync::Arc<R>,
5788 header: &Header,
5789) -> TextIndexSlot {
5790 let (ti_off, ti_len) = header.text_index_payload();
5791 if ti_len == 0 {
5792 return TextIndexSlot::Resident {
5793 index: None,
5794 token_table_len: None,
5795 };
5796 }
5797 let ti_reader = reader.clone();
5798 let probe_reader = reader.clone();
5799 let codec = header.block_codec;
5800 TextIndexSlot::Lazy {
5801 loader: Box::new(move || {
5802 let prefix_len = read_token_table_len(&*ti_reader, ti_off, ti_len)?;
5806 let prefix = ti_reader.read_at(ti_off, prefix_len).ok()?;
5807 let postings_base = crate::text_index::TextIndex::postings_base(&prefix)? as u64;
5808 let postings_abs = ti_off + postings_base;
5809 let pr = ti_reader.clone();
5810 let posting_loader =
5811 Box::new(move |off: u64, len: u64| pr.read_at(postings_abs + off, len).ok());
5812 crate::text_index::TextIndex::from_token_table(&prefix, codec, posting_loader).ok()
5813 }),
5814 cell: std::sync::OnceLock::new(),
5815 token_table: Box::new(move || read_token_table_len(&*probe_reader, ti_off, ti_len)),
5818 token_table_cell: std::sync::OnceLock::new(),
5819 }
5820}
5821
5822fn text_search_in(
5825 ti: Option<&crate::text_index::TextIndex>,
5826 dict: &Dictionary,
5827 words: &[&str],
5828 prefix: Option<&str>,
5829 limit: usize,
5830) -> Vec<String> {
5831 let Some(ti) = ti else {
5832 return Vec::new();
5833 };
5834 let mut acc: Option<Vec<u32>> = None;
5838 if let Some(p) = prefix {
5839 acc = Some(ti.prefix(&p.to_lowercase()));
5840 }
5841 for w in words {
5842 for tok in crate::text_index::tokenize(w) {
5843 let posting = ti.lookup(&tok);
5844 acc = Some(match acc {
5845 Some(a) => intersect_sorted(&a, &posting),
5846 None => posting,
5847 });
5848 if acc.as_ref().is_some_and(|a| a.is_empty()) {
5849 return Vec::new();
5850 }
5851 }
5852 }
5853 let ids = acc.unwrap_or_default();
5854 let mut out = Vec::with_capacity(if limit > 0 {
5855 limit.min(ids.len())
5856 } else {
5857 ids.len()
5858 });
5859 let page = if limit > 0 {
5863 limit.min(ids.len())
5864 } else {
5865 ids.len()
5866 };
5867 dict.prefetch_subject_terms(&ids[..page]);
5868 for id in ids {
5869 if let Some(iri) = dict.subject_term(id) {
5870 out.push(iri);
5871 if limit > 0 && out.len() >= limit {
5872 break;
5873 }
5874 }
5875 }
5876 out
5877}
5878
5879pub struct SearchView {
5902 header: Header,
5903 dict: SectionSlot<Dictionary>,
5904 pyramid: PyramidSlot,
5905 text_index: TextIndexSlot,
5906 labels: SectionSlot<crate::label_index::LabelIndex>,
5907}
5908
5909impl SearchView {
5910 pub fn open_ranged<R: RangeReader + Send + Sync + 'static>(
5915 reader: R,
5916 ) -> Result<Self, FileError> {
5917 let head = reader.read_at(0, HEADER_LEN as u64)?;
5918 let header = Header::from_bytes(&head)?;
5919 let reader = std::sync::Arc::new(reader);
5920 let dict = if header.label_index_section().is_some() {
5924 let (r, h) = (reader.clone(), header.clone());
5925 SectionSlot::Lazy {
5926 loader: Box::new(move || {
5927 ranged_chunked_dictionary(&r, &h, [true, true, false, false]).ok()
5928 }),
5929 cell: std::sync::OnceLock::new(),
5930 }
5931 } else {
5932 SectionSlot::Resident(Some(ranged_chunked_dictionary(
5933 &reader,
5934 &header,
5935 [true, true, false, false],
5936 )?))
5937 };
5938 let pyramid = ranged_pyramid_slot(&reader, &header);
5939 let text_index = ranged_text_index_slot(&reader, &header);
5940 let labels = ranged_label_slot(&reader, &header);
5941 Ok(Self {
5942 header,
5943 dict,
5944 pyramid,
5945 text_index,
5946 labels,
5947 })
5948 }
5949
5950 pub fn has_labels(&self) -> bool {
5954 self.header.label_index_section().is_some() || self.header.pyramid_meta_len > 0
5955 }
5956
5957 pub fn has_label_index(&self) -> bool {
5959 self.header.label_index_section().is_some()
5960 }
5961
5962 pub fn header(&self) -> &Header {
5964 &self.header
5965 }
5966
5967 pub fn has_text_index(&self) -> bool {
5969 self.header.text_index_len > 0
5970 }
5971
5972 pub fn has_pyramid(&self) -> bool {
5975 self.header.pyramid_meta_len > 0
5976 }
5977
5978 pub fn text_search(&self, words: &[&str], prefix: Option<&str>, limit: usize) -> Vec<String> {
5981 let Some(dict) = self.dict.get() else {
5982 return Vec::new();
5983 };
5984 text_search_in(self.text_index.index(), dict, words, prefix, limit)
5985 }
5986
5987 pub fn text_index_token_table_len(&self) -> Option<u64> {
5991 self.text_index.token_table_len()
5992 }
5993
5994 pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
5998 if let Some(li) = self.labels.get() {
5999 return li.prefix_search(prefix, limit);
6000 }
6001 let pyr = match &self.pyramid {
6002 PyramidSlot::Resident(p) => p.as_ref(),
6003 PyramidSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
6004 };
6005 let (Some(pyr), Some(dict)) = (pyr, self.dict.get()) else {
6006 return Vec::new();
6007 };
6008 pyr.prefix_search(prefix, limit)
6009 .into_iter()
6010 .filter_map(|e| {
6011 dict.subject_term(e.subject)
6012 .map(|iri| (e.label.clone(), iri))
6013 })
6014 .collect()
6015 }
6016}
6017
6018#[must_use]
6023pub struct SummaryView {
6024 pub round: u32,
6025 pub summary: Vec<SuperEdge>,
6026 pub class_hierarchy: Vec<ClassNode>,
6028 pub level_rollups: Vec<LevelRollup>,
6030 pub level_links: Vec<LevelLinks>,
6032 pub descriptors: Vec<CommunityDescriptor>,
6034 pub subclass_cycles: Vec<Vec<String>>,
6036 pub disjoint_pairs: Vec<(String, String)>,
6038 pub equivalent_pairs: Vec<(String, String)>,
6040 dict: Dictionary,
6041}
6042
6043impl SummaryView {
6044 pub fn open_ranged<R: RangeReader>(reader: &R) -> Result<Option<Self>, FileError> {
6046 let head = reader.read_at(0, HEADER_LEN as u64)?;
6047 let header = Header::from_bytes(&head)?;
6048 if header.pyramid_meta_len == 0 {
6049 return Ok(None);
6050 }
6051
6052 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
6053 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
6054
6055 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
6056 let mut meta =
6057 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
6058 if let Some(s) = read_schema_with_header(reader, &header) {
6061 meta.class_hierarchy = s.class_hierarchy;
6062 meta.level_rollups = s.level_rollups;
6063 meta.level_links = s.level_links;
6064 meta.subclass_cycles = s.subclass_cycles;
6065 meta.disjoint_pairs = s.disjoint_pairs;
6066 meta.equivalent_pairs = s.equivalent_pairs;
6067 }
6068
6069 Ok(Some(SummaryView {
6070 round: meta.round,
6071 summary: meta.summary,
6072 class_hierarchy: meta.class_hierarchy,
6073 level_rollups: meta.level_rollups,
6074 level_links: meta.level_links,
6075 descriptors: meta.descriptors,
6076 subclass_cycles: meta.subclass_cycles,
6077 disjoint_pairs: meta.disjoint_pairs,
6078 equivalent_pairs: meta.equivalent_pairs,
6079 dict,
6080 }))
6081 }
6082
6083 pub fn open_schema_ranged<R: RangeReader>(reader: &R) -> Result<Option<Self>, FileError> {
6091 let head = reader.read_at(0, HEADER_LEN as u64)?;
6092 let header = Header::from_bytes(&head)?;
6093 let Some(s) = read_schema_with_header(reader, &header) else {
6094 return Ok(None);
6095 };
6096 let dict = crate::DictionaryBuilder::new().build();
6097 Ok(Some(SummaryView {
6098 round: 0,
6099 summary: Vec::new(),
6100 class_hierarchy: s.class_hierarchy,
6101 level_rollups: s.level_rollups,
6102 level_links: s.level_links,
6103 descriptors: Vec::new(),
6104 subclass_cycles: s.subclass_cycles,
6105 disjoint_pairs: s.disjoint_pairs,
6106 equivalent_pairs: s.equivalent_pairs,
6107 dict,
6108 }))
6109 }
6110
6111 pub fn level_count(&self) -> usize {
6113 self.level_rollups.len()
6114 }
6115
6116 pub fn level_rollup(&self, k: usize) -> Option<&LevelRollup> {
6119 self.level_rollups.get(k)
6120 }
6121
6122 pub fn predicate_term(&self, id: u32) -> Option<String> {
6124 self.dict.predicate_term(id)
6125 }
6126
6127 pub fn predicate_total(&self, predicate: &str) -> u32 {
6130 match self.dict.predicate_id(predicate) {
6131 Some(pid) => self
6132 .summary
6133 .iter()
6134 .filter(|e| e.predicate == pid)
6135 .map(|e| e.count)
6136 .sum(),
6137 None => 0,
6138 }
6139 }
6140
6141 pub fn predicate_totals(&self) -> Vec<(String, u32)> {
6143 let mut by_pred: std::collections::BTreeMap<u32, u32> = std::collections::BTreeMap::new();
6144 for e in &self.summary {
6145 *by_pred.entry(e.predicate).or_default() += e.count;
6146 }
6147 let mut out: Vec<(String, u32)> = by_pred
6148 .into_iter()
6149 .filter_map(|(pid, c)| self.dict.predicate_term(pid).map(|t| (t, c)))
6150 .collect();
6151 out.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
6152 out
6153 }
6154
6155 pub fn community_count(&self) -> usize {
6157 let mut comms = std::collections::BTreeSet::new();
6158 for e in &self.summary {
6159 comms.insert(e.s_comm);
6160 comms.insert(e.o_comm);
6161 }
6162 comms.len()
6163 }
6164
6165 pub fn tbox_coherence(&self) -> Vec<crate::reason::Inconsistency> {
6180 schema_coherence(
6181 &self.class_hierarchy,
6182 &self.subclass_cycles,
6183 &self.disjoint_pairs,
6184 &self.equivalent_pairs,
6185 )
6186 }
6187
6188 pub fn tbox_is_coherent(&self) -> bool {
6191 self.tbox_coherence().is_empty()
6192 }
6193}
6194
6195pub fn schema_coherence(
6201 class_hierarchy: &[ClassNode],
6202 subclass_cycles: &[Vec<String>],
6203 disjoint_pairs: &[(String, String)],
6204 equivalent_pairs: &[(String, String)],
6205) -> Vec<crate::reason::Inconsistency> {
6206 use crate::reason::Inconsistency;
6207 use std::collections::{BTreeMap, BTreeSet, VecDeque};
6208 const MAX_REACH: usize = 100_000;
6209
6210 let mut out: Vec<Inconsistency> = Vec::new();
6211
6212 for cyc in subclass_cycles {
6213 let detail = if cyc.len() == 1 {
6214 format!("{} is rdfs:subClassOf itself (a cycle)", cyc[0])
6215 } else {
6216 format!(
6217 "classes {{{}}} are mutually rdfs:subClassOf (a cycle)",
6218 cyc.join(", ")
6219 )
6220 };
6221 out.push(Inconsistency {
6222 kind: "subclass-cycle",
6223 detail,
6224 });
6225 }
6226
6227 if !disjoint_pairs.is_empty() {
6228 let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
6230 for n in class_hierarchy {
6231 let e = adj.entry(n.class.as_str()).or_default();
6232 for p in &n.parents {
6233 e.push(p.as_str());
6234 }
6235 }
6236 for (a, b) in equivalent_pairs {
6237 adj.entry(a.as_str()).or_default().push(b.as_str());
6238 adj.entry(b.as_str()).or_default().push(a.as_str());
6239 }
6240
6241 let mut focuses: BTreeSet<&str> =
6243 class_hierarchy.iter().map(|n| n.class.as_str()).collect();
6244 for (a, b) in disjoint_pairs.iter().chain(equivalent_pairs) {
6245 focuses.insert(a.as_str());
6246 focuses.insert(b.as_str());
6247 }
6248
6249 let mut seen: BTreeSet<&str> = BTreeSet::new();
6250 for &c in &focuses {
6251 let mut reach: BTreeSet<&str> = BTreeSet::new();
6253 let mut q: VecDeque<&str> = VecDeque::new();
6254 reach.insert(c);
6255 q.push_back(c);
6256 while let Some(x) = q.pop_front() {
6257 if reach.len() > MAX_REACH {
6258 break;
6259 }
6260 if let Some(ns) = adj.get(x) {
6261 for &p in ns {
6262 if reach.insert(p) {
6263 q.push_back(p);
6264 }
6265 }
6266 }
6267 }
6268 for (x, y) in disjoint_pairs {
6269 if reach.contains(x.as_str()) && reach.contains(y.as_str()) && seen.insert(c) {
6270 out.push(Inconsistency {
6271 kind: "unsatisfiable-class",
6272 detail: format!(
6273 "{c} is a subclass of both {x} and {y}, which are \
6274 owl:disjointWith — no individual can be a {c}"
6275 ),
6276 });
6277 break;
6278 }
6279 }
6280 }
6281 }
6282
6283 out.sort_by(|a, b| (a.kind, &a.detail).cmp(&(b.kind, &b.detail)));
6284 out
6285}
6286
6287pub fn read_schema_coherence_ranged<R: RangeReader>(
6295 reader: &R,
6296) -> Result<Option<Vec<crate::reason::Inconsistency>>, FileError> {
6297 let head = reader.read_at(0, HEADER_LEN as u64)?;
6298 let header = Header::from_bytes(&head)?;
6299 if let Some(s) = read_schema_with_header(reader, &header) {
6301 return Ok(Some(schema_coherence(
6302 &s.class_hierarchy,
6303 &s.subclass_cycles,
6304 &s.disjoint_pairs,
6305 &s.equivalent_pairs,
6306 )));
6307 }
6308 if header.pyramid_meta_len == 0 {
6309 return Ok(None);
6310 }
6311 if header.schema_meta_len > 0 && (header.schema_meta_len as u64) <= header.pyramid_meta_len {
6315 let off =
6316 header.pyramid_meta_offset + header.pyramid_meta_len - header.schema_meta_len as u64;
6317 let block = reader.read_at(off, header.schema_meta_len as u64)?;
6318 let (hierarchy, cycles, disjoint, equivalent) = crate::meta::decode_schema_block(&block)
6319 .map_err(|_| FileError::Container("malformed schema block"))?;
6320 return Ok(Some(schema_coherence(
6321 &hierarchy,
6322 &cycles,
6323 &disjoint,
6324 &equivalent,
6325 )));
6326 }
6327 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
6329 let meta =
6330 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
6331 Ok(Some(schema_coherence(
6332 &meta.class_hierarchy,
6333 &meta.subclass_cycles,
6334 &meta.disjoint_pairs,
6335 &meta.equivalent_pairs,
6336 )))
6337}
6338
6339#[allow(clippy::type_complexity)]
6347pub fn read_schema_summary_ranged<R: RangeReader>(
6348 reader: &R,
6349) -> Result<Option<(Vec<(String, u64)>, Vec<(String, String, String, u64)>)>, FileError> {
6350 let head = reader.read_at(0, HEADER_LEN as u64)?;
6351 let header = Header::from_bytes(&head)?;
6352 let meta = if let Some(s) = read_schema_with_header(reader, &header) {
6354 PyramidMeta::new(0, Vec::new(), &[]).with_schema(
6355 Vec::new(),
6356 s.level_rollups,
6357 s.level_links,
6358 Vec::new(),
6359 Vec::new(),
6360 Vec::new(),
6361 Vec::new(),
6362 )
6363 } else {
6364 if header.pyramid_meta_len == 0 {
6365 return Ok(None);
6366 }
6367 if header.schema_meta_len > 0 && (header.schema_meta_len as u64) <= header.pyramid_meta_len
6368 {
6369 let off = header.pyramid_meta_offset + header.pyramid_meta_len
6370 - header.schema_meta_len as u64;
6371 let block = reader.read_at(off, header.schema_meta_len as u64)?;
6372 let summary = crate::meta::decode_schema_block_summary(&block)
6373 .map_err(|_| FileError::Container("malformed schema block"))?;
6374 return Ok(Some(summary));
6375 }
6376 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
6378 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?
6379 };
6380 if meta.level_rollups.is_empty() && meta.level_links.is_empty() {
6381 return Ok(None);
6382 }
6383 let classes = meta
6384 .level_rollups
6385 .iter()
6386 .max_by_key(|r| r.depth)
6387 .map(|r| r.classes.clone())
6388 .unwrap_or_default();
6389 let relations = meta
6390 .level_links
6391 .iter()
6392 .max_by_key(|l| l.depth)
6393 .map(|l| {
6394 l.links
6395 .iter()
6396 .map(|c| {
6397 (
6398 c.s_class.clone(),
6399 c.predicate.clone(),
6400 c.o_class.clone(),
6401 c.count,
6402 )
6403 })
6404 .collect()
6405 })
6406 .unwrap_or_default();
6407 Ok(Some((classes, relations)))
6408}
6409
6410#[cfg(test)]
6415pub(crate) fn dict_chunk_keys_for_test(image: &[u8]) -> Vec<Vec<Vec<u8>>> {
6416 let header = Header::from_bytes(&image[..HEADER_LEN]).unwrap();
6417 let s = header.dictionary_offset as usize;
6418 let e = s + header.dictionary_len as usize;
6419 decode_container(&image[s..e], CODEC_NONE)
6420 .unwrap()
6421 .iter()
6422 .map(|payload| {
6423 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
6424 entries.into_iter().map(|c| c.key).collect()
6425 })
6426 .collect()
6427}
6428
6429#[cfg(test)]
6430mod tests {
6431 use super::*;
6432 use crate::dictionary::DictionaryBuilder;
6433 use crate::index::GraphIndexBuilder;
6434
6435 #[cfg(not(feature = "compression"))]
6438 #[test]
6439 fn ruzstd_decodes_a_zstd_frame() {
6440 let data: Vec<u8> = (0..50_000u32)
6441 .flat_map(|i| (i % 251).to_le_bytes())
6442 .collect();
6443 let frame = ruzstd::encoding::compress_to_vec(
6444 &data[..],
6445 ruzstd::encoding::CompressionLevel::Fastest,
6446 );
6447 assert!(frame.len() < data.len());
6448 assert_eq!(decompress(CODEC_ZSTD, &frame).unwrap(), data);
6449 }
6450
6451 #[test]
6457 fn three_permutation_file_round_trips_every_open_path() {
6458 use crate::index::PermSet;
6459 use crate::reader::SliceReader;
6460
6461 let build = |perms: PermSet| {
6462 let mut db = crate::DictionaryBuilder::new();
6463 let mut triples = Vec::new();
6464 for i in 0..60u32 {
6465 let (s, p, o) = (
6466 format!("<http://ex/s{}>", i % 11),
6467 format!("<http://ex/p{}>", i % 3),
6468 format!("<http://ex/o{}>", i % 7),
6469 );
6470 db.observe(&s, &p, &o);
6471 triples.push((s, p, o));
6472 }
6473 let dict = db.build();
6474 let ids: Vec<(u32, u32, u32)> = triples
6475 .iter()
6476 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
6477 .collect();
6478 let def = GraphIndexBuilder::from_triples(ids.clone())
6479 .with_tile_budget(96)
6480 .with_perms(perms)
6481 .build();
6482 let named = GraphIndexBuilder::from_triples(ids)
6483 .with_tile_budget(96)
6484 .with_perms(perms)
6485 .build();
6486 write_dataset(
6487 &dict,
6488 &def,
6489 &[("<http://ex/g>".to_string(), named)],
6490 true,
6491 &[],
6492 0,
6493 )
6494 };
6495
6496 let six = build(PermSet::ALL);
6497 let three = build(PermSet::CORE);
6498 assert!(three.len() < six.len());
6499 assert_eq!(Header::from_bytes(&three).unwrap().perms, PermSet::CORE);
6500 assert_eq!(Header::from_bytes(&six).unwrap().perms, PermSet::ALL);
6501
6502 let shapes: Vec<(Option<&str>, Option<&str>, Option<&str>)> = vec![
6503 (None, None, None),
6504 (Some("<http://ex/s3>"), None, None),
6505 (None, Some("<http://ex/p1>"), None),
6506 (None, None, Some("<http://ex/o5>")),
6507 (Some("<http://ex/s3>"), Some("<http://ex/p1>"), None),
6508 (Some("<http://ex/s3>"), None, Some("<http://ex/o5>")),
6509 (None, Some("<http://ex/p1>"), Some("<http://ex/o5>")),
6510 (
6511 Some("<http://ex/s3>"),
6512 Some("<http://ex/p1>"),
6513 Some("<http://ex/o5>"),
6514 ),
6515 ];
6516
6517 for (image, tag) in [(&six, "six"), (&three, "three")] {
6518 let resident = Rete::open(image).unwrap();
6519 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
6520 let ranged = Rete::open_ranged(&SliceReader::new(leaked)).unwrap();
6521 let lazy = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
6522 for (s, p, o) in &shapes {
6523 let want: Vec<_> = {
6524 let mut v = Rete::open(&six).unwrap().query(*s, *p, *o);
6525 v.sort();
6526 v
6527 };
6528 for (got, path) in [
6529 (resident.query(*s, *p, *o), "resident"),
6530 (ranged.query(*s, *p, *o), "ranged"),
6531 (lazy.query(*s, *p, *o), "lazy"),
6532 ] {
6533 let mut got = got;
6534 got.sort();
6535 assert_eq!(got, want, "{tag}/{path} disagreed on {:?}", (s, p, o));
6536 }
6537 let mut g = resident.query_in_graph(Some("<http://ex/g>"), *s, *p, *o);
6539 g.sort();
6540 assert_eq!(g, want, "{tag}/named disagreed on {:?}", (s, p, o));
6541 }
6542 }
6543
6544 let leaked: &'static [u8] = Box::leak(three.clone().into_boxed_slice());
6548 let reader = SliceReader::new(leaked);
6549 let routed = Rete::query_ranged(&reader, None, None, Some("<http://ex/o5>")).unwrap();
6550 let mut want = Rete::open(&six)
6551 .unwrap()
6552 .query(None, None, Some("<http://ex/o5>"));
6553 want.sort();
6554 let mut got = routed;
6555 got.sort();
6556 assert_eq!(got, want, "routed single-pattern read on a lean file");
6557 }
6558
6559 #[test]
6560 fn read_coalesced_merges_within_gap_and_splits_beyond() {
6561 use crate::reader::{CountingReader, SliceReader};
6562 let bytes = vec![0u8; 4096];
6563 let ranges = [
6565 ByteRange { offset: 0, len: 16 },
6566 ByteRange {
6567 offset: 48,
6568 len: 16,
6569 },
6570 ByteRange {
6571 offset: 1088,
6572 len: 16,
6573 },
6574 ];
6575 let r = CountingReader::new(SliceReader::new(&bytes));
6577 let out = read_coalesced(&r, &ranges, 16).unwrap();
6578 assert_eq!(out.len(), 3);
6579 assert_eq!(r.requests(), 3);
6580 let r = CountingReader::new(SliceReader::new(&bytes));
6582 read_coalesced(&r, &ranges, 64).unwrap();
6583 assert_eq!(r.requests(), 2);
6584 let r = CountingReader::new(SliceReader::new(&bytes));
6586 read_coalesced(&r, &ranges, 4096).unwrap();
6587 assert_eq!(r.requests(), 1);
6588 }
6589
6590 fn build_image() -> Vec<u8> {
6591 let triples = [
6592 ("Alice", "knows", "Bob"),
6593 ("Bob", "knows", "Carol"),
6594 ("Alice", "age", "30"),
6595 ];
6596 let mut db = DictionaryBuilder::new();
6597 for (s, p, o) in triples {
6598 db.observe(s, p, o);
6599 }
6600 let dict = db.build();
6601
6602 let mut ib = GraphIndexBuilder::new();
6603 for (s, p, o) in triples {
6604 ib.push(dict.encode(s, p, o).unwrap());
6605 }
6606 let index = ib.build();
6607
6608 let (meta, levels) = build_pyramid_meta(&dict, &triples_ids(&dict), DEFAULT_TILE_BUDGET);
6609 write_file(&dict, &index, false, &meta, levels)
6610 }
6611
6612 fn triples_ids(dict: &Dictionary) -> Vec<(u32, u32, u32)> {
6613 [
6614 ("Alice", "knows", "Bob"),
6615 ("Bob", "knows", "Carol"),
6616 ("Alice", "age", "30"),
6617 ]
6618 .iter()
6619 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
6620 .collect()
6621 }
6622
6623 #[test]
6624 fn file_round_trips_header_and_counts() {
6625 let bytes = build_image();
6626 let rete = Rete::open(&bytes).unwrap();
6627 assert_eq!(rete.header().quad_count, 3);
6628 assert!(rete.header().term_count >= 5);
6629 let expected_codec = writer_codec();
6630 assert_eq!(rete.header().dict_codec, expected_codec);
6631 assert_eq!(rete.header().block_codec, expected_codec);
6632 assert_eq!(&bytes[bytes.len() - 4..], &MAGIC); }
6634
6635 #[test]
6640 fn multi_tile_file_round_trips_and_routes() {
6641 let triples: Vec<(String, String, String)> = (0..200)
6642 .map(|i| {
6643 (
6644 format!("<http://ex/s/{i}>"),
6645 format!("<http://ex/p/{}>", i % 5),
6646 format!("<http://ex/o/{}>", i % 23),
6647 )
6648 })
6649 .collect();
6650 let mut db = DictionaryBuilder::new();
6651 for (s, p, o) in &triples {
6652 db.observe(s, p, o);
6653 }
6654 let dict = db.build();
6655 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6656 for (s, p, o) in &triples {
6657 ib.push(dict.encode(s, p, o).unwrap());
6658 }
6659 let index = ib.build();
6660 assert!(
6661 index.tile_sections()[0].len() > 3,
6662 "tiny budget must force many tiles"
6663 );
6664 let bytes = write_file(&dict, &index, false, &[], 0);
6665
6666 let rete = Rete::open(&bytes).unwrap();
6667 assert_eq!(rete.header().version, crate::header::CURRENT_FORMAT_VERSION);
6668 assert_eq!(rete.query(None, None, None).len(), 200);
6669 assert_eq!(rete.query(Some("<http://ex/s/7>"), None, None).len(), 1);
6670 assert_eq!(
6671 rete.query(None, Some("<http://ex/p/3>"), None).len(),
6672 40,
6673 "predicate extent spans tiles"
6674 );
6675 assert_eq!(
6676 rete.query(None, None, Some("<http://ex/o/22>")).len(),
6677 8 );
6679
6680 use crate::reader::SliceReader;
6682 let reader = SliceReader::new(&bytes);
6683 let routed = Rete::query_ranged(&reader, Some("<http://ex/s/7>"), None, None).unwrap();
6684 assert_eq!(routed.len(), 1);
6685 let routed = Rete::query_ranged(&reader, None, Some("<http://ex/p/3>"), None).unwrap();
6686 assert_eq!(routed.len(), 40);
6687 let routed = Rete::query_ranged(&reader, None, None, Some("<http://ex/o/22>")).unwrap();
6688 assert_eq!(routed.len(), 8);
6689 }
6690
6691 #[test]
6700 fn tile_directory_offsets_survive_past_4gib() {
6701 let mut dir = Vec::new();
6702 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);
6708 write_uvarint(&mut dir, 2 << 30); let total = dir.len() as u64 + (3u64 << 30) + (2u64 << 30) + 64;
6710 let entries = parse_tile_directory(&dir, total).unwrap();
6711 assert_eq!(entries.len(), 2);
6712 assert_eq!(entries[1].start, dir.len() as u64 + (3u64 << 30));
6713 assert!(
6714 entries[1].end > u32::MAX as u64,
6715 "tail tile sits past 4 GiB"
6716 );
6717 assert!(parse_tile_directory(&dir, 1 << 20).is_err());
6719 }
6720
6721 #[test]
6722 fn tile_synopsis_trailer_round_trips() {
6723 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6724 for i in 0..200u32 {
6725 ib.push((i, i % 7, i % 13));
6726 }
6727 let index = ib.build();
6728 let tile_count = index.tile_sections()[0].len();
6729 assert!(tile_count > 3, "tiny budget forces many tiles");
6730
6731 let payload = encode_tiled_section(&index, 0, CODEC_NONE);
6732 let dir = parse_tile_directory(&payload, payload.len() as u64).unwrap();
6733 assert_eq!(dir.len(), tile_count);
6734 let trailer_start = dir.iter().map(|e| e.end).max().unwrap();
6736 assert!(
6737 trailer_start < payload.len() as u64,
6738 "a trailer follows the tiles"
6739 );
6740 for e in &dir {
6741 assert!(
6742 e.end <= payload.len() as u64,
6743 "tiles still located within the payload"
6744 );
6745 }
6746 let syn = parse_tile_synopsis(&payload, trailer_start as usize, dir.len()).unwrap();
6747 for (e, (min_b, max_b, min_c, max_c)) in dir.iter().zip(syn) {
6748 let block = decompress(CODEC_NONE, &payload[e.start as usize..e.end as usize]).unwrap();
6749 let z = *crate::triples::TripleBlock::parse(&block).unwrap().zone();
6750 assert_eq!(
6751 (min_b, max_b, min_c, max_c),
6752 (z.min_b, z.max_b, z.min_c, z.max_c),
6753 "synopsis equals the tile's own zone"
6754 );
6755 }
6756 }
6757
6758 #[test]
6762 fn tile_synopsis_lazy_matches_reference_every_shape() {
6763 use crate::reader::{CountingReader, SliceReader};
6764 let triples: Vec<(String, String, String)> = (0..200u32)
6765 .map(|i| {
6766 (
6767 format!("<http://ex/s/{i:04}>"),
6768 format!("<http://ex/p/{}>", i % 7),
6769 format!("<http://ex/o/{:04}>", i % 13),
6770 )
6771 })
6772 .collect();
6773 let mut db = DictionaryBuilder::new();
6774 for (s, p, o) in &triples {
6775 db.observe(s, p, o);
6776 }
6777 let dict = db.build();
6778 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6779 for (s, p, o) in &triples {
6780 ib.push(dict.encode(s, p, o).unwrap());
6781 }
6782 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
6783
6784 let eager = Rete::open(&bytes).unwrap();
6785 assert!(
6786 eager.header().has_tile_synopsis(),
6787 "new files set the synopsis flag"
6788 );
6789
6790 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
6793 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
6794 let lazy = Rete::open_ranged_lazy(reader).unwrap();
6795
6796 let brute = |s: Option<&str>, p: Option<&str>, o: Option<&str>| {
6797 let mut v: Vec<(String, String, String)> = triples
6798 .iter()
6799 .filter(|(a, b, c)| {
6800 s.is_none_or(|x| x == a) && p.is_none_or(|x| x == b) && o.is_none_or(|x| x == c)
6801 })
6802 .cloned()
6803 .collect();
6804 v.sort();
6805 v
6806 };
6807 let sv = [
6809 None,
6810 Some("<http://ex/s/0007>"),
6811 Some("<http://ex/s/0130>"),
6812 Some("<http://ex/s/9999>"),
6813 ];
6814 let pv = [
6815 None,
6816 Some("<http://ex/p/3>"),
6817 Some("<http://ex/p/6>"),
6818 Some("<http://ex/p/999>"),
6819 ];
6820 let ov = [
6821 None,
6822 Some("<http://ex/o/0000>"),
6823 Some("<http://ex/o/0012>"),
6824 Some("<http://ex/o/9999>"),
6825 ];
6826 for &s in &sv {
6827 for &p in &pv {
6828 for &o in &ov {
6829 let mut e = eager.query(s, p, o);
6830 e.sort();
6831 let mut l = lazy.query(s, p, o);
6832 l.sort();
6833 let r = brute(s, p, o);
6834 assert_eq!(e, r, "eager {s:?} {p:?} {o:?}");
6835 assert_eq!(l, r, "lazy {s:?} {p:?} {o:?} — synopsis over-pruned");
6836 }
6837 }
6838 }
6839 assert!(!lazy.index_incomplete(), "no lazy fetch failed");
6840 }
6841
6842 #[test]
6846 fn polyglot_offset_reads_lazily() {
6847 use crate::reader::{
6848 detect_polyglot_base, CountingReader, OffsetReader, SliceReader, POLYGLOT_DIGITS,
6849 POLYGLOT_MARKER,
6850 };
6851
6852 let image = build_image();
6856 let mut shell = Vec::new();
6857 shell.extend_from_slice(b"<!DOCTYPE html><html><head><!--");
6858 shell.extend_from_slice(POLYGLOT_MARKER);
6859 let digits_at = shell.len();
6860 shell.extend_from_slice(&[b'0'; POLYGLOT_DIGITS]); shell.extend_from_slice(b"--></head><body>a web page</body></html>\n");
6862 shell.resize(50_000, b' '); let base = shell.len() as u64;
6864 let digits = format!("{base:0width$}", width = POLYGLOT_DIGITS);
6865 shell[digits_at..digits_at + POLYGLOT_DIGITS].copy_from_slice(digits.as_bytes());
6866
6867 let mut poly = shell;
6868 poly.extend_from_slice(&image);
6869
6870 assert_ne!(
6872 &poly[0..4],
6873 b"RETE",
6874 "polyglot must not start with the magic"
6875 );
6876 let detected = detect_polyglot_base(&poly[..crate::header::HEADER_LEN]).unwrap();
6878 assert_eq!(detected, base);
6879
6880 let leaked: &'static [u8] = Box::leak(poly.into_boxed_slice());
6882 let counting = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
6883 let lazy = Rete::open_ranged_lazy(OffsetReader::new(counting.clone(), detected)).unwrap();
6884
6885 let plain = Rete::open(&image).unwrap();
6887 let mut want = plain.query(None, None, None);
6888 want.sort();
6889 let mut got = lazy.query(None, None, None);
6890 got.sort();
6891 assert_eq!(
6892 got, want,
6893 "polyglot lazy query differs from the plain .rete"
6894 );
6895 assert!(!got.is_empty(), "expected some triples");
6896 assert!(!lazy.index_incomplete(), "a lazy fetch failed");
6897
6898 let read = counting.bytes_read();
6901 eprintln!(
6902 "polyglot lazy read: HTML shell {base} B + .rete image {} B; \
6903 the query touched only {read} B (never the shell).",
6904 image.len()
6905 );
6906 assert!(
6907 read < base,
6908 "read {read} bytes; the HTML shell alone is {base} — the reader \
6909 touched the prefix instead of only the embedded .rete"
6910 );
6911 }
6912
6913 #[cfg(test)]
6916 fn build_text_indexed(triples: &[(String, String, String)]) -> Vec<u8> {
6917 let mut db = DictionaryBuilder::new();
6918 for (s, p, o) in triples {
6919 db.observe(s, p, o);
6920 }
6921 let dict = db.build();
6922 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6923 let mut id_triples: Vec<(u32, u32, u32)> = Vec::with_capacity(triples.len());
6924 for (s, p, o) in triples {
6925 let t = dict.encode(s, p, o).unwrap();
6926 ib.push(t);
6927 id_triples.push(t);
6928 }
6929 let index = ib.build();
6930 let text_index = compute_text_index(&dict, &id_triples);
6931 assert!(
6932 !text_index.is_empty(),
6933 "literals should produce a text index"
6934 );
6935 write_dataset_with_metadata(&dict, &index, &[], false, &[], 0, &[], &text_index)
6936 }
6937
6938 #[test]
6942 fn text_index_eager_matches_brute_force() {
6943 let triples: Vec<(String, String, String)> = vec![
6944 (
6945 "<http://ex/s0>",
6946 "<http://ex/label>",
6947 "\"alpha glucose phosphate\"",
6948 ),
6949 ("<http://ex/s1>", "<http://ex/label>", "\"beta Glucose\""),
6950 ("<http://ex/s2>", "<http://ex/label>", "\"gamma fructose\""),
6951 (
6952 "<http://ex/s3>",
6953 "<http://ex/note>",
6954 "\"einstein relativity\"",
6955 ),
6956 (
6957 "<http://ex/s4>",
6958 "<http://ex/ref>",
6959 "<http://ex/not-a-literal>",
6960 ),
6961 ]
6962 .into_iter()
6963 .map(|(s, p, o)| (s.to_string(), p.to_string(), o.to_string()))
6964 .collect();
6965 let bytes = build_text_indexed(&triples);
6966 let rete = Rete::open(&bytes).unwrap();
6967 assert!(rete.has_text_index());
6968
6969 let brute = |words: &[&str]| -> Vec<String> {
6971 let mut v: Vec<String> = triples
6972 .iter()
6973 .filter(|(_, _, o)| {
6974 crate::terms::is_literal(o)
6975 && words.iter().all(|w| {
6976 let wl = w.to_lowercase();
6977 crate::terms::literal_lexical(o)
6978 .unwrap()
6979 .split(|c: char| !c.is_alphanumeric())
6980 .any(|t| t.to_lowercase() == wl)
6981 })
6982 })
6983 .map(|(s, _, _)| s.clone())
6984 .collect();
6985 v.sort();
6986 v.dedup();
6987 v
6988 };
6989
6990 let mut got = rete.text_search(&["glucose"], None, 0);
6991 got.sort();
6992 assert_eq!(got, brute(&["glucose"]), "case-insensitive single word");
6993
6994 let mut got = rete.text_search(&["glucose", "phosphate"], None, 0);
6996 got.sort();
6997 assert_eq!(got, brute(&["glucose", "phosphate"]));
6998
6999 assert!(rete.text_search(&["zzznope"], None, 0).is_empty());
7001
7002 let got = rete.text_search(&[], Some("ein"), 0);
7004 assert_eq!(got, vec!["<http://ex/s3>".to_string()]);
7005
7006 let mut db = DictionaryBuilder::new();
7008 for (s, p, o) in &triples {
7009 db.observe(s, p, o);
7010 }
7011 let dict = db.build();
7012 let mut ib = GraphIndexBuilder::new();
7013 for (s, p, o) in &triples {
7014 ib.push(dict.encode(s, p, o).unwrap());
7015 }
7016 let plain = write_dataset(&dict, &ib.build(), &[], false, &[], 0);
7017 let plain_rete = Rete::open(&plain).unwrap();
7018 assert!(!plain_rete.has_text_index());
7019 assert!(plain_rete.text_search(&["glucose"], None, 0).is_empty());
7020 }
7021
7022 #[test]
7026 fn text_index_lazy_faults_only_queried_postings() {
7027 use crate::reader::{CountingReader, SliceReader};
7028 let mut triples: Vec<(String, String, String)> = (0..300u32)
7031 .map(|i| {
7032 (
7033 format!("<http://ex/s/{i:04}>"),
7034 "<http://ex/label>".to_string(),
7035 format!("\"common word number {i}\""),
7036 )
7037 })
7038 .collect();
7039 for i in [3u32, 77, 250] {
7040 triples.push((
7041 format!("<http://ex/s/{i:04}>"),
7042 "<http://ex/tag>".to_string(),
7043 "\"raretoken\"".to_string(),
7044 ));
7045 }
7046 let bytes = build_text_indexed(&triples);
7047 let eager = Rete::open(&bytes).unwrap();
7048 let mut want = eager.text_search(&["raretoken"], None, 0);
7049 want.sort();
7050 assert_eq!(want.len(), 3);
7051
7052 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
7053 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7054 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
7055 let before = reader.bytes_read();
7058 let mut got = lazy.text_search(&["raretoken"], None, 0);
7059 got.sort();
7060 assert_eq!(got, want, "lazy search matches eager");
7061 let pulled = reader.bytes_read() - before;
7062 let ti_len = eager.header().text_index_len;
7065 assert!(
7066 pulled < ti_len,
7067 "search pulled {pulled} B but the section is {ti_len} B — faulted too much"
7068 );
7069 assert!(!lazy.index_incomplete());
7070 }
7071
7072 #[test]
7077 fn search_view_matches_full_open_for_far_fewer_bytes() {
7078 use crate::reader::{CountingReader, SliceReader};
7079 let filler = "lorem ipsum dolor sit amet consectetur adipiscing elit ".repeat(20);
7082 let mut triples: Vec<(String, String, String)> = (0..300u32)
7083 .map(|i| {
7084 (
7085 format!("<http://ex/s/{i:04}>"),
7086 "<http://ex/abstract>".to_string(),
7087 format!("\"{filler} number {i}\""),
7088 )
7089 })
7090 .collect();
7091 for i in [3u32, 77, 250] {
7092 triples.push((
7093 format!("<http://ex/s/{i:04}>"),
7094 "<http://ex/tag>".to_string(),
7095 "\"raretoken\"".to_string(),
7096 ));
7097 }
7098 let bytes = build_text_indexed(&triples);
7099 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
7100
7101 let full_reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7102 let full = Rete::open_ranged_lazy(full_reader.clone()).unwrap();
7103 let mut want = full.text_search(&["raretoken"], None, 0);
7104 want.sort();
7105 assert_eq!(want.len(), 3);
7106
7107 let search_reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7108 let view = SearchView::open_ranged(search_reader.clone()).unwrap();
7109 assert!(view.has_text_index());
7110 let mut got = view.text_search(&["raretoken"], None, 0);
7111 got.sort();
7112 assert_eq!(got, want, "search view matches the full open");
7113 assert!(
7114 search_reader.bytes_read() < full_reader.bytes_read(),
7115 "search read {} B, full open read {} B — the narrow open bought nothing",
7116 search_reader.bytes_read(),
7117 full_reader.bytes_read()
7118 );
7119
7120 let mut prefix_hits = view.prefix_search("", 5);
7123 prefix_hits.sort();
7124 assert!(!prefix_hits.is_empty() || !view.has_pyramid());
7125 }
7126
7127 #[test]
7137 fn dict_chunk_directory_probe_costs_about_one_directory() {
7138 use crate::reader::{CountingReader, SliceReader};
7139 let head = "x".repeat(3000);
7153 let triples: Vec<(String, String, String)> = (0..2000u32)
7154 .map(|i| {
7155 let tail = char::from(b'a' + (i % 26) as u8).to_string().repeat(1000);
7156 (
7157 format!("<http://ex/s/{i:04}>"),
7158 "<http://ex/abstract>".to_string(),
7159 format!("\"{head}{i:04}{tail}\""),
7160 )
7161 })
7162 .collect();
7163 let mut db = DictionaryBuilder::new();
7164 for (s, p, o) in &triples {
7165 db.observe(s, p, o);
7166 }
7167 let dict = db.build();
7168 let mut ib = GraphIndexBuilder::new();
7169 for (s, p, o) in &triples {
7170 ib.push(dict.encode(s, p, o).unwrap());
7171 }
7172 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7173
7174 let reader = CountingReader::new(SliceReader::new(&bytes));
7175 let header = Header::from_bytes(&reader.read_at(0, HEADER_LEN as u64).unwrap()).unwrap();
7176 let section = locate_container_section_ranged(
7178 &reader,
7179 header.dictionary_offset,
7180 header.dictionary_len,
7181 2,
7182 4,
7183 )
7184 .unwrap();
7185
7186 let before = reader.bytes_read();
7187 let (_meta, entries) = read_dict_dir_ranged(&reader, section).unwrap();
7188 let spent = reader.bytes_read() - before;
7189
7190 let dir_end = entries[0].start;
7192 let head = reader.read_at(section.offset, 64).unwrap();
7193 let (header_len, n0) = read_uvarint(&head).unwrap();
7194 let dir_start = n0 as u64 + header_len;
7195 let dir_len = dir_end - dir_start;
7196 let dir_total = section.len - dir_start;
7197 assert!(
7198 dir_start + dir_len > 8192,
7199 "directory fits the header prefix ({dir_len} B) — the probe never runs"
7200 );
7201
7202 let mut old = 0u64;
7204 let mut p = 4096u64.min(dir_total).max(1);
7205 loop {
7206 let got = p.min(dir_total);
7207 old += got;
7208 if got >= dir_len || p >= dir_total {
7209 break;
7210 }
7211 p = p.saturating_mul(2).min(dir_total);
7212 }
7213 assert!(
7214 spent <= dir_len * 2,
7215 "probe read {spent} B for a {dir_len} B directory — past the append-only bound"
7216 );
7217 assert!(
7218 spent < old,
7219 "probe read {spent} B; the re-reading loop it replaced spent {old} B"
7220 );
7221 }
7222
7223 fn verbatim_keyed_twin(payload: &[u8], codec: u8) -> Vec<u8> {
7233 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
7234 let (header_len, n0) = read_uvarint(payload).unwrap();
7235 let mut out = Vec::new();
7236 write_uvarint(&mut out, header_len);
7237 out.extend_from_slice(&payload[n0..n0 + header_len as usize]);
7238 write_uvarint(&mut out, entries.len() as u64);
7239 let mut prev_run = 0usize;
7240 for e in &entries {
7241 let body = decompress(codec, &payload[e.start as usize..e.end as usize]).unwrap();
7242 let first = crate::dict::run_first_term(&body, 0).unwrap();
7243 write_uvarint(&mut out, (e.first_run - prev_run) as u64);
7244 write_uvarint(&mut out, first.len() as u64);
7245 out.extend_from_slice(&first);
7246 write_uvarint(&mut out, e.end - e.start);
7247 prev_run = e.first_run;
7248 }
7249 for e in &entries {
7250 out.extend_from_slice(&payload[e.start as usize..e.end as usize]);
7251 }
7252 out
7253 }
7254
7255 fn chunk_bounds_of(payload: &[u8], codec: u8) -> Vec<(Vec<u8>, Vec<u8>)> {
7257 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
7258 entries
7259 .iter()
7260 .map(|e| {
7261 let body = decompress(codec, &payload[e.start as usize..e.end as usize]).unwrap();
7262 (
7263 crate::dict::run_first_term(&body, 0).unwrap(),
7264 crate::dict::run_last_term(&body, 0, body.len()).unwrap(),
7265 )
7266 })
7267 .collect()
7268 }
7269
7270 fn adversarial_terms() -> Vec<String> {
7276 let mut terms: Vec<String> = Vec::new();
7277 for i in 0..6_000 {
7278 terms.push(format!("<http://example.org/work/{i:06}>"));
7279 }
7280 let shared = "X".repeat(900);
7281 for i in 0..4_000 {
7282 terms.push(format!("\"{shared}-{i:05}-tail\""));
7283 }
7284 let deep = "Y".repeat(3_000); for i in 0..120 {
7286 terms.push(format!("\"{deep}{i:03}\""));
7287 }
7288 for i in 0..6 {
7289 terms.push(format!("\"BIG-{i:02}-{}\"", "Z".repeat(90_000)));
7291 }
7292 for i in 0..800 {
7293 terms.push(format!(
7294 "\"h\u{e9}llo-{}-\u{1F600}{i:04}\"",
7295 "\u{e9}".repeat(40)
7296 ));
7297 }
7298 for i in 0..2_000 {
7299 terms.push(format!("\"{i:05}-{}\"", "T".repeat(1_800)));
7300 }
7301 terms.sort();
7302 terms.dedup();
7303 terms
7304 }
7305
7306 fn chunked_section_of(terms: &[String]) -> Vec<u8> {
7307 let mut b = crate::dict::DictSectionBuilder::new().with_restart_interval(16);
7308 for t in terms {
7309 b.push(t.clone());
7310 }
7311 b.build()
7312 }
7313
7314 #[test]
7324 fn chunk_directory_keys_are_separators_not_terms() {
7325 let terms = adversarial_terms();
7326 let raw = chunked_section_of(&terms);
7327 let payload = encode_chunked_dict_section(&raw, CODEC_NONE);
7328 let (_meta, entries) = parse_chunked_dict_dir(&payload, payload.len() as u64).unwrap();
7329 let bounds = chunk_bounds_of(&payload, CODEC_NONE);
7330 assert!(
7331 entries.len() > 20,
7332 "need a many-chunk section, got {}",
7333 entries.len()
7334 );
7335
7336 assert!(entries[0].key.is_empty(), "chunk 0 needs no separator");
7337 let mut key_bytes = 0usize;
7338 let mut term_bytes = 0usize;
7339 let mut differ = 0usize;
7340 for (i, e) in entries.iter().enumerate() {
7341 key_bytes += e.key.len();
7342 term_bytes += bounds[i].0.len();
7343 if e.key != bounds[i].0 {
7344 differ += 1;
7345 }
7346 assert!(
7347 e.key <= bounds[i].0,
7348 "chunk {i}: key must be <= its own first term"
7349 );
7350 if i > 0 {
7351 assert!(
7352 e.key.as_slice() > bounds[i - 1].1.as_slice(),
7353 "chunk {i}: key must be > the previous chunk's last term"
7354 );
7355 assert!(
7356 e.key > entries[i - 1].key,
7357 "chunk {i}: keys must ascend for partition_point"
7358 );
7359 assert_eq!(
7361 e.key,
7362 crate::dict::shortest_separator(&bounds[i - 1].1, &bounds[i].0),
7363 "chunk {i}: key is not the SHORTEST separator"
7364 );
7365 }
7366 }
7367 assert!(
7368 differ > 0,
7369 "every key equalled its first term — the writer stores terms again"
7370 );
7371 assert!(
7372 key_bytes * 4 < term_bytes,
7373 "separators bought almost nothing: {key_bytes} B vs {term_bytes} B of first terms"
7374 );
7375 eprintln!(
7376 "chunks={} first-term keys={term_bytes} B separator keys={key_bytes} B ({:.1}x)",
7377 entries.len(),
7378 term_bytes as f64 / key_bytes.max(1) as f64
7379 );
7380 }
7381
7382 #[test]
7393 fn separator_keys_route_identically_to_verbatim_first_terms() {
7394 let terms = adversarial_terms();
7395 let raw = chunked_section_of(&terms);
7396 let sep_payload = encode_chunked_dict_section(&raw, CODEC_NONE);
7397 let ver_payload = verbatim_keyed_twin(&sep_payload, CODEC_NONE);
7398
7399 let (_ms, es) = parse_chunked_dict_dir(&sep_payload, sep_payload.len() as u64).unwrap();
7401 let (_mv, ev) = parse_chunked_dict_dir(&ver_payload, ver_payload.len() as u64).unwrap();
7402 assert_eq!(es.len(), ev.len());
7403 for (a, b) in es.iter().zip(ev.iter()) {
7404 assert_eq!(a.first_run, b.first_run);
7405 assert_eq!(
7406 &sep_payload[a.start as usize..a.end as usize],
7407 &ver_payload[b.start as usize..b.end as usize],
7408 "chunk bodies must be identical"
7409 );
7410 }
7411 assert!(
7412 sep_payload.len() < ver_payload.len(),
7413 "separator payload is not smaller"
7414 );
7415
7416 let sec_s = decode_chunked_dict_section(
7417 &sep_payload,
7418 CODEC_NONE,
7419 crate::chunk_cache::ChunkCache::unlimited_arc(),
7420 0,
7421 )
7422 .unwrap();
7423 let sec_v = decode_chunked_dict_section(
7424 &ver_payload,
7425 CODEC_NONE,
7426 crate::chunk_cache::ChunkCache::unlimited_arc(),
7427 0,
7428 )
7429 .unwrap();
7430
7431 for (i, t) in terms.iter().enumerate() {
7433 let want = Some(i as u32 + 1);
7434 assert_eq!(sec_v.id(t), want, "verbatim id() lost a term");
7435 assert_eq!(sec_s.id(t), want, "separator id() lost a term");
7436 }
7437 for i in 0..terms.len() as u32 {
7439 assert_eq!(sec_s.term(i + 1), sec_v.term(i + 1));
7440 assert_eq!(
7441 sec_s.term(i + 1).as_deref(),
7442 Some(terms[i as usize].as_str())
7443 );
7444 }
7445 let bounds = chunk_bounds_of(&sep_payload, CODEC_NONE);
7447 let mut probes: Vec<String> = Vec::new();
7448 let push = |bytes: &[u8], probes: &mut Vec<String>| {
7449 if let Ok(s) = std::str::from_utf8(bytes) {
7450 probes.push(s.to_string());
7451 }
7452 };
7453 for (first, last) in &bounds {
7454 for base in [first, last] {
7455 push(base, &mut probes); let mut after = base.clone();
7457 after.push(0x01); push(&after, &mut probes);
7459 let mut far = base.clone();
7460 far.push(b'~');
7461 push(&far, &mut probes);
7462 if !base.is_empty() {
7463 push(&base[..base.len() - 1], &mut probes); let mut down = base.clone();
7465 *down.last_mut().unwrap() = down.last().unwrap().wrapping_sub(1);
7466 push(&down, &mut probes);
7467 let mut up = base.clone();
7468 *up.last_mut().unwrap() = up.last().unwrap().wrapping_add(1);
7469 push(&up, &mut probes);
7470 }
7471 }
7472 }
7473 for e in &es {
7475 push(&e.key, &mut probes);
7476 let mut plus = e.key.clone();
7477 plus.push(0);
7478 push(&plus, &mut probes);
7479 }
7480 probes.push(String::new()); probes.push("\u{1}".to_string()); probes.push("\u{10FFFF}\u{10FFFF}".to_string()); probes.push(terms.first().unwrap().clone());
7484 probes.push(terms.last().unwrap().clone());
7485
7486 let mut hits = 0usize;
7487 for p in &probes {
7488 let (a, b) = (sec_v.id(p), sec_s.id(p));
7489 assert_eq!(
7490 a,
7491 b,
7492 "probe diverged: {:?}",
7493 p.get(..p.len().min(48)).unwrap_or(p.as_str())
7494 );
7495 hits += usize::from(a.is_some());
7496 }
7497 assert!(
7498 probes.len() > 500 && hits > 50,
7499 "probe set is too thin: {} probes, {hits} of them present",
7500 probes.len()
7501 );
7502 eprintln!(
7503 "{} boundary probes, {hits} present, all identical",
7504 probes.len()
7505 );
7506 }
7507
7508 mod separator_props {
7514 use super::*;
7515 use proptest::prelude::*;
7516
7517 fn term_set() -> impl Strategy<Value = Vec<String>> {
7524 prop::collection::vec(("[a-c]{1,6}", 0u8..3, 300usize..1200), 60..250).prop_map(
7525 |specs| {
7526 let mut terms: Vec<String> = specs
7527 .into_iter()
7528 .enumerate()
7529 .map(|(i, (core, kind, pad))| {
7530 let block = "z".repeat(pad);
7531 match kind {
7532 0 => format!("<http://ex/{core}/{i:05}>"),
7533 1 => format!("\"{block}{core}{i:05}\""),
7534 _ => format!("\"{core}{i:05}{block}\""),
7535 }
7536 })
7537 .collect();
7538 terms.sort();
7539 terms.dedup();
7540 terms
7541 },
7542 )
7543 }
7544
7545 proptest! {
7546 #[test]
7547 fn prop_separator_keyed_section_answers_like_verbatim(
7548 terms in term_set(),
7549 extra in prop::collection::vec(any::<String>(), 0..8),
7550 ) {
7551 let raw = chunked_section_of(&terms);
7552 let sep = encode_chunked_dict_section(&raw, CODEC_NONE);
7553 let ver = verbatim_keyed_twin(&sep, CODEC_NONE);
7554 prop_assert!(sep.len() <= ver.len());
7555
7556 let (_m, es) = parse_chunked_dict_dir(&sep, sep.len() as u64).unwrap();
7558 let bounds = chunk_bounds_of(&sep, CODEC_NONE);
7559 prop_assert!(es[0].key.is_empty());
7560 for i in 1..es.len() {
7561 prop_assert!(es[i].key.as_slice() > bounds[i - 1].1.as_slice());
7562 prop_assert!(es[i].key <= bounds[i].0);
7563 prop_assert!(es[i].key > es[i - 1].key);
7564 }
7565
7566 let sec_s = decode_chunked_dict_section(
7567 &sep,
7568 CODEC_NONE,
7569 crate::chunk_cache::ChunkCache::unlimited_arc(),
7570 0,
7571 )
7572 .unwrap();
7573 let sec_v = decode_chunked_dict_section(
7574 &ver,
7575 CODEC_NONE,
7576 crate::chunk_cache::ChunkCache::unlimited_arc(),
7577 0,
7578 )
7579 .unwrap();
7580
7581 for (i, t) in terms.iter().enumerate() {
7582 prop_assert_eq!(sec_s.id(t), Some(i as u32 + 1));
7583 prop_assert_eq!(sec_v.id(t), Some(i as u32 + 1));
7584 prop_assert_eq!(sec_s.term(i as u32 + 1), sec_v.term(i as u32 + 1));
7585 }
7586
7587 let mut probes: Vec<String> = extra;
7589 probes.push(String::new());
7590 for (first, last) in &bounds {
7591 for base in [first, last] {
7592 if let Ok(s) = std::str::from_utf8(base) {
7593 probes.push(s.to_string());
7594 probes.push(format!("{s}\u{1}"));
7595 probes.push(s[..s.len() - s.chars().next_back()
7596 .map(char::len_utf8).unwrap_or(0)].to_string());
7597 }
7598 }
7599 }
7600 for e in &es {
7601 if let Ok(s) = std::str::from_utf8(&e.key) {
7602 probes.push(s.to_string());
7603 }
7604 }
7605 for p in &probes {
7606 prop_assert_eq!(sec_s.id(p), sec_v.id(p), "probe diverged: {:?}", p);
7607 }
7608 }
7609 }
7610 }
7611
7612 #[test]
7617 fn text_index_is_tamper_evident_and_verifies() {
7618 let triples: Vec<(String, String, String)> = vec![(
7619 "<http://ex/s0>".to_string(),
7620 "<http://ex/label>".to_string(),
7621 "\"alpha glucose phosphate\"".to_string(),
7622 )];
7623 let bytes = build_text_indexed(&triples);
7624 let header = Rete::open(&bytes).unwrap().header().clone();
7625 assert!(header.text_index_len > 0);
7626 assert!(verify(&bytes).unwrap(), "a text-indexed build must verify");
7627
7628 let mut tampered = bytes.clone();
7629 tampered[header.text_index_offset as usize] ^= 0xff;
7630 assert!(
7631 !verify(&tampered).unwrap(),
7632 "tampering with the text index must break verify()"
7633 );
7634 }
7635
7636 #[test]
7640 fn synopsis_cuts_remote_fetch_bytes() {
7641 use crate::header::FLAG_TILE_SYNOPSIS;
7642 use crate::reader::{CountingReader, SliceReader};
7643
7644 let triples: Vec<(String, String, String)> = (0..400u32)
7648 .map(|i| {
7649 (
7650 format!("<http://ex/s/{i:04}>"),
7651 "<http://ex/p>".to_string(),
7652 format!("<http://ex/o/{i:04}>"),
7653 )
7654 })
7655 .collect();
7656 let mut db = DictionaryBuilder::new();
7657 for (s, p, o) in &triples {
7658 db.observe(s, p, o);
7659 }
7660 let dict = db.build();
7661 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
7662 for (s, p, o) in &triples {
7663 ib.push(dict.encode(s, p, o).unwrap());
7664 }
7665 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7666
7667 let q = (Some("<http://ex/s/0395>"), None, Some("<http://ex/o/0005>"));
7671 let query_bytes = |image: &[u8]| -> (u64, usize) {
7675 let leaked: &'static [u8] = Box::leak(image.to_vec().into_boxed_slice());
7676 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7677 let rete = Rete::open_ranged_lazy(reader.clone()).unwrap();
7678 let before = reader.bytes_read(); let n = rete.query(q.0, q.1, q.2).len();
7680 assert!(!rete.index_incomplete());
7681 (reader.bytes_read() - before, n)
7682 };
7683
7684 let (on_bytes, on_n) = query_bytes(&bytes);
7685 let mut off = bytes.clone();
7687 off[5] &= !FLAG_TILE_SYNOPSIS;
7688 let (off_bytes, off_n) = query_bytes(&off);
7689
7690 assert_eq!(on_n, 0, "the pair never co-occurs");
7691 assert_eq!(off_n, 0, "same answer without the synopsis");
7692 assert!(
7696 on_bytes < off_bytes,
7697 "synopsis skips the routed tile fetch: {on_bytes} < {off_bytes}"
7698 );
7699 }
7700
7701 #[test]
7707 fn double_bound_object_join_eager_matches_lazy() {
7708 use crate::reader::SliceReader;
7709 let occ = "<http://ex/occ>";
7710 let phys = "<http://ex/physicist>";
7711 let phil = "<http://ex/philosopher>";
7712 let label = "<http://www.w3.org/2000/01/rdf-schema#label>";
7713 let mut triples: Vec<(String, String, String)> = Vec::new();
7715 for i in 0..20u32 {
7716 triples.push((format!("<http://ex/p/{i:02}>"), occ.into(), phys.into()));
7717 if i < 10 {
7718 triples.push((format!("<http://ex/p/{i:02}>"), occ.into(), phil.into()));
7719 }
7720 triples.push((
7721 format!("<http://ex/p/{i:02}>"),
7722 label.into(),
7723 format!("\"Name {i:02}\""),
7724 ));
7725 }
7726 let mut db = DictionaryBuilder::new();
7727 for (s, p, o) in &triples {
7728 db.observe(s, p, o);
7729 }
7730 let dict = db.build();
7731 let mut ib = GraphIndexBuilder::new().with_tile_budget(16);
7732 for (s, p, o) in &triples {
7733 ib.push(dict.encode(s, p, o).unwrap());
7734 }
7735 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7736
7737 let q = "SELECT ?l WHERE { \
7738 ?p <http://ex/occ> <http://ex/physicist> ; \
7739 <http://ex/occ> <http://ex/philosopher> ; \
7740 <http://www.w3.org/2000/01/rdf-schema#label> ?l }";
7741 let run = |rete: &Rete| -> Vec<String> {
7742 let (_, sols) = crate::eval_sparql(rete, q).unwrap();
7743 let mut v: Vec<String> = sols.iter().filter_map(|b| b.get("l").cloned()).collect();
7744 v.sort();
7745 v
7746 };
7747
7748 let eager_rows = run(&Rete::open(&bytes).unwrap());
7749 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
7750 let lazy = Rete::open_ranged_lazy(std::sync::Arc::new(SliceReader::new(leaked))).unwrap();
7751 let lazy_rows = run(&lazy);
7752 assert!(!lazy.index_incomplete());
7753
7754 assert_eq!(eager_rows.len(), 10, "the 10 physicist∩philosopher labels");
7755 assert_eq!(eager_rows, lazy_rows, "eager and lazy must agree exactly");
7756 }
7757
7758 #[test]
7762 fn multi_chunk_dictionary_round_trips() {
7763 let mut db = DictionaryBuilder::new();
7764 let term = |i: u32| format!("<http://example.org/some/long/prefix/entity/{i:06}>");
7765 for i in 0..6000u32 {
7766 db.observe(&term(i), "<http://ex/p>", &term(i + 1));
7767 }
7768 let dict = db.build();
7769 let mut ib = GraphIndexBuilder::new();
7770 for i in 0..6000u32 {
7771 ib.push(
7772 dict.encode(&term(i), "<http://ex/p>", &term(i + 1))
7773 .unwrap(),
7774 );
7775 }
7776 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7777 let rete = Rete::open(&bytes).unwrap();
7778 let d = rete.dictionary();
7779 assert_eq!(d.term_count(), dict.term_count());
7780 for i in (0..6000).step_by(97).chain([0, 1, 5999, 6000]) {
7781 let t = term(i);
7782 let sid = dict.subject_id(&t);
7783 assert_eq!(d.subject_id(&t), sid, "subject_id({t})");
7784 if let Some(id) = sid {
7785 assert_eq!(d.subject_term(id).as_deref(), Some(t.as_str()));
7786 }
7787 let oid = dict.object_id(&t);
7788 assert_eq!(d.object_id(&t), oid, "object_id({t})");
7789 }
7790 assert_eq!(d.subject_id("<http://example.org/absent>"), None);
7791 assert_eq!(d.predicate_id("<http://ex/p>"), Some(1));
7792 assert_eq!(d.predicate_term(1).as_deref(), Some("<http://ex/p>"));
7793 }
7794
7795 #[test]
7796 #[cfg(feature = "compression")]
7797 fn compression_shrinks_repetitive_data() {
7798 let mut db = DictionaryBuilder::new();
7802 let triples: Vec<(String, String, String)> = (0..500)
7803 .map(|i| {
7804 (
7805 format!("<http://example.org/entity/{i}>"),
7806 "<http://example.org/p/relatedTo>".to_string(),
7807 format!("<http://example.org/entity/{}>", (i + 1) % 500),
7808 )
7809 })
7810 .collect();
7811 for (s, p, o) in &triples {
7812 db.observe(s, p, o);
7813 }
7814 let dict = db.build();
7815 let mut ib = GraphIndexBuilder::new();
7816 for (s, p, o) in &triples {
7817 ib.push(dict.encode(s, p, o).unwrap());
7818 }
7819 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7820
7821 let raw: usize = triples
7822 .iter()
7823 .map(|(s, p, o)| s.len() + p.len() + o.len())
7824 .sum();
7825 assert!(
7826 bytes.len() < raw / 2,
7827 "expected strong compression: file {} vs raw terms {raw}",
7828 bytes.len()
7829 );
7830
7831 let rete = Rete::open(&bytes).unwrap();
7833 let r = rete.query(Some("<http://example.org/entity/0>"), None, None);
7834 assert_eq!(r.len(), 1);
7835 assert_eq!(r[0].2, "<http://example.org/entity/1>");
7836 }
7837
7838 fn big_file_with_pyramid() -> Vec<u8> {
7839 let triples: Vec<(String, String, String)> = (0..300)
7841 .map(|i| {
7842 (
7843 format!("<http://ex/e{i}>"),
7844 "<http://ex/next>".to_string(),
7845 format!("<http://ex/e{}>", (i + 1) % 300),
7846 )
7847 })
7848 .collect();
7849 let mut db = DictionaryBuilder::new();
7850 for (s, p, o) in &triples {
7851 db.observe(s, p, o);
7852 }
7853 let dict = db.build();
7854 let ids: Vec<_> = triples
7855 .iter()
7856 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
7857 .collect();
7858 let mut ib = GraphIndexBuilder::new();
7859 for &t in &ids {
7860 ib.push(t);
7861 }
7862 let (meta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
7863 write_file(&dict, &ib.build(), false, &meta, levels)
7864 }
7865
7866 #[test]
7867 fn ranged_open_is_minimal_and_correct() {
7868 use crate::reader::{CountingReader, SliceReader};
7869 let bytes = big_file_with_pyramid();
7870
7871 let full = CountingReader::new(SliceReader::new(&bytes));
7872 let rete = Rete::open_ranged(&full).unwrap();
7873 assert!(full.requests() <= 4, "requests = {}", full.requests());
7875 assert_eq!(
7876 rete.query(Some("<http://ex/e0>"), None, None)[0].2,
7877 "<http://ex/e1>"
7878 );
7879
7880 let summ_reader = CountingReader::new(SliceReader::new(&bytes));
7882 let view = SummaryView::open_ranged(&summ_reader).unwrap().unwrap();
7883 assert!(!view.summary.is_empty());
7884 assert!(
7885 summ_reader.bytes_read() < bytes.len() as u64,
7886 "summary read {} of {} bytes",
7887 summ_reader.bytes_read(),
7888 bytes.len()
7889 );
7890 assert!(summ_reader.bytes_read() < full.bytes_read());
7892 }
7893
7894 #[test]
7895 fn content_hash_is_set_and_verifies() {
7896 let bytes = build_image();
7897 let rete = Rete::open(&bytes).unwrap();
7898 assert_ne!(
7899 rete.header().content_hash,
7900 [0u8; 16],
7901 "hash must be populated"
7902 );
7903 assert!(verify(&bytes).unwrap(), "freshly built file verifies");
7904 assert!(
7905 verify_ranged(&crate::reader::SliceReader::new(&bytes)).unwrap(),
7906 "the streaming verify agrees"
7907 );
7908
7909 assert_eq!(
7911 Rete::open(&build_image()).unwrap().header().content_hash,
7912 rete.header().content_hash
7913 );
7914
7915 let mut tampered = bytes.clone();
7917 let last = tampered.len() - 5; tampered[last] ^= 0xff;
7919 assert!(!verify(&tampered).unwrap());
7920 assert!(!verify_ranged(&crate::reader::SliceReader::new(&tampered)).unwrap());
7921 let short = &bytes[..bytes.len() / 2];
7923 assert!(verify_ranged(&crate::reader::SliceReader::new(short)).is_err());
7924 assert!(verify_ranged(&crate::reader::SliceReader::new(&bytes[..10])).is_err());
7925 }
7926
7927 fn build_with_metadata(meta: &[u8]) -> Vec<u8> {
7929 let triples = [
7930 ("Alice", "knows", "Bob"),
7931 ("Bob", "knows", "Carol"),
7932 ("Alice", "age", "30"),
7933 ];
7934 let mut db = DictionaryBuilder::new();
7935 for (s, p, o) in triples {
7936 db.observe(s, p, o);
7937 }
7938 let dict = db.build();
7939 let ids: Vec<_> = triples
7940 .iter()
7941 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
7942 .collect();
7943 let mut ib = GraphIndexBuilder::new();
7944 for &t in &ids {
7945 ib.push(t);
7946 }
7947 let (pmeta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
7948 write_dataset_with_metadata(&dict, &ib.build(), &[], false, &pmeta, levels, meta, &[])
7949 }
7950
7951 #[test]
7952 fn metadata_round_trips_and_shifts_offsets() {
7953 let card = br#"{"title":"My Dataset"}"#;
7954 let bytes = build_with_metadata(card);
7955 let rete = Rete::open(&bytes).unwrap();
7956
7957 assert_eq!(rete.metadata(), Some(card.as_slice()));
7959 let h = rete.header();
7960 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
7961 assert_eq!(h.metadata_len, card.len() as u64);
7962 assert_eq!(h.dictionary_offset, HEADER_LEN as u64 + card.len() as u64);
7964
7965 assert_eq!(
7967 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
7968 1
7969 );
7970 assert!(verify(&bytes).unwrap());
7972 }
7973
7974 #[test]
7975 fn empty_metadata_is_byte_identical_to_plain_writer() {
7976 assert_eq!(
7979 build_with_metadata(&[]),
7980 build_image(),
7981 "empty-metadata output must equal the plain writer byte-for-byte"
7982 );
7983 }
7984
7985 #[test]
7990 fn replace_metadata_matches_a_direct_write() {
7991 let first = br#"{"title":"My Dataset","queries":[1,2,3]}"#;
7992 let bytes = build_with_metadata(first);
7993 for target in [
7994 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(), ] {
7999 let spliced = replace_metadata(&bytes, target).unwrap();
8000 assert_eq!(
8001 spliced,
8002 build_with_metadata(target),
8003 "splicing {} bytes of metadata must equal a direct write",
8004 target.len()
8005 );
8006 assert!(
8007 verify(&spliced).unwrap(),
8008 "the new hash covers the new card"
8009 );
8010 let rete = Rete::open(&spliced).unwrap();
8011 assert_eq!(
8012 rete.metadata(),
8013 (!target.is_empty()).then_some(target),
8014 "the new payload reads back verbatim"
8015 );
8016 assert_eq!(
8018 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
8019 1
8020 );
8021 }
8022 }
8023
8024 #[test]
8027 fn replace_metadata_keeps_build_info_adjacent() {
8028 let with_info =
8029 attach_build_info(&build_with_metadata(br#"{"a":1}"#), br#"{"builder":"x"}"#).unwrap();
8030 let spliced = replace_metadata(&with_info, br#"{"a":1,"b":2}"#).unwrap();
8031 let h = Header::from_bytes(&spliced).unwrap();
8032 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
8033 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + h.metadata_len);
8034 assert_eq!(
8035 read_build_info(&spliced).unwrap().as_deref(),
8036 Some(br#"{"builder":"x"}"#.as_slice())
8037 );
8038 assert!(verify(&spliced).unwrap());
8039 assert_eq!(
8041 h.content_hash,
8042 Header::from_bytes(&build_with_metadata(br#"{"a":1,"b":2}"#))
8043 .unwrap()
8044 .content_hash
8045 );
8046 }
8047
8048 #[test]
8049 fn metadata_is_tamper_evident() {
8050 let card = br#"{"title":"x"}"#;
8051 let mut bytes = build_with_metadata(card);
8052 assert!(verify(&bytes).unwrap());
8053 bytes[HEADER_LEN + 2] ^= 0xff;
8055 assert!(
8056 !verify(&bytes).unwrap(),
8057 "tampering with the card must break verify()"
8058 );
8059 }
8060
8061 #[test]
8062 fn ranged_opens_do_not_fetch_metadata() {
8063 use crate::reader::{CountingReader, SliceReader};
8064 let card = vec![0xABu8; 512]; let bytes = build_with_metadata(&card);
8066 let total = bytes.len() as u64;
8067
8068 let r = CountingReader::new(SliceReader::new(&bytes));
8070 let rete = Rete::open_ranged(&r).unwrap();
8071 assert!(
8072 rete.metadata().is_none(),
8073 "open_ranged must not load the card"
8074 );
8075 assert!(r.requests() <= 4, "requests = {}", r.requests());
8076 assert!(
8077 r.bytes_read() <= total - card.len() as u64,
8078 "read {} of {} bytes; the {}-byte card must be skipped",
8079 r.bytes_read(),
8080 total,
8081 card.len()
8082 );
8083
8084 let rs = CountingReader::new(SliceReader::new(&bytes));
8086 let view = SummaryView::open_ranged(&rs).unwrap().unwrap();
8087 assert!(!view.summary.is_empty());
8088 assert!(rs.bytes_read() <= total - card.len() as u64);
8089 }
8090
8091 #[test]
8092 fn metadata_ranged_fetches_only_header_and_card() {
8093 use crate::reader::{CountingReader, SliceReader};
8094 let card = vec![0xCDu8; 384];
8097 let bytes = build_with_metadata(&card);
8098
8099 let r = CountingReader::new(SliceReader::new(&bytes));
8100 let got = read_metadata_ranged(&r).unwrap().unwrap();
8101 assert_eq!(got, card, "the card reads back verbatim");
8102 assert_eq!(r.requests(), 2, "exactly header + metadata ranges");
8103 assert_eq!(
8104 r.bytes_read(),
8105 HEADER_LEN as u64 + card.len() as u64,
8106 "no dictionary/index/pyramid bytes are touched"
8107 );
8108
8109 let plain = build_image();
8111 let rp = CountingReader::new(SliceReader::new(&plain));
8112 assert!(read_metadata_ranged(&rp).unwrap().is_none());
8113 assert_eq!(rp.requests(), 1, "header only for a cardless file");
8114 assert_eq!(rp.bytes_read(), HEADER_LEN as u64);
8115 }
8116
8117 #[test]
8118 fn build_info_attaches_outside_the_hash() {
8119 let card = br#"{"title":"My Dataset"}"#;
8120 let base = build_with_metadata(card);
8121 let base_hash = Header::from_bytes(&base).unwrap().content_hash;
8122
8123 let info = br#"{"built_at":"2026-08-04T00:00:00Z","builder":"rete-cli 0.3.2"}"#;
8124 let with = attach_build_info(&base, info).unwrap();
8125
8126 assert_eq!(read_build_info(&with).unwrap().unwrap(), info);
8128 let h = Header::from_bytes(&with).unwrap();
8129 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + card.len() as u64);
8130 assert_eq!(h.build_info_len, info.len() as u64);
8131 assert_eq!(h.expected_file_len(), Some(with.len() as u64));
8132
8133 assert_eq!(h.content_hash, base_hash);
8137 assert!(verify(&with).unwrap());
8138 let other = attach_build_info(&base, br#"{"built_at":"1999-01-01T00:00:00Z"}"#).unwrap();
8139 assert_eq!(Header::from_bytes(&other).unwrap().content_hash, base_hash);
8140
8141 let rete = Rete::open(&with).unwrap();
8143 assert_eq!(
8144 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
8145 1
8146 );
8147 assert_eq!(rete.metadata(), Some(card.as_slice()));
8148
8149 let replaced = attach_build_info(&with, br#"{"builder":"other"}"#).unwrap();
8152 assert_eq!(
8153 read_build_info(&replaced).unwrap().unwrap(),
8154 br#"{"builder":"other"}"#
8155 );
8156 assert!(verify(&replaced).unwrap());
8157 let stripped = attach_build_info(&replaced, &[]).unwrap();
8158 assert_eq!(stripped, base, "stripping build-info restores the image");
8159 }
8160
8161 #[test]
8162 fn build_info_attaches_on_a_metadata_free_file() {
8163 let base = build_image();
8165 let info = b"{\"builder\":\"x\"}";
8166 let with = attach_build_info(&base, info).unwrap();
8167 let h = Header::from_bytes(&with).unwrap();
8168 assert_eq!(h.build_info_offset, HEADER_LEN as u64);
8169 assert_eq!(h.metadata_len, 0);
8170 assert_eq!(read_build_info(&with).unwrap().unwrap(), info);
8171 assert!(verify(&with).unwrap());
8172 assert_eq!(
8173 h.content_hash,
8174 Header::from_bytes(&base).unwrap().content_hash
8175 );
8176 Rete::open(&with).unwrap();
8177 }
8178
8179 #[test]
8186 fn a_plan_rebuilds_exactly_what_the_in_memory_splice_produces() {
8187 let card = br#"{"title":"My Dataset"}"#;
8188 let base = build_with_metadata(card);
8189 let base_hash = Header::from_bytes(&base).unwrap().content_hash;
8190 for start in [
8191 base.clone(),
8192 attach_build_info(&base, b"{\"a\":1}").unwrap(),
8193 ] {
8194 for info in [
8195 br#"{"builder":"rete-cli 0.3.2","query_costs":{}}"#.as_slice(), b"{}".as_slice(), b"".as_slice(), ] {
8199 let plan = plan_build_info(&start, start.len() as u64, info.len() as u64).unwrap();
8200 let mut streamed = Vec::new();
8201 streamed.extend_from_slice(&plan.header);
8202 streamed.extend_from_slice(&start[HEADER_LEN..plan.insert as usize]);
8203 streamed.extend_from_slice(info);
8204 streamed.extend_from_slice(&start[plan.tail_start as usize..]);
8205 assert_eq!(streamed.len() as u64, plan.new_len);
8206 assert_eq!(
8207 streamed,
8208 attach_build_info(&start, info).unwrap(),
8209 "a {}-byte section streamed must equal the same section spliced",
8210 info.len()
8211 );
8212 assert!(verify(&streamed).unwrap());
8214 let h = Header::from_bytes(&streamed).unwrap();
8215 assert_eq!(h.content_hash, base_hash);
8216 assert_eq!(h.expected_file_len(), Some(plan.new_len));
8217 let rete = Rete::open(&streamed).unwrap();
8218 assert_eq!(rete.metadata(), Some(card.as_slice()));
8219 assert_eq!(
8220 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
8221 1
8222 );
8223 }
8224 }
8225 }
8226
8227 #[test]
8230 fn a_plan_refuses_a_header_that_overruns_the_file() {
8231 let base = build_with_metadata(br#"{"title":"x"}"#);
8232 assert!(plan_build_info(&base, 8, 16).is_err());
8233 assert!(plan_build_info(&base[..HEADER_LEN], base.len() as u64, 16).is_ok());
8234 }
8235
8236 #[test]
8237 fn card_and_build_info_ranged_is_one_header_plus_one_range() {
8238 use crate::reader::{CountingReader, SliceReader};
8239 let card = vec![0xCDu8; 384];
8240 let info = vec![0xEFu8; 200];
8241 let bytes = attach_build_info(&build_with_metadata(&card), &info).unwrap();
8242
8243 let r = CountingReader::new(SliceReader::new(&bytes));
8246 let (m, b) = read_card_and_build_info_ranged(&r).unwrap();
8247 assert_eq!(m.unwrap(), card);
8248 assert_eq!(b.unwrap(), info);
8249 assert_eq!(r.requests(), 2, "header + one coalesced range");
8250 assert_eq!(
8251 r.bytes_read(),
8252 HEADER_LEN as u64 + card.len() as u64 + info.len() as u64
8253 );
8254
8255 let plain = build_with_metadata(&card);
8257 let rp = CountingReader::new(SliceReader::new(&plain));
8258 let (m, b) = read_card_and_build_info_ranged(&rp).unwrap();
8259 assert_eq!(m.unwrap(), card);
8260 assert!(b.is_none());
8261 assert_eq!(rp.requests(), 2);
8262 assert_eq!(
8267 r.requests(),
8268 rp.requests(),
8269 "build info must not cost an extra request",
8270 );
8271
8272 let none = build_image();
8274 let rn = CountingReader::new(SliceReader::new(&none));
8275 let (m, b) = read_card_and_build_info_ranged(&rn).unwrap();
8276 assert!(m.is_none() && b.is_none());
8277 assert_eq!(rn.requests(), 1);
8278 }
8279
8280 #[test]
8281 fn schema_summary_groups_by_type() {
8282 let rt = RDF_TYPE;
8283 let bytes = build_from(&[
8284 ("Alice", rt, "Person"),
8285 ("Bob", rt, "Person"),
8286 ("NYC", rt, "City"),
8287 ("Alice", "knows", "Bob"),
8288 ("Alice", "livesIn", "NYC"),
8289 ("Alice", "name", "\"Alice\""),
8290 ]);
8291 let rete = Rete::open(&bytes).unwrap();
8292 let summary = schema_summary(&rete);
8293 assert!(summary.contains(&("Person".into(), "knows".into(), "Person".into(), 1)));
8295 assert!(summary.contains(&("Person".into(), "livesIn".into(), "City".into(), 1)));
8296 assert!(summary.contains(&("Person".into(), "name".into(), "(literal)".into(), 1)));
8297 assert!(!summary.iter().any(|(_, p, _, _)| p == RDF_TYPE));
8299
8300 let classes = schema_classes(&rete);
8302 assert_eq!(
8303 classes,
8304 vec![("Person".into(), 2u32), ("City".into(), 1u32)]
8305 );
8306 }
8307
8308 fn build_from(triples: &[(&str, &str, &str)]) -> Vec<u8> {
8309 let mut db = DictionaryBuilder::new();
8310 for (s, p, o) in triples {
8311 db.observe(s, p, o);
8312 }
8313 let dict = db.build();
8314 let mut ib = GraphIndexBuilder::new();
8315 for (s, p, o) in triples {
8316 ib.push(dict.encode(s, p, o).unwrap());
8317 }
8318 write_file(&dict, &ib.build(), false, &[], 0)
8319 }
8320
8321 fn build_with_pyramid(triples: &[(&str, &str, &str)]) -> Vec<u8> {
8323 let mut db = DictionaryBuilder::new();
8324 for (s, p, o) in triples {
8325 db.observe(s, p, o);
8326 }
8327 let dict = db.build();
8328 let encoded: Vec<_> = triples
8329 .iter()
8330 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
8331 .collect();
8332 let mut ib = GraphIndexBuilder::new();
8333 for t in &encoded {
8334 ib.push(*t);
8335 }
8336 let (meta, levels) = build_pyramid_meta(&dict, &encoded, DEFAULT_TILE_BUDGET);
8337 write_dataset(&dict, &ib.build(), &[], false, &meta, levels)
8338 }
8339
8340 #[test]
8341 fn tbox_coherence_flags_unsatisfiable_class_index_free() {
8342 use crate::reader::{CountingReader, SliceReader};
8343 let rt = RDF_TYPE;
8344 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
8345 let disj = "<http://www.w3.org/2002/07/owl#disjointWith>";
8346 let bytes = build_with_pyramid(&[
8349 ("<http://ex/C>", sub, "<http://ex/D>"),
8350 ("<http://ex/C>", sub, "<http://ex/E>"),
8351 ("<http://ex/D>", disj, "<http://ex/E>"),
8352 ("<http://ex/x>", rt, "<http://ex/C>"),
8353 ]);
8354
8355 let r = CountingReader::new(SliceReader::new(&bytes));
8356 let view = SummaryView::open_ranged(&r).unwrap().unwrap();
8357 let points = view.tbox_coherence();
8358 assert!(
8359 points
8360 .iter()
8361 .any(|i| i.kind == "unsatisfiable-class" && i.detail.contains("http://ex/C>")),
8362 "expected C unsatisfiable from the schema alone, got {points:?}"
8363 );
8364
8365 let header = Header::from_bytes(&bytes[..HEADER_LEN]).unwrap();
8368 assert!(
8369 r.bytes_read() <= bytes.len() as u64 - header.root_dir_len,
8370 "tbox_coherence must not read the triple index"
8371 );
8372 }
8373
8374 #[test]
8375 fn schema_coherence_reads_only_the_schema_block() {
8376 use crate::reader::{CountingReader, SliceReader};
8377 let rt = RDF_TYPE;
8378 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
8379 let disj = "<http://www.w3.org/2002/07/owl#disjointWith>";
8380 let mut triples: Vec<(String, String, String)> = vec![
8384 ("<http://ex/C>".into(), sub.into(), "<http://ex/D>".into()),
8385 ("<http://ex/C>".into(), sub.into(), "<http://ex/E>".into()),
8386 ("<http://ex/D>".into(), disj.into(), "<http://ex/E>".into()),
8387 ];
8388 for i in 0..500 {
8389 let s = format!("<http://ex/x{i}>");
8390 triples.push((s.clone(), rt.into(), "<http://ex/C>".into()));
8391 triples.push((
8392 s,
8393 "<http://ex/label>".into(),
8394 format!("\"unique label {i}\""),
8395 ));
8396 }
8397 let trefs: Vec<(&str, &str, &str)> = triples
8398 .iter()
8399 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
8400 .collect();
8401 let bytes = build_with_pyramid(&trefs);
8402
8403 let header = Header::from_bytes(&bytes[..HEADER_LEN]).unwrap();
8404 assert!(
8405 header.schema_meta_len > 0,
8406 "the writer recorded a schema-block length"
8407 );
8408 assert!(
8409 (header.schema_meta_len as u64) < header.pyramid_meta_len,
8410 "schema block ({}) should be far smaller than the whole pyramid-meta ({})",
8411 header.schema_meta_len,
8412 header.pyramid_meta_len
8413 );
8414
8415 let r = CountingReader::new(SliceReader::new(&bytes));
8416 let points = read_schema_coherence_ranged(&r).unwrap().unwrap();
8417 assert!(points.iter().any(|i| i.kind == "unsatisfiable-class"));
8418 assert!(
8420 r.bytes_read() <= HEADER_LEN as u64 + header.schema_meta_len as u64,
8421 "read {} bytes; expected <= header + schema block ({})",
8422 r.bytes_read(),
8423 HEADER_LEN as u64 + header.schema_meta_len as u64
8424 );
8425 }
8426
8427 #[test]
8428 fn tbox_coherence_clean_schema_is_coherent() {
8429 use crate::reader::SliceReader;
8430 let rt = RDF_TYPE;
8431 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
8432 let bytes = build_with_pyramid(&[
8433 ("<http://ex/Dog>", sub, "<http://ex/Animal>"),
8434 ("<http://ex/x>", rt, "<http://ex/Dog>"),
8435 ]);
8436 let view = SummaryView::open_ranged(&SliceReader::new(&bytes))
8437 .unwrap()
8438 .unwrap();
8439 assert!(view.tbox_is_coherent(), "a plain hierarchy is coherent");
8440 }
8441
8442 #[test]
8450 fn readers_before_the_quad_index_refuse_its_files() {
8451 for perms in [PermSet::ALL, PermSet::CORE] {
8452 let quads: Vec<crate::ingest::RawQuad> = (0..50)
8453 .map(|i| {
8454 (
8455 format!("<http://ex/s{i}>"),
8456 "<http://ex/p>".to_string(),
8457 format!("\"{i}\""),
8458 Some(format!("<http://ex/g{}>", i % 7)),
8459 )
8460 })
8461 .collect();
8462 let (bytes, _) = crate::ingest::assemble_dataset_with_layout(
8463 quads,
8464 false,
8465 false,
8466 None,
8467 crate::PyramidAlgo::Louvain,
8468 perms,
8469 crate::ingest::GraphLayout::Quad,
8470 |_, _| Vec::<u8>::new(),
8471 );
8472 let h = Header::from_bytes(&bytes).unwrap();
8473 assert!(h.has_quad_index());
8474 let root =
8475 &bytes[h.root_dir_offset as usize..(h.root_dir_offset + h.root_dir_len) as usize];
8476 assert!(decode_index_container(root, h.block_codec, h.perms).is_err());
8478 assert!(
8479 container_section_payload_ranges(root, h.root_dir_offset, h.perms.len()).is_err()
8480 );
8481 assert_ne!(decode_container(root, CODEC_NONE).unwrap().len(), NUM_PERMS);
8483 assert_ne!(
8484 decode_container(root, CODEC_NONE).unwrap().len(),
8485 h.perms.len()
8486 );
8487 let r = crate::reader::SliceReader::new(&bytes);
8489 for expect in [h.perms.len(), NUM_PERMS] {
8490 assert!(locate_container_section_ranged(
8491 &r,
8492 h.root_dir_offset,
8493 h.root_dir_len,
8494 0,
8495 expect as u64
8496 )
8497 .is_err());
8498 }
8499 assert_eq!(hash_of_sections(&h, &bytes).unwrap(), h.content_hash);
8501 let rete = Rete::open(&bytes).unwrap();
8503 assert_eq!(rete.named_graph_count(), 7);
8504 assert_eq!(rete.dump(Some("<http://ex/g3>")).len(), 7);
8505 }
8506 }
8507
8508 #[test]
8509 fn named_graphs_round_trip() {
8510 let all = [
8512 ("Alice", "knows", "Bob"), ("Bob", "age", "30"), ];
8515 let mut db = DictionaryBuilder::new();
8516 for (s, p, o) in all {
8517 db.observe(s, p, o);
8518 }
8519 let dict = db.build();
8520
8521 let mut def = GraphIndexBuilder::new();
8522 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
8523 let mut g1 = GraphIndexBuilder::new();
8524 g1.push(dict.encode("Bob", "age", "30").unwrap());
8525
8526 let named = vec![("http://ex/g1".to_string(), g1.build())];
8527 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8528
8529 assert!(verify(&bytes).unwrap());
8530 let rete = Rete::open(&bytes).unwrap();
8531 assert_eq!(rete.graph_names(), vec!["http://ex/g1"]);
8532 let gi = rete.graph_index("http://ex/g1").unwrap();
8534 assert_eq!(gi.triple_count(), 1);
8535 assert!(rete.graph_index("http://ex/missing").is_none());
8536
8537 assert_eq!(rete.header().quad_count, 2);
8540
8541 assert_eq!(rete.query(Some("Alice"), None, None).len(), 1);
8543
8544 assert_eq!(
8546 rete.dump(None),
8547 vec![("Alice".into(), "knows".into(), "Bob".into())]
8548 );
8549 assert_eq!(
8550 rete.dump(Some("http://ex/g1")),
8551 vec![("Bob".into(), "age".into(), "30".into())]
8552 );
8553 }
8554
8555 #[test]
8561 fn named_graph_container_opens_tile_lazily_like_the_root() {
8562 let mut db = DictionaryBuilder::new();
8563 let node = |n: u32| format!("<http://ex/n{n}>");
8564 let p = "<http://ex/p>".to_string();
8565 db.observe(&node(0), &p, &node(1));
8566 for i in 0..500u32 {
8567 db.observe(&node(1000 + i), &p, &node(2000 + i));
8568 }
8569 let dict = db.build();
8570 let mut def = GraphIndexBuilder::new();
8571 def.push(dict.encode(&node(0), &p, &node(1)).unwrap());
8572 let mut g = GraphIndexBuilder::new().with_tile_budget(256); for i in 0..500u32 {
8574 g.push(dict.encode(&node(1000 + i), &p, &node(2000 + i)).unwrap());
8575 }
8576 let named = vec![("<http://ex/g>".to_string(), g.build())];
8577 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8578 let rete = Rete::open(&bytes).unwrap();
8579 let header = rete.header();
8580
8581 let soff = header.named_graphs_offset as usize;
8584 let send = soff + header.named_graphs_len as usize;
8585 let sec = &bytes[soff..send];
8586 let (n, mut pos) = read_uvarint(sec).unwrap();
8587 assert_eq!(n, 1);
8588 let (ilen, u1) = read_uvarint(&sec[pos..]).unwrap();
8589 pos += u1 + ilen as usize;
8590 let (clen, u2) = read_uvarint(&sec[pos..]).unwrap();
8591 pos += u2;
8592 let container = ByteRange {
8593 offset: (soff + pos) as u64,
8594 len: clen,
8595 };
8596
8597 use crate::reader::SliceReader;
8598 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8599 let reader: std::sync::Arc<dyn RangeReader + Send + Sync> =
8600 std::sync::Arc::new(SliceReader::new(leaked));
8601 let (lazy_idx, _, _) = open_index_container_lazy(
8602 &reader,
8603 container,
8604 header.block_codec,
8605 header.has_tile_synopsis(),
8606 1,
8607 header.perms,
8608 header.perms.len(),
8609 )
8610 .unwrap();
8611 let mut want = rete
8612 .graph_index("<http://ex/g>")
8613 .unwrap()
8614 .match_pattern((None, None, None));
8615 let mut got = lazy_idx.match_pattern((None, None, None));
8616 want.sort_unstable();
8617 got.sort_unstable();
8618 assert_eq!(got.len(), 500);
8619 assert_eq!(got, want);
8620 assert!(!lazy_idx.load_incomplete());
8621 }
8622
8623 #[test]
8629 fn dump_iter_matches_dump_and_stops_early() {
8630 use crate::reader::{CountingReader, SliceReader};
8631
8632 let triples: Vec<(String, String, String)> = (0..2000u32)
8634 .map(|i| {
8635 (
8636 format!("<http://ex/s/{i:04}>"),
8637 "<http://ex/p>".to_string(),
8638 format!("<http://ex/o/{i:04}>"),
8639 )
8640 })
8641 .collect();
8642 let mut db = DictionaryBuilder::new();
8643 for (s, p, o) in &triples {
8644 db.observe(s, p, o);
8645 }
8646 let dict = db.build();
8647 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
8648 for (s, p, o) in &triples {
8649 ib.push(dict.encode(s, p, o).unwrap());
8650 }
8651 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8652
8653 let rete = Rete::open(&bytes).unwrap();
8655 assert_eq!(rete.dump_iter(None).collect::<Vec<_>>(), rete.dump(None));
8656 assert_eq!(rete.dump_iter(Some("http://ex/nope")).count(), 0);
8658
8659 let read_bytes = |take: usize| -> u64 {
8661 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8662 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8663 let rete = Rete::open_ranged_lazy(reader.clone()).unwrap();
8664 let before = reader.bytes_read();
8665 let n = rete.dump_iter(None).take(take).count();
8666 assert_eq!(n, take.min(triples.len()));
8667 assert!(!rete.index_incomplete());
8668 reader.bytes_read() - before
8669 };
8670 let first_only = read_bytes(1);
8671 let everything = read_bytes(triples.len());
8672 assert!(
8674 first_only < everything,
8675 "dump_iter is not lazy: one triple read {first_only} B, a full scan {everything} B"
8676 );
8677 #[cfg(feature = "compression")]
8684 assert!(
8685 first_only * 2 < everything,
8686 "dump_iter is not lazy: one triple read {first_only} B of the {everything} B a full scan reads"
8687 );
8688
8689 let fresh = Rete::open(&bytes).unwrap();
8694 let drain = |max_quads: usize| {
8699 let (mut out, mut cursor, mut calls) = (Vec::new(), 0u32, 0);
8700 loop {
8701 let (batch, next, done) = fresh.dump_batch(None, cursor, max_quads);
8702 out.extend(batch);
8703 cursor = next;
8704 calls += 1;
8705 assert!(calls < 10_000, "cursor failed to advance");
8706 if done {
8707 break out;
8708 }
8709 }
8710 };
8711 let eager = fresh.dump(None);
8712 for max in [1usize, 7, 128, 100_000] {
8713 assert_eq!(drain(max), eager, "batch size {max} changed the dump");
8714 }
8715 let (t, _, done) = fresh.dump_batch(Some("http://ex/nope"), 0, 16);
8718 assert!(t.is_empty() && done);
8719 }
8720
8721 #[test]
8729 fn query_batch_matches_query_in_graph_and_stops_early() {
8730 use crate::reader::{CountingReader, SliceReader};
8731
8732 let triples: Vec<(String, String, String)> = (0..2000u32)
8735 .map(|i| {
8736 (
8737 format!("<http://ex/s/{:04}>", i / 2),
8738 format!("<http://ex/p/{}>", i % 2),
8739 format!("<http://ex/o/{i:04}>"),
8740 )
8741 })
8742 .collect();
8743 let mut db = DictionaryBuilder::new();
8744 for (s, p, o) in &triples {
8745 db.observe(s, p, o);
8746 }
8747 let dict = db.build();
8748 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
8749 for (s, p, o) in &triples {
8750 ib.push(dict.encode(s, p, o).unwrap());
8751 }
8752 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8753 let rete = Rete::open(&bytes).unwrap();
8754
8755 let drain = |s: Option<&str>, p: Option<&str>, o: Option<&str>, max: usize| {
8756 let (mut out, mut cursor, mut calls) = (Vec::new(), 0u64, 0);
8757 loop {
8758 let (batch, next, done) = rete.query_batch(None, s, p, o, cursor, max);
8759 assert!(!batch.is_empty() || done, "empty non-final batch");
8762 out.extend(batch);
8763 cursor = next;
8764 calls += 1;
8765 assert!(calls < 20_000, "cursor failed to advance");
8766 if done {
8767 break out;
8768 }
8769 }
8770 };
8771 let shapes: [(Option<&str>, Option<&str>, Option<&str>); 6] = [
8772 (None, None, None),
8773 (Some("<http://ex/s/0007>"), None, None),
8774 (None, Some("<http://ex/p/1>"), None),
8775 (None, None, Some("<http://ex/o/0042>")),
8776 (Some("<http://ex/s/0007>"), Some("<http://ex/p/0>"), None),
8777 (
8778 Some("<http://ex/s/0007>"),
8779 Some("<http://ex/p/0>"),
8780 Some("<http://ex/o/0014>"),
8781 ),
8782 ];
8783 for (s, p, o) in shapes {
8784 let eager = rete.query_in_graph(None, s, p, o);
8785 assert!(
8786 !eager.is_empty(),
8787 "fixture shape {s:?} {p:?} {o:?} matches nothing"
8788 );
8789 assert_eq!(
8791 rete.query_iter(None, s, p, o).collect::<Vec<_>>(),
8792 eager,
8793 "query_iter disagrees for {s:?} {p:?} {o:?}"
8794 );
8795 for max in [1usize, 3, 64, 100_000] {
8796 let mut got = drain(s, p, o, max);
8797 let mut want = eager.clone();
8798 got.sort();
8801 want.sort();
8802 assert_eq!(got, want, "batch size {max} changed {s:?} {p:?} {o:?}");
8803 }
8804 if s.is_none() && p.is_none() && o.is_none() {
8806 assert_eq!(drain(s, p, o, 7), eager, "unbound batching reordered rows");
8807 }
8808 }
8809
8810 let (t, _, done) = rete.query_batch(None, Some("<http://ex/nope>"), None, None, 0, 16);
8813 assert!(t.is_empty() && done);
8814 let (t, _, done) = rete.query_batch(Some("http://ex/nope"), None, None, None, 0, 16);
8815 assert!(t.is_empty() && done);
8816 assert_eq!(
8817 rete.query_iter(None, Some("<http://ex/nope>"), None, None)
8818 .count(),
8819 0
8820 );
8821
8822 let read_bytes = |take: usize| -> u64 {
8825 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8826 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8827 let rete = Rete::open_ranged_lazy(reader.clone()).unwrap();
8828 let before = reader.bytes_read();
8829 let (mut n, mut cursor) = (0usize, 0u64);
8830 loop {
8831 let (batch, next, done) = rete.query_batch(None, None, None, None, cursor, 1);
8832 n += batch.len();
8833 cursor = next;
8834 if done || n >= take {
8835 break;
8836 }
8837 }
8838 assert!(n >= take.min(triples.len()));
8839 assert!(!rete.index_incomplete());
8840 reader.bytes_read() - before
8841 };
8842 let first_only = read_bytes(1);
8843 let everything = read_bytes(triples.len());
8844 assert!(
8845 first_only < everything,
8846 "query_batch is not lazy: one row read {first_only} B, a full scan {everything} B"
8847 );
8848 #[cfg(feature = "compression")]
8849 assert!(
8850 first_only * 2 < everything,
8851 "query_batch is not lazy: one row read {first_only} B of the {everything} B a full scan reads"
8852 );
8853 }
8854
8855 #[test]
8858 fn query_batch_is_graph_scoped() {
8859 let mut db = DictionaryBuilder::new();
8860 for (s, p, o) in [
8861 ("Alice", "knows", "Bob"),
8862 ("Alice", "knows", "Carol"),
8863 ("Alice", "knows", "Dave"),
8864 ("Erin", "knows", "Frank"),
8865 ("Alice", "knows", "Zoe"),
8866 ] {
8867 db.observe(s, p, o);
8868 }
8869 let dict = db.build();
8870 let mut def = GraphIndexBuilder::new();
8871 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
8872 def.push(dict.encode("Alice", "knows", "Carol").unwrap());
8873 let mut g1 = GraphIndexBuilder::new();
8874 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
8875 g1.push(dict.encode("Erin", "knows", "Frank").unwrap());
8876 let mut g2 = GraphIndexBuilder::new();
8877 g2.push(dict.encode("Alice", "knows", "Zoe").unwrap());
8878 let named = vec![
8879 ("http://ex/g1".to_string(), g1.build()),
8880 ("http://ex/g2".to_string(), g2.build()),
8881 ];
8882 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8883 let rete = Rete::open(&bytes).unwrap();
8884 for graph in [None, Some("http://ex/g1"), Some("http://ex/g2")] {
8885 let eager = rete.query_in_graph(graph, None, None, None);
8886 let (mut out, mut cursor) = (Vec::new(), 0u64);
8887 loop {
8888 let (batch, next, done) = rete.query_batch(graph, None, None, None, cursor, 1);
8889 out.extend(batch);
8890 cursor = next;
8891 if done {
8892 break;
8893 }
8894 }
8895 assert_eq!(out, eager, "graph {graph:?} batched differently");
8896 }
8897 }
8898
8899 #[test]
8900 fn query_in_graph_is_graph_scoped() {
8901 let mut db = DictionaryBuilder::new();
8904 for (s, p, o) in [
8905 ("Alice", "knows", "Bob"),
8906 ("Alice", "knows", "Carol"),
8907 ("Alice", "knows", "Dave"),
8908 ] {
8909 db.observe(s, p, o);
8910 }
8911 let dict = db.build();
8912
8913 let mut def = GraphIndexBuilder::new();
8914 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
8915 def.push(dict.encode("Alice", "knows", "Carol").unwrap());
8916 let mut g1 = GraphIndexBuilder::new();
8917 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
8918
8919 let named = vec![("http://ex/g1".to_string(), g1.build())];
8920 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8921 let rete = Rete::open(&bytes).unwrap();
8922
8923 let mut def_objs: Vec<String> = rete
8925 .query_in_graph(None, Some("Alice"), Some("knows"), None)
8926 .into_iter()
8927 .map(|(_, _, o)| o)
8928 .collect();
8929 def_objs.sort();
8930 assert_eq!(def_objs, vec!["Bob".to_string(), "Carol".to_string()]);
8931
8932 assert_eq!(
8934 rete.query_in_graph(Some("http://ex/g1"), Some("Alice"), None, None),
8935 vec![("Alice".into(), "knows".into(), "Dave".into())]
8936 );
8937
8938 assert_eq!(rete.query_in_graph(None, None, None, None).len(), 2);
8940 assert_eq!(
8941 rete.query_in_graph(Some("http://ex/g1"), None, None, None)
8942 .len(),
8943 1
8944 );
8945
8946 assert!(rete
8948 .query_in_graph(Some("http://ex/missing"), None, None, None)
8949 .is_empty());
8950 }
8951
8952 #[test]
8965 fn bounded_query_in_graph_does_not_fault_the_whole_dictionary() {
8966 use crate::reader::{CountingReader, SliceReader};
8967
8968 let payload = |i: u32| -> String {
8974 let mut s = String::with_capacity(800);
8975 let mut x = i as u64 ^ 0x9e37_79b9_7f4a_7c15;
8976 while s.len() < 800 {
8977 x = x
8978 .wrapping_mul(6364136223846793005)
8979 .wrapping_add(1442695040888963407);
8980 s.push_str(&format!("{:016x}", x ^ (x >> 29)));
8981 }
8982 s
8983 };
8984 let quads: Vec<(String, String, String, bool)> = (0..2000u32)
8991 .map(|i| {
8992 let named = i % 2 == 0;
8993 let ns = if named { 'a' } else { 'b' };
8994 (
8995 format!("<http://ex/{ns}/s/{i:04}>"),
8996 "<http://ex/abstract>".to_string(),
8997 format!("\"{ns}{} number {i}\"", payload(i)),
8998 named,
8999 )
9000 })
9001 .collect();
9002 let mut db = DictionaryBuilder::new();
9003 for (s, p, o, _) in &quads {
9004 db.observe(s, p, o);
9005 }
9006 let dict = db.build();
9007 let mut def = GraphIndexBuilder::new().with_tile_budget(256);
9008 let mut g1 = GraphIndexBuilder::new().with_tile_budget(256);
9009 for (s, p, o, named) in &quads {
9010 let t = dict.encode(s, p, o).unwrap();
9011 if *named {
9012 g1.push(t);
9013 } else {
9014 def.push(t);
9015 }
9016 }
9017 let named = vec![("http://ex/g1".to_string(), g1.build())];
9018 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9019
9020 let eager = Rete::open(&bytes).unwrap();
9021 let dict_len = eager.header().dictionary_len;
9022
9023 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
9026 let probe = |f: &dyn Fn(&Rete) -> Vec<TermTriple>| -> (Vec<TermTriple>, u64) {
9027 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9028 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
9029 let before = reader.bytes_read();
9030 let out = f(&lazy);
9031 assert!(!lazy.index_incomplete(), "a chunk fetch failed");
9032 (out, reader.bytes_read() - before)
9033 };
9034
9035 let want = eager.query_in_graph(
9037 Some("http://ex/g1"),
9038 Some("<http://ex/a/s/0100>"),
9039 None,
9040 None,
9041 );
9042 assert_eq!(want.len(), 1, "fixture: expected exactly one match");
9043 let (got, pulled) = probe(&|r| {
9044 r.query_in_graph(
9045 Some("http://ex/g1"),
9046 Some("<http://ex/a/s/0100>"),
9047 None,
9048 None,
9049 )
9050 });
9051 assert_eq!(got, want, "lazy query_in_graph disagrees with eager");
9052 assert!(
9053 pulled * 4 < dict_len,
9054 "a 1-row query_in_graph faulted {pulled} B of a {dict_len} B dictionary — \
9055 it is prefetching the whole thing again"
9056 );
9057
9058 let (empty, q_pulled) =
9066 probe(&|r| r.query_in_graph(Some("http://ex/nope"), None, None, None));
9067 assert!(empty.is_empty());
9068 let (_, d_pulled) = probe(&|r| {
9069 let mut n = 0usize;
9070 r.dump_each(Some("http://ex/nope"), |_, _, _| n += 1);
9071 assert_eq!(n, 0, "an absent graph yielded triples");
9072 Vec::new()
9073 });
9074 assert_eq!(
9075 q_pulled, d_pulled,
9076 "query_in_graph and dump_each disagree on what an absent graph costs"
9077 );
9078 assert!(
9079 q_pulled * 4 < dict_len,
9080 "an absent graph faulted {q_pulled} B of a {dict_len} B dictionary"
9081 );
9082
9083 let want: Vec<TermTriple> = eager.dump(Some("http://ex/g1"));
9086 assert_eq!(want.len(), 1000);
9087 let (got, pulled) = probe(&|r| {
9088 let mut out = Vec::new();
9089 r.dump_each(Some("http://ex/g1"), |s, p, o| {
9090 out.push((s.to_string(), p.to_string(), o.to_string()))
9091 });
9092 out
9093 });
9094 assert_eq!(got, want, "lazy dump_each disagrees with eager dump");
9095 assert!(
9096 pulled * 3 < dict_len * 2,
9097 "dumping half the graph faulted {pulled} B of a {dict_len} B dictionary — \
9098 the other graph's chunks are being pulled in too"
9099 );
9100 }
9101
9102 #[cfg(test)]
9107 fn build_multichunk_image() -> Vec<u8> {
9108 use crate::index::PermSet;
9109 let mut db = DictionaryBuilder::new();
9110 let mut triples: Vec<(String, String, String)> = (0..4000u32)
9111 .map(|i| {
9112 (
9113 format!("<http://ex/s/{i:07}>"),
9114 format!("<http://ex/p/{}>", i % 8),
9115 format!("\"object literal number {i:07} with padding text to widen the term\""),
9116 )
9117 })
9118 .collect();
9119 triples.push((
9120 "<http://ex/s/big>".to_string(),
9121 "<http://ex/p/big>".to_string(),
9122 format!("\"{}\"", "Z".repeat(8000)),
9123 ));
9124 for (s, p, o) in &triples {
9125 db.observe(s, p, o);
9126 }
9127 let dict = db.build();
9128 let mut idx = GraphIndexBuilder::new()
9129 .with_tile_budget(64)
9130 .with_perms(PermSet::ALL);
9131 for (s, p, o) in &triples {
9132 idx.push(dict.encode(s, p, o).unwrap());
9133 }
9134 write_dataset(&dict, &idx.build(), &[], true, &[], 0)
9135 }
9136
9137 #[test]
9146 fn tiny_dict_cache_evicts_yet_dump_is_byte_identical() {
9147 use crate::reader::SliceReader;
9148 let leaked: &'static [u8] = Box::leak(build_multichunk_image().into_boxed_slice());
9149 let dump = |cap: u64| -> (
9150 Vec<(String, String, String)>,
9151 crate::chunk_cache::CacheStats,
9152 ) {
9153 let rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9154 rete.dict.set_cache_cap(cap);
9155 let mut out = Vec::new();
9156 rete.dump_filtered_each(None, None, None, None, |s, p, o| {
9157 out.push((s.to_string(), p.to_string(), o.to_string()))
9158 });
9159 assert!(
9160 !rete.index_incomplete(),
9161 "no lazy fault should fail at cap {cap}"
9162 );
9163 let stats = rete.dict_cache_stats();
9164 (out, stats)
9165 };
9166
9167 let (unlimited, us) = dump(u64::MAX);
9168 assert_eq!(unlimited.len(), 4001, "every triple dumped");
9169 assert_eq!(us.evictions, 0, "unlimited cap evicts nothing");
9170
9171 let (tiny, ts) = dump(128 * 1024);
9174 assert_eq!(tiny, unlimited, "128 KiB-cap dump differs from unlimited");
9175 assert!(ts.evictions > 0, "a tiny cap must evict (stats {ts:?})");
9176
9177 let (starved, ss) = dump(1);
9179 assert_eq!(starved, unlimited, "1-byte-cap dump differs from unlimited");
9180 assert!(
9181 ss.evictions > 0,
9182 "1-byte cap evicts every insert (stats {ss:?})"
9183 );
9184 }
9185
9186 #[test]
9197 fn memory_budget_charges_the_fixed_directories() {
9198 use crate::reader::SliceReader;
9199 let leaked: &'static [u8] = Box::leak(build_multichunk_image().into_boxed_slice());
9200 let rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9201
9202 let fixed = rete.fixed_overhead();
9203 assert!(
9206 fixed.dict_directory > 0 && fixed.index_directory > 0,
9207 "{fixed:?}"
9208 );
9209 assert_eq!(fixed.named_graphs, 0, "no named graphs in this file");
9210 assert_eq!(
9211 fixed.total(),
9212 fixed.dict_directory + fixed.index_directory + fixed.named_graphs
9213 );
9214
9215 let total: u64 = 512 << 20;
9218 let applied = rete.set_memory_budget(Some(total));
9219 let plain = split_memory_budget(total);
9220 assert_eq!(applied.dict_cache, plain.dict_cache - fixed.total());
9221 assert_eq!(rete.dict_cache_cap(), applied.dict_cache);
9222 assert_eq!(applied.tile_cache, plain.tile_cache);
9223 assert_eq!(applied.block_cache, plain.block_cache);
9224 assert!(
9225 applied.dict_cache + applied.tile_cache + applied.block_cache + fixed.total() <= total,
9226 "caps + fixed directories exceed the budget: {applied:?} + {fixed:?} > {total}"
9227 );
9228
9229 let applied = rete.set_memory_budget(Some(1 << 20));
9231 assert_eq!(applied.dict_cache, MIN_DICT_CACHE_CAP);
9232
9233 let applied = rete.set_memory_budget(None);
9235 assert_eq!(applied.dict_cache, u64::MAX);
9236 assert_eq!(rete.dict_cache_cap(), u64::MAX);
9237
9238 let rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9241 rete.dict.set_cache_cap(128 * 1024);
9242 let mut rows = 0usize;
9243 rete.dump_filtered_each(None, None, None, None, |_, _, _| rows += 1);
9244 assert_eq!(rows, 4001);
9245 let s = rete.dict_cache_stats();
9246 assert!(s.evictions > 0, "the cap must bind: {s:?}");
9247 assert!(
9250 s.entries <= 1 || s.resident_bytes <= 128 * 1024,
9251 "dictionary cache over its cap after the dump: {s:?}"
9252 );
9253 }
9254
9255 #[test]
9260 fn releasing_a_named_graph_drops_and_reopens_its_index() {
9261 use crate::index::PermSet;
9262 use crate::reader::SliceReader;
9263 let mut db = DictionaryBuilder::new();
9264 let all: Vec<(String, String, String)> = (0..40u32)
9265 .map(|i| {
9266 (
9267 format!("<http://ex/s{}>", i % 7),
9268 format!("<http://ex/p{}>", i % 3),
9269 format!("<http://ex/o{}>", i % 5),
9270 )
9271 })
9272 .collect();
9273 for (s, p, o) in &all {
9274 db.observe(s, p, o);
9275 }
9276 let dict = db.build();
9277 let mk = |take: fn(u32) -> bool| {
9278 let mut g = GraphIndexBuilder::new()
9279 .with_tile_budget(16)
9280 .with_perms(PermSet::ALL);
9281 for (i, (s, p, o)) in all.iter().enumerate() {
9282 if take(i as u32) {
9283 g.push(dict.encode(s, p, o).unwrap());
9284 }
9285 }
9286 g.build()
9287 };
9288 let def = mk(|_| true);
9289 let g1 = mk(|i| i % 2 == 0);
9290 let g2 = mk(|i| i % 3 == 0);
9291 let image = write_dataset(
9292 &dict,
9293 &def,
9294 &[
9295 ("<http://ex/g1>".to_string(), g1),
9296 ("<http://ex/g2>".to_string(), g2),
9297 ],
9298 true,
9299 &[],
9300 0,
9301 );
9302 let leaked: &'static [u8] = Box::leak(image.into_boxed_slice());
9303 let mut rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9304
9305 let opened = |r: &Rete| -> usize {
9306 match &r.named_graphs {
9307 NamedGraphsSlot::Lazy(l) => {
9308 let mut n = 0;
9309 l.for_each_opened(|_| n += 1);
9310 n
9311 }
9312 _ => 0,
9313 }
9314 };
9315
9316 assert_eq!(opened(&rete), 0, "nothing opened before first touch");
9317 assert!(rete.graph_index("<http://ex/g1>").is_some());
9319 assert_eq!(opened(&rete), 1, "g1 opened");
9320 rete.release_named_graph("<http://ex/g1>");
9322 assert_eq!(opened(&rete), 0, "g1 released");
9323 rete.release_named_graph("<http://ex/absent>");
9325 rete.release_named_graph("<http://ex/g1>");
9326 assert_eq!(opened(&rete), 0);
9327 assert!(rete.graph_index("<http://ex/g1>").is_some());
9329 assert_eq!(opened(&rete), 1, "g1 re-opened after release");
9330 }
9331
9332 #[test]
9350 fn filtered_dump_is_exactly_the_unfiltered_dump_filtered() {
9351 use crate::index::PermSet;
9352 use crate::reader::SliceReader;
9353
9354 let terms = |i: u32| {
9355 (
9356 format!("<http://ex/s{}>", i % 11),
9357 format!("<http://ex/p{}>", i % 3),
9358 if i.is_multiple_of(4) {
9362 format!("\"lit {}\"@en", i % 5)
9363 } else {
9364 format!("<http://ex/o{}>", i % 7)
9365 },
9366 )
9367 };
9368 let build = |perms: PermSet| {
9369 let mut db = DictionaryBuilder::new();
9370 let all: Vec<_> = (0..90u32).map(terms).collect();
9371 for (s, p, o) in &all {
9372 db.observe(s, p, o);
9373 }
9374 let dict = db.build();
9375 let mut def = GraphIndexBuilder::new()
9376 .with_tile_budget(16)
9377 .with_perms(perms);
9378 let mut g1 = GraphIndexBuilder::new()
9379 .with_tile_budget(16)
9380 .with_perms(perms);
9381 for (i, (s, p, o)) in all.iter().enumerate() {
9382 let t = dict.encode(s, p, o).unwrap();
9383 if i % 2 == 0 {
9387 g1.push(t);
9388 }
9389 def.push(t);
9390 }
9391 write_dataset(
9392 &dict,
9393 &def.build(),
9394 &[("<http://ex/g1>".to_string(), g1.build())],
9395 true,
9396 &[],
9397 0,
9398 )
9399 };
9400
9401 let shapes: Vec<(Option<&str>, Option<&str>, Option<&str>)> = vec![
9402 (None, None, None),
9403 (Some("<http://ex/s3>"), None, None),
9404 (None, Some("<http://ex/p1>"), None),
9405 (None, None, Some("<http://ex/o5>")),
9406 (None, None, Some("\"lit 0\"@en")),
9407 (Some("<http://ex/s3>"), Some("<http://ex/p0>"), None),
9408 (Some("<http://ex/s4>"), None, Some("<http://ex/o4>")),
9409 (None, Some("<http://ex/p1>"), Some("<http://ex/o5>")),
9410 (
9411 Some("<http://ex/s3>"),
9412 Some("<http://ex/p0>"),
9413 Some("<http://ex/o3>"),
9414 ),
9415 (Some("<http://ex/nope>"), None, None),
9419 (None, Some("<http://ex/nope>"), None),
9420 (None, None, Some("<http://ex/nope>")),
9421 ];
9422
9423 let mut any_pruned = false;
9424 for perms in [PermSet::ALL, PermSet::CORE] {
9425 let image = build(perms);
9426 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
9427 let opens: Vec<(&str, Rete)> = vec![
9428 ("resident", Rete::open(&image).unwrap()),
9429 (
9430 "ranged",
9431 Rete::open_ranged(&SliceReader::new(leaked)).unwrap(),
9432 ),
9433 (
9434 "lazy",
9435 Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap(),
9436 ),
9437 ];
9438 for graph in [None, Some("<http://ex/g1>"), Some("<http://ex/absent>")] {
9439 let full: Vec<TermTriple> = {
9441 let mut v = Vec::new();
9442 Rete::open(&image).unwrap().dump_each(graph, |s, p, o| {
9443 v.push((s.to_string(), p.to_string(), o.to_string()))
9444 });
9445 v.sort();
9446 v
9447 };
9448 for &(s, p, o) in &shapes {
9449 let mut want = full.clone();
9450 want.retain(|(ts, tp, to)| {
9451 s.is_none_or(|x| x == ts)
9452 && p.is_none_or(|x| x == tp)
9453 && o.is_none_or(|x| x == to)
9454 });
9455 for (path, rete) in &opens {
9456 let mut got = Vec::new();
9457 rete.dump_filtered_each(graph, s, p, o, |s, p, o| {
9458 got.push((s.to_string(), p.to_string(), o.to_string()))
9459 });
9460 got.sort();
9461 assert_eq!(
9462 got,
9463 want,
9464 "{path}/{perms:?} graph={graph:?} pattern={:?} — filtered dump \
9465 is not the unfiltered dump filtered",
9466 (s, p, o)
9467 );
9468 assert!(!rete.index_incomplete(), "{path}: a fetch failed");
9469 }
9470 let plan = opens[2].1.dump_plan(graph, s, p, o);
9474 if graph == Some("<http://ex/absent>")
9475 || [s, p, o].contains(&Some("<http://ex/nope>"))
9476 {
9477 assert!(
9478 plan.scan.is_none(),
9479 "an unmatchable dump still planned a scan: {:?}",
9480 (graph, s, p, o)
9481 );
9482 } else {
9483 let scan = plan.scan.expect("a matchable dump plans a scan");
9484 assert!(
9485 PermSet::CORE.contains(scan.permutation),
9486 "a dump routed to {:?}, outside PermSet::CORE — a \
9487 --permutations 3 file has no such section",
9488 scan.permutation
9489 );
9490 assert!(scan.tiles_admitted <= scan.tiles_routed);
9491 assert!(scan.tiles_routed <= scan.tiles_total);
9492 assert!(scan.tile_bytes <= scan.section_bytes);
9493 if scan.tiles_admitted < scan.tiles_total {
9494 any_pruned = true;
9495 }
9496 }
9497 }
9498 }
9499 }
9500 assert!(
9501 any_pruned,
9502 "no shape pruned a single tile — the fixture is too small to be \
9503 measuring anything"
9504 );
9505 }
9506
9507 #[test]
9517 fn a_predicate_scoped_dump_fetches_less_than_the_graph() {
9518 use crate::reader::{CountingReader, SliceReader};
9519
9520 let mut db = DictionaryBuilder::new();
9526 let mut all = Vec::new();
9527 for s in 0..200u32 {
9528 for p in 0..20u32 {
9529 let t = (
9530 format!("<http://ex/s/{s:04}>"),
9531 format!("<http://ex/p/{p:02}>"),
9532 format!("\"object {s:04}/{p:02}\""),
9533 );
9534 db.observe(&t.0, &t.1, &t.2);
9535 all.push(t);
9536 }
9537 }
9538 let dict = db.build();
9539 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
9540 for (s, p, o) in &all {
9541 ib.push(dict.encode(s, p, o).unwrap());
9542 }
9543 let image = write_dataset(&dict, &ib.build(), &[], false, &[], 0);
9544 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
9545
9546 let probe = |s: Option<&str>, p: Option<&str>, o: Option<&str>| -> (usize, u64) {
9548 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9549 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
9550 let before = reader.bytes_read();
9551 let mut n = 0usize;
9552 lazy.dump_filtered_each(None, s, p, o, |_, _, _| n += 1);
9553 assert!(!lazy.index_incomplete());
9554 (n, reader.bytes_read() - before)
9555 };
9556
9557 let (full_rows, full_bytes) = probe(None, None, None);
9558 let (pred_rows, pred_bytes) = probe(None, Some("<http://ex/p/07>"), None);
9559 let (subj_rows, subj_bytes) = probe(Some("<http://ex/s/0021>"), None, None);
9560 assert_eq!(full_rows, 4000);
9561 assert_eq!(pred_rows, 200, "one predicate covers every subject once");
9562 assert_eq!(subj_rows, 20);
9563 assert!(
9565 pred_bytes < full_bytes && subj_bytes < full_bytes,
9566 "a slice fetched {pred_bytes} / {subj_bytes} B where the whole graph fetched \
9567 {full_bytes} B — the scan is not being pruned at all"
9568 );
9569 #[cfg(feature = "compression")]
9580 assert!(
9581 pred_bytes * 4 < full_bytes && subj_bytes * 4 < full_bytes,
9582 "a 1-in-20 predicate slice fetched {pred_bytes} B and one subject \
9583 {subj_bytes} B where the whole graph fetched {full_bytes} B"
9584 );
9585
9586 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9588 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
9589 let before = reader.bytes_read();
9590 let plan = lazy
9591 .dump_plan(None, None, Some("<http://ex/p/07>"), None)
9592 .scan
9593 .expect("a known predicate plans a scan");
9594 assert!(
9595 reader.bytes_read() - before < 64 * 1024,
9596 "planning fetched {} B — it is supposed to read directories, not tiles",
9597 reader.bytes_read() - before
9598 );
9599 assert!(plan.tiles_admitted < plan.tiles_total);
9600 assert!(
9606 plan.tile_bytes * 5 < plan.section_bytes,
9607 "one predicate of twenty planned {} B of a {} B section",
9608 plan.tile_bytes,
9609 plan.section_bytes
9610 );
9611 }
9612
9613 #[test]
9614 fn query_quads_tags_every_graph() {
9615 let mut db = DictionaryBuilder::new();
9616 for (s, p, o) in [("Alice", "knows", "Bob"), ("Alice", "knows", "Dave")] {
9617 db.observe(s, p, o);
9618 }
9619 let dict = db.build();
9620 let mut def = GraphIndexBuilder::new();
9621 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
9622 let mut g1 = GraphIndexBuilder::new();
9623 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
9624 let named = vec![("http://ex/g1".to_string(), g1.build())];
9625 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9626 let rete = Rete::open(&bytes).unwrap();
9627
9628 let quads = rete.query_quads(Some("Alice"), Some("knows"), None);
9630 assert_eq!(quads.len(), 2);
9631 assert_eq!(
9632 quads[0],
9633 (("Alice".into(), "knows".into(), "Bob".into()), None)
9634 );
9635 assert_eq!(
9636 quads[1],
9637 (
9638 ("Alice".into(), "knows".into(), "Dave".into()),
9639 Some("http://ex/g1".to_string())
9640 )
9641 );
9642
9643 assert!(rete.query_quads(Some("Nobody"), None, None).is_empty());
9645 }
9646
9647 #[test]
9648 fn pyramid_meta_round_trips_in_file() {
9649 let rete = Rete::open(&build_image()).unwrap();
9650 let pyr = rete.pyramid().expect("file has a pyramid");
9651 let total: u32 = pyr.summary.iter().map(|e| e.count).sum();
9653 assert_eq!(total, 3);
9654 assert!(!pyr.summary.is_empty());
9655 assert!(pyr.tiles.is_empty());
9656 }
9657
9658 #[test]
9659 fn schema_pyramid_round_trips_through_file_index_free() {
9660 use crate::reader::{CountingReader, SliceReader};
9661 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
9662 let q = |s: &str, p: &str, o: &str| {
9663 (s.to_string(), p.to_string(), o.to_string(), None::<String>)
9664 };
9665 let quads = vec![
9667 q("<a>", RDF_TYPE, "<Astronomer>"),
9668 q("<b>", RDF_TYPE, "<Astronomer>"),
9669 q("<c>", RDF_TYPE, "<Person>"),
9670 q("<Astronomer>", sub, "<Scientist>"),
9671 q("<Scientist>", sub, "<Person>"),
9672 q("<Person>", sub, "<Agent>"),
9673 q("<a>", "<knows>", "<b>"),
9674 q("<b>", "<knows>", "<c>"),
9675 ];
9676 let (bytes, _) =
9677 crate::ingest::assemble_dataset_with_opts(quads, true, false, None, |_, _| Vec::new());
9678
9679 let rete = Rete::open(&bytes).unwrap();
9681 let pyr = rete.pyramid().expect("pyramid present");
9682 assert!(!pyr.level_rollups.is_empty(), "schema pyramid shipped");
9683 assert!(pyr
9684 .class_hierarchy
9685 .iter()
9686 .any(|n| n.class == "<Agent>" && n.depth == 0));
9687
9688 let r = CountingReader::new(SliceReader::new(&bytes));
9690 let view = SummaryView::open_ranged(&r).unwrap().unwrap();
9691 assert!(view.level_count() >= 2, "multi-level pyramid");
9692 let coarse = view.level_rollup(0).unwrap();
9693 assert!(
9694 coarse.classes.iter().any(|(c, _)| c == "<Agent>"),
9695 "coarsest level rolls up to the root Agent"
9696 );
9697 let h = Header::from_bytes(&bytes).unwrap();
9698 assert!(
9699 r.bytes_read() <= bytes.len() as u64 - h.root_dir_len,
9700 "summary read {} bytes; the {}-byte index section must be skipped",
9701 r.bytes_read(),
9702 h.root_dir_len
9703 );
9704 }
9705
9706 #[test]
9707 fn predicate_totals_from_summary_only() {
9708 use crate::reader::SliceReader;
9709 let bytes = build_image();
9711 let reader = SliceReader::new(&bytes);
9712 let view = SummaryView::open_ranged(&reader).unwrap().unwrap();
9713 assert_eq!(view.predicate_total("knows"), 2);
9714 assert_eq!(view.predicate_total("age"), 1);
9715 assert_eq!(view.predicate_total("missing"), 0);
9716 let totals = view.predicate_totals();
9717 assert_eq!(totals[0], ("knows".to_string(), 2)); }
9719
9720 #[test]
9721 fn query_patterns_resolve_to_terms() {
9722 let rete = Rete::open(&build_image()).unwrap();
9723
9724 assert_eq!(rete.query(None, None, None).len(), 3);
9726
9727 let mut alice = rete.query(Some("Alice"), None, None);
9729 alice.sort();
9730 assert_eq!(
9731 alice,
9732 vec![
9733 ("Alice".into(), "age".into(), "30".into()),
9734 ("Alice".into(), "knows".into(), "Bob".into()),
9735 ]
9736 );
9737
9738 assert_eq!(rete.query(None, Some("knows"), None).len(), 2);
9740
9741 assert_eq!(
9743 rete.query(Some("Bob"), Some("knows"), Some("Carol")),
9744 vec![("Bob".into(), "knows".into(), "Carol".into())]
9745 );
9746 assert!(rete.query(Some("Nobody"), None, None).is_empty());
9747 assert!(rete.query(None, Some("likes"), None).is_empty());
9748 }
9749
9750 #[test]
9751 fn query_provenance_reports_terms_ids_sections_and_index_choice() {
9752 let bytes = build_image();
9753 let rete = Rete::open(&bytes).unwrap();
9754
9755 let mut matches = rete.query_with_provenance(None, Some("knows"), None);
9756 matches.sort_by(|a, b| a.terms.cmp(&b.terms));
9757
9758 assert_eq!(matches.len(), 2);
9759 assert_eq!(
9760 matches[0].terms,
9761 ("Alice".into(), "knows".into(), "Bob".into())
9762 );
9763 assert_eq!(
9764 matches[0].ids,
9765 rete.dictionary().encode("Alice", "knows", "Bob").unwrap()
9766 );
9767 assert_eq!(matches[0].graph.as_deref(), None);
9768 assert_eq!(
9769 matches[0].matched_pattern,
9770 (None, Some(matches[0].ids.1), None)
9771 );
9772 assert_eq!(
9773 matches[0].index_permutation,
9774 crate::index::IndexPermutation::Pos
9775 );
9776
9777 let h = rete.header();
9778 assert_eq!(matches[0].dictionary_range.offset, h.dictionary_offset);
9779 assert_eq!(matches[0].dictionary_range.len, h.dictionary_len);
9780 assert_eq!(matches[0].index_range.offset, h.root_dir_offset);
9781 assert_eq!(matches[0].index_range.len, h.root_dir_len);
9782 assert!(
9783 matches[0].index_section_range.offset > h.root_dir_offset,
9784 "POS is section 1, so its payload starts after the container header and SPO payload"
9785 );
9786 assert!(matches[0].index_section_range.len > 0);
9787 assert!(matches[0].index_section_range.end() <= matches[0].index_range.end());
9788 assert!(matches[0].index_section_range.len < matches[0].index_range.len);
9789 assert_eq!(
9790 matches[0].pyramid_range.as_ref().map(|r| (r.offset, r.len)),
9791 Some((h.pyramid_meta_offset, h.pyramid_meta_len))
9792 );
9793 let tile_range = matches[0].tile_range.expect("tiled file reports a tile");
9796 assert!(matches[0]
9797 .tile
9798 .as_deref()
9799 .unwrap()
9800 .starts_with(matches[0].index_permutation.name()));
9801 assert!(matches[0].index_section_range.offset <= tile_range.offset);
9802 assert!(tile_range.end() <= matches[0].index_section_range.end());
9803 }
9804
9805 fn build_labeled(n: usize) -> Vec<u8> {
9809 const LABEL: &str = "<http://www.w3.org/2000/01/rdf-schema#label>";
9810 const WORDS: &[&str] = &[
9811 "alanine",
9812 "benzene",
9813 "glucose",
9814 "dextrose",
9815 "ethanol",
9816 "formate",
9817 "heptane",
9818 "isoleucine",
9819 ];
9820 let triples: Vec<(String, String, String)> = (0..n)
9821 .flat_map(|i| {
9822 let s = format!("<http://ex/e{i}>");
9823 let w = WORDS[i % WORDS.len()];
9824 [
9825 (s.clone(), LABEL.to_string(), format!("\"{w}-{i:06}\"")),
9826 (
9827 s,
9828 "<http://ex/p>".to_string(),
9829 format!("<http://ex/c{}>", i % 64),
9830 ),
9831 ]
9832 })
9833 .collect();
9834 let mut db = DictionaryBuilder::new();
9835 for (s, p, o) in &triples {
9836 db.observe(s, p, o);
9837 }
9838 let dict = db.build();
9839 let ids: Vec<(u32, u32, u32)> = triples
9840 .iter()
9841 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
9842 .collect();
9843 let mut ib = GraphIndexBuilder::new();
9844 for &t in &ids {
9845 ib.push(t);
9846 }
9847 let (meta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
9848 write_file(&dict, &ib.build(), false, &meta, levels)
9849 }
9850
9851 #[test]
9852 fn prefix_search_matches_a_filter_scan() {
9853 let bytes = build_labeled(800);
9856 let rete = Rete::open(&bytes).unwrap();
9857 let idx_subjects: std::collections::BTreeSet<String> = rete
9858 .prefix_search("glucose", 10_000)
9859 .into_iter()
9860 .map(|(_label, subject)| subject)
9861 .collect();
9862 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
9864 FILTER(STRSTARTS(LCASE(?l), \"glucose\")) }";
9865 let crate::QueryOutput::Select(_, rows) = crate::eval_query(&rete, q).unwrap() else {
9866 panic!("expected SELECT");
9867 };
9868 let scan_subjects: std::collections::BTreeSet<String> =
9869 rows.iter().map(|r| r.get("s").cloned().unwrap()).collect();
9870 assert_eq!(idx_subjects, scan_subjects, "index agrees with the scan");
9871 assert_eq!(
9872 idx_subjects.len(),
9873 100,
9874 "800/8 words = 100 glucose-* labels"
9875 );
9876 }
9877
9878 #[test]
9882 #[ignore]
9883 fn bench_prefix_search_vs_filter_scan() {
9884 use std::time::Instant;
9885 let n = 6000; let bytes = build_labeled(n);
9887 let rete = Rete::open(&bytes).unwrap();
9888 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
9889 FILTER(STRSTARTS(LCASE(?l), \"glucose\")) }";
9890 let reps = 200;
9891 let idx_n = rete.prefix_search("glucose", 100_000).len();
9892 let t = Instant::now();
9893 for _ in 0..reps {
9894 std::hint::black_box(rete.prefix_search("glucose", 100_000));
9895 }
9896 let idx_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
9897 let t = Instant::now();
9898 for _ in 0..reps {
9899 let _ = std::hint::black_box(crate::eval_query(&rete, q).unwrap());
9900 }
9901 let scan_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
9902 println!(
9903 "label prefix search over {n} labeled subjects ({idx_n} matches): \
9904 index {idx_ms:.4} ms vs FILTER scan {scan_ms:.3} ms ({:.0}× faster)",
9905 scan_ms / idx_ms
9906 );
9907 }
9908
9909 #[test]
9913 #[ignore]
9914 fn bench_text_search_vs_contains_scan() {
9915 use std::time::Instant;
9916 const LABEL: &str = "<http://www.w3.org/2000/01/rdf-schema#label>";
9917 const WORDS: &[&str] = &[
9918 "alanine",
9919 "benzene",
9920 "glucose",
9921 "dextrose",
9922 "ethanol",
9923 "formate",
9924 "heptane",
9925 "isoleucine",
9926 ];
9927 let n = 6000;
9928 let triples: Vec<(String, String, String)> = (0..n)
9929 .map(|i| {
9930 (
9931 format!("<http://ex/e{i}>"),
9932 LABEL.to_string(),
9933 format!("\"{} sample number {i:06}\"", WORDS[i % WORDS.len()]),
9934 )
9935 })
9936 .collect();
9937 let mut db = DictionaryBuilder::new();
9938 for (s, p, o) in &triples {
9939 db.observe(s, p, o);
9940 }
9941 let dict = db.build();
9942 let ids: Vec<(u32, u32, u32)> = triples
9943 .iter()
9944 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
9945 .collect();
9946 let mut ib = GraphIndexBuilder::new();
9947 for &t in &ids {
9948 ib.push(t);
9949 }
9950 let ti = compute_text_index(&dict, &ids);
9951 let bytes = write_dataset_with_metadata(&dict, &ib.build(), &[], false, &[], 0, &[], &ti);
9952 let rete = Rete::open(&bytes).unwrap();
9953
9954 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
9955 FILTER(CONTAINS(LCASE(?l), \"glucose\")) }";
9956 let reps = 200;
9957 let idx_n = rete.text_search(&["glucose"], None, 100_000).len();
9958 let t = Instant::now();
9959 for _ in 0..reps {
9960 std::hint::black_box(rete.text_search(&["glucose"], None, 100_000));
9961 }
9962 let idx_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
9963 let t = Instant::now();
9964 for _ in 0..reps {
9965 let _ = std::hint::black_box(crate::eval_query(&rete, q).unwrap());
9966 }
9967 let scan_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
9968 println!(
9969 "text search over {n} literals ({idx_n} matches): \
9970 index {idx_ms:.4} ms vs FILTER(CONTAINS) scan {scan_ms:.3} ms ({:.0}× faster)",
9971 scan_ms / idx_ms
9972 );
9973 }
9974
9975 #[test]
9979 #[ignore = "operational tool, driven by RETE_DEBUG_* env vars"]
9980 fn debug_bound_po_routing() {
9981 struct FR(std::fs::File);
9982 impl crate::RangeReader for FR {
9983 fn len(&self) -> u64 {
9984 self.0.metadata().map(|m| m.len()).unwrap_or(0)
9985 }
9986 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
9987 use std::os::unix::fs::FileExt;
9988 let mut buf = vec![0u8; len as usize];
9989 self.0.read_exact_at(&mut buf, offset)?;
9990 Ok(buf)
9991 }
9992 }
9993 let path = std::env::var("RETE_DEBUG_FILE").expect("RETE_DEBUG_FILE");
9994 let p_iri = std::env::var("RETE_DEBUG_P").expect("RETE_DEBUG_P");
9995 let o_iri = std::env::var("RETE_DEBUG_O").expect("RETE_DEBUG_O");
9996 let rete =
9997 Rete::open_ranged_lazy(std::sync::Arc::new(FR(std::fs::File::open(&path).unwrap())))
9998 .unwrap();
9999 let pid = rete.dict.predicate_id(&p_iri).expect("p resolves");
10000 let oid = rete.dict.object_id(&o_iri).expect("o resolves");
10001 eprintln!("pid={pid} oid={oid}");
10002 let pattern = (None, Some(pid), Some(oid));
10003 let perm = GraphIndex::best_permutation(pattern);
10004 eprintln!("best_permutation = {}", perm.name());
10005 let si = perm.section_index();
10006 let tiles = &rete.index.sections[si];
10007 eprintln!("section {} tiles = {}", perm.name(), tiles.len());
10008 let [pa, pb, pc] = perm.order_pattern(pattern);
10009 eprintln!("permuted pattern pa={pa:?} pb={pb:?} pc={pc:?}");
10010 let (start, end) = rete.index.tile_span(si, pa);
10011 eprintln!("tile_span = [{start}, {end}) -> {} tiles", end - start);
10012 let mut admitted = 0usize;
10013 for (ti, t) in tiles.iter().enumerate().take(end).skip(start) {
10014 if t.syn_admits(pb, pc) {
10015 admitted += 1;
10016 if admitted <= 10 {
10017 let (lo, hi) = t.leading_range();
10018 eprintln!(" admit tile {ti}: a=[{lo},{hi}] syn={:?}", t.syn);
10019 }
10020 }
10021 }
10022 eprintln!("admitted {admitted} tile(s) by synopsis");
10023 let n = rete.index.scan_iter(pattern).count();
10024 eprintln!("scan_iter matches = {n}");
10025 let hi_res = rete.query(None, Some(&p_iri), Some(&o_iri));
10026 eprintln!("high-level query matches = {}", hi_res.len());
10027 }
10028
10029 #[test]
10037 fn layout_header_label_matches_its_span() {
10038 let mut db = DictionaryBuilder::new();
10039 let mut triples = Vec::new();
10040 for i in 0..24u32 {
10041 let (s, p, o) = (
10042 format!("<http://ex/s{}>", i % 5),
10043 format!("<http://ex/p{}>", i % 3),
10044 format!("<http://ex/o{i}>"),
10045 );
10046 db.observe(&s, &p, &o);
10047 triples.push((s, p, o));
10048 }
10049 let dict = db.build();
10050 let ids: Vec<(u32, u32, u32)> = triples
10051 .iter()
10052 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
10053 .collect();
10054 let index = GraphIndexBuilder::from_triples(ids).build();
10055 let image = write_dataset_with_metadata(
10056 &dict,
10057 &index,
10058 &[],
10059 false,
10060 &[],
10061 0,
10062 br#"{"name":"layout-label-fixture"}"#,
10063 &[],
10064 );
10065 let rete = Rete::open(&image).unwrap();
10066
10067 let layout = rete.file_layout();
10068 let header = layout
10069 .iter()
10070 .find(|s| s.kind == "header")
10071 .expect("every layout starts with a header segment");
10072
10073 assert_eq!(
10074 header.offset, 0,
10075 "the header is the first thing in the file"
10076 );
10077 assert_eq!(
10078 header.len, HEADER_LEN as u64,
10079 "the header segment must span exactly HEADER_LEN"
10080 );
10081
10082 let digits: String = header
10085 .label
10086 .chars()
10087 .skip_while(|c| !c.is_ascii_digit())
10088 .take_while(|c| c.is_ascii_digit())
10089 .collect();
10090 assert!(
10091 !digits.is_empty(),
10092 "header label {:?} states no size at all",
10093 header.label
10094 );
10095 assert_eq!(
10096 digits.parse::<u64>().unwrap(),
10097 header.len,
10098 "header label {:?} disagrees with the {}-byte span it describes",
10099 header.label,
10100 header.len
10101 );
10102
10103 assert!(
10106 layout.windows(2).all(|w| w[0].offset <= w[1].offset),
10107 "file_layout must return segments sorted by offset"
10108 );
10109 }
10110}