use super::allocation::AllocationError;
use super::codec::DecodedPageShape;
use super::file_table::{
encode_file_table_internal_children, encode_file_table_leaf_records,
file_record_encoded_length, file_table_page_decode_shape,
};
use super::persistent_btree::{self, Child, Format, Page, PageRef};
use super::{
CanonicalDecodeError, DecodeLimits, FileRecord, FileTablePage, decode_file_table_page,
};
use crate::foundation::FileId;
use crate::performance::{OperationFailure, WorkBudget, WorkCounters, WorkError};
use crate::storage::{ObjectId, ObjectKind, ObjectStoreError};
use thiserror::Error;
pub(crate) struct FileTableFormat;
impl Format for FileTableFormat {
type Key = FileId;
type Value = FileRecord;
fn kind() -> ObjectKind {
ObjectKind::FileTablePage
}
fn key(value: &Self::Value) -> &Self::Key {
&value.file_id
}
fn decode(bytes: &[u8], limits: DecodeLimits) -> Result<Page<Self>, CanonicalDecodeError> {
Ok(match decode_file_table_page(bytes, limits)? {
FileTablePage::Leaf(records) => Page::Leaf(records),
FileTablePage::Internal(children) => Page::Internal(
children
.into_iter()
.map(|child| Child {
first: child.first_file_id,
page: child.page,
})
.collect(),
),
})
}
fn decode_shape(
bytes: &[u8],
limits: DecodeLimits,
) -> Result<DecodedPageShape, CanonicalDecodeError> {
file_table_page_decode_shape(bytes, limits)
}
fn encode(
page: &PageRef<'_, Self>,
maximum_items: u32,
) -> Result<Vec<u8>, CanonicalDecodeError> {
match page {
PageRef::Leaf(records) => encode_file_table_leaf_records(records, maximum_items),
PageRef::Internal(children) => encode_file_table_internal_children(
children.iter().map(|child| (child.first, child.page)),
maximum_items,
),
}
}
fn page_encoded_length(
page: &PageRef<'_, Self>,
maximum_items: u32,
) -> Result<usize, CanonicalDecodeError> {
let count = match page {
PageRef::Leaf(records) => records.len(),
PageRef::Internal(children) => children.len(),
};
if u32::try_from(count).unwrap_or(u32::MAX) > maximum_items {
return Err(CanonicalDecodeError::LengthOverflow);
}
let mut bytes = 8_usize + 2 + 1 + 4;
match page {
PageRef::Leaf(records) => {
for record in *records {
bytes = bytes
.checked_add(Self::leaf_item_encoded_length(record)?)
.ok_or(CanonicalDecodeError::LengthOverflow)?;
}
}
PageRef::Internal(children) => {
for child in *children {
bytes = bytes
.checked_add(Self::internal_item_encoded_length(&child.first)?)
.ok_or(CanonicalDecodeError::LengthOverflow)?;
}
}
}
Ok(bytes)
}
fn leaf_item_encoded_length(value: &Self::Value) -> Result<usize, CanonicalDecodeError> {
file_record_encoded_length(*value)
}
fn internal_item_encoded_length(_key: &Self::Key) -> Result<usize, CanonicalDecodeError> {
Ok(16 + 32)
}
fn key_nested_bytes(_key: &Self::Key) -> u64 {
0
}
fn value_nested_bytes(_value: &Self::Value) -> u64 {
0
}
fn try_clone_key(
key: &Self::Key,
_maximum_bytes: u32,
) -> Result<Self::Key, CanonicalDecodeError> {
Ok(*key)
}
fn try_clone_value(
value: &Self::Value,
_maximum_bytes: u32,
) -> Result<Self::Value, CanonicalDecodeError> {
Ok(*value)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FileTableMutation {
Insert(FileRecord),
Remove {
file_id: FileId,
expected: Option<FileRecord>,
},
Replace {
expected: FileRecord,
replacement: FileRecord,
},
}
impl FileTableMutation {
fn validate(self) -> Result<(), FileTableSemanticError> {
match self {
Self::Insert(record) => record
.validate()
.map_err(|_| FileTableSemanticError::InvalidRecord),
Self::Remove {
file_id,
expected: Some(expected),
} => {
expected
.validate()
.map_err(|_| FileTableSemanticError::InvalidRecord)?;
if expected.file_id != file_id {
return Err(FileTableSemanticError::IdentityMismatch);
}
Ok(())
}
Self::Replace {
expected,
replacement,
} => {
expected
.validate()
.map_err(|_| FileTableSemanticError::InvalidRecord)?;
replacement
.validate()
.map_err(|_| FileTableSemanticError::InvalidRecord)?;
if expected.file_id != replacement.file_id {
return Err(FileTableSemanticError::IdentityMismatch);
}
Ok(())
}
Self::Remove { expected: None, .. } => Ok(()),
}
}
}
impl persistent_btree::Mutation<FileTableFormat> for FileTableMutation {
type Error = FileTableSemanticError;
fn key(&self) -> &FileId {
match self {
Self::Insert(record) => &record.file_id,
Self::Remove { file_id, .. } => file_id,
Self::Replace { replacement, .. } => &replacement.file_id,
}
}
fn changes_cardinality(&self) -> bool {
matches!(self, Self::Insert(_) | Self::Remove { .. })
}
fn apply_current(&self, current: &mut Option<FileRecord>) -> Result<(), Self::Error> {
match self {
Self::Insert(_) if current.is_some() => Err(FileTableSemanticError::AlreadyExists),
Self::Insert(record) => {
*current = Some(*record);
Ok(())
}
Self::Remove { .. } | Self::Replace { .. } if current.is_none() => {
Err(FileTableSemanticError::Missing)
}
Self::Remove { expected, .. } => {
let existing = current.ok_or(FileTableSemanticError::Missing)?;
if expected.is_some_and(|value| existing != value) {
return Err(FileTableSemanticError::StateConflict);
}
*current = None;
Ok(())
}
Self::Replace {
expected,
replacement,
} => {
let existing = current.ok_or(FileTableSemanticError::Missing)?;
if existing != *expected {
return Err(FileTableSemanticError::StateConflict);
}
*current = Some(*replacement);
Ok(())
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FileTableMutationReceipt {
pub root: ObjectId,
pub work: WorkCounters,
}
pub type FileTableMutationFailure = OperationFailure<FileTableMutationError>;
pub fn apply_file_table_mutations<S: crate::ImmediateObjectStore>(
store: &S,
root: ObjectId,
mutations: Vec<FileTableMutation>,
maximum_mutations: u32,
limits: DecodeLimits,
budget: WorkBudget,
) -> Result<FileTableMutationReceipt, FileTableMutationFailure> {
for mutation in &mutations {
mutation
.validate()
.map_err(|error| OperationFailure::before_work(error.into()))?;
}
let receipt = persistent_btree::apply::<S, FileTableFormat, FileTableMutation>(
store,
root,
mutations,
maximum_mutations,
limits,
budget,
)
.map_err(|failure| OperationFailure::new(map_error(failure.error), *failure.work))?;
Ok(FileTableMutationReceipt {
root: receipt.root,
work: receipt.work,
})
}
pub async fn apply_file_table_mutations_async<S: crate::AsyncObjectStore>(
store: &S,
root: ObjectId,
mutations: Vec<FileTableMutation>,
maximum_mutations: u32,
limits: DecodeLimits,
budget: WorkBudget,
cancellation: &crate::CancellationToken,
) -> Result<FileTableMutationReceipt, FileTableMutationFailure> {
let receipt = persistent_btree::apply_async::<S, FileTableFormat, FileTableMutation>(
store,
root,
mutations,
maximum_mutations,
limits,
budget,
cancellation,
)
.await
.map_err(|failure| OperationFailure::new(map_error(failure.error), *failure.work))?;
Ok(FileTableMutationReceipt {
root: receipt.root,
work: receipt.work,
})
}
fn map_error(error: persistent_btree::Error<FileTableSemanticError>) -> FileTableMutationError {
match error {
persistent_btree::Error::Empty => FileTableMutationError::Empty,
persistent_btree::Error::TooManyMutations => FileTableMutationError::TooManyMutations,
persistent_btree::Error::WrongRootKind => FileTableMutationError::WrongRootKind,
persistent_btree::Error::InvalidLimits => FileTableMutationError::InvalidLimits,
persistent_btree::Error::PageItemTooLarge => FileTableMutationError::PageItemTooLarge,
persistent_btree::Error::HeightExceeded => FileTableMutationError::HeightExceeded,
persistent_btree::Error::CycleOrAlias => FileTableMutationError::CycleOrAlias,
persistent_btree::Error::ChildBoundsMismatch => FileTableMutationError::ChildBoundsMismatch,
persistent_btree::Error::MutationContract => FileTableMutationError::MutationContract,
persistent_btree::Error::Allocation(AllocationError::Work(error)) => {
FileTableMutationError::Work(error)
}
persistent_btree::Error::AllocationFailed | persistent_btree::Error::Allocation(_) => {
FileTableMutationError::AllocationFailed
}
persistent_btree::Error::Semantic(error) => error.into(),
persistent_btree::Error::Storage(error) => FileTableMutationError::Storage(error),
persistent_btree::Error::Decode(error) => FileTableMutationError::Decode(error),
persistent_btree::Error::Work(error) => FileTableMutationError::Work(error),
}
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
pub enum FileTableSemanticError {
#[error("file-table mutation record is invalid")]
InvalidRecord,
#[error("file-table mutation identities differ")]
IdentityMismatch,
#[error("file-table record already exists")]
AlreadyExists,
#[error("file-table record is missing")]
Missing,
#[error("file-table record state precondition failed")]
StateConflict,
}
#[derive(Debug, Error)]
pub enum FileTableMutationError {
#[error("file-table mutation batch is empty")]
Empty,
#[error("file-table mutation count exceeds its bound")]
TooManyMutations,
#[error("file-table mutation root has the wrong kind")]
WrongRootKind,
#[error("file-table mutation limits are invalid")]
InvalidLimits,
#[error("file-table mutation item exceeds its page byte bound")]
PageItemTooLarge,
#[error("file-table mutation exceeds its height bound")]
HeightExceeded,
#[error("file-table mutation encountered a cycle or alias")]
CycleOrAlias,
#[error("file-table child bounds mismatch")]
ChildBoundsMismatch,
#[error("file-table mutation violated its physical-plan contract")]
MutationContract,
#[error("file-table mutation scratch allocation failed")]
AllocationFailed,
#[error("file-table mutation record is invalid")]
InvalidRecord,
#[error("file-table mutation identities differ")]
IdentityMismatch,
#[error("file-table record already exists")]
AlreadyExists,
#[error("file-table record is missing")]
Missing,
#[error("file-table record state precondition failed")]
StateConflict,
#[error(transparent)]
Storage(#[from] ObjectStoreError),
#[error(transparent)]
Decode(#[from] CanonicalDecodeError),
#[error(transparent)]
Work(#[from] WorkError),
}
impl From<FileTableSemanticError> for FileTableMutationError {
fn from(value: FileTableSemanticError) -> Self {
match value {
FileTableSemanticError::InvalidRecord => Self::InvalidRecord,
FileTableSemanticError::IdentityMismatch => Self::IdentityMismatch,
FileTableSemanticError::AlreadyExists => Self::AlreadyExists,
FileTableSemanticError::Missing => Self::Missing,
FileTableSemanticError::StateConflict => Self::StateConflict,
}
}
}
#[cfg(all(test, feature = "memory"))]
#[path = "tests/file_table_mutation.rs"]
mod tests;