use rustc_hash::FxHashMap;
use std::sync::Arc;
use crate::config::Config;
use crate::traits::{Index, Transaction};
use radixdb_core::{
CompactArc, Error, IsolationLevel, NavigationErrorCode, ReferenceDescriptor,
ReferenceTargetKey, Result, RowVec, Schema, SchemaColumnId, SchemaTableId,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PhysicalSnapshotIdentity {
pub snapshot_id: String,
pub database_id: String,
pub physical_format_major: u16,
pub physical_format_minor: u16,
}
pub trait Engine: Send + Sync {
fn open(&mut self) -> Result<()>;
fn close(&mut self) -> Result<()>;
fn begin_transaction(&self) -> Result<Box<dyn Transaction>>;
fn begin_transaction_with_level(&self, level: IsolationLevel) -> Result<Box<dyn Transaction>>;
fn path(&self) -> Option<&str>;
fn table_exists(&self, table_name: &str) -> Result<bool>;
fn index_exists(&self, index_name: &str, table_name: &str) -> Result<bool>;
fn get_index(&self, table_name: &str, index_name: &str) -> Result<Arc<dyn Index>>;
fn get_table_schema(&self, table_name: &str) -> Result<CompactArc<Schema>>;
fn schema_epoch(&self) -> u64;
fn schema_scope_id(&self) -> u64;
fn bind_schema_table_id(&self, table_name: &str) -> Result<SchemaTableId> {
let generation = self.schema_epoch();
let schema = self.get_table_schema(table_name)?;
let id = SchemaTableId::new(
self.schema_scope_id(),
generation,
schema.table_name().to_lowercase(),
);
if self.schema_epoch() != generation {
return Err(Error::navigation(
NavigationErrorCode::SchemaChanged,
format!("schema generation changed while binding table '{table_name}'"),
));
}
Ok(id)
}
fn bind_schema_column_id(
&self,
table: &SchemaTableId,
column_name: &str,
) -> Result<SchemaColumnId> {
self.validate_schema_table_id(table)?;
let schema = self.get_table_schema(table.table_name())?;
let (ordinal, column) = schema.find_column(column_name).ok_or_else(|| {
Error::ColumnNotFound(format!("{}.{}", table.table_name(), column_name))
})?;
if column.id != ordinal {
return Err(Error::navigation(
NavigationErrorCode::UnsupportedReferenceShape,
format!(
"column '{}.{}' has stale identity",
table.table_name(),
column_name
),
));
}
if self.schema_epoch() != table.schema_generation() {
return Err(Error::navigation(
NavigationErrorCode::SchemaChanged,
format!(
"schema generation changed while binding '{}.{}'",
table.table_name(),
column_name
),
));
}
Ok(SchemaColumnId::new(table.clone(), ordinal))
}
fn get_reference_descriptor(
&self,
source: &SchemaColumnId,
) -> Result<Option<ReferenceDescriptor>> {
self.validate_schema_table_id(source.table())?;
let generation = source.table().schema_generation();
let source_schema = self.get_table_schema(source.table().table_name())?;
let source_column = source_schema.get_column(source.ordinal()).ok_or_else(|| {
Error::navigation(
NavigationErrorCode::SchemaChanged,
format!(
"source column ordinal {} no longer exists in '{}'",
source.ordinal(),
source.table().table_name()
),
)
})?;
if source_column.id != source.ordinal() {
return Err(Error::navigation(
NavigationErrorCode::UnsupportedReferenceShape,
"source column identity is stale",
));
}
let mut constraints = source_schema
.foreign_keys()
.iter()
.filter(|foreign_key| foreign_key.column_index == source.ordinal());
let Some(foreign_key) = constraints.next() else {
return Ok(None);
};
if constraints.next().is_some() {
return Err(Error::navigation(
NavigationErrorCode::UnsupportedReferenceShape,
format!(
"'{}.{}' has more than one reference target",
source.table().table_name(),
source_column.name
),
));
}
let target_schema = self
.get_table_schema(&foreign_key.referenced_table)
.map_err(|error| {
Error::navigation(
NavigationErrorCode::UnsupportedReferenceShape,
format!(
"target table '{}' is unavailable: {error}",
foreign_key.referenced_table
),
)
})?;
let (target_ordinal, target_column) = target_schema
.find_column(&foreign_key.referenced_column)
.ok_or_else(|| {
Error::navigation(
NavigationErrorCode::UnsupportedReferenceShape,
format!(
"target column '{}.{}' is unavailable",
foreign_key.referenced_table, foreign_key.referenced_column
),
)
})?;
if target_column.id != target_ordinal || source_column.data_type != target_column.data_type
{
return Err(Error::navigation(
NavigationErrorCode::UnsupportedReferenceShape,
"reference column identity or type is inconsistent",
));
}
let target_key = if target_column.primary_key
&& target_schema.primary_key_indices().len() == 1
{
ReferenceTargetKey::PrimaryKey
} else {
let unique_not_null = !target_column.nullable
&& self
.get_all_indexes(target_schema.table_name())?
.iter()
.any(|index| {
index.is_unique()
&& index.partial_predicate().is_none()
&& index.column_ids().len() == 1
&& usize::try_from(index.column_ids()[0]).ok() == Some(target_ordinal)
});
if !unique_not_null {
return Err(Error::navigation(
NavigationErrorCode::UnsupportedReferenceShape,
format!(
"target '{}.{}' is not PRIMARY KEY or UNIQUE NOT NULL",
target_schema.table_name(),
target_column.name
),
));
}
ReferenceTargetKey::UniqueNotNull
};
if self.schema_epoch() != generation {
return Err(Error::navigation(
NavigationErrorCode::SchemaChanged,
format!(
"schema generation changed while resolving '{}.{}'",
source.table().table_name(),
source_column.name
),
));
}
let target_table = SchemaTableId::new(
self.schema_scope_id(),
generation,
target_schema.table_name().to_lowercase(),
);
Ok(Some(ReferenceDescriptor::new(
source.clone(),
SchemaColumnId::new(target_table, target_ordinal),
source_column.nullable,
source_column.data_type,
target_key,
)))
}
fn validate_schema_table_id(&self, table: &SchemaTableId) -> Result<()> {
if table.scope_id() != self.schema_scope_id() {
return Err(Error::navigation(
NavigationErrorCode::UnsupportedReferenceShape,
"cross-database reference identity",
));
}
let actual = self.schema_epoch();
if table.schema_generation() != actual {
return Err(Error::navigation(
NavigationErrorCode::SchemaChanged,
format!(
"expected generation {}, found {actual}",
table.schema_generation()
),
));
}
Ok(())
}
fn list_table_indexes(&self, table_name: &str) -> Result<FxHashMap<String, String>>;
fn get_all_indexes(&self, table_name: &str) -> Result<Vec<std::sync::Arc<dyn Index>>>;
fn get_isolation_level(&self) -> IsolationLevel;
fn set_isolation_level(&mut self, level: IsolationLevel) -> Result<()>;
fn get_config(&self) -> Config;
fn update_config(&mut self, config: Config) -> Result<()>;
fn create_snapshot(&self) -> Result<PhysicalSnapshotIdentity>;
fn create_snapshot_cancellable(
&self,
is_cancelled: &(dyn Fn() -> bool + Send + Sync),
) -> Result<PhysicalSnapshotIdentity> {
if is_cancelled() {
return Err(radixdb_core::Error::QueryCancelled);
}
self.create_snapshot()
}
fn restore_snapshot(&self, _snapshot_id: Option<&str>) -> Result<String> {
Err(radixdb_core::Error::internal(
"restore_snapshot not supported by this engine",
))
}
fn checkpoint_cycle(&self) -> Result<()>;
fn force_checkpoint_cycle(&self) -> Result<()>;
fn record_truncate_table(&self, table_name: &str) -> Result<()> {
let _ = table_name;
Ok(())
}
fn fetch_rows_by_ids(&self, table_name: &str, row_ids: &[i64]) -> Result<RowVec> {
let _ = (table_name, row_ids);
Err(radixdb_core::Error::internal(
"fetch_rows_by_ids not supported by this engine",
))
}
#[allow(clippy::type_complexity)]
fn get_row_fetcher(
&self,
table_name: &str,
) -> Result<Box<dyn Fn(&[i64]) -> Result<RowVec> + Send + Sync>> {
let _ = table_name;
Err(radixdb_core::Error::internal(
"get_row_fetcher not supported by this engine",
))
}
#[allow(clippy::type_complexity)]
fn get_row_counter(
&self,
table_name: &str,
) -> Result<Box<dyn Fn(&[i64]) -> usize + Send + Sync>> {
let _ = table_name;
Err(radixdb_core::Error::internal(
"get_row_counter not supported by this engine",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn _assert_object_safe(_: &dyn Engine) {}
}