wlk 0.1.0

File-centric, event-sourced version control system with implicit branching
Documentation
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::path::PathBuf;

/// Operations that can be logged in the WAL
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum WalOperation {
    /// Write a shadow file
    WriteShadowFile { path: PathBuf, content: String },

    /// Write a metalog file
    WriteMetaLog { path: PathBuf, content: String },

    /// Delete a file
    DeleteFile { path: PathBuf },

    /// Rename a file
    RenameFile { from: PathBuf, to: PathBuf },

    /// Create a directory
    CreateDirectory { path: PathBuf },
}

/// A single entry in the WAL
#[derive(Debug, Serialize, Deserialize)]
pub struct WalEntry {
    /// The operation to perform
    pub operation: WalOperation,

    /// Sequence number for ordering
    pub sequence: u64,
}

/// Write-Ahead Log for atomic operations
pub struct WriteAheadLog {
    /// Path to the WAL file
    path: PathBuf,

    /// Current sequence number
    sequence: u64,

    /// Whether we're currently in a transaction
    in_transaction: bool,
}

impl WriteAheadLog {
    /// Create or open a WAL
    pub fn new(wlk_root: &Path) -> Result<Self> {
        let path = wlk_root.join("wal");
        let sequence = if path.exists() {
            Self::read_last_sequence(&path)?
        } else {
            0
        };

        Ok(WriteAheadLog {
            path,
            sequence,
            in_transaction: false,
        })
    }

    /// Begin a transaction
    pub fn begin(&mut self) -> Result<()> {
        if self.in_transaction {
            return Err(anyhow::anyhow!("Transaction already in progress"));
        }
        self.in_transaction = true;
        Ok(())
    }

    /// Append an operation to the WAL
    pub fn append(&mut self, operation: WalOperation) -> Result<()> {
        if !self.in_transaction {
            return Err(anyhow::anyhow!("No transaction in progress"));
        }

        self.sequence += 1;
        let entry = WalEntry {
            operation,
            sequence: self.sequence,
        };

        let mut file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&self.path)
            .context("Failed to open WAL file")?;

        let json = serde_json::to_string(&entry).context("Failed to serialize WAL entry")?;
        writeln!(file, "{}", json).context("Failed to write to WAL")?;

        // Ensure data is written to disk
        file.sync_all().context("Failed to sync WAL to disk")?;

        Ok(())
    }

    /// Commit the transaction by executing all operations and clearing the WAL
    pub fn commit(&mut self) -> Result<()> {
        if !self.in_transaction {
            return Err(anyhow::anyhow!("No transaction in progress"));
        }

        let operations = self.read_all().context("Failed to read WAL entries")?;

        // Execute all operations
        for entry in operations {
            self.execute_operation(entry.operation).context(format!(
                "Failed to execute WAL operation {}",
                entry.sequence
            ))?;
        }

        // Clear WAL after successful execution
        std::fs::remove_file(&self.path).context("Failed to remove WAL file")?;
        self.sequence = 0;
        self.in_transaction = false;

        Ok(())
    }

    /// Rollback the transaction without executing operations
    pub fn rollback(&mut self) -> Result<()> {
        if !self.in_transaction {
            return Err(anyhow::anyhow!("No transaction in progress"));
        }

        // Just clear the WAL
        if self.path.exists() {
            std::fs::remove_file(&self.path).context("Failed to remove WAL file")?;
        }
        self.sequence = 0;
        self.in_transaction = false;

        Ok(())
    }

    /// Recover from a crash by executing any pending operations
    pub fn recover(&mut self) -> Result<()> {
        if !self.path.exists() {
            return Ok(());
        }

        let operations = self
            .read_all()
            .context("Failed to read WAL entries for recovery")?;

        // Execute all pending operations
        for entry in operations {
            // Ignore errors during recovery - operation may have partially completed
            let _ = self.execute_operation(entry.operation);
        }

        // Clear WAL after recovery
        std::fs::remove_file(&self.path).context("Failed to remove WAL file after recovery")?;
        self.sequence = 0;

        Ok(())
    }

    /// Execute a single WAL operation
    fn execute_operation(&self, op: WalOperation) -> Result<()> {
        match op {
            WalOperation::WriteShadowFile { path, content } => {
                // Ensure parent directory exists
                if let Some(parent) = path.parent() {
                    std::fs::create_dir_all(parent).context("Failed to create parent directory")?;
                }
                std::fs::write(&path, content)
                    .context(format!("Failed to write shadow file to {:?}", path))?;
            }

            WalOperation::WriteMetaLog { path, content } => {
                if let Some(parent) = path.parent() {
                    std::fs::create_dir_all(parent).context("Failed to create parent directory")?;
                }
                std::fs::write(&path, content)
                    .context(format!("Failed to write metalog to {:?}", path))?;
            }

            WalOperation::DeleteFile { path } => {
                if path.exists() {
                    std::fs::remove_file(&path)
                        .context(format!("Failed to delete file {:?}", path))?;
                }
            }

            WalOperation::RenameFile { from, to } => {
                if from.exists() {
                    if let Some(parent) = to.parent() {
                        std::fs::create_dir_all(parent)
                            .context("Failed to create destination directory")?;
                    }
                    std::fs::rename(&from, &to)
                        .context(format!("Failed to rename {:?} to {:?}", from, to))?;
                }
            }

            WalOperation::CreateDirectory { path } => {
                std::fs::create_dir_all(&path)
                    .context(format!("Failed to create directory {:?}", path))?;
            }
        }
        Ok(())
    }

    /// Read all entries from the WAL
    fn read_all(&self) -> Result<Vec<WalEntry>> {
        if !self.path.exists() {
            return Ok(vec![]);
        }

        let file = File::open(&self.path).context("Failed to open WAL file")?;
        let reader = BufReader::new(file);
        let mut entries = vec![];

        for line in reader.lines() {
            let line = line.context("Failed to read line from WAL")?;
            let entry: WalEntry =
                serde_json::from_str(&line).context("Failed to deserialize WAL entry")?;
            entries.push(entry);
        }

        Ok(entries)
    }

    /// Read the last sequence number from the WAL
    fn read_last_sequence(path: &PathBuf) -> Result<u64> {
        let entries = Self::read_all_static(path)?;
        Ok(entries.last().map(|e| e.sequence).unwrap_or(0))
    }

    fn read_all_static(path: &PathBuf) -> Result<Vec<WalEntry>> {
        if !path.exists() {
            return Ok(vec![]);
        }

        let file = File::open(path).context("Failed to open WAL file")?;
        let reader = BufReader::new(file);
        let mut entries = vec![];

        for line in reader.lines() {
            let line = line.context("Failed to read line from WAL")?;
            let entry: WalEntry =
                serde_json::from_str(&line).context("Failed to deserialize WAL entry")?;
            entries.push(entry);
        }

        Ok(entries)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    #[test]
    fn test_wal_basic_transaction() {
        let temp_dir = TempDir::new().unwrap();
        let mut wal = WriteAheadLog::new(&temp_dir.path().to_path_buf()).unwrap();

        wal.begin().unwrap();

        let test_file = temp_dir.path().join("test.txt");
        wal.append(WalOperation::WriteShadowFile {
            path: test_file.clone(),
            content: "test content".to_string(),
        })
        .unwrap();

        // File shouldn't exist yet
        assert!(!test_file.exists());

        // Commit should execute the operation
        wal.commit().unwrap();
        assert!(test_file.exists());
        assert_eq!(std::fs::read_to_string(&test_file).unwrap(), "test content");
    }

    #[test]
    fn test_wal_rollback() {
        let temp_dir = TempDir::new().unwrap();
        let mut wal = WriteAheadLog::new(&temp_dir.path().to_path_buf()).unwrap();

        wal.begin().unwrap();

        let test_file = temp_dir.path().join("test.txt");
        wal.append(WalOperation::WriteShadowFile {
            path: test_file.clone(),
            content: "test content".to_string(),
        })
        .unwrap();

        // Rollback should not execute
        wal.rollback().unwrap();
        assert!(!test_file.exists());
    }

    #[test]
    fn test_wal_multiple_operations() {
        let temp_dir = TempDir::new().unwrap();
        let mut wal = WriteAheadLog::new(&temp_dir.path().to_path_buf()).unwrap();

        wal.begin().unwrap();

        let file1 = temp_dir.path().join("file1.txt");
        let file2 = temp_dir.path().join("file2.txt");

        wal.append(WalOperation::WriteShadowFile {
            path: file1.clone(),
            content: "content 1".to_string(),
        })
        .unwrap();

        wal.append(WalOperation::WriteShadowFile {
            path: file2.clone(),
            content: "content 2".to_string(),
        })
        .unwrap();

        wal.commit().unwrap();

        assert!(file1.exists());
        assert!(file2.exists());
        assert_eq!(std::fs::read_to_string(&file1).unwrap(), "content 1");
        assert_eq!(std::fs::read_to_string(&file2).unwrap(), "content 2");
    }

    #[test]
    fn test_wal_recovery() {
        let temp_dir = TempDir::new().unwrap();
        let test_file = temp_dir.path().join("test.txt");

        {
            let mut wal = WriteAheadLog::new(&temp_dir.path().to_path_buf()).unwrap();
            wal.begin().unwrap();
            wal.append(WalOperation::WriteShadowFile {
                path: test_file.clone(),
                content: "recovered content".to_string(),
            })
            .unwrap();
            // Simulate crash - don't commit
        }

        // Create new WAL and recover
        let mut wal = WriteAheadLog::new(&temp_dir.path().to_path_buf()).unwrap();
        wal.recover().unwrap();

        assert!(test_file.exists());
        assert_eq!(
            std::fs::read_to_string(&test_file).unwrap(),
            "recovered content"
        );
    }
}