Skip to main content

oxirs_tdb/store/
store_params.rs

1//! Store parameters and configuration builder
2//!
3//! Provides comprehensive configuration management inspired by Apache Jena TDB2's StoreParams.
4//! Supports:
5//! - Builder pattern for flexible configuration
6//! - Parameter validation and constraints
7//! - Serialization/deserialization for configuration files
8//! - Default presets (development, production, performance)
9//! - Parameter inheritance and overlays
10
11use crate::error::{Result, TdbError};
12use serde::{Deserialize, Serialize};
13use std::path::{Path, PathBuf};
14
15/// Store parameters containing all configuration options
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct StoreParams {
18    // Storage configuration
19    /// Directory for storing RDF data
20    pub data_dir: PathBuf,
21    /// Page size in bytes (must be power of 2)
22    pub page_size: usize,
23    /// Buffer pool size (number of pages to cache)
24    pub buffer_pool_size: usize,
25
26    // Index configuration
27    /// Enable Subject-Predicate-Object index.
28    /// Reserved — not yet honored: the engine always maintains all three triple
29    /// indexes (SPO/POS/OSP) for optimal pattern selection.
30    pub enable_spo_index: bool,
31    /// Enable Predicate-Object-Subject index.
32    /// Reserved — not yet honored (see [`enable_spo_index`](Self::enable_spo_index)).
33    pub enable_pos_index: bool,
34    /// Enable Object-Subject-Predicate index.
35    /// Reserved — not yet honored (see [`enable_spo_index`](Self::enable_spo_index)).
36    pub enable_osp_index: bool,
37    /// Enable quad (named graph) indexes. Honored: threaded to
38    /// [`TdbConfig::enable_quad_indexes`](crate::store::TdbConfig).
39    pub enable_quad_indexes: bool,
40
41    // Dictionary configuration
42    /// Enable inline storage of small values.
43    /// Reserved — not yet honored by the dictionary encoder.
44    pub enable_inline_values: bool,
45    /// Enable prefix compression for URIs.
46    /// Reserved — not yet honored: URI compression is gated by
47    /// [`enable_compression`](Self::enable_compression) /
48    /// [`compression_algorithm`](Self::compression_algorithm), not this flag.
49    pub enable_prefix_compression: bool,
50    /// Dictionary cache size (number of entries).
51    /// Reserved — not yet honored by the dictionary cache.
52    pub dictionary_cache_size: usize,
53
54    // Transaction configuration
55    /// Enable write-ahead logging for durability
56    pub enable_wal: bool,
57    /// Write-ahead log buffer size in bytes
58    pub wal_buffer_size: usize,
59    /// Maximum transaction size (number of triples).
60    /// Reserved — not yet honored by the transaction manager.
61    pub max_transaction_size: usize,
62    /// Transaction timeout in seconds.
63    /// Reserved — not yet honored by the transaction manager.
64    pub transaction_timeout_secs: u64,
65
66    // Compression configuration
67    /// Enable data compression
68    pub enable_compression: bool,
69    /// Compression algorithm to use
70    pub compression_algorithm: CompressionAlgorithm,
71    /// Compression level (algorithm-specific)
72    pub compression_level: u32,
73
74    // Bloom filter configuration
75    /// Enable bloom filters for indexes
76    pub enable_bloom_filters: bool,
77    /// Bloom filter false positive rate (0.0 to 1.0)
78    pub bloom_filter_fpr: f64,
79    /// Bloom filter size per index
80    pub bloom_filter_size_per_index: usize,
81
82    // Query optimization
83    /// Enable query result caching
84    pub enable_query_cache: bool,
85    /// Query cache size (number of cached queries)
86    pub query_cache_size: usize,
87    /// Enable statistics collection
88    pub enable_statistics: bool,
89    /// Statistics sampling rate (0.0 to 1.0)
90    pub statistics_sample_rate: f64,
91
92    // Query monitoring
93    /// Enable query performance monitoring
94    pub enable_query_monitoring: bool,
95    /// Slow query threshold in milliseconds
96    pub slow_query_threshold_ms: u64,
97    /// Query timeout in milliseconds
98    pub query_timeout_ms: u64,
99
100    // Spatial indexing
101    /// Enable spatial indexing for geospatial queries
102    pub enable_spatial_indexing: bool,
103    /// Maximum entries per spatial index node
104    pub spatial_index_max_entries: usize,
105
106    // Production features
107    /// Enable diagnostic logging and error reporting.
108    /// Reserved — not yet honored: the diagnostic engine is always constructed.
109    pub enable_diagnostics: bool,
110    /// Enable metrics collection and export.
111    /// Reserved — not yet honored by the storage engine.
112    pub enable_metrics: bool,
113    /// Enable performance profiling.
114    /// Reserved — not yet honored by the storage engine.
115    pub enable_profiling: bool,
116
117    // Connection pooling
118    /// Minimum number of connections to maintain.
119    /// Reserved — not yet honored: the store has no connection pool.
120    pub min_connections: usize,
121    /// Maximum number of concurrent connections.
122    /// Reserved — not yet honored: the store has no connection pool.
123    pub max_connections: usize,
124    /// Connection timeout in seconds.
125    /// Reserved — not yet honored: the store has no connection pool.
126    pub connection_timeout_secs: u64,
127
128    // Backup configuration
129    /// Enable online (hot) backups.
130    /// Reserved — not yet honored: backups are driven by a separate API.
131    pub enable_online_backup: bool,
132    /// Number of days to retain backups.
133    /// Reserved — not yet honored: backups are driven by a separate API.
134    pub backup_retention_days: u32,
135    /// Enable backup encryption.
136    /// Reserved — not yet honored: backups are driven by a separate API.
137    pub enable_backup_encryption: bool,
138
139    // Performance tuning
140    /// Enable direct I/O bypassing the OS cache. Honored: threaded to
141    /// [`TdbConfig::enable_direct_io`](crate::store::TdbConfig), which gates the
142    /// opt-in [`DirectIOFile`](crate::storage::direct_io::DirectIOFile) path for
143    /// large sequential scans (default `false`, unix-only).
144    pub enable_direct_io: bool,
145    /// Enable asynchronous I/O operations.
146    /// Reserved — not yet honored by the storage engine.
147    pub enable_async_io: bool,
148    /// Enable NUMA (Non-Uniform Memory Access) awareness.
149    /// Reserved — not yet honored by the storage engine.
150    pub enable_numa_awareness: bool,
151    /// Enable GPU acceleration for computations.
152    /// Reserved — not yet honored by the storage engine.
153    pub enable_gpu_acceleration: bool,
154
155    // Distributed systems
156    /// Enable data replication.
157    /// Reserved — not yet honored by the storage engine.
158    pub enable_replication: bool,
159    /// Replication mode (master-slave, master-master, etc.).
160    /// Reserved — not yet honored by the storage engine.
161    pub replication_mode: ReplicationMode,
162}
163
164/// Compression algorithm selection
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166pub enum CompressionAlgorithm {
167    /// No compression
168    None,
169    /// LZ4 - fast compression
170    Lz4,
171    /// Zstandard - balanced compression
172    Zstd,
173    /// Brotli - high compression
174    Brotli,
175    /// Snappy - ultra-fast compression
176    Snappy,
177}
178
179/// Replication mode
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
181pub enum ReplicationMode {
182    /// No replication
183    None,
184    /// Master-slave replication
185    MasterSlave,
186    /// Master-master replication
187    MasterMaster,
188}
189
190impl StoreParams {
191    /// Create new store parameters with minimal configuration
192    pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
193        Self {
194            // Storage configuration
195            data_dir: data_dir.as_ref().to_path_buf(),
196            page_size: 4096,
197            buffer_pool_size: 1000,
198
199            // Index configuration
200            enable_spo_index: true,
201            enable_pos_index: true,
202            enable_osp_index: true,
203            enable_quad_indexes: false,
204
205            // Dictionary configuration
206            enable_inline_values: true,
207            enable_prefix_compression: true,
208            dictionary_cache_size: 10000,
209
210            // Transaction configuration
211            enable_wal: true,
212            wal_buffer_size: 1024 * 1024, // 1MB
213            max_transaction_size: 1_000_000,
214            transaction_timeout_secs: 60,
215
216            // Compression configuration
217            enable_compression: true,
218            compression_algorithm: CompressionAlgorithm::Lz4,
219            compression_level: 3,
220
221            // Bloom filter configuration
222            enable_bloom_filters: true,
223            bloom_filter_fpr: 0.01,
224            bloom_filter_size_per_index: 1_000_000,
225
226            // Query optimization
227            enable_query_cache: true,
228            query_cache_size: 1000,
229            enable_statistics: true,
230            statistics_sample_rate: 1.0,
231
232            // Query monitoring
233            enable_query_monitoring: true,
234            slow_query_threshold_ms: 1000,
235            query_timeout_ms: 30_000,
236
237            // Spatial indexing
238            enable_spatial_indexing: true,
239            spatial_index_max_entries: 1000,
240
241            // Production features
242            enable_diagnostics: true,
243            enable_metrics: true,
244            enable_profiling: false,
245
246            // Connection pooling
247            min_connections: 2,
248            max_connections: 10,
249            connection_timeout_secs: 30,
250
251            // Backup configuration
252            enable_online_backup: true,
253            backup_retention_days: 7,
254            enable_backup_encryption: false,
255
256            // Performance tuning
257            enable_direct_io: false,
258            enable_async_io: false,
259            enable_numa_awareness: false,
260            enable_gpu_acceleration: false,
261
262            // Distributed systems
263            enable_replication: false,
264            replication_mode: ReplicationMode::None,
265        }
266    }
267
268    /// Validate parameters and return errors if invalid
269    pub fn validate(&self) -> Result<()> {
270        // Page size must be power of 2 and >= 512
271        if !self.page_size.is_power_of_two() || self.page_size < 512 {
272            return Err(TdbError::InvalidConfiguration(format!(
273                "Page size must be power of 2 and >= 512, got {}",
274                self.page_size
275            )));
276        }
277
278        // Buffer pool size must be > 0
279        if self.buffer_pool_size == 0 {
280            return Err(TdbError::InvalidConfiguration(
281                "Buffer pool size must be > 0".to_string(),
282            ));
283        }
284
285        // Bloom filter FPR must be between 0 and 1
286        if self.bloom_filter_fpr <= 0.0 || self.bloom_filter_fpr >= 1.0 {
287            return Err(TdbError::InvalidConfiguration(format!(
288                "Bloom filter FPR must be between 0 and 1, got {}",
289                self.bloom_filter_fpr
290            )));
291        }
292
293        // Statistics sample rate must be between 0 and 1
294        if self.statistics_sample_rate < 0.0 || self.statistics_sample_rate > 1.0 {
295            return Err(TdbError::InvalidConfiguration(format!(
296                "Statistics sample rate must be between 0 and 1, got {}",
297                self.statistics_sample_rate
298            )));
299        }
300
301        // Connection pool constraints
302        if self.min_connections > self.max_connections {
303            return Err(TdbError::InvalidConfiguration(format!(
304                "Min connections ({}) > max connections ({})",
305                self.min_connections, self.max_connections
306            )));
307        }
308
309        Ok(())
310    }
311
312    /// Save parameters to JSON file
313    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
314        let json = serde_json::to_string_pretty(self)
315            .map_err(|e| TdbError::Serialization(format!("Failed to serialize config: {}", e)))?;
316        std::fs::write(path, json)?;
317        Ok(())
318    }
319
320    /// Load parameters from JSON file
321    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
322        let json = std::fs::read_to_string(path)?;
323        let params: StoreParams = serde_json::from_str(&json)
324            .map_err(|e| TdbError::Deserialization(format!("Failed to parse config: {}", e)))?;
325        params.validate()?;
326        Ok(params)
327    }
328}
329
330/// Builder for store parameters with fluent API
331pub struct StoreParamsBuilder {
332    params: StoreParams,
333}
334
335impl StoreParamsBuilder {
336    /// Create new builder with default parameters
337    pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
338        Self {
339            params: StoreParams::new(data_dir),
340        }
341    }
342
343    /// Create builder from existing parameters
344    pub fn from_params(params: StoreParams) -> Self {
345        Self { params }
346    }
347
348    /// Set page size (must be power of 2)
349    pub fn page_size(mut self, size: usize) -> Self {
350        self.params.page_size = size;
351        self
352    }
353
354    /// Set buffer pool size
355    pub fn buffer_pool_size(mut self, size: usize) -> Self {
356        self.params.buffer_pool_size = size;
357        self
358    }
359
360    /// Enable/disable triple indexes
361    pub fn with_triple_indexes(mut self, spo: bool, pos: bool, osp: bool) -> Self {
362        self.params.enable_spo_index = spo;
363        self.params.enable_pos_index = pos;
364        self.params.enable_osp_index = osp;
365        self
366    }
367
368    /// Enable/disable quad indexes
369    pub fn with_quad_indexes(mut self, enable: bool) -> Self {
370        self.params.enable_quad_indexes = enable;
371        self
372    }
373
374    /// Enable/disable inline values optimization
375    pub fn with_inline_values(mut self, enable: bool) -> Self {
376        self.params.enable_inline_values = enable;
377        self
378    }
379
380    /// Enable/disable prefix compression
381    pub fn with_prefix_compression(mut self, enable: bool) -> Self {
382        self.params.enable_prefix_compression = enable;
383        self
384    }
385
386    /// Set dictionary cache size
387    pub fn dictionary_cache_size(mut self, size: usize) -> Self {
388        self.params.dictionary_cache_size = size;
389        self
390    }
391
392    /// Enable/disable write-ahead logging
393    pub fn with_wal(mut self, enable: bool) -> Self {
394        self.params.enable_wal = enable;
395        self
396    }
397
398    /// Set WAL buffer size
399    pub fn wal_buffer_size(mut self, size: usize) -> Self {
400        self.params.wal_buffer_size = size;
401        self
402    }
403
404    /// Set maximum transaction size
405    pub fn max_transaction_size(mut self, size: usize) -> Self {
406        self.params.max_transaction_size = size;
407        self
408    }
409
410    /// Set transaction timeout
411    pub fn transaction_timeout(mut self, seconds: u64) -> Self {
412        self.params.transaction_timeout_secs = seconds;
413        self
414    }
415
416    /// Configure compression
417    pub fn with_compression(mut self, algorithm: CompressionAlgorithm, level: u32) -> Self {
418        self.params.enable_compression = algorithm != CompressionAlgorithm::None;
419        self.params.compression_algorithm = algorithm;
420        self.params.compression_level = level;
421        self
422    }
423
424    /// Configure bloom filters
425    pub fn with_bloom_filters(mut self, enable: bool, fpr: f64, size: usize) -> Self {
426        self.params.enable_bloom_filters = enable;
427        self.params.bloom_filter_fpr = fpr;
428        self.params.bloom_filter_size_per_index = size;
429        self
430    }
431
432    /// Configure query cache
433    pub fn with_query_cache(mut self, enable: bool, size: usize) -> Self {
434        self.params.enable_query_cache = enable;
435        self.params.query_cache_size = size;
436        self
437    }
438
439    /// Configure statistics
440    pub fn with_statistics(mut self, enable: bool, sample_rate: f64) -> Self {
441        self.params.enable_statistics = enable;
442        self.params.statistics_sample_rate = sample_rate;
443        self
444    }
445
446    /// Configure query monitoring
447    pub fn with_query_monitoring(
448        mut self,
449        enable: bool,
450        slow_threshold_ms: u64,
451        timeout_ms: u64,
452    ) -> Self {
453        self.params.enable_query_monitoring = enable;
454        self.params.slow_query_threshold_ms = slow_threshold_ms;
455        self.params.query_timeout_ms = timeout_ms;
456        self
457    }
458
459    /// Configure spatial indexing
460    pub fn with_spatial_indexing(mut self, enable: bool, max_entries: usize) -> Self {
461        self.params.enable_spatial_indexing = enable;
462        self.params.spatial_index_max_entries = max_entries;
463        self
464    }
465
466    /// Configure production features
467    pub fn with_production_features(
468        mut self,
469        diagnostics: bool,
470        metrics: bool,
471        profiling: bool,
472    ) -> Self {
473        self.params.enable_diagnostics = diagnostics;
474        self.params.enable_metrics = metrics;
475        self.params.enable_profiling = profiling;
476        self
477    }
478
479    /// Configure connection pooling
480    pub fn with_connection_pool(mut self, min: usize, max: usize, timeout_secs: u64) -> Self {
481        self.params.min_connections = min;
482        self.params.max_connections = max;
483        self.params.connection_timeout_secs = timeout_secs;
484        self
485    }
486
487    /// Configure backup
488    pub fn with_backup(mut self, enable_online: bool, retention_days: u32, encrypt: bool) -> Self {
489        self.params.enable_online_backup = enable_online;
490        self.params.backup_retention_days = retention_days;
491        self.params.enable_backup_encryption = encrypt;
492        self
493    }
494
495    /// Configure performance features
496    pub fn with_performance_features(
497        mut self,
498        direct_io: bool,
499        async_io: bool,
500        numa: bool,
501        gpu: bool,
502    ) -> Self {
503        self.params.enable_direct_io = direct_io;
504        self.params.enable_async_io = async_io;
505        self.params.enable_numa_awareness = numa;
506        self.params.enable_gpu_acceleration = gpu;
507        self
508    }
509
510    /// Configure replication
511    pub fn with_replication(mut self, mode: ReplicationMode) -> Self {
512        self.params.enable_replication = mode != ReplicationMode::None;
513        self.params.replication_mode = mode;
514        self
515    }
516
517    /// Build and validate parameters
518    pub fn build(self) -> Result<StoreParams> {
519        self.params.validate()?;
520        Ok(self.params)
521    }
522
523    /// Build without validation (use with caution)
524    pub fn build_unchecked(self) -> StoreParams {
525        self.params
526    }
527}
528
529/// Preset configurations for common use cases
530pub struct StorePresets;
531
532impl StorePresets {
533    /// Development preset - optimized for fast iteration
534    pub fn development<P: AsRef<Path>>(data_dir: P) -> StoreParamsBuilder {
535        StoreParamsBuilder::new(data_dir)
536            .buffer_pool_size(100)
537            .with_compression(CompressionAlgorithm::None, 0)
538            .with_statistics(false, 0.0)
539            .with_production_features(false, false, false)
540            .with_performance_features(false, false, false, false)
541    }
542
543    /// Production preset - balanced for production workloads
544    pub fn production<P: AsRef<Path>>(data_dir: P) -> StoreParamsBuilder {
545        StoreParamsBuilder::new(data_dir)
546            .buffer_pool_size(10000)
547            .with_compression(CompressionAlgorithm::Lz4, 3)
548            .with_statistics(true, 1.0)
549            .with_production_features(true, true, false)
550            .with_backup(true, 30, true)
551            .with_connection_pool(5, 50, 60)
552    }
553
554    /// Performance preset - optimized for maximum throughput
555    pub fn performance<P: AsRef<Path>>(data_dir: P) -> StoreParamsBuilder {
556        StoreParamsBuilder::new(data_dir)
557            .buffer_pool_size(50000)
558            .with_compression(CompressionAlgorithm::Snappy, 1)
559            .with_bloom_filters(true, 0.001, 10_000_000)
560            .with_performance_features(true, true, true, true)
561            .with_query_cache(true, 10000)
562    }
563
564    /// Minimal preset - bare minimum configuration
565    pub fn minimal<P: AsRef<Path>>(data_dir: P) -> StoreParamsBuilder {
566        StoreParamsBuilder::new(data_dir)
567            .buffer_pool_size(10)
568            .with_compression(CompressionAlgorithm::None, 0)
569            .with_bloom_filters(false, 0.01, 0)
570            .with_query_cache(false, 0)
571            .with_statistics(false, 0.0)
572            .with_production_features(false, false, false)
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use std::env;
580    use std::path::PathBuf;
581
582    /// Temp directory path used as the store base path in tests. The path is
583    /// only stored on the params struct (never written to), so a shared temp
584    /// path is sufficient and collision-safe.
585    fn test_store_path() -> PathBuf {
586        env::temp_dir().join("oxirs_tdb_store_params_test")
587    }
588
589    #[test]
590    fn test_store_params_new() {
591        let params = StoreParams::new(test_store_path());
592        assert_eq!(params.page_size, 4096);
593        assert_eq!(params.buffer_pool_size, 1000);
594        assert!(params.enable_spo_index);
595    }
596
597    #[test]
598    fn test_store_params_validation() {
599        let params = StoreParams::new(test_store_path());
600        assert!(params.validate().is_ok());
601
602        let mut invalid_params = params.clone();
603        invalid_params.page_size = 1000; // Not power of 2
604        assert!(invalid_params.validate().is_err());
605
606        let mut invalid_params2 = params.clone();
607        invalid_params2.bloom_filter_fpr = 1.5; // Out of range
608        assert!(invalid_params2.validate().is_err());
609    }
610
611    #[test]
612    fn test_builder_basic() {
613        let params = StoreParamsBuilder::new(test_store_path())
614            .page_size(8192)
615            .buffer_pool_size(2000)
616            .build()
617            .unwrap();
618
619        assert_eq!(params.page_size, 8192);
620        assert_eq!(params.buffer_pool_size, 2000);
621    }
622
623    #[test]
624    fn test_builder_compression() {
625        let params = StoreParamsBuilder::new(test_store_path())
626            .with_compression(CompressionAlgorithm::Zstd, 5)
627            .build()
628            .unwrap();
629
630        assert!(params.enable_compression);
631        assert_eq!(params.compression_algorithm, CompressionAlgorithm::Zstd);
632        assert_eq!(params.compression_level, 5);
633    }
634
635    #[test]
636    fn test_builder_bloom_filters() {
637        let params = StoreParamsBuilder::new(test_store_path())
638            .with_bloom_filters(true, 0.001, 5_000_000)
639            .build()
640            .unwrap();
641
642        assert!(params.enable_bloom_filters);
643        assert_eq!(params.bloom_filter_fpr, 0.001);
644        assert_eq!(params.bloom_filter_size_per_index, 5_000_000);
645    }
646
647    #[test]
648    fn test_presets_development() {
649        let params = StorePresets::development(test_store_path())
650            .build()
651            .unwrap();
652        assert_eq!(params.buffer_pool_size, 100);
653        assert_eq!(params.compression_algorithm, CompressionAlgorithm::None);
654        assert!(!params.enable_statistics);
655    }
656
657    #[test]
658    fn test_presets_production() {
659        let params = StorePresets::production(test_store_path()).build().unwrap();
660        assert_eq!(params.buffer_pool_size, 10000);
661        assert!(params.enable_diagnostics);
662        assert!(params.enable_backup_encryption);
663    }
664
665    #[test]
666    fn test_presets_performance() {
667        let params = StorePresets::performance(test_store_path())
668            .build()
669            .unwrap();
670        assert_eq!(params.buffer_pool_size, 50000);
671        assert!(params.enable_direct_io);
672        assert!(params.enable_gpu_acceleration);
673    }
674
675    #[test]
676    fn test_save_load_params() {
677        let temp_dir = env::temp_dir();
678        let config_file = temp_dir.join("test_store_params.json");
679
680        let params = StoreParamsBuilder::new(&temp_dir)
681            .page_size(8192)
682            .buffer_pool_size(5000)
683            .build()
684            .unwrap();
685
686        params.save_to_file(&config_file).unwrap();
687        let loaded_params = StoreParams::load_from_file(&config_file).unwrap();
688
689        assert_eq!(params.page_size, loaded_params.page_size);
690        assert_eq!(params.buffer_pool_size, loaded_params.buffer_pool_size);
691    }
692
693    #[test]
694    fn test_replication_config() {
695        let params = StoreParamsBuilder::new(test_store_path())
696            .with_replication(ReplicationMode::MasterSlave)
697            .build()
698            .unwrap();
699
700        assert!(params.enable_replication);
701        assert_eq!(params.replication_mode, ReplicationMode::MasterSlave);
702    }
703
704    #[test]
705    fn test_connection_pool_validation() {
706        let result = StoreParamsBuilder::new(test_store_path())
707            .with_connection_pool(10, 5, 30) // min > max (invalid)
708            .build();
709
710        assert!(result.is_err());
711    }
712}