use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use crate::authz::DeltaAction;
use crate::backend::{
BackendResult, CreateTableSpec, CredentialAccess, DeltaBackend, DeltaCapabilities,
ResolvedTable, SchemaRef, StagingReservation, TableRef, UpdateTableSpec, VendedCredential,
VendedCredentialKind, etag_of,
};
use crate::coordinator::{CommitCoordinator, InMemoryCommitCoordinator};
use crate::error::DeltaBackendError;
use crate::models::DeltaDataSourceFormat;
#[derive(Debug, Clone)]
struct StoredTable {
full_name: String,
comment: Option<String>,
resolved: ResolvedTable,
}
#[derive(Debug, Default)]
struct State {
tables: BTreeMap<String, StoredTable>,
staging: BTreeMap<String, StagingReservation>,
catalogs: Vec<String>,
clock: i64,
}
#[derive(Clone)]
pub struct InMemoryDeltaBackend {
state: Arc<Mutex<State>>,
coordinator: Arc<InMemoryCommitCoordinator>,
principal: Option<String>,
}
impl Default for InMemoryDeltaBackend {
fn default() -> Self {
Self::new()
}
}
impl InMemoryDeltaBackend {
pub fn new() -> Self {
Self {
state: Arc::new(Mutex::new(State {
catalogs: vec!["catalog".to_string()],
..Default::default()
})),
coordinator: Arc::new(InMemoryCommitCoordinator::default()),
principal: None,
}
}
pub fn with_catalog(self, name: impl Into<String>) -> Self {
self.state.lock().unwrap().catalogs.push(name.into());
self
}
pub fn with_principal(mut self, principal: impl Into<String>) -> Self {
self.principal = Some(principal.into());
self
}
fn next_ts(state: &mut State) -> i64 {
state.clock += 1;
state.clock
}
}
fn fake_credential(url: &str) -> VendedCredential {
VendedCredential {
url: url.to_string(),
expiration_time_ms: 4_102_444_800_000, kind: VendedCredentialKind::S3 {
access_key_id: "AKIAEXAMPLE".to_string(),
secret_access_key: "secret".to_string(),
session_token: Some("token".to_string()),
},
}
}
#[async_trait]
impl<Cx: Send + Sync + 'static> DeltaBackend<Cx> for InMemoryDeltaBackend {
fn capabilities(&self) -> DeltaCapabilities {
DeltaCapabilities { rename: true }
}
async fn authorize(&self, action: DeltaAction<'_>, _cx: &Cx) -> BackendResult<()> {
if let DeltaAction::AdoptStaging { reservation } = action
&& reservation.created_by.as_deref() != self.principal.as_deref()
{
return Err(DeltaBackendError::PermissionDenied(
"caller is not the creator of the staging table".to_string(),
));
}
Ok(())
}
async fn catalog_exists(&self, catalog: &str, _cx: &Cx) -> BackendResult<()> {
let state = self.state.lock().unwrap();
if state.catalogs.iter().any(|c| c == catalog) {
Ok(())
} else {
Err(DeltaBackendError::NotFound(format!(
"catalog '{catalog}' not found"
)))
}
}
async fn resolve_table(&self, table: &TableRef, _cx: &Cx) -> BackendResult<ResolvedTable> {
let state = self.state.lock().unwrap();
state
.tables
.get(&table.full_name())
.map(|t| t.resolved.clone())
.ok_or_else(|| {
DeltaBackendError::NotFound(format!("table '{}' not found", table.full_name()))
})
}
async fn validate_external_location(&self, _location: &str, _cx: &Cx) -> BackendResult<()> {
Ok(())
}
async fn create_table_row(
&self,
spec: CreateTableSpec,
_cx: &Cx,
) -> BackendResult<ResolvedTable> {
let mut state = self.state.lock().unwrap();
let ts = Self::next_ts(&mut state);
let full_name = format!("{}.{}.{}", spec.at.catalog, spec.at.schema, spec.name);
if state.tables.contains_key(&full_name) {
return Err(DeltaBackendError::AlreadyExists(format!(
"table '{full_name}' already exists"
)));
}
if let Some(reservation) = &spec.adopt_staging {
state.staging.remove(&reservation.location);
}
let table_id = spec
.table_id
.unwrap_or_else(|| uuid::Uuid::now_v7().to_string());
let resolved = ResolvedTable {
table_id: Some(table_id),
location: spec.location,
table_type: Some(spec.table_type),
data_source_format: Some(DeltaDataSourceFormat::Delta),
columns: spec.columns,
properties: spec.properties,
created_at_ms: Some(ts),
updated_at_ms: Some(ts),
version: 0,
};
state.tables.insert(
full_name.clone(),
StoredTable {
full_name,
comment: spec.comment,
resolved: resolved.clone(),
},
);
Ok(resolved)
}
async fn update_table_row(
&self,
spec: UpdateTableSpec,
_cx: &Cx,
) -> BackendResult<ResolvedTable> {
let mut state = self.state.lock().unwrap();
let ts = Self::next_ts(&mut state);
let stored = state
.tables
.values_mut()
.find(|t| t.resolved.table_id.as_deref() == Some(spec.table_id.as_str()))
.ok_or_else(|| {
DeltaBackendError::NotFound(format!("table id '{}' not found", spec.table_id))
})?;
if let Some(expected) = &spec.expected_etag
&& expected != &etag_of(&stored.resolved)
{
return Err(DeltaBackendError::UpdateRequirementConflict(
"assert-etag failed: table has been modified".to_string(),
));
}
stored.resolved.columns = spec.columns;
stored.resolved.properties = spec.properties;
if let Some(comment) = spec.comment {
stored.comment = Some(comment);
}
stored.resolved.updated_at_ms = Some(ts);
stored.resolved.version += 1;
Ok(stored.resolved.clone())
}
async fn delete_table(&self, table: &TableRef, _cx: &Cx) -> BackendResult<()> {
let mut state = self.state.lock().unwrap();
state
.tables
.remove(&table.full_name())
.map(|_| ())
.ok_or_else(|| {
DeltaBackendError::NotFound(format!("table '{}' not found", table.full_name()))
})
}
async fn rename_table(&self, from: &TableRef, to_name: &str, _cx: &Cx) -> BackendResult<()> {
let mut state = self.state.lock().unwrap();
let Some(mut stored) = state.tables.remove(&from.full_name()) else {
return Err(DeltaBackendError::NotFound(format!(
"table '{}' not found",
from.full_name()
)));
};
let new_full = format!("{}.{}.{}", from.catalog, from.schema, to_name);
stored.full_name = new_full.clone();
state.tables.insert(new_full, stored);
Ok(())
}
async fn allocate_staging(
&self,
_at: &SchemaRef,
name: &str,
_cx: &Cx,
) -> BackendResult<StagingReservation> {
let mut state = self.state.lock().unwrap();
let id = uuid::Uuid::now_v7().to_string();
let location = format!("s3://bucket/staging/{id}");
let reservation = StagingReservation {
table_id: id,
name: name.to_string(),
location: location.clone(),
created_by: self.principal.clone(),
stage_committed: false,
};
state.staging.insert(location, reservation.clone());
Ok(reservation)
}
async fn resolve_staging_by_location(
&self,
location: &str,
_cx: &Cx,
) -> BackendResult<StagingReservation> {
let state = self.state.lock().unwrap();
state
.staging
.get(location)
.cloned()
.ok_or_else(|| DeltaBackendError::NotFound(format!("no staging table at '{location}'")))
}
async fn resolve_staging_by_id(
&self,
table_id: &str,
_cx: &Cx,
) -> BackendResult<StagingReservation> {
let state = self.state.lock().unwrap();
state
.staging
.values()
.find(|s| s.table_id == table_id)
.cloned()
.ok_or_else(|| {
DeltaBackendError::NotFound(format!("staging table '{table_id}' not found"))
})
}
async fn vend_table_credential(
&self,
table_id: &str,
_access: CredentialAccess,
_cx: &Cx,
) -> BackendResult<VendedCredential> {
let state = self.state.lock().unwrap();
let location = state
.tables
.values()
.find(|t| t.resolved.table_id.as_deref() == Some(table_id))
.map(|t| t.resolved.location.clone())
.ok_or_else(|| {
DeltaBackendError::NotFound(format!("table id '{table_id}' not found"))
})?;
Ok(fake_credential(&location))
}
async fn vend_path_credential(
&self,
location: &str,
_access: CredentialAccess,
_cx: &Cx,
) -> BackendResult<VendedCredential> {
Ok(fake_credential(location))
}
fn commit_coordinator(&self) -> &dyn CommitCoordinator {
self.coordinator.as_ref()
}
}