lcpfs 2026.1.102

LCP File System - A ZFS-inspired copy-on-write filesystem for Rust
// Copyright 2025 LunaOS Contributors
// SPDX-License-Identifier: Apache-2.0

//! Time-Travel Query Engine for LCPFS.
//!
//! This module provides SQL-like query capabilities for accessing historical
//! filesystem state at any point in time, leveraging LCPFS's copy-on-write
//! architecture and transaction groups (TXGs).
//!
//! # Overview
//!
//! The time-travel query engine enables:
//!
//! - **Point-in-time queries**: Access filesystem state at any historical moment
//! - **Change diffs**: Compare state between two points in time
//! - **Version history**: List all versions of a file
//! - **Restore**: Recover files from any historical version
//! - **Snapshot queries**: Work with named snapshots
//!
//! # Query Language
//!
//! The query language uses familiar SQL-like syntax:
//!
//! ```text
//! -- List files as of a specific date
//! SELECT * FROM /data AS OF '2024-01-15'
//!
//! -- Find large files with filtering
//! SELECT path, size FROM /data AS OF '2024-01-15' WHERE size > 1000000
//!
//! -- Compare state between two points
//! DIFF /data BETWEEN '2024-01-01' AND '2024-06-01'
//!
//! -- List all versions of a file
//! VERSIONS /data/config.yaml LIMIT 100
//!
//! -- Restore a file
//! RESTORE /data/file.txt TO '2024-03-15'
//!
//! -- Show all snapshots
//! SHOW SNAPSHOTS FOR /pool/data
//!
//! -- Aggregate queries
//! SELECT COUNT(*), SUM(size) FROM /data AS OF NOW
//! ```
//!
//! # Time Specifications
//!
//! Multiple formats are supported for specifying points in time:
//!
//! - **Timestamp**: Unix timestamp (e.g., `1704067200`)
//! - **DateTime**: ISO 8601 format (e.g., `'2024-01-15'`, `'2024-01-15 10:30:00'`)
//! - **Snapshot**: Named snapshot (e.g., `SNAPSHOT 'daily-backup'`)
//! - **Relative**: Human-readable relative time (e.g., `'1 hour ago'`, `'yesterday'`)
//! - **TXG**: Direct transaction group number (e.g., `TXG 12345`)
//! - **NOW**: Current state
//!
//! # Architecture
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────┐
//! │                    TimeTravelEngine                         │
//! │  ┌───────────┐  ┌──────────┐  ┌─────────┐  ┌──────────────┐│
//! │  │ Parser    │→ │ Resolver │→ │ Walker  │→ │ Result       ││
//! │  │ (SQL→AST) │  │ (Time→TXG)│ │ (TXG→FS)│  │ (Rows/Diffs) ││
//! │  └───────────┘  └──────────┘  └─────────┘  └──────────────┘│
//! │                        │                                    │
//! │  ┌───────────┐  ┌──────┴─────┐  ┌──────────┐               │
//! │  │ History   │  │ Diff       │  │ Restore  │               │
//! │  │ (Versions)│  │ (Compare)  │  │ (Recover)│               │
//! │  └───────────┘  └────────────┘  └──────────┘               │
//! └─────────────────────────────────────────────────────────────┘
//!//!//!               ┌─────────────────────────────┐
//!               │     DMU / Block Layer       │
//!               │  (Historical Block Access)  │
//!               └─────────────────────────────┘
//! ```
//!
//! # Examples
//!
//! ## Basic Query
//!
//! ```rust,ignore
//! use lcpfs::timetravel::{TimeTravelEngine, TimeSpec};
//!
//! let engine = TimeTravelEngine::new(&provider);
//!
//! // Execute SQL-like query
//! let result = engine.query("SELECT * FROM /data AS OF '2024-01-15'")?;
//!
//! // Use convenience methods
//! let files = engine.ls("/data", &TimeSpec::Snapshot("daily".into()))?;
//! let diffs = engine.diff("/data", &TimeSpec::Relative("1 week ago".into()), &TimeSpec::Now)?;
//! ```
//!
//! ## Version History
//!
//! ```rust,ignore
//! use lcpfs::timetravel::TimeTravelEngine;
//!
//! let engine = TimeTravelEngine::new(&provider);
//!
//! // Get all versions of a file
//! let versions = engine.versions("/data/important.txt")?;
//!
//! for version in versions {
//!     println!("TXG {}: {} bytes, {:?}",
//!         version.txg, version.size, version.change_type);
//! }
//! ```
//!
//! ## Restore Operation
//!
//! ```rust,ignore
//! use lcpfs::timetravel::{TimeTravelEngine, TimeSpec, RestoreOptions};
//!
//! let engine = TimeTravelEngine::new(&provider);
//!
//! // Restore a file to a previous state
//! let result = engine.restore(
//!     &mut target,
//!     "/data/file.txt",
//!     &TimeSpec::DateTime("2024-03-15".into()),
//!     None,  // Same path
//!     &RestoreOptions::default()
//! )?;
//!
//! println!("Restored {} files, {} bytes", result.files_restored, result.bytes_restored);
//! ```
//!
//! # Module Structure
//!
//! - `types`: Core types (TimeSpec, TimeQuery AST, QueryResult, etc.)
//! - `parser`: SQL-like query parser (recursive descent)
//! - `resolver`: TimeSpec to TXG resolution
//! - `walker`: Historical tree traversal
//! - `diff`: Diff computation between TXGs
//! - `history`: File version history tracking
//! - `restore`: File restoration from history
//! - `engine`: Unified query engine API

pub mod diff;
pub mod engine;
pub mod history;
pub mod parser;
pub mod resolver;
pub mod restore;
pub mod types;
pub mod walker;
/// ZPL adapter for time-travel queries.
pub mod zpl_adapter;

// Imports for tests
#[cfg(test)]
use alloc::vec;

// Re-export main types and engine
pub use engine::{TimeTravelEngine, TimeTravelProvider};
pub use types::{
    AggregateFunc, AggregateResult, ChangeType, Column, DiffEntry, FileType, FileVersion, Filter,
    OrderBy, QueryResult, QueryRow, SnapshotInfo, TimeError, TimeQuery, TimeSpec, Value,
};

// Re-export commonly used types from submodules
pub use diff::{DiffEngine, DiffOptions, DiffSummary};
pub use history::{VersionComparison, VersionHistory, VersionHistoryProvider};
pub use parser::QueryParser;
pub use resolver::{InMemoryTxgHistory, TimeSpecResolver, TxgHistoryProvider, TxgTimestamp};
pub use restore::{RestoreEngine, RestoreOptions, RestoreResult, RestoreTarget};
pub use walker::{HistoricalEntry, HistoricalTreeProvider, HistoricalTreeWalker, WalkOptions};

// Re-export ZPL adapter
pub use zpl_adapter::{
    TIME_TRAVEL_ADAPTER, ZplTimeTravelAdapter, create_snapshot, current_txg, delete_snapshot,
    get_snapshot, list_snapshots, lookup_at_txg, readdir_at_txg, record_file_change,
    record_txg_sync,
};

// ═══════════════════════════════════════════════════════════════════════════════
// MODULE TESTS
// ═══════════════════════════════════════════════════════════════════════════════

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

    /// Verify all public types are accessible.
    #[test]
    fn test_public_types() {
        // Types from types.rs
        let _spec = TimeSpec::Now;
        let _query = TimeQuery::ShowSnapshots { path: "/".into() };
        let _result = QueryResult::Empty;
        let _error = TimeError::NoHistory;

        // Verify Column enum
        let _col = Column::All;

        // Verify Filter enum
        let _filter = Filter::IsNull {
            column: "test".into(),
        };
    }

    /// Test query parsing and execution flow.
    #[test]
    fn test_query_parser() {
        let query = QueryParser::parse("SHOW SNAPSHOTS FOR /data").unwrap();
        assert!(matches!(query, TimeQuery::ShowSnapshots { .. }));
    }

    /// Test TimeSpec variants.
    #[test]
    fn test_timespec_variants() {
        assert!(TimeSpec::Relative("1 hour ago".into()).is_relative());
        assert!(TimeSpec::Snapshot("backup".into()).is_snapshot());
        assert!(!TimeSpec::Txg(100).is_relative());
        assert!(!TimeSpec::Now.is_snapshot());
    }

    /// Test QueryResult lengths.
    #[test]
    fn test_query_result_len() {
        assert!(QueryResult::Empty.is_empty());
        assert_eq!(QueryResult::Empty.len(), 0);

        let rows = QueryResult::Rows(vec![QueryRow {
            path: "/test".into(),
            object_id: 1,
            size: 100,
            mtime: 1000,
            ctime: 1000,
            atime: 1000,
            mode: 0o644,
            uid: 1000,
            gid: 1000,
            txg: 100,
            file_type: FileType::Regular,
            checksum: [0; 4],
        }]);
        assert_eq!(rows.len(), 1);
    }

    /// Test ChangeType parsing.
    #[test]
    fn test_change_type_parse() {
        assert!(matches!(
            ChangeType::from_str("created"),
            Some(ChangeType::Created)
        ));
        assert!(matches!(
            ChangeType::from_str("MODIFIED"),
            Some(ChangeType::Modified)
        ));
        assert!(ChangeType::from_str("invalid").is_none());
    }

    /// Test DiffSummary.
    #[test]
    fn test_diff_summary() {
        let summary = DiffSummary {
            created: 5,
            modified: 3,
            deleted: 2,
            renamed: 1,
            metadata_changed: 0,
        };
        assert_eq!(summary.total(), 11);
        assert!(!summary.is_empty());
    }

    /// Test RestoreOptions defaults.
    #[test]
    fn test_restore_options_default() {
        let opts = RestoreOptions::default();
        assert!(!opts.overwrite);
        assert!(opts.preserve_metadata);
        assert!(opts.recursive);
        assert!(!opts.dry_run);
    }

    /// Test WalkOptions defaults.
    #[test]
    fn test_walk_options_default() {
        let opts = WalkOptions::default();
        assert_eq!(opts.max_depth, -1);
        assert!(!opts.follow_symlinks);
        assert!(opts.include_hidden);
    }

    /// Test FileType names.
    #[test]
    fn test_file_type_names() {
        assert_eq!(FileType::Regular.name(), "file");
        assert_eq!(FileType::Directory.name(), "directory");
        assert_eq!(FileType::Symlink.name(), "symlink");
    }

    /// Test Value conversions.
    #[test]
    fn test_value_conversions() {
        let v = Value::String("test".into());
        assert_eq!(v.as_str(), Some("test"));

        let v = Value::Integer(-42);
        assert_eq!(v.as_i64(), Some(-42));

        let v = Value::Unsigned(100);
        assert_eq!(v.as_u64(), Some(100));
    }
}