Skip to main content

cqlite_core/storage/
mod.rs

1//! Storage engine implementation for CQLite
2
3/// Shared, bytes-bounded, sharded decompressed-chunk cache (issue #1567).
4pub mod cache;
5pub mod sstable;
6
7// Canonical partition-key (de)serialization, shared by the read (query) path
8// and the write engine so the two never drift (Issue #586). Always compiled —
9// the scan path needs it even without `write-support`.
10pub mod partition_key_codec;
11
12// M5: Write engine and serialization (Issue #359)
13#[cfg(feature = "write-support")]
14pub mod serialization;
15#[cfg(feature = "write-support")]
16pub mod write_engine;
17
18// REPL data access components (Issue #249: CLI-specific)
19#[cfg(feature = "cli-helpers")]
20pub mod repl_data_api;
21pub mod schema_discovery;
22pub mod sstable_data_manager;
23
24use std::path::{Path, PathBuf};
25use std::sync::Arc;
26#[cfg(feature = "state_machine")]
27use tokio::sync::RwLock;
28
29use crate::platform::Platform;
30use crate::{
31    types::{CellWriteMetadata, TableId},
32    Config, Result, RowKey, ScanRow,
33};
34// `Value` is only referenced by the experimental write API (`put` / `BatchOperation`);
35// gate the import so the default build does not flag it unused (issue #1334).
36#[cfg(feature = "experimental")]
37use crate::types::Value;
38
39// Test/feature-gated whole-table-scan invocation counter (issue #1691).
40//
41// Counts entries into the two whole-table scan initiators — `StorageEngine::scan`
42// (materializing) and `StorageEngine::scan_stream` (bounded streaming). It exists to
43// pin the retirement of `execute_parallel_table_scan`: a `TableScan` plan must issue
44// exactly ONE whole-table pass, not the 4× duplicate passes the retired multi-worker
45// path produced.
46//
47// It is thread-local, mirroring the "thread-local invocation counter reflects exactly
48// this future's calls" pattern (issue #831 / `access_path`). A test drives its
49// measured operation on a current-thread Tokio runtime, so both `scan` and
50// `scan_stream` increment on the *calling* thread (the count records the entry, not
51// the spawned producer's work). This isolates the count from the thousands of other
52// lib tests running on their own threads — a process-global atomic would be polluted
53// by any concurrent test that scans. Zero-overhead in release: the body compiles to a
54// no-op and the cell is not even linked.
55#[cfg(any(test, feature = "work-counters"))]
56thread_local! {
57    static TABLE_SCAN_CALLS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
58}
59
60/// Record one whole-table scan initiation (`scan` / `scan_stream`) on the current
61/// thread. Unconditional at the call site; the body is a no-op in release builds
62/// (issue #1691).
63#[inline(always)]
64pub(crate) fn record_table_scan_call() {
65    #[cfg(any(test, feature = "work-counters"))]
66    TABLE_SCAN_CALLS.with(|c| c.set(c.get().saturating_add(1)));
67}
68
69/// Number of whole-table scan initiations on the current thread since the last
70/// [`reset_table_scan_calls`] (issue #1691; test/feature builds only).
71#[cfg(any(test, feature = "work-counters"))]
72pub fn table_scan_call_count() -> u64 {
73    TABLE_SCAN_CALLS.with(|c| c.get())
74}
75
76/// Clear the current thread's whole-table scan counter before a measured operation
77/// (issue #1691; test/feature builds only).
78#[cfg(any(test, feature = "work-counters"))]
79pub fn reset_table_scan_calls() {
80    TABLE_SCAN_CALLS.with(|c| c.set(0));
81}
82
83/// Main storage engine that coordinates all storage components
84///
85/// NOTE: Issue #176 removed write infrastructure (compaction, manifest).
86/// This is now a read-only storage layer focused on SSTable access.
87#[derive(Debug)]
88pub struct StorageEngine {
89    /// SSTable manager for persistent storage
90    sstables: Arc<sstable::SSTableManager>,
91
92    /// Platform abstraction
93    #[allow(dead_code)]
94    _platform: Arc<Platform>,
95
96    /// Storage configuration
97    #[allow(dead_code)]
98    config: Config,
99
100    /// Schema registry for schema-aware operations (feature-gated)
101    #[cfg(feature = "state_machine")]
102    schema_registry: Arc<RwLock<Option<Arc<RwLock<crate::schema::SchemaRegistry>>>>>,
103}
104
105impl StorageEngine {
106    /// Open a storage engine at the given path
107    ///
108    /// This method discovers SSTables by scanning the storage directory.
109    /// For pre-discovered SSTables, use `open_with_sstables` instead.
110    ///
111    /// NOTE: Issue #176 removed write infrastructure (compaction, manifest).
112    /// This is now a read-only storage layer focused on SSTable access.
113    // `skip_all` (not an explicit skip list): the `schema_registry` parameter is
114    // `#[cfg(feature = "state_machine")]`-gated, so naming it in `skip(...)` would
115    // reference a nonexistent binding in the minimal build. `skip_all` records no
116    // args as fields regardless of cfg; the `fields(...)` below are set in the body.
117    #[tracing::instrument(
118        name = "storage.engine.open",
119        skip_all,
120        fields(sstables = tracing::field::Empty, bytes = tracing::field::Empty)
121    )]
122    pub async fn open(
123        path: &Path,
124        config: &Config,
125        platform: Arc<Platform>,
126        #[cfg(feature = "state_machine")] schema_registry: Option<
127            Arc<RwLock<crate::schema::SchemaRegistry>>,
128        >,
129    ) -> Result<Self> {
130        // Create storage directory if it doesn't exist
131        crate::observability::record_result("reader", platform.fs().create_dir_all(path).await)?;
132
133        // Initialize SSTable manager with schema registry
134        let sstables = Arc::new(crate::observability::record_result(
135            "reader",
136            sstable::SSTableManager::new(
137                path,
138                config,
139                platform.clone(),
140                #[cfg(feature = "state_machine")]
141                schema_registry.clone(),
142            )
143            .await,
144        )?);
145
146        Self::record_discovery_metrics(&sstables).await;
147
148        Ok(Self {
149            sstables,
150            _platform: platform,
151            config: config.clone(),
152            #[cfg(feature = "state_machine")]
153            schema_registry: Arc::new(RwLock::new(schema_registry)),
154        })
155    }
156
157    /// Emit SSTable-discovery telemetry (issue #1034) for a freshly built
158    /// manager: total SSTables discovered, their on-disk byte total, and the
159    /// number of logical tables. Best-effort — a stats failure is logged and the
160    /// open continues, since telemetry must never change open behaviour.
161    async fn record_discovery_metrics(sstables: &sstable::SSTableManager) {
162        use crate::observability::{self as obs, catalog};
163        match sstables.stats().await {
164            Ok(stats) => {
165                tracing::Span::current().record("sstables", stats.sstable_count as u64);
166                tracing::Span::current().record("bytes", stats.total_size);
167                obs::add_counter(
168                    catalog::STORAGE_OPEN_SSTABLES,
169                    stats.sstable_count as u64,
170                    &[],
171                );
172                obs::add_counter(catalog::STORAGE_OPEN_BYTES, stats.total_size, &[]);
173                obs::add_counter(catalog::STORAGE_OPEN_TABLES, stats.total_tables, &[]);
174            }
175            Err(e) => {
176                log::debug!("storage.engine.open: discovery metrics unavailable: {}", e);
177            }
178        }
179    }
180
181    /// Open a storage engine with pre-discovered SSTable table directories
182    ///
183    /// This method is used when SSTables have been discovered externally (e.g., by DiscoveryService)
184    /// and allows the storage engine to be initialized with specific table directories rather than
185    /// scanning the storage directory. Each table directory will be scanned for Data.db files.
186    ///
187    /// # Arguments
188    /// * `path` - Base storage path for manifest and SSTable operations
189    /// * `discovered_table_dirs` - Vector of table directory paths (each containing SSTable files)
190    /// * `config` - Storage configuration
191    /// * `platform` - Platform abstraction for I/O operations
192    ///
193    /// # Returns
194    /// A StorageEngine instance with all components initialized, including SSTable readers
195    /// for all Data.db files found in the discovered table directories.
196    ///
197    /// # Example
198    /// ```no_run
199    /// # use std::path::{Path, PathBuf};
200    /// # use std::sync::Arc;
201    /// # use cqlite_core::{Config, Platform, storage::StorageEngine};
202    /// # async fn example() -> cqlite_core::Result<()> {
203    /// let config = Config::default();
204    /// let platform = Arc::new(Platform::new(&config).await?);
205    /// let storage_path = Path::new("/var/lib/cqlite/storage");
206    /// let discovered_table_dirs = vec![
207    ///     PathBuf::from("/var/lib/cassandra/keyspace1/table1-abc123"),
208    ///     PathBuf::from("/var/lib/cassandra/keyspace1/table2-def456"),
209    /// ];
210    ///
211    /// let engine = StorageEngine::open_with_sstables(
212    ///     storage_path,
213    ///     discovered_table_dirs,
214    ///     &config,
215    ///     platform,
216    ///     #[cfg(feature = "state_machine")]
217    ///     None,
218    /// ).await?;
219    /// # Ok(())
220    /// # }
221    /// ```
222    // `skip_all`: see the note on `open` — the cfg-gated `schema_registry` cannot
223    // be named in an explicit `skip(...)` without breaking the minimal build.
224    #[tracing::instrument(
225        name = "storage.engine.open",
226        skip_all,
227        fields(sstables = tracing::field::Empty, bytes = tracing::field::Empty)
228    )]
229    pub async fn open_with_sstables(
230        path: &Path,
231        discovered_table_dirs: Vec<PathBuf>,
232        config: &Config,
233        platform: Arc<Platform>,
234        #[cfg(feature = "state_machine")] schema_registry: Option<
235            Arc<RwLock<crate::schema::SchemaRegistry>>,
236        >,
237    ) -> Result<Self> {
238        // Create storage directory if it doesn't exist
239        crate::observability::record_result("reader", platform.fs().create_dir_all(path).await)?;
240
241        // Initialize SSTable manager with pre-discovered paths and schema registry
242        let sstables = Arc::new(crate::observability::record_result(
243            "reader",
244            sstable::SSTableManager::new_from_discovered_paths(
245                path,
246                discovered_table_dirs,
247                config,
248                platform.clone(),
249                #[cfg(feature = "state_machine")]
250                schema_registry.clone(),
251            )
252            .await,
253        )?);
254
255        Self::record_discovery_metrics(&sstables).await;
256
257        Ok(Self {
258            sstables,
259            _platform: platform,
260            config: config.clone(),
261            #[cfg(feature = "state_machine")]
262            schema_registry: Arc::new(RwLock::new(schema_registry)),
263        })
264    }
265
266    /// Insert a key-value pair
267    ///
268    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
269    /// This method is feature-gated behind 'experimental' but currently unimplemented.
270    #[cfg(feature = "experimental")]
271    pub async fn put(&self, _table_id: &TableId, _key: RowKey, _value: Value) -> Result<()> {
272        Err(crate::error::Error::UnsupportedFormat(
273            "Write operations (put) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
274        ))
275    }
276
277    /// Get a value by key
278    pub async fn get(&self, table_id: &TableId, key: &RowKey) -> Result<Option<ScanRow>> {
279        // Check SSTables
280        self.sstables.get(table_id, key).await
281    }
282
283    /// Re-scan the data directory and atomically apply added/removed SSTable
284    /// generations to the held reader set (issue #1749).
285    ///
286    /// # Freshness contract
287    ///
288    /// A `StorageEngine` (and the [`Database`](crate::Database) built on it)
289    /// snapshots the discovered SSTable generations **at open**; it does not
290    /// re-scan on its own. This is the ONLY way the reader set changes for a
291    /// long-lived handle. Re-runs the same TOC/filename-based discovery `open`
292    /// used — no content sniffing.
293    ///
294    /// - Added generations become queryable; removed generations stop being
295    ///   queried; unchanged generations keep their warm parsed state.
296    /// - **In-flight queries are unaffected**: a scan already running holds its
297    ///   own `Arc` reader clones and completes against the pre-refresh set;
298    ///   queries started after the refresh see the new set.
299    /// - **Atomic / fail-closed**: if any newly discovered generation fails to
300    ///   open (e.g. a corrupt `Statistics.db`, issue #1626), the typed error is
301    ///   returned and the previously held reader set is left fully unchanged.
302    pub async fn refresh(&self) -> Result<sstable::RefreshReport> {
303        self.sstables.refresh_tables().await
304    }
305
306    /// Delete a key
307    ///
308    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
309    /// This method is feature-gated behind 'experimental' but currently unimplemented.
310    #[cfg(feature = "experimental")]
311    pub async fn delete(&self, _table_id: &TableId, _key: RowKey) -> Result<()> {
312        Err(crate::error::Error::UnsupportedFormat(
313            "Write operations (delete) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
314        ))
315    }
316
317    /// Scan a range of keys
318    ///
319    /// # Arguments
320    /// * `table_id` - The table to scan
321    /// * `start_key` - Optional start key for range scan
322    /// * `end_key` - Optional end key for range scan
323    /// * `limit` - Optional limit on number of results
324    /// * `schema` - Optional table schema for schema-aware parsing. When provided,
325    ///   enables accurate type detection and avoids heuristic-based parsing.
326    ///   Strongly recommended for Cassandra 5.0+ formats.
327    pub async fn scan(
328        &self,
329        table_id: &TableId,
330        start_key: Option<&RowKey>,
331        end_key: Option<&RowKey>,
332        limit: Option<usize>,
333        schema: Option<&crate::schema::TableSchema>,
334    ) -> Result<Vec<(RowKey, ScanRow)>> {
335        record_table_scan_call();
336        // Scan SSTables directly
337        self.sstables
338            .scan(table_id, start_key, end_key, limit, schema)
339            .await
340    }
341
342    /// Partition-targeted scan for a fully-constrained `WHERE pk = ?` (Issue #949).
343    ///
344    /// Returns only the rows for the single partition identified by the raw
345    /// `partition_key` bytes, after pruning the SSTable set down to those whose
346    /// bloom filter / BTI trie admit the key — so unrelated SSTables are never
347    /// parsed. Output matches filtering the full [`scan`](Self::scan) result to the
348    /// partition. Delegates to [`SSTableManager::scan_partition`] (which has a
349    /// bloom-prune implementation for the default build and a scan-and-filter
350    /// fallback for the `tombstones` build, so callers need no cfg branching).
351    ///
352    /// Returns `(rows, engaged)`: `engaged` is `true` only when the call actually
353    /// pruned the SSTable set to partition candidates (the default build). The
354    /// `tombstones` build returns `false` because it full-scans and retains with no
355    /// prune, so the caller reports an honest fallback access path (Epic #951).
356    pub async fn scan_partition(
357        &self,
358        table_id: &TableId,
359        partition_key: &[u8],
360        schema: Option<&crate::schema::TableSchema>,
361    ) -> Result<(Vec<(RowKey, ScanRow)>, bool)> {
362        self.sstables
363            .scan_partition(table_id, partition_key, schema)
364            .await
365    }
366
367    /// Clustering-slice-aware partition-targeted scan (Issue #954, Epic #951).
368    ///
369    /// Like [`scan_partition`](Self::scan_partition) but pushes a single-column
370    /// clustering-key restriction (`ck </>/= ?` / two-bound range) down to a
371    /// within-partition seek when the candidate's authoritative row index supports
372    /// it, so a wide-partition slice decodes O(matched rows + index) rather than
373    /// the whole partition. Returns `(rows, clustering_seek_engaged)`: the rows are
374    /// the full partition (or a clustering-narrowed superset) so the caller's
375    /// post-scan filter yields byte-identical output, and the bool reports whether
376    /// the clustering narrowing actually engaged (for the `ClusteringSlice` access
377    /// path). Delegates to [`SSTableManager::scan_partition_clustering`].
378    #[cfg(not(feature = "tombstones"))]
379    pub async fn scan_partition_clustering(
380        &self,
381        table_id: &TableId,
382        partition_key: &[u8],
383        clustering: Option<&crate::storage::sstable::reader::ClusteringSlice>,
384        schema: Option<&crate::schema::TableSchema>,
385    ) -> Result<(Vec<(RowKey, ScanRow)>, bool)> {
386        self.sstables
387            .scan_partition_clustering(table_id, partition_key, clustering, schema)
388            .await
389    }
390
391    /// Reverse single-partition clustering scan for a BIG (`nb`) wide partition
392    /// (Issue #1184). Returns `Ok(Some(rows))` in DESCENDING clustering order when
393    /// the BIG promoted-index reverse iterator applied, or `Ok(None)` to tell the
394    /// caller to keep the in-memory `ORDER BY DESC` sort (small / BTI /
395    /// multi-generation cases). Delegates to
396    /// [`SSTableManager::scan_partition_clustering_reverse`].
397    #[cfg(not(feature = "tombstones"))]
398    pub async fn scan_partition_clustering_reverse(
399        &self,
400        table_id: &TableId,
401        partition_key: &[u8],
402        schema: Option<&crate::schema::TableSchema>,
403    ) -> Result<Option<Vec<(RowKey, ScanRow)>>> {
404        self.sstables
405            .scan_partition_clustering_reverse(table_id, partition_key, schema)
406            .await
407    }
408
409    /// Partition-targeted, metadata-carrying scan for a fully-constrained
410    /// `WHERE pk = ?` WRITETIME/TTL projection (Issue #962).
411    ///
412    /// The metadata sibling of [`scan_partition`](Self::scan_partition): returns
413    /// only the rows for the single partition identified by the raw
414    /// `partition_key` bytes, WITH per-cell write metadata, after pruning the
415    /// SSTable set down to the candidates whose bloom filter / BTI trie admit the
416    /// key — so a `SELECT WRITETIME(col) ... WHERE pk = ?` never opens all N
417    /// SSTables. Output matches filtering the full
418    /// [`scan_with_cell_metadata`](Self::scan_with_cell_metadata) result to the
419    /// partition; cross-generation reconciliation runs over the pruned candidates.
420    /// Delegates to [`SSTableManager::scan_partition_with_cell_metadata`].
421    ///
422    /// Returns `(rows, engaged)`: `engaged` is `true` only when the call pruned the
423    /// SSTable set to partition candidates (the default build). The `tombstones`
424    /// build returns `false` because it full-scans with metadata and retains with no
425    /// prune, so the caller reports an honest fallback access path (Epic #951).
426    pub async fn scan_partition_with_cell_metadata(
427        &self,
428        table_id: &TableId,
429        partition_key: &[u8],
430        schema: Option<&crate::schema::TableSchema>,
431    ) -> Result<(
432        Vec<(
433            RowKey,
434            ScanRow,
435            std::collections::HashMap<String, CellWriteMetadata>,
436        )>,
437        bool,
438    )> {
439        self.sstables
440            .scan_partition_with_cell_metadata(table_id, partition_key, schema)
441            .await
442    }
443
444    /// Scan a table and return per-cell write metadata alongside row values.
445    ///
446    /// Delegates to [`SSTableManager::scan_with_cell_metadata`].  Used when
447    /// `ProjectionFlags::include_cell_metadata` is set (issue #693).
448    pub async fn scan_with_cell_metadata(
449        &self,
450        table_id: &TableId,
451        start_key: Option<&RowKey>,
452        end_key: Option<&RowKey>,
453        limit: Option<usize>,
454        schema: Option<&crate::schema::TableSchema>,
455    ) -> Result<
456        Vec<(
457            RowKey,
458            ScanRow,
459            std::collections::HashMap<String, CellWriteMetadata>,
460        )>,
461    > {
462        self.sstables
463            .scan_with_cell_metadata(table_id, start_key, end_key, limit, schema)
464            .await
465    }
466
467    /// Streaming scan (issue #790): return a bounded channel that yields
468    /// `(RowKey, ScanRow)` entries lazily in key (token) order, instead of the
469    /// materializing [`scan`](Self::scan) that returns the whole `Vec`.
470    ///
471    /// Live heap is bounded by `buffer_size` rows rather than growing O(rows),
472    /// so streaming a large `SELECT *` no longer holds the entire result set in
473    /// memory at once. Delegates to [`SSTableManager::scan_stream`].
474    ///
475    /// [`SSTableManager::scan_stream`]: sstable::SSTableManager::scan_stream
476    pub async fn scan_stream(
477        &self,
478        table_id: &TableId,
479        start_key: Option<&RowKey>,
480        end_key: Option<&RowKey>,
481        schema: Option<&crate::schema::TableSchema>,
482        buffer_size: usize,
483    ) -> Result<tokio::sync::mpsc::Receiver<Result<(RowKey, ScanRow)>>> {
484        record_table_scan_call();
485        self.sstables
486            .scan_stream(table_id, start_key, end_key, schema, buffer_size)
487            .await
488    }
489
490    /// Flush MemTable to SSTable
491    ///
492    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
493    /// This method is feature-gated behind 'experimental' but currently unimplemented.
494    #[allow(dead_code)]
495    #[cfg(feature = "experimental")]
496    async fn flush_memtable(&self) -> Result<()> {
497        Err(crate::error::Error::UnsupportedFormat(
498            "Write operations (flush_memtable) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
499        ))
500    }
501
502    /// Force flush all pending writes
503    ///
504    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
505    /// This method is feature-gated behind 'experimental' but currently unimplemented.
506    #[cfg(feature = "experimental")]
507    pub async fn flush(&self) -> Result<()> {
508        Err(crate::error::Error::UnsupportedFormat(
509            "Write operations (flush) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
510        ))
511    }
512
513    /// Perform manual compaction
514    #[cfg(feature = "experimental")]
515    pub async fn compact(&self) -> Result<()> {
516        // TODO: Implement proper compaction logic
517        // This would need to identify candidates and call CompactionManager::run_compaction
518        Ok(())
519    }
520
521    /// Get storage statistics
522    ///
523    /// NOTE: Issue #176 removed compaction stats (compaction.rs deleted).
524    pub async fn stats(&self) -> Result<StorageStats> {
525        let sstable_stats = self.sstables.stats().await?;
526
527        Ok(StorageStats {
528            sstables: sstable_stats,
529        })
530    }
531
532    /// Batch write operations for better performance
533    ///
534    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
535    /// This method is feature-gated behind 'experimental' but currently unimplemented.
536    #[cfg(feature = "experimental")]
537    pub async fn batch_write(&mut self, _operations: Vec<BatchOperation>) -> Result<()> {
538        Err(crate::error::Error::UnsupportedFormat(
539            "Write operations (batch_write) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
540        ))
541    }
542
543    /// Explicit batch flush
544    ///
545    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
546    /// This method is feature-gated behind 'experimental' but currently unimplemented.
547    #[cfg(feature = "experimental")]
548    pub async fn flush_batch(&mut self) -> Result<()> {
549        Err(crate::error::Error::UnsupportedFormat(
550            "Write operations (flush_batch) removed in Issue #175 - WAL and MemTable infrastructure deleted".to_string()
551        ))
552    }
553
554    /// Get batch writer statistics
555    ///
556    /// NOTE: Write functionality removed in Issue #175 (WAL/MemTable infrastructure deleted).
557    /// This method is feature-gated behind 'experimental' but currently unimplemented.
558    #[cfg(feature = "experimental")]
559    pub fn batch_stats(&self) -> Option<()> {
560        None
561    }
562
563    /// Shutdown the storage engine
564    ///
565    /// NOTE: Issue #176 removed compaction shutdown (compaction.rs deleted).
566    /// Issue #175 removed flush operations (WAL/MemTable deleted).
567    pub async fn shutdown(&self) -> Result<()> {
568        // Nothing to shutdown - read-only storage layer
569        Ok(())
570    }
571
572    /// Set the schema registry for schema-aware operations
573    ///
574    /// This method propagates the schema registry to the SSTable manager,
575    /// which will apply it to all SSTable readers for schema-aware parsing.
576    #[cfg(feature = "state_machine")]
577    pub async fn set_schema_registry(
578        &self,
579        registry: Arc<RwLock<crate::schema::SchemaRegistry>>,
580    ) -> Result<()> {
581        // Store in our field
582        {
583            let mut schema_reg = self.schema_registry.write().await;
584            *schema_reg = Some(registry.clone());
585        }
586
587        // Propagate to SSTable manager
588        self.sstables.set_schema_registry(registry).await
589    }
590}
591
592/// Batch operation types
593#[cfg(feature = "experimental")]
594#[derive(Debug, Clone)]
595pub enum BatchOperation {
596    /// Put operation
597    Put {
598        table_id: TableId,
599        key: RowKey,
600        value: Value,
601    },
602    /// Delete operation
603    Delete { table_id: TableId, key: RowKey },
604    /// Merge operation
605    Merge {
606        table_id: TableId,
607        key: RowKey,
608        value: Value,
609    },
610}
611
612/// Storage engine statistics
613///
614/// NOTE: Issue #176 removed compaction statistics (compaction.rs deleted).
615#[derive(Debug, Clone)]
616pub struct StorageStats {
617    /// SSTable statistics
618    pub sstables: sstable::SSTableStats,
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624    use tempfile::TempDir;
625
626    #[tokio::test]
627    async fn test_storage_engine_creation() {
628        let temp_dir = TempDir::new().unwrap();
629        let config = Config::test_config();
630        let platform = Arc::new(Platform::new(&config).await.unwrap());
631
632        let storage = StorageEngine::open(
633            temp_dir.path(),
634            &config,
635            platform,
636            #[cfg(feature = "state_machine")]
637            None,
638        )
639        .await
640        .unwrap();
641        let stats = storage.stats().await.unwrap();
642
643        assert_eq!(stats.sstables.sstable_count, 0);
644        storage.shutdown().await.unwrap();
645    }
646
647    #[tokio::test]
648    async fn test_storage_engine_with_discovered_sstables() {
649        let temp_dir = TempDir::new().unwrap();
650        let config = Config::test_config();
651        let platform = Arc::new(Platform::new(&config).await.unwrap());
652
653        // Create an empty list of discovered SSTables for this test
654        let discovered_paths = Vec::new();
655
656        let storage = StorageEngine::open_with_sstables(
657            temp_dir.path(),
658            discovered_paths,
659            &config,
660            platform,
661            #[cfg(feature = "state_machine")]
662            None,
663        )
664        .await
665        .unwrap();
666
667        let stats = storage.stats().await.unwrap();
668
669        // Should have 0 SSTables since we provided an empty list
670        assert_eq!(stats.sstables.sstable_count, 0);
671        storage.shutdown().await.unwrap();
672    }
673
674    #[tokio::test]
675    #[cfg(all(feature = "legacy-heuristics", feature = "experimental"))]
676    async fn test_batch_operations() {
677        let temp_dir = TempDir::new().unwrap();
678        let config = Config::default();
679        let platform = Arc::new(Platform::new(&config).await.unwrap());
680
681        let mut storage = StorageEngine::open(
682            temp_dir.path(),
683            &config,
684            platform,
685            #[cfg(feature = "state_machine")]
686            None,
687        )
688        .await
689        .unwrap();
690
691        // Test batch write operations
692        let batch_ops = vec![
693            BatchOperation::Put {
694                table_id: TableId::new("test_table"),
695                key: RowKey::from("key1"),
696                value: Value::Text("value1".to_string()),
697            },
698            BatchOperation::Put {
699                table_id: TableId::new("test_table"),
700                key: RowKey::from("key2"),
701                value: Value::Text("value2".to_string()),
702            },
703            BatchOperation::Delete {
704                table_id: TableId::new("test_table"),
705                key: RowKey::from("key3"),
706            },
707        ];
708
709        storage.batch_write(batch_ops).await.unwrap();
710        storage.shutdown().await.unwrap();
711    }
712
713    #[tokio::test]
714    #[cfg(all(feature = "legacy-heuristics", feature = "experimental"))]
715    async fn test_batch_operations_fallback() {
716        // Add timeout to prevent hanging in parallel test execution
717        tokio::time::timeout(std::time::Duration::from_secs(30), async {
718            let temp_dir = TempDir::new().unwrap();
719            let mut config = Config::default();
720            // Force fallback path by setting small threshold so batch writer is not initialized
721            config.storage.memtable_size_threshold = 1024; // 1KB - smaller than 1MB threshold
722            let platform = Arc::new(Platform::new(&config).await.unwrap());
723
724            let mut storage = StorageEngine::open(
725                temp_dir.path(),
726                &config,
727                platform,
728                #[cfg(feature = "state_machine")]
729                None,
730            )
731            .await
732            .unwrap();
733
734            // Test batch write operations (should use fallback path)
735            let batch_ops = vec![
736                BatchOperation::Put {
737                    table_id: TableId::new("test_table"),
738                    key: RowKey::from("key1"),
739                    value: Value::Text("value1".to_string()),
740                },
741                BatchOperation::Put {
742                    table_id: TableId::new("test_table"),
743                    key: RowKey::from("key2"),
744                    value: Value::Text("value2".to_string()),
745                },
746                BatchOperation::Delete {
747                    table_id: TableId::new("test_table"),
748                    key: RowKey::from("key3"),
749                },
750            ];
751
752            storage.batch_write(batch_ops).await.unwrap();
753            storage.shutdown().await.unwrap();
754        })
755        .await
756        .expect("Test should complete within 30 seconds");
757    }
758}