Skip to main content

radixdb_api/
database.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Database struct and operations
16//!
17//! Provides a modern, ergonomic Rust API for database operations.
18//!
19//! # Examples
20//!
21//! ```no_run
22//! use radixdb_api::{Database, params};
23//! # fn main() -> radixdb_core::Result<()> {
24//!
25//! let db = Database::open("memory://")?;
26//!
27//! // DDL - no params needed
28//! db.execute("CREATE TABLE users (id INTEGER, name TEXT, age INTEGER)", ())?;
29//!
30//! // Insert with params - using tuple syntax
31//! db.execute("INSERT INTO users VALUES ($1, $2, $3)", (1, "Alice", 30))?;
32//!
33//! // Insert with params! macro
34//! db.execute("INSERT INTO users VALUES ($1, $2, $3)", params![2, "Bob", 25])?;
35//!
36//! // Query with iteration
37//! for row in db.query("SELECT * FROM users WHERE age > $1", (20,))? {
38//!     let row = row?;
39//!     let name: String = row.get(1)?;
40//!     println!("{}", name);
41//! }
42//!
43//! // Query single value
44//! let count: i64 = db.query_one("SELECT COUNT(*) FROM users", ())?;
45//! # assert_eq!(count, 2);
46//! # Ok(())
47//! # }
48//! ```
49
50use rustc_hash::FxHashMap;
51use std::collections::HashSet;
52use std::sync::{Arc, Condvar, Mutex, RwLock};
53
54#[cfg(test)]
55type DatabaseOpenTestHook = Arc<dyn Fn(&str) + Send + Sync>;
56
57#[cfg(test)]
58static DATABASE_OPEN_TEST_HOOK: std::sync::LazyLock<Mutex<Option<DatabaseOpenTestHook>>> =
59    std::sync::LazyLock::new(|| Mutex::new(None));
60#[cfg(test)]
61static DATABASE_OPEN_TEST_HOOK_OWNER: Mutex<()> = Mutex::new(());
62
63#[cfg(test)]
64struct DatabaseOpenTestHookGuard {
65    _owner: std::sync::MutexGuard<'static, ()>,
66}
67
68#[cfg(test)]
69impl DatabaseOpenTestHookGuard {
70    fn install(hook: DatabaseOpenTestHook) -> Self {
71        let owner = DATABASE_OPEN_TEST_HOOK_OWNER
72            .lock()
73            .unwrap_or_else(|poisoned| poisoned.into_inner());
74        *DATABASE_OPEN_TEST_HOOK
75            .lock()
76            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(hook);
77        Self { _owner: owner }
78    }
79}
80
81#[cfg(test)]
82impl Drop for DatabaseOpenTestHookGuard {
83    fn drop(&mut self) {
84        *DATABASE_OPEN_TEST_HOOK
85            .lock()
86            .unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
87    }
88}
89
90#[cfg(test)]
91fn run_database_open_test_hook(dsn: &str) {
92    let hook = DATABASE_OPEN_TEST_HOOK
93        .lock()
94        .expect("database open test hook lock")
95        .clone();
96    if let Some(hook) = hook {
97        hook(dsn);
98    }
99}
100
101use radixdb_core::{Error, IsolationLevel, Result};
102use radixdb_executor::context::{
103    clear_all_thread_local_caches, ExecutionContext, ExecutionContextBuilder,
104};
105use radixdb_executor::optimizer::FeedbackCache;
106use radixdb_executor::query_cache::CachedPlanRef;
107use radixdb_executor::semantic_cache::{SemanticCache, SemanticCacheStatsSnapshot};
108use radixdb_executor::Executor;
109use radixdb_executor::{DatabasePluginAdmission, PluginRegistry};
110use radixdb_storage::mvcc::engine::MVCCEngine;
111use radixdb_storage::traits::Engine;
112use radixdb_storage::{Config, SyncMode};
113
114use super::params::{NamedParams, Params};
115use super::rows::{FromRow, Rows};
116use super::statement::Statement;
117use super::transaction::Transaction;
118
119pub use super::value::FromValue;
120
121/// Storage scheme constants
122pub const MEMORY_SCHEME: &str = "memory";
123pub const FILE_SCHEME: &str = "file";
124
125fn composed_mvcc_engine(config: Config, plugin_registry: Arc<PluginRegistry>) -> MVCCEngine {
126    MVCCEngine::new_with_composition_binders(
127        config,
128        radixdb_executor::mutation::partial_index::bind_from_sql,
129        radixdb_executor::mutation::row_validation::bind,
130        radixdb_executor::mutation::view_binding::bind_from_sql,
131        radixdb_executor::plugin_catalog_runtime_binder(plugin_registry),
132    )
133}
134
135#[cfg(not(feature = "test-filedb"))]
136fn composed_in_memory_engine() -> MVCCEngine {
137    composed_mvcc_engine(Config::default(), Arc::clone(&DEFAULT_PLUGIN_REGISTRY))
138}
139
140#[derive(Clone)]
141enum DatabaseRegistryEntry {
142    Opening(Arc<DatabaseOpenSlot>),
143    Ready(Arc<DatabaseOwner>),
144}
145
146struct DatabaseOpenSlot {
147    outcome: Mutex<Option<Result<()>>>,
148    changed: Condvar,
149}
150
151impl DatabaseOpenSlot {
152    fn new() -> Self {
153        Self {
154            outcome: Mutex::new(None),
155            changed: Condvar::new(),
156        }
157    }
158
159    fn finish(&self, outcome: Result<()>) {
160        let mut state = self
161            .outcome
162            .lock()
163            .unwrap_or_else(|error| error.into_inner());
164        *state = Some(outcome);
165        self.changed.notify_all();
166    }
167
168    fn wait(&self) -> Result<()> {
169        let mut state = self
170            .outcome
171            .lock()
172            .map_err(|_| Error::LockAcquisitionFailed("database open slot".to_string()))?;
173        while state.is_none() {
174            state = self
175                .changed
176                .wait(state)
177                .map_err(|_| Error::LockAcquisitionFailed("database open slot".to_string()))?;
178        }
179        state
180            .as_ref()
181            .expect("database open slot outcome was checked")
182            .clone()
183    }
184}
185
186/// Global database registry to ensure single instance per DSN while allowing
187/// independent DSNs to recover concurrently.
188static DATABASE_REGISTRY: std::sync::LazyLock<RwLock<FxHashMap<String, DatabaseRegistryEntry>>> =
189    std::sync::LazyLock::new(|| RwLock::new(FxHashMap::default()));
190static DEFAULT_PLUGIN_REGISTRY: std::sync::LazyLock<Arc<PluginRegistry>> =
191    std::sync::LazyLock::new(|| Arc::new(PluginRegistry::empty()));
192
193/// Registry-owned durable engine/file-lock lifetime. It intentionally has no
194/// SQL executor or connection-local transaction state.
195struct DatabaseOwner {
196    engine: Arc<MVCCEngine>,
197    semantic_cache: Arc<SemanticCache>,
198    feedback_cache: Arc<FeedbackCache>,
199    plugin_registry: Arc<PluginRegistry>,
200    registry_key: String,
201    dsn: String,
202    requested_config: Config,
203    /// Temp directory for test-filedb feature. Deleted on drop.
204    #[cfg(feature = "test-filedb")]
205    _temp_dir: Option<tempfile::TempDir>,
206}
207
208/// Connection-local database state. Multiple connections may share the owner
209/// and engine, but never this Executor or its hidden SQL transaction.
210pub(crate) struct DatabaseInner {
211    engine: Arc<MVCCEngine>,
212    executor: Mutex<Executor>,
213    owner: Arc<DatabaseOwner>,
214}
215
216/// Type alias for Statement to use (avoids exposing DatabaseInner directly)
217pub(crate) type DatabaseInnerHandle = DatabaseInner;
218
219impl DatabaseInner {
220    /// Build a transaction-local executor while retaining the engine-owner
221    /// caches shared by every connection. Parsed plans and transaction state
222    /// stay local, but committed DML must invalidate the same semantic and
223    /// feedback caches that later readers consult.
224    pub(crate) fn transaction_executor(&self) -> Executor {
225        Executor::with_shared_runtime_caches_and_plugin_registry(
226            Arc::clone(&self.engine),
227            Arc::clone(&self.owner.semantic_cache),
228            Arc::clone(&self.owner.feedback_cache),
229            Arc::clone(&self.owner.plugin_registry),
230        )
231    }
232}
233
234impl Drop for DatabaseOwner {
235    fn drop(&mut self) {
236        if let Err(error) = self.engine.close_engine() {
237            eprintln!("automatic database close failed: {error}");
238        }
239    }
240}
241
242impl Drop for DatabaseInner {
243    fn drop(&mut self) {
244        clear_all_thread_local_caches();
245        // Statements and FFI objects retain this whole connection inner. Only
246        // its final drop releases the owner's last connection reference.
247        Database::try_unregister_owner(&self.owner);
248    }
249}
250
251/// Database represents a RadixDB database connection.
252///
253/// This is the main entry point for using RadixDB. It wraps the storage engine
254/// and executor, providing a simple API for executing SQL queries.
255///
256/// # Thread Safety
257///
258/// Database is thread-safe and can be shared across threads via cloning.
259/// Each clone shares the same underlying storage engine.
260///
261/// # Examples
262///
263/// ```ignore
264/// use radixdb::{Database, params};
265///
266/// // Open in-memory database
267/// let db = Database::open("memory://")?;
268///
269/// // Create table
270/// db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", ())?;
271///
272/// // Insert with parameters
273/// db.execute("INSERT INTO users VALUES ($1, $2)", (1, "Alice"))?;
274///
275/// // Query
276/// for row in db.query("SELECT * FROM users", ())? {
277///     let row = row?;
278///     println!("{}: {}", row.get::<i64>("id")?, row.get::<String>("name")?);
279/// }
280/// ```
281pub struct Database {
282    inner: Arc<DatabaseInner>,
283}
284
285impl Database {
286    /// Authenticate one durable catalog Principal and return its stable ID.
287    #[doc(hidden)]
288    pub fn authenticate_principal(&self, login: &str, password: &str) -> Result<crate::ObjectId> {
289        self.ensure_open()?;
290        let executor = self
291            .inner
292            .executor
293            .lock()
294            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
295        executor.authenticate_principal(login, password)
296    }
297
298    /// Return the bounded package-binding admission result without decoding
299    /// any external value or exposing storage internals.
300    #[doc(hidden)]
301    pub fn plugin_admission(&self) -> Result<DatabasePluginAdmission> {
302        self.ensure_open()?;
303        let executor = self
304            .inner
305            .executor
306            .lock()
307            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
308        executor.plugin_admission()
309    }
310
311    #[doc(hidden)]
312    pub fn plugin_admission_diagnostic(&self) -> Result<Option<String>> {
313        self.ensure_open()?;
314        let executor = self
315            .inner
316            .executor
317            .lock()
318            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
319        executor.plugin_admission_diagnostic()
320    }
321
322    pub(crate) fn with_connection_executor<T>(
323        &self,
324        operation: impl FnOnce(&Executor) -> Result<T>,
325    ) -> Result<T> {
326        self.ensure_open()?;
327        let executor = self
328            .inner
329            .executor
330            .lock()
331            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
332        operation(&executor)
333    }
334
335    fn canonical_registry_key(dsn: &str) -> Result<String> {
336        let (scheme, path) = Self::parse_dsn(dsn)?;
337        if scheme == MEMORY_SCHEME {
338            return Ok(format!("{MEMORY_SCHEME}://{path}"));
339        }
340
341        let (clean_path, _) = Self::parse_file_config(&path)?;
342        let absolute = if std::path::Path::new(&clean_path).is_absolute() {
343            std::path::PathBuf::from(clean_path)
344        } else {
345            std::env::current_dir()
346                .map_err(|error| Error::internal(format!("cannot resolve database path: {error}")))?
347                .join(clean_path)
348        };
349        let mut normalized = std::path::PathBuf::new();
350        for component in absolute.components() {
351            match component {
352                std::path::Component::CurDir => {}
353                std::path::Component::ParentDir => {
354                    normalized.pop();
355                }
356                other => normalized.push(other.as_os_str()),
357            }
358        }
359        Ok(format!("{FILE_SCHEME}://{}", normalized.display()))
360    }
361
362    fn requested_config(dsn: &str, registry_key: &str) -> Result<Config> {
363        let (scheme, path) = Self::parse_dsn(dsn)?;
364        if scheme == MEMORY_SCHEME {
365            return Ok(Config::in_memory());
366        }
367        let (_, mut config) = Self::parse_file_config(&path)?;
368        config.path = Some(
369            registry_key
370                .strip_prefix("file://")
371                .expect("file registry key must have file scheme")
372                .to_string(),
373        );
374        Ok(config)
375    }
376
377    fn ensure_matching_config(owner: &DatabaseOwner, requested: &Config, dsn: &str) -> Result<()> {
378        if &owner.requested_config == requested {
379            Ok(())
380        } else {
381            Err(Error::invalid_argument(format!(
382                "database `{dsn}` is already open with a different effective configuration"
383            )))
384        }
385    }
386
387    fn ensure_matching_plugin_registry(
388        owner: &DatabaseOwner,
389        requested: &Arc<PluginRegistry>,
390        dsn: &str,
391    ) -> Result<()> {
392        if Arc::ptr_eq(&owner.plugin_registry, requested) {
393            Ok(())
394        } else {
395            Err(Error::invalid_argument(format!(
396                "database `{dsn}` is already open with a different plugin registry generation"
397            )))
398        }
399    }
400
401    pub(crate) fn ensure_open(&self) -> Result<()> {
402        match self.inner.engine.lifecycle_state() {
403            radixdb_storage::mvcc::engine::EngineLifecycleState::Ready => Ok(()),
404            radixdb_storage::mvcc::engine::EngineLifecycleState::CloseFailed(error)
405            | radixdb_storage::mvcc::engine::EngineLifecycleState::Failed(error) => Err(error),
406            _ => Err(Error::EngineNotOpen),
407        }
408    }
409
410    fn owner_arc(&self) -> &Arc<DatabaseOwner> {
411        &self.inner.owner
412    }
413
414    /// Build a connection-local facade around the registry-owned engine.
415    fn connection_from_owner(owner: Arc<DatabaseOwner>) -> Self {
416        let engine = Arc::clone(&owner.engine);
417        let executor = Executor::with_shared_runtime_caches_and_plugin_registry(
418            Arc::clone(&engine),
419            Arc::clone(&owner.semantic_cache),
420            Arc::clone(&owner.feedback_cache),
421            Arc::clone(&owner.plugin_registry),
422        );
423        Self {
424            inner: Arc::new(DatabaseInner {
425                engine,
426                executor: Mutex::new(executor),
427                owner,
428            }),
429        }
430    }
431
432    /// Remove an engine owner only after its final connection is released.
433    fn try_unregister_owner(owner: &Arc<DatabaseOwner>) {
434        if let Ok(mut registry) = DATABASE_REGISTRY.write() {
435            if let Some(DatabaseRegistryEntry::Ready(entry)) = registry.get(&owner.registry_key) {
436                if Arc::ptr_eq(entry, owner) && Arc::strong_count(owner) == 2 {
437                    registry.remove(&owner.registry_key);
438                }
439            }
440        }
441    }
442}
443
444impl Clone for Database {
445    /// Clone with independent transaction state over the shared engine.
446    fn clone(&self) -> Self {
447        Self::connection_from_owner(Arc::clone(self.owner_arc()))
448    }
449}
450
451impl Drop for Database {
452    fn drop(&mut self) {
453        // DatabaseInner may be retained by a Statement/FFI object. Registry
454        // cleanup therefore belongs to its final Drop, not to this facade.
455    }
456}
457
458impl Database {
459    /// Open a memory or file DSN, reusing the engine owner for an identical DSN.
460    pub fn open(dsn: &str) -> Result<Self> {
461        Self::open_with_plugin_registry(dsn, Arc::clone(&DEFAULT_PLUGIN_REGISTRY))
462    }
463
464    /// Open against the immutable registry admitted before server bind.
465    #[doc(hidden)]
466    pub fn open_with_plugin_registry(
467        dsn: &str,
468        plugin_registry: Arc<PluginRegistry>,
469    ) -> Result<Self> {
470        let registry_key = Self::canonical_registry_key(dsn)?;
471        let requested_config = Self::requested_config(dsn, &registry_key)?;
472        let (opening_slot, owns_open) = {
473            let mut registry = DATABASE_REGISTRY
474                .write()
475                .map_err(|_| Error::LockAcquisitionFailed("registry write".to_string()))?;
476            match registry.get(&registry_key) {
477                Some(DatabaseRegistryEntry::Ready(inner)) => match inner.engine.lifecycle_state() {
478                    radixdb_storage::mvcc::engine::EngineLifecycleState::Ready => {
479                        Self::ensure_matching_config(inner, &requested_config, dsn)?;
480                        Self::ensure_matching_plugin_registry(inner, &plugin_registry, dsn)?;
481                        return Ok(Self::connection_from_owner(Arc::clone(inner)));
482                    }
483                    radixdb_storage::mvcc::engine::EngineLifecycleState::Closed => {
484                        let slot = Arc::new(DatabaseOpenSlot::new());
485                        registry.insert(
486                            registry_key.clone(),
487                            DatabaseRegistryEntry::Opening(Arc::clone(&slot)),
488                        );
489                        (slot, true)
490                    }
491                    radixdb_storage::mvcc::engine::EngineLifecycleState::CloseFailed(error)
492                    | radixdb_storage::mvcc::engine::EngineLifecycleState::Failed(error) => {
493                        return Err(error);
494                    }
495                    state => {
496                        return Err(Error::internal(format!(
497                            "database `{dsn}` is not ready for open: {state:?}"
498                        )));
499                    }
500                },
501                Some(DatabaseRegistryEntry::Opening(slot)) => (Arc::clone(slot), false),
502                None => {
503                    let slot = Arc::new(DatabaseOpenSlot::new());
504                    registry.insert(
505                        registry_key.clone(),
506                        DatabaseRegistryEntry::Opening(Arc::clone(&slot)),
507                    );
508                    (slot, true)
509                }
510            }
511        };
512
513        if !owns_open {
514            opening_slot.wait()?;
515            let registry = DATABASE_REGISTRY
516                .read()
517                .map_err(|_| Error::LockAcquisitionFailed("registry read".to_string()))?;
518            let Some(DatabaseRegistryEntry::Ready(inner)) = registry.get(&registry_key) else {
519                return Err(Error::internal(format!(
520                    "database `{dsn}` open completed without publishing its owner"
521                )));
522            };
523            Self::ensure_matching_config(inner, &requested_config, dsn)?;
524            Self::ensure_matching_plugin_registry(inner, &plugin_registry, dsn)?;
525            return Ok(Self::connection_from_owner(Arc::clone(inner)));
526        }
527
528        #[cfg(test)]
529        run_database_open_test_hook(dsn);
530
531        let open_result = (|| -> Result<Arc<DatabaseOwner>> {
532            let (scheme, _path) = Self::parse_dsn(dsn)?;
533
534            #[cfg(feature = "test-filedb")]
535            let mut _temp_dir_holder: Option<tempfile::TempDir> = None;
536
537            let engine = match scheme.as_str() {
538                MEMORY_SCHEME => {
539                    #[cfg(feature = "test-filedb")]
540                    {
541                        let tmp = tempfile::tempdir().map_err(|error| {
542                            Error::internal(format!("failed to create temp dir: {error}"))
543                        })?;
544                        let file_dsn = format!("file://{}", tmp.path().display());
545                        let (_clean_path, config) = Self::parse_file_config(&file_dsn[7..])?;
546                        let engine = composed_mvcc_engine(config, Arc::clone(&plugin_registry));
547                        engine.open_engine()?;
548                        let engine = Arc::new(engine);
549                        engine.start_cleanup();
550                        _temp_dir_holder = Some(tmp);
551                        engine
552                    }
553                    #[cfg(not(feature = "test-filedb"))]
554                    {
555                        let engine =
556                            composed_mvcc_engine(Config::default(), Arc::clone(&plugin_registry));
557                        engine.open_engine()?;
558                        let engine = Arc::new(engine);
559                        engine.start_cleanup();
560                        engine
561                    }
562                }
563                FILE_SCHEME => {
564                    let engine = composed_mvcc_engine(
565                        requested_config.clone(),
566                        Arc::clone(&plugin_registry),
567                    );
568                    engine.open_engine()?;
569                    let engine = Arc::new(engine);
570                    engine.start_cleanup();
571                    engine
572                }
573                _ => {
574                    return Err(Error::parse(format!(
575                        "Unsupported scheme '{scheme}'. Use 'memory://' or 'file://path'"
576                    )));
577                }
578            };
579
580            Ok(Arc::new(DatabaseOwner {
581                engine,
582                semantic_cache: Arc::new(SemanticCache::new()),
583                feedback_cache: Arc::new(FeedbackCache::new()),
584                plugin_registry: Arc::clone(&plugin_registry),
585                registry_key: registry_key.clone(),
586                dsn: dsn.to_string(),
587                requested_config: requested_config.clone(),
588                #[cfg(feature = "test-filedb")]
589                _temp_dir: _temp_dir_holder,
590            }))
591        })();
592
593        match open_result {
594            Ok(inner) => {
595                let mut registry = match DATABASE_REGISTRY.write() {
596                    Ok(registry) => registry,
597                    Err(_) => {
598                        let error = Error::LockAcquisitionFailed("registry write".to_string());
599                        opening_slot.finish(Err(error.clone()));
600                        return Err(error);
601                    }
602                };
603                let owns_slot = matches!(
604                    registry.get(&registry_key),
605                    Some(DatabaseRegistryEntry::Opening(slot))
606                        if Arc::ptr_eq(slot, &opening_slot)
607                );
608                if !owns_slot {
609                    let error = Error::internal(format!(
610                        "database `{dsn}` opening owner changed before publication"
611                    ));
612                    opening_slot.finish(Err(error.clone()));
613                    return Err(error);
614                }
615                registry.insert(
616                    registry_key.clone(),
617                    DatabaseRegistryEntry::Ready(Arc::clone(&inner)),
618                );
619                drop(registry);
620                opening_slot.finish(Ok(()));
621                Ok(Self::connection_from_owner(inner))
622            }
623            Err(error) => {
624                if let Ok(mut registry) = DATABASE_REGISTRY.write() {
625                    let owns_slot = matches!(
626                        registry.get(&registry_key),
627                        Some(DatabaseRegistryEntry::Opening(slot))
628                            if Arc::ptr_eq(slot, &opening_slot)
629                    );
630                    if owns_slot {
631                        registry.remove(&registry_key);
632                    }
633                }
634                opening_slot.finish(Err(error.clone()));
635                Err(error)
636            }
637        }
638    }
639
640    /// Open an in-memory database
641    ///
642    /// This is a convenience method that creates a new in-memory database.
643    /// Each call creates a unique instance (unlike `open("memory://")` which
644    /// would share the same instance).
645    pub fn open_in_memory() -> Result<Self> {
646        Self::create_in_memory_engine()
647    }
648
649    #[cfg(feature = "test-filedb")]
650    fn create_in_memory_engine() -> Result<Self> {
651        let tmp = tempfile::tempdir()
652            .map_err(|e| Error::internal(format!("failed to create temp dir: {}", e)))?;
653        let file_dsn = format!("file://{}", tmp.path().display());
654        let (_clean_path, config) = Self::parse_file_config(&file_dsn[7..])?;
655        let engine = composed_mvcc_engine(config, Arc::clone(&DEFAULT_PLUGIN_REGISTRY));
656        engine.open_engine()?;
657        let engine = Arc::new(engine);
658        engine.start_cleanup();
659        let owner = Arc::new(DatabaseOwner {
660            engine,
661            semantic_cache: Arc::new(SemanticCache::new()),
662            feedback_cache: Arc::new(FeedbackCache::new()),
663            plugin_registry: Arc::clone(&DEFAULT_PLUGIN_REGISTRY),
664            registry_key: "memory://".to_string(),
665            dsn: "memory://".to_string(),
666            requested_config: Config::in_memory(),
667            _temp_dir: Some(tmp),
668        });
669        Ok(Self::connection_from_owner(owner))
670    }
671
672    #[cfg(not(feature = "test-filedb"))]
673    fn create_in_memory_engine() -> Result<Self> {
674        let engine = composed_in_memory_engine();
675        engine.open_engine()?;
676        let engine = Arc::new(engine);
677        engine.start_cleanup();
678        let owner = Arc::new(DatabaseOwner {
679            engine,
680            semantic_cache: Arc::new(SemanticCache::new()),
681            feedback_cache: Arc::new(FeedbackCache::new()),
682            plugin_registry: Arc::clone(&DEFAULT_PLUGIN_REGISTRY),
683            registry_key: "memory://".to_string(),
684            dsn: "memory://".to_string(),
685            requested_config: Config::in_memory(),
686        });
687        Ok(Self::connection_from_owner(owner))
688    }
689
690    /// Parse a DSN into scheme and path
691    fn parse_dsn(dsn: &str) -> Result<(String, String)> {
692        let idx = dsn
693            .find("://")
694            .ok_or_else(|| Error::parse("Invalid DSN format: expected scheme://path"))?;
695
696        let scheme = dsn[..idx].to_lowercase();
697        let path = dsn[idx + 3..].to_string();
698
699        // Validate scheme
700        match scheme.as_str() {
701            MEMORY_SCHEME | FILE_SCHEME => {}
702            _ => {
703                return Err(Error::parse(format!(
704                    "Unsupported scheme '{}'. Use 'memory://' or 'file://path'",
705                    scheme
706                )));
707            }
708        }
709
710        // Validate file path
711        if scheme == FILE_SCHEME {
712            let clean_path = if path.contains('?') {
713                &path[..path.find('?').unwrap()]
714            } else {
715                &path
716            };
717
718            if clean_path.is_empty() {
719                return Err(Error::parse("file:// scheme requires a non-empty path"));
720            }
721        }
722
723        Ok((scheme, path))
724    }
725
726    /// Parse file:// config from query parameters
727    fn parse_file_config(path: &str) -> Result<(String, Config)> {
728        fn parse_bool_option(key: &str, value: &str) -> Result<bool> {
729            match value.to_ascii_lowercase().as_str() {
730                "on" | "true" | "1" | "yes" => Ok(true),
731                "off" | "false" | "0" | "no" => Ok(false),
732                _ => Err(Error::invalid_argument(format!(
733                    "invalid {key}: '{value}' (expected on/off)"
734                ))),
735            }
736        }
737
738        let (clean_path, query) = if let Some(idx) = path.find('?') {
739            (path[..idx].to_string(), Some(&path[idx + 1..]))
740        } else {
741            (path.to_string(), None)
742        };
743
744        let mut config = Config::with_path(&clean_path);
745
746        // Parse query parameters
747        if let Some(query) = query {
748            let mut seen_options = HashSet::new();
749            for param in query.split('&') {
750                let mut parts = param.splitn(2, '=');
751                let key = parts.next().unwrap_or("");
752                let value = parts.next().unwrap_or("");
753                if !seen_options.insert(key) {
754                    return Err(Error::invalid_argument(format!(
755                        "duplicate file database option: '{key}'"
756                    )));
757                }
758
759                match key {
760                    // Sync mode: sync_mode=none|normal|full
761                    "sync_mode" => {
762                        config.persistence.sync_mode = match value.to_lowercase().as_str() {
763                            "none" | "off" | "0" => SyncMode::None,
764                            "normal" | "1" => SyncMode::Normal,
765                            "full" | "2" => SyncMode::Full,
766                            _ => {
767                                return Err(Error::invalid_argument(format!(
768                                    "invalid sync mode: '{value}' (expected none/normal/full)"
769                                )))
770                            }
771                        };
772                    }
773                    // Checkpoint interval in seconds: checkpoint_interval=60
774                    "checkpoint_interval" => {
775                        config.persistence.checkpoint_interval =
776                            value.parse::<u32>().map_err(|_| {
777                                Error::invalid_argument(format!(
778                                    "invalid checkpoint_interval: '{}'",
779                                    value
780                                ))
781                            })?;
782                    }
783                    // Compaction threshold: compact_threshold=4
784                    "compact_threshold" => {
785                        config.persistence.compact_threshold =
786                            value.parse::<u32>().map_err(|_| {
787                                Error::invalid_argument(format!(
788                                    "invalid compact_threshold: '{}'",
789                                    value
790                                ))
791                            })?;
792                    }
793                    "max_compaction_jobs" => {
794                        let count = value.parse::<usize>().map_err(|_| {
795                            Error::invalid_argument(format!(
796                                "invalid max_compaction_jobs: '{}'",
797                                value
798                            ))
799                        })?;
800                        if !(1..=radixdb_storage::config::MAX_COMPACTION_JOBS).contains(&count) {
801                            return Err(Error::invalid_argument(format!(
802                                "max_compaction_jobs must be in 1..={}",
803                                radixdb_storage::config::MAX_COMPACTION_JOBS
804                            )));
805                        }
806                        config.persistence.max_compaction_jobs = count;
807                    }
808                    // Shared CPU-heavy storage worker budget. Zero selects
809                    // host/cgroup-visible automatic parallelism.
810                    "storage_cpu_workers" => {
811                        config.persistence.storage_cpu_workers =
812                            value.parse::<usize>().map_err(|_| {
813                                Error::invalid_argument(format!(
814                                    "invalid storage_cpu_workers: '{}'",
815                                    value
816                                ))
817                            })?;
818                    }
819                    "page_cache_level" => {
820                        let level = value.parse::<u8>().map_err(|_| {
821                            Error::invalid_argument(format!(
822                                "invalid page_cache_level: '{}'",
823                                value
824                            ))
825                        })?;
826                        if level > radixdb_storage::config::MAX_PAGE_CACHE_LEVEL {
827                            return Err(Error::invalid_argument(format!(
828                                "page_cache_level must be in 0..={}",
829                                radixdb_storage::config::MAX_PAGE_CACHE_LEVEL
830                            )));
831                        }
832                        config.persistence.page_cache_level = level;
833                    }
834                    "page_cache_max_bytes" => {
835                        config.persistence.page_cache_max_bytes =
836                            value.parse::<u64>().map_err(|_| {
837                                Error::invalid_argument(format!(
838                                    "invalid page_cache_max_bytes: '{}'",
839                                    value
840                                ))
841                            })?;
842                    }
843                    "page_cache_memory_reserve" => {
844                        config.persistence.page_cache_memory_reserve =
845                            value.parse::<u64>().map_err(|_| {
846                                Error::invalid_argument(format!(
847                                    "invalid page_cache_memory_reserve: '{}'",
848                                    value
849                                ))
850                            })?;
851                    }
852                    // Maximum immutable inputs owned by one compaction job.
853                    "max_compaction_input_segments" => {
854                        let count = value.parse::<usize>().map_err(|_| {
855                            Error::invalid_argument(format!(
856                                "invalid max_compaction_input_segments: '{}'",
857                                value
858                            ))
859                        })?;
860                        config.persistence.max_compaction_input_segments = count.max(1);
861                    }
862                    // Maximum physical payload + posting bytes owned by one
863                    // compaction job.
864                    "max_compaction_input_bytes" => {
865                        let bytes = value.parse::<u64>().map_err(|_| {
866                            Error::invalid_argument(format!(
867                                "invalid max_compaction_input_bytes: '{}'",
868                                value
869                            ))
870                        })?;
871                        config.persistence.max_compaction_input_bytes = bytes.max(1);
872                    }
873                    "max_compaction_output_bytes" => {
874                        let bytes = value.parse::<u64>().map_err(|_| {
875                            Error::invalid_argument(format!(
876                                "invalid max_compaction_output_bytes: '{}'",
877                                value
878                            ))
879                        })?;
880                        config.persistence.max_compaction_output_bytes = bytes.max(1);
881                    }
882                    "compaction_job_time_budget_ms" => {
883                        config.persistence.compaction_job_time_budget_ms =
884                            value.parse::<u64>().map_err(|_| {
885                                Error::invalid_argument(format!(
886                                    "invalid compaction_job_time_budget_ms: '{}'",
887                                    value
888                                ))
889                            })?;
890                    }
891                    "compaction_io_bytes_per_sec" => {
892                        config.persistence.compaction_io_bytes_per_sec =
893                            value.parse::<u64>().map_err(|_| {
894                                Error::invalid_argument(format!(
895                                    "invalid compaction_io_bytes_per_sec: '{}'",
896                                    value
897                                ))
898                            })?;
899                    }
900                    "compaction_disk_reserve_bytes" => {
901                        config.persistence.compaction_disk_reserve_bytes =
902                            value.parse::<u64>().map_err(|_| {
903                                Error::invalid_argument(format!(
904                                    "invalid compaction_disk_reserve_bytes: '{}'",
905                                    value
906                                ))
907                            })?;
908                    }
909                    "compaction_retry_cooldown_ms" => {
910                        config.persistence.compaction_retry_cooldown_ms =
911                            value.parse::<u64>().map_err(|_| {
912                                Error::invalid_argument(format!(
913                                    "invalid compaction_retry_cooldown_ms: '{}'",
914                                    value
915                                ))
916                            })?;
917                    }
918                    "l0_soft_limit_segments" => {
919                        config.persistence.l0_soft_limit_segments =
920                            value.parse::<usize>().map_err(|_| {
921                                Error::invalid_argument(format!(
922                                    "invalid l0_soft_limit_segments: '{}'",
923                                    value
924                                ))
925                            })?;
926                    }
927                    "l0_hard_limit_segments" => {
928                        config.persistence.l0_hard_limit_segments =
929                            value.parse::<usize>().map_err(|_| {
930                                Error::invalid_argument(format!(
931                                    "invalid l0_hard_limit_segments: '{}'",
932                                    value
933                                ))
934                            })?;
935                    }
936                    "l0_soft_limit_bytes" => {
937                        config.persistence.l0_soft_limit_bytes =
938                            value.parse::<u64>().map_err(|_| {
939                                Error::invalid_argument(format!(
940                                    "invalid l0_soft_limit_bytes: '{}'",
941                                    value
942                                ))
943                            })?;
944                    }
945                    "l0_hard_limit_bytes" => {
946                        config.persistence.l0_hard_limit_bytes =
947                            value.parse::<u64>().map_err(|_| {
948                                Error::invalid_argument(format!(
949                                    "invalid l0_hard_limit_bytes: '{}'",
950                                    value
951                                ))
952                            })?;
953                    }
954                    "l0_soft_backpressure_wait_ms" => {
955                        config.persistence.l0_soft_backpressure_wait_ms =
956                            value.parse::<u64>().map_err(|_| {
957                                Error::invalid_argument(format!(
958                                    "invalid l0_soft_backpressure_wait_ms: '{}'",
959                                    value
960                                ))
961                            })?;
962                    }
963                    // Number of backup snapshots to keep: keep_snapshots=3
964                    "keep_snapshots" => {
965                        config.persistence.keep_snapshots = value.parse::<u32>().map_err(|_| {
966                            Error::invalid_argument(format!("invalid keep_snapshots: '{}'", value))
967                        })?;
968                    }
969                    // WAL flush trigger in bytes: wal_flush_trigger=32768
970                    "wal_flush_trigger" => {
971                        config.persistence.wal_flush_trigger =
972                            value.parse::<usize>().map_err(|_| {
973                                Error::invalid_argument(format!(
974                                    "invalid wal_flush_trigger: '{}'",
975                                    value
976                                ))
977                            })?;
978                    }
979                    // WAL buffer size in bytes: wal_buffer_size=65536
980                    "wal_buffer_size" => {
981                        config.persistence.wal_buffer_size =
982                            value.parse::<usize>().map_err(|_| {
983                                Error::invalid_argument(format!(
984                                    "invalid wal_buffer_size: '{}'",
985                                    value
986                                ))
987                            })?;
988                    }
989                    // WAL max size in bytes: wal_max_size=67108864
990                    "wal_max_size" => {
991                        config.persistence.wal_max_size = value.parse::<usize>().map_err(|_| {
992                            Error::invalid_argument(format!("invalid wal_max_size: '{}'", value))
993                        })?;
994                    }
995                    // Fail-closed memory envelope for one atomic COPY.
996                    "copy_max_transaction_bytes" => {
997                        let bytes = value.parse::<usize>().map_err(|_| {
998                            Error::invalid_argument(format!(
999                                "invalid copy_max_transaction_bytes: '{}'",
1000                                value
1001                            ))
1002                        })?;
1003                        if bytes == 0 {
1004                            return Err(Error::invalid_argument(
1005                                "copy_max_transaction_bytes must not be zero",
1006                            ));
1007                        }
1008                        config.persistence.copy_max_transaction_bytes = bytes;
1009                    }
1010                    // Removed before v1: Normal mode durably syncs every
1011                    // terminal commit, so accepting this knob would promise a
1012                    // batching policy that does not exist.
1013                    "commit_batch_size" => {
1014                        return Err(Error::invalid_argument(format!(
1015                            "commit_batch_size is unsupported; choose sync_mode instead (got '{}')",
1016                            value
1017                        )));
1018                    }
1019                    // Sync interval in ms: sync_interval_ms=10
1020                    "sync_interval_ms" => {
1021                        config.persistence.sync_interval_ms =
1022                            value.parse::<u32>().map_err(|_| {
1023                                Error::invalid_argument(format!(
1024                                    "invalid sync_interval_ms: '{}'",
1025                                    value
1026                                ))
1027                            })?;
1028                    }
1029                    // WAL compression: wal_compression=on|off
1030                    "wal_compression" => {
1031                        config.persistence.wal_compression =
1032                            parse_bool_option("wal_compression", value)?;
1033                    }
1034                    // Volume LZ4 compression: volume_compression=on|off
1035                    "volume_compression" => {
1036                        config.persistence.volume_compression =
1037                            parse_bool_option("volume_compression", value)?;
1038                    }
1039                    // All compressions (WAL + volume): compression=on|off
1040                    "compression" => {
1041                        let enabled = parse_bool_option("compression", value)?;
1042                        config.persistence.wal_compression = enabled;
1043                        config.persistence.volume_compression = enabled;
1044                    }
1045                    // Target rows per volume: target_volume_rows=1048576
1046                    "target_volume_rows" => {
1047                        let rows = value.parse::<usize>().map_err(|_| {
1048                            Error::invalid_argument(format!(
1049                                "invalid target_volume_rows: '{}'",
1050                                value
1051                            ))
1052                        })?;
1053                        config.persistence.target_volume_rows = rows.max(65_536);
1054                    }
1055                    // First seal hot byte threshold: seal_hot_bytes_threshold=67108864
1056                    "seal_hot_bytes_threshold" => {
1057                        let bytes = value.parse::<usize>().map_err(|_| {
1058                            Error::invalid_argument(format!(
1059                                "invalid seal_hot_bytes_threshold: '{}'",
1060                                value
1061                            ))
1062                        })?;
1063                        config.persistence.seal_hot_bytes_threshold = bytes.max(1);
1064                    }
1065                    // Incremental seal hot byte threshold:
1066                    // seal_incremental_hot_bytes_threshold=16777216
1067                    "seal_incremental_hot_bytes_threshold" => {
1068                        let bytes = value.parse::<usize>().map_err(|_| {
1069                            Error::invalid_argument(format!(
1070                                "invalid seal_incremental_hot_bytes_threshold: '{}'",
1071                                value
1072                            ))
1073                        })?;
1074                        config.persistence.seal_incremental_hot_bytes_threshold = bytes.max(1);
1075                    }
1076                    // Global resident cold-volume payload cache budget:
1077                    // volume_cache_bytes=1073741824
1078                    "volume_cache_bytes" => {
1079                        config.persistence.volume_cache_bytes =
1080                            value.parse::<usize>().map_err(|_| {
1081                                Error::invalid_argument(format!(
1082                                    "invalid volume_cache_bytes: '{}'",
1083                                    value
1084                                ))
1085                            })?;
1086                    }
1087                    // Cold-volume read queue depth:
1088                    // read_queue_depth=4
1089                    "read_queue_depth" => {
1090                        let depth = value.parse::<usize>().map_err(|_| {
1091                            Error::invalid_argument(format!(
1092                                "invalid read_queue_depth: '{}'",
1093                                value
1094                            ))
1095                        })?;
1096                        config.persistence.read_queue_depth = depth.max(1);
1097                    }
1098                    // Checkpoint on close: checkpoint_on_close=off
1099                    // Set to off to simulate crashes in tests (WAL not truncated)
1100                    "checkpoint_on_close" => {
1101                        config.persistence.checkpoint_on_close =
1102                            parse_bool_option("checkpoint_on_close", value)?;
1103                    }
1104                    // Cleanup interval in seconds: cleanup_interval=60
1105                    "cleanup_interval" => {
1106                        config.cleanup.interval_secs = value.parse::<u64>().map_err(|_| {
1107                            Error::invalid_argument(format!(
1108                                "invalid cleanup_interval: '{}'",
1109                                value
1110                            ))
1111                        })?;
1112                    }
1113                    // Deleted row retention in seconds: deleted_row_retention=300
1114                    "deleted_row_retention" => {
1115                        config.cleanup.deleted_row_retention_secs =
1116                            value.parse::<u64>().map_err(|_| {
1117                                Error::invalid_argument(format!(
1118                                    "invalid deleted_row_retention: '{}'",
1119                                    value
1120                                ))
1121                            })?;
1122                    }
1123                    // Transaction retention in seconds: transaction_retention=3600
1124                    "transaction_retention" => {
1125                        config.cleanup.transaction_retention_secs =
1126                            value.parse::<u64>().map_err(|_| {
1127                                Error::invalid_argument(format!(
1128                                    "invalid transaction_retention: '{}'",
1129                                    value
1130                                ))
1131                            })?;
1132                    }
1133                    // Disable cleanup: cleanup=off
1134                    "cleanup" => {
1135                        config.cleanup.enabled = parse_bool_option("cleanup", value)?;
1136                    }
1137                    _ => {
1138                        return Err(Error::invalid_argument(format!(
1139                            "unknown file database option: '{key}'"
1140                        )))
1141                    }
1142                }
1143            }
1144        }
1145
1146        if config.persistence.l0_soft_limit_segments == 0
1147            || config.persistence.l0_soft_limit_segments
1148                >= config.persistence.l0_hard_limit_segments
1149        {
1150            return Err(Error::invalid_argument(
1151                "l0 segment limits require 0 < soft < hard",
1152            ));
1153        }
1154        if config.persistence.l0_soft_limit_bytes == 0
1155            || config.persistence.l0_soft_limit_bytes >= config.persistence.l0_hard_limit_bytes
1156        {
1157            return Err(Error::invalid_argument(
1158                "l0 byte limits require 0 < soft < hard",
1159            ));
1160        }
1161
1162        Ok((clean_path, config))
1163    }
1164
1165    /// Execute a SQL statement
1166    ///
1167    /// Use this for DDL (CREATE, DROP, ALTER) and DML (INSERT, UPDATE, DELETE) statements.
1168    ///
1169    /// # Parameters
1170    ///
1171    /// Parameters can be passed using:
1172    /// - Empty tuple `()` for no parameters
1173    /// - Tuple syntax `(1, "Alice", 30)` for multiple parameters
1174    /// - `params!` macro `params![1, "Alice", 30]`
1175    ///
1176    /// # Returns
1177    ///
1178    /// Returns the number of rows affected for DML statements, or 0 for DDL.
1179    ///
1180    /// # Examples
1181    ///
1182    /// ```ignore
1183    /// // DDL - no parameters
1184    /// db.execute("CREATE TABLE users (id INTEGER, name TEXT)", ())?;
1185    ///
1186    /// // DML with tuple parameters
1187    /// db.execute("INSERT INTO users VALUES ($1, $2)", (1, "Alice"))?;
1188    ///
1189    /// // DML with params! macro
1190    /// db.execute("INSERT INTO users VALUES ($1, $2)", params![2, "Bob"])?;
1191    ///
1192    /// // Update with mixed types
1193    /// let affected = db.execute(
1194    ///     "UPDATE users SET name = $1 WHERE id = $2",
1195    ///     ("Charlie", 1)
1196    /// )?;
1197    /// ```
1198    pub fn execute<P: Params>(&self, sql: &str, params: P) -> Result<i64> {
1199        self.ensure_open()?;
1200        let executor = self
1201            .inner
1202            .executor
1203            .lock()
1204            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1205
1206        let result = executor.execute_with_params(sql, params.into_params())?;
1207        Ok(result.rows_affected())
1208    }
1209
1210    /// Execute a query that returns rows
1211    ///
1212    /// # Parameters
1213    ///
1214    /// Parameters can be passed using:
1215    /// - Empty tuple `()` for no parameters
1216    /// - Tuple syntax `(value,)` for single parameter (note trailing comma)
1217    /// - Tuple syntax `(1, "Alice")` for multiple parameters
1218    /// - `params!` macro `params![1, "Alice"]`
1219    ///
1220    /// # Examples
1221    ///
1222    /// ```ignore
1223    /// // Query all rows
1224    /// for row in db.query("SELECT * FROM users", ())? {
1225    ///     let row = row?;
1226    ///     let id: i64 = row.get(0)?;
1227    ///     let name: String = row.get(1)?;
1228    /// }
1229    ///
1230    /// // Query with parameters
1231    /// for row in db.query("SELECT * FROM users WHERE age > $1", (18,))? {
1232    ///     // ...
1233    /// }
1234    ///
1235    /// // Collect into Vec
1236    /// let users: Vec<_> = db.query("SELECT * FROM users", ())?
1237    ///     .collect::<Result<Vec<_>, _>>()?;
1238    /// ```
1239    pub fn query<P: Params>(&self, sql: &str, params: P) -> Result<Rows> {
1240        self.ensure_open()?;
1241        let executor = self
1242            .inner
1243            .executor
1244            .lock()
1245            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1246
1247        let result = executor.execute_with_params(sql, params.into_params())?;
1248        Ok(Rows::new(result))
1249    }
1250
1251    /// Execute a query and return a single value
1252    ///
1253    /// This is a convenience method for queries that return a single row with a single column.
1254    /// Returns an error if the query returns no rows.
1255    ///
1256    /// # Examples
1257    ///
1258    /// ```ignore
1259    /// let count: i64 = db.query_one("SELECT COUNT(*) FROM users", ())?;
1260    /// let name: String = db.query_one("SELECT name FROM users WHERE id = $1", (1,))?;
1261    /// ```
1262    pub fn query_one<T: FromValue, P: Params>(&self, sql: &str, params: P) -> Result<T> {
1263        let row = self
1264            .query(sql, params)?
1265            .next()
1266            .ok_or(Error::NoRowsReturned)??;
1267        row.get(0)
1268    }
1269
1270    /// Execute a query and return an optional single value
1271    ///
1272    /// Like `query_one`, but returns `None` if no rows are returned.
1273    ///
1274    /// # Examples
1275    ///
1276    /// ```ignore
1277    /// let name: Option<String> = db.query_opt("SELECT name FROM users WHERE id = $1", (999,))?;
1278    /// assert!(name.is_none());
1279    /// ```
1280    pub fn query_opt<T: FromValue, P: Params>(&self, sql: &str, params: P) -> Result<Option<T>> {
1281        match self.query(sql, params)?.next() {
1282            Some(row) => Ok(Some(row?.get(0)?)),
1283            None => Ok(None),
1284        }
1285    }
1286
1287    /// Execute a write statement with a timeout
1288    ///
1289    /// Like `execute`, but cancels the query if it exceeds the timeout.
1290    /// Timeout is specified in milliseconds. Use 0 for no timeout.
1291    ///
1292    /// # Examples
1293    ///
1294    /// ```ignore
1295    /// // Execute with 5 second timeout
1296    /// db.execute_with_timeout("DELETE FROM large_table WHERE old = true", (), 5000)?;
1297    /// ```
1298    pub fn execute_with_timeout<P: Params>(
1299        &self,
1300        sql: &str,
1301        params: P,
1302        timeout_ms: u64,
1303    ) -> Result<i64> {
1304        self.ensure_open()?;
1305        let executor = self
1306            .inner
1307            .executor
1308            .lock()
1309            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1310
1311        let param_values = params.into_params();
1312        let ctx = ExecutionContextBuilder::new()
1313            .params(param_values)
1314            .timeout_ms(timeout_ms)
1315            .build();
1316
1317        let result = executor.execute_with_context(sql, &ctx)?;
1318        Ok(result.rows_affected())
1319    }
1320
1321    /// Execute a query with a timeout
1322    ///
1323    /// Like `query`, but cancels the query if it exceeds the timeout.
1324    /// Timeout is specified in milliseconds. Use 0 for no timeout.
1325    ///
1326    /// # Examples
1327    ///
1328    /// ```ignore
1329    /// // Query with 10 second timeout
1330    /// for row in db.query_with_timeout("SELECT * FROM large_table", (), 10000)? {
1331    ///     // process row
1332    /// }
1333    /// ```
1334    pub fn query_with_timeout<P: Params>(
1335        &self,
1336        sql: &str,
1337        params: P,
1338        timeout_ms: u64,
1339    ) -> Result<Rows> {
1340        self.ensure_open()?;
1341        let executor = self
1342            .inner
1343            .executor
1344            .lock()
1345            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1346
1347        let param_values = params.into_params();
1348        let ctx = ExecutionContextBuilder::new()
1349            .params(param_values)
1350            .timeout_ms(timeout_ms)
1351            .build();
1352
1353        let result = executor.execute_with_context(sql, &ctx)?;
1354        Ok(Rows::new(result))
1355    }
1356
1357    /// Prepare a SQL statement for repeated execution
1358    ///
1359    /// Prepared statements are more efficient when executing the same query
1360    /// multiple times with different parameters.
1361    ///
1362    /// # Examples
1363    ///
1364    /// ```ignore
1365    /// let stmt = db.prepare("SELECT * FROM users WHERE id = $1")?;
1366    ///
1367    /// // Execute multiple times with different parameters
1368    /// for id in 1..=10 {
1369    ///     for row in stmt.query((id,))? {
1370    ///         // ...
1371    ///     }
1372    /// }
1373    /// ```
1374    pub fn prepare(&self, sql: &str) -> Result<Statement> {
1375        self.ensure_open()?;
1376        Statement::new(Arc::downgrade(&self.inner), sql.to_string(), self)
1377    }
1378
1379    /// Create a Database from an existing Arc<DatabaseInner>.
1380    /// Used by Statement to upgrade weak references.
1381    pub(crate) fn from_inner(inner: Arc<DatabaseInner>) -> Self {
1382        Database { inner }
1383    }
1384
1385    /// Execute a statement with named parameters
1386    ///
1387    /// Named parameters use the `:name` syntax in SQL queries.
1388    ///
1389    /// # Examples
1390    ///
1391    /// ```ignore
1392    /// use radixdb::{Database, named_params};
1393    ///
1394    /// let db = Database::open("memory://")?;
1395    /// db.execute("CREATE TABLE users (id INTEGER, name TEXT, age INTEGER)", ())?;
1396    ///
1397    /// // Insert with named params
1398    /// db.execute_named(
1399    ///     "INSERT INTO users VALUES (:id, :name, :age)",
1400    ///     named_params!{ id: 1, name: "Alice", age: 30 }
1401    /// )?;
1402    ///
1403    /// // Update with named params
1404    /// db.execute_named(
1405    ///     "UPDATE users SET name = :name WHERE id = :id",
1406    ///     named_params!{ id: 1, name: "Alicia" }
1407    /// )?;
1408    /// ```
1409    pub fn execute_named(&self, sql: &str, params: NamedParams) -> Result<i64> {
1410        self.ensure_open()?;
1411        let executor = self
1412            .inner
1413            .executor
1414            .lock()
1415            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1416
1417        let result = executor.execute_with_named_params(sql, params.into_inner())?;
1418        Ok(result.rows_affected())
1419    }
1420
1421    /// Execute a query with named parameters
1422    ///
1423    /// Named parameters use the `:name` syntax in SQL queries.
1424    ///
1425    /// # Examples
1426    ///
1427    /// ```ignore
1428    /// use radixdb::{Database, named_params};
1429    ///
1430    /// let db = Database::open("memory://")?;
1431    /// db.execute("CREATE TABLE users (id INTEGER, name TEXT)", ())?;
1432    /// db.execute("INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob')", ())?;
1433    ///
1434    /// // Query with named params
1435    /// for row in db.query_named(
1436    ///     "SELECT * FROM users WHERE name = :name",
1437    ///     named_params!{ name: "Alice" }
1438    /// )? {
1439    ///     let row = row?;
1440    ///     println!("Found user: id={}", row.get::<i64>(0)?);
1441    /// }
1442    /// ```
1443    pub fn query_named(&self, sql: &str, params: NamedParams) -> Result<Rows> {
1444        self.ensure_open()?;
1445        let executor = self
1446            .inner
1447            .executor
1448            .lock()
1449            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1450
1451        let result = executor.execute_with_named_params(sql, params.into_inner())?;
1452        Ok(Rows::new(result))
1453    }
1454
1455    /// Execute a query with named parameters and a cancellation deadline.
1456    ///
1457    /// This is the named-parameter counterpart of [`Database::query_with_timeout`].
1458    /// A timeout of zero keeps the existing unlimited behavior.
1459    pub fn query_named_with_timeout(
1460        &self,
1461        sql: &str,
1462        params: NamedParams,
1463        timeout_ms: u64,
1464    ) -> Result<Rows> {
1465        self.ensure_open()?;
1466        let executor = self
1467            .inner
1468            .executor
1469            .lock()
1470            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1471
1472        let mut ctx = ExecutionContext::with_named_params(params.into_inner());
1473        ctx.set_timeout_ms(timeout_ms);
1474        let result = executor.execute_with_context(sql, &ctx)?;
1475        Ok(Rows::new(result))
1476    }
1477
1478    /// Execute one server request through the embedded facade contract.
1479    #[doc(hidden)]
1480    pub fn query_for_server(
1481        &self,
1482        sql: &str,
1483        context: &super::ServerExecutionContext,
1484    ) -> Result<Rows> {
1485        self.ensure_open()?;
1486        let executor = self
1487            .inner
1488            .executor
1489            .lock()
1490            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1491        let result = executor.execute_with_context(sql, context.inner())?;
1492        Ok(Rows::new(result))
1493    }
1494
1495    /// Execute a query with named parameters and return a single value
1496    ///
1497    /// # Examples
1498    ///
1499    /// ```ignore
1500    /// use radixdb::{Database, named_params};
1501    ///
1502    /// let count: i64 = db.query_one_named(
1503    ///     "SELECT COUNT(*) FROM users WHERE age > :min_age",
1504    ///     named_params!{ min_age: 18 }
1505    /// )?;
1506    /// ```
1507    pub fn query_one_named<T: FromValue>(&self, sql: &str, params: NamedParams) -> Result<T> {
1508        let mut rows = self.query_named(sql, params)?;
1509        match rows.next() {
1510            Some(Ok(row)) => row.get(0),
1511            Some(Err(e)) => Err(e),
1512            None => Err(Error::NoRowsReturned),
1513        }
1514    }
1515
1516    /// Execute a query and map results to structs
1517    ///
1518    /// This method executes a query and converts each row to a struct
1519    /// that implements the `FromRow` trait.
1520    ///
1521    /// # Examples
1522    ///
1523    /// ```ignore
1524    /// use radixdb::{Database, FromRow, ResultRow, Result};
1525    ///
1526    /// struct User {
1527    ///     id: i64,
1528    ///     name: String,
1529    /// }
1530    ///
1531    /// impl FromRow for User {
1532    ///     fn from_row(row: &ResultRow) -> Result<Self> {
1533    ///         Ok(User {
1534    ///             id: row.get(0)?,
1535    ///             name: row.get(1)?,
1536    ///         })
1537    ///     }
1538    /// }
1539    ///
1540    /// let db = Database::open("memory://")?;
1541    /// db.execute("CREATE TABLE users (id INTEGER, name TEXT)", ())?;
1542    /// db.execute("INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob')", ())?;
1543    ///
1544    /// // Query and map to structs
1545    /// let users: Vec<User> = db.query_as("SELECT id, name FROM users", ())?;
1546    /// assert_eq!(users.len(), 2);
1547    /// assert_eq!(users[0].name, "Alice");
1548    /// ```
1549    pub fn query_as<T: FromRow, P: Params>(&self, sql: &str, params: P) -> Result<Vec<T>> {
1550        let rows = self.query(sql, params)?;
1551        rows.map(|r| r.and_then(|row| T::from_row(&row))).collect()
1552    }
1553
1554    /// Execute a query with named parameters and map results to structs
1555    ///
1556    /// # Examples
1557    ///
1558    /// ```ignore
1559    /// use radixdb::{Database, FromRow, ResultRow, Result, named_params};
1560    ///
1561    /// struct Product {
1562    ///     id: i64,
1563    ///     name: String,
1564    ///     price: f64,
1565    /// }
1566    ///
1567    /// impl FromRow for Product {
1568    ///     fn from_row(row: &ResultRow) -> Result<Self> {
1569    ///         Ok(Product {
1570    ///             id: row.get(0)?,
1571    ///             name: row.get(1)?,
1572    ///             price: row.get(2)?,
1573    ///         })
1574    ///     }
1575    /// }
1576    ///
1577    /// let products: Vec<Product> = db.query_as_named(
1578    ///     "SELECT id, name, price FROM products WHERE price > :min_price",
1579    ///     named_params!{ min_price: 10.0 }
1580    /// )?;
1581    /// ```
1582    pub fn query_as_named<T: FromRow>(&self, sql: &str, params: NamedParams) -> Result<Vec<T>> {
1583        let rows = self.query_named(sql, params)?;
1584        rows.map(|r| r.and_then(|row| T::from_row(&row))).collect()
1585    }
1586
1587    /// Begin a new transaction with default isolation level
1588    ///
1589    /// # Examples
1590    ///
1591    /// ```ignore
1592    /// let tx = db.begin()?;
1593    /// tx.execute("INSERT INTO users VALUES ($1, $2)", (1, "Alice"))?;
1594    /// tx.commit()?;
1595    /// ```
1596    pub fn begin(&self) -> Result<Transaction> {
1597        self.ensure_open()?;
1598        let executor = self
1599            .inner
1600            .executor
1601            .lock()
1602            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1603
1604        let tx = executor.begin_transaction()?;
1605        Ok(Transaction::new(tx, Arc::clone(&self.inner)))
1606    }
1607
1608    /// Begin the read-only transaction used by the logical SQL exporter.
1609    ///
1610    /// Lock ordering intentionally matches normal connection execution:
1611    /// connection executor first, then the shared DDL fence. The returned
1612    /// transaction retains that fence and streams rows from one MVCC snapshot.
1613    #[doc(hidden)]
1614    pub fn begin_logical_export(&self) -> Result<Transaction> {
1615        self.ensure_open()?;
1616        let executor = self
1617            .inner
1618            .executor
1619            .lock()
1620            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1621        let engine = executor.engine().clone();
1622        let plugin_registry = executor.plugin_registry();
1623        let fence = engine.acquire_ddl_statement_fence(false);
1624        let tx = executor.begin_transaction_with_isolation(IsolationLevel::SnapshotIsolation)?;
1625        Ok(Transaction::new_logical_export(
1626            tx,
1627            engine,
1628            plugin_registry,
1629            Arc::clone(&self.inner),
1630            fence,
1631        ))
1632    }
1633
1634    /// Begin a new transaction with a specific isolation level
1635    ///
1636    /// # Examples
1637    ///
1638    /// ```ignore
1639    /// use radixdb::IsolationLevel;
1640    ///
1641    /// let tx = db.begin_with_isolation(IsolationLevel::SnapshotIsolation)?;
1642    /// // All reads in this transaction see a consistent snapshot
1643    /// tx.execute("UPDATE users SET balance = balance - 100 WHERE id = $1", (1,))?;
1644    /// tx.commit()?;
1645    /// ```
1646    pub fn begin_with_isolation(&self, isolation: IsolationLevel) -> Result<Transaction> {
1647        self.ensure_open()?;
1648        let executor = self
1649            .inner
1650            .executor
1651            .lock()
1652            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1653
1654        let tx = executor.begin_transaction_with_isolation(isolation)?;
1655        Ok(Transaction::new(tx, Arc::clone(&self.inner)))
1656    }
1657
1658    /// Get the underlying storage engine
1659    ///
1660    /// This is primarily for advanced use cases and testing.
1661    pub fn engine(&self) -> &Arc<MVCCEngine> {
1662        &self.inner.engine
1663    }
1664
1665    /// Close the database connection
1666    ///
1667    /// This removes the database from the global registry and closes the engine,
1668    /// releasing the file lock immediately so another process can open the database.
1669    ///
1670    /// Note: The engine is also closed automatically when all Database instances
1671    /// are dropped.
1672    pub fn close(&self) -> Result<()> {
1673        if Arc::strong_count(&self.inner) != 1 {
1674            return Err(Error::invalid_argument(
1675                "cannot close database while a Transaction or retained connection owner is active",
1676            ));
1677        }
1678        // Explicit close is terminal for the shared engine, but registry
1679        // ownership remains intact until every mandatory close stage succeeds.
1680        self.inner.engine.close_engine()?;
1681        let owner = self.owner_arc();
1682        let mut registry = DATABASE_REGISTRY
1683            .write()
1684            .map_err(|_| Error::LockAcquisitionFailed("registry write".to_string()))?;
1685        if matches!(
1686            registry.get(&owner.registry_key),
1687            Some(DatabaseRegistryEntry::Ready(entry)) if Arc::ptr_eq(entry, owner)
1688        ) {
1689            registry.remove(&owner.registry_key);
1690        }
1691
1692        Ok(())
1693    }
1694
1695    /// Get a cached plan for a SQL statement (parse once, execute many times).
1696    ///
1697    /// Returns a `CachedPlanRef` that can be stored and passed to
1698    /// `execute_plan()` / `query_plan()` for zero-lookup execution.
1699    pub fn cached_plan(&self, sql: &str) -> Result<CachedPlanRef> {
1700        self.ensure_open()?;
1701        let executor = self
1702            .inner
1703            .executor
1704            .lock()
1705            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1706        executor.get_or_create_plan(sql)
1707    }
1708
1709    /// Execute a pre-cached plan with positional parameters (no parsing, no cache lookup).
1710    pub fn execute_plan<P: Params>(&self, plan: &CachedPlanRef, params: P) -> Result<i64> {
1711        self.ensure_open()?;
1712        let executor = self
1713            .inner
1714            .executor
1715            .lock()
1716            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1717        let param_values = params.into_params();
1718        let ctx = if param_values.is_empty() {
1719            ExecutionContext::new()
1720        } else {
1721            ExecutionContext::with_params(param_values)
1722        };
1723        let result = executor.execute_with_cached_plan(plan, &ctx)?;
1724        Ok(result.rows_affected())
1725    }
1726
1727    /// Query using a pre-cached plan with positional parameters (no parsing, no cache lookup).
1728    pub fn query_plan<P: Params>(&self, plan: &CachedPlanRef, params: P) -> Result<Rows> {
1729        self.ensure_open()?;
1730        let executor = self
1731            .inner
1732            .executor
1733            .lock()
1734            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1735        let param_values = params.into_params();
1736        let ctx = if param_values.is_empty() {
1737            ExecutionContext::new()
1738        } else {
1739            ExecutionContext::with_params(param_values)
1740        };
1741        let result = executor.execute_with_cached_plan(plan, &ctx)?;
1742        Ok(Rows::new(result))
1743    }
1744
1745    /// Execute a pre-cached plan with named parameters (no parsing, no cache lookup).
1746    pub fn execute_named_plan(&self, plan: &CachedPlanRef, params: NamedParams) -> Result<i64> {
1747        self.ensure_open()?;
1748        let executor = self
1749            .inner
1750            .executor
1751            .lock()
1752            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1753        let ctx = ExecutionContext::with_named_params(params.into_inner());
1754        let result = executor.execute_with_cached_plan(plan, &ctx)?;
1755        Ok(result.rows_affected())
1756    }
1757
1758    /// Query using a pre-cached plan with named parameters (no parsing, no cache lookup).
1759    pub fn query_named_plan(&self, plan: &CachedPlanRef, params: NamedParams) -> Result<Rows> {
1760        self.ensure_open()?;
1761        let executor = self
1762            .inner
1763            .executor
1764            .lock()
1765            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1766        let ctx = ExecutionContext::with_named_params(params.into_inner());
1767        let result = executor.execute_with_cached_plan(plan, &ctx)?;
1768        Ok(Rows::new(result))
1769    }
1770
1771    /// Check if a table exists
1772    pub fn table_exists(&self, name: &str) -> Result<bool> {
1773        self.ensure_open()?;
1774        let engine = &self.inner.engine;
1775        let tx = engine.begin_transaction()?;
1776        match tx.get_table(name) {
1777            Ok(_) => Ok(true),
1778            Err(Error::TableNotFound(_)) => Ok(false),
1779            Err(error) => Err(error),
1780        }
1781    }
1782
1783    /// Get the DSN this database was opened with
1784    pub fn dsn(&self) -> &str {
1785        &self.inner.owner.dsn
1786    }
1787
1788    /// Set the default isolation level for new transactions
1789    pub fn set_default_isolation_level(&self, level: IsolationLevel) -> Result<()> {
1790        self.ensure_open()?;
1791        let executor = self
1792            .inner
1793            .executor
1794            .lock()
1795            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1796        executor.set_default_isolation_level(level);
1797        Ok(())
1798    }
1799
1800    /// Return this connection's default isolation for future transactions.
1801    pub fn default_isolation_level(&self) -> Result<IsolationLevel> {
1802        self.ensure_open()?;
1803        let executor = self
1804            .inner
1805            .executor
1806            .lock()
1807            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1808        Ok(executor.default_isolation_level())
1809    }
1810
1811    /// Create a backup snapshot of the database
1812    ///
1813    /// This pins one complete catalog/data/index generation and copies every
1814    /// reachable immutable member into a committed physical snapshot.
1815    /// Normal persistence advances the same generation graph plus WAL.
1816    ///
1817    /// In-memory databases reject this operation because no durable snapshot
1818    /// artifact can be created.
1819    pub fn create_snapshot(&self) -> Result<radixdb_storage::PhysicalSnapshotIdentity> {
1820        use radixdb_storage::Engine;
1821        self.ensure_open()?;
1822        self.inner.engine.create_snapshot()
1823    }
1824
1825    /// Restore the database from a backup snapshot.
1826    ///
1827    /// If no identity is provided, restores from the latest snapshot.
1828    /// Otherwise restores the snapshot with the specified stable identity.
1829    ///
1830    /// This is a destructive operation that atomically replaces the current
1831    /// catalog/data/index generation with the selected complete snapshot.
1832    pub fn restore_snapshot(&self, snapshot_id: Option<&str>) -> Result<String> {
1833        use radixdb_storage::Engine;
1834        self.ensure_open()?;
1835        // Cache admission is fallible, so acquire it before the destructive
1836        // engine operation. A poisoned lock must never turn a committed
1837        // restore into an error outcome.
1838        let executor = self
1839            .inner
1840            .executor
1841            .lock()
1842            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1843        let result = self.inner.engine.restore_snapshot(snapshot_id)?;
1844        // Clear all query caches since all data has changed.
1845        executor.clear_semantic_cache();
1846        radixdb_executor::context::clear_scalar_subquery_cache();
1847        radixdb_executor::context::clear_in_subquery_cache();
1848        radixdb_executor::context::clear_semi_join_cache();
1849        Ok(result)
1850    }
1851
1852    /// Get the internal executor (for Statement use)
1853    pub(crate) fn executor(&self) -> &Mutex<Executor> {
1854        &self.inner.executor
1855    }
1856
1857    #[doc(hidden)]
1858    pub fn describe_query_output(
1859        &self,
1860        sql: &str,
1861    ) -> Result<Option<Vec<radixdb_executor::QueryOutputColumn>>> {
1862        self.ensure_open()?;
1863        self.inner
1864            .executor
1865            .lock()
1866            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?
1867            .describe_query_output(sql)
1868    }
1869
1870    /// Whether this connection-local SQL executor owns a BEGIN transaction.
1871    #[doc(hidden)]
1872    pub fn has_active_sql_transaction(&self) -> Result<bool> {
1873        self.ensure_open()?;
1874        let executor = self
1875            .inner
1876            .executor
1877            .lock()
1878            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1879        Ok(executor.has_active_transaction())
1880    }
1881
1882    /// Get semantic-cache statistics.
1883    pub fn semantic_cache_stats(&self) -> Result<SemanticCacheStatsSnapshot> {
1884        self.ensure_open()?;
1885        let executor = self
1886            .inner
1887            .executor
1888            .lock()
1889            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1890        Ok(executor.semantic_cache_stats())
1891    }
1892
1893    /// Clear all semantic-cache entries.
1894    pub fn clear_semantic_cache(&self) -> Result<()> {
1895        self.ensure_open()?;
1896        let executor = self
1897            .inner
1898            .executor
1899            .lock()
1900            .map_err(|_| Error::LockAcquisitionFailed("executor".to_string()))?;
1901        executor.clear_semantic_cache();
1902        Ok(())
1903    }
1904}
1905
1906#[cfg(test)]
1907mod tests {
1908    use super::*;
1909    use crate::named_params;
1910    use radixdb_core::Value;
1911
1912    #[test]
1913    fn checkpoint_reopens_from_canonical_artifact_generation() {
1914        let directory = tempfile::tempdir().expect("create database directory");
1915        let database_path = directory.path().join("canonical-checkpoint");
1916        let dsn = format!(
1917            "file://{}?checkpoint_on_close=false",
1918            database_path.display()
1919        );
1920        let database = Database::open(&dsn).expect("open persistent database");
1921        database
1922            .execute(
1923                "CREATE TABLE items (id INTEGER PRIMARY KEY, payload TEXT NOT NULL)",
1924                (),
1925            )
1926            .expect("create table");
1927        let catalog = database.engine().pin_catalog().expect("pin catalog");
1928        assert!(
1929            catalog
1930                .graph()
1931                .objects()
1932                .any(|object| object.name().display().as_str() == "items"),
1933            "CREATE TABLE must publish the logical catalog before returning"
1934        );
1935        database
1936            .execute("INSERT INTO items VALUES (1, 'one'), (2, 'two')", ())
1937            .expect("insert rows");
1938        radixdb_storage::traits::Engine::force_checkpoint_cycle(database.engine().as_ref())
1939            .expect("publish physical checkpoint");
1940        database.close().expect("close database");
1941
1942        let mut pending = vec![database_path.clone()];
1943        while let Some(directory) = pending.pop() {
1944            for entry in std::fs::read_dir(&directory).expect("read artifact tree") {
1945                let entry = entry.expect("read artifact member");
1946                let path = entry.path();
1947                if entry.file_type().expect("read artifact type").is_dir() {
1948                    pending.push(path);
1949                    continue;
1950                }
1951                let name = path
1952                    .file_name()
1953                    .and_then(|name| name.to_str())
1954                    .expect("artifact name is UTF-8");
1955                assert!(!name.starts_with("ddl-"), "legacy DDL owner: {name}");
1956                assert!(!name.ends_with(".vol"), "legacy volume: {name}");
1957                assert!(!name.ends_with(".rpi"), "legacy postings: {name}");
1958            }
1959        }
1960        assert!(!database_path.join("volumes").exists());
1961
1962        let reopened = Database::open(&dsn).expect("reopen persistent database");
1963        let rows = reopened
1964            .query("SELECT id, payload FROM items ORDER BY id", ())
1965            .expect("query recovered rows")
1966            .collect_vec()
1967            .expect("collect recovered rows");
1968        assert_eq!(rows.len(), 2);
1969        assert_eq!(rows[0].get::<i64>(0).expect("first id"), 1);
1970        assert_eq!(rows[0].get::<String>(1).expect("first payload"), "one");
1971        assert_eq!(rows[1].get::<i64>(0).expect("second id"), 2);
1972        assert_eq!(rows[1].get::<String>(1).expect("second payload"), "two");
1973        reopened.close().expect("close reopened database");
1974    }
1975
1976    #[test]
1977    fn transactional_index_rename_and_drop_survive_reopen() {
1978        let directory = tempfile::tempdir().expect("create database directory");
1979        let database_path = directory.path().join("transactional-index-ddl");
1980        let dsn = format!(
1981            "file://{}?checkpoint_on_close=false",
1982            database_path.display()
1983        );
1984        let database = Database::open(&dsn).expect("open persistent database");
1985        database
1986            .execute(
1987                "CREATE TABLE items (id INTEGER PRIMARY KEY, payload TEXT NOT NULL)",
1988                (),
1989            )
1990            .expect("create table");
1991        database
1992            .execute("CREATE INDEX idx_payload ON items(payload)", ())
1993            .expect("create index");
1994        database
1995            .execute("ALTER INDEX idx_payload RENAME TO idx_payload_live", ())
1996            .expect("rename index");
1997        database.close().expect("close renamed-index database");
1998
1999        let reopened = Database::open(&dsn).expect("reopen renamed-index database");
2000        let indexes = reopened
2001            .schema()
2002            .table("items")
2003            .indexes()
2004            .fetch()
2005            .expect("fetch indexes after rename reopen");
2006        assert!(indexes.iter().any(|index| index.name == "idx_payload_live"));
2007        assert!(!indexes.iter().any(|index| index.name == "idx_payload"));
2008        reopened
2009            .execute("DROP INDEX idx_payload_live ON items", ())
2010            .expect("drop renamed index");
2011        reopened.close().expect("close dropped-index database");
2012
2013        let reopened = Database::open(&dsn).expect("reopen dropped-index database");
2014        let indexes = reopened
2015            .schema()
2016            .table("items")
2017            .indexes()
2018            .fetch()
2019            .expect("fetch indexes after drop reopen");
2020        assert!(!indexes.iter().any(|index| index.name == "idx_payload_live"));
2021        reopened.close().expect("close final database");
2022    }
2023
2024    #[test]
2025    fn ctas_and_view_catalog_survive_reopen_without_legacy_ddl_owner() {
2026        let directory = tempfile::tempdir().expect("create database directory");
2027        let database_path = directory.path().join("ctas-view-catalog");
2028        let dsn = format!(
2029            "file://{}?checkpoint_on_close=false&checkpoint_interval=0",
2030            database_path.display()
2031        );
2032        let database = Database::open(&dsn).expect("open persistent database");
2033        database
2034            .execute(
2035                "CREATE TABLE source_rows (id INTEGER PRIMARY KEY, payload TEXT NOT NULL)",
2036                (),
2037            )
2038            .expect("create source table");
2039        database
2040            .execute("INSERT INTO source_rows VALUES (1, 'one'), (2, 'two')", ())
2041            .expect("insert source rows");
2042        database
2043            .execute(
2044                "CREATE TABLE copied_rows AS SELECT id, payload FROM source_rows",
2045                (),
2046            )
2047            .expect("create table as select");
2048        database
2049            .execute(
2050                "CREATE VIEW copied_view AS SELECT id, payload FROM copied_rows",
2051                (),
2052            )
2053            .expect("create view");
2054        assert_eq!(
2055            database
2056                .query_one::<i64, _>("SELECT COUNT(*) FROM copied_view", ())
2057                .expect("query live view"),
2058            2
2059        );
2060        database.close().expect("close catalog database");
2061
2062        let reopened = Database::open(&dsn).expect("reopen catalog database");
2063        assert_eq!(
2064            reopened
2065                .query_one::<i64, _>("SELECT COUNT(*) FROM copied_rows", ())
2066                .expect("query reopened CTAS table"),
2067            2
2068        );
2069        assert_eq!(
2070            reopened
2071                .query_one::<i64, _>("SELECT COUNT(*) FROM copied_view", ())
2072                .expect("query reopened view"),
2073            2
2074        );
2075        reopened
2076            .execute("DROP VIEW copied_view", ())
2077            .expect("drop view");
2078        assert!(reopened.query("SELECT * FROM copied_view", ()).is_err());
2079        reopened.close().expect("close dropped-view database");
2080
2081        let reopened = Database::open(&dsn).expect("reopen dropped-view database");
2082        assert!(reopened.query("SELECT * FROM copied_view", ()).is_err());
2083        reopened.close().expect("close final database");
2084    }
2085
2086    #[test]
2087    fn checkpoint_uses_the_actual_wal_generation_after_size_rotations() {
2088        fn selected_wal_floor(root: &std::path::Path) -> u64 {
2089            [
2090                ("CONTROL.0", radixdb_storage::v6::ControlSlotIndex::Zero),
2091                ("CONTROL.1", radixdb_storage::v6::ControlSlotIndex::One),
2092            ]
2093            .into_iter()
2094            .filter_map(|(name, slot)| {
2095                let bytes = std::fs::read(root.join(name)).ok()?;
2096                radixdb_storage::v6::decode_control_slot(&bytes, slot).ok()
2097            })
2098            .max_by_key(|control| control.database_generation())
2099            .expect("at least one valid CONTROL")
2100            .wal_replay_floor()
2101            .generation()
2102            .get()
2103        }
2104
2105        fn active_wal_generations(root: &std::path::Path) -> Vec<u64> {
2106            let mut generations = std::fs::read_dir(root.join("wal"))
2107                .expect("read WAL directory")
2108                .filter_map(std::result::Result::ok)
2109                .filter_map(|entry| {
2110                    let name = entry.file_name();
2111                    let name = name.to_str()?;
2112                    let encoded = name.strip_prefix("wal-")?.strip_suffix(".log")?;
2113                    (encoded.len() == 16)
2114                        .then(|| u64::from_str_radix(encoded, 16).ok())
2115                        .flatten()
2116                })
2117                .collect::<Vec<_>>();
2118            generations.sort_unstable();
2119            generations
2120        }
2121
2122        let directory = tempfile::tempdir().expect("create database directory");
2123        let database_path = directory.path().join("rotated-wal-checkpoint");
2124        let dsn = format!(
2125            "file://{}?checkpoint_on_close=false&checkpoint_interval=0&wal_max_size=512",
2126            database_path.display()
2127        );
2128        let database = Database::open(&dsn).expect("open persistent database");
2129        database
2130            .execute(
2131                "CREATE TABLE items (id INTEGER PRIMARY KEY, payload TEXT NOT NULL)",
2132                (),
2133            )
2134            .expect("create table");
2135        for id in 1..=12_i64 {
2136            database
2137                .execute(
2138                    "INSERT INTO items VALUES (?, ?)",
2139                    (id, format!("{id:04}-{}", "x".repeat(512))),
2140                )
2141                .expect("insert rotation row");
2142        }
2143
2144        let wal_files_before_checkpoint = std::fs::read_dir(database_path.join("wal"))
2145            .expect("read WAL directory")
2146            .filter_map(std::result::Result::ok)
2147            .filter(|entry| {
2148                entry
2149                    .file_name()
2150                    .to_str()
2151                    .is_some_and(|name| name.starts_with("wal-") && name.ends_with(".log"))
2152            })
2153            .count();
2154        assert!(
2155            wal_files_before_checkpoint >= 3,
2156            "test did not create multiple WAL rotations"
2157        );
2158
2159        radixdb_storage::traits::Engine::force_checkpoint_cycle(database.engine().as_ref())
2160            .expect("publish checkpoint at actual WAL generation");
2161        let first_checkpoint_floor = selected_wal_floor(&database_path);
2162        assert!(
2163            first_checkpoint_floor > 1,
2164            "checkpoint did not retain the actual rotated generation"
2165        );
2166
2167        for id in 13..=24_i64 {
2168            database
2169                .execute(
2170                    "INSERT INTO items VALUES (?, ?)",
2171                    (id, format!("{id:04}-{}", "y".repeat(512))),
2172                )
2173                .expect("insert second rotation row");
2174        }
2175        radixdb_storage::traits::Engine::force_checkpoint_cycle(database.engine().as_ref())
2176            .expect("publish second checkpoint and retire old rotation prefix");
2177        let active_generations = active_wal_generations(&database_path);
2178        assert!(
2179            active_generations
2180                .iter()
2181                .all(|generation| *generation >= first_checkpoint_floor),
2182            "WAL generations below retained floor {first_checkpoint_floor} leaked: {active_generations:?}"
2183        );
2184        database.close().expect("close database");
2185
2186        let reopened = Database::open(&dsn).expect("reopen rotated WAL database");
2187        let rows = reopened
2188            .query("SELECT COUNT(*) FROM items", ())
2189            .expect("count recovered rows")
2190            .collect_vec()
2191            .expect("collect count row");
2192        let count = rows
2193            .first()
2194            .expect("count row")
2195            .get::<i64>(0)
2196            .expect("count value");
2197        assert_eq!(count, 24);
2198        reopened.close().expect("close reopened database");
2199    }
2200
2201    #[test]
2202    fn compacted_tier_and_row_precedence_survive_reopen() {
2203        fn data_artifact_count(root: &std::path::Path) -> usize {
2204            let mut count = 0;
2205            let mut pending = vec![root.to_path_buf()];
2206            while let Some(directory) = pending.pop() {
2207                for entry in std::fs::read_dir(directory).expect("read artifact directory") {
2208                    let entry = entry.expect("read artifact member");
2209                    let path = entry.path();
2210                    if entry.file_type().expect("read artifact type").is_dir() {
2211                        pending.push(path);
2212                    } else if path.extension().and_then(|value| value.to_str()) == Some("data") {
2213                        count += 1;
2214                    }
2215                }
2216            }
2217            count
2218        }
2219
2220        let directory = tempfile::tempdir().expect("create database directory");
2221        let database_path = directory.path().join("canonical-compaction");
2222        let dsn = format!(
2223            "file://{}?checkpoint_on_close=false&checkpoint_interval=0&compact_threshold=2&target_volume_rows=65536",
2224            database_path.display()
2225        );
2226        let database = Database::open(&dsn).expect("open persistent database");
2227        database
2228            .execute(
2229                "CREATE TABLE items (id INTEGER PRIMARY KEY, payload TEXT NOT NULL)",
2230                (),
2231            )
2232            .expect("create table");
2233        database
2234            .execute("INSERT INTO items VALUES (1, 'old'), (2, 'two')", ())
2235            .expect("insert first generation");
2236        radixdb_storage::traits::Engine::force_checkpoint_cycle(database.engine().as_ref())
2237            .expect("publish first L0 generation");
2238        database
2239            .execute("UPDATE items SET payload = 'new' WHERE id = 1", ())
2240            .expect("update overlapping row");
2241        database
2242            .execute("INSERT INTO items VALUES (3, 'three')", ())
2243            .expect("insert second generation");
2244        radixdb_storage::traits::Engine::force_checkpoint_cycle(database.engine().as_ref())
2245            .expect("publish second L0 generation");
2246
2247        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
2248        loop {
2249            let stats = database.engine().runtime_stats_snapshot();
2250            if stats.cold_l1_segments == 1
2251                && stats.cold_l0_segments == 0
2252                && stats.cold_unleveled_segments == 0
2253                && !stats.compaction_running
2254            {
2255                break;
2256            }
2257            assert!(
2258                std::time::Instant::now() < deadline,
2259                "canonical compaction did not settle: {stats:?}"
2260            );
2261            std::thread::sleep(std::time::Duration::from_millis(25));
2262        }
2263        let rows = database
2264            .query("SELECT id, payload FROM items ORDER BY id", ())
2265            .expect("query compacted rows")
2266            .collect_vec()
2267            .expect("collect compacted rows");
2268        assert_eq!(rows.len(), 3);
2269        assert_eq!(rows[0].get::<i64>(0).expect("first id"), 1);
2270        assert_eq!(rows[0].get::<String>(1).expect("updated payload"), "new");
2271        database.close().expect("close compacted database");
2272
2273        let artifact_count_before_reopen = data_artifact_count(&database_path);
2274        let reopened = Database::open(&dsn).expect("reopen compacted database");
2275        let recovered_stats = reopened.engine().runtime_stats_snapshot();
2276        assert_eq!(recovered_stats.cold_unleveled_segments, 0);
2277        assert_eq!(recovered_stats.cold_l0_segments, 0);
2278        assert_eq!(recovered_stats.cold_l1_segments, 1);
2279        let rows = reopened
2280            .query("SELECT id, payload FROM items ORDER BY id", ())
2281            .expect("query recovered compacted rows")
2282            .collect_vec()
2283            .expect("collect recovered compacted rows");
2284        assert_eq!(rows.len(), 3);
2285        assert_eq!(rows[0].get::<i64>(0).expect("first id"), 1);
2286        assert_eq!(rows[0].get::<String>(1).expect("recovered payload"), "new");
2287
2288        radixdb_storage::traits::Engine::force_checkpoint_cycle(reopened.engine().as_ref())
2289            .expect("request post-recovery maintenance");
2290        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
2291        loop {
2292            let stats = reopened.engine().runtime_stats_snapshot();
2293            if stats.maintenance.compaction.completed >= 1 && !stats.compaction_running {
2294                assert_eq!(stats.cold_unleveled_segments, 0);
2295                assert_eq!(stats.cold_l0_segments, 0);
2296                assert_eq!(stats.cold_l1_segments, 1);
2297                break;
2298            }
2299            assert!(
2300                std::time::Instant::now() < deadline,
2301                "post-recovery compaction check did not settle: {stats:?}"
2302            );
2303            std::thread::sleep(std::time::Duration::from_millis(25));
2304        }
2305        reopened.close().expect("close reopened database");
2306        assert!(
2307            data_artifact_count(&database_path) <= artifact_count_before_reopen,
2308            "stable L1 recovery must not publish another DATA artifact"
2309        );
2310    }
2311
2312    #[test]
2313    fn named_query_timeout_preserves_named_parameter_binding() {
2314        let database = Database::open_in_memory().expect("open database");
2315        database
2316            .execute("CREATE TABLE named_timeout (id INTEGER PRIMARY KEY)", ())
2317            .expect("create table");
2318        database
2319            .execute("INSERT INTO named_timeout VALUES (7)", ())
2320            .expect("insert row");
2321
2322        let mut rows = database
2323            .query_named_with_timeout(
2324                "SELECT id FROM named_timeout WHERE id = :id",
2325                named_params! { id: 7 },
2326                1_000,
2327            )
2328            .expect("query with named deadline");
2329        assert!(rows.advance());
2330        assert_eq!(rows.current_row().unwrap().get(0), Some(&Value::Integer(7)));
2331        assert!(!rows.advance());
2332        assert!(rows.error().is_none());
2333    }
2334
2335    #[test]
2336    fn r3_l02_batch_b_same_dsn_opens_have_independent_sql_transactions() {
2337        let dsn = "memory://r3-l02-b-independent-opens";
2338        let first = Database::open(dsn).expect("open first connection");
2339        first
2340            .execute(
2341                "CREATE TABLE batch_b_connection (id INTEGER PRIMARY KEY, value INTEGER)",
2342                (),
2343            )
2344            .expect("create table");
2345        let second = Database::open(dsn).expect("open second connection");
2346
2347        first.execute("BEGIN", ()).expect("begin first connection");
2348        first
2349            .execute("INSERT INTO batch_b_connection VALUES (1, 10)", ())
2350            .expect("insert in first connection");
2351        let visible_to_second: i64 = second
2352            .query_one("SELECT COUNT(*) FROM batch_b_connection", ())
2353            .expect("query second connection");
2354        assert_eq!(
2355            visible_to_second, 0,
2356            "a separately opened connection must not inherit the first connection's SQL transaction"
2357        );
2358        first.execute("ROLLBACK", ()).expect("rollback first");
2359    }
2360
2361    #[test]
2362    fn r3_l02_batch_b_engine_get_index_returns_the_existing_public_index() {
2363        use radixdb_storage::Engine;
2364
2365        let database = Database::open_in_memory().expect("open database");
2366        database
2367            .execute(
2368                "CREATE TABLE batch_b_index (id INTEGER PRIMARY KEY, value INTEGER)",
2369                (),
2370            )
2371            .expect("create table");
2372        database
2373            .execute(
2374                "CREATE INDEX idx_batch_b_value ON batch_b_index (value)",
2375                (),
2376            )
2377            .expect("create index");
2378
2379        let index = Engine::get_index(
2380            database.engine().as_ref(),
2381            "batch_b_index",
2382            "idx_batch_b_value",
2383        )
2384        .expect("public Engine::get_index must return an existing index");
2385        assert_eq!(index.name(), "idx_batch_b_value");
2386    }
2387
2388    #[test]
2389    fn r2_l05_c_independent_database_open_does_not_wait_for_another_dsn() {
2390        use std::sync::mpsc;
2391        use std::time::Duration;
2392
2393        let temp = tempfile::tempdir().expect("temp dir");
2394        let slow_dsn = format!("file://{}", temp.path().join("slow").display());
2395        let fast_dsn = format!("file://{}", temp.path().join("fast").display());
2396        let (entered_tx, entered_rx) = mpsc::channel();
2397        let release = Arc::new((Mutex::new(false), std::sync::Condvar::new()));
2398        let release_for_hook = Arc::clone(&release);
2399        let slow_for_hook = slow_dsn.clone();
2400        let open_hook = DatabaseOpenTestHookGuard::install(Arc::new(move |dsn| {
2401            if dsn != slow_for_hook {
2402                return;
2403            }
2404            entered_tx.send(()).expect("report slow open admission");
2405            let (lock, changed) = &*release_for_hook;
2406            let mut released = lock.lock().expect("release lock");
2407            while !*released {
2408                released = changed.wait(released).expect("release wait");
2409            }
2410        }));
2411
2412        let slow_waiter_dsn = slow_dsn.clone();
2413        let slow = std::thread::spawn(move || Database::open(&slow_dsn));
2414        entered_rx
2415            .recv_timeout(Duration::from_secs(2))
2416            .expect("slow open reached deterministic recovery hook");
2417
2418        let same_dsn_waiter = std::thread::spawn(move || Database::open(&slow_waiter_dsn));
2419
2420        let (fast_tx, fast_rx) = mpsc::channel();
2421        let fast = std::thread::spawn(move || {
2422            fast_tx
2423                .send(Database::open(&fast_dsn))
2424                .expect("report independent open result");
2425        });
2426        let independent_completed = fast_rx.recv_timeout(Duration::from_millis(250));
2427        let completed_before_release = independent_completed.is_ok();
2428
2429        {
2430            let (lock, changed) = &*release;
2431            *lock.lock().expect("release lock") = true;
2432            changed.notify_all();
2433        }
2434        let slow_database = slow
2435            .join()
2436            .expect("slow open thread")
2437            .expect("slow database opens");
2438        let same_dsn_database = same_dsn_waiter
2439            .join()
2440            .expect("same DSN waiter thread")
2441            .expect("same DSN waiter receives open outcome");
2442        let fast_database = match independent_completed {
2443            Ok(result) => result.expect("independent database opens"),
2444            Err(_) => fast_rx
2445                .recv_timeout(Duration::from_secs(2))
2446                .expect("independent open eventually reports")
2447                .expect("independent database opens after release"),
2448        };
2449        fast.join().expect("independent open thread");
2450        drop(open_hook);
2451
2452        assert!(Arc::ptr_eq(
2453            slow_database.owner_arc(),
2454            same_dsn_database.owner_arc()
2455        ));
2456        drop(same_dsn_database);
2457        slow_database.close().expect("close slow database");
2458        fast_database.close().expect("close fast database");
2459        assert!(
2460            completed_before_release,
2461            "opening one DSN held the process-wide registry lock across recovery"
2462        );
2463
2464        let blocked_parent = temp.path().join("not-a-directory");
2465        std::fs::write(&blocked_parent, b"registry failure oracle")
2466            .expect("create non-directory parent");
2467        let failed_dsn = format!("file://{}", blocked_parent.join("database").display());
2468        let failed_retry_dsn = failed_dsn.clone();
2469        let failed_registry_key =
2470            Database::canonical_registry_key(&failed_dsn).expect("canonical failed-open key");
2471        let failed_waiter_dsn = failed_dsn.clone();
2472        let (failed_entered_tx, failed_entered_rx) = mpsc::channel();
2473        let failed_release = Arc::new((Mutex::new(false), std::sync::Condvar::new()));
2474        let failed_release_for_hook = Arc::clone(&failed_release);
2475        let failed_for_hook = failed_dsn.clone();
2476        let failure_hook = DatabaseOpenTestHookGuard::install(Arc::new(move |dsn| {
2477            if dsn != failed_for_hook {
2478                return;
2479            }
2480            failed_entered_tx
2481                .send(())
2482                .expect("report failed open admission");
2483            let (lock, changed) = &*failed_release_for_hook;
2484            let mut released = lock.lock().expect("failure release lock");
2485            while !*released {
2486                released = changed.wait(released).expect("failure release wait");
2487            }
2488        }));
2489        let failed_owner = std::thread::spawn(move || Database::open(&failed_dsn));
2490        failed_entered_rx
2491            .recv_timeout(Duration::from_secs(2))
2492            .expect("failed open reached hook");
2493        let failed_waiter = std::thread::spawn(move || Database::open(&failed_waiter_dsn));
2494        let waiter_deadline = std::time::Instant::now() + Duration::from_secs(2);
2495        loop {
2496            let waiter_joined = DATABASE_REGISTRY
2497                .read()
2498                .expect("registry read")
2499                .get(&failed_registry_key)
2500                .is_some_and(|entry| {
2501                    matches!(
2502                        entry,
2503                        DatabaseRegistryEntry::Opening(slot) if Arc::strong_count(slot) >= 3
2504                    )
2505                });
2506            if waiter_joined {
2507                break;
2508            }
2509            assert!(
2510                std::time::Instant::now() < waiter_deadline,
2511                "same DSN waiter did not join opening slot"
2512            );
2513            std::thread::yield_now();
2514        }
2515        {
2516            let (lock, changed) = &*failed_release;
2517            *lock.lock().expect("failure release lock") = true;
2518            changed.notify_all();
2519        }
2520        let owner_error = match failed_owner.join().expect("failed owner thread") {
2521            Ok(_) => panic!("owner open through a non-directory parent must fail"),
2522            Err(error) => error,
2523        };
2524        let waiter_error = match failed_waiter.join().expect("failed waiter thread") {
2525            Ok(_) => panic!("waiter open through a non-directory parent must fail"),
2526            Err(error) => error,
2527        };
2528        drop(failure_hook);
2529        assert_eq!(owner_error.to_string(), waiter_error.to_string());
2530        assert!(Database::open(&failed_retry_dsn).is_err());
2531    }
2532
2533    #[test]
2534    fn r2_l05_a_database_default_is_connection_local_and_explicit_isolation_wins() {
2535        let db = Database::open_in_memory().expect("open database");
2536        db.set_default_isolation_level(IsolationLevel::SnapshotIsolation)
2537            .expect("set default isolation");
2538
2539        let default_tx = db.begin().expect("begin with database default");
2540        assert_eq!(
2541            db.engine().registry().get_isolation_level(default_tx.id()),
2542            IsolationLevel::SnapshotIsolation
2543        );
2544
2545        let explicit_tx = db
2546            .begin_with_isolation(IsolationLevel::ReadCommitted)
2547            .expect("begin explicit read committed");
2548        assert_eq!(
2549            db.engine().registry().get_isolation_level(explicit_tx.id()),
2550            IsolationLevel::ReadCommitted
2551        );
2552
2553        let clone = db.clone();
2554        let clone_default = clone.begin().expect("clone uses its local default");
2555        assert_eq!(
2556            db.engine()
2557                .registry()
2558                .get_isolation_level(clone_default.id()),
2559            IsolationLevel::ReadCommitted
2560        );
2561    }
2562
2563    #[test]
2564    fn r2_l05_b_failed_close_is_retained_until_retry_and_fresh_open() {
2565        let _failpoint_guard = radixdb_storage::test_failpoints::FailpointGuard::new();
2566        radixdb_storage::test_failpoints::reset_all();
2567        let temp = tempfile::tempdir().expect("temp dir");
2568        let dsn = format!("file://{}", temp.path().join("close-retry").display());
2569        let db = Database::open(&dsn).expect("open database");
2570        db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)", ())
2571            .expect("create table");
2572        db.execute("INSERT INTO t VALUES (1)", ())
2573            .expect("insert durable row");
2574
2575        radixdb_storage::test_failpoints::WAL_SYNC_FAIL
2576            .store(true, std::sync::atomic::Ordering::Release);
2577        let close_error = db.close().expect_err("WAL failure must fail close");
2578        let reopen_error = match Database::open(&dsn) {
2579            Ok(_) => panic!("close-failed registry owner must not reopen as ready"),
2580            Err(error) => error,
2581        };
2582        assert_eq!(close_error.to_string(), reopen_error.to_string());
2583
2584        radixdb_storage::test_failpoints::WAL_SYNC_FAIL
2585            .store(false, std::sync::atomic::Ordering::Release);
2586        db.close().expect("retry close succeeds");
2587        let reopened = Database::open(&dsn).expect("fresh engine opens after terminal close");
2588        assert!(!Arc::ptr_eq(db.owner_arc(), reopened.owner_arc()));
2589        let rows = reopened
2590            .query("SELECT id FROM t", ())
2591            .expect("query recovered row")
2592            .collect_vec()
2593            .expect("collect recovered row");
2594        assert_eq!(rows.len(), 1);
2595        assert_eq!(rows[0].get::<i64>(0).expect("recovered id"), 1);
2596        reopened.close().expect("close fresh engine");
2597        radixdb_storage::test_failpoints::reset_all();
2598    }
2599
2600    #[test]
2601    fn test_open_memory() {
2602        let db = Database::open("memory://").unwrap();
2603        assert_eq!(db.dsn(), "memory://");
2604    }
2605
2606    #[test]
2607    fn test_open_in_memory() {
2608        let db = Database::open_in_memory().unwrap();
2609        db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)", ())
2610            .unwrap();
2611        db.execute("INSERT INTO test VALUES ($1)", (1,)).unwrap();
2612
2613        for row in db.query("SELECT * FROM test", ()).unwrap() {
2614            let row = row.unwrap();
2615            let id: i64 = row.get(0).unwrap();
2616            assert_eq!(id, 1);
2617        }
2618    }
2619
2620    #[test]
2621    fn test_execute_and_query_new_api() {
2622        let db = Database::open_in_memory().unwrap();
2623
2624        // Create table - no params
2625        db.execute(
2626            "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)",
2627            (),
2628        )
2629        .unwrap();
2630
2631        // Insert with tuple params
2632        let affected = db
2633            .execute(
2634                "INSERT INTO users VALUES ($1, $2, $3), ($4, $5, $6)",
2635                (1, "Alice", 30, 2, "Bob", 25),
2636            )
2637            .unwrap();
2638        assert_eq!(affected, 2);
2639
2640        // Query with tuple params
2641        let rows: Vec<_> = db
2642            .query("SELECT * FROM users ORDER BY id", ())
2643            .unwrap()
2644            .collect::<std::result::Result<Vec<_>, _>>()
2645            .unwrap();
2646
2647        assert_eq!(rows.len(), 2);
2648        assert_eq!(rows[0].get::<i64>(0).unwrap(), 1);
2649        assert_eq!(rows[0].get::<String>(1).unwrap(), "Alice");
2650        assert_eq!(rows[0].get::<i64>(2).unwrap(), 30);
2651    }
2652
2653    #[test]
2654    fn test_query_one() {
2655        let db = Database::open_in_memory().unwrap();
2656        db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)", ())
2657            .unwrap();
2658        db.execute("INSERT INTO test VALUES ($1), ($2), ($3)", (1, 2, 3))
2659            .unwrap();
2660
2661        let count: i64 = db.query_one("SELECT COUNT(*) FROM test", ()).unwrap();
2662        assert_eq!(count, 3);
2663    }
2664
2665    #[test]
2666    fn test_query_opt() {
2667        let db = Database::open_in_memory().unwrap();
2668        db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)", ())
2669            .unwrap();
2670        db.execute("INSERT INTO test VALUES ($1)", (1,)).unwrap();
2671
2672        // Found
2673        let result: Option<i64> = db
2674            .query_opt("SELECT id FROM test WHERE id = $1", (1,))
2675            .unwrap();
2676        assert_eq!(result, Some(1));
2677
2678        // Not found
2679        let result: Option<i64> = db
2680            .query_opt("SELECT id FROM test WHERE id = $1", (999,))
2681            .unwrap();
2682        assert_eq!(result, None);
2683    }
2684
2685    #[test]
2686    fn test_params_macro() {
2687        let db = Database::open_in_memory().unwrap();
2688        db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", ())
2689            .unwrap();
2690
2691        // Use params! macro
2692        db.execute(
2693            "INSERT INTO users VALUES ($1, $2)",
2694            crate::params![1, "Alice"],
2695        )
2696        .unwrap();
2697
2698        let names: Vec<String> = db
2699            .query("SELECT name FROM users WHERE id = $1", crate::params![1])
2700            .unwrap()
2701            .map(|r| r.and_then(|row| row.get(0)))
2702            .collect::<std::result::Result<Vec<_>, _>>()
2703            .unwrap();
2704
2705        assert_eq!(names, vec!["Alice"]);
2706    }
2707
2708    #[test]
2709    fn test_parse_dsn() {
2710        // Memory
2711        let (scheme, path) = Database::parse_dsn("memory://").unwrap();
2712        assert_eq!(scheme, "memory");
2713        assert_eq!(path, "");
2714
2715        // File
2716        let (scheme, path) = Database::parse_dsn("file:///tmp/test.db").unwrap();
2717        assert_eq!(scheme, "file");
2718        assert_eq!(path, "/tmp/test.db");
2719
2720        // File with params
2721        let (scheme, path) = Database::parse_dsn("file:///tmp/test.db?sync=full").unwrap();
2722        assert_eq!(scheme, "file");
2723        assert_eq!(path, "/tmp/test.db?sync=full");
2724
2725        // Invalid
2726        assert!(Database::parse_dsn("invalid").is_err());
2727        assert!(Database::parse_dsn("unknown://test").is_err());
2728    }
2729
2730    #[test]
2731    fn test_parse_file_config_seal_hot_bytes() {
2732        let (_path, config) = Database::parse_file_config(
2733            "/tmp/test.db?copy_max_transaction_bytes=16384&max_compaction_jobs=4&storage_cpu_workers=3&page_cache_level=5&page_cache_max_bytes=1073741824&page_cache_memory_reserve=536870912&max_compaction_input_segments=6&max_compaction_input_bytes=33554432&max_compaction_output_bytes=50331648&compaction_job_time_budget_ms=30000&compaction_io_bytes_per_sec=8388608&compaction_disk_reserve_bytes=16777216&compaction_retry_cooldown_ms=5000&l0_soft_limit_segments=10&l0_hard_limit_segments=20&l0_soft_limit_bytes=67108864&l0_hard_limit_bytes=134217728&l0_soft_backpressure_wait_ms=25&seal_hot_bytes_threshold=1024&seal_incremental_hot_bytes_threshold=512&volume_cache_bytes=2048&read_queue_depth=8",
2734        )
2735        .unwrap();
2736
2737        assert_eq!(config.persistence.copy_max_transaction_bytes, 16_384);
2738        assert_eq!(config.persistence.max_compaction_jobs, 4);
2739        assert_eq!(config.persistence.storage_cpu_workers, 3);
2740        assert_eq!(config.persistence.page_cache_level, 5);
2741        assert_eq!(config.persistence.page_cache_max_bytes, 1_073_741_824);
2742        assert_eq!(config.persistence.page_cache_memory_reserve, 536_870_912);
2743        assert_eq!(config.persistence.max_compaction_input_segments, 6);
2744        assert_eq!(config.persistence.max_compaction_input_bytes, 33_554_432);
2745        assert_eq!(config.persistence.max_compaction_output_bytes, 50_331_648);
2746        assert_eq!(config.persistence.compaction_job_time_budget_ms, 30_000);
2747        assert_eq!(config.persistence.compaction_io_bytes_per_sec, 8_388_608);
2748        assert_eq!(config.persistence.compaction_disk_reserve_bytes, 16_777_216);
2749        assert_eq!(config.persistence.compaction_retry_cooldown_ms, 5_000);
2750        assert_eq!(config.persistence.l0_soft_limit_segments, 10);
2751        assert_eq!(config.persistence.l0_hard_limit_segments, 20);
2752        assert_eq!(config.persistence.l0_soft_limit_bytes, 67_108_864);
2753        assert_eq!(config.persistence.l0_hard_limit_bytes, 134_217_728);
2754        assert_eq!(config.persistence.l0_soft_backpressure_wait_ms, 25);
2755        assert_eq!(config.persistence.seal_hot_bytes_threshold, 1024);
2756        assert_eq!(config.persistence.seal_incremental_hot_bytes_threshold, 512);
2757        assert_eq!(config.persistence.volume_cache_bytes, 2048);
2758        assert_eq!(config.persistence.read_queue_depth, 8);
2759
2760        let error = Database::parse_file_config(
2761            "/tmp/test.db?l0_soft_limit_segments=8&l0_hard_limit_segments=8",
2762        )
2763        .expect_err("unordered L0 segment limits must fail closed");
2764        assert!(error.to_string().contains("0 < soft < hard"));
2765
2766        let error = Database::parse_file_config("/tmp/test.db?max_compaction_jobs=0")
2767            .expect_err("zero compaction workers must fail closed");
2768        assert!(error.to_string().contains("max_compaction_jobs must be in"));
2769
2770        let error = Database::parse_file_config("/tmp/test.db?page_cache_level=11")
2771            .expect_err("page cache levels above ten must fail closed");
2772        assert!(error.to_string().contains("page_cache_level must be in"));
2773    }
2774
2775    #[test]
2776    fn file_config_rejects_retired_aliases_and_duplicate_canonical_options() {
2777        for alias in [
2778            "sync=full",
2779            "snapshot_interval=60",
2780            "sync_interval=10",
2781            "snapshot_compression=on",
2782            "seal_hot_bytes=1024",
2783            "seal_incremental_hot_bytes=512",
2784            "volume_cache=2048",
2785            "compression_threshold=64",
2786            "scan_prefetch_cache_bytes=4096",
2787            "block_cache_bytes=8192",
2788        ] {
2789            let error = Database::parse_file_config(&format!("/tmp/radix?{alias}"))
2790                .expect_err("retired DSN alias must fail closed");
2791            assert!(error.to_string().contains("unknown file database option"));
2792        }
2793
2794        let error = Database::parse_file_config("/tmp/radix?sync_mode=full&sync_mode=none")
2795            .expect_err("duplicate canonical option must not be order-dependent");
2796        assert!(error.to_string().contains("duplicate file database option"));
2797    }
2798
2799    #[test]
2800    fn r2_l06_batch_a_rejects_obsolete_commit_batch_size() {
2801        let error = Database::parse_file_config("/tmp/radix?commit_batch_size=2")
2802            .expect_err("r2_l06_batch_a: removed durability knob must not be accepted");
2803        assert!(error.to_string().contains("commit_batch_size"));
2804    }
2805
2806    #[test]
2807    fn r2_l06_batch_b_restore_never_commits_before_fallible_cache_admission() {
2808        let dir = tempfile::tempdir().unwrap();
2809        let dsn = format!("file://{}", dir.path().join("restore_cache").display());
2810        let db = Database::open(&dsn).unwrap();
2811        db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)", ())
2812            .unwrap();
2813        db.create_snapshot().unwrap();
2814        db.execute("INSERT INTO t VALUES (1)", ()).unwrap();
2815
2816        let poisoned = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2817            let _executor = db.inner.executor.lock().unwrap();
2818            panic!("poison executor before restore");
2819        }));
2820        assert!(poisoned.is_err());
2821        assert!(db.restore_snapshot(None).is_err());
2822        db.close().unwrap();
2823        drop(db);
2824
2825        let reopened = Database::open(&dsn).unwrap();
2826        let count: i64 = reopened.query_one("SELECT COUNT(*) FROM t", ()).unwrap();
2827        assert_eq!(
2828            count, 1,
2829            "restore must not replace durable state before cache admission can succeed"
2830        );
2831        reopened.close().unwrap();
2832    }
2833
2834    #[test]
2835    fn test_from_value_types() {
2836        assert_eq!(i64::from_value(&Value::Integer(42)).unwrap(), 42);
2837        assert_eq!(f64::from_value(&Value::Float(3.5)).unwrap(), 3.5);
2838        assert_eq!(
2839            f64::from_value(&Value::decimal(12_345, 5, 2)).unwrap(),
2840            123.45
2841        );
2842        assert_eq!(
2843            String::from_value(&Value::Text("hello".into())).unwrap(),
2844            "hello"
2845        );
2846        assert!(bool::from_value(&Value::Boolean(true)).unwrap());
2847
2848        // Optional
2849        assert_eq!(
2850            Option::<i64>::from_value(&Value::Integer(42)).unwrap(),
2851            Some(42)
2852        );
2853        assert_eq!(
2854            Option::<i64>::from_value(&Value::null_unknown()).unwrap(),
2855            None
2856        );
2857    }
2858
2859    #[test]
2860    fn test_cached_plan_insert_and_query() {
2861        let db = Database::open_in_memory().unwrap();
2862        db.execute(
2863            "CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, score FLOAT)",
2864            (),
2865        )
2866        .unwrap();
2867
2868        let insert_plan = db
2869            .cached_plan("INSERT INTO test VALUES ($1, $2, $3)")
2870            .unwrap();
2871
2872        // Batch insert using cached plan
2873        db.execute_plan(&insert_plan, (1, "Alice", 95.5)).unwrap();
2874        db.execute_plan(&insert_plan, (2, "Bob", 82.0)).unwrap();
2875        db.execute_plan(&insert_plan, (3, "Charlie", 91.0)).unwrap();
2876
2877        // Query using cached plan
2878        let query_plan = db
2879            .cached_plan("SELECT name FROM test WHERE id = $1")
2880            .unwrap();
2881        let mut rows = db.query_plan(&query_plan, (2,)).unwrap();
2882        let row = rows.next().unwrap().unwrap();
2883        assert_eq!(row.get::<String>(0).unwrap(), "Bob");
2884    }
2885
2886    #[test]
2887    fn test_cached_plan_reuse() {
2888        let db = Database::open_in_memory().unwrap();
2889        db.execute(
2890            "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)",
2891            (),
2892        )
2893        .unwrap();
2894
2895        // Get the same plan twice — second call should hit the cache
2896        let plan1 = db.cached_plan("INSERT INTO test VALUES ($1, $2)").unwrap();
2897        let plan2 = db.cached_plan("INSERT INTO test VALUES ($1, $2)").unwrap();
2898
2899        // Both should work independently
2900        db.execute_plan(&plan1, (1, 100)).unwrap();
2901        db.execute_plan(&plan2, (2, 200)).unwrap();
2902
2903        let count: i64 = db.query_one("SELECT COUNT(*) FROM test", ()).unwrap();
2904        assert_eq!(count, 2);
2905    }
2906
2907    #[test]
2908    fn test_cached_plan_update_delete() {
2909        let db = Database::open_in_memory().unwrap();
2910        db.execute(
2911            "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)",
2912            (),
2913        )
2914        .unwrap();
2915        db.execute("INSERT INTO test VALUES (1, 100)", ()).unwrap();
2916        db.execute("INSERT INTO test VALUES (2, 200)", ()).unwrap();
2917
2918        // Update via cached plan
2919        let update_plan = db
2920            .cached_plan("UPDATE test SET value = $1 WHERE id = $2")
2921            .unwrap();
2922        let affected = db.execute_plan(&update_plan, (999, 1)).unwrap();
2923        assert_eq!(affected, 1);
2924
2925        let val: i64 = db
2926            .query_one("SELECT value FROM test WHERE id = 1", ())
2927            .unwrap();
2928        assert_eq!(val, 999);
2929
2930        // Delete via cached plan
2931        let delete_plan = db.cached_plan("DELETE FROM test WHERE id = $1").unwrap();
2932        let affected = db.execute_plan(&delete_plan, (2,)).unwrap();
2933        assert_eq!(affected, 1);
2934
2935        let count: i64 = db.query_one("SELECT COUNT(*) FROM test", ()).unwrap();
2936        assert_eq!(count, 1);
2937    }
2938
2939    #[test]
2940    fn test_cached_plan_no_params() {
2941        let db = Database::open_in_memory().unwrap();
2942        db.execute(
2943            "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)",
2944            (),
2945        )
2946        .unwrap();
2947        db.execute("INSERT INTO test VALUES (1, 10)", ()).unwrap();
2948        db.execute("INSERT INTO test VALUES (2, 20)", ()).unwrap();
2949
2950        let plan = db.cached_plan("SELECT COUNT(*) FROM test").unwrap();
2951        let mut rows = db.query_plan(&plan, ()).unwrap();
2952        let row = rows.next().unwrap().unwrap();
2953        assert_eq!(row.get::<i64>(0).unwrap(), 2);
2954    }
2955
2956    #[test]
2957    fn test_cached_plan_named_params() {
2958        let db = Database::open_in_memory().unwrap();
2959        db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)", ())
2960            .unwrap();
2961
2962        let plan = db
2963            .cached_plan("INSERT INTO test VALUES (:id, :name)")
2964            .unwrap();
2965        db.execute_named_plan(&plan, named_params! { id: 1, name: "Alice" })
2966            .unwrap();
2967        db.execute_named_plan(&plan, named_params! { id: 2, name: "Bob" })
2968            .unwrap();
2969
2970        let query_plan = db
2971            .cached_plan("SELECT name FROM test WHERE id = :id")
2972            .unwrap();
2973        let mut rows = db
2974            .query_named_plan(&query_plan, named_params! { id: 1 })
2975            .unwrap();
2976        let row = rows.next().unwrap().unwrap();
2977        assert_eq!(row.get::<String>(0).unwrap(), "Alice");
2978    }
2979
2980    #[test]
2981    fn test_cached_plan_multi_statement_error() {
2982        let db = Database::open_in_memory().unwrap();
2983        db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)", ())
2984            .unwrap();
2985
2986        // Multiple statements should fail
2987        let result = db.cached_plan("INSERT INTO test VALUES (1); INSERT INTO test VALUES (2)");
2988        assert!(result.is_err());
2989    }
2990}