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 name_owned(&self, dict: &Dictionary, i: usize, exhaustive: bool) -> Option<String> {
4016 match self {
4017 NamedGraphsSlot::Quad(q) => q.name_owned(dict, i, exhaustive),
4018 _ => self.name_at(dict, i, exhaustive).map(str::to_owned),
4019 }
4020 }
4021
4022 fn graph_at<'a>(
4023 &'a self,
4024 dict: &Dictionary,
4025 i: usize,
4026 exhaustive: bool,
4027 ) -> Option<(&'a str, &'a GraphIndex)> {
4028 match self {
4029 NamedGraphsSlot::Resident(v) => v.get(i).map(|(iri, g)| (iri.as_str(), g)),
4030 NamedGraphsSlot::Lazy(l) => l.graph_at(i, exhaustive),
4031 NamedGraphsSlot::Quad(q) => q.graph_at(dict, i, exhaustive),
4032 }
4033 }
4034
4035 fn find<'a>(&'a self, dict: &Dictionary, iri: &str) -> Option<&'a GraphIndex> {
4036 match self {
4037 NamedGraphsSlot::Quad(q) => q.find(dict, iri),
4038 NamedGraphsSlot::Resident(v) => v.iter().find(|(name, _)| name == iri).map(|(_, g)| g),
4039 NamedGraphsSlot::Lazy(l) => {
4040 for i in 0..l.count() {
4045 if l.name_at(i, false)? == iri {
4046 return l.graph_at(i, false).map(|(_, g)| g);
4047 }
4048 }
4049 None
4050 }
4051 }
4052 }
4053
4054 fn load_incomplete(&self) -> bool {
4055 match self {
4056 NamedGraphsSlot::Resident(v) => v.iter().any(|(_, g)| g.load_incomplete()),
4057 NamedGraphsSlot::Lazy(l) => {
4058 if l.failed.load(std::sync::atomic::Ordering::Relaxed) {
4059 return true;
4060 }
4061 let mut bad = false;
4062 l.for_each_opened(|g| bad |= g.load_incomplete());
4063 bad
4064 }
4065 NamedGraphsSlot::Quad(q) => q.load_incomplete(),
4066 }
4067 }
4068
4069 fn reset_load_failures(&self) {
4070 match self {
4071 NamedGraphsSlot::Resident(v) => {
4072 for (_, g) in v {
4073 g.reset_load_failure();
4074 }
4075 }
4076 NamedGraphsSlot::Lazy(l) => {
4077 l.failed.store(false, std::sync::atomic::Ordering::Relaxed);
4078 l.for_each_opened(|g| g.reset_load_failure());
4079 }
4080 NamedGraphsSlot::Quad(q) => q.reset_load_failures(),
4081 }
4082 }
4083
4084 fn resident(
4088 header: &Header,
4089 quad_section: impl FnOnce(u64, u64) -> Result<Vec<u8>, FileError>,
4090 per_graph: impl FnOnce() -> Result<Vec<(String, GraphIndex)>, FileError>,
4091 ) -> Result<Self, FileError> {
4092 check_named_layout(header)?;
4093 if let Some(q) = header.quad_index_section() {
4094 let bytes = quad_section(q.offset, q.length)?;
4095 return Ok(NamedGraphsSlot::Quad(crate::quad::QuadNamed::resident(
4096 bytes,
4097 header.block_codec,
4098 header.perms,
4099 )));
4100 }
4101 Ok(NamedGraphsSlot::Resident(if header.named_graphs_len > 0 {
4102 per_graph()?
4103 } else {
4104 Vec::new()
4105 }))
4106 }
4107}
4108
4109pub struct Rete {
4111 header: Header,
4112 dict: Dictionary,
4113 index: GraphIndex,
4114 index_section_ranges: [ByteRange; NUM_PERMS],
4115 tile_ranges: [Vec<(u32, u32, ByteRange)>; NUM_PERMS],
4119 pyramid: PyramidSlot,
4120 text_index: TextIndexSlot,
4121 labels: SectionSlot<crate::label_index::LabelIndex>,
4123 schema: SectionSlot<crate::schema_section::SchemaMeta>,
4125 named_graphs: NamedGraphsSlot,
4126 metadata: Vec<u8>,
4131 service_client: Option<Box<dyn crate::service::ServiceClient>>,
4136 service_error: std::sync::Mutex<Option<String>>,
4140 spill_threshold: std::sync::atomic::AtomicU64,
4143 v06: Option<std::sync::Arc<crate::v06::open::V06File>>,
4145}
4146
4147impl Rete {
4148 pub fn open(bytes: &[u8]) -> Result<Self, FileError> {
4151 #[cfg(feature = "v06-parity")]
4152 if let Some(v6) = parity_image(bytes) {
4153 return Self::open(&v6);
4154 }
4155 if crate::v06::open::is_v06(bytes) {
4156 let r: std::sync::Arc<dyn RangeReader + Send + Sync> =
4159 std::sync::Arc::new(crate::reader::BytesReader(std::sync::Arc::from(bytes)));
4160 let mut rete = Self::open_v06(r, bytes, false)?;
4161 rete.metadata = rete
4162 .v06
4163 .as_ref()
4164 .and_then(|f| f.card().ok().flatten())
4165 .unwrap_or_default();
4166 return Ok(rete);
4167 }
4168 let header = Header::from_bytes(bytes)?;
4169
4170 let region = |off: u64, len: u64| -> Result<&[u8], FileError> {
4174 checked_slice(bytes, off, len)
4175 .ok_or(FileError::Container("section range out of bounds"))
4176 };
4177
4178 let dict = decode_dictionary_container(
4179 region(header.dictionary_offset, header.dictionary_len)?,
4180 header.dict_codec,
4181 )?;
4182
4183 check_named_layout(&header)?;
4184 let index_bytes = region(header.root_dir_offset, header.root_dir_len)?;
4185 let (index, _) = decode_root_index_container(index_bytes, &header)?;
4186 let index_section_ranges = decode_index_section_ranges(
4187 index_bytes,
4188 header.root_dir_offset,
4189 header.perms,
4190 header.index_container_sections(),
4191 )?;
4192
4193 let pyramid = PyramidSlot::Resident(if header.pyramid_meta_len > 0 {
4194 Some(
4195 PyramidMeta::decode(region(header.pyramid_meta_offset, header.pyramid_meta_len)?)
4196 .map_err(|_| FileError::Container("malformed pyramid meta"))?,
4197 )
4198 } else {
4199 None
4200 });
4201
4202 let (ti_off, ti_len) = header.text_index_payload();
4205 let text_index = resident_text_index_slot(
4206 if ti_len > 0 {
4207 Some(region(ti_off, ti_len)?)
4208 } else {
4209 None
4210 },
4211 header.block_codec,
4212 )?;
4213 let labels = resident_label_slot(&header, ®ion);
4214 let schema = resident_schema_slot(&header, ®ion);
4215
4216 let named_graphs = NamedGraphsSlot::resident(
4217 &header,
4218 |off, len| Ok(region(off, len)?.to_vec()),
4219 || {
4220 decode_named_graphs(
4221 region(header.named_graphs_offset, header.named_graphs_len)?,
4222 header.block_codec,
4223 header.perms,
4224 )
4225 },
4226 )?;
4227
4228 let metadata = if header.metadata_len > 0 {
4229 region(header.metadata_offset, header.metadata_len)?.to_vec()
4230 } else {
4231 Vec::new()
4232 };
4233
4234 let tile_ranges =
4235 tile_file_ranges(index_bytes, header.root_dir_offset, &index_section_ranges);
4236 Ok(Self {
4237 header,
4238 dict,
4239 index,
4240 index_section_ranges,
4241 tile_ranges,
4242 pyramid,
4243 text_index,
4244 labels,
4245 schema,
4246 named_graphs,
4247 metadata,
4248 service_client: None,
4249 service_error: std::sync::Mutex::new(None),
4250 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
4251 v06: None,
4252 })
4253 }
4254
4255 pub fn set_service_client(&mut self, client: Box<dyn crate::service::ServiceClient>) {
4260 self.service_client = Some(client);
4261 }
4262
4263 pub(crate) fn service_client(&self) -> Option<&dyn crate::service::ServiceClient> {
4264 self.service_client.as_deref()
4265 }
4266
4267 pub(crate) fn record_service_error(&self, msg: &str) {
4269 let mut e = self.service_error.lock().unwrap();
4270 if e.is_none() {
4271 *e = Some(msg.to_string());
4272 }
4273 }
4274
4275 pub(crate) fn take_service_error(&self) -> Option<String> {
4278 self.service_error.lock().unwrap().take()
4279 }
4280
4281 pub fn header(&self) -> &Header {
4282 &self.header
4283 }
4284
4285 pub fn file_layout(&self) -> Vec<LayoutSegment> {
4291 if let Some(f) = &self.v06 {
4292 return v06_layout(f);
4293 }
4294 let h = &self.header;
4295 let seg = |kind: &'static str, label: String, offset: u64, len: u64| LayoutSegment {
4296 kind,
4297 label,
4298 offset,
4299 len,
4300 };
4301 let mut out = vec![seg(
4307 "header",
4308 format!("header (fixed {} bytes)", crate::header::HEADER_LEN),
4309 0,
4310 crate::header::HEADER_LEN as u64,
4311 )];
4312 if h.metadata_len > 0 {
4313 out.push(seg(
4314 "metadata",
4315 "metadata (dataset card)".into(),
4316 h.metadata_offset,
4317 h.metadata_len,
4318 ));
4319 }
4320 out.push(seg(
4321 "dictionary",
4322 "dictionary (4 front-coded term sections)".into(),
4323 h.dictionary_offset,
4324 h.dictionary_len,
4325 ));
4326 for (si, perm) in crate::index::ALL_PERMS.into_iter().enumerate() {
4327 let sec = self.index_section_ranges[si];
4328 if sec.len == 0 {
4329 continue;
4330 }
4331 let first_tile = self.tile_ranges[si]
4332 .first()
4333 .map(|&(_, _, r)| r.offset)
4334 .unwrap_or(sec.offset + sec.len);
4335 if first_tile > sec.offset {
4336 out.push(seg(
4337 "directory",
4338 format!("{} tile directory", perm.name()),
4339 sec.offset,
4340 first_tile - sec.offset,
4341 ));
4342 }
4343 for (ti, &(min_a, max_a, r)) in self.tile_ranges[si].iter().enumerate() {
4344 out.push(seg(
4345 "tile",
4346 format!("{} tile {ti} (leading ids {min_a}..{max_a})", perm.name()),
4347 r.offset,
4348 r.len,
4349 ));
4350 }
4351 }
4352 if h.pyramid_meta_len > 0 {
4353 out.push(seg(
4354 "pyramid",
4355 "pyramid summary (communities + superedges)".into(),
4356 h.pyramid_meta_offset,
4357 h.pyramid_meta_len,
4358 ));
4359 }
4360 if h.named_graphs_len > 0 {
4361 out.push(seg(
4362 "named-graphs",
4363 format!("named graphs ({})", self.named_graphs.count()),
4364 h.named_graphs_offset,
4365 h.named_graphs_len,
4366 ));
4367 }
4368 if let Some(s) = h.label_index_section() {
4369 let label = match self.labels.get_if_loaded() {
4370 Some(li) => format!("label index ({} entries)", li.entry_count()),
4371 None => "label index".to_string(),
4372 };
4373 out.push(seg("label-index", label, s.offset, s.length));
4374 }
4375 if let Some(s) = h.schema_section() {
4376 out.push(seg(
4377 "schema",
4378 "schema (classes, query_stats, shapes)".into(),
4379 s.offset,
4380 s.length,
4381 ));
4382 }
4383 if let Some(q) = h.quad_index_section() {
4384 out.push(seg(
4385 "quad-index",
4386 format!("quad index ({} named graphs)", self.named_graphs.count()),
4387 q.offset,
4388 q.length,
4389 ));
4390 }
4391 out.sort_by_key(|s| s.offset);
4392 out
4393 }
4394
4395 pub fn metadata(&self) -> Option<&[u8]> {
4400 if self.metadata.is_empty() {
4401 None
4402 } else {
4403 Some(&self.metadata)
4404 }
4405 }
4406
4407 pub fn dictionary(&self) -> &Dictionary {
4408 &self.dict
4409 }
4410
4411 pub fn pyramid(&self) -> Option<&PyramidMeta> {
4413 match &self.pyramid {
4414 PyramidSlot::Resident(p) => p.as_ref(),
4415 PyramidSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
4417 }
4418 }
4419
4420 pub fn pyramid_if_loaded(&self) -> Option<&PyramidMeta> {
4425 match &self.pyramid {
4426 PyramidSlot::Resident(p) => p.as_ref(),
4427 PyramidSlot::Lazy { cell, .. } => cell.get().and_then(|o| o.as_ref()),
4428 }
4429 }
4430
4431 pub fn schema(&self) -> Option<&crate::schema_section::SchemaMeta> {
4437 self.schema.get()
4438 }
4439
4440 pub fn schema_or_pyramid(
4444 &self,
4445 ) -> Option<std::borrow::Cow<'_, crate::schema_section::SchemaMeta>> {
4446 if let Some(s) = self.schema() {
4447 return Some(std::borrow::Cow::Borrowed(s));
4448 }
4449 self.pyramid()
4450 .map(|p| std::borrow::Cow::Owned(crate::schema_section::SchemaMeta::from_pyramid(p)))
4451 }
4452
4453 pub fn predicate_stats(&self) -> &[crate::meta::PredStat] {
4458 if let Some(s) = self.schema().filter(|s| !s.predicate_stats.is_empty()) {
4459 return &s.predicate_stats;
4460 }
4461 self.pyramid_if_loaded()
4462 .map(|p| p.predicate_stats.as_slice())
4463 .unwrap_or(&[])
4464 }
4465
4466 pub(crate) fn planner_stats(&self, fault: bool) -> &[crate::meta::PredStat] {
4471 let schema = if fault {
4472 self.schema.get()
4473 } else {
4474 self.schema.get_if_loaded()
4475 };
4476 if let Some(s) = schema.filter(|s| !s.predicate_stats.is_empty()) {
4477 return &s.predicate_stats;
4478 }
4479 self.pyramid_if_loaded()
4480 .map(|p| p.predicate_stats.as_slice())
4481 .unwrap_or(&[])
4482 }
4483
4484 pub fn char_sets(&self) -> &[crate::meta::CharSet] {
4487 if let Some(s) = self.schema().filter(|s| !s.char_sets.is_empty()) {
4488 return &s.char_sets;
4489 }
4490 self.pyramid_if_loaded()
4491 .map(|p| p.char_sets.as_slice())
4492 .unwrap_or(&[])
4493 }
4494
4495 pub fn label_index(&self) -> &[crate::meta::LabelEntry] {
4501 self.pyramid_if_loaded()
4502 .map(|p| p.label_index.as_slice())
4503 .unwrap_or(&[])
4504 }
4505
4506 pub fn has_label_index(&self) -> bool {
4508 self.header.label_index_section().is_some()
4509 }
4510
4511 pub fn label_index_section(&self) -> Option<&crate::label_index::LabelIndex> {
4513 self.labels.get()
4514 }
4515
4516 pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
4523 if let Some(li) = self.labels.get() {
4524 return li.prefix_search(prefix, limit);
4525 }
4526 let Some(pyr) = self.pyramid() else {
4527 return Vec::new();
4528 };
4529 resolve_label_hits(&self.dict, pyr.prefix_search(prefix, limit))
4530 }
4531
4532 pub(crate) fn text_index(&self) -> Option<&crate::text_index::TextIndex> {
4535 self.text_index.index()
4536 }
4537
4538 pub fn has_text_index(&self) -> bool {
4541 self.header.text_index_len > 0
4542 }
4543
4544 pub fn text_index_scope(&self) -> Option<crate::text_index::TextIndexScope> {
4548 self.text_index().map(|ti| ti.scope().clone())
4549 }
4550
4551 pub fn text_index_token_table_len(&self) -> Option<u64> {
4566 self.text_index.token_table_len()
4567 }
4568
4569 pub fn text_search(&self, words: &[&str], prefix: Option<&str>, limit: usize) -> Vec<String> {
4579 text_search_in(self.text_index(), &self.dict, words, prefix, limit)
4580 }
4581
4582 pub fn default_index(&self) -> &GraphIndex {
4584 &self.index
4585 }
4586
4587 pub fn dump(&self, graph: Option<&str>) -> Vec<TermTriple> {
4589 let index = match graph {
4590 None => &self.index,
4591 Some(g) => match self.graph_index(g) {
4592 Some(i) => i,
4593 None => return Vec::new(),
4594 },
4595 };
4596 index
4597 .match_pattern((None, None, None))
4598 .into_iter()
4599 .filter_map(|(s, p, o)| {
4600 Some((
4601 self.dict.subject_term(s)?,
4602 self.dict.predicate_term(p)?,
4603 self.dict.object_term(o)?,
4604 ))
4605 })
4606 .collect()
4607 }
4608
4609 pub fn dump_each<F: FnMut(&str, &str, &str)>(&self, graph: Option<&str>, f: F) {
4625 self.dump_filtered_each(graph, None, None, None, f)
4626 }
4627
4628 pub fn dump_filtered_each<F: FnMut(&str, &str, &str)>(
4699 &self,
4700 graph: Option<&str>,
4701 s: Option<&str>,
4702 p: Option<&str>,
4703 o: Option<&str>,
4704 mut f: F,
4705 ) {
4706 let index = match graph {
4707 None => Some(&self.index),
4708 Some(g) => self.graph_index(g),
4709 };
4710 let Some(index) = index else { return };
4711 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
4714 return;
4715 };
4716 let dict = &self.dict;
4717 const WINDOW: usize = 4096;
4762 let cap = dict.cache_cap();
4773 let prefetch_window =
4774 cap == u64::MAX || cap >= (WINDOW as u64) * 2 * (DICT_CHUNK_BUDGET as u64);
4775 const PREFETCH_BATCH: usize = 64;
4782 let prefetch_batch = if cap == u64::MAX {
4783 usize::MAX
4784 } else {
4785 PREFETCH_BATCH
4786 };
4787 let mut ids: Vec<(u32, u32, u32)> = Vec::with_capacity(WINDOW);
4788 let mut nodes: Vec<u32> = Vec::with_capacity(WINDOW * 2);
4789 let mut preds: Vec<u32> = Vec::with_capacity(WINDOW);
4790 let mut resolver = crate::dictionary::WindowResolver::default();
4791 let mut flush = |ids: &mut Vec<(u32, u32, u32)>,
4792 resolver: &mut crate::dictionary::WindowResolver,
4793 f: &mut F| {
4794 if ids.is_empty() {
4795 return;
4796 }
4797 if prefetch_window {
4801 nodes.clear();
4802 preds.clear();
4803 for &(s, p, o) in ids.iter() {
4804 nodes.push(dict.subject_node(s));
4805 nodes.push(dict.object_node(o));
4806 preds.push(p);
4807 }
4808 dict.prefetch_terms_batched(&nodes, &preds, prefetch_batch);
4809 }
4810 dict.resolve_window(ids, resolver, |s, p, o| f(s, p, o));
4811 ids.clear();
4812 };
4813 for t in index.scan_iter(pattern) {
4814 ids.push(t);
4815 if ids.len() == WINDOW {
4816 flush(&mut ids, &mut resolver, &mut f);
4817 }
4818 }
4819 flush(&mut ids, &mut resolver, &mut f);
4820 }
4821
4822 pub fn dump_plan(
4830 &self,
4831 graph: Option<&str>,
4832 s: Option<&str>,
4833 p: Option<&str>,
4834 o: Option<&str>,
4835 ) -> DumpPlan {
4836 let index = match graph {
4837 None => Some(&self.index),
4838 Some(g) => self.graph_index(g),
4839 };
4840 let scan = index
4841 .zip(self.resolve_query_pattern(s, p, o))
4842 .map(|(ix, pattern)| ix.scan_plan(pattern));
4843 DumpPlan {
4844 scan,
4845 dictionary_bytes: match &self.v06 {
4846 Some(f) => f.section(crate::v06::kind::DICT_DATA).map_or(0, |r| r.len),
4847 None => self.header.dictionary_len,
4848 },
4849 }
4850 }
4851
4852 pub fn dump_iter(&self, graph: Option<&str>) -> impl Iterator<Item = TermTriple> + '_ {
4870 self.query_iter(graph, None, None, None)
4871 }
4872
4873 pub fn query_iter(
4884 &self,
4885 graph: Option<&str>,
4886 s: Option<&str>,
4887 p: Option<&str>,
4888 o: Option<&str>,
4889 ) -> impl Iterator<Item = TermTriple> + '_ {
4890 let index = match graph {
4891 None => Some(&self.index),
4892 Some(g) => self.graph_index(g),
4893 };
4894 index
4895 .zip(self.resolve_query_pattern(s, p, o))
4896 .into_iter()
4897 .flat_map(|(ix, pattern)| ix.scan_iter(pattern))
4898 .filter_map(move |(s, p, o)| {
4899 Some((
4900 self.dict.subject_term(s)?,
4901 self.dict.predicate_term(p)?,
4902 self.dict.object_term(o)?,
4903 ))
4904 })
4905 }
4906
4907 pub fn query_batch(
4926 &self,
4927 graph: Option<&str>,
4928 s: Option<&str>,
4929 p: Option<&str>,
4930 o: Option<&str>,
4931 cursor: u64,
4932 max_quads: usize,
4933 ) -> (Vec<TermTriple>, u64, bool) {
4934 let index = match graph {
4935 None => &self.index,
4936 Some(g) => match self.graph_index(g) {
4939 Some(i) => i,
4940 None => return (Vec::new(), cursor, true),
4941 },
4942 };
4943 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
4946 return (Vec::new(), cursor, true);
4947 };
4948 let (ids, next, done) = index.scan_batch(pattern, cursor, max_quads);
4949 if !ids.is_empty() {
4953 let mut nodes = Vec::with_capacity(ids.len() * 2);
4954 let mut preds = Vec::with_capacity(ids.len());
4955 for &(s, p, o) in &ids {
4956 nodes.push(self.dict.subject_node(s));
4957 nodes.push(self.dict.object_node(o));
4958 preds.push(p);
4959 }
4960 self.dict.prefetch_terms(&nodes, &preds);
4961 }
4962 let triples = ids
4963 .into_iter()
4964 .filter_map(|(s, p, o)| {
4965 Some((
4966 self.dict.subject_term(s)?,
4967 self.dict.predicate_term(p)?,
4968 self.dict.object_term(o)?,
4969 ))
4970 })
4971 .collect();
4972 (triples, next, done)
4973 }
4974
4975 pub fn dump_batch(
5001 &self,
5002 graph: Option<&str>,
5003 cursor: u32,
5004 max_quads: usize,
5005 ) -> (Vec<TermTriple>, u32, bool) {
5006 fn next_tile_start(tiles: &crate::index::TileList, sid: u32) -> Option<u32> {
5011 let i = tiles.first_max_ge(sid);
5012 (i < tiles.len()).then(|| tiles[i].leading_range().0.max(sid))
5013 }
5014
5015 let index = match graph {
5016 None => self.default_index(),
5017 Some(g) => match self.graph_index(g) {
5020 Some(i) => i,
5021 None => return (Vec::new(), cursor, true),
5022 },
5023 };
5024 let tiles = index.tile_sections()[IndexPermutation::Spo.section_index()];
5028 let (Some(first), Some(last)) = (
5029 tiles.first().map(|t| t.leading_range().0),
5030 tiles.last().map(|t| t.leading_range().1),
5031 ) else {
5032 return (Vec::new(), cursor, true); };
5034
5035 let mut sid = cursor.max(first);
5036 let mut probes = max_quads.saturating_mul(4).max(1 << 16);
5037 let mut ids: Vec<(u32, u32, u32)> = Vec::new();
5038 let mut exhausted = false;
5039 while sid <= last && probes > 0 {
5040 probes -= 1;
5041 let hits = index.match_pattern((Some(sid), None, None));
5042 if hits.is_empty() {
5043 match index.next_leading(IndexPermutation::Spo.section_index(), sid) {
5053 Some(next) if next > sid => {
5054 sid = next;
5055 continue;
5056 }
5057 None if !index.load_incomplete() => {
5058 exhausted = true;
5059 break;
5060 }
5061 _ => {}
5062 }
5063 match next_tile_start(tiles, sid) {
5064 None => {
5065 exhausted = true;
5066 break;
5067 }
5068 Some(next) if next > sid => {
5069 sid = next;
5070 continue;
5071 }
5072 Some(_) => {}
5073 }
5074 } else {
5075 ids.extend(hits);
5076 }
5077 if sid == u32::MAX {
5078 exhausted = true;
5079 break;
5080 }
5081 sid += 1;
5082 if ids.len() >= max_quads {
5083 break;
5084 }
5085 }
5086 let done = exhausted || sid > last;
5087
5088 let dict = self.dictionary();
5095 if !ids.is_empty() {
5096 let mut nodes = Vec::with_capacity(ids.len() * 2);
5097 let mut preds = Vec::with_capacity(ids.len());
5098 for &(s, p, o) in &ids {
5099 nodes.push(dict.subject_node(s));
5100 nodes.push(dict.object_node(o));
5101 preds.push(p);
5102 }
5103 dict.prefetch_terms(&nodes, &preds);
5104 }
5105 let triples = ids
5106 .into_iter()
5107 .filter_map(|(s, p, o)| {
5108 Some((
5109 dict.subject_term(s)?,
5110 dict.predicate_term(p)?,
5111 dict.object_term(o)?,
5112 ))
5113 })
5114 .collect();
5115 (triples, sid, done)
5116 }
5117
5118 pub fn named_graph_count(&self) -> usize {
5121 self.named_graphs.count()
5122 }
5123
5124 pub fn named_graph_quad_count(&self, i: usize) -> Option<u64> {
5129 match &self.named_graphs {
5130 NamedGraphsSlot::Quad(q) => q.quad_count_at(i),
5131 _ => None,
5132 }
5133 }
5134
5135 pub fn named_graph_transient(&self, i: usize) -> Option<GraphIndex> {
5144 match &self.named_graphs {
5145 NamedGraphsSlot::Quad(q) => q.transient_view(i, TRANSIENT_GRAPH_QUADS),
5146 _ => None,
5147 }
5148 }
5149
5150 pub(crate) fn named_graphs_with_subjects(
5157 &self,
5158 subjects: &std::collections::BTreeSet<u32>,
5159 ) -> Option<Vec<usize>> {
5160 match &self.named_graphs {
5161 NamedGraphsSlot::Quad(q) => q.graphs_with_subjects(subjects),
5162 _ => None,
5163 }
5164 }
5165
5166 pub fn named_quad_count(&self) -> Option<u64> {
5171 match &self.named_graphs {
5172 NamedGraphsSlot::Quad(q) => Some(q.quad_count()),
5173 _ => None,
5174 }
5175 }
5176
5177 pub(crate) fn index_triples(&self, index: &GraphIndex) -> u64 {
5183 index.size_hint().unwrap_or(self.header().quad_count)
5184 }
5185
5186 pub fn named_graph_name_at(&self, i: usize) -> Option<&str> {
5189 self.named_graphs.name_at(&self.dict, i, false)
5190 }
5191
5192 pub fn named_graph_at(&self, i: usize) -> Option<(&str, &GraphIndex)> {
5196 self.named_graphs.graph_at(&self.dict, i, false)
5197 }
5198
5199 pub(crate) fn named_graph_name_owned_demand(
5207 &self,
5208 i: usize,
5209 exhaustive: bool,
5210 ) -> Option<String> {
5211 self.named_graphs.name_owned(&self.dict, i, exhaustive)
5212 }
5213
5214 pub(crate) fn named_graph_ordinal(&self, iri: &str) -> Option<usize> {
5218 match &self.named_graphs {
5219 NamedGraphsSlot::Quad(q) => q.ordinal_of_iri(&self.dict, iri),
5220 _ => None,
5221 }
5222 }
5223
5224 pub(crate) fn named_graph_ordinal_of_id(&self, g: u32) -> Option<usize> {
5227 match &self.named_graphs {
5228 NamedGraphsSlot::Quad(q) => q.ordinal_of_id(g),
5229 _ => None,
5230 }
5231 }
5232
5233 pub(crate) fn named_quad_scan(
5238 &self,
5239 pat: (Option<u32>, Option<u32>, Option<u32>),
5240 ) -> Option<crate::quad::QuadScan> {
5241 match &self.named_graphs {
5242 NamedGraphsSlot::Quad(q) => q.scan(pat),
5243 _ => None,
5244 }
5245 }
5246
5247 pub(crate) fn named_graph_at_demand(
5250 &self,
5251 i: usize,
5252 exhaustive: bool,
5253 ) -> Option<(&str, &GraphIndex)> {
5254 self.named_graphs.graph_at(&self.dict, i, exhaustive)
5255 }
5256
5257 pub fn graph_names(&self) -> Vec<&str> {
5262 (0..self.named_graphs.count())
5263 .filter_map(|i| self.named_graphs.name_at(&self.dict, i, false))
5264 .collect()
5265 }
5266
5267 pub fn graph_index(&self, iri: &str) -> Option<&GraphIndex> {
5269 self.named_graphs.find(&self.dict, iri)
5270 }
5271
5272 pub fn match_ids(
5275 &self,
5276 pattern: (Option<u32>, Option<u32>, Option<u32>),
5277 ) -> Vec<(u32, u32, u32)> {
5278 self.index.match_pattern(pattern)
5279 }
5280
5281 pub fn predicate_pairs(&self, predicate: &str) -> Vec<(u32, u32)> {
5284 let pid = match self.dict.predicate_id(predicate) {
5285 Some(p) => p,
5286 None => return Vec::new(),
5287 };
5288 self.index
5289 .match_pattern((None, Some(pid), None))
5290 .into_iter()
5291 .map(|(s, _p, o)| (self.dict.subject_node(s), self.dict.object_node(o)))
5292 .collect()
5293 }
5294
5295 pub fn open_ranged<R: RangeReader>(reader: &R) -> Result<Self, FileError> {
5299 let head = reader.read_at(0, (HEADER_LEN as u64).min(reader.len()))?;
5300 if crate::v06::open::is_v06(&head) {
5301 let mut all = head;
5304 let at = all.len() as u64;
5305 if reader.len() > at {
5306 all.extend_from_slice(&reader.read_at(at, reader.len() - at)?);
5307 }
5308 return Self::open(&all);
5309 }
5310 let header = Header::from_bytes(&head)?;
5311
5312 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
5313 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
5314
5315 check_named_layout(&header)?;
5316 let index_bytes = reader.read_at(header.root_dir_offset, header.root_dir_len)?;
5317 let (index, _) = decode_root_index_container(&index_bytes, &header)?;
5318 let index_section_ranges = decode_index_section_ranges(
5319 &index_bytes,
5320 header.root_dir_offset,
5321 header.perms,
5322 header.index_container_sections(),
5323 )?;
5324
5325 let pyramid = PyramidSlot::Resident(if header.pyramid_meta_len > 0 {
5326 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
5327 Some(
5328 PyramidMeta::decode(&mb)
5329 .map_err(|_| FileError::Container("malformed pyramid meta"))?,
5330 )
5331 } else {
5332 None
5333 });
5334
5335 let (ti_off, ti_len) = header.text_index_payload();
5338 let text_index_bytes = if ti_len > 0 {
5339 Some(reader.read_at(ti_off, ti_len)?)
5340 } else {
5341 None
5342 };
5343 let text_index = resident_text_index_slot(text_index_bytes.as_deref(), header.block_codec)?;
5344 let labels = SectionSlot::Resident(header.label_index_section().and_then(|s| {
5347 let b = reader.read_at(s.offset, s.length).ok()?;
5348 crate::label_index::LabelIndex::from_section(&b, header.block_codec).ok()
5349 }));
5350 let schema = SectionSlot::Resident(read_schema_with_header(reader, &header));
5351
5352 let named_graphs = NamedGraphsSlot::resident(
5353 &header,
5354 |off, len| Ok(reader.read_at(off, len)?),
5355 || {
5356 let nb = reader.read_at(header.named_graphs_offset, header.named_graphs_len)?;
5357 decode_named_graphs(&nb, header.block_codec, header.perms)
5358 },
5359 )?;
5360
5361 let tile_ranges =
5365 tile_file_ranges(&index_bytes, header.root_dir_offset, &index_section_ranges);
5366 Ok(Self {
5367 header,
5368 dict,
5369 index,
5370 index_section_ranges,
5371 tile_ranges,
5372 pyramid,
5373 text_index,
5374 labels,
5375 schema,
5376 named_graphs,
5377 metadata: Vec::new(),
5378 service_client: None,
5379 service_error: std::sync::Mutex::new(None),
5380 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
5381 v06: None,
5382 })
5383 }
5384
5385 fn open_v06(
5391 reader: std::sync::Arc<dyn RangeReader + Send + Sync>,
5392 first: &[u8],
5393 budget: bool,
5394 ) -> Result<Self, FileError> {
5395 let f = std::sync::Arc::new(crate::v06::open::V06File::open(reader.clone(), first)?);
5396 let header = crate::v06::logical_header(&f.h);
5397 let rc = reader.concurrency();
5398 let dict = f.dictionary([true; 4])?;
5399 let index = f.index(rc)?;
5400 let (pyramid, text_index, labels, schema) = v06_slots(&f);
5401 let named_graphs = match f.quad(rc) {
5402 Some(q) => NamedGraphsSlot::Quad(q?),
5403 None => NamedGraphsSlot::Resident(Vec::new()),
5404 };
5405 let rete = Self {
5406 header,
5407 dict,
5408 index,
5409 index_section_ranges: [ByteRange::default(); NUM_PERMS],
5410 tile_ranges: Default::default(),
5411 pyramid,
5412 text_index,
5413 labels,
5414 schema,
5415 named_graphs,
5416 metadata: Vec::new(),
5417 service_client: None,
5418 service_error: std::sync::Mutex::new(None),
5419 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
5420 v06: Some(f),
5421 };
5422 #[cfg(not(target_arch = "wasm32"))]
5423 if budget {
5424 rete.set_memory_budget(match crate::sysmem::default_query_budget_mb() {
5425 0 => None,
5426 mb => Some(mb << 20),
5427 });
5428 }
5429 #[cfg(target_arch = "wasm32")]
5430 let _ = budget;
5431 Ok(rete)
5432 }
5433
5434 pub fn v06(&self) -> Option<&crate::v06::open::V06File> {
5436 self.v06.as_deref()
5437 }
5438
5439 pub fn open_ranged_lazy<R: RangeReader + Send + Sync + 'static>(
5451 reader: R,
5452 ) -> Result<Self, FileError> {
5453 Self::open_ranged_lazy_with(reader, crate::v06::FIRST_READ)
5454 }
5455
5456 pub fn open_ranged_lazy_with<R: RangeReader + Send + Sync + 'static>(
5461 reader: R,
5462 first_read: u64,
5463 ) -> Result<Self, FileError> {
5464 let first_len = first_read.max(HEADER_LEN as u64).min(reader.len());
5467 let first: std::sync::Arc<[u8]> = std::sync::Arc::from(reader.read_at(0, first_len)?);
5468 #[cfg(feature = "v06-parity")]
5469 if !crate::v06::open::is_v06(&first) {
5470 let all = reader.read_at(0, reader.len())?;
5471 if let Some(v6) = parity_image(&all) {
5472 let r: std::sync::Arc<dyn RangeReader + Send + Sync> =
5473 std::sync::Arc::new(crate::reader::BytesReader(std::sync::Arc::from(v6)));
5474 let head = r.read_at(0, crate::v06::FIRST_READ.min(r.len()))?;
5475 return Self::open_v06(r, &head, true);
5476 }
5477 }
5478 let reader = crate::reader::PrefixReader::new(reader, first.clone());
5479 if crate::v06::open::is_v06(&first) {
5480 let r: std::sync::Arc<dyn RangeReader + Send + Sync> = std::sync::Arc::new(reader);
5481 return Self::open_v06(r, &first, true);
5482 }
5483 let head = reader.read_at(0, HEADER_LEN as u64)?;
5484 let header = Header::from_bytes(&head)?;
5485 check_named_layout(&header)?;
5486 let reader = std::sync::Arc::new(reader);
5487 let read_concurrency = reader.concurrency();
5490
5491 let open_dict = || ranged_chunked_dictionary(&reader, &header, [true; 4]);
5495
5496 let reader_dyn: std::sync::Arc<dyn RangeReader + Send + Sync> = reader.clone();
5500 let open_index = || {
5501 open_index_container_lazy(
5502 &reader_dyn,
5503 ByteRange {
5504 offset: header.root_dir_offset,
5505 len: header.root_dir_len,
5506 },
5507 header.block_codec,
5508 header.has_tile_synopsis(),
5509 read_concurrency,
5510 header.perms,
5511 header.index_container_sections(),
5512 )
5513 };
5514
5515 let (dict, index) = open_side_by_side(read_concurrency, open_dict, open_index);
5522 let dict = dict?;
5523 let (index, index_section_ranges, tile_ranges) = index?;
5524
5525 let pyramid = ranged_pyramid_slot(&reader, &header);
5529
5530 let text_index = ranged_text_index_slot(&reader, &header);
5535
5536 let labels = ranged_label_slot(&reader, &header);
5540 let schema = ranged_schema_slot(&reader, &header);
5541
5542 let named_graphs = if let Some(q) = header.quad_index_section() {
5549 NamedGraphsSlot::Quad(crate::quad::QuadNamed::new(
5553 reader_dyn,
5554 ByteRange {
5555 offset: q.offset,
5556 len: q.length,
5557 },
5558 header.block_codec,
5559 header.perms,
5560 read_concurrency,
5561 ))
5562 } else if header.named_graphs_len > 0 {
5563 NamedGraphsSlot::Lazy(LazyNamedGraphs::new(
5564 reader_dyn,
5565 ByteRange {
5566 offset: header.named_graphs_offset,
5567 len: header.named_graphs_len,
5568 },
5569 header.block_codec,
5570 header.has_tile_synopsis(),
5571 read_concurrency,
5572 header.perms,
5573 ))
5574 } else {
5575 NamedGraphsSlot::Resident(Vec::new())
5576 };
5577
5578 let rete = Self {
5579 header,
5580 dict,
5581 index,
5582 index_section_ranges,
5583 tile_ranges,
5584 pyramid,
5585 text_index,
5586 labels,
5587 schema,
5588 named_graphs,
5589 metadata: Vec::new(),
5590 service_client: None,
5591 service_error: std::sync::Mutex::new(None),
5592 spill_threshold: std::sync::atomic::AtomicU64::new(u64::MAX),
5593 v06: None,
5594 };
5595 #[cfg(not(target_arch = "wasm32"))]
5607 rete.set_memory_budget(match crate::sysmem::default_query_budget_mb() {
5608 0 => None,
5609 mb => Some(mb << 20),
5610 });
5611 Ok(rete)
5612 }
5613
5614 pub fn index_incomplete(&self) -> bool {
5619 self.index.load_incomplete()
5620 || self.dict.load_incomplete()
5621 || self.named_graphs.load_incomplete()
5622 }
5623
5624 pub fn reset_load_failures(&self) {
5632 self.index.reset_load_failure();
5633 self.dict.reset_load_failure();
5634 self.named_graphs.reset_load_failures();
5635 }
5636
5637 pub fn set_memory_budget(&self, budget: Option<u64>) -> MemoryBudget {
5668 let total = budget.unwrap_or(u64::MAX);
5669 self.set_spill_threshold(budget.map(spill_threshold_for));
5670 let mut split = split_memory_budget(total);
5671 let fixed = if total == u64::MAX {
5672 0
5673 } else {
5674 self.fixed_overhead().total()
5675 };
5676 if split.dict_cache != u64::MAX {
5677 split.dict_cache = split
5678 .dict_cache
5679 .saturating_sub(fixed)
5680 .max(MIN_DICT_CACHE_CAP);
5681 }
5682 self.dict.set_cache_cap(split.dict_cache);
5683 self.index.set_cache_cap(split.tile_cache);
5684 match &self.named_graphs {
5689 NamedGraphsSlot::Lazy(lazy) => lazy.set_tile_cap(split.tile_cache),
5690 NamedGraphsSlot::Quad(q) => q.set_tile_cap(split.tile_cache),
5693 NamedGraphsSlot::Resident(_) => {}
5694 }
5695 if std::env::var("RETE_OPEN_DEBUG").is_ok() {
5696 eprintln!(
5697 "[memory-budget] total={} dict_cache={} tile_cache={} block_cache={} fixed={:.1}MiB",
5698 if total == u64::MAX {
5699 "unlimited".to_string()
5700 } else {
5701 format!("{}MiB", total >> 20)
5702 },
5703 fmt_cap(split.dict_cache),
5704 fmt_cap(split.tile_cache),
5705 fmt_cap(split.block_cache),
5706 fixed as f64 / (1u64 << 20) as f64,
5707 );
5708 }
5709 split
5710 }
5711
5712 pub fn spill_threshold(&self) -> u64 {
5719 self.spill_threshold
5720 .load(std::sync::atomic::Ordering::Relaxed)
5721 }
5722
5723 pub fn set_spill_threshold(&self, bytes: Option<u64>) {
5727 self.spill_threshold.store(
5728 bytes.unwrap_or(u64::MAX),
5729 std::sync::atomic::Ordering::Relaxed,
5730 );
5731 }
5732
5733 pub fn dict_cache_stats(&self) -> crate::chunk_cache::CacheStats {
5737 self.dict.cache_stats()
5738 }
5739
5740 pub fn fixed_overhead(&self) -> FixedOverhead {
5744 FixedOverhead {
5745 dict_directory: self.dict.directory_bytes(),
5746 index_directory: self.index.directory_bytes(),
5747 named_graphs: self.named_graphs.directory_bytes(),
5748 }
5749 }
5750
5751 pub fn index_cache_stats(&self) -> crate::chunk_cache::CacheStats {
5753 self.index.cache_stats()
5754 }
5755
5756 pub fn dict_cache_cap(&self) -> u64 {
5758 self.dict.cache_cap()
5759 }
5760
5761 pub fn release_named_graph(&mut self, iri: &str) {
5774 match &mut self.named_graphs {
5775 NamedGraphsSlot::Lazy(lazy) => lazy.release(iri),
5776 NamedGraphsSlot::Quad(q) => q.release(&self.dict, iri),
5777 NamedGraphsSlot::Resident(_) => {}
5778 }
5779 }
5780
5781 fn resolve_query_pattern(
5782 &self,
5783 s: Option<&str>,
5784 p: Option<&str>,
5785 o: Option<&str>,
5786 ) -> Option<Pattern> {
5787 let sid = match s {
5788 Some(t) => match self.dict.subject_id(t) {
5789 Some(id) => Some(id),
5790 None => return None,
5791 },
5792 None => None,
5793 };
5794 let pid = match p {
5795 Some(t) => match self.dict.predicate_id(t) {
5796 Some(id) => Some(id),
5797 None => return None,
5798 },
5799 None => None,
5800 };
5801 let oid = match o {
5802 Some(t) => match self.dict.object_id(t) {
5803 Some(id) => Some(id),
5804 None => return None,
5805 },
5806 None => None,
5807 };
5808 Some((sid, pid, oid))
5809 }
5810
5811 pub fn query_with_provenance(
5815 &self,
5816 s: Option<&str>,
5817 p: Option<&str>,
5818 o: Option<&str>,
5819 ) -> Vec<TripleProvenance> {
5820 let pattern = match self.resolve_query_pattern(s, p, o) {
5821 Some(pattern) => pattern,
5822 None => return Vec::new(),
5823 };
5824
5825 let index_permutation = GraphIndex::best_permutation_in(self.header.perms, pattern);
5826 let si = index_permutation.section_index();
5827 let (dictionary_range, index_range, index_section_range, pyramid_range) = match &self.v06 {
5828 Some(f) => {
5829 use crate::v06::kind;
5830 let sec = |k| f.section(k).unwrap_or_default();
5831 let perm = f
5832 .root
5833 .unit(crate::v06::tag::INDEX)
5834 .and_then(|u| crate::v06::decode_index_root(u).ok())
5835 .and_then(|v| v.into_iter().find(|p| p.si == si))
5836 .map(|p| ByteRange {
5837 offset: sec(kind::INDEX_DATA).offset + p.data_off,
5838 len: p.data_len,
5839 })
5840 .unwrap_or_default();
5841 (
5842 sec(kind::DICT_DATA),
5843 sec(kind::INDEX_DATA),
5844 perm,
5845 f.section(kind::PYRAMID),
5846 )
5847 }
5848 None => (
5849 ByteRange {
5850 offset: self.header.dictionary_offset,
5851 len: self.header.dictionary_len,
5852 },
5853 ByteRange {
5854 offset: self.header.root_dir_offset,
5855 len: self.header.root_dir_len,
5856 },
5857 self.index_section_ranges[si],
5858 (self.header.pyramid_meta_len > 0).then_some(ByteRange {
5859 offset: self.header.pyramid_meta_offset,
5860 len: self.header.pyramid_meta_len,
5861 }),
5862 ),
5863 };
5864
5865 let tiles = &self.tile_ranges[si];
5866 let v06_tiles = self.v06.is_some().then(|| self.index.tile_sections()[si]);
5867 self.index
5868 .match_pattern(pattern)
5869 .into_iter()
5870 .filter_map(|(s, p, o)| {
5871 let terms = (
5872 self.dict.subject_term(s)?,
5873 self.dict.predicate_term(p)?,
5874 self.dict.object_term(o)?,
5875 );
5876 let a = index_permutation.forward((s, p, o)).0;
5879 let (tile, tile_range) = if let Some(tl) = v06_tiles {
5880 let ti = tl.first_max_ge(a);
5881 match (ti < tl.len() && tl[ti].leading_range().0 <= a)
5882 .then(|| tl.range(ti))
5883 .flatten()
5884 {
5885 Some((offset, len)) => (
5886 Some(format!("{}/{ti}", index_permutation.name())),
5887 Some(ByteRange { offset, len }),
5888 ),
5889 None => (None, None),
5890 }
5891 } else {
5892 let ti = tiles.partition_point(|&(_, max_a, _)| max_a < a);
5893 match tiles.get(ti) {
5894 Some(&(min_a, _, range)) if min_a <= a => (
5895 Some(format!("{}/{ti}", index_permutation.name())),
5896 Some(range),
5897 ),
5898 _ => (None, None),
5899 }
5900 };
5901 Some(TripleProvenance {
5902 terms,
5903 ids: (s, p, o),
5904 graph: None,
5905 matched_pattern: pattern,
5906 index_permutation,
5907 dictionary_range,
5908 index_range,
5909 index_section_range,
5910 pyramid_range,
5911 tile,
5912 tile_range,
5913 })
5914 })
5915 .collect()
5916 }
5917
5918 pub fn query(&self, s: Option<&str>, p: Option<&str>, o: Option<&str>) -> Vec<TermTriple> {
5922 self.query_with_provenance(s, p, o)
5923 .into_iter()
5924 .map(|m| m.terms)
5925 .collect()
5926 }
5927
5928 pub fn query_in_graph(
5936 &self,
5937 graph: Option<&str>,
5938 s: Option<&str>,
5939 p: Option<&str>,
5940 o: Option<&str>,
5941 ) -> Vec<TermTriple> {
5942 let pattern = match self.resolve_query_pattern(s, p, o) {
5943 Some(pattern) => pattern,
5944 None => return Vec::new(),
5945 };
5946 let index = match graph {
5947 None => &self.index,
5948 Some(g) => match self.graph_index(g) {
5949 Some(i) => i,
5950 None => return Vec::new(),
5951 },
5952 };
5953 self.resolve_matches(index.match_pattern(pattern))
5954 }
5955
5956 fn resolve_matches(&self, ids: Vec<Triple>) -> Vec<TermTriple> {
5959 if !ids.is_empty() {
5967 let mut nodes = Vec::with_capacity(ids.len() * 2);
5968 let mut preds = Vec::with_capacity(ids.len());
5969 for &(s, p, o) in &ids {
5970 nodes.push(self.dict.subject_node(s));
5971 nodes.push(self.dict.object_node(o));
5972 preds.push(p);
5973 }
5974 self.dict.prefetch_terms(&nodes, &preds);
5975 }
5976 ids.into_iter()
5977 .filter_map(|(s, p, o)| {
5978 Some((
5979 self.dict.subject_term(s)?,
5980 self.dict.predicate_term(p)?,
5981 self.dict.object_term(o)?,
5982 ))
5983 })
5984 .collect()
5985 }
5986
5987 pub fn query_quads(
5992 &self,
5993 s: Option<&str>,
5994 p: Option<&str>,
5995 o: Option<&str>,
5996 ) -> Vec<(TermTriple, Option<String>)> {
5997 let mut out: Vec<(TermTriple, Option<String>)> = self
5998 .query_in_graph(None, s, p, o)
5999 .into_iter()
6000 .map(|t| (t, None))
6001 .collect();
6002 let Some(pattern) = self.resolve_query_pattern(s, p, o) else {
6003 return out;
6004 };
6005 for i in 0..self.named_graphs.count() {
6006 let Some(iri) = self.named_graphs.name_at(&self.dict, i, true) else {
6009 continue;
6010 };
6011 let owned = self.named_graph_transient(i);
6014 let index = match &owned {
6015 Some(view) => view,
6016 None => match self.named_graphs.graph_at(&self.dict, i, true) {
6017 Some((_, g)) => g,
6018 None => continue,
6019 },
6020 };
6021 for triple in self.resolve_matches(index.match_pattern(pattern)) {
6022 out.push((triple, Some(iri.to_string())));
6023 }
6024 }
6025 out
6026 }
6027
6028 pub fn query_ranged<R: RangeReader>(
6035 reader: &R,
6036 s: Option<&str>,
6037 p: Option<&str>,
6038 o: Option<&str>,
6039 ) -> Result<Vec<TermTriple>, FileError> {
6040 let head = reader.read_at(0, (HEADER_LEN as u64).min(reader.len()))?;
6041 if crate::v06::open::is_v06(&head) {
6042 return Ok(v06_query_ranged(reader, head, s, p, o, true)?.unwrap_or_default());
6043 }
6044 let routed = match route_pattern(reader, head, s, p, o)? {
6045 Some(routed) => routed,
6046 None => return Ok(Vec::new()),
6047 };
6048 let matches = fetch_routed_matches(reader, &routed)?;
6049 Ok(matches
6050 .into_iter()
6051 .filter_map(|(s, p, o)| {
6052 Some((
6053 routed.dict.subject_term(s)?,
6054 routed.dict.predicate_term(p)?,
6055 routed.dict.object_term(o)?,
6056 ))
6057 })
6058 .collect())
6059 }
6060
6061 pub fn route_pattern_ranged<R: RangeReader>(
6065 reader: &R,
6066 s: Option<&str>,
6067 p: Option<&str>,
6068 o: Option<&str>,
6069 ) -> Result<bool, FileError> {
6070 let head = reader.read_at(0, (HEADER_LEN as u64).min(reader.len()))?;
6071 if crate::v06::open::is_v06(&head) {
6072 return Ok(v06_query_ranged(reader, head, s, p, o, false)?.is_some());
6073 }
6074 Ok(route_pattern(reader, head, s, p, o)?.is_some())
6075 }
6076}
6077
6078fn v06_query_ranged<R: RangeReader>(
6084 reader: &R,
6085 head: Vec<u8>,
6086 s: Option<&str>,
6087 p: Option<&str>,
6088 o: Option<&str>,
6089 fetch: bool,
6090) -> Result<Option<Vec<TermTriple>>, FileError> {
6091 use crate::v06::{kind, open::SparseReader, Header06, Root06, HEADER_LEN};
6092 let len = reader.len();
6093 let h = Header06::from_bytes(&head)?;
6094 let root_end = (HEADER_LEN as u64 + h.root_len as u64).min(len);
6095 let root_bytes = if root_end <= head.len() as u64 {
6096 head[HEADER_LEN..root_end as usize].to_vec()
6097 } else {
6098 reader.read_at(HEADER_LEN as u64, root_end - HEADER_LEN as u64)?
6099 };
6100 let root = Root06::decode(&root_bytes, h.internal_codec)?;
6101 let dict = root.section(kind::DICT_DATA).and_then(|d| d.abs(&h).ok());
6102 let end = dict
6105 .map_or(h.data_offset, |d| d.offset + d.len)
6106 .max(crate::header::HEADER_LEN as u64)
6107 .min(len);
6108 let mut held = head;
6109 if end > held.len() as u64 {
6110 let at = held.len() as u64;
6111 held.extend_from_slice(&reader.read_at(at, end - at)?);
6112 }
6113 let sparse = SparseReader::fetch(reader, held, &[])?;
6114 let rete = Rete::open_ranged_lazy_with(sparse, 0)?;
6115 let Some(pattern) = resolve_query_pattern(rete.dictionary(), s, p, o) else {
6116 return Ok(None);
6117 };
6118 if !fetch {
6119 return Ok(Some(Vec::new()));
6120 }
6121 let perm = GraphIndex::best_permutation_in(h.perms(), pattern);
6122 let si = perm.section_index();
6123 let index = rete.default_index();
6124 let list = index.tile_sections()[si];
6125 let n = list.len();
6126 let [pa, _, _] = perm.order_pattern(pattern);
6127 let (start, stop) = match pa {
6128 Some(a) => {
6129 let start = list.first_max_ge(a);
6130 let mut stop = start;
6131 while stop < n {
6132 list.ensure_span(stop, stop + 1);
6133 if list[stop].leading_range().0 > a {
6134 break;
6135 }
6136 stop += 1;
6137 }
6138 (start, stop)
6139 }
6140 None => {
6141 list.ensure_span(0, n);
6142 (0, n)
6143 }
6144 };
6145 let ranges: Vec<(u64, u64)> = (start..stop)
6146 .filter_map(|i| index.tile_range(si, i))
6147 .collect();
6148 let mut out = Vec::new();
6149 if let (Some(&(lo, _)), Some(&(last, last_len))) = (ranges.first(), ranges.last()) {
6150 let body = reader.read_at(lo, last + last_len - lo)?;
6152 for &(off, l) in &ranges {
6153 let at = (off - lo) as usize;
6154 let tile = decompress(h.block_codec, &body[at..at + l as usize])?;
6155 out.extend(GraphIndex::match_serialized_block(&tile, perm, pattern));
6156 }
6157 }
6158 out.sort_unstable();
6159 let d = rete.dictionary();
6160 Ok(Some(
6161 out.into_iter()
6162 .filter_map(|(s, p, o)| {
6163 Some((d.subject_term(s)?, d.predicate_term(p)?, d.object_term(o)?))
6164 })
6165 .collect(),
6166 ))
6167}
6168
6169struct RoutedPattern {
6172 dict: Dictionary,
6173 pattern: Pattern,
6174 permutation: IndexPermutation,
6175 header: Header,
6176 section: ByteRange,
6178}
6179
6180fn route_pattern<R: RangeReader>(
6183 reader: &R,
6184 head: Vec<u8>,
6185 s: Option<&str>,
6186 p: Option<&str>,
6187 o: Option<&str>,
6188) -> Result<Option<RoutedPattern>, FileError> {
6189 let header = Header::from_bytes(&head)?;
6190
6191 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
6192 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
6193
6194 let Some(pattern) = resolve_query_pattern(&dict, s, p, o) else {
6195 return Ok(None);
6196 };
6197 let permutation = GraphIndex::best_permutation_in(header.perms, pattern);
6198 let section = locate_container_section_ranged(
6199 reader,
6200 header.root_dir_offset,
6201 header.root_dir_len,
6202 header
6203 .perms
6204 .position(permutation)
6205 .ok_or(FileError::Container("routed to an absent permutation"))?,
6206 header.index_container_sections() as u64,
6207 )?;
6208 Ok(Some(RoutedPattern {
6209 dict,
6210 pattern,
6211 permutation,
6212 header,
6213 section,
6214 }))
6215}
6216
6217fn fetch_routed_matches<R: RangeReader>(
6222 reader: &R,
6223 routed: &RoutedPattern,
6224) -> Result<Vec<Triple>, FileError> {
6225 let dir = read_tile_directory_ranged(reader, routed.section)?;
6226 let [pa, _, _] = routed.permutation.order_pattern(routed.pattern);
6227 let codec = routed.header.block_codec;
6228 let mut out = Vec::new();
6229 match pa {
6230 Some(a) => {
6233 for e in dir.iter().filter(|e| e.min_a <= a && a <= e.max_a) {
6234 let len = e.end - e.start;
6235 let bytes = reader.read_at(checked_end(routed.section.offset, e.start)?, len)?;
6236 if bytes.len() as u64 != len {
6237 return Err(FileError::Container("short tile read"));
6238 }
6239 let tile = decompress(codec, &bytes)?;
6240 out.extend(GraphIndex::match_serialized_block(
6241 &tile,
6242 routed.permutation,
6243 routed.pattern,
6244 ));
6245 }
6246 }
6247 None => {
6250 if let (Some(first), Some(last)) = (dir.first(), dir.last()) {
6251 let base = first.start;
6252 let body =
6253 reader.read_at(checked_end(routed.section.offset, base)?, last.end - base)?;
6254 for e in &dir {
6255 let stored = checked_slice(&body, e.start - base, e.end - e.start)
6257 .ok_or(FileError::Container("short tile read"))?;
6258 let tile = decompress(codec, stored)?;
6259 out.extend(GraphIndex::match_serialized_block(
6260 &tile,
6261 routed.permutation,
6262 routed.pattern,
6263 ));
6264 }
6265 }
6266 }
6267 }
6268 out.sort_unstable();
6269 Ok(out)
6270}
6271
6272fn resolve_query_pattern(
6273 dict: &Dictionary,
6274 s: Option<&str>,
6275 p: Option<&str>,
6276 o: Option<&str>,
6277) -> Option<Pattern> {
6278 let sid = match s {
6279 Some(t) => Some(dict.subject_id(t)?),
6280 None => None,
6281 };
6282 let pid = match p {
6283 Some(t) => Some(dict.predicate_id(t)?),
6284 None => None,
6285 };
6286 let oid = match o {
6287 Some(t) => Some(dict.object_id(t)?),
6288 None => None,
6289 };
6290 Some((sid, pid, oid))
6291}
6292
6293fn ranged_chunked_dictionary<R: RangeReader + Send + Sync + 'static>(
6308 reader: &std::sync::Arc<R>,
6309 header: &Header,
6310 want: [bool; 4],
6311) -> Result<Dictionary, FileError> {
6312 let cache = crate::chunk_cache::ChunkCache::unlimited_arc();
6315 let mut dict_sections: Vec<crate::dict::ChunkedSection> = Vec::with_capacity(4);
6316 let mut section_ranges: [std::sync::Arc<Vec<ByteRange>>; 4] = Default::default();
6317 for si in 0..4 {
6318 if !want[si] {
6319 dict_sections.push(crate::dict::ChunkedSection::from_parts(
6320 crate::dict::SectionMeta {
6321 term_count: 0,
6322 restart_interval: 1,
6323 restart_offsets: Vec::new(),
6324 },
6325 Vec::new(),
6326 None,
6327 cache.clone(),
6328 si as u8,
6329 ));
6330 continue;
6331 }
6332 let section = locate_container_section_ranged(
6333 reader.as_ref(),
6334 header.dictionary_offset,
6335 header.dictionary_len,
6336 si,
6337 4,
6338 )?;
6339 let (meta, entries) = read_dict_dir_ranged(reader.as_ref(), section)?;
6340 let ranges: std::sync::Arc<Vec<ByteRange>> = std::sync::Arc::new(
6343 entries
6344 .iter()
6345 .map(|e| ByteRange {
6346 offset: section.offset + e.start,
6347 len: (e.end - e.start),
6348 })
6349 .collect(),
6350 );
6351 section_ranges[si] = ranges.clone();
6352 let chunks: Vec<crate::dict::SectionChunk> = entries
6353 .into_iter()
6354 .map(|e| crate::dict::SectionChunk::new(e.first_run, e.key, e.body_start))
6355 .collect();
6356 let chunk_reader = reader.clone();
6357 let codec = header.dict_codec;
6358 let loader_ranges = ranges.clone();
6359 let loader: crate::dict::ChunkLoader = Box::new(move |ci| {
6360 let range = loader_ranges.get(ci)?;
6361 let bytes = chunk_reader.read_at(range.offset, range.len).ok()?;
6362 decompress(codec, &bytes).ok()
6363 });
6364 let bulk_reader = reader.clone();
6367 let bulk: crate::dict::ChunkBulkLoader = Box::new(move |cis| {
6368 let want: Option<Vec<ByteRange>> =
6369 cis.iter().map(|&ci| ranges.get(ci).copied()).collect();
6370 let blobs = read_coalesced(bulk_reader.as_ref(), &want?, DICT_COALESCE_GAP)?;
6371 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
6372 });
6373 dict_sections.push(
6374 crate::dict::ChunkedSection::from_parts(
6375 meta,
6376 chunks,
6377 Some(loader),
6378 cache.clone(),
6379 si as u8,
6380 )
6381 .with_bulk_loader(bulk),
6382 );
6383 }
6384 let dict_arr: [crate::dict::ChunkedSection; 4] = dict_sections
6385 .try_into()
6386 .map_err(|_| FileError::Container("expected 4 dictionary sections"))?;
6387 let multi_reader = reader.clone();
6391 let codec = header.dict_codec;
6392 let multi: crate::dict::MultiChunkBulkLoader = Box::new(move |pairs| {
6393 let want: Option<Vec<ByteRange>> = pairs
6394 .iter()
6395 .map(|&(si, ci)| section_ranges.get(si as usize)?.get(ci).copied())
6396 .collect();
6397 let blobs = read_coalesced(multi_reader.as_ref(), &want?, DICT_COALESCE_GAP)?;
6398 blobs.iter().map(|b| decompress(codec, b).ok()).collect()
6399 });
6400 Ok(Dictionary::from_chunked_sections(dict_arr).with_multi_bulk_loader(multi))
6401}
6402
6403#[cfg(not(target_arch = "wasm32"))]
6408fn open_side_by_side<A, B: Send>(
6409 concurrency: usize,
6410 a: impl FnOnce() -> A,
6411 b: impl FnOnce() -> B + Send,
6412) -> (A, B) {
6413 if concurrency <= 1 {
6414 return (a(), b());
6415 }
6416 std::thread::scope(|scope| {
6417 let hb = scope.spawn(b);
6418 let ra = a();
6419 let rb = hb
6420 .join()
6421 .unwrap_or_else(|panic| std::panic::resume_unwind(panic));
6422 (ra, rb)
6423 })
6424}
6425
6426#[cfg(target_arch = "wasm32")]
6428fn open_side_by_side<A, B>(
6429 _concurrency: usize,
6430 a: impl FnOnce() -> A,
6431 b: impl FnOnce() -> B,
6432) -> (A, B) {
6433 (a(), b())
6434}
6435
6436#[cfg(feature = "v06-parity")]
6439fn parity_image(bytes: &[u8]) -> Option<Vec<u8>> {
6440 if crate::v06::open::is_v06(bytes) || bytes.len() < HEADER_LEN {
6441 return None;
6442 }
6443 crate::v06::write::convert_image(bytes, crate::v06::write::WriteOpts::default()).ok()
6444}
6445
6446fn v06_layout(f: &crate::v06::open::V06File) -> Vec<LayoutSegment> {
6449 let h = &f.h;
6450 let seg = |kind: &'static str, label: String, offset: u64, len: u64| LayoutSegment {
6451 kind,
6452 label,
6453 offset,
6454 len,
6455 };
6456 let mut out = vec![
6457 seg(
6458 "header",
6459 format!("header (fixed {} bytes)", crate::v06::HEADER_LEN),
6460 0,
6461 crate::v06::HEADER_LEN as u64,
6462 ),
6463 seg(
6464 "directory",
6465 "root block (section table, directory roots)".into(),
6466 h.root_offset(),
6467 h.root_len as u64,
6468 ),
6469 ];
6470 let c = h.card_range();
6471 if c.len > 0 {
6472 out.push(seg(
6473 "metadata",
6474 "metadata (dataset card, zstd)".into(),
6475 c.offset,
6476 c.len,
6477 ));
6478 }
6479 let b = h.build_range();
6480 if b.len > 0 {
6481 out.push(seg("build-info", "build record".into(), b.offset, b.len));
6482 }
6483 for s in &f.root.sections {
6484 let Ok(a) = s.abs(h) else { continue };
6485 let kind: &'static str = match s.kind {
6486 crate::v06::kind::SCHEMA => "schema",
6487 crate::v06::kind::DICT_DATA => "dictionary",
6488 crate::v06::kind::INDEX_DATA => "tile",
6489 crate::v06::kind::QUAD_DATA => "named-graphs",
6490 crate::v06::kind::LABEL_DATA => "label-index",
6491 crate::v06::kind::TEXT_DATA => "text-index",
6492 crate::v06::kind::PYRAMID => "pyramid",
6493 _ => "directory",
6494 };
6495 out.push(seg(
6496 kind,
6497 crate::v06::kind::name(s.kind).to_string(),
6498 a.offset,
6499 a.len,
6500 ));
6501 }
6502 out.sort_by_key(|s| s.offset);
6503 out
6504}
6505
6506#[allow(clippy::type_complexity)]
6509fn v06_slots(
6510 f: &std::sync::Arc<crate::v06::open::V06File>,
6511) -> (
6512 PyramidSlot,
6513 TextIndexSlot,
6514 SectionSlot<crate::label_index::LabelIndex>,
6515 SectionSlot<crate::schema_section::SchemaMeta>,
6516) {
6517 use crate::v06::{P_LABELS, P_PYRAMID, P_SCHEMA, P_TEXT};
6518 let pyramid = if f.h.has(P_PYRAMID) {
6519 let a = f.clone();
6520 PyramidSlot::Lazy {
6521 loader: Box::new(move || PyramidMeta::decode(&a.pyramid_bytes()?).ok()),
6522 cell: std::sync::OnceLock::new(),
6523 }
6524 } else {
6525 PyramidSlot::Resident(None)
6526 };
6527 let text_index = if f.h.has(P_TEXT) {
6528 let (a, b) = (f.clone(), f.clone());
6529 TextIndexSlot::Lazy {
6530 loader: Box::new(move || a.text_index()?.ok()),
6531 cell: std::sync::OnceLock::new(),
6532 token_table: Box::new(move || b.token_table_bytes()),
6533 token_table_cell: std::sync::OnceLock::new(),
6534 }
6535 } else {
6536 TextIndexSlot::Resident {
6537 index: None,
6538 token_table_len: None,
6539 }
6540 };
6541 let labels = if f.h.has(P_LABELS) {
6542 let a = f.clone();
6543 SectionSlot::Lazy {
6544 loader: Box::new(move || a.label_index()?.ok()),
6545 cell: std::sync::OnceLock::new(),
6546 }
6547 } else {
6548 SectionSlot::Resident(None)
6549 };
6550 let schema = if f.h.has(P_SCHEMA) {
6551 let a = f.clone();
6552 SectionSlot::Lazy {
6553 loader: Box::new(move || a.schema()),
6554 cell: std::sync::OnceLock::new(),
6555 }
6556 } else {
6557 SectionSlot::Resident(None)
6558 };
6559 (pyramid, text_index, labels, schema)
6560}
6561
6562fn ranged_pyramid_slot<R: RangeReader + Send + Sync + 'static>(
6566 reader: &std::sync::Arc<R>,
6567 header: &Header,
6568) -> PyramidSlot {
6569 if header.pyramid_meta_len == 0 {
6570 return PyramidSlot::Resident(None);
6571 }
6572 let pyr_reader = reader.clone();
6573 let pyr_off = header.pyramid_meta_offset;
6574 let pyr_len = header.pyramid_meta_len;
6575 PyramidSlot::Lazy {
6576 loader: Box::new(move || {
6577 let mb = pyr_reader.read_at(pyr_off, pyr_len).ok()?;
6578 PyramidMeta::decode(&mb).ok()
6579 }),
6580 cell: std::sync::OnceLock::new(),
6581 }
6582}
6583
6584fn ranged_text_index_slot<R: RangeReader + Send + Sync + 'static>(
6589 reader: &std::sync::Arc<R>,
6590 header: &Header,
6591) -> TextIndexSlot {
6592 let (ti_off, ti_len) = header.text_index_payload();
6593 if ti_len == 0 {
6594 return TextIndexSlot::Resident {
6595 index: None,
6596 token_table_len: None,
6597 };
6598 }
6599 let ti_reader = reader.clone();
6600 let probe_reader = reader.clone();
6601 let codec = header.block_codec;
6602 TextIndexSlot::Lazy {
6603 loader: Box::new(move || {
6604 let prefix_len = read_token_table_len(&*ti_reader, ti_off, ti_len)?;
6608 let prefix = ti_reader.read_at(ti_off, prefix_len).ok()?;
6609 let postings_base = crate::text_index::TextIndex::postings_base(&prefix)? as u64;
6610 let postings_abs = ti_off + postings_base;
6611 let pr = ti_reader.clone();
6612 let posting_loader =
6613 Box::new(move |off: u64, len: u64| pr.read_at(postings_abs + off, len).ok());
6614 crate::text_index::TextIndex::from_token_table(&prefix, codec, posting_loader).ok()
6615 }),
6616 cell: std::sync::OnceLock::new(),
6617 token_table: Box::new(move || read_token_table_len(&*probe_reader, ti_off, ti_len)),
6620 token_table_cell: std::sync::OnceLock::new(),
6621 }
6622}
6623
6624fn resolve_label_hits(
6631 dict: &Dictionary,
6632 hits: Vec<&crate::meta::LabelEntry>,
6633) -> Vec<(String, String)> {
6634 let ids: Vec<u32> = hits.iter().map(|e| e.subject).collect();
6635 dict.prefetch_subject_terms(&ids);
6636 hits.iter()
6637 .zip(dict.subject_terms(&ids))
6638 .filter_map(|(e, iri)| Some((e.label.clone(), iri?)))
6639 .collect()
6640}
6641
6642fn text_search_in(
6645 ti: Option<&crate::text_index::TextIndex>,
6646 dict: &Dictionary,
6647 words: &[&str],
6648 prefix: Option<&str>,
6649 limit: usize,
6650) -> Vec<String> {
6651 let Some(ti) = ti else {
6652 return Vec::new();
6653 };
6654 let mut acc: Option<Vec<u32>> = None;
6658 if let Some(p) = prefix {
6659 acc = Some(ti.prefix(&p.to_lowercase()));
6660 }
6661 for w in words {
6662 for tok in crate::text_index::tokenize(w) {
6663 let posting = ti.lookup(&tok);
6664 acc = Some(match acc {
6665 Some(a) => intersect_sorted(&a, &posting),
6666 None => posting,
6667 });
6668 if acc.as_ref().is_some_and(|a| a.is_empty()) {
6669 return Vec::new();
6670 }
6671 }
6672 }
6673 let ids = acc.unwrap_or_default();
6674 let mut out = Vec::with_capacity(if limit > 0 {
6675 limit.min(ids.len())
6676 } else {
6677 ids.len()
6678 });
6679 let page = if limit > 0 {
6683 limit.min(ids.len())
6684 } else {
6685 ids.len()
6686 };
6687 dict.prefetch_subject_terms(&ids[..page]);
6688 for id in ids {
6689 if let Some(iri) = dict.subject_term(id) {
6690 out.push(iri);
6691 if limit > 0 && out.len() >= limit {
6692 break;
6693 }
6694 }
6695 }
6696 out
6697}
6698
6699pub struct SearchView {
6722 header: Header,
6723 dict: SectionSlot<Dictionary>,
6724 pyramid: PyramidSlot,
6725 text_index: TextIndexSlot,
6726 labels: SectionSlot<crate::label_index::LabelIndex>,
6727}
6728
6729impl SearchView {
6730 pub fn open_ranged<R: RangeReader + Send + Sync + 'static>(
6735 reader: R,
6736 ) -> Result<Self, FileError> {
6737 Self::open_ranged_with(reader, crate::v06::FIRST_READ)
6738 }
6739
6740 pub fn open_ranged_with<R: RangeReader + Send + Sync + 'static>(
6743 reader: R,
6744 first_read: u64,
6745 ) -> Result<Self, FileError> {
6746 let first_len = first_read.max(HEADER_LEN as u64).min(reader.len());
6747 let first: std::sync::Arc<[u8]> = std::sync::Arc::from(reader.read_at(0, first_len)?);
6748 let reader = crate::reader::PrefixReader::new(reader, first.clone());
6749 if crate::v06::open::is_v06(&first) {
6750 let r: std::sync::Arc<dyn RangeReader + Send + Sync> = std::sync::Arc::new(reader);
6751 let f = std::sync::Arc::new(crate::v06::open::V06File::open(r, &first)?);
6752 let header = crate::v06::logical_header(&f.h);
6753 let (pyramid, text_index, labels, _) = v06_slots(&f);
6754 let want = [true, true, false, false];
6755 let dict = if f.h.has(crate::v06::P_LABELS) {
6756 let a = f.clone();
6757 SectionSlot::Lazy {
6758 loader: Box::new(move || a.dictionary(want).ok()),
6759 cell: std::sync::OnceLock::new(),
6760 }
6761 } else {
6762 SectionSlot::Resident(Some(f.dictionary(want)?))
6763 };
6764 return Ok(Self {
6765 header,
6766 dict,
6767 pyramid,
6768 text_index,
6769 labels,
6770 });
6771 }
6772 let head = reader.read_at(0, HEADER_LEN as u64)?;
6773 let header = Header::from_bytes(&head)?;
6774 let reader = std::sync::Arc::new(reader);
6775 let dict = if header.label_index_section().is_some() {
6779 let (r, h) = (reader.clone(), header.clone());
6780 SectionSlot::Lazy {
6781 loader: Box::new(move || {
6782 ranged_chunked_dictionary(&r, &h, [true, true, false, false]).ok()
6783 }),
6784 cell: std::sync::OnceLock::new(),
6785 }
6786 } else {
6787 SectionSlot::Resident(Some(ranged_chunked_dictionary(
6788 &reader,
6789 &header,
6790 [true, true, false, false],
6791 )?))
6792 };
6793 let pyramid = ranged_pyramid_slot(&reader, &header);
6794 let text_index = ranged_text_index_slot(&reader, &header);
6795 let labels = ranged_label_slot(&reader, &header);
6796 Ok(Self {
6797 header,
6798 dict,
6799 pyramid,
6800 text_index,
6801 labels,
6802 })
6803 }
6804
6805 pub fn has_labels(&self) -> bool {
6809 self.header.label_index_section().is_some() || self.header.pyramid_meta_len > 0
6810 }
6811
6812 pub fn has_label_index(&self) -> bool {
6814 self.header.label_index_section().is_some()
6815 }
6816
6817 pub fn header(&self) -> &Header {
6819 &self.header
6820 }
6821
6822 pub fn has_text_index(&self) -> bool {
6824 self.header.text_index_len > 0
6825 }
6826
6827 pub fn has_pyramid(&self) -> bool {
6830 self.header.pyramid_meta_len > 0
6831 }
6832
6833 pub fn text_search(&self, words: &[&str], prefix: Option<&str>, limit: usize) -> Vec<String> {
6836 let Some(dict) = self.dict.get() else {
6837 return Vec::new();
6838 };
6839 text_search_in(self.text_index.index(), dict, words, prefix, limit)
6840 }
6841
6842 pub fn text_index_token_table_len(&self) -> Option<u64> {
6846 self.text_index.token_table_len()
6847 }
6848
6849 pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
6853 if let Some(li) = self.labels.get() {
6854 return li.prefix_search(prefix, limit);
6855 }
6856 let pyr = match &self.pyramid {
6857 PyramidSlot::Resident(p) => p.as_ref(),
6858 PyramidSlot::Lazy { loader, cell } => cell.get_or_init(loader).as_ref(),
6859 };
6860 let (Some(pyr), Some(dict)) = (pyr, self.dict.get()) else {
6861 return Vec::new();
6862 };
6863 resolve_label_hits(dict, pyr.prefix_search(prefix, limit))
6864 }
6865}
6866
6867#[must_use]
6872pub struct SummaryView {
6873 pub round: u32,
6874 pub summary: Vec<SuperEdge>,
6875 pub class_hierarchy: Vec<ClassNode>,
6877 pub level_rollups: Vec<LevelRollup>,
6879 pub level_links: Vec<LevelLinks>,
6881 pub descriptors: Vec<CommunityDescriptor>,
6883 pub subclass_cycles: Vec<Vec<String>>,
6885 pub disjoint_pairs: Vec<(String, String)>,
6887 pub equivalent_pairs: Vec<(String, String)>,
6889 dict: Dictionary,
6890}
6891
6892impl SummaryView {
6893 pub fn open_ranged<R: RangeReader>(reader: &R) -> Result<Option<Self>, FileError> {
6895 let head = reader.read_at(0, (HEADER_LEN as u64).min(reader.len()))?;
6896 let header = Header::from_bytes(&head)?;
6897 if header.pyramid_meta_len == 0 {
6898 return Ok(None);
6899 }
6900 if header.version == crate::v06::VERSION {
6901 return Self::open_ranged_v06(reader, &header, head).map(Some);
6902 }
6903
6904 let dict_bytes = reader.read_at(header.dictionary_offset, header.dictionary_len)?;
6905 let dict = decode_dictionary_container(&dict_bytes, header.dict_codec)?;
6906
6907 let mb = reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?;
6908 let mut meta =
6909 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
6910 if let Some(s) = read_schema_with_header(reader, &header) {
6913 meta.class_hierarchy = s.class_hierarchy;
6914 meta.level_rollups = s.level_rollups;
6915 meta.level_links = s.level_links;
6916 meta.subclass_cycles = s.subclass_cycles;
6917 meta.disjoint_pairs = s.disjoint_pairs;
6918 meta.equivalent_pairs = s.equivalent_pairs;
6919 }
6920
6921 Ok(Some(SummaryView {
6922 round: meta.round,
6923 summary: meta.summary,
6924 class_hierarchy: meta.class_hierarchy,
6925 level_rollups: meta.level_rollups,
6926 level_links: meta.level_links,
6927 descriptors: meta.descriptors,
6928 subclass_cycles: meta.subclass_cycles,
6929 disjoint_pairs: meta.disjoint_pairs,
6930 equivalent_pairs: meta.equivalent_pairs,
6931 dict,
6932 }))
6933 }
6934
6935 fn open_ranged_v06<R: RangeReader>(
6939 reader: &R,
6940 header: &Header,
6941 head: Vec<u8>,
6942 ) -> Result<Self, FileError> {
6943 use crate::v06::{kind, open};
6944 let mut first = head;
6948 let want_first = crate::v06::FIRST_READ.min(reader.len());
6949 if want_first > first.len() as u64 {
6950 let at = first.len() as u64;
6951 first.extend_from_slice(&reader.read_at(at, want_first - at)?);
6952 }
6953 let pre = open::Prefixed {
6954 first: &first,
6955 r: reader,
6956 };
6957 let (h, root) = open::read_front(&pre)?;
6958 let want: Vec<ByteRange> = [
6959 kind::PYRAMID,
6960 kind::SCHEMA,
6961 kind::DICT_LEAVES,
6962 kind::DICT_DATA,
6963 ]
6964 .iter()
6965 .filter_map(|&k| root.section(k).and_then(|s| s.abs(&h).ok()))
6966 .collect();
6967 let sparse = open::SparseReader::fetch(&pre, first.clone(), &want)?;
6968 let mb = open::read_section(&sparse, &h, &root, kind::PYRAMID)?
6969 .ok_or(FileError::Container("0x06 file has no pyramid"))?;
6970 let mut meta =
6971 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
6972 if let Some(s) = read_schema_with_header(&sparse, header) {
6973 meta.class_hierarchy = s.class_hierarchy;
6974 meta.level_rollups = s.level_rollups;
6975 meta.level_links = s.level_links;
6976 meta.subclass_cycles = s.subclass_cycles;
6977 meta.disjoint_pairs = s.disjoint_pairs;
6978 meta.equivalent_pairs = s.equivalent_pairs;
6979 }
6980 let f = open::V06File::open(std::sync::Arc::new(sparse), &first)?;
6981 let dict = f.dictionary([true; 4])?;
6982 Ok(SummaryView {
6983 round: meta.round,
6984 summary: meta.summary,
6985 class_hierarchy: meta.class_hierarchy,
6986 level_rollups: meta.level_rollups,
6987 level_links: meta.level_links,
6988 descriptors: meta.descriptors,
6989 subclass_cycles: meta.subclass_cycles,
6990 disjoint_pairs: meta.disjoint_pairs,
6991 equivalent_pairs: meta.equivalent_pairs,
6992 dict,
6993 })
6994 }
6995
6996 pub fn open_schema_ranged<R: RangeReader>(reader: &R) -> Result<Option<Self>, FileError> {
7004 let head = reader.read_at(0, HEADER_LEN as u64)?;
7005 let header = Header::from_bytes(&head)?;
7006 let Some(s) = read_schema_with_header(reader, &header) else {
7007 return Ok(None);
7008 };
7009 let dict = crate::DictionaryBuilder::new().build();
7010 Ok(Some(SummaryView {
7011 round: 0,
7012 summary: Vec::new(),
7013 class_hierarchy: s.class_hierarchy,
7014 level_rollups: s.level_rollups,
7015 level_links: s.level_links,
7016 descriptors: Vec::new(),
7017 subclass_cycles: s.subclass_cycles,
7018 disjoint_pairs: s.disjoint_pairs,
7019 equivalent_pairs: s.equivalent_pairs,
7020 dict,
7021 }))
7022 }
7023
7024 pub fn level_count(&self) -> usize {
7026 self.level_rollups.len()
7027 }
7028
7029 pub fn level_rollup(&self, k: usize) -> Option<&LevelRollup> {
7032 self.level_rollups.get(k)
7033 }
7034
7035 pub fn predicate_term(&self, id: u32) -> Option<String> {
7037 self.dict.predicate_term(id)
7038 }
7039
7040 pub fn predicate_total(&self, predicate: &str) -> u32 {
7043 match self.dict.predicate_id(predicate) {
7044 Some(pid) => self
7045 .summary
7046 .iter()
7047 .filter(|e| e.predicate == pid)
7048 .map(|e| e.count)
7049 .sum(),
7050 None => 0,
7051 }
7052 }
7053
7054 pub fn predicate_totals(&self) -> Vec<(String, u32)> {
7056 let mut by_pred: std::collections::BTreeMap<u32, u32> = std::collections::BTreeMap::new();
7057 for e in &self.summary {
7058 *by_pred.entry(e.predicate).or_default() += e.count;
7059 }
7060 let mut out: Vec<(String, u32)> = by_pred
7061 .into_iter()
7062 .filter_map(|(pid, c)| self.dict.predicate_term(pid).map(|t| (t, c)))
7063 .collect();
7064 out.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
7065 out
7066 }
7067
7068 pub fn community_count(&self) -> usize {
7070 let mut comms = std::collections::BTreeSet::new();
7071 for e in &self.summary {
7072 comms.insert(e.s_comm);
7073 comms.insert(e.o_comm);
7074 }
7075 comms.len()
7076 }
7077
7078 pub fn tbox_coherence(&self) -> Vec<crate::reason::Inconsistency> {
7093 schema_coherence(
7094 &self.class_hierarchy,
7095 &self.subclass_cycles,
7096 &self.disjoint_pairs,
7097 &self.equivalent_pairs,
7098 )
7099 }
7100
7101 pub fn tbox_is_coherent(&self) -> bool {
7104 self.tbox_coherence().is_empty()
7105 }
7106}
7107
7108pub fn schema_coherence(
7114 class_hierarchy: &[ClassNode],
7115 subclass_cycles: &[Vec<String>],
7116 disjoint_pairs: &[(String, String)],
7117 equivalent_pairs: &[(String, String)],
7118) -> Vec<crate::reason::Inconsistency> {
7119 use crate::reason::Inconsistency;
7120 use std::collections::{BTreeMap, BTreeSet, VecDeque};
7121 const MAX_REACH: usize = 100_000;
7122
7123 let mut out: Vec<Inconsistency> = Vec::new();
7124
7125 for cyc in subclass_cycles {
7126 let detail = if cyc.len() == 1 {
7127 format!("{} is rdfs:subClassOf itself (a cycle)", cyc[0])
7128 } else {
7129 format!(
7130 "classes {{{}}} are mutually rdfs:subClassOf (a cycle)",
7131 cyc.join(", ")
7132 )
7133 };
7134 out.push(Inconsistency {
7135 kind: "subclass-cycle",
7136 detail,
7137 });
7138 }
7139
7140 if !disjoint_pairs.is_empty() {
7141 let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
7143 for n in class_hierarchy {
7144 let e = adj.entry(n.class.as_str()).or_default();
7145 for p in &n.parents {
7146 e.push(p.as_str());
7147 }
7148 }
7149 for (a, b) in equivalent_pairs {
7150 adj.entry(a.as_str()).or_default().push(b.as_str());
7151 adj.entry(b.as_str()).or_default().push(a.as_str());
7152 }
7153
7154 let mut focuses: BTreeSet<&str> =
7156 class_hierarchy.iter().map(|n| n.class.as_str()).collect();
7157 for (a, b) in disjoint_pairs.iter().chain(equivalent_pairs) {
7158 focuses.insert(a.as_str());
7159 focuses.insert(b.as_str());
7160 }
7161
7162 let mut seen: BTreeSet<&str> = BTreeSet::new();
7163 for &c in &focuses {
7164 let mut reach: BTreeSet<&str> = BTreeSet::new();
7166 let mut q: VecDeque<&str> = VecDeque::new();
7167 reach.insert(c);
7168 q.push_back(c);
7169 while let Some(x) = q.pop_front() {
7170 if reach.len() > MAX_REACH {
7171 break;
7172 }
7173 if let Some(ns) = adj.get(x) {
7174 for &p in ns {
7175 if reach.insert(p) {
7176 q.push_back(p);
7177 }
7178 }
7179 }
7180 }
7181 for (x, y) in disjoint_pairs {
7182 if reach.contains(x.as_str()) && reach.contains(y.as_str()) && seen.insert(c) {
7183 out.push(Inconsistency {
7184 kind: "unsatisfiable-class",
7185 detail: format!(
7186 "{c} is a subclass of both {x} and {y}, which are \
7187 owl:disjointWith — no individual can be a {c}"
7188 ),
7189 });
7190 break;
7191 }
7192 }
7193 }
7194 }
7195
7196 out.sort_by(|a, b| (a.kind, &a.detail).cmp(&(b.kind, &b.detail)));
7197 out
7198}
7199
7200pub fn read_schema_coherence_ranged<R: RangeReader>(
7208 reader: &R,
7209) -> Result<Option<Vec<crate::reason::Inconsistency>>, FileError> {
7210 let head = reader.read_at(0, HEADER_LEN as u64)?;
7211 let header = Header::from_bytes(&head)?;
7212 if let Some(s) = read_schema_with_header(reader, &header) {
7214 return Ok(Some(schema_coherence(
7215 &s.class_hierarchy,
7216 &s.subclass_cycles,
7217 &s.disjoint_pairs,
7218 &s.equivalent_pairs,
7219 )));
7220 }
7221 if header.pyramid_meta_len == 0 {
7222 return Ok(None);
7223 }
7224 if header.schema_meta_len > 0 && (header.schema_meta_len as u64) <= header.pyramid_meta_len {
7228 let off =
7229 header.pyramid_meta_offset + header.pyramid_meta_len - header.schema_meta_len as u64;
7230 let block = reader.read_at(off, header.schema_meta_len as u64)?;
7231 let (hierarchy, cycles, disjoint, equivalent) = crate::meta::decode_schema_block(&block)
7232 .map_err(|_| FileError::Container("malformed schema block"))?;
7233 return Ok(Some(schema_coherence(
7234 &hierarchy,
7235 &cycles,
7236 &disjoint,
7237 &equivalent,
7238 )));
7239 }
7240 let mb = if header.version == crate::v06::VERSION {
7242 crate::v06::open::read_pyramid(reader)?.unwrap_or_default()
7243 } else {
7244 reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?
7245 };
7246 let meta =
7247 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?;
7248 Ok(Some(schema_coherence(
7249 &meta.class_hierarchy,
7250 &meta.subclass_cycles,
7251 &meta.disjoint_pairs,
7252 &meta.equivalent_pairs,
7253 )))
7254}
7255
7256#[allow(clippy::type_complexity)]
7264pub fn read_schema_summary_ranged<R: RangeReader>(
7265 reader: &R,
7266) -> Result<Option<(Vec<(String, u64)>, Vec<(String, String, String, u64)>)>, FileError> {
7267 let head = reader.read_at(0, HEADER_LEN as u64)?;
7268 let header = Header::from_bytes(&head)?;
7269 let meta = if let Some(s) = read_schema_with_header(reader, &header) {
7271 PyramidMeta::new(0, Vec::new(), &[]).with_schema(
7272 Vec::new(),
7273 s.level_rollups,
7274 s.level_links,
7275 Vec::new(),
7276 Vec::new(),
7277 Vec::new(),
7278 Vec::new(),
7279 )
7280 } else {
7281 if header.pyramid_meta_len == 0 {
7282 return Ok(None);
7283 }
7284 if header.schema_meta_len > 0 && (header.schema_meta_len as u64) <= header.pyramid_meta_len
7285 {
7286 let off = header.pyramid_meta_offset + header.pyramid_meta_len
7287 - header.schema_meta_len as u64;
7288 let block = reader.read_at(off, header.schema_meta_len as u64)?;
7289 let summary = crate::meta::decode_schema_block_summary(&block)
7290 .map_err(|_| FileError::Container("malformed schema block"))?;
7291 return Ok(Some(summary));
7292 }
7293 let mb = if header.version == crate::v06::VERSION {
7295 crate::v06::open::read_pyramid(reader)?.unwrap_or_default()
7296 } else {
7297 reader.read_at(header.pyramid_meta_offset, header.pyramid_meta_len)?
7298 };
7299 PyramidMeta::decode(&mb).map_err(|_| FileError::Container("malformed pyramid meta"))?
7300 };
7301 if meta.level_rollups.is_empty() && meta.level_links.is_empty() {
7302 return Ok(None);
7303 }
7304 let classes = meta
7305 .level_rollups
7306 .iter()
7307 .max_by_key(|r| r.depth)
7308 .map(|r| r.classes.clone())
7309 .unwrap_or_default();
7310 let relations = meta
7311 .level_links
7312 .iter()
7313 .max_by_key(|l| l.depth)
7314 .map(|l| {
7315 l.links
7316 .iter()
7317 .map(|c| {
7318 (
7319 c.s_class.clone(),
7320 c.predicate.clone(),
7321 c.o_class.clone(),
7322 c.count,
7323 )
7324 })
7325 .collect()
7326 })
7327 .unwrap_or_default();
7328 Ok(Some((classes, relations)))
7329}
7330
7331#[cfg(test)]
7336pub(crate) fn dict_chunk_keys_for_test(image: &[u8]) -> Vec<Vec<Vec<u8>>> {
7337 if crate::v06::open::is_v06(image) {
7338 use crate::v06::{decode_chunk_leaf, decode_dict_root, kind, open, tag};
7340 let r = crate::reader::SliceReader::new(image);
7341 let (h, root) = open::read_front(&r).unwrap();
7342 let roots = decode_dict_root(root.unit(tag::DICT).unwrap()).unwrap();
7343 let leaves = open::read_section(&r, &h, &root, kind::DICT_LEAVES)
7344 .unwrap()
7345 .unwrap();
7346 return roots
7347 .iter()
7348 .map(|s| {
7349 s.leaves
7350 .iter()
7351 .flat_map(|(l, _, _, _)| {
7352 let stored = &leaves[l.off as usize..(l.off + l.len) as usize];
7353 let raw = decompress(h.internal_codec, stored).unwrap();
7354 decode_chunk_leaf(&raw, l.n).unwrap()
7355 })
7356 .map(|c| c.key)
7357 .collect()
7358 })
7359 .collect();
7360 }
7361 let header = Header::from_bytes(&image[..HEADER_LEN]).unwrap();
7362 let s = header.dictionary_offset as usize;
7363 let e = s + header.dictionary_len as usize;
7364 decode_container(&image[s..e], CODEC_NONE)
7365 .unwrap()
7366 .iter()
7367 .map(|payload| {
7368 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
7369 entries.into_iter().map(|c| c.key).collect()
7370 })
7371 .collect()
7372}
7373
7374#[cfg(test)]
7375mod tests {
7376 use super::*;
7377 use super::{
7378 write_dataset_v05 as write_dataset,
7379 write_dataset_with_metadata_v05 as write_dataset_with_metadata,
7380 write_file_v05 as write_file,
7381 };
7382 use crate::dictionary::DictionaryBuilder;
7383 use crate::index::GraphIndexBuilder;
7384
7385 #[cfg(not(feature = "compression"))]
7388 #[test]
7389 fn ruzstd_decodes_a_zstd_frame() {
7390 let data: Vec<u8> = (0..50_000u32)
7391 .flat_map(|i| (i % 251).to_le_bytes())
7392 .collect();
7393 let frame = ruzstd::encoding::compress_to_vec(
7394 &data[..],
7395 ruzstd::encoding::CompressionLevel::Fastest,
7396 );
7397 assert!(frame.len() < data.len());
7398 assert_eq!(decompress(CODEC_ZSTD, &frame).unwrap(), data);
7399 }
7400
7401 #[cfg(feature = "compression")]
7406 #[test]
7407 fn decompression_is_bounded() {
7408 let data: Vec<u8> = (0..100_000u32).map(|i| (i % 251) as u8).collect();
7409 let sized = zstd::bulk::compress(&data, 3).unwrap();
7410 assert_eq!(zstd_frame_content_size(&sized), Some(100_000));
7411 assert_eq!(block_raw_len(CODEC_ZSTD, &sized, 1 << 20).unwrap(), 100_000);
7412 assert_eq!(
7413 decompress_bounded(CODEC_ZSTD, &sized, 100_000).unwrap(),
7414 data
7415 );
7416 assert!(decompress_bounded(CODEC_ZSTD, &sized, 99_999).is_err());
7417 let stream = zstd::encode_all(&data[..], 3).unwrap();
7419 assert_eq!(zstd_frame_content_size(&stream), None);
7420 assert_eq!(
7421 block_raw_len(CODEC_ZSTD, &stream, 1 << 20).unwrap(),
7422 100_000
7423 );
7424 assert_eq!(
7425 decompress_bounded(CODEC_ZSTD, &stream, 100_000).unwrap(),
7426 data
7427 );
7428 assert!(decompress_bounded(CODEC_ZSTD, &stream, 50_000).is_err());
7429 let mut liar = sized.clone();
7431 let fhd = liar[4];
7432 let fcs_at = 5 + usize::from(fhd & 0x20 == 0);
7433 liar[fcs_at] = liar[fcs_at].wrapping_sub(1);
7434 assert!(decompress_bounded(CODEC_ZSTD, &liar, 200_000).is_err());
7435 assert!(decompress_bounded(CODEC_NONE, &data, 99_999).is_err());
7437 assert_eq!(
7438 decompress_bounded(CODEC_NONE, &data, 100_000).unwrap(),
7439 data
7440 );
7441 assert_eq!(zstd_frame_content_size(b"RETE"), None);
7442 }
7443
7444 #[test]
7450 fn three_permutation_file_round_trips_every_open_path() {
7451 use crate::index::PermSet;
7452 use crate::reader::SliceReader;
7453
7454 let build = |perms: PermSet| {
7455 let mut db = crate::DictionaryBuilder::new();
7456 let mut triples = Vec::new();
7457 for i in 0..60u32 {
7458 let (s, p, o) = (
7459 format!("<http://ex/s{}>", i % 11),
7460 format!("<http://ex/p{}>", i % 3),
7461 format!("<http://ex/o{}>", i % 7),
7462 );
7463 db.observe(&s, &p, &o);
7464 triples.push((s, p, o));
7465 }
7466 let dict = db.build();
7467 let ids: Vec<(u32, u32, u32)> = triples
7468 .iter()
7469 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
7470 .collect();
7471 let def = GraphIndexBuilder::from_triples(ids.clone())
7472 .with_tile_budget(96)
7473 .with_perms(perms)
7474 .build();
7475 let named = GraphIndexBuilder::from_triples(ids)
7476 .with_tile_budget(96)
7477 .with_perms(perms)
7478 .build();
7479 write_dataset(
7480 &dict,
7481 &def,
7482 &[("<http://ex/g>".to_string(), named)],
7483 true,
7484 &[],
7485 0,
7486 )
7487 };
7488
7489 let six = build(PermSet::ALL);
7490 let three = build(PermSet::CORE);
7491 assert!(three.len() < six.len());
7492 assert_eq!(Header::from_bytes(&three).unwrap().perms, PermSet::CORE);
7493 assert_eq!(Header::from_bytes(&six).unwrap().perms, PermSet::ALL);
7494
7495 let shapes: Vec<(Option<&str>, Option<&str>, Option<&str>)> = vec![
7496 (None, None, None),
7497 (Some("<http://ex/s3>"), None, None),
7498 (None, Some("<http://ex/p1>"), None),
7499 (None, None, Some("<http://ex/o5>")),
7500 (Some("<http://ex/s3>"), Some("<http://ex/p1>"), None),
7501 (Some("<http://ex/s3>"), None, Some("<http://ex/o5>")),
7502 (None, Some("<http://ex/p1>"), Some("<http://ex/o5>")),
7503 (
7504 Some("<http://ex/s3>"),
7505 Some("<http://ex/p1>"),
7506 Some("<http://ex/o5>"),
7507 ),
7508 ];
7509
7510 for (image, tag) in [(&six, "six"), (&three, "three")] {
7511 let resident = Rete::open(image).unwrap();
7512 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
7513 let ranged = Rete::open_ranged(&SliceReader::new(leaked)).unwrap();
7514 let lazy =
7515 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64).unwrap();
7516 for (s, p, o) in &shapes {
7517 let want: Vec<_> = {
7518 let mut v = Rete::open(&six).unwrap().query(*s, *p, *o);
7519 v.sort();
7520 v
7521 };
7522 for (got, path) in [
7523 (resident.query(*s, *p, *o), "resident"),
7524 (ranged.query(*s, *p, *o), "ranged"),
7525 (lazy.query(*s, *p, *o), "lazy"),
7526 ] {
7527 let mut got = got;
7528 got.sort();
7529 assert_eq!(got, want, "{tag}/{path} disagreed on {:?}", (s, p, o));
7530 }
7531 let mut g = resident.query_in_graph(Some("<http://ex/g>"), *s, *p, *o);
7533 g.sort();
7534 assert_eq!(g, want, "{tag}/named disagreed on {:?}", (s, p, o));
7535 }
7536 }
7537
7538 let leaked: &'static [u8] = Box::leak(three.clone().into_boxed_slice());
7542 let reader = SliceReader::new(leaked);
7543 let routed = Rete::query_ranged(&reader, None, None, Some("<http://ex/o5>")).unwrap();
7544 let mut want = Rete::open(&six)
7545 .unwrap()
7546 .query(None, None, Some("<http://ex/o5>"));
7547 want.sort();
7548 let mut got = routed;
7549 got.sort();
7550 assert_eq!(got, want, "routed single-pattern read on a lean file");
7551 }
7552
7553 #[test]
7554 fn read_coalesced_merges_within_gap_and_splits_beyond() {
7555 use crate::reader::{CountingReader, SliceReader};
7556 let bytes = vec![0u8; 4096];
7557 let ranges = [
7559 ByteRange { offset: 0, len: 16 },
7560 ByteRange {
7561 offset: 48,
7562 len: 16,
7563 },
7564 ByteRange {
7565 offset: 1088,
7566 len: 16,
7567 },
7568 ];
7569 let r = CountingReader::new(SliceReader::new(&bytes));
7571 let out = read_coalesced(&r, &ranges, 16).unwrap();
7572 assert_eq!(out.len(), 3);
7573 assert_eq!(r.requests(), 3);
7574 let r = CountingReader::new(SliceReader::new(&bytes));
7576 read_coalesced(&r, &ranges, 64).unwrap();
7577 assert_eq!(r.requests(), 2);
7578 let r = CountingReader::new(SliceReader::new(&bytes));
7580 read_coalesced(&r, &ranges, 4096).unwrap();
7581 assert_eq!(r.requests(), 1);
7582 }
7583
7584 fn build_image() -> Vec<u8> {
7585 let triples = [
7586 ("Alice", "knows", "Bob"),
7587 ("Bob", "knows", "Carol"),
7588 ("Alice", "age", "30"),
7589 ];
7590 let mut db = DictionaryBuilder::new();
7591 for (s, p, o) in triples {
7592 db.observe(s, p, o);
7593 }
7594 let dict = db.build();
7595
7596 let mut ib = GraphIndexBuilder::new();
7597 for (s, p, o) in triples {
7598 ib.push(dict.encode(s, p, o).unwrap());
7599 }
7600 let index = ib.build();
7601
7602 let (meta, levels) = build_pyramid_meta(&dict, &triples_ids(&dict), DEFAULT_TILE_BUDGET);
7603 write_file(&dict, &index, false, &meta, levels)
7604 }
7605
7606 fn triples_ids(dict: &Dictionary) -> Vec<(u32, u32, u32)> {
7607 [
7608 ("Alice", "knows", "Bob"),
7609 ("Bob", "knows", "Carol"),
7610 ("Alice", "age", "30"),
7611 ]
7612 .iter()
7613 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
7614 .collect()
7615 }
7616
7617 #[test]
7618 fn file_round_trips_header_and_counts() {
7619 let bytes = build_image();
7620 let rete = Rete::open(&bytes).unwrap();
7621 assert_eq!(rete.header().quad_count, 3);
7622 assert!(rete.header().term_count >= 5);
7623 let expected_codec = writer_codec();
7624 assert_eq!(rete.header().dict_codec, expected_codec);
7625 assert_eq!(rete.header().block_codec, expected_codec);
7626 assert_eq!(&bytes[bytes.len() - 4..], &MAGIC); }
7628
7629 #[test]
7634 fn multi_tile_file_round_trips_and_routes() {
7635 let triples: Vec<(String, String, String)> = (0..200)
7636 .map(|i| {
7637 (
7638 format!("<http://ex/s/{i}>"),
7639 format!("<http://ex/p/{}>", i % 5),
7640 format!("<http://ex/o/{}>", i % 23),
7641 )
7642 })
7643 .collect();
7644 let mut db = DictionaryBuilder::new();
7645 for (s, p, o) in &triples {
7646 db.observe(s, p, o);
7647 }
7648 let dict = db.build();
7649 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
7650 for (s, p, o) in &triples {
7651 ib.push(dict.encode(s, p, o).unwrap());
7652 }
7653 let index = ib.build();
7654 assert!(
7655 index.tile_sections()[0].len() > 3,
7656 "tiny budget must force many tiles"
7657 );
7658 let bytes = write_file(&dict, &index, false, &[], 0);
7659
7660 let rete = Rete::open(&bytes).unwrap();
7661 assert_eq!(rete.header().version, crate::header::LEGACY_FORMAT_VERSION);
7662 assert_eq!(rete.query(None, None, None).len(), 200);
7663 assert_eq!(rete.query(Some("<http://ex/s/7>"), None, None).len(), 1);
7664 assert_eq!(
7665 rete.query(None, Some("<http://ex/p/3>"), None).len(),
7666 40,
7667 "predicate extent spans tiles"
7668 );
7669 assert_eq!(
7670 rete.query(None, None, Some("<http://ex/o/22>")).len(),
7671 8 );
7673
7674 use crate::reader::SliceReader;
7676 let reader = SliceReader::new(&bytes);
7677 let routed = Rete::query_ranged(&reader, Some("<http://ex/s/7>"), None, None).unwrap();
7678 assert_eq!(routed.len(), 1);
7679 let routed = Rete::query_ranged(&reader, None, Some("<http://ex/p/3>"), None).unwrap();
7680 assert_eq!(routed.len(), 40);
7681 let routed = Rete::query_ranged(&reader, None, None, Some("<http://ex/o/22>")).unwrap();
7682 assert_eq!(routed.len(), 8);
7683 }
7684
7685 #[test]
7694 fn tile_directory_offsets_survive_past_4gib() {
7695 let mut dir = Vec::new();
7696 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);
7702 write_uvarint(&mut dir, 2 << 30); let total = dir.len() as u64 + (3u64 << 30) + (2u64 << 30) + 64;
7704 let entries = parse_tile_directory(&dir, total).unwrap();
7705 assert_eq!(entries.len(), 2);
7706 assert_eq!(entries[1].start, dir.len() as u64 + (3u64 << 30));
7707 assert!(
7708 entries[1].end > u32::MAX as u64,
7709 "tail tile sits past 4 GiB"
7710 );
7711 assert!(parse_tile_directory(&dir, 1 << 20).is_err());
7713 }
7714
7715 #[test]
7716 fn tile_synopsis_trailer_round_trips() {
7717 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
7718 for i in 0..200u32 {
7719 ib.push((i, i % 7, i % 13));
7720 }
7721 let index = ib.build();
7722 let tile_count = index.tile_sections()[0].len();
7723 assert!(tile_count > 3, "tiny budget forces many tiles");
7724
7725 let payload = encode_tiled_section(&index, 0, CODEC_NONE);
7726 let dir = parse_tile_directory(&payload, payload.len() as u64).unwrap();
7727 assert_eq!(dir.len(), tile_count);
7728 let trailer_start = dir.iter().map(|e| e.end).max().unwrap();
7730 assert!(
7731 trailer_start < payload.len() as u64,
7732 "a trailer follows the tiles"
7733 );
7734 for e in &dir {
7735 assert!(
7736 e.end <= payload.len() as u64,
7737 "tiles still located within the payload"
7738 );
7739 }
7740 let syn = parse_tile_synopsis(&payload, trailer_start as usize, dir.len()).unwrap();
7741 for (e, (min_b, max_b, min_c, max_c)) in dir.iter().zip(syn) {
7742 let block = decompress(CODEC_NONE, &payload[e.start as usize..e.end as usize]).unwrap();
7743 let z = *crate::triples::TripleBlock::parse(&block).unwrap().zone();
7744 assert_eq!(
7745 (min_b, max_b, min_c, max_c),
7746 (z.min_b, z.max_b, z.min_c, z.max_c),
7747 "synopsis equals the tile's own zone"
7748 );
7749 }
7750 }
7751
7752 #[test]
7756 fn tile_synopsis_lazy_matches_reference_every_shape() {
7757 use crate::reader::{CountingReader, SliceReader};
7758 let triples: Vec<(String, String, String)> = (0..200u32)
7759 .map(|i| {
7760 (
7761 format!("<http://ex/s/{i:04}>"),
7762 format!("<http://ex/p/{}>", i % 7),
7763 format!("<http://ex/o/{:04}>", i % 13),
7764 )
7765 })
7766 .collect();
7767 let mut db = DictionaryBuilder::new();
7768 for (s, p, o) in &triples {
7769 db.observe(s, p, o);
7770 }
7771 let dict = db.build();
7772 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
7773 for (s, p, o) in &triples {
7774 ib.push(dict.encode(s, p, o).unwrap());
7775 }
7776 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
7777
7778 let eager = Rete::open(&bytes).unwrap();
7779 assert!(
7780 eager.header().has_tile_synopsis(),
7781 "new files set the synopsis flag"
7782 );
7783
7784 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
7787 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7788 let lazy = Rete::open_ranged_lazy_with(reader, HEADER_LEN as u64).unwrap();
7789
7790 let brute = |s: Option<&str>, p: Option<&str>, o: Option<&str>| {
7791 let mut v: Vec<(String, String, String)> = triples
7792 .iter()
7793 .filter(|(a, b, c)| {
7794 s.is_none_or(|x| x == a) && p.is_none_or(|x| x == b) && o.is_none_or(|x| x == c)
7795 })
7796 .cloned()
7797 .collect();
7798 v.sort();
7799 v
7800 };
7801 let sv = [
7803 None,
7804 Some("<http://ex/s/0007>"),
7805 Some("<http://ex/s/0130>"),
7806 Some("<http://ex/s/9999>"),
7807 ];
7808 let pv = [
7809 None,
7810 Some("<http://ex/p/3>"),
7811 Some("<http://ex/p/6>"),
7812 Some("<http://ex/p/999>"),
7813 ];
7814 let ov = [
7815 None,
7816 Some("<http://ex/o/0000>"),
7817 Some("<http://ex/o/0012>"),
7818 Some("<http://ex/o/9999>"),
7819 ];
7820 for &s in &sv {
7821 for &p in &pv {
7822 for &o in &ov {
7823 let mut e = eager.query(s, p, o);
7824 e.sort();
7825 let mut l = lazy.query(s, p, o);
7826 l.sort();
7827 let r = brute(s, p, o);
7828 assert_eq!(e, r, "eager {s:?} {p:?} {o:?}");
7829 assert_eq!(l, r, "lazy {s:?} {p:?} {o:?} — synopsis over-pruned");
7830 }
7831 }
7832 }
7833 assert!(!lazy.index_incomplete(), "no lazy fetch failed");
7834 }
7835
7836 #[test]
7840 fn polyglot_offset_reads_lazily() {
7841 use crate::reader::{
7842 detect_polyglot_base, CountingReader, OffsetReader, SliceReader, POLYGLOT_DIGITS,
7843 POLYGLOT_MARKER,
7844 };
7845
7846 let image = build_image();
7850 let mut shell = Vec::new();
7851 shell.extend_from_slice(b"<!DOCTYPE html><html><head><!--");
7852 shell.extend_from_slice(POLYGLOT_MARKER);
7853 let digits_at = shell.len();
7854 shell.extend_from_slice(&[b'0'; POLYGLOT_DIGITS]); shell.extend_from_slice(b"--></head><body>a web page</body></html>\n");
7856 shell.resize(50_000, b' '); let base = shell.len() as u64;
7858 let digits = format!("{base:0width$}", width = POLYGLOT_DIGITS);
7859 shell[digits_at..digits_at + POLYGLOT_DIGITS].copy_from_slice(digits.as_bytes());
7860
7861 let mut poly = shell;
7862 poly.extend_from_slice(&image);
7863
7864 assert_ne!(
7866 &poly[0..4],
7867 b"RETE",
7868 "polyglot must not start with the magic"
7869 );
7870 let detected = detect_polyglot_base(&poly[..crate::header::HEADER_LEN]).unwrap();
7872 assert_eq!(detected, base);
7873
7874 let leaked: &'static [u8] = Box::leak(poly.into_boxed_slice());
7876 let counting = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
7877 let lazy = Rete::open_ranged_lazy_with(
7878 OffsetReader::new(counting.clone(), detected),
7879 HEADER_LEN as u64,
7880 )
7881 .unwrap();
7882
7883 let plain = Rete::open(&image).unwrap();
7885 let mut want = plain.query(None, None, None);
7886 want.sort();
7887 let mut got = lazy.query(None, None, None);
7888 got.sort();
7889 assert_eq!(
7890 got, want,
7891 "polyglot lazy query differs from the plain .rete"
7892 );
7893 assert!(!got.is_empty(), "expected some triples");
7894 assert!(!lazy.index_incomplete(), "a lazy fetch failed");
7895
7896 let read = counting.bytes_read();
7899 eprintln!(
7900 "polyglot lazy read: HTML shell {base} B + .rete image {} B; \
7901 the query touched only {read} B (never the shell).",
7902 image.len()
7903 );
7904 assert!(
7905 read < base,
7906 "read {read} bytes; the HTML shell alone is {base} — the reader \
7907 touched the prefix instead of only the embedded .rete"
7908 );
7909 }
7910
7911 #[cfg(test)]
7914 fn build_text_indexed(triples: &[(String, String, String)]) -> Vec<u8> {
7915 let mut db = DictionaryBuilder::new();
7916 for (s, p, o) in triples {
7917 db.observe(s, p, o);
7918 }
7919 let dict = db.build();
7920 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
7921 let mut id_triples: Vec<(u32, u32, u32)> = Vec::with_capacity(triples.len());
7922 for (s, p, o) in triples {
7923 let t = dict.encode(s, p, o).unwrap();
7924 ib.push(t);
7925 id_triples.push(t);
7926 }
7927 let index = ib.build();
7928 let text_index = compute_text_index(&dict, &id_triples);
7929 assert!(
7930 !text_index.is_empty(),
7931 "literals should produce a text index"
7932 );
7933 write_dataset_with_metadata(&dict, &index, &[], false, &[], 0, &[], &text_index)
7934 }
7935
7936 #[test]
7940 fn text_index_eager_matches_brute_force() {
7941 let triples: Vec<(String, String, String)> = vec![
7942 (
7943 "<http://ex/s0>",
7944 "<http://ex/label>",
7945 "\"alpha glucose phosphate\"",
7946 ),
7947 ("<http://ex/s1>", "<http://ex/label>", "\"beta Glucose\""),
7948 ("<http://ex/s2>", "<http://ex/label>", "\"gamma fructose\""),
7949 (
7950 "<http://ex/s3>",
7951 "<http://ex/note>",
7952 "\"einstein relativity\"",
7953 ),
7954 (
7955 "<http://ex/s4>",
7956 "<http://ex/ref>",
7957 "<http://ex/not-a-literal>",
7958 ),
7959 ]
7960 .into_iter()
7961 .map(|(s, p, o)| (s.to_string(), p.to_string(), o.to_string()))
7962 .collect();
7963 let bytes = build_text_indexed(&triples);
7964 let rete = Rete::open(&bytes).unwrap();
7965 assert!(rete.has_text_index());
7966
7967 let brute = |words: &[&str]| -> Vec<String> {
7969 let mut v: Vec<String> = triples
7970 .iter()
7971 .filter(|(_, _, o)| {
7972 crate::terms::is_literal(o)
7973 && words.iter().all(|w| {
7974 let wl = w.to_lowercase();
7975 crate::terms::literal_lexical(o)
7976 .unwrap()
7977 .split(|c: char| !c.is_alphanumeric())
7978 .any(|t| t.to_lowercase() == wl)
7979 })
7980 })
7981 .map(|(s, _, _)| s.clone())
7982 .collect();
7983 v.sort();
7984 v.dedup();
7985 v
7986 };
7987
7988 let mut got = rete.text_search(&["glucose"], None, 0);
7989 got.sort();
7990 assert_eq!(got, brute(&["glucose"]), "case-insensitive single word");
7991
7992 let mut got = rete.text_search(&["glucose", "phosphate"], None, 0);
7994 got.sort();
7995 assert_eq!(got, brute(&["glucose", "phosphate"]));
7996
7997 assert!(rete.text_search(&["zzznope"], None, 0).is_empty());
7999
8000 let got = rete.text_search(&[], Some("ein"), 0);
8002 assert_eq!(got, vec!["<http://ex/s3>".to_string()]);
8003
8004 let mut db = DictionaryBuilder::new();
8006 for (s, p, o) in &triples {
8007 db.observe(s, p, o);
8008 }
8009 let dict = db.build();
8010 let mut ib = GraphIndexBuilder::new();
8011 for (s, p, o) in &triples {
8012 ib.push(dict.encode(s, p, o).unwrap());
8013 }
8014 let plain = write_dataset(&dict, &ib.build(), &[], false, &[], 0);
8015 let plain_rete = Rete::open(&plain).unwrap();
8016 assert!(!plain_rete.has_text_index());
8017 assert!(plain_rete.text_search(&["glucose"], None, 0).is_empty());
8018 }
8019
8020 #[test]
8024 fn text_index_lazy_faults_only_queried_postings() {
8025 use crate::reader::{CountingReader, SliceReader};
8026 let mut triples: Vec<(String, String, String)> = (0..300u32)
8029 .map(|i| {
8030 (
8031 format!("<http://ex/s/{i:04}>"),
8032 "<http://ex/label>".to_string(),
8033 format!("\"common word number {i}\""),
8034 )
8035 })
8036 .collect();
8037 for i in [3u32, 77, 250] {
8038 triples.push((
8039 format!("<http://ex/s/{i:04}>"),
8040 "<http://ex/tag>".to_string(),
8041 "\"raretoken\"".to_string(),
8042 ));
8043 }
8044 let bytes = build_text_indexed(&triples);
8045 let eager = Rete::open(&bytes).unwrap();
8046 let mut want = eager.text_search(&["raretoken"], None, 0);
8047 want.sort();
8048 assert_eq!(want.len(), 3);
8049
8050 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8051 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8052 let lazy = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
8053 let before = reader.bytes_read();
8056 let mut got = lazy.text_search(&["raretoken"], None, 0);
8057 got.sort();
8058 assert_eq!(got, want, "lazy search matches eager");
8059 let pulled = reader.bytes_read() - before;
8060 let ti_len = eager.header().text_index_len;
8063 assert!(
8064 pulled < ti_len,
8065 "search pulled {pulled} B but the section is {ti_len} B — faulted too much"
8066 );
8067 assert!(!lazy.index_incomplete());
8068 }
8069
8070 #[test]
8075 fn search_view_matches_full_open_for_far_fewer_bytes() {
8076 use crate::reader::{CountingReader, SliceReader};
8077 let filler = "lorem ipsum dolor sit amet consectetur adipiscing elit ".repeat(20);
8080 let mut triples: Vec<(String, String, String)> = (0..300u32)
8081 .map(|i| {
8082 (
8083 format!("<http://ex/s/{i:04}>"),
8084 "<http://ex/abstract>".to_string(),
8085 format!("\"{filler} number {i}\""),
8086 )
8087 })
8088 .collect();
8089 for i in [3u32, 77, 250] {
8090 triples.push((
8091 format!("<http://ex/s/{i:04}>"),
8092 "<http://ex/tag>".to_string(),
8093 "\"raretoken\"".to_string(),
8094 ));
8095 }
8096 let bytes = build_text_indexed(&triples);
8097 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8098
8099 let full_reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8100 let full = Rete::open_ranged_lazy_with(full_reader.clone(), HEADER_LEN as u64).unwrap();
8101 let mut want = full.text_search(&["raretoken"], None, 0);
8102 want.sort();
8103 assert_eq!(want.len(), 3);
8104
8105 let search_reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8106 let view = SearchView::open_ranged_with(search_reader.clone(), HEADER_LEN as u64).unwrap();
8107 assert!(view.has_text_index());
8108 let mut got = view.text_search(&["raretoken"], None, 0);
8109 got.sort();
8110 assert_eq!(got, want, "search view matches the full open");
8111 assert!(
8112 search_reader.bytes_read() < full_reader.bytes_read(),
8113 "search read {} B, full open read {} B — the narrow open bought nothing",
8114 search_reader.bytes_read(),
8115 full_reader.bytes_read()
8116 );
8117
8118 let mut prefix_hits = view.prefix_search("", 5);
8121 prefix_hits.sort();
8122 assert!(!prefix_hits.is_empty() || !view.has_pyramid());
8123 }
8124
8125 #[test]
8135 fn dict_chunk_directory_probe_costs_about_one_directory() {
8136 use crate::reader::{CountingReader, SliceReader};
8137 let head = "x".repeat(3000);
8151 let triples: Vec<(String, String, String)> = (0..2000u32)
8152 .map(|i| {
8153 let tail = char::from(b'a' + (i % 26) as u8).to_string().repeat(1000);
8154 (
8155 format!("<http://ex/s/{i:04}>"),
8156 "<http://ex/abstract>".to_string(),
8157 format!("\"{head}{i:04}{tail}\""),
8158 )
8159 })
8160 .collect();
8161 let mut db = DictionaryBuilder::new();
8162 for (s, p, o) in &triples {
8163 db.observe(s, p, o);
8164 }
8165 let dict = db.build();
8166 let mut ib = GraphIndexBuilder::new();
8167 for (s, p, o) in &triples {
8168 ib.push(dict.encode(s, p, o).unwrap());
8169 }
8170 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8171
8172 let reader = CountingReader::new(SliceReader::new(&bytes));
8173 let header = Header::from_bytes(&reader.read_at(0, HEADER_LEN as u64).unwrap()).unwrap();
8174 let section = locate_container_section_ranged(
8176 &reader,
8177 header.dictionary_offset,
8178 header.dictionary_len,
8179 2,
8180 4,
8181 )
8182 .unwrap();
8183
8184 let before = reader.bytes_read();
8185 let (_meta, entries) = read_dict_dir_ranged(&reader, section).unwrap();
8186 let spent = reader.bytes_read() - before;
8187
8188 let dir_end = entries[0].start;
8190 let head = reader.read_at(section.offset, 64).unwrap();
8191 let (header_len, n0) = read_uvarint(&head).unwrap();
8192 let dir_start = n0 as u64 + header_len;
8193 let dir_len = dir_end - dir_start;
8194 let dir_total = section.len - dir_start;
8195 assert!(
8196 dir_start + dir_len > 8192,
8197 "directory fits the header prefix ({dir_len} B) — the probe never runs"
8198 );
8199
8200 let mut old = 0u64;
8202 let mut p = 4096u64.min(dir_total).max(1);
8203 loop {
8204 let got = p.min(dir_total);
8205 old += got;
8206 if got >= dir_len || p >= dir_total {
8207 break;
8208 }
8209 p = p.saturating_mul(2).min(dir_total);
8210 }
8211 assert!(
8212 spent <= dir_len * 2,
8213 "probe read {spent} B for a {dir_len} B directory — past the append-only bound"
8214 );
8215 assert!(
8216 spent < old,
8217 "probe read {spent} B; the re-reading loop it replaced spent {old} B"
8218 );
8219 }
8220
8221 fn verbatim_keyed_twin(payload: &[u8], codec: u8) -> Vec<u8> {
8231 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
8232 let (header_len, n0) = read_uvarint(payload).unwrap();
8233 let mut out = Vec::new();
8234 write_uvarint(&mut out, header_len);
8235 out.extend_from_slice(&payload[n0..n0 + header_len as usize]);
8236 write_uvarint(&mut out, entries.len() as u64);
8237 let mut prev_run = 0usize;
8238 for e in &entries {
8239 let body = decompress(codec, &payload[e.start as usize..e.end as usize]).unwrap();
8240 let first = crate::dict::run_first_term(&body, 0).unwrap();
8241 write_uvarint(&mut out, (e.first_run - prev_run) as u64);
8242 write_uvarint(&mut out, first.len() as u64);
8243 out.extend_from_slice(&first);
8244 write_uvarint(&mut out, e.end - e.start);
8245 prev_run = e.first_run;
8246 }
8247 for e in &entries {
8248 out.extend_from_slice(&payload[e.start as usize..e.end as usize]);
8249 }
8250 out
8251 }
8252
8253 fn chunk_bounds_of(payload: &[u8], codec: u8) -> Vec<(Vec<u8>, Vec<u8>)> {
8255 let (_meta, entries) = parse_chunked_dict_dir(payload, payload.len() as u64).unwrap();
8256 entries
8257 .iter()
8258 .map(|e| {
8259 let body = decompress(codec, &payload[e.start as usize..e.end as usize]).unwrap();
8260 (
8261 crate::dict::run_first_term(&body, 0).unwrap(),
8262 crate::dict::run_last_term(&body, 0, body.len()).unwrap(),
8263 )
8264 })
8265 .collect()
8266 }
8267
8268 fn adversarial_terms() -> Vec<String> {
8274 let mut terms: Vec<String> = Vec::new();
8275 for i in 0..6_000 {
8276 terms.push(format!("<http://example.org/work/{i:06}>"));
8277 }
8278 let shared = "X".repeat(900);
8279 for i in 0..4_000 {
8280 terms.push(format!("\"{shared}-{i:05}-tail\""));
8281 }
8282 let deep = "Y".repeat(3_000); for i in 0..120 {
8284 terms.push(format!("\"{deep}{i:03}\""));
8285 }
8286 for i in 0..6 {
8287 terms.push(format!("\"BIG-{i:02}-{}\"", "Z".repeat(90_000)));
8289 }
8290 for i in 0..800 {
8291 terms.push(format!(
8292 "\"h\u{e9}llo-{}-\u{1F600}{i:04}\"",
8293 "\u{e9}".repeat(40)
8294 ));
8295 }
8296 for i in 0..2_000 {
8297 terms.push(format!("\"{i:05}-{}\"", "T".repeat(1_800)));
8298 }
8299 terms.sort();
8300 terms.dedup();
8301 terms
8302 }
8303
8304 fn chunked_section_of(terms: &[String]) -> Vec<u8> {
8305 let mut b = crate::dict::DictSectionBuilder::new().with_restart_interval(16);
8306 for t in terms {
8307 b.push(t.clone());
8308 }
8309 b.build()
8310 }
8311
8312 #[test]
8322 fn chunk_directory_keys_are_separators_not_terms() {
8323 let terms = adversarial_terms();
8324 let raw = chunked_section_of(&terms);
8325 let payload = encode_chunked_dict_section(&raw, CODEC_NONE);
8326 let (_meta, entries) = parse_chunked_dict_dir(&payload, payload.len() as u64).unwrap();
8327 let bounds = chunk_bounds_of(&payload, CODEC_NONE);
8328 assert!(
8329 entries.len() > 20,
8330 "need a many-chunk section, got {}",
8331 entries.len()
8332 );
8333
8334 assert!(entries[0].key.is_empty(), "chunk 0 needs no separator");
8335 let mut key_bytes = 0usize;
8336 let mut term_bytes = 0usize;
8337 let mut differ = 0usize;
8338 for (i, e) in entries.iter().enumerate() {
8339 key_bytes += e.key.len();
8340 term_bytes += bounds[i].0.len();
8341 if e.key != bounds[i].0 {
8342 differ += 1;
8343 }
8344 assert!(
8345 e.key <= bounds[i].0,
8346 "chunk {i}: key must be <= its own first term"
8347 );
8348 if i > 0 {
8349 assert!(
8350 e.key.as_slice() > bounds[i - 1].1.as_slice(),
8351 "chunk {i}: key must be > the previous chunk's last term"
8352 );
8353 assert!(
8354 e.key > entries[i - 1].key,
8355 "chunk {i}: keys must ascend for partition_point"
8356 );
8357 assert_eq!(
8359 e.key,
8360 crate::dict::shortest_separator(&bounds[i - 1].1, &bounds[i].0),
8361 "chunk {i}: key is not the SHORTEST separator"
8362 );
8363 }
8364 }
8365 assert!(
8366 differ > 0,
8367 "every key equalled its first term — the writer stores terms again"
8368 );
8369 assert!(
8370 key_bytes * 4 < term_bytes,
8371 "separators bought almost nothing: {key_bytes} B vs {term_bytes} B of first terms"
8372 );
8373 eprintln!(
8374 "chunks={} first-term keys={term_bytes} B separator keys={key_bytes} B ({:.1}x)",
8375 entries.len(),
8376 term_bytes as f64 / key_bytes.max(1) as f64
8377 );
8378 }
8379
8380 #[test]
8391 fn separator_keys_route_identically_to_verbatim_first_terms() {
8392 let terms = adversarial_terms();
8393 let raw = chunked_section_of(&terms);
8394 let sep_payload = encode_chunked_dict_section(&raw, CODEC_NONE);
8395 let ver_payload = verbatim_keyed_twin(&sep_payload, CODEC_NONE);
8396
8397 let (_ms, es) = parse_chunked_dict_dir(&sep_payload, sep_payload.len() as u64).unwrap();
8399 let (_mv, ev) = parse_chunked_dict_dir(&ver_payload, ver_payload.len() as u64).unwrap();
8400 assert_eq!(es.len(), ev.len());
8401 for (a, b) in es.iter().zip(ev.iter()) {
8402 assert_eq!(a.first_run, b.first_run);
8403 assert_eq!(
8404 &sep_payload[a.start as usize..a.end as usize],
8405 &ver_payload[b.start as usize..b.end as usize],
8406 "chunk bodies must be identical"
8407 );
8408 }
8409 assert!(
8410 sep_payload.len() < ver_payload.len(),
8411 "separator payload is not smaller"
8412 );
8413
8414 let sec_s = decode_chunked_dict_section(
8415 &sep_payload,
8416 CODEC_NONE,
8417 crate::chunk_cache::ChunkCache::unlimited_arc(),
8418 0,
8419 )
8420 .unwrap();
8421 let sec_v = decode_chunked_dict_section(
8422 &ver_payload,
8423 CODEC_NONE,
8424 crate::chunk_cache::ChunkCache::unlimited_arc(),
8425 0,
8426 )
8427 .unwrap();
8428
8429 for (i, t) in terms.iter().enumerate() {
8431 let want = Some(i as u32 + 1);
8432 assert_eq!(sec_v.id(t), want, "verbatim id() lost a term");
8433 assert_eq!(sec_s.id(t), want, "separator id() lost a term");
8434 }
8435 for i in 0..terms.len() as u32 {
8437 assert_eq!(sec_s.term(i + 1), sec_v.term(i + 1));
8438 assert_eq!(
8439 sec_s.term(i + 1).as_deref(),
8440 Some(terms[i as usize].as_str())
8441 );
8442 }
8443 let bounds = chunk_bounds_of(&sep_payload, CODEC_NONE);
8445 let mut probes: Vec<String> = Vec::new();
8446 let push = |bytes: &[u8], probes: &mut Vec<String>| {
8447 if let Ok(s) = std::str::from_utf8(bytes) {
8448 probes.push(s.to_string());
8449 }
8450 };
8451 for (first, last) in &bounds {
8452 for base in [first, last] {
8453 push(base, &mut probes); let mut after = base.clone();
8455 after.push(0x01); push(&after, &mut probes);
8457 let mut far = base.clone();
8458 far.push(b'~');
8459 push(&far, &mut probes);
8460 if !base.is_empty() {
8461 push(&base[..base.len() - 1], &mut probes); let mut down = base.clone();
8463 *down.last_mut().unwrap() = down.last().unwrap().wrapping_sub(1);
8464 push(&down, &mut probes);
8465 let mut up = base.clone();
8466 *up.last_mut().unwrap() = up.last().unwrap().wrapping_add(1);
8467 push(&up, &mut probes);
8468 }
8469 }
8470 }
8471 for e in &es {
8473 push(&e.key, &mut probes);
8474 let mut plus = e.key.clone();
8475 plus.push(0);
8476 push(&plus, &mut probes);
8477 }
8478 probes.push(String::new()); probes.push("\u{1}".to_string()); probes.push("\u{10FFFF}\u{10FFFF}".to_string()); probes.push(terms.first().unwrap().clone());
8482 probes.push(terms.last().unwrap().clone());
8483
8484 let mut hits = 0usize;
8485 for p in &probes {
8486 let (a, b) = (sec_v.id(p), sec_s.id(p));
8487 assert_eq!(
8488 a,
8489 b,
8490 "probe diverged: {:?}",
8491 p.get(..p.len().min(48)).unwrap_or(p.as_str())
8492 );
8493 hits += usize::from(a.is_some());
8494 }
8495 assert!(
8496 probes.len() > 500 && hits > 50,
8497 "probe set is too thin: {} probes, {hits} of them present",
8498 probes.len()
8499 );
8500 eprintln!(
8501 "{} boundary probes, {hits} present, all identical",
8502 probes.len()
8503 );
8504 }
8505
8506 mod separator_props {
8512 use super::*;
8513 use proptest::prelude::*;
8514
8515 fn term_set() -> impl Strategy<Value = Vec<String>> {
8522 prop::collection::vec(("[a-c]{1,6}", 0u8..3, 300usize..1200), 60..250).prop_map(
8523 |specs| {
8524 let mut terms: Vec<String> = specs
8525 .into_iter()
8526 .enumerate()
8527 .map(|(i, (core, kind, pad))| {
8528 let block = "z".repeat(pad);
8529 match kind {
8530 0 => format!("<http://ex/{core}/{i:05}>"),
8531 1 => format!("\"{block}{core}{i:05}\""),
8532 _ => format!("\"{core}{i:05}{block}\""),
8533 }
8534 })
8535 .collect();
8536 terms.sort();
8537 terms.dedup();
8538 terms
8539 },
8540 )
8541 }
8542
8543 proptest! {
8544 #[test]
8545 fn prop_separator_keyed_section_answers_like_verbatim(
8546 terms in term_set(),
8547 extra in prop::collection::vec(any::<String>(), 0..8),
8548 ) {
8549 let raw = chunked_section_of(&terms);
8550 let sep = encode_chunked_dict_section(&raw, CODEC_NONE);
8551 let ver = verbatim_keyed_twin(&sep, CODEC_NONE);
8552 prop_assert!(sep.len() <= ver.len());
8553
8554 let (_m, es) = parse_chunked_dict_dir(&sep, sep.len() as u64).unwrap();
8556 let bounds = chunk_bounds_of(&sep, CODEC_NONE);
8557 prop_assert!(es[0].key.is_empty());
8558 for i in 1..es.len() {
8559 prop_assert!(es[i].key.as_slice() > bounds[i - 1].1.as_slice());
8560 prop_assert!(es[i].key <= bounds[i].0);
8561 prop_assert!(es[i].key > es[i - 1].key);
8562 }
8563
8564 let sec_s = decode_chunked_dict_section(
8565 &sep,
8566 CODEC_NONE,
8567 crate::chunk_cache::ChunkCache::unlimited_arc(),
8568 0,
8569 )
8570 .unwrap();
8571 let sec_v = decode_chunked_dict_section(
8572 &ver,
8573 CODEC_NONE,
8574 crate::chunk_cache::ChunkCache::unlimited_arc(),
8575 0,
8576 )
8577 .unwrap();
8578
8579 for (i, t) in terms.iter().enumerate() {
8580 prop_assert_eq!(sec_s.id(t), Some(i as u32 + 1));
8581 prop_assert_eq!(sec_v.id(t), Some(i as u32 + 1));
8582 prop_assert_eq!(sec_s.term(i as u32 + 1), sec_v.term(i as u32 + 1));
8583 }
8584
8585 let mut probes: Vec<String> = extra;
8587 probes.push(String::new());
8588 for (first, last) in &bounds {
8589 for base in [first, last] {
8590 if let Ok(s) = std::str::from_utf8(base) {
8591 probes.push(s.to_string());
8592 probes.push(format!("{s}\u{1}"));
8593 probes.push(s[..s.len() - s.chars().next_back()
8594 .map(char::len_utf8).unwrap_or(0)].to_string());
8595 }
8596 }
8597 }
8598 for e in &es {
8599 if let Ok(s) = std::str::from_utf8(&e.key) {
8600 probes.push(s.to_string());
8601 }
8602 }
8603 for p in &probes {
8604 prop_assert_eq!(sec_s.id(p), sec_v.id(p), "probe diverged: {:?}", p);
8605 }
8606 }
8607 }
8608 }
8609
8610 #[test]
8615 fn text_index_is_tamper_evident_and_verifies() {
8616 let triples: Vec<(String, String, String)> = vec![(
8617 "<http://ex/s0>".to_string(),
8618 "<http://ex/label>".to_string(),
8619 "\"alpha glucose phosphate\"".to_string(),
8620 )];
8621 let bytes = build_text_indexed(&triples);
8622 let header = Rete::open(&bytes).unwrap().header().clone();
8623 assert!(header.text_index_len > 0);
8624 assert!(verify(&bytes).unwrap(), "a text-indexed build must verify");
8625
8626 let mut tampered = bytes.clone();
8627 tampered[header.text_index_offset as usize] ^= 0xff;
8628 assert!(
8629 !verify(&tampered).unwrap(),
8630 "tampering with the text index must break verify()"
8631 );
8632 }
8633
8634 #[test]
8638 fn synopsis_cuts_remote_fetch_bytes() {
8639 use crate::header::FLAG_TILE_SYNOPSIS;
8640 use crate::reader::{CountingReader, SliceReader};
8641
8642 let triples: Vec<(String, String, String)> = (0..400u32)
8646 .map(|i| {
8647 (
8648 format!("<http://ex/s/{i:04}>"),
8649 "<http://ex/p>".to_string(),
8650 format!("<http://ex/o/{i:04}>"),
8651 )
8652 })
8653 .collect();
8654 let mut db = DictionaryBuilder::new();
8655 for (s, p, o) in &triples {
8656 db.observe(s, p, o);
8657 }
8658 let dict = db.build();
8659 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
8660 for (s, p, o) in &triples {
8661 ib.push(dict.encode(s, p, o).unwrap());
8662 }
8663 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8664
8665 let q = (Some("<http://ex/s/0395>"), None, Some("<http://ex/o/0005>"));
8669 let query_bytes = |image: &[u8]| -> (u64, usize) {
8673 let leaked: &'static [u8] = Box::leak(image.to_vec().into_boxed_slice());
8674 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
8675 let rete = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
8676 let before = reader.bytes_read(); let n = rete.query(q.0, q.1, q.2).len();
8678 assert!(!rete.index_incomplete());
8679 (reader.bytes_read() - before, n)
8680 };
8681
8682 let (on_bytes, on_n) = query_bytes(&bytes);
8683 let mut off = bytes.clone();
8685 off[5] &= !FLAG_TILE_SYNOPSIS;
8686 let (off_bytes, off_n) = query_bytes(&off);
8687
8688 assert_eq!(on_n, 0, "the pair never co-occurs");
8689 assert_eq!(off_n, 0, "same answer without the synopsis");
8690 assert!(
8694 on_bytes < off_bytes,
8695 "synopsis skips the routed tile fetch: {on_bytes} < {off_bytes}"
8696 );
8697 }
8698
8699 #[test]
8705 fn double_bound_object_join_eager_matches_lazy() {
8706 use crate::reader::SliceReader;
8707 let occ = "<http://ex/occ>";
8708 let phys = "<http://ex/physicist>";
8709 let phil = "<http://ex/philosopher>";
8710 let label = "<http://www.w3.org/2000/01/rdf-schema#label>";
8711 let mut triples: Vec<(String, String, String)> = Vec::new();
8713 for i in 0..20u32 {
8714 triples.push((format!("<http://ex/p/{i:02}>"), occ.into(), phys.into()));
8715 if i < 10 {
8716 triples.push((format!("<http://ex/p/{i:02}>"), occ.into(), phil.into()));
8717 }
8718 triples.push((
8719 format!("<http://ex/p/{i:02}>"),
8720 label.into(),
8721 format!("\"Name {i:02}\""),
8722 ));
8723 }
8724 let mut db = DictionaryBuilder::new();
8725 for (s, p, o) in &triples {
8726 db.observe(s, p, o);
8727 }
8728 let dict = db.build();
8729 let mut ib = GraphIndexBuilder::new().with_tile_budget(16);
8730 for (s, p, o) in &triples {
8731 ib.push(dict.encode(s, p, o).unwrap());
8732 }
8733 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8734
8735 let q = "SELECT ?l WHERE { \
8736 ?p <http://ex/occ> <http://ex/physicist> ; \
8737 <http://ex/occ> <http://ex/philosopher> ; \
8738 <http://www.w3.org/2000/01/rdf-schema#label> ?l }";
8739 let run = |rete: &Rete| -> Vec<String> {
8740 let (_, sols) = crate::eval_sparql(rete, q).unwrap();
8741 let mut v: Vec<String> = sols.iter().filter_map(|b| b.get("l").cloned()).collect();
8742 v.sort();
8743 v
8744 };
8745
8746 let eager_rows = run(&Rete::open(&bytes).unwrap());
8747 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
8748 let lazy = Rete::open_ranged_lazy_with(
8749 std::sync::Arc::new(SliceReader::new(leaked)),
8750 HEADER_LEN as u64,
8751 )
8752 .unwrap();
8753 let lazy_rows = run(&lazy);
8754 assert!(!lazy.index_incomplete());
8755
8756 assert_eq!(eager_rows.len(), 10, "the 10 physicist∩philosopher labels");
8757 assert_eq!(eager_rows, lazy_rows, "eager and lazy must agree exactly");
8758 }
8759
8760 #[test]
8764 fn multi_chunk_dictionary_round_trips() {
8765 let mut db = DictionaryBuilder::new();
8766 let term = |i: u32| format!("<http://example.org/some/long/prefix/entity/{i:06}>");
8767 for i in 0..6000u32 {
8768 db.observe(&term(i), "<http://ex/p>", &term(i + 1));
8769 }
8770 let dict = db.build();
8771 let mut ib = GraphIndexBuilder::new();
8772 for i in 0..6000u32 {
8773 ib.push(
8774 dict.encode(&term(i), "<http://ex/p>", &term(i + 1))
8775 .unwrap(),
8776 );
8777 }
8778 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8779 let rete = Rete::open(&bytes).unwrap();
8780 let d = rete.dictionary();
8781 assert_eq!(d.term_count(), dict.term_count());
8782 for i in (0..6000).step_by(97).chain([0, 1, 5999, 6000]) {
8783 let t = term(i);
8784 let sid = dict.subject_id(&t);
8785 assert_eq!(d.subject_id(&t), sid, "subject_id({t})");
8786 if let Some(id) = sid {
8787 assert_eq!(d.subject_term(id).as_deref(), Some(t.as_str()));
8788 }
8789 let oid = dict.object_id(&t);
8790 assert_eq!(d.object_id(&t), oid, "object_id({t})");
8791 }
8792 assert_eq!(d.subject_id("<http://example.org/absent>"), None);
8793 assert_eq!(d.predicate_id("<http://ex/p>"), Some(1));
8794 assert_eq!(d.predicate_term(1).as_deref(), Some("<http://ex/p>"));
8795 }
8796
8797 #[test]
8798 #[cfg(feature = "compression")]
8799 fn compression_shrinks_repetitive_data() {
8800 let mut db = DictionaryBuilder::new();
8804 let triples: Vec<(String, String, String)> = (0..500)
8805 .map(|i| {
8806 (
8807 format!("<http://example.org/entity/{i}>"),
8808 "<http://example.org/p/relatedTo>".to_string(),
8809 format!("<http://example.org/entity/{}>", (i + 1) % 500),
8810 )
8811 })
8812 .collect();
8813 for (s, p, o) in &triples {
8814 db.observe(s, p, o);
8815 }
8816 let dict = db.build();
8817 let mut ib = GraphIndexBuilder::new();
8818 for (s, p, o) in &triples {
8819 ib.push(dict.encode(s, p, o).unwrap());
8820 }
8821 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
8822
8823 let raw: usize = triples
8824 .iter()
8825 .map(|(s, p, o)| s.len() + p.len() + o.len())
8826 .sum();
8827 assert!(
8828 bytes.len() < raw / 2,
8829 "expected strong compression: file {} vs raw terms {raw}",
8830 bytes.len()
8831 );
8832
8833 let rete = Rete::open(&bytes).unwrap();
8835 let r = rete.query(Some("<http://example.org/entity/0>"), None, None);
8836 assert_eq!(r.len(), 1);
8837 assert_eq!(r[0].2, "<http://example.org/entity/1>");
8838 }
8839
8840 fn big_file_with_pyramid() -> Vec<u8> {
8841 let triples: Vec<(String, String, String)> = (0..300)
8843 .map(|i| {
8844 (
8845 format!("<http://ex/e{i}>"),
8846 "<http://ex/next>".to_string(),
8847 format!("<http://ex/e{}>", (i + 1) % 300),
8848 )
8849 })
8850 .collect();
8851 let mut db = DictionaryBuilder::new();
8852 for (s, p, o) in &triples {
8853 db.observe(s, p, o);
8854 }
8855 let dict = db.build();
8856 let ids: Vec<_> = triples
8857 .iter()
8858 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
8859 .collect();
8860 let mut ib = GraphIndexBuilder::new();
8861 for &t in &ids {
8862 ib.push(t);
8863 }
8864 let (meta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
8865 write_file(&dict, &ib.build(), false, &meta, levels)
8866 }
8867
8868 #[test]
8869 fn ranged_open_is_minimal_and_correct() {
8870 use crate::reader::{CountingReader, SliceReader};
8871 let bytes = big_file_with_pyramid();
8872
8873 let full = CountingReader::new(SliceReader::new(&bytes));
8874 let rete = Rete::open_ranged(&full).unwrap();
8875 assert!(full.requests() <= 4, "requests = {}", full.requests());
8877 assert_eq!(
8878 rete.query(Some("<http://ex/e0>"), None, None)[0].2,
8879 "<http://ex/e1>"
8880 );
8881
8882 let summ_reader = CountingReader::new(SliceReader::new(&bytes));
8884 let view = SummaryView::open_ranged(&summ_reader).unwrap().unwrap();
8885 assert!(!view.summary.is_empty());
8886 assert!(
8887 summ_reader.bytes_read() < bytes.len() as u64,
8888 "summary read {} of {} bytes",
8889 summ_reader.bytes_read(),
8890 bytes.len()
8891 );
8892 assert!(summ_reader.bytes_read() < full.bytes_read());
8894 }
8895
8896 #[test]
8897 fn content_hash_is_set_and_verifies() {
8898 let bytes = build_image();
8899 let rete = Rete::open(&bytes).unwrap();
8900 assert_ne!(
8901 rete.header().content_hash,
8902 [0u8; 16],
8903 "hash must be populated"
8904 );
8905 assert!(verify(&bytes).unwrap(), "freshly built file verifies");
8906 assert!(
8907 verify_ranged(&crate::reader::SliceReader::new(&bytes)).unwrap(),
8908 "the streaming verify agrees"
8909 );
8910
8911 assert_eq!(
8913 Rete::open(&build_image()).unwrap().header().content_hash,
8914 rete.header().content_hash
8915 );
8916
8917 let mut tampered = bytes.clone();
8919 let last = tampered.len() - 5; tampered[last] ^= 0xff;
8921 assert!(!verify(&tampered).unwrap());
8922 assert!(!verify_ranged(&crate::reader::SliceReader::new(&tampered)).unwrap());
8923 let short = &bytes[..bytes.len() / 2];
8925 assert!(verify_ranged(&crate::reader::SliceReader::new(short)).is_err());
8926 assert!(verify_ranged(&crate::reader::SliceReader::new(&bytes[..10])).is_err());
8927 }
8928
8929 fn build_with_metadata(meta: &[u8]) -> Vec<u8> {
8931 let triples = [
8932 ("Alice", "knows", "Bob"),
8933 ("Bob", "knows", "Carol"),
8934 ("Alice", "age", "30"),
8935 ];
8936 let mut db = DictionaryBuilder::new();
8937 for (s, p, o) in triples {
8938 db.observe(s, p, o);
8939 }
8940 let dict = db.build();
8941 let ids: Vec<_> = triples
8942 .iter()
8943 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
8944 .collect();
8945 let mut ib = GraphIndexBuilder::new();
8946 for &t in &ids {
8947 ib.push(t);
8948 }
8949 let (pmeta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
8950 write_dataset_with_metadata(&dict, &ib.build(), &[], false, &pmeta, levels, meta, &[])
8951 }
8952
8953 #[test]
8954 fn metadata_round_trips_and_shifts_offsets() {
8955 let card = br#"{"title":"My Dataset"}"#;
8956 let bytes = build_with_metadata(card);
8957 let rete = Rete::open(&bytes).unwrap();
8958
8959 assert_eq!(rete.metadata(), Some(card.as_slice()));
8961 let h = rete.header();
8962 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
8963 assert_eq!(h.metadata_len, card.len() as u64);
8964 assert_eq!(h.dictionary_offset, HEADER_LEN as u64 + card.len() as u64);
8966
8967 assert_eq!(
8969 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
8970 1
8971 );
8972 assert!(verify(&bytes).unwrap());
8974 }
8975
8976 #[test]
8977 fn empty_metadata_is_byte_identical_to_plain_writer() {
8978 assert_eq!(
8981 build_with_metadata(&[]),
8982 build_image(),
8983 "empty-metadata output must equal the plain writer byte-for-byte"
8984 );
8985 }
8986
8987 #[test]
8992 fn replace_metadata_matches_a_direct_write() {
8993 let first = br#"{"title":"My Dataset","queries":[1,2,3]}"#;
8994 let bytes = build_with_metadata(first);
8995 for target in [
8996 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(), ] {
9001 let spliced = replace_metadata(&bytes, target).unwrap();
9002 assert_eq!(
9003 spliced,
9004 build_with_metadata(target),
9005 "splicing {} bytes of metadata must equal a direct write",
9006 target.len()
9007 );
9008 assert!(
9009 verify(&spliced).unwrap(),
9010 "the new hash covers the new card"
9011 );
9012 let rete = Rete::open(&spliced).unwrap();
9013 assert_eq!(
9014 rete.metadata(),
9015 (!target.is_empty()).then_some(target),
9016 "the new payload reads back verbatim"
9017 );
9018 assert_eq!(
9020 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
9021 1
9022 );
9023 }
9024 }
9025
9026 #[test]
9029 fn replace_metadata_keeps_build_info_adjacent() {
9030 let with_info =
9031 attach_build_info(&build_with_metadata(br#"{"a":1}"#), br#"{"builder":"x"}"#).unwrap();
9032 let spliced = replace_metadata(&with_info, br#"{"a":1,"b":2}"#).unwrap();
9033 let h = Header::from_bytes(&spliced).unwrap();
9034 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
9035 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + h.metadata_len);
9036 assert_eq!(
9037 read_build_info(&spliced).unwrap().as_deref(),
9038 Some(br#"{"builder":"x"}"#.as_slice())
9039 );
9040 assert!(verify(&spliced).unwrap());
9041 assert_eq!(
9043 h.content_hash,
9044 Header::from_bytes(&build_with_metadata(br#"{"a":1,"b":2}"#))
9045 .unwrap()
9046 .content_hash
9047 );
9048 }
9049
9050 #[test]
9051 fn metadata_is_tamper_evident() {
9052 let card = br#"{"title":"x"}"#;
9053 let mut bytes = build_with_metadata(card);
9054 assert!(verify(&bytes).unwrap());
9055 bytes[HEADER_LEN + 2] ^= 0xff;
9057 assert!(
9058 !verify(&bytes).unwrap(),
9059 "tampering with the card must break verify()"
9060 );
9061 }
9062
9063 #[test]
9064 fn ranged_opens_do_not_fetch_metadata() {
9065 use crate::reader::{CountingReader, SliceReader};
9066 let card = vec![0xABu8; 512]; let bytes = build_with_metadata(&card);
9068 let total = bytes.len() as u64;
9069
9070 let r = CountingReader::new(SliceReader::new(&bytes));
9072 let rete = Rete::open_ranged(&r).unwrap();
9073 assert!(
9074 rete.metadata().is_none(),
9075 "open_ranged must not load the card"
9076 );
9077 assert!(r.requests() <= 4, "requests = {}", r.requests());
9078 assert!(
9079 r.bytes_read() <= total - card.len() as u64,
9080 "read {} of {} bytes; the {}-byte card must be skipped",
9081 r.bytes_read(),
9082 total,
9083 card.len()
9084 );
9085
9086 let rs = CountingReader::new(SliceReader::new(&bytes));
9088 let view = SummaryView::open_ranged(&rs).unwrap().unwrap();
9089 assert!(!view.summary.is_empty());
9090 assert!(rs.bytes_read() <= total - card.len() as u64);
9091 }
9092
9093 #[test]
9094 fn metadata_ranged_fetches_only_header_and_card() {
9095 use crate::reader::{CountingReader, SliceReader};
9096 let card = vec![0xCDu8; 384];
9099 let bytes = build_with_metadata(&card);
9100
9101 let r = CountingReader::new(SliceReader::new(&bytes));
9102 let got = read_metadata_ranged(&r).unwrap().unwrap();
9103 assert_eq!(got, card, "the card reads back verbatim");
9104 assert_eq!(r.requests(), 2, "exactly header + metadata ranges");
9105 assert_eq!(
9106 r.bytes_read(),
9107 HEADER_LEN as u64 + card.len() as u64,
9108 "no dictionary/index/pyramid bytes are touched"
9109 );
9110
9111 let plain = build_image();
9113 let rp = CountingReader::new(SliceReader::new(&plain));
9114 assert!(read_metadata_ranged(&rp).unwrap().is_none());
9115 assert_eq!(rp.requests(), 1, "header only for a cardless file");
9116 assert_eq!(rp.bytes_read(), HEADER_LEN as u64);
9117 }
9118
9119 #[test]
9120 fn build_info_attaches_outside_the_hash() {
9121 let card = br#"{"title":"My Dataset"}"#;
9122 let base = build_with_metadata(card);
9123 let base_hash = Header::from_bytes(&base).unwrap().content_hash;
9124
9125 let info = br#"{"built_at":"2026-08-04T00:00:00Z","builder":"rete-cli 0.3.2"}"#;
9126 let with = attach_build_info(&base, info).unwrap();
9127
9128 assert_eq!(read_build_info(&with).unwrap().unwrap(), info);
9130 let h = Header::from_bytes(&with).unwrap();
9131 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + card.len() as u64);
9132 assert_eq!(h.build_info_len, info.len() as u64);
9133 assert_eq!(h.expected_file_len(), Some(with.len() as u64));
9134
9135 assert_eq!(h.content_hash, base_hash);
9139 assert!(verify(&with).unwrap());
9140 let other = attach_build_info(&base, br#"{"built_at":"1999-01-01T00:00:00Z"}"#).unwrap();
9141 assert_eq!(Header::from_bytes(&other).unwrap().content_hash, base_hash);
9142
9143 let rete = Rete::open(&with).unwrap();
9145 assert_eq!(
9146 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
9147 1
9148 );
9149 assert_eq!(rete.metadata(), Some(card.as_slice()));
9150
9151 let replaced = attach_build_info(&with, br#"{"builder":"other"}"#).unwrap();
9154 assert_eq!(
9155 read_build_info(&replaced).unwrap().unwrap(),
9156 br#"{"builder":"other"}"#
9157 );
9158 assert!(verify(&replaced).unwrap());
9159 let stripped = attach_build_info(&replaced, &[]).unwrap();
9160 assert_eq!(stripped, base, "stripping build-info restores the image");
9161 }
9162
9163 #[test]
9164 fn build_info_attaches_on_a_metadata_free_file() {
9165 let base = build_image();
9167 let info = b"{\"builder\":\"x\"}";
9168 let with = attach_build_info(&base, info).unwrap();
9169 let h = Header::from_bytes(&with).unwrap();
9170 assert_eq!(h.build_info_offset, HEADER_LEN as u64);
9171 assert_eq!(h.metadata_len, 0);
9172 assert_eq!(read_build_info(&with).unwrap().unwrap(), info);
9173 assert!(verify(&with).unwrap());
9174 assert_eq!(
9175 h.content_hash,
9176 Header::from_bytes(&base).unwrap().content_hash
9177 );
9178 Rete::open(&with).unwrap();
9179 }
9180
9181 #[test]
9188 fn a_plan_rebuilds_exactly_what_the_in_memory_splice_produces() {
9189 let card = br#"{"title":"My Dataset"}"#;
9190 let base = build_with_metadata(card);
9191 let base_hash = Header::from_bytes(&base).unwrap().content_hash;
9192 for start in [
9193 base.clone(),
9194 attach_build_info(&base, b"{\"a\":1}").unwrap(),
9195 ] {
9196 for info in [
9197 br#"{"builder":"rete-cli 0.3.2","query_costs":{}}"#.as_slice(), b"{}".as_slice(), b"".as_slice(), ] {
9201 let plan = plan_build_info(&start, start.len() as u64, info.len() as u64).unwrap();
9202 let mut streamed = Vec::new();
9203 streamed.extend_from_slice(&plan.header);
9204 streamed.extend_from_slice(&start[HEADER_LEN..plan.insert as usize]);
9205 streamed.extend_from_slice(info);
9206 streamed.extend_from_slice(&start[plan.tail_start as usize..]);
9207 assert_eq!(streamed.len() as u64, plan.new_len);
9208 assert_eq!(
9209 streamed,
9210 attach_build_info(&start, info).unwrap(),
9211 "a {}-byte section streamed must equal the same section spliced",
9212 info.len()
9213 );
9214 assert!(verify(&streamed).unwrap());
9216 let h = Header::from_bytes(&streamed).unwrap();
9217 assert_eq!(h.content_hash, base_hash);
9218 assert_eq!(h.expected_file_len(), Some(plan.new_len));
9219 let rete = Rete::open(&streamed).unwrap();
9220 assert_eq!(rete.metadata(), Some(card.as_slice()));
9221 assert_eq!(
9222 rete.query(Some("Bob"), Some("knows"), Some("Carol")).len(),
9223 1
9224 );
9225 }
9226 }
9227 }
9228
9229 #[test]
9232 fn a_plan_refuses_a_header_that_overruns_the_file() {
9233 let base = build_with_metadata(br#"{"title":"x"}"#);
9234 assert!(plan_build_info(&base, 8, 16).is_err());
9235 assert!(plan_build_info(&base[..HEADER_LEN], base.len() as u64, 16).is_ok());
9236 }
9237
9238 #[test]
9239 fn card_and_build_info_ranged_is_one_header_plus_one_range() {
9240 use crate::reader::{CountingReader, SliceReader};
9241 let card = vec![0xCDu8; 384];
9242 let info = vec![0xEFu8; 200];
9243 let bytes = attach_build_info(&build_with_metadata(&card), &info).unwrap();
9244
9245 let r = CountingReader::new(SliceReader::new(&bytes));
9248 let (m, b) = read_card_and_build_info_ranged(&r).unwrap();
9249 assert_eq!(m.unwrap(), card);
9250 assert_eq!(b.unwrap(), info);
9251 assert_eq!(r.requests(), 2, "header + one coalesced range");
9252 assert_eq!(
9253 r.bytes_read(),
9254 HEADER_LEN as u64 + card.len() as u64 + info.len() as u64
9255 );
9256
9257 let plain = build_with_metadata(&card);
9259 let rp = CountingReader::new(SliceReader::new(&plain));
9260 let (m, b) = read_card_and_build_info_ranged(&rp).unwrap();
9261 assert_eq!(m.unwrap(), card);
9262 assert!(b.is_none());
9263 assert_eq!(rp.requests(), 2);
9264 assert_eq!(
9269 r.requests(),
9270 rp.requests(),
9271 "build info must not cost an extra request",
9272 );
9273
9274 let none = build_image();
9276 let rn = CountingReader::new(SliceReader::new(&none));
9277 let (m, b) = read_card_and_build_info_ranged(&rn).unwrap();
9278 assert!(m.is_none() && b.is_none());
9279 assert_eq!(rn.requests(), 1);
9280 }
9281
9282 #[test]
9283 fn schema_summary_groups_by_type() {
9284 let rt = RDF_TYPE;
9285 let bytes = build_from(&[
9286 ("Alice", rt, "Person"),
9287 ("Bob", rt, "Person"),
9288 ("NYC", rt, "City"),
9289 ("Alice", "knows", "Bob"),
9290 ("Alice", "livesIn", "NYC"),
9291 ("Alice", "name", "\"Alice\""),
9292 ]);
9293 let rete = Rete::open(&bytes).unwrap();
9294 let summary = schema_summary(&rete);
9295 assert!(summary.contains(&("Person".into(), "knows".into(), "Person".into(), 1)));
9297 assert!(summary.contains(&("Person".into(), "livesIn".into(), "City".into(), 1)));
9298 assert!(summary.contains(&("Person".into(), "name".into(), "(literal)".into(), 1)));
9299 assert!(!summary.iter().any(|(_, p, _, _)| p == RDF_TYPE));
9301
9302 let classes = schema_classes(&rete);
9304 assert_eq!(
9305 classes,
9306 vec![("Person".into(), 2u32), ("City".into(), 1u32)]
9307 );
9308 }
9309
9310 fn build_from(triples: &[(&str, &str, &str)]) -> Vec<u8> {
9311 let mut db = DictionaryBuilder::new();
9312 for (s, p, o) in triples {
9313 db.observe(s, p, o);
9314 }
9315 let dict = db.build();
9316 let mut ib = GraphIndexBuilder::new();
9317 for (s, p, o) in triples {
9318 ib.push(dict.encode(s, p, o).unwrap());
9319 }
9320 write_file(&dict, &ib.build(), false, &[], 0)
9321 }
9322
9323 fn build_with_pyramid(triples: &[(&str, &str, &str)]) -> Vec<u8> {
9325 let mut db = DictionaryBuilder::new();
9326 for (s, p, o) in triples {
9327 db.observe(s, p, o);
9328 }
9329 let dict = db.build();
9330 let encoded: Vec<_> = triples
9331 .iter()
9332 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
9333 .collect();
9334 let mut ib = GraphIndexBuilder::new();
9335 for t in &encoded {
9336 ib.push(*t);
9337 }
9338 let (meta, levels) = build_pyramid_meta(&dict, &encoded, DEFAULT_TILE_BUDGET);
9339 write_dataset(&dict, &ib.build(), &[], false, &meta, levels)
9340 }
9341
9342 #[test]
9343 fn tbox_coherence_flags_unsatisfiable_class_index_free() {
9344 use crate::reader::{CountingReader, SliceReader};
9345 let rt = RDF_TYPE;
9346 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
9347 let disj = "<http://www.w3.org/2002/07/owl#disjointWith>";
9348 let bytes = build_with_pyramid(&[
9351 ("<http://ex/C>", sub, "<http://ex/D>"),
9352 ("<http://ex/C>", sub, "<http://ex/E>"),
9353 ("<http://ex/D>", disj, "<http://ex/E>"),
9354 ("<http://ex/x>", rt, "<http://ex/C>"),
9355 ]);
9356
9357 let r = CountingReader::new(SliceReader::new(&bytes));
9358 let view = SummaryView::open_ranged(&r).unwrap().unwrap();
9359 let points = view.tbox_coherence();
9360 assert!(
9361 points
9362 .iter()
9363 .any(|i| i.kind == "unsatisfiable-class" && i.detail.contains("http://ex/C>")),
9364 "expected C unsatisfiable from the schema alone, got {points:?}"
9365 );
9366
9367 let header = Header::from_bytes(&bytes[..HEADER_LEN]).unwrap();
9370 assert!(
9371 r.bytes_read() <= bytes.len() as u64 - header.root_dir_len,
9372 "tbox_coherence must not read the triple index"
9373 );
9374 }
9375
9376 #[test]
9377 fn schema_coherence_reads_only_the_schema_block() {
9378 use crate::reader::{CountingReader, SliceReader};
9379 let rt = RDF_TYPE;
9380 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
9381 let disj = "<http://www.w3.org/2002/07/owl#disjointWith>";
9382 let mut triples: Vec<(String, String, String)> = vec![
9386 ("<http://ex/C>".into(), sub.into(), "<http://ex/D>".into()),
9387 ("<http://ex/C>".into(), sub.into(), "<http://ex/E>".into()),
9388 ("<http://ex/D>".into(), disj.into(), "<http://ex/E>".into()),
9389 ];
9390 for i in 0..500 {
9391 let s = format!("<http://ex/x{i}>");
9392 triples.push((s.clone(), rt.into(), "<http://ex/C>".into()));
9393 triples.push((
9394 s,
9395 "<http://ex/label>".into(),
9396 format!("\"unique label {i}\""),
9397 ));
9398 }
9399 let trefs: Vec<(&str, &str, &str)> = triples
9400 .iter()
9401 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
9402 .collect();
9403 let bytes = build_with_pyramid(&trefs);
9404
9405 let header = Header::from_bytes(&bytes[..HEADER_LEN]).unwrap();
9406 assert!(
9407 header.schema_meta_len > 0,
9408 "the writer recorded a schema-block length"
9409 );
9410 assert!(
9411 (header.schema_meta_len as u64) < header.pyramid_meta_len,
9412 "schema block ({}) should be far smaller than the whole pyramid-meta ({})",
9413 header.schema_meta_len,
9414 header.pyramid_meta_len
9415 );
9416
9417 let r = CountingReader::new(SliceReader::new(&bytes));
9418 let points = read_schema_coherence_ranged(&r).unwrap().unwrap();
9419 assert!(points.iter().any(|i| i.kind == "unsatisfiable-class"));
9420 assert!(
9422 r.bytes_read() <= HEADER_LEN as u64 + header.schema_meta_len as u64,
9423 "read {} bytes; expected <= header + schema block ({})",
9424 r.bytes_read(),
9425 HEADER_LEN as u64 + header.schema_meta_len as u64
9426 );
9427 }
9428
9429 #[test]
9430 fn tbox_coherence_clean_schema_is_coherent() {
9431 use crate::reader::SliceReader;
9432 let rt = RDF_TYPE;
9433 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
9434 let bytes = build_with_pyramid(&[
9435 ("<http://ex/Dog>", sub, "<http://ex/Animal>"),
9436 ("<http://ex/x>", rt, "<http://ex/Dog>"),
9437 ]);
9438 let view = SummaryView::open_ranged(&SliceReader::new(&bytes))
9439 .unwrap()
9440 .unwrap();
9441 assert!(view.tbox_is_coherent(), "a plain hierarchy is coherent");
9442 }
9443
9444 #[test]
9452 fn readers_before_the_quad_index_refuse_its_files() {
9453 for perms in [PermSet::ALL, PermSet::CORE] {
9454 let quads: Vec<crate::ingest::RawQuad> = (0..50)
9455 .map(|i| {
9456 (
9457 format!("<http://ex/s{i}>"),
9458 "<http://ex/p>".to_string(),
9459 format!("\"{i}\""),
9460 Some(format!("<http://ex/g{}>", i % 7)),
9461 )
9462 })
9463 .collect();
9464 let (bytes, _) = crate::ingest::assemble_dataset_with_layout(
9465 quads,
9466 false,
9467 false,
9468 None,
9469 crate::PyramidAlgo::Louvain,
9470 perms,
9471 crate::ingest::GraphLayout::Quad,
9472 |_, _| Vec::<u8>::new(),
9473 );
9474 let h = Header::from_bytes(&bytes).unwrap();
9475 assert!(h.has_quad_index());
9476 let root =
9477 &bytes[h.root_dir_offset as usize..(h.root_dir_offset + h.root_dir_len) as usize];
9478 assert!(decode_index_container(root, h.block_codec, h.perms).is_err());
9480 assert!(
9481 container_section_payload_ranges(root, h.root_dir_offset, h.perms.len()).is_err()
9482 );
9483 assert_ne!(decode_container(root, CODEC_NONE).unwrap().len(), NUM_PERMS);
9485 assert_ne!(
9486 decode_container(root, CODEC_NONE).unwrap().len(),
9487 h.perms.len()
9488 );
9489 let r = crate::reader::SliceReader::new(&bytes);
9491 for expect in [h.perms.len(), NUM_PERMS] {
9492 assert!(locate_container_section_ranged(
9493 &r,
9494 h.root_dir_offset,
9495 h.root_dir_len,
9496 0,
9497 expect as u64
9498 )
9499 .is_err());
9500 }
9501 assert_eq!(hash_of_sections(&h, &bytes).unwrap(), h.content_hash);
9503 let rete = Rete::open(&bytes).unwrap();
9505 assert_eq!(rete.named_graph_count(), 7);
9506 assert_eq!(rete.dump(Some("<http://ex/g3>")).len(), 7);
9507 }
9508 }
9509
9510 #[test]
9511 fn named_graphs_round_trip() {
9512 let all = [
9514 ("Alice", "knows", "Bob"), ("Bob", "age", "30"), ];
9517 let mut db = DictionaryBuilder::new();
9518 for (s, p, o) in all {
9519 db.observe(s, p, o);
9520 }
9521 let dict = db.build();
9522
9523 let mut def = GraphIndexBuilder::new();
9524 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
9525 let mut g1 = GraphIndexBuilder::new();
9526 g1.push(dict.encode("Bob", "age", "30").unwrap());
9527
9528 let named = vec![("http://ex/g1".to_string(), g1.build())];
9529 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9530
9531 assert!(verify(&bytes).unwrap());
9532 let rete = Rete::open(&bytes).unwrap();
9533 assert_eq!(rete.graph_names(), vec!["http://ex/g1"]);
9534 let gi = rete.graph_index("http://ex/g1").unwrap();
9536 assert_eq!(gi.triple_count(), 1);
9537 assert!(rete.graph_index("http://ex/missing").is_none());
9538
9539 assert_eq!(rete.header().quad_count, 2);
9542
9543 assert_eq!(rete.query(Some("Alice"), None, None).len(), 1);
9545
9546 assert_eq!(
9548 rete.dump(None),
9549 vec![("Alice".into(), "knows".into(), "Bob".into())]
9550 );
9551 assert_eq!(
9552 rete.dump(Some("http://ex/g1")),
9553 vec![("Bob".into(), "age".into(), "30".into())]
9554 );
9555 }
9556
9557 #[test]
9563 fn named_graph_container_opens_tile_lazily_like_the_root() {
9564 let mut db = DictionaryBuilder::new();
9565 let node = |n: u32| format!("<http://ex/n{n}>");
9566 let p = "<http://ex/p>".to_string();
9567 db.observe(&node(0), &p, &node(1));
9568 for i in 0..500u32 {
9569 db.observe(&node(1000 + i), &p, &node(2000 + i));
9570 }
9571 let dict = db.build();
9572 let mut def = GraphIndexBuilder::new();
9573 def.push(dict.encode(&node(0), &p, &node(1)).unwrap());
9574 let mut g = GraphIndexBuilder::new().with_tile_budget(256); for i in 0..500u32 {
9576 g.push(dict.encode(&node(1000 + i), &p, &node(2000 + i)).unwrap());
9577 }
9578 let named = vec![("<http://ex/g>".to_string(), g.build())];
9579 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9580 let rete = Rete::open(&bytes).unwrap();
9581 let header = rete.header();
9582
9583 let soff = header.named_graphs_offset as usize;
9586 let send = soff + header.named_graphs_len as usize;
9587 let sec = &bytes[soff..send];
9588 let (n, mut pos) = read_uvarint(sec).unwrap();
9589 assert_eq!(n, 1);
9590 let (ilen, u1) = read_uvarint(&sec[pos..]).unwrap();
9591 pos += u1 + ilen as usize;
9592 let (clen, u2) = read_uvarint(&sec[pos..]).unwrap();
9593 pos += u2;
9594 let container = ByteRange {
9595 offset: (soff + pos) as u64,
9596 len: clen,
9597 };
9598
9599 use crate::reader::SliceReader;
9600 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
9601 let reader: std::sync::Arc<dyn RangeReader + Send + Sync> =
9602 std::sync::Arc::new(SliceReader::new(leaked));
9603 let (lazy_idx, _, _) = open_index_container_lazy(
9604 &reader,
9605 container,
9606 header.block_codec,
9607 header.has_tile_synopsis(),
9608 1,
9609 header.perms,
9610 header.perms.len(),
9611 )
9612 .unwrap();
9613 let mut want = rete
9614 .graph_index("<http://ex/g>")
9615 .unwrap()
9616 .match_pattern((None, None, None));
9617 let mut got = lazy_idx.match_pattern((None, None, None));
9618 want.sort_unstable();
9619 got.sort_unstable();
9620 assert_eq!(got.len(), 500);
9621 assert_eq!(got, want);
9622 assert!(!lazy_idx.load_incomplete());
9623 }
9624
9625 #[test]
9631 fn dump_iter_matches_dump_and_stops_early() {
9632 use crate::reader::{CountingReader, SliceReader};
9633
9634 let triples: Vec<(String, String, String)> = (0..2000u32)
9636 .map(|i| {
9637 (
9638 format!("<http://ex/s/{i:04}>"),
9639 "<http://ex/p>".to_string(),
9640 format!("<http://ex/o/{i:04}>"),
9641 )
9642 })
9643 .collect();
9644 let mut db = DictionaryBuilder::new();
9645 for (s, p, o) in &triples {
9646 db.observe(s, p, o);
9647 }
9648 let dict = db.build();
9649 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
9650 for (s, p, o) in &triples {
9651 ib.push(dict.encode(s, p, o).unwrap());
9652 }
9653 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
9654
9655 let rete = Rete::open(&bytes).unwrap();
9657 assert_eq!(rete.dump_iter(None).collect::<Vec<_>>(), rete.dump(None));
9658 assert_eq!(rete.dump_iter(Some("http://ex/nope")).count(), 0);
9660
9661 let read_bytes = |take: usize| -> u64 {
9663 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
9664 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9665 let rete = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
9666 let before = reader.bytes_read();
9667 let n = rete.dump_iter(None).take(take).count();
9668 assert_eq!(n, take.min(triples.len()));
9669 assert!(!rete.index_incomplete());
9670 reader.bytes_read() - before
9671 };
9672 let first_only = read_bytes(1);
9673 let everything = read_bytes(triples.len());
9674 assert!(
9676 first_only < everything,
9677 "dump_iter is not lazy: one triple read {first_only} B, a full scan {everything} B"
9678 );
9679 #[cfg(feature = "compression")]
9686 assert!(
9687 first_only * 2 < everything,
9688 "dump_iter is not lazy: one triple read {first_only} B of the {everything} B a full scan reads"
9689 );
9690
9691 let fresh = Rete::open(&bytes).unwrap();
9696 let drain = |max_quads: usize| {
9701 let (mut out, mut cursor, mut calls) = (Vec::new(), 0u32, 0);
9702 loop {
9703 let (batch, next, done) = fresh.dump_batch(None, cursor, max_quads);
9704 out.extend(batch);
9705 cursor = next;
9706 calls += 1;
9707 assert!(calls < 10_000, "cursor failed to advance");
9708 if done {
9709 break out;
9710 }
9711 }
9712 };
9713 let eager = fresh.dump(None);
9714 for max in [1usize, 7, 128, 100_000] {
9715 assert_eq!(drain(max), eager, "batch size {max} changed the dump");
9716 }
9717 let (t, _, done) = fresh.dump_batch(Some("http://ex/nope"), 0, 16);
9720 assert!(t.is_empty() && done);
9721 }
9722
9723 #[test]
9731 fn query_batch_matches_query_in_graph_and_stops_early() {
9732 use crate::reader::{CountingReader, SliceReader};
9733
9734 let triples: Vec<(String, String, String)> = (0..2000u32)
9737 .map(|i| {
9738 (
9739 format!("<http://ex/s/{:04}>", i / 2),
9740 format!("<http://ex/p/{}>", i % 2),
9741 format!("<http://ex/o/{i:04}>"),
9742 )
9743 })
9744 .collect();
9745 let mut db = DictionaryBuilder::new();
9746 for (s, p, o) in &triples {
9747 db.observe(s, p, o);
9748 }
9749 let dict = db.build();
9750 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
9751 for (s, p, o) in &triples {
9752 ib.push(dict.encode(s, p, o).unwrap());
9753 }
9754 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
9755 let rete = Rete::open(&bytes).unwrap();
9756
9757 let drain = |s: Option<&str>, p: Option<&str>, o: Option<&str>, max: usize| {
9758 let (mut out, mut cursor, mut calls) = (Vec::new(), 0u64, 0);
9759 loop {
9760 let (batch, next, done) = rete.query_batch(None, s, p, o, cursor, max);
9761 assert!(!batch.is_empty() || done, "empty non-final batch");
9764 out.extend(batch);
9765 cursor = next;
9766 calls += 1;
9767 assert!(calls < 20_000, "cursor failed to advance");
9768 if done {
9769 break out;
9770 }
9771 }
9772 };
9773 let shapes: [(Option<&str>, Option<&str>, Option<&str>); 6] = [
9774 (None, None, None),
9775 (Some("<http://ex/s/0007>"), None, None),
9776 (None, Some("<http://ex/p/1>"), None),
9777 (None, None, Some("<http://ex/o/0042>")),
9778 (Some("<http://ex/s/0007>"), Some("<http://ex/p/0>"), None),
9779 (
9780 Some("<http://ex/s/0007>"),
9781 Some("<http://ex/p/0>"),
9782 Some("<http://ex/o/0014>"),
9783 ),
9784 ];
9785 for (s, p, o) in shapes {
9786 let eager = rete.query_in_graph(None, s, p, o);
9787 assert!(
9788 !eager.is_empty(),
9789 "fixture shape {s:?} {p:?} {o:?} matches nothing"
9790 );
9791 assert_eq!(
9793 rete.query_iter(None, s, p, o).collect::<Vec<_>>(),
9794 eager,
9795 "query_iter disagrees for {s:?} {p:?} {o:?}"
9796 );
9797 for max in [1usize, 3, 64, 100_000] {
9798 let mut got = drain(s, p, o, max);
9799 let mut want = eager.clone();
9800 got.sort();
9803 want.sort();
9804 assert_eq!(got, want, "batch size {max} changed {s:?} {p:?} {o:?}");
9805 }
9806 if s.is_none() && p.is_none() && o.is_none() {
9808 assert_eq!(drain(s, p, o, 7), eager, "unbound batching reordered rows");
9809 }
9810 }
9811
9812 let (t, _, done) = rete.query_batch(None, Some("<http://ex/nope>"), None, None, 0, 16);
9815 assert!(t.is_empty() && done);
9816 let (t, _, done) = rete.query_batch(Some("http://ex/nope"), None, None, None, 0, 16);
9817 assert!(t.is_empty() && done);
9818 assert_eq!(
9819 rete.query_iter(None, Some("<http://ex/nope>"), None, None)
9820 .count(),
9821 0
9822 );
9823
9824 let read_bytes = |take: usize| -> u64 {
9827 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
9828 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
9829 let rete = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
9830 let before = reader.bytes_read();
9831 let (mut n, mut cursor) = (0usize, 0u64);
9832 loop {
9833 let (batch, next, done) = rete.query_batch(None, None, None, None, cursor, 1);
9834 n += batch.len();
9835 cursor = next;
9836 if done || n >= take {
9837 break;
9838 }
9839 }
9840 assert!(n >= take.min(triples.len()));
9841 assert!(!rete.index_incomplete());
9842 reader.bytes_read() - before
9843 };
9844 let first_only = read_bytes(1);
9845 let everything = read_bytes(triples.len());
9846 assert!(
9847 first_only < everything,
9848 "query_batch is not lazy: one row read {first_only} B, a full scan {everything} B"
9849 );
9850 #[cfg(feature = "compression")]
9851 assert!(
9852 first_only * 2 < everything,
9853 "query_batch is not lazy: one row read {first_only} B of the {everything} B a full scan reads"
9854 );
9855 }
9856
9857 #[test]
9860 fn query_batch_is_graph_scoped() {
9861 let mut db = DictionaryBuilder::new();
9862 for (s, p, o) in [
9863 ("Alice", "knows", "Bob"),
9864 ("Alice", "knows", "Carol"),
9865 ("Alice", "knows", "Dave"),
9866 ("Erin", "knows", "Frank"),
9867 ("Alice", "knows", "Zoe"),
9868 ] {
9869 db.observe(s, p, o);
9870 }
9871 let dict = db.build();
9872 let mut def = GraphIndexBuilder::new();
9873 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
9874 def.push(dict.encode("Alice", "knows", "Carol").unwrap());
9875 let mut g1 = GraphIndexBuilder::new();
9876 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
9877 g1.push(dict.encode("Erin", "knows", "Frank").unwrap());
9878 let mut g2 = GraphIndexBuilder::new();
9879 g2.push(dict.encode("Alice", "knows", "Zoe").unwrap());
9880 let named = vec![
9881 ("http://ex/g1".to_string(), g1.build()),
9882 ("http://ex/g2".to_string(), g2.build()),
9883 ];
9884 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9885 let rete = Rete::open(&bytes).unwrap();
9886 for graph in [None, Some("http://ex/g1"), Some("http://ex/g2")] {
9887 let eager = rete.query_in_graph(graph, None, None, None);
9888 let (mut out, mut cursor) = (Vec::new(), 0u64);
9889 loop {
9890 let (batch, next, done) = rete.query_batch(graph, None, None, None, cursor, 1);
9891 out.extend(batch);
9892 cursor = next;
9893 if done {
9894 break;
9895 }
9896 }
9897 assert_eq!(out, eager, "graph {graph:?} batched differently");
9898 }
9899 }
9900
9901 #[test]
9902 fn query_in_graph_is_graph_scoped() {
9903 let mut db = DictionaryBuilder::new();
9906 for (s, p, o) in [
9907 ("Alice", "knows", "Bob"),
9908 ("Alice", "knows", "Carol"),
9909 ("Alice", "knows", "Dave"),
9910 ] {
9911 db.observe(s, p, o);
9912 }
9913 let dict = db.build();
9914
9915 let mut def = GraphIndexBuilder::new();
9916 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
9917 def.push(dict.encode("Alice", "knows", "Carol").unwrap());
9918 let mut g1 = GraphIndexBuilder::new();
9919 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
9920
9921 let named = vec![("http://ex/g1".to_string(), g1.build())];
9922 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
9923 let rete = Rete::open(&bytes).unwrap();
9924
9925 let mut def_objs: Vec<String> = rete
9927 .query_in_graph(None, Some("Alice"), Some("knows"), None)
9928 .into_iter()
9929 .map(|(_, _, o)| o)
9930 .collect();
9931 def_objs.sort();
9932 assert_eq!(def_objs, vec!["Bob".to_string(), "Carol".to_string()]);
9933
9934 assert_eq!(
9936 rete.query_in_graph(Some("http://ex/g1"), Some("Alice"), None, None),
9937 vec![("Alice".into(), "knows".into(), "Dave".into())]
9938 );
9939
9940 assert_eq!(rete.query_in_graph(None, None, None, None).len(), 2);
9942 assert_eq!(
9943 rete.query_in_graph(Some("http://ex/g1"), None, None, None)
9944 .len(),
9945 1
9946 );
9947
9948 assert!(rete
9950 .query_in_graph(Some("http://ex/missing"), None, None, None)
9951 .is_empty());
9952 }
9953
9954 #[test]
9967 fn bounded_query_in_graph_does_not_fault_the_whole_dictionary() {
9968 use crate::reader::{CountingReader, SliceReader};
9969
9970 let payload = |i: u32| -> String {
9976 let mut s = String::with_capacity(800);
9977 let mut x = i as u64 ^ 0x9e37_79b9_7f4a_7c15;
9978 while s.len() < 800 {
9979 x = x
9980 .wrapping_mul(6364136223846793005)
9981 .wrapping_add(1442695040888963407);
9982 s.push_str(&format!("{:016x}", x ^ (x >> 29)));
9983 }
9984 s
9985 };
9986 let quads: Vec<(String, String, String, bool)> = (0..2000u32)
9993 .map(|i| {
9994 let named = i % 2 == 0;
9995 let ns = if named { 'a' } else { 'b' };
9996 (
9997 format!("<http://ex/{ns}/s/{i:04}>"),
9998 "<http://ex/abstract>".to_string(),
9999 format!("\"{ns}{} number {i}\"", payload(i)),
10000 named,
10001 )
10002 })
10003 .collect();
10004 let mut db = DictionaryBuilder::new();
10005 for (s, p, o, _) in &quads {
10006 db.observe(s, p, o);
10007 }
10008 let dict = db.build();
10009 let mut def = GraphIndexBuilder::new().with_tile_budget(256);
10010 let mut g1 = GraphIndexBuilder::new().with_tile_budget(256);
10011 for (s, p, o, named) in &quads {
10012 let t = dict.encode(s, p, o).unwrap();
10013 if *named {
10014 g1.push(t);
10015 } else {
10016 def.push(t);
10017 }
10018 }
10019 let named = vec![("http://ex/g1".to_string(), g1.build())];
10020 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
10021
10022 let eager = Rete::open(&bytes).unwrap();
10023 let dict_len = eager.header().dictionary_len;
10024
10025 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
10028 let probe = |f: &dyn Fn(&Rete) -> Vec<TermTriple>| -> (Vec<TermTriple>, u64) {
10029 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
10030 let lazy = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
10031 let before = reader.bytes_read();
10032 let out = f(&lazy);
10033 assert!(!lazy.index_incomplete(), "a chunk fetch failed");
10034 (out, reader.bytes_read() - before)
10035 };
10036
10037 let want = eager.query_in_graph(
10039 Some("http://ex/g1"),
10040 Some("<http://ex/a/s/0100>"),
10041 None,
10042 None,
10043 );
10044 assert_eq!(want.len(), 1, "fixture: expected exactly one match");
10045 let (got, pulled) = probe(&|r| {
10046 r.query_in_graph(
10047 Some("http://ex/g1"),
10048 Some("<http://ex/a/s/0100>"),
10049 None,
10050 None,
10051 )
10052 });
10053 assert_eq!(got, want, "lazy query_in_graph disagrees with eager");
10054 assert!(
10055 pulled * 4 < dict_len,
10056 "a 1-row query_in_graph faulted {pulled} B of a {dict_len} B dictionary — \
10057 it is prefetching the whole thing again"
10058 );
10059
10060 let (empty, q_pulled) =
10068 probe(&|r| r.query_in_graph(Some("http://ex/nope"), None, None, None));
10069 assert!(empty.is_empty());
10070 let (_, d_pulled) = probe(&|r| {
10071 let mut n = 0usize;
10072 r.dump_each(Some("http://ex/nope"), |_, _, _| n += 1);
10073 assert_eq!(n, 0, "an absent graph yielded triples");
10074 Vec::new()
10075 });
10076 assert_eq!(
10077 q_pulled, d_pulled,
10078 "query_in_graph and dump_each disagree on what an absent graph costs"
10079 );
10080 assert!(
10081 q_pulled * 4 < dict_len,
10082 "an absent graph faulted {q_pulled} B of a {dict_len} B dictionary"
10083 );
10084
10085 let want: Vec<TermTriple> = eager.dump(Some("http://ex/g1"));
10088 assert_eq!(want.len(), 1000);
10089 let (got, pulled) = probe(&|r| {
10090 let mut out = Vec::new();
10091 r.dump_each(Some("http://ex/g1"), |s, p, o| {
10092 out.push((s.to_string(), p.to_string(), o.to_string()))
10093 });
10094 out
10095 });
10096 assert_eq!(got, want, "lazy dump_each disagrees with eager dump");
10097 assert!(
10098 pulled * 3 < dict_len * 2,
10099 "dumping half the graph faulted {pulled} B of a {dict_len} B dictionary — \
10100 the other graph's chunks are being pulled in too"
10101 );
10102 }
10103
10104 #[cfg(test)]
10109 fn build_multichunk_image() -> Vec<u8> {
10110 use crate::index::PermSet;
10111 let mut db = DictionaryBuilder::new();
10112 let mut triples: Vec<(String, String, String)> = (0..4000u32)
10113 .map(|i| {
10114 (
10115 format!("<http://ex/s/{i:07}>"),
10116 format!("<http://ex/p/{}>", i % 8),
10117 format!("\"object literal number {i:07} with padding text to widen the term\""),
10118 )
10119 })
10120 .collect();
10121 triples.push((
10122 "<http://ex/s/big>".to_string(),
10123 "<http://ex/p/big>".to_string(),
10124 format!("\"{}\"", "Z".repeat(8000)),
10125 ));
10126 for (s, p, o) in &triples {
10127 db.observe(s, p, o);
10128 }
10129 let dict = db.build();
10130 let mut idx = GraphIndexBuilder::new()
10131 .with_tile_budget(64)
10132 .with_perms(PermSet::ALL);
10133 for (s, p, o) in &triples {
10134 idx.push(dict.encode(s, p, o).unwrap());
10135 }
10136 write_dataset(&dict, &idx.build(), &[], true, &[], 0)
10137 }
10138
10139 #[test]
10148 fn tiny_dict_cache_evicts_yet_dump_is_byte_identical() {
10149 use crate::reader::SliceReader;
10150 let leaked: &'static [u8] = Box::leak(build_multichunk_image().into_boxed_slice());
10151 let dump = |cap: u64| -> (
10152 Vec<(String, String, String)>,
10153 crate::chunk_cache::CacheStats,
10154 ) {
10155 let rete =
10156 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64).unwrap();
10157 rete.dict.set_cache_cap(cap);
10158 let mut out = Vec::new();
10159 rete.dump_filtered_each(None, None, None, None, |s, p, o| {
10160 out.push((s.to_string(), p.to_string(), o.to_string()))
10161 });
10162 assert!(
10163 !rete.index_incomplete(),
10164 "no lazy fault should fail at cap {cap}"
10165 );
10166 let stats = rete.dict_cache_stats();
10167 (out, stats)
10168 };
10169
10170 let (unlimited, us) = dump(u64::MAX);
10171 assert_eq!(unlimited.len(), 4001, "every triple dumped");
10172 assert_eq!(us.evictions, 0, "unlimited cap evicts nothing");
10173
10174 let (tiny, ts) = dump(128 * 1024);
10177 assert_eq!(tiny, unlimited, "128 KiB-cap dump differs from unlimited");
10178 assert!(ts.evictions > 0, "a tiny cap must evict (stats {ts:?})");
10179
10180 let (starved, ss) = dump(1);
10182 assert_eq!(starved, unlimited, "1-byte-cap dump differs from unlimited");
10183 assert!(
10184 ss.evictions > 0,
10185 "1-byte cap evicts every insert (stats {ss:?})"
10186 );
10187 }
10188
10189 #[test]
10200 fn memory_budget_charges_the_fixed_directories() {
10201 use crate::reader::SliceReader;
10202 let leaked: &'static [u8] = Box::leak(build_multichunk_image().into_boxed_slice());
10203 let rete =
10204 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64).unwrap();
10205
10206 let fixed = rete.fixed_overhead();
10207 assert!(
10210 fixed.dict_directory > 0 && fixed.index_directory > 0,
10211 "{fixed:?}"
10212 );
10213 assert_eq!(fixed.named_graphs, 0, "no named graphs in this file");
10214 assert_eq!(
10215 fixed.total(),
10216 fixed.dict_directory + fixed.index_directory + fixed.named_graphs
10217 );
10218
10219 let total: u64 = 512 << 20;
10222 let applied = rete.set_memory_budget(Some(total));
10223 let plain = split_memory_budget(total);
10224 assert_eq!(applied.dict_cache, plain.dict_cache - fixed.total());
10225 assert_eq!(rete.dict_cache_cap(), applied.dict_cache);
10226 assert_eq!(applied.tile_cache, plain.tile_cache);
10227 assert_eq!(applied.block_cache, plain.block_cache);
10228 assert!(
10229 applied.dict_cache + applied.tile_cache + applied.block_cache + fixed.total() <= total,
10230 "caps + fixed directories exceed the budget: {applied:?} + {fixed:?} > {total}"
10231 );
10232
10233 let applied = rete.set_memory_budget(Some(1 << 20));
10235 assert_eq!(applied.dict_cache, MIN_DICT_CACHE_CAP);
10236
10237 let applied = rete.set_memory_budget(None);
10239 assert_eq!(applied.dict_cache, u64::MAX);
10240 assert_eq!(rete.dict_cache_cap(), u64::MAX);
10241
10242 let rete =
10245 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64).unwrap();
10246 rete.dict.set_cache_cap(128 * 1024);
10247 let mut rows = 0usize;
10248 rete.dump_filtered_each(None, None, None, None, |_, _, _| rows += 1);
10249 assert_eq!(rows, 4001);
10250 let s = rete.dict_cache_stats();
10251 assert!(s.evictions > 0, "the cap must bind: {s:?}");
10252 assert!(
10255 s.entries <= 1 || s.resident_bytes <= 128 * 1024,
10256 "dictionary cache over its cap after the dump: {s:?}"
10257 );
10258 }
10259
10260 #[test]
10265 fn releasing_a_named_graph_drops_and_reopens_its_index() {
10266 use crate::index::PermSet;
10267 use crate::reader::SliceReader;
10268 let mut db = DictionaryBuilder::new();
10269 let all: Vec<(String, String, String)> = (0..40u32)
10270 .map(|i| {
10271 (
10272 format!("<http://ex/s{}>", i % 7),
10273 format!("<http://ex/p{}>", i % 3),
10274 format!("<http://ex/o{}>", i % 5),
10275 )
10276 })
10277 .collect();
10278 for (s, p, o) in &all {
10279 db.observe(s, p, o);
10280 }
10281 let dict = db.build();
10282 let mk = |take: fn(u32) -> bool| {
10283 let mut g = GraphIndexBuilder::new()
10284 .with_tile_budget(16)
10285 .with_perms(PermSet::ALL);
10286 for (i, (s, p, o)) in all.iter().enumerate() {
10287 if take(i as u32) {
10288 g.push(dict.encode(s, p, o).unwrap());
10289 }
10290 }
10291 g.build()
10292 };
10293 let def = mk(|_| true);
10294 let g1 = mk(|i| i % 2 == 0);
10295 let g2 = mk(|i| i % 3 == 0);
10296 let image = write_dataset(
10297 &dict,
10298 &def,
10299 &[
10300 ("<http://ex/g1>".to_string(), g1),
10301 ("<http://ex/g2>".to_string(), g2),
10302 ],
10303 true,
10304 &[],
10305 0,
10306 );
10307 let leaked: &'static [u8] = Box::leak(image.into_boxed_slice());
10308 let mut rete =
10309 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64).unwrap();
10310
10311 let opened = |r: &Rete| -> usize {
10312 match &r.named_graphs {
10313 NamedGraphsSlot::Lazy(l) => {
10314 let mut n = 0;
10315 l.for_each_opened(|_| n += 1);
10316 n
10317 }
10318 _ => 0,
10319 }
10320 };
10321
10322 assert_eq!(opened(&rete), 0, "nothing opened before first touch");
10323 assert!(rete.graph_index("<http://ex/g1>").is_some());
10325 assert_eq!(opened(&rete), 1, "g1 opened");
10326 rete.release_named_graph("<http://ex/g1>");
10328 assert_eq!(opened(&rete), 0, "g1 released");
10329 rete.release_named_graph("<http://ex/absent>");
10331 rete.release_named_graph("<http://ex/g1>");
10332 assert_eq!(opened(&rete), 0);
10333 assert!(rete.graph_index("<http://ex/g1>").is_some());
10335 assert_eq!(opened(&rete), 1, "g1 re-opened after release");
10336 }
10337
10338 #[test]
10356 fn filtered_dump_is_exactly_the_unfiltered_dump_filtered() {
10357 use crate::index::PermSet;
10358 use crate::reader::SliceReader;
10359
10360 let terms = |i: u32| {
10361 (
10362 format!("<http://ex/s{}>", i % 11),
10363 format!("<http://ex/p{}>", i % 3),
10364 if i.is_multiple_of(4) {
10368 format!("\"lit {}\"@en", i % 5)
10369 } else {
10370 format!("<http://ex/o{}>", i % 7)
10371 },
10372 )
10373 };
10374 let build = |perms: PermSet| {
10375 let mut db = DictionaryBuilder::new();
10376 let all: Vec<_> = (0..90u32).map(terms).collect();
10377 for (s, p, o) in &all {
10378 db.observe(s, p, o);
10379 }
10380 let dict = db.build();
10381 let mut def = GraphIndexBuilder::new()
10382 .with_tile_budget(16)
10383 .with_perms(perms);
10384 let mut g1 = GraphIndexBuilder::new()
10385 .with_tile_budget(16)
10386 .with_perms(perms);
10387 for (i, (s, p, o)) in all.iter().enumerate() {
10388 let t = dict.encode(s, p, o).unwrap();
10389 if i % 2 == 0 {
10393 g1.push(t);
10394 }
10395 def.push(t);
10396 }
10397 write_dataset(
10398 &dict,
10399 &def.build(),
10400 &[("<http://ex/g1>".to_string(), g1.build())],
10401 true,
10402 &[],
10403 0,
10404 )
10405 };
10406
10407 let shapes: Vec<(Option<&str>, Option<&str>, Option<&str>)> = vec![
10408 (None, None, None),
10409 (Some("<http://ex/s3>"), None, None),
10410 (None, Some("<http://ex/p1>"), None),
10411 (None, None, Some("<http://ex/o5>")),
10412 (None, None, Some("\"lit 0\"@en")),
10413 (Some("<http://ex/s3>"), Some("<http://ex/p0>"), None),
10414 (Some("<http://ex/s4>"), None, Some("<http://ex/o4>")),
10415 (None, Some("<http://ex/p1>"), Some("<http://ex/o5>")),
10416 (
10417 Some("<http://ex/s3>"),
10418 Some("<http://ex/p0>"),
10419 Some("<http://ex/o3>"),
10420 ),
10421 (Some("<http://ex/nope>"), None, None),
10425 (None, Some("<http://ex/nope>"), None),
10426 (None, None, Some("<http://ex/nope>")),
10427 ];
10428
10429 let mut any_pruned = false;
10430 for perms in [PermSet::ALL, PermSet::CORE] {
10431 let image = build(perms);
10432 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
10433 let opens: Vec<(&str, Rete)> = vec![
10434 ("resident", Rete::open(&image).unwrap()),
10435 (
10436 "ranged",
10437 Rete::open_ranged(&SliceReader::new(leaked)).unwrap(),
10438 ),
10439 (
10440 "lazy",
10441 Rete::open_ranged_lazy_with(SliceReader::new(leaked), HEADER_LEN as u64)
10442 .unwrap(),
10443 ),
10444 ];
10445 for graph in [None, Some("<http://ex/g1>"), Some("<http://ex/absent>")] {
10446 let full: Vec<TermTriple> = {
10448 let mut v = Vec::new();
10449 Rete::open(&image).unwrap().dump_each(graph, |s, p, o| {
10450 v.push((s.to_string(), p.to_string(), o.to_string()))
10451 });
10452 v.sort();
10453 v
10454 };
10455 for &(s, p, o) in &shapes {
10456 let mut want = full.clone();
10457 want.retain(|(ts, tp, to)| {
10458 s.is_none_or(|x| x == ts)
10459 && p.is_none_or(|x| x == tp)
10460 && o.is_none_or(|x| x == to)
10461 });
10462 for (path, rete) in &opens {
10463 let mut got = Vec::new();
10464 rete.dump_filtered_each(graph, s, p, o, |s, p, o| {
10465 got.push((s.to_string(), p.to_string(), o.to_string()))
10466 });
10467 got.sort();
10468 assert_eq!(
10469 got,
10470 want,
10471 "{path}/{perms:?} graph={graph:?} pattern={:?} — filtered dump \
10472 is not the unfiltered dump filtered",
10473 (s, p, o)
10474 );
10475 assert!(!rete.index_incomplete(), "{path}: a fetch failed");
10476 }
10477 let plan = opens[2].1.dump_plan(graph, s, p, o);
10481 if graph == Some("<http://ex/absent>")
10482 || [s, p, o].contains(&Some("<http://ex/nope>"))
10483 {
10484 assert!(
10485 plan.scan.is_none(),
10486 "an unmatchable dump still planned a scan: {:?}",
10487 (graph, s, p, o)
10488 );
10489 } else {
10490 let scan = plan.scan.expect("a matchable dump plans a scan");
10491 assert!(
10492 PermSet::CORE.contains(scan.permutation),
10493 "a dump routed to {:?}, outside PermSet::CORE — a \
10494 --permutations 3 file has no such section",
10495 scan.permutation
10496 );
10497 assert!(scan.tiles_admitted <= scan.tiles_routed);
10498 assert!(scan.tiles_routed <= scan.tiles_total);
10499 assert!(scan.tile_bytes <= scan.section_bytes);
10500 if scan.tiles_admitted < scan.tiles_total {
10501 any_pruned = true;
10502 }
10503 }
10504 }
10505 }
10506 }
10507 assert!(
10508 any_pruned,
10509 "no shape pruned a single tile — the fixture is too small to be \
10510 measuring anything"
10511 );
10512 }
10513
10514 #[test]
10524 fn a_predicate_scoped_dump_fetches_less_than_the_graph() {
10525 use crate::reader::{CountingReader, SliceReader};
10526
10527 let mut db = DictionaryBuilder::new();
10533 let mut all = Vec::new();
10534 for s in 0..200u32 {
10535 for p in 0..20u32 {
10536 let t = (
10537 format!("<http://ex/s/{s:04}>"),
10538 format!("<http://ex/p/{p:02}>"),
10539 format!("\"object {s:04}/{p:02}\""),
10540 );
10541 db.observe(&t.0, &t.1, &t.2);
10542 all.push(t);
10543 }
10544 }
10545 let dict = db.build();
10546 let mut ib = GraphIndexBuilder::new().with_tile_budget(64);
10547 for (s, p, o) in &all {
10548 ib.push(dict.encode(s, p, o).unwrap());
10549 }
10550 let image = write_dataset(&dict, &ib.build(), &[], false, &[], 0);
10551 let leaked: &'static [u8] = Box::leak(image.clone().into_boxed_slice());
10552
10553 let probe = |s: Option<&str>, p: Option<&str>, o: Option<&str>| -> (usize, u64) {
10555 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
10556 let lazy = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
10557 let before = reader.bytes_read();
10558 let mut n = 0usize;
10559 lazy.dump_filtered_each(None, s, p, o, |_, _, _| n += 1);
10560 assert!(!lazy.index_incomplete());
10561 (n, reader.bytes_read() - before)
10562 };
10563
10564 let (full_rows, full_bytes) = probe(None, None, None);
10565 let (pred_rows, pred_bytes) = probe(None, Some("<http://ex/p/07>"), None);
10566 let (subj_rows, subj_bytes) = probe(Some("<http://ex/s/0021>"), None, None);
10567 assert_eq!(full_rows, 4000);
10568 assert_eq!(pred_rows, 200, "one predicate covers every subject once");
10569 assert_eq!(subj_rows, 20);
10570 assert!(
10572 pred_bytes < full_bytes && subj_bytes < full_bytes,
10573 "a slice fetched {pred_bytes} / {subj_bytes} B where the whole graph fetched \
10574 {full_bytes} B — the scan is not being pruned at all"
10575 );
10576 #[cfg(feature = "compression")]
10587 assert!(
10588 pred_bytes * 4 < full_bytes && subj_bytes * 4 < full_bytes,
10589 "a 1-in-20 predicate slice fetched {pred_bytes} B and one subject \
10590 {subj_bytes} B where the whole graph fetched {full_bytes} B"
10591 );
10592
10593 let reader = std::sync::Arc::new(CountingReader::new(SliceReader::new(leaked)));
10595 let lazy = Rete::open_ranged_lazy_with(reader.clone(), HEADER_LEN as u64).unwrap();
10596 let before = reader.bytes_read();
10597 let plan = lazy
10598 .dump_plan(None, None, Some("<http://ex/p/07>"), None)
10599 .scan
10600 .expect("a known predicate plans a scan");
10601 assert!(
10602 reader.bytes_read() - before < 64 * 1024,
10603 "planning fetched {} B — it is supposed to read directories, not tiles",
10604 reader.bytes_read() - before
10605 );
10606 assert!(plan.tiles_admitted < plan.tiles_total);
10607 assert!(
10613 plan.tile_bytes * 5 < plan.section_bytes,
10614 "one predicate of twenty planned {} B of a {} B section",
10615 plan.tile_bytes,
10616 plan.section_bytes
10617 );
10618 }
10619
10620 #[test]
10621 fn query_quads_tags_every_graph() {
10622 let mut db = DictionaryBuilder::new();
10623 for (s, p, o) in [("Alice", "knows", "Bob"), ("Alice", "knows", "Dave")] {
10624 db.observe(s, p, o);
10625 }
10626 let dict = db.build();
10627 let mut def = GraphIndexBuilder::new();
10628 def.push(dict.encode("Alice", "knows", "Bob").unwrap());
10629 let mut g1 = GraphIndexBuilder::new();
10630 g1.push(dict.encode("Alice", "knows", "Dave").unwrap());
10631 let named = vec![("http://ex/g1".to_string(), g1.build())];
10632 let bytes = write_dataset(&dict, &def.build(), &named, true, &[], 0);
10633 let rete = Rete::open(&bytes).unwrap();
10634
10635 let quads = rete.query_quads(Some("Alice"), Some("knows"), None);
10637 assert_eq!(quads.len(), 2);
10638 assert_eq!(
10639 quads[0],
10640 (("Alice".into(), "knows".into(), "Bob".into()), None)
10641 );
10642 assert_eq!(
10643 quads[1],
10644 (
10645 ("Alice".into(), "knows".into(), "Dave".into()),
10646 Some("http://ex/g1".to_string())
10647 )
10648 );
10649
10650 assert!(rete.query_quads(Some("Nobody"), None, None).is_empty());
10652 }
10653
10654 #[test]
10655 fn pyramid_meta_round_trips_in_file() {
10656 let rete = Rete::open(&build_image()).unwrap();
10657 let pyr = rete.pyramid().expect("file has a pyramid");
10658 let total: u32 = pyr.summary.iter().map(|e| e.count).sum();
10660 assert_eq!(total, 3);
10661 assert!(!pyr.summary.is_empty());
10662 assert!(pyr.tiles.is_empty());
10663 }
10664
10665 #[test]
10666 fn schema_pyramid_round_trips_through_file_index_free() {
10667 use crate::reader::{CountingReader, SliceReader};
10668 let sub = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
10669 let q = |s: &str, p: &str, o: &str| {
10670 (s.to_string(), p.to_string(), o.to_string(), None::<String>)
10671 };
10672 let quads = vec![
10674 q("<a>", RDF_TYPE, "<Astronomer>"),
10675 q("<b>", RDF_TYPE, "<Astronomer>"),
10676 q("<c>", RDF_TYPE, "<Person>"),
10677 q("<Astronomer>", sub, "<Scientist>"),
10678 q("<Scientist>", sub, "<Person>"),
10679 q("<Person>", sub, "<Agent>"),
10680 q("<a>", "<knows>", "<b>"),
10681 q("<b>", "<knows>", "<c>"),
10682 ];
10683 let (bytes, _) =
10684 crate::ingest::assemble_dataset_with_opts(quads, true, false, None, |_, _| Vec::new());
10685
10686 let rete = Rete::open(&bytes).unwrap();
10688 let pyr = rete.pyramid().expect("pyramid present");
10689 assert!(!pyr.level_rollups.is_empty(), "schema pyramid shipped");
10690 assert!(pyr
10691 .class_hierarchy
10692 .iter()
10693 .any(|n| n.class == "<Agent>" && n.depth == 0));
10694
10695 let r = CountingReader::new(SliceReader::new(&bytes));
10697 let view = SummaryView::open_ranged(&r).unwrap().unwrap();
10698 assert!(view.level_count() >= 2, "multi-level pyramid");
10699 let coarse = view.level_rollup(0).unwrap();
10700 assert!(
10701 coarse.classes.iter().any(|(c, _)| c == "<Agent>"),
10702 "coarsest level rolls up to the root Agent"
10703 );
10704 let h = Header::from_bytes(&bytes).unwrap();
10705 assert!(
10706 r.bytes_read() <= bytes.len() as u64 - h.root_dir_len,
10707 "summary read {} bytes; the {}-byte index section must be skipped",
10708 r.bytes_read(),
10709 h.root_dir_len
10710 );
10711 }
10712
10713 #[test]
10714 fn predicate_totals_from_summary_only() {
10715 use crate::reader::SliceReader;
10716 let bytes = build_image();
10718 let reader = SliceReader::new(&bytes);
10719 let view = SummaryView::open_ranged(&reader).unwrap().unwrap();
10720 assert_eq!(view.predicate_total("knows"), 2);
10721 assert_eq!(view.predicate_total("age"), 1);
10722 assert_eq!(view.predicate_total("missing"), 0);
10723 let totals = view.predicate_totals();
10724 assert_eq!(totals[0], ("knows".to_string(), 2)); }
10726
10727 #[test]
10728 fn query_patterns_resolve_to_terms() {
10729 let rete = Rete::open(&build_image()).unwrap();
10730
10731 assert_eq!(rete.query(None, None, None).len(), 3);
10733
10734 let mut alice = rete.query(Some("Alice"), None, None);
10736 alice.sort();
10737 assert_eq!(
10738 alice,
10739 vec![
10740 ("Alice".into(), "age".into(), "30".into()),
10741 ("Alice".into(), "knows".into(), "Bob".into()),
10742 ]
10743 );
10744
10745 assert_eq!(rete.query(None, Some("knows"), None).len(), 2);
10747
10748 assert_eq!(
10750 rete.query(Some("Bob"), Some("knows"), Some("Carol")),
10751 vec![("Bob".into(), "knows".into(), "Carol".into())]
10752 );
10753 assert!(rete.query(Some("Nobody"), None, None).is_empty());
10754 assert!(rete.query(None, Some("likes"), None).is_empty());
10755 }
10756
10757 #[test]
10758 fn query_provenance_reports_terms_ids_sections_and_index_choice() {
10759 let bytes = build_image();
10760 let rete = Rete::open(&bytes).unwrap();
10761
10762 let mut matches = rete.query_with_provenance(None, Some("knows"), None);
10763 matches.sort_by(|a, b| a.terms.cmp(&b.terms));
10764
10765 assert_eq!(matches.len(), 2);
10766 assert_eq!(
10767 matches[0].terms,
10768 ("Alice".into(), "knows".into(), "Bob".into())
10769 );
10770 assert_eq!(
10771 matches[0].ids,
10772 rete.dictionary().encode("Alice", "knows", "Bob").unwrap()
10773 );
10774 assert_eq!(matches[0].graph.as_deref(), None);
10775 assert_eq!(
10776 matches[0].matched_pattern,
10777 (None, Some(matches[0].ids.1), None)
10778 );
10779 assert_eq!(
10780 matches[0].index_permutation,
10781 crate::index::IndexPermutation::Pos
10782 );
10783
10784 let h = rete.header();
10785 assert_eq!(matches[0].dictionary_range.offset, h.dictionary_offset);
10786 assert_eq!(matches[0].dictionary_range.len, h.dictionary_len);
10787 assert_eq!(matches[0].index_range.offset, h.root_dir_offset);
10788 assert_eq!(matches[0].index_range.len, h.root_dir_len);
10789 assert!(
10790 matches[0].index_section_range.offset > h.root_dir_offset,
10791 "POS is section 1, so its payload starts after the container header and SPO payload"
10792 );
10793 assert!(matches[0].index_section_range.len > 0);
10794 assert!(matches[0].index_section_range.end() <= matches[0].index_range.end());
10795 assert!(matches[0].index_section_range.len < matches[0].index_range.len);
10796 assert_eq!(
10797 matches[0].pyramid_range.as_ref().map(|r| (r.offset, r.len)),
10798 Some((h.pyramid_meta_offset, h.pyramid_meta_len))
10799 );
10800 let tile_range = matches[0].tile_range.expect("tiled file reports a tile");
10803 assert!(matches[0]
10804 .tile
10805 .as_deref()
10806 .unwrap()
10807 .starts_with(matches[0].index_permutation.name()));
10808 assert!(matches[0].index_section_range.offset <= tile_range.offset);
10809 assert!(tile_range.end() <= matches[0].index_section_range.end());
10810 }
10811
10812 fn build_labeled(n: usize) -> Vec<u8> {
10816 const LABEL: &str = "<http://www.w3.org/2000/01/rdf-schema#label>";
10817 const WORDS: &[&str] = &[
10818 "alanine",
10819 "benzene",
10820 "glucose",
10821 "dextrose",
10822 "ethanol",
10823 "formate",
10824 "heptane",
10825 "isoleucine",
10826 ];
10827 let triples: Vec<(String, String, String)> = (0..n)
10828 .flat_map(|i| {
10829 let s = format!("<http://ex/e{i}>");
10830 let w = WORDS[i % WORDS.len()];
10831 [
10832 (s.clone(), LABEL.to_string(), format!("\"{w}-{i:06}\"")),
10833 (
10834 s,
10835 "<http://ex/p>".to_string(),
10836 format!("<http://ex/c{}>", i % 64),
10837 ),
10838 ]
10839 })
10840 .collect();
10841 let mut db = DictionaryBuilder::new();
10842 for (s, p, o) in &triples {
10843 db.observe(s, p, o);
10844 }
10845 let dict = db.build();
10846 let ids: Vec<(u32, u32, u32)> = triples
10847 .iter()
10848 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
10849 .collect();
10850 let mut ib = GraphIndexBuilder::new();
10851 for &t in &ids {
10852 ib.push(t);
10853 }
10854 let (meta, levels) = build_pyramid_meta(&dict, &ids, DEFAULT_TILE_BUDGET);
10855 write_file(&dict, &ib.build(), false, &meta, levels)
10856 }
10857
10858 #[test]
10859 fn prefix_search_matches_a_filter_scan() {
10860 let bytes = build_labeled(800);
10863 let rete = Rete::open(&bytes).unwrap();
10864 let idx_subjects: std::collections::BTreeSet<String> = rete
10865 .prefix_search("glucose", 10_000)
10866 .into_iter()
10867 .map(|(_label, subject)| subject)
10868 .collect();
10869 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
10871 FILTER(STRSTARTS(LCASE(?l), \"glucose\")) }";
10872 let crate::QueryOutput::Select(_, rows) = crate::eval_query(&rete, q).unwrap() else {
10873 panic!("expected SELECT");
10874 };
10875 let scan_subjects: std::collections::BTreeSet<String> =
10876 rows.iter().map(|r| r.get("s").cloned().unwrap()).collect();
10877 assert_eq!(idx_subjects, scan_subjects, "index agrees with the scan");
10878 assert_eq!(
10879 idx_subjects.len(),
10880 100,
10881 "800/8 words = 100 glucose-* labels"
10882 );
10883 }
10884
10885 #[test]
10889 fn legacy_label_hits_resolve_in_one_batched_read() {
10890 use std::sync::{Arc, Mutex};
10891 type CallLog = Arc<Mutex<Vec<Vec<(u64, u64)>>>>;
10892 #[derive(Clone)]
10893 struct Calls(Arc<Vec<u8>>, CallLog);
10894 impl crate::RangeReader for Calls {
10895 fn len(&self) -> u64 {
10896 self.0.len() as u64
10897 }
10898 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
10899 self.1.lock().unwrap().push(vec![(offset, len)]);
10900 crate::SliceReader::new(&self.0).read_at(offset, len)
10901 }
10902 fn read_many(&self, ranges: &[(u64, u64)]) -> std::io::Result<Vec<Vec<u8>>> {
10903 self.1.lock().unwrap().push(ranges.to_vec());
10904 let r = crate::SliceReader::new(&self.0);
10905 ranges.iter().map(|&(o, l)| r.read_at(o, l)).collect()
10906 }
10907 }
10908 let bytes = build_labeled(20_000);
10909 let h = Header::from_bytes(&bytes).unwrap();
10910 let (d0, dl) = (h.dictionary_offset, h.dictionary_len);
10911 let want = Rete::open(&bytes).unwrap().prefix_search("glucose", 1000);
10912 assert_eq!(want.len(), 1000);
10913 for search_view in [false, true] {
10914 let calls = Calls(Arc::new(bytes.clone()), Arc::new(Mutex::new(Vec::new())));
10915 let got = if search_view {
10916 let view = SearchView::open_ranged_with(calls.clone(), HEADER_LEN as u64).unwrap();
10917 calls.1.lock().unwrap().clear();
10918 view.prefix_search("glucose", 1000)
10919 } else {
10920 let rete = Rete::open_ranged_lazy_with(calls.clone(), HEADER_LEN as u64).unwrap();
10921 calls.1.lock().unwrap().clear();
10922 rete.prefix_search("glucose", 1000)
10923 };
10924 assert_eq!(got, want, "search view = {search_view}");
10925 let dict_reads = calls
10926 .1
10927 .lock()
10928 .unwrap()
10929 .iter()
10930 .filter(|c| c.iter().any(|&(o, l)| o < d0 + dl && d0 < o + l))
10931 .count();
10932 assert_eq!(dict_reads, 1, "search view = {search_view}");
10933 }
10934 }
10935
10936 #[test]
10940 #[ignore]
10941 fn bench_prefix_search_vs_filter_scan() {
10942 use std::time::Instant;
10943 let n = 6000; let bytes = build_labeled(n);
10945 let rete = Rete::open(&bytes).unwrap();
10946 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
10947 FILTER(STRSTARTS(LCASE(?l), \"glucose\")) }";
10948 let reps = 200;
10949 let idx_n = rete.prefix_search("glucose", 100_000).len();
10950 let t = Instant::now();
10951 for _ in 0..reps {
10952 std::hint::black_box(rete.prefix_search("glucose", 100_000));
10953 }
10954 let idx_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
10955 let t = Instant::now();
10956 for _ in 0..reps {
10957 let _ = std::hint::black_box(crate::eval_query(&rete, q).unwrap());
10958 }
10959 let scan_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
10960 println!(
10961 "label prefix search over {n} labeled subjects ({idx_n} matches): \
10962 index {idx_ms:.4} ms vs FILTER scan {scan_ms:.3} ms ({:.0}× faster)",
10963 scan_ms / idx_ms
10964 );
10965 }
10966
10967 #[test]
10971 #[ignore]
10972 fn bench_text_search_vs_contains_scan() {
10973 use std::time::Instant;
10974 const LABEL: &str = "<http://www.w3.org/2000/01/rdf-schema#label>";
10975 const WORDS: &[&str] = &[
10976 "alanine",
10977 "benzene",
10978 "glucose",
10979 "dextrose",
10980 "ethanol",
10981 "formate",
10982 "heptane",
10983 "isoleucine",
10984 ];
10985 let n = 6000;
10986 let triples: Vec<(String, String, String)> = (0..n)
10987 .map(|i| {
10988 (
10989 format!("<http://ex/e{i}>"),
10990 LABEL.to_string(),
10991 format!("\"{} sample number {i:06}\"", WORDS[i % WORDS.len()]),
10992 )
10993 })
10994 .collect();
10995 let mut db = DictionaryBuilder::new();
10996 for (s, p, o) in &triples {
10997 db.observe(s, p, o);
10998 }
10999 let dict = db.build();
11000 let ids: Vec<(u32, u32, u32)> = triples
11001 .iter()
11002 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
11003 .collect();
11004 let mut ib = GraphIndexBuilder::new();
11005 for &t in &ids {
11006 ib.push(t);
11007 }
11008 let ti = compute_text_index(&dict, &ids);
11009 let bytes = write_dataset_with_metadata(&dict, &ib.build(), &[], false, &[], 0, &[], &ti);
11010 let rete = Rete::open(&bytes).unwrap();
11011
11012 let q = "SELECT ?s WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?l \
11013 FILTER(CONTAINS(LCASE(?l), \"glucose\")) }";
11014 let reps = 200;
11015 let idx_n = rete.text_search(&["glucose"], None, 100_000).len();
11016 let t = Instant::now();
11017 for _ in 0..reps {
11018 std::hint::black_box(rete.text_search(&["glucose"], None, 100_000));
11019 }
11020 let idx_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
11021 let t = Instant::now();
11022 for _ in 0..reps {
11023 let _ = std::hint::black_box(crate::eval_query(&rete, q).unwrap());
11024 }
11025 let scan_ms = t.elapsed().as_secs_f64() * 1000.0 / reps as f64;
11026 println!(
11027 "text search over {n} literals ({idx_n} matches): \
11028 index {idx_ms:.4} ms vs FILTER(CONTAINS) scan {scan_ms:.3} ms ({:.0}× faster)",
11029 scan_ms / idx_ms
11030 );
11031 }
11032
11033 #[test]
11037 #[ignore = "operational tool, driven by RETE_DEBUG_* env vars"]
11038 fn debug_bound_po_routing() {
11039 struct FR(std::fs::File);
11040 impl crate::RangeReader for FR {
11041 fn len(&self) -> u64 {
11042 self.0.metadata().map(|m| m.len()).unwrap_or(0)
11043 }
11044 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
11045 use std::os::unix::fs::FileExt;
11046 let mut buf = vec![0u8; len as usize];
11047 self.0.read_exact_at(&mut buf, offset)?;
11048 Ok(buf)
11049 }
11050 }
11051 let path = std::env::var("RETE_DEBUG_FILE").expect("RETE_DEBUG_FILE");
11052 let p_iri = std::env::var("RETE_DEBUG_P").expect("RETE_DEBUG_P");
11053 let o_iri = std::env::var("RETE_DEBUG_O").expect("RETE_DEBUG_O");
11054 let rete = Rete::open_ranged_lazy_with(
11055 std::sync::Arc::new(FR(std::fs::File::open(&path).unwrap())),
11056 HEADER_LEN as u64,
11057 )
11058 .unwrap();
11059 let pid = rete.dict.predicate_id(&p_iri).expect("p resolves");
11060 let oid = rete.dict.object_id(&o_iri).expect("o resolves");
11061 eprintln!("pid={pid} oid={oid}");
11062 let pattern = (None, Some(pid), Some(oid));
11063 let perm = GraphIndex::best_permutation(pattern);
11064 eprintln!("best_permutation = {}", perm.name());
11065 let si = perm.section_index();
11066 let tiles = &rete.index.sections[si];
11067 eprintln!("section {} tiles = {}", perm.name(), tiles.len());
11068 let [pa, pb, pc] = perm.order_pattern(pattern);
11069 eprintln!("permuted pattern pa={pa:?} pb={pb:?} pc={pc:?}");
11070 let (start, end) = rete.index.tile_span(si, pa);
11071 eprintln!("tile_span = [{start}, {end}) -> {} tiles", end - start);
11072 let mut admitted = 0usize;
11073 for (ti, t) in tiles.iter().enumerate().take(end).skip(start) {
11074 if t.syn_admits(pb, pc) {
11075 admitted += 1;
11076 if admitted <= 10 {
11077 let (lo, hi) = t.leading_range();
11078 eprintln!(" admit tile {ti}: a=[{lo},{hi}] syn={:?}", t.syn);
11079 }
11080 }
11081 }
11082 eprintln!("admitted {admitted} tile(s) by synopsis");
11083 let n = rete.index.scan_iter(pattern).count();
11084 eprintln!("scan_iter matches = {n}");
11085 let hi_res = rete.query(None, Some(&p_iri), Some(&o_iri));
11086 eprintln!("high-level query matches = {}", hi_res.len());
11087 }
11088
11089 #[test]
11097 fn layout_header_label_matches_its_span() {
11098 let mut db = DictionaryBuilder::new();
11099 let mut triples = Vec::new();
11100 for i in 0..24u32 {
11101 let (s, p, o) = (
11102 format!("<http://ex/s{}>", i % 5),
11103 format!("<http://ex/p{}>", i % 3),
11104 format!("<http://ex/o{i}>"),
11105 );
11106 db.observe(&s, &p, &o);
11107 triples.push((s, p, o));
11108 }
11109 let dict = db.build();
11110 let ids: Vec<(u32, u32, u32)> = triples
11111 .iter()
11112 .map(|(s, p, o)| dict.encode(s, p, o).unwrap())
11113 .collect();
11114 let index = GraphIndexBuilder::from_triples(ids).build();
11115 let image = write_dataset_with_metadata(
11116 &dict,
11117 &index,
11118 &[],
11119 false,
11120 &[],
11121 0,
11122 br#"{"name":"layout-label-fixture"}"#,
11123 &[],
11124 );
11125 let rete = Rete::open(&image).unwrap();
11126
11127 let layout = rete.file_layout();
11128 let header = layout
11129 .iter()
11130 .find(|s| s.kind == "header")
11131 .expect("every layout starts with a header segment");
11132
11133 assert_eq!(
11134 header.offset, 0,
11135 "the header is the first thing in the file"
11136 );
11137 assert_eq!(
11138 header.len, HEADER_LEN as u64,
11139 "the header segment must span exactly HEADER_LEN"
11140 );
11141
11142 let digits: String = header
11145 .label
11146 .chars()
11147 .skip_while(|c| !c.is_ascii_digit())
11148 .take_while(|c| c.is_ascii_digit())
11149 .collect();
11150 assert!(
11151 !digits.is_empty(),
11152 "header label {:?} states no size at all",
11153 header.label
11154 );
11155 assert_eq!(
11156 digits.parse::<u64>().unwrap(),
11157 header.len,
11158 "header label {:?} disagrees with the {}-byte span it describes",
11159 header.label,
11160 header.len
11161 );
11162
11163 assert!(
11166 layout.windows(2).all(|w| w[0].offset <= w[1].offset),
11167 "file_layout must return segments sorted by offset"
11168 );
11169 }
11170}