use crate::dictionary::Term;
use crate::error::{Result, TdbError};
use crate::index::{Quad, QuadIndexes, Triple};
use crate::store::store_impl::TdbStore;
use crate::transaction::wal::{LogRecord, TxnId};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum StoreOp {
InsertTriple {
subject: Term,
predicate: Term,
object: Term,
},
DeleteTriple {
subject: Term,
predicate: Term,
object: Term,
},
InsertQuad {
graph: Term,
subject: Term,
predicate: Term,
object: Term,
},
DeleteQuad {
graph: Term,
subject: Term,
predicate: Term,
object: Term,
},
}
pub(crate) fn encode_store_op(op: &StoreOp) -> Result<Vec<u8>> {
oxicode::serde::encode_to_vec(op, oxicode::config::standard())
.map_err(|e| TdbError::Serialization(e.to_string()))
}
fn decode_store_op(bytes: &[u8]) -> Result<StoreOp> {
oxicode::serde::decode_from_slice(bytes, oxicode::config::standard())
.map(|(op, _)| op)
.map_err(|e| TdbError::Deserialization(e.to_string()))
}
impl TdbStore {
fn next_wal_txn(&mut self) -> TxnId {
self.wal_txn_counter += 1;
TxnId::new(self.wal_txn_counter)
}
pub(crate) fn wal_log_op(&mut self, op: StoreOp) -> Result<()> {
if !self.config.enable_wal {
return Ok(());
}
let txn_id = self.next_wal_txn();
let payload = encode_store_op(&op)?;
let wal = self.txn_manager.wal();
wal.append(LogRecord::Begin { txn_id })?;
wal.append(LogRecord::DataOp { txn_id, payload })?;
wal.append(LogRecord::Commit { txn_id })?;
if self.config.wal_sync_on_commit {
wal.flush()?;
}
let threshold = self.config.wal_checkpoint_op_threshold;
if threshold != 0 {
let n = self
.wal_ops_since_checkpoint
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
+ 1;
if n >= threshold {
self.sync()?;
}
}
Ok(())
}
pub(crate) fn wal_log_batch(&mut self, ops: &[StoreOp]) -> Result<()> {
if !self.config.enable_wal || ops.is_empty() {
return Ok(());
}
let txn_id = self.next_wal_txn();
let wal = self.txn_manager.wal();
wal.append(LogRecord::Begin { txn_id })?;
for op in ops {
let payload = encode_store_op(op)?;
wal.append(LogRecord::DataOp { txn_id, payload })?;
}
wal.append(LogRecord::Commit { txn_id })?;
Ok(())
}
pub(crate) fn recover_from_wal(&mut self) -> Result<usize> {
let entries = self.txn_manager.wal().all_entries();
if entries.is_empty() {
return Ok(0);
}
let committed: HashSet<TxnId> = entries
.iter()
.filter_map(|e| match &e.record {
LogRecord::Commit { txn_id } => Some(*txn_id),
_ => None,
})
.collect();
let mut max_txn = self.wal_txn_counter;
let mut applied = 0usize;
for entry in &entries {
match &entry.record {
LogRecord::Begin { txn_id }
| LogRecord::Commit { txn_id }
| LogRecord::Abort { txn_id } => {
max_txn = max_txn.max(txn_id.as_u64());
}
LogRecord::DataOp { txn_id, payload } => {
max_txn = max_txn.max(txn_id.as_u64());
if committed.contains(txn_id) {
let op = decode_store_op(payload)?;
self.apply_store_op(op)?;
applied += 1;
}
}
_ => {}
}
}
self.wal_txn_counter = max_txn;
Ok(applied)
}
fn apply_store_op(&mut self, op: StoreOp) -> Result<()> {
match op {
StoreOp::InsertTriple {
subject,
predicate,
object,
} => self.replay_insert_triple(&subject, &predicate, &object),
StoreOp::DeleteTriple {
subject,
predicate,
object,
} => self.replay_delete_triple(&subject, &predicate, &object),
StoreOp::InsertQuad {
graph,
subject,
predicate,
object,
} => self.replay_insert_named_quad(&graph, &subject, &predicate, &object),
StoreOp::DeleteQuad {
graph,
subject,
predicate,
object,
} => self.replay_delete_named_quad(&graph, &subject, &predicate, &object),
}
}
fn replay_insert_triple(&mut self, s: &Term, p: &Term, o: &Term) -> Result<()> {
let s_id = self.dictionary.encode(s)?;
let p_id = self.dictionary.encode(p)?;
let o_id = self.dictionary.encode(o)?;
let triple = Triple::new(s_id, p_id, o_id);
let is_new = self.indexes.insert(triple)?;
if let Some(ref mut bloom) = self.bloom_filter {
bloom.insert(&triple);
}
if is_new {
self.triple_count += 1;
}
Ok(())
}
fn replay_delete_triple(&mut self, s: &Term, p: &Term, o: &Term) -> Result<()> {
let (s_id, p_id, o_id) = match (
self.dictionary.lookup(s)?,
self.dictionary.lookup(p)?,
self.dictionary.lookup(o)?,
) {
(Some(s_id), Some(p_id), Some(o_id)) => (s_id, p_id, o_id),
_ => return Ok(()),
};
let triple = Triple::new(s_id, p_id, o_id);
if self.indexes.delete(&triple)? {
self.triple_count = self.triple_count.saturating_sub(1);
}
Ok(())
}
fn replay_insert_named_quad(
&mut self,
graph: &Term,
s: &Term,
p: &Term,
o: &Term,
) -> Result<()> {
let g_id = self.dictionary.encode(graph)?;
let s_id = self.dictionary.encode(s)?;
let p_id = self.dictionary.encode(p)?;
let o_id = self.dictionary.encode(o)?;
if self.quad_indexes.is_none() {
self.quad_indexes = Some(QuadIndexes::new(self.buffer_pool.clone()));
self.quads_writable = true;
}
let quad_indexes = self.quad_indexes.as_mut().ok_or_else(|| {
TdbError::Other("quad indexes unexpectedly absent during WAL replay".to_string())
})?;
let quad = Quad::new(g_id, s_id, p_id, o_id);
if quad_indexes.insert(quad)? {
self.quad_count += 1;
}
Ok(())
}
fn replay_delete_named_quad(
&mut self,
graph: &Term,
s: &Term,
p: &Term,
o: &Term,
) -> Result<()> {
let (g_id, s_id, p_id, o_id) = match (
self.dictionary.lookup(graph)?,
self.dictionary.lookup(s)?,
self.dictionary.lookup(p)?,
self.dictionary.lookup(o)?,
) {
(Some(g_id), Some(s_id), Some(p_id), Some(o_id)) => (g_id, s_id, p_id, o_id),
_ => return Ok(()),
};
if let Some(quad_indexes) = self.quad_indexes.as_mut() {
let quad = Quad::new(g_id, s_id, p_id, o_id);
if quad_indexes.delete(quad)? {
self.quad_count = self.quad_count.saturating_sub(1);
}
}
Ok(())
}
}