use super::dictionary::ColumnDictionary;
use super::persistence::PersistentState;
use crate::indexing::UltraIndex;
use crate::model::*;
use crate::optimization::RdfArena;
use std::collections::BTreeSet;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone)]
pub enum StorageBackend {
UltraMemory(Arc<UltraIndex>, Arc<RdfArena>),
Memory(Arc<RwLock<MemoryStorage>>),
Persistent(Arc<RwLock<MemoryStorage>>, Arc<PersistentState>),
}
#[derive(Debug, Clone, Default)]
pub struct MemoryStorage {
subjects: ColumnDictionary<Subject>,
predicates: ColumnDictionary<Predicate>,
objects: ColumnDictionary<Object>,
graphs: ColumnDictionary<GraphName>,
spog: BTreeSet<[u32; 4]>,
posg: BTreeSet<[u32; 4]>,
ospg: BTreeSet<[u32; 4]>,
gspo: BTreeSet<[u32; 4]>,
pub named_graphs: BTreeSet<NamedNode>,
}
#[inline]
fn id_matches(bound: Option<u32>, actual: u32) -> bool {
match bound {
Some(want) => want == actual,
None => true,
}
}
#[inline]
fn resolve_live<T>(resolved: Option<&T>) -> Option<&T> {
debug_assert!(
resolved.is_some(),
"permutation index references an id with no live term (dictionary/index desync)"
);
resolved
}
impl MemoryStorage {
pub fn new() -> Self {
MemoryStorage {
subjects: ColumnDictionary::new(),
predicates: ColumnDictionary::new(),
objects: ColumnDictionary::new(),
graphs: ColumnDictionary::new(),
spog: BTreeSet::new(),
posg: BTreeSet::new(),
ospg: BTreeSet::new(),
gspo: BTreeSet::new(),
named_graphs: BTreeSet::new(),
}
}
pub fn insert_quad(&mut self, quad: Quad) -> bool {
let s = self.subjects.intern(quad.subject());
let p = self.predicates.intern(quad.predicate());
let o = self.objects.intern(quad.object());
let g = self.graphs.intern(quad.graph_name());
if !self.spog.insert([s, p, o, g]) {
return false;
}
self.subjects.retain(s);
self.predicates.retain(p);
self.objects.retain(o);
self.graphs.retain(g);
self.posg.insert([p, o, s, g]);
self.ospg.insert([o, s, p, g]);
self.gspo.insert([g, s, p, o]);
if let GraphName::NamedNode(graph_name) = quad.graph_name() {
self.named_graphs.insert(graph_name.clone());
}
true
}
pub fn remove_quad(&mut self, quad: &Quad) -> bool {
let (s, p, o, g) = match (
self.subjects.get_id(quad.subject()),
self.predicates.get_id(quad.predicate()),
self.objects.get_id(quad.object()),
self.graphs.get_id(quad.graph_name()),
) {
(Some(s), Some(p), Some(o), Some(g)) => (s, p, o, g),
_ => return false,
};
if !self.spog.remove(&[s, p, o, g]) {
return false;
}
self.posg.remove(&[p, o, s, g]);
self.ospg.remove(&[o, s, p, g]);
self.gspo.remove(&[g, s, p, o]);
self.subjects.release(s);
self.predicates.release(p);
self.objects.release(o);
self.graphs.release(g);
if let GraphName::NamedNode(graph_name) = quad.graph_name() {
let still_present = self
.gspo
.range([g, 0, 0, 0]..=[g, u32::MAX, u32::MAX, u32::MAX])
.next()
.is_some();
if !still_present {
self.named_graphs.remove(graph_name);
}
}
true
}
pub fn contains_quad(&self, quad: &Quad) -> bool {
match (
self.subjects.get_id(quad.subject()),
self.predicates.get_id(quad.predicate()),
self.objects.get_id(quad.object()),
self.graphs.get_id(quad.graph_name()),
) {
(Some(s), Some(p), Some(o), Some(g)) => self.spog.contains(&[s, p, o, g]),
_ => false,
}
}
fn materialize(&self, s: u32, p: u32, o: u32, g: u32) -> Option<Quad> {
Some(Quad::new(
resolve_live(self.subjects.resolve(s))?.clone(),
resolve_live(self.predicates.resolve(p))?.clone(),
resolve_live(self.objects.resolve(o))?.clone(),
resolve_live(self.graphs.resolve(g))?.clone(),
))
}
fn scan_ids<'a>(
&'a self,
sid: Option<u32>,
pid: Option<u32>,
oid: Option<u32>,
gid: Option<u32>,
) -> Box<dyn Iterator<Item = [u32; 4]> + 'a> {
const LO: [u32; 4] = [0, 0, 0, 0];
let hi = |k: u32| [k, u32::MAX, u32::MAX, u32::MAX];
let base: Box<dyn Iterator<Item = [u32; 4]> + 'a> = if let Some(s) = sid {
Box::new(
self.spog
.range([s, LO[1], LO[2], LO[3]]..=hi(s))
.map(|t| [t[0], t[1], t[2], t[3]]),
)
} else if let Some(o) = oid {
Box::new(
self.ospg
.range([o, LO[1], LO[2], LO[3]]..=hi(o))
.map(|t| [t[1], t[2], t[0], t[3]]),
)
} else if let Some(p) = pid {
Box::new(
self.posg
.range([p, LO[1], LO[2], LO[3]]..=hi(p))
.map(|t| [t[2], t[0], t[1], t[3]]),
)
} else if let Some(g) = gid {
Box::new(
self.gspo
.range([g, LO[1], LO[2], LO[3]]..=hi(g))
.map(|t| [t[1], t[2], t[3], t[0]]),
)
} else {
Box::new(self.spog.iter().copied())
};
Box::new(base.filter(move |q| {
id_matches(sid, q[0])
&& id_matches(pid, q[1])
&& id_matches(oid, q[2])
&& id_matches(gid, q[3])
}))
}
#[allow(clippy::type_complexity)]
fn resolve_pattern_ids(
&self,
subject: Option<&Subject>,
predicate: Option<&Predicate>,
object: Option<&Object>,
graph_name: Option<&GraphName>,
) -> Option<(Option<u32>, Option<u32>, Option<u32>, Option<u32>)> {
let sid = match subject {
Some(s) => Some(self.subjects.get_id(s)?),
None => None,
};
let pid = match predicate {
Some(p) => Some(self.predicates.get_id(p)?),
None => None,
};
let oid = match object {
Some(o) => Some(self.objects.get_id(o)?),
None => None,
};
let gid = match graph_name {
Some(g) => Some(self.graphs.get_id(g)?),
None => None,
};
Some((sid, pid, oid, gid))
}
pub fn query_quads(
&self,
subject: Option<&Subject>,
predicate: Option<&Predicate>,
object: Option<&Object>,
graph_name: Option<&GraphName>,
) -> Vec<Quad> {
let (sid, pid, oid, gid) =
match self.resolve_pattern_ids(subject, predicate, object, graph_name) {
Some(ids) => ids,
None => return Vec::new(),
};
let mut results: BTreeSet<Quad> = BTreeSet::new();
for [s, p, o, g] in self.scan_ids(sid, pid, oid, gid) {
if let Some(quad) = self.materialize(s, p, o, g) {
results.insert(quad);
}
}
results.into_iter().collect()
}
pub fn iter_quads(&self) -> impl Iterator<Item = Quad> + '_ {
self.spog
.iter()
.filter_map(move |t| self.materialize(t[0], t[1], t[2], t[3]))
}
pub fn for_each_quad(
&self,
subject: Option<&Subject>,
predicate: Option<&Predicate>,
object: Option<&Object>,
graph_name: Option<&GraphName>,
f: &mut dyn FnMut(Quad),
) {
let (sid, pid, oid, gid) =
match self.resolve_pattern_ids(subject, predicate, object, graph_name) {
Some(ids) => ids,
None => return,
};
for [s, p, o, g] in self.scan_ids(sid, pid, oid, gid) {
if let Some(quad) = self.materialize(s, p, o, g) {
f(quad);
}
}
}
pub fn len(&self) -> usize {
self.spog.len()
}
pub fn is_empty(&self) -> bool {
self.spog.is_empty()
}
pub fn size_estimate(&self) -> usize {
use std::mem::size_of;
let tuple = size_of::<[u32; 4]>();
let perm_bytes =
(self.spog.len() + self.posg.len() + self.ospg.len() + self.gspo.len()) * tuple;
let dict_bytes = self.subjects.size_estimate()
+ self.predicates.size_estimate()
+ self.objects.size_estimate()
+ self.graphs.size_estimate();
let named_graph_bytes = self.named_graphs.len() * size_of::<NamedNode>();
perm_bytes + dict_bytes + named_graph_bytes
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn nn(iri: &str) -> NamedNode {
NamedNode::new(iri).expect("valid IRI")
}
fn naive_query(
reference: &BTreeSet<Quad>,
subject: Option<&Subject>,
predicate: Option<&Predicate>,
object: Option<&Object>,
graph_name: Option<&GraphName>,
) -> BTreeSet<Quad> {
reference
.iter()
.filter(|q| match subject {
Some(s) => q.subject() == s,
None => true,
})
.filter(|q| match predicate {
Some(p) => q.predicate() == p,
None => true,
})
.filter(|q| match object {
Some(o) => q.object() == o,
None => true,
})
.filter(|q| match graph_name {
Some(g) => q.graph_name() == g,
None => true,
})
.cloned()
.collect()
}
fn build_dataset() -> (MemoryStorage, BTreeSet<Quad>) {
let subjects = [
Subject::NamedNode(nn("http://ex.org/s1")),
Subject::NamedNode(nn("http://ex.org/s2")),
Subject::BlankNode(BlankNode::new("b1").expect("valid blank node")),
];
let predicates = [
Predicate::NamedNode(nn("http://ex.org/p1")),
Predicate::NamedNode(nn("http://ex.org/p2")),
];
let objects = [
Object::NamedNode(nn("http://ex.org/o1")),
Object::Literal(Literal::new("hello")),
Object::Literal(Literal::new_lang("hello", "en").expect("valid lang literal")),
Object::BlankNode(BlankNode::new("b2").expect("valid blank node")),
];
let graphs = [
GraphName::DefaultGraph,
GraphName::NamedNode(nn("http://ex.org/g1")),
];
let mut storage = MemoryStorage::new();
let mut reference = BTreeSet::new();
let mut counter = 0usize;
for s in &subjects {
for p in &predicates {
for o in &objects {
for g in &graphs {
counter += 1;
if counter % 7 == 0 {
continue;
}
let quad = Quad::new(s.clone(), p.clone(), o.clone(), g.clone());
assert_eq!(
storage.insert_quad(quad.clone()),
reference.insert(quad),
"insert novelty must agree with the reference set"
);
}
}
}
}
(storage, reference)
}
#[test]
fn test_interning_matches_naive_all_16_binding_combinations() {
let (storage, reference) = build_dataset();
let subj_opts: Vec<Option<Subject>> = std::iter::once(None)
.chain(
[
Subject::NamedNode(nn("http://ex.org/s1")),
Subject::NamedNode(nn("http://ex.org/s2")),
Subject::BlankNode(BlankNode::new("b1").expect("valid blank node")),
Subject::NamedNode(nn("http://ex.org/absent")),
]
.into_iter()
.map(Some),
)
.collect();
let pred_opts: Vec<Option<Predicate>> = std::iter::once(None)
.chain(
[
Predicate::NamedNode(nn("http://ex.org/p1")),
Predicate::NamedNode(nn("http://ex.org/p2")),
Predicate::NamedNode(nn("http://ex.org/absent")),
]
.into_iter()
.map(Some),
)
.collect();
let obj_opts: Vec<Option<Object>> = std::iter::once(None)
.chain(
[
Object::NamedNode(nn("http://ex.org/o1")),
Object::Literal(Literal::new("hello")),
Object::Literal(Literal::new_lang("hello", "en").expect("valid lang literal")),
Object::BlankNode(BlankNode::new("b2").expect("valid blank node")),
Object::Literal(Literal::new("absent")),
]
.into_iter()
.map(Some),
)
.collect();
let graph_opts: Vec<Option<GraphName>> = std::iter::once(None)
.chain(
[
GraphName::DefaultGraph,
GraphName::NamedNode(nn("http://ex.org/g1")),
GraphName::NamedNode(nn("http://ex.org/absent")),
]
.into_iter()
.map(Some),
)
.collect();
for so in &subj_opts {
for po in &pred_opts {
for oo in &obj_opts {
for go in &graph_opts {
let got: BTreeSet<Quad> = storage
.query_quads(so.as_ref(), po.as_ref(), oo.as_ref(), go.as_ref())
.into_iter()
.collect();
let want = naive_query(
&reference,
so.as_ref(),
po.as_ref(),
oo.as_ref(),
go.as_ref(),
);
assert_eq!(
got, want,
"mismatch for pattern s={so:?} p={po:?} o={oo:?} g={go:?}"
);
}
}
}
}
}
#[test]
fn test_interning_remove_updates_all_indexes_and_named_graphs() {
let (mut storage, mut reference) = build_dataset();
let g1 = GraphName::NamedNode(nn("http://ex.org/g1"));
let g1_quads: Vec<Quad> = reference
.iter()
.filter(|q| q.graph_name() == &g1)
.cloned()
.collect();
assert!(!g1_quads.is_empty(), "dataset must contain g1 quads");
for (i, quad) in g1_quads.iter().enumerate() {
assert!(storage.remove_quad(quad));
reference.remove(quad);
let last = i + 1 == g1_quads.len();
assert_eq!(
storage.named_graphs.contains(&nn("http://ex.org/g1")),
!last,
"named graph g1 must persist until its last quad is removed"
);
}
let all: BTreeSet<Quad> = storage
.query_quads(None, None, None, None)
.into_iter()
.collect();
assert_eq!(all, reference);
assert_eq!(storage.len(), reference.len());
}
#[test]
fn test_interning_duplicate_insert_is_noop() {
let mut storage = MemoryStorage::new();
let quad = Quad::new(
nn("http://ex.org/s"),
nn("http://ex.org/p"),
nn("http://ex.org/o"),
GraphName::DefaultGraph,
);
assert!(storage.insert_quad(quad.clone()));
assert!(!storage.insert_quad(quad.clone()));
assert_eq!(storage.len(), 1);
assert!(storage.contains_quad(&quad));
}
#[test]
fn test_pattern_queries_select_correct_permutation() {
let (storage, reference) = build_dataset();
let s = Subject::NamedNode(nn("http://ex.org/s1"));
let p = Predicate::NamedNode(nn("http://ex.org/p1"));
let o = Object::NamedNode(nn("http://ex.org/o1"));
let g = GraphName::NamedNode(nn("http://ex.org/g1"));
let got: BTreeSet<Quad> = storage
.query_quads(Some(&s), None, None, None)
.into_iter()
.collect();
assert_eq!(got, naive_query(&reference, Some(&s), None, None, None));
let got: BTreeSet<Quad> = storage
.query_quads(Some(&s), Some(&p), None, None)
.into_iter()
.collect();
assert_eq!(got, naive_query(&reference, Some(&s), Some(&p), None, None));
let got: BTreeSet<Quad> = storage
.query_quads(Some(&s), Some(&p), Some(&o), None)
.into_iter()
.collect();
assert_eq!(
got,
naive_query(&reference, Some(&s), Some(&p), Some(&o), None)
);
let got: BTreeSet<Quad> = storage
.query_quads(None, Some(&p), None, None)
.into_iter()
.collect();
assert_eq!(got, naive_query(&reference, None, Some(&p), None, None));
let got: BTreeSet<Quad> = storage
.query_quads(None, None, Some(&o), None)
.into_iter()
.collect();
assert_eq!(got, naive_query(&reference, None, None, Some(&o), None));
let got: BTreeSet<Quad> = storage
.query_quads(None, None, None, Some(&g))
.into_iter()
.collect();
assert_eq!(got, naive_query(&reference, None, None, None, Some(&g)));
let got: BTreeSet<Quad> = storage
.query_quads(None, None, None, None)
.into_iter()
.collect();
assert_eq!(got, reference);
}
#[test]
fn test_insert_remove_reinsert_round_trip() {
let mut storage = MemoryStorage::new();
let quad = Quad::new(
Subject::NamedNode(nn("http://ex.org/s")),
Predicate::NamedNode(nn("http://ex.org/p")),
Object::Literal(Literal::new("v")),
GraphName::NamedNode(nn("http://ex.org/g")),
);
assert!(storage.insert_quad(quad.clone()));
assert!(storage.contains_quad(&quad));
assert_eq!(storage.len(), 1);
assert!(storage.named_graphs.contains(&nn("http://ex.org/g")));
assert!(storage.remove_quad(&quad));
assert!(!storage.contains_quad(&quad));
assert_eq!(storage.len(), 0);
assert!(storage.is_empty());
assert!(!storage.named_graphs.contains(&nn("http://ex.org/g")));
assert!(storage.query_quads(None, None, None, None).is_empty());
assert!(!storage.remove_quad(&quad));
assert!(storage.insert_quad(quad.clone()));
assert!(storage.contains_quad(&quad));
assert_eq!(storage.len(), 1);
assert!(storage.named_graphs.contains(&nn("http://ex.org/g")));
let got: Vec<Quad> = storage.query_quads(
Some(&Subject::NamedNode(nn("http://ex.org/s"))),
None,
None,
None,
);
assert_eq!(got, vec![quad]);
}
#[test]
fn test_named_graphs_tracked_incrementally() {
let mut storage = MemoryStorage::new();
let g1 = nn("http://ex.org/g1");
let g2 = nn("http://ex.org/g2");
let q_g1a = Quad::new(
Subject::NamedNode(nn("http://ex.org/s1")),
Predicate::NamedNode(nn("http://ex.org/p")),
Object::Literal(Literal::new("a")),
GraphName::NamedNode(g1.clone()),
);
let q_g1b = Quad::new(
Subject::NamedNode(nn("http://ex.org/s2")),
Predicate::NamedNode(nn("http://ex.org/p")),
Object::Literal(Literal::new("b")),
GraphName::NamedNode(g1.clone()),
);
let q_g2 = Quad::new(
Subject::NamedNode(nn("http://ex.org/s3")),
Predicate::NamedNode(nn("http://ex.org/p")),
Object::Literal(Literal::new("c")),
GraphName::NamedNode(g2.clone()),
);
let q_default = Quad::new(
Subject::NamedNode(nn("http://ex.org/s4")),
Predicate::NamedNode(nn("http://ex.org/p")),
Object::Literal(Literal::new("d")),
GraphName::DefaultGraph,
);
for q in [&q_g1a, &q_g1b, &q_g2, &q_default] {
assert!(storage.insert_quad(q.clone()));
}
assert_eq!(
storage.named_graphs,
[g1.clone(), g2.clone()].into_iter().collect()
);
assert!(storage.remove_quad(&q_g1a));
assert!(storage.named_graphs.contains(&g1));
assert!(storage.remove_quad(&q_g1b));
assert!(!storage.named_graphs.contains(&g1));
assert!(storage.named_graphs.contains(&g2));
assert_eq!(storage.named_graphs, [g2].into_iter().collect());
}
#[test]
fn test_id_reclamation_bounded_growth() {
let mut storage = MemoryStorage::new();
let quad = Quad::new(
Subject::NamedNode(nn("http://ex.org/s")),
Predicate::NamedNode(nn("http://ex.org/p")),
Object::Literal(Literal::new("v")),
GraphName::NamedNode(nn("http://ex.org/g")),
);
assert!(storage.insert_quad(quad.clone()));
for dict_slots in [
storage.subjects.slot_count(),
storage.predicates.slot_count(),
storage.objects.slot_count(),
storage.graphs.slot_count(),
] {
assert_eq!(dict_slots, 1);
}
assert!(storage.remove_quad(&quad));
assert!(storage.insert_quad(quad.clone()));
let baseline = storage.size_estimate();
for _ in 0..1_000 {
assert!(storage.remove_quad(&quad));
assert_eq!(storage.subjects.live_count(), 0);
assert_eq!(storage.subjects.free_count(), 1);
assert_eq!(storage.graphs.live_count(), 0);
assert!(storage.insert_quad(quad.clone()));
}
assert_eq!(storage.subjects.slot_count(), 1);
assert_eq!(storage.predicates.slot_count(), 1);
assert_eq!(storage.objects.slot_count(), 1);
assert_eq!(storage.graphs.slot_count(), 1);
assert_eq!(storage.size_estimate(), baseline);
assert_eq!(storage.len(), 1);
assert!(storage.contains_quad(&quad));
}
#[test]
fn test_id_reuse_round_trip_matches_naive_model() {
let mk = |i: usize| {
Quad::new(
Subject::NamedNode(nn(&format!("http://ex.org/s{i}"))),
Predicate::NamedNode(nn("http://ex.org/p")),
Object::Literal(Literal::new(format!("v{i}"))),
GraphName::DefaultGraph,
)
};
let mut storage = MemoryStorage::new();
let mut reference: BTreeSet<Quad> = BTreeSet::new();
for i in 0..20 {
let q = mk(i);
assert_eq!(storage.insert_quad(q.clone()), reference.insert(q));
}
for i in (0..20).step_by(2) {
let q = mk(i);
assert_eq!(storage.remove_quad(&q), reference.remove(&q));
}
let slots_before = storage.subjects.slot_count();
for i in 20..30 {
let q = mk(i);
assert_eq!(storage.insert_quad(q.clone()), reference.insert(q));
}
assert_eq!(storage.subjects.slot_count(), slots_before);
let got: BTreeSet<Quad> = storage
.query_quads(None, None, None, None)
.into_iter()
.collect();
assert_eq!(got, reference);
for q in &reference {
assert!(storage.contains_quad(q));
}
assert!(!storage.contains_quad(&mk(0)));
}
#[test]
fn test_shared_term_retained_until_last_quad_removed() {
let mut storage = MemoryStorage::new();
let s = Subject::NamedNode(nn("http://ex.org/s"));
let p = Predicate::NamedNode(nn("http://ex.org/p"));
let q1 = Quad::new(
s.clone(),
p.clone(),
Object::Literal(Literal::new("o1")),
GraphName::DefaultGraph,
);
let q2 = Quad::new(
s.clone(),
p.clone(),
Object::Literal(Literal::new("o2")),
GraphName::DefaultGraph,
);
assert!(storage.insert_quad(q1.clone()));
assert!(storage.insert_quad(q2.clone()));
assert!(!storage.insert_quad(q1.clone()));
let s_id = storage.subjects.get_id(&s).expect("subject interned");
assert!(storage.remove_quad(&q1));
assert_eq!(storage.subjects.get_id(&s), Some(s_id));
assert!(storage.subjects.resolve(s_id).is_some());
assert_eq!(storage.subjects.live_count(), 1);
assert!(storage.remove_quad(&q2));
assert_eq!(storage.subjects.get_id(&s), None);
assert_eq!(storage.subjects.live_count(), 0);
assert_eq!(storage.subjects.free_count(), 1);
assert!(storage.is_empty());
}
#[test]
fn test_clearing_a_graph_reclaims_its_terms() {
let mut storage = MemoryStorage::new();
let g = GraphName::NamedNode(nn("http://ex.org/g"));
let quads: Vec<Quad> = (0..5)
.map(|i| {
Quad::new(
Subject::NamedNode(nn(&format!("http://ex.org/s{i}"))),
Predicate::NamedNode(nn("http://ex.org/p")),
Object::Literal(Literal::new(format!("o{i}"))),
g.clone(),
)
})
.collect();
for q in &quads {
assert!(storage.insert_quad(q.clone()));
}
assert!(storage.named_graphs.contains(&nn("http://ex.org/g")));
let graph_slots = storage.graphs.slot_count();
assert_eq!(storage.graphs.live_count(), 1);
for q in &quads {
assert!(storage.remove_quad(q));
}
assert!(!storage.named_graphs.contains(&nn("http://ex.org/g")));
assert_eq!(storage.graphs.live_count(), 0);
assert_eq!(storage.graphs.free_count(), graph_slots);
assert_eq!(storage.subjects.live_count(), 0);
assert_eq!(storage.predicates.live_count(), 0);
assert_eq!(storage.objects.live_count(), 0);
assert!(storage.is_empty());
}
#[test]
fn test_named_graphs_bookkeeping_with_id_reclamation() {
let mut storage = MemoryStorage::new();
let g1 = nn("http://ex.org/g1");
let g2 = nn("http://ex.org/g2");
let mk = |g: &NamedNode, o: &str| {
Quad::new(
Subject::NamedNode(nn("http://ex.org/s")),
Predicate::NamedNode(nn("http://ex.org/p")),
Object::Literal(Literal::new(o)),
GraphName::NamedNode(g.clone()),
)
};
let q1 = mk(&g1, "a");
let q2 = mk(&g2, "b");
assert!(storage.insert_quad(q1.clone()));
assert!(storage.insert_quad(q2.clone()));
assert_eq!(
storage.named_graphs,
[g1.clone(), g2.clone()].into_iter().collect()
);
assert!(storage.remove_quad(&q1));
assert!(!storage.named_graphs.contains(&g1));
assert!(storage.named_graphs.contains(&g2));
assert_eq!(
storage.graphs.get_id(&GraphName::NamedNode(g1.clone())),
None
);
let g3 = nn("http://ex.org/g3");
let q3 = mk(&g3, "c");
assert!(storage.insert_quad(q3.clone()));
assert_eq!(
storage.named_graphs,
[g2.clone(), g3.clone()].into_iter().collect()
);
assert_eq!(storage.graphs.slot_count(), 2);
assert_eq!(storage.graphs.live_count(), 2);
let got: BTreeSet<Quad> = storage
.query_quads(None, None, None, None)
.into_iter()
.collect();
assert_eq!(got, [q2, q3].into_iter().collect());
}
#[test]
#[ignore = "manual memory-footprint measurement"]
fn memory_footprint_100k_triples() {
let mut storage = MemoryStorage::new();
for i in 0..100_000u32 {
let quad = Quad::new(
Subject::NamedNode(nn(&format!("http://ex.org/s{}", i % 10_000))),
Predicate::NamedNode(nn(&format!("http://ex.org/p{}", i % 100))),
Object::Literal(Literal::new(format!("value-{i}"))),
GraphName::DefaultGraph,
);
storage.insert_quad(quad);
}
assert_eq!(storage.len(), 100_000);
let after = storage.size_estimate();
let owned_quad_bytes: usize = storage
.iter_quads()
.map(|q| {
std::mem::size_of::<Quad>()
+ q.subject().to_string().len()
+ q.predicate().to_string().len()
+ q.object().to_string().len()
+ q.graph_name().to_string().len()
})
.sum();
let before = after + owned_quad_bytes;
eprintln!(
"memory_footprint(100k triples): interned-only ~= {after} bytes \
({:.1} MiB); previous design with owned quad set ~= {before} bytes \
({:.1} MiB); dropping the owned set saves ~= {} bytes ({:.2}x smaller)",
after as f64 / (1024.0 * 1024.0),
before as f64 / (1024.0 * 1024.0),
owned_quad_bytes,
before as f64 / after as f64,
);
assert!(after < before, "interned-only must be smaller than before");
}
}