use akar_catalog::Catalog;
use akar_common::file_system::VirtualFileSystemRegistry;
use akar_common::memory::MemoryManager;
use akar_common::task_system::TaskSystem;
use akar_extension::{ExtensionContext, ExtensionRegistry};
use akar_function::FunctionRegistry;
use akar_storage::StorageManager;
use akar_storage::stats::StatsStore;
use akar_storage::table::ColumnDefinition;
use akar_transaction::TransactionManager;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
pub const CATALOG_FILE_NAME: &str = "catalog.json";
pub const LOCK_FILE_NAME: &str = "akar.lock";
#[derive(Debug, Clone)]
pub struct SystemConfig {
pub buffer_pool_size: u64,
pub max_num_threads: u64,
pub enable_compression: bool,
pub read_only: bool,
pub max_db_size: u64,
pub auto_checkpoint: bool,
pub checkpoint_threshold: i64,
pub concurrent_writes: bool,
pub spill_threshold: u64,
}
impl Default for SystemConfig {
fn default() -> Self {
Self {
buffer_pool_size: 0,
max_num_threads: 0,
enable_compression: true,
read_only: false,
max_db_size: u64::from(u32::MAX),
auto_checkpoint: true,
checkpoint_threshold: -1,
concurrent_writes: true,
spill_threshold: 0,
}
}
}
static PROCESS_PATH_LOCKS: OnceLock<Mutex<HashMap<PathBuf, (std::fs::File, u32)>>> = OnceLock::new();
fn process_path_locks() -> &'static Mutex<HashMap<PathBuf, (std::fs::File, u32)>> {
PROCESS_PATH_LOCKS.get_or_init(|| Mutex::new(HashMap::new()))
}
struct PathLock {
key: PathBuf,
}
impl Drop for PathLock {
fn drop(&mut self) {
let mut reg = process_path_locks().lock().unwrap();
if let Some((_, count)) = reg.get_mut(&self.key) {
*count -= 1;
if *count == 0 {
reg.remove(&self.key);
}
}
}
}
fn lock_key(db_path: &Path) -> PathBuf {
std::fs::canonicalize(db_path)
.unwrap_or_else(|_| db_path.to_path_buf())
.join(LOCK_FILE_NAME)
}
#[allow(dead_code)]
pub struct Database {
pub(crate) storage_manager: Arc<StorageManager>,
pub(crate) catalog: Arc<Mutex<Catalog>>,
pub(crate) transaction_manager: Arc<TransactionManager>,
pub(crate) function_registry: Arc<Mutex<FunctionRegistry>>,
pub(crate) task_system: Arc<TaskSystem>,
pub(crate) memory_manager: Arc<MemoryManager>,
pub(crate) extension_registry: Mutex<ExtensionRegistry>,
pub(crate) stats_store: Arc<Mutex<StatsStore>>,
pub(crate) vfs: Arc<VirtualFileSystemRegistry>,
pub(crate) config: SystemConfig,
spill_threshold_override: AtomicU64,
spill_threshold_overridden: AtomicBool,
_lock: Option<PathLock>,
}
impl Database {
pub fn set_spill_threshold(&self, bytes: u64) {
self.spill_threshold_override.store(bytes, Ordering::Relaxed);
self.spill_threshold_overridden.store(true, Ordering::Relaxed);
self.storage_manager.set_spiller(self.spiller());
}
pub fn effective_spill_threshold(&self) -> u64 {
if self.spill_threshold_overridden.load(Ordering::Relaxed) {
return self.spill_threshold_override.load(Ordering::Relaxed);
}
if self.config.spill_threshold > 0 {
return self.config.spill_threshold;
}
if self.config.buffer_pool_size > 0 {
return (self.config.buffer_pool_size as f64 * 0.8) as u64;
}
0
}
pub fn spiller(&self) -> Option<Arc<akar_storage::Spiller>> {
let threshold = self.effective_spill_threshold();
if threshold == 0 {
return None;
}
let spill_dir = self.storage_manager.db_path().join("spill");
Some(Arc::new(akar_storage::Spiller::new(spill_dir, threshold)))
}
pub fn storage_driver(&self) -> crate::storage_driver::StorageDriver {
crate::storage_driver::StorageDriver::new(self.storage_manager.clone(), self.catalog.clone(), self.vfs.clone())
}
pub fn catalog(&self) -> Arc<Mutex<Catalog>> {
self.catalog.clone()
}
pub fn table_catalog(&self) -> Arc<akar_storage::TableCatalog> {
self.storage_manager.table_catalog()
}
pub fn create_node_table(&self, name: String, columns: Vec<akar_catalog::CatalogColumn>) -> Result<u64, String> {
let storage_columns: Vec<ColumnDefinition> = columns
.iter()
.map(|c| ColumnDefinition {
name: c.name.clone(),
logical_type: c.logical_type,
is_primary_key: c.is_primary_key,
compression: c.compression,
})
.collect();
let node_table = self.storage_manager.create_node_table(name.clone(), storage_columns);
let table_id = node_table.table_id;
{
let mut cat = self.catalog.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
for col in &columns {
if col.logical_type == akar_common::types::LogicalTypeID::Serial {
if let akar_catalog::CatalogResult::Created { .. } = cat.create_serial_sequence(&name, &col.name) {
tracing::info!("Created serial sequence for {name}.{}", col.name);
}
}
}
}
if columns.iter().any(|c| c.is_primary_key) {
let index_name = format!("{name}_pk_idx");
self.storage_manager
.create_art_index(&name, &index_name)
.map_err(|e| format!("Failed to create ART PK index for table '{name}': {e}"))?;
}
tracing::info!("Created node table '{name}'");
Ok(table_id)
}
pub fn create_rel_table(
&self,
name: String,
src_table_id: u64,
dst_table_id: u64,
columns: Vec<akar_catalog::CatalogColumn>,
) -> Result<u64, String> {
let storage_columns: Vec<ColumnDefinition> = columns
.iter()
.map(|c| ColumnDefinition {
name: c.name.clone(),
logical_type: c.logical_type,
is_primary_key: c.is_primary_key,
compression: c.compression,
})
.collect();
let rel_table =
self.storage_manager
.create_rel_table(name.clone(), src_table_id, dst_table_id, storage_columns);
let table_id = rel_table.table_id;
tracing::info!("Created rel table '{name}'");
Ok(table_id)
}
pub fn drop_table(&self, name: &str) -> Result<(), String> {
{
let mut cat = self.catalog.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
let node_cols: Vec<String> = cat
.node_tables()
.into_iter()
.filter(|t| t.name == name)
.flat_map(|t| t.columns.iter())
.filter(|c| c.logical_type == akar_common::types::LogicalTypeID::Serial)
.map(|c| c.name.clone())
.collect();
let rel_cols: Vec<String> = cat
.rel_tables()
.into_iter()
.filter(|t| t.name == name)
.flat_map(|t| t.columns.iter())
.filter(|c| c.logical_type == akar_common::types::LogicalTypeID::Serial)
.map(|c| c.name.clone())
.collect();
for col in node_cols.into_iter().chain(rel_cols) {
let seq_name = akar_catalog::SequenceEntry::get_serial_name(name, &col);
if let akar_catalog::CatalogResult::Dropped { .. } = cat.drop_sequence(&seq_name) {
tracing::info!("Dropped serial sequence '{seq_name}'");
}
}
cat.drop_table(name);
}
let table_catalog = self.storage_manager.table_catalog();
let node_tid = table_catalog.get_node_table_by_name(name).map(|t| t.table_id);
let rel_tid = table_catalog.get_rel_table_by_name(name).map(|t| t.table_id);
table_catalog.drop_node_table(name);
table_catalog.drop_rel_table(name);
if let Some(tid) = node_tid {
self.storage_manager.drop_table_persistence(tid);
}
if let Some(tid) = rel_tid {
self.storage_manager.drop_table_persistence(tid);
}
tracing::info!("Dropped table '{name}'");
Ok(())
}
#[cfg(feature = "vector-extension")]
pub fn create_vector_index(
&self,
index_name: String,
table_name: String,
column_name: String,
metric: akar_vector::hnsw::DistanceMetric,
dimensions: u32,
) -> Result<(), String> {
self.storage_manager.create_vector_index(
index_name.clone(),
table_name.clone(),
column_name.clone(),
metric,
dimensions,
);
let table_catalog = self.storage_manager.table_catalog();
if let Some(table) = table_catalog.get_node_table_by_name(&table_name) {
let col_idx = table.columns.iter().position(|c| c.name == column_name);
if let Some(col_idx) = col_idx {
for row_id in 0..table.num_rows as usize {
if let Some(val) = table.get_value(row_id, col_idx) {
if let Ok(vec) = akar_storage::extract_f64_list_from_value(val) {
if let Some(mut vi) = table_catalog.get_vector_index_by_name_mut(&index_name) {
vi.hnsw_mut().insert(vec, row_id);
}
}
}
}
}
}
tracing::info!("Created vector index '{index_name}'");
Ok(())
}
#[cfg(feature = "vector-extension")]
pub fn refresh_vector_indexes(&self, table_ids: &[u64]) {
self.storage_manager
.table_catalog()
.refresh_vector_indexes_for_tables(table_ids);
}
#[cfg(not(feature = "vector-extension"))]
pub fn refresh_vector_indexes(&self, _table_ids: &[u64]) {}
pub fn create_art_index(&self, table_name: &str, index_name: &str) -> Result<(), String> {
self.storage_manager.create_art_index(table_name, index_name)?;
Ok(())
}
pub fn drop_art_index(&self, table_name: &str, _index_name: &str) -> Result<(), String> {
self.storage_manager.drop_art_index(table_name, _index_name)?;
{
let mut cat = self.catalog.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
if let Some(entry) = cat.get_entry_by_name_mut(table_name) {
if let akar_catalog::CatalogEntry::NodeTable(t) = entry {
t.index_type = None;
t.index_name = None;
}
}
}
Ok(())
}
pub fn table_num_rows(&self, name: &str) -> u64 {
self.storage_manager.table_catalog().node_table_num_rows(name)
}
pub fn get_table_id(&self, name: &str) -> Result<Option<u64>, String> {
let catalog = self.catalog.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
Ok(catalog.get_table_id(name))
}
pub fn catalog_file_path(&self) -> PathBuf {
self.storage_manager.db_path().join(CATALOG_FILE_NAME)
}
pub fn connect_tcp(addr: impl Into<String>) -> Result<crate::remote::RemoteDatabase, String> {
crate::remote::RemoteDatabase::connect_tcp(addr)
}
pub fn is_in_memory(&self) -> bool {
self.storage_manager.db_path().to_string_lossy() == ":memory:"
}
pub fn persist_catalog(&self) -> Result<(), String> {
if self.is_in_memory() {
return Ok(());
}
let catalog = self.catalog.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
catalog
.save_to_path(&self.catalog_file_path())
.map_err(|e| format!("Failed to persist catalog: {e}"))
}
fn restore_storage_from_catalog(&self) {
let catalog = match self.catalog.lock() {
Ok(c) => c,
Err(_) => return,
};
for entry in catalog.all_entries() {
match entry {
akar_catalog::CatalogEntry::NodeTable(t) => {
let columns: Vec<_> = t.columns.iter().map(ColumnDefinition::from).collect();
let index_name = if t.has_art_index() {
t.index_name.as_deref()
} else {
None
};
self.storage_manager
.restore_node_table(t.table_id, t.name.clone(), columns, index_name);
}
akar_catalog::CatalogEntry::RelTable(t) => {
let columns: Vec<_> = t.columns.iter().map(ColumnDefinition::from).collect();
self.storage_manager.restore_rel_table(
t.table_id,
t.name.clone(),
t.src_table_id,
t.dst_table_id,
columns,
);
}
_ => {}
}
}
}
pub fn new(db_path: impl Into<PathBuf>, config: SystemConfig) -> Result<Self, String> {
let db_path = db_path.into();
let is_memory = db_path.to_string_lossy() == ":memory:";
let lock = if is_memory {
None
} else {
std::fs::create_dir_all(&db_path)
.map_err(|e| format!("Failed to create database directory '{}': {e}", db_path.display()))?;
let key = lock_key(&db_path);
let mut reg = process_path_locks().lock().unwrap();
match reg.get_mut(&key) {
Some((_, count)) => {
*count += 1;
}
None => {
let file = std::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&key)
.map_err(|e| format!("Failed to open lock file '{}': {e}", key.display()))?;
let result = if config.read_only {
file.try_lock_shared()
} else {
file.try_lock()
};
result
.map_err(|_| format!("Database '{}' is already open by another process", db_path.display()))?;
reg.insert(key.clone(), (file, 1));
}
}
Some(PathLock { key })
};
let memory_manager = Arc::new(MemoryManager::new(config.max_db_size));
let task_system = Arc::new(TaskSystem::new(config.max_num_threads as usize));
let catalog_file = db_path.join(CATALOG_FILE_NAME);
let catalog = Arc::new(Mutex::new(
Catalog::load_from_path(&catalog_file)
.map_err(|e| format!("Failed to load persisted catalog: {e}"))?
.unwrap_or_default(),
));
let transaction_manager = {
let tx_config = akar_transaction::TransactionManagerConfig {
concurrent_writes: config.concurrent_writes,
};
Arc::new(TransactionManager::new_with_config(tx_config))
};
let function_registry = Arc::new(Mutex::new(FunctionRegistry::new()));
let storage_manager = Arc::new(StorageManager::new(db_path.clone(), memory_manager.clone()));
let stats_store = Arc::new(Mutex::new(StatsStore::new()));
let vfs = Arc::new(VirtualFileSystemRegistry::new());
let mut db = Self {
storage_manager,
catalog,
transaction_manager,
function_registry,
task_system,
memory_manager,
extension_registry: Mutex::new(ExtensionRegistry::new()),
stats_store,
vfs,
spill_threshold_override: AtomicU64::new(0),
spill_threshold_overridden: AtomicBool::new(false),
_lock: lock,
config,
};
db.restore_storage_from_catalog();
db.storage_manager.set_spiller(db.spiller());
db.register_builtin_extensions();
{
let mut ext_registry = db
.extension_registry
.lock()
.map_err(|e| format!("Lock poisoned: {e}"))?;
let context = ExtensionContext::new(db.function_registry.clone(), db.catalog.clone(), db.vfs.clone());
for result in ext_registry.load_all(&context) {
match result {
(name, Ok(())) => tracing::info!("Extension '{name}' loaded successfully"),
(name, Err(e)) => tracing::warn!("Extension '{name}' failed to load: {e}"),
}
}
}
{
let mut reg = db.function_registry.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
crate::connection::utils::register_sequence_scalars(&mut reg, db.catalog.clone());
}
if let Err(e) = db.storage_manager.recover() {
return Err(format!(
"WAL recovery failed (database may need manual repair): {e}. \
Refusing to start with an empty database — check the WAL."
));
}
Ok(db)
}
fn register_builtin_extensions(&mut self) {
#[cfg(feature = "json-extension")]
{
let ext = Box::new(akar_json::JsonExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(feature = "fts-extension")]
{
let ext = Box::new(akar_fts::FtsExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(feature = "vector-extension")]
{
let ext = Box::new(akar_vector::VectorExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(all(feature = "httpfs-extension", not(akar_wasm)))]
{
let ext = Box::new(akar_httpfs::HttpfsExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(all(feature = "duckdb-extension", not(akar_wasm)))]
{
let ext = Box::new(akar_duckdb::DuckDbExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(feature = "algo-extension")]
{
let ext = Box::new(akar_algo::AlgoExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(feature = "neo4j-extension")]
{
let ext = Box::new(akar_neo4j::Neo4jExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(feature = "llm-extension")]
{
let ext = Box::new(akar_llm::LlmExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(all(feature = "sqlite-extension", not(akar_wasm)))]
{
let ext = Box::new(akar_sqlite::SqliteExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(any(feature = "delta-extension", feature = "delta-native"))]
{
let ext = Box::new(akar_delta::DeltaExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(any(feature = "iceberg-extension", feature = "iceberg-native"))]
{
let ext = Box::new(akar_iceberg::IcebergExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(any(feature = "azure-extension", feature = "azure-native"))]
{
let ext = Box::new(akar_azure::AzureExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(all(feature = "postgres-extension", not(akar_wasm)))]
{
let ext = Box::new(akar_postgres::PostgresExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
#[cfg(any(feature = "unity-catalog-extension", feature = "unity-catalog-native"))]
{
let ext = Box::new(akar_unity_catalog::UnityCatalogExtension::new());
if let Ok(mut reg) = self.extension_registry.lock() {
reg.register(ext);
}
}
}
}