Skip to main content

heliosdb_nano/
config.rs

1//! Configuration for HeliosDB Lite
2//!
3//! This module provides comprehensive configuration options for the database.
4//! Configuration can be loaded from TOML files or constructed programmatically.
5//!
6//! # Configuration Sections
7//!
8//! - [`StorageConfig`] - Database path, WAL, compression, caching
9//! - [`EncryptionConfig`] - Encryption at rest with AES-256-GCM
10//! - [`ServerConfig`] - Network settings, TLS, connection limits
11//! - [`SessionConfig`] - Session timeouts and per-user limits
12//! - [`LockConfig`] - Lock acquisition timeouts, deadlock detection
13//! - [`DumpConfig`] - Backup scheduling and compression
14//! - [`VectorConfig`] - Vector index settings (HNSW, PQ)
15//!
16//! # Example: TOML Configuration
17//!
18//! ```toml
19//! [storage]
20//! path = "./data"
21//! memory_only = false
22//! wal_enabled = true
23//! compression = "Zstd"
24//!
25//! [server]
26//! listen_addr = "0.0.0.0"
27//! port = 5432
28//! max_connections = 100
29//!
30//! [session]
31//! timeout_secs = 3600
32//! max_sessions_per_user = 10
33//! ```
34//!
35//! # Example: Programmatic Configuration
36//!
37//! ```rust
38//! use heliosdb_nano::Config;
39//!
40//! // In-memory database
41//! let config = Config::in_memory();
42//!
43//! // Load from file
44//! // let config = Config::from_file("heliosdb.toml")?;
45//! ```
46
47use serde::{Deserialize, Serialize};
48use std::path::PathBuf;
49
50/// Database configuration
51///
52/// The main configuration struct for HeliosDB Lite. All configuration
53/// sections use sensible defaults and can be customized individually.
54///
55/// # Loading Configuration
56///
57/// ```rust,no_run
58/// use heliosdb_nano::Config;
59///
60/// // Default configuration with file-based storage
61/// let config = Config::default();
62///
63/// // In-memory database (for testing)
64/// let config = Config::in_memory();
65///
66/// // Load from TOML file
67/// let config = Config::from_file("heliosdb.toml")?;
68/// # Ok::<(), Box<dyn std::error::Error>>(())
69/// ```
70///
71/// # Validation
72///
73/// Call [`validate()`](Config::validate) to check all configuration values
74/// before using the configuration.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct Config {
77    /// Storage configuration
78    #[serde(default)]
79    pub storage: StorageConfig,
80    /// Encryption configuration
81    #[serde(default)]
82    pub encryption: EncryptionConfig,
83    /// Server configuration
84    #[serde(default)]
85    pub server: ServerConfig,
86    /// Performance configuration
87    #[serde(default)]
88    pub performance: PerformanceConfig,
89    /// Audit configuration
90    #[serde(default)]
91    pub audit: crate::audit::AuditConfig,
92    /// Optimizer configuration (v2.1)
93    #[serde(default)]
94    pub optimizer: OptimizerConfig,
95    /// Authentication configuration (v2.1)
96    #[serde(default)]
97    pub authentication: AuthenticationConfig,
98    /// Compression configuration (v2.1)
99    #[serde(default)]
100    pub compression: CompressionConfig,
101    /// Materialized view configuration (v2.1)
102    #[serde(default)]
103    pub materialized_views: MaterializedViewConfig,
104    /// Vector index configuration (v2.1)
105    #[serde(default)]
106    pub vector: VectorConfig,
107    /// Sync configuration (v2.3 - Experimental)
108    #[serde(default)]
109    pub sync: SyncConfig,
110    /// Session configuration (v3.1.0)
111    #[serde(default)]
112    pub session: SessionConfig,
113    /// Lock configuration (v3.1.0)
114    #[serde(default)]
115    pub locks: LockConfig,
116    /// Dump configuration (v3.1.0)
117    #[serde(default)]
118    pub dump: DumpConfig,
119    /// Resource quota configuration (v3.1.0)
120    #[serde(default)]
121    pub resource_quotas: ResourceQuotaConfig,
122    /// BaaS REST API configuration (v3.2.0)
123    #[serde(default)]
124    pub api: ApiConfig,
125}
126
127impl Default for Config {
128    fn default() -> Self {
129        Self {
130            storage: StorageConfig::default(),
131            encryption: EncryptionConfig::default(),
132            server: ServerConfig::default(),
133            performance: PerformanceConfig::default(),
134            audit: crate::audit::AuditConfig::default(),
135            optimizer: OptimizerConfig::default(),
136            authentication: AuthenticationConfig::default(),
137            compression: CompressionConfig::default(),
138            materialized_views: MaterializedViewConfig::default(),
139            vector: VectorConfig::default(),
140            sync: SyncConfig::default(),
141            session: SessionConfig::default(),
142            locks: LockConfig::default(),
143            dump: DumpConfig::default(),
144            resource_quotas: ResourceQuotaConfig::default(),
145            api: ApiConfig::default(),
146        }
147    }
148}
149
150impl Config {
151    /// Create in-memory configuration
152    pub fn in_memory() -> Self {
153        Self {
154            storage: StorageConfig {
155                path: None,
156                memory_only: true,
157                wal_enabled: false, // No WAL needed for in-memory mode
158                ..Default::default()
159            },
160            audit: crate::audit::AuditConfig::default(),
161            ..Default::default()
162        }
163    }
164
165    /// Load configuration from file
166    pub fn from_file(path: impl AsRef<std::path::Path>) -> crate::Result<Self> {
167        let content = std::fs::read_to_string(path)?;
168        let config: Config = toml::from_str(&content)
169            .map_err(|e| crate::Error::config(format!("Failed to parse config: {}", e)))?;
170        Ok(config)
171    }
172
173    /// Save configuration to file
174    pub fn save_to_file(&self, path: impl AsRef<std::path::Path>) -> crate::Result<()> {
175        let content = toml::to_string_pretty(self)
176            .map_err(|e| crate::Error::config(format!("Failed to serialize config: {}", e)))?;
177        std::fs::write(path, content)?;
178        Ok(())
179    }
180
181    /// Validate all configuration sections
182    pub fn validate(&self) -> crate::Result<()> {
183        self.session.validate()?;
184        self.locks.validate()?;
185        self.dump.validate()?;
186        self.resource_quotas.validate()?;
187        Ok(())
188    }
189}
190
191/// Storage configuration
192#[derive(Debug, Clone, Serialize, Deserialize)]
193#[serde(default)]
194pub struct StorageConfig {
195    /// Database path (None for in-memory)
196    pub path: Option<PathBuf>,
197    /// Memory-only mode
198    pub memory_only: bool,
199    /// Write-ahead log enabled
200    pub wal_enabled: bool,
201    /// WAL synchronization mode (sync, async, or group_commit)
202    pub wal_sync_mode: WalSyncModeConfig,
203    /// Maximum memory for cache (bytes)
204    pub cache_size: usize,
205    /// Compression enabled
206    pub compression: CompressionType,
207    /// Automatic time-travel versioning enabled (default: true)
208    ///
209    /// When enabled, all insert/update operations automatically create
210    /// versioned snapshots for time-travel queries. This enables AS OF
211    /// TIMESTAMP/TRANSACTION/SCN queries with zero configuration.
212    ///
213    /// Set to false to disable automatic versioning and reduce write overhead
214    /// for workloads that don't require time-travel queries.
215    pub time_travel_enabled: bool,
216    /// Query timeout in milliseconds (None for unlimited)
217    ///
218    /// If set, queries that exceed this duration will be automatically
219    /// terminated to prevent resource exhaustion. Applies to the entire
220    /// query execution from start to finish.
221    ///
222    /// Default: None (unlimited)
223    /// Recommended: 30000 (30 seconds) for production environments
224    pub query_timeout_ms: Option<u64>,
225    /// Statement timeout in milliseconds (None for unlimited)
226    ///
227    /// If set, individual statement operations (e.g., a single scan or join)
228    /// that exceed this duration will be terminated. This provides finer-grained
229    /// timeout control than query_timeout_ms.
230    ///
231    /// Default: None (unlimited)
232    /// Note: Currently not implemented, reserved for future use
233    pub statement_timeout_ms: Option<u64>,
234    /// Transaction isolation level
235    ///
236    /// Controls the visibility of concurrent transaction changes.
237    /// Default: ReadCommitted (standard PostgreSQL default)
238    pub transaction_isolation: TransactionIsolation,
239    /// Slow query log threshold in milliseconds (None to disable)
240    ///
241    /// Queries exceeding this threshold are logged at WARN level with their
242    /// SQL text (truncated to 200 chars), duration, and row count.
243    ///
244    /// Default: Some(1000) (1 second)
245    #[serde(default = "default_slow_query_threshold")]
246    pub slow_query_threshold_ms: Option<u64>,
247}
248
249fn default_slow_query_threshold() -> Option<u64> {
250    Some(1000)
251}
252
253fn default_idle_timeout_secs() -> u64 {
254    300 // 5 minutes
255}
256
257impl Default for StorageConfig {
258    fn default() -> Self {
259        Self {
260            path: Some(PathBuf::from("./heliosdb-data")),
261            memory_only: false,
262            wal_enabled: true,
263            wal_sync_mode: WalSyncModeConfig::Sync, // Safest default for single-user workloads
264            cache_size: 512 * 1024 * 1024, // 512 MB
265            compression: CompressionType::Zstd,
266            time_travel_enabled: true, // Enable by default for zero-config transparency
267            query_timeout_ms: None, // Unlimited by default
268            statement_timeout_ms: None, // Unlimited by default
269            transaction_isolation: TransactionIsolation::ReadCommitted, // PostgreSQL default
270            slow_query_threshold_ms: Some(1000), // 1 second default
271        }
272    }
273}
274
275/// WAL synchronization mode configuration
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
277#[serde(rename_all = "snake_case")]
278pub enum WalSyncModeConfig {
279    /// Synchronous - fsync on every write (safest, slowest)
280    Sync,
281    /// Asynchronous - OS-managed flush (faster, less safe)
282    Async,
283    /// Group commit - batch multiple operations (balanced)
284    GroupCommit,
285}
286
287/// Compression type
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
289pub enum CompressionType {
290    /// No compression
291    None,
292    /// Zstandard compression (recommended)
293    Zstd,
294    /// LZ4 compression (faster, lower ratio)
295    Lz4,
296}
297
298/// Transaction isolation level
299///
300/// Controls how transactions see concurrent changes from other transactions.
301/// HeliosDB-Lite implements snapshot isolation by default.
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
303#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
304pub enum TransactionIsolation {
305    /// Read Uncommitted (not recommended)
306    ///
307    /// Transactions can see uncommitted changes from other transactions (dirty reads).
308    /// Not fully supported - maps to ReadCommitted for safety.
309    ReadUncommitted,
310    /// Read Committed (PostgreSQL default)
311    ///
312    /// Transactions only see committed changes from other transactions.
313    /// Each statement sees a fresh snapshot at statement start.
314    ReadCommitted,
315    /// Repeatable Read
316    ///
317    /// Transactions see a consistent snapshot from transaction start.
318    /// No dirty reads, non-repeatable reads, or phantom reads.
319    RepeatableRead,
320    /// Serializable (strictest)
321    ///
322    /// Transactions execute as if they run serially.
323    /// Prevents all anomalies but may cause serialization failures.
324    Serializable,
325}
326
327/// Encryption configuration
328#[derive(Debug, Clone, Serialize, Deserialize)]
329#[serde(default)]
330pub struct EncryptionConfig {
331    /// Encryption enabled
332    pub enabled: bool,
333    /// Encryption algorithm
334    pub algorithm: EncryptionAlgorithm,
335    /// Key source
336    pub key_source: KeySource,
337    /// Key rotation interval (days)
338    pub rotation_interval_days: u32,
339    /// Zero-Knowledge Encryption mode (v3.5)
340    #[serde(default)]
341    pub zke: ZkeEncryptionConfig,
342}
343
344impl Default for EncryptionConfig {
345    fn default() -> Self {
346        Self {
347            enabled: false,
348            algorithm: EncryptionAlgorithm::Aes256Gcm,
349            key_source: KeySource::Environment("HELIOSDB_ENCRYPTION_KEY".to_string()),
350            rotation_interval_days: 90,
351            zke: ZkeEncryptionConfig::default(),
352        }
353    }
354}
355
356/// Zero-Knowledge Encryption configuration (v3.5)
357///
358/// ZKE ensures encryption keys never leave the client. The server only
359/// ever sees encrypted data and cannot decrypt without client-provided keys.
360///
361/// # Security Properties
362///
363/// - **Client-Side Encryption**: All data encrypted before transmission
364/// - **Per-Request Keys**: Keys provided with each request, never stored
365/// - **Key Hash Validation**: Server validates key via SHA-256 hash
366/// - **Nonce-Based Replay Protection**: Each request has unique nonce
367///
368/// # Example Configuration
369///
370/// ```toml
371/// [encryption.zke]
372/// enabled = true
373/// mode = "per_request"
374/// require_key_hash = true
375/// replay_protection = true
376/// nonce_window_secs = 300
377/// ```
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct ZkeEncryptionConfig {
380    /// Enable Zero-Knowledge Encryption mode
381    pub enabled: bool,
382    /// ZKE mode: "full", "hybrid", or "per_request"
383    pub mode: ZkeMode,
384    /// Require key hash validation on every request
385    pub require_key_hash: bool,
386    /// Enable nonce-based replay protection
387    pub replay_protection: bool,
388    /// Nonce validity window in seconds (default: 300 = 5 minutes)
389    pub nonce_window_secs: u64,
390    /// Maximum cached nonces for replay protection (default: 10000)
391    pub max_cached_nonces: usize,
392}
393
394impl Default for ZkeEncryptionConfig {
395    fn default() -> Self {
396        Self {
397            enabled: false,
398            mode: ZkeMode::PerRequest,
399            require_key_hash: true,
400            replay_protection: true,
401            nonce_window_secs: 300,
402            max_cached_nonces: 10000,
403        }
404    }
405}
406
407/// Zero-Knowledge Encryption mode
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
409#[serde(rename_all = "snake_case")]
410pub enum ZkeMode {
411    /// Full zero-knowledge: client encrypts all data
412    /// - No server-side search capabilities
413    /// - Maximum privacy
414    Full,
415    /// Hybrid: metadata visible, row data encrypted
416    /// - Table/column names unencrypted
417    /// - Basic filtering possible
418    Hybrid,
419    /// Per-request decryption with client-provided key
420    /// - Full SQL capabilities
421    /// - Key zeroed after each request
422    PerRequest,
423}
424
425/// Encryption algorithm
426#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
427pub enum EncryptionAlgorithm {
428    /// AES-256-GCM (recommended)
429    Aes256Gcm,
430}
431
432/// Encryption key source
433///
434/// Specifies where the encryption key is retrieved from. For security,
435/// keys should never be stored in configuration files directly.
436///
437/// # Security Recommendations
438///
439/// - **Production**: Use `Kms` with a cloud provider for key management
440/// - **Development**: Use `Environment` with a secure secret manager
441/// - **Testing**: Use `File` with proper file permissions (chmod 600)
442///
443/// # Examples
444///
445/// ```toml
446/// # Environment variable (recommended for development)
447/// [encryption]
448/// key_source = { Environment = "HELIOSDB_KEY" }
449///
450/// # AWS KMS (recommended for production)
451/// [encryption]
452/// key_source = { Kms = { provider = "aws", key_id = "arn:aws:kms:..." } }
453/// ```
454#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
455pub enum KeySource {
456    /// Read key from environment variable
457    Environment(String),
458    /// Read key from file (ensure proper file permissions)
459    File(PathBuf),
460    /// Use cloud Key Management Service
461    Kms {
462        /// Cloud provider: "aws", "azure", or "gcp"
463        provider: String,
464        /// Key identifier (ARN for AWS, key URI for others)
465        key_id: String,
466    },
467}
468
469/// Server configuration
470#[derive(Debug, Clone, Serialize, Deserialize)]
471#[serde(default)]
472pub struct ServerConfig {
473    /// Listen address
474    pub listen_addr: String,
475    /// PostgreSQL protocol port
476    pub port: u16,
477    /// Oracle TNS protocol port (optional, disabled if None)
478    pub oracle_port: Option<u16>,
479    /// Maximum connections
480    pub max_connections: usize,
481    /// Idle connection timeout in seconds (0 = no timeout, default 300s = 5 min)
482    #[serde(default = "default_idle_timeout_secs")]
483    pub idle_timeout_secs: u64,
484    /// TLS enabled
485    pub tls_enabled: bool,
486    /// TLS certificate path
487    pub tls_cert_path: Option<PathBuf>,
488    /// TLS key path
489    pub tls_key_path: Option<PathBuf>,
490}
491
492impl Default for ServerConfig {
493    fn default() -> Self {
494        Self {
495            listen_addr: "127.0.0.1".to_string(),
496            port: 5432,
497            oracle_port: Some(1521), // Enable Oracle protocol by default
498            max_connections: 100,
499            idle_timeout_secs: default_idle_timeout_secs(),
500            tls_enabled: false,
501            tls_cert_path: None,
502            tls_key_path: None,
503        }
504    }
505}
506
507/// Performance configuration
508#[derive(Debug, Clone, Serialize, Deserialize)]
509#[serde(default)]
510pub struct PerformanceConfig {
511    /// Worker threads
512    pub worker_threads: usize,
513    /// Query timeout (seconds)
514    pub query_timeout_secs: u64,
515    /// Enable SIMD
516    pub simd_enabled: bool,
517    /// Parallel query execution
518    pub parallel_query: bool,
519}
520
521impl Default for PerformanceConfig {
522    fn default() -> Self {
523        Self {
524            worker_threads: num_cpus::get(),
525            query_timeout_secs: 300,
526            simd_enabled: true,
527            parallel_query: true,
528        }
529    }
530}
531
532// Add num_cpus to dependencies
533// For now use a simple fallback
534mod num_cpus {
535    pub fn get() -> usize {
536        std::thread::available_parallelism()
537            .map(|n| n.get())
538            .unwrap_or(4)
539    }
540}
541
542/// Optimizer configuration (v2.1)
543#[derive(Debug, Clone, Serialize, Deserialize)]
544#[serde(default)]
545pub struct OptimizerConfig {
546    /// Enable query optimizer
547    pub enabled: bool,
548    /// Enable sequential scan
549    pub enable_seqscan: bool,
550    /// Enable index scan
551    pub enable_indexscan: bool,
552    /// Enable hash join
553    pub enable_hashjoin: bool,
554    /// Enable merge join
555    pub enable_mergejoin: bool,
556    /// Enable nested loop join
557    pub enable_nestloop: bool,
558    /// Cost model parameters
559    pub seq_page_cost: f64,
560    pub random_page_cost: f64,
561    pub cpu_tuple_cost: f64,
562    pub cpu_index_tuple_cost: f64,
563}
564
565impl Default for OptimizerConfig {
566    fn default() -> Self {
567        Self {
568            enabled: true,
569            enable_seqscan: true,
570            enable_indexscan: true,
571            enable_hashjoin: true,
572            enable_mergejoin: true,
573            enable_nestloop: true,
574            seq_page_cost: 1.0,
575            random_page_cost: 4.0,
576            cpu_tuple_cost: 0.01,
577            cpu_index_tuple_cost: 0.005,
578        }
579    }
580}
581
582/// Authentication configuration (v2.1)
583#[derive(Debug, Clone, Serialize, Deserialize)]
584#[serde(default)]
585pub struct AuthenticationConfig {
586    /// Enable authentication
587    pub enabled: bool,
588    /// Authentication method
589    pub method: AuthMethod,
590    /// JWT secret key (for JWT auth)
591    pub jwt_secret: Option<String>,
592    /// JWT expiration time (seconds)
593    pub jwt_expiration_secs: u64,
594    /// Password hash algorithm
595    pub password_hash_algorithm: PasswordHashAlgorithm,
596    /// Users file path (for file-based auth)
597    pub users_file: Option<PathBuf>,
598}
599
600impl Default for AuthenticationConfig {
601    fn default() -> Self {
602        Self {
603            enabled: false,
604            method: AuthMethod::Trust,
605            jwt_secret: None,
606            jwt_expiration_secs: 86400, // 24 hours
607            password_hash_algorithm: PasswordHashAlgorithm::Argon2,
608            users_file: None,
609        }
610    }
611}
612
613/// Authentication method
614#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(rename_all = "lowercase")]
616pub enum AuthMethod {
617    /// No authentication (dev mode)
618    Trust,
619    /// Password authentication
620    Password,
621    /// JWT authentication
622    Jwt,
623    /// LDAP authentication
624    Ldap,
625}
626
627/// Password hash algorithm
628#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
629#[serde(rename_all = "lowercase")]
630pub enum PasswordHashAlgorithm {
631    /// Argon2 (recommended)
632    Argon2,
633    /// BCrypt
634    Bcrypt,
635    /// PBKDF2
636    Pbkdf2,
637}
638
639/// Compression configuration (v2.1)
640#[derive(Debug, Clone, Serialize, Deserialize)]
641#[serde(default)]
642pub struct CompressionConfig {
643    /// Default compression type
644    pub default_type: CompressionType,
645    /// Compression level (1-22 for zstd)
646    pub level: i32,
647    /// Enable ALP compression for numeric columns
648    pub enable_alp: bool,
649    /// Enable FSST compression for string columns
650    pub enable_fsst: bool,
651    /// Minimum data size to trigger compression (bytes)
652    pub min_size_bytes: usize,
653}
654
655impl Default for CompressionConfig {
656    fn default() -> Self {
657        Self {
658            default_type: CompressionType::Zstd,
659            level: 3,
660            enable_alp: true,
661            enable_fsst: true,
662            min_size_bytes: 1024, // 1KB
663        }
664    }
665}
666
667/// Materialized view configuration (v2.3 - Incremental MVs)
668#[derive(Debug, Clone, Serialize, Deserialize)]
669#[serde(default)]
670pub struct MaterializedViewConfig {
671    /// Enable auto-refresh by default
672    pub auto_refresh_default: bool,
673    /// Default max CPU percentage for refresh
674    pub default_max_cpu_percent: u8,
675    /// Refresh check interval (seconds)
676    pub refresh_check_interval_secs: u64,
677    /// Maximum concurrent refreshes
678    pub max_concurrent_refreshes: usize,
679    /// Enable incremental refresh feature (v2.3.0)
680    pub enable_incremental: bool,
681    /// Enable delta tracking for base tables (v2.3.0)
682    pub enable_delta_tracking: bool,
683    /// Enable CPU-aware scheduling (v2.3.0)
684    pub enable_scheduler: bool,
685    /// Delta retention period in hours (purge old deltas)
686    pub delta_retention_hours: u64,
687}
688
689impl Default for MaterializedViewConfig {
690    fn default() -> Self {
691        Self {
692            auto_refresh_default: false,
693            default_max_cpu_percent: 15,
694            refresh_check_interval_secs: 60,
695            max_concurrent_refreshes: 2,
696            enable_incremental: false,  // Disabled by default, opt-in
697            enable_delta_tracking: false,  // Disabled by default
698            enable_scheduler: false,  // Disabled by default
699            delta_retention_hours: 168,  // 7 days
700        }
701    }
702}
703
704/// Vector index configuration (v2.1)
705#[derive(Debug, Clone, Serialize, Deserialize)]
706#[serde(default)]
707pub struct VectorConfig {
708    /// Default vector index type
709    pub default_index_type: VectorIndexType,
710    /// HNSW ef_construction parameter
711    pub hnsw_ef_construction: usize,
712    /// HNSW M parameter (connections per layer)
713    pub hnsw_m: usize,
714    /// Enable product quantization
715    pub enable_pq: bool,
716    /// PQ subvector count
717    pub pq_subvectors: usize,
718    /// PQ bits per subvector
719    pub pq_bits: usize,
720}
721
722impl Default for VectorConfig {
723    fn default() -> Self {
724        Self {
725            default_index_type: VectorIndexType::Hnsw,
726            hnsw_ef_construction: 200,
727            hnsw_m: 16,
728            enable_pq: true,
729            pq_subvectors: 8,
730            pq_bits: 8,
731        }
732    }
733}
734
735/// Vector index type
736#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
737#[serde(rename_all = "lowercase")]
738pub enum VectorIndexType {
739    /// Flat (brute force, exact)
740    Flat,
741    /// HNSW (approximate, fast)
742    Hnsw,
743    /// IVF (inverted file)
744    Ivf,
745}
746
747/// Sync configuration (v2.3 - Experimental)
748#[derive(Debug, Clone, Serialize, Deserialize)]
749#[serde(default)]
750pub struct SyncConfig {
751    /// Enable sync protocol
752    pub enabled: bool,
753    /// Node ID for this instance (generated if None)
754    pub node_id: Option<String>,
755    /// Sync server URL (for clients)
756    pub server_url: Option<String>,
757    /// Client ID (for authentication)
758    pub client_id: Option<String>,
759    /// Sync interval in seconds
760    pub sync_interval_secs: u64,
761    /// Enable change log capture
762    pub change_log_enabled: bool,
763}
764
765impl Default for SyncConfig {
766    fn default() -> Self {
767        Self {
768            enabled: false,
769            node_id: None,
770            server_url: None,
771            client_id: None,
772            sync_interval_secs: 30,
773            change_log_enabled: true,
774        }
775    }
776}
777
778/// Session configuration (v3.1.0)
779#[derive(Debug, Clone, Serialize, Deserialize)]
780#[serde(default)]
781pub struct SessionConfig {
782    /// Session timeout in seconds (default: 3600 = 1 hour)
783    pub timeout_secs: u64,
784    /// Maximum sessions per user (default: 10)
785    pub max_sessions_per_user: u32,
786    /// Session cleanup interval in seconds (default: 300 = 5 minutes)
787    pub cleanup_interval_secs: u64,
788}
789
790impl Default for SessionConfig {
791    fn default() -> Self {
792        Self {
793            timeout_secs: 3600,
794            max_sessions_per_user: 10,
795            cleanup_interval_secs: 300,
796        }
797    }
798}
799
800impl SessionConfig {
801    /// Validate configuration values
802    pub fn validate(&self) -> crate::Result<()> {
803        if self.timeout_secs < 1 {
804            return Err(crate::Error::config(
805                "session.timeout_secs must be at least 1 second",
806            ));
807        }
808        if self.max_sessions_per_user < 1 {
809            return Err(crate::Error::config(
810                "session.max_sessions_per_user must be at least 1",
811            ));
812        }
813        if self.cleanup_interval_secs < 1 {
814            return Err(crate::Error::config(
815                "session.cleanup_interval_secs must be at least 1 second",
816            ));
817        }
818        Ok(())
819    }
820}
821
822/// Lock configuration (v3.1.0)
823#[derive(Debug, Clone, Serialize, Deserialize)]
824#[serde(default)]
825pub struct LockConfig {
826    /// Lock acquisition timeout in milliseconds (default: 30000 = 30 seconds)
827    pub timeout_ms: u32,
828    /// Deadlock detection interval in milliseconds (default: 100)
829    pub deadlock_check_interval_ms: u32,
830    /// Maximum number of concurrent lock holders (default: 10000)
831    pub max_lock_holders: u32,
832}
833
834impl Default for LockConfig {
835    fn default() -> Self {
836        Self {
837            timeout_ms: 30000,
838            deadlock_check_interval_ms: 100,
839            max_lock_holders: 10000,
840        }
841    }
842}
843
844impl LockConfig {
845    /// Validate configuration values
846    pub fn validate(&self) -> crate::Result<()> {
847        if self.timeout_ms < 100 {
848            return Err(crate::Error::config(
849                "locks.timeout_ms must be at least 100 milliseconds",
850            ));
851        }
852        if self.deadlock_check_interval_ms < 10 {
853            return Err(crate::Error::config(
854                "locks.deadlock_check_interval_ms must be at least 10 milliseconds",
855            ));
856        }
857        if self.max_lock_holders < 1 {
858            return Err(crate::Error::config(
859                "locks.max_lock_holders must be at least 1",
860            ));
861        }
862        Ok(())
863    }
864}
865
866/// Dump configuration (v3.1.0)
867#[derive(Debug, Clone, Serialize, Deserialize)]
868#[serde(default)]
869pub struct DumpConfig {
870    /// Enable automatic dumps on a schedule (default: false)
871    pub auto_dump_enabled: bool,
872    /// Cron-style schedule for automatic dumps (e.g., "0 */6 * * *")
873    pub schedule: String,
874    /// Compression algorithm: "zstd", "gzip", "none" (default: "zstd")
875    pub compression: String,
876    /// Maximum size of a single dump file in MB before rolling (default: 10000)
877    pub max_dump_size_mb: u64,
878    /// Number of old dumps to keep (0 = keep all, default: 10)
879    pub keep_dumps: usize,
880    /// Directory to store dumps (default: ".dumps")
881    pub dump_dir: String,
882}
883
884impl Default for DumpConfig {
885    fn default() -> Self {
886        Self {
887            auto_dump_enabled: false,
888            schedule: String::new(),
889            compression: "zstd".to_string(),
890            max_dump_size_mb: 10000,
891            keep_dumps: 10,
892            dump_dir: ".dumps".to_string(),
893        }
894    }
895}
896
897impl DumpConfig {
898    /// Validate configuration values
899    pub fn validate(&self) -> crate::Result<()> {
900        // Validate compression type
901        match self.compression.as_str() {
902            "zstd" | "gzip" | "none" => {}
903            _ => {
904                return Err(crate::Error::config(format!(
905                    "dump.compression must be 'zstd', 'gzip', or 'none', got '{}'",
906                    self.compression
907                )));
908            }
909        }
910
911        // Validate max dump size
912        if self.max_dump_size_mb < 1 {
913            return Err(crate::Error::config(
914                "dump.max_dump_size_mb must be at least 1 MB",
915            ));
916        }
917
918        // Validate cron schedule if auto_dump_enabled
919        if self.auto_dump_enabled && !self.schedule.is_empty() {
920            Self::validate_cron_schedule(&self.schedule)?;
921        }
922
923        Ok(())
924    }
925
926    /// Validate cron schedule format (basic validation)
927    fn validate_cron_schedule(schedule: &str) -> crate::Result<()> {
928        let parts: Vec<&str> = schedule.split_whitespace().collect();
929        if parts.len() != 5 {
930            return Err(crate::Error::config(format!(
931                "Invalid cron schedule format '{}'. Expected 5 fields: minute hour day month weekday",
932                schedule
933            )));
934        }
935        Ok(())
936    }
937}
938
939/// Resource quota configuration (v3.1.0)
940#[derive(Debug, Clone, Serialize, Deserialize)]
941#[serde(default)]
942pub struct ResourceQuotaConfig {
943    /// Memory limit per user in MB (default: 1024)
944    pub memory_limit_per_user_mb: u64,
945    /// Maximum concurrent queries per user (default: 100)
946    pub max_concurrent_queries: u32,
947    /// Query execution timeout in seconds (default: 300 = 5 minutes)
948    pub query_timeout_secs: u64,
949}
950
951impl Default for ResourceQuotaConfig {
952    fn default() -> Self {
953        Self {
954            memory_limit_per_user_mb: 1024,
955            max_concurrent_queries: 100,
956            query_timeout_secs: 300,
957        }
958    }
959}
960
961impl ResourceQuotaConfig {
962    /// Validate configuration values
963    pub fn validate(&self) -> crate::Result<()> {
964        if self.memory_limit_per_user_mb < 1 {
965            return Err(crate::Error::config(
966                "resource_quotas.memory_limit_per_user_mb must be at least 1 MB",
967            ));
968        }
969        if self.max_concurrent_queries < 1 {
970            return Err(crate::Error::config(
971                "resource_quotas.max_concurrent_queries must be at least 1",
972            ));
973        }
974        if self.query_timeout_secs < 1 {
975            return Err(crate::Error::config(
976                "resource_quotas.query_timeout_secs must be at least 1 second",
977            ));
978        }
979        Ok(())
980    }
981}
982
983/// BaaS REST API configuration (v3.2.0)
984///
985/// Controls the PostgREST-compatible REST API layer that exposes tables
986/// as RESTful endpoints at `/rest/v1/`.
987///
988/// # Example (TOML)
989///
990/// ```toml
991/// [api]
992/// jwt_secret = "super-secret-key"
993/// anon_key = "eyJhbGciOi..."
994/// service_role_key = "eyJhbGciOi..."
995/// ```
996#[derive(Debug, Clone, Serialize, Deserialize)]
997#[serde(default)]
998pub struct ApiConfig {
999    /// JWT secret used to sign and verify authentication tokens.
1000    ///
1001    /// If not set, a random 64-byte hex string is generated at startup.
1002    pub jwt_secret: String,
1003    /// Anonymous API key that grants unauthenticated read access.
1004    pub anon_key: Option<String>,
1005    /// Service-role API key that grants full admin access (bypasses RLS).
1006    pub service_role_key: Option<String>,
1007    /// OAuth2 provider configurations (Google, GitHub, etc.).
1008    ///
1009    /// # Example (TOML)
1010    ///
1011    /// ```toml
1012    /// [[api.oauth_providers]]
1013    /// name = "google"
1014    /// client_id = "123456.apps.googleusercontent.com"
1015    /// client_secret = "GOCSPX-..."
1016    /// redirect_uri = "http://localhost:8080/auth/v1/callback"
1017    ///
1018    /// [[api.oauth_providers]]
1019    /// name = "github"
1020    /// client_id = "Iv1.abc123"
1021    /// client_secret = "deadbeef..."
1022    /// redirect_uri = "http://localhost:8080/auth/v1/callback"
1023    /// ```
1024    #[serde(default)]
1025    pub oauth_providers: Vec<OAuthProviderConfig>,
1026}
1027
1028/// Configuration for a single OAuth2 provider.
1029#[derive(Debug, Clone, Serialize, Deserialize)]
1030pub struct OAuthProviderConfig {
1031    /// Provider name: `"google"` or `"github"`.
1032    pub name: String,
1033    /// OAuth2 Client ID issued by the provider.
1034    pub client_id: String,
1035    /// OAuth2 Client Secret issued by the provider.
1036    pub client_secret: String,
1037    /// The absolute URL the provider should redirect back to after authorization.
1038    pub redirect_uri: String,
1039}
1040
1041impl Default for ApiConfig {
1042    fn default() -> Self {
1043        Self {
1044            jwt_secret: generate_random_secret(),
1045            anon_key: None,
1046            service_role_key: None,
1047            oauth_providers: Vec::new(),
1048        }
1049    }
1050}
1051
1052/// Generate a random 64-character hex string for use as a JWT secret.
1053fn generate_random_secret() -> String {
1054    use std::collections::hash_map::RandomState;
1055    use std::hash::{BuildHasher, Hasher};
1056
1057    // Use two independent random hashers to get 128 bits of randomness
1058    let s = RandomState::new();
1059    let h1 = s.build_hasher().finish();
1060    let h2 = RandomState::new().build_hasher().finish();
1061    format!("{h1:016x}{h2:016x}{h1:016x}{h2:016x}")
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066    use super::*;
1067
1068    #[test]
1069    fn test_session_config_default() {
1070        let config = SessionConfig::default();
1071        assert_eq!(config.timeout_secs, 3600);
1072        assert_eq!(config.max_sessions_per_user, 10);
1073        assert_eq!(config.cleanup_interval_secs, 300);
1074        assert!(config.validate().is_ok());
1075    }
1076
1077    #[test]
1078    fn test_session_config_validation() {
1079        let mut config = SessionConfig::default();
1080
1081        // Valid config
1082        assert!(config.validate().is_ok());
1083
1084        // Invalid timeout
1085        config.timeout_secs = 0;
1086        assert!(config.validate().is_err());
1087        config.timeout_secs = 3600;
1088
1089        // Invalid max sessions
1090        config.max_sessions_per_user = 0;
1091        assert!(config.validate().is_err());
1092        config.max_sessions_per_user = 10;
1093
1094        // Invalid cleanup interval
1095        config.cleanup_interval_secs = 0;
1096        assert!(config.validate().is_err());
1097    }
1098
1099    #[test]
1100    fn test_lock_config_default() {
1101        let config = LockConfig::default();
1102        assert_eq!(config.timeout_ms, 30000);
1103        assert_eq!(config.deadlock_check_interval_ms, 100);
1104        assert_eq!(config.max_lock_holders, 10000);
1105        assert!(config.validate().is_ok());
1106    }
1107
1108    #[test]
1109    fn test_lock_config_validation() {
1110        let mut config = LockConfig::default();
1111
1112        // Valid config
1113        assert!(config.validate().is_ok());
1114
1115        // Invalid timeout (too low)
1116        config.timeout_ms = 50;
1117        assert!(config.validate().is_err());
1118        config.timeout_ms = 30000;
1119
1120        // Invalid deadlock check interval
1121        config.deadlock_check_interval_ms = 5;
1122        assert!(config.validate().is_err());
1123        config.deadlock_check_interval_ms = 100;
1124
1125        // Invalid max lock holders
1126        config.max_lock_holders = 0;
1127        assert!(config.validate().is_err());
1128    }
1129
1130    #[test]
1131    fn test_dump_config_default() {
1132        let config = DumpConfig::default();
1133        assert!(!config.auto_dump_enabled);
1134        assert_eq!(config.schedule, "");
1135        assert_eq!(config.compression, "zstd");
1136        assert_eq!(config.max_dump_size_mb, 10000);
1137        assert_eq!(config.keep_dumps, 10);
1138        assert_eq!(config.dump_dir, ".dumps");
1139        assert!(config.validate().is_ok());
1140    }
1141
1142    #[test]
1143    fn test_dump_config_validation() {
1144        let mut config = DumpConfig::default();
1145
1146        // Valid config
1147        assert!(config.validate().is_ok());
1148
1149        // Invalid compression type
1150        config.compression = "invalid".to_string();
1151        assert!(config.validate().is_err());
1152        config.compression = "zstd".to_string();
1153
1154        // Valid compression types
1155        config.compression = "gzip".to_string();
1156        assert!(config.validate().is_ok());
1157        config.compression = "none".to_string();
1158        assert!(config.validate().is_ok());
1159        config.compression = "zstd".to_string();
1160
1161        // Invalid max dump size
1162        config.max_dump_size_mb = 0;
1163        assert!(config.validate().is_err());
1164        config.max_dump_size_mb = 10000;
1165
1166        // Valid cron schedule
1167        config.auto_dump_enabled = true;
1168        config.schedule = "0 */6 * * *".to_string();
1169        assert!(config.validate().is_ok());
1170
1171        // Invalid cron schedule
1172        config.schedule = "invalid".to_string();
1173        assert!(config.validate().is_err());
1174
1175        // Empty schedule with auto_dump_enabled is OK
1176        config.schedule = "".to_string();
1177        assert!(config.validate().is_ok());
1178    }
1179
1180    #[test]
1181    fn test_resource_quota_config_default() {
1182        let config = ResourceQuotaConfig::default();
1183        assert_eq!(config.memory_limit_per_user_mb, 1024);
1184        assert_eq!(config.max_concurrent_queries, 100);
1185        assert_eq!(config.query_timeout_secs, 300);
1186        assert!(config.validate().is_ok());
1187    }
1188
1189    #[test]
1190    fn test_resource_quota_config_validation() {
1191        let mut config = ResourceQuotaConfig::default();
1192
1193        // Valid config
1194        assert!(config.validate().is_ok());
1195
1196        // Invalid memory limit
1197        config.memory_limit_per_user_mb = 0;
1198        assert!(config.validate().is_err());
1199        config.memory_limit_per_user_mb = 1024;
1200
1201        // Invalid max concurrent queries
1202        config.max_concurrent_queries = 0;
1203        assert!(config.validate().is_err());
1204        config.max_concurrent_queries = 100;
1205
1206        // Invalid query timeout
1207        config.query_timeout_secs = 0;
1208        assert!(config.validate().is_err());
1209    }
1210
1211    #[test]
1212    fn test_config_with_new_sections() {
1213        let config = Config::default();
1214
1215        // Verify new sections are initialized
1216        assert_eq!(config.session.timeout_secs, 3600);
1217        assert_eq!(config.locks.timeout_ms, 30000);
1218        assert_eq!(config.dump.compression, "zstd");
1219        assert_eq!(config.resource_quotas.memory_limit_per_user_mb, 1024);
1220
1221        // Validate entire config
1222        assert!(config.validate().is_ok());
1223    }
1224
1225    #[test]
1226    fn test_config_serialization() {
1227        let config = Config::default();
1228
1229        // Serialize to TOML
1230        let toml_str = toml::to_string(&config).expect("Failed to serialize config");
1231
1232        // Should contain new sections
1233        assert!(toml_str.contains("[session]"));
1234        assert!(toml_str.contains("[locks]"));
1235        assert!(toml_str.contains("[dump]"));
1236        assert!(toml_str.contains("[resource_quotas]"));
1237    }
1238
1239    #[test]
1240    fn test_config_deserialization() {
1241        let toml_str = r#"
1242            [storage]
1243            memory_only = true
1244            wal_enabled = false
1245            wal_sync_mode = "sync"
1246            cache_size = 536870912
1247            compression = "Zstd"
1248            time_travel_enabled = true
1249            transaction_isolation = "READ_COMMITTED"
1250
1251            [session]
1252            timeout_secs = 7200
1253            max_sessions_per_user = 20
1254            cleanup_interval_secs = 600
1255
1256            [locks]
1257            timeout_ms = 60000
1258            deadlock_check_interval_ms = 200
1259            max_lock_holders = 20000
1260
1261            [dump]
1262            auto_dump_enabled = true
1263            schedule = "0 2 * * *"
1264            compression = "gzip"
1265            max_dump_size_mb = 5000
1266            keep_dumps = 5
1267            dump_dir = "/var/dumps"
1268
1269            [resource_quotas]
1270            memory_limit_per_user_mb = 2048
1271            max_concurrent_queries = 200
1272            query_timeout_secs = 600
1273        "#;
1274
1275        let config: Config = toml::from_str(toml_str).expect("Failed to deserialize config");
1276
1277        // Verify session config
1278        assert_eq!(config.session.timeout_secs, 7200);
1279        assert_eq!(config.session.max_sessions_per_user, 20);
1280        assert_eq!(config.session.cleanup_interval_secs, 600);
1281
1282        // Verify lock config
1283        assert_eq!(config.locks.timeout_ms, 60000);
1284        assert_eq!(config.locks.deadlock_check_interval_ms, 200);
1285        assert_eq!(config.locks.max_lock_holders, 20000);
1286
1287        // Verify dump config
1288        assert!(config.dump.auto_dump_enabled);
1289        assert_eq!(config.dump.schedule, "0 2 * * *");
1290        assert_eq!(config.dump.compression, "gzip");
1291        assert_eq!(config.dump.max_dump_size_mb, 5000);
1292        assert_eq!(config.dump.keep_dumps, 5);
1293        assert_eq!(config.dump.dump_dir, "/var/dumps");
1294
1295        // Verify resource quota config
1296        assert_eq!(config.resource_quotas.memory_limit_per_user_mb, 2048);
1297        assert_eq!(config.resource_quotas.max_concurrent_queries, 200);
1298        assert_eq!(config.resource_quotas.query_timeout_secs, 600);
1299
1300        // Validate entire config
1301        assert!(config.validate().is_ok());
1302    }
1303
1304    #[test]
1305    fn test_config_with_missing_sections() {
1306        // Config with only storage section (old format)
1307        let toml_str = r#"
1308            [storage]
1309            memory_only = true
1310            wal_enabled = false
1311            wal_sync_mode = "sync"
1312            cache_size = 536870912
1313            compression = "Zstd"
1314            time_travel_enabled = true
1315            transaction_isolation = "READ_COMMITTED"
1316        "#;
1317
1318        let config: Config = toml::from_str(toml_str).expect("Failed to deserialize config");
1319
1320        // New sections should have default values
1321        assert_eq!(config.session.timeout_secs, 3600);
1322        assert_eq!(config.locks.timeout_ms, 30000);
1323        assert_eq!(config.dump.compression, "zstd");
1324        assert_eq!(config.resource_quotas.memory_limit_per_user_mb, 1024);
1325
1326        // Should still validate
1327        assert!(config.validate().is_ok());
1328    }
1329
1330    #[test]
1331    fn test_cron_schedule_validation() {
1332        // Valid cron schedules
1333        assert!(DumpConfig::validate_cron_schedule("0 */6 * * *").is_ok());
1334        assert!(DumpConfig::validate_cron_schedule("0 2 * * *").is_ok());
1335        assert!(DumpConfig::validate_cron_schedule("*/15 * * * *").is_ok());
1336        assert!(DumpConfig::validate_cron_schedule("0 0 1 * *").is_ok());
1337        assert!(DumpConfig::validate_cron_schedule("30 3 * * 0").is_ok());
1338
1339        // Invalid cron schedules
1340        assert!(DumpConfig::validate_cron_schedule("invalid").is_err());
1341        assert!(DumpConfig::validate_cron_schedule("0 * * *").is_err());  // Only 4 fields
1342        assert!(DumpConfig::validate_cron_schedule("0 * * * * *").is_err());  // 6 fields
1343        assert!(DumpConfig::validate_cron_schedule("").is_err());  // Empty
1344    }
1345}