1use akar_catalog::Catalog;
4use akar_common::file_system::VirtualFileSystemRegistry;
5use akar_common::memory::MemoryManager;
6use akar_common::task_system::TaskSystem;
7use akar_extension::{ExtensionContext, ExtensionRegistry};
8use akar_function::FunctionRegistry;
9use akar_storage::StorageManager;
10use akar_storage::stats::StatsStore;
11use akar_storage::table::ColumnDefinition;
12use akar_transaction::TransactionManager;
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
16use std::sync::{Arc, Mutex, OnceLock};
17
18pub const CATALOG_FILE_NAME: &str = "catalog.json";
23
24pub const LOCK_FILE_NAME: &str = "akar.lock";
30
31#[derive(Debug, Clone)]
33pub struct SystemConfig {
34 pub buffer_pool_size: u64,
35 pub max_num_threads: u64,
36 pub enable_compression: bool,
37 pub read_only: bool,
38 pub max_db_size: u64,
39 pub auto_checkpoint: bool,
40 pub checkpoint_threshold: i64,
41 pub concurrent_writes: bool,
44 pub spill_threshold: u64,
54}
55
56impl Default for SystemConfig {
57 fn default() -> Self {
58 Self {
59 buffer_pool_size: 0,
60 max_num_threads: 0,
61 enable_compression: true,
62 read_only: false,
63 max_db_size: u64::from(u32::MAX),
64 auto_checkpoint: true,
65 checkpoint_threshold: -1,
66 concurrent_writes: true,
67 spill_threshold: 0,
69 }
70 }
71}
72
73static PROCESS_PATH_LOCKS: OnceLock<Mutex<HashMap<PathBuf, (std::fs::File, u32)>>> = OnceLock::new();
100
101fn process_path_locks() -> &'static Mutex<HashMap<PathBuf, (std::fs::File, u32)>> {
102 PROCESS_PATH_LOCKS.get_or_init(|| Mutex::new(HashMap::new()))
103}
104
105struct PathLock {
109 key: PathBuf,
110}
111
112impl Drop for PathLock {
113 fn drop(&mut self) {
114 let mut reg = process_path_locks().lock().unwrap();
115 if let Some((_, count)) = reg.get_mut(&self.key) {
116 *count -= 1;
117 if *count == 0 {
118 reg.remove(&self.key);
119 }
120 }
121 }
122}
123
124fn lock_key(db_path: &Path) -> PathBuf {
127 std::fs::canonicalize(db_path)
128 .unwrap_or_else(|_| db_path.to_path_buf())
129 .join(LOCK_FILE_NAME)
130}
131
132#[allow(dead_code)]
133pub struct Database {
134 pub(crate) storage_manager: Arc<StorageManager>,
135 pub(crate) catalog: Arc<Mutex<Catalog>>,
136 pub(crate) transaction_manager: Arc<TransactionManager>,
137 pub(crate) function_registry: Arc<Mutex<FunctionRegistry>>,
138 pub(crate) task_system: Arc<TaskSystem>,
139 pub(crate) memory_manager: Arc<MemoryManager>,
140 pub(crate) extension_registry: Mutex<ExtensionRegistry>,
141 pub(crate) stats_store: Arc<Mutex<StatsStore>>,
142 pub(crate) vfs: Arc<VirtualFileSystemRegistry>,
143 pub(crate) config: SystemConfig,
145 spill_threshold_override: AtomicU64,
149 spill_threshold_overridden: AtomicBool,
150 _lock: Option<PathLock>,
153}
154
155impl Database {
156 pub fn set_spill_threshold(&self, bytes: u64) {
159 self.spill_threshold_override.store(bytes, Ordering::Relaxed);
160 self.spill_threshold_overridden.store(true, Ordering::Relaxed);
161 self.storage_manager.set_spiller(self.spiller());
164 }
165
166 pub fn effective_spill_threshold(&self) -> u64 {
174 if self.spill_threshold_overridden.load(Ordering::Relaxed) {
175 return self.spill_threshold_override.load(Ordering::Relaxed);
176 }
177 if self.config.spill_threshold > 0 {
178 return self.config.spill_threshold;
179 }
180 if self.config.buffer_pool_size > 0 {
181 return (self.config.buffer_pool_size as f64 * 0.8) as u64;
182 }
183 0
184 }
185
186 pub fn spiller(&self) -> Option<Arc<akar_storage::Spiller>> {
190 let threshold = self.effective_spill_threshold();
191 if threshold == 0 {
192 return None;
193 }
194 let spill_dir = self.storage_manager.db_path().join("spill");
195 Some(Arc::new(akar_storage::Spiller::new(spill_dir, threshold)))
196 }
197
198 pub fn storage_driver(&self) -> crate::storage_driver::StorageDriver {
201 crate::storage_driver::StorageDriver::new(self.storage_manager.clone(), self.catalog.clone(), self.vfs.clone())
202 }
203
204 pub fn catalog(&self) -> Arc<Mutex<Catalog>> {
206 self.catalog.clone()
207 }
208
209 pub fn table_catalog(&self) -> Arc<akar_storage::TableCatalog> {
214 self.storage_manager.table_catalog()
215 }
216
217 pub fn create_node_table(&self, name: String, columns: Vec<akar_catalog::CatalogColumn>) -> Result<u64, String> {
229 let storage_columns: Vec<ColumnDefinition> = columns
231 .iter()
232 .map(|c| ColumnDefinition {
233 name: c.name.clone(),
234 logical_type: c.logical_type,
235 is_primary_key: c.is_primary_key,
236 compression: c.compression,
237 })
238 .collect();
239 let node_table = self.storage_manager.create_node_table(name.clone(), storage_columns);
240 let table_id = node_table.table_id;
241
242 {
244 let mut cat = self.catalog.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
245 for col in &columns {
246 if col.logical_type == akar_common::types::LogicalTypeID::Serial {
247 if let akar_catalog::CatalogResult::Created { .. } = cat.create_serial_sequence(&name, &col.name) {
248 tracing::info!("Created serial sequence for {name}.{}", col.name);
249 }
250 }
251 }
252 }
253
254 if columns.iter().any(|c| c.is_primary_key) {
256 let index_name = format!("{name}_pk_idx");
257 self.storage_manager
258 .create_art_index(&name, &index_name)
259 .map_err(|e| format!("Failed to create ART PK index for table '{name}': {e}"))?;
260 }
261
262 tracing::info!("Created node table '{name}'");
263 Ok(table_id)
264 }
265
266 pub fn create_rel_table(
271 &self,
272 name: String,
273 src_table_id: u64,
274 dst_table_id: u64,
275 columns: Vec<akar_catalog::CatalogColumn>,
276 ) -> Result<u64, String> {
277 let storage_columns: Vec<ColumnDefinition> = columns
279 .iter()
280 .map(|c| ColumnDefinition {
281 name: c.name.clone(),
282 logical_type: c.logical_type,
283 is_primary_key: c.is_primary_key,
284 compression: c.compression,
285 })
286 .collect();
287 let rel_table =
288 self.storage_manager
289 .create_rel_table(name.clone(), src_table_id, dst_table_id, storage_columns);
290 let table_id = rel_table.table_id;
291
292 tracing::info!("Created rel table '{name}'");
293 Ok(table_id)
294 }
295
296 pub fn drop_table(&self, name: &str) -> Result<(), String> {
298 {
304 let mut cat = self.catalog.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
305 let node_cols: Vec<String> = cat
306 .node_tables()
307 .into_iter()
308 .filter(|t| t.name == name)
309 .flat_map(|t| t.columns.iter())
310 .filter(|c| c.logical_type == akar_common::types::LogicalTypeID::Serial)
311 .map(|c| c.name.clone())
312 .collect();
313 let rel_cols: Vec<String> = cat
314 .rel_tables()
315 .into_iter()
316 .filter(|t| t.name == name)
317 .flat_map(|t| t.columns.iter())
318 .filter(|c| c.logical_type == akar_common::types::LogicalTypeID::Serial)
319 .map(|c| c.name.clone())
320 .collect();
321 for col in node_cols.into_iter().chain(rel_cols) {
322 let seq_name = akar_catalog::SequenceEntry::get_serial_name(name, &col);
323 if let akar_catalog::CatalogResult::Dropped { .. } = cat.drop_sequence(&seq_name) {
324 tracing::info!("Dropped serial sequence '{seq_name}'");
325 }
326 }
327 cat.drop_table(name);
329 }
330
331 let table_catalog = self.storage_manager.table_catalog();
333 let node_tid = table_catalog.get_node_table_by_name(name).map(|t| t.table_id);
334 let rel_tid = table_catalog.get_rel_table_by_name(name).map(|t| t.table_id);
335 table_catalog.drop_node_table(name);
336 table_catalog.drop_rel_table(name);
337 if let Some(tid) = node_tid {
338 self.storage_manager.drop_table_persistence(tid);
339 }
340 if let Some(tid) = rel_tid {
341 self.storage_manager.drop_table_persistence(tid);
342 }
343
344 tracing::info!("Dropped table '{name}'");
345 Ok(())
346 }
347
348 #[cfg(feature = "vector-extension")]
352 pub fn create_vector_index(
353 &self,
354 index_name: String,
355 table_name: String,
356 column_name: String,
357 metric: akar_vector::hnsw::DistanceMetric,
358 dimensions: u32,
359 ) -> Result<(), String> {
360 self.storage_manager.create_vector_index(
362 index_name.clone(),
363 table_name.clone(),
364 column_name.clone(),
365 metric,
366 dimensions,
367 );
368
369 let table_catalog = self.storage_manager.table_catalog();
371 if let Some(table) = table_catalog.get_node_table_by_name(&table_name) {
372 let col_idx = table.columns.iter().position(|c| c.name == column_name);
373 if let Some(col_idx) = col_idx {
374 for row_id in 0..table.num_rows as usize {
375 if let Some(val) = table.get_value(row_id, col_idx) {
376 if let Ok(vec) = akar_storage::extract_f64_list_from_value(val) {
377 if let Some(mut vi) = table_catalog.get_vector_index_by_name_mut(&index_name) {
378 vi.hnsw_mut().insert(vec, row_id);
379 }
380 }
381 }
382 }
383 }
384 }
385
386 tracing::info!("Created vector index '{index_name}'");
387 Ok(())
388 }
389
390 #[cfg(feature = "vector-extension")]
396 pub fn refresh_vector_indexes(&self, table_ids: &[u64]) {
397 self.storage_manager
398 .table_catalog()
399 .refresh_vector_indexes_for_tables(table_ids);
400 }
401
402 #[cfg(not(feature = "vector-extension"))]
404 pub fn refresh_vector_indexes(&self, _table_ids: &[u64]) {}
405
406 pub fn create_art_index(&self, table_name: &str, index_name: &str) -> Result<(), String> {
410 self.storage_manager.create_art_index(table_name, index_name)?;
411 Ok(())
412 }
413
414 pub fn drop_art_index(&self, table_name: &str, _index_name: &str) -> Result<(), String> {
416 self.storage_manager.drop_art_index(table_name, _index_name)?;
417
418 {
420 let mut cat = self.catalog.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
421 if let Some(entry) = cat.get_entry_by_name_mut(table_name) {
422 if let akar_catalog::CatalogEntry::NodeTable(t) = entry {
423 t.index_type = None;
424 t.index_name = None;
425 }
426 }
427 }
428
429 Ok(())
430 }
431
432 pub fn table_num_rows(&self, name: &str) -> u64 {
434 self.storage_manager.table_catalog().node_table_num_rows(name)
435 }
436
437 pub fn get_table_id(&self, name: &str) -> Result<Option<u64>, String> {
442 let catalog = self.catalog.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
443 Ok(catalog.get_table_id(name))
444 }
445
446 pub fn catalog_file_path(&self) -> PathBuf {
448 self.storage_manager.db_path().join(CATALOG_FILE_NAME)
449 }
450
451 pub fn connect_tcp(addr: impl Into<String>) -> Result<crate::remote::RemoteDatabase, String> {
462 crate::remote::RemoteDatabase::connect_tcp(addr)
463 }
464
465 pub fn is_in_memory(&self) -> bool {
468 self.storage_manager.db_path().to_string_lossy() == ":memory:"
469 }
470
471 pub fn persist_catalog(&self) -> Result<(), String> {
477 if self.is_in_memory() {
478 return Ok(());
479 }
480 let catalog = self.catalog.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
481 catalog
482 .save_to_path(&self.catalog_file_path())
483 .map_err(|e| format!("Failed to persist catalog: {e}"))
484 }
485
486 fn restore_storage_from_catalog(&self) {
492 let catalog = match self.catalog.lock() {
493 Ok(c) => c,
494 Err(_) => return,
495 };
496 for entry in catalog.all_entries() {
497 match entry {
498 akar_catalog::CatalogEntry::NodeTable(t) => {
499 let columns: Vec<_> = t.columns.iter().map(ColumnDefinition::from).collect();
500 let index_name = if t.has_art_index() {
501 t.index_name.as_deref()
502 } else {
503 None
504 };
505 self.storage_manager
506 .restore_node_table(t.table_id, t.name.clone(), columns, index_name);
507 }
508 akar_catalog::CatalogEntry::RelTable(t) => {
509 let columns: Vec<_> = t.columns.iter().map(ColumnDefinition::from).collect();
510 self.storage_manager.restore_rel_table(
511 t.table_id,
512 t.name.clone(),
513 t.src_table_id,
514 t.dst_table_id,
515 columns,
516 );
517 }
518 _ => {}
519 }
520 }
521 }
522
523 pub fn new(db_path: impl Into<PathBuf>, config: SystemConfig) -> Result<Self, String> {
532 let db_path = db_path.into();
533 let is_memory = db_path.to_string_lossy() == ":memory:";
534
535 let lock = if is_memory {
541 None
542 } else {
543 std::fs::create_dir_all(&db_path)
544 .map_err(|e| format!("Failed to create database directory '{}': {e}", db_path.display()))?;
545 let key = lock_key(&db_path);
546 let mut reg = process_path_locks().lock().unwrap();
547 match reg.get_mut(&key) {
548 Some((_, count)) => {
550 *count += 1;
551 }
552 None => {
553 let file = std::fs::OpenOptions::new()
554 .create(true)
555 .read(true)
556 .write(true)
557 .truncate(false)
558 .open(&key)
559 .map_err(|e| format!("Failed to open lock file '{}': {e}", key.display()))?;
560 let result = if config.read_only {
561 file.try_lock_shared()
562 } else {
563 file.try_lock()
564 };
565 result
566 .map_err(|_| format!("Database '{}' is already open by another process", db_path.display()))?;
567 reg.insert(key.clone(), (file, 1));
568 }
569 }
570 Some(PathLock { key })
571 };
572
573 let memory_manager = Arc::new(MemoryManager::new(config.max_db_size));
574 let task_system = Arc::new(TaskSystem::new(config.max_num_threads as usize));
575
576 let catalog_file = db_path.join(CATALOG_FILE_NAME);
580 let catalog = Arc::new(Mutex::new(
581 Catalog::load_from_path(&catalog_file)
582 .map_err(|e| format!("Failed to load persisted catalog: {e}"))?
583 .unwrap_or_default(),
584 ));
585 let transaction_manager = {
586 let tx_config = akar_transaction::TransactionManagerConfig {
587 concurrent_writes: config.concurrent_writes,
588 };
589 Arc::new(TransactionManager::new_with_config(tx_config))
590 };
591 let function_registry = Arc::new(Mutex::new(FunctionRegistry::new()));
592 let storage_manager = Arc::new(StorageManager::new(db_path.clone(), memory_manager.clone()));
593 let stats_store = Arc::new(Mutex::new(StatsStore::new()));
594 let vfs = Arc::new(VirtualFileSystemRegistry::new());
595
596 let mut db = Self {
597 storage_manager,
598 catalog,
599 transaction_manager,
600 function_registry,
601 task_system,
602 memory_manager,
603 extension_registry: Mutex::new(ExtensionRegistry::new()),
604 stats_store,
605 vfs,
606 spill_threshold_override: AtomicU64::new(0),
607 spill_threshold_overridden: AtomicBool::new(false),
608 _lock: lock,
609 config,
610 };
611
612 db.restore_storage_from_catalog();
615
616 db.storage_manager.set_spiller(db.spiller());
620
621 db.register_builtin_extensions();
623
624 {
626 let mut ext_registry = db
627 .extension_registry
628 .lock()
629 .map_err(|e| format!("Lock poisoned: {e}"))?;
630 let context = ExtensionContext::new(db.function_registry.clone(), db.catalog.clone(), db.vfs.clone());
631 for result in ext_registry.load_all(&context) {
632 match result {
633 (name, Ok(())) => tracing::info!("Extension '{name}' loaded successfully"),
634 (name, Err(e)) => tracing::warn!("Extension '{name}' failed to load: {e}"),
635 }
636 }
637 }
638
639 {
641 let mut reg = db.function_registry.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
642 crate::connection::utils::register_sequence_scalars(&mut reg, db.catalog.clone());
643 }
644
645 if let Err(e) = db.storage_manager.recover() {
657 return Err(format!(
658 "WAL recovery failed (database may need manual repair): {e}. \
659 Refusing to start with an empty database — check the WAL."
660 ));
661 }
662
663 Ok(db)
664 }
665
666 fn register_builtin_extensions(&mut self) {
668 #[cfg(feature = "json-extension")]
669 {
670 let ext = Box::new(akar_json::JsonExtension::new());
671 if let Ok(mut reg) = self.extension_registry.lock() {
672 reg.register(ext);
673 }
674 }
675 #[cfg(feature = "fts-extension")]
676 {
677 let ext = Box::new(akar_fts::FtsExtension::new());
678 if let Ok(mut reg) = self.extension_registry.lock() {
679 reg.register(ext);
680 }
681 }
682 #[cfg(feature = "vector-extension")]
683 {
684 let ext = Box::new(akar_vector::VectorExtension::new());
685 if let Ok(mut reg) = self.extension_registry.lock() {
686 reg.register(ext);
687 }
688 }
689 #[cfg(all(feature = "httpfs-extension", not(akar_wasm)))]
690 {
691 let ext = Box::new(akar_httpfs::HttpfsExtension::new());
692 if let Ok(mut reg) = self.extension_registry.lock() {
693 reg.register(ext);
694 }
695 }
696 #[cfg(all(feature = "duckdb-extension", not(akar_wasm)))]
697 {
698 let ext = Box::new(akar_duckdb::DuckDbExtension::new());
699 if let Ok(mut reg) = self.extension_registry.lock() {
700 reg.register(ext);
701 }
702 }
703 #[cfg(feature = "algo-extension")]
704 {
705 let ext = Box::new(akar_algo::AlgoExtension::new());
706 if let Ok(mut reg) = self.extension_registry.lock() {
707 reg.register(ext);
708 }
709 }
710 #[cfg(feature = "neo4j-extension")]
711 {
712 let ext = Box::new(akar_neo4j::Neo4jExtension::new());
713 if let Ok(mut reg) = self.extension_registry.lock() {
714 reg.register(ext);
715 }
716 }
717 #[cfg(feature = "llm-extension")]
718 {
719 let ext = Box::new(akar_llm::LlmExtension::new());
720 if let Ok(mut reg) = self.extension_registry.lock() {
721 reg.register(ext);
722 }
723 }
724 #[cfg(all(feature = "ml-extension", not(akar_wasm)))]
725 {
726 let ext = Box::new(akar_ml::extension::MlExtension::new());
727 if let Ok(mut reg) = self.extension_registry.lock() {
728 reg.register(ext);
729 }
730 }
731 #[cfg(all(feature = "sqlite-extension", not(akar_wasm)))]
732 {
733 let ext = Box::new(akar_sqlite::SqliteExtension::new());
734 if let Ok(mut reg) = self.extension_registry.lock() {
735 reg.register(ext);
736 }
737 }
738 #[cfg(any(feature = "delta-extension", feature = "delta-native"))]
739 {
740 let ext = Box::new(akar_delta::DeltaExtension::new());
741 if let Ok(mut reg) = self.extension_registry.lock() {
742 reg.register(ext);
743 }
744 }
745 #[cfg(any(feature = "iceberg-extension", feature = "iceberg-native"))]
746 {
747 let ext = Box::new(akar_iceberg::IcebergExtension::new());
748 if let Ok(mut reg) = self.extension_registry.lock() {
749 reg.register(ext);
750 }
751 }
752 #[cfg(any(feature = "azure-extension", feature = "azure-native"))]
753 {
754 let ext = Box::new(akar_azure::AzureExtension::new());
755 if let Ok(mut reg) = self.extension_registry.lock() {
756 reg.register(ext);
757 }
758 }
759 #[cfg(all(feature = "postgres-extension", not(akar_wasm)))]
760 {
761 let ext = Box::new(akar_postgres::PostgresExtension::new());
762 if let Ok(mut reg) = self.extension_registry.lock() {
763 reg.register(ext);
764 }
765 }
766 #[cfg(any(feature = "unity-catalog-extension", feature = "unity-catalog-native"))]
767 {
768 let ext = Box::new(akar_unity_catalog::UnityCatalogExtension::new());
769 if let Ok(mut reg) = self.extension_registry.lock() {
770 reg.register(ext);
771 }
772 }
773 }
774}