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;