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