Skip to main content

cqlite_core/storage/
mod.rs

1//! Storage engine implementation for CQLite
2
3pub mod sstable;
4
5// Canonical partition-key (de)serialization, shared by the read (query) path
6// and the write engine so the two never drift (Issue #586). Always compiled —
7// the scan path needs it even without `write-support`.
8pub mod partition_key_codec;
9
10// M5: Write engine and serialization (Issue #359)
11#[cfg(feature = "write-support")]
12pub mod serialization;
13#[cfg(feature = "write-support")]
14pub mod write_engine;
15
16// REPL data access components (Issue #249: CLI-specific)
17#[cfg(feature = "cli-helpers")]
18pub mod repl_data_api;
19pub mod schema_discovery;
20pub mod sstable_data_manager;
21
22use std::path::{Path, PathBuf};
23use std::sync::Arc;
24#[cfg(feature = "state_machine")]
25use tokio::sync::RwLock;
26
27use crate::platform::Platform;
28use crate::{
29    types::{CellWriteMetadata, TableId},
30    Config, Result, RowKey, Value,
31};
32
33/// Main storage engine that coordinates all storage components
34///
35/// NOTE: Issue #176 removed write infrastructure (compaction, manifest).
36/// This is now a read-only storage layer focused on SSTable access.
37#[derive(Debug)]
38pub struct StorageEngine {
39    /// SSTable manager for persistent storage
40    sstables: Arc<sstable::SSTableManager>,
41
42    /// Platform abstraction
43    #[allow(dead_code)]
44    _platform: Arc<Platform>,
45
46    /// Storage configuration
47    #[allow(dead_code)]
48    config: Config,
49
50    /// Schema registry for schema-aware operations (feature-gated)
51    #[cfg(feature = "state_machine")]
52    schema_registry: Arc<RwLock<Option<Arc<RwLock<crate::schema::SchemaRegistry>>>>>,
53}
54
55impl StorageEngine {
56    /// Open a storage engine at the given path
57    ///
58    /// This method discovers SSTables by scanning the storage directory.
59    /// For pre-discovered SSTables, use `open_with_sstables` instead.
60    ///
61    /// NOTE: Issue #176 removed write infrastructure (compaction, manifest).
62    /// This is now a read-only storage layer focused on SSTable access.
63    pub async fn open(
64        path: &Path,
65        config: &Config,
66        platform: Arc<Platform>,
67        #[cfg(feature = "state_machine")] schema_registry: Option<
68            Arc<RwLock<crate::schema::SchemaRegistry>>,
69        >,
70    ) -> Result<Self> {
71        // Create storage directory if it doesn't exist
72        platform.fs().create_dir_all(path).await?;
73
74        // Initialize SSTable manager with schema registry
75        let sstables = Arc::new(
76            sstable::SSTableManager::new(
77                path,
78                config,
79                platform.clone(),
80                #[cfg(feature = "state_machine")]
81                schema_registry.clone(),
82            )
83            .await?,
84        );
85
86        Ok(Self {
87            sstables,
88            _platform: platform,
89            config: config.clone(),
90            #[cfg(feature = "state_machine")]
91            schema_registry: Arc::new(RwLock::new(schema_registry)),
92        })
93    }
94
95    /// Open a storage engine with pre-discovered SSTable table directories
96    ///
97    /// This method is used when SSTables have been discovered externally (e.g., by DiscoveryService)
98    /// and allows the storage engine to be initialized with specific table directories rather than
99    /// scanning the storage directory. Each table directory will be scanned for Data.db files.
100    ///
101    /// # Arguments
102    /// * `path` - Base storage path for manifest and SSTable operations
103    /// * `discovered_table_dirs` - Vector of table directory paths (each containing SSTable files)
104    /// * `config` - Storage configuration
105    /// * `platform` - Platform abstraction for I/O operations
106    ///
107    /// # Returns
108    /// A StorageEngine instance with all components initialized, including SSTable readers
109    /// for all Data.db files found in the discovered table directories.
110    ///
111    /// # Example
112    /// ```no_run
113    /// # use std::path::{Path, PathBuf};
114    /// # use std::sync::Arc;
115    /// # use cqlite_core::{Config, Platform, storage::StorageEngine};
116    /// # async fn example() -> cqlite_core::Result<()> {
117    /// let config = Config::default();
118    /// let platform = Arc::new(Platform::new(&config).await?);
119    /// let storage_path = Path::new("/var/lib/cqlite/storage");
120    /// let discovered_table_dirs = vec![
121    ///     PathBuf::from("/var/lib/cassandra/keyspace1/table1-abc123"),
122    ///     PathBuf::from("/var/lib/cassandra/keyspace1/table2-def456"),
123    /// ];
124    ///
125    /// let engine = StorageEngine::open_with_sstables(
126    ///     storage_path,
127    ///     discovered_table_dirs,
128    ///     &config,
129    ///     platform,
130    ///     #[cfg(feature = "state_machine")]
131    ///     None,
132    /// ).await?;
133    /// # Ok(())
134    /// # }
135    /// ```
136    pub async fn open_with_sstables(
137        path: &Path,
138        discovered_table_dirs: Vec<PathBuf>,
139        config: &Config,
140        platform: Arc<Platform>,
141        #[cfg(feature = "state_machine")] schema_registry: Option<
142            Arc<RwLock<crate::schema::SchemaRegistry>>,
143        >,
144    ) -> Result<Self> {
145        // Create storage directory if it doesn't exist
146        platform.fs().create_dir_all(path).await?;
147
148        // Initialize SSTable manager with pre-discovered paths and schema registry
149        let sstables = Arc::new(
150            sstable::SSTableManager::new_from_discovered_paths(
151                path,
152                discovered_table_dirs,
153                config,
154                platform.clone(),
155                #[cfg(feature = "state_machine")]
156                schema_registry.clone(),
157            )
158            .await?,
159        );
160
161        Ok(Self {
162            sstables,
163            _platform: platform,
164            config: config.clone(),
165            #[cfg(feature = "state_machine")]
166            schema_registry: Arc::new(RwLock::new(schema_registry)),
167        })
168    }
169
170    /// Insert a key-value pair
171    ///
172    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
173    /// This method is feature-gated behind 'experimental' but currently unimplemented.
174    #[cfg(feature = "experimental")]
175    pub async fn put(&self, _table_id: &TableId, _key: RowKey, _value: Value) -> Result<()> {
176        Err(crate::error::Error::UnsupportedFormat(
177            "Write operations (put) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
178        ))
179    }
180
181    /// Get a value by key
182    pub async fn get(&self, table_id: &TableId, key: &RowKey) -> Result<Option<Value>> {
183        // Check SSTables
184        self.sstables.get(table_id, key).await
185    }
186
187    /// Delete a key
188    ///
189    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
190    /// This method is feature-gated behind 'experimental' but currently unimplemented.
191    #[cfg(feature = "experimental")]
192    pub async fn delete(&self, _table_id: &TableId, _key: RowKey) -> Result<()> {
193        Err(crate::error::Error::UnsupportedFormat(
194            "Write operations (delete) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
195        ))
196    }
197
198    /// Scan a range of keys
199    ///
200    /// # Arguments
201    /// * `table_id` - The table to scan
202    /// * `start_key` - Optional start key for range scan
203    /// * `end_key` - Optional end key for range scan
204    /// * `limit` - Optional limit on number of results
205    /// * `schema` - Optional table schema for schema-aware parsing. When provided,
206    ///   enables accurate type detection and avoids heuristic-based parsing.
207    ///   Strongly recommended for Cassandra 5.0+ formats.
208    pub async fn scan(
209        &self,
210        table_id: &TableId,
211        start_key: Option<&RowKey>,
212        end_key: Option<&RowKey>,
213        limit: Option<usize>,
214        schema: Option<&crate::schema::TableSchema>,
215    ) -> Result<Vec<(RowKey, Value)>> {
216        // Scan SSTables directly
217        self.sstables
218            .scan(table_id, start_key, end_key, limit, schema)
219            .await
220    }
221
222    /// Partition-targeted scan for a fully-constrained `WHERE pk = ?` (Issue #949).
223    ///
224    /// Returns only the rows for the single partition identified by the raw
225    /// `partition_key` bytes, after pruning the SSTable set down to those whose
226    /// bloom filter / BTI trie admit the key — so unrelated SSTables are never
227    /// parsed. Output matches filtering the full [`scan`](Self::scan) result to the
228    /// partition. Delegates to [`SSTableManager::scan_partition`] (which has a
229    /// bloom-prune implementation for the default build and a scan-and-filter
230    /// fallback for the `tombstones` build, so callers need no cfg branching).
231    pub async fn scan_partition(
232        &self,
233        table_id: &TableId,
234        partition_key: &[u8],
235        schema: Option<&crate::schema::TableSchema>,
236    ) -> Result<Vec<(RowKey, Value)>> {
237        self.sstables
238            .scan_partition(table_id, partition_key, schema)
239            .await
240    }
241
242    /// Scan a table and return per-cell write metadata alongside row values.
243    ///
244    /// Delegates to [`SSTableManager::scan_with_cell_metadata`].  Used when
245    /// `ProjectionFlags::include_cell_metadata` is set (issue #693).
246    pub async fn scan_with_cell_metadata(
247        &self,
248        table_id: &TableId,
249        start_key: Option<&RowKey>,
250        end_key: Option<&RowKey>,
251        limit: Option<usize>,
252        schema: Option<&crate::schema::TableSchema>,
253    ) -> Result<
254        Vec<(
255            RowKey,
256            Value,
257            std::collections::HashMap<String, CellWriteMetadata>,
258        )>,
259    > {
260        self.sstables
261            .scan_with_cell_metadata(table_id, start_key, end_key, limit, schema)
262            .await
263    }
264
265    /// Streaming scan (issue #790): return a bounded channel that yields
266    /// `(RowKey, Value)` entries lazily in key (token) order, instead of the
267    /// materializing [`scan`](Self::scan) that returns the whole `Vec`.
268    ///
269    /// Live heap is bounded by `buffer_size` rows rather than growing O(rows),
270    /// so streaming a large `SELECT *` no longer holds the entire result set in
271    /// memory at once. Delegates to [`SSTableManager::scan_stream`].
272    ///
273    /// [`SSTableManager::scan_stream`]: sstable::SSTableManager::scan_stream
274    pub async fn scan_stream(
275        &self,
276        table_id: &TableId,
277        start_key: Option<&RowKey>,
278        end_key: Option<&RowKey>,
279        schema: Option<&crate::schema::TableSchema>,
280        buffer_size: usize,
281    ) -> Result<tokio::sync::mpsc::Receiver<Result<(RowKey, Value)>>> {
282        self.sstables
283            .scan_stream(table_id, start_key, end_key, schema, buffer_size)
284            .await
285    }
286
287    /// Flush MemTable to SSTable
288    ///
289    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
290    /// This method is feature-gated behind 'experimental' but currently unimplemented.
291    #[allow(dead_code)]
292    #[cfg(feature = "experimental")]
293    async fn flush_memtable(&self) -> Result<()> {
294        Err(crate::error::Error::UnsupportedFormat(
295            "Write operations (flush_memtable) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
296        ))
297    }
298
299    /// Force flush all pending writes
300    ///
301    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
302    /// This method is feature-gated behind 'experimental' but currently unimplemented.
303    #[cfg(feature = "experimental")]
304    pub async fn flush(&self) -> Result<()> {
305        Err(crate::error::Error::UnsupportedFormat(
306            "Write operations (flush) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
307        ))
308    }
309
310    /// Perform manual compaction
311    #[cfg(feature = "experimental")]
312    pub async fn compact(&self) -> Result<()> {
313        // TODO: Implement proper compaction logic
314        // This would need to identify candidates and call CompactionManager::run_compaction
315        Ok(())
316    }
317
318    /// Get storage statistics
319    ///
320    /// NOTE: Issue #176 removed compaction stats (compaction.rs deleted).
321    pub async fn stats(&self) -> Result<StorageStats> {
322        let sstable_stats = self.sstables.stats().await?;
323
324        Ok(StorageStats {
325            sstables: sstable_stats,
326        })
327    }
328
329    /// Batch write operations for better performance
330    ///
331    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
332    /// This method is feature-gated behind 'experimental' but currently unimplemented.
333    #[cfg(feature = "experimental")]
334    pub async fn batch_write(&mut self, _operations: Vec<BatchOperation>) -> Result<()> {
335        Err(crate::error::Error::UnsupportedFormat(
336            "Write operations (batch_write) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
337        ))
338    }
339
340    /// Explicit batch flush
341    ///
342    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
343    /// This method is feature-gated behind 'experimental' but currently unimplemented.
344    #[cfg(feature = "experimental")]
345    pub async fn flush_batch(&mut self) -> Result<()> {
346        Err(crate::error::Error::UnsupportedFormat(
347            "Write operations (flush_batch) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
348        ))
349    }
350
351    /// Get batch writer statistics
352    ///
353    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
354    /// This method is feature-gated behind 'experimental' but currently unimplemented.
355    #[cfg(feature = "experimental")]
356    pub fn batch_stats(&self) -> Option<()> {
357        None
358    }
359
360    /// Shutdown the storage engine
361    ///
362    /// NOTE: Issue #176 removed compaction shutdown (compaction.rs deleted).
363    /// Issue #175 removed flush operations (WAL/MemTable deleted).
364    pub async fn shutdown(&self) -> Result<()> {
365        // Nothing to shutdown - read-only storage layer
366        Ok(())
367    }
368
369    /// Set the schema registry for schema-aware operations
370    ///
371    /// This method propagates the schema registry to the SSTable manager,
372    /// which will apply it to all SSTable readers for schema-aware parsing.
373    #[cfg(feature = "state_machine")]
374    pub async fn set_schema_registry(
375        &self,
376        registry: Arc<RwLock<crate::schema::SchemaRegistry>>,
377    ) -> Result<()> {
378        // Store in our field
379        {
380            let mut schema_reg = self.schema_registry.write().await;
381            *schema_reg = Some(registry.clone());
382        }
383
384        // Propagate to SSTable manager
385        self.sstables.set_schema_registry(registry).await
386    }
387}
388
389/// Batch operation types
390#[cfg(feature = "experimental")]
391#[derive(Debug, Clone)]
392pub enum BatchOperation {
393    /// Put operation
394    Put {
395        table_id: TableId,
396        key: RowKey,
397        value: Value,
398    },
399    /// Delete operation
400    Delete { table_id: TableId, key: RowKey },
401    /// Merge operation
402    Merge {
403        table_id: TableId,
404        key: RowKey,
405        value: Value,
406    },
407}
408
409/// Storage engine statistics
410///
411/// NOTE: Issue #176 removed compaction statistics (compaction.rs deleted).
412#[derive(Debug, Clone)]
413pub struct StorageStats {
414    /// SSTable statistics
415    pub sstables: sstable::SSTableStats,
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use tempfile::TempDir;
422
423    #[tokio::test]
424    async fn test_storage_engine_creation() {
425        let temp_dir = TempDir::new().unwrap();
426        let config = Config::test_config();
427        let platform = Arc::new(Platform::new(&config).await.unwrap());
428
429        let storage = StorageEngine::open(
430            temp_dir.path(),
431            &config,
432            platform,
433            #[cfg(feature = "state_machine")]
434            None,
435        )
436        .await
437        .unwrap();
438        let stats = storage.stats().await.unwrap();
439
440        assert_eq!(stats.sstables.sstable_count, 0);
441        storage.shutdown().await.unwrap();
442    }
443
444    #[tokio::test]
445    async fn test_storage_engine_with_discovered_sstables() {
446        let temp_dir = TempDir::new().unwrap();
447        let config = Config::test_config();
448        let platform = Arc::new(Platform::new(&config).await.unwrap());
449
450        // Create an empty list of discovered SSTables for this test
451        let discovered_paths = Vec::new();
452
453        let storage = StorageEngine::open_with_sstables(
454            temp_dir.path(),
455            discovered_paths,
456            &config,
457            platform,
458            #[cfg(feature = "state_machine")]
459            None,
460        )
461        .await
462        .unwrap();
463
464        let stats = storage.stats().await.unwrap();
465
466        // Should have 0 SSTables since we provided an empty list
467        assert_eq!(stats.sstables.sstable_count, 0);
468        storage.shutdown().await.unwrap();
469    }
470
471    #[tokio::test]
472    #[cfg(all(feature = "legacy-heuristics", feature = "experimental"))]
473    async fn test_batch_operations() {
474        let temp_dir = TempDir::new().unwrap();
475        let config = Config::default();
476        let platform = Arc::new(Platform::new(&config).await.unwrap());
477
478        let mut storage = StorageEngine::open(
479            temp_dir.path(),
480            &config,
481            platform,
482            #[cfg(feature = "state_machine")]
483            None,
484        )
485        .await
486        .unwrap();
487
488        // Test batch write operations
489        let batch_ops = vec![
490            BatchOperation::Put {
491                table_id: TableId::new("test_table"),
492                key: RowKey::from("key1"),
493                value: Value::Text("value1".to_string()),
494            },
495            BatchOperation::Put {
496                table_id: TableId::new("test_table"),
497                key: RowKey::from("key2"),
498                value: Value::Text("value2".to_string()),
499            },
500            BatchOperation::Delete {
501                table_id: TableId::new("test_table"),
502                key: RowKey::from("key3"),
503            },
504        ];
505
506        storage.batch_write(batch_ops).await.unwrap();
507        storage.shutdown().await.unwrap();
508    }
509
510    #[tokio::test]
511    #[cfg(all(feature = "legacy-heuristics", feature = "experimental"))]
512    async fn test_batch_operations_fallback() {
513        // Add timeout to prevent hanging in parallel test execution
514        tokio::time::timeout(std::time::Duration::from_secs(30), async {
515            let temp_dir = TempDir::new().unwrap();
516            let mut config = Config::default();
517            // Force fallback path by setting small threshold so batch writer is not initialized
518            config.storage.memtable_size_threshold = 1024; // 1KB - smaller than 1MB threshold
519            let platform = Arc::new(Platform::new(&config).await.unwrap());
520
521            let mut storage = StorageEngine::open(
522                temp_dir.path(),
523                &config,
524                platform,
525                #[cfg(feature = "state_machine")]
526                None,
527            )
528            .await
529            .unwrap();
530
531            // Test batch write operations (should use fallback path)
532            let batch_ops = vec![
533                BatchOperation::Put {
534                    table_id: TableId::new("test_table"),
535                    key: RowKey::from("key1"),
536                    value: Value::Text("value1".to_string()),
537                },
538                BatchOperation::Put {
539                    table_id: TableId::new("test_table"),
540                    key: RowKey::from("key2"),
541                    value: Value::Text("value2".to_string()),
542                },
543                BatchOperation::Delete {
544                    table_id: TableId::new("test_table"),
545                    key: RowKey::from("key3"),
546                },
547            ];
548
549            storage.batch_write(batch_ops).await.unwrap();
550            storage.shutdown().await.unwrap();
551        })
552        .await
553        .expect("Test should complete within 30 seconds");
554    }
555}