use std::collections::HashMap;
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
fn strategy_read<'a, T>(lock: &'a RwLock<T>) -> RwLockReadGuard<'a, T> {
lock.read().unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn strategy_write<'a, T>(lock: &'a RwLock<T>) -> RwLockWriteGuard<'a, T> {
lock.write()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PatternType {
SubPreObj,
SubPre,
SubObj,
PreObj,
Sub,
Pre,
Obj,
Any,
}
impl PatternType {
pub fn from_bounds(sub: bool, pre: bool, obj: bool) -> Self {
match (sub, pre, obj) {
(true, true, true) => PatternType::SubPreObj,
(true, true, false) => PatternType::SubPre,
(true, false, true) => PatternType::SubObj,
(false, true, true) => PatternType::PreObj,
(true, false, false) => PatternType::Sub,
(false, true, false) => PatternType::Pre,
(false, false, true) => PatternType::Obj,
(false, false, false) => PatternType::Any,
}
}
pub fn selectivity(&self) -> f64 {
match self {
PatternType::SubPreObj => 0.001, PatternType::SubPre => 0.01,
PatternType::SubObj => 0.02,
PatternType::PreObj => 0.03,
PatternType::Sub => 0.1,
PatternType::Pre => 0.3,
PatternType::Obj => 0.2,
PatternType::Any => 1.0, }
}
pub fn recommended_index(&self) -> IndexType {
match self {
PatternType::SubPreObj => IndexType::SPO,
PatternType::SubPre => IndexType::SPO,
PatternType::SubObj => IndexType::SOP,
PatternType::PreObj => IndexType::POS,
PatternType::Sub => IndexType::SPO,
PatternType::Pre => IndexType::POS,
PatternType::Obj => IndexType::OPS,
PatternType::Any => IndexType::SPO,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IndexType {
SPO,
SOP,
POS,
PSO,
OPS,
OSP,
}
impl IndexType {
pub fn all() -> &'static [IndexType] {
&[
IndexType::SPO,
IndexType::SOP,
IndexType::POS,
IndexType::PSO,
IndexType::OPS,
IndexType::OSP,
]
}
pub fn key_order(&self) -> (usize, usize, usize) {
match self {
IndexType::SPO => (0, 1, 2),
IndexType::SOP => (0, 2, 1),
IndexType::POS => (1, 2, 0),
IndexType::PSO => (1, 0, 2),
IndexType::OPS => (2, 1, 0),
IndexType::OSP => (2, 0, 1),
}
}
}
pub trait StoreStrategy: Send + Sync {
fn name(&self) -> &str;
fn indexes(&self) -> &[IndexType];
fn index_on_add(&self) -> bool;
fn select_index(&self, pattern: PatternType) -> IndexType;
fn estimate_cost(&self, pattern: PatternType) -> f64;
}
#[derive(Debug, Clone)]
pub struct EagerStoreStrategy {
indexes: Vec<IndexType>,
}
impl EagerStoreStrategy {
pub fn new() -> Self {
Self {
indexes: vec![IndexType::SPO, IndexType::POS, IndexType::OSP],
}
}
pub fn full() -> Self {
Self {
indexes: IndexType::all().to_vec(),
}
}
pub fn with_indexes(indexes: Vec<IndexType>) -> Self {
Self { indexes }
}
}
impl Default for EagerStoreStrategy {
fn default() -> Self {
Self::new()
}
}
impl StoreStrategy for EagerStoreStrategy {
fn name(&self) -> &str {
"Eager"
}
fn indexes(&self) -> &[IndexType] {
&self.indexes
}
fn index_on_add(&self) -> bool {
true
}
fn select_index(&self, pattern: PatternType) -> IndexType {
let preferred = pattern.recommended_index();
if self.indexes.contains(&preferred) {
preferred
} else {
self.indexes.first().copied().unwrap_or(IndexType::SPO)
}
}
fn estimate_cost(&self, pattern: PatternType) -> f64 {
pattern.selectivity()
}
}
#[derive(Debug)]
pub struct LazyStoreStrategy {
primary: IndexType,
secondary: RwLock<Vec<IndexType>>,
materialized: RwLock<Vec<IndexType>>,
}
impl LazyStoreStrategy {
pub fn new() -> Self {
Self {
primary: IndexType::SPO,
secondary: RwLock::new(vec![
IndexType::POS,
IndexType::OSP,
IndexType::SOP,
IndexType::PSO,
IndexType::OPS,
]),
materialized: RwLock::new(vec![IndexType::SPO]),
}
}
pub fn is_materialized(&self, index: IndexType) -> bool {
strategy_read(&self.materialized).contains(&index)
}
pub fn mark_materialized(&self, index: IndexType) {
let mut mat = strategy_write(&self.materialized);
if !mat.contains(&index) {
mat.push(index);
}
}
}
impl Default for LazyStoreStrategy {
fn default() -> Self {
Self::new()
}
}
impl StoreStrategy for LazyStoreStrategy {
fn name(&self) -> &str {
"Lazy"
}
fn indexes(&self) -> &[IndexType] {
std::slice::from_ref(&self.primary)
}
fn index_on_add(&self) -> bool {
false }
fn select_index(&self, pattern: PatternType) -> IndexType {
let preferred = pattern.recommended_index();
if self.is_materialized(preferred) {
preferred
} else {
self.primary
}
}
fn estimate_cost(&self, pattern: PatternType) -> f64 {
let preferred = pattern.recommended_index();
if self.is_materialized(preferred) {
pattern.selectivity()
} else {
pattern.selectivity() * 10.0
}
}
}
#[derive(Debug, Clone)]
pub struct MinimalStoreStrategy {
index: IndexType,
}
impl MinimalStoreStrategy {
pub fn new() -> Self {
Self {
index: IndexType::SPO,
}
}
pub fn with_index(index: IndexType) -> Self {
Self { index }
}
}
impl Default for MinimalStoreStrategy {
fn default() -> Self {
Self::new()
}
}
impl StoreStrategy for MinimalStoreStrategy {
fn name(&self) -> &str {
"Minimal"
}
fn indexes(&self) -> &[IndexType] {
std::slice::from_ref(&self.index)
}
fn index_on_add(&self) -> bool {
true
}
fn select_index(&self, _pattern: PatternType) -> IndexType {
self.index }
fn estimate_cost(&self, pattern: PatternType) -> f64 {
let optimal = pattern.recommended_index();
if optimal == self.index {
pattern.selectivity()
} else {
pattern.selectivity() * 5.0
}
}
}
#[derive(Debug, Clone, Default)]
pub struct IndexStats {
pub entries: u64,
pub lookups: u64,
pub scans: u64,
pub inserts: u64,
pub cache_hits: u64,
pub cache_misses: u64,
}
impl IndexStats {
pub fn hit_rate(&self) -> f64 {
let total = self.cache_hits + self.cache_misses;
if total == 0 {
0.0
} else {
self.cache_hits as f64 / total as f64
}
}
}
pub struct TripleIndex {
index_type: IndexType,
data: RwLock<HashMap<(String, String), Vec<String>>>,
stats: RwLock<IndexStats>,
}
impl TripleIndex {
pub fn new(index_type: IndexType) -> Self {
Self {
index_type,
data: RwLock::new(HashMap::new()),
stats: RwLock::new(IndexStats::default()),
}
}
pub fn insert(&self, subject: &str, predicate: &str, object: &str) {
let (k1, k2, v) = self.order_triple(subject, predicate, object);
let key = (k1.to_string(), k2.to_string());
let mut data = strategy_write(&self.data);
data.entry(key).or_default().push(v.to_string());
let mut stats = strategy_write(&self.stats);
stats.inserts += 1;
stats.entries += 1;
}
pub fn lookup(&self, first: &str, second: Option<&str>) -> Vec<(String, String, String)> {
let data = strategy_read(&self.data);
let mut results = Vec::new();
let mut stats = strategy_write(&self.stats);
stats.lookups += 1;
for ((k1, k2), values) in data.iter() {
if k1 == first {
if let Some(s) = second {
if k2 == s {
for v in values {
results.push(self.restore_triple(k1, k2, v));
}
}
} else {
for v in values {
results.push(self.restore_triple(k1, k2, v));
}
}
}
}
results
}
pub fn scan(&self) -> Vec<(String, String, String)> {
let data = strategy_read(&self.data);
let mut results = Vec::new();
let mut stats = strategy_write(&self.stats);
stats.scans += 1;
for ((k1, k2), values) in data.iter() {
for v in values {
results.push(self.restore_triple(k1, k2, v));
}
}
results
}
pub fn stats(&self) -> IndexStats {
strategy_read(&self.stats).clone()
}
fn order_triple<'a>(&self, s: &'a str, p: &'a str, o: &'a str) -> (&'a str, &'a str, &'a str) {
let parts = [s, p, o];
let (i1, i2, i3) = self.index_type.key_order();
(parts[i1], parts[i2], parts[i3])
}
fn restore_triple(&self, k1: &str, k2: &str, v: &str) -> (String, String, String) {
let (i1, i2, i3) = self.index_type.key_order();
let mut result = ["", "", ""];
result[i1] = k1;
result[i2] = k2;
result[i3] = v;
(
result[0].to_string(),
result[1].to_string(),
result[2].to_string(),
)
}
}
pub struct TripleStore {
strategy: Arc<dyn StoreStrategy>,
indexes: RwLock<HashMap<IndexType, Arc<TripleIndex>>>,
}
impl TripleStore {
pub fn new(strategy: Arc<dyn StoreStrategy>) -> Self {
let mut indexes = HashMap::new();
for &idx_type in strategy.indexes() {
indexes.insert(idx_type, Arc::new(TripleIndex::new(idx_type)));
}
Self {
strategy,
indexes: RwLock::new(indexes),
}
}
pub fn eager() -> Self {
Self::new(Arc::new(EagerStoreStrategy::new()))
}
pub fn lazy() -> Self {
Self::new(Arc::new(LazyStoreStrategy::new()))
}
pub fn minimal() -> Self {
Self::new(Arc::new(MinimalStoreStrategy::new()))
}
pub fn add(&self, subject: &str, predicate: &str, object: &str) {
if self.strategy.index_on_add() {
let indexes = strategy_read(&self.indexes);
for index in indexes.values() {
index.insert(subject, predicate, object);
}
} else {
let indexes = strategy_read(&self.indexes);
if let Some(primary) = indexes.values().next() {
primary.insert(subject, predicate, object);
}
}
}
pub fn query(
&self,
subject: Option<&str>,
predicate: Option<&str>,
object: Option<&str>,
) -> Vec<(String, String, String)> {
let pattern =
PatternType::from_bounds(subject.is_some(), predicate.is_some(), object.is_some());
let index_type = self.strategy.select_index(pattern);
let indexes = strategy_read(&self.indexes);
if let Some(index) = indexes.get(&index_type) {
match pattern {
PatternType::SubPreObj => {
let s = subject.expect("invariant: PatternType::SubPreObj => subject set");
let p = predicate.expect("invariant: PatternType::SubPreObj => predicate set");
let o = object.expect("invariant: PatternType::SubPreObj => object set");
index
.lookup(s, Some(p))
.into_iter()
.filter(|(_, _, triple_o)| triple_o == o)
.collect()
}
PatternType::SubPre => {
let s = subject.expect("invariant: PatternType::SubPre => subject set");
let p = predicate.expect("invariant: PatternType::SubPre => predicate set");
index.lookup(s, Some(p))
}
PatternType::Sub => {
let s = subject.expect("invariant: PatternType::Sub => subject set");
index.lookup(s, None)
}
_ => {
index
.scan()
.into_iter()
.filter(|(s, p, o)| {
subject.is_none_or(|sub| s == sub)
&& predicate.is_none_or(|pre| p == pre)
&& object.is_none_or(|obj| o == obj)
})
.collect()
}
}
} else {
Vec::new()
}
}
pub fn strategy_name(&self) -> &str {
self.strategy.name()
}
pub fn index_stats(&self) -> HashMap<IndexType, IndexStats> {
let indexes = strategy_read(&self.indexes);
indexes.iter().map(|(&k, v)| (k, v.stats())).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pattern_type() {
assert_eq!(
PatternType::from_bounds(true, true, true),
PatternType::SubPreObj
);
assert_eq!(
PatternType::from_bounds(true, false, false),
PatternType::Sub
);
assert_eq!(
PatternType::from_bounds(false, false, false),
PatternType::Any
);
}
#[test]
fn test_eager_strategy() {
let strategy = EagerStoreStrategy::new();
assert_eq!(strategy.name(), "Eager");
assert!(strategy.index_on_add());
assert_eq!(strategy.indexes().len(), 3);
}
#[test]
fn test_minimal_strategy() {
let strategy = MinimalStoreStrategy::new();
assert_eq!(strategy.name(), "Minimal");
assert_eq!(strategy.indexes().len(), 1);
}
#[test]
fn test_triple_index() {
let index = TripleIndex::new(IndexType::SPO);
index.insert("alice", "knows", "bob");
index.insert("alice", "knows", "charlie");
index.insert("bob", "knows", "alice");
let results = index.lookup("alice", None);
assert_eq!(results.len(), 2);
let results = index.lookup("alice", Some("knows"));
assert_eq!(results.len(), 2);
}
#[test]
fn test_triple_store_eager() {
let store = TripleStore::eager();
store.add("alice", "knows", "bob");
store.add("alice", "likes", "coffee");
store.add("bob", "knows", "charlie");
let results = store.query(Some("alice"), None, None);
assert_eq!(results.len(), 2);
let results = store.query(Some("alice"), Some("knows"), None);
assert_eq!(results.len(), 1);
let results = store.query(None, Some("knows"), None);
assert_eq!(results.len(), 2);
}
#[test]
fn test_triple_store_minimal() {
let store = TripleStore::minimal();
store.add("alice", "knows", "bob");
store.add("bob", "knows", "charlie");
let results = store.query(Some("alice"), None, None);
assert_eq!(results.len(), 1);
}
#[test]
fn test_index_type_ordering() {
let spo = IndexType::SPO;
assert_eq!(spo.key_order(), (0, 1, 2));
let pos = IndexType::POS;
assert_eq!(pos.key_order(), (1, 2, 0));
}
#[test]
fn test_pattern_selectivity() {
assert!(PatternType::SubPreObj.selectivity() < PatternType::Sub.selectivity());
assert!(PatternType::Sub.selectivity() < PatternType::Any.selectivity());
}
#[test]
fn test_lazy_strategy_recovers_after_materialized_lock_poisoning() {
let strategy = Arc::new(LazyStoreStrategy::new());
let poison_target = Arc::clone(&strategy);
let _ = std::thread::spawn(move || {
let _guard = poison_target
.materialized
.write()
.expect("materialized lock should be acquired");
panic!("poison materialized lock");
})
.join();
strategy.mark_materialized(IndexType::POS);
assert!(strategy.is_materialized(IndexType::POS));
}
#[test]
fn test_triple_store_recovers_after_indexes_lock_poisoning() {
let store = Arc::new(TripleStore::eager());
let poison_target = Arc::clone(&store);
let _ = std::thread::spawn(move || {
let _guard = poison_target
.indexes
.write()
.expect("indexes lock should be acquired");
panic!("poison indexes lock");
})
.join();
store.add("alice", "knows", "bob");
let results = store.query(Some("alice"), Some("knows"), Some("bob"));
assert_eq!(results.len(), 1);
assert!(!store.index_stats().is_empty());
}
}