use super::{LogOp, LogRecord};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransactionState {
Committed,
Aborted,
Incomplete,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Transaction {
pub transaction_id: u32,
pub records: Vec<usize>,
pub lsns: Vec<u64>,
pub operations: Vec<super::FileOperation>,
pub state: TransactionState,
}
#[must_use]
pub fn reconstruct_transactions(records: &[LogRecord]) -> Vec<Transaction> {
use std::collections::HashMap;
let mut slots: HashMap<u32, Vec<usize>> = HashMap::new();
for (idx, rec) in records.iter().enumerate() {
slots.entry(rec.transaction_id).or_default().push(idx);
}
let mut txns: Vec<Transaction> = Vec::new();
for (transaction_id, mut indices) in slots {
indices.sort_by_key(|&i| records[i].this_lsn);
let mut run: Vec<usize> = Vec::new();
let mut compensated = false;
for &i in &indices {
run.push(i);
match records[i].redo_op {
LogOp::CommitTransaction | LogOp::ForgetTransaction => {
txns.push(build_transaction(
transaction_id,
std::mem::take(&mut run),
records,
TransactionState::Committed,
));
compensated = false;
}
LogOp::CompensationLogRecord => compensated = true,
_ => {}
}
}
if !run.is_empty() {
let state = if compensated {
TransactionState::Aborted
} else {
TransactionState::Incomplete
};
txns.push(build_transaction(transaction_id, run, records, state));
}
}
txns.sort_by_key(|t| t.lsns.first().copied().unwrap_or(u64::MAX));
txns
}
fn build_transaction(
transaction_id: u32,
records_idx: Vec<usize>,
all: &[LogRecord],
state: TransactionState,
) -> Transaction {
let mut lsns = Vec::with_capacity(records_idx.len());
let mut operations = Vec::with_capacity(records_idx.len());
for &i in &records_idx {
lsns.push(all[i].this_lsn);
operations.push(super::classify(all[i].redo_op, all[i].undo_op));
}
Transaction {
transaction_id,
records: records_idx,
lsns,
operations,
state,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::logfile::FileOperation;
fn rec(this_lsn: u64, slot: u32, redo: LogOp, undo: LogOp) -> LogRecord {
LogRecord {
page_offset: 0,
this_lsn,
client_previous_lsn: 0,
client_undo_next_lsn: 0,
record_type: 1,
transaction_id: slot,
redo_op: redo,
undo_op: undo,
target_attribute: 0,
mft_cluster_index: 0,
target_vcn: 0,
}
}
#[test]
fn reused_slot_splits_into_two_transactions() {
let recs = vec![
rec(10, 0x40, LogOp::InitializeFileRecordSegment, LogOp::Noop),
rec(
11,
0x40,
LogOp::ForgetTransaction,
LogOp::CompensationLogRecord,
),
rec(12, 0x40, LogOp::CreateAttribute, LogOp::DeleteAttribute),
rec(
13,
0x40,
LogOp::ForgetTransaction,
LogOp::CompensationLogRecord,
),
];
let txns = reconstruct_transactions(&recs);
assert_eq!(txns.len(), 2, "one slot, two terminals => two transactions");
assert_eq!(txns[0].lsns, vec![10, 11]);
assert_eq!(txns[0].state, TransactionState::Committed);
assert_eq!(txns[1].lsns, vec![12, 13]);
assert_eq!(txns[1].state, TransactionState::Committed);
assert_eq!(txns[0].transaction_id, 0x40);
assert_eq!(txns[1].transaction_id, 0x40);
}
#[test]
fn commit_is_a_terminal() {
let recs = vec![
rec(1, 5, LogOp::InitializeFileRecordSegment, LogOp::Noop),
rec(2, 5, LogOp::CommitTransaction, LogOp::Noop),
rec(3, 5, LogOp::CreateAttribute, LogOp::DeleteAttribute),
rec(4, 5, LogOp::CommitTransaction, LogOp::Noop),
];
let txns = reconstruct_transactions(&recs);
assert_eq!(txns.len(), 2);
assert!(txns.iter().all(|t| t.state == TransactionState::Committed));
}
#[test]
fn forget_with_compensation_undo_is_committed_not_aborted() {
let recs = vec![rec(
1,
7,
LogOp::ForgetTransaction,
LogOp::CompensationLogRecord,
)];
let txns = reconstruct_transactions(&recs);
assert_eq!(txns.len(), 1);
assert_eq!(txns[0].state, TransactionState::Committed);
}
#[test]
fn compensation_redo_without_terminal_is_aborted() {
let recs = vec![
rec(1, 9, LogOp::InitializeFileRecordSegment, LogOp::Noop),
rec(2, 9, LogOp::CompensationLogRecord, LogOp::Noop),
];
let txns = reconstruct_transactions(&recs);
assert_eq!(txns.len(), 1);
assert_eq!(txns[0].state, TransactionState::Aborted);
}
#[test]
fn terminal_wins_over_compensation_and_flag_resets() {
let recs = vec![
rec(1, 3, LogOp::InitializeFileRecordSegment, LogOp::Noop),
rec(2, 3, LogOp::CompensationLogRecord, LogOp::Noop),
rec(3, 3, LogOp::CommitTransaction, LogOp::Noop),
rec(4, 3, LogOp::CreateAttribute, LogOp::DeleteAttribute),
];
let txns = reconstruct_transactions(&recs);
assert_eq!(txns.len(), 2);
assert_eq!(txns[0].lsns, vec![1, 2, 3]);
assert_eq!(txns[0].state, TransactionState::Committed);
assert_eq!(txns[1].lsns, vec![4]);
assert_eq!(txns[1].state, TransactionState::Incomplete);
}
#[test]
fn trailing_run_without_terminal_is_incomplete() {
let recs = vec![
rec(1, 1, LogOp::InitializeFileRecordSegment, LogOp::Noop),
rec(2, 1, LogOp::ForgetTransaction, LogOp::CompensationLogRecord),
rec(3, 1, LogOp::CreateAttribute, LogOp::DeleteAttribute),
];
let txns = reconstruct_transactions(&recs);
assert_eq!(txns.len(), 2);
assert_eq!(txns[1].lsns, vec![3]);
assert_eq!(txns[1].state, TransactionState::Incomplete);
}
#[test]
fn separates_interleaved_slots() {
let recs = vec![
rec(21, 1, LogOp::InitializeFileRecordSegment, LogOp::Noop),
rec(22, 1, LogOp::CreateAttribute, LogOp::DeleteAttribute),
rec(23, 2, LogOp::InitializeFileRecordSegment, LogOp::Noop),
rec(24, 1, LogOp::CommitTransaction, LogOp::Noop),
rec(25, 2, LogOp::CreateAttribute, LogOp::DeleteAttribute),
rec(26, 2, LogOp::CommitTransaction, LogOp::Noop),
];
let txns = reconstruct_transactions(&recs);
assert_eq!(txns.len(), 2);
assert_eq!(txns[0].transaction_id, 1);
assert_eq!(txns[0].lsns, vec![21, 22, 24]);
assert_eq!(txns[1].transaction_id, 2);
assert_eq!(txns[1].lsns, vec![23, 25, 26]);
}
#[test]
fn orders_records_by_lsn_within_slot() {
let recs = vec![
rec(30, 5, LogOp::CommitTransaction, LogOp::Noop),
rec(28, 5, LogOp::InitializeFileRecordSegment, LogOp::Noop),
rec(29, 5, LogOp::CreateAttribute, LogOp::DeleteAttribute),
];
let txns = reconstruct_transactions(&recs);
assert_eq!(txns.len(), 1);
assert_eq!(txns[0].lsns, vec![28, 29, 30]);
assert_eq!(txns[0].records, vec![1, 2, 0]);
}
#[test]
fn assigns_every_record_exactly_once() {
let recs = vec![
rec(1, 1, LogOp::InitializeFileRecordSegment, LogOp::Noop),
rec(2, 2, LogOp::InitializeFileRecordSegment, LogOp::Noop),
rec(3, 1, LogOp::CommitTransaction, LogOp::Noop),
rec(4, 3, LogOp::InitializeFileRecordSegment, LogOp::Noop),
];
let txns = reconstruct_transactions(&recs);
let total: usize = txns.iter().map(|t| t.records.len()).sum();
assert_eq!(total, recs.len(), "no record dropped");
let mut seen: Vec<usize> = txns
.iter()
.flat_map(|t| t.records.iter().copied())
.collect();
seen.sort_unstable();
assert_eq!(seen, vec![0, 1, 2, 3], "each index assigned exactly once");
}
#[test]
fn operations_parallel_records() {
let recs = vec![
rec(1, 9, LogOp::InitializeFileRecordSegment, LogOp::Noop),
rec(2, 9, LogOp::CommitTransaction, LogOp::Noop),
];
let txns = reconstruct_transactions(&recs);
assert_eq!(
txns[0].operations,
vec![FileOperation::Create, FileOperation::TransactionControl]
);
}
#[test]
fn unknown_opcode_record_is_retained() {
let recs = vec![
rec(1, 6, LogOp::Unknown(0x40), LogOp::Unknown(0x41)),
rec(2, 6, LogOp::CommitTransaction, LogOp::Noop),
];
let txns = reconstruct_transactions(&recs);
assert_eq!(txns.len(), 1);
assert_eq!(txns[0].records.len(), 2);
assert_eq!(txns[0].operations[0], FileOperation::Unknown(0x40, 0x41));
assert_eq!(txns[0].state, TransactionState::Committed);
}
#[test]
fn prepare_and_end_top_level_are_not_terminals() {
let recs = vec![
rec(1, 4, LogOp::PrepareTransaction, LogOp::Noop),
rec(2, 4, LogOp::EndTopLevelAction, LogOp::Noop),
];
let txns = reconstruct_transactions(&recs);
assert_eq!(txns.len(), 1, "no terminal => single trailing run");
assert_eq!(txns[0].state, TransactionState::Incomplete);
assert_eq!(txns[0].records.len(), 2);
}
#[test]
fn empty_input_yields_no_transactions() {
assert!(reconstruct_transactions(&[]).is_empty());
}
}