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}