use std::collections::hash_map::Entry;
use std::collections::HashMap;
use delta_kernel_derive::internal_api;
use super::file_stats::FileStats;
use crate::actions::{DomainMetadata, SetTransaction};
use crate::{KernelError, KernelResult};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileStatsState {
Complete(FileStats),
Indeterminate,
}
impl FileStatsState {
pub fn file_stats(&self) -> Option<&FileStats> {
match self {
Self::Complete(stats) => Some(stats),
_ => None,
}
}
pub fn is_complete(&self) -> bool {
matches!(self, Self::Complete(_))
}
#[cfg(any(test, feature = "test-utils"))]
pub fn is_indeterminate(&self) -> bool {
self == &Self::Indeterminate
}
}
impl Default for FileStatsState {
fn default() -> Self {
Self::Complete(FileStats::default())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DomainMetadataState {
Complete(HashMap<String, DomainMetadata>),
Partial(HashMap<String, DomainMetadata>),
}
impl Default for DomainMetadataState {
fn default() -> Self {
Self::Partial(HashMap::new())
}
}
impl DomainMetadataState {
#[internal_api]
pub(crate) fn try_complete(
values: impl IntoIterator<Item = DomainMetadata>,
) -> KernelResult<Self> {
Ok(Self::Complete(domain_metadata_map(values, true)?))
}
#[internal_api]
pub(crate) fn try_partial(
values: impl IntoIterator<Item = DomainMetadata>,
) -> KernelResult<Self> {
Ok(Self::Partial(domain_metadata_map(values, false)?))
}
}
fn domain_metadata_map(
values: impl IntoIterator<Item = DomainMetadata>,
reject_tombstones: bool,
) -> KernelResult<HashMap<String, DomainMetadata>> {
let values = values.into_iter();
let mut result = HashMap::with_capacity(values.size_hint().0);
for value in values {
let domain = value.domain().to_string();
if reject_tombstones && value.is_removed() {
return Err(KernelError::generic(format!(
"complete CRC state contains domain-metadata tombstone for {domain}"
)));
}
match result.entry(domain) {
Entry::Vacant(entry) => {
entry.insert(value);
}
Entry::Occupied(entry) => {
return Err(KernelError::generic(format!(
"CRC state contains duplicate domain {}",
entry.key()
)));
}
}
}
Ok(result)
}
#[cfg(any(test, feature = "test-utils"))]
#[allow(clippy::panic)]
impl DomainMetadataState {
pub fn expect_complete(&self) -> &HashMap<String, DomainMetadata> {
match self {
Self::Complete(map) => map,
other => panic!("expected Complete, got {other:?}"),
}
}
pub fn expect_partial(&self) -> &HashMap<String, DomainMetadata> {
match self {
Self::Partial(map) => map,
other => panic!("expected Partial, got {other:?}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SetTransactionState {
Complete(HashMap<String, SetTransaction>),
Partial(HashMap<String, SetTransaction>),
}
impl Default for SetTransactionState {
fn default() -> Self {
Self::Partial(HashMap::new())
}
}
impl SetTransactionState {
#[internal_api]
pub(crate) fn try_complete(
values: impl IntoIterator<Item = SetTransaction>,
) -> KernelResult<Self> {
Ok(Self::Complete(transaction_map(values)?))
}
#[internal_api]
pub(crate) fn try_partial(
values: impl IntoIterator<Item = SetTransaction>,
) -> KernelResult<Self> {
Ok(Self::Partial(transaction_map(values)?))
}
}
fn transaction_map(
values: impl IntoIterator<Item = SetTransaction>,
) -> KernelResult<HashMap<String, SetTransaction>> {
let values = values.into_iter();
let mut result = HashMap::with_capacity(values.size_hint().0);
for value in values {
let app_id = value.app_id.clone();
match result.entry(app_id) {
Entry::Vacant(entry) => {
entry.insert(value);
}
Entry::Occupied(entry) => {
return Err(KernelError::generic(format!(
"CRC state contains duplicate transaction application id {}",
entry.key()
)));
}
}
}
Ok(result)
}
#[cfg(any(test, feature = "test-utils"))]
#[allow(clippy::panic)]
impl SetTransactionState {
pub fn expect_complete(&self) -> &HashMap<String, SetTransaction> {
match self {
Self::Complete(map) => map,
other => panic!("expected Complete, got {other:?}"),
}
}
pub fn expect_partial(&self) -> &HashMap<String, SetTransaction> {
match self {
Self::Partial(map) => map,
other => panic!("expected Partial, got {other:?}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crc::FileSizeHistogram;
#[test]
fn complete_with_histogram_returns_file_stats_with_histogram() {
let state = FileStatsState::Complete(FileStats {
num_files: 10,
table_size_bytes: 5000,
file_size_histogram: Some(FileSizeHistogram::create_default()),
});
let stats = state.file_stats().unwrap();
assert_eq!(stats.num_files(), 10);
assert_eq!(stats.table_size_bytes(), 5000);
assert!(stats.file_size_histogram().is_some());
}
#[test]
fn complete_without_histogram_returns_file_stats_without_histogram() {
let state = FileStatsState::Complete(FileStats {
num_files: 10,
table_size_bytes: 5000,
file_size_histogram: None,
});
let stats = state.file_stats().unwrap();
assert_eq!(stats.num_files(), 10);
assert_eq!(stats.table_size_bytes(), 5000);
assert!(stats.file_size_histogram().is_none());
}
#[test]
fn indeterminate_returns_none_for_file_stats() {
assert!(FileStatsState::Indeterminate.file_stats().is_none());
}
#[test]
fn is_predicates_match_variants() {
let complete = FileStatsState::default();
assert!(complete.is_complete());
assert!(!complete.is_indeterminate());
let indet = FileStatsState::Indeterminate;
assert!(!indet.is_complete());
assert!(indet.is_indeterminate());
}
#[test]
fn default_is_complete_zero() {
assert_eq!(
FileStatsState::default(),
FileStatsState::Complete(FileStats {
num_files: 0,
table_size_bytes: 0,
file_size_histogram: None,
})
);
}
#[test]
fn dm_state_default_is_empty_partial() {
assert_eq!(
DomainMetadataState::default(),
DomainMetadataState::Partial(HashMap::new())
);
}
#[test]
fn partial_domain_metadata_preserves_tombstones_and_rejects_duplicates() {
let tombstone = DomainMetadata::remove("domain".to_string(), "{}".to_string());
let state = DomainMetadataState::try_partial([tombstone]).unwrap();
assert!(state.expect_partial()["domain"].is_removed());
let domains = [
DomainMetadata::new("domain".to_string(), "{}".to_string()),
DomainMetadata::new("domain".to_string(), "{}".to_string()),
];
assert!(DomainMetadataState::try_partial(domains).is_err());
}
#[test]
fn set_txn_state_default_is_empty_partial() {
assert_eq!(
SetTransactionState::default(),
SetTransactionState::Partial(HashMap::new())
);
}
#[test]
fn partial_set_transactions_reject_duplicates() {
let transactions = [
SetTransaction::new("app".to_string(), 1, None),
SetTransaction::new("app".to_string(), 2, None),
];
assert!(SetTransactionState::try_partial(transactions).is_err());
}
}