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
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}