Skip to main content

featherdb_core/
lib.rs

1//! Core types and traits for FeatherDB
2//!
3//! This crate provides the fundamental types used throughout FeatherDB:
4//! - `Value`: The core data type for storing values
5//! - `Error`: Error types for all database operations
6//! - `PageId`, `TransactionId`: Type-safe identifiers
7//! - `ToRow`, `FromRow`: Traits for type-safe row conversion
8//! - `WalSyncMode`, `WalGroupCommitConfig`: WAL configuration for group commit
9//! - `TransactionConfig`: Transaction timeout configuration
10//! - `Permissions`: Table-level permission flags for authorization
11
12mod error;
13mod permissions;
14mod row;
15mod value;
16
17pub use error::{
18    find_best_match, levenshtein_distance, suggest_keyword, ConfigError, Error, QueryContext,
19    QueryError, Result,
20};
21pub use permissions::Permissions;
22pub use row::{ColumnDef, FromRow, FromValue, TableSchema, ToRow, ToValue};
23pub use value::{ColumnType, Value};
24
25/// Page identifier - uniquely identifies a page in the database file
26#[derive(
27    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
28)]
29pub struct PageId(pub u64);
30
31impl PageId {
32    pub const INVALID: PageId = PageId(u64::MAX);
33
34    pub fn is_valid(&self) -> bool {
35        *self != Self::INVALID
36    }
37}
38
39impl From<u64> for PageId {
40    fn from(val: u64) -> Self {
41        PageId(val)
42    }
43}
44
45impl From<PageId> for u64 {
46    fn from(val: PageId) -> Self {
47        val.0
48    }
49}
50
51/// Transaction identifier
52#[derive(
53    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
54)]
55pub struct TransactionId(pub u64);
56
57impl TransactionId {
58    pub const NONE: TransactionId = TransactionId(0);
59
60    pub fn next(&self) -> TransactionId {
61        TransactionId(self.0 + 1)
62    }
63}
64
65impl From<u64> for TransactionId {
66    fn from(val: u64) -> Self {
67        TransactionId(val)
68    }
69}
70
71impl From<TransactionId> for u64 {
72    fn from(val: TransactionId) -> Self {
73        val.0
74    }
75}
76
77/// Log Sequence Number - identifies a position in the WAL
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
79pub struct Lsn(pub u64);
80
81impl Lsn {
82    pub const ZERO: Lsn = Lsn(0);
83
84    pub fn next(&self) -> Lsn {
85        Lsn(self.0 + 1)
86    }
87}
88
89impl From<u64> for Lsn {
90    fn from(val: u64) -> Self {
91        Lsn(val)
92    }
93}
94
95/// Encryption configuration
96#[derive(Debug, Clone, Default)]
97pub enum EncryptionConfig {
98    /// No encryption
99    #[default]
100    None,
101    /// Password-based encryption (derives key using Argon2id)
102    Password(String),
103    /// Direct key encryption (32 bytes / 256 bits)
104    Key([u8; 32]),
105}
106
107/// Compression algorithm type for page compression
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
109pub enum CompressionType {
110    /// No compression
111    #[default]
112    None,
113    /// LZ4 - fast compression with moderate ratio
114    Lz4,
115    /// ZSTD - high compression ratio with configurable level
116    /// Level ranges from 1 (fastest) to 22 (best compression)
117    Zstd { level: i32 },
118}
119
120impl CompressionType {
121    /// Create ZSTD compression with default level (3)
122    pub fn zstd_default() -> Self {
123        CompressionType::Zstd { level: 3 }
124    }
125
126    /// Create ZSTD compression with high ratio (level 9)
127    pub fn zstd_high() -> Self {
128        CompressionType::Zstd { level: 9 }
129    }
130
131    /// Create ZSTD compression with maximum ratio (level 19)
132    pub fn zstd_max() -> Self {
133        CompressionType::Zstd { level: 19 }
134    }
135
136    /// Check if compression is enabled
137    pub fn is_enabled(&self) -> bool {
138        !matches!(self, CompressionType::None)
139    }
140}
141
142/// Compression configuration for the storage engine
143#[derive(Debug, Clone)]
144pub struct CompressionConfig {
145    /// The compression algorithm to use
146    pub compression_type: CompressionType,
147    /// Minimum page data size to compress (smaller pages are stored uncompressed)
148    /// Default: 512 bytes
149    pub threshold: usize,
150}
151
152impl Default for CompressionConfig {
153    fn default() -> Self {
154        CompressionConfig {
155            compression_type: CompressionType::None,
156            threshold: 512,
157        }
158    }
159}
160
161impl CompressionConfig {
162    /// Create a new compression config with no compression
163    pub fn none() -> Self {
164        Self::default()
165    }
166
167    /// Create a compression config with LZ4 (fast compression)
168    pub fn lz4() -> Self {
169        CompressionConfig {
170            compression_type: CompressionType::Lz4,
171            threshold: 512,
172        }
173    }
174
175    /// Create a compression config with ZSTD at default level
176    pub fn zstd() -> Self {
177        CompressionConfig {
178            compression_type: CompressionType::zstd_default(),
179            threshold: 512,
180        }
181    }
182
183    /// Create a compression config with ZSTD at specified level
184    pub fn zstd_level(level: i32) -> Self {
185        CompressionConfig {
186            compression_type: CompressionType::Zstd { level },
187            threshold: 512,
188        }
189    }
190
191    /// Set the compression threshold
192    pub fn with_threshold(mut self, threshold: usize) -> Self {
193        self.threshold = threshold;
194        self
195    }
196
197    /// Check if compression is enabled
198    pub fn is_enabled(&self) -> bool {
199        self.compression_type.is_enabled()
200    }
201
202    /// Validate the compression configuration
203    ///
204    /// # Validation Rules
205    /// - threshold: must be between 64 bytes and 64KB
206    /// - ZSTD level: must be between 1 and 22
207    ///
208    /// # Errors
209    /// Returns `ConfigError` if validation fails
210    pub fn validate(&self) -> std::result::Result<(), crate::ConfigError> {
211        use crate::ConfigError;
212
213        const MIN_THRESHOLD: u64 = 64; // 64 bytes minimum
214        const MAX_THRESHOLD: u64 = 65536; // 64KB maximum
215        const MIN_ZSTD_LEVEL: i32 = 1;
216        const MAX_ZSTD_LEVEL: i32 = 22;
217
218        // Validate threshold
219        let threshold = self.threshold as u64;
220        if threshold < MIN_THRESHOLD {
221            return Err(ConfigError::ValueTooLow {
222                field: "threshold".to_string(),
223                min: MIN_THRESHOLD,
224                actual: threshold,
225            });
226        }
227        if threshold > MAX_THRESHOLD {
228            return Err(ConfigError::ValueTooHigh {
229                field: "threshold".to_string(),
230                max: MAX_THRESHOLD,
231                actual: threshold,
232            });
233        }
234
235        // Validate ZSTD compression level
236        if let CompressionType::Zstd { level } = self.compression_type {
237            if level < MIN_ZSTD_LEVEL {
238                return Err(ConfigError::InvalidValue {
239                    field: "compression_type.level".to_string(),
240                    reason: format!(
241                        "ZSTD compression level {} is too low (minimum: {})",
242                        level, MIN_ZSTD_LEVEL
243                    ),
244                });
245            }
246            if level > MAX_ZSTD_LEVEL {
247                return Err(ConfigError::InvalidValue {
248                    field: "compression_type.level".to_string(),
249                    reason: format!(
250                        "ZSTD compression level {} is too high (maximum: {})",
251                        level, MAX_ZSTD_LEVEL
252                    ),
253                });
254            }
255        }
256
257        Ok(())
258    }
259}
260
261/// WAL synchronization mode
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
263pub enum WalSyncMode {
264    /// Sync immediately on every commit (safest, slowest)
265    #[default]
266    Immediate,
267    /// Batch multiple commits into a single fsync (2-5x faster)
268    GroupCommit,
269    /// No sync - data may be lost on crash (fastest, least safe)
270    NoSync,
271}
272
273/// Buffer pool eviction policy type
274///
275/// Different eviction policies offer different trade-offs:
276/// - Clock: Simple, fast, good for general workloads
277/// - LRU-2: Better scan resistance (5-15% higher hit rate for mixed workloads)
278/// - LIRS: Best for workloads with varying access patterns
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
280pub enum EvictionPolicyType {
281    /// Clock algorithm (second-chance) - simple and fast
282    #[default]
283    Clock,
284    /// LRU-2 algorithm - tracks last 2 access times for scan resistance
285    Lru2,
286    /// LIRS algorithm - adapts to workload patterns
287    Lirs,
288}
289
290/// Configuration for transaction behavior
291#[derive(Debug, Clone)]
292pub struct TransactionConfig {
293    /// Maximum duration before transaction auto-rollback (None = no timeout)
294    pub timeout_ms: Option<u64>,
295    /// Emit warning if transaction exceeds this duration (None = no warning)
296    pub warn_after_ms: Option<u64>,
297}
298
299impl Default for TransactionConfig {
300    fn default() -> Self {
301        TransactionConfig {
302            timeout_ms: Some(30_000),    // 30 seconds default timeout
303            warn_after_ms: Some(10_000), // 10 seconds warning threshold
304        }
305    }
306}
307
308impl TransactionConfig {
309    /// Create a new transaction configuration with safe defaults
310    /// - timeout_ms: 30 seconds
311    /// - warn_after_ms: 10 seconds
312    pub fn new() -> Self {
313        Self::default()
314    }
315
316    /// Create a configuration with no timeout or warnings
317    pub fn no_limits() -> Self {
318        TransactionConfig {
319            timeout_ms: None,
320            warn_after_ms: None,
321        }
322    }
323
324    /// Set the transaction timeout in milliseconds
325    pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
326        self.timeout_ms = Some(timeout_ms);
327        self
328    }
329
330    /// Set the warning threshold in milliseconds
331    pub fn with_warning(mut self, warn_after_ms: u64) -> Self {
332        self.warn_after_ms = Some(warn_after_ms);
333        self
334    }
335
336    /// Create a configuration with both timeout and warning
337    pub fn with_timeout_and_warning(timeout_ms: u64, warn_after_ms: u64) -> Self {
338        TransactionConfig {
339            timeout_ms: Some(timeout_ms),
340            warn_after_ms: Some(warn_after_ms),
341        }
342    }
343
344    /// Validate the transaction configuration
345    ///
346    /// # Validation Rules
347    /// - timeout_ms: must be between 100ms and 1 hour (3,600,000ms)
348    /// - warn_after_ms: must be between 100ms and 1 hour (3,600,000ms)
349    /// - warn_after_ms must be less than timeout_ms if both are set
350    ///
351    /// # Errors
352    /// Returns `ConfigError` if validation fails
353    pub fn validate(&self) -> std::result::Result<(), crate::ConfigError> {
354        use crate::ConfigError;
355
356        const MIN_MS: u64 = 100; // 100ms minimum
357        const MAX_MS: u64 = 3_600_000; // 1 hour maximum
358
359        // Validate timeout_ms
360        if let Some(timeout) = self.timeout_ms {
361            if timeout < MIN_MS {
362                return Err(ConfigError::ValueTooLow {
363                    field: "timeout_ms".to_string(),
364                    min: MIN_MS,
365                    actual: timeout,
366                });
367            }
368            if timeout > MAX_MS {
369                return Err(ConfigError::ValueTooHigh {
370                    field: "timeout_ms".to_string(),
371                    max: MAX_MS,
372                    actual: timeout,
373                });
374            }
375        }
376
377        // Validate warn_after_ms
378        if let Some(warn_after) = self.warn_after_ms {
379            if warn_after < MIN_MS {
380                return Err(ConfigError::ValueTooLow {
381                    field: "warn_after_ms".to_string(),
382                    min: MIN_MS,
383                    actual: warn_after,
384                });
385            }
386            if warn_after > MAX_MS {
387                return Err(ConfigError::ValueTooHigh {
388                    field: "warn_after_ms".to_string(),
389                    max: MAX_MS,
390                    actual: warn_after,
391                });
392            }
393        }
394
395        // Validate that warn_after_ms < timeout_ms if both are set
396        if let (Some(timeout), Some(warn_after)) = (self.timeout_ms, self.warn_after_ms) {
397            if warn_after >= timeout {
398                return Err(ConfigError::InvalidValue {
399                    field: "warn_after_ms".to_string(),
400                    reason: format!(
401                        "warn_after_ms ({}) must be less than timeout_ms ({})",
402                        warn_after, timeout
403                    ),
404                });
405            }
406        }
407
408        Ok(())
409    }
410}
411
412/// WAL group commit configuration
413#[derive(Debug, Clone)]
414pub struct WalGroupCommitConfig {
415    /// Sync mode for the WAL
416    pub sync_mode: WalSyncMode,
417    /// Maximum time to wait before flushing a group (in milliseconds)
418    /// Only used when sync_mode is GroupCommit
419    pub group_commit_interval_ms: u64,
420    /// Maximum number of records to batch before forcing a flush
421    /// Only used when sync_mode is GroupCommit
422    pub group_commit_max_batch: usize,
423}
424
425impl Default for WalGroupCommitConfig {
426    fn default() -> Self {
427        WalGroupCommitConfig {
428            sync_mode: WalSyncMode::Immediate,
429            group_commit_interval_ms: 10, // 10ms default
430            group_commit_max_batch: 1000, // 1000 records max batch
431        }
432    }
433}
434
435impl WalGroupCommitConfig {
436    /// Create a new group commit configuration
437    pub fn new() -> Self {
438        Self::default()
439    }
440
441    /// Set the sync mode
442    pub fn sync_mode(mut self, mode: WalSyncMode) -> Self {
443        self.sync_mode = mode;
444        self
445    }
446
447    /// Set the group commit interval in milliseconds
448    pub fn group_commit_interval_ms(mut self, ms: u64) -> Self {
449        self.group_commit_interval_ms = ms;
450        self
451    }
452
453    /// Set the maximum batch size for group commit
454    pub fn group_commit_max_batch(mut self, max: usize) -> Self {
455        self.group_commit_max_batch = max;
456        self
457    }
458
459    /// Enable immediate sync mode (default)
460    pub fn immediate() -> Self {
461        Self {
462            sync_mode: WalSyncMode::Immediate,
463            ..Default::default()
464        }
465    }
466
467    /// Enable group commit mode with default settings
468    pub fn group_commit() -> Self {
469        Self {
470            sync_mode: WalSyncMode::GroupCommit,
471            ..Default::default()
472        }
473    }
474
475    /// Enable no-sync mode (dangerous - data loss on crash)
476    pub fn no_sync() -> Self {
477        Self {
478            sync_mode: WalSyncMode::NoSync,
479            ..Default::default()
480        }
481    }
482
483    /// Validate the WAL group commit configuration
484    ///
485    /// # Validation Rules
486    /// - group_commit_interval_ms: must be between 1ms and 1000ms (1 second)
487    /// - group_commit_max_batch: must be between 1 and 100,000 records
488    ///
489    /// # Errors
490    /// Returns `ConfigError` if validation fails
491    pub fn validate(&self) -> std::result::Result<(), crate::ConfigError> {
492        use crate::ConfigError;
493
494        const MIN_INTERVAL_MS: u64 = 1;
495        const MAX_INTERVAL_MS: u64 = 1000;
496        const MIN_BATCH: u64 = 1;
497        const MAX_BATCH: u64 = 100_000;
498
499        // Validate interval
500        if self.group_commit_interval_ms < MIN_INTERVAL_MS {
501            return Err(ConfigError::ValueTooLow {
502                field: "group_commit_interval_ms".to_string(),
503                min: MIN_INTERVAL_MS,
504                actual: self.group_commit_interval_ms,
505            });
506        }
507        if self.group_commit_interval_ms > MAX_INTERVAL_MS {
508            return Err(ConfigError::ValueTooHigh {
509                field: "group_commit_interval_ms".to_string(),
510                max: MAX_INTERVAL_MS,
511                actual: self.group_commit_interval_ms,
512            });
513        }
514
515        // Validate max batch size
516        let batch_size = self.group_commit_max_batch as u64;
517        if batch_size < MIN_BATCH {
518            return Err(ConfigError::ValueTooLow {
519                field: "group_commit_max_batch".to_string(),
520                min: MIN_BATCH,
521                actual: batch_size,
522            });
523        }
524        if batch_size > MAX_BATCH {
525            return Err(ConfigError::ValueTooHigh {
526                field: "group_commit_max_batch".to_string(),
527                max: MAX_BATCH,
528                actual: batch_size,
529            });
530        }
531
532        Ok(())
533    }
534}
535
536/// Configuration for database storage limits
537#[derive(Debug, Clone)]
538pub struct StorageLimitsConfig {
539    /// Maximum database file size in bytes (None = unlimited)
540    pub max_database_size: Option<u64>,
541    /// Maximum WAL file size in bytes (None = unlimited)
542    pub max_wal_size: Option<u64>,
543    /// Safety margin percentage - reject writes when this close to limit (default 5%)
544    pub safety_margin_percent: u8,
545}
546
547impl Default for StorageLimitsConfig {
548    fn default() -> Self {
549        Self {
550            max_database_size: None,
551            max_wal_size: None,
552            safety_margin_percent: 5,
553        }
554    }
555}
556
557impl StorageLimitsConfig {
558    pub fn new() -> Self {
559        Self::default()
560    }
561
562    pub fn with_max_database_size(mut self, size: u64) -> Self {
563        self.max_database_size = Some(size);
564        self
565    }
566
567    pub fn with_max_database_size_mb(mut self, mb: u64) -> Self {
568        self.max_database_size = Some(mb * 1024 * 1024);
569        self
570    }
571
572    pub fn with_max_wal_size(mut self, size: u64) -> Self {
573        self.max_wal_size = Some(size);
574        self
575    }
576
577    pub fn with_safety_margin_percent(mut self, percent: u8) -> Self {
578        self.safety_margin_percent = percent.min(50); // Cap at 50%
579        self
580    }
581
582    /// Calculate effective limit accounting for safety margin
583    pub fn effective_database_limit(&self) -> Option<u64> {
584        self.max_database_size.map(|limit| {
585            let margin = (limit as f64 * (self.safety_margin_percent as f64 / 100.0)) as u64;
586            limit.saturating_sub(margin)
587        })
588    }
589
590    /// Validate the storage limits configuration
591    ///
592    /// # Validation Rules
593    /// - max_database_size: must be at least 1MB if set
594    /// - max_wal_size: must be at least 64KB if set
595    /// - safety_margin_percent: must be between 0 and 50
596    ///
597    /// # Errors
598    /// Returns `ConfigError` if validation fails
599    pub fn validate(&self) -> std::result::Result<(), crate::ConfigError> {
600        use crate::ConfigError;
601
602        const MIN_DATABASE_SIZE: u64 = 1024 * 1024; // 1MB
603        const MIN_WAL_SIZE: u64 = 64 * 1024; // 64KB
604        const MAX_SAFETY_MARGIN: u64 = 50; // 50%
605
606        // Validate max_database_size
607        if let Some(size) = self.max_database_size {
608            if size < MIN_DATABASE_SIZE {
609                return Err(ConfigError::ValueTooLow {
610                    field: "max_database_size".to_string(),
611                    min: MIN_DATABASE_SIZE,
612                    actual: size,
613                });
614            }
615        }
616
617        // Validate max_wal_size
618        if let Some(size) = self.max_wal_size {
619            if size < MIN_WAL_SIZE {
620                return Err(ConfigError::ValueTooLow {
621                    field: "max_wal_size".to_string(),
622                    min: MIN_WAL_SIZE,
623                    actual: size,
624                });
625            }
626        }
627
628        // Validate safety_margin_percent
629        let margin = self.safety_margin_percent as u64;
630        if margin > MAX_SAFETY_MARGIN {
631            return Err(ConfigError::ValueTooHigh {
632                field: "safety_margin_percent".to_string(),
633                max: MAX_SAFETY_MARGIN,
634                actual: margin,
635            });
636        }
637
638        Ok(())
639    }
640}
641
642/// Database configuration options
643#[derive(Debug, Clone)]
644pub struct Config {
645    /// Page size in bytes (default: 4096)
646    pub page_size: usize,
647    /// Buffer pool size in pages (default: 16384 = 64MB with 4KB pages)
648    pub buffer_pool_pages: usize,
649    /// Path to the database file
650    pub path: std::path::PathBuf,
651    /// Create database if it doesn't exist
652    pub create_if_missing: bool,
653    /// Enable WAL for durability
654    pub enable_wal: bool,
655    /// Sync data file on every commit for durability (default: true)
656    /// Set to false for benchmarks or applications that prioritize speed over durability
657    pub sync_on_commit: bool,
658    /// Encryption configuration
659    pub encryption: EncryptionConfig,
660    /// WAL group commit configuration
661    pub wal_config: WalGroupCommitConfig,
662    /// Compression configuration
663    pub compression: CompressionConfig,
664    /// Buffer pool eviction policy (default: Clock)
665    pub eviction_policy: EvictionPolicyType,
666    /// Storage limits configuration
667    pub storage_limits: StorageLimitsConfig,
668    /// API key for authentication (v0.3.5)
669    pub api_key: Option<String>,
670}
671
672impl Default for Config {
673    fn default() -> Self {
674        Config {
675            page_size: 4096,
676            buffer_pool_pages: 16384,
677            path: std::path::PathBuf::from("featherdb.db"),
678            create_if_missing: true,
679            enable_wal: true,
680            sync_on_commit: true,
681            encryption: EncryptionConfig::None,
682            wal_config: WalGroupCommitConfig::default(),
683            compression: CompressionConfig::default(),
684            eviction_policy: EvictionPolicyType::default(),
685            storage_limits: StorageLimitsConfig::default(),
686            api_key: None,
687        }
688    }
689}
690
691impl Config {
692    pub fn new<P: Into<std::path::PathBuf>>(path: P) -> Self {
693        Config {
694            path: path.into(),
695            ..Default::default()
696        }
697    }
698
699    pub fn page_size(mut self, size: usize) -> Self {
700        self.page_size = size;
701        self
702    }
703
704    pub fn buffer_pool_size_mb(mut self, mb: usize) -> Self {
705        self.buffer_pool_pages = (mb * 1024 * 1024) / self.page_size;
706        self
707    }
708
709    pub fn create_if_missing(mut self, create: bool) -> Self {
710        self.create_if_missing = create;
711        self
712    }
713
714    /// Set whether to sync the data file on every commit (default: true)
715    ///
716    /// When false, committed data may be lost on crash but write operations
717    /// are significantly faster. Useful for benchmarks and non-critical data.
718    pub fn with_sync_on_commit(mut self, sync: bool) -> Self {
719        self.sync_on_commit = sync;
720        self
721    }
722
723    /// Enable password-based encryption
724    pub fn with_password<S: Into<String>>(mut self, password: S) -> Self {
725        self.encryption = EncryptionConfig::Password(password.into());
726        self
727    }
728
729    /// Enable encryption with a raw key
730    pub fn with_key(mut self, key: [u8; 32]) -> Self {
731        self.encryption = EncryptionConfig::Key(key);
732        self
733    }
734
735    /// Check if encryption is enabled
736    pub fn is_encrypted(&self) -> bool {
737        !matches!(self.encryption, EncryptionConfig::None)
738    }
739
740    /// Set WAL configuration for group commit
741    pub fn wal_config(mut self, config: WalGroupCommitConfig) -> Self {
742        self.wal_config = config;
743        self
744    }
745
746    /// Enable group commit mode for improved throughput
747    pub fn with_group_commit(mut self) -> Self {
748        self.wal_config = WalGroupCommitConfig::group_commit();
749        self
750    }
751
752    /// Set group commit interval in milliseconds
753    pub fn group_commit_interval_ms(mut self, ms: u64) -> Self {
754        self.wal_config.group_commit_interval_ms = ms;
755        self
756    }
757
758    /// Set maximum batch size for group commit
759    pub fn group_commit_max_batch(mut self, max: usize) -> Self {
760        self.wal_config.group_commit_max_batch = max;
761        self
762    }
763
764    /// Set compression configuration
765    pub fn with_compression(mut self, compression: CompressionConfig) -> Self {
766        self.compression = compression;
767        self
768    }
769
770    /// Enable LZ4 compression (fast)
771    pub fn with_lz4_compression(mut self) -> Self {
772        self.compression = CompressionConfig::lz4();
773        self
774    }
775
776    /// Enable ZSTD compression with default level
777    pub fn with_zstd_compression(mut self) -> Self {
778        self.compression = CompressionConfig::zstd();
779        self
780    }
781
782    /// Enable ZSTD compression with specified level (1-22)
783    pub fn with_zstd_compression_level(mut self, level: i32) -> Self {
784        self.compression = CompressionConfig::zstd_level(level);
785        self
786    }
787
788    /// Set the compression threshold (minimum size to compress)
789    pub fn compression_threshold(mut self, threshold: usize) -> Self {
790        self.compression.threshold = threshold;
791        self
792    }
793
794    /// Check if compression is enabled
795    pub fn is_compressed(&self) -> bool {
796        self.compression.is_enabled()
797    }
798
799    /// Set the buffer pool eviction policy
800    ///
801    /// # Example
802    ///
803    /// ```rust
804    /// use featherdb_core::{Config, EvictionPolicyType};
805    ///
806    /// let config = Config::new("mydb.db")
807    ///     .with_eviction_policy(EvictionPolicyType::Lru2);
808    /// ```
809    pub fn with_eviction_policy(mut self, policy: EvictionPolicyType) -> Self {
810        self.eviction_policy = policy;
811        self
812    }
813
814    /// Use LRU-2 eviction policy (5-15% higher cache hit rate for mixed workloads)
815    pub fn with_lru2_eviction(mut self) -> Self {
816        self.eviction_policy = EvictionPolicyType::Lru2;
817        self
818    }
819
820    /// Use LIRS eviction policy (best for varying access patterns)
821    pub fn with_lirs_eviction(mut self) -> Self {
822        self.eviction_policy = EvictionPolicyType::Lirs;
823        self
824    }
825
826    /// Use Clock eviction policy (simple and fast, default)
827    pub fn with_clock_eviction(mut self) -> Self {
828        self.eviction_policy = EvictionPolicyType::Clock;
829        self
830    }
831
832    /// Set storage limits configuration
833    pub fn with_storage_limits(mut self, config: StorageLimitsConfig) -> Self {
834        self.storage_limits = config;
835        self
836    }
837
838    /// Set maximum database file size in bytes
839    pub fn with_max_database_size(mut self, size: u64) -> Self {
840        self.storage_limits.max_database_size = Some(size);
841        self
842    }
843
844    /// Set maximum database file size in megabytes
845    pub fn with_max_database_size_mb(mut self, mb: u64) -> Self {
846        self.storage_limits.max_database_size = Some(mb * 1024 * 1024);
847        self
848    }
849
850    /// Set the API key for authentication
851    ///
852    /// If the database requires authentication, provide the API key here.
853    /// Keys are in the format `fdb_<base64>`.
854    pub fn with_api_key<S: Into<String>>(mut self, key: S) -> Self {
855        self.api_key = Some(key.into());
856        self
857    }
858
859    /// Validate the database configuration
860    ///
861    /// # Validation Rules
862    /// - page_size: must be a power of 2 between 512 bytes and 64KB
863    /// - buffer_pool_pages: must be at least 16 pages
864    /// - wal_config: must pass validation
865    /// - compression: must pass validation
866    ///
867    /// # Errors
868    /// Returns `ConfigError` if validation fails
869    pub fn validate(&self) -> std::result::Result<(), crate::ConfigError> {
870        use crate::ConfigError;
871
872        const MIN_PAGE_SIZE: u64 = 512;
873        const MAX_PAGE_SIZE: u64 = 65536; // 64KB
874        const MIN_BUFFER_PAGES: u64 = 16;
875
876        // Validate page_size is power of 2
877        let page_size = self.page_size as u64;
878        if !page_size.is_power_of_two() {
879            return Err(ConfigError::InvalidValue {
880                field: "page_size".to_string(),
881                reason: format!("page_size must be a power of 2, got {}", page_size),
882            });
883        }
884
885        if page_size < MIN_PAGE_SIZE {
886            return Err(ConfigError::ValueTooLow {
887                field: "page_size".to_string(),
888                min: MIN_PAGE_SIZE,
889                actual: page_size,
890            });
891        }
892        if page_size > MAX_PAGE_SIZE {
893            return Err(ConfigError::ValueTooHigh {
894                field: "page_size".to_string(),
895                max: MAX_PAGE_SIZE,
896                actual: page_size,
897            });
898        }
899
900        // Validate buffer pool size
901        let buffer_pages = self.buffer_pool_pages as u64;
902        if buffer_pages < MIN_BUFFER_PAGES {
903            return Err(ConfigError::ValueTooLow {
904                field: "buffer_pool_pages".to_string(),
905                min: MIN_BUFFER_PAGES,
906                actual: buffer_pages,
907            });
908        }
909
910        // Validate nested configs
911        self.wal_config.validate()?;
912        self.compression.validate()?;
913        self.storage_limits.validate()?;
914
915        Ok(())
916    }
917}
918
919/// Constants for the database file format
920pub mod constants {
921    /// Magic bytes at the start of the database file
922    pub const MAGIC: &[u8; 10] = b"FEATHERDB\0";
923
924    /// Current file format version
925    pub const FORMAT_VERSION: u32 = 1;
926
927    /// Default page size
928    pub const DEFAULT_PAGE_SIZE: usize = 4096;
929
930    /// Superblock size in bytes
931    pub const SUPERBLOCK_SIZE: usize = 512;
932
933    /// Page header size
934    pub const PAGE_HEADER_SIZE: usize = 64;
935
936    /// Slot entry size (offset:u16, len:u16, flags:u8, reserved:u8)
937    pub const SLOT_SIZE: usize = 6;
938
939    /// Schema catalog root page number
940    pub const SCHEMA_ROOT_PAGE: u64 = 1;
941
942    /// Free list bitmap root page number
943    pub const FREELIST_ROOT_PAGE: u64 = 2;
944
945    /// First data page number
946    pub const FIRST_DATA_PAGE: u64 = 3;
947}
948
949#[cfg(test)]
950mod tests {
951    use super::*;
952
953    #[test]
954    fn test_transaction_config_default() {
955        let config = TransactionConfig::default();
956        // Default now has safe limits
957        assert_eq!(config.timeout_ms, Some(30_000));
958        assert_eq!(config.warn_after_ms, Some(10_000));
959    }
960
961    #[test]
962    fn test_transaction_config_new() {
963        let config = TransactionConfig::new();
964        // new() uses default() which has safe limits
965        assert_eq!(config.timeout_ms, Some(30_000));
966        assert_eq!(config.warn_after_ms, Some(10_000));
967    }
968
969    #[test]
970    fn test_transaction_config_no_limits() {
971        let config = TransactionConfig::no_limits();
972        assert_eq!(config.timeout_ms, None);
973        assert_eq!(config.warn_after_ms, None);
974    }
975
976    #[test]
977    fn test_transaction_config_with_timeout() {
978        // Start with no_limits() to test setting just timeout
979        let config = TransactionConfig::no_limits().with_timeout(5000);
980        assert_eq!(config.timeout_ms, Some(5000));
981        assert_eq!(config.warn_after_ms, None);
982
983        // Can also override default timeout
984        let config = TransactionConfig::new().with_timeout(5000);
985        assert_eq!(config.timeout_ms, Some(5000));
986        assert_eq!(config.warn_after_ms, Some(10_000)); // Default warning
987    }
988
989    #[test]
990    fn test_transaction_config_with_warning() {
991        // Start with no_limits() to test setting just warning
992        let config = TransactionConfig::no_limits().with_warning(1000);
993        assert_eq!(config.timeout_ms, None);
994        assert_eq!(config.warn_after_ms, Some(1000));
995
996        // Can also override default warning
997        let config = TransactionConfig::new().with_warning(1000);
998        assert_eq!(config.timeout_ms, Some(30_000)); // Default timeout
999        assert_eq!(config.warn_after_ms, Some(1000));
1000    }
1001
1002    #[test]
1003    fn test_transaction_config_with_timeout_and_warning() {
1004        let config = TransactionConfig::with_timeout_and_warning(5000, 1000);
1005        assert_eq!(config.timeout_ms, Some(5000));
1006        assert_eq!(config.warn_after_ms, Some(1000));
1007    }
1008
1009    #[test]
1010    fn test_transaction_config_builder_pattern() {
1011        let config = TransactionConfig::new()
1012            .with_timeout(10000)
1013            .with_warning(2000);
1014        assert_eq!(config.timeout_ms, Some(10000));
1015        assert_eq!(config.warn_after_ms, Some(2000));
1016    }
1017
1018    #[test]
1019    fn test_transaction_config_clone() {
1020        let config1 = TransactionConfig::new().with_timeout(5000);
1021        let config2 = config1.clone();
1022        assert_eq!(config1.timeout_ms, config2.timeout_ms);
1023        assert_eq!(config1.warn_after_ms, config2.warn_after_ms);
1024    }
1025
1026    // ============ Validation Tests ============
1027
1028    #[test]
1029    fn test_transaction_config_validate_success() {
1030        // Default config should be valid
1031        let config = TransactionConfig::default();
1032        assert!(config.validate().is_ok());
1033
1034        // Custom valid config
1035        let config = TransactionConfig::new()
1036            .with_timeout(5000)
1037            .with_warning(2000);
1038        assert!(config.validate().is_ok());
1039
1040        // No limits is valid
1041        let config = TransactionConfig::no_limits();
1042        assert!(config.validate().is_ok());
1043    }
1044
1045    #[test]
1046    fn test_transaction_config_validate_timeout_too_low() {
1047        let config = TransactionConfig::new().with_timeout(50); // < 100ms
1048        let err = config.validate().unwrap_err();
1049        assert!(matches!(err, ConfigError::ValueTooLow { .. }));
1050        assert!(err.to_string().contains("timeout_ms"));
1051        assert!(err.to_string().contains("100"));
1052    }
1053
1054    #[test]
1055    fn test_transaction_config_validate_timeout_too_high() {
1056        let config = TransactionConfig::new().with_timeout(4_000_000); // > 1 hour
1057        let err = config.validate().unwrap_err();
1058        assert!(matches!(err, ConfigError::ValueTooHigh { .. }));
1059        assert!(err.to_string().contains("timeout_ms"));
1060        assert!(err.to_string().contains("3600000"));
1061    }
1062
1063    #[test]
1064    fn test_transaction_config_validate_warn_too_low() {
1065        let config = TransactionConfig::new().with_warning(50); // < 100ms
1066        let err = config.validate().unwrap_err();
1067        assert!(matches!(err, ConfigError::ValueTooLow { .. }));
1068        assert!(err.to_string().contains("warn_after_ms"));
1069    }
1070
1071    #[test]
1072    fn test_transaction_config_validate_warn_too_high() {
1073        let config = TransactionConfig::new().with_warning(4_000_000); // > 1 hour
1074        let err = config.validate().unwrap_err();
1075        assert!(matches!(err, ConfigError::ValueTooHigh { .. }));
1076        assert!(err.to_string().contains("warn_after_ms"));
1077    }
1078
1079    #[test]
1080    fn test_transaction_config_validate_warn_greater_than_timeout() {
1081        let config = TransactionConfig::with_timeout_and_warning(5000, 6000); // warn > timeout
1082        let err = config.validate().unwrap_err();
1083        assert!(matches!(err, ConfigError::InvalidValue { .. }));
1084        assert!(err.to_string().contains("warn_after_ms"));
1085        assert!(err.to_string().contains("must be less than timeout_ms"));
1086    }
1087
1088    #[test]
1089    fn test_transaction_config_validate_warn_equal_timeout() {
1090        let config = TransactionConfig::with_timeout_and_warning(5000, 5000); // warn == timeout
1091        let err = config.validate().unwrap_err();
1092        assert!(matches!(err, ConfigError::InvalidValue { .. }));
1093    }
1094
1095    #[test]
1096    fn test_wal_config_validate_success() {
1097        let config = WalGroupCommitConfig::default();
1098        assert!(config.validate().is_ok());
1099
1100        let config = WalGroupCommitConfig::new()
1101            .group_commit_interval_ms(50)
1102            .group_commit_max_batch(500);
1103        assert!(config.validate().is_ok());
1104    }
1105
1106    #[test]
1107    fn test_wal_config_validate_interval_too_low() {
1108        let config = WalGroupCommitConfig::new().group_commit_interval_ms(0); // < 1ms
1109        let err = config.validate().unwrap_err();
1110        assert!(matches!(err, ConfigError::ValueTooLow { .. }));
1111        assert!(err.to_string().contains("group_commit_interval_ms"));
1112    }
1113
1114    #[test]
1115    fn test_wal_config_validate_interval_too_high() {
1116        let config = WalGroupCommitConfig::new().group_commit_interval_ms(2000); // > 1000ms
1117        let err = config.validate().unwrap_err();
1118        assert!(matches!(err, ConfigError::ValueTooHigh { .. }));
1119        assert!(err.to_string().contains("group_commit_interval_ms"));
1120    }
1121
1122    #[test]
1123    fn test_wal_config_validate_batch_too_high() {
1124        let config = WalGroupCommitConfig::new().group_commit_max_batch(200_000); // > 100,000
1125        let err = config.validate().unwrap_err();
1126        assert!(matches!(err, ConfigError::ValueTooHigh { .. }));
1127        assert!(err.to_string().contains("group_commit_max_batch"));
1128    }
1129
1130    #[test]
1131    fn test_compression_config_validate_success() {
1132        assert!(CompressionConfig::default().validate().is_ok());
1133        assert!(CompressionConfig::lz4().validate().is_ok());
1134        assert!(CompressionConfig::zstd().validate().is_ok());
1135        assert!(CompressionConfig::zstd_level(9).validate().is_ok());
1136    }
1137
1138    #[test]
1139    fn test_compression_config_validate_threshold_too_low() {
1140        let config = CompressionConfig::lz4().with_threshold(32); // < 64 bytes
1141        let err = config.validate().unwrap_err();
1142        assert!(matches!(err, ConfigError::ValueTooLow { .. }));
1143        assert!(err.to_string().contains("threshold"));
1144    }
1145
1146    #[test]
1147    fn test_compression_config_validate_threshold_too_high() {
1148        let config = CompressionConfig::lz4().with_threshold(100_000); // > 64KB
1149        let err = config.validate().unwrap_err();
1150        assert!(matches!(err, ConfigError::ValueTooHigh { .. }));
1151        assert!(err.to_string().contains("threshold"));
1152    }
1153
1154    #[test]
1155    fn test_compression_config_validate_zstd_level_too_low() {
1156        let config = CompressionConfig::zstd_level(0); // < 1
1157        let err = config.validate().unwrap_err();
1158        assert!(matches!(err, ConfigError::InvalidValue { .. }));
1159        assert!(err.to_string().contains("ZSTD"));
1160        assert!(err.to_string().contains("too low"));
1161    }
1162
1163    #[test]
1164    fn test_compression_config_validate_zstd_level_too_high() {
1165        let config = CompressionConfig::zstd_level(25); // > 22
1166        let err = config.validate().unwrap_err();
1167        assert!(matches!(err, ConfigError::InvalidValue { .. }));
1168        assert!(err.to_string().contains("ZSTD"));
1169        assert!(err.to_string().contains("too high"));
1170    }
1171
1172    #[test]
1173    fn test_config_validate_success() {
1174        let config = Config::default();
1175        assert!(config.validate().is_ok());
1176
1177        let config = Config::new("test.db")
1178            .page_size(8192)
1179            .buffer_pool_size_mb(128);
1180        assert!(config.validate().is_ok());
1181    }
1182
1183    #[test]
1184    fn test_config_validate_page_size_not_power_of_two() {
1185        let mut config = Config::default();
1186        config.page_size = 3000; // Not a power of 2
1187        let err = config.validate().unwrap_err();
1188        assert!(matches!(err, ConfigError::InvalidValue { .. }));
1189        assert!(err.to_string().contains("power of 2"));
1190    }
1191
1192    #[test]
1193    fn test_config_validate_page_size_too_low() {
1194        let mut config = Config::default();
1195        config.page_size = 256; // < 512 bytes
1196        let err = config.validate().unwrap_err();
1197        assert!(matches!(err, ConfigError::ValueTooLow { .. }));
1198        assert!(err.to_string().contains("page_size"));
1199    }
1200
1201    #[test]
1202    fn test_config_validate_page_size_too_high() {
1203        let mut config = Config::default();
1204        config.page_size = 128 * 1024; // > 64KB
1205        let err = config.validate().unwrap_err();
1206        assert!(matches!(err, ConfigError::ValueTooHigh { .. }));
1207        assert!(err.to_string().contains("page_size"));
1208    }
1209
1210    #[test]
1211    fn test_config_validate_buffer_pool_too_small() {
1212        let mut config = Config::default();
1213        config.buffer_pool_pages = 8; // < 16 pages
1214        let err = config.validate().unwrap_err();
1215        assert!(matches!(err, ConfigError::ValueTooLow { .. }));
1216        assert!(err.to_string().contains("buffer_pool_pages"));
1217    }
1218
1219    #[test]
1220    fn test_config_validate_nested_wal_config() {
1221        let mut config = Config::default();
1222        config.wal_config.group_commit_interval_ms = 2000; // Invalid
1223        let err = config.validate().unwrap_err();
1224        assert!(matches!(err, ConfigError::ValueTooHigh { .. }));
1225        assert!(err.to_string().contains("group_commit_interval_ms"));
1226    }
1227
1228    #[test]
1229    fn test_config_validate_nested_compression_config() {
1230        let mut config = Config::default();
1231        config.compression.threshold = 32; // Invalid
1232        let err = config.validate().unwrap_err();
1233        assert!(matches!(err, ConfigError::ValueTooLow { .. }));
1234        assert!(err.to_string().contains("threshold"));
1235    }
1236
1237    // ============ StorageLimitsConfig Tests ============
1238
1239    #[test]
1240    fn test_storage_limits_config_default() {
1241        let config = StorageLimitsConfig::default();
1242        assert_eq!(config.max_database_size, None);
1243        assert_eq!(config.max_wal_size, None);
1244        assert_eq!(config.safety_margin_percent, 5);
1245    }
1246
1247    #[test]
1248    fn test_storage_limits_config_new() {
1249        let config = StorageLimitsConfig::new();
1250        assert_eq!(config.max_database_size, None);
1251        assert_eq!(config.max_wal_size, None);
1252        assert_eq!(config.safety_margin_percent, 5);
1253    }
1254
1255    #[test]
1256    fn test_storage_limits_config_with_max_database_size() {
1257        let config = StorageLimitsConfig::new().with_max_database_size(100_000_000);
1258        assert_eq!(config.max_database_size, Some(100_000_000));
1259    }
1260
1261    #[test]
1262    fn test_storage_limits_config_with_max_database_size_mb() {
1263        let config = StorageLimitsConfig::new().with_max_database_size_mb(100);
1264        assert_eq!(config.max_database_size, Some(100 * 1024 * 1024));
1265    }
1266
1267    #[test]
1268    fn test_storage_limits_config_with_max_wal_size() {
1269        let config = StorageLimitsConfig::new().with_max_wal_size(10_000_000);
1270        assert_eq!(config.max_wal_size, Some(10_000_000));
1271    }
1272
1273    #[test]
1274    fn test_storage_limits_config_with_safety_margin_percent() {
1275        let config = StorageLimitsConfig::new().with_safety_margin_percent(10);
1276        assert_eq!(config.safety_margin_percent, 10);
1277    }
1278
1279    #[test]
1280    fn test_storage_limits_config_safety_margin_capped_at_50() {
1281        let config = StorageLimitsConfig::new().with_safety_margin_percent(80);
1282        assert_eq!(config.safety_margin_percent, 50); // Capped at 50
1283    }
1284
1285    #[test]
1286    fn test_storage_limits_config_effective_database_limit() {
1287        let config = StorageLimitsConfig::new()
1288            .with_max_database_size(100_000_000)
1289            .with_safety_margin_percent(5);
1290        assert_eq!(config.effective_database_limit(), Some(95_000_000)); // 100M - 5%
1291    }
1292
1293    #[test]
1294    fn test_storage_limits_config_effective_database_limit_none() {
1295        let config = StorageLimitsConfig::new();
1296        assert_eq!(config.effective_database_limit(), None);
1297    }
1298
1299    #[test]
1300    fn test_storage_limits_config_validate_success() {
1301        // Default is valid
1302        assert!(StorageLimitsConfig::default().validate().is_ok());
1303
1304        // Valid with limits
1305        let config = StorageLimitsConfig::new()
1306            .with_max_database_size(10 * 1024 * 1024) // 10MB
1307            .with_max_wal_size(1024 * 1024); // 1MB
1308        assert!(config.validate().is_ok());
1309    }
1310
1311    #[test]
1312    fn test_storage_limits_config_validate_database_size_too_low() {
1313        let config = StorageLimitsConfig::new().with_max_database_size(500_000); // < 1MB
1314        let err = config.validate().unwrap_err();
1315        assert!(matches!(err, ConfigError::ValueTooLow { .. }));
1316        assert!(err.to_string().contains("max_database_size"));
1317        assert!(err.to_string().contains("1048576")); // 1MB
1318    }
1319
1320    #[test]
1321    fn test_storage_limits_config_validate_wal_size_too_low() {
1322        let config = StorageLimitsConfig::new().with_max_wal_size(32_000); // < 64KB
1323        let err = config.validate().unwrap_err();
1324        assert!(matches!(err, ConfigError::ValueTooLow { .. }));
1325        assert!(err.to_string().contains("max_wal_size"));
1326        assert!(err.to_string().contains("65536")); // 64KB
1327    }
1328
1329    #[test]
1330    fn test_storage_limits_config_validate_safety_margin_too_high() {
1331        let mut config = StorageLimitsConfig::new();
1332        config.safety_margin_percent = 60; // > 50%, manually set to bypass cap
1333        let err = config.validate().unwrap_err();
1334        assert!(matches!(err, ConfigError::ValueTooHigh { .. }));
1335        assert!(err.to_string().contains("safety_margin_percent"));
1336        assert!(err.to_string().contains("50"));
1337    }
1338
1339    #[test]
1340    fn test_config_with_storage_limits() {
1341        let storage_limits = StorageLimitsConfig::new()
1342            .with_max_database_size_mb(100)
1343            .with_max_wal_size(10_000_000);
1344
1345        let config = Config::new("test.db").with_storage_limits(storage_limits);
1346        assert_eq!(
1347            config.storage_limits.max_database_size,
1348            Some(100 * 1024 * 1024)
1349        );
1350        assert_eq!(config.storage_limits.max_wal_size, Some(10_000_000));
1351    }
1352
1353    #[test]
1354    fn test_config_with_max_database_size() {
1355        let config = Config::new("test.db").with_max_database_size(50_000_000);
1356        assert_eq!(config.storage_limits.max_database_size, Some(50_000_000));
1357    }
1358
1359    #[test]
1360    fn test_config_with_max_database_size_mb() {
1361        let config = Config::new("test.db").with_max_database_size_mb(50);
1362        assert_eq!(
1363            config.storage_limits.max_database_size,
1364            Some(50 * 1024 * 1024)
1365        );
1366    }
1367
1368    #[test]
1369    fn test_config_validate_nested_storage_limits() {
1370        let mut config = Config::default();
1371        config.storage_limits.max_database_size = Some(500_000); // Invalid (< 1MB)
1372        let err = config.validate().unwrap_err();
1373        assert!(matches!(err, ConfigError::ValueTooLow { .. }));
1374        assert!(err.to_string().contains("max_database_size"));
1375    }
1376}