1use std::collections::BTreeMap;
30
31use crate::tiling::{SuperEdge, Tile};
32use crate::varint::{read_uvarint, write_uvarint};
33
34const SCHEMA_V2: u8 = 2;
36const QSTATS_V1: u8 = 3;
41const CHARSET_V1: u8 = 4;
44const LABELIDX_V1: u8 = 5;
49
50#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct CharSet {
57 pub predicates: Vec<u32>,
59 pub subjects: u64,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct LabelEntry {
72 pub label: String,
74 pub subject: u32,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct PredStat {
86 pub predicate: u32,
87 pub count: u64,
88 pub distinct_subjects: u64,
89 pub distinct_objects: u64,
90 pub max_objects_per_subject: u32,
91 pub max_subjects_per_object: u32,
92}
93
94#[derive(Debug, thiserror::Error)]
95pub enum MetaError {
96 #[error("malformed pyramid meta: {0}")]
97 Malformed(&'static str),
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct ClassNode {
107 pub class: String,
108 pub parents: Vec<String>,
109 pub depth: u16,
110}
111
112impl ClassNode {
113 pub fn canonical_parent(&self) -> Option<&String> {
115 self.parents.first()
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct LevelRollup {
125 pub round: u32,
126 pub depth: u16,
127 pub classes: Vec<(String, u64)>,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct ClassRelation {
135 pub s_class: String,
136 pub predicate: String,
137 pub o_class: String,
138 pub count: u64,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct LevelLinks {
147 pub round: u32,
148 pub depth: u16,
149 pub links: Vec<ClassRelation>,
150}
151
152#[derive(Debug, Clone, PartialEq)]
159pub struct CommunityDescriptor {
160 pub community: u32,
161 pub dominant_class: Option<String>,
162 pub class_counts: Vec<(String, u64)>,
163 pub bbox: Option<[f64; 4]>,
165 pub time_range: Option<(String, String)>,
167}
168
169#[derive(Debug, Clone, PartialEq)]
171pub struct PyramidMeta {
172 pub round: u32,
173 pub summary: Vec<SuperEdge>,
174 pub tiles: Vec<(u32, Vec<u8>)>,
176 pub class_hierarchy: Vec<ClassNode>,
178 pub level_rollups: Vec<LevelRollup>,
179 pub level_links: Vec<LevelLinks>,
181 pub descriptors: Vec<CommunityDescriptor>,
182 pub subclass_cycles: Vec<Vec<String>>,
185 pub disjoint_pairs: Vec<(String, String)>,
187 pub equivalent_pairs: Vec<(String, String)>,
189 pub predicate_stats: Vec<PredStat>,
192 pub char_sets: Vec<CharSet>,
195 pub label_index: Vec<LabelEntry>,
198}
199
200impl PyramidMeta {
201 pub fn new(round: u32, summary: Vec<SuperEdge>, tiles: &[Tile]) -> Self {
205 let tiles = tiles
206 .iter()
207 .map(|t| (t.community as u32, t.encoded.clone()))
208 .collect();
209 PyramidMeta {
210 round,
211 summary,
212 tiles,
213 class_hierarchy: Vec::new(),
214 level_rollups: Vec::new(),
215 level_links: Vec::new(),
216 descriptors: Vec::new(),
217 subclass_cycles: Vec::new(),
218 disjoint_pairs: Vec::new(),
219 equivalent_pairs: Vec::new(),
220 predicate_stats: Vec::new(),
221 char_sets: Vec::new(),
222 label_index: Vec::new(),
223 }
224 }
225
226 #[allow(clippy::too_many_arguments)]
228 pub fn with_schema(
229 mut self,
230 class_hierarchy: Vec<ClassNode>,
231 level_rollups: Vec<LevelRollup>,
232 level_links: Vec<LevelLinks>,
233 descriptors: Vec<CommunityDescriptor>,
234 subclass_cycles: Vec<Vec<String>>,
235 disjoint_pairs: Vec<(String, String)>,
236 equivalent_pairs: Vec<(String, String)>,
237 ) -> Self {
238 self.class_hierarchy = class_hierarchy;
239 self.level_rollups = level_rollups;
240 self.level_links = level_links;
241 self.descriptors = descriptors;
242 self.subclass_cycles = subclass_cycles;
243 self.disjoint_pairs = disjoint_pairs;
244 self.equivalent_pairs = equivalent_pairs;
245 self
246 }
247
248 pub fn with_predicate_stats(mut self, predicate_stats: Vec<PredStat>) -> Self {
251 self.predicate_stats = predicate_stats;
252 self
253 }
254
255 pub fn with_char_sets(mut self, char_sets: Vec<CharSet>) -> Self {
257 self.char_sets = char_sets;
258 self
259 }
260
261 pub fn with_label_index(mut self, label_index: Vec<LabelEntry>) -> Self {
265 self.label_index = label_index;
266 self
267 }
268
269 pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<&LabelEntry> {
275 let needle = prefix.to_lowercase();
276 let start = self
278 .label_index
279 .partition_point(|e| e.label.to_lowercase().as_str() < needle.as_str());
280 let mut out = Vec::new();
281 for e in &self.label_index[start..] {
282 if out.len() >= limit {
283 break;
284 }
285 let lower = e.label.to_lowercase();
286 if needle.is_empty() || lower.starts_with(&needle) {
287 out.push(e);
288 } else if lower.as_str() > needle.as_str() {
289 break;
291 }
292 }
293 out
294 }
295
296 fn schema_is_empty(&self) -> bool {
298 self.class_hierarchy.is_empty()
299 && self.level_rollups.is_empty()
300 && self.level_links.is_empty()
301 && self.descriptors.is_empty()
302 && self.subclass_cycles.is_empty()
303 && self.disjoint_pairs.is_empty()
304 && self.equivalent_pairs.is_empty()
305 }
306
307 pub fn encode(&self) -> Vec<u8> {
308 let mut out = Vec::new();
309 write_uvarint(&mut out, self.round as u64);
310 write_uvarint(&mut out, self.summary.len() as u64);
311 for e in &self.summary {
312 write_uvarint(&mut out, e.s_comm as u64);
313 write_uvarint(&mut out, e.predicate as u64);
314 write_uvarint(&mut out, e.o_comm as u64);
315 write_uvarint(&mut out, e.count as u64);
316 }
317 write_uvarint(&mut out, self.tiles.len() as u64);
318 for (community, block) in &self.tiles {
319 write_uvarint(&mut out, *community as u64);
320 write_uvarint(&mut out, block.len() as u64);
321 out.extend_from_slice(block);
322 }
323 if !self.predicate_stats.is_empty() {
328 let mut payload = Vec::new();
329 write_uvarint(&mut payload, self.predicate_stats.len() as u64);
330 for p in &self.predicate_stats {
331 write_uvarint(&mut payload, p.predicate as u64);
332 write_uvarint(&mut payload, p.count);
333 write_uvarint(&mut payload, p.distinct_subjects);
334 write_uvarint(&mut payload, p.distinct_objects);
335 write_uvarint(&mut payload, p.max_objects_per_subject as u64);
336 write_uvarint(&mut payload, p.max_subjects_per_object as u64);
337 }
338 out.push(QSTATS_V1);
339 write_uvarint(&mut out, payload.len() as u64);
340 out.extend_from_slice(&payload);
341 }
342 if !self.char_sets.is_empty() {
345 let mut payload = Vec::new();
346 write_uvarint(&mut payload, self.char_sets.len() as u64);
347 for c in &self.char_sets {
348 write_uvarint(&mut payload, c.predicates.len() as u64);
349 for &p in &c.predicates {
350 write_uvarint(&mut payload, p as u64);
351 }
352 write_uvarint(&mut payload, c.subjects);
353 }
354 out.push(CHARSET_V1);
355 write_uvarint(&mut out, payload.len() as u64);
356 out.extend_from_slice(&payload);
357 }
358 if !self.label_index.is_empty() {
361 let mut payload = Vec::new();
362 write_uvarint(&mut payload, self.label_index.len() as u64);
363 for e in &self.label_index {
364 write_str(&mut payload, &e.label);
365 write_uvarint(&mut payload, e.subject as u64);
366 }
367 out.push(LABELIDX_V1);
368 write_uvarint(&mut out, payload.len() as u64);
369 out.extend_from_slice(&payload);
370 }
371 if !self.schema_is_empty() {
374 self.encode_schema(&mut out);
375 }
376 out
377 }
378
379 fn encode_schema(&self, out: &mut Vec<u8>) {
382 let mut table: Vec<String> = Vec::new();
385 let mut index: BTreeMap<String, u32> = BTreeMap::new();
386 fn intern(table: &mut Vec<String>, index: &mut BTreeMap<String, u32>, s: &str) {
387 if !index.contains_key(s) {
388 index.insert(s.to_string(), table.len() as u32);
389 table.push(s.to_string());
390 }
391 }
392 for n in &self.class_hierarchy {
393 intern(&mut table, &mut index, &n.class);
394 for p in &n.parents {
395 intern(&mut table, &mut index, p);
396 }
397 }
398 for r in &self.level_rollups {
399 for (c, _) in &r.classes {
400 intern(&mut table, &mut index, c);
401 }
402 }
403 for l in &self.level_links {
404 for r in &l.links {
405 intern(&mut table, &mut index, &r.s_class);
406 intern(&mut table, &mut index, &r.predicate);
407 intern(&mut table, &mut index, &r.o_class);
408 }
409 }
410 for d in &self.descriptors {
411 if let Some(c) = &d.dominant_class {
412 intern(&mut table, &mut index, c);
413 }
414 for (c, _) in &d.class_counts {
415 intern(&mut table, &mut index, c);
416 }
417 }
418 for cyc in &self.subclass_cycles {
421 for c in cyc {
422 intern(&mut table, &mut index, c);
423 }
424 }
425 for (a, b) in self.disjoint_pairs.iter().chain(&self.equivalent_pairs) {
426 intern(&mut table, &mut index, a);
427 intern(&mut table, &mut index, b);
428 }
429 let idx = |s: &str| *index.get(s).expect("interned");
430
431 out.push(SCHEMA_V2);
432 write_uvarint(out, table.len() as u64);
433 for s in &table {
434 write_str(out, s);
435 }
436 write_uvarint(out, self.class_hierarchy.len() as u64);
437 for n in &self.class_hierarchy {
438 write_uvarint(out, idx(&n.class) as u64);
439 write_uvarint(out, n.parents.len() as u64);
440 for p in &n.parents {
441 write_uvarint(out, idx(p) as u64);
442 }
443 write_uvarint(out, n.depth as u64);
444 }
445 write_uvarint(out, self.level_rollups.len() as u64);
446 for r in &self.level_rollups {
447 write_uvarint(out, r.round as u64);
448 write_uvarint(out, r.depth as u64);
449 write_uvarint(out, r.classes.len() as u64);
450 for (c, count) in &r.classes {
451 write_uvarint(out, idx(c) as u64);
452 write_uvarint(out, *count);
453 }
454 }
455 write_uvarint(out, self.level_links.len() as u64);
456 for l in &self.level_links {
457 write_uvarint(out, l.round as u64);
458 write_uvarint(out, l.depth as u64);
459 write_uvarint(out, l.links.len() as u64);
460 for r in &l.links {
461 write_uvarint(out, idx(&r.s_class) as u64);
462 write_uvarint(out, idx(&r.predicate) as u64);
463 write_uvarint(out, idx(&r.o_class) as u64);
464 write_uvarint(out, r.count);
465 }
466 }
467 write_uvarint(out, self.descriptors.len() as u64);
468 for d in &self.descriptors {
469 write_uvarint(out, d.community as u64);
470 match &d.dominant_class {
471 Some(c) => write_uvarint(out, idx(c) as u64 + 1),
472 None => write_uvarint(out, 0),
473 }
474 write_uvarint(out, d.class_counts.len() as u64);
475 for (c, count) in &d.class_counts {
476 write_uvarint(out, idx(c) as u64);
477 write_uvarint(out, *count);
478 }
479 match d.bbox {
480 Some(b) => {
481 out.push(1);
482 for v in b {
483 out.extend_from_slice(&v.to_le_bytes());
484 }
485 }
486 None => out.push(0),
487 }
488 match &d.time_range {
489 Some((from, to)) => {
490 out.push(1);
491 write_str(out, from);
492 write_str(out, to);
493 }
494 None => out.push(0),
495 }
496 }
497 if !self.subclass_cycles.is_empty()
502 || !self.disjoint_pairs.is_empty()
503 || !self.equivalent_pairs.is_empty()
504 {
505 write_uvarint(out, self.subclass_cycles.len() as u64);
506 for cyc in &self.subclass_cycles {
507 write_uvarint(out, cyc.len() as u64);
508 for c in cyc {
509 write_uvarint(out, idx(c) as u64);
510 }
511 }
512 write_uvarint(out, self.disjoint_pairs.len() as u64);
513 for (a, b) in &self.disjoint_pairs {
514 write_uvarint(out, idx(a) as u64);
515 write_uvarint(out, idx(b) as u64);
516 }
517 write_uvarint(out, self.equivalent_pairs.len() as u64);
518 for (a, b) in &self.equivalent_pairs {
519 write_uvarint(out, idx(a) as u64);
520 write_uvarint(out, idx(b) as u64);
521 }
522 }
523 }
524
525 pub fn decode(bytes: &[u8]) -> Result<Self, MetaError> {
526 let mut pos = 0;
527 let g = |pos: &mut usize| -> Result<u64, MetaError> {
528 let (v, n) = read_uvarint(&bytes[*pos..]).ok_or(MetaError::Malformed("truncated"))?;
529 *pos += n;
530 Ok(v)
531 };
532 let round = g(&mut pos)? as u32;
533 let n_edges = g(&mut pos)? as usize;
536 let mut summary = Vec::with_capacity(n_edges.min(bytes.len()));
537 for _ in 0..n_edges {
538 summary.push(SuperEdge {
539 s_comm: g(&mut pos)? as usize,
540 predicate: g(&mut pos)? as u32,
541 o_comm: g(&mut pos)? as usize,
542 count: g(&mut pos)? as u32,
543 });
544 }
545 let n_tiles = g(&mut pos)? as usize;
546 let mut tiles = Vec::with_capacity(n_tiles.min(bytes.len()));
547 for _ in 0..n_tiles {
548 let community = g(&mut pos)? as u32;
549 let len = g(&mut pos)? as usize;
550 let end = pos
552 .checked_add(len)
553 .filter(|&e| e <= bytes.len())
554 .ok_or(MetaError::Malformed("tile block overruns buffer"))?;
555 tiles.push((community, bytes[pos..end].to_vec()));
556 pos = end;
557 }
558
559 let mut meta = PyramidMeta {
560 round,
561 summary,
562 tiles,
563 class_hierarchy: Vec::new(),
564 level_rollups: Vec::new(),
565 level_links: Vec::new(),
566 descriptors: Vec::new(),
567 subclass_cycles: Vec::new(),
568 disjoint_pairs: Vec::new(),
569 equivalent_pairs: Vec::new(),
570 predicate_stats: Vec::new(),
571 char_sets: Vec::new(),
572 label_index: Vec::new(),
573 };
574 if pos < bytes.len() && bytes[pos] == QSTATS_V1 {
578 pos += 1;
579 let len = g(&mut pos)? as usize;
580 let end = pos
581 .checked_add(len)
582 .filter(|&e| e <= bytes.len())
583 .ok_or(MetaError::Malformed("query-stats block overruns buffer"))?;
584 let mut p = pos;
585 let np = g(&mut p)? as usize;
586 let mut stats = Vec::with_capacity(np.min(bytes.len()));
587 for _ in 0..np {
588 stats.push(PredStat {
589 predicate: g(&mut p)? as u32,
590 count: g(&mut p)?,
591 distinct_subjects: g(&mut p)?,
592 distinct_objects: g(&mut p)?,
593 max_objects_per_subject: g(&mut p)? as u32,
594 max_subjects_per_object: g(&mut p)? as u32,
595 });
596 }
597 meta.predicate_stats = stats;
598 pos = end;
599 }
600 if pos < bytes.len() && bytes[pos] == CHARSET_V1 {
602 pos += 1;
603 let len = g(&mut pos)? as usize;
604 let end = pos
605 .checked_add(len)
606 .filter(|&e| e <= bytes.len())
607 .ok_or(MetaError::Malformed("char-sets block overruns buffer"))?;
608 let mut p = pos;
609 let nc = g(&mut p)? as usize;
610 let mut sets = Vec::with_capacity(nc.min(bytes.len()));
611 for _ in 0..nc {
612 let np = g(&mut p)? as usize;
613 let mut predicates = Vec::with_capacity(np.min(bytes.len()));
614 for _ in 0..np {
615 predicates.push(g(&mut p)? as u32);
616 }
617 let subjects = g(&mut p)?;
618 sets.push(CharSet {
619 predicates,
620 subjects,
621 });
622 }
623 meta.char_sets = sets;
624 pos = end;
625 }
626 if pos < bytes.len() && bytes[pos] == LABELIDX_V1 {
628 pos += 1;
629 let len = g(&mut pos)? as usize;
630 let end = pos
631 .checked_add(len)
632 .filter(|&e| e <= bytes.len())
633 .ok_or(MetaError::Malformed("label-index block overruns buffer"))?;
634 let mut p = pos;
635 let nl = g(&mut p)? as usize;
636 let mut entries = Vec::with_capacity(nl.min(bytes.len()));
637 for _ in 0..nl {
638 let label = read_str(bytes, &mut p)?;
639 let subject = g(&mut p)? as u32;
640 entries.push(LabelEntry { label, subject });
641 }
642 meta.label_index = entries;
643 pos = end;
644 }
645 if pos < bytes.len() && bytes[pos] == SCHEMA_V2 {
652 let mut p = pos + 1;
653 let _ = decode_schema(bytes, &mut p, &mut meta);
654 }
655 Ok(meta)
656 }
657}
658
659fn decode_schema(bytes: &[u8], pos: &mut usize, meta: &mut PyramidMeta) -> Result<(), MetaError> {
661 let g = |pos: &mut usize| -> Result<u64, MetaError> {
662 let (v, n) = read_uvarint(&bytes[*pos..]).ok_or(MetaError::Malformed("truncated v2"))?;
663 *pos += n;
664 Ok(v)
665 };
666 let n_table = g(pos)? as usize;
667 let mut table = Vec::with_capacity(n_table.min(bytes.len()));
668 for _ in 0..n_table {
669 table.push(read_str(bytes, pos)?);
670 }
671 let lookup = |idx: u64| -> Result<String, MetaError> {
672 table
673 .get(idx as usize)
674 .cloned()
675 .ok_or(MetaError::Malformed("class index out of range"))
676 };
677
678 let n_hier = g(pos)? as usize;
679 let mut class_hierarchy = Vec::with_capacity(n_hier.min(bytes.len()));
680 for _ in 0..n_hier {
681 let class = lookup(g(pos)?)?;
682 let n_parents = g(pos)? as usize;
683 let mut parents = Vec::with_capacity(n_parents.min(bytes.len()));
684 for _ in 0..n_parents {
685 parents.push(lookup(g(pos)?)?);
686 }
687 let depth = g(pos)? as u16;
688 class_hierarchy.push(ClassNode {
689 class,
690 parents,
691 depth,
692 });
693 }
694
695 let n_roll = g(pos)? as usize;
696 let mut level_rollups = Vec::with_capacity(n_roll.min(bytes.len()));
697 for _ in 0..n_roll {
698 let round = g(pos)? as u32;
699 let depth = g(pos)? as u16;
700 let n = g(pos)? as usize;
701 let mut classes = Vec::with_capacity(n.min(bytes.len()));
702 for _ in 0..n {
703 let c = lookup(g(pos)?)?;
704 let count = g(pos)?;
705 classes.push((c, count));
706 }
707 level_rollups.push(LevelRollup {
708 round,
709 depth,
710 classes,
711 });
712 }
713
714 let n_links = g(pos)? as usize;
715 let mut level_links = Vec::with_capacity(n_links.min(bytes.len()));
716 for _ in 0..n_links {
717 let round = g(pos)? as u32;
718 let depth = g(pos)? as u16;
719 let n = g(pos)? as usize;
720 let mut links = Vec::with_capacity(n.min(bytes.len()));
721 for _ in 0..n {
722 let s_class = lookup(g(pos)?)?;
723 let predicate = lookup(g(pos)?)?;
724 let o_class = lookup(g(pos)?)?;
725 let count = g(pos)?;
726 links.push(ClassRelation {
727 s_class,
728 predicate,
729 o_class,
730 count,
731 });
732 }
733 level_links.push(LevelLinks {
734 round,
735 depth,
736 links,
737 });
738 }
739
740 let n_desc = g(pos)? as usize;
741 let mut descriptors = Vec::with_capacity(n_desc.min(bytes.len()));
742 for _ in 0..n_desc {
743 let community = g(pos)? as u32;
744 let dom_plus1 = g(pos)?;
745 let dominant_class = if dom_plus1 == 0 {
746 None
747 } else {
748 Some(lookup(dom_plus1 - 1)?)
749 };
750 let n = g(pos)? as usize;
751 let mut class_counts = Vec::with_capacity(n.min(bytes.len()));
752 for _ in 0..n {
753 let c = lookup(g(pos)?)?;
754 let count = g(pos)?;
755 class_counts.push((c, count));
756 }
757 let bbox = if read_u8(bytes, pos)? == 1 {
758 let mut b = [0f64; 4];
759 for v in b.iter_mut() {
760 *v = read_f64(bytes, pos)?;
761 }
762 Some(b)
763 } else {
764 None
765 };
766 let time_range = if read_u8(bytes, pos)? == 1 {
767 let from = read_str(bytes, pos)?;
768 let to = read_str(bytes, pos)?;
769 Some((from, to))
770 } else {
771 None
772 };
773 descriptors.push(CommunityDescriptor {
774 community,
775 dominant_class,
776 class_counts,
777 bbox,
778 time_range,
779 });
780 }
781
782 meta.class_hierarchy = class_hierarchy;
786 meta.level_rollups = level_rollups;
787 meta.level_links = level_links;
788 meta.descriptors = descriptors;
789
790 if *pos >= bytes.len() {
793 return Ok(());
794 }
795 let n_cycles = g(pos)? as usize;
796 let mut subclass_cycles = Vec::with_capacity(n_cycles.min(bytes.len()));
797 for _ in 0..n_cycles {
798 let k = g(pos)? as usize;
799 let mut members = Vec::with_capacity(k.min(bytes.len()));
800 for _ in 0..k {
801 members.push(lookup(g(pos)?)?);
802 }
803 subclass_cycles.push(members);
804 }
805 let n_disjoint = g(pos)? as usize;
806 let mut disjoint_pairs = Vec::with_capacity(n_disjoint.min(bytes.len()));
807 for _ in 0..n_disjoint {
808 let a = lookup(g(pos)?)?;
809 let b = lookup(g(pos)?)?;
810 disjoint_pairs.push((a, b));
811 }
812 let n_equiv = g(pos)? as usize;
813 let mut equivalent_pairs = Vec::with_capacity(n_equiv.min(bytes.len()));
814 for _ in 0..n_equiv {
815 let a = lookup(g(pos)?)?;
816 let b = lookup(g(pos)?)?;
817 equivalent_pairs.push((a, b));
818 }
819 meta.subclass_cycles = subclass_cycles;
820 meta.disjoint_pairs = disjoint_pairs;
821 meta.equivalent_pairs = equivalent_pairs;
822 Ok(())
823}
824
825pub fn schema_block_len(bytes: &[u8]) -> u32 {
832 fn walk(bytes: &[u8]) -> Option<usize> {
833 let mut pos = 0usize;
834 macro_rules! uv {
835 () => {{
836 let (v, n) = read_uvarint(bytes.get(pos..)?)?;
837 pos += n;
838 v
839 }};
840 }
841 let _round = uv!();
842 let n_edges = uv!() as usize;
843 for _ in 0..n_edges {
844 uv!();
845 uv!();
846 uv!();
847 uv!();
848 }
849 let n_tiles = uv!() as usize;
850 for _ in 0..n_tiles {
851 let _comm = uv!();
852 let len = uv!() as usize;
853 pos = pos.checked_add(len)?;
854 if pos > bytes.len() {
855 return None;
856 }
857 }
858 for tag in [QSTATS_V1, CHARSET_V1, LABELIDX_V1] {
861 if pos < bytes.len() && bytes[pos] == tag {
862 pos += 1;
863 let len = uv!() as usize;
864 pos = pos.checked_add(len)?;
865 if pos > bytes.len() {
866 return None;
867 }
868 }
869 }
870 Some(pos)
871 }
872 match walk(bytes) {
873 Some(start) if start < bytes.len() && bytes[start] == SCHEMA_V2 => {
874 (bytes.len() - start) as u32
875 }
876 _ => 0,
877 }
878}
879
880#[allow(clippy::type_complexity)]
885pub fn decode_schema_block(
886 block: &[u8],
887) -> Result<
888 (
889 Vec<ClassNode>,
890 Vec<Vec<String>>,
891 Vec<(String, String)>,
892 Vec<(String, String)>,
893 ),
894 MetaError,
895> {
896 if block.first() != Some(&SCHEMA_V2) {
897 return Err(MetaError::Malformed("not a schema block"));
898 }
899 let mut meta = PyramidMeta {
900 round: 0,
901 summary: Vec::new(),
902 tiles: Vec::new(),
903 class_hierarchy: Vec::new(),
904 level_rollups: Vec::new(),
905 level_links: Vec::new(),
906 descriptors: Vec::new(),
907 subclass_cycles: Vec::new(),
908 disjoint_pairs: Vec::new(),
909 equivalent_pairs: Vec::new(),
910 predicate_stats: Vec::new(),
911 char_sets: Vec::new(),
912 label_index: Vec::new(),
913 };
914 let mut pos = 1; decode_schema(block, &mut pos, &mut meta)?;
916 Ok((
917 meta.class_hierarchy,
918 meta.subclass_cycles,
919 meta.disjoint_pairs,
920 meta.equivalent_pairs,
921 ))
922}
923
924#[allow(clippy::type_complexity)]
931pub fn decode_schema_block_summary(
932 block: &[u8],
933) -> Result<(Vec<(String, u64)>, Vec<(String, String, String, u64)>), MetaError> {
934 if block.first() != Some(&SCHEMA_V2) {
935 return Err(MetaError::Malformed("not a schema block"));
936 }
937 let mut meta = PyramidMeta {
938 round: 0,
939 summary: Vec::new(),
940 tiles: Vec::new(),
941 class_hierarchy: Vec::new(),
942 level_rollups: Vec::new(),
943 level_links: Vec::new(),
944 descriptors: Vec::new(),
945 subclass_cycles: Vec::new(),
946 disjoint_pairs: Vec::new(),
947 equivalent_pairs: Vec::new(),
948 predicate_stats: Vec::new(),
949 char_sets: Vec::new(),
950 label_index: Vec::new(),
951 };
952 let mut pos = 1;
953 decode_schema(block, &mut pos, &mut meta)?;
954 let classes = meta
956 .level_rollups
957 .iter()
958 .max_by_key(|r| r.depth)
959 .map(|r| r.classes.clone())
960 .unwrap_or_default();
961 let relations = meta
962 .level_links
963 .iter()
964 .max_by_key(|l| l.depth)
965 .map(|l| {
966 l.links
967 .iter()
968 .map(|c| {
969 (
970 c.s_class.clone(),
971 c.predicate.clone(),
972 c.o_class.clone(),
973 c.count,
974 )
975 })
976 .collect()
977 })
978 .unwrap_or_default();
979 Ok((classes, relations))
980}
981
982fn write_str(out: &mut Vec<u8>, s: &str) {
983 write_uvarint(out, s.len() as u64);
984 out.extend_from_slice(s.as_bytes());
985}
986
987fn read_str(bytes: &[u8], pos: &mut usize) -> Result<String, MetaError> {
988 let (len, n) = read_uvarint(&bytes[*pos..]).ok_or(MetaError::Malformed("truncated str len"))?;
989 *pos += n;
990 let len = len as usize;
991 let end = pos
992 .checked_add(len)
993 .filter(|&e| e <= bytes.len())
994 .ok_or(MetaError::Malformed("string overruns buffer"))?;
995 let s = std::str::from_utf8(&bytes[*pos..end])
996 .map_err(|_| MetaError::Malformed("invalid utf8"))?
997 .to_string();
998 *pos = end;
999 Ok(s)
1000}
1001
1002fn read_u8(bytes: &[u8], pos: &mut usize) -> Result<u8, MetaError> {
1003 let b = *bytes
1004 .get(*pos)
1005 .ok_or(MetaError::Malformed("truncated u8"))?;
1006 *pos += 1;
1007 Ok(b)
1008}
1009
1010fn read_f64(bytes: &[u8], pos: &mut usize) -> Result<f64, MetaError> {
1011 let end = pos
1012 .checked_add(8)
1013 .filter(|&e| e <= bytes.len())
1014 .ok_or(MetaError::Malformed("truncated f64"))?;
1015 let arr: [u8; 8] = bytes[*pos..end].try_into().unwrap();
1016 *pos = end;
1017 Ok(f64::from_le_bytes(arr))
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::*;
1023
1024 fn sample_v1() -> PyramidMeta {
1025 let summary = vec![
1026 SuperEdge {
1027 s_comm: 0,
1028 predicate: 5,
1029 o_comm: 0,
1030 count: 4,
1031 },
1032 SuperEdge {
1033 s_comm: 0,
1034 predicate: 5,
1035 o_comm: 1,
1036 count: 1,
1037 },
1038 ];
1039 let tiles = vec![
1040 Tile {
1041 community: 0,
1042 triples: vec![],
1043 encoded: vec![1, 2, 3],
1044 },
1045 Tile {
1046 community: 1,
1047 triples: vec![],
1048 encoded: vec![9, 8],
1049 },
1050 ];
1051 PyramidMeta::new(2, summary, &tiles)
1052 }
1053
1054 #[test]
1055 fn meta_round_trips() {
1056 let meta = sample_v1();
1057 let bytes = meta.encode();
1058 let back = PyramidMeta::decode(&bytes).unwrap();
1059 assert_eq!(meta, back);
1060 assert_eq!(back.round, 2);
1061 assert_eq!(back.tiles[0], (0, vec![1, 2, 3]));
1062 assert!(back.class_hierarchy.is_empty(), "v1 shape has no schema");
1063 }
1064
1065 #[test]
1066 fn empty_meta() {
1067 let meta = PyramidMeta::new(0, vec![], &[]);
1068 assert_eq!(PyramidMeta::decode(&meta.encode()).unwrap(), meta);
1069 }
1070
1071 #[test]
1072 fn v1_encoding_is_unchanged_without_schema() {
1073 let meta = sample_v1();
1076 let bytes = meta.encode();
1077 assert_eq!(*bytes.last().unwrap(), 8);
1079 assert_ne!(*bytes.last().unwrap(), SCHEMA_V2);
1080 }
1081
1082 #[test]
1083 fn schema_pyramid_round_trips() {
1084 let mut meta = sample_v1();
1085 meta = meta.with_schema(
1086 vec![
1087 ClassNode {
1088 class: "<http://ex/Agent>".into(),
1089 parents: vec![],
1090 depth: 0,
1091 },
1092 ClassNode {
1093 class: "<http://ex/Astronaut>".into(),
1095 parents: vec!["<http://ex/Explorer>".into(), "<http://ex/Person>".into()],
1096 depth: 2,
1097 },
1098 ],
1099 vec![
1100 LevelRollup {
1101 round: 1,
1102 depth: 0,
1103 classes: vec![("<http://ex/Agent>".into(), 12)],
1104 },
1105 LevelRollup {
1106 round: 0,
1107 depth: 1,
1108 classes: vec![
1109 ("<http://ex/Person>".into(), 9),
1110 ("<http://ex/Agent>".into(), 3),
1111 ],
1112 },
1113 ],
1114 vec![LevelLinks {
1115 round: 1,
1116 depth: 0,
1117 links: vec![ClassRelation {
1118 s_class: "<http://ex/Agent>".into(),
1119 predicate: "<http://ex/memberOf>".into(),
1120 o_class: "<http://ex/Agent>".into(),
1121 count: 6,
1122 }],
1123 }],
1124 vec![CommunityDescriptor {
1125 community: 7,
1126 dominant_class: Some("<http://ex/Person>".into()),
1127 class_counts: vec![("<http://ex/Person>".into(), 5)],
1128 bbox: Some([-10.0, 40.0, 12.5, 51.2]),
1129 time_range: Some(("1700".into(), "1900".into())),
1130 }],
1131 vec![],
1132 vec![],
1133 vec![],
1134 );
1135 let bytes = meta.encode();
1136 let back = PyramidMeta::decode(&bytes).unwrap();
1138 assert_eq!(meta, back);
1139 assert_eq!(back.level_rollups.len(), 2);
1140 assert_eq!(back.class_hierarchy[1].parents.len(), 2);
1142 assert_eq!(
1144 back.level_links[0].links[0].predicate,
1145 "<http://ex/memberOf>"
1146 );
1147 assert_eq!(back.descriptors[0].bbox, Some([-10.0, 40.0, 12.5, 51.2]));
1148 assert_eq!(
1149 back.descriptors[0].time_range,
1150 Some(("1700".into(), "1900".into()))
1151 );
1152 }
1153
1154 #[test]
1155 fn coherence_axioms_round_trip_and_stay_additive() {
1156 let hier = || {
1160 vec![
1161 ClassNode {
1162 class: "<http://ex/C>".into(),
1163 parents: vec![],
1164 depth: 0,
1165 },
1166 ClassNode {
1167 class: "<http://ex/D>".into(),
1168 parents: vec![],
1169 depth: 0,
1170 },
1171 ClassNode {
1172 class: "<http://ex/E>".into(),
1173 parents: vec![],
1174 depth: 0,
1175 },
1176 ]
1177 };
1178 let v20 = sample_v1()
1180 .with_schema(hier(), vec![], vec![], vec![], vec![], vec![], vec![])
1181 .encode();
1182 let full = sample_v1().with_schema(
1184 hier(),
1185 vec![],
1186 vec![],
1187 vec![],
1188 vec![],
1189 vec![("<http://ex/D>".into(), "<http://ex/E>".into())],
1190 vec![],
1191 );
1192 let v21 = full.encode();
1193 assert!(
1194 v21.starts_with(&v20),
1195 "the extension is appended; the v2.0 prefix is byte-identical"
1196 );
1197 assert!(v21.len() > v20.len());
1198
1199 let back = PyramidMeta::decode(&v21).unwrap();
1200 assert_eq!(back, full, "the coherence axioms round-trip");
1201 assert_eq!(
1202 back.disjoint_pairs,
1203 vec![("<http://ex/D>".to_string(), "<http://ex/E>".to_string())]
1204 );
1205
1206 let back20 = PyramidMeta::decode(&v20).unwrap();
1209 assert!(back20.disjoint_pairs.is_empty());
1210 assert!(back20.subclass_cycles.is_empty());
1211 assert_eq!(back20.class_hierarchy.len(), 3, "v2.0 hierarchy intact");
1212 }
1213
1214 #[test]
1215 fn v2_is_readable_as_v1_prefix() {
1216 let mut meta = sample_v1();
1219 let v1_bytes = meta.encode();
1220 meta = meta.with_schema(
1221 vec![ClassNode {
1222 class: "<http://ex/C>".into(),
1223 parents: vec![],
1224 depth: 0,
1225 }],
1226 vec![LevelRollup {
1227 round: 0,
1228 depth: 0,
1229 classes: vec![("<http://ex/C>".into(), 1)],
1230 }],
1231 vec![],
1232 vec![],
1233 vec![],
1234 vec![],
1235 vec![],
1236 );
1237 let v2_bytes = meta.encode();
1238 assert!(
1239 v2_bytes.starts_with(&v1_bytes),
1240 "v2 extends v1 byte-for-byte"
1241 );
1242 assert_eq!(v2_bytes[v1_bytes.len()], SCHEMA_V2, "v2 tag follows");
1243 }
1244
1245 #[test]
1246 fn query_stats_round_trip_and_stay_additive() {
1247 let v1 = sample_v1().encode();
1248 let stats = vec![
1249 PredStat {
1250 predicate: 5,
1251 count: 100,
1252 distinct_subjects: 40,
1253 distinct_objects: 25,
1254 max_objects_per_subject: 6,
1255 max_subjects_per_object: 9,
1256 },
1257 PredStat {
1258 predicate: 9,
1259 count: 3,
1260 distinct_subjects: 3,
1261 distinct_objects: 1,
1262 max_objects_per_subject: 1,
1263 max_subjects_per_object: 3,
1264 },
1265 ];
1266 let bytes = sample_v1().with_predicate_stats(stats.clone()).encode();
1267 assert!(bytes.starts_with(&v1), "query-stats is appended after v1");
1269 assert_eq!(bytes[v1.len()], QSTATS_V1, "the qstats tag follows v1");
1270 let back = PyramidMeta::decode(&bytes).unwrap();
1271 assert_eq!(back.predicate_stats, stats);
1272 assert_eq!(back.round, 2, "v1 fields intact");
1273 assert_eq!(back.tiles[0], (0, vec![1, 2, 3]));
1274 }
1275
1276 #[test]
1277 fn query_stats_before_schema_keeps_schema_trailing() {
1278 let stats = vec![PredStat {
1281 predicate: 1,
1282 count: 9,
1283 distinct_subjects: 3,
1284 distinct_objects: 3,
1285 max_objects_per_subject: 3,
1286 max_subjects_per_object: 3,
1287 }];
1288 let bytes = sample_v1()
1289 .with_schema(
1290 vec![ClassNode {
1291 class: "<http://ex/C>".into(),
1292 parents: vec![],
1293 depth: 0,
1294 }],
1295 vec![LevelRollup {
1296 round: 0,
1297 depth: 0,
1298 classes: vec![("<http://ex/C>".into(), 1)],
1299 }],
1300 vec![],
1301 vec![],
1302 vec![],
1303 vec![],
1304 vec![],
1305 )
1306 .with_predicate_stats(stats.clone())
1307 .encode();
1308 let back = PyramidMeta::decode(&bytes).unwrap();
1309 assert_eq!(back.predicate_stats, stats);
1310 assert_eq!(back.class_hierarchy.len(), 1, "schema decoded past qstats");
1311 let len = schema_block_len(&bytes) as usize;
1312 assert!(len > 0);
1313 assert_eq!(
1314 bytes[bytes.len() - len],
1315 SCHEMA_V2,
1316 "schema is still the trailing block"
1317 );
1318 }
1319
1320 #[test]
1321 fn char_sets_coexist_with_query_stats_and_schema() {
1322 let sets = vec![
1323 CharSet {
1324 predicates: vec![1, 5, 9],
1325 subjects: 100,
1326 },
1327 CharSet {
1328 predicates: vec![1, 5],
1329 subjects: 40,
1330 },
1331 ];
1332 let stats = vec![PredStat {
1333 predicate: 1,
1334 count: 9,
1335 distinct_subjects: 3,
1336 distinct_objects: 3,
1337 max_objects_per_subject: 3,
1338 max_subjects_per_object: 3,
1339 }];
1340 let bytes = sample_v1()
1341 .with_predicate_stats(stats.clone())
1342 .with_char_sets(sets.clone())
1343 .with_schema(
1344 vec![ClassNode {
1345 class: "<http://ex/C>".into(),
1346 parents: vec![],
1347 depth: 0,
1348 }],
1349 vec![LevelRollup {
1350 round: 0,
1351 depth: 0,
1352 classes: vec![("<http://ex/C>".into(), 1)],
1353 }],
1354 vec![],
1355 vec![],
1356 vec![],
1357 vec![],
1358 vec![],
1359 )
1360 .encode();
1361 let back = PyramidMeta::decode(&bytes).unwrap();
1362 assert_eq!(back.predicate_stats, stats);
1363 assert_eq!(back.char_sets, sets);
1364 assert_eq!(
1365 back.class_hierarchy.len(),
1366 1,
1367 "schema decoded past both blocks"
1368 );
1369 let len = schema_block_len(&bytes) as usize;
1370 assert_eq!(bytes[bytes.len() - len], SCHEMA_V2, "schema still trailing");
1371 }
1372
1373 #[test]
1374 fn label_index_round_trips_and_prefix_searches() {
1375 let labels = vec![
1377 LabelEntry {
1378 label: "Alanine".into(),
1379 subject: 10,
1380 },
1381 LabelEntry {
1382 label: "alpha-D-glucose".into(),
1383 subject: 11,
1384 },
1385 LabelEntry {
1386 label: "Benzene".into(),
1387 subject: 12,
1388 },
1389 ];
1390 let meta = sample_v1().with_label_index(labels.clone());
1391 let bytes = meta.encode();
1392 let back = PyramidMeta::decode(&bytes).unwrap();
1393 assert_eq!(back.label_index, labels, "label index survives round trip");
1394
1395 let hits = back.prefix_search("al", 10);
1397 assert_eq!(
1398 hits.iter().map(|e| e.subject).collect::<Vec<_>>(),
1399 vec![10, 11]
1400 );
1401 let hits = back.prefix_search("BEN", 10);
1403 assert_eq!(hits.len(), 1);
1404 assert_eq!(hits[0].subject, 12);
1405 assert!(back.prefix_search("zzz", 10).is_empty());
1407 assert_eq!(back.prefix_search("", 2).len(), 2);
1409 }
1410
1411 #[test]
1412 fn label_index_stays_additive_before_schema() {
1413 let labels = vec![LabelEntry {
1414 label: "x".into(),
1415 subject: 1,
1416 }];
1417 let stats = vec![PredStat {
1418 predicate: 1,
1419 count: 1,
1420 distinct_subjects: 1,
1421 distinct_objects: 1,
1422 max_objects_per_subject: 1,
1423 max_subjects_per_object: 1,
1424 }];
1425 let sets = vec![CharSet {
1426 predicates: vec![1],
1427 subjects: 1,
1428 }];
1429 let bytes = sample_v1()
1430 .with_predicate_stats(stats.clone())
1431 .with_char_sets(sets.clone())
1432 .with_label_index(labels.clone())
1433 .with_schema(
1434 vec![ClassNode {
1435 class: "<http://ex/C>".into(),
1436 parents: vec![],
1437 depth: 0,
1438 }],
1439 vec![],
1440 vec![],
1441 vec![],
1442 vec![],
1443 vec![],
1444 vec![],
1445 )
1446 .encode();
1447 let back = PyramidMeta::decode(&bytes).unwrap();
1448 assert_eq!(back.predicate_stats, stats);
1449 assert_eq!(back.char_sets, sets);
1450 assert_eq!(back.label_index, labels);
1451 assert_eq!(
1452 back.class_hierarchy.len(),
1453 1,
1454 "schema decoded past 3 blocks"
1455 );
1456 let len = schema_block_len(&bytes) as usize;
1457 assert_eq!(
1458 bytes[bytes.len() - len],
1459 SCHEMA_V2,
1460 "schema still trailing past the label index"
1461 );
1462 }
1463}