use std::sync::atomic::{AtomicU64, Ordering};
use linkrs_core::types::{Timestamp, TransactionId};
use linkrs_core::wal::{OutboxIntent, WalOpType};
use crate::types::MutationEntityKey;
use crate::undo_log::UndoLogEntry;
use crate::wal::TransactionWalEntry;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum MutationResource {
Vertex,
Edge,
VertexProperty,
EdgeProperty,
Schema,
Index,
Sequence,
Macro,
TypeAlias,
SyncIntent,
#[default]
Unknown,
}
impl std::fmt::Display for MutationResource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
MutationResource::Vertex => "vertex",
MutationResource::Edge => "edge",
MutationResource::VertexProperty => "vertex_property",
MutationResource::EdgeProperty => "edge_property",
MutationResource::Schema => "schema",
MutationResource::Index => "index",
MutationResource::Sequence => "sequence",
MutationResource::Macro => "macro",
MutationResource::TypeAlias => "type_alias",
MutationResource::SyncIntent => "sync_intent",
MutationResource::Unknown => "unknown",
};
write!(f, "{}", s)
}
}
impl MutationResource {
pub fn from_wal_op(op: WalOpType) -> Self {
match op {
WalOpType::InsertVertex | WalOpType::DeleteVertex => MutationResource::Vertex,
WalOpType::InsertEdge | WalOpType::DeleteEdge => MutationResource::Edge,
WalOpType::UpdateVertexProp
| WalOpType::AddVertexProp
| WalOpType::DeleteVertexProp
| WalOpType::RenameVertexProp => MutationResource::VertexProperty,
WalOpType::UpdateEdgeProp
| WalOpType::AddEdgeProp
| WalOpType::DeleteEdgeProp
| WalOpType::RenameEdgeProp => MutationResource::EdgeProperty,
WalOpType::CreateVertexType
| WalOpType::DeleteVertexType
| WalOpType::CreateEdgeType
| WalOpType::DeleteEdgeType
| WalOpType::CreateSpace
| WalOpType::DropSpace
| WalOpType::ClearSpace => MutationResource::Schema,
WalOpType::CreateTagIndex
| WalOpType::DropTagIndex
| WalOpType::CreateEdgeIndex
| WalOpType::DropEdgeIndex => MutationResource::Index,
WalOpType::UpdateSequence => MutationResource::Sequence,
WalOpType::CreateMacro | WalOpType::DropMacro => MutationResource::Macro,
WalOpType::CreateTypeAlias | WalOpType::DropTypeAlias => MutationResource::TypeAlias,
WalOpType::OutboxIntent => MutationResource::SyncIntent,
_ => MutationResource::Unknown,
}
}
pub fn from_modified_table(table: Option<&str>) -> Self {
match table {
Some("vertex") => MutationResource::Vertex,
Some("edge") => MutationResource::Edge,
Some("schema") => MutationResource::Schema,
Some("index") => MutationResource::Index,
Some("sequence") => MutationResource::Sequence,
Some("macro") => MutationResource::Macro,
Some("type_alias") => MutationResource::TypeAlias,
Some("sync") => MutationResource::SyncIntent,
_ => MutationResource::Unknown,
}
}
}
#[derive(Debug, Clone)]
pub struct TransactionMutationRecord {
pub sequence: u64,
pub transaction_id: TransactionId,
pub entity_keys: Vec<MutationEntityKey>,
pub resource: MutationResource,
pub undo: Option<UndoLogEntry>,
pub redo: Option<TransactionWalEntry>,
pub index_intents: Vec<OutboxIntent>,
pub modified_table: Option<String>,
pub write_timestamp: Timestamp,
pub commit_timestamp: Option<Timestamp>,
}
impl TransactionMutationRecord {
pub fn has_redo(&self) -> bool {
self.redo.is_some()
}
pub fn has_undo(&self) -> bool {
self.undo.is_some()
}
}
#[derive(Debug, Default)]
pub struct MutationJournal {
records: Vec<TransactionMutationRecord>,
next_sequence: u64,
}
impl MutationJournal {
pub fn new() -> Self {
Self {
records: Vec::new(),
next_sequence: 0,
}
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn next_sequence(&self) -> u64 {
self.next_sequence
}
pub fn push(&mut self, mut record: TransactionMutationRecord) -> u64 {
let seq = self.next_sequence;
record.sequence = seq;
self.next_sequence += 1;
self.records.push(record);
seq
}
pub fn records(&self) -> &[TransactionMutationRecord] {
&self.records
}
pub fn truncate(&mut self, len: usize) {
if len < self.records.len() {
let removed = self.records.len() - len;
self.records.truncate(len);
self.next_sequence = self.records.len() as u64;
let _ = removed;
}
}
pub fn range(&self, start: usize, end: usize) -> &[TransactionMutationRecord] {
let end = end.min(self.records.len());
let start = start.min(end);
&self.records[start..end]
}
pub fn check_invariants(&self) -> Result<(), String> {
for (idx, rec) in self.records.iter().enumerate() {
if rec.sequence != idx as u64 {
return Err(format!(
"journal sequence gap: expected {}, got {} at index {}",
idx, rec.sequence, idx
));
}
if let Some(ref redo) = rec.redo {
if let Err(e) = WalOpType::try_from(redo.op_type as u8) {
return Err(format!(
"journal record {} has invalid WAL op type: {}",
rec.sequence, e
));
}
}
}
if self.next_sequence != self.records.len() as u64 {
return Err(format!(
"journal next_sequence {} != len {}",
self.next_sequence,
self.records.len()
));
}
Ok(())
}
pub fn total_redo_entries(&self) -> usize {
self.records.iter().filter(|r| r.redo.is_some()).count()
}
pub fn total_intents(&self) -> usize {
self.records.iter().map(|r| r.index_intents.len()).sum()
}
pub fn redo_entries(&self) -> Vec<TransactionWalEntry> {
self.records.iter().filter_map(|r| r.redo.clone()).collect()
}
pub fn wal_intents(&self) -> Vec<OutboxIntent> {
self.records
.iter()
.flat_map(|r| r.index_intents.iter().cloned())
.collect()
}
pub fn publish_commit_timestamp(&mut self, commit_ts: Timestamp) {
for rec in &mut self.records {
if rec.commit_timestamp.is_none() {
rec.commit_timestamp = Some(commit_ts);
}
}
}
pub fn estimated_bytes(&self) -> usize {
self.records
.iter()
.map(|r| {
r.entity_keys.len() * std::mem::size_of::<MutationEntityKey>()
+ r.redo.as_ref().map(|e| e.payload.len()).unwrap_or(0)
+ r.index_intents
.iter()
.map(|i| i.mutation.document_or_vector.len())
.sum::<usize>()
})
.sum()
}
}
#[derive(Debug, Clone)]
pub struct MutationJournalPosition {
pub journal_len: usize,
pub next_sequence: u64,
pub undo_log_index: usize,
pub modified_tables: Vec<String>,
pub write_set_snapshot: crate::types::WriteSet,
pub read_set_snapshot: crate::types::WriteSet,
pub sync_sequence: u64,
pub savepoint_sequence: u64,
}
impl MutationJournalPosition {
pub fn validate_against(&self, journal: &MutationJournal) -> Result<(), String> {
if self.journal_len > journal.len() {
return Err(format!(
"journal truncated beyond savepoint: savepoint len {} > current len {}",
self.journal_len,
journal.len()
));
}
if self.next_sequence != self.journal_len as u64 {
return Err(format!(
"savepoint next_sequence {} != journal_len {}",
self.next_sequence, self.journal_len
));
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct MutationSequenceGenerator {
next: AtomicU64,
}
impl MutationSequenceGenerator {
pub fn new() -> Self {
Self {
next: AtomicU64::new(0),
}
}
pub fn next(&self) -> u64 {
self.next.fetch_add(1, Ordering::Relaxed)
}
pub fn current(&self) -> u64 {
self.next.load(Ordering::Relaxed)
}
pub fn reset_to(&self, val: u64) {
self.next.store(val, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
use linkrs_core::types::VertexId;
#[test]
fn journal_assigns_contiguous_sequences() {
let mut j = MutationJournal::new();
for i in 0..5 {
let rec = TransactionMutationRecord {
sequence: 0,
transaction_id: TransactionId(1),
entity_keys: vec![MutationEntityKey::Vertex(
VertexId::try_from_int64(i).expect("test vertex id"),
)],
resource: MutationResource::Vertex,
undo: None,
redo: None,
index_intents: vec![],
modified_table: None,
write_timestamp: 1,
commit_timestamp: None,
};
let seq = j.push(rec);
assert_eq!(seq, i as u64);
}
assert!(j.check_invariants().is_ok());
j.truncate(3);
assert_eq!(j.len(), 3);
assert_eq!(j.next_sequence(), 3);
assert!(j.check_invariants().is_ok());
}
#[test]
fn journal_invariant_detects_gap() {
let mut j = MutationJournal::new();
j.records.push(TransactionMutationRecord {
sequence: 99,
transaction_id: TransactionId(1),
entity_keys: vec![],
resource: MutationResource::Unknown,
undo: None,
redo: None,
index_intents: vec![],
modified_table: None,
write_timestamp: 0,
commit_timestamp: None,
});
j.next_sequence = 1;
assert!(j.check_invariants().is_err());
}
#[test]
fn resource_from_wal_op() {
assert_eq!(
MutationResource::from_wal_op(WalOpType::InsertVertex),
MutationResource::Vertex
);
assert_eq!(
MutationResource::from_wal_op(WalOpType::OutboxIntent),
MutationResource::SyncIntent
);
}
}