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"))]
451pub(crate) fn compress_to(
452 codec: u8,
453 src_len: u64,
454 src: &mut dyn std::io::Read,
455 dst: &mut dyn std::io::Write,
456) -> std::io::Result<()> {
457 match codec {
458 #[cfg(feature = "compression")]
459 CODEC_ZSTD => {
460 let mut enc = zstd::stream::Encoder::new(dst, crate::write_opts::zstd_level())?;
461 if crate::write_opts::frame_shape() == crate::write_opts::FrameShape::Sized {
462 enc.set_pledged_src_size(Some(src_len))?;
463 enc.include_contentsize(true)?;
464 }
465 let copied = std::io::copy(src, &mut enc)?;
466 if copied != src_len {
467 return Err(std::io::Error::other(format!(
468 "compress_to: {copied} bytes read, {src_len} announced"
469 )));
470 }
471 enc.finish()?;
472 Ok(())
473 }
474 _ => {
475 let _ = src_len;
476 std::io::copy(src, dst).map(|_| ())
477 }
478 }
479}
480
481pub(crate) fn decompress(codec: u8, bytes: &[u8]) -> Result<Vec<u8>, FileError> {
482 match codec {
483 CODEC_NONE => Ok(bytes.to_vec()),
484 #[cfg(feature = "compression")]
493 CODEC_ZSTD => zstd::decode_all(bytes).map_err(FileError::Decompress),
494 #[cfg(not(feature = "compression"))]
499 CODEC_ZSTD => {
500 use std::io::Read;
501 let mut dec = ruzstd::decoding::StreamingDecoder::new(bytes)
502 .map_err(|e| FileError::Decompress(std::io::Error::other(e.to_string())))?;
503 let mut out = Vec::new();
504 dec.read_to_end(&mut out).map_err(FileError::Decompress)?;
505 Ok(out)
506 }
507 other => Err(FileError::UnknownCodec(other)),
508 }
509}
510
511const TILE_COALESCE_GAP: u64 = 4096;
515
516const DICT_COALESCE_GAP: u64 = 64 * 1024;
522
523pub const DEFAULT_EXPORT_BUDGET_MB: u64 = 4096;
530
531pub const DEFAULT_QUERY_BUDGET_MB: u64 = DEFAULT_EXPORT_BUDGET_MB;
540
541pub fn spill_threshold_for(budget: u64) -> u64 {
545 budget / 8
546}
547
548const MIN_DICT_CACHE_CAP: u64 = 64 << 20; const TRANSIENT_RESERVE_MAX: u64 = 128 << 20; #[derive(Debug, Clone, Copy)]
569pub struct MemoryBudget {
570 pub block_cache: u64,
572 pub dict_cache: u64,
574 pub tile_cache: u64,
576}
577
578pub fn split_memory_budget(total: u64) -> MemoryBudget {
589 if total == u64::MAX {
590 return MemoryBudget {
591 block_cache: u64::MAX,
592 dict_cache: u64::MAX,
593 tile_cache: u64::MAX,
594 };
595 }
596 let block_cache = (total / 8).min(crate::block_cache::DEFAULT_CACHE_CAP);
599 let tile_cache = (total / 16).min(256 << 20);
602 let reserve = (total / 32).min(TRANSIENT_RESERVE_MAX);
608 let dict_cache = total
611 .saturating_sub(block_cache)
612 .saturating_sub(tile_cache)
613 .saturating_sub(reserve)
614 .max(MIN_DICT_CACHE_CAP);
615 MemoryBudget {
616 block_cache,
617 dict_cache,
618 tile_cache,
619 }
620}
621
622#[derive(Debug, Clone, Copy, Default)]
625pub struct FixedOverhead {
626 pub dict_directory: u64,
628 pub index_directory: u64,
630 pub named_graphs: u64,
632}
633
634impl FixedOverhead {
635 pub fn total(&self) -> u64 {
637 self.dict_directory + self.index_directory + self.named_graphs
638 }
639}
640
641fn fmt_cap(cap: u64) -> String {
643 if cap == u64::MAX {
644 "unlimited".to_string()
645 } else {
646 format!("{}MiB", cap >> 20)
647 }
648}
649
650fn read_coalesced<R: RangeReader + ?Sized>(
656 reader: &R,
657 ranges: &[ByteRange],
658 gap: u64,
659) -> Option<Vec<Vec<u8>>> {
660 let mut spans: Vec<(u64, u64)> = Vec::new();
663 let mut span_of: Vec<usize> = Vec::with_capacity(ranges.len());
664 let mut i = 0;
665 while i < ranges.len() {
666 let start = ranges[i].offset;
667 let mut end = ranges[i].offset.checked_add(ranges[i].len)?;
668 let mut j = i + 1;
669 while j < ranges.len() {
670 let r = &ranges[j];
671 if r.offset < end || r.offset - end > gap {
672 break;
673 }
674 end = r.offset.checked_add(r.len)?;
675 j += 1;
676 }
677 let si = spans.len();
678 spans.push((start, end - start));
679 for _ in i..j {
680 span_of.push(si);
681 }
682 i = j;
683 }
684 let blobs = reader.read_many(&spans).ok()?;
685 if blobs.len() != spans.len() {
686 return None;
687 }
688 let mut out = Vec::with_capacity(ranges.len());
689 for (k, r) in ranges.iter().enumerate() {
690 let (span_start, _) = spans[span_of[k]];
691 let blob = &blobs[span_of[k]];
692 let lo = (r.offset - span_start) as usize;
693 let hi = lo.checked_add(r.len as usize)?;
694 out.push(blob.get(lo..hi)?.to_vec());
695 }
696 Some(out)
697}
698
699fn content_hash(parts: &[&[u8]]) -> [u8; 16] {
702 let mut h = blake3::Hasher::new();
703 for p in parts {
704 h.update(p);
705 }
706 let mut out = [0u8; 16];
707 out.copy_from_slice(&h.finalize().as_bytes()[..16]);
708 out
709}
710
711pub type TermTriple = (String, String, String);
713
714#[derive(Debug, Clone, Copy, PartialEq, Eq)]
717pub struct DumpPlan {
718 pub scan: Option<crate::index::ScanPlan>,
722 pub dictionary_bytes: u64,
731}
732
733#[derive(Debug, Clone)]
737pub struct LayoutSegment {
738 pub kind: &'static str,
739 pub label: String,
740 pub offset: u64,
741 pub len: u64,
742}
743
744#[derive(Debug, Clone, Copy, PartialEq, Eq)]
746pub struct ByteRange {
747 pub offset: u64,
748 pub len: u64,
749}
750
751impl ByteRange {
752 pub fn end(self) -> u64 {
753 self.offset + self.len
754 }
755}
756
757#[derive(Debug, Clone, PartialEq, Eq)]
759pub struct TripleProvenance {
760 pub terms: TermTriple,
762 pub ids: Triple,
764 pub graph: Option<String>,
766 pub matched_pattern: Pattern,
768 pub index_permutation: IndexPermutation,
770 pub dictionary_range: ByteRange,
772 pub index_range: ByteRange,
774 pub index_section_range: ByteRange,
777 pub pyramid_range: Option<ByteRange>,
779 pub tile: Option<String>,
783 pub tile_range: Option<ByteRange>,
786}
787
788fn encode_container(sections: &[&[u8]], codec: u8) -> Vec<u8> {
792 let mut out = Vec::new();
793 write_uvarint(&mut out, sections.len() as u64);
794 for s in sections {
795 let payload = compress(codec, s);
796 write_uvarint(&mut out, payload.len() as u64);
797 out.extend_from_slice(&payload);
798 }
799 out
800}
801
802fn decode_container(bytes: &[u8], codec: u8) -> Result<Vec<Vec<u8>>, FileError> {
804 let (n, mut pos) = read_uvarint(bytes).ok_or(FileError::Container("truncated count"))?;
805 let mut out = Vec::with_capacity((n as usize).min(bytes.len()));
809 for _ in 0..n {
810 let (len, used) =
811 read_uvarint(&bytes[pos..]).ok_or(FileError::Container("truncated length"))?;
812 pos += used;
813 let end = pos + len as usize;
814 if end > bytes.len() {
815 return Err(FileError::Container("section overruns buffer"));
816 }
817 out.push(decompress(codec, &bytes[pos..end])?);
818 pos = end;
819 }
820 Ok(out)
821}
822
823fn checked_end(off: u64, len: u64) -> Result<u64, FileError> {
824 off.checked_add(len)
825 .ok_or(FileError::Container("section range overflows"))
826}
827
828const DICT_CHUNK_BUDGET: usize = crate::write_opts::DEFAULT_DICT_CHUNK_BYTES;
831
832fn encode_chunked_dict_section(raw: &[u8], codec: u8) -> Vec<u8> {
849 let meta = crate::dict::parse_meta(raw).unwrap_or(crate::dict::SectionMeta {
850 term_count: 0,
851 restart_interval: 1,
852 restart_offsets: Vec::new(),
853 });
854 let body_start = meta
855 .restart_offsets
856 .first()
857 .copied()
858 .unwrap_or(raw.len() as u64);
859 let header = &raw[..(body_start.min(raw.len() as u64)) as usize];
860
861 let chunk_budget = crate::write_opts::dict_chunk_budget() as u64;
863 let n_runs = meta.restart_offsets.len();
864 let mut bounds: Vec<(usize, u64, u64)> = Vec::new(); let mut r = 0;
866 while r < n_runs {
867 let start = meta.restart_offsets[r];
868 let mut r2 = r + 1;
869 while r2 < n_runs && meta.restart_offsets[r2] - start < chunk_budget {
870 r2 += 1;
871 }
872 let end = if r2 < n_runs {
873 meta.restart_offsets[r2]
874 } else {
875 raw.len() as u64
876 };
877 bounds.push((r, start, end));
878 r = r2;
879 }
880
881 #[cfg(feature = "parallel")]
885 let compressed: Vec<Vec<u8>> = {
886 use rayon::prelude::*;
887 bounds
888 .par_iter()
889 .map(|&(_, s, e)| compress(codec, &raw[s as usize..e as usize]))
890 .collect()
891 };
892 #[cfg(not(feature = "parallel"))]
893 let compressed: Vec<Vec<u8>> = bounds
894 .iter()
895 .map(|&(_, s, e)| compress(codec, &raw[s as usize..e as usize]))
896 .collect();
897 let mut out = Vec::new();
898 write_uvarint(&mut out, header.len() as u64);
899 out.extend_from_slice(header);
900 write_uvarint(&mut out, bounds.len() as u64);
901 let mut prev_run = 0usize;
902 let mut prev_last: Option<Vec<u8>> = None;
906 for (i, (&(first_run, start, end), comp)) in bounds.iter().zip(&compressed).enumerate() {
907 let key = if i == 0 {
908 Vec::new()
910 } else {
911 let first = crate::dict::run_first_term(raw, start as usize).unwrap_or_default();
912 match &prev_last {
913 None => first,
916 Some(pl) => crate::dict::shortest_separator(pl, &first),
917 }
918 };
919 if i + 1 < bounds.len() {
920 let last_run_off = meta.restart_offsets[bounds[i + 1].0 - 1] as usize;
922 prev_last = crate::dict::run_last_term(raw, last_run_off, end as usize);
923 }
924 write_uvarint(&mut out, (first_run - prev_run) as u64);
925 write_uvarint(&mut out, key.len() as u64);
926 out.extend_from_slice(&key);
927 write_uvarint(&mut out, comp.len() as u64);
928 prev_run = first_run;
929 }
930 for comp in &compressed {
931 out.extend_from_slice(comp);
932 }
933 out
934}
935
936struct DictChunkEntry {
940 first_run: usize,
941 key: Vec<u8>,
942 body_start: u64,
943 start: u64,
944 end: u64,
945}
946
947fn parse_chunked_dict_dir(
951 bytes: &[u8],
952 total_len: u64,
953) -> Result<(crate::dict::SectionMeta, Vec<DictChunkEntry>), FileError> {
954 let mut pos = 0usize;
955 let take = |pos: &mut usize| -> Result<u64, FileError> {
956 let (v, n) = read_uvarint(bytes.get(*pos..).unwrap_or(&[]))
957 .ok_or(FileError::Container("truncated dict chunk directory"))?;
958 *pos += n;
959 Ok(v)
960 };
961 let header_len = take(&mut pos)? as usize;
962 let header = bytes
963 .get(pos..pos.saturating_add(header_len))
964 .ok_or(FileError::Container("truncated dict header"))?;
965 let meta = crate::dict::parse_meta(header)
966 .map_err(|_| FileError::Container("malformed dict header"))?;
967 pos += header_len;
968
969 let num_chunks = take(&mut pos)? as usize;
970 let mut entries = Vec::with_capacity(num_chunks.min(bytes.len()));
971 let mut lens = Vec::with_capacity(num_chunks.min(bytes.len()));
972 let mut prev_run = 0usize;
973 for _ in 0..num_chunks {
974 let drun = take(&mut pos)? as usize;
975 let klen = take(&mut pos)? as usize;
976 let key = bytes
977 .get(pos..pos.saturating_add(klen))
978 .ok_or(FileError::Container("truncated dict chunk key"))?
979 .to_vec();
980 pos += klen;
981 let clen = take(&mut pos)?;
982 let first_run = prev_run + drun;
983 let body_start = meta
984 .restart_offsets
985 .get(first_run)
986 .copied()
987 .ok_or(FileError::Container("dict chunk run out of range"))?;
988 entries.push(DictChunkEntry {
989 first_run,
990 key,
991 body_start,
992 start: 0,
993 end: 0,
994 });
995 lens.push(clen);
996 prev_run = first_run;
997 }
998 let mut start = pos as u64;
999 for (e, len) in entries.iter_mut().zip(lens) {
1000 let end = start
1001 .checked_add(len)
1002 .filter(|&e| e <= total_len)
1003 .ok_or(FileError::Container("dict chunk overruns section"))?;
1004 e.start = start;
1005 e.end = end;
1006 start = end;
1007 }
1008 Ok((meta, entries))
1009}
1010
1011fn read_dict_dir_ranged<R: RangeReader>(
1023 reader: &R,
1024 section: ByteRange,
1025) -> Result<(crate::dict::SectionMeta, Vec<DictChunkEntry>), FileError> {
1026 let total = section.len;
1027 let init = 8192.min(total); let head = reader.read_at(section.offset, init)?;
1035 let (header_len, n0) =
1036 read_uvarint(&head).ok_or(FileError::Container("truncated dict header len"))?;
1037 let hbase = n0; let (term_count, n1) = read_uvarint(head.get(hbase..).unwrap_or(&[]))
1039 .ok_or(FileError::Container("truncated dict term_count"))?;
1040 let (restart_interval, _n2) = read_uvarint(head.get(hbase + n1..).unwrap_or(&[]))
1041 .ok_or(FileError::Container("truncated dict interval"))?;
1042 if restart_interval == 0 {
1043 return Err(FileError::Container("zero restart interval"));
1044 }
1045 let dir_start = (hbase as u64)
1048 .checked_add(header_len)
1049 .filter(|&d| d <= total)
1050 .ok_or(FileError::Container("dict header overruns section"))?;
1051 let dir_total = total - dir_start;
1052 let meta = crate::dict::SectionMeta {
1053 term_count: term_count as u32,
1054 restart_interval: restart_interval as u32,
1055 restart_offsets: Vec::new(),
1056 };
1057 let finish = |mut entries: Vec<DictChunkEntry>| {
1058 for e in &mut entries {
1059 e.start += dir_start; e.end += dir_start;
1061 }
1062 (meta.clone(), entries)
1063 };
1064 let mut have: Vec<u8> = Vec::new();
1068 if dir_start < head.len() as u64 {
1069 match parse_chunk_dir_only(&head[dir_start as usize..], dir_total)? {
1070 ChunkDirParse::Done(entries) => return Ok(finish(entries)),
1071 ChunkDirParse::Truncated { .. } => have = head[dir_start as usize..].to_vec(),
1072 }
1073 }
1074 let mut want = 4096u64.min(dir_total).max(1);
1087 loop {
1088 let held = have.len() as u64;
1089 if want > held {
1090 let extra = reader.read_at(section.offset + dir_start + held, want - held)?;
1091 if extra.is_empty() {
1092 return Err(FileError::Container("truncated dict chunk directory"));
1093 }
1094 have.extend_from_slice(&extra);
1095 }
1096 let held = have.len() as u64;
1097 match parse_chunk_dir_only(&have, dir_total)? {
1098 ChunkDirParse::Done(entries) => return Ok(finish(entries)),
1099 ChunkDirParse::Truncated { .. } if held >= dir_total => {
1100 return Err(FileError::Container("truncated dict chunk directory"));
1101 }
1102 ChunkDirParse::Truncated {
1103 parsed,
1104 used,
1105 total,
1106 } => {
1107 let est = if parsed >= 16 && used > 0 {
1112 let whole = ((used as u64) / (parsed as u64)).saturating_mul(total as u64);
1113 whole.saturating_add(whole / 8).saturating_add(64)
1114 } else {
1115 held.saturating_mul(2)
1117 };
1118 want = est
1119 .max(held.saturating_add(1))
1120 .min(held.saturating_mul(4))
1121 .min(dir_total);
1122 }
1123 }
1124 }
1125}
1126
1127enum ChunkDirParse {
1132 Done(Vec<DictChunkEntry>),
1133 Truncated {
1134 parsed: usize,
1136 used: usize,
1138 total: usize,
1140 },
1141}
1142
1143fn parse_chunk_dir_only(dir: &[u8], dir_total: u64) -> Result<ChunkDirParse, FileError> {
1153 let mut pos = 0usize;
1154 let take = |pos: &mut usize| -> Option<u64> {
1155 let (v, n) = read_uvarint(dir.get(*pos..).unwrap_or(&[]))?;
1156 *pos += n;
1157 Some(v)
1158 };
1159 let Some(num_chunks) = take(&mut pos) else {
1160 return Ok(ChunkDirParse::Truncated {
1161 parsed: 0,
1162 used: 0,
1163 total: 0,
1164 });
1165 };
1166 let num_chunks = num_chunks as usize;
1167 let mut entries = Vec::with_capacity(num_chunks.min(dir.len()));
1168 let mut lens: Vec<u64> = Vec::with_capacity(num_chunks.min(dir.len()));
1169 let mut prev_run = 0usize;
1170 for _ in 0..num_chunks {
1171 let entry_start = pos;
1172 let short = ChunkDirParse::Truncated {
1173 parsed: entries.len(),
1174 used: entry_start,
1175 total: num_chunks,
1176 };
1177 let (Some(drun), Some(klen)) = (take(&mut pos), take(&mut pos)) else {
1178 return Ok(short);
1179 };
1180 let (drun, klen) = (drun as usize, klen as usize);
1181 let Some(key) = dir.get(pos..pos.saturating_add(klen)) else {
1182 return Ok(short);
1183 };
1184 let key = key.to_vec();
1185 pos += klen;
1186 let Some(clen) = take(&mut pos) else {
1187 return Ok(short);
1188 };
1189 let first_run = prev_run + drun;
1190 entries.push(DictChunkEntry {
1191 first_run,
1192 key,
1193 body_start: 0,
1194 start: 0,
1195 end: 0,
1196 });
1197 lens.push(clen);
1198 prev_run = first_run;
1199 }
1200 let mut start = pos as u64;
1201 for (e, len) in entries.iter_mut().zip(lens) {
1202 let end = start
1203 .checked_add(len)
1204 .filter(|&e| e <= dir_total)
1205 .ok_or(FileError::Container("dict chunk overruns section"))?;
1206 e.start = start;
1207 e.end = end;
1208 start = end;
1209 }
1210 Ok(ChunkDirParse::Done(entries))
1211}
1212
1213fn decode_chunked_dict_section(
1217 payload: &[u8],
1218 codec: u8,
1219 cache: std::sync::Arc<crate::chunk_cache::ChunkCache>,
1220 section_index: u8,
1221) -> Result<crate::dict::ChunkedSection, FileError> {
1222 let (meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64)?;
1223 let chunks = entries
1224 .into_iter()
1225 .map(|e| {
1226 let body = decompress(codec, &payload[e.start as usize..e.end as usize])?;
1227 Ok((
1228 crate::dict::SectionChunk::new(e.first_run, e.key, e.body_start),
1229 body,
1230 ))
1231 })
1232 .collect::<Result<Vec<_>, FileError>>()?;
1233 Ok(crate::dict::ChunkedSection::resident(
1234 meta,
1235 chunks,
1236 cache,
1237 section_index,
1238 ))
1239}
1240
1241fn decode_dictionary_container(bytes: &[u8], codec: u8) -> Result<Dictionary, FileError> {
1242 let dsecs = decode_container(bytes, CODEC_NONE)?;
1243 if dsecs.len() != 4 {
1244 return Err(FileError::Container("expected 4 dictionary sections"));
1245 }
1246 let cache = crate::chunk_cache::ChunkCache::unlimited_arc();
1249 let mut sections = Vec::with_capacity(4);
1250 for (si, sec) in dsecs.iter().enumerate() {
1251 sections.push(decode_chunked_dict_section(
1252 sec,
1253 codec,
1254 cache.clone(),
1255 si as u8,
1256 )?);
1257 }
1258 let arr: [crate::dict::ChunkedSection; 4] = sections
1259 .try_into()
1260 .map_err(|_| FileError::Container("expected 4 dictionary sections"))?;
1261 Ok(Dictionary::from_chunked_sections(arr))
1262}
1263
1264fn encode_tiled_section(index: &GraphIndex, si: usize, codec: u8) -> Vec<u8> {
1270 let tiles = index.tile_sections()[si];
1271 let n = tiles.len();
1272 #[cfg(feature = "parallel")]
1279 let compressed: Vec<Vec<u8>> = {
1280 use rayon::prelude::*;
1281 (0..n)
1282 .into_par_iter()
1283 .map(|ti| compress(codec, &index.tile_body(si, ti)))
1284 .collect()
1285 };
1286 #[cfg(not(feature = "parallel"))]
1287 let compressed: Vec<Vec<u8>> = (0..n)
1288 .map(|ti| compress(codec, &index.tile_body(si, ti)))
1289 .collect();
1290 let mut out = Vec::new();
1291 write_uvarint(&mut out, tiles.len() as u64);
1292 let mut prev_min = 0u32;
1293 for (tile, comp) in tiles.iter().zip(&compressed) {
1294 let (min_a, max_a) = tile.leading_range();
1295 write_uvarint(&mut out, (min_a - prev_min) as u64);
1296 write_uvarint(&mut out, (max_a - min_a) as u64);
1297 write_uvarint(&mut out, comp.len() as u64);
1298 prev_min = min_a;
1299 }
1300 for comp in &compressed {
1301 out.extend_from_slice(comp);
1302 }
1303 for ti in 0..n {
1311 let body = index.tile_body(si, ti);
1312 let (min_b, max_b, min_c, max_c) = match crate::triples::TripleBlock::parse(&body) {
1313 Ok(b) => {
1314 let z = b.zone();
1315 (z.min_b, z.max_b, z.min_c, z.max_c)
1316 }
1317 Err(_) => (0, u32::MAX, 0, u32::MAX),
1318 };
1319 write_uvarint(&mut out, min_b as u64);
1320 write_uvarint(&mut out, (max_b - min_b) as u64);
1321 write_uvarint(&mut out, min_c as u64);
1322 write_uvarint(&mut out, (max_c - min_c) as u64);
1323 }
1324 out
1325}
1326
1327struct TileDirEntry {
1330 min_a: u32,
1331 max_a: u32,
1332 start: u64,
1333 end: u64,
1334}
1335
1336type TileSynopsis = (u32, u32, u32, u32);
1338
1339fn parse_tile_synopsis(
1347 payload: &[u8],
1348 trailer_start: usize,
1349 num_tiles: usize,
1350) -> Option<Vec<TileSynopsis>> {
1351 let mut pos = trailer_start;
1352 let take = |pos: &mut usize| -> Option<u32> {
1353 let (v, n) = read_uvarint(payload.get(*pos..)?)?;
1354 *pos += n;
1355 u32::try_from(v).ok()
1356 };
1357 let mut out = Vec::with_capacity(num_tiles.min(payload.len()));
1358 for _ in 0..num_tiles {
1359 let min_b = take(&mut pos)?;
1360 let max_b = min_b.checked_add(take(&mut pos)?)?;
1361 let min_c = take(&mut pos)?;
1362 let max_c = min_c.checked_add(take(&mut pos)?)?;
1363 out.push((min_b, max_b, min_c, max_c));
1364 }
1365 Some(out)
1366}
1367
1368fn parse_tile_directory(bytes: &[u8], total_len: u64) -> Result<Vec<TileDirEntry>, FileError> {
1373 let mut pos = 0usize;
1374 let take = |pos: &mut usize| -> Result<u64, FileError> {
1375 let (v, n) = read_uvarint(bytes.get(*pos..).unwrap_or(&[]))
1376 .ok_or(FileError::Container("truncated tile directory"))?;
1377 *pos += n;
1378 Ok(v)
1379 };
1380 let num_tiles = take(&mut pos)? as usize;
1381 let mut entries = Vec::with_capacity(num_tiles.min(bytes.len()));
1382 let mut prev_min = 0u32;
1383 let mut lens = Vec::with_capacity(num_tiles.min(bytes.len()));
1384 for _ in 0..num_tiles {
1385 let dmin = take(&mut pos)? as u32;
1386 let span = take(&mut pos)? as u32;
1387 let len = take(&mut pos)?;
1388 let min_a = prev_min.wrapping_add(dmin);
1389 entries.push(TileDirEntry {
1390 min_a,
1391 max_a: min_a.wrapping_add(span),
1392 start: 0,
1393 end: 0,
1394 });
1395 lens.push(len);
1396 prev_min = min_a;
1397 }
1398 let mut start = pos as u64;
1399 for (e, len) in entries.iter_mut().zip(lens) {
1400 let end = start
1401 .checked_add(len)
1402 .filter(|&e| e <= total_len)
1403 .ok_or(FileError::Container("tile overruns section"))?;
1404 e.start = start;
1405 e.end = end;
1406 start = end;
1407 }
1408 Ok(entries)
1409}
1410
1411fn read_tile_directory_ranged<R: RangeReader>(
1415 reader: &R,
1416 section: ByteRange,
1417) -> Result<Vec<TileDirEntry>, FileError> {
1418 let total = section.len;
1419 let mut prefetch = 4096u64.min(total);
1420 loop {
1421 let prefix = reader.read_at(section.offset, prefetch)?;
1422 match parse_tile_directory(&prefix, total) {
1423 Ok(dir) => return Ok(dir),
1424 Err(_) if prefetch < total => prefetch = prefetch.saturating_mul(2).min(total),
1425 Err(e) => return Err(e),
1426 }
1427 }
1428}
1429
1430fn read_tile_synopsis_ranged<R: RangeReader>(
1437 reader: &R,
1438 section: ByteRange,
1439 dir: &[TileDirEntry],
1440) -> Vec<Option<TileSynopsis>> {
1441 let n = dir.len();
1442 let none = vec![None; n];
1443 let trailer_start = dir.iter().map(|e| e.end).max().unwrap_or(0);
1444 let total = section.len;
1445 if n == 0 || trailer_start >= total {
1446 return none; }
1448 let trailer_len = total - trailer_start;
1449 let Ok(bytes) = reader.read_at(section.offset + trailer_start, trailer_len) else {
1450 return none;
1451 };
1452 match parse_tile_synopsis(&bytes, 0, n) {
1453 Some(v) => v.into_iter().map(Some).collect(),
1454 None => none,
1455 }
1456}
1457
1458fn tile_file_ranges(
1462 index_bytes: &[u8],
1463 container_offset: u64,
1464 section_ranges: &[ByteRange; NUM_PERMS],
1465) -> [Vec<(u32, u32, ByteRange)>; NUM_PERMS] {
1466 let mut out: [Vec<(u32, u32, ByteRange)>; NUM_PERMS] = Default::default();
1467 for (section, range) in out.iter_mut().zip(section_ranges) {
1468 if range.len == 0 || range.offset < container_offset {
1472 continue;
1473 }
1474 let start = (range.offset - container_offset) as usize;
1475 let Some(payload) = index_bytes.get(start..start + range.len as usize) else {
1476 continue;
1477 };
1478 if let Ok(dir) = parse_tile_directory(payload, payload.len() as u64) {
1479 *section = dir
1480 .into_iter()
1481 .map(|e| {
1482 (
1483 e.min_a,
1484 e.max_a,
1485 ByteRange {
1486 offset: range.offset + e.start,
1487 len: (e.end - e.start),
1488 },
1489 )
1490 })
1491 .collect();
1492 }
1493 }
1494 out
1495}
1496
1497fn decode_tiled_section(payload: &[u8], codec: u8) -> Result<Vec<(u32, u32, Vec<u8>)>, FileError> {
1500 parse_tile_directory(payload, payload.len() as u64)?
1501 .into_iter()
1502 .map(|e| {
1503 Ok((
1504 e.min_a,
1505 e.max_a,
1506 decompress(codec, &payload[e.start as usize..e.end as usize])?,
1507 ))
1508 })
1509 .collect()
1510}
1511
1512fn decode_index_container(
1522 bytes: &[u8],
1523 codec: u8,
1524 perms: PermSet,
1525) -> Result<GraphIndex, FileError> {
1526 let isecs = decode_container(bytes, CODEC_NONE)?;
1527 if isecs.len() != perms.len() {
1528 return Err(FileError::Container(
1529 "index container section count does not match the header permutation mask",
1530 ));
1531 }
1532 decode_index_sections(isecs, codec, perms)
1533}
1534
1535fn decode_index_sections(
1536 mut isecs: Vec<Vec<u8>>,
1537 codec: u8,
1538 perms: PermSet,
1539) -> Result<GraphIndex, FileError> {
1540 let mut sections: [Vec<(u32, u32, Vec<u8>)>; NUM_PERMS] = Default::default();
1541 for (perm, sec) in perms.iter().zip(isecs.iter_mut()) {
1542 sections[perm.section_index()] = decode_tiled_section(sec, codec)?;
1543 }
1544 Ok(GraphIndex::from_tiles(sections, perms))
1545}
1546
1547fn decode_root_index_container(
1552 bytes: &[u8],
1553 header: &Header,
1554) -> Result<(GraphIndex, Option<[u8; 16]>), FileError> {
1555 if !header.has_quad_index() {
1556 return Ok((
1557 decode_index_container(bytes, header.block_codec, header.perms)?,
1558 None,
1559 ));
1560 }
1561 let mut isecs = decode_container(bytes, CODEC_NONE)?;
1562 if isecs.len() != header.index_container_sections() {
1563 return Err(FileError::Container(
1564 "index container section count does not match the header permutation mask",
1565 ));
1566 }
1567 let seal = isecs.pop().unwrap_or_default();
1568 let digest = crate::quad::parse_seal(&seal)?;
1569 Ok((
1570 decode_index_sections(isecs, header.block_codec, header.perms)?,
1571 Some(digest),
1572 ))
1573}
1574
1575fn check_named_layout(header: &Header) -> Result<(), FileError> {
1580 let quad = header.quad_index_section().is_some();
1581 if header.has_quad_index() {
1582 if !quad {
1583 return Err(FileError::Container(
1584 "quad-index flag set but the file has no quad-index section",
1585 ));
1586 }
1587 if header.named_graphs_len > 0 {
1588 return Err(FileError::Container(
1589 "a file cannot carry both a quad index and a named-graphs section",
1590 ));
1591 }
1592 crate::quad::check_perms(header.perms)?;
1593 } else if quad {
1594 return Err(FileError::Container(
1595 "quad-index section present but the header does not flag it",
1596 ));
1597 }
1598 Ok(())
1599}
1600
1601fn container_section_payload_ranges(
1602 bytes: &[u8],
1603 container_offset: u64,
1604 expected_sections: usize,
1605) -> Result<Vec<ByteRange>, FileError> {
1606 let (section_count, mut pos) =
1607 read_uvarint(bytes).ok_or(FileError::Container("truncated count"))?;
1608 let section_count = usize::try_from(section_count)
1609 .map_err(|_| FileError::Container("section count too large"))?;
1610 if section_count != expected_sections {
1611 return Err(FileError::Container("unexpected section count"));
1612 }
1613
1614 let mut ranges = Vec::with_capacity(section_count);
1615 for _ in 0..section_count {
1616 let remaining = bytes
1617 .get(pos..)
1618 .ok_or(FileError::Container("truncated length"))?;
1619 let (payload_len, used) =
1620 read_uvarint(remaining).ok_or(FileError::Container("truncated length"))?;
1621 pos = pos
1622 .checked_add(used)
1623 .ok_or(FileError::Container("section range overflows"))?;
1624 let payload_len_usize = usize::try_from(payload_len)
1625 .map_err(|_| FileError::Container("section length too large"))?;
1626 let payload_end = pos
1627 .checked_add(payload_len_usize)
1628 .ok_or(FileError::Container("section range overflows"))?;
1629 if payload_end > bytes.len() {
1630 return Err(FileError::Container("section overruns buffer"));
1631 }
1632 ranges.push(ByteRange {
1633 offset: checked_end(container_offset, pos as u64)?,
1634 len: payload_len,
1635 });
1636 pos = payload_end;
1637 }
1638
1639 Ok(ranges)
1640}
1641
1642fn decode_index_section_ranges(
1646 bytes: &[u8],
1647 container_offset: u64,
1648 perms: PermSet,
1649 sections: usize,
1650) -> Result<[ByteRange; NUM_PERMS], FileError> {
1651 let ranges = container_section_payload_ranges(bytes, container_offset, sections)?;
1652 let mut out = [ByteRange { offset: 0, len: 0 }; NUM_PERMS];
1653 for (perm, range) in perms.iter().zip(ranges) {
1654 out[perm.section_index()] = range;
1655 }
1656 Ok(out)
1657}
1658
1659fn read_uvarint_at<R: RangeReader + ?Sized>(
1660 reader: &R,
1661 absolute_offset: u64,
1662 container_end: u64,
1663) -> Result<(u64, u64), FileError> {
1664 if absolute_offset >= container_end {
1665 return Err(FileError::Container("truncated container varint"));
1666 }
1667 let remaining = container_end - absolute_offset;
1668 let probe_len = remaining.min(10);
1669 let bytes = reader.read_at(absolute_offset, probe_len)?;
1670 read_uvarint(&bytes)
1671 .map(|(value, used)| (value, used as u64))
1672 .ok_or(FileError::Container("truncated container varint"))
1673}
1674
1675fn locate_container_section_ranged<R: RangeReader + ?Sized>(
1678 reader: &R,
1679 container_offset: u64,
1680 container_len: u64,
1681 section_index: usize,
1682 expected_sections: u64,
1683) -> Result<ByteRange, FileError> {
1684 let container_end = checked_end(container_offset, container_len)?;
1685 let (section_count, used) = read_uvarint_at(reader, container_offset, container_end)?;
1686 if section_count != expected_sections {
1687 return Err(FileError::Container("unexpected container section count"));
1688 }
1689 if section_index >= section_count as usize {
1690 return Err(FileError::Container(
1691 "container section index out of bounds",
1692 ));
1693 }
1694
1695 let mut pos = checked_end(container_offset, used)?;
1696 for i in 0..section_count as usize {
1697 let (payload_len, len_used) = read_uvarint_at(reader, pos, container_end)?;
1698 pos = checked_end(pos, len_used)?;
1699 let payload_end = checked_end(pos, payload_len)?;
1700 if payload_end > container_end {
1701 return Err(FileError::Container("section overruns buffer"));
1702 }
1703 if i == section_index {
1704 return Ok(ByteRange {
1705 offset: pos,
1706 len: payload_len,
1707 });
1708 }
1709 pos = payload_end;
1710 }
1711 Err(FileError::Container("container section not found"))
1712}
1713
1714#[allow(clippy::type_complexity)]
1724fn open_index_container_lazy(
1725 reader: &std::sync::Arc<dyn RangeReader + Send + Sync>,
1726 container: ByteRange,
1727 block_codec: u8,
1728 has_synopsis: bool,
1729 read_concurrency: usize,
1730 perms: PermSet,
1731 sections: usize,
1732) -> Result<
1733 (
1734 GraphIndex,
1735 [ByteRange; NUM_PERMS],
1736 [Vec<(u32, u32, ByteRange)>; NUM_PERMS],
1737 ),
1738 FileError,
1739> {
1740 let mut index_section_ranges = [ByteRange { offset: 0, len: 0 }; NUM_PERMS];
1741 let mut tile_ranges: [Vec<(u32, u32, ByteRange)>; NUM_PERMS] = Default::default();
1742 #[allow(clippy::type_complexity)]
1743 let mut directories: [Vec<(u32, u32, Option<TileSynopsis>)>; NUM_PERMS] = Default::default();
1744 for (pos, perm) in perms.iter().enumerate() {
1745 let si = perm.section_index();
1746 let section = locate_container_section_ranged(
1747 reader,
1748 container.offset,
1749 container.len,
1750 pos,
1751 sections as u64,
1752 )?;
1753 index_section_ranges[si] = section;
1754 let dir = read_tile_directory_ranged(reader, section)?;
1755 let syn = if has_synopsis {
1758 read_tile_synopsis_ranged(reader, section, &dir)
1759 } else {
1760 vec![None; dir.len()]
1761 };
1762 directories[si] = dir
1763 .iter()
1764 .zip(syn)
1765 .map(|(e, s)| (e.min_a, e.max_a, s))
1766 .collect();
1767 tile_ranges[si] = dir
1768 .into_iter()
1769 .map(|e| {
1770 (
1771 e.min_a,
1772 e.max_a,
1773 ByteRange {
1774 offset: section.offset + e.start,
1775 len: (e.end - e.start),
1776 },
1777 )
1778 })
1779 .collect();
1780 }
1781
1782 let codec = block_codec;
1787 let loader_ranges = tile_ranges.clone();
1788 let loader_reader = reader.clone();
1789 let loader: crate::index::TileLoader = Box::new(move |si, ti| {
1790 let (_, _, range) = loader_ranges.get(si)?.get(ti)?;
1791 let bytes = loader_reader.read_at(range.offset, range.len).ok()?;
1792 decompress(codec, &bytes).ok()
1793 });
1794 let bulk_ranges = tile_ranges.clone();
1795 let bulk_reader = reader.clone();
1796 let bulk: crate::index::TileBulkLoader = Box::new(move |si, tis| {
1797 let section = bulk_ranges.get(si)?;
1798 let want: Option<Vec<ByteRange>> = tis
1799 .iter()
1800 .map(|&ti| section.get(ti).map(|&(_, _, r)| r))
1801 .collect();
1802 let blobs = read_coalesced(bulk_reader.as_ref(), &want?, TILE_COALESCE_GAP)?;
1803 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
1804 });
1805 let mut index =
1806 GraphIndex::from_remote_directories(directories, perms, loader).with_bulk_loader(bulk);
1807 index.set_tile_lens(std::array::from_fn(|si| {
1810 tile_ranges[si]
1811 .iter()
1812 .map(|&(_, _, r)| r.len.min(u32::MAX as u64) as u32)
1813 .collect()
1814 }));
1815 index.set_read_concurrency(read_concurrency);
1819 Ok((index, index_section_ranges, tile_ranges))
1820}
1821
1822pub fn write_file(
1826 dict: &Dictionary,
1827 index: &GraphIndex,
1828 has_quads: bool,
1829 pyramid_meta: &[u8],
1830 pyramid_levels: u16,
1831) -> Vec<u8> {
1832 write_dataset(dict, index, &[], has_quads, pyramid_meta, pyramid_levels)
1833}
1834
1835pub(crate) fn encode_index_container(index: &GraphIndex, codec: u8) -> Vec<u8> {
1841 encode_index_container_sealed(index, codec, None)
1842}
1843
1844fn encode_index_container_sealed(index: &GraphIndex, codec: u8, seal: Option<&[u8]>) -> Vec<u8> {
1849 let payloads: Vec<Vec<u8>> = index
1850 .perms()
1851 .iter()
1852 .map(|perm| encode_tiled_section(index, perm.section_index(), codec))
1853 .collect();
1854 let mut refs: Vec<&[u8]> = payloads.iter().map(|p| p.as_slice()).collect();
1855 if let Some(seal) = seal {
1856 refs.push(seal);
1857 }
1858 encode_container(&refs, CODEC_NONE)
1859}
1860
1861fn encode_named_graphs(named: &[(String, GraphIndex)], codec: u8) -> Vec<u8> {
1863 let mut out = Vec::new();
1864 write_uvarint(&mut out, named.len() as u64);
1865 for (iri, index) in named {
1866 write_uvarint(&mut out, iri.len() as u64);
1867 out.extend_from_slice(iri.as_bytes());
1868 let container = encode_index_container(index, codec);
1869 write_uvarint(&mut out, container.len() as u64);
1870 out.extend_from_slice(&container);
1871 }
1872 out
1873}
1874
1875fn decode_named_graphs(
1876 bytes: &[u8],
1877 codec: u8,
1878 perms: PermSet,
1879) -> Result<Vec<(String, GraphIndex)>, FileError> {
1880 let (n, mut pos) = read_uvarint(bytes).ok_or(FileError::Container("truncated graph count"))?;
1881 let bound = |start: usize, len: u64| -> Result<usize, FileError> {
1884 start
1885 .checked_add(len as usize)
1886 .filter(|&e| e <= bytes.len())
1887 .ok_or(FileError::Container("named-graph field overruns buffer"))
1888 };
1889 let mut out = Vec::with_capacity((n as usize).min(bytes.len()));
1890 for _ in 0..n {
1891 let (ilen, u1) = read_uvarint(bytes.get(pos..).unwrap_or(&[]))
1892 .ok_or(FileError::Container("truncated iri len"))?;
1893 pos += u1;
1894 let iend = bound(pos, ilen)?;
1895 let iri = String::from_utf8_lossy(&bytes[pos..iend]).into_owned();
1896 pos = iend;
1897 let (clen, u2) = read_uvarint(bytes.get(pos..).unwrap_or(&[]))
1898 .ok_or(FileError::Container("truncated container len"))?;
1899 pos += u2;
1900 let cend = bound(pos, clen)?;
1901 let index = decode_index_container(&bytes[pos..cend], codec, perms)?;
1902 out.push((iri, index));
1903 pos = cend;
1904 }
1905 Ok(out)
1906}
1907
1908pub fn write_dataset(
1911 dict: &Dictionary,
1912 default_index: &GraphIndex,
1913 named: &[(String, GraphIndex)],
1914 has_quads: bool,
1915 pyramid_meta: &[u8],
1916 pyramid_levels: u16,
1917) -> Vec<u8> {
1918 write_dataset_with_metadata(
1919 dict,
1920 default_index,
1921 named,
1922 has_quads,
1923 pyramid_meta,
1924 pyramid_levels,
1925 &[],
1926 &[],
1927 )
1928}
1929
1930pub(crate) fn encode_dict_container(dict: &Dictionary, codec: u8) -> Vec<u8> {
1935 let raw_sections = dict.sections();
1936 let dict_payloads: Vec<Vec<u8>> = raw_sections
1937 .iter()
1938 .map(|raw| encode_chunked_dict_section(raw, codec))
1939 .collect();
1940 encode_container(
1941 &[
1942 dict_payloads[0].as_slice(),
1943 dict_payloads[1].as_slice(),
1944 dict_payloads[2].as_slice(),
1945 dict_payloads[3].as_slice(),
1946 ],
1947 CODEC_NONE,
1948 )
1949}
1950
1951#[allow(clippy::too_many_arguments)]
1962pub fn write_dataset_with_metadata(
1963 dict: &Dictionary,
1964 default_index: &GraphIndex,
1965 named: &[(String, GraphIndex)],
1966 has_quads: bool,
1967 pyramid_meta: &[u8],
1968 pyramid_levels: u16,
1969 metadata: &[u8],
1970 text_index: &[u8],
1971) -> Vec<u8> {
1972 let codec = writer_codec();
1973 let dict_container = encode_dict_container(dict, codec);
1974 let scope = (!text_index.is_empty())
1978 .then(|| crate::text_index::TextIndex::from_section(text_index, codec).ok())
1979 .flatten()
1980 .map(|ti| ti.scope().clone());
1981 let text_index_partial = scope
1982 .as_ref()
1983 .is_some_and(crate::text_index::TextIndexScope::restricts_predicates);
1984 let text_index_named = scope
1985 .as_ref()
1986 .is_some_and(crate::text_index::TextIndexScope::covers_named_graphs);
1987 write_dataset_from_parts(
1988 &dict_container,
1989 dict.term_count() as u64,
1990 default_index,
1991 NamedPart::PerGraph(named),
1992 has_quads,
1993 dict.has_quoted_triples(),
1994 pyramid_meta,
1995 pyramid_levels,
1996 metadata,
1997 text_index,
1998 text_index_partial,
1999 text_index_named,
2000 codec,
2001 )
2002}
2003
2004pub(crate) enum NamedPart<'a> {
2007 PerGraph(&'a [(String, GraphIndex)]),
2008 Quad(&'a crate::quad::EncodedQuadIndex),
2009}
2010
2011#[allow(clippy::too_many_arguments)]
2016pub(crate) fn write_dataset_from_parts(
2017 dict_container: &[u8],
2018 term_count: u64,
2019 default_index: &GraphIndex,
2020 named: NamedPart<'_>,
2021 has_quads: bool,
2022 has_quoted_triples: bool,
2023 pyramid_meta: &[u8],
2024 pyramid_levels: u16,
2025 metadata: &[u8],
2026 text_index: &[u8],
2027 text_index_partial: bool,
2028 text_index_named: bool,
2029 codec: u8,
2030) -> Vec<u8> {
2031 let (named_section, quad, named_quads) = match named {
2035 NamedPart::PerGraph(named) => (
2036 if named.is_empty() {
2037 Vec::new()
2038 } else {
2039 encode_named_graphs(named, codec)
2040 },
2041 None,
2042 named
2043 .iter()
2044 .map(|(_, idx)| idx.triple_count() as u64)
2045 .sum::<u64>(),
2046 ),
2047 NamedPart::Quad(q) if q.graph_count == 0 => (Vec::new(), None, 0),
2048 NamedPart::Quad(q) => (Vec::new(), Some(q), q.quad_count),
2049 };
2050 let seal = quad.map(|q| crate::quad::encode_seal(crate::quad::digest(&q.bytes)));
2051 let index_container = encode_index_container_sealed(default_index, codec, seal.as_deref());
2052 let text_index_partial = text_index_partial && !text_index.is_empty();
2057 let digested;
2058 let text_index: &[u8] = if text_index_partial {
2059 digested = crate::text_index::with_partial_digest(text_index);
2060 &digested
2061 } else {
2062 text_index
2063 };
2064
2065 let meta_section_len = metadata.len() as u64;
2068 let dict_offset = HEADER_LEN as u64 + meta_section_len;
2069 let dict_len = dict_container.len() as u64;
2070 let index_offset = dict_offset + dict_len;
2071 let index_len = index_container.len() as u64;
2072 let pyr_offset = index_offset + index_len;
2073 let pyr_len = pyramid_meta.len() as u64;
2074 let text_offset = pyr_offset + pyr_len;
2076 let text_len = text_index.len() as u64;
2077 let named_offset = text_offset + text_len;
2078 let named_len = named_section.len() as u64;
2079 let quad_len = quad.map_or(0, |q| q.bytes.len() as u64);
2082
2083 let mut parts: Vec<&[u8]> = Vec::with_capacity(5);
2089 if meta_section_len > 0 {
2090 parts.push(metadata);
2091 }
2092 parts.push(dict_container);
2093 parts.push(&index_container);
2094 parts.push(pyramid_meta);
2095 if text_len > 0 && !text_index_partial {
2096 parts.push(text_index);
2097 }
2098 if named_len > 0 {
2099 parts.push(&named_section);
2100 }
2101
2102 let schema_meta_len = crate::meta::schema_block_len(pyramid_meta);
2105
2106 let header = Header {
2110 version: crate::header::CURRENT_FORMAT_VERSION,
2111 flags: FLAG_TILE_SYNOPSIS
2112 | if has_quads { FLAG_HAS_QUADS } else { 0 }
2113 | if quad.is_some() {
2114 crate::header::FLAG_QUAD_INDEX
2115 } else {
2116 0
2117 }
2118 | if has_quoted_triples {
2119 FLAG_HAS_QUOTED_TRIPLES
2120 } else {
2121 0
2122 }
2123 | if text_len > 0 && text_index_named {
2124 crate::header::FLAG_TEXT_INDEX_NAMED_GRAPHS
2125 } else {
2126 0
2127 },
2128 metadata_offset: HEADER_LEN as u64,
2129 metadata_len: meta_section_len,
2130 dictionary_offset: dict_offset,
2131 dictionary_len: dict_len,
2132 root_dir_offset: index_offset,
2133 root_dir_len: index_len,
2134 pyramid_meta_offset: if pyr_len > 0 { pyr_offset } else { 0 },
2135 pyramid_meta_len: pyr_len,
2136 dict_codec: codec,
2137 block_codec: codec,
2138 pyramid_levels,
2139 perms: default_index.perms(),
2143 quad_count: default_index.triple_count() as u64 + named_quads,
2144 term_count,
2145 content_hash: content_hash(&parts),
2146 named_graphs_offset: if named_len > 0 { named_offset } else { 0 },
2147 named_graphs_len: named_len,
2148 schema_meta_len,
2149 text_index_offset: if text_len > 0 { text_offset } else { 0 },
2150 text_index_len: text_len,
2151 text_index_partial: text_len > 0 && text_index_partial,
2152 build_info_offset: 0,
2153 build_info_len: 0,
2154 extra_sections: if quad_len > 0 {
2155 vec![crate::header::Section {
2156 kind: crate::header::SectionKind::QuadIndex,
2157 flags: 0,
2158 offset: named_offset,
2159 length: quad_len,
2160 }]
2161 } else {
2162 Vec::new()
2163 },
2164 };
2165
2166 let mut out = Vec::with_capacity(
2167 HEADER_LEN
2168 + metadata.len()
2169 + dict_container.len()
2170 + index_container.len()
2171 + pyramid_meta.len()
2172 + text_index.len()
2173 + named_section.len()
2174 + quad_len as usize
2175 + MAGIC.len(),
2176 );
2177 out.extend_from_slice(&header.to_bytes());
2178 if meta_section_len > 0 {
2179 out.extend_from_slice(metadata);
2180 }
2181 out.extend_from_slice(dict_container);
2182 out.extend_from_slice(&index_container);
2183 out.extend_from_slice(pyramid_meta);
2184 if text_len > 0 {
2185 out.extend_from_slice(text_index);
2186 }
2187 if named_len > 0 {
2188 out.extend_from_slice(&named_section);
2189 }
2190 if let Some(q) = quad {
2191 out.extend_from_slice(&q.bytes);
2192 }
2193 out.extend_from_slice(&MAGIC); out
2195}
2196
2197pub const RDF_TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
2199
2200pub fn schema_summary(rete: &Rete) -> Vec<(String, String, String, u32)> {
2207 use std::collections::{BTreeMap, HashMap};
2208 let triples = rete.dump(None);
2209
2210 let mut class_of: HashMap<&str, &str> = HashMap::new();
2211 for (s, p, o) in &triples {
2212 if p == RDF_TYPE {
2213 class_of.insert(s.as_str(), o.as_str());
2214 }
2215 }
2216 let classify = |t: &str| -> String {
2217 if let Some(c) = class_of.get(t) {
2218 (*c).to_string()
2219 } else if t.starts_with('"') {
2220 "(literal)".to_string()
2221 } else {
2222 "(untyped)".to_string()
2223 }
2224 };
2225
2226 let mut counts: BTreeMap<(String, String, String), u32> = BTreeMap::new();
2227 for (s, p, o) in &triples {
2228 if p == RDF_TYPE {
2229 continue; }
2231 *counts
2232 .entry((classify(s), p.clone(), classify(o)))
2233 .or_default() += 1;
2234 }
2235 counts
2236 .into_iter()
2237 .map(|((a, p, b), c)| (a, p, b, c))
2238 .collect()
2239}
2240
2241pub fn schema_classes(rete: &Rete) -> Vec<(String, u32)> {
2245 use std::collections::BTreeMap;
2246 let mut counts: BTreeMap<String, u32> = BTreeMap::new();
2247 for (_s, p, o) in rete.dump(None) {
2248 if p == RDF_TYPE {
2249 *counts.entry(o).or_default() += 1;
2250 }
2251 }
2252 let mut out: Vec<(String, u32)> = counts.into_iter().collect();
2253 out.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
2254 out
2255}
2256
2257pub fn read_metadata_ranged<R: RangeReader>(reader: &R) -> Result<Option<Vec<u8>>, FileError> {
2267 let head = reader.read_at(0, HEADER_LEN as u64)?;
2268 let header = Header::from_bytes(&head)?;
2269 if header.metadata_len == 0 {
2270 return Ok(None);
2271 }
2272 let bytes = reader.read_at(header.metadata_offset, header.metadata_len)?;
2273 Ok(Some(bytes))
2274}
2275
2276#[allow(clippy::type_complexity)]
2286pub fn read_card_and_build_info_ranged<R: RangeReader>(
2287 reader: &R,
2288) -> Result<(Option<Vec<u8>>, Option<Vec<u8>>), FileError> {
2289 read_card_and_build_info_with_header(reader).map(|(_, m, b)| (m, b))
2290}
2291
2292#[allow(clippy::type_complexity)]
2303pub fn read_card_and_build_info_with_header<R: RangeReader>(
2304 reader: &R,
2305) -> Result<(Header, Option<Vec<u8>>, Option<Vec<u8>>), FileError> {
2306 let head = reader.read_at(0, HEADER_LEN as u64)?;
2307 let header = Header::from_bytes(&head)?;
2308 let meta = (header.metadata_offset, header.metadata_len);
2309 let build = (header.build_info_offset, header.build_info_len);
2310 if meta.1 > 0 && build.1 > 0 && build.0 == meta.0 + meta.1 {
2311 let both = reader.read_at(meta.0, meta.1 + build.1)?;
2313 let (m, b) = both.split_at(meta.1 as usize);
2314 return Ok((header, Some(m.to_vec()), Some(b.to_vec())));
2315 }
2316 let fetch = |off: u64, len: u64| -> Result<Option<Vec<u8>>, FileError> {
2317 if len == 0 {
2318 return Ok(None);
2319 }
2320 Ok(Some(reader.read_at(off, len)?))
2321 };
2322 Ok((header, fetch(meta.0, meta.1)?, fetch(build.0, build.1)?))
2323}
2324
2325pub fn read_build_info(bytes: &[u8]) -> Result<Option<Vec<u8>>, FileError> {
2330 let header = Header::from_bytes(bytes)?;
2331 if header.build_info_len == 0 {
2332 return Ok(None);
2333 }
2334 let start = header.build_info_offset as usize;
2335 let end = start
2336 .checked_add(header.build_info_len as usize)
2337 .filter(|&e| e <= bytes.len())
2338 .ok_or(FileError::Container("build-info section overruns buffer"))?;
2339 Ok(Some(bytes[start..end].to_vec()))
2340}
2341
2342#[derive(Debug, Clone, PartialEq, Eq)]
2349pub struct BuildInfoPlan {
2350 pub header: [u8; HEADER_LEN],
2352 pub insert: u64,
2355 pub tail_start: u64,
2359 pub new_len: u64,
2361}
2362
2363pub fn plan_build_info(
2385 head: &[u8],
2386 file_len: u64,
2387 info_len: u64,
2388) -> Result<BuildInfoPlan, FileError> {
2389 let mut header = Header::from_bytes(head)?;
2390 let insert = HEADER_LEN as u64 + header.metadata_len;
2393 let old_len = header.build_info_len;
2394 if old_len > 0 && header.build_info_offset != insert {
2395 return Err(FileError::Container(
2396 "existing build-info section is not adjacent to the metadata",
2397 ));
2398 }
2399 let tail_start = insert.saturating_add(old_len);
2400 if tail_start > file_len || insert > file_len {
2401 return Err(FileError::Container("build-info splice out of bounds"));
2402 }
2403
2404 let shift = |off: &mut u64, len: u64| {
2406 if len > 0 && *off >= tail_start {
2407 *off = *off - old_len + info_len;
2408 }
2409 };
2410 shift(&mut header.dictionary_offset, header.dictionary_len);
2411 shift(&mut header.root_dir_offset, header.root_dir_len);
2412 shift(&mut header.pyramid_meta_offset, header.pyramid_meta_len);
2413 shift(&mut header.named_graphs_offset, header.named_graphs_len);
2414 shift(&mut header.text_index_offset, header.text_index_len);
2415 for s in &mut header.extra_sections {
2416 if s.length > 0 && s.offset >= tail_start {
2417 s.offset = s.offset - old_len + info_len;
2418 }
2419 }
2420 if info_len == 0 {
2421 header.build_info_offset = 0;
2422 header.build_info_len = 0;
2423 } else {
2424 header.build_info_offset = insert;
2425 header.build_info_len = info_len;
2426 }
2427
2428 Ok(BuildInfoPlan {
2429 header: header.to_bytes(),
2430 insert,
2431 tail_start,
2432 new_len: file_len - old_len + info_len,
2433 })
2434}
2435
2436pub fn attach_build_info(image: &[u8], info: &[u8]) -> Result<Vec<u8>, FileError> {
2442 let plan = plan_build_info(image, image.len() as u64, info.len() as u64)?;
2443 let mut out = Vec::with_capacity(plan.new_len as usize);
2444 out.extend_from_slice(&plan.header);
2445 out.extend_from_slice(&image[HEADER_LEN..plan.insert as usize]);
2446 out.extend_from_slice(info);
2447 out.extend_from_slice(&image[plan.tail_start as usize..]);
2448 debug_assert_eq!(out.len() as u64, plan.new_len);
2449 Ok(out)
2450}
2451
2452pub fn replace_metadata(image: &[u8], metadata: &[u8]) -> Result<Vec<u8>, FileError> {
2472 let mut header = Header::from_bytes(image)?;
2473 let insert = HEADER_LEN as u64;
2476 if header.metadata_len > 0 && header.metadata_offset != insert {
2477 return Err(FileError::Container(
2478 "metadata section does not sit immediately after the header",
2479 ));
2480 }
2481 let old_len = header.metadata_len;
2482 let tail_start = (insert + old_len) as usize;
2483 if tail_start > image.len() {
2484 return Err(FileError::Container("metadata splice out of bounds"));
2485 }
2486 let new_len = metadata.len() as u64;
2487
2488 let shift = |off: &mut u64, len: u64| {
2489 if len > 0 && *off >= insert + old_len {
2490 *off = *off - old_len + new_len;
2491 }
2492 };
2493 shift(&mut header.dictionary_offset, header.dictionary_len);
2494 shift(&mut header.root_dir_offset, header.root_dir_len);
2495 shift(&mut header.pyramid_meta_offset, header.pyramid_meta_len);
2496 shift(&mut header.named_graphs_offset, header.named_graphs_len);
2497 shift(&mut header.text_index_offset, header.text_index_len);
2498 shift(&mut header.build_info_offset, header.build_info_len);
2499 for s in &mut header.extra_sections {
2500 if s.length > 0 && s.offset >= insert + old_len {
2501 s.offset = s.offset - old_len + new_len;
2502 }
2503 }
2504 header.metadata_offset = insert;
2505 header.metadata_len = new_len;
2506
2507 let mut out = Vec::with_capacity(image.len() - old_len as usize + metadata.len());
2508 out.extend_from_slice(&header.to_bytes());
2509 out.extend_from_slice(metadata);
2510 out.extend_from_slice(&image[tail_start..]);
2511 header.content_hash = hash_of_sections(&header, &out)?;
2515 out[..HEADER_LEN].copy_from_slice(&header.to_bytes());
2516 Ok(out)
2517}
2518
2519fn hash_of_sections(header: &Header, bytes: &[u8]) -> Result<[u8; 16], FileError> {
2525 let slice = |off: u64, len: u64| -> Result<&[u8], FileError> {
2526 let end = off.saturating_add(len);
2533 bytes
2534 .get(off as usize..end as usize)
2535 .ok_or(FileError::Container("section overruns buffer"))
2536 };
2537 let parts = content_hash_parts(header)
2538 .into_iter()
2539 .map(|(off, len)| slice(off, len))
2540 .collect::<Result<Vec<&[u8]>, FileError>>()?;
2541 Ok(content_hash(&parts))
2542}
2543
2544pub fn content_hash_parts(header: &Header) -> Vec<(u64, u64)> {
2557 let mut parts: Vec<(u64, u64)> = Vec::with_capacity(6);
2558 if header.metadata_len > 0 {
2559 parts.push((header.metadata_offset, header.metadata_len));
2560 }
2561 parts.push((header.dictionary_offset, header.dictionary_len));
2562 parts.push((header.root_dir_offset, header.root_dir_len));
2563 if header.pyramid_meta_len > 0 {
2566 parts.push((header.pyramid_meta_offset, header.pyramid_meta_len));
2567 }
2568 if header.text_index_len > 0 && !header.text_index_partial {
2570 parts.push((header.text_index_offset, header.text_index_len));
2571 }
2572 if header.named_graphs_len > 0 {
2573 parts.push((header.named_graphs_offset, header.named_graphs_len));
2574 }
2575 parts
2576}
2577
2578pub fn verify(bytes: &[u8]) -> Result<bool, FileError> {
2586 let header = Header::from_bytes(bytes)?;
2587 if hash_of_sections(&header, bytes)? != header.content_hash {
2588 return Ok(false);
2589 }
2590 if header.has_quad_index() || header.quad_index_section().is_some() {
2593 check_named_layout(&header)?;
2594 let slice = |off: u64, len: u64| -> Result<&[u8], FileError> {
2595 bytes
2596 .get(off as usize..off.saturating_add(len) as usize)
2597 .ok_or(FileError::Container("section overruns buffer"))
2598 };
2599 let root = slice(header.root_dir_offset, header.root_dir_len)?;
2600 let ranges = container_section_payload_ranges(
2601 root,
2602 header.root_dir_offset,
2603 header.index_container_sections(),
2604 )?;
2605 let seal = ranges
2606 .last()
2607 .ok_or(FileError::Container("missing quad-index seal"))?;
2608 let want = crate::quad::parse_seal(slice(seal.offset, seal.len)?)?;
2609 let q = header
2610 .quad_index_section()
2611 .ok_or(FileError::Container("missing quad-index section"))?;
2612 if crate::quad::digest(slice(q.offset, q.length)?) != want {
2613 return Ok(false);
2614 }
2615 }
2616 for s in self_digested_sections(&header) {
2617 let end = s.offset.saturating_add(s.length);
2618 let section = bytes
2619 .get(s.offset as usize..end as usize)
2620 .ok_or(FileError::Container("section overruns buffer"))?;
2621 if !crate::label_index::digest_ok(section) {
2622 return Ok(false);
2623 }
2624 }
2625 Ok(true)
2626}
2627
2628fn self_digested_sections(header: &Header) -> Vec<Section> {
2632 [
2633 header.label_index_section(),
2634 header.schema_section(),
2635 header
2636 .section(crate::header::SectionKind::TextIndexPartial)
2637 .filter(|s| s.length > 0),
2638 ]
2639 .into_iter()
2640 .flatten()
2641 .collect()
2642}
2643
2644pub fn append_sections(
2652 image: &[u8],
2653 sections: &[(SectionKind, &[u8])],
2654) -> Result<Vec<u8>, FileError> {
2655 let lens: Vec<(SectionKind, u64)> =
2656 sections.iter().map(|(k, b)| (*k, b.len() as u64)).collect();
2657 let (header, body_end) = plan_append(image, image.len() as u64, &lens)?;
2658 let extra: usize = sections.iter().map(|(_, b)| b.len()).sum();
2659 let mut out = Vec::with_capacity(image.len() + extra);
2660 out.extend_from_slice(&header.to_bytes());
2661 out.extend_from_slice(&image[HEADER_LEN..body_end as usize]);
2662 for (_, b) in sections {
2663 out.extend_from_slice(b);
2664 }
2665 out.extend_from_slice(&MAGIC);
2666 Ok(out)
2667}
2668
2669fn plan_append(
2671 head: &[u8],
2672 file_len: u64,
2673 sections: &[(SectionKind, u64)],
2674) -> Result<(Header, u64), FileError> {
2675 let mut header = Header::from_bytes(head)?;
2676 if header.expected_file_len() != Some(file_len) {
2677 return Err(FileError::Container(
2678 "cannot append sections: the file length does not match its header",
2679 ));
2680 }
2681 let body_end = file_len - MAGIC.len() as u64;
2682 let mut at = body_end;
2683 for &(kind, len) in sections {
2684 if !matches!(kind, SectionKind::LabelIndex | SectionKind::Schema) {
2685 return Err(FileError::Container(
2686 "only LABEL_INDEX and SCHEMA sections can be appended",
2687 ));
2688 }
2689 if len == 0 {
2690 continue;
2691 }
2692 if header.section(kind).is_some_and(|s| s.length > 0) {
2693 return Err(FileError::Container(
2694 "the file already carries that section",
2695 ));
2696 }
2697 header = header.with_section(kind, at, len);
2698 at += len;
2699 }
2700 Ok((header, body_end))
2701}
2702
2703#[cfg(not(target_arch = "wasm32"))]
2705pub(crate) enum PayloadPart<'a> {
2706 Bytes(&'a [u8]),
2708 File(&'a std::path::Path),
2710}
2711
2712#[cfg(not(target_arch = "wasm32"))]
2717pub(crate) fn append_sections_to_file(
2718 path: &std::path::Path,
2719 sections: &[(SectionKind, Vec<PayloadPart<'_>>)],
2720) -> Result<(), FileError> {
2721 use std::io::{Read, Seek, SeekFrom, Write};
2722 let part_len = |p: &PayloadPart<'_>| -> std::io::Result<u64> {
2723 match p {
2724 PayloadPart::Bytes(b) => Ok(b.len() as u64),
2725 PayloadPart::File(f) => Ok(std::fs::metadata(f)?.len()),
2726 }
2727 };
2728 let mut lens = Vec::with_capacity(sections.len());
2729 for (kind, parts) in sections {
2730 let mut n = 0u64;
2731 for p in parts {
2732 n += part_len(p)?;
2733 }
2734 lens.push((*kind, n));
2735 }
2736 let mut f = std::fs::OpenOptions::new()
2737 .read(true)
2738 .write(true)
2739 .open(path)?;
2740 let len = f.metadata()?.len();
2741 let mut head = vec![0u8; HEADER_LEN];
2742 f.read_exact(&mut head)?;
2743 let (header, body_end) = plan_append(&head, len, &lens)?;
2744 f.seek(SeekFrom::Start(body_end))?;
2745 let mut w = std::io::BufWriter::with_capacity(1 << 20, &mut f);
2746 for (_, parts) in sections {
2747 for p in parts {
2748 match p {
2749 PayloadPart::Bytes(b) => w.write_all(b)?,
2750 PayloadPart::File(src) => {
2751 std::io::copy(&mut std::fs::File::open(src)?, &mut w)?;
2752 }
2753 }
2754 }
2755 }
2756 w.write_all(&MAGIC)?;
2757 w.flush()?;
2758 drop(w);
2759 f.seek(SeekFrom::Start(0))?;
2760 f.write_all(&header.to_bytes())?;
2761 f.flush()?;
2762 Ok(())
2763}
2764
2765#[cfg(not(target_arch = "wasm32"))]
2770pub(crate) fn open_dictionary_lazy<R: RangeReader + Send + Sync + 'static>(
2771 reader: R,
2772) -> Result<Dictionary, FileError> {
2773 let head = reader.read_at(0, HEADER_LEN as u64)?;
2774 let header = Header::from_bytes(&head)?;
2775 ranged_chunked_dictionary(&std::sync::Arc::new(reader), &header, [true; 4])
2776}
2777
2778pub fn verify_ranged(reader: &dyn crate::reader::RangeReader) -> Result<bool, FileError> {
2784 const SLICE: u64 = 8 << 20;
2787 let len = reader.len();
2788 if len < HEADER_LEN as u64 {
2789 return Err(FileError::Container("file shorter than its header"));
2790 }
2791 let header = Header::from_bytes(&reader.read_at(0, HEADER_LEN as u64)?)?;
2792 let mut h = blake3::Hasher::new();
2793 for (off, n) in content_hash_parts(&header) {
2794 if off.checked_add(n).is_none_or(|end| end > len) {
2797 return Err(FileError::Container("section overruns buffer"));
2798 }
2799 let mut at = off;
2800 while at < off + n {
2801 let take = (off + n - at).min(SLICE);
2802 h.update(&reader.read_at(at, take)?);
2803 at += take;
2804 }
2805 }
2806 let mut out = [0u8; 16];
2807 out.copy_from_slice(&h.finalize().as_bytes()[..16]);
2808 if out != header.content_hash {
2809 return Ok(false);
2810 }
2811 if header.has_quad_index() || header.quad_index_section().is_some() {
2814 check_named_layout(&header)?;
2815 let sections = header.index_container_sections();
2816 let seal = locate_container_section_ranged(
2817 reader,
2818 header.root_dir_offset,
2819 header.root_dir_len,
2820 sections - 1,
2821 sections as u64,
2822 )?;
2823 let want = crate::quad::parse_seal(&reader.read_at(seal.offset, seal.len)?)?;
2824 let q = header
2825 .quad_index_section()
2826 .ok_or(FileError::Container("missing quad-index section"))?;
2827 if q.offset.checked_add(q.length).is_none_or(|end| end > len) {
2828 return Err(FileError::Container("section overruns buffer"));
2829 }
2830 let mut h = blake3::Hasher::new();
2831 let mut at = q.offset;
2832 while at < q.offset + q.length {
2833 let take = (q.offset + q.length - at).min(SLICE);
2834 h.update(&reader.read_at(at, take)?);
2835 at += take;
2836 }
2837 let mut got = [0u8; 16];
2838 got.copy_from_slice(&h.finalize().as_bytes()[..16]);
2839 if got != want {
2840 return Ok(false);
2841 }
2842 }
2843 for s in self_digested_sections(&header) {
2845 if s.length < 16 || s.offset.checked_add(s.length).is_none_or(|end| end > len) {
2846 return Err(FileError::Container("section overruns buffer"));
2847 }
2848 let want = reader.read_at(s.offset, 16)?;
2849 let mut h = blake3::Hasher::new();
2850 let mut at = s.offset + 16;
2851 while at < s.offset + s.length {
2852 let take = (s.offset + s.length - at).min(SLICE);
2853 h.update(&reader.read_at(at, take)?);
2854 at += take;
2855 }
2856 if h.finalize().as_bytes()[..16] != want[..] {
2857 return Ok(false);
2858 }
2859 }
2860 Ok(true)
2861}
2862
2863type PyramidLoader = Box<dyn Fn() -> Option<PyramidMeta> + Send + Sync>;
2865
2866enum PyramidSlot {
2872 Resident(Option<PyramidMeta>),
2873 Lazy {
2874 loader: PyramidLoader,
2875 cell: std::sync::OnceLock<Option<PyramidMeta>>,
2876 },
2877}
2878
2879pub(crate) enum SectionSlot<T> {
2884 Resident(Option<T>),
2885 Lazy {
2886 loader: Box<dyn Fn() -> Option<T> + Send + Sync>,
2887 cell: std::sync::OnceLock<Option<T>>,
2888 },
2889}
2890
2891impl<T> SectionSlot<T> {
2892 pub(crate) fn get(&self) -> Option<&T> {
2894 match self {
2895 SectionSlot::Resident(v) => v.as_ref(),
2896 SectionSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
2897 }
2898 }
2899
2900 pub(crate) fn get_if_loaded(&self) -> Option<&T> {
2902 match self {
2903 SectionSlot::Resident(v) => v.as_ref(),
2904 SectionSlot::Lazy { cell, .. } => cell.get().and_then(|o| o.as_ref()),
2905 }
2906 }
2907}
2908
2909fn resident_label_slot<'a>(
2911 header: &Header,
2912 region: &dyn Fn(u64, u64) -> Result<&'a [u8], FileError>,
2913) -> SectionSlot<crate::label_index::LabelIndex> {
2914 SectionSlot::Resident(header.label_index_section().and_then(|s| {
2915 let bytes = region(s.offset, s.length).ok()?;
2916 crate::label_index::LabelIndex::from_section(bytes, header.block_codec).ok()
2917 }))
2918}
2919
2920fn resident_schema_slot<'a>(
2922 header: &Header,
2923 region: &dyn Fn(u64, u64) -> Result<&'a [u8], FileError>,
2924) -> SectionSlot<crate::schema_section::SchemaMeta> {
2925 SectionSlot::Resident(header.schema_section().and_then(|s| {
2926 crate::schema_section::SchemaMeta::decode(region(s.offset, s.length).ok()?).ok()
2927 }))
2928}
2929
2930fn ranged_label_slot<R: RangeReader + Send + Sync + 'static>(
2934 reader: &std::sync::Arc<R>,
2935 header: &Header,
2936) -> SectionSlot<crate::label_index::LabelIndex> {
2937 let Some(s) = header.label_index_section() else {
2938 return SectionSlot::Resident(None);
2939 };
2940 let r = reader.clone();
2941 let codec = header.block_codec;
2942 SectionSlot::Lazy {
2943 loader: Box::new(move || {
2944 let head = r
2945 .read_at(s.offset, crate::label_index::HEAD_READ.min(s.length))
2946 .ok()?;
2947 let rr = r.clone();
2948 let loader = Box::new(move |off: u64, len: u64| rr.read_at(s.offset + off, len).ok());
2949 crate::label_index::LabelIndex::from_head(&head, s.length, codec, loader).ok()
2950 }),
2951 cell: std::sync::OnceLock::new(),
2952 }
2953}
2954
2955fn ranged_schema_slot<R: RangeReader + Send + Sync + 'static>(
2957 reader: &std::sync::Arc<R>,
2958 header: &Header,
2959) -> SectionSlot<crate::schema_section::SchemaMeta> {
2960 let Some(s) = header.schema_section() else {
2961 return SectionSlot::Resident(None);
2962 };
2963 let r = reader.clone();
2964 SectionSlot::Lazy {
2965 loader: Box::new(move || {
2966 let bytes = r.read_at(s.offset, s.length).ok()?;
2967 crate::schema_section::SchemaMeta::decode(&bytes).ok()
2968 }),
2969 cell: std::sync::OnceLock::new(),
2970 }
2971}
2972
2973pub fn read_schema_ranged<R: RangeReader + ?Sized>(
2977 reader: &R,
2978) -> Result<Option<crate::schema_section::SchemaMeta>, FileError> {
2979 let head = reader.read_at(0, HEADER_LEN as u64)?;
2980 let header = Header::from_bytes(&head)?;
2981 Ok(read_schema_with_header(reader, &header))
2982}
2983
2984fn read_schema_with_header<R: RangeReader + ?Sized>(
2985 reader: &R,
2986 header: &Header,
2987) -> Option<crate::schema_section::SchemaMeta> {
2988 let s = header.schema_section()?;
2989 let bytes = reader.read_at(s.offset, s.length).ok()?;
2990 crate::schema_section::SchemaMeta::decode(&bytes).ok()
2991}
2992
2993type TextIndexLoader = Box<dyn Fn() -> Option<crate::text_index::TextIndex> + Send + Sync>;
2996
2997type TokenTableProbe = Box<dyn Fn() -> Option<u64> + Send + Sync>;
3000
3001enum TextIndexSlot {
3011 Resident {
3012 index: Option<crate::text_index::TextIndex>,
3013 token_table_len: Option<u64>,
3016 },
3017 Lazy {
3018 loader: TextIndexLoader,
3019 cell: std::sync::OnceLock<Option<crate::text_index::TextIndex>>,
3020 token_table: TokenTableProbe,
3022 token_table_cell: std::sync::OnceLock<Option<u64>>,
3023 },
3024}
3025
3026impl TextIndexSlot {
3027 fn index(&self) -> Option<&crate::text_index::TextIndex> {
3029 match self {
3030 TextIndexSlot::Resident { index, .. } => index.as_ref(),
3031 TextIndexSlot::Lazy { loader, cell, .. } => cell.get_or_init(loader).as_ref(),
3032 }
3033 }
3034
3035 fn token_table_len(&self) -> Option<u64> {
3038 match self {
3039 TextIndexSlot::Resident {
3040 token_table_len, ..
3041 } => *token_table_len,
3042 TextIndexSlot::Lazy {
3043 token_table,
3044 token_table_cell,
3045 ..
3046 } => *token_table_cell.get_or_init(token_table),
3047 }
3048 }
3049}
3050
3051fn resident_text_index_slot(section: Option<&[u8]>, codec: u8) -> Result<TextIndexSlot, FileError> {
3054 let Some(section) = section else {
3055 return Ok(TextIndexSlot::Resident {
3056 index: None,
3057 token_table_len: None,
3058 });
3059 };
3060 Ok(TextIndexSlot::Resident {
3061 index: Some(
3062 crate::text_index::TextIndex::from_section(section, codec)
3063 .map_err(|_| FileError::Container("malformed text index"))?,
3064 ),
3065 token_table_len: crate::text_index::TextIndex::postings_base(section).map(|b| b as u64),
3066 })
3067}
3068
3069pub fn read_text_index_token_table_len_ranged<R: RangeReader + ?Sized>(
3082 reader: &R,
3083 header: &Header,
3084) -> Option<u64> {
3085 let (ti_off, ti_len) = header.text_index_payload();
3086 if ti_len == 0 {
3087 return None;
3088 }
3089 read_token_table_len(reader, ti_off, ti_len)
3090}
3091
3092fn read_token_table_len<R: RangeReader + ?Sized>(reader: &R, off: u64, len: u64) -> Option<u64> {
3096 let head = reader.read_at(off, 10u64.min(len)).ok()?;
3097 let (ttlen, n) = crate::varint::read_uvarint(&head)?;
3098 Some((n as u64 + ttlen).min(len))
3099}
3100
3101const NAMED_GRAPH_RESIDENT_MAX: u64 = 1 << 20; pub const TRANSIENT_GRAPH_QUADS: u64 = 1024;
3113
3114const NAMED_SLAB: usize = 1024;
3119
3120const NAMED_WALK_CHUNK: u64 = 64 * 1024;
3124
3125const NAMED_WALK_CHUNK_MAX: u64 = 8 * 1024 * 1024;
3138
3139#[derive(Default)]
3141struct NamedEntry {
3142 meta: std::sync::OnceLock<(String, ByteRange)>,
3145 index: std::sync::OnceLock<Box<GraphIndex>>,
3148}
3149
3150#[derive(Default)]
3154struct NamedWalk {
3155 next: usize,
3157 pos: u64,
3161 buf: Vec<u8>,
3168 buf_off: u64,
3169 chunk: u64,
3176 big_seen: bool,
3183}
3184
3185struct LazyNamedGraphs {
3199 reader: std::sync::Arc<dyn RangeReader + Send + Sync>,
3200 section: ByteRange,
3202 codec: u8,
3203 has_synopsis: bool,
3204 read_concurrency: usize,
3205 perms: PermSet,
3208 #[allow(clippy::type_complexity)]
3212 dir: std::sync::OnceLock<(usize, Box<[std::sync::OnceLock<Box<[NamedEntry]>>]>)>,
3213 walk: std::sync::Mutex<NamedWalk>,
3214 failed: std::sync::atomic::AtomicBool,
3215 tile_cap: std::sync::atomic::AtomicU64,
3221}
3222
3223impl LazyNamedGraphs {
3224 fn new(
3225 reader: std::sync::Arc<dyn RangeReader + Send + Sync>,
3226 section: ByteRange,
3227 codec: u8,
3228 has_synopsis: bool,
3229 read_concurrency: usize,
3230 perms: PermSet,
3231 ) -> Self {
3232 LazyNamedGraphs {
3233 reader,
3234 section,
3235 codec,
3236 has_synopsis,
3237 read_concurrency,
3238 perms,
3239 dir: std::sync::OnceLock::new(),
3240 walk: std::sync::Mutex::new(NamedWalk::default()),
3241 failed: std::sync::atomic::AtomicBool::new(false),
3242 tile_cap: std::sync::atomic::AtomicU64::new(u64::MAX),
3243 }
3244 }
3245
3246 fn set_tile_cap(&self, cap: u64) {
3250 self.tile_cap
3251 .store(cap, std::sync::atomic::Ordering::Relaxed);
3252 self.for_each_opened(|g| {
3253 if g.is_lazy() {
3254 g.set_cache_cap(cap);
3255 }
3256 });
3257 }
3258
3259 fn fail(&self) {
3261 self.failed
3262 .store(true, std::sync::atomic::Ordering::Relaxed);
3263 }
3264
3265 #[allow(clippy::type_complexity)]
3267 fn directory(&self) -> Option<&(usize, Box<[std::sync::OnceLock<Box<[NamedEntry]>>]>)> {
3268 if let Some(d) = self.dir.get() {
3269 return Some(d);
3270 }
3271 let end = self.section.offset.checked_add(self.section.len)?;
3272 let (n, used) = match read_uvarint_at(&self.reader, self.section.offset, end) {
3273 Ok(v) => v,
3274 Err(_) => {
3275 self.fail();
3276 return None;
3277 }
3278 };
3279 if n > self.section.len / 2 {
3282 self.fail();
3283 return None;
3284 }
3285 let n = n as usize;
3286 let slabs = n.div_ceil(NAMED_SLAB);
3287 let table: Box<[std::sync::OnceLock<Box<[NamedEntry]>>]> =
3288 (0..slabs).map(|_| std::sync::OnceLock::new()).collect();
3289 let _ = self.dir.set((n, table));
3290 {
3291 let mut w = self.walk.lock().unwrap();
3294 if w.pos == 0 {
3295 w.pos = self.section.offset + used;
3296 }
3297 }
3298 self.dir.get()
3299 }
3300
3301 fn count(&self) -> usize {
3302 self.directory().map(|(n, _)| *n).unwrap_or(0)
3303 }
3304
3305 fn entry(&self, i: usize) -> Option<&NamedEntry> {
3307 let (n, table) = self.directory()?;
3308 if i >= *n {
3309 return None;
3310 }
3311 let slab = table[i / NAMED_SLAB].get_or_init(|| {
3312 let len = NAMED_SLAB.min(n - (i / NAMED_SLAB) * NAMED_SLAB);
3313 (0..len).map(|_| NamedEntry::default()).collect()
3314 });
3315 slab.get(i % NAMED_SLAB)
3316 }
3317
3318 fn next_read_len(w: &mut NamedWalk, pos: u64, end: u64, need: u64) -> u64 {
3326 let cur = w.chunk.max(NAMED_WALK_CHUNK);
3327 w.chunk = (cur * 2).min(NAMED_WALK_CHUNK_MAX);
3328 cur.max(need).min(end - pos)
3329 }
3330
3331 fn ensure_meta(&self, upto: usize, exhaustive: bool) -> Option<()> {
3346 let target = self.entry(upto)?;
3347 if target.meta.get().is_some() {
3348 return Some(());
3349 }
3350 let end = self.section.offset.checked_add(self.section.len)?;
3351 let mut w = self.walk.lock().unwrap();
3352 if exhaustive && !w.big_seen {
3353 w.chunk = NAMED_WALK_CHUNK_MAX;
3354 }
3355 while w.next <= upto {
3356 let pos = w.pos;
3357 if pos >= end {
3358 self.fail();
3360 return None;
3361 }
3362 let have =
3365 |b: &[u8], off: u64, need: u64| pos >= off && pos + need <= off + b.len() as u64;
3366 if !have(&w.buf, w.buf_off, 20.min(end - pos)) {
3367 let len = Self::next_read_len(&mut w, pos, end, 20.min(end - pos));
3368 w.buf = match self.reader.read_at(pos, len) {
3369 Ok(b) => b,
3370 Err(_) => {
3371 self.fail();
3372 return None;
3373 }
3374 };
3375 w.buf_off = pos;
3376 }
3377 let rel = (pos - w.buf_off) as usize;
3378 let Some((ilen, u1)) = read_uvarint(&w.buf[rel..]) else {
3379 self.fail();
3380 return None;
3381 };
3382 let header_need = u1 as u64 + ilen + 10; if pos + u1 as u64 + ilen > end {
3384 self.fail(); return None;
3386 }
3387 if !have(&w.buf, w.buf_off, header_need.min(end - pos)) {
3388 let len = Self::next_read_len(&mut w, pos, end, header_need);
3389 w.buf = match self.reader.read_at(pos, len) {
3390 Ok(b) => b,
3391 Err(_) => {
3392 self.fail();
3393 return None;
3394 }
3395 };
3396 w.buf_off = pos;
3397 }
3398 let rel = (pos - w.buf_off) as usize;
3399 let istart = rel + u1;
3400 let iend = istart + ilen as usize;
3401 let iri = String::from_utf8_lossy(&w.buf[istart..iend]).into_owned();
3402 let Some((clen, u2)) = read_uvarint(&w.buf[iend..]) else {
3403 self.fail();
3404 return None;
3405 };
3406 let cstart = pos + u1 as u64 + ilen + u2 as u64;
3407 let cend = match cstart.checked_add(clen) {
3408 Some(e) if e <= end => e,
3409 _ => {
3410 self.fail(); return None;
3412 }
3413 };
3414 if clen > NAMED_GRAPH_RESIDENT_MAX {
3415 w.big_seen = true;
3420 w.chunk = NAMED_WALK_CHUNK;
3421 }
3422 let range = ByteRange {
3423 offset: cstart,
3424 len: clen,
3425 };
3426 if let Some(e) = self.entry(w.next) {
3427 let _ = e.meta.set((iri, range));
3428 }
3429 w.pos = cend;
3430 w.next += 1;
3431 }
3432 Some(())
3433 }
3434
3435 fn name_at(&self, i: usize, exhaustive: bool) -> Option<&str> {
3438 self.ensure_meta(i, exhaustive)?;
3439 self.entry(i)?.meta.get().map(|(iri, _)| iri.as_str())
3440 }
3441
3442 fn graph_at(&self, i: usize, exhaustive: bool) -> Option<(&str, &GraphIndex)> {
3445 self.ensure_meta(i, exhaustive)?;
3446 let e = self.entry(i)?;
3447 let (iri, range) = e.meta.get()?;
3448 if e.index.get().is_none() {
3449 let opened = self.open_graph(*range)?;
3450 let _ = e.index.set(Box::new(opened));
3451 }
3452 Some((iri.as_str(), e.index.get()?.as_ref()))
3453 }
3454
3455 fn container_bytes(&self, range: ByteRange) -> Option<Vec<u8>> {
3459 {
3460 let w = self.walk.lock().unwrap();
3461 let buf_end = w.buf_off + w.buf.len() as u64;
3462 if range.offset >= w.buf_off && range.offset + range.len <= buf_end {
3463 let a = (range.offset - w.buf_off) as usize;
3464 return Some(w.buf[a..a + range.len as usize].to_vec());
3465 }
3466 }
3467 match self.reader.read_at(range.offset, range.len) {
3468 Ok(b) => Some(b),
3469 Err(_) => {
3470 self.fail();
3471 None
3472 }
3473 }
3474 }
3475
3476 fn open_graph(&self, range: ByteRange) -> Option<GraphIndex> {
3481 if range.len <= NAMED_GRAPH_RESIDENT_MAX {
3482 let bytes = self.container_bytes(range)?;
3483 match decode_index_container(&bytes, self.codec, self.perms) {
3484 Ok(g) => Some(g),
3485 Err(_) => {
3486 self.fail();
3487 None
3488 }
3489 }
3490 } else {
3491 match open_index_container_lazy(
3492 &self.reader,
3493 range,
3494 self.codec,
3495 self.has_synopsis,
3496 self.read_concurrency,
3497 self.perms,
3498 self.perms.len(),
3499 ) {
3500 Ok((g, _, _)) => {
3501 let cap = self.tile_cap.load(std::sync::atomic::Ordering::Relaxed);
3505 if cap != u64::MAX {
3506 g.set_cache_cap(cap);
3507 }
3508 Some(g)
3509 }
3510 Err(_) => {
3511 self.fail();
3512 None
3513 }
3514 }
3515 }
3516 }
3517
3518 fn release(&mut self, iri: &str) {
3524 let Some((_, table)) = self.dir.get_mut() else {
3525 return;
3526 };
3527 for slab in table.iter_mut() {
3528 let Some(entries) = slab.get_mut() else {
3529 continue;
3530 };
3531 for e in entries.iter_mut() {
3532 let matches = e
3533 .meta
3534 .get()
3535 .map(|(name, _)| name.as_str() == iri)
3536 .unwrap_or(false);
3537 if matches {
3538 e.index.take();
3539 return;
3540 }
3541 }
3542 }
3543 }
3544
3545 fn for_each_opened(&self, mut f: impl FnMut(&GraphIndex)) {
3547 if let Some((_, table)) = self.dir.get() {
3548 for slab in table.iter().filter_map(|s| s.get()) {
3549 for e in slab.iter() {
3550 if let Some(g) = e.index.get() {
3551 f(g);
3552 }
3553 }
3554 }
3555 }
3556 }
3557}
3558
3559enum NamedGraphsSlot {
3564 Resident(Vec<(String, GraphIndex)>),
3565 Lazy(LazyNamedGraphs),
3566 Quad(crate::quad::QuadNamed),
3569}
3570
3571impl NamedGraphsSlot {
3572 fn directory_bytes(&self) -> u64 {
3576 match self {
3577 NamedGraphsSlot::Resident(v) => v
3578 .iter()
3579 .map(|(iri, g)| iri.capacity() as u64 + g.directory_bytes())
3580 .sum(),
3581 NamedGraphsSlot::Lazy(l) => {
3582 let mut bytes = l.walk.lock().map(|w| w.buf.capacity() as u64).unwrap_or(0);
3583 if let Some((_, table)) = l.dir.get() {
3584 bytes += table.len() as u64
3585 * std::mem::size_of::<std::sync::OnceLock<Box<[NamedEntry]>>>() as u64;
3586 for slab in table.iter().filter_map(|s| s.get()) {
3587 bytes += slab.len() as u64 * std::mem::size_of::<NamedEntry>() as u64;
3588 for e in slab.iter() {
3589 if let Some((iri, _)) = e.meta.get() {
3590 bytes += iri.capacity() as u64;
3591 }
3592 if let Some(g) = e.index.get() {
3593 bytes += g.directory_bytes();
3594 }
3595 }
3596 }
3597 }
3598 bytes
3599 }
3600 NamedGraphsSlot::Quad(q) => q.directory_bytes(),
3601 }
3602 }
3603
3604 fn count(&self) -> usize {
3605 match self {
3606 NamedGraphsSlot::Resident(v) => v.len(),
3607 NamedGraphsSlot::Lazy(l) => l.count(),
3608 NamedGraphsSlot::Quad(q) => q.count(),
3609 }
3610 }
3611
3612 fn name_at<'a>(&'a self, dict: &Dictionary, i: usize, exhaustive: bool) -> Option<&'a str> {
3616 match self {
3617 NamedGraphsSlot::Resident(v) => v.get(i).map(|(iri, _)| iri.as_str()),
3618 NamedGraphsSlot::Lazy(l) => l.name_at(i, exhaustive),
3619 NamedGraphsSlot::Quad(q) => q.name_at(dict, i, exhaustive),
3620 }
3621 }
3622
3623 fn graph_at<'a>(
3624 &'a self,
3625 dict: &Dictionary,
3626 i: usize,
3627 exhaustive: bool,
3628 ) -> Option<(&'a str, &'a GraphIndex)> {
3629 match self {
3630 NamedGraphsSlot::Resident(v) => v.get(i).map(|(iri, g)| (iri.as_str(), g)),
3631 NamedGraphsSlot::Lazy(l) => l.graph_at(i, exhaustive),
3632 NamedGraphsSlot::Quad(q) => q.graph_at(dict, i, exhaustive),
3633 }
3634 }
3635
3636 fn find<'a>(&'a self, dict: &Dictionary, iri: &str) -> Option<&'a GraphIndex> {
3637 match self {
3638 NamedGraphsSlot::Quad(q) => q.find(dict, iri),
3639 NamedGraphsSlot::Resident(v) => v.iter().find(|(name, _)| name == iri).map(|(_, g)| g),
3640 NamedGraphsSlot::Lazy(l) => {
3641 for i in 0..l.count() {
3646 if l.name_at(i, false)? == iri {
3647 return l.graph_at(i, false).map(|(_, g)| g);
3648 }
3649 }
3650 None
3651 }
3652 }
3653 }
3654
3655 fn load_incomplete(&self) -> bool {
3656 match self {
3657 NamedGraphsSlot::Resident(v) => v.iter().any(|(_, g)| g.load_incomplete()),
3658 NamedGraphsSlot::Lazy(l) => {
3659 if l.failed.load(std::sync::atomic::Ordering::Relaxed) {
3660 return true;
3661 }
3662 let mut bad = false;
3663 l.for_each_opened(|g| bad |= g.load_incomplete());
3664 bad
3665 }
3666 NamedGraphsSlot::Quad(q) => q.load_incomplete(),
3667 }
3668 }
3669
3670 fn reset_load_failures(&self) {
3671 match self {
3672 NamedGraphsSlot::Resident(v) => {
3673 for (_, g) in v {
3674 g.reset_load_failure();
3675 }
3676 }
3677 NamedGraphsSlot::Lazy(l) => {
3678 l.failed.store(false, std::sync::atomic::Ordering::Relaxed);
3679 l.for_each_opened(|g| g.reset_load_failure());
3680 }
3681 NamedGraphsSlot::Quad(q) => q.reset_load_failures(),
3682 }
3683 }
3684
3685 fn resident(
3689 header: &Header,
3690 quad_section: impl FnOnce(u64, u64) -> Result<Vec<u8>, FileError>,
3691 per_graph: impl FnOnce() -> Result<Vec<(String, GraphIndex)>, FileError>,
3692 ) -> Result<Self, FileError> {
3693 check_named_layout(header)?;
3694 if let Some(q) = header.quad_index_section() {
3695 let bytes = quad_section(q.offset, q.length)?;
3696 return Ok(NamedGraphsSlot::Quad(crate::quad::QuadNamed::resident(
3697 bytes,
3698 header.block_codec,
3699 header.perms,
3700 )));
3701 }
3702 Ok(NamedGraphsSlot::Resident(if header.named_graphs_len > 0 {
3703 per_graph()?
3704 } else {
3705 Vec::new()
3706 }))
3707 }
3708}
3709
3710pub struct Rete {
3712 header: Header,
3713 dict: Dictionary,
3714 index: GraphIndex,
3715 index_section_ranges: [ByteRange; NUM_PERMS],
3716 tile_ranges: [Vec<(u32, u32, ByteRange)>; NUM_PERMS],
3720 pyramid: PyramidSlot,
3721 text_index: TextIndexSlot,
3722 labels: SectionSlot<crate::label_index::LabelIndex>,
3724 schema: SectionSlot<crate::schema_section::SchemaMeta>,
3726 named_graphs: NamedGraphsSlot,
3727 metadata: Vec<u8>,
3732 service_client: Option<Box<dyn crate::service::ServiceClient>>,
3737 service_error: std::sync::Mutex<Option<String>>,
3741 spill_threshold: std::sync::atomic::AtomicU64,
3744}
3745
3746impl Rete {
3747 pub fn open(bytes: &[u8]) -> Result<Self, FileError> {
3750 let header = Header::from_bytes(bytes)?;
3751
3752 let region = |off: u64, len: u64| -> Result<&[u8], FileError> {
3756 let start = off as usize;
3757 let end = start
3758 .checked_add(len as usize)
3759 .filter(|&e| e <= bytes.len())
3760 .ok_or(FileError::Container("section range out of bounds"))?;
3761 Ok(&bytes[start..end])
3762 };
3763
3764 let dict = decode_dictionary_container(
3765 region(header.dictionary_offset, header.dictionary_len)?,
3766 header.dict_codec,
3767 )?;
3768
3769 check_named_layout(&header)?;
3770 let index_bytes = region(header.root_dir_offset, header.root_dir_len)?;
3771 let (index, _) = decode_root_index_container(index_bytes, &header)?;
3772 let index_section_ranges = decode_index_section_ranges(
3773 index_bytes,
3774 header.root_dir_offset,
3775 header.perms,
3776 header.index_container_sections(),
3777 )?;
3778
3779 let pyramid = PyramidSlot::Resident(if header.pyramid_meta_len > 0 {
3780 Some(
3781 PyramidMeta::decode(region(header.pyramid_meta_offset, header.pyramid_meta_len)?)
3782 .map_err(|_| FileError::Container("malformed pyramid meta"))?,
3783 )
3784 } else {
3785 None
3786 });
3787
3788 let (ti_off, ti_len) = header.text_index_payload();
3791 let text_index = resident_text_index_slot(
3792 if ti_len > 0 {
3793 Some(region(ti_off, ti_len)?)
3794 } else {
3795 None
3796 },
3797 header.block_codec,
3798 )?;
3799 let labels = resident_label_slot(&header, ®ion);
3800 let schema = resident_schema_slot(&header, ®ion);
3801
3802 let named_graphs = NamedGraphsSlot::resident(
3803 &header,
3804 |off, len| Ok(region(off, len)?.to_vec()),
3805 || {
3806 decode_named_graphs(
3807 region(header.named_graphs_offset, header.named_graphs_len)?,
3808 header.block_codec,
3809 header.perms,
3810 )
3811 },
3812 )?;
3813
3814 let metadata = if header.metadata_len > 0 {
3815 region(header.metadata_offset, header.metadata_len)?.to_vec()
3816 } else {
3817 Vec::new()
3818 };
3819
3820 let tile_ranges =
3821 tile_file_ranges(index_bytes, header.root_dir_offset, &index_section_ranges);
3822 Ok(Self {
3823 header,
3824 dict,
3825 index,
3826 index_section_ranges,
3827 tile_ranges,
3828 pyramid,
3829 text_index,
3830 labels,
3831 schema,
3832 named_graphs,
3833 metadata,
3834 service_client: None,
3835 service_error: std::sync::Mutex::new(None),
3836 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
3837 })
3838 }
3839
3840 pub fn set_service_client(&mut self, client: Box<dyn crate::service::ServiceClient>) {
3845 self.service_client = Some(client);
3846 }
3847
3848 pub(crate) fn service_client(&self) -> Option<&dyn crate::service::ServiceClient> {
3849 self.service_client.as_deref()
3850 }
3851
3852 pub(crate) fn record_service_error(&self, msg: &str) {
3854 let mut e = self.service_error.lock().unwrap();
3855 if e.is_none() {
3856 *e = Some(msg.to_string());
3857 }
3858 }
3859
3860 pub(crate) fn take_service_error(&self) -> Option<String> {
3863 self.service_error.lock().unwrap().take()
3864 }
3865
3866 pub fn header(&self) -> &Header {
3867 &self.header
3868 }
3869
3870 pub fn file_layout(&self) -> Vec<LayoutSegment> {
3876 let h = &self.header;
3877 let seg = |kind: &'static str, label: String, offset: u64, len: u64| LayoutSegment {
3878 kind,
3879 label,
3880 offset,
3881 len,
3882 };
3883 let mut out = vec![seg(
3889 "header",
3890 format!("header (fixed {} bytes)", crate::header::HEADER_LEN),
3891 0,
3892 crate::header::HEADER_LEN as u64,
3893 )];
3894 if h.metadata_len > 0 {
3895 out.push(seg(
3896 "metadata",
3897 "metadata (dataset card)".into(),
3898 h.metadata_offset,
3899 h.metadata_len,
3900 ));
3901 }
3902 out.push(seg(
3903 "dictionary",
3904 "dictionary (4 front-coded term sections)".into(),
3905 h.dictionary_offset,
3906 h.dictionary_len,
3907 ));
3908 for (si, perm) in crate::index::ALL_PERMS.into_iter().enumerate() {
3909 let sec = self.index_section_ranges[si];
3910 if sec.len == 0 {
3911 continue;
3912 }
3913 let first_tile = self.tile_ranges[si]
3914 .first()
3915 .map(|&(_, _, r)| r.offset)
3916 .unwrap_or(sec.offset + sec.len);
3917 if first_tile > sec.offset {
3918 out.push(seg(
3919 "directory",
3920 format!("{} tile directory", perm.name()),
3921 sec.offset,
3922 first_tile - sec.offset,
3923 ));
3924 }
3925 for (ti, &(min_a, max_a, r)) in self.tile_ranges[si].iter().enumerate() {
3926 out.push(seg(
3927 "tile",
3928 format!("{} tile {ti} (leading ids {min_a}..{max_a})", perm.name()),
3929 r.offset,
3930 r.len,
3931 ));
3932 }
3933 }
3934 if h.pyramid_meta_len > 0 {
3935 out.push(seg(
3936 "pyramid",
3937 "pyramid summary (communities + superedges)".into(),
3938 h.pyramid_meta_offset,
3939 h.pyramid_meta_len,
3940 ));
3941 }
3942 if h.named_graphs_len > 0 {
3943 out.push(seg(
3944 "named-graphs",
3945 format!("named graphs ({})", self.named_graphs.count()),
3946 h.named_graphs_offset,
3947 h.named_graphs_len,
3948 ));
3949 }
3950 if let Some(s) = h.label_index_section() {
3951 let label = match self.labels.get_if_loaded() {
3952 Some(li) => format!("label index ({} entries)", li.entry_count()),
3953 None => "label index".to_string(),
3954 };
3955 out.push(seg("label-index", label, s.offset, s.length));
3956 }
3957 if let Some(s) = h.schema_section() {
3958 out.push(seg(
3959 "schema",
3960 "schema (classes, query_stats, shapes)".into(),
3961 s.offset,
3962 s.length,
3963 ));
3964 }
3965 if let Some(q) = h.quad_index_section() {
3966 out.push(seg(
3967 "quad-index",
3968 format!("quad index ({} named graphs)", self.named_graphs.count()),
3969 q.offset,
3970 q.length,
3971 ));
3972 }
3973 out.sort_by_key(|s| s.offset);
3974 out
3975 }
3976
3977 pub fn metadata(&self) -> Option<&[u8]> {
3982 if self.metadata.is_empty() {
3983 None
3984 } else {
3985 Some(&self.metadata)
3986 }
3987 }
3988
3989 pub fn dictionary(&self) -> &Dictionary {
3990 &self.dict
3991 }
3992
3993 pub fn pyramid(&self) -> Option<&PyramidMeta> {
3995 match &self.pyramid {
3996 PyramidSlot::Resident(p) => p.as_ref(),
3997 PyramidSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
3999 }
4000 }
4001
4002 pub fn pyramid_if_loaded(&self) -> Option<&PyramidMeta> {
4007 match &self.pyramid {
4008 PyramidSlot::Resident(p) => p.as_ref(),
4009 PyramidSlot::Lazy { cell, .. } => cell.get().and_then(|o| o.as_ref()),
4010 }
4011 }
4012
4013 pub fn schema(&self) -> Option<&crate::schema_section::SchemaMeta> {
4019 self.schema.get()
4020 }
4021
4022 pub fn schema_or_pyramid(
4026 &self,
4027 ) -> Option<std::borrow::Cow<'_, crate::schema_section::SchemaMeta>> {
4028 if let Some(s) = self.schema() {
4029 return Some(std::borrow::Cow::Borrowed(s));
4030 }
4031 self.pyramid()
4032 .map(|p| std::borrow::Cow::Owned(crate::schema_section::SchemaMeta::from_pyramid(p)))
4033 }
4034
4035 pub fn predicate_stats(&self) -> &[crate::meta::PredStat] {
4040 if let Some(s) = self.schema().filter(|s| !s.predicate_stats.is_empty()) {
4041 return &s.predicate_stats;
4042 }
4043 self.pyramid_if_loaded()
4044 .map(|p| p.predicate_stats.as_slice())
4045 .unwrap_or(&[])
4046 }
4047
4048 pub(crate) fn planner_stats(&self, fault: bool) -> &[crate::meta::PredStat] {
4053 let schema = if fault {
4054 self.schema.get()
4055 } else {
4056 self.schema.get_if_loaded()
4057 };
4058 if let Some(s) = schema.filter(|s| !s.predicate_stats.is_empty()) {
4059 return &s.predicate_stats;
4060 }
4061 self.pyramid_if_loaded()
4062 .map(|p| p.predicate_stats.as_slice())
4063 .unwrap_or(&[])
4064 }
4065
4066 pub fn char_sets(&self) -> &[crate::meta::CharSet] {
4069 if let Some(s) = self.schema().filter(|s| !s.char_sets.is_empty()) {
4070 return &s.char_sets;
4071 }
4072 self.pyramid_if_loaded()
4073 .map(|p| p.char_sets.as_slice())
4074 .unwrap_or(&[])
4075 }
4076
4077 pub fn label_index(&self) -> &[crate::meta::LabelEntry] {
4083 self.pyramid_if_loaded()
4084 .map(|p| p.label_index.as_slice())
4085 .unwrap_or(&[])
4086 }
4087
4088 pub fn has_label_index(&self) -> bool {
4090 self.header.label_index_section().is_some()
4091 }
4092
4093 pub fn label_index_section(&self) -> Option<&crate::label_index::LabelIndex> {
4095 self.labels.get()
4096 }
4097
4098 pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
4105 if let Some(li) = self.labels.get() {
4106 return li.prefix_search(prefix, limit);
4107 }
4108 let Some(pyr) = self.pyramid() else {
4109 return Vec::new();
4110 };
4111 pyr.prefix_search(prefix, limit)
4112 .into_iter()
4113 .filter_map(|e| {
4114 self.dict
4115 .subject_term(e.subject)
4116 .map(|iri| (e.label.clone(), iri))
4117 })
4118 .collect()
4119 }
4120
4121 pub(crate) fn text_index(&self) -> Option<&crate::text_index::TextIndex> {
4124 self.text_index.index()
4125 }
4126
4127 pub fn has_text_index(&self) -> bool {
4130 self.header.text_index_len > 0
4131 }
4132
4133 pub fn text_index_scope(&self) -> Option<crate::text_index::TextIndexScope> {
4137 self.text_index().map(|ti| ti.scope().clone())
4138 }
4139
4140 pub fn text_index_token_table_len(&self) -> Option<u64> {
4155 self.text_index.token_table_len()
4156 }
4157
4158 pub fn text_search(&self, words: &[&str], prefix: Option<&str>, limit: usize) -> Vec<String> {
4168 text_search_in(self.text_index(), &self.dict, words, prefix, limit)
4169 }
4170
4171 pub fn default_index(&self) -> &GraphIndex {
4173 &self.index
4174 }
4175
4176 pub fn dump(&self, graph: Option<&str>) -> Vec<TermTriple> {
4178 let index = match graph {
4179 None => &self.index,
4180 Some(g) => match self.graph_index(g) {
4181 Some(i) => i,
4182 None => return Vec::new(),
4183 },
4184 };
4185 index
4186 .match_pattern((None, None, None))
4187 .into_iter()
4188 .filter_map(|(s, p, o)| {
4189 Some((
4190 self.dict.subject_term(s)?,
4191 self.dict.predicate_term(p)?,
4192 self.dict.object_term(o)?,
4193 ))
4194 })
4195 .collect()
4196 }
4197
4198 pub fn dump_each<F: FnMut(&str, &str, &str)>(&self, graph: Option<&str>, f: F) {
4214 self.dump_filtered_each(graph, None, None, None, f)
4215 }
4216
4217 pub fn dump_filtered_each<F: FnMut(&str, &str, &str)>(
4288 &self,
4289 graph: Option<&str>,
4290 s: Option<&str>,
4291 p: Option<&str>,
4292 o: Option<&str>,
4293 mut f: F,
4294 ) {
4295 let index = match graph {
4296 None => Some(&self.index),
4297 Some(g) => self.graph_index(g),
4298 };
4299 let Some(index) = index else { return };
4300 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
4303 return;
4304 };
4305 let dict = &self.dict;
4306 const WINDOW: usize = 4096;
4351 let cap = dict.cache_cap();
4362 let prefetch_window =
4363 cap == u64::MAX || cap >= (WINDOW as u64) * 2 * (DICT_CHUNK_BUDGET as u64);
4364 const PREFETCH_BATCH: usize = 64;
4371 let prefetch_batch = if cap == u64::MAX {
4372 usize::MAX
4373 } else {
4374 PREFETCH_BATCH
4375 };
4376 let mut ids: Vec<(u32, u32, u32)> = Vec::with_capacity(WINDOW);
4377 let mut nodes: Vec<u32> = Vec::with_capacity(WINDOW * 2);
4378 let mut preds: Vec<u32> = Vec::with_capacity(WINDOW);
4379 let mut resolver = crate::dictionary::WindowResolver::default();
4380 let mut flush = |ids: &mut Vec<(u32, u32, u32)>,
4381 resolver: &mut crate::dictionary::WindowResolver,
4382 f: &mut F| {
4383 if ids.is_empty() {
4384 return;
4385 }
4386 if prefetch_window {
4390 nodes.clear();
4391 preds.clear();
4392 for &(s, p, o) in ids.iter() {
4393 nodes.push(dict.subject_node(s));
4394 nodes.push(dict.object_node(o));
4395 preds.push(p);
4396 }
4397 dict.prefetch_terms_batched(&nodes, &preds, prefetch_batch);
4398 }
4399 dict.resolve_window(ids, resolver, |s, p, o| f(s, p, o));
4400 ids.clear();
4401 };
4402 for t in index.scan_iter(pattern) {
4403 ids.push(t);
4404 if ids.len() == WINDOW {
4405 flush(&mut ids, &mut resolver, &mut f);
4406 }
4407 }
4408 flush(&mut ids, &mut resolver, &mut f);
4409 }
4410
4411 pub fn dump_plan(
4419 &self,
4420 graph: Option<&str>,
4421 s: Option<&str>,
4422 p: Option<&str>,
4423 o: Option<&str>,
4424 ) -> DumpPlan {
4425 let index = match graph {
4426 None => Some(&self.index),
4427 Some(g) => self.graph_index(g),
4428 };
4429 let scan = index
4430 .zip(self.resolve_query_pattern(s, p, o))
4431 .map(|(ix, pattern)| ix.scan_plan(pattern));
4432 DumpPlan {
4433 scan,
4434 dictionary_bytes: self.header.dictionary_len,
4435 }
4436 }
4437
4438 pub fn dump_iter(&self, graph: Option<&str>) -> impl Iterator<Item = TermTriple> + '_ {
4456 self.query_iter(graph, None, None, None)
4457 }
4458
4459 pub fn query_iter(
4470 &self,
4471 graph: Option<&str>,
4472 s: Option<&str>,
4473 p: Option<&str>,
4474 o: Option<&str>,
4475 ) -> impl Iterator<Item = TermTriple> + '_ {
4476 let index = match graph {
4477 None => Some(&self.index),
4478 Some(g) => self.graph_index(g),
4479 };
4480 index
4481 .zip(self.resolve_query_pattern(s, p, o))
4482 .into_iter()
4483 .flat_map(|(ix, pattern)| ix.scan_iter(pattern))
4484 .filter_map(move |(s, p, o)| {
4485 Some((
4486 self.dict.subject_term(s)?,
4487 self.dict.predicate_term(p)?,
4488 self.dict.object_term(o)?,
4489 ))
4490 })
4491 }
4492
4493 pub fn query_batch(
4512 &self,
4513 graph: Option<&str>,
4514 s: Option<&str>,
4515 p: Option<&str>,
4516 o: Option<&str>,
4517 cursor: u64,
4518 max_quads: usize,
4519 ) -> (Vec<TermTriple>, u64, bool) {
4520 let index = match graph {
4521 None => &self.index,
4522 Some(g) => match self.graph_index(g) {
4525 Some(i) => i,
4526 None => return (Vec::new(), cursor, true),
4527 },
4528 };
4529 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
4532 return (Vec::new(), cursor, true);
4533 };
4534 let (ids, next, done) = index.scan_batch(pattern, cursor, max_quads);
4535 if !ids.is_empty() {
4539 let mut nodes = Vec::with_capacity(ids.len() * 2);
4540 let mut preds = Vec::with_capacity(ids.len());
4541 for &(s, p, o) in &ids {
4542 nodes.push(self.dict.subject_node(s));
4543 nodes.push(self.dict.object_node(o));
4544 preds.push(p);
4545 }
4546 self.dict.prefetch_terms(&nodes, &preds);
4547 }
4548 let triples = ids
4549 .into_iter()
4550 .filter_map(|(s, p, o)| {
4551 Some((
4552 self.dict.subject_term(s)?,
4553 self.dict.predicate_term(p)?,
4554 self.dict.object_term(o)?,
4555 ))
4556 })
4557 .collect();
4558 (triples, next, done)
4559 }
4560
4561 pub fn dump_batch(
4587 &self,
4588 graph: Option<&str>,
4589 cursor: u32,
4590 max_quads: usize,
4591 ) -> (Vec<TermTriple>, u32, bool) {
4592 fn next_tile_start(tiles: &[crate::index::Tile], sid: u32) -> Option<u32> {
4597 let i = tiles.partition_point(|t| t.leading_range().1 < sid);
4598 tiles.get(i).map(|t| t.leading_range().0.max(sid))
4599 }
4600
4601 let index = match graph {
4602 None => self.default_index(),
4603 Some(g) => match self.graph_index(g) {
4606 Some(i) => i,
4607 None => return (Vec::new(), cursor, true),
4608 },
4609 };
4610 let tiles = index.tile_sections()[IndexPermutation::Spo.section_index()];
4614 let (Some(first), Some(last)) = (
4615 tiles.first().map(|t| t.leading_range().0),
4616 tiles.last().map(|t| t.leading_range().1),
4617 ) else {
4618 return (Vec::new(), cursor, true); };
4620
4621 let mut sid = cursor.max(first);
4622 let mut probes = max_quads.saturating_mul(4).max(1 << 16);
4623 let mut ids: Vec<(u32, u32, u32)> = Vec::new();
4624 let mut exhausted = false;
4625 while sid <= last && probes > 0 {
4626 probes -= 1;
4627 let hits = index.match_pattern((Some(sid), None, None));
4628 if hits.is_empty() {
4629 match index.next_leading(IndexPermutation::Spo.section_index(), sid) {
4639 Some(next) if next > sid => {
4640 sid = next;
4641 continue;
4642 }
4643 None if !index.load_incomplete() => {
4644 exhausted = true;
4645 break;
4646 }
4647 _ => {}
4648 }
4649 match next_tile_start(tiles, sid) {
4650 None => {
4651 exhausted = true;
4652 break;
4653 }
4654 Some(next) if next > sid => {
4655 sid = next;
4656 continue;
4657 }
4658 Some(_) => {}
4659 }
4660 } else {
4661 ids.extend(hits);
4662 }
4663 if sid == u32::MAX {
4664 exhausted = true;
4665 break;
4666 }
4667 sid += 1;
4668 if ids.len() >= max_quads {
4669 break;
4670 }
4671 }
4672 let done = exhausted || sid > last;
4673
4674 let dict = self.dictionary();
4681 if !ids.is_empty() {
4682 let mut nodes = Vec::with_capacity(ids.len() * 2);
4683 let mut preds = Vec::with_capacity(ids.len());
4684 for &(s, p, o) in &ids {
4685 nodes.push(dict.subject_node(s));
4686 nodes.push(dict.object_node(o));
4687 preds.push(p);
4688 }
4689 dict.prefetch_terms(&nodes, &preds);
4690 }
4691 let triples = ids
4692 .into_iter()
4693 .filter_map(|(s, p, o)| {
4694 Some((
4695 dict.subject_term(s)?,
4696 dict.predicate_term(p)?,
4697 dict.object_term(o)?,
4698 ))
4699 })
4700 .collect();
4701 (triples, sid, done)
4702 }
4703
4704 pub fn named_graph_count(&self) -> usize {
4707 self.named_graphs.count()
4708 }
4709
4710 pub fn named_graph_quad_count(&self, i: usize) -> Option<u64> {
4715 match &self.named_graphs {
4716 NamedGraphsSlot::Quad(q) => q.quad_count_at(i),
4717 _ => None,
4718 }
4719 }
4720
4721 pub fn named_graph_transient(&self, i: usize) -> Option<GraphIndex> {
4730 match &self.named_graphs {
4731 NamedGraphsSlot::Quad(q) => q.transient_view(i, TRANSIENT_GRAPH_QUADS),
4732 _ => None,
4733 }
4734 }
4735
4736 pub fn named_quad_count(&self) -> Option<u64> {
4741 match &self.named_graphs {
4742 NamedGraphsSlot::Quad(q) => Some(q.quad_count()),
4743 _ => None,
4744 }
4745 }
4746
4747 pub fn named_graph_name_at(&self, i: usize) -> Option<&str> {
4750 self.named_graphs.name_at(&self.dict, i, false)
4751 }
4752
4753 pub fn named_graph_at(&self, i: usize) -> Option<(&str, &GraphIndex)> {
4757 self.named_graphs.graph_at(&self.dict, i, false)
4758 }
4759
4760 pub(crate) fn named_graph_name_at_demand(&self, i: usize, exhaustive: bool) -> Option<&str> {
4766 self.named_graphs.name_at(&self.dict, i, exhaustive)
4767 }
4768
4769 pub(crate) fn named_graph_at_demand(
4772 &self,
4773 i: usize,
4774 exhaustive: bool,
4775 ) -> Option<(&str, &GraphIndex)> {
4776 self.named_graphs.graph_at(&self.dict, i, exhaustive)
4777 }
4778
4779 pub fn graph_names(&self) -> Vec<&str> {
4784 (0..self.named_graphs.count())
4785 .filter_map(|i| self.named_graphs.name_at(&self.dict, i, false))
4786 .collect()
4787 }
4788
4789 pub fn graph_index(&self, iri: &str) -> Option<&GraphIndex> {
4791 self.named_graphs.find(&self.dict, iri)
4792 }
4793
4794 pub fn match_ids(
4797 &self,
4798 pattern: (Option<u32>, Option<u32>, Option<u32>),
4799 ) -> Vec<(u32, u32, u32)> {
4800 self.index.match_pattern(pattern)
4801 }
4802
4803 pub fn predicate_pairs(&self, predicate: &str) -> Vec<(u32, u32)> {
4806 let pid = match self.dict.predicate_id(predicate) {
4807 Some(p) => p,
4808 None => return Vec::new(),
4809 };
4810 self.index
4811 .match_pattern((None, Some(pid), None))
4812 .into_iter()
4813 .map(|(s, _p, o)| (self.dict.subject_node(s), self.dict.object_node(o)))
4814 .collect()
4815 }
4816
4817 pub fn open_ranged<R: RangeReader>(reader: &R) -> Result<Self, FileError> {
4821 let head = reader.read_at(0, HEADER_LEN as u64)?;
4822 let header = Header::from_bytes(&head)?;
4823
4824 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
4825 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
4826
4827 check_named_layout(&header)?;
4828 let index_bytes = reader.read_at(header.root_dir_offset, header.root_dir_len)?;
4829 let (index, _) = decode_root_index_container(&index_bytes, &header)?;
4830 let index_section_ranges = decode_index_section_ranges(
4831 &index_bytes,
4832 header.root_dir_offset,
4833 header.perms,
4834 header.index_container_sections(),
4835 )?;
4836
4837 let pyramid = PyramidSlot::Resident(if header.pyramid_meta_len > 0 {
4838 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
4839 Some(
4840 PyramidMeta::decode(&mb)
4841 .map_err(|_| FileError::Container("malformed pyramid meta"))?,
4842 )
4843 } else {
4844 None
4845 });
4846
4847 let (ti_off, ti_len) = header.text_index_payload();
4850 let text_index_bytes = if ti_len > 0 {
4851 Some(reader.read_at(ti_off, ti_len)?)
4852 } else {
4853 None
4854 };
4855 let text_index = resident_text_index_slot(text_index_bytes.as_deref(), header.block_codec)?;
4856 let labels = SectionSlot::Resident(header.label_index_section().and_then(|s| {
4859 let b = reader.read_at(s.offset, s.length).ok()?;
4860 crate::label_index::LabelIndex::from_section(&b, header.block_codec).ok()
4861 }));
4862 let schema = SectionSlot::Resident(read_schema_with_header(reader, &header));
4863
4864 let named_graphs = NamedGraphsSlot::resident(
4865 &header,
4866 |off, len| Ok(reader.read_at(off, len)?),
4867 || {
4868 let nb = reader.read_at(header.named_graphs_offset, header.named_graphs_len)?;
4869 decode_named_graphs(&nb, header.block_codec, header.perms)
4870 },
4871 )?;
4872
4873 let tile_ranges =
4877 tile_file_ranges(&index_bytes, header.root_dir_offset, &index_section_ranges);
4878 Ok(Self {
4879 header,
4880 dict,
4881 index,
4882 index_section_ranges,
4883 tile_ranges,
4884 pyramid,
4885 text_index,
4886 labels,
4887 schema,
4888 named_graphs,
4889 metadata: Vec::new(),
4890 service_client: None,
4891 service_error: std::sync::Mutex::new(None),
4892 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
4893 })
4894 }
4895
4896 pub fn open_ranged_lazy<R: RangeReader + Send + Sync + 'static>(
4908 reader: R,
4909 ) -> Result<Self, FileError> {
4910 let head = reader.read_at(0, HEADER_LEN as u64)?;
4911 let header = Header::from_bytes(&head)?;
4912 check_named_layout(&header)?;
4913 let reader = std::sync::Arc::new(reader);
4914 let read_concurrency = reader.concurrency();
4917
4918 let open_dict = || ranged_chunked_dictionary(&reader, &header, [true; 4]);
4922
4923 let reader_dyn: std::sync::Arc<dyn RangeReader + Send + Sync> = reader.clone();
4927 let open_index = || {
4928 open_index_container_lazy(
4929 &reader_dyn,
4930 ByteRange {
4931 offset: header.root_dir_offset,
4932 len: header.root_dir_len,
4933 },
4934 header.block_codec,
4935 header.has_tile_synopsis(),
4936 read_concurrency,
4937 header.perms,
4938 header.index_container_sections(),
4939 )
4940 };
4941
4942 let (dict, index) = open_side_by_side(read_concurrency, open_dict, open_index);
4949 let dict = dict?;
4950 let (index, index_section_ranges, tile_ranges) = index?;
4951
4952 let pyramid = ranged_pyramid_slot(&reader, &header);
4956
4957 let text_index = ranged_text_index_slot(&reader, &header);
4962
4963 let labels = ranged_label_slot(&reader, &header);
4967 let schema = ranged_schema_slot(&reader, &header);
4968
4969 let named_graphs = if let Some(q) = header.quad_index_section() {
4976 NamedGraphsSlot::Quad(crate::quad::QuadNamed::new(
4980 reader_dyn,
4981 ByteRange {
4982 offset: q.offset,
4983 len: q.length,
4984 },
4985 header.block_codec,
4986 header.perms,
4987 read_concurrency,
4988 ))
4989 } else if header.named_graphs_len > 0 {
4990 NamedGraphsSlot::Lazy(LazyNamedGraphs::new(
4991 reader_dyn,
4992 ByteRange {
4993 offset: header.named_graphs_offset,
4994 len: header.named_graphs_len,
4995 },
4996 header.block_codec,
4997 header.has_tile_synopsis(),
4998 read_concurrency,
4999 header.perms,
5000 ))
5001 } else {
5002 NamedGraphsSlot::Resident(Vec::new())
5003 };
5004
5005 let rete = Self {
5006 header,
5007 dict,
5008 index,
5009 index_section_ranges,
5010 tile_ranges,
5011 pyramid,
5012 text_index,
5013 labels,
5014 schema,
5015 named_graphs,
5016 metadata: Vec::new(),
5017 service_client: None,
5018 service_error: std::sync::Mutex::new(None),
5019 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
5020 };
5021 #[cfg(not(target_arch = "wasm32"))]
5031 rete.set_memory_budget(Some(DEFAULT_QUERY_BUDGET_MB << 20));
5032 Ok(rete)
5033 }
5034
5035 pub fn index_incomplete(&self) -> bool {
5040 self.index.load_incomplete()
5041 || self.dict.load_incomplete()
5042 || self.named_graphs.load_incomplete()
5043 }
5044
5045 pub fn reset_load_failures(&self) {
5053 self.index.reset_load_failure();
5054 self.dict.reset_load_failure();
5055 self.named_graphs.reset_load_failures();
5056 }
5057
5058 pub fn set_memory_budget(&self, budget: Option<u64>) -> MemoryBudget {
5089 let total = budget.unwrap_or(u64::MAX);
5090 self.set_spill_threshold(budget.map(spill_threshold_for));
5091 let mut split = split_memory_budget(total);
5092 let fixed = if total == u64::MAX {
5093 0
5094 } else {
5095 self.fixed_overhead().total()
5096 };
5097 if split.dict_cache != u64::MAX {
5098 split.dict_cache = split
5099 .dict_cache
5100 .saturating_sub(fixed)
5101 .max(MIN_DICT_CACHE_CAP);
5102 }
5103 self.dict.set_cache_cap(split.dict_cache);
5104 self.index.set_cache_cap(split.tile_cache);
5105 match &self.named_graphs {
5110 NamedGraphsSlot::Lazy(lazy) => lazy.set_tile_cap(split.tile_cache),
5111 NamedGraphsSlot::Quad(q) => q.set_tile_cap(split.tile_cache),
5114 NamedGraphsSlot::Resident(_) => {}
5115 }
5116 if std::env::var("RETE_OPEN_DEBUG").is_ok() {
5117 eprintln!(
5118 "[memory-budget] total={} dict_cache={} tile_cache={} block_cache={} fixed={:.1}MiB",
5119 if total == u64::MAX {
5120 "unlimited".to_string()
5121 } else {
5122 format!("{}MiB", total >> 20)
5123 },
5124 fmt_cap(split.dict_cache),
5125 fmt_cap(split.tile_cache),
5126 fmt_cap(split.block_cache),
5127 fixed as f64 / (1u64 << 20) as f64,
5128 );
5129 }
5130 split
5131 }
5132
5133 pub fn spill_threshold(&self) -> u64 {
5140 self.spill_threshold
5141 .load(std::sync::atomic::Ordering::Relaxed)
5142 }
5143
5144 pub fn set_spill_threshold(&self, bytes: Option<u64>) {
5148 self.spill_threshold.store(
5149 bytes.unwrap_or(u64::MAX),
5150 std::sync::atomic::Ordering::Relaxed,
5151 );
5152 }
5153
5154 pub fn dict_cache_stats(&self) -> crate::chunk_cache::CacheStats {
5158 self.dict.cache_stats()
5159 }
5160
5161 pub fn fixed_overhead(&self) -> FixedOverhead {
5165 FixedOverhead {
5166 dict_directory: self.dict.directory_bytes(),
5167 index_directory: self.index.directory_bytes(),
5168 named_graphs: self.named_graphs.directory_bytes(),
5169 }
5170 }
5171
5172 pub fn index_cache_stats(&self) -> crate::chunk_cache::CacheStats {
5174 self.index.cache_stats()
5175 }
5176
5177 pub fn dict_cache_cap(&self) -> u64 {
5179 self.dict.cache_cap()
5180 }
5181
5182 pub fn release_named_graph(&mut self, iri: &str) {
5195 match &mut self.named_graphs {
5196 NamedGraphsSlot::Lazy(lazy) => lazy.release(iri),
5197 NamedGraphsSlot::Quad(q) => q.release(&self.dict, iri),
5198 NamedGraphsSlot::Resident(_) => {}
5199 }
5200 }
5201
5202 fn resolve_query_pattern(
5203 &self,
5204 s: Option<&str>,
5205 p: Option<&str>,
5206 o: Option<&str>,
5207 ) -> Option<Pattern> {
5208 let sid = match s {
5209 Some(t) => match self.dict.subject_id(t) {
5210 Some(id) => Some(id),
5211 None => return None,
5212 },
5213 None => None,
5214 };
5215 let pid = match p {
5216 Some(t) => match self.dict.predicate_id(t) {
5217 Some(id) => Some(id),
5218 None => return None,
5219 },
5220 None => None,
5221 };
5222 let oid = match o {
5223 Some(t) => match self.dict.object_id(t) {
5224 Some(id) => Some(id),
5225 None => return None,
5226 },
5227 None => None,
5228 };
5229 Some((sid, pid, oid))
5230 }
5231
5232 pub fn query_with_provenance(
5236 &self,
5237 s: Option<&str>,
5238 p: Option<&str>,
5239 o: Option<&str>,
5240 ) -> Vec<TripleProvenance> {
5241 let pattern = match self.resolve_query_pattern(s, p, o) {
5242 Some(pattern) => pattern,
5243 None => return Vec::new(),
5244 };
5245
5246 let index_permutation = GraphIndex::best_permutation_in(self.header.perms, pattern);
5247 let dictionary_range = ByteRange {
5248 offset: self.header.dictionary_offset,
5249 len: self.header.dictionary_len,
5250 };
5251 let index_range = ByteRange {
5252 offset: self.header.root_dir_offset,
5253 len: self.header.root_dir_len,
5254 };
5255 let index_section_range = self.index_section_ranges[index_permutation.section_index()];
5256 let pyramid_range = (self.header.pyramid_meta_len > 0).then_some(ByteRange {
5257 offset: self.header.pyramid_meta_offset,
5258 len: self.header.pyramid_meta_len,
5259 });
5260
5261 let tiles = &self.tile_ranges[index_permutation.section_index()];
5262 self.index
5263 .match_pattern(pattern)
5264 .into_iter()
5265 .filter_map(|(s, p, o)| {
5266 let terms = (
5267 self.dict.subject_term(s)?,
5268 self.dict.predicate_term(p)?,
5269 self.dict.object_term(o)?,
5270 );
5271 let a = index_permutation.forward((s, p, o)).0;
5274 let ti = tiles.partition_point(|&(_, max_a, _)| max_a < a);
5275 let (tile, tile_range) = match tiles.get(ti) {
5276 Some(&(min_a, _, range)) if min_a <= a => (
5277 Some(format!("{}/{ti}", index_permutation.name())),
5278 Some(range),
5279 ),
5280 _ => (None, None),
5281 };
5282 Some(TripleProvenance {
5283 terms,
5284 ids: (s, p, o),
5285 graph: None,
5286 matched_pattern: pattern,
5287 index_permutation,
5288 dictionary_range,
5289 index_range,
5290 index_section_range,
5291 pyramid_range,
5292 tile,
5293 tile_range,
5294 })
5295 })
5296 .collect()
5297 }
5298
5299 pub fn query(&self, s: Option<&str>, p: Option<&str>, o: Option<&str>) -> Vec<TermTriple> {
5303 self.query_with_provenance(s, p, o)
5304 .into_iter()
5305 .map(|m| m.terms)
5306 .collect()
5307 }
5308
5309 pub fn query_in_graph(
5317 &self,
5318 graph: Option<&str>,
5319 s: Option<&str>,
5320 p: Option<&str>,
5321 o: Option<&str>,
5322 ) -> Vec<TermTriple> {
5323 let pattern = match self.resolve_query_pattern(s, p, o) {
5324 Some(pattern) => pattern,
5325 None => return Vec::new(),
5326 };
5327 let index = match graph {
5328 None => &self.index,
5329 Some(g) => match self.graph_index(g) {
5330 Some(i) => i,
5331 None => return Vec::new(),
5332 },
5333 };
5334 self.resolve_matches(index.match_pattern(pattern))
5335 }
5336
5337 fn resolve_matches(&self, ids: Vec<Triple>) -> Vec<TermTriple> {
5340 if !ids.is_empty() {
5348 let mut nodes = Vec::with_capacity(ids.len() * 2);
5349 let mut preds = Vec::with_capacity(ids.len());
5350 for &(s, p, o) in &ids {
5351 nodes.push(self.dict.subject_node(s));
5352 nodes.push(self.dict.object_node(o));
5353 preds.push(p);
5354 }
5355 self.dict.prefetch_terms(&nodes, &preds);
5356 }
5357 ids.into_iter()
5358 .filter_map(|(s, p, o)| {
5359 Some((
5360 self.dict.subject_term(s)?,
5361 self.dict.predicate_term(p)?,
5362 self.dict.object_term(o)?,
5363 ))
5364 })
5365 .collect()
5366 }
5367
5368 pub fn query_quads(
5373 &self,
5374 s: Option<&str>,
5375 p: Option<&str>,
5376 o: Option<&str>,
5377 ) -> Vec<(TermTriple, Option<String>)> {
5378 let mut out: Vec<(TermTriple, Option<String>)> = self
5379 .query_in_graph(None, s, p, o)
5380 .into_iter()
5381 .map(|t| (t, None))
5382 .collect();
5383 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
5384 return out;
5385 };
5386 for i in 0..self.named_graphs.count() {
5387 let Some(iri) = self.named_graphs.name_at(&self.dict, i, true) else {
5390 continue;
5391 };
5392 let owned = self.named_graph_transient(i);
5395 let index = match &owned {
5396 Some(view) => view,
5397 None => match self.named_graphs.graph_at(&self.dict, i, true) {
5398 Some((_, g)) => g,
5399 None => continue,
5400 },
5401 };
5402 for triple in self.resolve_matches(index.match_pattern(pattern)) {
5403 out.push((triple, Some(iri.to_string())));
5404 }
5405 }
5406 out
5407 }
5408
5409 pub fn query_ranged<R: RangeReader>(
5416 reader: &R,
5417 s: Option<&str>,
5418 p: Option<&str>,
5419 o: Option<&str>,
5420 ) -> Result<Vec<TermTriple>, FileError> {
5421 let routed = match route_pattern(reader, s, p, o)? {
5422 Some(routed) => routed,
5423 None => return Ok(Vec::new()),
5424 };
5425 let matches = fetch_routed_matches(reader, &routed)?;
5426 Ok(matches
5427 .into_iter()
5428 .filter_map(|(s, p, o)| {
5429 Some((
5430 routed.dict.subject_term(s)?,
5431 routed.dict.predicate_term(p)?,
5432 routed.dict.object_term(o)?,
5433 ))
5434 })
5435 .collect())
5436 }
5437
5438 pub fn route_pattern_ranged<R: RangeReader>(
5442 reader: &R,
5443 s: Option<&str>,
5444 p: Option<&str>,
5445 o: Option<&str>,
5446 ) -> Result<bool, FileError> {
5447 Ok(route_pattern(reader, s, p, o)?.is_some())
5448 }
5449}
5450
5451struct RoutedPattern {
5454 dict: Dictionary,
5455 pattern: Pattern,
5456 permutation: IndexPermutation,
5457 header: Header,
5458 section: ByteRange,
5460}
5461
5462fn route_pattern<R: RangeReader>(
5465 reader: &R,
5466 s: Option<&str>,
5467 p: Option<&str>,
5468 o: Option<&str>,
5469) -> Result<Option<RoutedPattern>, FileError> {
5470 let head = reader.read_at(0, HEADER_LEN as u64)?;
5471 let header = Header::from_bytes(&head)?;
5472
5473 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
5474 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
5475
5476 let Some(pattern) = resolve_query_pattern(&dict, s, p, o) else {
5477 return Ok(None);
5478 };
5479 let permutation = GraphIndex::best_permutation_in(header.perms, pattern);
5480 let section = locate_container_section_ranged(
5481 reader,
5482 header.root_dir_offset,
5483 header.root_dir_len,
5484 header
5485 .perms
5486 .position(permutation)
5487 .ok_or(FileError::Container("routed to an absent permutation"))?,
5488 header.index_container_sections() as u64,
5489 )?;
5490 Ok(Some(RoutedPattern {
5491 dict,
5492 pattern,
5493 permutation,
5494 header,
5495 section,
5496 }))
5497}
5498
5499fn fetch_routed_matches<R: RangeReader>(
5504 reader: &R,
5505 routed: &RoutedPattern,
5506) -> Result<Vec<Triple>, FileError> {
5507 let dir = read_tile_directory_ranged(reader, routed.section)?;
5508 let [pa, _, _] = routed.permutation.order_pattern(routed.pattern);
5509 let codec = routed.header.block_codec;
5510 let mut out = Vec::new();
5511 match pa {
5512 Some(a) => {
5515 for e in dir.iter().filter(|e| e.min_a <= a && a <= e.max_a) {
5516 let bytes = reader.read_at(routed.section.offset + e.start, e.end - e.start)?;
5517 let tile = decompress(codec, &bytes)?;
5518 out.extend(GraphIndex::match_serialized_block(
5519 &tile,
5520 routed.permutation,
5521 routed.pattern,
5522 ));
5523 }
5524 }
5525 None => {
5528 if let (Some(first), Some(last)) = (dir.first(), dir.last()) {
5529 let base = first.start;
5530 let body = reader.read_at(routed.section.offset + base, last.end - base)?;
5531 for e in &dir {
5532 let tile = decompress(
5533 codec,
5534 &body[(e.start - base) as usize..(e.end - base) as usize],
5535 )?;
5536 out.extend(GraphIndex::match_serialized_block(
5537 &tile,
5538 routed.permutation,
5539 routed.pattern,
5540 ));
5541 }
5542 }
5543 }
5544 }
5545 out.sort_unstable();
5546 Ok(out)
5547}
5548
5549fn resolve_query_pattern(
5550 dict: &Dictionary,
5551 s: Option<&str>,
5552 p: Option<&str>,
5553 o: Option<&str>,
5554) -> Option<Pattern> {
5555 let sid = match s {
5556 Some(t) => Some(dict.subject_id(t)?),
5557 None => None,
5558 };
5559 let pid = match p {
5560 Some(t) => Some(dict.predicate_id(t)?),
5561 None => None,
5562 };
5563 let oid = match o {
5564 Some(t) => Some(dict.object_id(t)?),
5565 None => None,
5566 };
5567 Some((sid, pid, oid))
5568}
5569
5570fn ranged_chunked_dictionary<R: RangeReader + Send + Sync + 'static>(
5585 reader: &std::sync::Arc<R>,
5586 header: &Header,
5587 want: [bool; 4],
5588) -> Result<Dictionary, FileError> {
5589 let cache = crate::chunk_cache::ChunkCache::unlimited_arc();
5592 let mut dict_sections: Vec<crate::dict::ChunkedSection> = Vec::with_capacity(4);
5593 let mut section_ranges: [std::sync::Arc<Vec<ByteRange>>; 4] = Default::default();
5594 for si in 0..4 {
5595 if !want[si] {
5596 dict_sections.push(crate::dict::ChunkedSection::from_parts(
5597 crate::dict::SectionMeta {
5598 term_count: 0,
5599 restart_interval: 1,
5600 restart_offsets: Vec::new(),
5601 },
5602 Vec::new(),
5603 None,
5604 cache.clone(),
5605 si as u8,
5606 ));
5607 continue;
5608 }
5609 let section = locate_container_section_ranged(
5610 reader.as_ref(),
5611 header.dictionary_offset,
5612 header.dictionary_len,
5613 si,
5614 4,
5615 )?;
5616 let (meta, entries) = read_dict_dir_ranged(reader.as_ref(), section)?;
5617 let ranges: std::sync::Arc<Vec<ByteRange>> = std::sync::Arc::new(
5620 entries
5621 .iter()
5622 .map(|e| ByteRange {
5623 offset: section.offset + e.start,
5624 len: (e.end - e.start),
5625 })
5626 .collect(),
5627 );
5628 section_ranges[si] = ranges.clone();
5629 let chunks: Vec<crate::dict::SectionChunk> = entries
5630 .into_iter()
5631 .map(|e| crate::dict::SectionChunk::new(e.first_run, e.key, e.body_start))
5632 .collect();
5633 let chunk_reader = reader.clone();
5634 let codec = header.dict_codec;
5635 let loader_ranges = ranges.clone();
5636 let loader: crate::dict::ChunkLoader = Box::new(move |ci| {
5637 let range = loader_ranges.get(ci)?;
5638 let bytes = chunk_reader.read_at(range.offset, range.len).ok()?;
5639 decompress(codec, &bytes).ok()
5640 });
5641 let bulk_reader = reader.clone();
5644 let bulk: crate::dict::ChunkBulkLoader = Box::new(move |cis| {
5645 let want: Option<Vec<ByteRange>> =
5646 cis.iter().map(|&ci| ranges.get(ci).copied()).collect();
5647 let blobs = read_coalesced(bulk_reader.as_ref(), &want?, DICT_COALESCE_GAP)?;
5648 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
5649 });
5650 dict_sections.push(
5651 crate::dict::ChunkedSection::from_parts(
5652 meta,
5653 chunks,
5654 Some(loader),
5655 cache.clone(),
5656 si as u8,
5657 )
5658 .with_bulk_loader(bulk),
5659 );
5660 }
5661 let dict_arr: [crate::dict::ChunkedSection; 4] = dict_sections
5662 .try_into()
5663 .map_err(|_| FileError::Container("expected 4 dictionary sections"))?;
5664 let multi_reader = reader.clone();
5668 let codec = header.dict_codec;
5669 let multi: crate::dict::MultiChunkBulkLoader = Box::new(move |pairs| {
5670 let want: Option<Vec<ByteRange>> = pairs
5671 .iter()
5672 .map(|&(si, ci)| section_ranges.get(si as usize)?.get(ci).copied())
5673 .collect();
5674 let blobs = read_coalesced(multi_reader.as_ref(), &want?, DICT_COALESCE_GAP)?;
5675 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
5676 });
5677 Ok(Dictionary::from_chunked_sections(dict_arr).with_multi_bulk_loader(multi))
5678}
5679
5680#[cfg(not(target_arch = "wasm32"))]
5685fn open_side_by_side<A, B: Send>(
5686 concurrency: usize,
5687 a: impl FnOnce() -> A,
5688 b: impl FnOnce() -> B + Send,
5689) -> (A, B) {
5690 if concurrency <= 1 {
5691 return (a(), b());
5692 }
5693 std::thread::scope(|scope| {
5694 let hb = scope.spawn(b);
5695 let ra = a();
5696 let rb = hb
5697 .join()
5698 .unwrap_or_else(|panic| std::panic::resume_unwind(panic));
5699 (ra, rb)
5700 })
5701}
5702
5703#[cfg(target_arch = "wasm32")]
5705fn open_side_by_side<A, B>(
5706 _concurrency: usize,
5707 a: impl FnOnce() -> A,
5708 b: impl FnOnce() -> B,
5709) -> (A, B) {
5710 (a(), b())
5711}
5712
5713fn ranged_pyramid_slot<R: RangeReader + Send + Sync + 'static>(
5717 reader: &std::sync::Arc<R>,
5718 header: &Header,
5719) -> PyramidSlot {
5720 if header.pyramid_meta_len == 0 {
5721 return PyramidSlot::Resident(None);
5722 }
5723 let pyr_reader = reader.clone();
5724 let pyr_off = header.pyramid_meta_offset;
5725 let pyr_len = header.pyramid_meta_len;
5726 PyramidSlot::Lazy {
5727 loader: Box::new(move || {
5728 let mb = pyr_reader.read_at(pyr_off, pyr_len).ok()?;
5729 PyramidMeta::decode(&mb).ok()
5730 }),
5731 cell: std::sync::OnceLock::new(),
5732 }
5733}
5734
5735fn ranged_text_index_slot<R: RangeReader + Send + Sync + 'static>(
5740 reader: &std::sync::Arc<R>,
5741 header: &Header,
5742) -> TextIndexSlot {
5743 let (ti_off, ti_len) = header.text_index_payload();
5744 if ti_len == 0 {
5745 return TextIndexSlot::Resident {
5746 index: None,
5747 token_table_len: None,
5748 };
5749 }
5750 let ti_reader = reader.clone();
5751 let probe_reader = reader.clone();
5752 let codec = header.block_codec;
5753 TextIndexSlot::Lazy {
5754 loader: Box::new(move || {
5755 let prefix_len = read_token_table_len(&*ti_reader, ti_off, ti_len)?;
5759 let prefix = ti_reader.read_at(ti_off, prefix_len).ok()?;
5760 let postings_base = crate::text_index::TextIndex::postings_base(&prefix)? as u64;
5761 let postings_abs = ti_off + postings_base;
5762 let pr = ti_reader.clone();
5763 let posting_loader =
5764 Box::new(move |off: u64, len: u64| pr.read_at(postings_abs + off, len).ok());
5765 crate::text_index::TextIndex::from_token_table(&prefix, codec, posting_loader).ok()
5766 }),
5767 cell: std::sync::OnceLock::new(),
5768 token_table: Box::new(move || read_token_table_len(&*probe_reader, ti_off, ti_len)),
5771 token_table_cell: std::sync::OnceLock::new(),
5772 }
5773}
5774
5775fn text_search_in(
5778 ti: Option<&crate::text_index::TextIndex>,
5779 dict: &Dictionary,
5780 words: &[&str],
5781 prefix: Option<&str>,
5782 limit: usize,
5783) -> Vec<String> {
5784 let Some(ti) = ti else {
5785 return Vec::new();
5786 };
5787 let mut acc: Option<Vec<u32>> = None;
5791 if let Some(p) = prefix {
5792 acc = Some(ti.prefix(&p.to_lowercase()));
5793 }
5794 for w in words {
5795 for tok in crate::text_index::tokenize(w) {
5796 let posting = ti.lookup(&tok);
5797 acc = Some(match acc {
5798 Some(a) => intersect_sorted(&a, &posting),
5799 None => posting,
5800 });
5801 if acc.as_ref().is_some_and(|a| a.is_empty()) {
5802 return Vec::new();
5803 }
5804 }
5805 }
5806 let ids = acc.unwrap_or_default();
5807 let mut out = Vec::with_capacity(if limit > 0 {
5808 limit.min(ids.len())
5809 } else {
5810 ids.len()
5811 });
5812 let page = if limit > 0 {
5816 limit.min(ids.len())
5817 } else {
5818 ids.len()
5819 };
5820 dict.prefetch_subject_terms(&ids[..page]);
5821 for id in ids {
5822 if let Some(iri) = dict.subject_term(id) {
5823 out.push(iri);
5824 if limit > 0 && out.len() >= limit {
5825 break;
5826 }
5827 }
5828 }
5829 out
5830}
5831
5832pub struct SearchView {
5855 header: Header,
5856 dict: SectionSlot<Dictionary>,
5857 pyramid: PyramidSlot,
5858 text_index: TextIndexSlot,
5859 labels: SectionSlot<crate::label_index::LabelIndex>,
5860}
5861
5862impl SearchView {
5863 pub fn open_ranged<R: RangeReader + Send + Sync + 'static>(
5868 reader: R,
5869 ) -> Result<Self, FileError> {
5870 let head = reader.read_at(0, HEADER_LEN as u64)?;
5871 let header = Header::from_bytes(&head)?;
5872 let reader = std::sync::Arc::new(reader);
5873 let dict = if header.label_index_section().is_some() {
5877 let (r, h) = (reader.clone(), header.clone());
5878 SectionSlot::Lazy {
5879 loader: Box::new(move || {
5880 ranged_chunked_dictionary(&r, &h, [true, true, false, false]).ok()
5881 }),
5882 cell: std::sync::OnceLock::new(),
5883 }
5884 } else {
5885 SectionSlot::Resident(Some(ranged_chunked_dictionary(
5886 &reader,
5887 &header,
5888 [true, true, false, false],
5889 )?))
5890 };
5891 let pyramid = ranged_pyramid_slot(&reader, &header);
5892 let text_index = ranged_text_index_slot(&reader, &header);
5893 let labels = ranged_label_slot(&reader, &header);
5894 Ok(Self {
5895 header,
5896 dict,
5897 pyramid,
5898 text_index,
5899 labels,
5900 })
5901 }
5902
5903 pub fn has_labels(&self) -> bool {
5907 self.header.label_index_section().is_some() || self.header.pyramid_meta_len > 0
5908 }
5909
5910 pub fn has_label_index(&self) -> bool {
5912 self.header.label_index_section().is_some()
5913 }
5914
5915 pub fn header(&self) -> &Header {
5917 &self.header
5918 }
5919
5920 pub fn has_text_index(&self) -> bool {
5922 self.header.text_index_len > 0
5923 }
5924
5925 pub fn has_pyramid(&self) -> bool {
5928 self.header.pyramid_meta_len > 0
5929 }
5930
5931 pub fn text_search(&self, words: &[&str], prefix: Option<&str>, limit: usize) -> Vec<String> {
5934 let Some(dict) = self.dict.get() else {
5935 return Vec::new();
5936 };
5937 text_search_in(self.text_index.index(), dict, words, prefix, limit)
5938 }
5939
5940 pub fn text_index_token_table_len(&self) -> Option<u64> {
5944 self.text_index.token_table_len()
5945 }
5946
5947 pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
5951 if let Some(li) = self.labels.get() {
5952 return li.prefix_search(prefix, limit);
5953 }
5954 let pyr = match &self.pyramid {
5955 PyramidSlot::Resident(p) => p.as_ref(),
5956 PyramidSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
5957 };
5958 let (Some(pyr), Some(dict)) = (pyr, self.dict.get()) else {
5959 return Vec::new();
5960 };
5961 pyr.prefix_search(prefix, limit)
5962 .into_iter()
5963 .filter_map(|e| {
5964 dict.subject_term(e.subject)
5965 .map(|iri| (e.label.clone(), iri))
5966 })
5967 .collect()
5968 }
5969}
5970
5971#[must_use]
5976pub struct SummaryView {
5977 pub round: u32,
5978 pub summary: Vec<SuperEdge>,
5979 pub class_hierarchy: Vec<ClassNode>,
5981 pub level_rollups: Vec<LevelRollup>,
5983 pub level_links: Vec<LevelLinks>,
5985 pub descriptors: Vec<CommunityDescriptor>,
5987 pub subclass_cycles: Vec<Vec<String>>,
5989 pub disjoint_pairs: Vec<(String, String)>,
5991 pub equivalent_pairs: Vec<(String, String)>,
5993 dict: Dictionary,
5994}
5995
5996impl SummaryView {
5997 pub fn open_ranged<R: RangeReader>(reader: &R) -> Result<Option<Self>, FileError> {
5999 let head = reader.read_at(0, HEADER_LEN as u64)?;
6000 let header = Header::from_bytes(&head)?;
6001 if header.pyramid_meta_len == 0 {
6002 return Ok(None);
6003 }
6004
6005 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
6006 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
6007
6008 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
6009 let mut meta =
6010 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
6011 if let Some(s) = read_schema_with_header(reader, &header) {
6014 meta.class_hierarchy = s.class_hierarchy;
6015 meta.level_rollups = s.level_rollups;
6016 meta.level_links = s.level_links;
6017 meta.subclass_cycles = s.subclass_cycles;
6018 meta.disjoint_pairs = s.disjoint_pairs;
6019 meta.equivalent_pairs = s.equivalent_pairs;
6020 }
6021
6022 Ok(Some(SummaryView {
6023 round: meta.round,
6024 summary: meta.summary,
6025 class_hierarchy: meta.class_hierarchy,
6026 level_rollups: meta.level_rollups,
6027 level_links: meta.level_links,
6028 descriptors: meta.descriptors,
6029 subclass_cycles: meta.subclass_cycles,
6030 disjoint_pairs: meta.disjoint_pairs,
6031 equivalent_pairs: meta.equivalent_pairs,
6032 dict,
6033 }))
6034 }
6035
6036 pub fn open_schema_ranged<R: RangeReader>(reader: &R) -> Result<Option<Self>, FileError> {
6044 let head = reader.read_at(0, HEADER_LEN as u64)?;
6045 let header = Header::from_bytes(&head)?;
6046 let Some(s) = read_schema_with_header(reader, &header) else {
6047 return Ok(None);
6048 };
6049 let dict = crate::DictionaryBuilder::new().build();
6050 Ok(Some(SummaryView {
6051 round: 0,
6052 summary: Vec::new(),
6053 class_hierarchy: s.class_hierarchy,
6054 level_rollups: s.level_rollups,
6055 level_links: s.level_links,
6056 descriptors: Vec::new(),
6057 subclass_cycles: s.subclass_cycles,
6058 disjoint_pairs: s.disjoint_pairs,
6059 equivalent_pairs: s.equivalent_pairs,
6060 dict,
6061 }))
6062 }
6063
6064 pub fn level_count(&self) -> usize {
6066 self.level_rollups.len()
6067 }
6068
6069 pub fn level_rollup(&self, k: usize) -> Option<&LevelRollup> {
6072 self.level_rollups.get(k)
6073 }
6074
6075 pub fn predicate_term(&self, id: u32) -> Option<String> {
6077 self.dict.predicate_term(id)
6078 }
6079
6080 pub fn predicate_total(&self, predicate: &str) -> u32 {
6083 match self.dict.predicate_id(predicate) {
6084 Some(pid) => self
6085 .summary
6086 .iter()
6087 .filter(|e| e.predicate == pid)
6088 .map(|e| e.count)
6089 .sum(),
6090 None => 0,
6091 }
6092 }
6093
6094 pub fn predicate_totals(&self) -> Vec<(String, u32)> {
6096 let mut by_pred: std::collections::BTreeMap<u32, u32> = std::collections::BTreeMap::new();
6097 for e in &self.summary {
6098 *by_pred.entry(e.predicate).or_default() += e.count;
6099 }
6100 let mut out: Vec<(String, u32)> = by_pred
6101 .into_iter()
6102 .filter_map(|(pid, c)| self.dict.predicate_term(pid).map(|t| (t, c)))
6103 .collect();
6104 out.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
6105 out
6106 }
6107
6108 pub fn community_count(&self) -> usize {
6110 let mut comms = std::collections::BTreeSet::new();
6111 for e in &self.summary {
6112 comms.insert(e.s_comm);
6113 comms.insert(e.o_comm);
6114 }
6115 comms.len()
6116 }
6117
6118 pub fn tbox_coherence(&self) -> Vec<crate::reason::Inconsistency> {
6133 schema_coherence(
6134 &self.class_hierarchy,
6135 &self.subclass_cycles,
6136 &self.disjoint_pairs,
6137 &self.equivalent_pairs,
6138 )
6139 }
6140
6141 pub fn tbox_is_coherent(&self) -> bool {
6144 self.tbox_coherence().is_empty()
6145 }
6146}
6147
6148pub fn schema_coherence(
6154 class_hierarchy: &[ClassNode],
6155 subclass_cycles: &[Vec<String>],
6156 disjoint_pairs: &[(String, String)],
6157 equivalent_pairs: &[(String, String)],
6158) -> Vec<crate::reason::Inconsistency> {
6159 use crate::reason::Inconsistency;
6160 use std::collections::{BTreeMap, BTreeSet, VecDeque};
6161 const MAX_REACH: usize = 100_000;
6162
6163 let mut out: Vec<Inconsistency> = Vec::new();
6164
6165 for cyc in subclass_cycles {
6166 let detail = if cyc.len() == 1 {
6167 format!("{} is rdfs:subClassOf itself (a cycle)", cyc[0])
6168 } else {
6169 format!(
6170 "classes {{{}}} are mutually rdfs:subClassOf (a cycle)",
6171 cyc.join(", ")
6172 )
6173 };
6174 out.push(Inconsistency {
6175 kind: "subclass-cycle",
6176 detail,
6177 });
6178 }
6179
6180 if !disjoint_pairs.is_empty() {
6181 let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
6183 for n in class_hierarchy {
6184 let e = adj.entry(n.class.as_str()).or_default();
6185 for p in &n.parents {
6186 e.push(p.as_str());
6187 }
6188 }
6189 for (a, b) in equivalent_pairs {
6190 adj.entry(a.as_str()).or_default().push(b.as_str());
6191 adj.entry(b.as_str()).or_default().push(a.as_str());
6192 }
6193
6194 let mut focuses: BTreeSet<&str> =
6196 class_hierarchy.iter().map(|n| n.class.as_str()).collect();
6197 for (a, b) in disjoint_pairs.iter().chain(equivalent_pairs) {
6198 focuses.insert(a.as_str());
6199 focuses.insert(b.as_str());
6200 }
6201
6202 let mut seen: BTreeSet<&str> = BTreeSet::new();
6203 for &c in &focuses {
6204 let mut reach: BTreeSet<&str> = BTreeSet::new();
6206 let mut q: VecDeque<&str> = VecDeque::new();
6207 reach.insert(c);
6208 q.push_back(c);
6209 while let Some(x) = q.pop_front() {
6210 if reach.len() > MAX_REACH {
6211 break;
6212 }
6213 if let Some(ns) = adj.get(x) {
6214 for &p in ns {
6215 if reach.insert(p) {
6216 q.push_back(p);
6217 }
6218 }
6219 }
6220 }
6221 for (x, y) in disjoint_pairs {
6222 if reach.contains(x.as_str()) && reach.contains(y.as_str()) && seen.insert(c) {
6223 out.push(Inconsistency {
6224 kind: "unsatisfiable-class",
6225 detail: format!(
6226 "{c} is a subclass of both {x} and {y}, which are \
6227 owl:disjointWith — no individual can be a {c}"
6228 ),
6229 });
6230 break;
6231 }
6232 }
6233 }
6234 }
6235
6236 out.sort_by(|a, b| (a.kind, &a.detail).cmp(&(b.kind, &b.detail)));
6237 out
6238}
6239
6240pub fn read_schema_coherence_ranged<R: RangeReader>(
6248 reader: &R,
6249) -> Result<Option<Vec<crate::reason::Inconsistency>>, FileError> {
6250 let head = reader.read_at(0, HEADER_LEN as u64)?;
6251 let header = Header::from_bytes(&head)?;
6252 if let Some(s) = read_schema_with_header(reader, &header) {
6254 return Ok(Some(schema_coherence(
6255 &s.class_hierarchy,
6256 &s.subclass_cycles,
6257 &s.disjoint_pairs,
6258 &s.equivalent_pairs,
6259 )));
6260 }
6261 if header.pyramid_meta_len == 0 {
6262 return Ok(None);
6263 }
6264 if header.schema_meta_len > 0 && (header.schema_meta_len as u64) <= header.pyramid_meta_len {
6268 let off =
6269 header.pyramid_meta_offset + header.pyramid_meta_len - header.schema_meta_len as u64;
6270 let block = reader.read_at(off, header.schema_meta_len as u64)?;
6271 let (hierarchy, cycles, disjoint, equivalent) = crate::meta::decode_schema_block(&block)
6272 .map_err(|_| FileError::Container("malformed schema block"))?;
6273 return Ok(Some(schema_coherence(
6274 &hierarchy,
6275 &cycles,
6276 &disjoint,
6277 &equivalent,
6278 )));
6279 }
6280 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
6282 let meta =
6283 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
6284 Ok(Some(schema_coherence(
6285 &meta.class_hierarchy,
6286 &meta.subclass_cycles,
6287 &meta.disjoint_pairs,
6288 &meta.equivalent_pairs,
6289 )))
6290}
6291
6292#[allow(clippy::type_complexity)]
6300pub fn read_schema_summary_ranged<R: RangeReader>(
6301 reader: &R,
6302) -> Result<Option<(Vec<(String, u64)>, Vec<(String, String, String, u64)>)>, FileError> {
6303 let head = reader.read_at(0, HEADER_LEN as u64)?;
6304 let header = Header::from_bytes(&head)?;
6305 let meta = if let Some(s) = read_schema_with_header(reader, &header) {
6307 PyramidMeta::new(0, Vec::new(), &[]).with_schema(
6308 Vec::new(),
6309 s.level_rollups,
6310 s.level_links,
6311 Vec::new(),
6312 Vec::new(),
6313 Vec::new(),
6314 Vec::new(),
6315 )
6316 } else {
6317 if header.pyramid_meta_len == 0 {
6318 return Ok(None);
6319 }
6320 if header.schema_meta_len > 0 && (header.schema_meta_len as u64) <= header.pyramid_meta_len
6321 {
6322 let off = header.pyramid_meta_offset + header.pyramid_meta_len
6323 - header.schema_meta_len as u64;
6324 let block = reader.read_at(off, header.schema_meta_len as u64)?;
6325 let summary = crate::meta::decode_schema_block_summary(&block)
6326 .map_err(|_| FileError::Container("malformed schema block"))?;
6327 return Ok(Some(summary));
6328 }
6329 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
6331 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?
6332 };
6333 if meta.level_rollups.is_empty() && meta.level_links.is_empty() {
6334 return Ok(None);
6335 }
6336 let classes = meta
6337 .level_rollups
6338 .iter()
6339 .max_by_key(|r| r.depth)
6340 .map(|r| r.classes.clone())
6341 .unwrap_or_default();
6342 let relations = meta
6343 .level_links
6344 .iter()
6345 .max_by_key(|l| l.depth)
6346 .map(|l| {
6347 l.links
6348 .iter()
6349 .map(|c| {
6350 (
6351 c.s_class.clone(),
6352 c.predicate.clone(),
6353 c.o_class.clone(),
6354 c.count,
6355 )
6356 })
6357 .collect()
6358 })
6359 .unwrap_or_default();
6360 Ok(Some((classes, relations)))
6361}
6362
6363#[cfg(test)]
6368pub(crate) fn dict_chunk_keys_for_test(image: &[u8]) -> Vec<Vec<Vec<u8>>> {
6369 let header = Header::from_bytes(&image[..HEADER_LEN]).unwrap();
6370 let s = header.dictionary_offset as usize;
6371 let e = s + header.dictionary_len as usize;
6372 decode_container(&image[s..e], CODEC_NONE)
6373 .unwrap()
6374 .iter()
6375 .map(|payload| {
6376 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
6377 entries.into_iter().map(|c| c.key).collect()
6378 })
6379 .collect()
6380}
6381
6382#[cfg(test)]
6383mod tests {
6384 use super::*;
6385 use crate::dictionary::DictionaryBuilder;
6386 use crate::index::GraphIndexBuilder;
6387
6388 #[cfg(not(feature = "compression"))]
6391 #[test]
6392 fn ruzstd_decodes_a_zstd_frame() {
6393 let data: Vec<u8> = (0..50_000u32)
6394 .flat_map(|i| (i % 251).to_le_bytes())
6395 .collect();
6396 let frame = ruzstd::encoding::compress_to_vec(
6397 &data[..],
6398 ruzstd::encoding::CompressionLevel::Fastest,
6399 );
6400 assert!(frame.len() < data.len());
6401 assert_eq!(decompress(CODEC_ZSTD, &frame).unwrap(), data);
6402 }
6403
6404 #[test]
6410 fn three_permutation_file_round_trips_every_open_path() {
6411 use crate::index::PermSet;
6412 use crate::reader::SliceReader;
6413
6414 let build = |perms: PermSet| {
6415 let mut db = crate::DictionaryBuilder::new();
6416 let mut triples = Vec::new();
6417 for i in 0..60u32 {
6418 let (s, p, o) = (
6419 format!("<http://ex/s{}>", i % 11),
6420 format!("<http://ex/p{}>", i % 3),
6421 format!("<http://ex/o{}>", i % 7),
6422 );
6423 db.observe(&s, &p, &o);
6424 triples.push((s, p, o));
6425 }
6426 let dict = db.build();
6427 let ids: Vec<(u32, u32, u32)> = triples
6428 .iter()
6429 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
6430 .collect();
6431 let def = GraphIndexBuilder::from_triples(ids.clone())
6432 .with_tile_budget(96)
6433 .with_perms(perms)
6434 .build();
6435 let named = GraphIndexBuilder::from_triples(ids)
6436 .with_tile_budget(96)
6437 .with_perms(perms)
6438 .build();
6439 write_dataset(
6440 &dict,
6441 &def,
6442 &[("<http://ex/g>".to_string(), named)],
6443 true,
6444 &[],
6445 0,
6446 )
6447 };
6448
6449 let six = build(PermSet::ALL);
6450 let three = build(PermSet::CORE);
6451 assert!(three.len() < six.len());
6452 assert_eq!(Header::from_bytes(&three).unwrap().perms, PermSet::CORE);
6453 assert_eq!(Header::from_bytes(&six).unwrap().perms, PermSet::ALL);
6454
6455 let shapes: Vec<(Option<&str>, Option<&str>, Option<&str>)> = vec![
6456 (None, None, None),
6457 (Some("<http://ex/s3>"), None, None),
6458 (None, Some("<http://ex/p1>"), None),
6459 (None, None, Some("<http://ex/o5>")),
6460 (Some("<http://ex/s3>"), Some("<http://ex/p1>"), None),
6461 (Some("<http://ex/s3>"), None, Some("<http://ex/o5>")),
6462 (None, Some("<http://ex/p1>"), Some("<http://ex/o5>")),
6463 (
6464 Some("<http://ex/s3>"),
6465 Some("<http://ex/p1>"),
6466 Some("<http://ex/o5>"),
6467 ),
6468 ];
6469
6470 for (image, tag) in [(&six, "six"), (&three, "three")] {
6471 let resident = Rete::open(image).unwrap();
6472 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
6473 let ranged = Rete::open_ranged(&SliceReader::new(leaked)).unwrap();
6474 let lazy = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
6475 for (s, p, o) in &shapes {
6476 let want: Vec<_> = {
6477 let mut v = Rete::open(&six).unwrap().query(*s, *p, *o);
6478 v.sort();
6479 v
6480 };
6481 for (got, path) in [
6482 (resident.query(*s, *p, *o), "resident"),
6483 (ranged.query(*s, *p, *o), "ranged"),
6484 (lazy.query(*s, *p, *o), "lazy"),
6485 ] {
6486 let mut got = got;
6487 got.sort();
6488 assert_eq!(got, want, "{tag}/{path} disagreed on {:?}", (s, p, o));
6489 }
6490 let mut g = resident.query_in_graph(Some("<http://ex/g>"), *s, *p, *o);
6492 g.sort();
6493 assert_eq!(g, want, "{tag}/named disagreed on {:?}", (s, p, o));
6494 }
6495 }
6496
6497 let leaked: &'static [u8] = Box::leak(three.clone().into_boxed_slice());
6501 let reader = SliceReader::new(leaked);
6502 let routed = Rete::query_ranged(&reader, None, None, Some("<http://ex/o5>")).unwrap();
6503 let mut want = Rete::open(&six)
6504 .unwrap()
6505 .query(None, None, Some("<http://ex/o5>"));
6506 want.sort();
6507 let mut got = routed;
6508 got.sort();
6509 assert_eq!(got, want, "routed single-pattern read on a lean file");
6510 }
6511
6512 #[test]
6513 fn read_coalesced_merges_within_gap_and_splits_beyond() {
6514 use crate::reader::{CountingReader, SliceReader};
6515 let bytes = vec![0u8; 4096];
6516 let ranges = [
6518 ByteRange { offset: 0, len: 16 },
6519 ByteRange {
6520 offset: 48,
6521 len: 16,
6522 },
6523 ByteRange {
6524 offset: 1088,
6525 len: 16,
6526 },
6527 ];
6528 let r = CountingReader::new(SliceReader::new(&bytes));
6530 let out = read_coalesced(&r, &ranges, 16).unwrap();
6531 assert_eq!(out.len(), 3);
6532 assert_eq!(r.requests(), 3);
6533 let r = CountingReader::new(SliceReader::new(&bytes));
6535 read_coalesced(&r, &ranges, 64).unwrap();
6536 assert_eq!(r.requests(), 2);
6537 let r = CountingReader::new(SliceReader::new(&bytes));
6539 read_coalesced(&r, &ranges, 4096).unwrap();
6540 assert_eq!(r.requests(), 1);
6541 }
6542
6543 fn build_image() -> Vec<u8> {
6544 let triples = [
6545 ("Alice", "knows", "Bob"),
6546 ("Bob", "knows", "Carol"),
6547 ("Alice", "age", "30"),
6548 ];
6549 let mut db = DictionaryBuilder::new();
6550 for (s, p, o) in triples {
6551 db.observe(s, p, o);
6552 }
6553 let dict = db.build();
6554
6555 let mut ib = GraphIndexBuilder::new();
6556 for (s, p, o) in triples {
6557 ib.push(dict.encode(s, p, o).unwrap());
6558 }
6559 let index = ib.build();
6560
6561 let (meta, levels) = build_pyramid_meta(&dict, &triples_ids(&dict), DEFAULT_TILE_BUDGET);
6562 write_file(&dict, &index, false, &meta, levels)
6563 }
6564
6565 fn triples_ids(dict: &Dictionary) -> Vec<(u32, u32, u32)> {
6566 [
6567 ("Alice", "knows", "Bob"),
6568 ("Bob", "knows", "Carol"),
6569 ("Alice", "age", "30"),
6570 ]
6571 .iter()
6572 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
6573 .collect()
6574 }
6575
6576 #[test]
6577 fn file_round_trips_header_and_counts() {
6578 let bytes = build_image();
6579 let rete = Rete::open(&bytes).unwrap();
6580 assert_eq!(rete.header().quad_count, 3);
6581 assert!(rete.header().term_count >= 5);
6582 let expected_codec = writer_codec();
6583 assert_eq!(rete.header().dict_codec, expected_codec);
6584 assert_eq!(rete.header().block_codec, expected_codec);
6585 assert_eq!(&bytes[bytes.len() - 4..], &MAGIC); }
6587
6588 #[test]
6593 fn multi_tile_file_round_trips_and_routes() {
6594 let triples: Vec<(String, String, String)> = (0..200)
6595 .map(|i| {
6596 (
6597 format!("<http://ex/s/{i}>"),
6598 format!("<http://ex/p/{}>", i % 5),
6599 format!("<http://ex/o/{}>", i % 23),
6600 )
6601 })
6602 .collect();
6603 let mut db = DictionaryBuilder::new();
6604 for (s, p, o) in &triples {
6605 db.observe(s, p, o);
6606 }
6607 let dict = db.build();
6608 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6609 for (s, p, o) in &triples {
6610 ib.push(dict.encode(s, p, o).unwrap());
6611 }
6612 let index = ib.build();
6613 assert!(
6614 index.tile_sections()[0].len() > 3,
6615 "tiny budget must force many tiles"
6616 );
6617 let bytes = write_file(&dict, &index, false, &[], 0);
6618
6619 let rete = Rete::open(&bytes).unwrap();
6620 assert_eq!(rete.header().version, crate::header::CURRENT_FORMAT_VERSION);
6621 assert_eq!(rete.query(None, None, None).len(), 200);
6622 assert_eq!(rete.query(Some("<http://ex/s/7>"), None, None).len(), 1);
6623 assert_eq!(
6624 rete.query(None, Some("<http://ex/p/3>"), None).len(),
6625 40,
6626 "predicate extent spans tiles"
6627 );
6628 assert_eq!(
6629 rete.query(None, None, Some("<http://ex/o/22>")).len(),
6630 8 );
6632
6633 use crate::reader::SliceReader;
6635 let reader = SliceReader::new(&bytes);
6636 let routed = Rete::query_ranged(&reader, Some("<http://ex/s/7>"), None, None).unwrap();
6637 assert_eq!(routed.len(), 1);
6638 let routed = Rete::query_ranged(&reader, None, Some("<http://ex/p/3>"), None).unwrap();
6639 assert_eq!(routed.len(), 40);
6640 let routed = Rete::query_ranged(&reader, None, None, Some("<http://ex/o/22>")).unwrap();
6641 assert_eq!(routed.len(), 8);
6642 }
6643
6644 #[test]
6653 fn tile_directory_offsets_survive_past_4gib() {
6654 let mut dir = Vec::new();
6655 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);
6661 write_uvarint(&mut dir, 2 << 30); let total = dir.len() as u64 + (3u64 << 30) + (2u64 << 30) + 64;
6663 let entries = parse_tile_directory(&dir, total).unwrap();
6664 assert_eq!(entries.len(), 2);
6665 assert_eq!(entries[1].start, dir.len() as u64 + (3u64 << 30));
6666 assert!(
6667 entries[1].end > u32::MAX as u64,
6668 "tail tile sits past 4 GiB"
6669 );
6670 assert!(parse_tile_directory(&dir, 1 << 20).is_err());
6672 }
6673
6674 #[test]
6675 fn tile_synopsis_trailer_round_trips() {
6676 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6677 for i in 0..200u32 {
6678 ib.push((i, i % 7, i % 13));
6679 }
6680 let index = ib.build();
6681 let tile_count = index.tile_sections()[0].len();
6682 assert!(tile_count > 3, "tiny budget forces many tiles");
6683
6684 let payload = encode_tiled_section(&index, 0, CODEC_NONE);
6685 let dir = parse_tile_directory(&payload, payload.len() as u64).unwrap();
6686 assert_eq!(dir.len(), tile_count);
6687 let trailer_start = dir.iter().map(|e| e.end).max().unwrap();
6689 assert!(
6690 trailer_start < payload.len() as u64,
6691 "a trailer follows the tiles"
6692 );
6693 for e in &dir {
6694 assert!(
6695 e.end <= payload.len() as u64,
6696 "tiles still located within the payload"
6697 );
6698 }
6699 let syn = parse_tile_synopsis(&payload, trailer_start as usize, dir.len()).unwrap();
6700 for (e, (min_b, max_b, min_c, max_c)) in dir.iter().zip(syn) {
6701 let block = decompress(CODEC_NONE, &payload[e.start as usize..e.end as usize]).unwrap();
6702 let z = *crate::triples::TripleBlock::parse(&block).unwrap().zone();
6703 assert_eq!(
6704 (min_b, max_b, min_c, max_c),
6705 (z.min_b, z.max_b, z.min_c, z.max_c),
6706 "synopsis equals the tile's own zone"
6707 );
6708 }
6709 }
6710
6711 #[test]
6715 fn tile_synopsis_lazy_matches_reference_every_shape() {
6716 use crate::reader::{CountingReader, SliceReader};
6717 let triples: Vec<(String, String, String)> = (0..200u32)
6718 .map(|i| {
6719 (
6720 format!("<http://ex/s/{i:04}>"),
6721 format!("<http://ex/p/{}>", i % 7),
6722 format!("<http://ex/o/{:04}>", i % 13),
6723 )
6724 })
6725 .collect();
6726 let mut db = DictionaryBuilder::new();
6727 for (s, p, o) in &triples {
6728 db.observe(s, p, o);
6729 }
6730 let dict = db.build();
6731 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6732 for (s, p, o) in &triples {
6733 ib.push(dict.encode(s, p, o).unwrap());
6734 }
6735 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
6736
6737 let eager = Rete::open(&bytes).unwrap();
6738 assert!(
6739 eager.header().has_tile_synopsis(),
6740 "new files set the synopsis flag"
6741 );
6742
6743 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
6746 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
6747 let lazy = Rete::open_ranged_lazy(reader).unwrap();
6748
6749 let brute = |s: Option<&str>, p: Option<&str>, o: Option<&str>| {
6750 let mut v: Vec<(String, String, String)> = triples
6751 .iter()
6752 .filter(|(a, b, c)| {
6753 s.is_none_or(|x| x == a) && p.is_none_or(|x| x == b) && o.is_none_or(|x| x == c)
6754 })
6755 .cloned()
6756 .collect();
6757 v.sort();
6758 v
6759 };
6760 let sv = [
6762 None,
6763 Some("<http://ex/s/0007>"),
6764 Some("<http://ex/s/0130>"),
6765 Some("<http://ex/s/9999>"),
6766 ];
6767 let pv = [
6768 None,
6769 Some("<http://ex/p/3>"),
6770 Some("<http://ex/p/6>"),
6771 Some("<http://ex/p/999>"),
6772 ];
6773 let ov = [
6774 None,
6775 Some("<http://ex/o/0000>"),
6776 Some("<http://ex/o/0012>"),
6777 Some("<http://ex/o/9999>"),
6778 ];
6779 for &s in &sv {
6780 for &p in &pv {
6781 for &o in &ov {
6782 let mut e = eager.query(s, p, o);
6783 e.sort();
6784 let mut l = lazy.query(s, p, o);
6785 l.sort();
6786 let r = brute(s, p, o);
6787 assert_eq!(e, r, "eager {s:?} {p:?} {o:?}");
6788 assert_eq!(l, r, "lazy {s:?} {p:?} {o:?} — synopsis over-pruned");
6789 }
6790 }
6791 }
6792 assert!(!lazy.index_incomplete(), "no lazy fetch failed");
6793 }
6794
6795 #[test]
6799 fn polyglot_offset_reads_lazily() {
6800 use crate::reader::{
6801 detect_polyglot_base, CountingReader, OffsetReader, SliceReader, POLYGLOT_DIGITS,
6802 POLYGLOT_MARKER,
6803 };
6804
6805 let image = build_image();
6809 let mut shell = Vec::new();
6810 shell.extend_from_slice(b"<!DOCTYPE html><html><head><!--");
6811 shell.extend_from_slice(POLYGLOT_MARKER);
6812 let digits_at = shell.len();
6813 shell.extend_from_slice(&[b'0'; POLYGLOT_DIGITS]); shell.extend_from_slice(b"--></head><body>a web page</body></html>\n");
6815 shell.resize(50_000, b' '); let base = shell.len() as u64;
6817 let digits = format!("{base:0width$}", width = POLYGLOT_DIGITS);
6818 shell[digits_at..digits_at + POLYGLOT_DIGITS].copy_from_slice(digits.as_bytes());
6819
6820 let mut poly = shell;
6821 poly.extend_from_slice(&image);
6822
6823 assert_ne!(
6825 &poly[0..4],
6826 b"RETE",
6827 "polyglot must not start with the magic"
6828 );
6829 let detected = detect_polyglot_base(&poly[..crate::header::HEADER_LEN]).unwrap();
6831 assert_eq!(detected, base);
6832
6833 let leaked: &'static [u8] = Box::leak(poly.into_boxed_slice());
6835 let counting = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
6836 let lazy = Rete::open_ranged_lazy(OffsetReader::new(counting.clone(), detected)).unwrap();
6837
6838 let plain = Rete::open(&image).unwrap();
6840 let mut want = plain.query(None, None, None);
6841 want.sort();
6842 let mut got = lazy.query(None, None, None);
6843 got.sort();
6844 assert_eq!(
6845 got, want,
6846 "polyglot lazy query differs from the plain .rete"
6847 );
6848 assert!(!got.is_empty(), "expected some triples");
6849 assert!(!lazy.index_incomplete(), "a lazy fetch failed");
6850
6851 let read = counting.bytes_read();
6854 eprintln!(
6855 "polyglot lazy read: HTML shell {base} B + .rete image {} B; \
6856 the query touched only {read} B (never the shell).",
6857 image.len()
6858 );
6859 assert!(
6860 read < base,
6861 "read {read} bytes; the HTML shell alone is {base} — the reader \
6862 touched the prefix instead of only the embedded .rete"
6863 );
6864 }
6865
6866 #[cfg(test)]
6869 fn build_text_indexed(triples: &[(String, String, String)]) -> Vec<u8> {
6870 let mut db = DictionaryBuilder::new();
6871 for (s, p, o) in triples {
6872 db.observe(s, p, o);
6873 }
6874 let dict = db.build();
6875 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
6876 let mut id_triples: Vec<(u32, u32, u32)> = Vec::with_capacity(triples.len());
6877 for (s, p, o) in triples {
6878 let t = dict.encode(s, p, o).unwrap();
6879 ib.push(t);
6880 id_triples.push(t);
6881 }
6882 let index = ib.build();
6883 let text_index = compute_text_index(&dict, &id_triples);
6884 assert!(
6885 !text_index.is_empty(),
6886 "literals should produce a text index"
6887 );
6888 write_dataset_with_metadata(&dict, &index, &[], false, &[], 0, &[], &text_index)
6889 }
6890
6891 #[test]
6895 fn text_index_eager_matches_brute_force() {
6896 let triples: Vec<(String, String, String)> = vec![
6897 (
6898 "<http://ex/s0>",
6899 "<http://ex/label>",
6900 "\"alpha glucose phosphate\"",
6901 ),
6902 ("<http://ex/s1>", "<http://ex/label>", "\"beta Glucose\""),
6903 ("<http://ex/s2>", "<http://ex/label>", "\"gamma fructose\""),
6904 (
6905 "<http://ex/s3>",
6906 "<http://ex/note>",
6907 "\"einstein relativity\"",
6908 ),
6909 (
6910 "<http://ex/s4>",
6911 "<http://ex/ref>",
6912 "<http://ex/not-a-literal>",
6913 ),
6914 ]
6915 .into_iter()
6916 .map(|(s, p, o)| (s.to_string(), p.to_string(), o.to_string()))
6917 .collect();
6918 let bytes = build_text_indexed(&triples);
6919 let rete = Rete::open(&bytes).unwrap();
6920 assert!(rete.has_text_index());
6921
6922 let brute = |words: &[&str]| -> Vec<String> {
6924 let mut v: Vec<String> = triples
6925 .iter()
6926 .filter(|(_, _, o)| {
6927 crate::terms::is_literal(o)
6928 && words.iter().all(|w| {
6929 let wl = w.to_lowercase();
6930 crate::terms::literal_lexical(o)
6931 .unwrap()
6932 .split(|c: char| !c.is_alphanumeric())
6933 .any(|t| t.to_lowercase() == wl)
6934 })
6935 })
6936 .map(|(s, _, _)| s.clone())
6937 .collect();
6938 v.sort();
6939 v.dedup();
6940 v
6941 };
6942
6943 let mut got = rete.text_search(&["glucose"], None, 0);
6944 got.sort();
6945 assert_eq!(got, brute(&["glucose"]), "case-insensitive single word");
6946
6947 let mut got = rete.text_search(&["glucose", "phosphate"], None, 0);
6949 got.sort();
6950 assert_eq!(got, brute(&["glucose", "phosphate"]));
6951
6952 assert!(rete.text_search(&["zzznope"], None, 0).is_empty());
6954
6955 let got = rete.text_search(&[], Some("ein"), 0);
6957 assert_eq!(got, vec!["<http://ex/s3>".to_string()]);
6958
6959 let mut db = DictionaryBuilder::new();
6961 for (s, p, o) in &triples {
6962 db.observe(s, p, o);
6963 }
6964 let dict = db.build();
6965 let mut ib = GraphIndexBuilder::new();
6966 for (s, p, o) in &triples {
6967 ib.push(dict.encode(s, p, o).unwrap());
6968 }
6969 let plain = write_dataset(&dict, &ib.build(), &[], false, &[], 0);
6970 let plain_rete = Rete::open(&plain).unwrap();
6971 assert!(!plain_rete.has_text_index());
6972 assert!(plain_rete.text_search(&["glucose"], None, 0).is_empty());
6973 }
6974
6975 #[test]
6979 fn text_index_lazy_faults_only_queried_postings() {
6980 use crate::reader::{CountingReader, SliceReader};
6981 let mut triples: Vec<(String, String, String)> = (0..300u32)
6984 .map(|i| {
6985 (
6986 format!("<http://ex/s/{i:04}>"),
6987 "<http://ex/label>".to_string(),
6988 format!("\"common word number {i}\""),
6989 )
6990 })
6991 .collect();
6992 for i in [3u32, 77, 250] {
6993 triples.push((
6994 format!("<http://ex/s/{i:04}>"),
6995 "<http://ex/tag>".to_string(),
6996 "\"raretoken\"".to_string(),
6997 ));
6998 }
6999 let bytes = build_text_indexed(&triples);
7000 let eager = Rete::open(&bytes).unwrap();
7001 let mut want = eager.text_search(&["raretoken"], None, 0);
7002 want.sort();
7003 assert_eq!(want.len(), 3);
7004
7005 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
7006 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7007 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
7008 let before = reader.bytes_read();
7011 let mut got = lazy.text_search(&["raretoken"], None, 0);
7012 got.sort();
7013 assert_eq!(got, want, "lazy search matches eager");
7014 let pulled = reader.bytes_read() - before;
7015 let ti_len = eager.header().text_index_len;
7018 assert!(
7019 pulled < ti_len,
7020 "search pulled {pulled} B but the section is {ti_len} B — faulted too much"
7021 );
7022 assert!(!lazy.index_incomplete());
7023 }
7024
7025 #[test]
7030 fn search_view_matches_full_open_for_far_fewer_bytes() {
7031 use crate::reader::{CountingReader, SliceReader};
7032 let filler = "lorem ipsum dolor sit amet consectetur adipiscing elit ".repeat(20);
7035 let mut triples: Vec<(String, String, String)> = (0..300u32)
7036 .map(|i| {
7037 (
7038 format!("<http://ex/s/{i:04}>"),
7039 "<http://ex/abstract>".to_string(),
7040 format!("\"{filler} number {i}\""),
7041 )
7042 })
7043 .collect();
7044 for i in [3u32, 77, 250] {
7045 triples.push((
7046 format!("<http://ex/s/{i:04}>"),
7047 "<http://ex/tag>".to_string(),
7048 "\"raretoken\"".to_string(),
7049 ));
7050 }
7051 let bytes = build_text_indexed(&triples);
7052 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
7053
7054 let full_reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7055 let full = Rete::open_ranged_lazy(full_reader.clone()).unwrap();
7056 let mut want = full.text_search(&["raretoken"], None, 0);
7057 want.sort();
7058 assert_eq!(want.len(), 3);
7059
7060 let search_reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7061 let view = SearchView::open_ranged(search_reader.clone()).unwrap();
7062 assert!(view.has_text_index());
7063 let mut got = view.text_search(&["raretoken"], None, 0);
7064 got.sort();
7065 assert_eq!(got, want, "search view matches the full open");
7066 assert!(
7067 search_reader.bytes_read() < full_reader.bytes_read(),
7068 "search read {} B, full open read {} B — the narrow open bought nothing",
7069 search_reader.bytes_read(),
7070 full_reader.bytes_read()
7071 );
7072
7073 let mut prefix_hits = view.prefix_search("", 5);
7076 prefix_hits.sort();
7077 assert!(!prefix_hits.is_empty() || !view.has_pyramid());
7078 }
7079
7080 #[test]
7090 fn dict_chunk_directory_probe_costs_about_one_directory() {
7091 use crate::reader::{CountingReader, SliceReader};
7092 let head = "x".repeat(3000);
7106 let triples: Vec<(String, String, String)> = (0..2000u32)
7107 .map(|i| {
7108 let tail = char::from(b'a' + (i % 26) as u8).to_string().repeat(1000);
7109 (
7110 format!("<http://ex/s/{i:04}>"),
7111 "<http://ex/abstract>".to_string(),
7112 format!("\"{head}{i:04}{tail}\""),
7113 )
7114 })
7115 .collect();
7116 let mut db = DictionaryBuilder::new();
7117 for (s, p, o) in &triples {
7118 db.observe(s, p, o);
7119 }
7120 let dict = db.build();
7121 let mut ib = GraphIndexBuilder::new();
7122 for (s, p, o) in &triples {
7123 ib.push(dict.encode(s, p, o).unwrap());
7124 }
7125 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7126
7127 let reader = CountingReader::new(SliceReader::new(&bytes));
7128 let header = Header::from_bytes(&reader.read_at(0, HEADER_LEN as u64).unwrap()).unwrap();
7129 let section = locate_container_section_ranged(
7131 &reader,
7132 header.dictionary_offset,
7133 header.dictionary_len,
7134 2,
7135 4,
7136 )
7137 .unwrap();
7138
7139 let before = reader.bytes_read();
7140 let (_meta, entries) = read_dict_dir_ranged(&reader, section).unwrap();
7141 let spent = reader.bytes_read() - before;
7142
7143 let dir_end = entries[0].start;
7145 let head = reader.read_at(section.offset, 64).unwrap();
7146 let (header_len, n0) = read_uvarint(&head).unwrap();
7147 let dir_start = n0 as u64 + header_len;
7148 let dir_len = dir_end - dir_start;
7149 let dir_total = section.len - dir_start;
7150 assert!(
7151 dir_start + dir_len > 8192,
7152 "directory fits the header prefix ({dir_len} B) — the probe never runs"
7153 );
7154
7155 let mut old = 0u64;
7157 let mut p = 4096u64.min(dir_total).max(1);
7158 loop {
7159 let got = p.min(dir_total);
7160 old += got;
7161 if got >= dir_len || p >= dir_total {
7162 break;
7163 }
7164 p = p.saturating_mul(2).min(dir_total);
7165 }
7166 assert!(
7167 spent <= dir_len * 2,
7168 "probe read {spent} B for a {dir_len} B directory — past the append-only bound"
7169 );
7170 assert!(
7171 spent < old,
7172 "probe read {spent} B; the re-reading loop it replaced spent {old} B"
7173 );
7174 }
7175
7176 fn verbatim_keyed_twin(payload: &[u8], codec: u8) -> Vec<u8> {
7186 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
7187 let (header_len, n0) = read_uvarint(payload).unwrap();
7188 let mut out = Vec::new();
7189 write_uvarint(&mut out, header_len);
7190 out.extend_from_slice(&payload[n0..n0 + header_len as usize]);
7191 write_uvarint(&mut out, entries.len() as u64);
7192 let mut prev_run = 0usize;
7193 for e in &entries {
7194 let body = decompress(codec, &payload[e.start as usize..e.end as usize]).unwrap();
7195 let first = crate::dict::run_first_term(&body, 0).unwrap();
7196 write_uvarint(&mut out, (e.first_run - prev_run) as u64);
7197 write_uvarint(&mut out, first.len() as u64);
7198 out.extend_from_slice(&first);
7199 write_uvarint(&mut out, e.end - e.start);
7200 prev_run = e.first_run;
7201 }
7202 for e in &entries {
7203 out.extend_from_slice(&payload[e.start as usize..e.end as usize]);
7204 }
7205 out
7206 }
7207
7208 fn chunk_bounds_of(payload: &[u8], codec: u8) -> Vec<(Vec<u8>, Vec<u8>)> {
7210 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
7211 entries
7212 .iter()
7213 .map(|e| {
7214 let body = decompress(codec, &payload[e.start as usize..e.end as usize]).unwrap();
7215 (
7216 crate::dict::run_first_term(&body, 0).unwrap(),
7217 crate::dict::run_last_term(&body, 0, body.len()).unwrap(),
7218 )
7219 })
7220 .collect()
7221 }
7222
7223 fn adversarial_terms() -> Vec<String> {
7229 let mut terms: Vec<String> = Vec::new();
7230 for i in 0..6_000 {
7231 terms.push(format!("<http://example.org/work/{i:06}>"));
7232 }
7233 let shared = "X".repeat(900);
7234 for i in 0..4_000 {
7235 terms.push(format!("\"{shared}-{i:05}-tail\""));
7236 }
7237 let deep = "Y".repeat(3_000); for i in 0..120 {
7239 terms.push(format!("\"{deep}{i:03}\""));
7240 }
7241 for i in 0..6 {
7242 terms.push(format!("\"BIG-{i:02}-{}\"", "Z".repeat(90_000)));
7244 }
7245 for i in 0..800 {
7246 terms.push(format!(
7247 "\"h\u{e9}llo-{}-\u{1F600}{i:04}\"",
7248 "\u{e9}".repeat(40)
7249 ));
7250 }
7251 for i in 0..2_000 {
7252 terms.push(format!("\"{i:05}-{}\"", "T".repeat(1_800)));
7253 }
7254 terms.sort();
7255 terms.dedup();
7256 terms
7257 }
7258
7259 fn chunked_section_of(terms: &[String]) -> Vec<u8> {
7260 let mut b = crate::dict::DictSectionBuilder::new().with_restart_interval(16);
7261 for t in terms {
7262 b.push(t.clone());
7263 }
7264 b.build()
7265 }
7266
7267 #[test]
7277 fn chunk_directory_keys_are_separators_not_terms() {
7278 let terms = adversarial_terms();
7279 let raw = chunked_section_of(&terms);
7280 let payload = encode_chunked_dict_section(&raw, CODEC_NONE);
7281 let (_meta, entries) = parse_chunked_dict_dir(&payload, payload.len() as u64).unwrap();
7282 let bounds = chunk_bounds_of(&payload, CODEC_NONE);
7283 assert!(
7284 entries.len() > 20,
7285 "need a many-chunk section, got {}",
7286 entries.len()
7287 );
7288
7289 assert!(entries[0].key.is_empty(), "chunk 0 needs no separator");
7290 let mut key_bytes = 0usize;
7291 let mut term_bytes = 0usize;
7292 let mut differ = 0usize;
7293 for (i, e) in entries.iter().enumerate() {
7294 key_bytes += e.key.len();
7295 term_bytes += bounds[i].0.len();
7296 if e.key != bounds[i].0 {
7297 differ += 1;
7298 }
7299 assert!(
7300 e.key <= bounds[i].0,
7301 "chunk {i}: key must be <= its own first term"
7302 );
7303 if i > 0 {
7304 assert!(
7305 e.key.as_slice() > bounds[i - 1].1.as_slice(),
7306 "chunk {i}: key must be > the previous chunk's last term"
7307 );
7308 assert!(
7309 e.key > entries[i - 1].key,
7310 "chunk {i}: keys must ascend for partition_point"
7311 );
7312 assert_eq!(
7314 e.key,
7315 crate::dict::shortest_separator(&bounds[i - 1].1, &bounds[i].0),
7316 "chunk {i}: key is not the SHORTEST separator"
7317 );
7318 }
7319 }
7320 assert!(
7321 differ > 0,
7322 "every key equalled its first term — the writer stores terms again"
7323 );
7324 assert!(
7325 key_bytes * 4 < term_bytes,
7326 "separators bought almost nothing: {key_bytes} B vs {term_bytes} B of first terms"
7327 );
7328 eprintln!(
7329 "chunks={} first-term keys={term_bytes} B separator keys={key_bytes} B ({:.1}x)",
7330 entries.len(),
7331 term_bytes as f64 / key_bytes.max(1) as f64
7332 );
7333 }
7334
7335 #[test]
7346 fn separator_keys_route_identically_to_verbatim_first_terms() {
7347 let terms = adversarial_terms();
7348 let raw = chunked_section_of(&terms);
7349 let sep_payload = encode_chunked_dict_section(&raw, CODEC_NONE);
7350 let ver_payload = verbatim_keyed_twin(&sep_payload, CODEC_NONE);
7351
7352 let (_ms, es) = parse_chunked_dict_dir(&sep_payload, sep_payload.len() as u64).unwrap();
7354 let (_mv, ev) = parse_chunked_dict_dir(&ver_payload, ver_payload.len() as u64).unwrap();
7355 assert_eq!(es.len(), ev.len());
7356 for (a, b) in es.iter().zip(ev.iter()) {
7357 assert_eq!(a.first_run, b.first_run);
7358 assert_eq!(
7359 &sep_payload[a.start as usize..a.end as usize],
7360 &ver_payload[b.start as usize..b.end as usize],
7361 "chunk bodies must be identical"
7362 );
7363 }
7364 assert!(
7365 sep_payload.len() < ver_payload.len(),
7366 "separator payload is not smaller"
7367 );
7368
7369 let sec_s = decode_chunked_dict_section(
7370 &sep_payload,
7371 CODEC_NONE,
7372 crate::chunk_cache::ChunkCache::unlimited_arc(),
7373 0,
7374 )
7375 .unwrap();
7376 let sec_v = decode_chunked_dict_section(
7377 &ver_payload,
7378 CODEC_NONE,
7379 crate::chunk_cache::ChunkCache::unlimited_arc(),
7380 0,
7381 )
7382 .unwrap();
7383
7384 for (i, t) in terms.iter().enumerate() {
7386 let want = Some(i as u32 + 1);
7387 assert_eq!(sec_v.id(t), want, "verbatim id() lost a term");
7388 assert_eq!(sec_s.id(t), want, "separator id() lost a term");
7389 }
7390 for i in 0..terms.len() as u32 {
7392 assert_eq!(sec_s.term(i + 1), sec_v.term(i + 1));
7393 assert_eq!(
7394 sec_s.term(i + 1).as_deref(),
7395 Some(terms[i as usize].as_str())
7396 );
7397 }
7398 let bounds = chunk_bounds_of(&sep_payload, CODEC_NONE);
7400 let mut probes: Vec<String> = Vec::new();
7401 let push = |bytes: &[u8], probes: &mut Vec<String>| {
7402 if let Ok(s) = std::str::from_utf8(bytes) {
7403 probes.push(s.to_string());
7404 }
7405 };
7406 for (first, last) in &bounds {
7407 for base in [first, last] {
7408 push(base, &mut probes); let mut after = base.clone();
7410 after.push(0x01); push(&after, &mut probes);
7412 let mut far = base.clone();
7413 far.push(b'~');
7414 push(&far, &mut probes);
7415 if !base.is_empty() {
7416 push(&base[..base.len() - 1], &mut probes); let mut down = base.clone();
7418 *down.last_mut().unwrap() = down.last().unwrap().wrapping_sub(1);
7419 push(&down, &mut probes);
7420 let mut up = base.clone();
7421 *up.last_mut().unwrap() = up.last().unwrap().wrapping_add(1);
7422 push(&up, &mut probes);
7423 }
7424 }
7425 }
7426 for e in &es {
7428 push(&e.key, &mut probes);
7429 let mut plus = e.key.clone();
7430 plus.push(0);
7431 push(&plus, &mut probes);
7432 }
7433 probes.push(String::new()); probes.push("\u{1}".to_string()); probes.push("\u{10FFFF}\u{10FFFF}".to_string()); probes.push(terms.first().unwrap().clone());
7437 probes.push(terms.last().unwrap().clone());
7438
7439 let mut hits = 0usize;
7440 for p in &probes {
7441 let (a, b) = (sec_v.id(p), sec_s.id(p));
7442 assert_eq!(
7443 a,
7444 b,
7445 "probe diverged: {:?}",
7446 p.get(..p.len().min(48)).unwrap_or(p.as_str())
7447 );
7448 hits += usize::from(a.is_some());
7449 }
7450 assert!(
7451 probes.len() > 500 && hits > 50,
7452 "probe set is too thin: {} probes, {hits} of them present",
7453 probes.len()
7454 );
7455 eprintln!(
7456 "{} boundary probes, {hits} present, all identical",
7457 probes.len()
7458 );
7459 }
7460
7461 mod separator_props {
7467 use super::*;
7468 use proptest::prelude::*;
7469
7470 fn term_set() -> impl Strategy<Value = Vec<String>> {
7477 prop::collection::vec(("[a-c]{1,6}", 0u8..3, 300usize..1200), 60..250).prop_map(
7478 |specs| {
7479 let mut terms: Vec<String> = specs
7480 .into_iter()
7481 .enumerate()
7482 .map(|(i, (core, kind, pad))| {
7483 let block = "z".repeat(pad);
7484 match kind {
7485 0 => format!("<http://ex/{core}/{i:05}>"),
7486 1 => format!("\"{block}{core}{i:05}\""),
7487 _ => format!("\"{core}{i:05}{block}\""),
7488 }
7489 })
7490 .collect();
7491 terms.sort();
7492 terms.dedup();
7493 terms
7494 },
7495 )
7496 }
7497
7498 proptest! {
7499 #[test]
7500 fn prop_separator_keyed_section_answers_like_verbatim(
7501 terms in term_set(),
7502 extra in prop::collection::vec(any::<String>(), 0..8),
7503 ) {
7504 let raw = chunked_section_of(&terms);
7505 let sep = encode_chunked_dict_section(&raw, CODEC_NONE);
7506 let ver = verbatim_keyed_twin(&sep, CODEC_NONE);
7507 prop_assert!(sep.len() <= ver.len());
7508
7509 let (_m, es) = parse_chunked_dict_dir(&sep, sep.len() as u64).unwrap();
7511 let bounds = chunk_bounds_of(&sep, CODEC_NONE);
7512 prop_assert!(es[0].key.is_empty());
7513 for i in 1..es.len() {
7514 prop_assert!(es[i].key.as_slice() > bounds[i - 1].1.as_slice());
7515 prop_assert!(es[i].key <= bounds[i].0);
7516 prop_assert!(es[i].key > es[i - 1].key);
7517 }
7518
7519 let sec_s = decode_chunked_dict_section(
7520 &sep,
7521 CODEC_NONE,
7522 crate::chunk_cache::ChunkCache::unlimited_arc(),
7523 0,
7524 )
7525 .unwrap();
7526 let sec_v = decode_chunked_dict_section(
7527 &ver,
7528 CODEC_NONE,
7529 crate::chunk_cache::ChunkCache::unlimited_arc(),
7530 0,
7531 )
7532 .unwrap();
7533
7534 for (i, t) in terms.iter().enumerate() {
7535 prop_assert_eq!(sec_s.id(t), Some(i as u32 + 1));
7536 prop_assert_eq!(sec_v.id(t), Some(i as u32 + 1));
7537 prop_assert_eq!(sec_s.term(i as u32 + 1), sec_v.term(i as u32 + 1));
7538 }
7539
7540 let mut probes: Vec<String> = extra;
7542 probes.push(String::new());
7543 for (first, last) in &bounds {
7544 for base in [first, last] {
7545 if let Ok(s) = std::str::from_utf8(base) {
7546 probes.push(s.to_string());
7547 probes.push(format!("{s}\u{1}"));
7548 probes.push(s[..s.len() - s.chars().next_back()
7549 .map(char::len_utf8).unwrap_or(0)].to_string());
7550 }
7551 }
7552 }
7553 for e in &es {
7554 if let Ok(s) = std::str::from_utf8(&e.key) {
7555 probes.push(s.to_string());
7556 }
7557 }
7558 for p in &probes {
7559 prop_assert_eq!(sec_s.id(p), sec_v.id(p), "probe diverged: {:?}", p);
7560 }
7561 }
7562 }
7563 }
7564
7565 #[test]
7570 fn text_index_is_tamper_evident_and_verifies() {
7571 let triples: Vec<(String, String, String)> = vec![(
7572 "<http://ex/s0>".to_string(),
7573 "<http://ex/label>".to_string(),
7574 "\"alpha glucose phosphate\"".to_string(),
7575 )];
7576 let bytes = build_text_indexed(&triples);
7577 let header = Rete::open(&bytes).unwrap().header().clone();
7578 assert!(header.text_index_len > 0);
7579 assert!(verify(&bytes).unwrap(), "a text-indexed build must verify");
7580
7581 let mut tampered = bytes.clone();
7582 tampered[header.text_index_offset as usize] ^= 0xff;
7583 assert!(
7584 !verify(&tampered).unwrap(),
7585 "tampering with the text index must break verify()"
7586 );
7587 }
7588
7589 #[test]
7593 fn synopsis_cuts_remote_fetch_bytes() {
7594 use crate::header::FLAG_TILE_SYNOPSIS;
7595 use crate::reader::{CountingReader, SliceReader};
7596
7597 let triples: Vec<(String, String, String)> = (0..400u32)
7601 .map(|i| {
7602 (
7603 format!("<http://ex/s/{i:04}>"),
7604 "<http://ex/p>".to_string(),
7605 format!("<http://ex/o/{i:04}>"),
7606 )
7607 })
7608 .collect();
7609 let mut db = DictionaryBuilder::new();
7610 for (s, p, o) in &triples {
7611 db.observe(s, p, o);
7612 }
7613 let dict = db.build();
7614 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
7615 for (s, p, o) in &triples {
7616 ib.push(dict.encode(s, p, o).unwrap());
7617 }
7618 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7619
7620 let q = (Some("<http://ex/s/0395>"), None, Some("<http://ex/o/0005>"));
7624 let query_bytes = |image: &[u8]| -> (u64, usize) {
7628 let leaked: &'static [u8] = Box::leak(image.to_vec().into_boxed_slice());
7629 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7630 let rete = Rete::open_ranged_lazy(reader.clone()).unwrap();
7631 let before = reader.bytes_read(); let n = rete.query(q.0, q.1, q.2).len();
7633 assert!(!rete.index_incomplete());
7634 (reader.bytes_read() - before, n)
7635 };
7636
7637 let (on_bytes, on_n) = query_bytes(&bytes);
7638 let mut off = bytes.clone();
7640 off[5] &= !FLAG_TILE_SYNOPSIS;
7641 let (off_bytes, off_n) = query_bytes(&off);
7642
7643 assert_eq!(on_n, 0, "the pair never co-occurs");
7644 assert_eq!(off_n, 0, "same answer without the synopsis");
7645 assert!(
7649 on_bytes < off_bytes,
7650 "synopsis skips the routed tile fetch: {on_bytes} < {off_bytes}"
7651 );
7652 }
7653
7654 #[test]
7660 fn double_bound_object_join_eager_matches_lazy() {
7661 use crate::reader::SliceReader;
7662 let occ = "<http://ex/occ>";
7663 let phys = "<http://ex/physicist>";
7664 let phil = "<http://ex/philosopher>";
7665 let label = "<http://www.w3.org/2000/01/rdf-schema#label>";
7666 let mut triples: Vec<(String, String, String)> = Vec::new();
7668 for i in 0..20u32 {
7669 triples.push((format!("<http://ex/p/{i:02}>"), occ.into(), phys.into()));
7670 if i < 10 {
7671 triples.push((format!("<http://ex/p/{i:02}>"), occ.into(), phil.into()));
7672 }
7673 triples.push((
7674 format!("<http://ex/p/{i:02}>"),
7675 label.into(),
7676 format!("\"Name {i:02}\""),
7677 ));
7678 }
7679 let mut db = DictionaryBuilder::new();
7680 for (s, p, o) in &triples {
7681 db.observe(s, p, o);
7682 }
7683 let dict = db.build();
7684 let mut ib = GraphIndexBuilder::new().with_tile_budget(16);
7685 for (s, p, o) in &triples {
7686 ib.push(dict.encode(s, p, o).unwrap());
7687 }
7688 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7689
7690 let q = "SELECT ?l WHERE { \
7691 ?p <http://ex/occ> <http://ex/physicist> ; \
7692 <http://ex/occ> <http://ex/philosopher> ; \
7693 <http://www.w3.org/2000/01/rdf-schema#label> ?l }";
7694 let run = |rete: &Rete| -> Vec<String> {
7695 let (_, sols) = crate::eval_sparql(rete, q).unwrap();
7696 let mut v: Vec<String> = sols.iter().filter_map(|b| b.get("l").cloned()).collect();
7697 v.sort();
7698 v
7699 };
7700
7701 let eager_rows = run(&Rete::open(&bytes).unwrap());
7702 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
7703 let lazy = Rete::open_ranged_lazy(std::sync::Arc::new(SliceReader::new(leaked))).unwrap();
7704 let lazy_rows = run(&lazy);
7705 assert!(!lazy.index_incomplete());
7706
7707 assert_eq!(eager_rows.len(), 10, "the 10 physicist∩philosopher labels");
7708 assert_eq!(eager_rows, lazy_rows, "eager and lazy must agree exactly");
7709 }
7710
7711 #[test]
7715 fn multi_chunk_dictionary_round_trips() {
7716 let mut db = DictionaryBuilder::new();
7717 let term = |i: u32| format!("<http://example.org/some/long/prefix/entity/{i:06}>");
7718 for i in 0..6000u32 {
7719 db.observe(&term(i), "<http://ex/p>", &term(i + 1));
7720 }
7721 let dict = db.build();
7722 let mut ib = GraphIndexBuilder::new();
7723 for i in 0..6000u32 {
7724 ib.push(
7725 dict.encode(&term(i), "<http://ex/p>", &term(i + 1))
7726 .unwrap(),
7727 );
7728 }
7729 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7730 let rete = Rete::open(&bytes).unwrap();
7731 let d = rete.dictionary();
7732 assert_eq!(d.term_count(), dict.term_count());
7733 for i in (0..6000).step_by(97).chain([0, 1, 5999, 6000]) {
7734 let t = term(i);
7735 let sid = dict.subject_id(&t);
7736 assert_eq!(d.subject_id(&t), sid, "subject_id({t})");
7737 if let Some(id) = sid {
7738 assert_eq!(d.subject_term(id).as_deref(), Some(t.as_str()));
7739 }
7740 let oid = dict.object_id(&t);
7741 assert_eq!(d.object_id(&t), oid, "object_id({t})");
7742 }
7743 assert_eq!(d.subject_id("<http://example.org/absent>"), None);
7744 assert_eq!(d.predicate_id("<http://ex/p>"), Some(1));
7745 assert_eq!(d.predicate_term(1).as_deref(), Some("<http://ex/p>"));
7746 }
7747
7748 #[test]
7749 #[cfg(feature = "compression")]
7750 fn compression_shrinks_repetitive_data() {
7751 let mut db = DictionaryBuilder::new();
7755 let triples: Vec<(String, String, String)> = (0..500)
7756 .map(|i| {
7757 (
7758 format!("<http://example.org/entity/{i}>"),
7759 "<http://example.org/p/relatedTo>".to_string(),
7760 format!("<http://example.org/entity/{}>", (i + 1) % 500),
7761 )
7762 })
7763 .collect();
7764 for (s, p, o) in &triples {
7765 db.observe(s, p, o);
7766 }
7767 let dict = db.build();
7768 let mut ib = GraphIndexBuilder::new();
7769 for (s, p, o) in &triples {
7770 ib.push(dict.encode(s, p, o).unwrap());
7771 }
7772 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7773
7774 let raw: usize = triples
7775 .iter()
7776 .map(|(s, p, o)| s.len() + p.len() + o.len())
7777 .sum();
7778 assert!(
7779 bytes.len() < raw / 2,
7780 "expected strong compression: file {} vs raw terms {raw}",
7781 bytes.len()
7782 );
7783
7784 let rete = Rete::open(&bytes).unwrap();
7786 let r = rete.query(Some("<http://example.org/entity/0>"), None, None);
7787 assert_eq!(r.len(), 1);
7788 assert_eq!(r[0].2, "<http://example.org/entity/1>");
7789 }
7790
7791 fn big_file_with_pyramid() -> Vec<u8> {
7792 let triples: Vec<(String, String, String)> = (0..300)
7794 .map(|i| {
7795 (
7796 format!("<http://ex/e{i}>"),
7797 "<http://ex/next>".to_string(),
7798 format!("<http://ex/e{}>", (i + 1) % 300),
7799 )
7800 })
7801 .collect();
7802 let mut db = DictionaryBuilder::new();
7803 for (s, p, o) in &triples {
7804 db.observe(s, p, o);
7805 }
7806 let dict = db.build();
7807 let ids: Vec<_> = triples
7808 .iter()
7809 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
7810 .collect();
7811 let mut ib = GraphIndexBuilder::new();
7812 for &t in &ids {
7813 ib.push(t);
7814 }
7815 let (meta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
7816 write_file(&dict, &ib.build(), false, &meta, levels)
7817 }
7818
7819 #[test]
7820 fn ranged_open_is_minimal_and_correct() {
7821 use crate::reader::{CountingReader, SliceReader};
7822 let bytes = big_file_with_pyramid();
7823
7824 let full = CountingReader::new(SliceReader::new(&bytes));
7825 let rete = Rete::open_ranged(&full).unwrap();
7826 assert!(full.requests() <= 4, "requests = {}", full.requests());
7828 assert_eq!(
7829 rete.query(Some("<http://ex/e0>"), None, None)[0].2,
7830 "<http://ex/e1>"
7831 );
7832
7833 let summ_reader = CountingReader::new(SliceReader::new(&bytes));
7835 let view = SummaryView::open_ranged(&summ_reader).unwrap().unwrap();
7836 assert!(!view.summary.is_empty());
7837 assert!(
7838 summ_reader.bytes_read() < bytes.len() as u64,
7839 "summary read {} of {} bytes",
7840 summ_reader.bytes_read(),
7841 bytes.len()
7842 );
7843 assert!(summ_reader.bytes_read() < full.bytes_read());
7845 }
7846
7847 #[test]
7848 fn content_hash_is_set_and_verifies() {
7849 let bytes = build_image();
7850 let rete = Rete::open(&bytes).unwrap();
7851 assert_ne!(
7852 rete.header().content_hash,
7853 [0u8; 16],
7854 "hash must be populated"
7855 );
7856 assert!(verify(&bytes).unwrap(), "freshly built file verifies");
7857 assert!(
7858 verify_ranged(&crate::reader::SliceReader::new(&bytes)).unwrap(),
7859 "the streaming verify agrees"
7860 );
7861
7862 assert_eq!(
7864 Rete::open(&build_image()).unwrap().header().content_hash,
7865 rete.header().content_hash
7866 );
7867
7868 let mut tampered = bytes.clone();
7870 let last = tampered.len() - 5; tampered[last] ^= 0xff;
7872 assert!(!verify(&tampered).unwrap());
7873 assert!(!verify_ranged(&crate::reader::SliceReader::new(&tampered)).unwrap());
7874 let short = &bytes[..bytes.len() / 2];
7876 assert!(verify_ranged(&crate::reader::SliceReader::new(short)).is_err());
7877 assert!(verify_ranged(&crate::reader::SliceReader::new(&bytes[..10])).is_err());
7878 }
7879
7880 fn build_with_metadata(meta: &[u8]) -> Vec<u8> {
7882 let triples = [
7883 ("Alice", "knows", "Bob"),
7884 ("Bob", "knows", "Carol"),
7885 ("Alice", "age", "30"),
7886 ];
7887 let mut db = DictionaryBuilder::new();
7888 for (s, p, o) in triples {
7889 db.observe(s, p, o);
7890 }
7891 let dict = db.build();
7892 let ids: Vec<_> = triples
7893 .iter()
7894 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
7895 .collect();
7896 let mut ib = GraphIndexBuilder::new();
7897 for &t in &ids {
7898 ib.push(t);
7899 }
7900 let (pmeta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
7901 write_dataset_with_metadata(&dict, &ib.build(), &[], false, &pmeta, levels, meta, &[])
7902 }
7903
7904 #[test]
7905 fn metadata_round_trips_and_shifts_offsets() {
7906 let card = br#"{"title":"My Dataset"}"#;
7907 let bytes = build_with_metadata(card);
7908 let rete = Rete::open(&bytes).unwrap();
7909
7910 assert_eq!(rete.metadata(), Some(card.as_slice()));
7912 let h = rete.header();
7913 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
7914 assert_eq!(h.metadata_len, card.len() as u64);
7915 assert_eq!(h.dictionary_offset, HEADER_LEN as u64 + card.len() as u64);
7917
7918 assert_eq!(
7920 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
7921 1
7922 );
7923 assert!(verify(&bytes).unwrap());
7925 }
7926
7927 #[test]
7928 fn empty_metadata_is_byte_identical_to_plain_writer() {
7929 assert_eq!(
7932 build_with_metadata(&[]),
7933 build_image(),
7934 "empty-metadata output must equal the plain writer byte-for-byte"
7935 );
7936 }
7937
7938 #[test]
7943 fn replace_metadata_matches_a_direct_write() {
7944 let first = br#"{"title":"My Dataset","queries":[1,2,3]}"#;
7945 let bytes = build_with_metadata(first);
7946 for target in [
7947 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(), ] {
7952 let spliced = replace_metadata(&bytes, target).unwrap();
7953 assert_eq!(
7954 spliced,
7955 build_with_metadata(target),
7956 "splicing {} bytes of metadata must equal a direct write",
7957 target.len()
7958 );
7959 assert!(
7960 verify(&spliced).unwrap(),
7961 "the new hash covers the new card"
7962 );
7963 let rete = Rete::open(&spliced).unwrap();
7964 assert_eq!(
7965 rete.metadata(),
7966 (!target.is_empty()).then_some(target),
7967 "the new payload reads back verbatim"
7968 );
7969 assert_eq!(
7971 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
7972 1
7973 );
7974 }
7975 }
7976
7977 #[test]
7980 fn replace_metadata_keeps_build_info_adjacent() {
7981 let with_info =
7982 attach_build_info(&build_with_metadata(br#"{"a":1}"#), br#"{"builder":"x"}"#).unwrap();
7983 let spliced = replace_metadata(&with_info, br#"{"a":1,"b":2}"#).unwrap();
7984 let h = Header::from_bytes(&spliced).unwrap();
7985 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
7986 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + h.metadata_len);
7987 assert_eq!(
7988 read_build_info(&spliced).unwrap().as_deref(),
7989 Some(br#"{"builder":"x"}"#.as_slice())
7990 );
7991 assert!(verify(&spliced).unwrap());
7992 assert_eq!(
7994 h.content_hash,
7995 Header::from_bytes(&build_with_metadata(br#"{"a":1,"b":2}"#))
7996 .unwrap()
7997 .content_hash
7998 );
7999 }
8000
8001 #[test]
8002 fn metadata_is_tamper_evident() {
8003 let card = br#"{"title":"x"}"#;
8004 let mut bytes = build_with_metadata(card);
8005 assert!(verify(&bytes).unwrap());
8006 bytes[HEADER_LEN + 2] ^= 0xff;
8008 assert!(
8009 !verify(&bytes).unwrap(),
8010 "tampering with the card must break verify()"
8011 );
8012 }
8013
8014 #[test]
8015 fn ranged_opens_do_not_fetch_metadata() {
8016 use crate::reader::{CountingReader, SliceReader};
8017 let card = vec![0xABu8; 512]; let bytes = build_with_metadata(&card);
8019 let total = bytes.len() as u64;
8020
8021 let r = CountingReader::new(SliceReader::new(&bytes));
8023 let rete = Rete::open_ranged(&r).unwrap();
8024 assert!(
8025 rete.metadata().is_none(),
8026 "open_ranged must not load the card"
8027 );
8028 assert!(r.requests() <= 4, "requests = {}", r.requests());
8029 assert!(
8030 r.bytes_read() <= total - card.len() as u64,
8031 "read {} of {} bytes; the {}-byte card must be skipped",
8032 r.bytes_read(),
8033 total,
8034 card.len()
8035 );
8036
8037 let rs = CountingReader::new(SliceReader::new(&bytes));
8039 let view = SummaryView::open_ranged(&rs).unwrap().unwrap();
8040 assert!(!view.summary.is_empty());
8041 assert!(rs.bytes_read() <= total - card.len() as u64);
8042 }
8043
8044 #[test]
8045 fn metadata_ranged_fetches_only_header_and_card() {
8046 use crate::reader::{CountingReader, SliceReader};
8047 let card = vec![0xCDu8; 384];
8050 let bytes = build_with_metadata(&card);
8051
8052 let r = CountingReader::new(SliceReader::new(&bytes));
8053 let got = read_metadata_ranged(&r).unwrap().unwrap();
8054 assert_eq!(got, card, "the card reads back verbatim");
8055 assert_eq!(r.requests(), 2, "exactly header + metadata ranges");
8056 assert_eq!(
8057 r.bytes_read(),
8058 HEADER_LEN as u64 + card.len() as u64,
8059 "no dictionary/index/pyramid bytes are touched"
8060 );
8061
8062 let plain = build_image();
8064 let rp = CountingReader::new(SliceReader::new(&plain));
8065 assert!(read_metadata_ranged(&rp).unwrap().is_none());
8066 assert_eq!(rp.requests(), 1, "header only for a cardless file");
8067 assert_eq!(rp.bytes_read(), HEADER_LEN as u64);
8068 }
8069
8070 #[test]
8071 fn build_info_attaches_outside_the_hash() {
8072 let card = br#"{"title":"My Dataset"}"#;
8073 let base = build_with_metadata(card);
8074 let base_hash = Header::from_bytes(&base).unwrap().content_hash;
8075
8076 let info = br#"{"built_at":"2026-08-04T00:00:00Z","builder":"rete-cli 0.3.2"}"#;
8077 let with = attach_build_info(&base, info).unwrap();
8078
8079 assert_eq!(read_build_info(&with).unwrap().unwrap(), info);
8081 let h = Header::from_bytes(&with).unwrap();
8082 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + card.len() as u64);
8083 assert_eq!(h.build_info_len, info.len() as u64);
8084 assert_eq!(h.expected_file_len(), Some(with.len() as u64));
8085
8086 assert_eq!(h.content_hash, base_hash);
8090 assert!(verify(&with).unwrap());
8091 let other = attach_build_info(&base, br#"{"built_at":"1999-01-01T00:00:00Z"}"#).unwrap();
8092 assert_eq!(Header::from_bytes(&other).unwrap().content_hash, base_hash);
8093
8094 let rete = Rete::open(&with).unwrap();
8096 assert_eq!(
8097 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
8098 1
8099 );
8100 assert_eq!(rete.metadata(), Some(card.as_slice()));
8101
8102 let replaced = attach_build_info(&with, br#"{"builder":"other"}"#).unwrap();
8105 assert_eq!(
8106 read_build_info(&replaced).unwrap().unwrap(),
8107 br#"{"builder":"other"}"#
8108 );
8109 assert!(verify(&replaced).unwrap());
8110 let stripped = attach_build_info(&replaced, &[]).unwrap();
8111 assert_eq!(stripped, base, "stripping build-info restores the image");
8112 }
8113
8114 #[test]
8115 fn build_info_attaches_on_a_metadata_free_file() {
8116 let base = build_image();
8118 let info = b"{\"builder\":\"x\"}";
8119 let with = attach_build_info(&base, info).unwrap();
8120 let h = Header::from_bytes(&with).unwrap();
8121 assert_eq!(h.build_info_offset, HEADER_LEN as u64);
8122 assert_eq!(h.metadata_len, 0);
8123 assert_eq!(read_build_info(&with).unwrap().unwrap(), info);
8124 assert!(verify(&with).unwrap());
8125 assert_eq!(
8126 h.content_hash,
8127 Header::from_bytes(&base).unwrap().content_hash
8128 );
8129 Rete::open(&with).unwrap();
8130 }
8131
8132 #[test]
8139 fn a_plan_rebuilds_exactly_what_the_in_memory_splice_produces() {
8140 let card = br#"{"title":"My Dataset"}"#;
8141 let base = build_with_metadata(card);
8142 let base_hash = Header::from_bytes(&base).unwrap().content_hash;
8143 for start in [
8144 base.clone(),
8145 attach_build_info(&base, b"{\"a\":1}").unwrap(),
8146 ] {
8147 for info in [
8148 br#"{"builder":"rete-cli 0.3.2","query_costs":{}}"#.as_slice(), b"{}".as_slice(), b"".as_slice(), ] {
8152 let plan = plan_build_info(&start, start.len() as u64, info.len() as u64).unwrap();
8153 let mut streamed = Vec::new();
8154 streamed.extend_from_slice(&plan.header);
8155 streamed.extend_from_slice(&start[HEADER_LEN..plan.insert as usize]);
8156 streamed.extend_from_slice(info);
8157 streamed.extend_from_slice(&start[plan.tail_start as usize..]);
8158 assert_eq!(streamed.len() as u64, plan.new_len);
8159 assert_eq!(
8160 streamed,
8161 attach_build_info(&start, info).unwrap(),
8162 "a {}-byte section streamed must equal the same section spliced",
8163 info.len()
8164 );
8165 assert!(verify(&streamed).unwrap());
8167 let h = Header::from_bytes(&streamed).unwrap();
8168 assert_eq!(h.content_hash, base_hash);
8169 assert_eq!(h.expected_file_len(), Some(plan.new_len));
8170 let rete = Rete::open(&streamed).unwrap();
8171 assert_eq!(rete.metadata(), Some(card.as_slice()));
8172 assert_eq!(
8173 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
8174 1
8175 );
8176 }
8177 }
8178 }
8179
8180 #[test]
8183 fn a_plan_refuses_a_header_that_overruns_the_file() {
8184 let base = build_with_metadata(br#"{"title":"x"}"#);
8185 assert!(plan_build_info(&base, 8, 16).is_err());
8186 assert!(plan_build_info(&base[..HEADER_LEN], base.len() as u64, 16).is_ok());
8187 }
8188
8189 #[test]
8190 fn card_and_build_info_ranged_is_one_header_plus_one_range() {
8191 use crate::reader::{CountingReader, SliceReader};
8192 let card = vec![0xCDu8; 384];
8193 let info = vec![0xEFu8; 200];
8194 let bytes = attach_build_info(&build_with_metadata(&card), &info).unwrap();
8195
8196 let r = CountingReader::new(SliceReader::new(&bytes));
8199 let (m, b) = read_card_and_build_info_ranged(&r).unwrap();
8200 assert_eq!(m.unwrap(), card);
8201 assert_eq!(b.unwrap(), info);
8202 assert_eq!(r.requests(), 2, "header + one coalesced range");
8203 assert_eq!(
8204 r.bytes_read(),
8205 HEADER_LEN as u64 + card.len() as u64 + info.len() as u64
8206 );
8207
8208 let plain = build_with_metadata(&card);
8210 let rp = CountingReader::new(SliceReader::new(&plain));
8211 let (m, b) = read_card_and_build_info_ranged(&rp).unwrap();
8212 assert_eq!(m.unwrap(), card);
8213 assert!(b.is_none());
8214 assert_eq!(rp.requests(), 2);
8215 assert_eq!(
8220 r.requests(),
8221 rp.requests(),
8222 "build info must not cost an extra request",
8223 );
8224
8225 let none = build_image();
8227 let rn = CountingReader::new(SliceReader::new(&none));
8228 let (m, b) = read_card_and_build_info_ranged(&rn).unwrap();
8229 assert!(m.is_none() && b.is_none());
8230 assert_eq!(rn.requests(), 1);
8231 }
8232
8233 #[test]
8234 fn schema_summary_groups_by_type() {
8235 let rt = RDF_TYPE;
8236 let bytes = build_from(&[
8237 ("Alice", rt, "Person"),
8238 ("Bob", rt, "Person"),
8239 ("NYC", rt, "City"),
8240 ("Alice", "knows", "Bob"),
8241 ("Alice", "livesIn", "NYC"),
8242 ("Alice", "name", "\"Alice\""),
8243 ]);
8244 let rete = Rete::open(&bytes).unwrap();
8245 let summary = schema_summary(&rete);
8246 assert!(summary.contains(&("Person".into(), "knows".into(), "Person".into(), 1)));
8248 assert!(summary.contains(&("Person".into(), "livesIn".into(), "City".into(), 1)));
8249 assert!(summary.contains(&("Person".into(), "name".into(), "(literal)".into(), 1)));
8250 assert!(!summary.iter().any(|(_, p, _, _)| p == RDF_TYPE));
8252
8253 let classes = schema_classes(&rete);
8255 assert_eq!(
8256 classes,
8257 vec![("Person".into(), 2u32), ("City".into(), 1u32)]
8258 );
8259 }
8260
8261 fn build_from(triples: &[(&str, &str, &str)]) -> Vec<u8> {
8262 let mut db = DictionaryBuilder::new();
8263 for (s, p, o) in triples {
8264 db.observe(s, p, o);
8265 }
8266 let dict = db.build();
8267 let mut ib = GraphIndexBuilder::new();
8268 for (s, p, o) in triples {
8269 ib.push(dict.encode(s, p, o).unwrap());
8270 }
8271 write_file(&dict, &ib.build(), false, &[], 0)
8272 }
8273
8274 fn build_with_pyramid(triples: &[(&str, &str, &str)]) -> Vec<u8> {
8276 let mut db = DictionaryBuilder::new();
8277 for (s, p, o) in triples {
8278 db.observe(s, p, o);
8279 }
8280 let dict = db.build();
8281 let encoded: Vec<_> = triples
8282 .iter()
8283 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
8284 .collect();
8285 let mut ib = GraphIndexBuilder::new();
8286 for t in &encoded {
8287 ib.push(*t);
8288 }
8289 let (meta, levels) = build_pyramid_meta(&dict, &encoded, DEFAULT_TILE_BUDGET);
8290 write_dataset(&dict, &ib.build(), &[], false, &meta, levels)
8291 }
8292
8293 #[test]
8294 fn tbox_coherence_flags_unsatisfiable_class_index_free() {
8295 use crate::reader::{CountingReader, SliceReader};
8296 let rt = RDF_TYPE;
8297 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
8298 let disj = "<http://www.w3.org/2002/07/owl#disjointWith>";
8299 let bytes = build_with_pyramid(&[
8302 ("<http://ex/C>", sub, "<http://ex/D>"),
8303 ("<http://ex/C>", sub, "<http://ex/E>"),
8304 ("<http://ex/D>", disj, "<http://ex/E>"),
8305 ("<http://ex/x>", rt, "<http://ex/C>"),
8306 ]);
8307
8308 let r = CountingReader::new(SliceReader::new(&bytes));
8309 let view = SummaryView::open_ranged(&r).unwrap().unwrap();
8310 let points = view.tbox_coherence();
8311 assert!(
8312 points
8313 .iter()
8314 .any(|i| i.kind == "unsatisfiable-class" && i.detail.contains("http://ex/C>")),
8315 "expected C unsatisfiable from the schema alone, got {points:?}"
8316 );
8317
8318 let header = Header::from_bytes(&bytes[..HEADER_LEN]).unwrap();
8321 assert!(
8322 r.bytes_read() <= bytes.len() as u64 - header.root_dir_len,
8323 "tbox_coherence must not read the triple index"
8324 );
8325 }
8326
8327 #[test]
8328 fn schema_coherence_reads_only_the_schema_block() {
8329 use crate::reader::{CountingReader, SliceReader};
8330 let rt = RDF_TYPE;
8331 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
8332 let disj = "<http://www.w3.org/2002/07/owl#disjointWith>";
8333 let mut triples: Vec<(String, String, String)> = vec![
8337 ("<http://ex/C>".into(), sub.into(), "<http://ex/D>".into()),
8338 ("<http://ex/C>".into(), sub.into(), "<http://ex/E>".into()),
8339 ("<http://ex/D>".into(), disj.into(), "<http://ex/E>".into()),
8340 ];
8341 for i in 0..500 {
8342 let s = format!("<http://ex/x{i}>");
8343 triples.push((s.clone(), rt.into(), "<http://ex/C>".into()));
8344 triples.push((
8345 s,
8346 "<http://ex/label>".into(),
8347 format!("\"unique label {i}\""),
8348 ));
8349 }
8350 let trefs: Vec<(&str, &str, &str)> = triples
8351 .iter()
8352 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
8353 .collect();
8354 let bytes = build_with_pyramid(&trefs);
8355
8356 let header = Header::from_bytes(&bytes[..HEADER_LEN]).unwrap();
8357 assert!(
8358 header.schema_meta_len > 0,
8359 "the writer recorded a schema-block length"
8360 );
8361 assert!(
8362 (header.schema_meta_len as u64) < header.pyramid_meta_len,
8363 "schema block ({}) should be far smaller than the whole pyramid-meta ({})",
8364 header.schema_meta_len,
8365 header.pyramid_meta_len
8366 );
8367
8368 let r = CountingReader::new(SliceReader::new(&bytes));
8369 let points = read_schema_coherence_ranged(&r).unwrap().unwrap();
8370 assert!(points.iter().any(|i| i.kind == "unsatisfiable-class"));
8371 assert!(
8373 r.bytes_read() <= HEADER_LEN as u64 + header.schema_meta_len as u64,
8374 "read {} bytes; expected <= header + schema block ({})",
8375 r.bytes_read(),
8376 HEADER_LEN as u64 + header.schema_meta_len as u64
8377 );
8378 }
8379
8380 #[test]
8381 fn tbox_coherence_clean_schema_is_coherent() {
8382 use crate::reader::SliceReader;
8383 let rt = RDF_TYPE;
8384 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
8385 let bytes = build_with_pyramid(&[
8386 ("<http://ex/Dog>", sub, "<http://ex/Animal>"),
8387 ("<http://ex/x>", rt, "<http://ex/Dog>"),
8388 ]);
8389 let view = SummaryView::open_ranged(&SliceReader::new(&bytes))
8390 .unwrap()
8391 .unwrap();
8392 assert!(view.tbox_is_coherent(), "a plain hierarchy is coherent");
8393 }
8394
8395 #[test]
8403 fn readers_before_the_quad_index_refuse_its_files() {
8404 for perms in [PermSet::ALL, PermSet::CORE] {
8405 let quads: Vec<crate::ingest::RawQuad> = (0..50)
8406 .map(|i| {
8407 (
8408 format!("<http://ex/s{i}>"),
8409 "<http://ex/p>".to_string(),
8410 format!("\"{i}\""),
8411 Some(format!("<http://ex/g{}>", i % 7)),
8412 )
8413 })
8414 .collect();
8415 let (bytes, _) = crate::ingest::assemble_dataset_with_layout(
8416 quads,
8417 false,
8418 false,
8419 None,
8420 crate::PyramidAlgo::Louvain,
8421 perms,
8422 crate::ingest::GraphLayout::Quad,
8423 |_, _| Vec::<u8>::new(),
8424 );
8425 let h = Header::from_bytes(&bytes).unwrap();
8426 assert!(h.has_quad_index());
8427 let root =
8428 &bytes[h.root_dir_offset as usize..(h.root_dir_offset + h.root_dir_len) as usize];
8429 assert!(decode_index_container(root, h.block_codec, h.perms).is_err());
8431 assert!(
8432 container_section_payload_ranges(root, h.root_dir_offset, h.perms.len()).is_err()
8433 );
8434 assert_ne!(decode_container(root, CODEC_NONE).unwrap().len(), NUM_PERMS);
8436 assert_ne!(
8437 decode_container(root, CODEC_NONE).unwrap().len(),
8438 h.perms.len()
8439 );
8440 let r = crate::reader::SliceReader::new(&bytes);
8442 for expect in [h.perms.len(), NUM_PERMS] {
8443 assert!(locate_container_section_ranged(
8444 &r,
8445 h.root_dir_offset,
8446 h.root_dir_len,
8447 0,
8448 expect as u64
8449 )
8450 .is_err());
8451 }
8452 assert_eq!(hash_of_sections(&h, &bytes).unwrap(), h.content_hash);
8454 let rete = Rete::open(&bytes).unwrap();
8456 assert_eq!(rete.named_graph_count(), 7);
8457 assert_eq!(rete.dump(Some("<http://ex/g3>")).len(), 7);
8458 }
8459 }
8460
8461 #[test]
8462 fn named_graphs_round_trip() {
8463 let all = [
8465 ("Alice", "knows", "Bob"), ("Bob", "age", "30"), ];
8468 let mut db = DictionaryBuilder::new();
8469 for (s, p, o) in all {
8470 db.observe(s, p, o);
8471 }
8472 let dict = db.build();
8473
8474 let mut def = GraphIndexBuilder::new();
8475 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
8476 let mut g1 = GraphIndexBuilder::new();
8477 g1.push(dict.encode("Bob", "age", "30").unwrap());
8478
8479 let named = vec![("http://ex/g1".to_string(), g1.build())];
8480 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8481
8482 assert!(verify(&bytes).unwrap());
8483 let rete = Rete::open(&bytes).unwrap();
8484 assert_eq!(rete.graph_names(), vec!["http://ex/g1"]);
8485 let gi = rete.graph_index("http://ex/g1").unwrap();
8487 assert_eq!(gi.triple_count(), 1);
8488 assert!(rete.graph_index("http://ex/missing").is_none());
8489
8490 assert_eq!(rete.header().quad_count, 2);
8493
8494 assert_eq!(rete.query(Some("Alice"), None, None).len(), 1);
8496
8497 assert_eq!(
8499 rete.dump(None),
8500 vec![("Alice".into(), "knows".into(), "Bob".into())]
8501 );
8502 assert_eq!(
8503 rete.dump(Some("http://ex/g1")),
8504 vec![("Bob".into(), "age".into(), "30".into())]
8505 );
8506 }
8507
8508 #[test]
8514 fn named_graph_container_opens_tile_lazily_like_the_root() {
8515 let mut db = DictionaryBuilder::new();
8516 let node = |n: u32| format!("<http://ex/n{n}>");
8517 let p = "<http://ex/p>".to_string();
8518 db.observe(&node(0), &p, &node(1));
8519 for i in 0..500u32 {
8520 db.observe(&node(1000 + i), &p, &node(2000 + i));
8521 }
8522 let dict = db.build();
8523 let mut def = GraphIndexBuilder::new();
8524 def.push(dict.encode(&node(0), &p, &node(1)).unwrap());
8525 let mut g = GraphIndexBuilder::new().with_tile_budget(256); for i in 0..500u32 {
8527 g.push(dict.encode(&node(1000 + i), &p, &node(2000 + i)).unwrap());
8528 }
8529 let named = vec![("<http://ex/g>".to_string(), g.build())];
8530 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8531 let rete = Rete::open(&bytes).unwrap();
8532 let header = rete.header();
8533
8534 let soff = header.named_graphs_offset as usize;
8537 let send = soff + header.named_graphs_len as usize;
8538 let sec = &bytes[soff..send];
8539 let (n, mut pos) = read_uvarint(sec).unwrap();
8540 assert_eq!(n, 1);
8541 let (ilen, u1) = read_uvarint(&sec[pos..]).unwrap();
8542 pos += u1 + ilen as usize;
8543 let (clen, u2) = read_uvarint(&sec[pos..]).unwrap();
8544 pos += u2;
8545 let container = ByteRange {
8546 offset: (soff + pos) as u64,
8547 len: clen,
8548 };
8549
8550 use crate::reader::SliceReader;
8551 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8552 let reader: std::sync::Arc<dyn RangeReader + Send + Sync> =
8553 std::sync::Arc::new(SliceReader::new(leaked));
8554 let (lazy_idx, _, _) = open_index_container_lazy(
8555 &reader,
8556 container,
8557 header.block_codec,
8558 header.has_tile_synopsis(),
8559 1,
8560 header.perms,
8561 header.perms.len(),
8562 )
8563 .unwrap();
8564 let mut want = rete
8565 .graph_index("<http://ex/g>")
8566 .unwrap()
8567 .match_pattern((None, None, None));
8568 let mut got = lazy_idx.match_pattern((None, None, None));
8569 want.sort_unstable();
8570 got.sort_unstable();
8571 assert_eq!(got.len(), 500);
8572 assert_eq!(got, want);
8573 assert!(!lazy_idx.load_incomplete());
8574 }
8575
8576 #[test]
8582 fn dump_iter_matches_dump_and_stops_early() {
8583 use crate::reader::{CountingReader, SliceReader};
8584
8585 let triples: Vec<(String, String, String)> = (0..2000u32)
8587 .map(|i| {
8588 (
8589 format!("<http://ex/s/{i:04}>"),
8590 "<http://ex/p>".to_string(),
8591 format!("<http://ex/o/{i:04}>"),
8592 )
8593 })
8594 .collect();
8595 let mut db = DictionaryBuilder::new();
8596 for (s, p, o) in &triples {
8597 db.observe(s, p, o);
8598 }
8599 let dict = db.build();
8600 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
8601 for (s, p, o) in &triples {
8602 ib.push(dict.encode(s, p, o).unwrap());
8603 }
8604 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8605
8606 let rete = Rete::open(&bytes).unwrap();
8608 assert_eq!(rete.dump_iter(None).collect::<Vec<_>>(), rete.dump(None));
8609 assert_eq!(rete.dump_iter(Some("http://ex/nope")).count(), 0);
8611
8612 let read_bytes = |take: usize| -> u64 {
8614 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8615 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8616 let rete = Rete::open_ranged_lazy(reader.clone()).unwrap();
8617 let before = reader.bytes_read();
8618 let n = rete.dump_iter(None).take(take).count();
8619 assert_eq!(n, take.min(triples.len()));
8620 assert!(!rete.index_incomplete());
8621 reader.bytes_read() - before
8622 };
8623 let first_only = read_bytes(1);
8624 let everything = read_bytes(triples.len());
8625 assert!(
8627 first_only < everything,
8628 "dump_iter is not lazy: one triple read {first_only} B, a full scan {everything} B"
8629 );
8630 #[cfg(feature = "compression")]
8637 assert!(
8638 first_only * 2 < everything,
8639 "dump_iter is not lazy: one triple read {first_only} B of the {everything} B a full scan reads"
8640 );
8641
8642 let fresh = Rete::open(&bytes).unwrap();
8647 let drain = |max_quads: usize| {
8652 let (mut out, mut cursor, mut calls) = (Vec::new(), 0u32, 0);
8653 loop {
8654 let (batch, next, done) = fresh.dump_batch(None, cursor, max_quads);
8655 out.extend(batch);
8656 cursor = next;
8657 calls += 1;
8658 assert!(calls < 10_000, "cursor failed to advance");
8659 if done {
8660 break out;
8661 }
8662 }
8663 };
8664 let eager = fresh.dump(None);
8665 for max in [1usize, 7, 128, 100_000] {
8666 assert_eq!(drain(max), eager, "batch size {max} changed the dump");
8667 }
8668 let (t, _, done) = fresh.dump_batch(Some("http://ex/nope"), 0, 16);
8671 assert!(t.is_empty() && done);
8672 }
8673
8674 #[test]
8682 fn query_batch_matches_query_in_graph_and_stops_early() {
8683 use crate::reader::{CountingReader, SliceReader};
8684
8685 let triples: Vec<(String, String, String)> = (0..2000u32)
8688 .map(|i| {
8689 (
8690 format!("<http://ex/s/{:04}>", i / 2),
8691 format!("<http://ex/p/{}>", i % 2),
8692 format!("<http://ex/o/{i:04}>"),
8693 )
8694 })
8695 .collect();
8696 let mut db = DictionaryBuilder::new();
8697 for (s, p, o) in &triples {
8698 db.observe(s, p, o);
8699 }
8700 let dict = db.build();
8701 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
8702 for (s, p, o) in &triples {
8703 ib.push(dict.encode(s, p, o).unwrap());
8704 }
8705 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8706 let rete = Rete::open(&bytes).unwrap();
8707
8708 let drain = |s: Option<&str>, p: Option<&str>, o: Option<&str>, max: usize| {
8709 let (mut out, mut cursor, mut calls) = (Vec::new(), 0u64, 0);
8710 loop {
8711 let (batch, next, done) = rete.query_batch(None, s, p, o, cursor, max);
8712 assert!(!batch.is_empty() || done, "empty non-final batch");
8715 out.extend(batch);
8716 cursor = next;
8717 calls += 1;
8718 assert!(calls < 20_000, "cursor failed to advance");
8719 if done {
8720 break out;
8721 }
8722 }
8723 };
8724 let shapes: [(Option<&str>, Option<&str>, Option<&str>); 6] = [
8725 (None, None, None),
8726 (Some("<http://ex/s/0007>"), None, None),
8727 (None, Some("<http://ex/p/1>"), None),
8728 (None, None, Some("<http://ex/o/0042>")),
8729 (Some("<http://ex/s/0007>"), Some("<http://ex/p/0>"), None),
8730 (
8731 Some("<http://ex/s/0007>"),
8732 Some("<http://ex/p/0>"),
8733 Some("<http://ex/o/0014>"),
8734 ),
8735 ];
8736 for (s, p, o) in shapes {
8737 let eager = rete.query_in_graph(None, s, p, o);
8738 assert!(
8739 !eager.is_empty(),
8740 "fixture shape {s:?} {p:?} {o:?} matches nothing"
8741 );
8742 assert_eq!(
8744 rete.query_iter(None, s, p, o).collect::<Vec<_>>(),
8745 eager,
8746 "query_iter disagrees for {s:?} {p:?} {o:?}"
8747 );
8748 for max in [1usize, 3, 64, 100_000] {
8749 let mut got = drain(s, p, o, max);
8750 let mut want = eager.clone();
8751 got.sort();
8754 want.sort();
8755 assert_eq!(got, want, "batch size {max} changed {s:?} {p:?} {o:?}");
8756 }
8757 if s.is_none() && p.is_none() && o.is_none() {
8759 assert_eq!(drain(s, p, o, 7), eager, "unbound batching reordered rows");
8760 }
8761 }
8762
8763 let (t, _, done) = rete.query_batch(None, Some("<http://ex/nope>"), None, None, 0, 16);
8766 assert!(t.is_empty() && done);
8767 let (t, _, done) = rete.query_batch(Some("http://ex/nope"), None, None, None, 0, 16);
8768 assert!(t.is_empty() && done);
8769 assert_eq!(
8770 rete.query_iter(None, Some("<http://ex/nope>"), None, None)
8771 .count(),
8772 0
8773 );
8774
8775 let read_bytes = |take: usize| -> u64 {
8778 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8779 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8780 let rete = Rete::open_ranged_lazy(reader.clone()).unwrap();
8781 let before = reader.bytes_read();
8782 let (mut n, mut cursor) = (0usize, 0u64);
8783 loop {
8784 let (batch, next, done) = rete.query_batch(None, None, None, None, cursor, 1);
8785 n += batch.len();
8786 cursor = next;
8787 if done || n >= take {
8788 break;
8789 }
8790 }
8791 assert!(n >= take.min(triples.len()));
8792 assert!(!rete.index_incomplete());
8793 reader.bytes_read() - before
8794 };
8795 let first_only = read_bytes(1);
8796 let everything = read_bytes(triples.len());
8797 assert!(
8798 first_only < everything,
8799 "query_batch is not lazy: one row read {first_only} B, a full scan {everything} B"
8800 );
8801 #[cfg(feature = "compression")]
8802 assert!(
8803 first_only * 2 < everything,
8804 "query_batch is not lazy: one row read {first_only} B of the {everything} B a full scan reads"
8805 );
8806 }
8807
8808 #[test]
8811 fn query_batch_is_graph_scoped() {
8812 let mut db = DictionaryBuilder::new();
8813 for (s, p, o) in [
8814 ("Alice", "knows", "Bob"),
8815 ("Alice", "knows", "Carol"),
8816 ("Alice", "knows", "Dave"),
8817 ("Erin", "knows", "Frank"),
8818 ("Alice", "knows", "Zoe"),
8819 ] {
8820 db.observe(s, p, o);
8821 }
8822 let dict = db.build();
8823 let mut def = GraphIndexBuilder::new();
8824 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
8825 def.push(dict.encode("Alice", "knows", "Carol").unwrap());
8826 let mut g1 = GraphIndexBuilder::new();
8827 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
8828 g1.push(dict.encode("Erin", "knows", "Frank").unwrap());
8829 let mut g2 = GraphIndexBuilder::new();
8830 g2.push(dict.encode("Alice", "knows", "Zoe").unwrap());
8831 let named = vec![
8832 ("http://ex/g1".to_string(), g1.build()),
8833 ("http://ex/g2".to_string(), g2.build()),
8834 ];
8835 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8836 let rete = Rete::open(&bytes).unwrap();
8837 for graph in [None, Some("http://ex/g1"), Some("http://ex/g2")] {
8838 let eager = rete.query_in_graph(graph, None, None, None);
8839 let (mut out, mut cursor) = (Vec::new(), 0u64);
8840 loop {
8841 let (batch, next, done) = rete.query_batch(graph, None, None, None, cursor, 1);
8842 out.extend(batch);
8843 cursor = next;
8844 if done {
8845 break;
8846 }
8847 }
8848 assert_eq!(out, eager, "graph {graph:?} batched differently");
8849 }
8850 }
8851
8852 #[test]
8853 fn query_in_graph_is_graph_scoped() {
8854 let mut db = DictionaryBuilder::new();
8857 for (s, p, o) in [
8858 ("Alice", "knows", "Bob"),
8859 ("Alice", "knows", "Carol"),
8860 ("Alice", "knows", "Dave"),
8861 ] {
8862 db.observe(s, p, o);
8863 }
8864 let dict = db.build();
8865
8866 let mut def = GraphIndexBuilder::new();
8867 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
8868 def.push(dict.encode("Alice", "knows", "Carol").unwrap());
8869 let mut g1 = GraphIndexBuilder::new();
8870 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
8871
8872 let named = vec![("http://ex/g1".to_string(), g1.build())];
8873 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8874 let rete = Rete::open(&bytes).unwrap();
8875
8876 let mut def_objs: Vec<String> = rete
8878 .query_in_graph(None, Some("Alice"), Some("knows"), None)
8879 .into_iter()
8880 .map(|(_, _, o)| o)
8881 .collect();
8882 def_objs.sort();
8883 assert_eq!(def_objs, vec!["Bob".to_string(), "Carol".to_string()]);
8884
8885 assert_eq!(
8887 rete.query_in_graph(Some("http://ex/g1"), Some("Alice"), None, None),
8888 vec![("Alice".into(), "knows".into(), "Dave".into())]
8889 );
8890
8891 assert_eq!(rete.query_in_graph(None, None, None, None).len(), 2);
8893 assert_eq!(
8894 rete.query_in_graph(Some("http://ex/g1"), None, None, None)
8895 .len(),
8896 1
8897 );
8898
8899 assert!(rete
8901 .query_in_graph(Some("http://ex/missing"), None, None, None)
8902 .is_empty());
8903 }
8904
8905 #[test]
8918 fn bounded_query_in_graph_does_not_fault_the_whole_dictionary() {
8919 use crate::reader::{CountingReader, SliceReader};
8920
8921 let payload = |i: u32| -> String {
8927 let mut s = String::with_capacity(800);
8928 let mut x = i as u64 ^ 0x9e37_79b9_7f4a_7c15;
8929 while s.len() < 800 {
8930 x = x
8931 .wrapping_mul(6364136223846793005)
8932 .wrapping_add(1442695040888963407);
8933 s.push_str(&format!("{:016x}", x ^ (x >> 29)));
8934 }
8935 s
8936 };
8937 let quads: Vec<(String, String, String, bool)> = (0..2000u32)
8944 .map(|i| {
8945 let named = i % 2 == 0;
8946 let ns = if named { 'a' } else { 'b' };
8947 (
8948 format!("<http://ex/{ns}/s/{i:04}>"),
8949 "<http://ex/abstract>".to_string(),
8950 format!("\"{ns}{} number {i}\"", payload(i)),
8951 named,
8952 )
8953 })
8954 .collect();
8955 let mut db = DictionaryBuilder::new();
8956 for (s, p, o, _) in &quads {
8957 db.observe(s, p, o);
8958 }
8959 let dict = db.build();
8960 let mut def = GraphIndexBuilder::new().with_tile_budget(256);
8961 let mut g1 = GraphIndexBuilder::new().with_tile_budget(256);
8962 for (s, p, o, named) in &quads {
8963 let t = dict.encode(s, p, o).unwrap();
8964 if *named {
8965 g1.push(t);
8966 } else {
8967 def.push(t);
8968 }
8969 }
8970 let named = vec![("http://ex/g1".to_string(), g1.build())];
8971 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
8972
8973 let eager = Rete::open(&bytes).unwrap();
8974 let dict_len = eager.header().dictionary_len;
8975
8976 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8979 let probe = |f: &dyn Fn(&Rete) -> Vec<TermTriple>| -> (Vec<TermTriple>, u64) {
8980 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8981 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
8982 let before = reader.bytes_read();
8983 let out = f(&lazy);
8984 assert!(!lazy.index_incomplete(), "a chunk fetch failed");
8985 (out, reader.bytes_read() - before)
8986 };
8987
8988 let want = eager.query_in_graph(
8990 Some("http://ex/g1"),
8991 Some("<http://ex/a/s/0100>"),
8992 None,
8993 None,
8994 );
8995 assert_eq!(want.len(), 1, "fixture: expected exactly one match");
8996 let (got, pulled) = probe(&|r| {
8997 r.query_in_graph(
8998 Some("http://ex/g1"),
8999 Some("<http://ex/a/s/0100>"),
9000 None,
9001 None,
9002 )
9003 });
9004 assert_eq!(got, want, "lazy query_in_graph disagrees with eager");
9005 assert!(
9006 pulled * 4 < dict_len,
9007 "a 1-row query_in_graph faulted {pulled} B of a {dict_len} B dictionary — \
9008 it is prefetching the whole thing again"
9009 );
9010
9011 let (empty, q_pulled) =
9019 probe(&|r| r.query_in_graph(Some("http://ex/nope"), None, None, None));
9020 assert!(empty.is_empty());
9021 let (_, d_pulled) = probe(&|r| {
9022 let mut n = 0usize;
9023 r.dump_each(Some("http://ex/nope"), |_, _, _| n += 1);
9024 assert_eq!(n, 0, "an absent graph yielded triples");
9025 Vec::new()
9026 });
9027 assert_eq!(
9028 q_pulled, d_pulled,
9029 "query_in_graph and dump_each disagree on what an absent graph costs"
9030 );
9031 assert!(
9032 q_pulled * 4 < dict_len,
9033 "an absent graph faulted {q_pulled} B of a {dict_len} B dictionary"
9034 );
9035
9036 let want: Vec<TermTriple> = eager.dump(Some("http://ex/g1"));
9039 assert_eq!(want.len(), 1000);
9040 let (got, pulled) = probe(&|r| {
9041 let mut out = Vec::new();
9042 r.dump_each(Some("http://ex/g1"), |s, p, o| {
9043 out.push((s.to_string(), p.to_string(), o.to_string()))
9044 });
9045 out
9046 });
9047 assert_eq!(got, want, "lazy dump_each disagrees with eager dump");
9048 assert!(
9049 pulled * 3 < dict_len * 2,
9050 "dumping half the graph faulted {pulled} B of a {dict_len} B dictionary — \
9051 the other graph's chunks are being pulled in too"
9052 );
9053 }
9054
9055 #[cfg(test)]
9060 fn build_multichunk_image() -> Vec<u8> {
9061 use crate::index::PermSet;
9062 let mut db = DictionaryBuilder::new();
9063 let mut triples: Vec<(String, String, String)> = (0..4000u32)
9064 .map(|i| {
9065 (
9066 format!("<http://ex/s/{i:07}>"),
9067 format!("<http://ex/p/{}>", i % 8),
9068 format!("\"object literal number {i:07} with padding text to widen the term\""),
9069 )
9070 })
9071 .collect();
9072 triples.push((
9073 "<http://ex/s/big>".to_string(),
9074 "<http://ex/p/big>".to_string(),
9075 format!("\"{}\"", "Z".repeat(8000)),
9076 ));
9077 for (s, p, o) in &triples {
9078 db.observe(s, p, o);
9079 }
9080 let dict = db.build();
9081 let mut idx = GraphIndexBuilder::new()
9082 .with_tile_budget(64)
9083 .with_perms(PermSet::ALL);
9084 for (s, p, o) in &triples {
9085 idx.push(dict.encode(s, p, o).unwrap());
9086 }
9087 write_dataset(&dict, &idx.build(), &[], true, &[], 0)
9088 }
9089
9090 #[test]
9099 fn tiny_dict_cache_evicts_yet_dump_is_byte_identical() {
9100 use crate::reader::SliceReader;
9101 let leaked: &'static [u8] = Box::leak(build_multichunk_image().into_boxed_slice());
9102 let dump = |cap: u64| -> (
9103 Vec<(String, String, String)>,
9104 crate::chunk_cache::CacheStats,
9105 ) {
9106 let rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9107 rete.dict.set_cache_cap(cap);
9108 let mut out = Vec::new();
9109 rete.dump_filtered_each(None, None, None, None, |s, p, o| {
9110 out.push((s.to_string(), p.to_string(), o.to_string()))
9111 });
9112 assert!(
9113 !rete.index_incomplete(),
9114 "no lazy fault should fail at cap {cap}"
9115 );
9116 let stats = rete.dict_cache_stats();
9117 (out, stats)
9118 };
9119
9120 let (unlimited, us) = dump(u64::MAX);
9121 assert_eq!(unlimited.len(), 4001, "every triple dumped");
9122 assert_eq!(us.evictions, 0, "unlimited cap evicts nothing");
9123
9124 let (tiny, ts) = dump(128 * 1024);
9127 assert_eq!(tiny, unlimited, "128 KiB-cap dump differs from unlimited");
9128 assert!(ts.evictions > 0, "a tiny cap must evict (stats {ts:?})");
9129
9130 let (starved, ss) = dump(1);
9132 assert_eq!(starved, unlimited, "1-byte-cap dump differs from unlimited");
9133 assert!(
9134 ss.evictions > 0,
9135 "1-byte cap evicts every insert (stats {ss:?})"
9136 );
9137 }
9138
9139 #[test]
9150 fn memory_budget_charges_the_fixed_directories() {
9151 use crate::reader::SliceReader;
9152 let leaked: &'static [u8] = Box::leak(build_multichunk_image().into_boxed_slice());
9153 let rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9154
9155 let fixed = rete.fixed_overhead();
9156 assert!(
9159 fixed.dict_directory > 0 && fixed.index_directory > 0,
9160 "{fixed:?}"
9161 );
9162 assert_eq!(fixed.named_graphs, 0, "no named graphs in this file");
9163 assert_eq!(
9164 fixed.total(),
9165 fixed.dict_directory + fixed.index_directory + fixed.named_graphs
9166 );
9167
9168 let total: u64 = 512 << 20;
9171 let applied = rete.set_memory_budget(Some(total));
9172 let plain = split_memory_budget(total);
9173 assert_eq!(applied.dict_cache, plain.dict_cache - fixed.total());
9174 assert_eq!(rete.dict_cache_cap(), applied.dict_cache);
9175 assert_eq!(applied.tile_cache, plain.tile_cache);
9176 assert_eq!(applied.block_cache, plain.block_cache);
9177 assert!(
9178 applied.dict_cache + applied.tile_cache + applied.block_cache + fixed.total() <= total,
9179 "caps + fixed directories exceed the budget: {applied:?} + {fixed:?} > {total}"
9180 );
9181
9182 let applied = rete.set_memory_budget(Some(1 << 20));
9184 assert_eq!(applied.dict_cache, MIN_DICT_CACHE_CAP);
9185
9186 let applied = rete.set_memory_budget(None);
9188 assert_eq!(applied.dict_cache, u64::MAX);
9189 assert_eq!(rete.dict_cache_cap(), u64::MAX);
9190
9191 let rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9194 rete.dict.set_cache_cap(128 * 1024);
9195 let mut rows = 0usize;
9196 rete.dump_filtered_each(None, None, None, None, |_, _, _| rows += 1);
9197 assert_eq!(rows, 4001);
9198 let s = rete.dict_cache_stats();
9199 assert!(s.evictions > 0, "the cap must bind: {s:?}");
9200 assert!(
9203 s.entries <= 1 || s.resident_bytes <= 128 * 1024,
9204 "dictionary cache over its cap after the dump: {s:?}"
9205 );
9206 }
9207
9208 #[test]
9213 fn releasing_a_named_graph_drops_and_reopens_its_index() {
9214 use crate::index::PermSet;
9215 use crate::reader::SliceReader;
9216 let mut db = DictionaryBuilder::new();
9217 let all: Vec<(String, String, String)> = (0..40u32)
9218 .map(|i| {
9219 (
9220 format!("<http://ex/s{}>", i % 7),
9221 format!("<http://ex/p{}>", i % 3),
9222 format!("<http://ex/o{}>", i % 5),
9223 )
9224 })
9225 .collect();
9226 for (s, p, o) in &all {
9227 db.observe(s, p, o);
9228 }
9229 let dict = db.build();
9230 let mk = |take: fn(u32) -> bool| {
9231 let mut g = GraphIndexBuilder::new()
9232 .with_tile_budget(16)
9233 .with_perms(PermSet::ALL);
9234 for (i, (s, p, o)) in all.iter().enumerate() {
9235 if take(i as u32) {
9236 g.push(dict.encode(s, p, o).unwrap());
9237 }
9238 }
9239 g.build()
9240 };
9241 let def = mk(|_| true);
9242 let g1 = mk(|i| i % 2 == 0);
9243 let g2 = mk(|i| i % 3 == 0);
9244 let image = write_dataset(
9245 &dict,
9246 &def,
9247 &[
9248 ("<http://ex/g1>".to_string(), g1),
9249 ("<http://ex/g2>".to_string(), g2),
9250 ],
9251 true,
9252 &[],
9253 0,
9254 );
9255 let leaked: &'static [u8] = Box::leak(image.into_boxed_slice());
9256 let mut rete = Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap();
9257
9258 let opened = |r: &Rete| -> usize {
9259 match &r.named_graphs {
9260 NamedGraphsSlot::Lazy(l) => {
9261 let mut n = 0;
9262 l.for_each_opened(|_| n += 1);
9263 n
9264 }
9265 _ => 0,
9266 }
9267 };
9268
9269 assert_eq!(opened(&rete), 0, "nothing opened before first touch");
9270 assert!(rete.graph_index("<http://ex/g1>").is_some());
9272 assert_eq!(opened(&rete), 1, "g1 opened");
9273 rete.release_named_graph("<http://ex/g1>");
9275 assert_eq!(opened(&rete), 0, "g1 released");
9276 rete.release_named_graph("<http://ex/absent>");
9278 rete.release_named_graph("<http://ex/g1>");
9279 assert_eq!(opened(&rete), 0);
9280 assert!(rete.graph_index("<http://ex/g1>").is_some());
9282 assert_eq!(opened(&rete), 1, "g1 re-opened after release");
9283 }
9284
9285 #[test]
9303 fn filtered_dump_is_exactly_the_unfiltered_dump_filtered() {
9304 use crate::index::PermSet;
9305 use crate::reader::SliceReader;
9306
9307 let terms = |i: u32| {
9308 (
9309 format!("<http://ex/s{}>", i % 11),
9310 format!("<http://ex/p{}>", i % 3),
9311 if i.is_multiple_of(4) {
9315 format!("\"lit {}\"@en", i % 5)
9316 } else {
9317 format!("<http://ex/o{}>", i % 7)
9318 },
9319 )
9320 };
9321 let build = |perms: PermSet| {
9322 let mut db = DictionaryBuilder::new();
9323 let all: Vec<_> = (0..90u32).map(terms).collect();
9324 for (s, p, o) in &all {
9325 db.observe(s, p, o);
9326 }
9327 let dict = db.build();
9328 let mut def = GraphIndexBuilder::new()
9329 .with_tile_budget(16)
9330 .with_perms(perms);
9331 let mut g1 = GraphIndexBuilder::new()
9332 .with_tile_budget(16)
9333 .with_perms(perms);
9334 for (i, (s, p, o)) in all.iter().enumerate() {
9335 let t = dict.encode(s, p, o).unwrap();
9336 if i % 2 == 0 {
9340 g1.push(t);
9341 }
9342 def.push(t);
9343 }
9344 write_dataset(
9345 &dict,
9346 &def.build(),
9347 &[("<http://ex/g1>".to_string(), g1.build())],
9348 true,
9349 &[],
9350 0,
9351 )
9352 };
9353
9354 let shapes: Vec<(Option<&str>, Option<&str>, Option<&str>)> = vec![
9355 (None, None, None),
9356 (Some("<http://ex/s3>"), None, None),
9357 (None, Some("<http://ex/p1>"), None),
9358 (None, None, Some("<http://ex/o5>")),
9359 (None, None, Some("\"lit 0\"@en")),
9360 (Some("<http://ex/s3>"), Some("<http://ex/p0>"), None),
9361 (Some("<http://ex/s4>"), None, Some("<http://ex/o4>")),
9362 (None, Some("<http://ex/p1>"), Some("<http://ex/o5>")),
9363 (
9364 Some("<http://ex/s3>"),
9365 Some("<http://ex/p0>"),
9366 Some("<http://ex/o3>"),
9367 ),
9368 (Some("<http://ex/nope>"), None, None),
9372 (None, Some("<http://ex/nope>"), None),
9373 (None, None, Some("<http://ex/nope>")),
9374 ];
9375
9376 let mut any_pruned = false;
9377 for perms in [PermSet::ALL, PermSet::CORE] {
9378 let image = build(perms);
9379 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
9380 let opens: Vec<(&str, Rete)> = vec![
9381 ("resident", Rete::open(&image).unwrap()),
9382 (
9383 "ranged",
9384 Rete::open_ranged(&SliceReader::new(leaked)).unwrap(),
9385 ),
9386 (
9387 "lazy",
9388 Rete::open_ranged_lazy(SliceReader::new(leaked)).unwrap(),
9389 ),
9390 ];
9391 for graph in [None, Some("<http://ex/g1>"), Some("<http://ex/absent>")] {
9392 let full: Vec<TermTriple> = {
9394 let mut v = Vec::new();
9395 Rete::open(&image).unwrap().dump_each(graph, |s, p, o| {
9396 v.push((s.to_string(), p.to_string(), o.to_string()))
9397 });
9398 v.sort();
9399 v
9400 };
9401 for &(s, p, o) in &shapes {
9402 let mut want = full.clone();
9403 want.retain(|(ts, tp, to)| {
9404 s.is_none_or(|x| x == ts)
9405 && p.is_none_or(|x| x == tp)
9406 && o.is_none_or(|x| x == to)
9407 });
9408 for (path, rete) in &opens {
9409 let mut got = Vec::new();
9410 rete.dump_filtered_each(graph, s, p, o, |s, p, o| {
9411 got.push((s.to_string(), p.to_string(), o.to_string()))
9412 });
9413 got.sort();
9414 assert_eq!(
9415 got,
9416 want,
9417 "{path}/{perms:?} graph={graph:?} pattern={:?} — filtered dump \
9418 is not the unfiltered dump filtered",
9419 (s, p, o)
9420 );
9421 assert!(!rete.index_incomplete(), "{path}: a fetch failed");
9422 }
9423 let plan = opens[2].1.dump_plan(graph, s, p, o);
9427 if graph == Some("<http://ex/absent>")
9428 || [s, p, o].contains(&Some("<http://ex/nope>"))
9429 {
9430 assert!(
9431 plan.scan.is_none(),
9432 "an unmatchable dump still planned a scan: {:?}",
9433 (graph, s, p, o)
9434 );
9435 } else {
9436 let scan = plan.scan.expect("a matchable dump plans a scan");
9437 assert!(
9438 PermSet::CORE.contains(scan.permutation),
9439 "a dump routed to {:?}, outside PermSet::CORE — a \
9440 --permutations 3 file has no such section",
9441 scan.permutation
9442 );
9443 assert!(scan.tiles_admitted <= scan.tiles_routed);
9444 assert!(scan.tiles_routed <= scan.tiles_total);
9445 assert!(scan.tile_bytes <= scan.section_bytes);
9446 if scan.tiles_admitted < scan.tiles_total {
9447 any_pruned = true;
9448 }
9449 }
9450 }
9451 }
9452 }
9453 assert!(
9454 any_pruned,
9455 "no shape pruned a single tile — the fixture is too small to be \
9456 measuring anything"
9457 );
9458 }
9459
9460 #[test]
9470 fn a_predicate_scoped_dump_fetches_less_than_the_graph() {
9471 use crate::reader::{CountingReader, SliceReader};
9472
9473 let mut db = DictionaryBuilder::new();
9479 let mut all = Vec::new();
9480 for s in 0..200u32 {
9481 for p in 0..20u32 {
9482 let t = (
9483 format!("<http://ex/s/{s:04}>"),
9484 format!("<http://ex/p/{p:02}>"),
9485 format!("\"object {s:04}/{p:02}\""),
9486 );
9487 db.observe(&t.0, &t.1, &t.2);
9488 all.push(t);
9489 }
9490 }
9491 let dict = db.build();
9492 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
9493 for (s, p, o) in &all {
9494 ib.push(dict.encode(s, p, o).unwrap());
9495 }
9496 let image = write_dataset(&dict, &ib.build(), &[], false, &[], 0);
9497 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
9498
9499 let probe = |s: Option<&str>, p: Option<&str>, o: Option<&str>| -> (usize, u64) {
9501 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9502 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
9503 let before = reader.bytes_read();
9504 let mut n = 0usize;
9505 lazy.dump_filtered_each(None, s, p, o, |_, _, _| n += 1);
9506 assert!(!lazy.index_incomplete());
9507 (n, reader.bytes_read() - before)
9508 };
9509
9510 let (full_rows, full_bytes) = probe(None, None, None);
9511 let (pred_rows, pred_bytes) = probe(None, Some("<http://ex/p/07>"), None);
9512 let (subj_rows, subj_bytes) = probe(Some("<http://ex/s/0021>"), None, None);
9513 assert_eq!(full_rows, 4000);
9514 assert_eq!(pred_rows, 200, "one predicate covers every subject once");
9515 assert_eq!(subj_rows, 20);
9516 assert!(
9518 pred_bytes < full_bytes && subj_bytes < full_bytes,
9519 "a slice fetched {pred_bytes} / {subj_bytes} B where the whole graph fetched \
9520 {full_bytes} B — the scan is not being pruned at all"
9521 );
9522 #[cfg(feature = "compression")]
9533 assert!(
9534 pred_bytes * 4 < full_bytes && subj_bytes * 4 < full_bytes,
9535 "a 1-in-20 predicate slice fetched {pred_bytes} B and one subject \
9536 {subj_bytes} B where the whole graph fetched {full_bytes} B"
9537 );
9538
9539 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9541 let lazy = Rete::open_ranged_lazy(reader.clone()).unwrap();
9542 let before = reader.bytes_read();
9543 let plan = lazy
9544 .dump_plan(None, None, Some("<http://ex/p/07>"), None)
9545 .scan
9546 .expect("a known predicate plans a scan");
9547 assert!(
9548 reader.bytes_read() - before < 64 * 1024,
9549 "planning fetched {} B — it is supposed to read directories, not tiles",
9550 reader.bytes_read() - before
9551 );
9552 assert!(plan.tiles_admitted < plan.tiles_total);
9553 assert!(
9559 plan.tile_bytes * 5 < plan.section_bytes,
9560 "one predicate of twenty planned {} B of a {} B section",
9561 plan.tile_bytes,
9562 plan.section_bytes
9563 );
9564 }
9565
9566 #[test]
9567 fn query_quads_tags_every_graph() {
9568 let mut db = DictionaryBuilder::new();
9569 for (s, p, o) in [("Alice", "knows", "Bob"), ("Alice", "knows", "Dave")] {
9570 db.observe(s, p, o);
9571 }
9572 let dict = db.build();
9573 let mut def = GraphIndexBuilder::new();
9574 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
9575 let mut g1 = GraphIndexBuilder::new();
9576 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
9577 let named = vec![("http://ex/g1".to_string(), g1.build())];
9578 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9579 let rete = Rete::open(&bytes).unwrap();
9580
9581 let quads = rete.query_quads(Some("Alice"), Some("knows"), None);
9583 assert_eq!(quads.len(), 2);
9584 assert_eq!(
9585 quads[0],
9586 (("Alice".into(), "knows".into(), "Bob".into()), None)
9587 );
9588 assert_eq!(
9589 quads[1],
9590 (
9591 ("Alice".into(), "knows".into(), "Dave".into()),
9592 Some("http://ex/g1".to_string())
9593 )
9594 );
9595
9596 assert!(rete.query_quads(Some("Nobody"), None, None).is_empty());
9598 }
9599
9600 #[test]
9601 fn pyramid_meta_round_trips_in_file() {
9602 let rete = Rete::open(&build_image()).unwrap();
9603 let pyr = rete.pyramid().expect("file has a pyramid");
9604 let total: u32 = pyr.summary.iter().map(|e| e.count).sum();
9606 assert_eq!(total, 3);
9607 assert!(!pyr.summary.is_empty());
9608 assert!(pyr.tiles.is_empty());
9609 }
9610
9611 #[test]
9612 fn schema_pyramid_round_trips_through_file_index_free() {
9613 use crate::reader::{CountingReader, SliceReader};
9614 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
9615 let q = |s: &str, p: &str, o: &str| {
9616 (s.to_string(), p.to_string(), o.to_string(), None::<String>)
9617 };
9618 let quads = vec![
9620 q("<a>", RDF_TYPE, "<Astronomer>"),
9621 q("<b>", RDF_TYPE, "<Astronomer>"),
9622 q("<c>", RDF_TYPE, "<Person>"),
9623 q("<Astronomer>", sub, "<Scientist>"),
9624 q("<Scientist>", sub, "<Person>"),
9625 q("<Person>", sub, "<Agent>"),
9626 q("<a>", "<knows>", "<b>"),
9627 q("<b>", "<knows>", "<c>"),
9628 ];
9629 let (bytes, _) =
9630 crate::ingest::assemble_dataset_with_opts(quads, true, false, None, |_, _| Vec::new());
9631
9632 let rete = Rete::open(&bytes).unwrap();
9634 let pyr = rete.pyramid().expect("pyramid present");
9635 assert!(!pyr.level_rollups.is_empty(), "schema pyramid shipped");
9636 assert!(pyr
9637 .class_hierarchy
9638 .iter()
9639 .any(|n| n.class == "<Agent>" && n.depth == 0));
9640
9641 let r = CountingReader::new(SliceReader::new(&bytes));
9643 let view = SummaryView::open_ranged(&r).unwrap().unwrap();
9644 assert!(view.level_count() >= 2, "multi-level pyramid");
9645 let coarse = view.level_rollup(0).unwrap();
9646 assert!(
9647 coarse.classes.iter().any(|(c, _)| c == "<Agent>"),
9648 "coarsest level rolls up to the root Agent"
9649 );
9650 let h = Header::from_bytes(&bytes).unwrap();
9651 assert!(
9652 r.bytes_read() <= bytes.len() as u64 - h.root_dir_len,
9653 "summary read {} bytes; the {}-byte index section must be skipped",
9654 r.bytes_read(),
9655 h.root_dir_len
9656 );
9657 }
9658
9659 #[test]
9660 fn predicate_totals_from_summary_only() {
9661 use crate::reader::SliceReader;
9662 let bytes = build_image();
9664 let reader = SliceReader::new(&bytes);
9665 let view = SummaryView::open_ranged(&reader).unwrap().unwrap();
9666 assert_eq!(view.predicate_total("knows"), 2);
9667 assert_eq!(view.predicate_total("age"), 1);
9668 assert_eq!(view.predicate_total("missing"), 0);
9669 let totals = view.predicate_totals();
9670 assert_eq!(totals[0], ("knows".to_string(), 2)); }
9672
9673 #[test]
9674 fn query_patterns_resolve_to_terms() {
9675 let rete = Rete::open(&build_image()).unwrap();
9676
9677 assert_eq!(rete.query(None, None, None).len(), 3);
9679
9680 let mut alice = rete.query(Some("Alice"), None, None);
9682 alice.sort();
9683 assert_eq!(
9684 alice,
9685 vec![
9686 ("Alice".into(), "age".into(), "30".into()),
9687 ("Alice".into(), "knows".into(), "Bob".into()),
9688 ]
9689 );
9690
9691 assert_eq!(rete.query(None, Some("knows"), None).len(), 2);
9693
9694 assert_eq!(
9696 rete.query(Some("Bob"), Some("knows"), Some("Carol")),
9697 vec![("Bob".into(), "knows".into(), "Carol".into())]
9698 );
9699 assert!(rete.query(Some("Nobody"), None, None).is_empty());
9700 assert!(rete.query(None, Some("likes"), None).is_empty());
9701 }
9702
9703 #[test]
9704 fn query_provenance_reports_terms_ids_sections_and_index_choice() {
9705 let bytes = build_image();
9706 let rete = Rete::open(&bytes).unwrap();
9707
9708 let mut matches = rete.query_with_provenance(None, Some("knows"), None);
9709 matches.sort_by(|a, b| a.terms.cmp(&b.terms));
9710
9711 assert_eq!(matches.len(), 2);
9712 assert_eq!(
9713 matches[0].terms,
9714 ("Alice".into(), "knows".into(), "Bob".into())
9715 );
9716 assert_eq!(
9717 matches[0].ids,
9718 rete.dictionary().encode("Alice", "knows", "Bob").unwrap()
9719 );
9720 assert_eq!(matches[0].graph.as_deref(), None);
9721 assert_eq!(
9722 matches[0].matched_pattern,
9723 (None, Some(matches[0].ids.1), None)
9724 );
9725 assert_eq!(
9726 matches[0].index_permutation,
9727 crate::index::IndexPermutation::Pos
9728 );
9729
9730 let h = rete.header();
9731 assert_eq!(matches[0].dictionary_range.offset, h.dictionary_offset);
9732 assert_eq!(matches[0].dictionary_range.len, h.dictionary_len);
9733 assert_eq!(matches[0].index_range.offset, h.root_dir_offset);
9734 assert_eq!(matches[0].index_range.len, h.root_dir_len);
9735 assert!(
9736 matches[0].index_section_range.offset > h.root_dir_offset,
9737 "POS is section 1, so its payload starts after the container header and SPO payload"
9738 );
9739 assert!(matches[0].index_section_range.len > 0);
9740 assert!(matches[0].index_section_range.end() <= matches[0].index_range.end());
9741 assert!(matches[0].index_section_range.len < matches[0].index_range.len);
9742 assert_eq!(
9743 matches[0].pyramid_range.as_ref().map(|r| (r.offset, r.len)),
9744 Some((h.pyramid_meta_offset, h.pyramid_meta_len))
9745 );
9746 let tile_range = matches[0].tile_range.expect("tiled file reports a tile");
9749 assert!(matches[0]
9750 .tile
9751 .as_deref()
9752 .unwrap()
9753 .starts_with(matches[0].index_permutation.name()));
9754 assert!(matches[0].index_section_range.offset <= tile_range.offset);
9755 assert!(tile_range.end() <= matches[0].index_section_range.end());
9756 }
9757
9758 fn build_labeled(n: usize) -> Vec<u8> {
9762 const LABEL: &str = "<http://www.w3.org/2000/01/rdf-schema#label>";
9763 const WORDS: &[&str] = &[
9764 "alanine",
9765 "benzene",
9766 "glucose",
9767 "dextrose",
9768 "ethanol",
9769 "formate",
9770 "heptane",
9771 "isoleucine",
9772 ];
9773 let triples: Vec<(String, String, String)> = (0..n)
9774 .flat_map(|i| {
9775 let s = format!("<http://ex/e{i}>");
9776 let w = WORDS[i % WORDS.len()];
9777 [
9778 (s.clone(), LABEL.to_string(), format!("\"{w}-{i:06}\"")),
9779 (
9780 s,
9781 "<http://ex/p>".to_string(),
9782 format!("<http://ex/c{}>", i % 64),
9783 ),
9784 ]
9785 })
9786 .collect();
9787 let mut db = DictionaryBuilder::new();
9788 for (s, p, o) in &triples {
9789 db.observe(s, p, o);
9790 }
9791 let dict = db.build();
9792 let ids: Vec<(u32, u32, u32)> = triples
9793 .iter()
9794 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
9795 .collect();
9796 let mut ib = GraphIndexBuilder::new();
9797 for &t in &ids {
9798 ib.push(t);
9799 }
9800 let (meta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
9801 write_file(&dict, &ib.build(), false, &meta, levels)
9802 }
9803
9804 #[test]
9805 fn prefix_search_matches_a_filter_scan() {
9806 let bytes = build_labeled(800);
9809 let rete = Rete::open(&bytes).unwrap();
9810 let idx_subjects: std::collections::BTreeSet<String> = rete
9811 .prefix_search("glucose", 10_000)
9812 .into_iter()
9813 .map(|(_label, subject)| subject)
9814 .collect();
9815 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
9817 FILTER(STRSTARTS(LCASE(?l), \"glucose\")) }";
9818 let crate::QueryOutput::Select(_, rows) = crate::eval_query(&rete, q).unwrap() else {
9819 panic!("expected SELECT");
9820 };
9821 let scan_subjects: std::collections::BTreeSet<String> =
9822 rows.iter().map(|r| r.get("s").cloned().unwrap()).collect();
9823 assert_eq!(idx_subjects, scan_subjects, "index agrees with the scan");
9824 assert_eq!(
9825 idx_subjects.len(),
9826 100,
9827 "800/8 words = 100 glucose-* labels"
9828 );
9829 }
9830
9831 #[test]
9835 #[ignore]
9836 fn bench_prefix_search_vs_filter_scan() {
9837 use std::time::Instant;
9838 let n = 6000; let bytes = build_labeled(n);
9840 let rete = Rete::open(&bytes).unwrap();
9841 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
9842 FILTER(STRSTARTS(LCASE(?l), \"glucose\")) }";
9843 let reps = 200;
9844 let idx_n = rete.prefix_search("glucose", 100_000).len();
9845 let t = Instant::now();
9846 for _ in 0..reps {
9847 std::hint::black_box(rete.prefix_search("glucose", 100_000));
9848 }
9849 let idx_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
9850 let t = Instant::now();
9851 for _ in 0..reps {
9852 let _ = std::hint::black_box(crate::eval_query(&rete, q).unwrap());
9853 }
9854 let scan_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
9855 println!(
9856 "label prefix search over {n} labeled subjects ({idx_n} matches): \
9857 index {idx_ms:.4} ms vs FILTER scan {scan_ms:.3} ms ({:.0}× faster)",
9858 scan_ms / idx_ms
9859 );
9860 }
9861
9862 #[test]
9866 #[ignore]
9867 fn bench_text_search_vs_contains_scan() {
9868 use std::time::Instant;
9869 const LABEL: &str = "<http://www.w3.org/2000/01/rdf-schema#label>";
9870 const WORDS: &[&str] = &[
9871 "alanine",
9872 "benzene",
9873 "glucose",
9874 "dextrose",
9875 "ethanol",
9876 "formate",
9877 "heptane",
9878 "isoleucine",
9879 ];
9880 let n = 6000;
9881 let triples: Vec<(String, String, String)> = (0..n)
9882 .map(|i| {
9883 (
9884 format!("<http://ex/e{i}>"),
9885 LABEL.to_string(),
9886 format!("\"{} sample number {i:06}\"", WORDS[i % WORDS.len()]),
9887 )
9888 })
9889 .collect();
9890 let mut db = DictionaryBuilder::new();
9891 for (s, p, o) in &triples {
9892 db.observe(s, p, o);
9893 }
9894 let dict = db.build();
9895 let ids: Vec<(u32, u32, u32)> = triples
9896 .iter()
9897 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
9898 .collect();
9899 let mut ib = GraphIndexBuilder::new();
9900 for &t in &ids {
9901 ib.push(t);
9902 }
9903 let ti = compute_text_index(&dict, &ids);
9904 let bytes = write_dataset_with_metadata(&dict, &ib.build(), &[], false, &[], 0, &[], &ti);
9905 let rete = Rete::open(&bytes).unwrap();
9906
9907 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
9908 FILTER(CONTAINS(LCASE(?l), \"glucose\")) }";
9909 let reps = 200;
9910 let idx_n = rete.text_search(&["glucose"], None, 100_000).len();
9911 let t = Instant::now();
9912 for _ in 0..reps {
9913 std::hint::black_box(rete.text_search(&["glucose"], None, 100_000));
9914 }
9915 let idx_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
9916 let t = Instant::now();
9917 for _ in 0..reps {
9918 let _ = std::hint::black_box(crate::eval_query(&rete, q).unwrap());
9919 }
9920 let scan_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
9921 println!(
9922 "text search over {n} literals ({idx_n} matches): \
9923 index {idx_ms:.4} ms vs FILTER(CONTAINS) scan {scan_ms:.3} ms ({:.0}× faster)",
9924 scan_ms / idx_ms
9925 );
9926 }
9927
9928 #[test]
9932 #[ignore = "operational tool, driven by RETE_DEBUG_* env vars"]
9933 fn debug_bound_po_routing() {
9934 struct FR(std::fs::File);
9935 impl crate::RangeReader for FR {
9936 fn len(&self) -> u64 {
9937 self.0.metadata().map(|m| m.len()).unwrap_or(0)
9938 }
9939 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
9940 use std::os::unix::fs::FileExt;
9941 let mut buf = vec![0u8; len as usize];
9942 self.0.read_exact_at(&mut buf, offset)?;
9943 Ok(buf)
9944 }
9945 }
9946 let path = std::env::var("RETE_DEBUG_FILE").expect("RETE_DEBUG_FILE");
9947 let p_iri = std::env::var("RETE_DEBUG_P").expect("RETE_DEBUG_P");
9948 let o_iri = std::env::var("RETE_DEBUG_O").expect("RETE_DEBUG_O");
9949 let rete =
9950 Rete::open_ranged_lazy(std::sync::Arc::new(FR(std::fs::File::open(&path).unwrap())))
9951 .unwrap();
9952 let pid = rete.dict.predicate_id(&p_iri).expect("p resolves");
9953 let oid = rete.dict.object_id(&o_iri).expect("o resolves");
9954 eprintln!("pid={pid} oid={oid}");
9955 let pattern = (None, Some(pid), Some(oid));
9956 let perm = GraphIndex::best_permutation(pattern);
9957 eprintln!("best_permutation = {}", perm.name());
9958 let si = perm.section_index();
9959 let tiles = &rete.index.sections[si];
9960 eprintln!("section {} tiles = {}", perm.name(), tiles.len());
9961 let [pa, pb, pc] = perm.order_pattern(pattern);
9962 eprintln!("permuted pattern pa={pa:?} pb={pb:?} pc={pc:?}");
9963 let (start, end) = rete.index.tile_span(si, pa);
9964 eprintln!("tile_span = [{start}, {end}) -> {} tiles", end - start);
9965 let mut admitted = 0usize;
9966 for (ti, t) in tiles.iter().enumerate().take(end).skip(start) {
9967 if t.syn_admits(pb, pc) {
9968 admitted += 1;
9969 if admitted <= 10 {
9970 let (lo, hi) = t.leading_range();
9971 eprintln!(" admit tile {ti}: a=[{lo},{hi}] syn={:?}", t.syn);
9972 }
9973 }
9974 }
9975 eprintln!("admitted {admitted} tile(s) by synopsis");
9976 let n = rete.index.scan_iter(pattern).count();
9977 eprintln!("scan_iter matches = {n}");
9978 let hi_res = rete.query(None, Some(&p_iri), Some(&o_iri));
9979 eprintln!("high-level query matches = {}", hi_res.len());
9980 }
9981
9982 #[test]
9990 fn layout_header_label_matches_its_span() {
9991 let mut db = DictionaryBuilder::new();
9992 let mut triples = Vec::new();
9993 for i in 0..24u32 {
9994 let (s, p, o) = (
9995 format!("<http://ex/s{}>", i % 5),
9996 format!("<http://ex/p{}>", i % 3),
9997 format!("<http://ex/o{i}>"),
9998 );
9999 db.observe(&s, &p, &o);
10000 triples.push((s, p, o));
10001 }
10002 let dict = db.build();
10003 let ids: Vec<(u32, u32, u32)> = triples
10004 .iter()
10005 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
10006 .collect();
10007 let index = GraphIndexBuilder::from_triples(ids).build();
10008 let image = write_dataset_with_metadata(
10009 &dict,
10010 &index,
10011 &[],
10012 false,
10013 &[],
10014 0,
10015 br#"{"name":"layout-label-fixture"}"#,
10016 &[],
10017 );
10018 let rete = Rete::open(&image).unwrap();
10019
10020 let layout = rete.file_layout();
10021 let header = layout
10022 .iter()
10023 .find(|s| s.kind == "header")
10024 .expect("every layout starts with a header segment");
10025
10026 assert_eq!(
10027 header.offset, 0,
10028 "the header is the first thing in the file"
10029 );
10030 assert_eq!(
10031 header.len, HEADER_LEN as u64,
10032 "the header segment must span exactly HEADER_LEN"
10033 );
10034
10035 let digits: String = header
10038 .label
10039 .chars()
10040 .skip_while(|c| !c.is_ascii_digit())
10041 .take_while(|c| c.is_ascii_digit())
10042 .collect();
10043 assert!(
10044 !digits.is_empty(),
10045 "header label {:?} states no size at all",
10046 header.label
10047 );
10048 assert_eq!(
10049 digits.parse::<u64>().unwrap(),
10050 header.len,
10051 "header label {:?} disagrees with the {}-byte span it describes",
10052 header.label,
10053 header.len
10054 );
10055
10056 assert!(
10059 layout.windows(2).all(|w| w[0].offset <= w[1].offset),
10060 "file_layout must return segments sorted by offset"
10061 );
10062 }
10063}