1use 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
121pub 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
186static 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
193struct 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 #[cfg(feature = "test-filedb")]
205 _temp_dir: Option<tempfile::TempDir>,
206}
207
208pub(crate) struct DatabaseInner {
211 engine: Arc<MVCCEngine>,
212 executor: Mutex<Executor>,
213 owner: Arc<DatabaseOwner>,
214}
215
216pub(crate) type DatabaseInnerHandle = DatabaseInner;
218
219impl DatabaseInner {
220 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 Database::try_unregister_owner(&self.owner);
248 }
249}
250
251pub struct Database {
282 inner: Arc<DatabaseInner>,
283}
284
285impl Database {
286 #[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 #[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 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 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 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 }
456}
457
458impl Database {
459 pub fn open(dsn: &str) -> Result<Self> {
461 Self::open_with_plugin_registry(dsn, Arc::clone(&DEFAULT_PLUGIN_REGISTRY))
462 }
463
464 #[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, ®istry_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(®istry_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(®istry_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(®istry_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(®istry_key),
627 Some(DatabaseRegistryEntry::Opening(slot))
628 if Arc::ptr_eq(slot, &opening_slot)
629 );
630 if owns_slot {
631 registry.remove(®istry_key);
632 }
633 }
634 opening_slot.finish(Err(error.clone()));
635 Err(error)
636 }
637 }
638 }
639
640 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 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 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 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 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 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" => {
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" => {
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 "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 "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 "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 "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 "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" => {
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" => {
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" => {
991 config.persistence.wal_max_size = value.parse::<usize>().map_err(|_| {
992 Error::invalid_argument(format!("invalid wal_max_size: '{}'", value))
993 })?;
994 }
995 "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 "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_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" => {
1031 config.persistence.wal_compression =
1032 parse_bool_option("wal_compression", value)?;
1033 }
1034 "volume_compression" => {
1036 config.persistence.volume_compression =
1037 parse_bool_option("volume_compression", value)?;
1038 }
1039 "compression" => {
1041 let enabled = parse_bool_option("compression", value)?;
1042 config.persistence.wal_compression = enabled;
1043 config.persistence.volume_compression = enabled;
1044 }
1045 "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 "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 "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 "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 "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" => {
1101 config.persistence.checkpoint_on_close =
1102 parse_bool_option("checkpoint_on_close", value)?;
1103 }
1104 "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" => {
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" => {
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 "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 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 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 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 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 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 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 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 pub(crate) fn from_inner(inner: Arc<DatabaseInner>) -> Self {
1382 Database { inner }
1383 }
1384
1385 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 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 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 #[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 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 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 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 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 #[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 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 pub fn engine(&self) -> &Arc<MVCCEngine> {
1662 &self.inner.engine
1663 }
1664
1665 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 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 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 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 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 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 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 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 pub fn dsn(&self) -> &str {
1785 &self.inner.owner.dsn
1786 }
1787
1788 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 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 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 pub fn restore_snapshot(&self, snapshot_id: Option<&str>) -> Result<String> {
1833 use radixdb_storage::Engine;
1834 self.ensure_open()?;
1835 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 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 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 #[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 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 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 db.execute(
2626 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)",
2627 (),
2628 )
2629 .unwrap();
2630
2631 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 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 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 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 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 let (scheme, path) = Database::parse_dsn("memory://").unwrap();
2712 assert_eq!(scheme, "memory");
2713 assert_eq!(path, "");
2714
2715 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 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 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 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 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 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 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 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 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 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 let result = db.cached_plan("INSERT INTO test VALUES (1); INSERT INTO test VALUES (2)");
2988 assert!(result.is_err());
2989 }
2990}