Skip to main content

cqlite_core/
lib.rs

1//! CQLite Core Database Engine
2//!
3//! A high-performance, embeddable database engine with SSTable-based storage,
4//! supporting both native and WASM deployments.
5
6// Value-representation-v2 (D1, issue #1583): keep the public `Value` enum's
7// inline layout bounded. `large_enum_variant` fails the build if a future change
8// re-inlines a fat variant (e.g. un-boxing `Tombstone`/`Udt`/`Json`) instead of
9// boxing it, so the `size_of::<Value>() <= 40` pin in `types.rs` cannot silently
10// regress.
11#![deny(clippy::large_enum_variant)]
12
13pub mod config;
14pub mod config_removed_keys;
15pub mod cql;
16pub mod error;
17// FFI error contract moved to `cqlite_ffi_common::error_contract` (#1452; no re-export, see CHANGELOG).
18pub(crate) mod float_cmp;
19pub mod parser;
20pub mod types;
21pub mod util;
22pub mod version_hints;
23
24#[cfg(feature = "benchmarks")]
25pub mod benchmarks; // #1712: gate HERE so the crate root tells the truth (opt-in perf runs).
26pub mod memory;
27// Observability foundation (epic #1031, issues #1032 + #1038). Always present;
28// the OpenTelemetry exporter wiring inside it is gated behind the optional
29// `observability` feature, and all helpers compile to no-ops when it is off.
30pub mod observability;
31pub mod platform;
32#[cfg(feature = "state_machine")]
33pub mod query;
34pub mod schema;
35pub mod storage;
36
37// Embeddable export writers (Epic #682). The module is always present; the
38// Parquet writer inside it is gated behind the optional `parquet` feature.
39pub mod export;
40
41// M5: Write engine and serialization modules (Issue #359)
42// Re-exported at crate level for convenience when write-support is enabled
43#[cfg(feature = "write-support")]
44pub use storage::serialization;
45#[cfg(feature = "write-support")]
46pub use storage::write_engine;
47
48// Ingestion module for one-shot schema & SSTable discovery (Issue #249: CLI-specific)
49#[cfg(feature = "cli-helpers")]
50pub mod ingestion;
51
52// Discovery module for SSTable scanning and coverage analysis
53#[cfg(feature = "state_machine")]
54pub mod discovery;
55
56// Testing utilities - hidden from public docs via #[doc(hidden)] but available for integration tests
57#[doc(hidden)]
58pub mod testing;
59
60// Fuzz-support surface (issue #1614). Only compiled under `--features fuzz`, and
61// `#[doc(hidden)]` even then, so the default public API and docs are unchanged.
62// Exposes thin `Result`-returning drivers over the internal decode entry points
63// for the external `fuzz/` cargo-fuzz crate. See `fuzz_support.rs`.
64#[cfg(feature = "fuzz")]
65#[doc(hidden)]
66pub mod fuzz_support;
67
68// NOTE: memory_safety_runner moved to tools/memory-safety-runner (Issue #245)
69// NOTE: the orphaned memory_safety_tests module (never compiled since MemTable
70// was removed in Issue #175) was deleted in Issue #1568 — it exercised the
71// now-deleted MemoryManager cache core.
72
73// Test-only heap-allocation probe (issue #1590, E8). Installed as the global
74// allocator for `cqlite-core`'s unit-test binary so a test can count the heap
75// allocations a specific code path performs (see the `cartesian_product`
76// allocation regression test in the SELECT executor's `lookup` module). It
77// delegates every operation to the system allocator and only bumps a per-thread
78// counter while a measurement is ACTIVE, so it is inert for every other test.
79// `not(dhat-heap)`: mutually exclusive with `DHAT_TEST_ALLOC` below (#1668) —
80// only one `#[global_allocator]` per binary.
81#[cfg(all(test, feature = "state_machine", not(feature = "dhat-heap")))] // sole `measure` caller lives in the state_machine `query` module; else `-D dead-code` under minimal (#1981)
82pub(crate) mod test_alloc_probe {
83    use std::alloc::{GlobalAlloc, Layout, System};
84    use std::cell::Cell;
85
86    thread_local! {
87        static COUNT: Cell<u64> = const { Cell::new(0) };
88        static ACTIVE: Cell<bool> = const { Cell::new(false) };
89    }
90
91    pub(crate) struct CountingAllocator;
92
93    // SAFETY: every storage operation is delegated verbatim to `System`; the only
94    // added work is bumping a `Copy` thread-local counter, which never allocates
95    // (the `thread_local!`s are `const`-initialized, so first access is
96    // alloc-free — no reentrancy into the allocator).
97    unsafe impl GlobalAlloc for CountingAllocator {
98        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
99            bump();
100            System.alloc(layout)
101        }
102        unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
103            System.dealloc(ptr, layout)
104        }
105        // Counting `realloc` as an allocation is deliberate: it distinguishes a
106        // `Vec::clone()` + `push()` (a clone-alloc then a grow-realloc) from a
107        // single sized `Vec::with_capacity()` fill.
108        unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
109            bump();
110            System.realloc(ptr, layout, new_size)
111        }
112    }
113
114    fn bump() {
115        let _ = ACTIVE.try_with(|a| {
116            if a.get() {
117                let _ = COUNT.try_with(|c| c.set(c.get() + 1));
118            }
119        });
120    }
121
122    /// Count the heap allocations `f` performs ON THE CURRENT THREAD (thread-local
123    /// counter, so concurrent tests do not pollute each other). Returns
124    /// `(allocations, f's result)`.
125    pub(crate) fn measure<R>(f: impl FnOnce() -> R) -> (u64, R) {
126        ACTIVE.with(|a| a.set(true));
127        COUNT.with(|c| c.set(0));
128        let r = f();
129        let n = COUNT.with(|c| c.get());
130        ACTIVE.with(|a| a.set(false));
131        (n, r)
132    }
133}
134
135#[cfg(all(test, feature = "state_machine", not(feature = "dhat-heap")))]
136#[global_allocator]
137static TEST_ALLOC: test_alloc_probe::CountingAllocator = test_alloc_probe::CountingAllocator;
138
139// Issue #1668: dhat allocator for `cqlite-core`'s unit-test binary, so an
140// in-tree `#[cfg(test)]` test can drive `pub(crate)` `StreamingMerger`
141// directly (see `merge::streaming::streaming_dhat_test`'s doc). Run with
142// `--no-default-features --features write-support,dhat-heap` (excludes
143// `state_machine`, whose own allocator this would otherwise collide with).
144#[cfg(all(test, feature = "dhat-heap"))]
145#[global_allocator]
146static DHAT_TEST_ALLOC: dhat::Alloc = dhat::Alloc;
147
148// Re-export main types for convenience
149pub use crate::{
150    config::Config,
151    error::{Error, Result},
152    platform::Platform,
153    types::*,
154};
155
156// Explicit SSTable directory refresh report (issue #1749). Not feature-gated —
157// `Database::refresh()` is available in the minimal build too.
158pub use storage::sstable::RefreshReport;
159
160// Re-export query types when state_machine feature is enabled
161#[cfg(feature = "state_machine")]
162pub use query::SchemaStatus;
163
164use std::path::Path;
165#[cfg(feature = "state_machine")]
166use std::path::PathBuf;
167use std::sync::Arc;
168
169use crate::{memory::MemoryManager, storage::StorageEngine};
170
171#[cfg(feature = "state_machine")]
172use crate::schema::SchemaManager;
173
174#[cfg(feature = "state_machine")]
175use crate::query::QueryEngine;
176
177/// Main database handle
178///
179/// This is the primary interface for interacting with a CQLite database.
180/// It coordinates between the storage engine, schema manager, and query engine.
181#[derive(Debug)]
182pub struct Database {
183    storage: Arc<StorageEngine>,
184    #[cfg(feature = "state_machine")]
185    query: Arc<QueryEngine>,
186    memory: Arc<MemoryManager>,
187    config: Config,
188}
189
190impl Database {
191    /// Open a database at the given path with the specified configuration
192    ///
193    /// # Arguments
194    ///
195    /// * `path` - The directory path where the database files will be stored
196    /// * `config` - Database configuration options
197    ///
198    /// # Errors
199    ///
200    /// Returns an error if:
201    /// - The path cannot be created or accessed
202    /// - Database files are corrupted
203    /// - Configuration is invalid — currently the `storage.direct_io_memory_fraction`
204    ///   range only, not every rule `Config::validate` states (residual: #3525)
205    ///
206    /// # Examples
207    ///
208    /// ```rust,no_run
209    /// use cqlite_core::{Database, Config};
210    /// use std::path::{Path, PathBuf};
211    ///
212    /// # tokio_test::block_on(async {
213    /// let config = Config::default();
214    /// let db = Database::open(Path::new("./data"), config).await?;
215    /// # Ok::<(), Box<dyn std::error::Error>>(())
216    /// # });
217    /// ```
218    pub async fn open(path: &Path, config: Config) -> Result<Self> {
219        // Judge the ONE rule this issue is about — the
220        // `direct_io_memory_fraction` range — BEFORE building anything from the
221        // config (#1696 AC2). Out of range it used to be silently CLAMPED by the
222        // reader, so a value set through the documented database-open API was not
223        // the value that ran.
224        //
225        // This is deliberately NOT `config.validate()`. Calling the full
226        // validator here was a scope overreach (roborev r3 F3): it also enforces
227        // the cache-budget rule, which the Node binding's documented and tested
228        // `memoryLimit: 1` contract violates (a 1-byte memory limit leaves the
229        // default 256 MiB block cache in place), so full validation broke a
230        // public contract this issue never proposed to change. The residual —
231        // that this method documents "Configuration is invalid" while enforcing
232        // only the fraction — is tracked in #3525.
233        config.storage.validated_direct_io_memory_fraction()?;
234
235        // Initialize platform abstraction layer
236        let platform = Arc::new(Platform::new(&config).await?);
237
238        // Initialize storage engine (no schema registry for simple open)
239        let storage = Arc::new(
240            StorageEngine::open(
241                path,
242                &config,
243                platform.clone(),
244                #[cfg(feature = "state_machine")]
245                None,
246            )
247            .await?,
248        );
249
250        // Initialize the memory-stats shell over the storage engine's live B1
251        // decompressed-chunk cache (issue #1568), so `stats()` reports real cache
252        // numbers rather than the deleted always-zero counters. When block caching
253        // is disabled (`block_cache.enabled == false`) there is no live cache, so
254        // the shell reports a structural zero.
255        let memory = Arc::new(match storage.chunk_cache() {
256            Some(cache) => MemoryManager::with_chunk_cache(cache),
257            None => MemoryManager::new(&config)?,
258        });
259
260        // Initialize schema manager
261        #[cfg(feature = "state_machine")]
262        let schema = Arc::new(SchemaManager::new_with_storage(storage.clone(), &config).await?);
263
264        // Initialize query engine (only when feature enabled)
265        #[cfg(feature = "state_machine")]
266        let query = Arc::new(QueryEngine::new(
267            storage.clone(),
268            schema.clone(),
269            memory.clone(),
270            &config,
271        )?);
272
273        Ok(Self {
274            storage,
275            #[cfg(feature = "state_machine")]
276            query,
277            memory,
278            config,
279        })
280    }
281
282    /// Open a database with pre-discovered SSTable table directories
283    ///
284    /// This method is used in the ingestion flow where SSTable discovery has been performed
285    /// externally (e.g., via `DiscoveryService`) and the database should be initialized with
286    /// specific SSTable files rather than scanning the storage directory.
287    ///
288    /// # Use Case
289    ///
290    /// This method is designed for the one-shot ingestion workflow:
291    /// 1. `DiscoveryService::discover()` scans external Cassandra data directories
292    /// 2. `SchemaManager` parses schema from discovered files
293    /// 3. `Database::open_with_discovered_sstables()` creates a queryable database instance
294    ///
295    /// # Arguments
296    ///
297    /// * `storage_path` - The directory path for database runtime files (WAL, manifest, memtable)
298    /// * `discovered_table_dirs` - Vector of table directory paths from DiscoveryService
299    ///   (e.g., `/var/lib/cassandra/data/keyspace1/table1-abc123`)
300    /// * `config` - Database configuration options
301    ///
302    /// # Errors
303    ///
304    /// Returns an error if:
305    /// - The storage path cannot be created or accessed
306    /// - Any discovered table directory cannot be read
307    /// - Configuration is invalid
308    /// - Storage engine or query engine initialization fails
309    ///
310    /// # Feature Gates
311    ///
312    /// This method is only available when the `state_machine` feature is enabled (default in M2+).
313    ///
314    /// # Examples
315    ///
316    /// ```rust,no_run
317    /// use cqlite_core::{Database, Config};
318    /// use std::path::{Path, PathBuf};
319    ///
320    /// # tokio_test::block_on(async {
321    /// let config = Config::default();
322    /// let storage_path = Path::new("./runtime");
323    /// let discovered_dirs = vec![
324    ///     PathBuf::from("/var/lib/cassandra/data/keyspace1/table1-abc123"),
325    ///     PathBuf::from("/var/lib/cassandra/data/keyspace1/table2-def456"),
326    /// ];
327    ///
328    /// let db = Database::open_with_discovered_sstables(
329    ///     storage_path,
330    ///     discovered_dirs,
331    ///     config
332    /// ).await?;
333    /// # Ok::<(), Box<dyn std::error::Error>>(())
334    /// # });
335    /// ```
336    #[cfg(feature = "state_machine")]
337    pub async fn open_with_discovered_sstables(
338        storage_path: &Path,
339        discovered_table_dirs: Vec<PathBuf>,
340        config: Config,
341    ) -> Result<Self> {
342        Self::open_with_discovered_sstables_and_registry(
343            storage_path,
344            discovered_table_dirs,
345            config,
346            None,
347        )
348        .await
349    }
350
351    /// Open a database with pre-discovered SSTable table directories and optional schema registry
352    ///
353    /// This is the internal implementation that supports passing a pre-loaded schema registry.
354    /// Public callers should use `open_with_discovered_sstables()` which calls this with None.
355    /// The ingestion module uses this directly to pass loaded schemas.
356    ///
357    /// # Arguments
358    ///
359    /// * `storage_path` - The directory path for database runtime files
360    /// * `discovered_table_dirs` - Vector of table directory paths from DiscoveryService
361    /// * `config` - Database configuration options
362    /// * `schema_registry` - Optional pre-loaded schema registry from ingestion
363    #[cfg(feature = "state_machine")]
364    pub(crate) async fn open_with_discovered_sstables_and_registry(
365        storage_path: &Path,
366        discovered_table_dirs: Vec<PathBuf>,
367        config: Config,
368        schema_registry: Option<Arc<tokio::sync::RwLock<schema::SchemaRegistry>>>,
369    ) -> Result<Self> {
370        // Same contract as `Database::open` (#1696 roborev F2/r3 F3): the
371        // `direct_io_memory_fraction` range — and only it — is judged before
372        // anything is built from the config. Not the full `Config::validate`:
373        // see the note on `open` for why widening this boundary's contract is a
374        // separate product decision (#3525).
375        config.storage.validated_direct_io_memory_fraction()?;
376
377        // Initialize platform abstraction layer
378        let platform = Arc::new(Platform::new(&config).await?);
379
380        // Initialize storage engine with pre-discovered SSTables and schema registry
381        let storage = Arc::new(
382            StorageEngine::open_with_sstables(
383                storage_path,
384                discovered_table_dirs,
385                &config,
386                platform.clone(),
387                schema_registry.clone(),
388            )
389            .await?,
390        );
391
392        // Memory-stats shell over the storage engine's live B1 chunk cache (#1568).
393        // No live cache when block caching is disabled → structural-zero stats.
394        let memory = Arc::new(match storage.chunk_cache() {
395            Some(cache) => MemoryManager::with_chunk_cache(cache),
396            None => MemoryManager::new(&config)?,
397        });
398
399        // Initialize schema manager - use registry if provided, otherwise create empty
400        let schema = if let Some(registry_rwlock) = schema_registry {
401            Arc::new(
402                SchemaManager::new_with_registry(storage.clone(), registry_rwlock, &config).await?,
403            )
404        } else {
405            Arc::new(SchemaManager::new_with_storage(storage.clone(), &config).await?)
406        };
407
408        // Initialize query engine
409        let query = Arc::new(QueryEngine::new(
410            storage.clone(),
411            schema.clone(),
412            memory.clone(),
413            &config,
414        )?);
415
416        Ok(Self {
417            storage,
418            query,
419            memory,
420            config,
421        })
422    }
423
424    /// Re-scan the data directory and atomically apply added/removed SSTable
425    /// generations to this handle's reader set (issue #1749).
426    ///
427    /// # Freshness contract
428    ///
429    /// A `Database` is a **snapshot at [`open`](Self::open)**: it discovers the
430    /// SSTable generations once and never re-scans on its own. A Cassandra
431    /// flush/compaction (or a CQLite `--flush`) may add or remove generations
432    /// underneath a warm handle at any time; those changes become visible only
433    /// after an explicit `refresh()`. Re-runs the same TOC/filename-based
434    /// discovery `open` used — no content sniffing, no heuristics.
435    ///
436    /// - Newly present generations become queryable; removed generations stop
437    ///   being queried; unchanged generations keep their warm parsed
438    ///   Index/Statistics/bloom state (not rebuilt).
439    /// - **In-flight queries are never affected**: a scan already running holds
440    ///   its own `Arc` reader clones and completes against the pre-refresh set. A
441    ///   query issued after `refresh()` returns sees the post-refresh set.
442    /// - **Atomic and fail-closed**: if any newly discovered generation fails to
443    ///   open (e.g. a corrupt `Statistics.db`, issue #1626), `refresh()` returns
444    ///   the typed error and leaves the previously held reader set fully
445    ///   unchanged — no partial view.
446    ///
447    /// Returns a [`RefreshReport`] describing what this call applied. Explicit
448    /// refresh only: there is no filesystem watching or per-query staleness check.
449    pub async fn refresh(&self) -> Result<RefreshReport> {
450        self.storage.refresh().await
451    }
452
453    /// Execute a SQL query and return the result
454    ///
455    /// # Arguments
456    ///
457    /// * `sql` - The SQL query string to execute
458    ///
459    /// # Errors
460    ///
461    /// Returns an error if:
462    /// - SQL syntax is invalid
463    /// - Referenced tables/columns don't exist
464    /// - Query execution fails
465    ///
466    /// # Examples
467    ///
468    /// ```rust,no_run
469    /// # use cqlite_core::{Database, Config};
470    /// # use std::path::{Path, PathBuf};
471    /// # tokio_test::block_on(async {
472    /// # let config = Config::default();
473    /// # let db = Database::open(Path::new("./data"), config).await?;
474    /// let result = db.execute("SELECT * FROM users WHERE id = 1").await?;
475    /// # Ok::<(), Box<dyn std::error::Error>>(())
476    /// # });
477    /// ```
478    #[cfg(feature = "state_machine")]
479    pub async fn execute(&self, sql: &str) -> Result<query::result::QueryResult> {
480        let result = self.query.execute(sql).await;
481
482        // Data-safety (issue #1694): log the SHAPE (rows affected), never the SQL
483        // text — a query string carries user data (WHERE-clause literals).
484        #[cfg(debug_assertions)]
485        if let Ok(ref query_result) = result {
486            tracing::debug!(
487                "Database::execute returning rows_affected: {}",
488                query_result.rows_affected
489            );
490        }
491
492        result
493    }
494
495    /// Execute a SQL query with streaming results (Issue #280)
496    ///
497    /// Returns a `QueryResultIterator` that yields rows incrementally via a bounded
498    /// channel, enabling memory-efficient processing of large result sets.
499    ///
500    /// This is the recommended method for exporting large tables, as it avoids
501    /// materializing all rows in memory at once.
502    ///
503    /// # Arguments
504    ///
505    /// * `sql` - The SQL query to execute (must be a SELECT statement)
506    /// * `config` - Streaming configuration (buffer size, chunk hints)
507    ///
508    /// # Errors
509    ///
510    /// Returns an error if:
511    /// - Query is not a SELECT statement
512    /// - SQL syntax is invalid
513    /// - Query execution fails
514    ///
515    /// # Examples
516    ///
517    /// ```rust,no_run
518    /// # use cqlite_core::{Database, Config};
519    /// # use cqlite_core::query::result::StreamingConfig;
520    /// # use std::path::Path;
521    /// # tokio_test::block_on(async {
522    /// # let db = Database::open(Path::new("./data"), Config::default()).await?;
523    /// let config = StreamingConfig::default();
524    /// let mut iter = db.execute_streaming(
525    ///     "SELECT * FROM large_table",
526    ///     config
527    /// ).await?;
528    ///
529    /// while let Some(row_result) = iter.next_async().await {
530    ///     let row = row_result?;
531    ///     // Process row incrementally
532    /// }
533    /// # Ok::<(), Box<dyn std::error::Error>>(())
534    /// # });
535    /// ```
536    #[cfg(feature = "state_machine")]
537    pub async fn execute_streaming(
538        &self,
539        sql: &str,
540        config: query::result::StreamingConfig,
541    ) -> Result<query::result::QueryResultIterator> {
542        self.query.execute_streaming(sql, config).await
543    }
544
545    /// Execute a SELECT with positional `?` parameters (Issue #961).
546    ///
547    /// The `params` are bound, in source order, into the statement's `?`
548    /// placeholders before planning, so they participate in partition-key
549    /// classification and encoding. A `WHERE pk = ?` therefore engages the same
550    /// partition-targeted fast path as the equivalent literal query.
551    ///
552    /// # Arguments
553    ///
554    /// * `sql` - A SELECT statement that may contain positional `?` placeholders
555    /// * `params` - Values bound positionally to the `?` placeholders
556    ///
557    /// # Errors
558    ///
559    /// Returns an error if the SQL is not a SELECT, the parameter count does not
560    /// match the number of `?` placeholders, or execution fails.
561    #[cfg(feature = "state_machine")]
562    pub async fn execute_with_params(
563        &self,
564        sql: &str,
565        params: &[Value],
566    ) -> Result<query::result::QueryResult> {
567        self.query.execute_with_params(sql, params).await
568    }
569
570    /// Prepare a SQL statement for repeated execution
571    ///
572    /// # Arguments
573    ///
574    /// * `sql` - The SQL statement to prepare
575    ///
576    /// # Errors
577    ///
578    /// Returns an error if SQL syntax is invalid or references non-existent objects
579    #[cfg(feature = "state_machine")]
580    pub async fn prepare(&self, sql: &str) -> Result<std::sync::Arc<query::PreparedQuery>> {
581        self.query.prepare(sql).await
582    }
583
584    /// Explain a SQL query without executing it
585    ///
586    /// # Arguments
587    ///
588    /// * `sql` - The SQL query to explain
589    ///
590    /// # Errors
591    ///
592    /// Returns an error if SQL syntax is invalid
593    #[cfg(feature = "state_machine")]
594    pub async fn explain(&self, sql: &str) -> Result<query::ExplainResult> {
595        self.query.explain(sql).await
596    }
597
598    /// Check if schema is available for a table
599    ///
600    /// This is a fast boolean check useful for pre-flight validation.
601    /// For detailed diagnostic information, use `schema_status()`.
602    ///
603    /// # Examples
604    ///
605    /// ```rust,no_run
606    /// # use cqlite_core::{Database, Config};
607    /// # tokio_test::block_on(async {
608    /// let db = Database::open(std::path::Path::new("./data"), Config::default()).await?;
609    ///
610    /// if !db.has_schema_for_table("users").await {
611    ///     eprintln!("Warning: No schema found for 'users' table");
612    /// }
613    /// # Ok::<(), Box<dyn std::error::Error>>(())
614    /// # });
615    /// ```
616    #[cfg(feature = "state_machine")]
617    pub async fn has_schema_for_table(&self, table: &str) -> bool {
618        self.query.has_schema_for_table(table).await
619    }
620
621    /// Get detailed schema status for debugging
622    ///
623    /// Returns diagnostic information about schema availability including
624    /// reasons for missing schemas or extraction failures.
625    ///
626    /// # Examples
627    ///
628    /// ```rust,no_run
629    /// # use cqlite_core::{Database, Config};
630    /// # use cqlite_core::query::SchemaStatus;
631    /// # tokio_test::block_on(async {
632    /// let db = Database::open(std::path::Path::new("./data"), Config::default()).await?;
633    ///
634    /// match db.schema_status("users").await {
635    ///     SchemaStatus::Available { .. } => println!("Schema ready"),
636    ///     SchemaStatus::ExtractionFailed { cause, suggestion, .. } => {
637    ///         eprintln!("Schema extraction failed: {}", cause);
638    ///         eprintln!("Suggestion: {}", suggestion);
639    ///     }
640    ///     _ => {}
641    /// }
642    /// # Ok::<(), Box<dyn std::error::Error>>(())
643    /// # });
644    /// ```
645    #[cfg(feature = "state_machine")]
646    pub async fn schema_status(&self, table: &str) -> query::SchemaStatus {
647        self.query.schema_status(table).await
648    }
649
650    /// Get database statistics
651    pub async fn stats(&self) -> Result<DatabaseStats> {
652        // The chunk-cache-derived block-cache fields come from the memory manager's
653        // live handle; the per-reader B4 key caches are aggregated here (issue
654        // #1571, B5) — this async site owns `storage` and reads live readers, so
655        // the aggregate is always current rather than a stale captured handle.
656        let mut memory_stats = self.memory.stats()?;
657        let key_cache = self.storage.key_cache_stats().await;
658        memory_stats.key_cache_hits = key_cache.hits;
659        memory_stats.key_cache_misses = key_cache.misses;
660        memory_stats.key_cache_evictions = key_cache.evictions;
661        memory_stats.key_cache_invalidations = key_cache.invalidations;
662        memory_stats.key_cache_resident_bytes = key_cache.resident_bytes;
663        memory_stats.key_cache_capacity_bytes = key_cache.capacity_bytes;
664
665        Ok(DatabaseStats {
666            storage_stats: self.storage.stats().await?,
667            memory_stats,
668            #[cfg(feature = "state_machine")]
669            query_stats: self.query.stats(),
670        })
671    }
672
673    /// Flush all pending writes to disk
674    #[cfg(feature = "experimental")]
675    pub async fn flush(&self) -> Result<()> {
676        self.storage.flush().await
677    }
678
679    /// Perform manual compaction of storage files
680    #[cfg(feature = "experimental")]
681    pub async fn compact(&self) -> Result<()> {
682        self.storage.compact().await
683    }
684
685    /// Shutdown the database storage engine without consuming self.
686    ///
687    /// This is useful for language bindings where the Database is wrapped
688    /// in an Arc and cannot be consumed. The shutdown operation is idempotent.
689    ///
690    /// For consuming close that also drops the Database, use `close()`.
691    pub async fn shutdown(&self) -> Result<()> {
692        self.storage.shutdown().await
693    }
694
695    /// Close the database and release all resources
696    ///
697    /// This method ensures all pending operations are completed and
698    /// all resources are properly cleaned up.
699    ///
700    /// ## Durability contract
701    ///
702    /// Embedders MUST call `close().await` for a graceful shutdown. `Drop` is
703    /// NOT a flush — Tokio has no async drop, so dropping a handle cannot await
704    /// a flush and any un-flushed writer state is left to recovery (WAL replay)
705    /// rather than being persisted here. For the write path this maps onto
706    /// [`storage::write_engine::WriteEngine::close`], which is the actual
707    /// memtable-to-SSTable durability boundary (issue #1693).
708    pub async fn close(self) -> Result<()> {
709        // Stop background tasks. There is nothing to flush: the WAL/MemTable write
710        // path was removed in Issue #175, so `StorageEngine::flush` is an
711        // always-erroring stub. Calling it here made `close()` fail unconditionally
712        // under the `experimental` feature; the shutdown above is the full teardown.
713        self.storage.shutdown().await?;
714        Ok(())
715    }
716
717    /// Get the database configuration
718    pub fn config(&self) -> &Config {
719        &self.config
720    }
721}
722
723impl Clone for Database {
724    fn clone(&self) -> Self {
725        Self {
726            storage: self.storage.clone(),
727            #[cfg(feature = "state_machine")]
728            query: self.query.clone(),
729            memory: self.memory.clone(),
730            config: self.config.clone(),
731        }
732    }
733}
734
735/// Database statistics
736#[derive(Debug, Clone)]
737pub struct DatabaseStats {
738    /// Storage engine statistics
739    pub storage_stats: storage::StorageStats,
740    /// Memory manager statistics
741    pub memory_stats: memory::MemoryStats,
742    /// Query engine statistics
743    #[cfg(feature = "state_machine")]
744    pub query_stats: query::QueryStats,
745}
746
747/// A prepared SQL statement that can be executed multiple times
748#[cfg(feature = "state_machine")]
749#[derive(Debug)]
750pub struct PreparedStatement {
751    statement: query::PreparedQuery,
752}
753
754#[cfg(feature = "state_machine")]
755impl PreparedStatement {
756    /// Execute the prepared statement with the given parameters
757    pub async fn execute(&self, params: &[Value]) -> Result<query::result::QueryResult> {
758        self.statement.execute(params).await
759    }
760}
761
762// Re-export query result types for convenience
763#[cfg(feature = "state_machine")]
764pub use query::result::{QueryResult, QueryRow};
765
766#[cfg(test)]
767#[path = "lib_tests.rs"]
768mod tests;