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}