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