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}