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
30#[cfg(test)]
31mod hardening_tests;
32
33pub const DEFAULT_TILE_BUDGET: usize = 64 * 1024;
35
36pub fn build_pyramid_meta(
45 dict: &Dictionary,
46 triples: &[(u32, u32, u32)],
47 budget: usize,
48) -> (Vec<u8>, u16) {
49 build_pyramid_meta_with(dict, triples, budget, None)
50}
51
52pub fn build_pyramid_meta_with(
56 dict: &Dictionary,
57 triples: &[(u32, u32, u32)],
58 budget: usize,
59 type_override: Option<&str>,
60) -> (Vec<u8>, u16) {
61 build_pyramid_meta_algo(dict, triples, budget, type_override, PyramidAlgo::Louvain)
62}
63
64pub fn build_pyramid_meta_algo(
71 dict: &Dictionary,
72 triples: &[(u32, u32, u32)],
73 budget: usize,
74 type_override: Option<&str>,
75 algo: PyramidAlgo,
76) -> (Vec<u8>, u16) {
77 build_pyramid_meta_layout(
78 dict,
79 triples,
80 budget,
81 type_override,
82 algo,
83 PyramidLayout::Combined,
84 )
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub enum PyramidLayout {
90 #[default]
94 Combined,
95 CommunityOnly,
100}
101
102pub fn build_pyramid_meta_layout(
104 dict: &Dictionary,
105 triples: &[(u32, u32, u32)],
106 budget: usize,
107 type_override: Option<&str>,
108 algo: PyramidAlgo,
109 layout: PyramidLayout,
110) -> (Vec<u8>, u16) {
111 let timing = std::env::var_os("RETE_BUILD_TIMING").is_some();
117 let mut t = timing.then(std::time::Instant::now);
118 let mut lap = |label: &str| {
119 if let Some(t0) = &mut t {
120 eprintln!(
121 " [pyramid] {label}: {:.0} ms",
122 t0.elapsed().as_secs_f64() * 1000.0
123 );
124 *t0 = std::time::Instant::now();
125 }
126 };
127
128 let louvain = |lap: &mut dyn FnMut(&str)| {
130 let g = project_graph(dict, triples);
131 lap("project_graph");
132 let d = build_dendrogram(&g);
133 lap("build_dendrogram (Louvain)");
134 d
135 };
136 let dend = match algo {
137 PyramidAlgo::Louvain => louvain(&mut lap),
138 PyramidAlgo::Types => {
139 match crate::schema_pyramid::build_type_dendrogram(dict, triples, type_override) {
140 Some(d) => {
141 lap("build_type_dendrogram");
142 d
143 }
144 None => {
145 eprintln!(
146 " [pyramid] --pyramid-algo types: no usable rdf:type \
147 predicate — falling back to louvain"
148 );
149 louvain(&mut lap)
150 }
151 }
152 }
153 };
154 let round = choose_round_for_budget(dict, triples, &dend, budget);
155 lap("choose_round_for_budget");
156 let summary = summarize(dict, triples, &dend, round);
157 lap("summarize");
158 if layout == PyramidLayout::CommunityOnly {
163 let descriptors = crate::schema_pyramid::build_community_descriptors(
166 dict,
167 triples,
168 &dend,
169 round,
170 type_override,
171 );
172 lap("build_community_descriptors");
173 let meta = PyramidMeta::new(round as u32, summary, &[]).with_schema(
174 Vec::new(),
175 Vec::new(),
176 Vec::new(),
177 descriptors,
178 Vec::new(),
179 Vec::new(),
180 Vec::new(),
181 );
182 let out = (meta.encode(), dend.rounds() as u16);
183 lap("encode");
184 return out;
185 }
186 let sp = crate::schema_pyramid::build_schema_pyramid_with(
187 dict,
188 triples,
189 &dend,
190 round,
191 type_override,
192 );
193 lap("build_schema_pyramid");
194 let predicate_stats = compute_predicate_stats(triples);
195 lap("compute_predicate_stats");
196 let char_sets = compute_char_sets(triples);
197 lap("compute_char_sets");
198 let label_index = compute_label_index(dict, triples);
199 lap("compute_label_index");
200 let meta = PyramidMeta::new(round as u32, summary, &[])
201 .with_schema(
202 sp.class_hierarchy,
203 sp.level_rollups,
204 sp.level_links,
205 sp.descriptors,
206 sp.subclass_cycles,
207 sp.disjoint_pairs,
208 sp.equivalent_pairs,
209 )
210 .with_predicate_stats(predicate_stats)
211 .with_char_sets(char_sets)
212 .with_label_index(label_index);
213 let out = (meta.encode(), dend.rounds() as u16);
214 lap("encode");
215 out
216}
217
218const LABEL_PREDICATES: &[&str] = crate::label_index::DIRECT_LABEL_PREDICATES;
223
224fn compute_label_index(
232 dict: &Dictionary,
233 triples: &[(u32, u32, u32)],
234) -> Vec<crate::meta::LabelEntry> {
235 use crate::terms::{is_literal, literal_lexical};
236 use std::collections::{HashMap, HashSet};
237 const MAX_LABELS: usize = 8192;
238
239 let label_pids: HashSet<u32> = LABEL_PREDICATES
241 .iter()
242 .filter_map(|p| dict.predicate_id(p))
243 .collect();
244 if label_pids.is_empty() {
245 return Vec::new();
246 }
247 let mut degree: HashMap<u32, u32> = HashMap::new();
249 for &(s, _p, _o) in triples {
250 *degree.entry(s).or_insert(0) += 1;
251 }
252 let mut seen: HashSet<(u32, String)> = HashSet::new();
254 let mut candidates: Vec<(u32, String, u32)> = Vec::new(); for &(s, p, o) in triples {
256 if !label_pids.contains(&p) {
257 continue;
258 }
259 let Some(term) = dict.object_term(o) else {
260 continue;
261 };
262 if !is_literal(&term) {
263 continue;
264 }
265 let Some(label) = literal_lexical(&term) else {
266 continue;
267 };
268 if label.is_empty() {
269 continue;
270 }
271 if seen.insert((s, label.to_lowercase())) {
272 candidates.push((*degree.get(&s).unwrap_or(&0), label, s));
273 }
274 }
275 if candidates.len() > MAX_LABELS {
278 candidates.sort_by(|a, b| {
279 b.0.cmp(&a.0)
280 .then_with(|| a.2.cmp(&b.2))
281 .then_with(|| a.1.cmp(&b.1))
282 });
283 candidates.truncate(MAX_LABELS);
284 }
285 candidates.sort_by(|a, b| {
287 a.1.to_lowercase()
288 .cmp(&b.1.to_lowercase())
289 .then_with(|| a.1.cmp(&b.1))
290 .then_with(|| a.2.cmp(&b.2))
291 });
292 candidates
293 .into_iter()
294 .map(|(_deg, label, subject)| crate::meta::LabelEntry { label, subject })
295 .collect()
296}
297
298pub fn compute_text_index(dict: &Dictionary, triples: &[(u32, u32, u32)]) -> Vec<u8> {
307 crate::text_index::build_section(
308 dict,
309 &crate::text_index::TextIndexScope::default(),
310 triples,
311 std::iter::empty(),
312 writer_codec(),
313 )
314}
315
316fn compute_char_sets(triples: &[(u32, u32, u32)]) -> Vec<crate::meta::CharSet> {
321 use std::collections::{BTreeSet, HashMap};
322 const MAX_CHAR_SETS: usize = 128;
323 let mut by_subject: HashMap<u32, BTreeSet<u32>> = HashMap::new();
324 for &(s, p, _o) in triples {
325 by_subject.entry(s).or_default().insert(p);
326 }
327 let mut shapes: HashMap<Vec<u32>, u64> = HashMap::new();
328 for set in by_subject.into_values() {
329 *shapes.entry(set.into_iter().collect()).or_insert(0) += 1;
330 }
331 let mut v: Vec<crate::meta::CharSet> = shapes
332 .into_iter()
333 .map(|(predicates, subjects)| crate::meta::CharSet {
334 predicates,
335 subjects,
336 })
337 .collect();
338 v.sort_by(|a, b| {
339 b.subjects
340 .cmp(&a.subjects)
341 .then_with(|| a.predicates.cmp(&b.predicates))
342 });
343 v.truncate(MAX_CHAR_SETS);
344 v
345}
346
347fn compute_predicate_stats(triples: &[(u32, u32, u32)]) -> Vec<crate::meta::PredStat> {
353 use std::collections::HashMap;
354 #[allow(clippy::type_complexity)]
355 let mut acc: HashMap<u32, (HashMap<u32, u32>, HashMap<u32, u32>, u64)> = HashMap::new();
356 for &(s, p, o) in triples {
357 let e = acc.entry(p).or_default();
358 *e.0.entry(s).or_insert(0) += 1;
359 *e.1.entry(o).or_insert(0) += 1;
360 e.2 += 1;
361 }
362 let mut stats: Vec<crate::meta::PredStat> = acc
363 .into_iter()
364 .map(|(predicate, (subj, obj, count))| crate::meta::PredStat {
365 predicate,
366 count,
367 distinct_subjects: subj.len() as u64,
368 distinct_objects: obj.len() as u64,
369 max_objects_per_subject: subj.values().copied().max().unwrap_or(0),
370 max_subjects_per_object: obj.values().copied().max().unwrap_or(0),
371 })
372 .collect();
373 stats.sort_by_key(|p| p.predicate);
374 stats
375}
376
377pub const CODEC_NONE: u8 = 0;
379pub const CODEC_ZSTD: u8 = 1;
381
382#[derive(Debug, thiserror::Error)]
383#[non_exhaustive]
384pub enum FileError {
385 #[error("header: {0}")]
386 Header(#[from] crate::header::HeaderError),
387 #[error("malformed container: {0}")]
388 Container(&'static str),
389 #[error("unknown codec: {0}")]
390 UnknownCodec(u8),
391 #[error("decompression failed: {0}")]
392 Decompress(std::io::Error),
393 #[error("io: {0}")]
394 Io(#[from] std::io::Error),
395}
396
397pub fn writer_codec() -> u8 {
400 if cfg!(feature = "compression") {
401 CODEC_ZSTD
402 } else {
403 CODEC_NONE
404 }
405}
406
407fn intersect_sorted(a: &[u32], b: &[u32]) -> Vec<u32> {
410 let mut out = Vec::with_capacity(a.len().min(b.len()));
411 let (mut i, mut j) = (0, 0);
412 while i < a.len() && j < b.len() {
413 match a[i].cmp(&b[j]) {
414 std::cmp::Ordering::Less => i += 1,
415 std::cmp::Ordering::Greater => j += 1,
416 std::cmp::Ordering::Equal => {
417 out.push(a[i]);
418 i += 1;
419 j += 1;
420 }
421 }
422 }
423 out
424}
425
426pub(crate) fn compress(codec: u8, bytes: &[u8]) -> Vec<u8> {
427 match codec {
428 #[cfg(feature = "compression")]
431 CODEC_ZSTD => {
432 let level = crate::write_opts::zstd_level();
433 match crate::write_opts::frame_shape() {
434 crate::write_opts::FrameShape::Stream => zstd::encode_all(bytes, level),
435 crate::write_opts::FrameShape::Sized => zstd::bulk::compress(bytes, level),
436 }
437 .expect("zstd encode is infallible in-memory")
438 }
439 _ => bytes.to_vec(),
440 }
441}
442
443#[cfg(not(target_arch = "wasm32"))]
461pub(crate) fn compress_to(
462 codec: u8,
463 src_len: u64,
464 src: &mut dyn std::io::Read,
465 dst: &mut dyn std::io::Write,
466) -> std::io::Result<()> {
467 match codec {
468 #[cfg(feature = "compression")]
469 CODEC_ZSTD => zstd_stream(src_len, src, dst),
470 _ => {
471 let _ = src_len;
472 std::io::copy(src, dst).map(|_| ())
473 }
474 }
475}
476
477pub(crate) fn compress_streamed(codec: u8, bytes: &[u8]) -> Vec<u8> {
483 match codec {
484 #[cfg(feature = "compression")]
485 CODEC_ZSTD => {
486 let mut out = Vec::with_capacity(bytes.len() / 4 + 64);
487 zstd_stream(bytes.len() as u64, &mut &bytes[..], &mut out)
488 .expect("zstd encode is infallible in-memory");
489 out
490 }
491 _ => bytes.to_vec(),
492 }
493}
494
495#[cfg(feature = "compression")]
497fn zstd_stream(
498 src_len: u64,
499 src: &mut dyn std::io::Read,
500 dst: &mut dyn std::io::Write,
501) -> std::io::Result<()> {
502 let mut enc = zstd::stream::Encoder::new(dst, crate::write_opts::zstd_level())?;
503 if crate::write_opts::frame_shape() == crate::write_opts::FrameShape::Sized {
504 enc.set_pledged_src_size(Some(src_len))?;
505 enc.include_contentsize(true)?;
506 }
507 let copied = std::io::copy(src, &mut enc)?;
508 if copied != src_len {
509 return Err(std::io::Error::other(format!(
510 "zstd stream: {copied} bytes read, {src_len} announced"
511 )));
512 }
513 enc.finish()?;
514 Ok(())
515}
516
517pub(crate) fn decompress(codec: u8, bytes: &[u8]) -> Result<Vec<u8>, FileError> {
518 match codec {
519 CODEC_NONE => Ok(bytes.to_vec()),
520 #[cfg(feature = "compression")]
529 CODEC_ZSTD => zstd::decode_all(bytes).map_err(FileError::Decompress),
530 #[cfg(not(feature = "compression"))]
535 CODEC_ZSTD => {
536 use std::io::Read;
537 let mut dec = ruzstd::decoding::StreamingDecoder::new(bytes)
538 .map_err(|e| FileError::Decompress(std::io::Error::other(e.to_string())))?;
539 let mut out = Vec::new();
540 dec.read_to_end(&mut out).map_err(FileError::Decompress)?;
541 Ok(out)
542 }
543 other => Err(FileError::UnknownCodec(other)),
544 }
545}
546
547pub(crate) fn zstd_frame_content_size(b: &[u8]) -> Option<u64> {
551 if b.len() < 5 || b[..4] != [0x28, 0xB5, 0x2F, 0xFD] {
552 return None;
553 }
554 let fhd = b[4];
555 let fcs_flag = fhd >> 6;
556 let single_segment = fhd & 0x20 != 0;
557 let did_size = [0usize, 1, 2, 4][(fhd & 0x03) as usize];
558 let mut pos = 5 + usize::from(!single_segment) + did_size;
559 let fcs_size = match (fcs_flag, single_segment) {
560 (0, false) => return None,
561 (0, true) => 1,
562 (1, _) => 2,
563 (2, _) => 4,
564 _ => 8,
565 };
566 let f = b.get(pos..pos + fcs_size)?;
567 pos += fcs_size;
568 let _ = pos;
569 let mut v = 0u64;
570 for (i, x) in f.iter().enumerate() {
571 v |= (*x as u64) << (8 * i);
572 }
573 Some(if fcs_size == 2 { v + 256 } else { v })
574}
575
576pub(crate) fn decompress_bounded(codec: u8, bytes: &[u8], max: u64) -> Result<Vec<u8>, FileError> {
582 let too_big = |n: u64| {
583 FileError::Decompress(std::io::Error::other(format!(
584 "a block of {n} bytes exceeds its section's {max}-byte maximum"
585 )))
586 };
587 match codec {
588 CODEC_NONE => {
589 if bytes.len() as u64 > max {
590 return Err(too_big(bytes.len() as u64));
591 }
592 Ok(bytes.to_vec())
593 }
594 CODEC_ZSTD => {
595 let declared = zstd_frame_content_size(bytes);
596 if let Some(n) = declared.filter(|&n| n > max) {
597 return Err(too_big(n));
598 }
599 #[cfg(feature = "compression")]
600 {
601 if let Some(n) = declared {
602 return zstd::bulk::decompress(bytes, n as usize)
605 .map_err(FileError::Decompress);
606 }
607 use std::io::Read;
608 let mut out = Vec::new();
609 let mut dec = zstd::stream::read::Decoder::with_buffer(bytes)
610 .map_err(FileError::Decompress)?;
611 dec.window_log_max(27).map_err(FileError::Decompress)?;
612 dec.take(max.saturating_add(1))
613 .read_to_end(&mut out)
614 .map_err(FileError::Decompress)?;
615 if out.len() as u64 > max {
616 return Err(too_big(out.len() as u64));
617 }
618 Ok(out)
619 }
620 #[cfg(not(feature = "compression"))]
621 {
622 use std::io::Read;
623 let dec = ruzstd::decoding::StreamingDecoder::new(bytes)
624 .map_err(|e| FileError::Decompress(std::io::Error::other(e.to_string())))?;
625 let mut out = Vec::new();
626 dec.take(max.saturating_add(1))
627 .read_to_end(&mut out)
628 .map_err(FileError::Decompress)?;
629 if out.len() as u64 > max {
630 return Err(too_big(out.len() as u64));
631 }
632 Ok(out)
633 }
634 }
635 other => Err(FileError::UnknownCodec(other)),
636 }
637}
638
639pub(crate) fn block_raw_len(codec: u8, bytes: &[u8], ceiling: u64) -> Result<u64, FileError> {
643 match codec {
644 CODEC_NONE => Ok(bytes.len() as u64),
645 CODEC_ZSTD => match zstd_frame_content_size(bytes) {
646 Some(n) => Ok(n),
647 None => Ok(decompress_bounded(codec, bytes, ceiling)?.len() as u64),
648 },
649 other => Err(FileError::UnknownCodec(other)),
650 }
651}
652
653const TILE_COALESCE_GAP: u64 = 4096;
657
658const DICT_COALESCE_GAP: u64 = 64 * 1024;
664
665pub const DEFAULT_EXPORT_BUDGET_MB: u64 = 4096;
672
673pub const DEFAULT_QUERY_BUDGET_MB: u64 = DEFAULT_EXPORT_BUDGET_MB;
685
686pub fn spill_threshold_for(budget: u64) -> u64 {
690 budget / 8
691}
692
693const MIN_DICT_CACHE_CAP: u64 = 64 << 20; const TRANSIENT_RESERVE_MAX: u64 = 128 << 20; #[derive(Debug, Clone, Copy)]
714pub struct MemoryBudget {
715 pub block_cache: u64,
717 pub dict_cache: u64,
719 pub tile_cache: u64,
721}
722
723pub fn split_memory_budget(total: u64) -> MemoryBudget {
734 if total == u64::MAX {
735 return MemoryBudget {
736 block_cache: u64::MAX,
737 dict_cache: u64::MAX,
738 tile_cache: u64::MAX,
739 };
740 }
741 let block_cache = (total / 8).min(crate::block_cache::DEFAULT_CACHE_CAP);
744 let tile_cache = (total / 16).min(256 << 20);
747 let reserve = (total / 32).min(TRANSIENT_RESERVE_MAX);
753 let dict_cache = total
756 .saturating_sub(block_cache)
757 .saturating_sub(tile_cache)
758 .saturating_sub(reserve)
759 .max(MIN_DICT_CACHE_CAP);
760 MemoryBudget {
761 block_cache,
762 dict_cache,
763 tile_cache,
764 }
765}
766
767#[derive(Debug, Clone, Copy, Default)]
770pub struct FixedOverhead {
771 pub dict_directory: u64,
773 pub index_directory: u64,
775 pub named_graphs: u64,
777}
778
779impl FixedOverhead {
780 pub fn total(&self) -> u64 {
782 self.dict_directory + self.index_directory + self.named_graphs
783 }
784}
785
786fn fmt_cap(cap: u64) -> String {
788 if cap == u64::MAX {
789 "unlimited".to_string()
790 } else {
791 format!("{}MiB", cap >> 20)
792 }
793}
794
795pub(crate) fn read_coalesced<R: RangeReader + ?Sized>(
801 reader: &R,
802 ranges: &[ByteRange],
803 gap: u64,
804) -> Option<Vec<Vec<u8>>> {
805 let mut spans: Vec<(u64, u64)> = Vec::new();
808 let mut span_of: Vec<usize> = Vec::with_capacity(ranges.len());
809 let mut i = 0;
810 while i < ranges.len() {
811 let start = ranges[i].offset;
812 let mut end = ranges[i].offset.checked_add(ranges[i].len)?;
813 let mut j = i + 1;
814 while j < ranges.len() {
815 let r = &ranges[j];
816 if r.offset < end || r.offset - end > gap {
817 break;
818 }
819 end = r.offset.checked_add(r.len)?;
820 j += 1;
821 }
822 let si = spans.len();
823 spans.push((start, end - start));
824 for _ in i..j {
825 span_of.push(si);
826 }
827 i = j;
828 }
829 let blobs = reader.read_many(&spans).ok()?;
830 if blobs.len() != spans.len() {
831 return None;
832 }
833 let mut out = Vec::with_capacity(ranges.len());
834 for (k, r) in ranges.iter().enumerate() {
835 let (span_start, _) = spans[span_of[k]];
836 let blob = &blobs[span_of[k]];
837 out.push(checked_slice(blob, r.offset - span_start, r.len)?.to_vec());
838 }
839 Some(out)
840}
841
842fn content_hash(parts: &[&[u8]]) -> [u8; 16] {
845 let mut h = blake3::Hasher::new();
846 for p in parts {
847 h.update(p);
848 }
849 let mut out = [0u8; 16];
850 out.copy_from_slice(&h.finalize().as_bytes()[..16]);
851 out
852}
853
854pub type TermTriple = (String, String, String);
856
857#[derive(Debug, Clone, Copy, PartialEq, Eq)]
860pub struct DumpPlan {
861 pub scan: Option<crate::index::ScanPlan>,
865 pub dictionary_bytes: u64,
874}
875
876#[derive(Debug, Clone)]
880pub struct LayoutSegment {
881 pub kind: &'static str,
882 pub label: String,
883 pub offset: u64,
884 pub len: u64,
885}
886
887#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
889pub struct ByteRange {
890 pub offset: u64,
891 pub len: u64,
892}
893
894impl ByteRange {
895 pub fn end(self) -> u64 {
896 self.offset + self.len
897 }
898}
899
900#[derive(Debug, Clone, PartialEq, Eq)]
902pub struct TripleProvenance {
903 pub terms: TermTriple,
905 pub ids: Triple,
907 pub graph: Option<String>,
909 pub matched_pattern: Pattern,
911 pub index_permutation: IndexPermutation,
913 pub dictionary_range: ByteRange,
915 pub index_range: ByteRange,
917 pub index_section_range: ByteRange,
920 pub pyramid_range: Option<ByteRange>,
922 pub tile: Option<String>,
926 pub tile_range: Option<ByteRange>,
929}
930
931fn encode_container(sections: &[&[u8]], codec: u8) -> Vec<u8> {
935 let mut out = Vec::new();
936 write_uvarint(&mut out, sections.len() as u64);
937 for s in sections {
938 let payload = compress(codec, s);
939 write_uvarint(&mut out, payload.len() as u64);
940 out.extend_from_slice(&payload);
941 }
942 out
943}
944
945fn decode_container(bytes: &[u8], codec: u8) -> Result<Vec<Vec<u8>>, FileError> {
947 let (n, mut pos) = read_uvarint(bytes).ok_or(FileError::Container("truncated count"))?;
948 let n = wire_usize(n, "section count too large")?;
952 let mut out = Vec::with_capacity(n.min(bytes.len()));
953 for _ in 0..n {
954 let (len, used) = read_uvarint(bytes.get(pos..).unwrap_or(&[]))
955 .ok_or(FileError::Container("truncated length"))?;
956 pos += used;
957 let end = pos
960 .checked_add(wire_usize(len, "section length too large")?)
961 .filter(|&end| end <= bytes.len())
962 .ok_or(FileError::Container("section overruns buffer"))?;
963 out.push(decompress(codec, &bytes[pos..end])?);
964 pos = end;
965 }
966 Ok(out)
967}
968
969fn checked_end(off: u64, len: u64) -> Result<u64, FileError> {
970 off.checked_add(len)
971 .ok_or(FileError::Container("section range overflows"))
972}
973
974fn wire_usize(v: u64, what: &'static str) -> Result<usize, FileError> {
978 usize::try_from(v).map_err(|_| FileError::Container(what))
979}
980
981fn checked_slice(bytes: &[u8], off: u64, len: u64) -> Option<&[u8]> {
984 let start = usize::try_from(off).ok()?;
985 let end = start.checked_add(usize::try_from(len).ok()?)?;
986 bytes.get(start..end)
987}
988
989const DICT_CHUNK_BUDGET: usize = crate::write_opts::DEFAULT_DICT_CHUNK_BYTES;
992
993fn encode_chunked_dict_section(raw: &[u8], codec: u8) -> Vec<u8> {
1010 let meta = crate::dict::parse_meta(raw).unwrap_or(crate::dict::SectionMeta {
1011 term_count: 0,
1012 restart_interval: 1,
1013 restart_offsets: Vec::new(),
1014 });
1015 let body_start = meta
1016 .restart_offsets
1017 .first()
1018 .copied()
1019 .unwrap_or(raw.len() as u64);
1020 let header = &raw[..(body_start.min(raw.len() as u64)) as usize];
1021
1022 let chunk_budget = crate::write_opts::dict_chunk_budget() as u64;
1024 let n_runs = meta.restart_offsets.len();
1025 let mut bounds: Vec<(usize, u64, u64)> = Vec::new(); let mut r = 0;
1027 while r < n_runs {
1028 let start = meta.restart_offsets[r];
1029 let mut r2 = r + 1;
1030 while r2 < n_runs && meta.restart_offsets[r2] - start < chunk_budget {
1031 r2 += 1;
1032 }
1033 let end = if r2 < n_runs {
1034 meta.restart_offsets[r2]
1035 } else {
1036 raw.len() as u64
1037 };
1038 bounds.push((r, start, end));
1039 r = r2;
1040 }
1041
1042 #[cfg(feature = "parallel")]
1046 let compressed: Vec<Vec<u8>> = {
1047 use rayon::prelude::*;
1048 bounds
1049 .par_iter()
1050 .map(|&(_, s, e)| compress(codec, &raw[s as usize..e as usize]))
1051 .collect()
1052 };
1053 #[cfg(not(feature = "parallel"))]
1054 let compressed: Vec<Vec<u8>> = bounds
1055 .iter()
1056 .map(|&(_, s, e)| compress(codec, &raw[s as usize..e as usize]))
1057 .collect();
1058 let mut out = Vec::new();
1059 write_uvarint(&mut out, header.len() as u64);
1060 out.extend_from_slice(header);
1061 write_uvarint(&mut out, bounds.len() as u64);
1062 let mut prev_run = 0usize;
1063 let mut prev_last: Option<Vec<u8>> = None;
1067 for (i, (&(first_run, start, end), comp)) in bounds.iter().zip(&compressed).enumerate() {
1068 let key = if i == 0 {
1069 Vec::new()
1071 } else {
1072 let first = crate::dict::run_first_term(raw, start as usize).unwrap_or_default();
1073 match &prev_last {
1074 None => first,
1077 Some(pl) => crate::dict::shortest_separator(pl, &first),
1078 }
1079 };
1080 if i + 1 < bounds.len() {
1081 let last_run_off = meta.restart_offsets[bounds[i + 1].0 - 1] as usize;
1083 prev_last = crate::dict::run_last_term(raw, last_run_off, end as usize);
1084 }
1085 write_uvarint(&mut out, (first_run - prev_run) as u64);
1086 write_uvarint(&mut out, key.len() as u64);
1087 out.extend_from_slice(&key);
1088 write_uvarint(&mut out, comp.len() as u64);
1089 prev_run = first_run;
1090 }
1091 for comp in &compressed {
1092 out.extend_from_slice(comp);
1093 }
1094 out
1095}
1096
1097pub(crate) struct DictChunkEntry {
1101 pub(crate) first_run: usize,
1102 pub(crate) key: Vec<u8>,
1103 body_start: u64,
1104 pub(crate) start: u64,
1105 pub(crate) end: u64,
1106}
1107
1108fn parse_chunked_dict_dir(
1112 bytes: &[u8],
1113 total_len: u64,
1114) -> Result<(crate::dict::SectionMeta, Vec<DictChunkEntry>), FileError> {
1115 let mut pos = 0usize;
1116 let take = |pos: &mut usize| -> Result<u64, FileError> {
1117 let (v, n) = read_uvarint(bytes.get(*pos..).unwrap_or(&[]))
1118 .ok_or(FileError::Container("truncated dict chunk directory"))?;
1119 *pos += n;
1120 Ok(v)
1121 };
1122 let header_len = wire_usize(take(&mut pos)?, "dict header too large")?;
1123 let header = bytes
1124 .get(pos..pos.saturating_add(header_len))
1125 .ok_or(FileError::Container("truncated dict header"))?;
1126 let meta = crate::dict::parse_meta(header)
1127 .map_err(|_| FileError::Container("malformed dict header"))?;
1128 pos += header_len;
1129
1130 let num_chunks = wire_usize(take(&mut pos)?, "dict chunk count too large")?;
1131 let mut entries = Vec::with_capacity(num_chunks.min(bytes.len()));
1132 let mut lens = Vec::with_capacity(num_chunks.min(bytes.len()));
1133 let mut prev_run = 0usize;
1134 for i in 0..num_chunks {
1135 let drun = wire_usize(take(&mut pos)?, "dict chunk run too large")?;
1136 let klen = wire_usize(take(&mut pos)?, "dict chunk key too large")?;
1137 let key = bytes
1138 .get(pos..pos.saturating_add(klen))
1139 .ok_or(FileError::Container("truncated dict chunk key"))?
1140 .to_vec();
1141 pos += klen;
1142 let clen = take(&mut pos)?;
1143 let first_run = next_chunk_run(i, prev_run, drun)?;
1144 let body_start = meta
1145 .restart_offsets
1146 .get(first_run)
1147 .copied()
1148 .ok_or(FileError::Container("dict chunk run out of range"))?;
1149 entries.push(DictChunkEntry {
1150 first_run,
1151 key,
1152 body_start,
1153 start: 0,
1154 end: 0,
1155 });
1156 lens.push(clen);
1157 prev_run = first_run;
1158 }
1159 let mut start = pos as u64;
1160 for (e, len) in entries.iter_mut().zip(lens) {
1161 let end = start
1162 .checked_add(len)
1163 .filter(|&e| e <= total_len)
1164 .ok_or(FileError::Container("dict chunk overruns section"))?;
1165 e.start = start;
1166 e.end = end;
1167 start = end;
1168 }
1169 Ok((meta, entries))
1170}
1171
1172fn next_chunk_run(i: usize, prev_run: usize, drun: usize) -> Result<usize, FileError> {
1179 match (i, drun) {
1180 (0, 0) => Ok(0),
1181 (0, _) => Err(FileError::Container(
1182 "first dict chunk does not start at run 0",
1183 )),
1184 (_, 0) => Err(FileError::Container("dict chunk runs do not ascend")),
1185 _ => prev_run
1186 .checked_add(drun)
1187 .ok_or(FileError::Container("dict chunk run overflows")),
1188 }
1189}
1190
1191pub(crate) fn read_dict_dir_ranged<R: RangeReader + ?Sized>(
1203 reader: &R,
1204 section: ByteRange,
1205) -> Result<(crate::dict::SectionMeta, Vec<DictChunkEntry>), FileError> {
1206 let total = section.len;
1207 let init = 8192.min(total); let head = reader.read_at(section.offset, init)?;
1215 let (header_len, n0) =
1216 read_uvarint(&head).ok_or(FileError::Container("truncated dict header len"))?;
1217 let hbase = n0; let (term_count, n1) = read_uvarint(head.get(hbase..).unwrap_or(&[]))
1219 .ok_or(FileError::Container("truncated dict term_count"))?;
1220 let (restart_interval, _n2) = read_uvarint(head.get(hbase + n1..).unwrap_or(&[]))
1221 .ok_or(FileError::Container("truncated dict interval"))?;
1222 if restart_interval == 0 {
1223 return Err(FileError::Container("zero restart interval"));
1224 }
1225 let dir_start = (hbase as u64)
1228 .checked_add(header_len)
1229 .filter(|&d| d <= total)
1230 .ok_or(FileError::Container("dict header overruns section"))?;
1231 let dir_total = total - dir_start;
1232 let meta = crate::dict::SectionMeta {
1233 term_count: u32::try_from(term_count)
1234 .map_err(|_| FileError::Container("dict term count too large"))?,
1235 restart_interval: u32::try_from(restart_interval)
1236 .map_err(|_| FileError::Container("dict restart interval too large"))?,
1237 restart_offsets: Vec::new(),
1238 };
1239 let num_runs = (meta.term_count as usize).div_ceil(meta.restart_interval as usize);
1241 let finish = |mut entries: Vec<DictChunkEntry>| {
1242 for e in &mut entries {
1243 e.start += dir_start; e.end += dir_start;
1245 }
1246 (meta.clone(), entries)
1247 };
1248 let mut have: Vec<u8> = Vec::new();
1252 if dir_start < head.len() as u64 {
1253 match parse_chunk_dir_only(&head[dir_start as usize..], dir_total, num_runs)? {
1254 ChunkDirParse::Done(entries) => return Ok(finish(entries)),
1255 ChunkDirParse::Truncated { .. } => have = head[dir_start as usize..].to_vec(),
1256 }
1257 }
1258 let mut want = 4096u64.min(dir_total).max(1);
1271 loop {
1272 let held = have.len() as u64;
1273 if want > held {
1274 let extra = reader.read_at(section.offset + dir_start + held, want - held)?;
1275 if extra.is_empty() {
1276 return Err(FileError::Container("truncated dict chunk directory"));
1277 }
1278 have.extend_from_slice(&extra);
1279 }
1280 let held = have.len() as u64;
1281 match parse_chunk_dir_only(&have, dir_total, num_runs)? {
1282 ChunkDirParse::Done(entries) => return Ok(finish(entries)),
1283 ChunkDirParse::Truncated { .. } if held >= dir_total => {
1284 return Err(FileError::Container("truncated dict chunk directory"));
1285 }
1286 ChunkDirParse::Truncated {
1287 parsed,
1288 used,
1289 total,
1290 } => {
1291 let est = if parsed >= 16 && used > 0 {
1296 let whole = ((used as u64) / (parsed as u64)).saturating_mul(total as u64);
1297 whole.saturating_add(whole / 8).saturating_add(64)
1298 } else {
1299 held.saturating_mul(2)
1301 };
1302 want = est
1303 .max(held.saturating_add(1))
1304 .min(held.saturating_mul(4))
1305 .min(dir_total);
1306 }
1307 }
1308 }
1309}
1310
1311enum ChunkDirParse {
1316 Done(Vec<DictChunkEntry>),
1317 Truncated {
1318 parsed: usize,
1320 used: usize,
1322 total: usize,
1324 },
1325}
1326
1327fn parse_chunk_dir_only(
1337 dir: &[u8],
1338 dir_total: u64,
1339 num_runs: usize,
1340) -> Result<ChunkDirParse, FileError> {
1341 let mut pos = 0usize;
1342 let take = |pos: &mut usize| -> Option<u64> {
1343 let (v, n) = read_uvarint(dir.get(*pos..).unwrap_or(&[]))?;
1344 *pos += n;
1345 Some(v)
1346 };
1347 let Some(num_chunks) = take(&mut pos) else {
1348 return Ok(ChunkDirParse::Truncated {
1349 parsed: 0,
1350 used: 0,
1351 total: 0,
1352 });
1353 };
1354 let num_chunks = wire_usize(num_chunks, "dict chunk count too large")?;
1355 let mut entries = Vec::with_capacity(num_chunks.min(dir.len()));
1356 let mut lens: Vec<u64> = Vec::with_capacity(num_chunks.min(dir.len()));
1357 let mut prev_run = 0usize;
1358 for i in 0..num_chunks {
1359 let entry_start = pos;
1360 let short = ChunkDirParse::Truncated {
1361 parsed: entries.len(),
1362 used: entry_start,
1363 total: num_chunks,
1364 };
1365 let (Some(drun), Some(klen)) = (take(&mut pos), take(&mut pos)) else {
1366 return Ok(short);
1367 };
1368 let drun = wire_usize(drun, "dict chunk run too large")?;
1369 let klen = wire_usize(klen, "dict chunk key too large")?;
1370 let Some(key) = dir.get(pos..pos.saturating_add(klen)) else {
1371 return Ok(short);
1372 };
1373 let key = key.to_vec();
1374 pos += klen;
1375 let Some(clen) = take(&mut pos) else {
1376 return Ok(short);
1377 };
1378 let first_run = next_chunk_run(i, prev_run, drun)?;
1379 if first_run >= num_runs {
1380 return Err(FileError::Container("dict chunk run out of range"));
1381 }
1382 entries.push(DictChunkEntry {
1383 first_run,
1384 key,
1385 body_start: 0,
1386 start: 0,
1387 end: 0,
1388 });
1389 lens.push(clen);
1390 prev_run = first_run;
1391 }
1392 let mut start = pos as u64;
1393 for (e, len) in entries.iter_mut().zip(lens) {
1394 let end = start
1395 .checked_add(len)
1396 .filter(|&e| e <= dir_total)
1397 .ok_or(FileError::Container("dict chunk overruns section"))?;
1398 e.start = start;
1399 e.end = end;
1400 start = end;
1401 }
1402 Ok(ChunkDirParse::Done(entries))
1403}
1404
1405fn decode_chunked_dict_section(
1409 payload: &[u8],
1410 codec: u8,
1411 cache: std::sync::Arc<crate::chunk_cache::ChunkCache>,
1412 section_index: u8,
1413) -> Result<crate::dict::ChunkedSection, FileError> {
1414 let (meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64)?;
1415 let chunks = entries
1416 .into_iter()
1417 .map(|e| {
1418 let body = decompress(codec, &payload[e.start as usize..e.end as usize])?;
1419 Ok((
1420 crate::dict::SectionChunk::new(e.first_run, e.key, e.body_start),
1421 body,
1422 ))
1423 })
1424 .collect::<Result<Vec<_>, FileError>>()?;
1425 Ok(crate::dict::ChunkedSection::resident(
1426 meta,
1427 chunks,
1428 cache,
1429 section_index,
1430 ))
1431}
1432
1433fn decode_dictionary_container(bytes: &[u8], codec: u8) -> Result<Dictionary, FileError> {
1434 let dsecs = decode_container(bytes, CODEC_NONE)?;
1435 if dsecs.len() != 4 {
1436 return Err(FileError::Container("expected 4 dictionary sections"));
1437 }
1438 let cache = crate::chunk_cache::ChunkCache::unlimited_arc();
1441 let mut sections = Vec::with_capacity(4);
1442 for (si, sec) in dsecs.iter().enumerate() {
1443 sections.push(decode_chunked_dict_section(
1444 sec,
1445 codec,
1446 cache.clone(),
1447 si as u8,
1448 )?);
1449 }
1450 let arr: [crate::dict::ChunkedSection; 4] = sections
1451 .try_into()
1452 .map_err(|_| FileError::Container("expected 4 dictionary sections"))?;
1453 Ok(Dictionary::from_chunked_sections(arr))
1454}
1455
1456fn encode_tiled_section(index: &GraphIndex, si: usize, codec: u8) -> Vec<u8> {
1462 let tiles = index.tile_sections()[si];
1463 let n = tiles.len();
1464 #[cfg(feature = "parallel")]
1471 let compressed: Vec<Vec<u8>> = {
1472 use rayon::prelude::*;
1473 (0..n)
1474 .into_par_iter()
1475 .map(|ti| compress(codec, &index.tile_body(si, ti)))
1476 .collect()
1477 };
1478 #[cfg(not(feature = "parallel"))]
1479 let compressed: Vec<Vec<u8>> = (0..n)
1480 .map(|ti| compress(codec, &index.tile_body(si, ti)))
1481 .collect();
1482 let mut out = Vec::new();
1483 write_uvarint(&mut out, tiles.len() as u64);
1484 let mut prev_min = 0u32;
1485 for (tile, comp) in tiles.iter().zip(&compressed) {
1486 let (min_a, max_a) = tile.leading_range();
1487 write_uvarint(&mut out, (min_a - prev_min) as u64);
1488 write_uvarint(&mut out, (max_a - min_a) as u64);
1489 write_uvarint(&mut out, comp.len() as u64);
1490 prev_min = min_a;
1491 }
1492 for comp in &compressed {
1493 out.extend_from_slice(comp);
1494 }
1495 for ti in 0..n {
1503 let body = index.tile_body(si, ti);
1504 let (min_b, max_b, min_c, max_c) = match crate::triples::TripleBlock::parse(&body) {
1505 Ok(b) => {
1506 let z = b.zone();
1507 (z.min_b, z.max_b, z.min_c, z.max_c)
1508 }
1509 Err(_) => (0, u32::MAX, 0, u32::MAX),
1510 };
1511 write_uvarint(&mut out, min_b as u64);
1512 write_uvarint(&mut out, (max_b - min_b) as u64);
1513 write_uvarint(&mut out, min_c as u64);
1514 write_uvarint(&mut out, (max_c - min_c) as u64);
1515 }
1516 out
1517}
1518
1519pub(crate) struct TileDirEntry {
1522 pub(crate) min_a: u32,
1523 pub(crate) max_a: u32,
1524 pub(crate) start: u64,
1525 pub(crate) end: u64,
1526}
1527
1528type TileSynopsis = (u32, u32, u32, u32);
1530
1531fn parse_tile_synopsis(
1539 payload: &[u8],
1540 trailer_start: usize,
1541 num_tiles: usize,
1542) -> Option<Vec<TileSynopsis>> {
1543 let mut pos = trailer_start;
1544 let take = |pos: &mut usize| -> Option<u32> {
1545 let (v, n) = read_uvarint(payload.get(*pos..)?)?;
1546 *pos += n;
1547 u32::try_from(v).ok()
1548 };
1549 let mut out = Vec::with_capacity(num_tiles.min(payload.len()));
1550 for _ in 0..num_tiles {
1551 let min_b = take(&mut pos)?;
1552 let max_b = min_b.checked_add(take(&mut pos)?)?;
1553 let min_c = take(&mut pos)?;
1554 let max_c = min_c.checked_add(take(&mut pos)?)?;
1555 out.push((min_b, max_b, min_c, max_c));
1556 }
1557 Some(out)
1558}
1559
1560fn parse_tile_directory(bytes: &[u8], total_len: u64) -> Result<Vec<TileDirEntry>, FileError> {
1565 let mut pos = 0usize;
1566 let take = |pos: &mut usize| -> Result<u64, FileError> {
1567 let (v, n) = read_uvarint(bytes.get(*pos..).unwrap_or(&[]))
1568 .ok_or(FileError::Container("truncated tile directory"))?;
1569 *pos += n;
1570 Ok(v)
1571 };
1572 let num_tiles = wire_usize(take(&mut pos)?, "tile count too large")?;
1573 let mut entries = Vec::with_capacity(num_tiles.min(bytes.len()));
1574 let mut prev_min = 0u32;
1575 let mut lens = Vec::with_capacity(num_tiles.min(bytes.len()));
1576 let id = |v: u64| u32::try_from(v).map_err(|_| FileError::Container("tile id out of range"));
1580 for _ in 0..num_tiles {
1581 let dmin = id(take(&mut pos)?)?;
1582 let span = id(take(&mut pos)?)?;
1583 let len = take(&mut pos)?;
1584 let min_a = prev_min
1585 .checked_add(dmin)
1586 .ok_or(FileError::Container("tile id out of range"))?;
1587 let max_a = min_a
1588 .checked_add(span)
1589 .ok_or(FileError::Container("tile id out of range"))?;
1590 entries.push(TileDirEntry {
1591 min_a,
1592 max_a,
1593 start: 0,
1594 end: 0,
1595 });
1596 lens.push(len);
1597 prev_min = min_a;
1598 }
1599 let mut start = pos as u64;
1600 for (e, len) in entries.iter_mut().zip(lens) {
1601 let end = start
1602 .checked_add(len)
1603 .filter(|&e| e <= total_len)
1604 .ok_or(FileError::Container("tile overruns section"))?;
1605 e.start = start;
1606 e.end = end;
1607 start = end;
1608 }
1609 Ok(entries)
1610}
1611
1612pub(crate) fn read_tile_directory_ranged<R: RangeReader + ?Sized>(
1616 reader: &R,
1617 section: ByteRange,
1618) -> Result<Vec<TileDirEntry>, FileError> {
1619 let total = section.len;
1620 let mut prefetch = 4096u64.min(total);
1621 loop {
1622 let prefix = reader.read_at(section.offset, prefetch)?;
1623 match parse_tile_directory(&prefix, total) {
1624 Ok(dir) => return Ok(dir),
1625 Err(_) if prefetch < total => prefetch = prefetch.saturating_mul(2).min(total),
1626 Err(e) => return Err(e),
1627 }
1628 }
1629}
1630
1631pub(crate) fn read_tile_synopsis_ranged<R: RangeReader + ?Sized>(
1638 reader: &R,
1639 section: ByteRange,
1640 dir: &[TileDirEntry],
1641) -> Vec<Option<TileSynopsis>> {
1642 let n = dir.len();
1643 let none = vec![None; n];
1644 let trailer_start = dir.iter().map(|e| e.end).max().unwrap_or(0);
1645 let total = section.len;
1646 if n == 0 || trailer_start >= total {
1647 return none; }
1649 let max_trailer = (n as u64).saturating_mul(4 * 5);
1653 let trailer_len = (total - trailer_start).min(max_trailer);
1654 let Some(offset) = section.offset.checked_add(trailer_start) else {
1655 return none;
1656 };
1657 let Ok(bytes) = reader.read_at(offset, trailer_len) else {
1658 return none;
1659 };
1660 match parse_tile_synopsis(&bytes, 0, n) {
1661 Some(v) => v.into_iter().map(Some).collect(),
1662 None => none,
1663 }
1664}
1665
1666fn tile_file_ranges(
1670 index_bytes: &[u8],
1671 container_offset: u64,
1672 section_ranges: &[ByteRange; NUM_PERMS],
1673) -> [Vec<(u32, u32, ByteRange)>; NUM_PERMS] {
1674 let mut out: [Vec<(u32, u32, ByteRange)>; NUM_PERMS] = Default::default();
1675 for (section, range) in out.iter_mut().zip(section_ranges) {
1676 if range.len == 0 || range.offset < container_offset {
1680 continue;
1681 }
1682 let Some(payload) = checked_slice(index_bytes, range.offset - container_offset, range.len)
1683 else {
1684 continue;
1685 };
1686 if let Ok(dir) = parse_tile_directory(payload, payload.len() as u64) {
1687 *section = dir
1688 .into_iter()
1689 .map(|e| {
1690 (
1691 e.min_a,
1692 e.max_a,
1693 ByteRange {
1694 offset: range.offset + e.start,
1695 len: (e.end - e.start),
1696 },
1697 )
1698 })
1699 .collect();
1700 }
1701 }
1702 out
1703}
1704
1705fn decode_tiled_section(payload: &[u8], codec: u8) -> Result<Vec<(u32, u32, Vec<u8>)>, FileError> {
1708 parse_tile_directory(payload, payload.len() as u64)?
1709 .into_iter()
1710 .map(|e| {
1711 Ok((
1712 e.min_a,
1713 e.max_a,
1714 decompress(codec, &payload[e.start as usize..e.end as usize])?,
1715 ))
1716 })
1717 .collect()
1718}
1719
1720fn decode_index_container(
1730 bytes: &[u8],
1731 codec: u8,
1732 perms: PermSet,
1733) -> Result<GraphIndex, FileError> {
1734 let isecs = decode_container(bytes, CODEC_NONE)?;
1735 if isecs.len() != perms.len() {
1736 return Err(FileError::Container(
1737 "index container section count does not match the header permutation mask",
1738 ));
1739 }
1740 decode_index_sections(isecs, codec, perms)
1741}
1742
1743fn decode_index_sections(
1744 mut isecs: Vec<Vec<u8>>,
1745 codec: u8,
1746 perms: PermSet,
1747) -> Result<GraphIndex, FileError> {
1748 let mut sections: [Vec<(u32, u32, Vec<u8>)>; NUM_PERMS] = Default::default();
1749 for (perm, sec) in perms.iter().zip(isecs.iter_mut()) {
1750 sections[perm.section_index()] = decode_tiled_section(sec, codec)?;
1751 }
1752 Ok(GraphIndex::from_tiles(sections, perms))
1753}
1754
1755fn decode_root_index_container(
1760 bytes: &[u8],
1761 header: &Header,
1762) -> Result<(GraphIndex, Option<[u8; 16]>), FileError> {
1763 if !header.has_quad_index() {
1764 return Ok((
1765 decode_index_container(bytes, header.block_codec, header.perms)?,
1766 None,
1767 ));
1768 }
1769 let mut isecs = decode_container(bytes, CODEC_NONE)?;
1770 if isecs.len() != header.index_container_sections() {
1771 return Err(FileError::Container(
1772 "index container section count does not match the header permutation mask",
1773 ));
1774 }
1775 let seal = isecs.pop().unwrap_or_default();
1776 let digest = crate::quad::parse_seal(&seal)?;
1777 Ok((
1778 decode_index_sections(isecs, header.block_codec, header.perms)?,
1779 Some(digest),
1780 ))
1781}
1782
1783fn check_named_layout(header: &Header) -> Result<(), FileError> {
1788 let quad = header.quad_index_section().is_some();
1789 if header.has_quad_index() {
1790 if !quad {
1791 return Err(FileError::Container(
1792 "quad-index flag set but the file has no quad-index section",
1793 ));
1794 }
1795 if header.named_graphs_len > 0 {
1796 return Err(FileError::Container(
1797 "a file cannot carry both a quad index and a named-graphs section",
1798 ));
1799 }
1800 crate::quad::check_perms(header.perms)?;
1801 } else if quad {
1802 return Err(FileError::Container(
1803 "quad-index section present but the header does not flag it",
1804 ));
1805 }
1806 Ok(())
1807}
1808
1809fn container_section_payload_ranges(
1810 bytes: &[u8],
1811 container_offset: u64,
1812 expected_sections: usize,
1813) -> Result<Vec<ByteRange>, FileError> {
1814 let (section_count, mut pos) =
1815 read_uvarint(bytes).ok_or(FileError::Container("truncated count"))?;
1816 let section_count = usize::try_from(section_count)
1817 .map_err(|_| FileError::Container("section count too large"))?;
1818 if section_count != expected_sections {
1819 return Err(FileError::Container("unexpected section count"));
1820 }
1821
1822 let mut ranges = Vec::with_capacity(section_count);
1823 for _ in 0..section_count {
1824 let remaining = bytes
1825 .get(pos..)
1826 .ok_or(FileError::Container("truncated length"))?;
1827 let (payload_len, used) =
1828 read_uvarint(remaining).ok_or(FileError::Container("truncated length"))?;
1829 pos = pos
1830 .checked_add(used)
1831 .ok_or(FileError::Container("section range overflows"))?;
1832 let payload_len_usize = usize::try_from(payload_len)
1833 .map_err(|_| FileError::Container("section length too large"))?;
1834 let payload_end = pos
1835 .checked_add(payload_len_usize)
1836 .ok_or(FileError::Container("section range overflows"))?;
1837 if payload_end > bytes.len() {
1838 return Err(FileError::Container("section overruns buffer"));
1839 }
1840 ranges.push(ByteRange {
1841 offset: checked_end(container_offset, pos as u64)?,
1842 len: payload_len,
1843 });
1844 pos = payload_end;
1845 }
1846
1847 Ok(ranges)
1848}
1849
1850fn decode_index_section_ranges(
1854 bytes: &[u8],
1855 container_offset: u64,
1856 perms: PermSet,
1857 sections: usize,
1858) -> Result<[ByteRange; NUM_PERMS], FileError> {
1859 let ranges = container_section_payload_ranges(bytes, container_offset, sections)?;
1860 let mut out = [ByteRange { offset: 0, len: 0 }; NUM_PERMS];
1861 for (perm, range) in perms.iter().zip(ranges) {
1862 out[perm.section_index()] = range;
1863 }
1864 Ok(out)
1865}
1866
1867fn read_uvarint_at<R: RangeReader + ?Sized>(
1868 reader: &R,
1869 absolute_offset: u64,
1870 container_end: u64,
1871) -> Result<(u64, u64), FileError> {
1872 if absolute_offset >= container_end {
1873 return Err(FileError::Container("truncated container varint"));
1874 }
1875 let remaining = container_end - absolute_offset;
1876 let probe_len = remaining.min(10);
1877 let bytes = reader.read_at(absolute_offset, probe_len)?;
1878 read_uvarint(&bytes)
1879 .map(|(value, used)| (value, used as u64))
1880 .ok_or(FileError::Container("truncated container varint"))
1881}
1882
1883pub(crate) fn locate_container_section_ranged<R: RangeReader + ?Sized>(
1886 reader: &R,
1887 container_offset: u64,
1888 container_len: u64,
1889 section_index: usize,
1890 expected_sections: u64,
1891) -> Result<ByteRange, FileError> {
1892 let container_end = checked_end(container_offset, container_len)?;
1893 let (section_count, used) = read_uvarint_at(reader, container_offset, container_end)?;
1894 if section_count != expected_sections {
1895 return Err(FileError::Container("unexpected container section count"));
1896 }
1897 if section_index >= section_count as usize {
1898 return Err(FileError::Container(
1899 "container section index out of bounds",
1900 ));
1901 }
1902
1903 let mut pos = checked_end(container_offset, used)?;
1904 for i in 0..section_count as usize {
1905 let (payload_len, len_used) = read_uvarint_at(reader, pos, container_end)?;
1906 pos = checked_end(pos, len_used)?;
1907 let payload_end = checked_end(pos, payload_len)?;
1908 if payload_end > container_end {
1909 return Err(FileError::Container("section overruns buffer"));
1910 }
1911 if i == section_index {
1912 return Ok(ByteRange {
1913 offset: pos,
1914 len: payload_len,
1915 });
1916 }
1917 pos = payload_end;
1918 }
1919 Err(FileError::Container("container section not found"))
1920}
1921
1922#[allow(clippy::type_complexity)]
1932fn open_index_container_lazy(
1933 reader: &std::sync::Arc<dyn RangeReader + Send + Sync>,
1934 container: ByteRange,
1935 block_codec: u8,
1936 has_synopsis: bool,
1937 read_concurrency: usize,
1938 perms: PermSet,
1939 sections: usize,
1940) -> Result<
1941 (
1942 GraphIndex,
1943 [ByteRange; NUM_PERMS],
1944 [Vec<(u32, u32, ByteRange)>; NUM_PERMS],
1945 ),
1946 FileError,
1947> {
1948 let mut index_section_ranges = [ByteRange { offset: 0, len: 0 }; NUM_PERMS];
1949 let mut tile_ranges: [Vec<(u32, u32, ByteRange)>; NUM_PERMS] = Default::default();
1950 #[allow(clippy::type_complexity)]
1951 let mut directories: [Vec<(u32, u32, Option<TileSynopsis>)>; NUM_PERMS] = Default::default();
1952 for (pos, perm) in perms.iter().enumerate() {
1953 let si = perm.section_index();
1954 let section = locate_container_section_ranged(
1955 reader,
1956 container.offset,
1957 container.len,
1958 pos,
1959 sections as u64,
1960 )?;
1961 index_section_ranges[si] = section;
1962 let dir = read_tile_directory_ranged(reader, section)?;
1963 let syn = if has_synopsis {
1966 read_tile_synopsis_ranged(reader, section, &dir)
1967 } else {
1968 vec![None; dir.len()]
1969 };
1970 directories[si] = dir
1971 .iter()
1972 .zip(syn)
1973 .map(|(e, s)| (e.min_a, e.max_a, s))
1974 .collect();
1975 tile_ranges[si] = dir
1976 .into_iter()
1977 .map(|e| {
1978 (
1979 e.min_a,
1980 e.max_a,
1981 ByteRange {
1982 offset: section.offset + e.start,
1983 len: (e.end - e.start),
1984 },
1985 )
1986 })
1987 .collect();
1988 }
1989
1990 let codec = block_codec;
2000 let loader_ranges = tile_ranges.clone();
2001 let loader_reader = reader.clone();
2002 let loader: crate::index::TileLoader = Box::new(move |si, ti| {
2003 let (_, _, range) = loader_ranges.get(si)?.get(ti)?;
2004 let bytes = loader_reader.read_at(range.offset, range.len).ok()?;
2005 if bytes.len() as u64 != range.len {
2006 return None;
2007 }
2008 checked_tile(codec, &bytes)
2009 });
2010 let bulk_ranges = tile_ranges.clone();
2011 let bulk_reader = reader.clone();
2012 let bulk: crate::index::TileBulkLoader = Box::new(move |si, tis| {
2013 let section = bulk_ranges.get(si)?;
2014 let want: Option<Vec<ByteRange>> = tis
2015 .iter()
2016 .map(|&ti| section.get(ti).map(|&(_, _, r)| r))
2017 .collect();
2018 let blobs = read_coalesced(bulk_reader.as_ref(), &want?, TILE_COALESCE_GAP)?;
2019 blobs.iter().map(|b| checked_tile(codec, b)).collect()
2020 });
2021 let mut index =
2022 GraphIndex::from_remote_directories(directories, perms, loader).with_bulk_loader(bulk);
2023 index.set_tile_lens(std::array::from_fn(|si| {
2026 tile_ranges[si]
2027 .iter()
2028 .map(|&(_, _, r)| r.len.min(u32::MAX as u64) as u32)
2029 .collect()
2030 }));
2031 index.set_read_concurrency(read_concurrency);
2035 Ok((index, index_section_ranges, tile_ranges))
2036}
2037
2038fn checked_tile(codec: u8, bytes: &[u8]) -> Option<Vec<u8>> {
2042 let tile = decompress(codec, bytes).ok()?;
2043 crate::triples::TripleBlock::parse(&tile).ok()?;
2044 Some(tile)
2045}
2046
2047pub fn write_file(
2054 dict: &Dictionary,
2055 index: &GraphIndex,
2056 has_quads: bool,
2057 pyramid_meta: &[u8],
2058 pyramid_levels: u16,
2059) -> Vec<u8> {
2060 write_dataset(dict, index, &[], has_quads, pyramid_meta, pyramid_levels)
2061}
2062
2063#[doc(hidden)]
2068pub fn write_file_v05(
2069 dict: &Dictionary,
2070 index: &GraphIndex,
2071 has_quads: bool,
2072 pyramid_meta: &[u8],
2073 pyramid_levels: u16,
2074) -> Vec<u8> {
2075 write_dataset_v05(dict, index, &[], has_quads, pyramid_meta, pyramid_levels)
2076}
2077
2078fn into_v06(image: Vec<u8>) -> Vec<u8> {
2081 crate::build_record::finish_in_ram(image, None)
2082 .unwrap_or_else(|e| panic!("writing format 0x06: {e}"))
2083}
2084
2085pub(crate) fn encode_index_container(index: &GraphIndex, codec: u8) -> Vec<u8> {
2091 encode_index_container_sealed(index, codec, None)
2092}
2093
2094fn encode_index_container_sealed(index: &GraphIndex, codec: u8, seal: Option<&[u8]>) -> Vec<u8> {
2099 let payloads: Vec<Vec<u8>> = index
2100 .perms()
2101 .iter()
2102 .map(|perm| encode_tiled_section(index, perm.section_index(), codec))
2103 .collect();
2104 let mut refs: Vec<&[u8]> = payloads.iter().map(|p| p.as_slice()).collect();
2105 if let Some(seal) = seal {
2106 refs.push(seal);
2107 }
2108 encode_container(&refs, CODEC_NONE)
2109}
2110
2111fn encode_named_graphs(named: &[(String, GraphIndex)], codec: u8) -> Vec<u8> {
2113 let mut out = Vec::new();
2114 write_uvarint(&mut out, named.len() as u64);
2115 for (iri, index) in named {
2116 write_uvarint(&mut out, iri.len() as u64);
2117 out.extend_from_slice(iri.as_bytes());
2118 let container = encode_index_container(index, codec);
2119 write_uvarint(&mut out, container.len() as u64);
2120 out.extend_from_slice(&container);
2121 }
2122 out
2123}
2124
2125fn decode_named_graphs(
2126 bytes: &[u8],
2127 codec: u8,
2128 perms: PermSet,
2129) -> Result<Vec<(String, GraphIndex)>, FileError> {
2130 let (n, mut pos) = read_uvarint(bytes).ok_or(FileError::Container("truncated graph count"))?;
2131 let bound = |start: usize, len: u64| -> Result<usize, FileError> {
2134 usize::try_from(len)
2135 .ok()
2136 .and_then(|len| start.checked_add(len))
2137 .filter(|&e| e <= bytes.len())
2138 .ok_or(FileError::Container("named-graph field overruns buffer"))
2139 };
2140 let n = wire_usize(n, "named-graph count too large")?;
2141 let mut out = Vec::with_capacity(n.min(bytes.len()));
2142 for _ in 0..n {
2143 let (ilen, u1) = read_uvarint(bytes.get(pos..).unwrap_or(&[]))
2144 .ok_or(FileError::Container("truncated iri len"))?;
2145 pos += u1;
2146 let iend = bound(pos, ilen)?;
2147 let iri = String::from_utf8_lossy(&bytes[pos..iend]).into_owned();
2148 pos = iend;
2149 let (clen, u2) = read_uvarint(bytes.get(pos..).unwrap_or(&[]))
2150 .ok_or(FileError::Container("truncated container len"))?;
2151 pos += u2;
2152 let cend = bound(pos, clen)?;
2153 let index = decode_index_container(&bytes[pos..cend], codec, perms)?;
2154 index.set_size_hint(u64::from(index.triple_count()));
2157 out.push((iri, index));
2158 pos = cend;
2159 }
2160 Ok(out)
2161}
2162
2163pub fn write_dataset(
2168 dict: &Dictionary,
2169 default_index: &GraphIndex,
2170 named: &[(String, GraphIndex)],
2171 has_quads: bool,
2172 pyramid_meta: &[u8],
2173 pyramid_levels: u16,
2174) -> Vec<u8> {
2175 write_dataset_with_metadata(
2176 dict,
2177 default_index,
2178 named,
2179 has_quads,
2180 pyramid_meta,
2181 pyramid_levels,
2182 &[],
2183 &[],
2184 )
2185}
2186
2187#[doc(hidden)]
2189pub fn write_dataset_v05(
2190 dict: &Dictionary,
2191 default_index: &GraphIndex,
2192 named: &[(String, GraphIndex)],
2193 has_quads: bool,
2194 pyramid_meta: &[u8],
2195 pyramid_levels: u16,
2196) -> Vec<u8> {
2197 write_dataset_with_metadata_v05(
2198 dict,
2199 default_index,
2200 named,
2201 has_quads,
2202 pyramid_meta,
2203 pyramid_levels,
2204 &[],
2205 &[],
2206 )
2207}
2208
2209pub(crate) fn encode_dict_container(dict: &Dictionary, codec: u8) -> Vec<u8> {
2214 let raw_sections = dict.sections();
2215 let dict_payloads: Vec<Vec<u8>> = raw_sections
2216 .iter()
2217 .map(|raw| encode_chunked_dict_section(raw, codec))
2218 .collect();
2219 encode_container(
2220 &[
2221 dict_payloads[0].as_slice(),
2222 dict_payloads[1].as_slice(),
2223 dict_payloads[2].as_slice(),
2224 dict_payloads[3].as_slice(),
2225 ],
2226 CODEC_NONE,
2227 )
2228}
2229
2230#[allow(clippy::too_many_arguments)]
2241pub fn write_dataset_with_metadata(
2242 dict: &Dictionary,
2243 default_index: &GraphIndex,
2244 named: &[(String, GraphIndex)],
2245 has_quads: bool,
2246 pyramid_meta: &[u8],
2247 pyramid_levels: u16,
2248 metadata: &[u8],
2249 text_index: &[u8],
2250) -> Vec<u8> {
2251 into_v06(write_dataset_with_metadata_v05(
2252 dict,
2253 default_index,
2254 named,
2255 has_quads,
2256 pyramid_meta,
2257 pyramid_levels,
2258 metadata,
2259 text_index,
2260 ))
2261}
2262
2263#[doc(hidden)]
2266#[allow(clippy::too_many_arguments)]
2267pub fn write_dataset_with_metadata_v05(
2268 dict: &Dictionary,
2269 default_index: &GraphIndex,
2270 named: &[(String, GraphIndex)],
2271 has_quads: bool,
2272 pyramid_meta: &[u8],
2273 pyramid_levels: u16,
2274 metadata: &[u8],
2275 text_index: &[u8],
2276) -> Vec<u8> {
2277 let codec = writer_codec();
2278 let dict_container = encode_dict_container(dict, codec);
2279 let scope = (!text_index.is_empty())
2283 .then(|| crate::text_index::TextIndex::from_section(text_index, codec).ok())
2284 .flatten()
2285 .map(|ti| ti.scope().clone());
2286 let text_index_partial = scope
2287 .as_ref()
2288 .is_some_and(crate::text_index::TextIndexScope::restricts_predicates);
2289 let text_index_named = scope
2290 .as_ref()
2291 .is_some_and(crate::text_index::TextIndexScope::covers_named_graphs);
2292 write_dataset_from_parts(
2293 &dict_container,
2294 dict.term_count() as u64,
2295 default_index,
2296 NamedPart::PerGraph(named),
2297 has_quads,
2298 dict.has_quoted_triples(),
2299 pyramid_meta,
2300 pyramid_levels,
2301 metadata,
2302 text_index,
2303 text_index_partial,
2304 text_index_named,
2305 codec,
2306 )
2307}
2308
2309pub(crate) enum NamedPart<'a> {
2312 PerGraph(&'a [(String, GraphIndex)]),
2313 Quad(&'a crate::quad::EncodedQuadIndex),
2314}
2315
2316#[allow(clippy::too_many_arguments)]
2321pub(crate) fn write_dataset_from_parts(
2322 dict_container: &[u8],
2323 term_count: u64,
2324 default_index: &GraphIndex,
2325 named: NamedPart<'_>,
2326 has_quads: bool,
2327 has_quoted_triples: bool,
2328 pyramid_meta: &[u8],
2329 pyramid_levels: u16,
2330 metadata: &[u8],
2331 text_index: &[u8],
2332 text_index_partial: bool,
2333 text_index_named: bool,
2334 codec: u8,
2335) -> Vec<u8> {
2336 let (named_section, quad, named_quads) = match named {
2340 NamedPart::PerGraph(named) => (
2341 if named.is_empty() {
2342 Vec::new()
2343 } else {
2344 encode_named_graphs(named, codec)
2345 },
2346 None,
2347 named
2348 .iter()
2349 .map(|(_, idx)| idx.triple_count() as u64)
2350 .sum::<u64>(),
2351 ),
2352 NamedPart::Quad(q) if q.graph_count == 0 => (Vec::new(), None, 0),
2353 NamedPart::Quad(q) => (Vec::new(), Some(q), q.quad_count),
2354 };
2355 let seal = quad.map(|q| crate::quad::encode_seal(crate::quad::digest(&q.bytes)));
2356 let index_container = encode_index_container_sealed(default_index, codec, seal.as_deref());
2357 let text_index_partial = text_index_partial && !text_index.is_empty();
2362 let digested;
2363 let text_index: &[u8] = if text_index_partial {
2364 digested = crate::text_index::with_partial_digest(text_index);
2365 &digested
2366 } else {
2367 text_index
2368 };
2369
2370 let meta_section_len = metadata.len() as u64;
2373 let dict_offset = HEADER_LEN as u64 + meta_section_len;
2374 let dict_len = dict_container.len() as u64;
2375 let index_offset = dict_offset + dict_len;
2376 let index_len = index_container.len() as u64;
2377 let pyr_offset = index_offset + index_len;
2378 let pyr_len = pyramid_meta.len() as u64;
2379 let text_offset = pyr_offset + pyr_len;
2381 let text_len = text_index.len() as u64;
2382 let named_offset = text_offset + text_len;
2383 let named_len = named_section.len() as u64;
2384 let quad_len = quad.map_or(0, |q| q.bytes.len() as u64);
2387
2388 let mut parts: Vec<&[u8]> = Vec::with_capacity(5);
2394 if meta_section_len > 0 {
2395 parts.push(metadata);
2396 }
2397 parts.push(dict_container);
2398 parts.push(&index_container);
2399 parts.push(pyramid_meta);
2400 if text_len > 0 && !text_index_partial {
2401 parts.push(text_index);
2402 }
2403 if named_len > 0 {
2404 parts.push(&named_section);
2405 }
2406
2407 let schema_meta_len = crate::meta::schema_block_len(pyramid_meta);
2410
2411 let header = Header {
2415 version: crate::header::LEGACY_FORMAT_VERSION,
2416 flags: FLAG_TILE_SYNOPSIS
2417 | if has_quads { FLAG_HAS_QUADS } else { 0 }
2418 | if quad.is_some() {
2419 crate::header::FLAG_QUAD_INDEX
2420 } else {
2421 0
2422 }
2423 | if has_quoted_triples {
2424 FLAG_HAS_QUOTED_TRIPLES
2425 } else {
2426 0
2427 }
2428 | if text_len > 0 && text_index_named {
2429 crate::header::FLAG_TEXT_INDEX_NAMED_GRAPHS
2430 } else {
2431 0
2432 },
2433 metadata_offset: HEADER_LEN as u64,
2434 metadata_len: meta_section_len,
2435 dictionary_offset: dict_offset,
2436 dictionary_len: dict_len,
2437 root_dir_offset: index_offset,
2438 root_dir_len: index_len,
2439 pyramid_meta_offset: if pyr_len > 0 { pyr_offset } else { 0 },
2440 pyramid_meta_len: pyr_len,
2441 dict_codec: codec,
2442 block_codec: codec,
2443 pyramid_levels,
2444 id_widths: 0,
2445 perms: default_index.perms(),
2449 quad_count: default_index.triple_count() as u64 + named_quads,
2450 term_count,
2451 content_hash: content_hash(&parts),
2452 named_graphs_offset: if named_len > 0 { named_offset } else { 0 },
2453 named_graphs_len: named_len,
2454 schema_meta_len,
2455 text_index_offset: if text_len > 0 { text_offset } else { 0 },
2456 text_index_len: text_len,
2457 text_index_partial: text_len > 0 && text_index_partial,
2458 build_info_offset: 0,
2459 build_info_len: 0,
2460 extra_sections: if quad_len > 0 {
2461 vec![crate::header::Section {
2462 kind: crate::header::SectionKind::QuadIndex,
2463 flags: 0,
2464 offset: named_offset,
2465 length: quad_len,
2466 }]
2467 } else {
2468 Vec::new()
2469 },
2470 };
2471
2472 let mut out = Vec::with_capacity(
2473 HEADER_LEN
2474 + metadata.len()
2475 + dict_container.len()
2476 + index_container.len()
2477 + pyramid_meta.len()
2478 + text_index.len()
2479 + named_section.len()
2480 + quad_len as usize
2481 + MAGIC.len(),
2482 );
2483 out.extend_from_slice(&header.to_bytes());
2484 if meta_section_len > 0 {
2485 out.extend_from_slice(metadata);
2486 }
2487 out.extend_from_slice(dict_container);
2488 out.extend_from_slice(&index_container);
2489 out.extend_from_slice(pyramid_meta);
2490 if text_len > 0 {
2491 out.extend_from_slice(text_index);
2492 }
2493 if named_len > 0 {
2494 out.extend_from_slice(&named_section);
2495 }
2496 if let Some(q) = quad {
2497 out.extend_from_slice(&q.bytes);
2498 }
2499 out.extend_from_slice(&MAGIC); out
2501}
2502
2503pub const RDF_TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
2505
2506pub fn schema_summary(rete: &Rete) -> Vec<(String, String, String, u32)> {
2513 use std::collections::{BTreeMap, HashMap};
2514 let triples = rete.dump(None);
2515
2516 let mut class_of: HashMap<&str, &str> = HashMap::new();
2517 for (s, p, o) in &triples {
2518 if p == RDF_TYPE {
2519 class_of.insert(s.as_str(), o.as_str());
2520 }
2521 }
2522 let classify = |t: &str| -> String {
2523 if let Some(c) = class_of.get(t) {
2524 (*c).to_string()
2525 } else if t.starts_with('"') {
2526 "(literal)".to_string()
2527 } else {
2528 "(untyped)".to_string()
2529 }
2530 };
2531
2532 let mut counts: BTreeMap<(String, String, String), u32> = BTreeMap::new();
2533 for (s, p, o) in &triples {
2534 if p == RDF_TYPE {
2535 continue; }
2537 *counts
2538 .entry((classify(s), p.clone(), classify(o)))
2539 .or_default() += 1;
2540 }
2541 counts
2542 .into_iter()
2543 .map(|((a, p, b), c)| (a, p, b, c))
2544 .collect()
2545}
2546
2547pub fn schema_classes(rete: &Rete) -> Vec<(String, u32)> {
2551 use std::collections::BTreeMap;
2552 let mut counts: BTreeMap<String, u32> = BTreeMap::new();
2553 for (_s, p, o) in rete.dump(None) {
2554 if p == RDF_TYPE {
2555 *counts.entry(o).or_default() += 1;
2556 }
2557 }
2558 let mut out: Vec<(String, u32)> = counts.into_iter().collect();
2559 out.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
2560 out
2561}
2562
2563pub fn read_metadata_ranged<R: RangeReader>(reader: &R) -> Result<Option<Vec<u8>>, FileError> {
2573 let head = reader.read_at(0, HEADER_LEN as u64)?;
2574 if crate::v06::open::is_v06(&head) {
2575 let h = crate::v06::Header06::from_bytes(&head)?;
2576 return Ok(crate::v06::open::read_card_build(reader, &h)?.0);
2577 }
2578 let header = Header::from_bytes(&head)?;
2579 if header.metadata_len == 0 {
2580 return Ok(None);
2581 }
2582 let bytes = reader.read_at(header.metadata_offset, header.metadata_len)?;
2583 Ok(Some(bytes))
2584}
2585
2586#[allow(clippy::type_complexity)]
2596pub fn read_card_and_build_info_ranged<R: RangeReader>(
2597 reader: &R,
2598) -> Result<(Option<Vec<u8>>, Option<Vec<u8>>), FileError> {
2599 read_card_and_build_info_with_header(reader).map(|(_, m, b)| (m, b))
2600}
2601
2602#[allow(clippy::type_complexity)]
2613pub fn read_card_and_build_info_with_header<R: RangeReader>(
2614 reader: &R,
2615) -> Result<(Header, Option<Vec<u8>>, Option<Vec<u8>>), FileError> {
2616 let head = reader.read_at(0, HEADER_LEN as u64)?;
2617 let header = Header::from_bytes(&head)?;
2618 if header.version == crate::v06::VERSION {
2619 let h = crate::v06::Header06::from_bytes(&head)?;
2620 let (card, build) = crate::v06::open::read_card_build(reader, &h)?;
2621 return Ok((header, card, build));
2622 }
2623 let meta = (header.metadata_offset, header.metadata_len);
2624 let build = (header.build_info_offset, header.build_info_len);
2625 if meta.1 > 0 && build.1 > 0 && build.0 == meta.0 + meta.1 {
2626 let both = reader.read_at(meta.0, meta.1 + build.1)?;
2628 let (m, b) = usize::try_from(meta.1)
2629 .ok()
2630 .and_then(|mid| both.split_at_checked(mid))
2631 .ok_or(FileError::Container("short card read"))?;
2632 return Ok((header, Some(m.to_vec()), Some(b.to_vec())));
2633 }
2634 let fetch = |off: u64, len: u64| -> Result<Option<Vec<u8>>, FileError> {
2635 if len == 0 {
2636 return Ok(None);
2637 }
2638 Ok(Some(reader.read_at(off, len)?))
2639 };
2640 Ok((header, fetch(meta.0, meta.1)?, fetch(build.0, build.1)?))
2641}
2642
2643pub fn read_build_info(bytes: &[u8]) -> Result<Option<Vec<u8>>, FileError> {
2648 if crate::v06::open::is_v06(bytes) {
2649 let h = crate::v06::Header06::from_bytes(bytes)?;
2650 let r = crate::reader::SliceReader::new(bytes);
2651 return Ok(crate::v06::open::read_card_build(&r, &h)?.1);
2652 }
2653 let header = Header::from_bytes(bytes)?;
2654 if header.build_info_len == 0 {
2655 return Ok(None);
2656 }
2657 let section = checked_slice(bytes, header.build_info_offset, header.build_info_len)
2658 .ok_or(FileError::Container("build-info section overruns buffer"))?;
2659 Ok(Some(section.to_vec()))
2660}
2661
2662#[derive(Debug, Clone, PartialEq, Eq)]
2671pub struct BuildInfoPlan {
2672 pub header: [u8; HEADER_LEN],
2674 pub insert: u64,
2677 pub tail_start: u64,
2681 pub new_len: u64,
2683}
2684
2685pub fn plan_build_info(
2707 head: &[u8],
2708 file_len: u64,
2709 info_len: u64,
2710) -> Result<BuildInfoPlan, FileError> {
2711 let mut header = Header::from_bytes(head)?;
2712 let insert = if header.metadata_len > 0 {
2717 header
2718 .metadata_offset
2719 .checked_add(header.metadata_len)
2720 .filter(|&end| header.metadata_offset >= HEADER_LEN as u64 && end <= file_len)
2721 .ok_or(FileError::Container(
2722 "the metadata section lies outside the file",
2723 ))?
2724 } else {
2725 HEADER_LEN as u64
2726 };
2727 let old_len = header.build_info_len;
2728 if old_len > 0 && header.build_info_offset != insert {
2729 return Err(FileError::Container(
2730 "existing build-info section is not adjacent to the metadata",
2731 ));
2732 }
2733 let tail_start = insert.saturating_add(old_len);
2734 if tail_start > file_len || insert > file_len {
2735 return Err(FileError::Container("build-info splice out of bounds"));
2736 }
2737
2738 let shift = |off: &mut u64, len: u64| {
2740 if len > 0 && *off >= tail_start {
2741 *off = *off - old_len + info_len;
2742 }
2743 };
2744 shift(&mut header.dictionary_offset, header.dictionary_len);
2745 shift(&mut header.root_dir_offset, header.root_dir_len);
2746 shift(&mut header.pyramid_meta_offset, header.pyramid_meta_len);
2747 shift(&mut header.named_graphs_offset, header.named_graphs_len);
2748 shift(&mut header.text_index_offset, header.text_index_len);
2749 for s in &mut header.extra_sections {
2750 if s.length > 0 && s.offset >= tail_start {
2751 s.offset = s.offset - old_len + info_len;
2752 }
2753 }
2754 if info_len == 0 {
2755 header.build_info_offset = 0;
2756 header.build_info_len = 0;
2757 } else {
2758 header.build_info_offset = insert;
2759 header.build_info_len = info_len;
2760 }
2761
2762 Ok(BuildInfoPlan {
2763 header: header.to_bytes(),
2764 insert,
2765 tail_start,
2766 new_len: file_len - old_len + info_len,
2767 })
2768}
2769
2770pub fn attach_build_info(image: &[u8], info: &[u8]) -> Result<Vec<u8>, FileError> {
2776 if crate::v06::open::is_v06(image) {
2777 let h = crate::v06::Header06::from_bytes(image)?;
2779 let (card, _) =
2780 crate::v06::open::read_card_build(&crate::reader::SliceReader::new(image), &h)?;
2781 return crate::v06::write::replace_card(
2782 image,
2783 card.as_deref(),
2784 (!info.is_empty()).then_some(info),
2785 );
2786 }
2787 let plan = plan_build_info(image, image.len() as u64, info.len() as u64)?;
2788 let mut out = Vec::with_capacity(plan.new_len as usize);
2789 out.extend_from_slice(&plan.header);
2790 out.extend_from_slice(&image[HEADER_LEN..plan.insert as usize]);
2791 out.extend_from_slice(info);
2792 out.extend_from_slice(&image[plan.tail_start as usize..]);
2793 debug_assert_eq!(out.len() as u64, plan.new_len);
2794 Ok(out)
2795}
2796
2797pub fn replace_metadata(image: &[u8], metadata: &[u8]) -> Result<Vec<u8>, FileError> {
2817 if crate::v06::open::is_v06(image) {
2818 let h = crate::v06::Header06::from_bytes(image)?;
2820 let (_, build) =
2821 crate::v06::open::read_card_build(&crate::reader::SliceReader::new(image), &h)?;
2822 return crate::v06::write::replace_card(
2823 image,
2824 (!metadata.is_empty()).then_some(metadata),
2825 build.as_deref(),
2826 );
2827 }
2828 let mut header = Header::from_bytes(image)?;
2829 let insert = HEADER_LEN as u64;
2832 if header.metadata_len > 0 && header.metadata_offset != insert {
2833 return Err(FileError::Container(
2834 "metadata section does not sit immediately after the header",
2835 ));
2836 }
2837 let old_len = header.metadata_len;
2838 let tail_start = (insert + old_len) as usize;
2839 if tail_start > image.len() {
2840 return Err(FileError::Container("metadata splice out of bounds"));
2841 }
2842 let new_len = metadata.len() as u64;
2843
2844 let shift = |off: &mut u64, len: u64| {
2845 if len > 0 && *off >= insert + old_len {
2846 *off = *off - old_len + new_len;
2847 }
2848 };
2849 shift(&mut header.dictionary_offset, header.dictionary_len);
2850 shift(&mut header.root_dir_offset, header.root_dir_len);
2851 shift(&mut header.pyramid_meta_offset, header.pyramid_meta_len);
2852 shift(&mut header.named_graphs_offset, header.named_graphs_len);
2853 shift(&mut header.text_index_offset, header.text_index_len);
2854 shift(&mut header.build_info_offset, header.build_info_len);
2855 for s in &mut header.extra_sections {
2856 if s.length > 0 && s.offset >= insert + old_len {
2857 s.offset = s.offset - old_len + new_len;
2858 }
2859 }
2860 header.metadata_offset = insert;
2861 header.metadata_len = new_len;
2862
2863 let mut out = Vec::with_capacity(image.len() - old_len as usize + metadata.len());
2864 out.extend_from_slice(&header.to_bytes());
2865 out.extend_from_slice(metadata);
2866 out.extend_from_slice(&image[tail_start..]);
2867 header.content_hash = hash_of_sections(&header, &out)?;
2871 out[..HEADER_LEN].copy_from_slice(&header.to_bytes());
2872 Ok(out)
2873}
2874
2875fn hash_of_sections(header: &Header, bytes: &[u8]) -> Result<[u8; 16], FileError> {
2881 let slice = |off: u64, len: u64| -> Result<&[u8], FileError> {
2882 checked_slice(bytes, off, len).ok_or(FileError::Container("section overruns buffer"))
2891 };
2892 let parts = content_hash_parts(header)
2893 .into_iter()
2894 .map(|(off, len)| slice(off, len))
2895 .collect::<Result<Vec<&[u8]>, FileError>>()?;
2896 Ok(content_hash(&parts))
2897}
2898
2899pub fn content_hash_parts(header: &Header) -> Vec<(u64, u64)> {
2912 let mut parts: Vec<(u64, u64)> = Vec::with_capacity(6);
2913 if header.metadata_len > 0 {
2914 parts.push((header.metadata_offset, header.metadata_len));
2915 }
2916 parts.push((header.dictionary_offset, header.dictionary_len));
2917 parts.push((header.root_dir_offset, header.root_dir_len));
2918 if header.pyramid_meta_len > 0 {
2921 parts.push((header.pyramid_meta_offset, header.pyramid_meta_len));
2922 }
2923 if header.text_index_len > 0 && !header.text_index_partial {
2925 parts.push((header.text_index_offset, header.text_index_len));
2926 }
2927 if header.named_graphs_len > 0 {
2928 parts.push((header.named_graphs_offset, header.named_graphs_len));
2929 }
2930 parts
2931}
2932
2933pub fn verify_ranged_v06(
2936 reader: &dyn crate::reader::RangeReader,
2937 head: &[u8],
2938 only: Option<u8>,
2939) -> Result<Vec<String>, FileError> {
2940 const SLICE: u64 = 8 << 20;
2941 let h = crate::v06::Header06::from_bytes(head)?;
2942 if h.file_len != reader.len() {
2943 return Ok(vec!["file length".to_string()]);
2944 }
2945 let mut bad = Vec::new();
2946 let root_bytes = reader.read_at(crate::v06::HEADER_LEN as u64, h.root_len as u64)?;
2947 if crate::v06::digest16(&root_bytes) != h.root_digest {
2948 bad.push("root".to_string());
2949 return Ok(bad);
2950 }
2951 let root = crate::v06::Root06::decode(&root_bytes, h.internal_codec)?;
2952 if crate::v06::content_hash(&h) != h.content_hash {
2953 bad.push("content hash".to_string());
2954 }
2955 let card = h.card_range();
2956 if only.is_none() && card.len > 0 {
2957 let stored = reader.read_at(card.offset, card.len)?;
2958 if crate::v06::digest16(&stored) != h.card_digest {
2959 bad.push("card".to_string());
2960 }
2961 }
2962 for s in &root.sections {
2963 if only.is_some_and(|k| k != s.kind) {
2964 continue;
2965 }
2966 let a = match s.abs(&h) {
2967 Ok(a) if a.offset + a.len <= h.file_len => a,
2968 _ => {
2969 bad.push(crate::v06::kind::name(s.kind).to_string());
2970 continue;
2971 }
2972 };
2973 let mut hs = blake3::Hasher::new();
2974 let mut at = a.offset;
2975 while at < a.offset + a.len {
2976 let n = (a.offset + a.len - at).min(SLICE);
2977 hs.update(&reader.read_at(at, n)?);
2978 at += n;
2979 }
2980 if hs.finalize().as_bytes()[..16] != s.digest {
2981 bad.push(crate::v06::kind::name(s.kind).to_string());
2982 }
2983 }
2984 if only.is_none() {
2985 let n = crate::v06::FOOTER.len() as u64;
2986 if reader.read_at(h.file_len - n, n)? != crate::v06::FOOTER {
2987 bad.push("footer".to_string());
2988 }
2989 }
2990 Ok(bad)
2991}
2992
2993pub fn verify(bytes: &[u8]) -> Result<bool, FileError> {
3001 if crate::v06::open::is_v06(bytes) {
3002 let r: std::sync::Arc<dyn RangeReader + Send + Sync> =
3003 std::sync::Arc::new(crate::reader::BytesReader(std::sync::Arc::from(bytes)));
3004 return crate::v06::open::V06File::open(r, bytes)?.verify(None);
3005 }
3006 let header = Header::from_bytes(bytes)?;
3007 if hash_of_sections(&header, bytes)? != header.content_hash {
3008 return Ok(false);
3009 }
3010 if header.has_quad_index() || header.quad_index_section().is_some() {
3013 check_named_layout(&header)?;
3014 let slice = |off: u64, len: u64| -> Result<&[u8], FileError> {
3015 checked_slice(bytes, off, len).ok_or(FileError::Container("section overruns buffer"))
3016 };
3017 let root = slice(header.root_dir_offset, header.root_dir_len)?;
3018 let ranges = container_section_payload_ranges(
3019 root,
3020 header.root_dir_offset,
3021 header.index_container_sections(),
3022 )?;
3023 let seal = ranges
3024 .last()
3025 .ok_or(FileError::Container("missing quad-index seal"))?;
3026 let want = crate::quad::parse_seal(slice(seal.offset, seal.len)?)?;
3027 let q = header
3028 .quad_index_section()
3029 .ok_or(FileError::Container("missing quad-index section"))?;
3030 if crate::quad::digest(slice(q.offset, q.length)?) != want {
3031 return Ok(false);
3032 }
3033 }
3034 for s in self_digested_sections(&header) {
3035 let section = checked_slice(bytes, s.offset, s.length)
3036 .ok_or(FileError::Container("section overruns buffer"))?;
3037 if !crate::label_index::digest_ok(section) {
3038 return Ok(false);
3039 }
3040 }
3041 Ok(true)
3042}
3043
3044fn self_digested_sections(header: &Header) -> Vec<Section> {
3048 [
3049 header.label_index_section(),
3050 header.schema_section(),
3051 header
3052 .section(crate::header::SectionKind::TextIndexPartial)
3053 .filter(|s| s.length > 0),
3054 ]
3055 .into_iter()
3056 .flatten()
3057 .collect()
3058}
3059
3060pub fn append_sections(
3068 image: &[u8],
3069 sections: &[(SectionKind, &[u8])],
3070) -> Result<Vec<u8>, FileError> {
3071 let lens: Vec<(SectionKind, u64)> =
3072 sections.iter().map(|(k, b)| (*k, b.len() as u64)).collect();
3073 let (header, body_end) = plan_append(image, image.len() as u64, &lens)?;
3074 let extra: usize = sections.iter().map(|(_, b)| b.len()).sum();
3075 let mut out = Vec::with_capacity(image.len() + extra);
3076 out.extend_from_slice(&header.to_bytes());
3077 out.extend_from_slice(&image[HEADER_LEN..body_end as usize]);
3078 for (_, b) in sections {
3079 out.extend_from_slice(b);
3080 }
3081 out.extend_from_slice(&MAGIC);
3082 Ok(out)
3083}
3084
3085fn plan_append(
3087 head: &[u8],
3088 file_len: u64,
3089 sections: &[(SectionKind, u64)],
3090) -> Result<(Header, u64), FileError> {
3091 let mut header = Header::from_bytes(head)?;
3092 if header.expected_file_len() != Some(file_len) {
3093 return Err(FileError::Container(
3094 "cannot append sections: the file length does not match its header",
3095 ));
3096 }
3097 let body_end = file_len - MAGIC.len() as u64;
3098 let mut at = body_end;
3099 for &(kind, len) in sections {
3100 if !matches!(kind, SectionKind::LabelIndex | SectionKind::Schema) {
3101 return Err(FileError::Container(
3102 "only LABEL_INDEX and SCHEMA sections can be appended",
3103 ));
3104 }
3105 if len == 0 {
3106 continue;
3107 }
3108 if header.section(kind).is_some_and(|s| s.length > 0) {
3109 return Err(FileError::Container(
3110 "the file already carries that section",
3111 ));
3112 }
3113 header = header.with_section(kind, at, len);
3114 at += len;
3115 }
3116 Ok((header, body_end))
3117}
3118
3119#[cfg(not(target_arch = "wasm32"))]
3124pub(crate) fn open_dictionary_lazy<R: RangeReader + Send + Sync + 'static>(
3125 reader: R,
3126) -> Result<Dictionary, FileError> {
3127 let head = reader.read_at(0, HEADER_LEN as u64)?;
3128 let header = Header::from_bytes(&head)?;
3129 ranged_chunked_dictionary(&std::sync::Arc::new(reader), &header, [true; 4])
3130}
3131
3132pub fn verify_ranged(reader: &dyn crate::reader::RangeReader) -> Result<bool, FileError> {
3138 const SLICE: u64 = 8 << 20;
3141 let len = reader.len();
3142 if len < HEADER_LEN as u64 {
3143 return Err(FileError::Container("file shorter than its header"));
3144 }
3145 let head = reader.read_at(0, HEADER_LEN as u64)?;
3146 if crate::v06::open::is_v06(&head) {
3147 return verify_ranged_v06(reader, &head, None).map(|bad| bad.is_empty());
3148 }
3149 let header = Header::from_bytes(&head)?;
3150 let mut h = blake3::Hasher::new();
3151 for (off, n) in content_hash_parts(&header) {
3152 if off.checked_add(n).is_none_or(|end| end > len) {
3155 return Err(FileError::Container("section overruns buffer"));
3156 }
3157 let mut at = off;
3158 while at < off + n {
3159 let take = (off + n - at).min(SLICE);
3160 h.update(&reader.read_at(at, take)?);
3161 at += take;
3162 }
3163 }
3164 let mut out = [0u8; 16];
3165 out.copy_from_slice(&h.finalize().as_bytes()[..16]);
3166 if out != header.content_hash {
3167 return Ok(false);
3168 }
3169 if header.has_quad_index() || header.quad_index_section().is_some() {
3172 check_named_layout(&header)?;
3173 let sections = header.index_container_sections();
3174 let seal = locate_container_section_ranged(
3175 reader,
3176 header.root_dir_offset,
3177 header.root_dir_len,
3178 sections - 1,
3179 sections as u64,
3180 )?;
3181 let want = crate::quad::parse_seal(&reader.read_at(seal.offset, seal.len)?)?;
3182 let q = header
3183 .quad_index_section()
3184 .ok_or(FileError::Container("missing quad-index section"))?;
3185 if q.offset.checked_add(q.length).is_none_or(|end| end > len) {
3186 return Err(FileError::Container("section overruns buffer"));
3187 }
3188 let mut h = blake3::Hasher::new();
3189 let mut at = q.offset;
3190 while at < q.offset + q.length {
3191 let take = (q.offset + q.length - at).min(SLICE);
3192 h.update(&reader.read_at(at, take)?);
3193 at += take;
3194 }
3195 let mut got = [0u8; 16];
3196 got.copy_from_slice(&h.finalize().as_bytes()[..16]);
3197 if got != want {
3198 return Ok(false);
3199 }
3200 }
3201 for s in self_digested_sections(&header) {
3203 if s.length < 16 || s.offset.checked_add(s.length).is_none_or(|end| end > len) {
3204 return Err(FileError::Container("section overruns buffer"));
3205 }
3206 let want = reader.read_at(s.offset, 16)?;
3207 let mut h = blake3::Hasher::new();
3208 let mut at = s.offset + 16;
3209 while at < s.offset + s.length {
3210 let take = (s.offset + s.length - at).min(SLICE);
3211 h.update(&reader.read_at(at, take)?);
3212 at += take;
3213 }
3214 if h.finalize().as_bytes()[..16] != want[..] {
3215 return Ok(false);
3216 }
3217 }
3218 Ok(true)
3219}
3220
3221type PyramidLoader = Box<dyn Fn() -> Option<PyramidMeta> + Send + Sync>;
3223
3224enum PyramidSlot {
3230 Resident(Option<PyramidMeta>),
3231 Lazy {
3232 loader: PyramidLoader,
3233 cell: std::sync::OnceLock<Option<PyramidMeta>>,
3234 },
3235}
3236
3237pub(crate) enum SectionSlot<T> {
3242 Resident(Option<T>),
3243 Lazy {
3244 loader: Box<dyn Fn() -> Option<T> + Send + Sync>,
3245 cell: std::sync::OnceLock<Option<T>>,
3246 },
3247}
3248
3249impl<T> SectionSlot<T> {
3250 pub(crate) fn get(&self) -> Option<&T> {
3252 match self {
3253 SectionSlot::Resident(v) => v.as_ref(),
3254 SectionSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
3255 }
3256 }
3257
3258 pub(crate) fn get_if_loaded(&self) -> Option<&T> {
3260 match self {
3261 SectionSlot::Resident(v) => v.as_ref(),
3262 SectionSlot::Lazy { cell, .. } => cell.get().and_then(|o| o.as_ref()),
3263 }
3264 }
3265}
3266
3267fn resident_label_slot<'a>(
3269 header: &Header,
3270 region: &dyn Fn(u64, u64) -> Result<&'a [u8], FileError>,
3271) -> SectionSlot<crate::label_index::LabelIndex> {
3272 SectionSlot::Resident(header.label_index_section().and_then(|s| {
3273 let bytes = region(s.offset, s.length).ok()?;
3274 crate::label_index::LabelIndex::from_section(bytes, header.block_codec).ok()
3275 }))
3276}
3277
3278fn resident_schema_slot<'a>(
3280 header: &Header,
3281 region: &dyn Fn(u64, u64) -> Result<&'a [u8], FileError>,
3282) -> SectionSlot<crate::schema_section::SchemaMeta> {
3283 SectionSlot::Resident(header.schema_section().and_then(|s| {
3284 crate::schema_section::SchemaMeta::decode(region(s.offset, s.length).ok()?).ok()
3285 }))
3286}
3287
3288fn ranged_label_slot<R: RangeReader + Send + Sync + 'static>(
3292 reader: &std::sync::Arc<R>,
3293 header: &Header,
3294) -> SectionSlot<crate::label_index::LabelIndex> {
3295 let Some(s) = header.label_index_section() else {
3296 return SectionSlot::Resident(None);
3297 };
3298 let r = reader.clone();
3299 let codec = header.block_codec;
3300 SectionSlot::Lazy {
3301 loader: Box::new(move || {
3302 let head = r
3303 .read_at(s.offset, crate::label_index::HEAD_READ.min(s.length))
3304 .ok()?;
3305 let rr = r.clone();
3306 let loader = Box::new(move |off: u64, len: u64| rr.read_at(s.offset + off, len).ok());
3307 crate::label_index::LabelIndex::from_head(&head, s.length, codec, loader).ok()
3308 }),
3309 cell: std::sync::OnceLock::new(),
3310 }
3311}
3312
3313fn ranged_schema_slot<R: RangeReader + Send + Sync + 'static>(
3315 reader: &std::sync::Arc<R>,
3316 header: &Header,
3317) -> SectionSlot<crate::schema_section::SchemaMeta> {
3318 let Some(s) = header.schema_section() else {
3319 return SectionSlot::Resident(None);
3320 };
3321 let r = reader.clone();
3322 SectionSlot::Lazy {
3323 loader: Box::new(move || {
3324 let bytes = r.read_at(s.offset, s.length).ok()?;
3325 crate::schema_section::SchemaMeta::decode(&bytes).ok()
3326 }),
3327 cell: std::sync::OnceLock::new(),
3328 }
3329}
3330
3331pub fn read_schema_ranged<R: RangeReader + ?Sized>(
3335 reader: &R,
3336) -> Result<Option<crate::schema_section::SchemaMeta>, FileError> {
3337 let head = reader.read_at(0, HEADER_LEN as u64)?;
3338 let header = Header::from_bytes(&head)?;
3339 Ok(read_schema_with_header(reader, &header))
3340}
3341
3342fn read_schema_with_header<R: RangeReader + ?Sized>(
3343 reader: &R,
3344 header: &Header,
3345) -> Option<crate::schema_section::SchemaMeta> {
3346 if header.version == crate::v06::VERSION {
3347 let b = crate::v06::open::read_schema_bytes(reader).ok()??;
3348 return crate::schema_section::SchemaMeta::decode(&b).ok();
3349 }
3350 let s = header.schema_section()?;
3351 let bytes = reader.read_at(s.offset, s.length).ok()?;
3352 crate::schema_section::SchemaMeta::decode(&bytes).ok()
3353}
3354
3355type TextIndexLoader = Box<dyn Fn() -> Option<crate::text_index::TextIndex> + Send + Sync>;
3358
3359type TokenTableProbe = Box<dyn Fn() -> Option<u64> + Send + Sync>;
3362
3363enum TextIndexSlot {
3373 Resident {
3374 index: Option<crate::text_index::TextIndex>,
3375 token_table_len: Option<u64>,
3378 },
3379 Lazy {
3380 loader: TextIndexLoader,
3381 cell: std::sync::OnceLock<Option<crate::text_index::TextIndex>>,
3382 token_table: TokenTableProbe,
3384 token_table_cell: std::sync::OnceLock<Option<u64>>,
3385 },
3386}
3387
3388impl TextIndexSlot {
3389 fn index(&self) -> Option<&crate::text_index::TextIndex> {
3391 match self {
3392 TextIndexSlot::Resident { index, .. } => index.as_ref(),
3393 TextIndexSlot::Lazy { loader, cell, .. } => cell.get_or_init(loader).as_ref(),
3394 }
3395 }
3396
3397 fn token_table_len(&self) -> Option<u64> {
3400 match self {
3401 TextIndexSlot::Resident {
3402 token_table_len, ..
3403 } => *token_table_len,
3404 TextIndexSlot::Lazy {
3405 token_table,
3406 token_table_cell,
3407 ..
3408 } => *token_table_cell.get_or_init(token_table),
3409 }
3410 }
3411}
3412
3413fn resident_text_index_slot(section: Option<&[u8]>, codec: u8) -> Result<TextIndexSlot, FileError> {
3416 let Some(section) = section else {
3417 return Ok(TextIndexSlot::Resident {
3418 index: None,
3419 token_table_len: None,
3420 });
3421 };
3422 Ok(TextIndexSlot::Resident {
3423 index: Some(
3424 crate::text_index::TextIndex::from_section(section, codec)
3425 .map_err(|_| FileError::Container("malformed text index"))?,
3426 ),
3427 token_table_len: crate::text_index::TextIndex::postings_base(section).map(|b| b as u64),
3428 })
3429}
3430
3431pub fn read_text_index_token_table_len_ranged<R: RangeReader + ?Sized>(
3444 reader: &R,
3445 header: &Header,
3446) -> Option<u64> {
3447 if header.version == crate::v06::VERSION {
3448 return crate::v06::open::read_token_table_bytes(reader);
3449 }
3450 let (ti_off, ti_len) = header.text_index_payload();
3451 if ti_len == 0 {
3452 return None;
3453 }
3454 read_token_table_len(reader, ti_off, ti_len)
3455}
3456
3457fn read_token_table_len<R: RangeReader + ?Sized>(reader: &R, off: u64, len: u64) -> Option<u64> {
3461 let head = reader.read_at(off, 10u64.min(len)).ok()?;
3462 let (ttlen, n) = crate::varint::read_uvarint(&head)?;
3463 Some((n as u64 + ttlen).min(len))
3464}
3465
3466const NAMED_GRAPH_RESIDENT_MAX: u64 = 1 << 20; pub const TRANSIENT_GRAPH_QUADS: u64 = 1024;
3478
3479const NAMED_SLAB: usize = 1024;
3484
3485const NAMED_WALK_CHUNK: u64 = 64 * 1024;
3489
3490const NAMED_WALK_CHUNK_MAX: u64 = 8 * 1024 * 1024;
3503
3504#[derive(Default)]
3506struct NamedEntry {
3507 meta: std::sync::OnceLock<(String, ByteRange)>,
3510 index: std::sync::OnceLock<Box<GraphIndex>>,
3513}
3514
3515#[derive(Default)]
3519struct NamedWalk {
3520 next: usize,
3522 pos: u64,
3526 buf: Vec<u8>,
3533 buf_off: u64,
3534 chunk: u64,
3541 big_seen: bool,
3548}
3549
3550struct LazyNamedGraphs {
3564 reader: std::sync::Arc<dyn RangeReader + Send + Sync>,
3565 section: ByteRange,
3567 codec: u8,
3568 has_synopsis: bool,
3569 read_concurrency: usize,
3570 perms: PermSet,
3573 #[allow(clippy::type_complexity)]
3577 dir: std::sync::OnceLock<(usize, Box<[std::sync::OnceLock<Box<[NamedEntry]>>]>)>,
3578 walk: std::sync::Mutex<NamedWalk>,
3579 failed: std::sync::atomic::AtomicBool,
3580 tile_cap: std::sync::atomic::AtomicU64,
3586}
3587
3588impl LazyNamedGraphs {
3589 fn new(
3590 reader: std::sync::Arc<dyn RangeReader + Send + Sync>,
3591 section: ByteRange,
3592 codec: u8,
3593 has_synopsis: bool,
3594 read_concurrency: usize,
3595 perms: PermSet,
3596 ) -> Self {
3597 LazyNamedGraphs {
3598 reader,
3599 section,
3600 codec,
3601 has_synopsis,
3602 read_concurrency,
3603 perms,
3604 dir: std::sync::OnceLock::new(),
3605 walk: std::sync::Mutex::new(NamedWalk::default()),
3606 failed: std::sync::atomic::AtomicBool::new(false),
3607 tile_cap: std::sync::atomic::AtomicU64::new(u64::MAX),
3608 }
3609 }
3610
3611 fn set_tile_cap(&self, cap: u64) {
3615 self.tile_cap
3616 .store(cap, std::sync::atomic::Ordering::Relaxed);
3617 self.for_each_opened(|g| {
3618 if g.is_lazy() {
3619 g.set_cache_cap(cap);
3620 }
3621 });
3622 }
3623
3624 fn fail(&self) {
3626 self.failed
3627 .store(true, std::sync::atomic::Ordering::Relaxed);
3628 }
3629
3630 #[allow(clippy::type_complexity)]
3632 fn directory(&self) -> Option<&(usize, Box<[std::sync::OnceLock<Box<[NamedEntry]>>]>)> {
3633 if let Some(d) = self.dir.get() {
3634 return Some(d);
3635 }
3636 let end = self.section.offset.checked_add(self.section.len)?;
3637 let (n, used) = match read_uvarint_at(&self.reader, self.section.offset, end) {
3638 Ok(v) => v,
3639 Err(_) => {
3640 self.fail();
3641 return None;
3642 }
3643 };
3644 if n > self.section.len / 2 {
3647 self.fail();
3648 return None;
3649 }
3650 let Ok(n) = usize::try_from(n) else {
3651 self.fail();
3652 return None;
3653 };
3654 let slabs = n.div_ceil(NAMED_SLAB);
3655 let table: Box<[std::sync::OnceLock<Box<[NamedEntry]>>]> =
3656 (0..slabs).map(|_| std::sync::OnceLock::new()).collect();
3657 let _ = self.dir.set((n, table));
3658 {
3659 let mut w = self.walk.lock().unwrap();
3662 if w.pos == 0 {
3663 w.pos = self.section.offset + used;
3664 }
3665 }
3666 self.dir.get()
3667 }
3668
3669 fn count(&self) -> usize {
3670 self.directory().map(|(n, _)| *n).unwrap_or(0)
3671 }
3672
3673 fn entry(&self, i: usize) -> Option<&NamedEntry> {
3675 let (n, table) = self.directory()?;
3676 if i >= *n {
3677 return None;
3678 }
3679 let slab = table[i / NAMED_SLAB].get_or_init(|| {
3680 let len = NAMED_SLAB.min(n - (i / NAMED_SLAB) * NAMED_SLAB);
3681 (0..len).map(|_| NamedEntry::default()).collect()
3682 });
3683 slab.get(i % NAMED_SLAB)
3684 }
3685
3686 fn next_read_len(w: &mut NamedWalk, pos: u64, end: u64, need: u64) -> u64 {
3694 let cur = w.chunk.max(NAMED_WALK_CHUNK);
3695 w.chunk = (cur * 2).min(NAMED_WALK_CHUNK_MAX);
3696 cur.max(need).min(end - pos)
3697 }
3698
3699 fn ensure_meta(&self, upto: usize, exhaustive: bool) -> Option<()> {
3714 let target = self.entry(upto)?;
3715 if target.meta.get().is_some() {
3716 return Some(());
3717 }
3718 let end = self.section.offset.checked_add(self.section.len)?;
3719 let mut w = self.walk.lock().unwrap();
3720 if exhaustive && !w.big_seen {
3721 w.chunk = NAMED_WALK_CHUNK_MAX;
3722 }
3723 while w.next <= upto {
3724 let pos = w.pos;
3725 if pos >= end {
3726 self.fail();
3728 return None;
3729 }
3730 let have =
3733 |b: &[u8], off: u64, need: u64| pos >= off && pos + need <= off + b.len() as u64;
3734 if !have(&w.buf, w.buf_off, 20.min(end - pos)) {
3735 let len = Self::next_read_len(&mut w, pos, end, 20.min(end - pos));
3736 w.buf = match self.reader.read_at(pos, len) {
3737 Ok(b) => b,
3738 Err(_) => {
3739 self.fail();
3740 return None;
3741 }
3742 };
3743 w.buf_off = pos;
3744 }
3745 let rel = (pos - w.buf_off) as usize;
3748 let Some((ilen, u1)) = w.buf.get(rel..).and_then(read_uvarint) else {
3749 self.fail();
3750 return None;
3751 };
3752 let Some(iri_end) = (u1 as u64)
3753 .checked_add(ilen)
3754 .and_then(|n| pos.checked_add(n))
3755 .filter(|&e| e <= end)
3756 else {
3757 self.fail(); return None;
3759 };
3760 let header_need = (iri_end - pos).saturating_add(10);
3762 if !have(&w.buf, w.buf_off, header_need.min(end - pos)) {
3763 let len = Self::next_read_len(&mut w, pos, end, header_need);
3764 w.buf = match self.reader.read_at(pos, len) {
3765 Ok(b) => b,
3766 Err(_) => {
3767 self.fail();
3768 return None;
3769 }
3770 };
3771 w.buf_off = pos;
3772 }
3773 let rel = (pos - w.buf_off) as usize;
3774 let istart = rel + u1;
3775 let Some(iend) = usize::try_from(ilen)
3776 .ok()
3777 .and_then(|l| istart.checked_add(l))
3778 .filter(|&e| e <= w.buf.len())
3779 else {
3780 self.fail(); return None;
3782 };
3783 let iri = String::from_utf8_lossy(&w.buf[istart..iend]).into_owned();
3784 let Some((clen, u2)) = read_uvarint(&w.buf[iend..]) else {
3785 self.fail();
3786 return None;
3787 };
3788 let cend = match iri_end
3789 .checked_add(u2 as u64)
3790 .and_then(|cstart| cstart.checked_add(clen))
3791 {
3792 Some(e) if e <= end => e,
3793 _ => {
3794 self.fail(); return None;
3796 }
3797 };
3798 if clen > NAMED_GRAPH_RESIDENT_MAX {
3799 w.big_seen = true;
3804 w.chunk = NAMED_WALK_CHUNK;
3805 }
3806 let range = ByteRange {
3807 offset: cend - clen,
3808 len: clen,
3809 };
3810 if let Some(e) = self.entry(w.next) {
3811 let _ = e.meta.set((iri, range));
3812 }
3813 w.pos = cend;
3814 w.next += 1;
3815 }
3816 Some(())
3817 }
3818
3819 fn name_at(&self, i: usize, exhaustive: bool) -> Option<&str> {
3822 self.ensure_meta(i, exhaustive)?;
3823 self.entry(i)?.meta.get().map(|(iri, _)| iri.as_str())
3824 }
3825
3826 fn graph_at(&self, i: usize, exhaustive: bool) -> Option<(&str, &GraphIndex)> {
3829 self.ensure_meta(i, exhaustive)?;
3830 let e = self.entry(i)?;
3831 let (iri, range) = e.meta.get()?;
3832 if e.index.get().is_none() {
3833 let opened = self.open_graph(*range)?;
3834 let _ = e.index.set(Box::new(opened));
3835 }
3836 Some((iri.as_str(), e.index.get()?.as_ref()))
3837 }
3838
3839 fn container_bytes(&self, range: ByteRange) -> Option<Vec<u8>> {
3843 {
3844 let w = self.walk.lock().unwrap();
3845 let buf_end = w.buf_off + w.buf.len() as u64;
3846 if range.offset >= w.buf_off && range.offset + range.len <= buf_end {
3847 let a = (range.offset - w.buf_off) as usize;
3848 return Some(w.buf[a..a + range.len as usize].to_vec());
3849 }
3850 }
3851 match self.reader.read_at(range.offset, range.len) {
3852 Ok(b) if b.len() as u64 == range.len => Some(b),
3854 _ => {
3855 self.fail();
3856 None
3857 }
3858 }
3859 }
3860
3861 fn open_graph(&self, range: ByteRange) -> Option<GraphIndex> {
3866 if range.len <= NAMED_GRAPH_RESIDENT_MAX {
3867 let bytes = self.container_bytes(range)?;
3868 match decode_index_container(&bytes, self.codec, self.perms) {
3869 Ok(g) => {
3870 g.set_size_hint(u64::from(g.triple_count()));
3873 Some(g)
3874 }
3875 Err(_) => {
3876 self.fail();
3877 None
3878 }
3879 }
3880 } else {
3881 match open_index_container_lazy(
3882 &self.reader,
3883 range,
3884 self.codec,
3885 self.has_synopsis,
3886 self.read_concurrency,
3887 self.perms,
3888 self.perms.len(),
3889 ) {
3890 Ok((g, _, _)) => {
3891 let cap = self.tile_cap.load(std::sync::atomic::Ordering::Relaxed);
3895 if cap != u64::MAX {
3896 g.set_cache_cap(cap);
3897 }
3898 Some(g)
3899 }
3900 Err(_) => {
3901 self.fail();
3902 None
3903 }
3904 }
3905 }
3906 }
3907
3908 fn release(&mut self, iri: &str) {
3914 let Some((_, table)) = self.dir.get_mut() else {
3915 return;
3916 };
3917 for slab in table.iter_mut() {
3918 let Some(entries) = slab.get_mut() else {
3919 continue;
3920 };
3921 for e in entries.iter_mut() {
3922 let matches = e
3923 .meta
3924 .get()
3925 .map(|(name, _)| name.as_str() == iri)
3926 .unwrap_or(false);
3927 if matches {
3928 e.index.take();
3929 return;
3930 }
3931 }
3932 }
3933 }
3934
3935 fn for_each_opened(&self, mut f: impl FnMut(&GraphIndex)) {
3937 if let Some((_, table)) = self.dir.get() {
3938 for slab in table.iter().filter_map(|s| s.get()) {
3939 for e in slab.iter() {
3940 if let Some(g) = e.index.get() {
3941 f(g);
3942 }
3943 }
3944 }
3945 }
3946 }
3947}
3948
3949enum NamedGraphsSlot {
3954 Resident(Vec<(String, GraphIndex)>),
3955 Lazy(LazyNamedGraphs),
3956 Quad(crate::quad::QuadNamed),
3959}
3960
3961impl NamedGraphsSlot {
3962 fn directory_bytes(&self) -> u64 {
3966 match self {
3967 NamedGraphsSlot::Resident(v) => v
3968 .iter()
3969 .map(|(iri, g)| iri.capacity() as u64 + g.directory_bytes())
3970 .sum(),
3971 NamedGraphsSlot::Lazy(l) => {
3972 let mut bytes = l.walk.lock().map(|w| w.buf.capacity() as u64).unwrap_or(0);
3973 if let Some((_, table)) = l.dir.get() {
3974 bytes += table.len() as u64
3975 * std::mem::size_of::<std::sync::OnceLock<Box<[NamedEntry]>>>() as u64;
3976 for slab in table.iter().filter_map(|s| s.get()) {
3977 bytes += slab.len() as u64 * std::mem::size_of::<NamedEntry>() as u64;
3978 for e in slab.iter() {
3979 if let Some((iri, _)) = e.meta.get() {
3980 bytes += iri.capacity() as u64;
3981 }
3982 if let Some(g) = e.index.get() {
3983 bytes += g.directory_bytes();
3984 }
3985 }
3986 }
3987 }
3988 bytes
3989 }
3990 NamedGraphsSlot::Quad(q) => q.directory_bytes(),
3991 }
3992 }
3993
3994 fn count(&self) -> usize {
3995 match self {
3996 NamedGraphsSlot::Resident(v) => v.len(),
3997 NamedGraphsSlot::Lazy(l) => l.count(),
3998 NamedGraphsSlot::Quad(q) => q.count(),
3999 }
4000 }
4001
4002 fn name_at<'a>(&'a self, dict: &Dictionary, i: usize, exhaustive: bool) -> Option<&'a str> {
4006 match self {
4007 NamedGraphsSlot::Resident(v) => v.get(i).map(|(iri, _)| iri.as_str()),
4008 NamedGraphsSlot::Lazy(l) => l.name_at(i, exhaustive),
4009 NamedGraphsSlot::Quad(q) => q.name_at(dict, i, exhaustive),
4010 }
4011 }
4012
4013 fn graph_at<'a>(
4014 &'a self,
4015 dict: &Dictionary,
4016 i: usize,
4017 exhaustive: bool,
4018 ) -> Option<(&'a str, &'a GraphIndex)> {
4019 match self {
4020 NamedGraphsSlot::Resident(v) => v.get(i).map(|(iri, g)| (iri.as_str(), g)),
4021 NamedGraphsSlot::Lazy(l) => l.graph_at(i, exhaustive),
4022 NamedGraphsSlot::Quad(q) => q.graph_at(dict, i, exhaustive),
4023 }
4024 }
4025
4026 fn find<'a>(&'a self, dict: &Dictionary, iri: &str) -> Option<&'a GraphIndex> {
4027 match self {
4028 NamedGraphsSlot::Quad(q) => q.find(dict, iri),
4029 NamedGraphsSlot::Resident(v) => v.iter().find(|(name, _)| name == iri).map(|(_, g)| g),
4030 NamedGraphsSlot::Lazy(l) => {
4031 for i in 0..l.count() {
4036 if l.name_at(i, false)? == iri {
4037 return l.graph_at(i, false).map(|(_, g)| g);
4038 }
4039 }
4040 None
4041 }
4042 }
4043 }
4044
4045 fn load_incomplete(&self) -> bool {
4046 match self {
4047 NamedGraphsSlot::Resident(v) => v.iter().any(|(_, g)| g.load_incomplete()),
4048 NamedGraphsSlot::Lazy(l) => {
4049 if l.failed.load(std::sync::atomic::Ordering::Relaxed) {
4050 return true;
4051 }
4052 let mut bad = false;
4053 l.for_each_opened(|g| bad |= g.load_incomplete());
4054 bad
4055 }
4056 NamedGraphsSlot::Quad(q) => q.load_incomplete(),
4057 }
4058 }
4059
4060 fn reset_load_failures(&self) {
4061 match self {
4062 NamedGraphsSlot::Resident(v) => {
4063 for (_, g) in v {
4064 g.reset_load_failure();
4065 }
4066 }
4067 NamedGraphsSlot::Lazy(l) => {
4068 l.failed.store(false, std::sync::atomic::Ordering::Relaxed);
4069 l.for_each_opened(|g| g.reset_load_failure());
4070 }
4071 NamedGraphsSlot::Quad(q) => q.reset_load_failures(),
4072 }
4073 }
4074
4075 fn resident(
4079 header: &Header,
4080 quad_section: impl FnOnce(u64, u64) -> Result<Vec<u8>, FileError>,
4081 per_graph: impl FnOnce() -> Result<Vec<(String, GraphIndex)>, FileError>,
4082 ) -> Result<Self, FileError> {
4083 check_named_layout(header)?;
4084 if let Some(q) = header.quad_index_section() {
4085 let bytes = quad_section(q.offset, q.length)?;
4086 return Ok(NamedGraphsSlot::Quad(crate::quad::QuadNamed::resident(
4087 bytes,
4088 header.block_codec,
4089 header.perms,
4090 )));
4091 }
4092 Ok(NamedGraphsSlot::Resident(if header.named_graphs_len > 0 {
4093 per_graph()?
4094 } else {
4095 Vec::new()
4096 }))
4097 }
4098}
4099
4100pub struct Rete {
4102 header: Header,
4103 dict: Dictionary,
4104 index: GraphIndex,
4105 index_section_ranges: [ByteRange; NUM_PERMS],
4106 tile_ranges: [Vec<(u32, u32, ByteRange)>; NUM_PERMS],
4110 pyramid: PyramidSlot,
4111 text_index: TextIndexSlot,
4112 labels: SectionSlot<crate::label_index::LabelIndex>,
4114 schema: SectionSlot<crate::schema_section::SchemaMeta>,
4116 named_graphs: NamedGraphsSlot,
4117 metadata: Vec<u8>,
4122 service_client: Option<Box<dyn crate::service::ServiceClient>>,
4127 service_error: std::sync::Mutex<Option<String>>,
4131 spill_threshold: std::sync::atomic::AtomicU64,
4134 v06: Option<std::sync::Arc<crate::v06::open::V06File>>,
4136}
4137
4138impl Rete {
4139 pub fn open(bytes: &[u8]) -> Result<Self, FileError> {
4142 #[cfg(feature = "v06-parity")]
4143 if let Some(v6) = parity_image(bytes) {
4144 return Self::open(&v6);
4145 }
4146 if crate::v06::open::is_v06(bytes) {
4147 let r: std::sync::Arc<dyn RangeReader + Send + Sync> =
4150 std::sync::Arc::new(crate::reader::BytesReader(std::sync::Arc::from(bytes)));
4151 let mut rete = Self::open_v06(r, bytes, false)?;
4152 rete.metadata = rete
4153 .v06
4154 .as_ref()
4155 .and_then(|f| f.card().ok().flatten())
4156 .unwrap_or_default();
4157 return Ok(rete);
4158 }
4159 let header = Header::from_bytes(bytes)?;
4160
4161 let region = |off: u64, len: u64| -> Result<&[u8], FileError> {
4165 checked_slice(bytes, off, len)
4166 .ok_or(FileError::Container("section range out of bounds"))
4167 };
4168
4169 let dict = decode_dictionary_container(
4170 region(header.dictionary_offset, header.dictionary_len)?,
4171 header.dict_codec,
4172 )?;
4173
4174 check_named_layout(&header)?;
4175 let index_bytes = region(header.root_dir_offset, header.root_dir_len)?;
4176 let (index, _) = decode_root_index_container(index_bytes, &header)?;
4177 let index_section_ranges = decode_index_section_ranges(
4178 index_bytes,
4179 header.root_dir_offset,
4180 header.perms,
4181 header.index_container_sections(),
4182 )?;
4183
4184 let pyramid = PyramidSlot::Resident(if header.pyramid_meta_len > 0 {
4185 Some(
4186 PyramidMeta::decode(region(header.pyramid_meta_offset, header.pyramid_meta_len)?)
4187 .map_err(|_| FileError::Container("malformed pyramid meta"))?,
4188 )
4189 } else {
4190 None
4191 });
4192
4193 let (ti_off, ti_len) = header.text_index_payload();
4196 let text_index = resident_text_index_slot(
4197 if ti_len > 0 {
4198 Some(region(ti_off, ti_len)?)
4199 } else {
4200 None
4201 },
4202 header.block_codec,
4203 )?;
4204 let labels = resident_label_slot(&header, ®ion);
4205 let schema = resident_schema_slot(&header, ®ion);
4206
4207 let named_graphs = NamedGraphsSlot::resident(
4208 &header,
4209 |off, len| Ok(region(off, len)?.to_vec()),
4210 || {
4211 decode_named_graphs(
4212 region(header.named_graphs_offset, header.named_graphs_len)?,
4213 header.block_codec,
4214 header.perms,
4215 )
4216 },
4217 )?;
4218
4219 let metadata = if header.metadata_len > 0 {
4220 region(header.metadata_offset, header.metadata_len)?.to_vec()
4221 } else {
4222 Vec::new()
4223 };
4224
4225 let tile_ranges =
4226 tile_file_ranges(index_bytes, header.root_dir_offset, &index_section_ranges);
4227 Ok(Self {
4228 header,
4229 dict,
4230 index,
4231 index_section_ranges,
4232 tile_ranges,
4233 pyramid,
4234 text_index,
4235 labels,
4236 schema,
4237 named_graphs,
4238 metadata,
4239 service_client: None,
4240 service_error: std::sync::Mutex::new(None),
4241 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
4242 v06: None,
4243 })
4244 }
4245
4246 pub fn set_service_client(&mut self, client: Box<dyn crate::service::ServiceClient>) {
4251 self.service_client = Some(client);
4252 }
4253
4254 pub(crate) fn service_client(&self) -> Option<&dyn crate::service::ServiceClient> {
4255 self.service_client.as_deref()
4256 }
4257
4258 pub(crate) fn record_service_error(&self, msg: &str) {
4260 let mut e = self.service_error.lock().unwrap();
4261 if e.is_none() {
4262 *e = Some(msg.to_string());
4263 }
4264 }
4265
4266 pub(crate) fn take_service_error(&self) -> Option<String> {
4269 self.service_error.lock().unwrap().take()
4270 }
4271
4272 pub fn header(&self) -> &Header {
4273 &self.header
4274 }
4275
4276 pub fn file_layout(&self) -> Vec<LayoutSegment> {
4282 if let Some(f) = &self.v06 {
4283 return v06_layout(f);
4284 }
4285 let h = &self.header;
4286 let seg = |kind: &'static str, label: String, offset: u64, len: u64| LayoutSegment {
4287 kind,
4288 label,
4289 offset,
4290 len,
4291 };
4292 let mut out = vec![seg(
4298 "header",
4299 format!("header (fixed {} bytes)", crate::header::HEADER_LEN),
4300 0,
4301 crate::header::HEADER_LEN as u64,
4302 )];
4303 if h.metadata_len > 0 {
4304 out.push(seg(
4305 "metadata",
4306 "metadata (dataset card)".into(),
4307 h.metadata_offset,
4308 h.metadata_len,
4309 ));
4310 }
4311 out.push(seg(
4312 "dictionary",
4313 "dictionary (4 front-coded term sections)".into(),
4314 h.dictionary_offset,
4315 h.dictionary_len,
4316 ));
4317 for (si, perm) in crate::index::ALL_PERMS.into_iter().enumerate() {
4318 let sec = self.index_section_ranges[si];
4319 if sec.len == 0 {
4320 continue;
4321 }
4322 let first_tile = self.tile_ranges[si]
4323 .first()
4324 .map(|&(_, _, r)| r.offset)
4325 .unwrap_or(sec.offset + sec.len);
4326 if first_tile > sec.offset {
4327 out.push(seg(
4328 "directory",
4329 format!("{} tile directory", perm.name()),
4330 sec.offset,
4331 first_tile - sec.offset,
4332 ));
4333 }
4334 for (ti, &(min_a, max_a, r)) in self.tile_ranges[si].iter().enumerate() {
4335 out.push(seg(
4336 "tile",
4337 format!("{} tile {ti} (leading ids {min_a}..{max_a})", perm.name()),
4338 r.offset,
4339 r.len,
4340 ));
4341 }
4342 }
4343 if h.pyramid_meta_len > 0 {
4344 out.push(seg(
4345 "pyramid",
4346 "pyramid summary (communities + superedges)".into(),
4347 h.pyramid_meta_offset,
4348 h.pyramid_meta_len,
4349 ));
4350 }
4351 if h.named_graphs_len > 0 {
4352 out.push(seg(
4353 "named-graphs",
4354 format!("named graphs ({})", self.named_graphs.count()),
4355 h.named_graphs_offset,
4356 h.named_graphs_len,
4357 ));
4358 }
4359 if let Some(s) = h.label_index_section() {
4360 let label = match self.labels.get_if_loaded() {
4361 Some(li) => format!("label index ({} entries)", li.entry_count()),
4362 None => "label index".to_string(),
4363 };
4364 out.push(seg("label-index", label, s.offset, s.length));
4365 }
4366 if let Some(s) = h.schema_section() {
4367 out.push(seg(
4368 "schema",
4369 "schema (classes, query_stats, shapes)".into(),
4370 s.offset,
4371 s.length,
4372 ));
4373 }
4374 if let Some(q) = h.quad_index_section() {
4375 out.push(seg(
4376 "quad-index",
4377 format!("quad index ({} named graphs)", self.named_graphs.count()),
4378 q.offset,
4379 q.length,
4380 ));
4381 }
4382 out.sort_by_key(|s| s.offset);
4383 out
4384 }
4385
4386 pub fn metadata(&self) -> Option<&[u8]> {
4391 if self.metadata.is_empty() {
4392 None
4393 } else {
4394 Some(&self.metadata)
4395 }
4396 }
4397
4398 pub fn dictionary(&self) -> &Dictionary {
4399 &self.dict
4400 }
4401
4402 pub fn pyramid(&self) -> Option<&PyramidMeta> {
4404 match &self.pyramid {
4405 PyramidSlot::Resident(p) => p.as_ref(),
4406 PyramidSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
4408 }
4409 }
4410
4411 pub fn pyramid_if_loaded(&self) -> Option<&PyramidMeta> {
4416 match &self.pyramid {
4417 PyramidSlot::Resident(p) => p.as_ref(),
4418 PyramidSlot::Lazy { cell, .. } => cell.get().and_then(|o| o.as_ref()),
4419 }
4420 }
4421
4422 pub fn schema(&self) -> Option<&crate::schema_section::SchemaMeta> {
4428 self.schema.get()
4429 }
4430
4431 pub fn schema_or_pyramid(
4435 &self,
4436 ) -> Option<std::borrow::Cow<'_, crate::schema_section::SchemaMeta>> {
4437 if let Some(s) = self.schema() {
4438 return Some(std::borrow::Cow::Borrowed(s));
4439 }
4440 self.pyramid()
4441 .map(|p| std::borrow::Cow::Owned(crate::schema_section::SchemaMeta::from_pyramid(p)))
4442 }
4443
4444 pub fn predicate_stats(&self) -> &[crate::meta::PredStat] {
4449 if let Some(s) = self.schema().filter(|s| !s.predicate_stats.is_empty()) {
4450 return &s.predicate_stats;
4451 }
4452 self.pyramid_if_loaded()
4453 .map(|p| p.predicate_stats.as_slice())
4454 .unwrap_or(&[])
4455 }
4456
4457 pub(crate) fn planner_stats(&self, fault: bool) -> &[crate::meta::PredStat] {
4462 let schema = if fault {
4463 self.schema.get()
4464 } else {
4465 self.schema.get_if_loaded()
4466 };
4467 if let Some(s) = schema.filter(|s| !s.predicate_stats.is_empty()) {
4468 return &s.predicate_stats;
4469 }
4470 self.pyramid_if_loaded()
4471 .map(|p| p.predicate_stats.as_slice())
4472 .unwrap_or(&[])
4473 }
4474
4475 pub fn char_sets(&self) -> &[crate::meta::CharSet] {
4478 if let Some(s) = self.schema().filter(|s| !s.char_sets.is_empty()) {
4479 return &s.char_sets;
4480 }
4481 self.pyramid_if_loaded()
4482 .map(|p| p.char_sets.as_slice())
4483 .unwrap_or(&[])
4484 }
4485
4486 pub fn label_index(&self) -> &[crate::meta::LabelEntry] {
4492 self.pyramid_if_loaded()
4493 .map(|p| p.label_index.as_slice())
4494 .unwrap_or(&[])
4495 }
4496
4497 pub fn has_label_index(&self) -> bool {
4499 self.header.label_index_section().is_some()
4500 }
4501
4502 pub fn label_index_section(&self) -> Option<&crate::label_index::LabelIndex> {
4504 self.labels.get()
4505 }
4506
4507 pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
4514 if let Some(li) = self.labels.get() {
4515 return li.prefix_search(prefix, limit);
4516 }
4517 let Some(pyr) = self.pyramid() else {
4518 return Vec::new();
4519 };
4520 resolve_label_hits(&self.dict, pyr.prefix_search(prefix, limit))
4521 }
4522
4523 pub(crate) fn text_index(&self) -> Option<&crate::text_index::TextIndex> {
4526 self.text_index.index()
4527 }
4528
4529 pub fn has_text_index(&self) -> bool {
4532 self.header.text_index_len > 0
4533 }
4534
4535 pub fn text_index_scope(&self) -> Option<crate::text_index::TextIndexScope> {
4539 self.text_index().map(|ti| ti.scope().clone())
4540 }
4541
4542 pub fn text_index_token_table_len(&self) -> Option<u64> {
4557 self.text_index.token_table_len()
4558 }
4559
4560 pub fn text_search(&self, words: &[&str], prefix: Option<&str>, limit: usize) -> Vec<String> {
4570 text_search_in(self.text_index(), &self.dict, words, prefix, limit)
4571 }
4572
4573 pub fn default_index(&self) -> &GraphIndex {
4575 &self.index
4576 }
4577
4578 pub fn dump(&self, graph: Option<&str>) -> Vec<TermTriple> {
4580 let index = match graph {
4581 None => &self.index,
4582 Some(g) => match self.graph_index(g) {
4583 Some(i) => i,
4584 None => return Vec::new(),
4585 },
4586 };
4587 index
4588 .match_pattern((None, None, None))
4589 .into_iter()
4590 .filter_map(|(s, p, o)| {
4591 Some((
4592 self.dict.subject_term(s)?,
4593 self.dict.predicate_term(p)?,
4594 self.dict.object_term(o)?,
4595 ))
4596 })
4597 .collect()
4598 }
4599
4600 pub fn dump_each<F: FnMut(&str, &str, &str)>(&self, graph: Option<&str>, f: F) {
4616 self.dump_filtered_each(graph, None, None, None, f)
4617 }
4618
4619 pub fn dump_filtered_each<F: FnMut(&str, &str, &str)>(
4690 &self,
4691 graph: Option<&str>,
4692 s: Option<&str>,
4693 p: Option<&str>,
4694 o: Option<&str>,
4695 mut f: F,
4696 ) {
4697 let index = match graph {
4698 None => Some(&self.index),
4699 Some(g) => self.graph_index(g),
4700 };
4701 let Some(index) = index else { return };
4702 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
4705 return;
4706 };
4707 let dict = &self.dict;
4708 const WINDOW: usize = 4096;
4753 let cap = dict.cache_cap();
4764 let prefetch_window =
4765 cap == u64::MAX || cap >= (WINDOW as u64) * 2 * (DICT_CHUNK_BUDGET as u64);
4766 const PREFETCH_BATCH: usize = 64;
4773 let prefetch_batch = if cap == u64::MAX {
4774 usize::MAX
4775 } else {
4776 PREFETCH_BATCH
4777 };
4778 let mut ids: Vec<(u32, u32, u32)> = Vec::with_capacity(WINDOW);
4779 let mut nodes: Vec<u32> = Vec::with_capacity(WINDOW * 2);
4780 let mut preds: Vec<u32> = Vec::with_capacity(WINDOW);
4781 let mut resolver = crate::dictionary::WindowResolver::default();
4782 let mut flush = |ids: &mut Vec<(u32, u32, u32)>,
4783 resolver: &mut crate::dictionary::WindowResolver,
4784 f: &mut F| {
4785 if ids.is_empty() {
4786 return;
4787 }
4788 if prefetch_window {
4792 nodes.clear();
4793 preds.clear();
4794 for &(s, p, o) in ids.iter() {
4795 nodes.push(dict.subject_node(s));
4796 nodes.push(dict.object_node(o));
4797 preds.push(p);
4798 }
4799 dict.prefetch_terms_batched(&nodes, &preds, prefetch_batch);
4800 }
4801 dict.resolve_window(ids, resolver, |s, p, o| f(s, p, o));
4802 ids.clear();
4803 };
4804 for t in index.scan_iter(pattern) {
4805 ids.push(t);
4806 if ids.len() == WINDOW {
4807 flush(&mut ids, &mut resolver, &mut f);
4808 }
4809 }
4810 flush(&mut ids, &mut resolver, &mut f);
4811 }
4812
4813 pub fn dump_plan(
4821 &self,
4822 graph: Option<&str>,
4823 s: Option<&str>,
4824 p: Option<&str>,
4825 o: Option<&str>,
4826 ) -> DumpPlan {
4827 let index = match graph {
4828 None => Some(&self.index),
4829 Some(g) => self.graph_index(g),
4830 };
4831 let scan = index
4832 .zip(self.resolve_query_pattern(s, p, o))
4833 .map(|(ix, pattern)| ix.scan_plan(pattern));
4834 DumpPlan {
4835 scan,
4836 dictionary_bytes: match &self.v06 {
4837 Some(f) => f.section(crate::v06::kind::DICT_DATA).map_or(0, |r| r.len),
4838 None => self.header.dictionary_len,
4839 },
4840 }
4841 }
4842
4843 pub fn dump_iter(&self, graph: Option<&str>) -> impl Iterator<Item = TermTriple> + '_ {
4861 self.query_iter(graph, None, None, None)
4862 }
4863
4864 pub fn query_iter(
4875 &self,
4876 graph: Option<&str>,
4877 s: Option<&str>,
4878 p: Option<&str>,
4879 o: Option<&str>,
4880 ) -> impl Iterator<Item = TermTriple> + '_ {
4881 let index = match graph {
4882 None => Some(&self.index),
4883 Some(g) => self.graph_index(g),
4884 };
4885 index
4886 .zip(self.resolve_query_pattern(s, p, o))
4887 .into_iter()
4888 .flat_map(|(ix, pattern)| ix.scan_iter(pattern))
4889 .filter_map(move |(s, p, o)| {
4890 Some((
4891 self.dict.subject_term(s)?,
4892 self.dict.predicate_term(p)?,
4893 self.dict.object_term(o)?,
4894 ))
4895 })
4896 }
4897
4898 pub fn query_batch(
4917 &self,
4918 graph: Option<&str>,
4919 s: Option<&str>,
4920 p: Option<&str>,
4921 o: Option<&str>,
4922 cursor: u64,
4923 max_quads: usize,
4924 ) -> (Vec<TermTriple>, u64, bool) {
4925 let index = match graph {
4926 None => &self.index,
4927 Some(g) => match self.graph_index(g) {
4930 Some(i) => i,
4931 None => return (Vec::new(), cursor, true),
4932 },
4933 };
4934 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
4937 return (Vec::new(), cursor, true);
4938 };
4939 let (ids, next, done) = index.scan_batch(pattern, cursor, max_quads);
4940 if !ids.is_empty() {
4944 let mut nodes = Vec::with_capacity(ids.len() * 2);
4945 let mut preds = Vec::with_capacity(ids.len());
4946 for &(s, p, o) in &ids {
4947 nodes.push(self.dict.subject_node(s));
4948 nodes.push(self.dict.object_node(o));
4949 preds.push(p);
4950 }
4951 self.dict.prefetch_terms(&nodes, &preds);
4952 }
4953 let triples = ids
4954 .into_iter()
4955 .filter_map(|(s, p, o)| {
4956 Some((
4957 self.dict.subject_term(s)?,
4958 self.dict.predicate_term(p)?,
4959 self.dict.object_term(o)?,
4960 ))
4961 })
4962 .collect();
4963 (triples, next, done)
4964 }
4965
4966 pub fn dump_batch(
4992 &self,
4993 graph: Option<&str>,
4994 cursor: u32,
4995 max_quads: usize,
4996 ) -> (Vec<TermTriple>, u32, bool) {
4997 fn next_tile_start(tiles: &crate::index::TileList, sid: u32) -> Option<u32> {
5002 let i = tiles.first_max_ge(sid);
5003 (i < tiles.len()).then(|| tiles[i].leading_range().0.max(sid))
5004 }
5005
5006 let index = match graph {
5007 None => self.default_index(),
5008 Some(g) => match self.graph_index(g) {
5011 Some(i) => i,
5012 None => return (Vec::new(), cursor, true),
5013 },
5014 };
5015 let tiles = index.tile_sections()[IndexPermutation::Spo.section_index()];
5019 let (Some(first), Some(last)) = (
5020 tiles.first().map(|t| t.leading_range().0),
5021 tiles.last().map(|t| t.leading_range().1),
5022 ) else {
5023 return (Vec::new(), cursor, true); };
5025
5026 let mut sid = cursor.max(first);
5027 let mut probes = max_quads.saturating_mul(4).max(1 << 16);
5028 let mut ids: Vec<(u32, u32, u32)> = Vec::new();
5029 let mut exhausted = false;
5030 while sid <= last && probes > 0 {
5031 probes -= 1;
5032 let hits = index.match_pattern((Some(sid), None, None));
5033 if hits.is_empty() {
5034 match index.next_leading(IndexPermutation::Spo.section_index(), sid) {
5044 Some(next) if next > sid => {
5045 sid = next;
5046 continue;
5047 }
5048 None if !index.load_incomplete() => {
5049 exhausted = true;
5050 break;
5051 }
5052 _ => {}
5053 }
5054 match next_tile_start(tiles, sid) {
5055 None => {
5056 exhausted = true;
5057 break;
5058 }
5059 Some(next) if next > sid => {
5060 sid = next;
5061 continue;
5062 }
5063 Some(_) => {}
5064 }
5065 } else {
5066 ids.extend(hits);
5067 }
5068 if sid == u32::MAX {
5069 exhausted = true;
5070 break;
5071 }
5072 sid += 1;
5073 if ids.len() >= max_quads {
5074 break;
5075 }
5076 }
5077 let done = exhausted || sid > last;
5078
5079 let dict = self.dictionary();
5086 if !ids.is_empty() {
5087 let mut nodes = Vec::with_capacity(ids.len() * 2);
5088 let mut preds = Vec::with_capacity(ids.len());
5089 for &(s, p, o) in &ids {
5090 nodes.push(dict.subject_node(s));
5091 nodes.push(dict.object_node(o));
5092 preds.push(p);
5093 }
5094 dict.prefetch_terms(&nodes, &preds);
5095 }
5096 let triples = ids
5097 .into_iter()
5098 .filter_map(|(s, p, o)| {
5099 Some((
5100 dict.subject_term(s)?,
5101 dict.predicate_term(p)?,
5102 dict.object_term(o)?,
5103 ))
5104 })
5105 .collect();
5106 (triples, sid, done)
5107 }
5108
5109 pub fn named_graph_count(&self) -> usize {
5112 self.named_graphs.count()
5113 }
5114
5115 pub fn named_graph_quad_count(&self, i: usize) -> Option<u64> {
5120 match &self.named_graphs {
5121 NamedGraphsSlot::Quad(q) => q.quad_count_at(i),
5122 _ => None,
5123 }
5124 }
5125
5126 pub fn named_graph_transient(&self, i: usize) -> Option<GraphIndex> {
5135 match &self.named_graphs {
5136 NamedGraphsSlot::Quad(q) => q.transient_view(i, TRANSIENT_GRAPH_QUADS),
5137 _ => None,
5138 }
5139 }
5140
5141 pub(crate) fn named_graphs_with_subjects(
5148 &self,
5149 subjects: &std::collections::BTreeSet<u32>,
5150 ) -> Option<Vec<usize>> {
5151 match &self.named_graphs {
5152 NamedGraphsSlot::Quad(q) => q.graphs_with_subjects(subjects),
5153 _ => None,
5154 }
5155 }
5156
5157 pub fn named_quad_count(&self) -> Option<u64> {
5162 match &self.named_graphs {
5163 NamedGraphsSlot::Quad(q) => Some(q.quad_count()),
5164 _ => None,
5165 }
5166 }
5167
5168 pub(crate) fn index_triples(&self, index: &GraphIndex) -> u64 {
5174 index.size_hint().unwrap_or(self.header().quad_count)
5175 }
5176
5177 pub fn named_graph_name_at(&self, i: usize) -> Option<&str> {
5180 self.named_graphs.name_at(&self.dict, i, false)
5181 }
5182
5183 pub fn named_graph_at(&self, i: usize) -> Option<(&str, &GraphIndex)> {
5187 self.named_graphs.graph_at(&self.dict, i, false)
5188 }
5189
5190 pub(crate) fn named_graph_name_at_demand(&self, i: usize, exhaustive: bool) -> Option<&str> {
5196 self.named_graphs.name_at(&self.dict, i, exhaustive)
5197 }
5198
5199 pub(crate) fn named_graph_at_demand(
5202 &self,
5203 i: usize,
5204 exhaustive: bool,
5205 ) -> Option<(&str, &GraphIndex)> {
5206 self.named_graphs.graph_at(&self.dict, i, exhaustive)
5207 }
5208
5209 pub fn graph_names(&self) -> Vec<&str> {
5214 (0..self.named_graphs.count())
5215 .filter_map(|i| self.named_graphs.name_at(&self.dict, i, false))
5216 .collect()
5217 }
5218
5219 pub fn graph_index(&self, iri: &str) -> Option<&GraphIndex> {
5221 self.named_graphs.find(&self.dict, iri)
5222 }
5223
5224 pub fn match_ids(
5227 &self,
5228 pattern: (Option<u32>, Option<u32>, Option<u32>),
5229 ) -> Vec<(u32, u32, u32)> {
5230 self.index.match_pattern(pattern)
5231 }
5232
5233 pub fn predicate_pairs(&self, predicate: &str) -> Vec<(u32, u32)> {
5236 let pid = match self.dict.predicate_id(predicate) {
5237 Some(p) => p,
5238 None => return Vec::new(),
5239 };
5240 self.index
5241 .match_pattern((None, Some(pid), None))
5242 .into_iter()
5243 .map(|(s, _p, o)| (self.dict.subject_node(s), self.dict.object_node(o)))
5244 .collect()
5245 }
5246
5247 pub fn open_ranged<R: RangeReader>(reader: &R) -> Result<Self, FileError> {
5251 let head = reader.read_at(0, (HEADER_LEN as u64).min(reader.len()))?;
5252 if crate::v06::open::is_v06(&head) {
5253 let mut all = head;
5256 let at = all.len() as u64;
5257 if reader.len() > at {
5258 all.extend_from_slice(&reader.read_at(at, reader.len() - at)?);
5259 }
5260 return Self::open(&all);
5261 }
5262 let header = Header::from_bytes(&head)?;
5263
5264 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
5265 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
5266
5267 check_named_layout(&header)?;
5268 let index_bytes = reader.read_at(header.root_dir_offset, header.root_dir_len)?;
5269 let (index, _) = decode_root_index_container(&index_bytes, &header)?;
5270 let index_section_ranges = decode_index_section_ranges(
5271 &index_bytes,
5272 header.root_dir_offset,
5273 header.perms,
5274 header.index_container_sections(),
5275 )?;
5276
5277 let pyramid = PyramidSlot::Resident(if header.pyramid_meta_len > 0 {
5278 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
5279 Some(
5280 PyramidMeta::decode(&mb)
5281 .map_err(|_| FileError::Container("malformed pyramid meta"))?,
5282 )
5283 } else {
5284 None
5285 });
5286
5287 let (ti_off, ti_len) = header.text_index_payload();
5290 let text_index_bytes = if ti_len > 0 {
5291 Some(reader.read_at(ti_off, ti_len)?)
5292 } else {
5293 None
5294 };
5295 let text_index = resident_text_index_slot(text_index_bytes.as_deref(), header.block_codec)?;
5296 let labels = SectionSlot::Resident(header.label_index_section().and_then(|s| {
5299 let b = reader.read_at(s.offset, s.length).ok()?;
5300 crate::label_index::LabelIndex::from_section(&b, header.block_codec).ok()
5301 }));
5302 let schema = SectionSlot::Resident(read_schema_with_header(reader, &header));
5303
5304 let named_graphs = NamedGraphsSlot::resident(
5305 &header,
5306 |off, len| Ok(reader.read_at(off, len)?),
5307 || {
5308 let nb = reader.read_at(header.named_graphs_offset, header.named_graphs_len)?;
5309 decode_named_graphs(&nb, header.block_codec, header.perms)
5310 },
5311 )?;
5312
5313 let tile_ranges =
5317 tile_file_ranges(&index_bytes, header.root_dir_offset, &index_section_ranges);
5318 Ok(Self {
5319 header,
5320 dict,
5321 index,
5322 index_section_ranges,
5323 tile_ranges,
5324 pyramid,
5325 text_index,
5326 labels,
5327 schema,
5328 named_graphs,
5329 metadata: Vec::new(),
5330 service_client: None,
5331 service_error: std::sync::Mutex::new(None),
5332 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
5333 v06: None,
5334 })
5335 }
5336
5337 fn open_v06(
5343 reader: std::sync::Arc<dyn RangeReader + Send + Sync>,
5344 first: &[u8],
5345 budget: bool,
5346 ) -> Result<Self, FileError> {
5347 let f = std::sync::Arc::new(crate::v06::open::V06File::open(reader.clone(), first)?);
5348 let header = crate::v06::logical_header(&f.h);
5349 let rc = reader.concurrency();
5350 let dict = f.dictionary([true; 4])?;
5351 let index = f.index(rc)?;
5352 let (pyramid, text_index, labels, schema) = v06_slots(&f);
5353 let named_graphs = match f.quad(rc) {
5354 Some(q) => NamedGraphsSlot::Quad(q?),
5355 None => NamedGraphsSlot::Resident(Vec::new()),
5356 };
5357 let rete = Self {
5358 header,
5359 dict,
5360 index,
5361 index_section_ranges: [ByteRange::default(); NUM_PERMS],
5362 tile_ranges: Default::default(),
5363 pyramid,
5364 text_index,
5365 labels,
5366 schema,
5367 named_graphs,
5368 metadata: Vec::new(),
5369 service_client: None,
5370 service_error: std::sync::Mutex::new(None),
5371 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
5372 v06: Some(f),
5373 };
5374 #[cfg(not(target_arch = "wasm32"))]
5375 if budget {
5376 rete.set_memory_budget(match crate::sysmem::default_query_budget_mb() {
5377 0 => None,
5378 mb => Some(mb << 20),
5379 });
5380 }
5381 #[cfg(target_arch = "wasm32")]
5382 let _ = budget;
5383 Ok(rete)
5384 }
5385
5386 pub fn v06(&self) -> Option<&crate::v06::open::V06File> {
5388 self.v06.as_deref()
5389 }
5390
5391 pub fn open_ranged_lazy<R: RangeReader + Send + Sync + 'static>(
5403 reader: R,
5404 ) -> Result<Self, FileError> {
5405 Self::open_ranged_lazy_with(reader, crate::v06::FIRST_READ)
5406 }
5407
5408 pub fn open_ranged_lazy_with<R: RangeReader + Send + Sync + 'static>(
5413 reader: R,
5414 first_read: u64,
5415 ) -> Result<Self, FileError> {
5416 let first_len = first_read.max(HEADER_LEN as u64).min(reader.len());
5419 let first: std::sync::Arc<[u8]> = std::sync::Arc::from(reader.read_at(0, first_len)?);
5420 #[cfg(feature = "v06-parity")]
5421 if !crate::v06::open::is_v06(&first) {
5422 let all = reader.read_at(0, reader.len())?;
5423 if let Some(v6) = parity_image(&all) {
5424 let r: std::sync::Arc<dyn RangeReader + Send + Sync> =
5425 std::sync::Arc::new(crate::reader::BytesReader(std::sync::Arc::from(v6)));
5426 let head = r.read_at(0, crate::v06::FIRST_READ.min(r.len()))?;
5427 return Self::open_v06(r, &head, true);
5428 }
5429 }
5430 let reader = crate::reader::PrefixReader::new(reader, first.clone());
5431 if crate::v06::open::is_v06(&first) {
5432 let r: std::sync::Arc<dyn RangeReader + Send + Sync> = std::sync::Arc::new(reader);
5433 return Self::open_v06(r, &first, true);
5434 }
5435 let head = reader.read_at(0, HEADER_LEN as u64)?;
5436 let header = Header::from_bytes(&head)?;
5437 check_named_layout(&header)?;
5438 let reader = std::sync::Arc::new(reader);
5439 let read_concurrency = reader.concurrency();
5442
5443 let open_dict = || ranged_chunked_dictionary(&reader, &header, [true; 4]);
5447
5448 let reader_dyn: std::sync::Arc<dyn RangeReader + Send + Sync> = reader.clone();
5452 let open_index = || {
5453 open_index_container_lazy(
5454 &reader_dyn,
5455 ByteRange {
5456 offset: header.root_dir_offset,
5457 len: header.root_dir_len,
5458 },
5459 header.block_codec,
5460 header.has_tile_synopsis(),
5461 read_concurrency,
5462 header.perms,
5463 header.index_container_sections(),
5464 )
5465 };
5466
5467 let (dict, index) = open_side_by_side(read_concurrency, open_dict, open_index);
5474 let dict = dict?;
5475 let (index, index_section_ranges, tile_ranges) = index?;
5476
5477 let pyramid = ranged_pyramid_slot(&reader, &header);
5481
5482 let text_index = ranged_text_index_slot(&reader, &header);
5487
5488 let labels = ranged_label_slot(&reader, &header);
5492 let schema = ranged_schema_slot(&reader, &header);
5493
5494 let named_graphs = if let Some(q) = header.quad_index_section() {
5501 NamedGraphsSlot::Quad(crate::quad::QuadNamed::new(
5505 reader_dyn,
5506 ByteRange {
5507 offset: q.offset,
5508 len: q.length,
5509 },
5510 header.block_codec,
5511 header.perms,
5512 read_concurrency,
5513 ))
5514 } else if header.named_graphs_len > 0 {
5515 NamedGraphsSlot::Lazy(LazyNamedGraphs::new(
5516 reader_dyn,
5517 ByteRange {
5518 offset: header.named_graphs_offset,
5519 len: header.named_graphs_len,
5520 },
5521 header.block_codec,
5522 header.has_tile_synopsis(),
5523 read_concurrency,
5524 header.perms,
5525 ))
5526 } else {
5527 NamedGraphsSlot::Resident(Vec::new())
5528 };
5529
5530 let rete = Self {
5531 header,
5532 dict,
5533 index,
5534 index_section_ranges,
5535 tile_ranges,
5536 pyramid,
5537 text_index,
5538 labels,
5539 schema,
5540 named_graphs,
5541 metadata: Vec::new(),
5542 service_client: None,
5543 service_error: std::sync::Mutex::new(None),
5544 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
5545 v06: None,
5546 };
5547 #[cfg(not(target_arch = "wasm32"))]
5559 rete.set_memory_budget(match crate::sysmem::default_query_budget_mb() {
5560 0 => None,
5561 mb => Some(mb << 20),
5562 });
5563 Ok(rete)
5564 }
5565
5566 pub fn index_incomplete(&self) -> bool {
5571 self.index.load_incomplete()
5572 || self.dict.load_incomplete()
5573 || self.named_graphs.load_incomplete()
5574 }
5575
5576 pub fn reset_load_failures(&self) {
5584 self.index.reset_load_failure();
5585 self.dict.reset_load_failure();
5586 self.named_graphs.reset_load_failures();
5587 }
5588
5589 pub fn set_memory_budget(&self, budget: Option<u64>) -> MemoryBudget {
5620 let total = budget.unwrap_or(u64::MAX);
5621 self.set_spill_threshold(budget.map(spill_threshold_for));
5622 let mut split = split_memory_budget(total);
5623 let fixed = if total == u64::MAX {
5624 0
5625 } else {
5626 self.fixed_overhead().total()
5627 };
5628 if split.dict_cache != u64::MAX {
5629 split.dict_cache = split
5630 .dict_cache
5631 .saturating_sub(fixed)
5632 .max(MIN_DICT_CACHE_CAP);
5633 }
5634 self.dict.set_cache_cap(split.dict_cache);
5635 self.index.set_cache_cap(split.tile_cache);
5636 match &self.named_graphs {
5641 NamedGraphsSlot::Lazy(lazy) => lazy.set_tile_cap(split.tile_cache),
5642 NamedGraphsSlot::Quad(q) => q.set_tile_cap(split.tile_cache),
5645 NamedGraphsSlot::Resident(_) => {}
5646 }
5647 if std::env::var("RETE_OPEN_DEBUG").is_ok() {
5648 eprintln!(
5649 "[memory-budget] total={} dict_cache={} tile_cache={} block_cache={} fixed={:.1}MiB",
5650 if total == u64::MAX {
5651 "unlimited".to_string()
5652 } else {
5653 format!("{}MiB", total >> 20)
5654 },
5655 fmt_cap(split.dict_cache),
5656 fmt_cap(split.tile_cache),
5657 fmt_cap(split.block_cache),
5658 fixed as f64 / (1u64 << 20) as f64,
5659 );
5660 }
5661 split
5662 }
5663
5664 pub fn spill_threshold(&self) -> u64 {
5671 self.spill_threshold
5672 .load(std::sync::atomic::Ordering::Relaxed)
5673 }
5674
5675 pub fn set_spill_threshold(&self, bytes: Option<u64>) {
5679 self.spill_threshold.store(
5680 bytes.unwrap_or(u64::MAX),
5681 std::sync::atomic::Ordering::Relaxed,
5682 );
5683 }
5684
5685 pub fn dict_cache_stats(&self) -> crate::chunk_cache::CacheStats {
5689 self.dict.cache_stats()
5690 }
5691
5692 pub fn fixed_overhead(&self) -> FixedOverhead {
5696 FixedOverhead {
5697 dict_directory: self.dict.directory_bytes(),
5698 index_directory: self.index.directory_bytes(),
5699 named_graphs: self.named_graphs.directory_bytes(),
5700 }
5701 }
5702
5703 pub fn index_cache_stats(&self) -> crate::chunk_cache::CacheStats {
5705 self.index.cache_stats()
5706 }
5707
5708 pub fn dict_cache_cap(&self) -> u64 {
5710 self.dict.cache_cap()
5711 }
5712
5713 pub fn release_named_graph(&mut self, iri: &str) {
5726 match &mut self.named_graphs {
5727 NamedGraphsSlot::Lazy(lazy) => lazy.release(iri),
5728 NamedGraphsSlot::Quad(q) => q.release(&self.dict, iri),
5729 NamedGraphsSlot::Resident(_) => {}
5730 }
5731 }
5732
5733 fn resolve_query_pattern(
5734 &self,
5735 s: Option<&str>,
5736 p: Option<&str>,
5737 o: Option<&str>,
5738 ) -> Option<Pattern> {
5739 let sid = match s {
5740 Some(t) => match self.dict.subject_id(t) {
5741 Some(id) => Some(id),
5742 None => return None,
5743 },
5744 None => None,
5745 };
5746 let pid = match p {
5747 Some(t) => match self.dict.predicate_id(t) {
5748 Some(id) => Some(id),
5749 None => return None,
5750 },
5751 None => None,
5752 };
5753 let oid = match o {
5754 Some(t) => match self.dict.object_id(t) {
5755 Some(id) => Some(id),
5756 None => return None,
5757 },
5758 None => None,
5759 };
5760 Some((sid, pid, oid))
5761 }
5762
5763 pub fn query_with_provenance(
5767 &self,
5768 s: Option<&str>,
5769 p: Option<&str>,
5770 o: Option<&str>,
5771 ) -> Vec<TripleProvenance> {
5772 let pattern = match self.resolve_query_pattern(s, p, o) {
5773 Some(pattern) => pattern,
5774 None => return Vec::new(),
5775 };
5776
5777 let index_permutation = GraphIndex::best_permutation_in(self.header.perms, pattern);
5778 let si = index_permutation.section_index();
5779 let (dictionary_range, index_range, index_section_range, pyramid_range) = match &self.v06 {
5780 Some(f) => {
5781 use crate::v06::kind;
5782 let sec = |k| f.section(k).unwrap_or_default();
5783 let perm = f
5784 .root
5785 .unit(crate::v06::tag::INDEX)
5786 .and_then(|u| crate::v06::decode_index_root(u).ok())
5787 .and_then(|v| v.into_iter().find(|p| p.si == si))
5788 .map(|p| ByteRange {
5789 offset: sec(kind::INDEX_DATA).offset + p.data_off,
5790 len: p.data_len,
5791 })
5792 .unwrap_or_default();
5793 (
5794 sec(kind::DICT_DATA),
5795 sec(kind::INDEX_DATA),
5796 perm,
5797 f.section(kind::PYRAMID),
5798 )
5799 }
5800 None => (
5801 ByteRange {
5802 offset: self.header.dictionary_offset,
5803 len: self.header.dictionary_len,
5804 },
5805 ByteRange {
5806 offset: self.header.root_dir_offset,
5807 len: self.header.root_dir_len,
5808 },
5809 self.index_section_ranges[si],
5810 (self.header.pyramid_meta_len > 0).then_some(ByteRange {
5811 offset: self.header.pyramid_meta_offset,
5812 len: self.header.pyramid_meta_len,
5813 }),
5814 ),
5815 };
5816
5817 let tiles = &self.tile_ranges[si];
5818 let v06_tiles = self.v06.is_some().then(|| self.index.tile_sections()[si]);
5819 self.index
5820 .match_pattern(pattern)
5821 .into_iter()
5822 .filter_map(|(s, p, o)| {
5823 let terms = (
5824 self.dict.subject_term(s)?,
5825 self.dict.predicate_term(p)?,
5826 self.dict.object_term(o)?,
5827 );
5828 let a = index_permutation.forward((s, p, o)).0;
5831 let (tile, tile_range) = if let Some(tl) = v06_tiles {
5832 let ti = tl.first_max_ge(a);
5833 match (ti < tl.len() && tl[ti].leading_range().0 <= a)
5834 .then(|| tl.range(ti))
5835 .flatten()
5836 {
5837 Some((offset, len)) => (
5838 Some(format!("{}/{ti}", index_permutation.name())),
5839 Some(ByteRange { offset, len }),
5840 ),
5841 None => (None, None),
5842 }
5843 } else {
5844 let ti = tiles.partition_point(|&(_, max_a, _)| max_a < a);
5845 match tiles.get(ti) {
5846 Some(&(min_a, _, range)) if min_a <= a => (
5847 Some(format!("{}/{ti}", index_permutation.name())),
5848 Some(range),
5849 ),
5850 _ => (None, None),
5851 }
5852 };
5853 Some(TripleProvenance {
5854 terms,
5855 ids: (s, p, o),
5856 graph: None,
5857 matched_pattern: pattern,
5858 index_permutation,
5859 dictionary_range,
5860 index_range,
5861 index_section_range,
5862 pyramid_range,
5863 tile,
5864 tile_range,
5865 })
5866 })
5867 .collect()
5868 }
5869
5870 pub fn query(&self, s: Option<&str>, p: Option<&str>, o: Option<&str>) -> Vec<TermTriple> {
5874 self.query_with_provenance(s, p, o)
5875 .into_iter()
5876 .map(|m| m.terms)
5877 .collect()
5878 }
5879
5880 pub fn query_in_graph(
5888 &self,
5889 graph: Option<&str>,
5890 s: Option<&str>,
5891 p: Option<&str>,
5892 o: Option<&str>,
5893 ) -> Vec<TermTriple> {
5894 let pattern = match self.resolve_query_pattern(s, p, o) {
5895 Some(pattern) => pattern,
5896 None => return Vec::new(),
5897 };
5898 let index = match graph {
5899 None => &self.index,
5900 Some(g) => match self.graph_index(g) {
5901 Some(i) => i,
5902 None => return Vec::new(),
5903 },
5904 };
5905 self.resolve_matches(index.match_pattern(pattern))
5906 }
5907
5908 fn resolve_matches(&self, ids: Vec<Triple>) -> Vec<TermTriple> {
5911 if !ids.is_empty() {
5919 let mut nodes = Vec::with_capacity(ids.len() * 2);
5920 let mut preds = Vec::with_capacity(ids.len());
5921 for &(s, p, o) in &ids {
5922 nodes.push(self.dict.subject_node(s));
5923 nodes.push(self.dict.object_node(o));
5924 preds.push(p);
5925 }
5926 self.dict.prefetch_terms(&nodes, &preds);
5927 }
5928 ids.into_iter()
5929 .filter_map(|(s, p, o)| {
5930 Some((
5931 self.dict.subject_term(s)?,
5932 self.dict.predicate_term(p)?,
5933 self.dict.object_term(o)?,
5934 ))
5935 })
5936 .collect()
5937 }
5938
5939 pub fn query_quads(
5944 &self,
5945 s: Option<&str>,
5946 p: Option<&str>,
5947 o: Option<&str>,
5948 ) -> Vec<(TermTriple, Option<String>)> {
5949 let mut out: Vec<(TermTriple, Option<String>)> = self
5950 .query_in_graph(None, s, p, o)
5951 .into_iter()
5952 .map(|t| (t, None))
5953 .collect();
5954 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
5955 return out;
5956 };
5957 for i in 0..self.named_graphs.count() {
5958 let Some(iri) = self.named_graphs.name_at(&self.dict, i, true) else {
5961 continue;
5962 };
5963 let owned = self.named_graph_transient(i);
5966 let index = match &owned {
5967 Some(view) => view,
5968 None => match self.named_graphs.graph_at(&self.dict, i, true) {
5969 Some((_, g)) => g,
5970 None => continue,
5971 },
5972 };
5973 for triple in self.resolve_matches(index.match_pattern(pattern)) {
5974 out.push((triple, Some(iri.to_string())));
5975 }
5976 }
5977 out
5978 }
5979
5980 pub fn query_ranged<R: RangeReader>(
5987 reader: &R,
5988 s: Option<&str>,
5989 p: Option<&str>,
5990 o: Option<&str>,
5991 ) -> Result<Vec<TermTriple>, FileError> {
5992 let head = reader.read_at(0, (HEADER_LEN as u64).min(reader.len()))?;
5993 if crate::v06::open::is_v06(&head) {
5994 return Ok(v06_query_ranged(reader, head, s, p, o, true)?.unwrap_or_default());
5995 }
5996 let routed = match route_pattern(reader, head, s, p, o)? {
5997 Some(routed) => routed,
5998 None => return Ok(Vec::new()),
5999 };
6000 let matches = fetch_routed_matches(reader, &routed)?;
6001 Ok(matches
6002 .into_iter()
6003 .filter_map(|(s, p, o)| {
6004 Some((
6005 routed.dict.subject_term(s)?,
6006 routed.dict.predicate_term(p)?,
6007 routed.dict.object_term(o)?,
6008 ))
6009 })
6010 .collect())
6011 }
6012
6013 pub fn route_pattern_ranged<R: RangeReader>(
6017 reader: &R,
6018 s: Option<&str>,
6019 p: Option<&str>,
6020 o: Option<&str>,
6021 ) -> Result<bool, FileError> {
6022 let head = reader.read_at(0, (HEADER_LEN as u64).min(reader.len()))?;
6023 if crate::v06::open::is_v06(&head) {
6024 return Ok(v06_query_ranged(reader, head, s, p, o, false)?.is_some());
6025 }
6026 Ok(route_pattern(reader, head, s, p, o)?.is_some())
6027 }
6028}
6029
6030fn v06_query_ranged<R: RangeReader>(
6036 reader: &R,
6037 head: Vec<u8>,
6038 s: Option<&str>,
6039 p: Option<&str>,
6040 o: Option<&str>,
6041 fetch: bool,
6042) -> Result<Option<Vec<TermTriple>>, FileError> {
6043 use crate::v06::{kind, open::SparseReader, Header06, Root06, HEADER_LEN};
6044 let len = reader.len();
6045 let h = Header06::from_bytes(&head)?;
6046 let root_end = (HEADER_LEN as u64 + h.root_len as u64).min(len);
6047 let root_bytes = if root_end <= head.len() as u64 {
6048 head[HEADER_LEN..root_end as usize].to_vec()
6049 } else {
6050 reader.read_at(HEADER_LEN as u64, root_end - HEADER_LEN as u64)?
6051 };
6052 let root = Root06::decode(&root_bytes, h.internal_codec)?;
6053 let dict = root.section(kind::DICT_DATA).and_then(|d| d.abs(&h).ok());
6054 let end = dict
6057 .map_or(h.data_offset, |d| d.offset + d.len)
6058 .max(crate::header::HEADER_LEN as u64)
6059 .min(len);
6060 let mut held = head;
6061 if end > held.len() as u64 {
6062 let at = held.len() as u64;
6063 held.extend_from_slice(&reader.read_at(at, end - at)?);
6064 }
6065 let sparse = SparseReader::fetch(reader, held, &[])?;
6066 let rete = Rete::open_ranged_lazy_with(sparse, 0)?;
6067 let Some(pattern) = resolve_query_pattern(rete.dictionary(), s, p, o) else {
6068 return Ok(None);
6069 };
6070 if !fetch {
6071 return Ok(Some(Vec::new()));
6072 }
6073 let perm = GraphIndex::best_permutation_in(h.perms(), pattern);
6074 let si = perm.section_index();
6075 let index = rete.default_index();
6076 let list = index.tile_sections()[si];
6077 let n = list.len();
6078 let [pa, _, _] = perm.order_pattern(pattern);
6079 let (start, stop) = match pa {
6080 Some(a) => {
6081 let start = list.first_max_ge(a);
6082 let mut stop = start;
6083 while stop < n {
6084 list.ensure_span(stop, stop + 1);
6085 if list[stop].leading_range().0 > a {
6086 break;
6087 }
6088 stop += 1;
6089 }
6090 (start, stop)
6091 }
6092 None => {
6093 list.ensure_span(0, n);
6094 (0, n)
6095 }
6096 };
6097 let ranges: Vec<(u64, u64)> = (start..stop)
6098 .filter_map(|i| index.tile_range(si, i))
6099 .collect();
6100 let mut out = Vec::new();
6101 if let (Some(&(lo, _)), Some(&(last, last_len))) = (ranges.first(), ranges.last()) {
6102 let body = reader.read_at(lo, last + last_len - lo)?;
6104 for &(off, l) in &ranges {
6105 let at = (off - lo) as usize;
6106 let tile = decompress(h.block_codec, &body[at..at + l as usize])?;
6107 out.extend(GraphIndex::match_serialized_block(&tile, perm, pattern));
6108 }
6109 }
6110 out.sort_unstable();
6111 let d = rete.dictionary();
6112 Ok(Some(
6113 out.into_iter()
6114 .filter_map(|(s, p, o)| {
6115 Some((d.subject_term(s)?, d.predicate_term(p)?, d.object_term(o)?))
6116 })
6117 .collect(),
6118 ))
6119}
6120
6121struct RoutedPattern {
6124 dict: Dictionary,
6125 pattern: Pattern,
6126 permutation: IndexPermutation,
6127 header: Header,
6128 section: ByteRange,
6130}
6131
6132fn route_pattern<R: RangeReader>(
6135 reader: &R,
6136 head: Vec<u8>,
6137 s: Option<&str>,
6138 p: Option<&str>,
6139 o: Option<&str>,
6140) -> Result<Option<RoutedPattern>, FileError> {
6141 let header = Header::from_bytes(&head)?;
6142
6143 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
6144 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
6145
6146 let Some(pattern) = resolve_query_pattern(&dict, s, p, o) else {
6147 return Ok(None);
6148 };
6149 let permutation = GraphIndex::best_permutation_in(header.perms, pattern);
6150 let section = locate_container_section_ranged(
6151 reader,
6152 header.root_dir_offset,
6153 header.root_dir_len,
6154 header
6155 .perms
6156 .position(permutation)
6157 .ok_or(FileError::Container("routed to an absent permutation"))?,
6158 header.index_container_sections() as u64,
6159 )?;
6160 Ok(Some(RoutedPattern {
6161 dict,
6162 pattern,
6163 permutation,
6164 header,
6165 section,
6166 }))
6167}
6168
6169fn fetch_routed_matches<R: RangeReader>(
6174 reader: &R,
6175 routed: &RoutedPattern,
6176) -> Result<Vec<Triple>, FileError> {
6177 let dir = read_tile_directory_ranged(reader, routed.section)?;
6178 let [pa, _, _] = routed.permutation.order_pattern(routed.pattern);
6179 let codec = routed.header.block_codec;
6180 let mut out = Vec::new();
6181 match pa {
6182 Some(a) => {
6185 for e in dir.iter().filter(|e| e.min_a <= a && a <= e.max_a) {
6186 let len = e.end - e.start;
6187 let bytes = reader.read_at(checked_end(routed.section.offset, e.start)?, len)?;
6188 if bytes.len() as u64 != len {
6189 return Err(FileError::Container("short tile read"));
6190 }
6191 let tile = decompress(codec, &bytes)?;
6192 out.extend(GraphIndex::match_serialized_block(
6193 &tile,
6194 routed.permutation,
6195 routed.pattern,
6196 ));
6197 }
6198 }
6199 None => {
6202 if let (Some(first), Some(last)) = (dir.first(), dir.last()) {
6203 let base = first.start;
6204 let body =
6205 reader.read_at(checked_end(routed.section.offset, base)?, last.end - base)?;
6206 for e in &dir {
6207 let stored = checked_slice(&body, e.start - base, e.end - e.start)
6209 .ok_or(FileError::Container("short tile read"))?;
6210 let tile = decompress(codec, stored)?;
6211 out.extend(GraphIndex::match_serialized_block(
6212 &tile,
6213 routed.permutation,
6214 routed.pattern,
6215 ));
6216 }
6217 }
6218 }
6219 }
6220 out.sort_unstable();
6221 Ok(out)
6222}
6223
6224fn resolve_query_pattern(
6225 dict: &Dictionary,
6226 s: Option<&str>,
6227 p: Option<&str>,
6228 o: Option<&str>,
6229) -> Option<Pattern> {
6230 let sid = match s {
6231 Some(t) => Some(dict.subject_id(t)?),
6232 None => None,
6233 };
6234 let pid = match p {
6235 Some(t) => Some(dict.predicate_id(t)?),
6236 None => None,
6237 };
6238 let oid = match o {
6239 Some(t) => Some(dict.object_id(t)?),
6240 None => None,
6241 };
6242 Some((sid, pid, oid))
6243}
6244
6245fn ranged_chunked_dictionary<R: RangeReader + Send + Sync + 'static>(
6260 reader: &std::sync::Arc<R>,
6261 header: &Header,
6262 want: [bool; 4],
6263) -> Result<Dictionary, FileError> {
6264 let cache = crate::chunk_cache::ChunkCache::unlimited_arc();
6267 let mut dict_sections: Vec<crate::dict::ChunkedSection> = Vec::with_capacity(4);
6268 let mut section_ranges: [std::sync::Arc<Vec<ByteRange>>; 4] = Default::default();
6269 for si in 0..4 {
6270 if !want[si] {
6271 dict_sections.push(crate::dict::ChunkedSection::from_parts(
6272 crate::dict::SectionMeta {
6273 term_count: 0,
6274 restart_interval: 1,
6275 restart_offsets: Vec::new(),
6276 },
6277 Vec::new(),
6278 None,
6279 cache.clone(),
6280 si as u8,
6281 ));
6282 continue;
6283 }
6284 let section = locate_container_section_ranged(
6285 reader.as_ref(),
6286 header.dictionary_offset,
6287 header.dictionary_len,
6288 si,
6289 4,
6290 )?;
6291 let (meta, entries) = read_dict_dir_ranged(reader.as_ref(), section)?;
6292 let ranges: std::sync::Arc<Vec<ByteRange>> = std::sync::Arc::new(
6295 entries
6296 .iter()
6297 .map(|e| ByteRange {
6298 offset: section.offset + e.start,
6299 len: (e.end - e.start),
6300 })
6301 .collect(),
6302 );
6303 section_ranges[si] = ranges.clone();
6304 let chunks: Vec<crate::dict::SectionChunk> = entries
6305 .into_iter()
6306 .map(|e| crate::dict::SectionChunk::new(e.first_run, e.key, e.body_start))
6307 .collect();
6308 let chunk_reader = reader.clone();
6309 let codec = header.dict_codec;
6310 let loader_ranges = ranges.clone();
6311 let loader: crate::dict::ChunkLoader = Box::new(move |ci| {
6312 let range = loader_ranges.get(ci)?;
6313 let bytes = chunk_reader.read_at(range.offset, range.len).ok()?;
6314 decompress(codec, &bytes).ok()
6315 });
6316 let bulk_reader = reader.clone();
6319 let bulk: crate::dict::ChunkBulkLoader = Box::new(move |cis| {
6320 let want: Option<Vec<ByteRange>> =
6321 cis.iter().map(|&ci| ranges.get(ci).copied()).collect();
6322 let blobs = read_coalesced(bulk_reader.as_ref(), &want?, DICT_COALESCE_GAP)?;
6323 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
6324 });
6325 dict_sections.push(
6326 crate::dict::ChunkedSection::from_parts(
6327 meta,
6328 chunks,
6329 Some(loader),
6330 cache.clone(),
6331 si as u8,
6332 )
6333 .with_bulk_loader(bulk),
6334 );
6335 }
6336 let dict_arr: [crate::dict::ChunkedSection; 4] = dict_sections
6337 .try_into()
6338 .map_err(|_| FileError::Container("expected 4 dictionary sections"))?;
6339 let multi_reader = reader.clone();
6343 let codec = header.dict_codec;
6344 let multi: crate::dict::MultiChunkBulkLoader = Box::new(move |pairs| {
6345 let want: Option<Vec<ByteRange>> = pairs
6346 .iter()
6347 .map(|&(si, ci)| section_ranges.get(si as usize)?.get(ci).copied())
6348 .collect();
6349 let blobs = read_coalesced(multi_reader.as_ref(), &want?, DICT_COALESCE_GAP)?;
6350 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
6351 });
6352 Ok(Dictionary::from_chunked_sections(dict_arr).with_multi_bulk_loader(multi))
6353}
6354
6355#[cfg(not(target_arch = "wasm32"))]
6360fn open_side_by_side<A, B: Send>(
6361 concurrency: usize,
6362 a: impl FnOnce() -> A,
6363 b: impl FnOnce() -> B + Send,
6364) -> (A, B) {
6365 if concurrency <= 1 {
6366 return (a(), b());
6367 }
6368 std::thread::scope(|scope| {
6369 let hb = scope.spawn(b);
6370 let ra = a();
6371 let rb = hb
6372 .join()
6373 .unwrap_or_else(|panic| std::panic::resume_unwind(panic));
6374 (ra, rb)
6375 })
6376}
6377
6378#[cfg(target_arch = "wasm32")]
6380fn open_side_by_side<A, B>(
6381 _concurrency: usize,
6382 a: impl FnOnce() -> A,
6383 b: impl FnOnce() -> B,
6384) -> (A, B) {
6385 (a(), b())
6386}
6387
6388#[cfg(feature = "v06-parity")]
6391fn parity_image(bytes: &[u8]) -> Option<Vec<u8>> {
6392 if crate::v06::open::is_v06(bytes) || bytes.len() < HEADER_LEN {
6393 return None;
6394 }
6395 crate::v06::write::convert_image(bytes, crate::v06::write::WriteOpts::default()).ok()
6396}
6397
6398fn v06_layout(f: &crate::v06::open::V06File) -> Vec<LayoutSegment> {
6401 let h = &f.h;
6402 let seg = |kind: &'static str, label: String, offset: u64, len: u64| LayoutSegment {
6403 kind,
6404 label,
6405 offset,
6406 len,
6407 };
6408 let mut out = vec![
6409 seg(
6410 "header",
6411 format!("header (fixed {} bytes)", crate::v06::HEADER_LEN),
6412 0,
6413 crate::v06::HEADER_LEN as u64,
6414 ),
6415 seg(
6416 "directory",
6417 "root block (section table, directory roots)".into(),
6418 h.root_offset(),
6419 h.root_len as u64,
6420 ),
6421 ];
6422 let c = h.card_range();
6423 if c.len > 0 {
6424 out.push(seg(
6425 "metadata",
6426 "metadata (dataset card, zstd)".into(),
6427 c.offset,
6428 c.len,
6429 ));
6430 }
6431 let b = h.build_range();
6432 if b.len > 0 {
6433 out.push(seg("build-info", "build record".into(), b.offset, b.len));
6434 }
6435 for s in &f.root.sections {
6436 let Ok(a) = s.abs(h) else { continue };
6437 let kind: &'static str = match s.kind {
6438 crate::v06::kind::SCHEMA => "schema",
6439 crate::v06::kind::DICT_DATA => "dictionary",
6440 crate::v06::kind::INDEX_DATA => "tile",
6441 crate::v06::kind::QUAD_DATA => "named-graphs",
6442 crate::v06::kind::LABEL_DATA => "label-index",
6443 crate::v06::kind::TEXT_DATA => "text-index",
6444 crate::v06::kind::PYRAMID => "pyramid",
6445 _ => "directory",
6446 };
6447 out.push(seg(
6448 kind,
6449 crate::v06::kind::name(s.kind).to_string(),
6450 a.offset,
6451 a.len,
6452 ));
6453 }
6454 out.sort_by_key(|s| s.offset);
6455 out
6456}
6457
6458#[allow(clippy::type_complexity)]
6461fn v06_slots(
6462 f: &std::sync::Arc<crate::v06::open::V06File>,
6463) -> (
6464 PyramidSlot,
6465 TextIndexSlot,
6466 SectionSlot<crate::label_index::LabelIndex>,
6467 SectionSlot<crate::schema_section::SchemaMeta>,
6468) {
6469 use crate::v06::{P_LABELS, P_PYRAMID, P_SCHEMA, P_TEXT};
6470 let pyramid = if f.h.has(P_PYRAMID) {
6471 let a = f.clone();
6472 PyramidSlot::Lazy {
6473 loader: Box::new(move || PyramidMeta::decode(&a.pyramid_bytes()?).ok()),
6474 cell: std::sync::OnceLock::new(),
6475 }
6476 } else {
6477 PyramidSlot::Resident(None)
6478 };
6479 let text_index = if f.h.has(P_TEXT) {
6480 let (a, b) = (f.clone(), f.clone());
6481 TextIndexSlot::Lazy {
6482 loader: Box::new(move || a.text_index()?.ok()),
6483 cell: std::sync::OnceLock::new(),
6484 token_table: Box::new(move || b.token_table_bytes()),
6485 token_table_cell: std::sync::OnceLock::new(),
6486 }
6487 } else {
6488 TextIndexSlot::Resident {
6489 index: None,
6490 token_table_len: None,
6491 }
6492 };
6493 let labels = if f.h.has(P_LABELS) {
6494 let a = f.clone();
6495 SectionSlot::Lazy {
6496 loader: Box::new(move || a.label_index()?.ok()),
6497 cell: std::sync::OnceLock::new(),
6498 }
6499 } else {
6500 SectionSlot::Resident(None)
6501 };
6502 let schema = if f.h.has(P_SCHEMA) {
6503 let a = f.clone();
6504 SectionSlot::Lazy {
6505 loader: Box::new(move || a.schema()),
6506 cell: std::sync::OnceLock::new(),
6507 }
6508 } else {
6509 SectionSlot::Resident(None)
6510 };
6511 (pyramid, text_index, labels, schema)
6512}
6513
6514fn ranged_pyramid_slot<R: RangeReader + Send + Sync + 'static>(
6518 reader: &std::sync::Arc<R>,
6519 header: &Header,
6520) -> PyramidSlot {
6521 if header.pyramid_meta_len == 0 {
6522 return PyramidSlot::Resident(None);
6523 }
6524 let pyr_reader = reader.clone();
6525 let pyr_off = header.pyramid_meta_offset;
6526 let pyr_len = header.pyramid_meta_len;
6527 PyramidSlot::Lazy {
6528 loader: Box::new(move || {
6529 let mb = pyr_reader.read_at(pyr_off, pyr_len).ok()?;
6530 PyramidMeta::decode(&mb).ok()
6531 }),
6532 cell: std::sync::OnceLock::new(),
6533 }
6534}
6535
6536fn ranged_text_index_slot<R: RangeReader + Send + Sync + 'static>(
6541 reader: &std::sync::Arc<R>,
6542 header: &Header,
6543) -> TextIndexSlot {
6544 let (ti_off, ti_len) = header.text_index_payload();
6545 if ti_len == 0 {
6546 return TextIndexSlot::Resident {
6547 index: None,
6548 token_table_len: None,
6549 };
6550 }
6551 let ti_reader = reader.clone();
6552 let probe_reader = reader.clone();
6553 let codec = header.block_codec;
6554 TextIndexSlot::Lazy {
6555 loader: Box::new(move || {
6556 let prefix_len = read_token_table_len(&*ti_reader, ti_off, ti_len)?;
6560 let prefix = ti_reader.read_at(ti_off, prefix_len).ok()?;
6561 let postings_base = crate::text_index::TextIndex::postings_base(&prefix)? as u64;
6562 let postings_abs = ti_off + postings_base;
6563 let pr = ti_reader.clone();
6564 let posting_loader =
6565 Box::new(move |off: u64, len: u64| pr.read_at(postings_abs + off, len).ok());
6566 crate::text_index::TextIndex::from_token_table(&prefix, codec, posting_loader).ok()
6567 }),
6568 cell: std::sync::OnceLock::new(),
6569 token_table: Box::new(move || read_token_table_len(&*probe_reader, ti_off, ti_len)),
6572 token_table_cell: std::sync::OnceLock::new(),
6573 }
6574}
6575
6576fn resolve_label_hits(
6583 dict: &Dictionary,
6584 hits: Vec<&crate::meta::LabelEntry>,
6585) -> Vec<(String, String)> {
6586 let ids: Vec<u32> = hits.iter().map(|e| e.subject).collect();
6587 dict.prefetch_subject_terms(&ids);
6588 hits.iter()
6589 .zip(dict.subject_terms(&ids))
6590 .filter_map(|(e, iri)| Some((e.label.clone(), iri?)))
6591 .collect()
6592}
6593
6594fn text_search_in(
6597 ti: Option<&crate::text_index::TextIndex>,
6598 dict: &Dictionary,
6599 words: &[&str],
6600 prefix: Option<&str>,
6601 limit: usize,
6602) -> Vec<String> {
6603 let Some(ti) = ti else {
6604 return Vec::new();
6605 };
6606 let mut acc: Option<Vec<u32>> = None;
6610 if let Some(p) = prefix {
6611 acc = Some(ti.prefix(&p.to_lowercase()));
6612 }
6613 for w in words {
6614 for tok in crate::text_index::tokenize(w) {
6615 let posting = ti.lookup(&tok);
6616 acc = Some(match acc {
6617 Some(a) => intersect_sorted(&a, &posting),
6618 None => posting,
6619 });
6620 if acc.as_ref().is_some_and(|a| a.is_empty()) {
6621 return Vec::new();
6622 }
6623 }
6624 }
6625 let ids = acc.unwrap_or_default();
6626 let mut out = Vec::with_capacity(if limit > 0 {
6627 limit.min(ids.len())
6628 } else {
6629 ids.len()
6630 });
6631 let page = if limit > 0 {
6635 limit.min(ids.len())
6636 } else {
6637 ids.len()
6638 };
6639 dict.prefetch_subject_terms(&ids[..page]);
6640 for id in ids {
6641 if let Some(iri) = dict.subject_term(id) {
6642 out.push(iri);
6643 if limit > 0 && out.len() >= limit {
6644 break;
6645 }
6646 }
6647 }
6648 out
6649}
6650
6651pub struct SearchView {
6674 header: Header,
6675 dict: SectionSlot<Dictionary>,
6676 pyramid: PyramidSlot,
6677 text_index: TextIndexSlot,
6678 labels: SectionSlot<crate::label_index::LabelIndex>,
6679}
6680
6681impl SearchView {
6682 pub fn open_ranged<R: RangeReader + Send + Sync + 'static>(
6687 reader: R,
6688 ) -> Result<Self, FileError> {
6689 Self::open_ranged_with(reader, crate::v06::FIRST_READ)
6690 }
6691
6692 pub fn open_ranged_with<R: RangeReader + Send + Sync + 'static>(
6695 reader: R,
6696 first_read: u64,
6697 ) -> Result<Self, FileError> {
6698 let first_len = first_read.max(HEADER_LEN as u64).min(reader.len());
6699 let first: std::sync::Arc<[u8]> = std::sync::Arc::from(reader.read_at(0, first_len)?);
6700 let reader = crate::reader::PrefixReader::new(reader, first.clone());
6701 if crate::v06::open::is_v06(&first) {
6702 let r: std::sync::Arc<dyn RangeReader + Send + Sync> = std::sync::Arc::new(reader);
6703 let f = std::sync::Arc::new(crate::v06::open::V06File::open(r, &first)?);
6704 let header = crate::v06::logical_header(&f.h);
6705 let (pyramid, text_index, labels, _) = v06_slots(&f);
6706 let want = [true, true, false, false];
6707 let dict = if f.h.has(crate::v06::P_LABELS) {
6708 let a = f.clone();
6709 SectionSlot::Lazy {
6710 loader: Box::new(move || a.dictionary(want).ok()),
6711 cell: std::sync::OnceLock::new(),
6712 }
6713 } else {
6714 SectionSlot::Resident(Some(f.dictionary(want)?))
6715 };
6716 return Ok(Self {
6717 header,
6718 dict,
6719 pyramid,
6720 text_index,
6721 labels,
6722 });
6723 }
6724 let head = reader.read_at(0, HEADER_LEN as u64)?;
6725 let header = Header::from_bytes(&head)?;
6726 let reader = std::sync::Arc::new(reader);
6727 let dict = if header.label_index_section().is_some() {
6731 let (r, h) = (reader.clone(), header.clone());
6732 SectionSlot::Lazy {
6733 loader: Box::new(move || {
6734 ranged_chunked_dictionary(&r, &h, [true, true, false, false]).ok()
6735 }),
6736 cell: std::sync::OnceLock::new(),
6737 }
6738 } else {
6739 SectionSlot::Resident(Some(ranged_chunked_dictionary(
6740 &reader,
6741 &header,
6742 [true, true, false, false],
6743 )?))
6744 };
6745 let pyramid = ranged_pyramid_slot(&reader, &header);
6746 let text_index = ranged_text_index_slot(&reader, &header);
6747 let labels = ranged_label_slot(&reader, &header);
6748 Ok(Self {
6749 header,
6750 dict,
6751 pyramid,
6752 text_index,
6753 labels,
6754 })
6755 }
6756
6757 pub fn has_labels(&self) -> bool {
6761 self.header.label_index_section().is_some() || self.header.pyramid_meta_len > 0
6762 }
6763
6764 pub fn has_label_index(&self) -> bool {
6766 self.header.label_index_section().is_some()
6767 }
6768
6769 pub fn header(&self) -> &Header {
6771 &self.header
6772 }
6773
6774 pub fn has_text_index(&self) -> bool {
6776 self.header.text_index_len > 0
6777 }
6778
6779 pub fn has_pyramid(&self) -> bool {
6782 self.header.pyramid_meta_len > 0
6783 }
6784
6785 pub fn text_search(&self, words: &[&str], prefix: Option<&str>, limit: usize) -> Vec<String> {
6788 let Some(dict) = self.dict.get() else {
6789 return Vec::new();
6790 };
6791 text_search_in(self.text_index.index(), dict, words, prefix, limit)
6792 }
6793
6794 pub fn text_index_token_table_len(&self) -> Option<u64> {
6798 self.text_index.token_table_len()
6799 }
6800
6801 pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
6805 if let Some(li) = self.labels.get() {
6806 return li.prefix_search(prefix, limit);
6807 }
6808 let pyr = match &self.pyramid {
6809 PyramidSlot::Resident(p) => p.as_ref(),
6810 PyramidSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
6811 };
6812 let (Some(pyr), Some(dict)) = (pyr, self.dict.get()) else {
6813 return Vec::new();
6814 };
6815 resolve_label_hits(dict, pyr.prefix_search(prefix, limit))
6816 }
6817}
6818
6819#[must_use]
6824pub struct SummaryView {
6825 pub round: u32,
6826 pub summary: Vec<SuperEdge>,
6827 pub class_hierarchy: Vec<ClassNode>,
6829 pub level_rollups: Vec<LevelRollup>,
6831 pub level_links: Vec<LevelLinks>,
6833 pub descriptors: Vec<CommunityDescriptor>,
6835 pub subclass_cycles: Vec<Vec<String>>,
6837 pub disjoint_pairs: Vec<(String, String)>,
6839 pub equivalent_pairs: Vec<(String, String)>,
6841 dict: Dictionary,
6842}
6843
6844impl SummaryView {
6845 pub fn open_ranged<R: RangeReader>(reader: &R) -> Result<Option<Self>, FileError> {
6847 let head = reader.read_at(0, (HEADER_LEN as u64).min(reader.len()))?;
6848 let header = Header::from_bytes(&head)?;
6849 if header.pyramid_meta_len == 0 {
6850 return Ok(None);
6851 }
6852 if header.version == crate::v06::VERSION {
6853 return Self::open_ranged_v06(reader, &header, head).map(Some);
6854 }
6855
6856 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
6857 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
6858
6859 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
6860 let mut meta =
6861 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
6862 if let Some(s) = read_schema_with_header(reader, &header) {
6865 meta.class_hierarchy = s.class_hierarchy;
6866 meta.level_rollups = s.level_rollups;
6867 meta.level_links = s.level_links;
6868 meta.subclass_cycles = s.subclass_cycles;
6869 meta.disjoint_pairs = s.disjoint_pairs;
6870 meta.equivalent_pairs = s.equivalent_pairs;
6871 }
6872
6873 Ok(Some(SummaryView {
6874 round: meta.round,
6875 summary: meta.summary,
6876 class_hierarchy: meta.class_hierarchy,
6877 level_rollups: meta.level_rollups,
6878 level_links: meta.level_links,
6879 descriptors: meta.descriptors,
6880 subclass_cycles: meta.subclass_cycles,
6881 disjoint_pairs: meta.disjoint_pairs,
6882 equivalent_pairs: meta.equivalent_pairs,
6883 dict,
6884 }))
6885 }
6886
6887 fn open_ranged_v06<R: RangeReader>(
6891 reader: &R,
6892 header: &Header,
6893 head: Vec<u8>,
6894 ) -> Result<Self, FileError> {
6895 use crate::v06::{kind, open};
6896 let mut first = head;
6900 let want_first = crate::v06::FIRST_READ.min(reader.len());
6901 if want_first > first.len() as u64 {
6902 let at = first.len() as u64;
6903 first.extend_from_slice(&reader.read_at(at, want_first - at)?);
6904 }
6905 let pre = open::Prefixed {
6906 first: &first,
6907 r: reader,
6908 };
6909 let (h, root) = open::read_front(&pre)?;
6910 let want: Vec<ByteRange> = [
6911 kind::PYRAMID,
6912 kind::SCHEMA,
6913 kind::DICT_LEAVES,
6914 kind::DICT_DATA,
6915 ]
6916 .iter()
6917 .filter_map(|&k| root.section(k).and_then(|s| s.abs(&h).ok()))
6918 .collect();
6919 let sparse = open::SparseReader::fetch(&pre, first.clone(), &want)?;
6920 let mb = open::read_section(&sparse, &h, &root, kind::PYRAMID)?
6921 .ok_or(FileError::Container("0x06 file has no pyramid"))?;
6922 let mut meta =
6923 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
6924 if let Some(s) = read_schema_with_header(&sparse, header) {
6925 meta.class_hierarchy = s.class_hierarchy;
6926 meta.level_rollups = s.level_rollups;
6927 meta.level_links = s.level_links;
6928 meta.subclass_cycles = s.subclass_cycles;
6929 meta.disjoint_pairs = s.disjoint_pairs;
6930 meta.equivalent_pairs = s.equivalent_pairs;
6931 }
6932 let f = open::V06File::open(std::sync::Arc::new(sparse), &first)?;
6933 let dict = f.dictionary([true; 4])?;
6934 Ok(SummaryView {
6935 round: meta.round,
6936 summary: meta.summary,
6937 class_hierarchy: meta.class_hierarchy,
6938 level_rollups: meta.level_rollups,
6939 level_links: meta.level_links,
6940 descriptors: meta.descriptors,
6941 subclass_cycles: meta.subclass_cycles,
6942 disjoint_pairs: meta.disjoint_pairs,
6943 equivalent_pairs: meta.equivalent_pairs,
6944 dict,
6945 })
6946 }
6947
6948 pub fn open_schema_ranged<R: RangeReader>(reader: &R) -> Result<Option<Self>, FileError> {
6956 let head = reader.read_at(0, HEADER_LEN as u64)?;
6957 let header = Header::from_bytes(&head)?;
6958 let Some(s) = read_schema_with_header(reader, &header) else {
6959 return Ok(None);
6960 };
6961 let dict = crate::DictionaryBuilder::new().build();
6962 Ok(Some(SummaryView {
6963 round: 0,
6964 summary: Vec::new(),
6965 class_hierarchy: s.class_hierarchy,
6966 level_rollups: s.level_rollups,
6967 level_links: s.level_links,
6968 descriptors: Vec::new(),
6969 subclass_cycles: s.subclass_cycles,
6970 disjoint_pairs: s.disjoint_pairs,
6971 equivalent_pairs: s.equivalent_pairs,
6972 dict,
6973 }))
6974 }
6975
6976 pub fn level_count(&self) -> usize {
6978 self.level_rollups.len()
6979 }
6980
6981 pub fn level_rollup(&self, k: usize) -> Option<&LevelRollup> {
6984 self.level_rollups.get(k)
6985 }
6986
6987 pub fn predicate_term(&self, id: u32) -> Option<String> {
6989 self.dict.predicate_term(id)
6990 }
6991
6992 pub fn predicate_total(&self, predicate: &str) -> u32 {
6995 match self.dict.predicate_id(predicate) {
6996 Some(pid) => self
6997 .summary
6998 .iter()
6999 .filter(|e| e.predicate == pid)
7000 .map(|e| e.count)
7001 .sum(),
7002 None => 0,
7003 }
7004 }
7005
7006 pub fn predicate_totals(&self) -> Vec<(String, u32)> {
7008 let mut by_pred: std::collections::BTreeMap<u32, u32> = std::collections::BTreeMap::new();
7009 for e in &self.summary {
7010 *by_pred.entry(e.predicate).or_default() += e.count;
7011 }
7012 let mut out: Vec<(String, u32)> = by_pred
7013 .into_iter()
7014 .filter_map(|(pid, c)| self.dict.predicate_term(pid).map(|t| (t, c)))
7015 .collect();
7016 out.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
7017 out
7018 }
7019
7020 pub fn community_count(&self) -> usize {
7022 let mut comms = std::collections::BTreeSet::new();
7023 for e in &self.summary {
7024 comms.insert(e.s_comm);
7025 comms.insert(e.o_comm);
7026 }
7027 comms.len()
7028 }
7029
7030 pub fn tbox_coherence(&self) -> Vec<crate::reason::Inconsistency> {
7045 schema_coherence(
7046 &self.class_hierarchy,
7047 &self.subclass_cycles,
7048 &self.disjoint_pairs,
7049 &self.equivalent_pairs,
7050 )
7051 }
7052
7053 pub fn tbox_is_coherent(&self) -> bool {
7056 self.tbox_coherence().is_empty()
7057 }
7058}
7059
7060pub fn schema_coherence(
7066 class_hierarchy: &[ClassNode],
7067 subclass_cycles: &[Vec<String>],
7068 disjoint_pairs: &[(String, String)],
7069 equivalent_pairs: &[(String, String)],
7070) -> Vec<crate::reason::Inconsistency> {
7071 use crate::reason::Inconsistency;
7072 use std::collections::{BTreeMap, BTreeSet, VecDeque};
7073 const MAX_REACH: usize = 100_000;
7074
7075 let mut out: Vec<Inconsistency> = Vec::new();
7076
7077 for cyc in subclass_cycles {
7078 let detail = if cyc.len() == 1 {
7079 format!("{} is rdfs:subClassOf itself (a cycle)", cyc[0])
7080 } else {
7081 format!(
7082 "classes {{{}}} are mutually rdfs:subClassOf (a cycle)",
7083 cyc.join(", ")
7084 )
7085 };
7086 out.push(Inconsistency {
7087 kind: "subclass-cycle",
7088 detail,
7089 });
7090 }
7091
7092 if !disjoint_pairs.is_empty() {
7093 let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
7095 for n in class_hierarchy {
7096 let e = adj.entry(n.class.as_str()).or_default();
7097 for p in &n.parents {
7098 e.push(p.as_str());
7099 }
7100 }
7101 for (a, b) in equivalent_pairs {
7102 adj.entry(a.as_str()).or_default().push(b.as_str());
7103 adj.entry(b.as_str()).or_default().push(a.as_str());
7104 }
7105
7106 let mut focuses: BTreeSet<&str> =
7108 class_hierarchy.iter().map(|n| n.class.as_str()).collect();
7109 for (a, b) in disjoint_pairs.iter().chain(equivalent_pairs) {
7110 focuses.insert(a.as_str());
7111 focuses.insert(b.as_str());
7112 }
7113
7114 let mut seen: BTreeSet<&str> = BTreeSet::new();
7115 for &c in &focuses {
7116 let mut reach: BTreeSet<&str> = BTreeSet::new();
7118 let mut q: VecDeque<&str> = VecDeque::new();
7119 reach.insert(c);
7120 q.push_back(c);
7121 while let Some(x) = q.pop_front() {
7122 if reach.len() > MAX_REACH {
7123 break;
7124 }
7125 if let Some(ns) = adj.get(x) {
7126 for &p in ns {
7127 if reach.insert(p) {
7128 q.push_back(p);
7129 }
7130 }
7131 }
7132 }
7133 for (x, y) in disjoint_pairs {
7134 if reach.contains(x.as_str()) && reach.contains(y.as_str()) && seen.insert(c) {
7135 out.push(Inconsistency {
7136 kind: "unsatisfiable-class",
7137 detail: format!(
7138 "{c} is a subclass of both {x} and {y}, which are \
7139 owl:disjointWith — no individual can be a {c}"
7140 ),
7141 });
7142 break;
7143 }
7144 }
7145 }
7146 }
7147
7148 out.sort_by(|a, b| (a.kind, &a.detail).cmp(&(b.kind, &b.detail)));
7149 out
7150}
7151
7152pub fn read_schema_coherence_ranged<R: RangeReader>(
7160 reader: &R,
7161) -> Result<Option<Vec<crate::reason::Inconsistency>>, FileError> {
7162 let head = reader.read_at(0, HEADER_LEN as u64)?;
7163 let header = Header::from_bytes(&head)?;
7164 if let Some(s) = read_schema_with_header(reader, &header) {
7166 return Ok(Some(schema_coherence(
7167 &s.class_hierarchy,
7168 &s.subclass_cycles,
7169 &s.disjoint_pairs,
7170 &s.equivalent_pairs,
7171 )));
7172 }
7173 if header.pyramid_meta_len == 0 {
7174 return Ok(None);
7175 }
7176 if header.schema_meta_len > 0 && (header.schema_meta_len as u64) <= header.pyramid_meta_len {
7180 let off =
7181 header.pyramid_meta_offset + header.pyramid_meta_len - header.schema_meta_len as u64;
7182 let block = reader.read_at(off, header.schema_meta_len as u64)?;
7183 let (hierarchy, cycles, disjoint, equivalent) = crate::meta::decode_schema_block(&block)
7184 .map_err(|_| FileError::Container("malformed schema block"))?;
7185 return Ok(Some(schema_coherence(
7186 &hierarchy,
7187 &cycles,
7188 &disjoint,
7189 &equivalent,
7190 )));
7191 }
7192 let mb = if header.version == crate::v06::VERSION {
7194 crate::v06::open::read_pyramid(reader)?.unwrap_or_default()
7195 } else {
7196 reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?
7197 };
7198 let meta =
7199 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
7200 Ok(Some(schema_coherence(
7201 &meta.class_hierarchy,
7202 &meta.subclass_cycles,
7203 &meta.disjoint_pairs,
7204 &meta.equivalent_pairs,
7205 )))
7206}
7207
7208#[allow(clippy::type_complexity)]
7216pub fn read_schema_summary_ranged<R: RangeReader>(
7217 reader: &R,
7218) -> Result<Option<(Vec<(String, u64)>, Vec<(String, String, String, u64)>)>, FileError> {
7219 let head = reader.read_at(0, HEADER_LEN as u64)?;
7220 let header = Header::from_bytes(&head)?;
7221 let meta = if let Some(s) = read_schema_with_header(reader, &header) {
7223 PyramidMeta::new(0, Vec::new(), &[]).with_schema(
7224 Vec::new(),
7225 s.level_rollups,
7226 s.level_links,
7227 Vec::new(),
7228 Vec::new(),
7229 Vec::new(),
7230 Vec::new(),
7231 )
7232 } else {
7233 if header.pyramid_meta_len == 0 {
7234 return Ok(None);
7235 }
7236 if header.schema_meta_len > 0 && (header.schema_meta_len as u64) <= header.pyramid_meta_len
7237 {
7238 let off = header.pyramid_meta_offset + header.pyramid_meta_len
7239 - header.schema_meta_len as u64;
7240 let block = reader.read_at(off, header.schema_meta_len as u64)?;
7241 let summary = crate::meta::decode_schema_block_summary(&block)
7242 .map_err(|_| FileError::Container("malformed schema block"))?;
7243 return Ok(Some(summary));
7244 }
7245 let mb = if header.version == crate::v06::VERSION {
7247 crate::v06::open::read_pyramid(reader)?.unwrap_or_default()
7248 } else {
7249 reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?
7250 };
7251 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?
7252 };
7253 if meta.level_rollups.is_empty() && meta.level_links.is_empty() {
7254 return Ok(None);
7255 }
7256 let classes = meta
7257 .level_rollups
7258 .iter()
7259 .max_by_key(|r| r.depth)
7260 .map(|r| r.classes.clone())
7261 .unwrap_or_default();
7262 let relations = meta
7263 .level_links
7264 .iter()
7265 .max_by_key(|l| l.depth)
7266 .map(|l| {
7267 l.links
7268 .iter()
7269 .map(|c| {
7270 (
7271 c.s_class.clone(),
7272 c.predicate.clone(),
7273 c.o_class.clone(),
7274 c.count,
7275 )
7276 })
7277 .collect()
7278 })
7279 .unwrap_or_default();
7280 Ok(Some((classes, relations)))
7281}
7282
7283#[cfg(test)]
7288pub(crate) fn dict_chunk_keys_for_test(image: &[u8]) -> Vec<Vec<Vec<u8>>> {
7289 if crate::v06::open::is_v06(image) {
7290 use crate::v06::{decode_chunk_leaf, decode_dict_root, kind, open, tag};
7292 let r = crate::reader::SliceReader::new(image);
7293 let (h, root) = open::read_front(&r).unwrap();
7294 let roots = decode_dict_root(root.unit(tag::DICT).unwrap()).unwrap();
7295 let leaves = open::read_section(&r, &h, &root, kind::DICT_LEAVES)
7296 .unwrap()
7297 .unwrap();
7298 return roots
7299 .iter()
7300 .map(|s| {
7301 s.leaves
7302 .iter()
7303 .flat_map(|(l, _, _, _)| {
7304 let stored = &leaves[l.off as usize..(l.off + l.len) as usize];
7305 let raw = decompress(h.internal_codec, stored).unwrap();
7306 decode_chunk_leaf(&raw, l.n).unwrap()
7307 })
7308 .map(|c| c.key)
7309 .collect()
7310 })
7311 .collect();
7312 }
7313 let header = Header::from_bytes(&image[..HEADER_LEN]).unwrap();
7314 let s = header.dictionary_offset as usize;
7315 let e = s + header.dictionary_len as usize;
7316 decode_container(&image[s..e], CODEC_NONE)
7317 .unwrap()
7318 .iter()
7319 .map(|payload| {
7320 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
7321 entries.into_iter().map(|c| c.key).collect()
7322 })
7323 .collect()
7324}
7325
7326#[cfg(test)]
7327mod tests {
7328 use super::*;
7329 use super::{
7330 write_dataset_v05 as write_dataset,
7331 write_dataset_with_metadata_v05 as write_dataset_with_metadata,
7332 write_file_v05 as write_file,
7333 };
7334 use crate::dictionary::DictionaryBuilder;
7335 use crate::index::GraphIndexBuilder;
7336
7337 #[cfg(not(feature = "compression"))]
7340 #[test]
7341 fn ruzstd_decodes_a_zstd_frame() {
7342 let data: Vec<u8> = (0..50_000u32)
7343 .flat_map(|i| (i % 251).to_le_bytes())
7344 .collect();
7345 let frame = ruzstd::encoding::compress_to_vec(
7346 &data[..],
7347 ruzstd::encoding::CompressionLevel::Fastest,
7348 );
7349 assert!(frame.len() < data.len());
7350 assert_eq!(decompress(CODEC_ZSTD, &frame).unwrap(), data);
7351 }
7352
7353 #[cfg(feature = "compression")]
7358 #[test]
7359 fn decompression_is_bounded() {
7360 let data: Vec<u8> = (0..100_000u32).map(|i| (i % 251) as u8).collect();
7361 let sized = zstd::bulk::compress(&data, 3).unwrap();
7362 assert_eq!(zstd_frame_content_size(&sized), Some(100_000));
7363 assert_eq!(block_raw_len(CODEC_ZSTD, &sized, 1 << 20).unwrap(), 100_000);
7364 assert_eq!(
7365 decompress_bounded(CODEC_ZSTD, &sized, 100_000).unwrap(),
7366 data
7367 );
7368 assert!(decompress_bounded(CODEC_ZSTD, &sized, 99_999).is_err());
7369 let stream = zstd::encode_all(&data[..], 3).unwrap();
7371 assert_eq!(zstd_frame_content_size(&stream), None);
7372 assert_eq!(
7373 block_raw_len(CODEC_ZSTD, &stream, 1 << 20).unwrap(),
7374 100_000
7375 );
7376 assert_eq!(
7377 decompress_bounded(CODEC_ZSTD, &stream, 100_000).unwrap(),
7378 data
7379 );
7380 assert!(decompress_bounded(CODEC_ZSTD, &stream, 50_000).is_err());
7381 let mut liar = sized.clone();
7383 let fhd = liar[4];
7384 let fcs_at = 5 + usize::from(fhd & 0x20 == 0);
7385 liar[fcs_at] = liar[fcs_at].wrapping_sub(1);
7386 assert!(decompress_bounded(CODEC_ZSTD, &liar, 200_000).is_err());
7387 assert!(decompress_bounded(CODEC_NONE, &data, 99_999).is_err());
7389 assert_eq!(
7390 decompress_bounded(CODEC_NONE, &data, 100_000).unwrap(),
7391 data
7392 );
7393 assert_eq!(zstd_frame_content_size(b"RETE"), None);
7394 }
7395
7396 #[test]
7402 fn three_permutation_file_round_trips_every_open_path() {
7403 use crate::index::PermSet;
7404 use crate::reader::SliceReader;
7405
7406 let build = |perms: PermSet| {
7407 let mut db = crate::DictionaryBuilder::new();
7408 let mut triples = Vec::new();
7409 for i in 0..60u32 {
7410 let (s, p, o) = (
7411 format!("<http://ex/s{}>", i % 11),
7412 format!("<http://ex/p{}>", i % 3),
7413 format!("<http://ex/o{}>", i % 7),
7414 );
7415 db.observe(&s, &p, &o);
7416 triples.push((s, p, o));
7417 }
7418 let dict = db.build();
7419 let ids: Vec<(u32, u32, u32)> = triples
7420 .iter()
7421 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
7422 .collect();
7423 let def = GraphIndexBuilder::from_triples(ids.clone())
7424 .with_tile_budget(96)
7425 .with_perms(perms)
7426 .build();
7427 let named = GraphIndexBuilder::from_triples(ids)
7428 .with_tile_budget(96)
7429 .with_perms(perms)
7430 .build();
7431 write_dataset(
7432 &dict,
7433 &def,
7434 &[("<http://ex/g>".to_string(), named)],
7435 true,
7436 &[],
7437 0,
7438 )
7439 };
7440
7441 let six = build(PermSet::ALL);
7442 let three = build(PermSet::CORE);
7443 assert!(three.len() < six.len());
7444 assert_eq!(Header::from_bytes(&three).unwrap().perms, PermSet::CORE);
7445 assert_eq!(Header::from_bytes(&six).unwrap().perms, PermSet::ALL);
7446
7447 let shapes: Vec<(Option<&str>, Option<&str>, Option<&str>)> = vec![
7448 (None, None, None),
7449 (Some("<http://ex/s3>"), None, None),
7450 (None, Some("<http://ex/p1>"), None),
7451 (None, None, Some("<http://ex/o5>")),
7452 (Some("<http://ex/s3>"), Some("<http://ex/p1>"), None),
7453 (Some("<http://ex/s3>"), None, Some("<http://ex/o5>")),
7454 (None, Some("<http://ex/p1>"), Some("<http://ex/o5>")),
7455 (
7456 Some("<http://ex/s3>"),
7457 Some("<http://ex/p1>"),
7458 Some("<http://ex/o5>"),
7459 ),
7460 ];
7461
7462 for (image, tag) in [(&six, "six"), (&three, "three")] {
7463 let resident = Rete::open(image).unwrap();
7464 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
7465 let ranged = Rete::open_ranged(&SliceReader::new(leaked)).unwrap();
7466 let lazy =
7467 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64).unwrap();
7468 for (s, p, o) in &shapes {
7469 let want: Vec<_> = {
7470 let mut v = Rete::open(&six).unwrap().query(*s, *p, *o);
7471 v.sort();
7472 v
7473 };
7474 for (got, path) in [
7475 (resident.query(*s, *p, *o), "resident"),
7476 (ranged.query(*s, *p, *o), "ranged"),
7477 (lazy.query(*s, *p, *o), "lazy"),
7478 ] {
7479 let mut got = got;
7480 got.sort();
7481 assert_eq!(got, want, "{tag}/{path} disagreed on {:?}", (s, p, o));
7482 }
7483 let mut g = resident.query_in_graph(Some("<http://ex/g>"), *s, *p, *o);
7485 g.sort();
7486 assert_eq!(g, want, "{tag}/named disagreed on {:?}", (s, p, o));
7487 }
7488 }
7489
7490 let leaked: &'static [u8] = Box::leak(three.clone().into_boxed_slice());
7494 let reader = SliceReader::new(leaked);
7495 let routed = Rete::query_ranged(&reader, None, None, Some("<http://ex/o5>")).unwrap();
7496 let mut want = Rete::open(&six)
7497 .unwrap()
7498 .query(None, None, Some("<http://ex/o5>"));
7499 want.sort();
7500 let mut got = routed;
7501 got.sort();
7502 assert_eq!(got, want, "routed single-pattern read on a lean file");
7503 }
7504
7505 #[test]
7506 fn read_coalesced_merges_within_gap_and_splits_beyond() {
7507 use crate::reader::{CountingReader, SliceReader};
7508 let bytes = vec![0u8; 4096];
7509 let ranges = [
7511 ByteRange { offset: 0, len: 16 },
7512 ByteRange {
7513 offset: 48,
7514 len: 16,
7515 },
7516 ByteRange {
7517 offset: 1088,
7518 len: 16,
7519 },
7520 ];
7521 let r = CountingReader::new(SliceReader::new(&bytes));
7523 let out = read_coalesced(&r, &ranges, 16).unwrap();
7524 assert_eq!(out.len(), 3);
7525 assert_eq!(r.requests(), 3);
7526 let r = CountingReader::new(SliceReader::new(&bytes));
7528 read_coalesced(&r, &ranges, 64).unwrap();
7529 assert_eq!(r.requests(), 2);
7530 let r = CountingReader::new(SliceReader::new(&bytes));
7532 read_coalesced(&r, &ranges, 4096).unwrap();
7533 assert_eq!(r.requests(), 1);
7534 }
7535
7536 fn build_image() -> Vec<u8> {
7537 let triples = [
7538 ("Alice", "knows", "Bob"),
7539 ("Bob", "knows", "Carol"),
7540 ("Alice", "age", "30"),
7541 ];
7542 let mut db = DictionaryBuilder::new();
7543 for (s, p, o) in triples {
7544 db.observe(s, p, o);
7545 }
7546 let dict = db.build();
7547
7548 let mut ib = GraphIndexBuilder::new();
7549 for (s, p, o) in triples {
7550 ib.push(dict.encode(s, p, o).unwrap());
7551 }
7552 let index = ib.build();
7553
7554 let (meta, levels) = build_pyramid_meta(&dict, &triples_ids(&dict), DEFAULT_TILE_BUDGET);
7555 write_file(&dict, &index, false, &meta, levels)
7556 }
7557
7558 fn triples_ids(dict: &Dictionary) -> Vec<(u32, u32, u32)> {
7559 [
7560 ("Alice", "knows", "Bob"),
7561 ("Bob", "knows", "Carol"),
7562 ("Alice", "age", "30"),
7563 ]
7564 .iter()
7565 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
7566 .collect()
7567 }
7568
7569 #[test]
7570 fn file_round_trips_header_and_counts() {
7571 let bytes = build_image();
7572 let rete = Rete::open(&bytes).unwrap();
7573 assert_eq!(rete.header().quad_count, 3);
7574 assert!(rete.header().term_count >= 5);
7575 let expected_codec = writer_codec();
7576 assert_eq!(rete.header().dict_codec, expected_codec);
7577 assert_eq!(rete.header().block_codec, expected_codec);
7578 assert_eq!(&bytes[bytes.len() - 4..], &MAGIC); }
7580
7581 #[test]
7586 fn multi_tile_file_round_trips_and_routes() {
7587 let triples: Vec<(String, String, String)> = (0..200)
7588 .map(|i| {
7589 (
7590 format!("<http://ex/s/{i}>"),
7591 format!("<http://ex/p/{}>", i % 5),
7592 format!("<http://ex/o/{}>", i % 23),
7593 )
7594 })
7595 .collect();
7596 let mut db = DictionaryBuilder::new();
7597 for (s, p, o) in &triples {
7598 db.observe(s, p, o);
7599 }
7600 let dict = db.build();
7601 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
7602 for (s, p, o) in &triples {
7603 ib.push(dict.encode(s, p, o).unwrap());
7604 }
7605 let index = ib.build();
7606 assert!(
7607 index.tile_sections()[0].len() > 3,
7608 "tiny budget must force many tiles"
7609 );
7610 let bytes = write_file(&dict, &index, false, &[], 0);
7611
7612 let rete = Rete::open(&bytes).unwrap();
7613 assert_eq!(rete.header().version, crate::header::LEGACY_FORMAT_VERSION);
7614 assert_eq!(rete.query(None, None, None).len(), 200);
7615 assert_eq!(rete.query(Some("<http://ex/s/7>"), None, None).len(), 1);
7616 assert_eq!(
7617 rete.query(None, Some("<http://ex/p/3>"), None).len(),
7618 40,
7619 "predicate extent spans tiles"
7620 );
7621 assert_eq!(
7622 rete.query(None, None, Some("<http://ex/o/22>")).len(),
7623 8 );
7625
7626 use crate::reader::SliceReader;
7628 let reader = SliceReader::new(&bytes);
7629 let routed = Rete::query_ranged(&reader, Some("<http://ex/s/7>"), None, None).unwrap();
7630 assert_eq!(routed.len(), 1);
7631 let routed = Rete::query_ranged(&reader, None, Some("<http://ex/p/3>"), None).unwrap();
7632 assert_eq!(routed.len(), 40);
7633 let routed = Rete::query_ranged(&reader, None, None, Some("<http://ex/o/22>")).unwrap();
7634 assert_eq!(routed.len(), 8);
7635 }
7636
7637 #[test]
7646 fn tile_directory_offsets_survive_past_4gib() {
7647 let mut dir = Vec::new();
7648 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);
7654 write_uvarint(&mut dir, 2 << 30); let total = dir.len() as u64 + (3u64 << 30) + (2u64 << 30) + 64;
7656 let entries = parse_tile_directory(&dir, total).unwrap();
7657 assert_eq!(entries.len(), 2);
7658 assert_eq!(entries[1].start, dir.len() as u64 + (3u64 << 30));
7659 assert!(
7660 entries[1].end > u32::MAX as u64,
7661 "tail tile sits past 4 GiB"
7662 );
7663 assert!(parse_tile_directory(&dir, 1 << 20).is_err());
7665 }
7666
7667 #[test]
7668 fn tile_synopsis_trailer_round_trips() {
7669 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
7670 for i in 0..200u32 {
7671 ib.push((i, i % 7, i % 13));
7672 }
7673 let index = ib.build();
7674 let tile_count = index.tile_sections()[0].len();
7675 assert!(tile_count > 3, "tiny budget forces many tiles");
7676
7677 let payload = encode_tiled_section(&index, 0, CODEC_NONE);
7678 let dir = parse_tile_directory(&payload, payload.len() as u64).unwrap();
7679 assert_eq!(dir.len(), tile_count);
7680 let trailer_start = dir.iter().map(|e| e.end).max().unwrap();
7682 assert!(
7683 trailer_start < payload.len() as u64,
7684 "a trailer follows the tiles"
7685 );
7686 for e in &dir {
7687 assert!(
7688 e.end <= payload.len() as u64,
7689 "tiles still located within the payload"
7690 );
7691 }
7692 let syn = parse_tile_synopsis(&payload, trailer_start as usize, dir.len()).unwrap();
7693 for (e, (min_b, max_b, min_c, max_c)) in dir.iter().zip(syn) {
7694 let block = decompress(CODEC_NONE, &payload[e.start as usize..e.end as usize]).unwrap();
7695 let z = *crate::triples::TripleBlock::parse(&block).unwrap().zone();
7696 assert_eq!(
7697 (min_b, max_b, min_c, max_c),
7698 (z.min_b, z.max_b, z.min_c, z.max_c),
7699 "synopsis equals the tile's own zone"
7700 );
7701 }
7702 }
7703
7704 #[test]
7708 fn tile_synopsis_lazy_matches_reference_every_shape() {
7709 use crate::reader::{CountingReader, SliceReader};
7710 let triples: Vec<(String, String, String)> = (0..200u32)
7711 .map(|i| {
7712 (
7713 format!("<http://ex/s/{i:04}>"),
7714 format!("<http://ex/p/{}>", i % 7),
7715 format!("<http://ex/o/{:04}>", i % 13),
7716 )
7717 })
7718 .collect();
7719 let mut db = DictionaryBuilder::new();
7720 for (s, p, o) in &triples {
7721 db.observe(s, p, o);
7722 }
7723 let dict = db.build();
7724 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
7725 for (s, p, o) in &triples {
7726 ib.push(dict.encode(s, p, o).unwrap());
7727 }
7728 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7729
7730 let eager = Rete::open(&bytes).unwrap();
7731 assert!(
7732 eager.header().has_tile_synopsis(),
7733 "new files set the synopsis flag"
7734 );
7735
7736 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
7739 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7740 let lazy = Rete::open_ranged_lazy_with(reader, HEADER_LEN as u64).unwrap();
7741
7742 let brute = |s: Option<&str>, p: Option<&str>, o: Option<&str>| {
7743 let mut v: Vec<(String, String, String)> = triples
7744 .iter()
7745 .filter(|(a, b, c)| {
7746 s.is_none_or(|x| x == a) && p.is_none_or(|x| x == b) && o.is_none_or(|x| x == c)
7747 })
7748 .cloned()
7749 .collect();
7750 v.sort();
7751 v
7752 };
7753 let sv = [
7755 None,
7756 Some("<http://ex/s/0007>"),
7757 Some("<http://ex/s/0130>"),
7758 Some("<http://ex/s/9999>"),
7759 ];
7760 let pv = [
7761 None,
7762 Some("<http://ex/p/3>"),
7763 Some("<http://ex/p/6>"),
7764 Some("<http://ex/p/999>"),
7765 ];
7766 let ov = [
7767 None,
7768 Some("<http://ex/o/0000>"),
7769 Some("<http://ex/o/0012>"),
7770 Some("<http://ex/o/9999>"),
7771 ];
7772 for &s in &sv {
7773 for &p in &pv {
7774 for &o in &ov {
7775 let mut e = eager.query(s, p, o);
7776 e.sort();
7777 let mut l = lazy.query(s, p, o);
7778 l.sort();
7779 let r = brute(s, p, o);
7780 assert_eq!(e, r, "eager {s:?} {p:?} {o:?}");
7781 assert_eq!(l, r, "lazy {s:?} {p:?} {o:?} — synopsis over-pruned");
7782 }
7783 }
7784 }
7785 assert!(!lazy.index_incomplete(), "no lazy fetch failed");
7786 }
7787
7788 #[test]
7792 fn polyglot_offset_reads_lazily() {
7793 use crate::reader::{
7794 detect_polyglot_base, CountingReader, OffsetReader, SliceReader, POLYGLOT_DIGITS,
7795 POLYGLOT_MARKER,
7796 };
7797
7798 let image = build_image();
7802 let mut shell = Vec::new();
7803 shell.extend_from_slice(b"<!DOCTYPE html><html><head><!--");
7804 shell.extend_from_slice(POLYGLOT_MARKER);
7805 let digits_at = shell.len();
7806 shell.extend_from_slice(&[b'0'; POLYGLOT_DIGITS]); shell.extend_from_slice(b"--></head><body>a web page</body></html>\n");
7808 shell.resize(50_000, b' '); let base = shell.len() as u64;
7810 let digits = format!("{base:0width$}", width = POLYGLOT_DIGITS);
7811 shell[digits_at..digits_at + POLYGLOT_DIGITS].copy_from_slice(digits.as_bytes());
7812
7813 let mut poly = shell;
7814 poly.extend_from_slice(&image);
7815
7816 assert_ne!(
7818 &poly[0..4],
7819 b"RETE",
7820 "polyglot must not start with the magic"
7821 );
7822 let detected = detect_polyglot_base(&poly[..crate::header::HEADER_LEN]).unwrap();
7824 assert_eq!(detected, base);
7825
7826 let leaked: &'static [u8] = Box::leak(poly.into_boxed_slice());
7828 let counting = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7829 let lazy = Rete::open_ranged_lazy_with(
7830 OffsetReader::new(counting.clone(), detected),
7831 HEADER_LEN as u64,
7832 )
7833 .unwrap();
7834
7835 let plain = Rete::open(&image).unwrap();
7837 let mut want = plain.query(None, None, None);
7838 want.sort();
7839 let mut got = lazy.query(None, None, None);
7840 got.sort();
7841 assert_eq!(
7842 got, want,
7843 "polyglot lazy query differs from the plain .rete"
7844 );
7845 assert!(!got.is_empty(), "expected some triples");
7846 assert!(!lazy.index_incomplete(), "a lazy fetch failed");
7847
7848 let read = counting.bytes_read();
7851 eprintln!(
7852 "polyglot lazy read: HTML shell {base} B + .rete image {} B; \
7853 the query touched only {read} B (never the shell).",
7854 image.len()
7855 );
7856 assert!(
7857 read < base,
7858 "read {read} bytes; the HTML shell alone is {base} — the reader \
7859 touched the prefix instead of only the embedded .rete"
7860 );
7861 }
7862
7863 #[cfg(test)]
7866 fn build_text_indexed(triples: &[(String, String, String)]) -> Vec<u8> {
7867 let mut db = DictionaryBuilder::new();
7868 for (s, p, o) in triples {
7869 db.observe(s, p, o);
7870 }
7871 let dict = db.build();
7872 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
7873 let mut id_triples: Vec<(u32, u32, u32)> = Vec::with_capacity(triples.len());
7874 for (s, p, o) in triples {
7875 let t = dict.encode(s, p, o).unwrap();
7876 ib.push(t);
7877 id_triples.push(t);
7878 }
7879 let index = ib.build();
7880 let text_index = compute_text_index(&dict, &id_triples);
7881 assert!(
7882 !text_index.is_empty(),
7883 "literals should produce a text index"
7884 );
7885 write_dataset_with_metadata(&dict, &index, &[], false, &[], 0, &[], &text_index)
7886 }
7887
7888 #[test]
7892 fn text_index_eager_matches_brute_force() {
7893 let triples: Vec<(String, String, String)> = vec![
7894 (
7895 "<http://ex/s0>",
7896 "<http://ex/label>",
7897 "\"alpha glucose phosphate\"",
7898 ),
7899 ("<http://ex/s1>", "<http://ex/label>", "\"beta Glucose\""),
7900 ("<http://ex/s2>", "<http://ex/label>", "\"gamma fructose\""),
7901 (
7902 "<http://ex/s3>",
7903 "<http://ex/note>",
7904 "\"einstein relativity\"",
7905 ),
7906 (
7907 "<http://ex/s4>",
7908 "<http://ex/ref>",
7909 "<http://ex/not-a-literal>",
7910 ),
7911 ]
7912 .into_iter()
7913 .map(|(s, p, o)| (s.to_string(), p.to_string(), o.to_string()))
7914 .collect();
7915 let bytes = build_text_indexed(&triples);
7916 let rete = Rete::open(&bytes).unwrap();
7917 assert!(rete.has_text_index());
7918
7919 let brute = |words: &[&str]| -> Vec<String> {
7921 let mut v: Vec<String> = triples
7922 .iter()
7923 .filter(|(_, _, o)| {
7924 crate::terms::is_literal(o)
7925 && words.iter().all(|w| {
7926 let wl = w.to_lowercase();
7927 crate::terms::literal_lexical(o)
7928 .unwrap()
7929 .split(|c: char| !c.is_alphanumeric())
7930 .any(|t| t.to_lowercase() == wl)
7931 })
7932 })
7933 .map(|(s, _, _)| s.clone())
7934 .collect();
7935 v.sort();
7936 v.dedup();
7937 v
7938 };
7939
7940 let mut got = rete.text_search(&["glucose"], None, 0);
7941 got.sort();
7942 assert_eq!(got, brute(&["glucose"]), "case-insensitive single word");
7943
7944 let mut got = rete.text_search(&["glucose", "phosphate"], None, 0);
7946 got.sort();
7947 assert_eq!(got, brute(&["glucose", "phosphate"]));
7948
7949 assert!(rete.text_search(&["zzznope"], None, 0).is_empty());
7951
7952 let got = rete.text_search(&[], Some("ein"), 0);
7954 assert_eq!(got, vec!["<http://ex/s3>".to_string()]);
7955
7956 let mut db = DictionaryBuilder::new();
7958 for (s, p, o) in &triples {
7959 db.observe(s, p, o);
7960 }
7961 let dict = db.build();
7962 let mut ib = GraphIndexBuilder::new();
7963 for (s, p, o) in &triples {
7964 ib.push(dict.encode(s, p, o).unwrap());
7965 }
7966 let plain = write_dataset(&dict, &ib.build(), &[], false, &[], 0);
7967 let plain_rete = Rete::open(&plain).unwrap();
7968 assert!(!plain_rete.has_text_index());
7969 assert!(plain_rete.text_search(&["glucose"], None, 0).is_empty());
7970 }
7971
7972 #[test]
7976 fn text_index_lazy_faults_only_queried_postings() {
7977 use crate::reader::{CountingReader, SliceReader};
7978 let mut triples: Vec<(String, String, String)> = (0..300u32)
7981 .map(|i| {
7982 (
7983 format!("<http://ex/s/{i:04}>"),
7984 "<http://ex/label>".to_string(),
7985 format!("\"common word number {i}\""),
7986 )
7987 })
7988 .collect();
7989 for i in [3u32, 77, 250] {
7990 triples.push((
7991 format!("<http://ex/s/{i:04}>"),
7992 "<http://ex/tag>".to_string(),
7993 "\"raretoken\"".to_string(),
7994 ));
7995 }
7996 let bytes = build_text_indexed(&triples);
7997 let eager = Rete::open(&bytes).unwrap();
7998 let mut want = eager.text_search(&["raretoken"], None, 0);
7999 want.sort();
8000 assert_eq!(want.len(), 3);
8001
8002 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8003 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8004 let lazy = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
8005 let before = reader.bytes_read();
8008 let mut got = lazy.text_search(&["raretoken"], None, 0);
8009 got.sort();
8010 assert_eq!(got, want, "lazy search matches eager");
8011 let pulled = reader.bytes_read() - before;
8012 let ti_len = eager.header().text_index_len;
8015 assert!(
8016 pulled < ti_len,
8017 "search pulled {pulled} B but the section is {ti_len} B — faulted too much"
8018 );
8019 assert!(!lazy.index_incomplete());
8020 }
8021
8022 #[test]
8027 fn search_view_matches_full_open_for_far_fewer_bytes() {
8028 use crate::reader::{CountingReader, SliceReader};
8029 let filler = "lorem ipsum dolor sit amet consectetur adipiscing elit ".repeat(20);
8032 let mut triples: Vec<(String, String, String)> = (0..300u32)
8033 .map(|i| {
8034 (
8035 format!("<http://ex/s/{i:04}>"),
8036 "<http://ex/abstract>".to_string(),
8037 format!("\"{filler} number {i}\""),
8038 )
8039 })
8040 .collect();
8041 for i in [3u32, 77, 250] {
8042 triples.push((
8043 format!("<http://ex/s/{i:04}>"),
8044 "<http://ex/tag>".to_string(),
8045 "\"raretoken\"".to_string(),
8046 ));
8047 }
8048 let bytes = build_text_indexed(&triples);
8049 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8050
8051 let full_reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8052 let full = Rete::open_ranged_lazy_with(full_reader.clone(), HEADER_LEN as u64).unwrap();
8053 let mut want = full.text_search(&["raretoken"], None, 0);
8054 want.sort();
8055 assert_eq!(want.len(), 3);
8056
8057 let search_reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8058 let view = SearchView::open_ranged_with(search_reader.clone(), HEADER_LEN as u64).unwrap();
8059 assert!(view.has_text_index());
8060 let mut got = view.text_search(&["raretoken"], None, 0);
8061 got.sort();
8062 assert_eq!(got, want, "search view matches the full open");
8063 assert!(
8064 search_reader.bytes_read() < full_reader.bytes_read(),
8065 "search read {} B, full open read {} B — the narrow open bought nothing",
8066 search_reader.bytes_read(),
8067 full_reader.bytes_read()
8068 );
8069
8070 let mut prefix_hits = view.prefix_search("", 5);
8073 prefix_hits.sort();
8074 assert!(!prefix_hits.is_empty() || !view.has_pyramid());
8075 }
8076
8077 #[test]
8087 fn dict_chunk_directory_probe_costs_about_one_directory() {
8088 use crate::reader::{CountingReader, SliceReader};
8089 let head = "x".repeat(3000);
8103 let triples: Vec<(String, String, String)> = (0..2000u32)
8104 .map(|i| {
8105 let tail = char::from(b'a' + (i % 26) as u8).to_string().repeat(1000);
8106 (
8107 format!("<http://ex/s/{i:04}>"),
8108 "<http://ex/abstract>".to_string(),
8109 format!("\"{head}{i:04}{tail}\""),
8110 )
8111 })
8112 .collect();
8113 let mut db = DictionaryBuilder::new();
8114 for (s, p, o) in &triples {
8115 db.observe(s, p, o);
8116 }
8117 let dict = db.build();
8118 let mut ib = GraphIndexBuilder::new();
8119 for (s, p, o) in &triples {
8120 ib.push(dict.encode(s, p, o).unwrap());
8121 }
8122 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8123
8124 let reader = CountingReader::new(SliceReader::new(&bytes));
8125 let header = Header::from_bytes(&reader.read_at(0, HEADER_LEN as u64).unwrap()).unwrap();
8126 let section = locate_container_section_ranged(
8128 &reader,
8129 header.dictionary_offset,
8130 header.dictionary_len,
8131 2,
8132 4,
8133 )
8134 .unwrap();
8135
8136 let before = reader.bytes_read();
8137 let (_meta, entries) = read_dict_dir_ranged(&reader, section).unwrap();
8138 let spent = reader.bytes_read() - before;
8139
8140 let dir_end = entries[0].start;
8142 let head = reader.read_at(section.offset, 64).unwrap();
8143 let (header_len, n0) = read_uvarint(&head).unwrap();
8144 let dir_start = n0 as u64 + header_len;
8145 let dir_len = dir_end - dir_start;
8146 let dir_total = section.len - dir_start;
8147 assert!(
8148 dir_start + dir_len > 8192,
8149 "directory fits the header prefix ({dir_len} B) — the probe never runs"
8150 );
8151
8152 let mut old = 0u64;
8154 let mut p = 4096u64.min(dir_total).max(1);
8155 loop {
8156 let got = p.min(dir_total);
8157 old += got;
8158 if got >= dir_len || p >= dir_total {
8159 break;
8160 }
8161 p = p.saturating_mul(2).min(dir_total);
8162 }
8163 assert!(
8164 spent <= dir_len * 2,
8165 "probe read {spent} B for a {dir_len} B directory — past the append-only bound"
8166 );
8167 assert!(
8168 spent < old,
8169 "probe read {spent} B; the re-reading loop it replaced spent {old} B"
8170 );
8171 }
8172
8173 fn verbatim_keyed_twin(payload: &[u8], codec: u8) -> Vec<u8> {
8183 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
8184 let (header_len, n0) = read_uvarint(payload).unwrap();
8185 let mut out = Vec::new();
8186 write_uvarint(&mut out, header_len);
8187 out.extend_from_slice(&payload[n0..n0 + header_len as usize]);
8188 write_uvarint(&mut out, entries.len() as u64);
8189 let mut prev_run = 0usize;
8190 for e in &entries {
8191 let body = decompress(codec, &payload[e.start as usize..e.end as usize]).unwrap();
8192 let first = crate::dict::run_first_term(&body, 0).unwrap();
8193 write_uvarint(&mut out, (e.first_run - prev_run) as u64);
8194 write_uvarint(&mut out, first.len() as u64);
8195 out.extend_from_slice(&first);
8196 write_uvarint(&mut out, e.end - e.start);
8197 prev_run = e.first_run;
8198 }
8199 for e in &entries {
8200 out.extend_from_slice(&payload[e.start as usize..e.end as usize]);
8201 }
8202 out
8203 }
8204
8205 fn chunk_bounds_of(payload: &[u8], codec: u8) -> Vec<(Vec<u8>, Vec<u8>)> {
8207 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
8208 entries
8209 .iter()
8210 .map(|e| {
8211 let body = decompress(codec, &payload[e.start as usize..e.end as usize]).unwrap();
8212 (
8213 crate::dict::run_first_term(&body, 0).unwrap(),
8214 crate::dict::run_last_term(&body, 0, body.len()).unwrap(),
8215 )
8216 })
8217 .collect()
8218 }
8219
8220 fn adversarial_terms() -> Vec<String> {
8226 let mut terms: Vec<String> = Vec::new();
8227 for i in 0..6_000 {
8228 terms.push(format!("<http://example.org/work/{i:06}>"));
8229 }
8230 let shared = "X".repeat(900);
8231 for i in 0..4_000 {
8232 terms.push(format!("\"{shared}-{i:05}-tail\""));
8233 }
8234 let deep = "Y".repeat(3_000); for i in 0..120 {
8236 terms.push(format!("\"{deep}{i:03}\""));
8237 }
8238 for i in 0..6 {
8239 terms.push(format!("\"BIG-{i:02}-{}\"", "Z".repeat(90_000)));
8241 }
8242 for i in 0..800 {
8243 terms.push(format!(
8244 "\"h\u{e9}llo-{}-\u{1F600}{i:04}\"",
8245 "\u{e9}".repeat(40)
8246 ));
8247 }
8248 for i in 0..2_000 {
8249 terms.push(format!("\"{i:05}-{}\"", "T".repeat(1_800)));
8250 }
8251 terms.sort();
8252 terms.dedup();
8253 terms
8254 }
8255
8256 fn chunked_section_of(terms: &[String]) -> Vec<u8> {
8257 let mut b = crate::dict::DictSectionBuilder::new().with_restart_interval(16);
8258 for t in terms {
8259 b.push(t.clone());
8260 }
8261 b.build()
8262 }
8263
8264 #[test]
8274 fn chunk_directory_keys_are_separators_not_terms() {
8275 let terms = adversarial_terms();
8276 let raw = chunked_section_of(&terms);
8277 let payload = encode_chunked_dict_section(&raw, CODEC_NONE);
8278 let (_meta, entries) = parse_chunked_dict_dir(&payload, payload.len() as u64).unwrap();
8279 let bounds = chunk_bounds_of(&payload, CODEC_NONE);
8280 assert!(
8281 entries.len() > 20,
8282 "need a many-chunk section, got {}",
8283 entries.len()
8284 );
8285
8286 assert!(entries[0].key.is_empty(), "chunk 0 needs no separator");
8287 let mut key_bytes = 0usize;
8288 let mut term_bytes = 0usize;
8289 let mut differ = 0usize;
8290 for (i, e) in entries.iter().enumerate() {
8291 key_bytes += e.key.len();
8292 term_bytes += bounds[i].0.len();
8293 if e.key != bounds[i].0 {
8294 differ += 1;
8295 }
8296 assert!(
8297 e.key <= bounds[i].0,
8298 "chunk {i}: key must be <= its own first term"
8299 );
8300 if i > 0 {
8301 assert!(
8302 e.key.as_slice() > bounds[i - 1].1.as_slice(),
8303 "chunk {i}: key must be > the previous chunk's last term"
8304 );
8305 assert!(
8306 e.key > entries[i - 1].key,
8307 "chunk {i}: keys must ascend for partition_point"
8308 );
8309 assert_eq!(
8311 e.key,
8312 crate::dict::shortest_separator(&bounds[i - 1].1, &bounds[i].0),
8313 "chunk {i}: key is not the SHORTEST separator"
8314 );
8315 }
8316 }
8317 assert!(
8318 differ > 0,
8319 "every key equalled its first term — the writer stores terms again"
8320 );
8321 assert!(
8322 key_bytes * 4 < term_bytes,
8323 "separators bought almost nothing: {key_bytes} B vs {term_bytes} B of first terms"
8324 );
8325 eprintln!(
8326 "chunks={} first-term keys={term_bytes} B separator keys={key_bytes} B ({:.1}x)",
8327 entries.len(),
8328 term_bytes as f64 / key_bytes.max(1) as f64
8329 );
8330 }
8331
8332 #[test]
8343 fn separator_keys_route_identically_to_verbatim_first_terms() {
8344 let terms = adversarial_terms();
8345 let raw = chunked_section_of(&terms);
8346 let sep_payload = encode_chunked_dict_section(&raw, CODEC_NONE);
8347 let ver_payload = verbatim_keyed_twin(&sep_payload, CODEC_NONE);
8348
8349 let (_ms, es) = parse_chunked_dict_dir(&sep_payload, sep_payload.len() as u64).unwrap();
8351 let (_mv, ev) = parse_chunked_dict_dir(&ver_payload, ver_payload.len() as u64).unwrap();
8352 assert_eq!(es.len(), ev.len());
8353 for (a, b) in es.iter().zip(ev.iter()) {
8354 assert_eq!(a.first_run, b.first_run);
8355 assert_eq!(
8356 &sep_payload[a.start as usize..a.end as usize],
8357 &ver_payload[b.start as usize..b.end as usize],
8358 "chunk bodies must be identical"
8359 );
8360 }
8361 assert!(
8362 sep_payload.len() < ver_payload.len(),
8363 "separator payload is not smaller"
8364 );
8365
8366 let sec_s = decode_chunked_dict_section(
8367 &sep_payload,
8368 CODEC_NONE,
8369 crate::chunk_cache::ChunkCache::unlimited_arc(),
8370 0,
8371 )
8372 .unwrap();
8373 let sec_v = decode_chunked_dict_section(
8374 &ver_payload,
8375 CODEC_NONE,
8376 crate::chunk_cache::ChunkCache::unlimited_arc(),
8377 0,
8378 )
8379 .unwrap();
8380
8381 for (i, t) in terms.iter().enumerate() {
8383 let want = Some(i as u32 + 1);
8384 assert_eq!(sec_v.id(t), want, "verbatim id() lost a term");
8385 assert_eq!(sec_s.id(t), want, "separator id() lost a term");
8386 }
8387 for i in 0..terms.len() as u32 {
8389 assert_eq!(sec_s.term(i + 1), sec_v.term(i + 1));
8390 assert_eq!(
8391 sec_s.term(i + 1).as_deref(),
8392 Some(terms[i as usize].as_str())
8393 );
8394 }
8395 let bounds = chunk_bounds_of(&sep_payload, CODEC_NONE);
8397 let mut probes: Vec<String> = Vec::new();
8398 let push = |bytes: &[u8], probes: &mut Vec<String>| {
8399 if let Ok(s) = std::str::from_utf8(bytes) {
8400 probes.push(s.to_string());
8401 }
8402 };
8403 for (first, last) in &bounds {
8404 for base in [first, last] {
8405 push(base, &mut probes); let mut after = base.clone();
8407 after.push(0x01); push(&after, &mut probes);
8409 let mut far = base.clone();
8410 far.push(b'~');
8411 push(&far, &mut probes);
8412 if !base.is_empty() {
8413 push(&base[..base.len() - 1], &mut probes); let mut down = base.clone();
8415 *down.last_mut().unwrap() = down.last().unwrap().wrapping_sub(1);
8416 push(&down, &mut probes);
8417 let mut up = base.clone();
8418 *up.last_mut().unwrap() = up.last().unwrap().wrapping_add(1);
8419 push(&up, &mut probes);
8420 }
8421 }
8422 }
8423 for e in &es {
8425 push(&e.key, &mut probes);
8426 let mut plus = e.key.clone();
8427 plus.push(0);
8428 push(&plus, &mut probes);
8429 }
8430 probes.push(String::new()); probes.push("\u{1}".to_string()); probes.push("\u{10FFFF}\u{10FFFF}".to_string()); probes.push(terms.first().unwrap().clone());
8434 probes.push(terms.last().unwrap().clone());
8435
8436 let mut hits = 0usize;
8437 for p in &probes {
8438 let (a, b) = (sec_v.id(p), sec_s.id(p));
8439 assert_eq!(
8440 a,
8441 b,
8442 "probe diverged: {:?}",
8443 p.get(..p.len().min(48)).unwrap_or(p.as_str())
8444 );
8445 hits += usize::from(a.is_some());
8446 }
8447 assert!(
8448 probes.len() > 500 && hits > 50,
8449 "probe set is too thin: {} probes, {hits} of them present",
8450 probes.len()
8451 );
8452 eprintln!(
8453 "{} boundary probes, {hits} present, all identical",
8454 probes.len()
8455 );
8456 }
8457
8458 mod separator_props {
8464 use super::*;
8465 use proptest::prelude::*;
8466
8467 fn term_set() -> impl Strategy<Value = Vec<String>> {
8474 prop::collection::vec(("[a-c]{1,6}", 0u8..3, 300usize..1200), 60..250).prop_map(
8475 |specs| {
8476 let mut terms: Vec<String> = specs
8477 .into_iter()
8478 .enumerate()
8479 .map(|(i, (core, kind, pad))| {
8480 let block = "z".repeat(pad);
8481 match kind {
8482 0 => format!("<http://ex/{core}/{i:05}>"),
8483 1 => format!("\"{block}{core}{i:05}\""),
8484 _ => format!("\"{core}{i:05}{block}\""),
8485 }
8486 })
8487 .collect();
8488 terms.sort();
8489 terms.dedup();
8490 terms
8491 },
8492 )
8493 }
8494
8495 proptest! {
8496 #[test]
8497 fn prop_separator_keyed_section_answers_like_verbatim(
8498 terms in term_set(),
8499 extra in prop::collection::vec(any::<String>(), 0..8),
8500 ) {
8501 let raw = chunked_section_of(&terms);
8502 let sep = encode_chunked_dict_section(&raw, CODEC_NONE);
8503 let ver = verbatim_keyed_twin(&sep, CODEC_NONE);
8504 prop_assert!(sep.len() <= ver.len());
8505
8506 let (_m, es) = parse_chunked_dict_dir(&sep, sep.len() as u64).unwrap();
8508 let bounds = chunk_bounds_of(&sep, CODEC_NONE);
8509 prop_assert!(es[0].key.is_empty());
8510 for i in 1..es.len() {
8511 prop_assert!(es[i].key.as_slice() > bounds[i - 1].1.as_slice());
8512 prop_assert!(es[i].key <= bounds[i].0);
8513 prop_assert!(es[i].key > es[i - 1].key);
8514 }
8515
8516 let sec_s = decode_chunked_dict_section(
8517 &sep,
8518 CODEC_NONE,
8519 crate::chunk_cache::ChunkCache::unlimited_arc(),
8520 0,
8521 )
8522 .unwrap();
8523 let sec_v = decode_chunked_dict_section(
8524 &ver,
8525 CODEC_NONE,
8526 crate::chunk_cache::ChunkCache::unlimited_arc(),
8527 0,
8528 )
8529 .unwrap();
8530
8531 for (i, t) in terms.iter().enumerate() {
8532 prop_assert_eq!(sec_s.id(t), Some(i as u32 + 1));
8533 prop_assert_eq!(sec_v.id(t), Some(i as u32 + 1));
8534 prop_assert_eq!(sec_s.term(i as u32 + 1), sec_v.term(i as u32 + 1));
8535 }
8536
8537 let mut probes: Vec<String> = extra;
8539 probes.push(String::new());
8540 for (first, last) in &bounds {
8541 for base in [first, last] {
8542 if let Ok(s) = std::str::from_utf8(base) {
8543 probes.push(s.to_string());
8544 probes.push(format!("{s}\u{1}"));
8545 probes.push(s[..s.len() - s.chars().next_back()
8546 .map(char::len_utf8).unwrap_or(0)].to_string());
8547 }
8548 }
8549 }
8550 for e in &es {
8551 if let Ok(s) = std::str::from_utf8(&e.key) {
8552 probes.push(s.to_string());
8553 }
8554 }
8555 for p in &probes {
8556 prop_assert_eq!(sec_s.id(p), sec_v.id(p), "probe diverged: {:?}", p);
8557 }
8558 }
8559 }
8560 }
8561
8562 #[test]
8567 fn text_index_is_tamper_evident_and_verifies() {
8568 let triples: Vec<(String, String, String)> = vec![(
8569 "<http://ex/s0>".to_string(),
8570 "<http://ex/label>".to_string(),
8571 "\"alpha glucose phosphate\"".to_string(),
8572 )];
8573 let bytes = build_text_indexed(&triples);
8574 let header = Rete::open(&bytes).unwrap().header().clone();
8575 assert!(header.text_index_len > 0);
8576 assert!(verify(&bytes).unwrap(), "a text-indexed build must verify");
8577
8578 let mut tampered = bytes.clone();
8579 tampered[header.text_index_offset as usize] ^= 0xff;
8580 assert!(
8581 !verify(&tampered).unwrap(),
8582 "tampering with the text index must break verify()"
8583 );
8584 }
8585
8586 #[test]
8590 fn synopsis_cuts_remote_fetch_bytes() {
8591 use crate::header::FLAG_TILE_SYNOPSIS;
8592 use crate::reader::{CountingReader, SliceReader};
8593
8594 let triples: Vec<(String, String, String)> = (0..400u32)
8598 .map(|i| {
8599 (
8600 format!("<http://ex/s/{i:04}>"),
8601 "<http://ex/p>".to_string(),
8602 format!("<http://ex/o/{i:04}>"),
8603 )
8604 })
8605 .collect();
8606 let mut db = DictionaryBuilder::new();
8607 for (s, p, o) in &triples {
8608 db.observe(s, p, o);
8609 }
8610 let dict = db.build();
8611 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
8612 for (s, p, o) in &triples {
8613 ib.push(dict.encode(s, p, o).unwrap());
8614 }
8615 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8616
8617 let q = (Some("<http://ex/s/0395>"), None, Some("<http://ex/o/0005>"));
8621 let query_bytes = |image: &[u8]| -> (u64, usize) {
8625 let leaked: &'static [u8] = Box::leak(image.to_vec().into_boxed_slice());
8626 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8627 let rete = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
8628 let before = reader.bytes_read(); let n = rete.query(q.0, q.1, q.2).len();
8630 assert!(!rete.index_incomplete());
8631 (reader.bytes_read() - before, n)
8632 };
8633
8634 let (on_bytes, on_n) = query_bytes(&bytes);
8635 let mut off = bytes.clone();
8637 off[5] &= !FLAG_TILE_SYNOPSIS;
8638 let (off_bytes, off_n) = query_bytes(&off);
8639
8640 assert_eq!(on_n, 0, "the pair never co-occurs");
8641 assert_eq!(off_n, 0, "same answer without the synopsis");
8642 assert!(
8646 on_bytes < off_bytes,
8647 "synopsis skips the routed tile fetch: {on_bytes} < {off_bytes}"
8648 );
8649 }
8650
8651 #[test]
8657 fn double_bound_object_join_eager_matches_lazy() {
8658 use crate::reader::SliceReader;
8659 let occ = "<http://ex/occ>";
8660 let phys = "<http://ex/physicist>";
8661 let phil = "<http://ex/philosopher>";
8662 let label = "<http://www.w3.org/2000/01/rdf-schema#label>";
8663 let mut triples: Vec<(String, String, String)> = Vec::new();
8665 for i in 0..20u32 {
8666 triples.push((format!("<http://ex/p/{i:02}>"), occ.into(), phys.into()));
8667 if i < 10 {
8668 triples.push((format!("<http://ex/p/{i:02}>"), occ.into(), phil.into()));
8669 }
8670 triples.push((
8671 format!("<http://ex/p/{i:02}>"),
8672 label.into(),
8673 format!("\"Name {i:02}\""),
8674 ));
8675 }
8676 let mut db = DictionaryBuilder::new();
8677 for (s, p, o) in &triples {
8678 db.observe(s, p, o);
8679 }
8680 let dict = db.build();
8681 let mut ib = GraphIndexBuilder::new().with_tile_budget(16);
8682 for (s, p, o) in &triples {
8683 ib.push(dict.encode(s, p, o).unwrap());
8684 }
8685 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8686
8687 let q = "SELECT ?l WHERE { \
8688 ?p <http://ex/occ> <http://ex/physicist> ; \
8689 <http://ex/occ> <http://ex/philosopher> ; \
8690 <http://www.w3.org/2000/01/rdf-schema#label> ?l }";
8691 let run = |rete: &Rete| -> Vec<String> {
8692 let (_, sols) = crate::eval_sparql(rete, q).unwrap();
8693 let mut v: Vec<String> = sols.iter().filter_map(|b| b.get("l").cloned()).collect();
8694 v.sort();
8695 v
8696 };
8697
8698 let eager_rows = run(&Rete::open(&bytes).unwrap());
8699 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8700 let lazy = Rete::open_ranged_lazy_with(
8701 std::sync::Arc::new(SliceReader::new(leaked)),
8702 HEADER_LEN as u64,
8703 )
8704 .unwrap();
8705 let lazy_rows = run(&lazy);
8706 assert!(!lazy.index_incomplete());
8707
8708 assert_eq!(eager_rows.len(), 10, "the 10 physicist∩philosopher labels");
8709 assert_eq!(eager_rows, lazy_rows, "eager and lazy must agree exactly");
8710 }
8711
8712 #[test]
8716 fn multi_chunk_dictionary_round_trips() {
8717 let mut db = DictionaryBuilder::new();
8718 let term = |i: u32| format!("<http://example.org/some/long/prefix/entity/{i:06}>");
8719 for i in 0..6000u32 {
8720 db.observe(&term(i), "<http://ex/p>", &term(i + 1));
8721 }
8722 let dict = db.build();
8723 let mut ib = GraphIndexBuilder::new();
8724 for i in 0..6000u32 {
8725 ib.push(
8726 dict.encode(&term(i), "<http://ex/p>", &term(i + 1))
8727 .unwrap(),
8728 );
8729 }
8730 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8731 let rete = Rete::open(&bytes).unwrap();
8732 let d = rete.dictionary();
8733 assert_eq!(d.term_count(), dict.term_count());
8734 for i in (0..6000).step_by(97).chain([0, 1, 5999, 6000]) {
8735 let t = term(i);
8736 let sid = dict.subject_id(&t);
8737 assert_eq!(d.subject_id(&t), sid, "subject_id({t})");
8738 if let Some(id) = sid {
8739 assert_eq!(d.subject_term(id).as_deref(), Some(t.as_str()));
8740 }
8741 let oid = dict.object_id(&t);
8742 assert_eq!(d.object_id(&t), oid, "object_id({t})");
8743 }
8744 assert_eq!(d.subject_id("<http://example.org/absent>"), None);
8745 assert_eq!(d.predicate_id("<http://ex/p>"), Some(1));
8746 assert_eq!(d.predicate_term(1).as_deref(), Some("<http://ex/p>"));
8747 }
8748
8749 #[test]
8750 #[cfg(feature = "compression")]
8751 fn compression_shrinks_repetitive_data() {
8752 let mut db = DictionaryBuilder::new();
8756 let triples: Vec<(String, String, String)> = (0..500)
8757 .map(|i| {
8758 (
8759 format!("<http://example.org/entity/{i}>"),
8760 "<http://example.org/p/relatedTo>".to_string(),
8761 format!("<http://example.org/entity/{}>", (i + 1) % 500),
8762 )
8763 })
8764 .collect();
8765 for (s, p, o) in &triples {
8766 db.observe(s, p, o);
8767 }
8768 let dict = db.build();
8769 let mut ib = GraphIndexBuilder::new();
8770 for (s, p, o) in &triples {
8771 ib.push(dict.encode(s, p, o).unwrap());
8772 }
8773 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8774
8775 let raw: usize = triples
8776 .iter()
8777 .map(|(s, p, o)| s.len() + p.len() + o.len())
8778 .sum();
8779 assert!(
8780 bytes.len() < raw / 2,
8781 "expected strong compression: file {} vs raw terms {raw}",
8782 bytes.len()
8783 );
8784
8785 let rete = Rete::open(&bytes).unwrap();
8787 let r = rete.query(Some("<http://example.org/entity/0>"), None, None);
8788 assert_eq!(r.len(), 1);
8789 assert_eq!(r[0].2, "<http://example.org/entity/1>");
8790 }
8791
8792 fn big_file_with_pyramid() -> Vec<u8> {
8793 let triples: Vec<(String, String, String)> = (0..300)
8795 .map(|i| {
8796 (
8797 format!("<http://ex/e{i}>"),
8798 "<http://ex/next>".to_string(),
8799 format!("<http://ex/e{}>", (i + 1) % 300),
8800 )
8801 })
8802 .collect();
8803 let mut db = DictionaryBuilder::new();
8804 for (s, p, o) in &triples {
8805 db.observe(s, p, o);
8806 }
8807 let dict = db.build();
8808 let ids: Vec<_> = triples
8809 .iter()
8810 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
8811 .collect();
8812 let mut ib = GraphIndexBuilder::new();
8813 for &t in &ids {
8814 ib.push(t);
8815 }
8816 let (meta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
8817 write_file(&dict, &ib.build(), false, &meta, levels)
8818 }
8819
8820 #[test]
8821 fn ranged_open_is_minimal_and_correct() {
8822 use crate::reader::{CountingReader, SliceReader};
8823 let bytes = big_file_with_pyramid();
8824
8825 let full = CountingReader::new(SliceReader::new(&bytes));
8826 let rete = Rete::open_ranged(&full).unwrap();
8827 assert!(full.requests() <= 4, "requests = {}", full.requests());
8829 assert_eq!(
8830 rete.query(Some("<http://ex/e0>"), None, None)[0].2,
8831 "<http://ex/e1>"
8832 );
8833
8834 let summ_reader = CountingReader::new(SliceReader::new(&bytes));
8836 let view = SummaryView::open_ranged(&summ_reader).unwrap().unwrap();
8837 assert!(!view.summary.is_empty());
8838 assert!(
8839 summ_reader.bytes_read() < bytes.len() as u64,
8840 "summary read {} of {} bytes",
8841 summ_reader.bytes_read(),
8842 bytes.len()
8843 );
8844 assert!(summ_reader.bytes_read() < full.bytes_read());
8846 }
8847
8848 #[test]
8849 fn content_hash_is_set_and_verifies() {
8850 let bytes = build_image();
8851 let rete = Rete::open(&bytes).unwrap();
8852 assert_ne!(
8853 rete.header().content_hash,
8854 [0u8; 16],
8855 "hash must be populated"
8856 );
8857 assert!(verify(&bytes).unwrap(), "freshly built file verifies");
8858 assert!(
8859 verify_ranged(&crate::reader::SliceReader::new(&bytes)).unwrap(),
8860 "the streaming verify agrees"
8861 );
8862
8863 assert_eq!(
8865 Rete::open(&build_image()).unwrap().header().content_hash,
8866 rete.header().content_hash
8867 );
8868
8869 let mut tampered = bytes.clone();
8871 let last = tampered.len() - 5; tampered[last] ^= 0xff;
8873 assert!(!verify(&tampered).unwrap());
8874 assert!(!verify_ranged(&crate::reader::SliceReader::new(&tampered)).unwrap());
8875 let short = &bytes[..bytes.len() / 2];
8877 assert!(verify_ranged(&crate::reader::SliceReader::new(short)).is_err());
8878 assert!(verify_ranged(&crate::reader::SliceReader::new(&bytes[..10])).is_err());
8879 }
8880
8881 fn build_with_metadata(meta: &[u8]) -> Vec<u8> {
8883 let triples = [
8884 ("Alice", "knows", "Bob"),
8885 ("Bob", "knows", "Carol"),
8886 ("Alice", "age", "30"),
8887 ];
8888 let mut db = DictionaryBuilder::new();
8889 for (s, p, o) in triples {
8890 db.observe(s, p, o);
8891 }
8892 let dict = db.build();
8893 let ids: Vec<_> = triples
8894 .iter()
8895 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
8896 .collect();
8897 let mut ib = GraphIndexBuilder::new();
8898 for &t in &ids {
8899 ib.push(t);
8900 }
8901 let (pmeta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
8902 write_dataset_with_metadata(&dict, &ib.build(), &[], false, &pmeta, levels, meta, &[])
8903 }
8904
8905 #[test]
8906 fn metadata_round_trips_and_shifts_offsets() {
8907 let card = br#"{"title":"My Dataset"}"#;
8908 let bytes = build_with_metadata(card);
8909 let rete = Rete::open(&bytes).unwrap();
8910
8911 assert_eq!(rete.metadata(), Some(card.as_slice()));
8913 let h = rete.header();
8914 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
8915 assert_eq!(h.metadata_len, card.len() as u64);
8916 assert_eq!(h.dictionary_offset, HEADER_LEN as u64 + card.len() as u64);
8918
8919 assert_eq!(
8921 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
8922 1
8923 );
8924 assert!(verify(&bytes).unwrap());
8926 }
8927
8928 #[test]
8929 fn empty_metadata_is_byte_identical_to_plain_writer() {
8930 assert_eq!(
8933 build_with_metadata(&[]),
8934 build_image(),
8935 "empty-metadata output must equal the plain writer byte-for-byte"
8936 );
8937 }
8938
8939 #[test]
8944 fn replace_metadata_matches_a_direct_write() {
8945 let first = br#"{"title":"My Dataset","queries":[1,2,3]}"#;
8946 let bytes = build_with_metadata(first);
8947 for target in [
8948 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(), ] {
8953 let spliced = replace_metadata(&bytes, target).unwrap();
8954 assert_eq!(
8955 spliced,
8956 build_with_metadata(target),
8957 "splicing {} bytes of metadata must equal a direct write",
8958 target.len()
8959 );
8960 assert!(
8961 verify(&spliced).unwrap(),
8962 "the new hash covers the new card"
8963 );
8964 let rete = Rete::open(&spliced).unwrap();
8965 assert_eq!(
8966 rete.metadata(),
8967 (!target.is_empty()).then_some(target),
8968 "the new payload reads back verbatim"
8969 );
8970 assert_eq!(
8972 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
8973 1
8974 );
8975 }
8976 }
8977
8978 #[test]
8981 fn replace_metadata_keeps_build_info_adjacent() {
8982 let with_info =
8983 attach_build_info(&build_with_metadata(br#"{"a":1}"#), br#"{"builder":"x"}"#).unwrap();
8984 let spliced = replace_metadata(&with_info, br#"{"a":1,"b":2}"#).unwrap();
8985 let h = Header::from_bytes(&spliced).unwrap();
8986 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
8987 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + h.metadata_len);
8988 assert_eq!(
8989 read_build_info(&spliced).unwrap().as_deref(),
8990 Some(br#"{"builder":"x"}"#.as_slice())
8991 );
8992 assert!(verify(&spliced).unwrap());
8993 assert_eq!(
8995 h.content_hash,
8996 Header::from_bytes(&build_with_metadata(br#"{"a":1,"b":2}"#))
8997 .unwrap()
8998 .content_hash
8999 );
9000 }
9001
9002 #[test]
9003 fn metadata_is_tamper_evident() {
9004 let card = br#"{"title":"x"}"#;
9005 let mut bytes = build_with_metadata(card);
9006 assert!(verify(&bytes).unwrap());
9007 bytes[HEADER_LEN + 2] ^= 0xff;
9009 assert!(
9010 !verify(&bytes).unwrap(),
9011 "tampering with the card must break verify()"
9012 );
9013 }
9014
9015 #[test]
9016 fn ranged_opens_do_not_fetch_metadata() {
9017 use crate::reader::{CountingReader, SliceReader};
9018 let card = vec![0xABu8; 512]; let bytes = build_with_metadata(&card);
9020 let total = bytes.len() as u64;
9021
9022 let r = CountingReader::new(SliceReader::new(&bytes));
9024 let rete = Rete::open_ranged(&r).unwrap();
9025 assert!(
9026 rete.metadata().is_none(),
9027 "open_ranged must not load the card"
9028 );
9029 assert!(r.requests() <= 4, "requests = {}", r.requests());
9030 assert!(
9031 r.bytes_read() <= total - card.len() as u64,
9032 "read {} of {} bytes; the {}-byte card must be skipped",
9033 r.bytes_read(),
9034 total,
9035 card.len()
9036 );
9037
9038 let rs = CountingReader::new(SliceReader::new(&bytes));
9040 let view = SummaryView::open_ranged(&rs).unwrap().unwrap();
9041 assert!(!view.summary.is_empty());
9042 assert!(rs.bytes_read() <= total - card.len() as u64);
9043 }
9044
9045 #[test]
9046 fn metadata_ranged_fetches_only_header_and_card() {
9047 use crate::reader::{CountingReader, SliceReader};
9048 let card = vec![0xCDu8; 384];
9051 let bytes = build_with_metadata(&card);
9052
9053 let r = CountingReader::new(SliceReader::new(&bytes));
9054 let got = read_metadata_ranged(&r).unwrap().unwrap();
9055 assert_eq!(got, card, "the card reads back verbatim");
9056 assert_eq!(r.requests(), 2, "exactly header + metadata ranges");
9057 assert_eq!(
9058 r.bytes_read(),
9059 HEADER_LEN as u64 + card.len() as u64,
9060 "no dictionary/index/pyramid bytes are touched"
9061 );
9062
9063 let plain = build_image();
9065 let rp = CountingReader::new(SliceReader::new(&plain));
9066 assert!(read_metadata_ranged(&rp).unwrap().is_none());
9067 assert_eq!(rp.requests(), 1, "header only for a cardless file");
9068 assert_eq!(rp.bytes_read(), HEADER_LEN as u64);
9069 }
9070
9071 #[test]
9072 fn build_info_attaches_outside_the_hash() {
9073 let card = br#"{"title":"My Dataset"}"#;
9074 let base = build_with_metadata(card);
9075 let base_hash = Header::from_bytes(&base).unwrap().content_hash;
9076
9077 let info = br#"{"built_at":"2026-08-04T00:00:00Z","builder":"rete-cli 0.3.2"}"#;
9078 let with = attach_build_info(&base, info).unwrap();
9079
9080 assert_eq!(read_build_info(&with).unwrap().unwrap(), info);
9082 let h = Header::from_bytes(&with).unwrap();
9083 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + card.len() as u64);
9084 assert_eq!(h.build_info_len, info.len() as u64);
9085 assert_eq!(h.expected_file_len(), Some(with.len() as u64));
9086
9087 assert_eq!(h.content_hash, base_hash);
9091 assert!(verify(&with).unwrap());
9092 let other = attach_build_info(&base, br#"{"built_at":"1999-01-01T00:00:00Z"}"#).unwrap();
9093 assert_eq!(Header::from_bytes(&other).unwrap().content_hash, base_hash);
9094
9095 let rete = Rete::open(&with).unwrap();
9097 assert_eq!(
9098 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
9099 1
9100 );
9101 assert_eq!(rete.metadata(), Some(card.as_slice()));
9102
9103 let replaced = attach_build_info(&with, br#"{"builder":"other"}"#).unwrap();
9106 assert_eq!(
9107 read_build_info(&replaced).unwrap().unwrap(),
9108 br#"{"builder":"other"}"#
9109 );
9110 assert!(verify(&replaced).unwrap());
9111 let stripped = attach_build_info(&replaced, &[]).unwrap();
9112 assert_eq!(stripped, base, "stripping build-info restores the image");
9113 }
9114
9115 #[test]
9116 fn build_info_attaches_on_a_metadata_free_file() {
9117 let base = build_image();
9119 let info = b"{\"builder\":\"x\"}";
9120 let with = attach_build_info(&base, info).unwrap();
9121 let h = Header::from_bytes(&with).unwrap();
9122 assert_eq!(h.build_info_offset, HEADER_LEN as u64);
9123 assert_eq!(h.metadata_len, 0);
9124 assert_eq!(read_build_info(&with).unwrap().unwrap(), info);
9125 assert!(verify(&with).unwrap());
9126 assert_eq!(
9127 h.content_hash,
9128 Header::from_bytes(&base).unwrap().content_hash
9129 );
9130 Rete::open(&with).unwrap();
9131 }
9132
9133 #[test]
9140 fn a_plan_rebuilds_exactly_what_the_in_memory_splice_produces() {
9141 let card = br#"{"title":"My Dataset"}"#;
9142 let base = build_with_metadata(card);
9143 let base_hash = Header::from_bytes(&base).unwrap().content_hash;
9144 for start in [
9145 base.clone(),
9146 attach_build_info(&base, b"{\"a\":1}").unwrap(),
9147 ] {
9148 for info in [
9149 br#"{"builder":"rete-cli 0.3.2","query_costs":{}}"#.as_slice(), b"{}".as_slice(), b"".as_slice(), ] {
9153 let plan = plan_build_info(&start, start.len() as u64, info.len() as u64).unwrap();
9154 let mut streamed = Vec::new();
9155 streamed.extend_from_slice(&plan.header);
9156 streamed.extend_from_slice(&start[HEADER_LEN..plan.insert as usize]);
9157 streamed.extend_from_slice(info);
9158 streamed.extend_from_slice(&start[plan.tail_start as usize..]);
9159 assert_eq!(streamed.len() as u64, plan.new_len);
9160 assert_eq!(
9161 streamed,
9162 attach_build_info(&start, info).unwrap(),
9163 "a {}-byte section streamed must equal the same section spliced",
9164 info.len()
9165 );
9166 assert!(verify(&streamed).unwrap());
9168 let h = Header::from_bytes(&streamed).unwrap();
9169 assert_eq!(h.content_hash, base_hash);
9170 assert_eq!(h.expected_file_len(), Some(plan.new_len));
9171 let rete = Rete::open(&streamed).unwrap();
9172 assert_eq!(rete.metadata(), Some(card.as_slice()));
9173 assert_eq!(
9174 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
9175 1
9176 );
9177 }
9178 }
9179 }
9180
9181 #[test]
9184 fn a_plan_refuses_a_header_that_overruns_the_file() {
9185 let base = build_with_metadata(br#"{"title":"x"}"#);
9186 assert!(plan_build_info(&base, 8, 16).is_err());
9187 assert!(plan_build_info(&base[..HEADER_LEN], base.len() as u64, 16).is_ok());
9188 }
9189
9190 #[test]
9191 fn card_and_build_info_ranged_is_one_header_plus_one_range() {
9192 use crate::reader::{CountingReader, SliceReader};
9193 let card = vec![0xCDu8; 384];
9194 let info = vec![0xEFu8; 200];
9195 let bytes = attach_build_info(&build_with_metadata(&card), &info).unwrap();
9196
9197 let r = CountingReader::new(SliceReader::new(&bytes));
9200 let (m, b) = read_card_and_build_info_ranged(&r).unwrap();
9201 assert_eq!(m.unwrap(), card);
9202 assert_eq!(b.unwrap(), info);
9203 assert_eq!(r.requests(), 2, "header + one coalesced range");
9204 assert_eq!(
9205 r.bytes_read(),
9206 HEADER_LEN as u64 + card.len() as u64 + info.len() as u64
9207 );
9208
9209 let plain = build_with_metadata(&card);
9211 let rp = CountingReader::new(SliceReader::new(&plain));
9212 let (m, b) = read_card_and_build_info_ranged(&rp).unwrap();
9213 assert_eq!(m.unwrap(), card);
9214 assert!(b.is_none());
9215 assert_eq!(rp.requests(), 2);
9216 assert_eq!(
9221 r.requests(),
9222 rp.requests(),
9223 "build info must not cost an extra request",
9224 );
9225
9226 let none = build_image();
9228 let rn = CountingReader::new(SliceReader::new(&none));
9229 let (m, b) = read_card_and_build_info_ranged(&rn).unwrap();
9230 assert!(m.is_none() && b.is_none());
9231 assert_eq!(rn.requests(), 1);
9232 }
9233
9234 #[test]
9235 fn schema_summary_groups_by_type() {
9236 let rt = RDF_TYPE;
9237 let bytes = build_from(&[
9238 ("Alice", rt, "Person"),
9239 ("Bob", rt, "Person"),
9240 ("NYC", rt, "City"),
9241 ("Alice", "knows", "Bob"),
9242 ("Alice", "livesIn", "NYC"),
9243 ("Alice", "name", "\"Alice\""),
9244 ]);
9245 let rete = Rete::open(&bytes).unwrap();
9246 let summary = schema_summary(&rete);
9247 assert!(summary.contains(&("Person".into(), "knows".into(), "Person".into(), 1)));
9249 assert!(summary.contains(&("Person".into(), "livesIn".into(), "City".into(), 1)));
9250 assert!(summary.contains(&("Person".into(), "name".into(), "(literal)".into(), 1)));
9251 assert!(!summary.iter().any(|(_, p, _, _)| p == RDF_TYPE));
9253
9254 let classes = schema_classes(&rete);
9256 assert_eq!(
9257 classes,
9258 vec![("Person".into(), 2u32), ("City".into(), 1u32)]
9259 );
9260 }
9261
9262 fn build_from(triples: &[(&str, &str, &str)]) -> Vec<u8> {
9263 let mut db = DictionaryBuilder::new();
9264 for (s, p, o) in triples {
9265 db.observe(s, p, o);
9266 }
9267 let dict = db.build();
9268 let mut ib = GraphIndexBuilder::new();
9269 for (s, p, o) in triples {
9270 ib.push(dict.encode(s, p, o).unwrap());
9271 }
9272 write_file(&dict, &ib.build(), false, &[], 0)
9273 }
9274
9275 fn build_with_pyramid(triples: &[(&str, &str, &str)]) -> Vec<u8> {
9277 let mut db = DictionaryBuilder::new();
9278 for (s, p, o) in triples {
9279 db.observe(s, p, o);
9280 }
9281 let dict = db.build();
9282 let encoded: Vec<_> = triples
9283 .iter()
9284 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
9285 .collect();
9286 let mut ib = GraphIndexBuilder::new();
9287 for t in &encoded {
9288 ib.push(*t);
9289 }
9290 let (meta, levels) = build_pyramid_meta(&dict, &encoded, DEFAULT_TILE_BUDGET);
9291 write_dataset(&dict, &ib.build(), &[], false, &meta, levels)
9292 }
9293
9294 #[test]
9295 fn tbox_coherence_flags_unsatisfiable_class_index_free() {
9296 use crate::reader::{CountingReader, SliceReader};
9297 let rt = RDF_TYPE;
9298 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
9299 let disj = "<http://www.w3.org/2002/07/owl#disjointWith>";
9300 let bytes = build_with_pyramid(&[
9303 ("<http://ex/C>", sub, "<http://ex/D>"),
9304 ("<http://ex/C>", sub, "<http://ex/E>"),
9305 ("<http://ex/D>", disj, "<http://ex/E>"),
9306 ("<http://ex/x>", rt, "<http://ex/C>"),
9307 ]);
9308
9309 let r = CountingReader::new(SliceReader::new(&bytes));
9310 let view = SummaryView::open_ranged(&r).unwrap().unwrap();
9311 let points = view.tbox_coherence();
9312 assert!(
9313 points
9314 .iter()
9315 .any(|i| i.kind == "unsatisfiable-class" && i.detail.contains("http://ex/C>")),
9316 "expected C unsatisfiable from the schema alone, got {points:?}"
9317 );
9318
9319 let header = Header::from_bytes(&bytes[..HEADER_LEN]).unwrap();
9322 assert!(
9323 r.bytes_read() <= bytes.len() as u64 - header.root_dir_len,
9324 "tbox_coherence must not read the triple index"
9325 );
9326 }
9327
9328 #[test]
9329 fn schema_coherence_reads_only_the_schema_block() {
9330 use crate::reader::{CountingReader, SliceReader};
9331 let rt = RDF_TYPE;
9332 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
9333 let disj = "<http://www.w3.org/2002/07/owl#disjointWith>";
9334 let mut triples: Vec<(String, String, String)> = vec![
9338 ("<http://ex/C>".into(), sub.into(), "<http://ex/D>".into()),
9339 ("<http://ex/C>".into(), sub.into(), "<http://ex/E>".into()),
9340 ("<http://ex/D>".into(), disj.into(), "<http://ex/E>".into()),
9341 ];
9342 for i in 0..500 {
9343 let s = format!("<http://ex/x{i}>");
9344 triples.push((s.clone(), rt.into(), "<http://ex/C>".into()));
9345 triples.push((
9346 s,
9347 "<http://ex/label>".into(),
9348 format!("\"unique label {i}\""),
9349 ));
9350 }
9351 let trefs: Vec<(&str, &str, &str)> = triples
9352 .iter()
9353 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
9354 .collect();
9355 let bytes = build_with_pyramid(&trefs);
9356
9357 let header = Header::from_bytes(&bytes[..HEADER_LEN]).unwrap();
9358 assert!(
9359 header.schema_meta_len > 0,
9360 "the writer recorded a schema-block length"
9361 );
9362 assert!(
9363 (header.schema_meta_len as u64) < header.pyramid_meta_len,
9364 "schema block ({}) should be far smaller than the whole pyramid-meta ({})",
9365 header.schema_meta_len,
9366 header.pyramid_meta_len
9367 );
9368
9369 let r = CountingReader::new(SliceReader::new(&bytes));
9370 let points = read_schema_coherence_ranged(&r).unwrap().unwrap();
9371 assert!(points.iter().any(|i| i.kind == "unsatisfiable-class"));
9372 assert!(
9374 r.bytes_read() <= HEADER_LEN as u64 + header.schema_meta_len as u64,
9375 "read {} bytes; expected <= header + schema block ({})",
9376 r.bytes_read(),
9377 HEADER_LEN as u64 + header.schema_meta_len as u64
9378 );
9379 }
9380
9381 #[test]
9382 fn tbox_coherence_clean_schema_is_coherent() {
9383 use crate::reader::SliceReader;
9384 let rt = RDF_TYPE;
9385 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
9386 let bytes = build_with_pyramid(&[
9387 ("<http://ex/Dog>", sub, "<http://ex/Animal>"),
9388 ("<http://ex/x>", rt, "<http://ex/Dog>"),
9389 ]);
9390 let view = SummaryView::open_ranged(&SliceReader::new(&bytes))
9391 .unwrap()
9392 .unwrap();
9393 assert!(view.tbox_is_coherent(), "a plain hierarchy is coherent");
9394 }
9395
9396 #[test]
9404 fn readers_before_the_quad_index_refuse_its_files() {
9405 for perms in [PermSet::ALL, PermSet::CORE] {
9406 let quads: Vec<crate::ingest::RawQuad> = (0..50)
9407 .map(|i| {
9408 (
9409 format!("<http://ex/s{i}>"),
9410 "<http://ex/p>".to_string(),
9411 format!("\"{i}\""),
9412 Some(format!("<http://ex/g{}>", i % 7)),
9413 )
9414 })
9415 .collect();
9416 let (bytes, _) = crate::ingest::assemble_dataset_with_layout(
9417 quads,
9418 false,
9419 false,
9420 None,
9421 crate::PyramidAlgo::Louvain,
9422 perms,
9423 crate::ingest::GraphLayout::Quad,
9424 |_, _| Vec::<u8>::new(),
9425 );
9426 let h = Header::from_bytes(&bytes).unwrap();
9427 assert!(h.has_quad_index());
9428 let root =
9429 &bytes[h.root_dir_offset as usize..(h.root_dir_offset + h.root_dir_len) as usize];
9430 assert!(decode_index_container(root, h.block_codec, h.perms).is_err());
9432 assert!(
9433 container_section_payload_ranges(root, h.root_dir_offset, h.perms.len()).is_err()
9434 );
9435 assert_ne!(decode_container(root, CODEC_NONE).unwrap().len(), NUM_PERMS);
9437 assert_ne!(
9438 decode_container(root, CODEC_NONE).unwrap().len(),
9439 h.perms.len()
9440 );
9441 let r = crate::reader::SliceReader::new(&bytes);
9443 for expect in [h.perms.len(), NUM_PERMS] {
9444 assert!(locate_container_section_ranged(
9445 &r,
9446 h.root_dir_offset,
9447 h.root_dir_len,
9448 0,
9449 expect as u64
9450 )
9451 .is_err());
9452 }
9453 assert_eq!(hash_of_sections(&h, &bytes).unwrap(), h.content_hash);
9455 let rete = Rete::open(&bytes).unwrap();
9457 assert_eq!(rete.named_graph_count(), 7);
9458 assert_eq!(rete.dump(Some("<http://ex/g3>")).len(), 7);
9459 }
9460 }
9461
9462 #[test]
9463 fn named_graphs_round_trip() {
9464 let all = [
9466 ("Alice", "knows", "Bob"), ("Bob", "age", "30"), ];
9469 let mut db = DictionaryBuilder::new();
9470 for (s, p, o) in all {
9471 db.observe(s, p, o);
9472 }
9473 let dict = db.build();
9474
9475 let mut def = GraphIndexBuilder::new();
9476 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
9477 let mut g1 = GraphIndexBuilder::new();
9478 g1.push(dict.encode("Bob", "age", "30").unwrap());
9479
9480 let named = vec![("http://ex/g1".to_string(), g1.build())];
9481 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9482
9483 assert!(verify(&bytes).unwrap());
9484 let rete = Rete::open(&bytes).unwrap();
9485 assert_eq!(rete.graph_names(), vec!["http://ex/g1"]);
9486 let gi = rete.graph_index("http://ex/g1").unwrap();
9488 assert_eq!(gi.triple_count(), 1);
9489 assert!(rete.graph_index("http://ex/missing").is_none());
9490
9491 assert_eq!(rete.header().quad_count, 2);
9494
9495 assert_eq!(rete.query(Some("Alice"), None, None).len(), 1);
9497
9498 assert_eq!(
9500 rete.dump(None),
9501 vec![("Alice".into(), "knows".into(), "Bob".into())]
9502 );
9503 assert_eq!(
9504 rete.dump(Some("http://ex/g1")),
9505 vec![("Bob".into(), "age".into(), "30".into())]
9506 );
9507 }
9508
9509 #[test]
9515 fn named_graph_container_opens_tile_lazily_like_the_root() {
9516 let mut db = DictionaryBuilder::new();
9517 let node = |n: u32| format!("<http://ex/n{n}>");
9518 let p = "<http://ex/p>".to_string();
9519 db.observe(&node(0), &p, &node(1));
9520 for i in 0..500u32 {
9521 db.observe(&node(1000 + i), &p, &node(2000 + i));
9522 }
9523 let dict = db.build();
9524 let mut def = GraphIndexBuilder::new();
9525 def.push(dict.encode(&node(0), &p, &node(1)).unwrap());
9526 let mut g = GraphIndexBuilder::new().with_tile_budget(256); for i in 0..500u32 {
9528 g.push(dict.encode(&node(1000 + i), &p, &node(2000 + i)).unwrap());
9529 }
9530 let named = vec![("<http://ex/g>".to_string(), g.build())];
9531 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9532 let rete = Rete::open(&bytes).unwrap();
9533 let header = rete.header();
9534
9535 let soff = header.named_graphs_offset as usize;
9538 let send = soff + header.named_graphs_len as usize;
9539 let sec = &bytes[soff..send];
9540 let (n, mut pos) = read_uvarint(sec).unwrap();
9541 assert_eq!(n, 1);
9542 let (ilen, u1) = read_uvarint(&sec[pos..]).unwrap();
9543 pos += u1 + ilen as usize;
9544 let (clen, u2) = read_uvarint(&sec[pos..]).unwrap();
9545 pos += u2;
9546 let container = ByteRange {
9547 offset: (soff + pos) as u64,
9548 len: clen,
9549 };
9550
9551 use crate::reader::SliceReader;
9552 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
9553 let reader: std::sync::Arc<dyn RangeReader + Send + Sync> =
9554 std::sync::Arc::new(SliceReader::new(leaked));
9555 let (lazy_idx, _, _) = open_index_container_lazy(
9556 &reader,
9557 container,
9558 header.block_codec,
9559 header.has_tile_synopsis(),
9560 1,
9561 header.perms,
9562 header.perms.len(),
9563 )
9564 .unwrap();
9565 let mut want = rete
9566 .graph_index("<http://ex/g>")
9567 .unwrap()
9568 .match_pattern((None, None, None));
9569 let mut got = lazy_idx.match_pattern((None, None, None));
9570 want.sort_unstable();
9571 got.sort_unstable();
9572 assert_eq!(got.len(), 500);
9573 assert_eq!(got, want);
9574 assert!(!lazy_idx.load_incomplete());
9575 }
9576
9577 #[test]
9583 fn dump_iter_matches_dump_and_stops_early() {
9584 use crate::reader::{CountingReader, SliceReader};
9585
9586 let triples: Vec<(String, String, String)> = (0..2000u32)
9588 .map(|i| {
9589 (
9590 format!("<http://ex/s/{i:04}>"),
9591 "<http://ex/p>".to_string(),
9592 format!("<http://ex/o/{i:04}>"),
9593 )
9594 })
9595 .collect();
9596 let mut db = DictionaryBuilder::new();
9597 for (s, p, o) in &triples {
9598 db.observe(s, p, o);
9599 }
9600 let dict = db.build();
9601 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
9602 for (s, p, o) in &triples {
9603 ib.push(dict.encode(s, p, o).unwrap());
9604 }
9605 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
9606
9607 let rete = Rete::open(&bytes).unwrap();
9609 assert_eq!(rete.dump_iter(None).collect::<Vec<_>>(), rete.dump(None));
9610 assert_eq!(rete.dump_iter(Some("http://ex/nope")).count(), 0);
9612
9613 let read_bytes = |take: usize| -> u64 {
9615 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
9616 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9617 let rete = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
9618 let before = reader.bytes_read();
9619 let n = rete.dump_iter(None).take(take).count();
9620 assert_eq!(n, take.min(triples.len()));
9621 assert!(!rete.index_incomplete());
9622 reader.bytes_read() - before
9623 };
9624 let first_only = read_bytes(1);
9625 let everything = read_bytes(triples.len());
9626 assert!(
9628 first_only < everything,
9629 "dump_iter is not lazy: one triple read {first_only} B, a full scan {everything} B"
9630 );
9631 #[cfg(feature = "compression")]
9638 assert!(
9639 first_only * 2 < everything,
9640 "dump_iter is not lazy: one triple read {first_only} B of the {everything} B a full scan reads"
9641 );
9642
9643 let fresh = Rete::open(&bytes).unwrap();
9648 let drain = |max_quads: usize| {
9653 let (mut out, mut cursor, mut calls) = (Vec::new(), 0u32, 0);
9654 loop {
9655 let (batch, next, done) = fresh.dump_batch(None, cursor, max_quads);
9656 out.extend(batch);
9657 cursor = next;
9658 calls += 1;
9659 assert!(calls < 10_000, "cursor failed to advance");
9660 if done {
9661 break out;
9662 }
9663 }
9664 };
9665 let eager = fresh.dump(None);
9666 for max in [1usize, 7, 128, 100_000] {
9667 assert_eq!(drain(max), eager, "batch size {max} changed the dump");
9668 }
9669 let (t, _, done) = fresh.dump_batch(Some("http://ex/nope"), 0, 16);
9672 assert!(t.is_empty() && done);
9673 }
9674
9675 #[test]
9683 fn query_batch_matches_query_in_graph_and_stops_early() {
9684 use crate::reader::{CountingReader, SliceReader};
9685
9686 let triples: Vec<(String, String, String)> = (0..2000u32)
9689 .map(|i| {
9690 (
9691 format!("<http://ex/s/{:04}>", i / 2),
9692 format!("<http://ex/p/{}>", i % 2),
9693 format!("<http://ex/o/{i:04}>"),
9694 )
9695 })
9696 .collect();
9697 let mut db = DictionaryBuilder::new();
9698 for (s, p, o) in &triples {
9699 db.observe(s, p, o);
9700 }
9701 let dict = db.build();
9702 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
9703 for (s, p, o) in &triples {
9704 ib.push(dict.encode(s, p, o).unwrap());
9705 }
9706 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
9707 let rete = Rete::open(&bytes).unwrap();
9708
9709 let drain = |s: Option<&str>, p: Option<&str>, o: Option<&str>, max: usize| {
9710 let (mut out, mut cursor, mut calls) = (Vec::new(), 0u64, 0);
9711 loop {
9712 let (batch, next, done) = rete.query_batch(None, s, p, o, cursor, max);
9713 assert!(!batch.is_empty() || done, "empty non-final batch");
9716 out.extend(batch);
9717 cursor = next;
9718 calls += 1;
9719 assert!(calls < 20_000, "cursor failed to advance");
9720 if done {
9721 break out;
9722 }
9723 }
9724 };
9725 let shapes: [(Option<&str>, Option<&str>, Option<&str>); 6] = [
9726 (None, None, None),
9727 (Some("<http://ex/s/0007>"), None, None),
9728 (None, Some("<http://ex/p/1>"), None),
9729 (None, None, Some("<http://ex/o/0042>")),
9730 (Some("<http://ex/s/0007>"), Some("<http://ex/p/0>"), None),
9731 (
9732 Some("<http://ex/s/0007>"),
9733 Some("<http://ex/p/0>"),
9734 Some("<http://ex/o/0014>"),
9735 ),
9736 ];
9737 for (s, p, o) in shapes {
9738 let eager = rete.query_in_graph(None, s, p, o);
9739 assert!(
9740 !eager.is_empty(),
9741 "fixture shape {s:?} {p:?} {o:?} matches nothing"
9742 );
9743 assert_eq!(
9745 rete.query_iter(None, s, p, o).collect::<Vec<_>>(),
9746 eager,
9747 "query_iter disagrees for {s:?} {p:?} {o:?}"
9748 );
9749 for max in [1usize, 3, 64, 100_000] {
9750 let mut got = drain(s, p, o, max);
9751 let mut want = eager.clone();
9752 got.sort();
9755 want.sort();
9756 assert_eq!(got, want, "batch size {max} changed {s:?} {p:?} {o:?}");
9757 }
9758 if s.is_none() && p.is_none() && o.is_none() {
9760 assert_eq!(drain(s, p, o, 7), eager, "unbound batching reordered rows");
9761 }
9762 }
9763
9764 let (t, _, done) = rete.query_batch(None, Some("<http://ex/nope>"), None, None, 0, 16);
9767 assert!(t.is_empty() && done);
9768 let (t, _, done) = rete.query_batch(Some("http://ex/nope"), None, None, None, 0, 16);
9769 assert!(t.is_empty() && done);
9770 assert_eq!(
9771 rete.query_iter(None, Some("<http://ex/nope>"), None, None)
9772 .count(),
9773 0
9774 );
9775
9776 let read_bytes = |take: usize| -> u64 {
9779 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
9780 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9781 let rete = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
9782 let before = reader.bytes_read();
9783 let (mut n, mut cursor) = (0usize, 0u64);
9784 loop {
9785 let (batch, next, done) = rete.query_batch(None, None, None, None, cursor, 1);
9786 n += batch.len();
9787 cursor = next;
9788 if done || n >= take {
9789 break;
9790 }
9791 }
9792 assert!(n >= take.min(triples.len()));
9793 assert!(!rete.index_incomplete());
9794 reader.bytes_read() - before
9795 };
9796 let first_only = read_bytes(1);
9797 let everything = read_bytes(triples.len());
9798 assert!(
9799 first_only < everything,
9800 "query_batch is not lazy: one row read {first_only} B, a full scan {everything} B"
9801 );
9802 #[cfg(feature = "compression")]
9803 assert!(
9804 first_only * 2 < everything,
9805 "query_batch is not lazy: one row read {first_only} B of the {everything} B a full scan reads"
9806 );
9807 }
9808
9809 #[test]
9812 fn query_batch_is_graph_scoped() {
9813 let mut db = DictionaryBuilder::new();
9814 for (s, p, o) in [
9815 ("Alice", "knows", "Bob"),
9816 ("Alice", "knows", "Carol"),
9817 ("Alice", "knows", "Dave"),
9818 ("Erin", "knows", "Frank"),
9819 ("Alice", "knows", "Zoe"),
9820 ] {
9821 db.observe(s, p, o);
9822 }
9823 let dict = db.build();
9824 let mut def = GraphIndexBuilder::new();
9825 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
9826 def.push(dict.encode("Alice", "knows", "Carol").unwrap());
9827 let mut g1 = GraphIndexBuilder::new();
9828 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
9829 g1.push(dict.encode("Erin", "knows", "Frank").unwrap());
9830 let mut g2 = GraphIndexBuilder::new();
9831 g2.push(dict.encode("Alice", "knows", "Zoe").unwrap());
9832 let named = vec![
9833 ("http://ex/g1".to_string(), g1.build()),
9834 ("http://ex/g2".to_string(), g2.build()),
9835 ];
9836 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9837 let rete = Rete::open(&bytes).unwrap();
9838 for graph in [None, Some("http://ex/g1"), Some("http://ex/g2")] {
9839 let eager = rete.query_in_graph(graph, None, None, None);
9840 let (mut out, mut cursor) = (Vec::new(), 0u64);
9841 loop {
9842 let (batch, next, done) = rete.query_batch(graph, None, None, None, cursor, 1);
9843 out.extend(batch);
9844 cursor = next;
9845 if done {
9846 break;
9847 }
9848 }
9849 assert_eq!(out, eager, "graph {graph:?} batched differently");
9850 }
9851 }
9852
9853 #[test]
9854 fn query_in_graph_is_graph_scoped() {
9855 let mut db = DictionaryBuilder::new();
9858 for (s, p, o) in [
9859 ("Alice", "knows", "Bob"),
9860 ("Alice", "knows", "Carol"),
9861 ("Alice", "knows", "Dave"),
9862 ] {
9863 db.observe(s, p, o);
9864 }
9865 let dict = db.build();
9866
9867 let mut def = GraphIndexBuilder::new();
9868 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
9869 def.push(dict.encode("Alice", "knows", "Carol").unwrap());
9870 let mut g1 = GraphIndexBuilder::new();
9871 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
9872
9873 let named = vec![("http://ex/g1".to_string(), g1.build())];
9874 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9875 let rete = Rete::open(&bytes).unwrap();
9876
9877 let mut def_objs: Vec<String> = rete
9879 .query_in_graph(None, Some("Alice"), Some("knows"), None)
9880 .into_iter()
9881 .map(|(_, _, o)| o)
9882 .collect();
9883 def_objs.sort();
9884 assert_eq!(def_objs, vec!["Bob".to_string(), "Carol".to_string()]);
9885
9886 assert_eq!(
9888 rete.query_in_graph(Some("http://ex/g1"), Some("Alice"), None, None),
9889 vec![("Alice".into(), "knows".into(), "Dave".into())]
9890 );
9891
9892 assert_eq!(rete.query_in_graph(None, None, None, None).len(), 2);
9894 assert_eq!(
9895 rete.query_in_graph(Some("http://ex/g1"), None, None, None)
9896 .len(),
9897 1
9898 );
9899
9900 assert!(rete
9902 .query_in_graph(Some("http://ex/missing"), None, None, None)
9903 .is_empty());
9904 }
9905
9906 #[test]
9919 fn bounded_query_in_graph_does_not_fault_the_whole_dictionary() {
9920 use crate::reader::{CountingReader, SliceReader};
9921
9922 let payload = |i: u32| -> String {
9928 let mut s = String::with_capacity(800);
9929 let mut x = i as u64 ^ 0x9e37_79b9_7f4a_7c15;
9930 while s.len() < 800 {
9931 x = x
9932 .wrapping_mul(6364136223846793005)
9933 .wrapping_add(1442695040888963407);
9934 s.push_str(&format!("{:016x}", x ^ (x >> 29)));
9935 }
9936 s
9937 };
9938 let quads: Vec<(String, String, String, bool)> = (0..2000u32)
9945 .map(|i| {
9946 let named = i % 2 == 0;
9947 let ns = if named { 'a' } else { 'b' };
9948 (
9949 format!("<http://ex/{ns}/s/{i:04}>"),
9950 "<http://ex/abstract>".to_string(),
9951 format!("\"{ns}{} number {i}\"", payload(i)),
9952 named,
9953 )
9954 })
9955 .collect();
9956 let mut db = DictionaryBuilder::new();
9957 for (s, p, o, _) in &quads {
9958 db.observe(s, p, o);
9959 }
9960 let dict = db.build();
9961 let mut def = GraphIndexBuilder::new().with_tile_budget(256);
9962 let mut g1 = GraphIndexBuilder::new().with_tile_budget(256);
9963 for (s, p, o, named) in &quads {
9964 let t = dict.encode(s, p, o).unwrap();
9965 if *named {
9966 g1.push(t);
9967 } else {
9968 def.push(t);
9969 }
9970 }
9971 let named = vec![("http://ex/g1".to_string(), g1.build())];
9972 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9973
9974 let eager = Rete::open(&bytes).unwrap();
9975 let dict_len = eager.header().dictionary_len;
9976
9977 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
9980 let probe = |f: &dyn Fn(&Rete) -> Vec<TermTriple>| -> (Vec<TermTriple>, u64) {
9981 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9982 let lazy = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
9983 let before = reader.bytes_read();
9984 let out = f(&lazy);
9985 assert!(!lazy.index_incomplete(), "a chunk fetch failed");
9986 (out, reader.bytes_read() - before)
9987 };
9988
9989 let want = eager.query_in_graph(
9991 Some("http://ex/g1"),
9992 Some("<http://ex/a/s/0100>"),
9993 None,
9994 None,
9995 );
9996 assert_eq!(want.len(), 1, "fixture: expected exactly one match");
9997 let (got, pulled) = probe(&|r| {
9998 r.query_in_graph(
9999 Some("http://ex/g1"),
10000 Some("<http://ex/a/s/0100>"),
10001 None,
10002 None,
10003 )
10004 });
10005 assert_eq!(got, want, "lazy query_in_graph disagrees with eager");
10006 assert!(
10007 pulled * 4 < dict_len,
10008 "a 1-row query_in_graph faulted {pulled} B of a {dict_len} B dictionary — \
10009 it is prefetching the whole thing again"
10010 );
10011
10012 let (empty, q_pulled) =
10020 probe(&|r| r.query_in_graph(Some("http://ex/nope"), None, None, None));
10021 assert!(empty.is_empty());
10022 let (_, d_pulled) = probe(&|r| {
10023 let mut n = 0usize;
10024 r.dump_each(Some("http://ex/nope"), |_, _, _| n += 1);
10025 assert_eq!(n, 0, "an absent graph yielded triples");
10026 Vec::new()
10027 });
10028 assert_eq!(
10029 q_pulled, d_pulled,
10030 "query_in_graph and dump_each disagree on what an absent graph costs"
10031 );
10032 assert!(
10033 q_pulled * 4 < dict_len,
10034 "an absent graph faulted {q_pulled} B of a {dict_len} B dictionary"
10035 );
10036
10037 let want: Vec<TermTriple> = eager.dump(Some("http://ex/g1"));
10040 assert_eq!(want.len(), 1000);
10041 let (got, pulled) = probe(&|r| {
10042 let mut out = Vec::new();
10043 r.dump_each(Some("http://ex/g1"), |s, p, o| {
10044 out.push((s.to_string(), p.to_string(), o.to_string()))
10045 });
10046 out
10047 });
10048 assert_eq!(got, want, "lazy dump_each disagrees with eager dump");
10049 assert!(
10050 pulled * 3 < dict_len * 2,
10051 "dumping half the graph faulted {pulled} B of a {dict_len} B dictionary — \
10052 the other graph's chunks are being pulled in too"
10053 );
10054 }
10055
10056 #[cfg(test)]
10061 fn build_multichunk_image() -> Vec<u8> {
10062 use crate::index::PermSet;
10063 let mut db = DictionaryBuilder::new();
10064 let mut triples: Vec<(String, String, String)> = (0..4000u32)
10065 .map(|i| {
10066 (
10067 format!("<http://ex/s/{i:07}>"),
10068 format!("<http://ex/p/{}>", i % 8),
10069 format!("\"object literal number {i:07} with padding text to widen the term\""),
10070 )
10071 })
10072 .collect();
10073 triples.push((
10074 "<http://ex/s/big>".to_string(),
10075 "<http://ex/p/big>".to_string(),
10076 format!("\"{}\"", "Z".repeat(8000)),
10077 ));
10078 for (s, p, o) in &triples {
10079 db.observe(s, p, o);
10080 }
10081 let dict = db.build();
10082 let mut idx = GraphIndexBuilder::new()
10083 .with_tile_budget(64)
10084 .with_perms(PermSet::ALL);
10085 for (s, p, o) in &triples {
10086 idx.push(dict.encode(s, p, o).unwrap());
10087 }
10088 write_dataset(&dict, &idx.build(), &[], true, &[], 0)
10089 }
10090
10091 #[test]
10100 fn tiny_dict_cache_evicts_yet_dump_is_byte_identical() {
10101 use crate::reader::SliceReader;
10102 let leaked: &'static [u8] = Box::leak(build_multichunk_image().into_boxed_slice());
10103 let dump = |cap: u64| -> (
10104 Vec<(String, String, String)>,
10105 crate::chunk_cache::CacheStats,
10106 ) {
10107 let rete =
10108 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64).unwrap();
10109 rete.dict.set_cache_cap(cap);
10110 let mut out = Vec::new();
10111 rete.dump_filtered_each(None, None, None, None, |s, p, o| {
10112 out.push((s.to_string(), p.to_string(), o.to_string()))
10113 });
10114 assert!(
10115 !rete.index_incomplete(),
10116 "no lazy fault should fail at cap {cap}"
10117 );
10118 let stats = rete.dict_cache_stats();
10119 (out, stats)
10120 };
10121
10122 let (unlimited, us) = dump(u64::MAX);
10123 assert_eq!(unlimited.len(), 4001, "every triple dumped");
10124 assert_eq!(us.evictions, 0, "unlimited cap evicts nothing");
10125
10126 let (tiny, ts) = dump(128 * 1024);
10129 assert_eq!(tiny, unlimited, "128 KiB-cap dump differs from unlimited");
10130 assert!(ts.evictions > 0, "a tiny cap must evict (stats {ts:?})");
10131
10132 let (starved, ss) = dump(1);
10134 assert_eq!(starved, unlimited, "1-byte-cap dump differs from unlimited");
10135 assert!(
10136 ss.evictions > 0,
10137 "1-byte cap evicts every insert (stats {ss:?})"
10138 );
10139 }
10140
10141 #[test]
10152 fn memory_budget_charges_the_fixed_directories() {
10153 use crate::reader::SliceReader;
10154 let leaked: &'static [u8] = Box::leak(build_multichunk_image().into_boxed_slice());
10155 let rete =
10156 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64).unwrap();
10157
10158 let fixed = rete.fixed_overhead();
10159 assert!(
10162 fixed.dict_directory > 0 && fixed.index_directory > 0,
10163 "{fixed:?}"
10164 );
10165 assert_eq!(fixed.named_graphs, 0, "no named graphs in this file");
10166 assert_eq!(
10167 fixed.total(),
10168 fixed.dict_directory + fixed.index_directory + fixed.named_graphs
10169 );
10170
10171 let total: u64 = 512 << 20;
10174 let applied = rete.set_memory_budget(Some(total));
10175 let plain = split_memory_budget(total);
10176 assert_eq!(applied.dict_cache, plain.dict_cache - fixed.total());
10177 assert_eq!(rete.dict_cache_cap(), applied.dict_cache);
10178 assert_eq!(applied.tile_cache, plain.tile_cache);
10179 assert_eq!(applied.block_cache, plain.block_cache);
10180 assert!(
10181 applied.dict_cache + applied.tile_cache + applied.block_cache + fixed.total() <= total,
10182 "caps + fixed directories exceed the budget: {applied:?} + {fixed:?} > {total}"
10183 );
10184
10185 let applied = rete.set_memory_budget(Some(1 << 20));
10187 assert_eq!(applied.dict_cache, MIN_DICT_CACHE_CAP);
10188
10189 let applied = rete.set_memory_budget(None);
10191 assert_eq!(applied.dict_cache, u64::MAX);
10192 assert_eq!(rete.dict_cache_cap(), u64::MAX);
10193
10194 let rete =
10197 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64).unwrap();
10198 rete.dict.set_cache_cap(128 * 1024);
10199 let mut rows = 0usize;
10200 rete.dump_filtered_each(None, None, None, None, |_, _, _| rows += 1);
10201 assert_eq!(rows, 4001);
10202 let s = rete.dict_cache_stats();
10203 assert!(s.evictions > 0, "the cap must bind: {s:?}");
10204 assert!(
10207 s.entries <= 1 || s.resident_bytes <= 128 * 1024,
10208 "dictionary cache over its cap after the dump: {s:?}"
10209 );
10210 }
10211
10212 #[test]
10217 fn releasing_a_named_graph_drops_and_reopens_its_index() {
10218 use crate::index::PermSet;
10219 use crate::reader::SliceReader;
10220 let mut db = DictionaryBuilder::new();
10221 let all: Vec<(String, String, String)> = (0..40u32)
10222 .map(|i| {
10223 (
10224 format!("<http://ex/s{}>", i % 7),
10225 format!("<http://ex/p{}>", i % 3),
10226 format!("<http://ex/o{}>", i % 5),
10227 )
10228 })
10229 .collect();
10230 for (s, p, o) in &all {
10231 db.observe(s, p, o);
10232 }
10233 let dict = db.build();
10234 let mk = |take: fn(u32) -> bool| {
10235 let mut g = GraphIndexBuilder::new()
10236 .with_tile_budget(16)
10237 .with_perms(PermSet::ALL);
10238 for (i, (s, p, o)) in all.iter().enumerate() {
10239 if take(i as u32) {
10240 g.push(dict.encode(s, p, o).unwrap());
10241 }
10242 }
10243 g.build()
10244 };
10245 let def = mk(|_| true);
10246 let g1 = mk(|i| i % 2 == 0);
10247 let g2 = mk(|i| i % 3 == 0);
10248 let image = write_dataset(
10249 &dict,
10250 &def,
10251 &[
10252 ("<http://ex/g1>".to_string(), g1),
10253 ("<http://ex/g2>".to_string(), g2),
10254 ],
10255 true,
10256 &[],
10257 0,
10258 );
10259 let leaked: &'static [u8] = Box::leak(image.into_boxed_slice());
10260 let mut rete =
10261 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64).unwrap();
10262
10263 let opened = |r: &Rete| -> usize {
10264 match &r.named_graphs {
10265 NamedGraphsSlot::Lazy(l) => {
10266 let mut n = 0;
10267 l.for_each_opened(|_| n += 1);
10268 n
10269 }
10270 _ => 0,
10271 }
10272 };
10273
10274 assert_eq!(opened(&rete), 0, "nothing opened before first touch");
10275 assert!(rete.graph_index("<http://ex/g1>").is_some());
10277 assert_eq!(opened(&rete), 1, "g1 opened");
10278 rete.release_named_graph("<http://ex/g1>");
10280 assert_eq!(opened(&rete), 0, "g1 released");
10281 rete.release_named_graph("<http://ex/absent>");
10283 rete.release_named_graph("<http://ex/g1>");
10284 assert_eq!(opened(&rete), 0);
10285 assert!(rete.graph_index("<http://ex/g1>").is_some());
10287 assert_eq!(opened(&rete), 1, "g1 re-opened after release");
10288 }
10289
10290 #[test]
10308 fn filtered_dump_is_exactly_the_unfiltered_dump_filtered() {
10309 use crate::index::PermSet;
10310 use crate::reader::SliceReader;
10311
10312 let terms = |i: u32| {
10313 (
10314 format!("<http://ex/s{}>", i % 11),
10315 format!("<http://ex/p{}>", i % 3),
10316 if i.is_multiple_of(4) {
10320 format!("\"lit {}\"@en", i % 5)
10321 } else {
10322 format!("<http://ex/o{}>", i % 7)
10323 },
10324 )
10325 };
10326 let build = |perms: PermSet| {
10327 let mut db = DictionaryBuilder::new();
10328 let all: Vec<_> = (0..90u32).map(terms).collect();
10329 for (s, p, o) in &all {
10330 db.observe(s, p, o);
10331 }
10332 let dict = db.build();
10333 let mut def = GraphIndexBuilder::new()
10334 .with_tile_budget(16)
10335 .with_perms(perms);
10336 let mut g1 = GraphIndexBuilder::new()
10337 .with_tile_budget(16)
10338 .with_perms(perms);
10339 for (i, (s, p, o)) in all.iter().enumerate() {
10340 let t = dict.encode(s, p, o).unwrap();
10341 if i % 2 == 0 {
10345 g1.push(t);
10346 }
10347 def.push(t);
10348 }
10349 write_dataset(
10350 &dict,
10351 &def.build(),
10352 &[("<http://ex/g1>".to_string(), g1.build())],
10353 true,
10354 &[],
10355 0,
10356 )
10357 };
10358
10359 let shapes: Vec<(Option<&str>, Option<&str>, Option<&str>)> = vec![
10360 (None, None, None),
10361 (Some("<http://ex/s3>"), None, None),
10362 (None, Some("<http://ex/p1>"), None),
10363 (None, None, Some("<http://ex/o5>")),
10364 (None, None, Some("\"lit 0\"@en")),
10365 (Some("<http://ex/s3>"), Some("<http://ex/p0>"), None),
10366 (Some("<http://ex/s4>"), None, Some("<http://ex/o4>")),
10367 (None, Some("<http://ex/p1>"), Some("<http://ex/o5>")),
10368 (
10369 Some("<http://ex/s3>"),
10370 Some("<http://ex/p0>"),
10371 Some("<http://ex/o3>"),
10372 ),
10373 (Some("<http://ex/nope>"), None, None),
10377 (None, Some("<http://ex/nope>"), None),
10378 (None, None, Some("<http://ex/nope>")),
10379 ];
10380
10381 let mut any_pruned = false;
10382 for perms in [PermSet::ALL, PermSet::CORE] {
10383 let image = build(perms);
10384 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
10385 let opens: Vec<(&str, Rete)> = vec![
10386 ("resident", Rete::open(&image).unwrap()),
10387 (
10388 "ranged",
10389 Rete::open_ranged(&SliceReader::new(leaked)).unwrap(),
10390 ),
10391 (
10392 "lazy",
10393 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64)
10394 .unwrap(),
10395 ),
10396 ];
10397 for graph in [None, Some("<http://ex/g1>"), Some("<http://ex/absent>")] {
10398 let full: Vec<TermTriple> = {
10400 let mut v = Vec::new();
10401 Rete::open(&image).unwrap().dump_each(graph, |s, p, o| {
10402 v.push((s.to_string(), p.to_string(), o.to_string()))
10403 });
10404 v.sort();
10405 v
10406 };
10407 for &(s, p, o) in &shapes {
10408 let mut want = full.clone();
10409 want.retain(|(ts, tp, to)| {
10410 s.is_none_or(|x| x == ts)
10411 && p.is_none_or(|x| x == tp)
10412 && o.is_none_or(|x| x == to)
10413 });
10414 for (path, rete) in &opens {
10415 let mut got = Vec::new();
10416 rete.dump_filtered_each(graph, s, p, o, |s, p, o| {
10417 got.push((s.to_string(), p.to_string(), o.to_string()))
10418 });
10419 got.sort();
10420 assert_eq!(
10421 got,
10422 want,
10423 "{path}/{perms:?} graph={graph:?} pattern={:?} — filtered dump \
10424 is not the unfiltered dump filtered",
10425 (s, p, o)
10426 );
10427 assert!(!rete.index_incomplete(), "{path}: a fetch failed");
10428 }
10429 let plan = opens[2].1.dump_plan(graph, s, p, o);
10433 if graph == Some("<http://ex/absent>")
10434 || [s, p, o].contains(&Some("<http://ex/nope>"))
10435 {
10436 assert!(
10437 plan.scan.is_none(),
10438 "an unmatchable dump still planned a scan: {:?}",
10439 (graph, s, p, o)
10440 );
10441 } else {
10442 let scan = plan.scan.expect("a matchable dump plans a scan");
10443 assert!(
10444 PermSet::CORE.contains(scan.permutation),
10445 "a dump routed to {:?}, outside PermSet::CORE — a \
10446 --permutations 3 file has no such section",
10447 scan.permutation
10448 );
10449 assert!(scan.tiles_admitted <= scan.tiles_routed);
10450 assert!(scan.tiles_routed <= scan.tiles_total);
10451 assert!(scan.tile_bytes <= scan.section_bytes);
10452 if scan.tiles_admitted < scan.tiles_total {
10453 any_pruned = true;
10454 }
10455 }
10456 }
10457 }
10458 }
10459 assert!(
10460 any_pruned,
10461 "no shape pruned a single tile — the fixture is too small to be \
10462 measuring anything"
10463 );
10464 }
10465
10466 #[test]
10476 fn a_predicate_scoped_dump_fetches_less_than_the_graph() {
10477 use crate::reader::{CountingReader, SliceReader};
10478
10479 let mut db = DictionaryBuilder::new();
10485 let mut all = Vec::new();
10486 for s in 0..200u32 {
10487 for p in 0..20u32 {
10488 let t = (
10489 format!("<http://ex/s/{s:04}>"),
10490 format!("<http://ex/p/{p:02}>"),
10491 format!("\"object {s:04}/{p:02}\""),
10492 );
10493 db.observe(&t.0, &t.1, &t.2);
10494 all.push(t);
10495 }
10496 }
10497 let dict = db.build();
10498 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
10499 for (s, p, o) in &all {
10500 ib.push(dict.encode(s, p, o).unwrap());
10501 }
10502 let image = write_dataset(&dict, &ib.build(), &[], false, &[], 0);
10503 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
10504
10505 let probe = |s: Option<&str>, p: Option<&str>, o: Option<&str>| -> (usize, u64) {
10507 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
10508 let lazy = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
10509 let before = reader.bytes_read();
10510 let mut n = 0usize;
10511 lazy.dump_filtered_each(None, s, p, o, |_, _, _| n += 1);
10512 assert!(!lazy.index_incomplete());
10513 (n, reader.bytes_read() - before)
10514 };
10515
10516 let (full_rows, full_bytes) = probe(None, None, None);
10517 let (pred_rows, pred_bytes) = probe(None, Some("<http://ex/p/07>"), None);
10518 let (subj_rows, subj_bytes) = probe(Some("<http://ex/s/0021>"), None, None);
10519 assert_eq!(full_rows, 4000);
10520 assert_eq!(pred_rows, 200, "one predicate covers every subject once");
10521 assert_eq!(subj_rows, 20);
10522 assert!(
10524 pred_bytes < full_bytes && subj_bytes < full_bytes,
10525 "a slice fetched {pred_bytes} / {subj_bytes} B where the whole graph fetched \
10526 {full_bytes} B — the scan is not being pruned at all"
10527 );
10528 #[cfg(feature = "compression")]
10539 assert!(
10540 pred_bytes * 4 < full_bytes && subj_bytes * 4 < full_bytes,
10541 "a 1-in-20 predicate slice fetched {pred_bytes} B and one subject \
10542 {subj_bytes} B where the whole graph fetched {full_bytes} B"
10543 );
10544
10545 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
10547 let lazy = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
10548 let before = reader.bytes_read();
10549 let plan = lazy
10550 .dump_plan(None, None, Some("<http://ex/p/07>"), None)
10551 .scan
10552 .expect("a known predicate plans a scan");
10553 assert!(
10554 reader.bytes_read() - before < 64 * 1024,
10555 "planning fetched {} B — it is supposed to read directories, not tiles",
10556 reader.bytes_read() - before
10557 );
10558 assert!(plan.tiles_admitted < plan.tiles_total);
10559 assert!(
10565 plan.tile_bytes * 5 < plan.section_bytes,
10566 "one predicate of twenty planned {} B of a {} B section",
10567 plan.tile_bytes,
10568 plan.section_bytes
10569 );
10570 }
10571
10572 #[test]
10573 fn query_quads_tags_every_graph() {
10574 let mut db = DictionaryBuilder::new();
10575 for (s, p, o) in [("Alice", "knows", "Bob"), ("Alice", "knows", "Dave")] {
10576 db.observe(s, p, o);
10577 }
10578 let dict = db.build();
10579 let mut def = GraphIndexBuilder::new();
10580 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
10581 let mut g1 = GraphIndexBuilder::new();
10582 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
10583 let named = vec![("http://ex/g1".to_string(), g1.build())];
10584 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
10585 let rete = Rete::open(&bytes).unwrap();
10586
10587 let quads = rete.query_quads(Some("Alice"), Some("knows"), None);
10589 assert_eq!(quads.len(), 2);
10590 assert_eq!(
10591 quads[0],
10592 (("Alice".into(), "knows".into(), "Bob".into()), None)
10593 );
10594 assert_eq!(
10595 quads[1],
10596 (
10597 ("Alice".into(), "knows".into(), "Dave".into()),
10598 Some("http://ex/g1".to_string())
10599 )
10600 );
10601
10602 assert!(rete.query_quads(Some("Nobody"), None, None).is_empty());
10604 }
10605
10606 #[test]
10607 fn pyramid_meta_round_trips_in_file() {
10608 let rete = Rete::open(&build_image()).unwrap();
10609 let pyr = rete.pyramid().expect("file has a pyramid");
10610 let total: u32 = pyr.summary.iter().map(|e| e.count).sum();
10612 assert_eq!(total, 3);
10613 assert!(!pyr.summary.is_empty());
10614 assert!(pyr.tiles.is_empty());
10615 }
10616
10617 #[test]
10618 fn schema_pyramid_round_trips_through_file_index_free() {
10619 use crate::reader::{CountingReader, SliceReader};
10620 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
10621 let q = |s: &str, p: &str, o: &str| {
10622 (s.to_string(), p.to_string(), o.to_string(), None::<String>)
10623 };
10624 let quads = vec![
10626 q("<a>", RDF_TYPE, "<Astronomer>"),
10627 q("<b>", RDF_TYPE, "<Astronomer>"),
10628 q("<c>", RDF_TYPE, "<Person>"),
10629 q("<Astronomer>", sub, "<Scientist>"),
10630 q("<Scientist>", sub, "<Person>"),
10631 q("<Person>", sub, "<Agent>"),
10632 q("<a>", "<knows>", "<b>"),
10633 q("<b>", "<knows>", "<c>"),
10634 ];
10635 let (bytes, _) =
10636 crate::ingest::assemble_dataset_with_opts(quads, true, false, None, |_, _| Vec::new());
10637
10638 let rete = Rete::open(&bytes).unwrap();
10640 let pyr = rete.pyramid().expect("pyramid present");
10641 assert!(!pyr.level_rollups.is_empty(), "schema pyramid shipped");
10642 assert!(pyr
10643 .class_hierarchy
10644 .iter()
10645 .any(|n| n.class == "<Agent>" && n.depth == 0));
10646
10647 let r = CountingReader::new(SliceReader::new(&bytes));
10649 let view = SummaryView::open_ranged(&r).unwrap().unwrap();
10650 assert!(view.level_count() >= 2, "multi-level pyramid");
10651 let coarse = view.level_rollup(0).unwrap();
10652 assert!(
10653 coarse.classes.iter().any(|(c, _)| c == "<Agent>"),
10654 "coarsest level rolls up to the root Agent"
10655 );
10656 let h = Header::from_bytes(&bytes).unwrap();
10657 assert!(
10658 r.bytes_read() <= bytes.len() as u64 - h.root_dir_len,
10659 "summary read {} bytes; the {}-byte index section must be skipped",
10660 r.bytes_read(),
10661 h.root_dir_len
10662 );
10663 }
10664
10665 #[test]
10666 fn predicate_totals_from_summary_only() {
10667 use crate::reader::SliceReader;
10668 let bytes = build_image();
10670 let reader = SliceReader::new(&bytes);
10671 let view = SummaryView::open_ranged(&reader).unwrap().unwrap();
10672 assert_eq!(view.predicate_total("knows"), 2);
10673 assert_eq!(view.predicate_total("age"), 1);
10674 assert_eq!(view.predicate_total("missing"), 0);
10675 let totals = view.predicate_totals();
10676 assert_eq!(totals[0], ("knows".to_string(), 2)); }
10678
10679 #[test]
10680 fn query_patterns_resolve_to_terms() {
10681 let rete = Rete::open(&build_image()).unwrap();
10682
10683 assert_eq!(rete.query(None, None, None).len(), 3);
10685
10686 let mut alice = rete.query(Some("Alice"), None, None);
10688 alice.sort();
10689 assert_eq!(
10690 alice,
10691 vec![
10692 ("Alice".into(), "age".into(), "30".into()),
10693 ("Alice".into(), "knows".into(), "Bob".into()),
10694 ]
10695 );
10696
10697 assert_eq!(rete.query(None, Some("knows"), None).len(), 2);
10699
10700 assert_eq!(
10702 rete.query(Some("Bob"), Some("knows"), Some("Carol")),
10703 vec![("Bob".into(), "knows".into(), "Carol".into())]
10704 );
10705 assert!(rete.query(Some("Nobody"), None, None).is_empty());
10706 assert!(rete.query(None, Some("likes"), None).is_empty());
10707 }
10708
10709 #[test]
10710 fn query_provenance_reports_terms_ids_sections_and_index_choice() {
10711 let bytes = build_image();
10712 let rete = Rete::open(&bytes).unwrap();
10713
10714 let mut matches = rete.query_with_provenance(None, Some("knows"), None);
10715 matches.sort_by(|a, b| a.terms.cmp(&b.terms));
10716
10717 assert_eq!(matches.len(), 2);
10718 assert_eq!(
10719 matches[0].terms,
10720 ("Alice".into(), "knows".into(), "Bob".into())
10721 );
10722 assert_eq!(
10723 matches[0].ids,
10724 rete.dictionary().encode("Alice", "knows", "Bob").unwrap()
10725 );
10726 assert_eq!(matches[0].graph.as_deref(), None);
10727 assert_eq!(
10728 matches[0].matched_pattern,
10729 (None, Some(matches[0].ids.1), None)
10730 );
10731 assert_eq!(
10732 matches[0].index_permutation,
10733 crate::index::IndexPermutation::Pos
10734 );
10735
10736 let h = rete.header();
10737 assert_eq!(matches[0].dictionary_range.offset, h.dictionary_offset);
10738 assert_eq!(matches[0].dictionary_range.len, h.dictionary_len);
10739 assert_eq!(matches[0].index_range.offset, h.root_dir_offset);
10740 assert_eq!(matches[0].index_range.len, h.root_dir_len);
10741 assert!(
10742 matches[0].index_section_range.offset > h.root_dir_offset,
10743 "POS is section 1, so its payload starts after the container header and SPO payload"
10744 );
10745 assert!(matches[0].index_section_range.len > 0);
10746 assert!(matches[0].index_section_range.end() <= matches[0].index_range.end());
10747 assert!(matches[0].index_section_range.len < matches[0].index_range.len);
10748 assert_eq!(
10749 matches[0].pyramid_range.as_ref().map(|r| (r.offset, r.len)),
10750 Some((h.pyramid_meta_offset, h.pyramid_meta_len))
10751 );
10752 let tile_range = matches[0].tile_range.expect("tiled file reports a tile");
10755 assert!(matches[0]
10756 .tile
10757 .as_deref()
10758 .unwrap()
10759 .starts_with(matches[0].index_permutation.name()));
10760 assert!(matches[0].index_section_range.offset <= tile_range.offset);
10761 assert!(tile_range.end() <= matches[0].index_section_range.end());
10762 }
10763
10764 fn build_labeled(n: usize) -> Vec<u8> {
10768 const LABEL: &str = "<http://www.w3.org/2000/01/rdf-schema#label>";
10769 const WORDS: &[&str] = &[
10770 "alanine",
10771 "benzene",
10772 "glucose",
10773 "dextrose",
10774 "ethanol",
10775 "formate",
10776 "heptane",
10777 "isoleucine",
10778 ];
10779 let triples: Vec<(String, String, String)> = (0..n)
10780 .flat_map(|i| {
10781 let s = format!("<http://ex/e{i}>");
10782 let w = WORDS[i % WORDS.len()];
10783 [
10784 (s.clone(), LABEL.to_string(), format!("\"{w}-{i:06}\"")),
10785 (
10786 s,
10787 "<http://ex/p>".to_string(),
10788 format!("<http://ex/c{}>", i % 64),
10789 ),
10790 ]
10791 })
10792 .collect();
10793 let mut db = DictionaryBuilder::new();
10794 for (s, p, o) in &triples {
10795 db.observe(s, p, o);
10796 }
10797 let dict = db.build();
10798 let ids: Vec<(u32, u32, u32)> = triples
10799 .iter()
10800 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
10801 .collect();
10802 let mut ib = GraphIndexBuilder::new();
10803 for &t in &ids {
10804 ib.push(t);
10805 }
10806 let (meta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
10807 write_file(&dict, &ib.build(), false, &meta, levels)
10808 }
10809
10810 #[test]
10811 fn prefix_search_matches_a_filter_scan() {
10812 let bytes = build_labeled(800);
10815 let rete = Rete::open(&bytes).unwrap();
10816 let idx_subjects: std::collections::BTreeSet<String> = rete
10817 .prefix_search("glucose", 10_000)
10818 .into_iter()
10819 .map(|(_label, subject)| subject)
10820 .collect();
10821 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
10823 FILTER(STRSTARTS(LCASE(?l), \"glucose\")) }";
10824 let crate::QueryOutput::Select(_, rows) = crate::eval_query(&rete, q).unwrap() else {
10825 panic!("expected SELECT");
10826 };
10827 let scan_subjects: std::collections::BTreeSet<String> =
10828 rows.iter().map(|r| r.get("s").cloned().unwrap()).collect();
10829 assert_eq!(idx_subjects, scan_subjects, "index agrees with the scan");
10830 assert_eq!(
10831 idx_subjects.len(),
10832 100,
10833 "800/8 words = 100 glucose-* labels"
10834 );
10835 }
10836
10837 #[test]
10841 fn legacy_label_hits_resolve_in_one_batched_read() {
10842 use std::sync::{Arc, Mutex};
10843 type CallLog = Arc<Mutex<Vec<Vec<(u64, u64)>>>>;
10844 #[derive(Clone)]
10845 struct Calls(Arc<Vec<u8>>, CallLog);
10846 impl crate::RangeReader for Calls {
10847 fn len(&self) -> u64 {
10848 self.0.len() as u64
10849 }
10850 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
10851 self.1.lock().unwrap().push(vec![(offset, len)]);
10852 crate::SliceReader::new(&self.0).read_at(offset, len)
10853 }
10854 fn read_many(&self, ranges: &[(u64, u64)]) -> std::io::Result<Vec<Vec<u8>>> {
10855 self.1.lock().unwrap().push(ranges.to_vec());
10856 let r = crate::SliceReader::new(&self.0);
10857 ranges.iter().map(|&(o, l)| r.read_at(o, l)).collect()
10858 }
10859 }
10860 let bytes = build_labeled(20_000);
10861 let h = Header::from_bytes(&bytes).unwrap();
10862 let (d0, dl) = (h.dictionary_offset, h.dictionary_len);
10863 let want = Rete::open(&bytes).unwrap().prefix_search("glucose", 1000);
10864 assert_eq!(want.len(), 1000);
10865 for search_view in [false, true] {
10866 let calls = Calls(Arc::new(bytes.clone()), Arc::new(Mutex::new(Vec::new())));
10867 let got = if search_view {
10868 let view = SearchView::open_ranged_with(calls.clone(), HEADER_LEN as u64).unwrap();
10869 calls.1.lock().unwrap().clear();
10870 view.prefix_search("glucose", 1000)
10871 } else {
10872 let rete = Rete::open_ranged_lazy_with(calls.clone(), HEADER_LEN as u64).unwrap();
10873 calls.1.lock().unwrap().clear();
10874 rete.prefix_search("glucose", 1000)
10875 };
10876 assert_eq!(got, want, "search view = {search_view}");
10877 let dict_reads = calls
10878 .1
10879 .lock()
10880 .unwrap()
10881 .iter()
10882 .filter(|c| c.iter().any(|&(o, l)| o < d0 + dl && d0 < o + l))
10883 .count();
10884 assert_eq!(dict_reads, 1, "search view = {search_view}");
10885 }
10886 }
10887
10888 #[test]
10892 #[ignore]
10893 fn bench_prefix_search_vs_filter_scan() {
10894 use std::time::Instant;
10895 let n = 6000; let bytes = build_labeled(n);
10897 let rete = Rete::open(&bytes).unwrap();
10898 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
10899 FILTER(STRSTARTS(LCASE(?l), \"glucose\")) }";
10900 let reps = 200;
10901 let idx_n = rete.prefix_search("glucose", 100_000).len();
10902 let t = Instant::now();
10903 for _ in 0..reps {
10904 std::hint::black_box(rete.prefix_search("glucose", 100_000));
10905 }
10906 let idx_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
10907 let t = Instant::now();
10908 for _ in 0..reps {
10909 let _ = std::hint::black_box(crate::eval_query(&rete, q).unwrap());
10910 }
10911 let scan_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
10912 println!(
10913 "label prefix search over {n} labeled subjects ({idx_n} matches): \
10914 index {idx_ms:.4} ms vs FILTER scan {scan_ms:.3} ms ({:.0}× faster)",
10915 scan_ms / idx_ms
10916 );
10917 }
10918
10919 #[test]
10923 #[ignore]
10924 fn bench_text_search_vs_contains_scan() {
10925 use std::time::Instant;
10926 const LABEL: &str = "<http://www.w3.org/2000/01/rdf-schema#label>";
10927 const WORDS: &[&str] = &[
10928 "alanine",
10929 "benzene",
10930 "glucose",
10931 "dextrose",
10932 "ethanol",
10933 "formate",
10934 "heptane",
10935 "isoleucine",
10936 ];
10937 let n = 6000;
10938 let triples: Vec<(String, String, String)> = (0..n)
10939 .map(|i| {
10940 (
10941 format!("<http://ex/e{i}>"),
10942 LABEL.to_string(),
10943 format!("\"{} sample number {i:06}\"", WORDS[i % WORDS.len()]),
10944 )
10945 })
10946 .collect();
10947 let mut db = DictionaryBuilder::new();
10948 for (s, p, o) in &triples {
10949 db.observe(s, p, o);
10950 }
10951 let dict = db.build();
10952 let ids: Vec<(u32, u32, u32)> = triples
10953 .iter()
10954 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
10955 .collect();
10956 let mut ib = GraphIndexBuilder::new();
10957 for &t in &ids {
10958 ib.push(t);
10959 }
10960 let ti = compute_text_index(&dict, &ids);
10961 let bytes = write_dataset_with_metadata(&dict, &ib.build(), &[], false, &[], 0, &[], &ti);
10962 let rete = Rete::open(&bytes).unwrap();
10963
10964 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
10965 FILTER(CONTAINS(LCASE(?l), \"glucose\")) }";
10966 let reps = 200;
10967 let idx_n = rete.text_search(&["glucose"], None, 100_000).len();
10968 let t = Instant::now();
10969 for _ in 0..reps {
10970 std::hint::black_box(rete.text_search(&["glucose"], None, 100_000));
10971 }
10972 let idx_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
10973 let t = Instant::now();
10974 for _ in 0..reps {
10975 let _ = std::hint::black_box(crate::eval_query(&rete, q).unwrap());
10976 }
10977 let scan_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
10978 println!(
10979 "text search over {n} literals ({idx_n} matches): \
10980 index {idx_ms:.4} ms vs FILTER(CONTAINS) scan {scan_ms:.3} ms ({:.0}× faster)",
10981 scan_ms / idx_ms
10982 );
10983 }
10984
10985 #[test]
10989 #[ignore = "operational tool, driven by RETE_DEBUG_* env vars"]
10990 fn debug_bound_po_routing() {
10991 struct FR(std::fs::File);
10992 impl crate::RangeReader for FR {
10993 fn len(&self) -> u64 {
10994 self.0.metadata().map(|m| m.len()).unwrap_or(0)
10995 }
10996 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
10997 use std::os::unix::fs::FileExt;
10998 let mut buf = vec![0u8; len as usize];
10999 self.0.read_exact_at(&mut buf, offset)?;
11000 Ok(buf)
11001 }
11002 }
11003 let path = std::env::var("RETE_DEBUG_FILE").expect("RETE_DEBUG_FILE");
11004 let p_iri = std::env::var("RETE_DEBUG_P").expect("RETE_DEBUG_P");
11005 let o_iri = std::env::var("RETE_DEBUG_O").expect("RETE_DEBUG_O");
11006 let rete = Rete::open_ranged_lazy_with(
11007 std::sync::Arc::new(FR(std::fs::File::open(&path).unwrap())),
11008 HEADER_LEN as u64,
11009 )
11010 .unwrap();
11011 let pid = rete.dict.predicate_id(&p_iri).expect("p resolves");
11012 let oid = rete.dict.object_id(&o_iri).expect("o resolves");
11013 eprintln!("pid={pid} oid={oid}");
11014 let pattern = (None, Some(pid), Some(oid));
11015 let perm = GraphIndex::best_permutation(pattern);
11016 eprintln!("best_permutation = {}", perm.name());
11017 let si = perm.section_index();
11018 let tiles = &rete.index.sections[si];
11019 eprintln!("section {} tiles = {}", perm.name(), tiles.len());
11020 let [pa, pb, pc] = perm.order_pattern(pattern);
11021 eprintln!("permuted pattern pa={pa:?} pb={pb:?} pc={pc:?}");
11022 let (start, end) = rete.index.tile_span(si, pa);
11023 eprintln!("tile_span = [{start}, {end}) -> {} tiles", end - start);
11024 let mut admitted = 0usize;
11025 for (ti, t) in tiles.iter().enumerate().take(end).skip(start) {
11026 if t.syn_admits(pb, pc) {
11027 admitted += 1;
11028 if admitted <= 10 {
11029 let (lo, hi) = t.leading_range();
11030 eprintln!(" admit tile {ti}: a=[{lo},{hi}] syn={:?}", t.syn);
11031 }
11032 }
11033 }
11034 eprintln!("admitted {admitted} tile(s) by synopsis");
11035 let n = rete.index.scan_iter(pattern).count();
11036 eprintln!("scan_iter matches = {n}");
11037 let hi_res = rete.query(None, Some(&p_iri), Some(&o_iri));
11038 eprintln!("high-level query matches = {}", hi_res.len());
11039 }
11040
11041 #[test]
11049 fn layout_header_label_matches_its_span() {
11050 let mut db = DictionaryBuilder::new();
11051 let mut triples = Vec::new();
11052 for i in 0..24u32 {
11053 let (s, p, o) = (
11054 format!("<http://ex/s{}>", i % 5),
11055 format!("<http://ex/p{}>", i % 3),
11056 format!("<http://ex/o{i}>"),
11057 );
11058 db.observe(&s, &p, &o);
11059 triples.push((s, p, o));
11060 }
11061 let dict = db.build();
11062 let ids: Vec<(u32, u32, u32)> = triples
11063 .iter()
11064 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
11065 .collect();
11066 let index = GraphIndexBuilder::from_triples(ids).build();
11067 let image = write_dataset_with_metadata(
11068 &dict,
11069 &index,
11070 &[],
11071 false,
11072 &[],
11073 0,
11074 br#"{"name":"layout-label-fixture"}"#,
11075 &[],
11076 );
11077 let rete = Rete::open(&image).unwrap();
11078
11079 let layout = rete.file_layout();
11080 let header = layout
11081 .iter()
11082 .find(|s| s.kind == "header")
11083 .expect("every layout starts with a header segment");
11084
11085 assert_eq!(
11086 header.offset, 0,
11087 "the header is the first thing in the file"
11088 );
11089 assert_eq!(
11090 header.len, HEADER_LEN as u64,
11091 "the header segment must span exactly HEADER_LEN"
11092 );
11093
11094 let digits: String = header
11097 .label
11098 .chars()
11099 .skip_while(|c| !c.is_ascii_digit())
11100 .take_while(|c| c.is_ascii_digit())
11101 .collect();
11102 assert!(
11103 !digits.is_empty(),
11104 "header label {:?} states no size at all",
11105 header.label
11106 );
11107 assert_eq!(
11108 digits.parse::<u64>().unwrap(),
11109 header.len,
11110 "header label {:?} disagrees with the {}-byte span it describes",
11111 header.label,
11112 header.len
11113 );
11114
11115 assert!(
11118 layout.windows(2).all(|w| w[0].offset <= w[1].offset),
11119 "file_layout must return segments sorted by offset"
11120 );
11121 }
11122}