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