use std::collections::HashMap;
use std::fmt;
use std::sync::LazyLock;
use delta_kernel_derive::{internal_api, IntoStructData, ToSchema, TryFromStructData};
use derive_more::Constructor;
use serde::de::{MapAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use tracing::warn;
use url::Url;
use visitors::{MetadataVisitor, ProtocolVisitor};
use self::deletion_vector::DeletionVectorDescriptor;
#[cfg(feature = "adaptive-metadata-in-dev")]
use crate::amt_path_util::resolve_amt_location;
#[cfg(feature = "adaptive-metadata-in-dev")]
use crate::expressions::Scalar;
#[cfg(feature = "adaptive-metadata-in-dev")]
use crate::expressions::{ArrayData, StructData};
use crate::schema::{
is_unsupported_delta_type_error, lazy_schema_ref, schema_ref, SchemaRef, StructField,
StructType, ToSchema as _,
};
#[cfg(feature = "adaptive-metadata-in-dev")]
use crate::schema::{schema, ArrayType, DataType};
use crate::table_features::{
FeatureType, TableFeature, LEGACY_READER_FEATURES, MIN_VALID_RW_VERSION,
TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION,
};
use crate::table_properties::TableProperties;
use crate::utils::require;
#[cfg(feature = "adaptive-metadata-in-dev")]
use crate::{create_row, Engine};
use crate::{EngineData, FileMeta, FileSize, KernelError, KernelResult, RowVisitor as _};
const KERNEL_VERSION: &str = env!("CARGO_PKG_VERSION");
const SERDE_JSON_RECURSION_LIMIT_ERROR_PREFIX: &str = "recursion limit exceeded";
const UNKNOWN_OPERATION: &str = "UNKNOWN";
pub(crate) const ROW_TRACKING_PRESERVED_TAG: &str = "delta.rowTracking.preserved";
pub mod deletion_vector;
pub mod deletion_vector_writer;
pub mod set_transaction;
#[cfg(feature = "internal-api")]
pub mod visitors;
#[cfg(not(feature = "internal-api"))]
pub(crate) mod visitors;
#[internal_api]
pub(crate) const ADD_NAME: &str = "add";
#[internal_api]
pub(crate) const REMOVE_NAME: &str = "remove";
#[internal_api]
pub(crate) const METADATA_NAME: &str = "metaData";
#[internal_api]
pub(crate) const PROTOCOL_NAME: &str = "protocol";
#[internal_api]
pub(crate) const SET_TRANSACTION_NAME: &str = "txn";
#[internal_api]
pub(crate) const COMMIT_INFO_NAME: &str = "commitInfo";
#[internal_api]
pub(crate) const CDC_NAME: &str = "cdc";
#[internal_api]
pub(crate) const SIDECAR_NAME: &str = "sidecar";
#[internal_api]
pub(crate) const CHECKPOINT_METADATA_NAME: &str = "checkpointMetadata";
pub(crate) const SIDECAR_FILE_SCHEMA_TAG: &str = "sidecarFileSchema";
#[internal_api]
pub(crate) const DOMAIN_METADATA_NAME: &str = "domainMetadata";
#[cfg(feature = "adaptive-metadata-in-dev")]
#[internal_api]
pub(crate) const CHECKPOINT_ACTION_NAME: &str = "checkpoint";
#[cfg(feature = "adaptive-metadata-in-dev")]
#[internal_api]
pub(crate) const CONTENT_ROOT_NAME: &str = "contentRoot";
pub(crate) const INTERNAL_DOMAIN_PREFIX: &str = "delta.";
pub(crate) fn action_presence_leaf(action_name: &str) -> Option<&'static str> {
match action_name {
ADD_NAME | REMOVE_NAME | CDC_NAME | SIDECAR_NAME => Some("path"),
METADATA_NAME => Some("id"),
PROTOCOL_NAME => Some("minReaderVersion"),
SET_TRANSACTION_NAME => Some("appId"),
DOMAIN_METADATA_NAME => Some("domain"),
CHECKPOINT_METADATA_NAME => Some("version"),
_ => None,
}
}
#[internal_api]
pub(crate) const NUM_RECORDS: &str = "numRecords";
#[internal_api]
pub(crate) const NULL_COUNT: &str = "nullCount";
#[internal_api]
pub(crate) const MIN_VALUES: &str = "minValues";
#[internal_api]
pub(crate) const MAX_VALUES: &str = "maxValues";
#[internal_api]
pub(crate) const TIGHT_BOUNDS: &str = "tightBounds";
#[internal_api]
pub(crate) const STATS_PARSED: &str = "stats_parsed";
pub(crate) static ADD_SCHEMA: LazyLock<StructType> = LazyLock::new(Add::to_schema);
pub(crate) static ADD_FIELD: LazyLock<StructField> =
LazyLock::new(|| StructField::nullable(ADD_NAME, ADD_SCHEMA.clone()));
pub(crate) static REMOVE_FIELD: LazyLock<StructField> =
LazyLock::new(|| StructField::nullable(REMOVE_NAME, Remove::to_schema()));
pub(crate) static METADATA_FIELD: LazyLock<StructField> =
LazyLock::new(|| StructField::nullable(METADATA_NAME, Metadata::to_schema()));
pub(crate) static PROTOCOL_FIELD: LazyLock<StructField> =
LazyLock::new(|| StructField::nullable(PROTOCOL_NAME, Protocol::to_schema()));
pub(crate) static SET_TRANSACTION_FIELD: LazyLock<StructField> =
LazyLock::new(|| StructField::nullable(SET_TRANSACTION_NAME, SetTransaction::to_schema()));
pub(crate) static COMMIT_INFO_FIELD: LazyLock<StructField> =
LazyLock::new(|| StructField::nullable(COMMIT_INFO_NAME, CommitInfo::to_schema()));
pub(crate) static CDC_FIELD: LazyLock<StructField> =
LazyLock::new(|| StructField::nullable(CDC_NAME, Cdc::to_schema()));
pub(crate) static DOMAIN_METADATA_FIELD: LazyLock<StructField> =
LazyLock::new(|| StructField::nullable(DOMAIN_METADATA_NAME, DomainMetadata::to_schema()));
pub(crate) static CHECKPOINT_METADATA_FIELD: LazyLock<StructField> = LazyLock::new(|| {
StructField::nullable(CHECKPOINT_METADATA_NAME, CheckpointMetadata::to_schema())
});
pub(crate) static SIDECAR_FIELD: LazyLock<StructField> =
LazyLock::new(|| StructField::nullable(SIDECAR_NAME, Sidecar::to_schema()));
#[cfg(feature = "adaptive-metadata-in-dev")]
pub(crate) static CONTENT_ROOT_FIELD: LazyLock<StructField> =
LazyLock::new(|| StructField::nullable(CONTENT_ROOT_NAME, ContentRoot::to_schema()));
#[cfg(feature = "adaptive-metadata-in-dev")]
static CONTENT_SIDECAR_FIELD: LazyLock<StructField> = LazyLock::new(|| {
StructField::nullable(
SIDECAR_NAME,
schema! {
not_null "type": STRING,
..(Sidecar::to_schema().into_fields()),
},
)
});
#[cfg(feature = "adaptive-metadata-in-dev")]
static CHECKPOINT_ACTION_ELEMENT_SCHEMA: LazyLock<SchemaRef> = lazy_schema_ref! {
(&CHECKPOINT_METADATA_FIELD),
(&CONTENT_ROOT_FIELD),
(&PROTOCOL_FIELD),
(&METADATA_FIELD),
(&DOMAIN_METADATA_FIELD),
(&SET_TRANSACTION_FIELD),
(&CONTENT_SIDECAR_FIELD),
};
#[cfg(feature = "adaptive-metadata-in-dev")]
pub(crate) static CHECKPOINT_ACTION_FIELD: LazyLock<StructField> = LazyLock::new(|| {
StructField::nullable(
CHECKPOINT_ACTION_NAME,
ArrayType::new(CHECKPOINT_ACTION_ELEMENT_SCHEMA.clone(), false),
)
});
#[cfg(feature = "adaptive-metadata-in-dev")]
pub(crate) static LAST_MANIFEST_COMMIT_FIELD: LazyLock<StructField> =
LazyLock::new(|| StructField::nullable("lastManifestCommit", LastManifestCommit::to_schema()));
fn checkpoint_action_field() -> impl IntoIterator<Item = &'static StructField> {
#[cfg(feature = "adaptive-metadata-in-dev")]
{
Some(&*CHECKPOINT_ACTION_FIELD)
}
#[cfg(not(feature = "adaptive-metadata-in-dev"))]
{
None::<&'static StructField>
}
}
#[cfg(any(test, feature = "internal-api"))]
static COMMIT_SCHEMA: LazyLock<SchemaRef> = lazy_schema_ref! {
(&ADD_FIELD),
(&REMOVE_FIELD),
(&METADATA_FIELD),
(&PROTOCOL_FIELD),
(&SET_TRANSACTION_FIELD),
(&COMMIT_INFO_FIELD),
(&CDC_FIELD),
(&DOMAIN_METADATA_FIELD),
..(checkpoint_action_field()),
};
static ALL_ACTIONS_SCHEMA: LazyLock<SchemaRef> = lazy_schema_ref! {
(&ADD_FIELD),
(&REMOVE_FIELD),
(&METADATA_FIELD),
(&PROTOCOL_FIELD),
(&SET_TRANSACTION_FIELD),
(&COMMIT_INFO_FIELD),
(&CDC_FIELD),
(&DOMAIN_METADATA_FIELD),
..(checkpoint_action_field()),
(&CHECKPOINT_METADATA_FIELD),
(&SIDECAR_FIELD),
};
#[internal_api]
pub(crate) static LOG_ADD_SCHEMA: LazyLock<SchemaRef> = lazy_schema_ref! { (&ADD_FIELD) };
#[internal_api]
pub(crate) static LOG_REMOVE_SCHEMA: LazyLock<SchemaRef> = lazy_schema_ref! { (&REMOVE_FIELD) };
#[internal_api]
pub(crate) static LOG_METADATA_SCHEMA: LazyLock<SchemaRef> = lazy_schema_ref! { (&METADATA_FIELD) };
#[cfg(feature = "adaptive-metadata-in-dev")]
#[internal_api]
pub(crate) static LOG_CHECKPOINT_SCHEMA: LazyLock<SchemaRef> =
lazy_schema_ref! { (&CHECKPOINT_ACTION_FIELD) };
#[internal_api]
pub(crate) static LOG_PROTOCOL_SCHEMA: LazyLock<SchemaRef> = lazy_schema_ref! { (&PROTOCOL_FIELD) };
#[internal_api]
pub(crate) static LOG_COMMIT_INFO_SCHEMA: LazyLock<SchemaRef> =
lazy_schema_ref! { (&COMMIT_INFO_FIELD) };
#[internal_api]
pub(crate) static LOG_TXN_SCHEMA: LazyLock<SchemaRef> =
lazy_schema_ref! { (&SET_TRANSACTION_FIELD) };
#[internal_api]
pub(crate) static LOG_DOMAIN_METADATA_SCHEMA: LazyLock<SchemaRef> =
lazy_schema_ref! { (&DOMAIN_METADATA_FIELD) };
#[cfg(any(test, feature = "internal-api"))]
#[internal_api]
pub(crate) fn get_commit_schema() -> &'static SchemaRef {
&COMMIT_SCHEMA
}
#[internal_api]
#[allow(dead_code)]
pub(crate) fn get_all_actions_schema() -> &'static SchemaRef {
&ALL_ACTIONS_SCHEMA
}
#[internal_api]
pub(crate) fn schema_contains_file_actions(schema: &SchemaRef) -> bool {
schema.contains(ADD_NAME) || schema.contains(REMOVE_NAME)
}
pub(crate) fn as_log_add_schema(add_schema: SchemaRef) -> SchemaRef {
schema_ref! { nullable ADD_NAME: (add_schema) }
}
#[derive(
Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, IntoStructData, TryFromStructData,
)]
#[serde(rename_all = "camelCase")]
#[internal_api]
pub(crate) struct Format {
pub(crate) provider: String,
pub(crate) options: HashMap<String, String>,
}
impl Default for Format {
fn default() -> Self {
Self {
provider: String::from("parquet"),
options: HashMap::new(),
}
}
}
#[derive(
Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, IntoStructData,
)]
#[serde(rename_all = "camelCase")]
pub struct Metadata {
id: String,
name: Option<String>,
description: Option<String>,
format: Format,
schema_string: String,
partition_columns: Vec<String>,
created_time: Option<i64>,
configuration: HashMap<String, String>,
}
impl Metadata {
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
#[allow(clippy::too_many_arguments)]
pub(crate) fn from_parts(
id: String,
name: Option<String>,
description: Option<String>,
format_provider: String,
format_options: HashMap<String, String>,
schema_string: String,
partition_columns: Vec<String>,
created_time: Option<i64>,
configuration: HashMap<String, String>,
) -> Self {
Self {
id,
name,
description,
format: Format {
provider: format_provider,
options: format_options,
},
schema_string,
partition_columns,
created_time,
configuration,
}
}
#[internal_api]
pub(crate) fn try_new(
name: Option<String>,
description: Option<String>,
schema: SchemaRef,
partition_columns: Vec<String>,
created_time: i64,
configuration: HashMap<String, String>,
) -> KernelResult<Self> {
if let Some(metadata_field) = schema.fields().find(|field| field.is_metadata_column()) {
return Err(KernelError::Schema(format!(
"Table schema must not contain metadata columns. Found metadata column: '{}'",
metadata_field.name
)));
}
Ok(Self {
id: uuid::Uuid::new_v4().to_string(),
name,
description,
format: Format::default(),
schema_string: serde_json::to_string(&schema)?,
partition_columns,
created_time: Some(created_time),
configuration,
})
}
#[internal_api]
pub(crate) fn try_new_from_data(data: &dyn EngineData) -> KernelResult<Option<Metadata>> {
let mut visitor = MetadataVisitor::default();
visitor.visit_rows_of(data)?;
Ok(visitor.metadata)
}
#[internal_api]
#[allow(dead_code)]
pub(crate) fn id(&self) -> &str {
&self.id
}
#[internal_api]
#[allow(dead_code)]
pub(crate) fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[internal_api]
#[allow(dead_code)]
pub(crate) fn description(&self) -> Option<&str> {
self.description.as_deref()
}
#[internal_api]
#[allow(dead_code)]
pub(crate) fn created_time(&self) -> Option<i64> {
self.created_time
}
#[internal_api]
pub(crate) fn configuration(&self) -> &HashMap<String, String> {
&self.configuration
}
#[internal_api]
#[allow(dead_code)]
pub(crate) fn format_provider(&self) -> &str {
&self.format.provider
}
#[internal_api]
#[allow(dead_code)]
pub(crate) fn format_options(&self) -> &HashMap<String, String> {
&self.format.options
}
#[internal_api]
pub(crate) fn schema_string(&self) -> &String {
&self.schema_string
}
#[internal_api]
pub(crate) fn parse_schema(&self) -> KernelResult<StructType> {
serde_json::from_str(&self.schema_string).map_err(|error| {
if error.is_syntax()
&& error
.to_string()
.starts_with(SERDE_JSON_RECURSION_LIMIT_ERROR_PREFIX)
{
KernelError::schema(format!(
"Table schema is too deeply nested: decoding metaData.schemaString exceeded \
serde_json's recursion limit: {error}"
))
.with_backtrace()
} else if is_unsupported_delta_type_error(&error) {
KernelError::schema(error.to_string()).with_backtrace()
} else {
error.into()
}
})
}
#[internal_api]
pub(crate) fn partition_columns(&self) -> &[String] {
&self.partition_columns
}
#[internal_api]
pub(crate) fn parse_table_properties(&self) -> TableProperties {
TableProperties::from(self.configuration.iter())
}
pub(crate) fn with_schema(self, schema: SchemaRef) -> KernelResult<Self> {
Ok(Self {
schema_string: serde_json::to_string(&schema)?,
..self
})
}
pub(crate) fn with_configuration_entry(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.configuration.insert(key.into(), value.into());
self
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn new_unchecked(
id: impl Into<String>,
name: Option<String>,
description: Option<String>,
format: Format,
schema_string: impl Into<String>,
partition_columns: Vec<String>,
created_time: Option<i64>,
configuration: HashMap<String, String>,
) -> Self {
Self {
id: id.into(),
name,
description,
format,
schema_string: schema_string.into(),
partition_columns,
created_time,
configuration,
}
}
}
#[derive(
Default, Debug, Clone, PartialEq, Eq, ToSchema, IntoStructData, Serialize, Deserialize,
)]
#[serde(rename_all = "camelCase", try_from = "ProtocolRaw")]
pub struct Protocol {
min_reader_version: i32,
min_writer_version: i32,
#[serde(skip_serializing_if = "Option::is_none")]
reader_features: Option<Vec<TableFeature>>,
#[serde(skip_serializing_if = "Option::is_none")]
writer_features: Option<Vec<TableFeature>>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ProtocolRaw {
min_reader_version: i32,
min_writer_version: i32,
reader_features: Option<Vec<TableFeature>>,
writer_features: Option<Vec<TableFeature>>,
}
impl TryFrom<ProtocolRaw> for Protocol {
type Error = KernelError;
fn try_from(protocol: ProtocolRaw) -> KernelResult<Self> {
Protocol::try_new(
protocol.min_reader_version,
protocol.min_writer_version,
protocol.reader_features,
protocol.writer_features,
)
}
}
fn parse_features(
features: Option<impl IntoIterator<Item = impl Into<TableFeature>>>,
) -> Option<Vec<TableFeature>> {
let features = features?.into_iter().map(Into::into);
Some(features.collect())
}
impl Protocol {
pub(crate) fn try_new_modern(
reader_features: impl IntoIterator<Item = impl Into<TableFeature>>,
writer_features: impl IntoIterator<Item = impl Into<TableFeature>>,
) -> KernelResult<Self> {
Self::try_new(
TABLE_FEATURES_MIN_READER_VERSION,
TABLE_FEATURES_MIN_WRITER_VERSION,
Some(reader_features),
Some(writer_features),
)
}
#[cfg(test)]
pub(crate) fn try_new_legacy(
min_reader_version: i32,
min_writer_version: i32,
) -> KernelResult<Self> {
Self::try_new(
min_reader_version,
min_writer_version,
TableFeature::NO_LIST,
TableFeature::NO_LIST,
)
}
#[internal_api]
pub(crate) fn try_new(
min_reader_version: i32,
min_writer_version: i32,
reader_features: Option<impl IntoIterator<Item = impl Into<TableFeature>>>,
writer_features: Option<impl IntoIterator<Item = impl Into<TableFeature>>>,
) -> KernelResult<Self> {
require!(
min_reader_version >= MIN_VALID_RW_VERSION,
KernelError::InvalidProtocol(format!(
"min_reader_version must be >= {MIN_VALID_RW_VERSION}, got {min_reader_version}"
))
);
require!(
min_writer_version >= MIN_VALID_RW_VERSION,
KernelError::InvalidProtocol(format!(
"min_writer_version must be >= {MIN_VALID_RW_VERSION}, got {min_writer_version}"
))
);
let reader_features = parse_features(reader_features);
let writer_features = parse_features(writer_features);
if min_reader_version == TABLE_FEATURES_MIN_READER_VERSION {
require!(
reader_features.is_some(),
KernelError::invalid_protocol(
"Reader features must be present when minimum reader version = 3"
)
);
} else {
require!(
reader_features.is_none(),
KernelError::invalid_protocol(
"Reader features must not be present when minimum reader version != 3"
)
);
}
if min_writer_version == TABLE_FEATURES_MIN_WRITER_VERSION {
require!(
writer_features.is_some(),
KernelError::invalid_protocol(
"Writer features must be present when minimum writer version = 7"
)
);
} else {
require!(
writer_features.is_none(),
KernelError::invalid_protocol(
"Writer features must not be present when minimum writer version != 7"
)
);
}
match (&reader_features, &writer_features) {
(Some(reader_features), Some(writer_features)) => {
if let Some(offending) = reader_features.iter().find(|feature| {
!matches!(
feature.feature_type(),
FeatureType::ReaderWriter | FeatureType::Unknown
) || !writer_features.contains(*feature)
}) {
return Err(KernelError::invalid_protocol(format!(
"Reader features must contain only ReaderWriter features that are also \
listed in writer features, but {offending:?} is not \
(readerFeatures={reader_features:?}, writerFeatures={writer_features:?}, \
minReaderVersion={min_reader_version}, minWriterVersion={min_writer_version})"
)));
}
let mut legacy_orphans = Vec::new();
for feature in writer_features.iter() {
let orphaned_reader_writer_feature = feature.feature_type()
== FeatureType::ReaderWriter
&& !reader_features.contains(feature);
if !orphaned_reader_writer_feature {
continue;
}
if LEGACY_READER_FEATURES.contains(feature) {
legacy_orphans.push(feature);
} else {
return Err(KernelError::invalid_protocol(format!(
"Writer features must be Writer-only or also listed in reader features, \
but ReaderWriter feature {feature:?} is listed in writerFeatures and \
missing from readerFeatures \
(readerFeatures={reader_features:?}, \
writerFeatures={writer_features:?}, \
minReaderVersion={min_reader_version}, \
minWriterVersion={min_writer_version})"
)));
}
}
for feature in legacy_orphans {
warn!(
"ReaderWriter feature {feature:?} is listed in writerFeatures but \
missing from readerFeatures at minReaderVersion={min_reader_version}; \
treating it as reader-enabled (malformed protocol)"
);
}
Ok(())
}
(None, None) => Ok(()),
(None, Some(writer_features)) => {
if let Some(offending) = writer_features.iter().find(|feature| {
match feature.feature_type() {
FeatureType::WriterOnly | FeatureType::Unknown => false,
FeatureType::ReaderWriter => {
!(min_reader_version == 2 && *feature == &TableFeature::ColumnMapping)
}
}
}) {
return Err(KernelError::invalid_protocol(format!(
"Writer features must be Writer-only or also listed in reader features, \
but ReaderWriter feature {offending:?} is listed in writerFeatures with \
no reader features present \
(writerFeatures={writer_features:?}, minReaderVersion={min_reader_version}, \
minWriterVersion={min_writer_version})"
)));
}
Ok(())
}
(Some(_), None) => Err(KernelError::invalid_protocol(
"Reader features should be present in writer features",
)),
}?;
Ok(Protocol {
min_reader_version,
min_writer_version,
reader_features,
writer_features,
})
}
pub(crate) fn try_new_from_data(data: &dyn EngineData) -> KernelResult<Option<Protocol>> {
let mut visitor = ProtocolVisitor::default();
visitor.visit_rows_of(data)?;
Ok(visitor.protocol)
}
#[internal_api]
pub(crate) fn min_reader_version(&self) -> i32 {
self.min_reader_version
}
#[internal_api]
pub(crate) fn min_writer_version(&self) -> i32 {
self.min_writer_version
}
#[internal_api]
pub(crate) fn reader_features(&self) -> Option<&[TableFeature]> {
self.reader_features.as_deref()
}
#[internal_api]
pub(crate) fn writer_features(&self) -> Option<&[TableFeature]> {
self.writer_features.as_deref()
}
pub(crate) fn has_table_feature(&self, feature: &TableFeature) -> bool {
self.writer_features()
.is_some_and(|features| features.contains(feature))
}
#[cfg(test)]
pub(crate) fn new_unchecked(
min_reader_version: i32,
min_writer_version: i32,
reader_features: Option<Vec<TableFeature>>,
writer_features: Option<Vec<TableFeature>>,
) -> Self {
Self {
min_reader_version,
min_writer_version,
reader_features,
writer_features,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, ToSchema, IntoStructData)]
#[internal_api]
#[cfg_attr(test, derive(Serialize, Default), serde(rename_all = "camelCase"))]
pub(crate) struct CommitInfo {
pub(crate) timestamp: Option<i64>,
pub(crate) in_commit_timestamp: Option<i64>,
pub(crate) operation: Option<String>,
pub(crate) operation_parameters: Option<HashMap<String, Option<String>>>,
pub(crate) operation_metrics: Option<HashMap<String, Option<String>>>,
pub(crate) kernel_version: Option<String>,
pub(crate) is_blind_append: Option<bool>,
pub(crate) engine_info: Option<String>,
pub(crate) txn_id: Option<String>,
pub(crate) tags: Option<HashMap<String, Option<String>>>,
#[cfg(feature = "adaptive-metadata-in-dev")]
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) data_change: Option<bool>,
#[cfg(feature = "adaptive-metadata-in-dev")]
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) last_manifest_commit: Option<LastManifestCommit>,
}
impl CommitInfo {
pub(crate) fn new(
timestamp: i64,
in_commit_timestamp: Option<i64>,
operation: Option<String>,
engine_info: Option<String>,
is_blind_append: bool,
) -> Self {
Self {
timestamp: Some(timestamp),
in_commit_timestamp,
operation: Some(operation.unwrap_or_else(|| UNKNOWN_OPERATION.to_string())),
operation_parameters: Some(HashMap::new()),
operation_metrics: None,
kernel_version: Some(format!("v{KERNEL_VERSION}")),
is_blind_append: is_blind_append.then_some(true),
engine_info,
txn_id: Some(uuid::Uuid::new_v4().to_string()),
tags: None,
#[cfg(feature = "adaptive-metadata-in-dev")]
data_change: None,
#[cfg(feature = "adaptive-metadata-in-dev")]
last_manifest_commit: None,
}
}
pub(crate) fn set_row_tracking_preserved(&mut self) {
self.tags.get_or_insert_default().insert(
ROW_TRACKING_PRESERVED_TAG.to_string(),
Some("true".to_string()),
);
}
pub(crate) fn set_operation_parameters(
&mut self,
operation_parameters: HashMap<String, Option<String>>,
) {
self.operation_parameters = Some(operation_parameters);
}
pub(crate) fn set_operation_metrics(
&mut self,
operation_metrics: HashMap<String, Option<String>>,
) {
self.operation_metrics = Some(operation_metrics);
}
#[cfg(feature = "adaptive-metadata-in-dev")]
pub(crate) fn set_data_change(&mut self, data_change: bool) {
self.data_change = Some(data_change);
}
pub(crate) fn merge_tags(&mut self, tags: Option<HashMap<String, Option<String>>>) {
let Some(tags) = tags else {
return;
};
let current_tags = self.tags.get_or_insert_default();
for (key, value) in tags {
current_tags.entry(key).or_insert(value);
}
}
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[derive(Debug, Clone, PartialEq, Eq, ToSchema, IntoStructData, Deserialize)]
#[cfg_attr(test, derive(Serialize))]
#[serde(rename_all = "camelCase")]
#[internal_api]
pub(crate) struct BackReference {
pub(crate) manifest: String,
pub(crate) pos: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, ToSchema, IntoStructData, Deserialize)]
#[cfg_attr(test, derive(Serialize, Default))]
#[serde(rename_all = "camelCase")]
#[internal_api]
pub(crate) struct Add {
pub(crate) path: String,
#[allow_null_container_values]
#[serde(deserialize_with = "deserialize_partition_values")]
pub(crate) partition_values: HashMap<String, String>,
pub(crate) size: i64,
pub(crate) modification_time: i64,
pub(crate) data_change: bool,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub stats: Option<String>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub tags: Option<HashMap<String, Option<String>>>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub deletion_vector: Option<DeletionVectorDescriptor>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub base_row_id: Option<i64>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub default_row_commit_version: Option<i64>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub clustering_provider: Option<String>,
#[cfg(feature = "adaptive-metadata-in-dev")]
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) back_reference: Option<BackReference>,
}
fn deserialize_partition_values<'de, D>(
deserializer: D,
) -> KernelResult<HashMap<String, String>, D::Error>
where
D: Deserializer<'de>,
{
struct PartitionValuesVisitor;
impl<'de> Visitor<'de> for PartitionValuesVisitor {
type Value = HashMap<String, String>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a map of nullable partition values")
}
fn visit_map<A>(self, mut map: A) -> KernelResult<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut values = HashMap::new();
while let Some((key, value)) = map.next_entry::<String, Option<String>>()? {
match value {
Some(value) => {
values.insert(key, value);
}
None => {
values.remove(&key);
}
}
}
Ok(values)
}
}
deserializer.deserialize_map(PartitionValuesVisitor)
}
impl Add {
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn path(&self) -> &str {
&self.path
}
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn partition_values(&self) -> &HashMap<String, String> {
&self.partition_values
}
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn size(&self) -> i64 {
self.size
}
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn modification_time(&self) -> i64 {
self.modification_time
}
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn data_change(&self) -> bool {
self.data_change
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn back_reference(&self) -> Option<&BackReference> {
self.back_reference.as_ref()
}
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
#[allow(clippy::too_many_arguments)]
pub(crate) fn from_parts(
path: String,
partition_values: HashMap<String, String>,
size: i64,
modification_time: i64,
data_change: bool,
stats: Option<String>,
tags: Option<HashMap<String, Option<String>>>,
deletion_vector: Option<DeletionVectorDescriptor>,
base_row_id: Option<i64>,
default_row_commit_version: Option<i64>,
clustering_provider: Option<String>,
) -> Self {
Self {
path,
partition_values,
size,
modification_time,
data_change,
stats,
tags,
deletion_vector,
base_row_id,
default_row_commit_version,
clustering_provider,
#[cfg(feature = "adaptive-metadata-in-dev")]
back_reference: None,
}
}
#[internal_api]
#[allow(dead_code)]
pub(crate) fn dv_unique_id(
&self,
dv_normalization_root: Option<&Url>,
) -> KernelResult<Option<String>> {
self.deletion_vector
.as_ref()
.map(|dv| {
DeletionVectorDescriptor::unique_id_with_root(
&dv.storage_type.to_string(),
&dv.path_or_inline_dv,
dv.offset,
dv_normalization_root,
)
})
.transpose()
}
}
#[derive(Debug, Clone, PartialEq, Eq, ToSchema)]
#[internal_api]
#[cfg_attr(test, derive(Serialize, Default), serde(rename_all = "camelCase"))]
pub(crate) struct Remove {
pub(crate) path: String,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) deletion_timestamp: Option<i64>,
pub(crate) data_change: bool,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) extended_file_metadata: Option<bool>,
#[allow_null_container_values]
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) partition_values: Option<HashMap<String, String>>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) size: Option<i64>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub stats: Option<String>,
#[allow_null_container_values]
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) tags: Option<HashMap<String, String>>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) deletion_vector: Option<DeletionVectorDescriptor>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) base_row_id: Option<i64>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) default_row_commit_version: Option<i64>,
#[cfg(feature = "adaptive-metadata-in-dev")]
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
pub(crate) back_reference: Option<BackReference>,
}
#[derive(Debug, Clone, PartialEq, Eq, ToSchema)]
#[internal_api]
#[cfg_attr(test, derive(Serialize, Default), serde(rename_all = "camelCase"))]
pub(crate) struct Cdc {
pub path: String,
#[allow_null_container_values]
pub partition_values: HashMap<String, String>,
pub size: i64,
pub data_change: bool,
#[allow_null_container_values]
pub tags: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[internal_api]
#[derive(Constructor, IntoStructData, ToSchema)]
pub(crate) struct SetTransaction {
pub(crate) app_id: String,
pub(crate) version: i64,
pub(crate) last_updated: Option<i64>,
}
impl SetTransaction {
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn app_id(&self) -> &str {
&self.app_id
}
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn version(&self) -> i64 {
self.version
}
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn last_updated(&self) -> Option<i64> {
self.last_updated
}
pub(crate) fn is_expired(&self, expiration_timestamp: Option<i64>) -> bool {
matches!(
(expiration_timestamp, self.last_updated),
(Some(exp_ts), Some(lu)) if lu <= exp_ts
)
}
pub(crate) fn non_expired_version(&self, expiration_timestamp: Option<i64>) -> Option<i64> {
(!self.is_expired(expiration_timestamp)).then_some(self.version)
}
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[derive(Debug, Clone, PartialEq, Eq, ToSchema, IntoStructData, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[internal_api]
#[cfg_attr(test, derive(Default))]
pub(crate) struct ContentRoot {
pub(crate) path: String,
size_in_bytes: i64,
version: i64,
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[derive(Debug, Clone, PartialEq, Eq, ToSchema, IntoStructData, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[internal_api]
pub(crate) struct LastManifestCommit {
pub(crate) version: i64,
pub(crate) content_root_version: i64,
}
#[cfg(feature = "adaptive-metadata-in-dev")]
impl LastManifestCommit {
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn new(version: i64, content_root_version: i64) -> KernelResult<Self> {
let last_manifest_commit = LastManifestCommit {
version,
content_root_version,
};
last_manifest_commit.validate()?;
Ok(last_manifest_commit)
}
pub(crate) fn validate(&self) -> KernelResult<()> {
require!(
self.content_root_version >= 0,
KernelError::generic(format!(
"lastManifestCommit contentRootVersion {} must be non-negative",
self.content_root_version
))
);
require!(
self.content_root_version <= self.version,
KernelError::generic(format!(
"lastManifestCommit contentRootVersion {} exceeds version {}",
self.content_root_version, self.version
))
);
Ok(())
}
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[internal_api]
pub(crate) struct CheckpointAction {
pub(crate) version: i64,
pub(crate) content_root: ContentRoot,
pub(crate) protocol: Protocol,
pub(crate) metadata: Metadata,
pub(crate) transactions: Vec<SetTransaction>,
pub(crate) domain_metadata: Vec<DomainMetadata>,
pub(crate) txn_sidecars: Vec<Sidecar>,
pub(crate) domain_metadata_sidecars: Vec<Sidecar>,
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[internal_api]
pub(crate) enum CheckpointActionElement {
CheckpointMetadata(CheckpointMetadata),
ContentRoot(ContentRoot),
Protocol(Protocol),
#[serde(rename = "metaData")]
Metadata(Metadata),
DomainMetadata(DomainMetadata),
Txn(SetTransaction),
Sidecar(CheckpointSidecar),
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[internal_api]
pub(crate) struct CheckpointSidecar {
#[serde(rename = "type")]
pub(crate) sidecar_type: String,
#[serde(flatten)]
pub(crate) sidecar: Sidecar,
}
#[cfg(feature = "adaptive-metadata-in-dev")]
impl Serialize for CheckpointAction {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> KernelResult<S::Ok, S::Error> {
self.validate().map_err(serde::ser::Error::custom)?;
let checkpoint_metadata = CheckpointMetadata {
version: self.version,
tags: None,
};
let mut elements = vec![
CheckpointActionElement::CheckpointMetadata(checkpoint_metadata),
CheckpointActionElement::ContentRoot(self.content_root.clone()),
CheckpointActionElement::Protocol(self.protocol.clone()),
CheckpointActionElement::Metadata(self.metadata.clone()),
];
elements.extend(
self.transactions
.iter()
.cloned()
.map(CheckpointActionElement::Txn),
);
elements.extend(
self.domain_metadata
.iter()
.cloned()
.map(CheckpointActionElement::DomainMetadata),
);
let sidecar_element = |type_str: &str, sidecar: &Sidecar| {
CheckpointActionElement::Sidecar(CheckpointSidecar {
sidecar_type: type_str.to_string(),
sidecar: sidecar.clone(),
})
};
elements.extend(
self.txn_sidecars
.iter()
.map(|s| sidecar_element(SET_TRANSACTION_NAME, s)),
);
elements.extend(
self.domain_metadata_sidecars
.iter()
.map(|s| sidecar_element(DOMAIN_METADATA_NAME, s)),
);
elements.serialize(serializer)
}
}
#[cfg(feature = "adaptive-metadata-in-dev")]
impl<'de> Deserialize<'de> for CheckpointAction {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> KernelResult<Self, D::Error> {
let elements = Vec::<CheckpointActionElement>::deserialize(deserializer)?;
Self::from_elements(elements).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "adaptive-metadata-in-dev")]
fn content_sidecar_element(type_str: &str, sidecar: Sidecar) -> KernelResult<Scalar> {
let sidecar: StructData = sidecar.into();
let fields = std::iter::once(StructField::not_null("type", DataType::STRING))
.chain(sidecar.fields().iter().cloned());
let values = std::iter::once(Scalar::from(type_str))
.chain(sidecar.values().iter().cloned())
.collect();
let fields = StructType::try_new(fields)?.into_fields().collect();
Ok(Scalar::Struct(StructData::try_new(fields, values)?))
}
#[cfg(feature = "adaptive-metadata-in-dev")]
fn checkpoint_action_union_element(field_name: &str, value: Scalar) -> KernelResult<Scalar> {
let fields: Vec<StructField> = CHECKPOINT_ACTION_ELEMENT_SCHEMA.fields().cloned().collect();
require!(
fields.iter().any(|f| f.name() == field_name),
KernelError::generic(format!(
"checkpoint union element field {field_name:?} not found in element schema"
))
);
let values = fields
.iter()
.map(|field| {
if field.name() == field_name {
value.clone()
} else {
Scalar::null(field.data_type().clone())
}
})
.collect();
Ok(Scalar::Struct(StructData::try_new(fields, values)?))
}
#[cfg(feature = "adaptive-metadata-in-dev")]
impl CheckpointAction {
fn try_into_scalar(self) -> KernelResult<Scalar> {
self.validate()?;
let checkpoint_metadata = CheckpointMetadata {
version: self.version,
tags: None,
};
let mut elements = vec![
checkpoint_action_union_element(CHECKPOINT_METADATA_NAME, checkpoint_metadata.into())?,
checkpoint_action_union_element(CONTENT_ROOT_NAME, self.content_root.into())?,
checkpoint_action_union_element(PROTOCOL_NAME, self.protocol.into())?,
checkpoint_action_union_element(METADATA_NAME, self.metadata.into())?,
];
for txn in self.transactions {
elements.push(checkpoint_action_union_element(
SET_TRANSACTION_NAME,
txn.into(),
)?);
}
for dm in self.domain_metadata {
elements.push(checkpoint_action_union_element(
DOMAIN_METADATA_NAME,
dm.into(),
)?);
}
for sidecar in self.txn_sidecars {
let element = content_sidecar_element(SET_TRANSACTION_NAME, sidecar)?;
elements.push(checkpoint_action_union_element(SIDECAR_NAME, element)?);
}
for sidecar in self.domain_metadata_sidecars {
let element = content_sidecar_element(DOMAIN_METADATA_NAME, sidecar)?;
elements.push(checkpoint_action_union_element(SIDECAR_NAME, element)?);
}
let array_type = ArrayType::new(CHECKPOINT_ACTION_ELEMENT_SCHEMA.clone(), false);
Ok(Scalar::Array(ArrayData::try_new(array_type, elements)?))
}
}
#[cfg(feature = "adaptive-metadata-in-dev")]
impl ContentRoot {
pub(crate) fn new(path: String, size_in_bytes: i64, version: i64) -> Self {
ContentRoot {
path,
size_in_bytes,
version,
}
}
}
#[cfg(feature = "adaptive-metadata-in-dev")]
impl CheckpointAction {
#[internal_api]
pub(crate) fn new(
version: i64,
content_root: ContentRoot,
protocol: Protocol,
metadata: Metadata,
transactions: Vec<SetTransaction>,
domain_metadata: Vec<DomainMetadata>,
) -> Self {
CheckpointAction {
version,
content_root,
protocol,
metadata,
transactions,
domain_metadata,
txn_sidecars: vec![],
domain_metadata_sidecars: vec![],
}
}
#[internal_api]
pub(crate) fn into_engine_data(self, engine: &dyn Engine) -> KernelResult<Box<dyn EngineData>> {
create_row(
engine,
LOG_CHECKPOINT_SCHEMA.clone(),
self.try_into_scalar()?,
)
}
#[internal_api]
pub(crate) fn try_new_from_data(
data: &dyn EngineData,
) -> KernelResult<Option<CheckpointAction>> {
let mut visitor = visitors::CheckpointVisitor::default();
visitor.visit_rows_of(data)?;
Ok(visitor.checkpoint)
}
fn from_elements(elements: Vec<CheckpointActionElement>) -> KernelResult<Self> {
fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> KernelResult<()> {
require!(
slot.replace(value).is_none(),
KernelError::generic(format!("duplicate `{name}` element in checkpoint action"))
);
Ok(())
}
let mut version = None;
let mut content_root = None;
let mut protocol = None;
let mut metadata = None;
let mut transactions = Vec::new();
let mut domain_metadata = Vec::new();
let mut txn_sidecars = Vec::new();
let mut domain_metadata_sidecars = Vec::new();
for element in elements {
match element {
CheckpointActionElement::CheckpointMetadata(cm) => {
set_once(&mut version, cm.version, CHECKPOINT_METADATA_NAME)?
}
CheckpointActionElement::ContentRoot(cr) => {
set_once(&mut content_root, cr, CONTENT_ROOT_NAME)?
}
CheckpointActionElement::Protocol(p) => set_once(&mut protocol, p, PROTOCOL_NAME)?,
CheckpointActionElement::Metadata(m) => set_once(&mut metadata, m, METADATA_NAME)?,
CheckpointActionElement::Txn(t) => transactions.push(t),
CheckpointActionElement::DomainMetadata(dm) => domain_metadata.push(dm),
CheckpointActionElement::Sidecar(cs) => match cs.sidecar_type.as_str() {
SET_TRANSACTION_NAME => txn_sidecars.push(cs.sidecar),
DOMAIN_METADATA_NAME => domain_metadata_sidecars.push(cs.sidecar),
other => {
return Err(KernelError::generic(format!(
"checkpoint sidecar has unsupported type `{other}`"
)))
}
},
}
}
let missing = |field: &str| {
KernelError::generic(format!(
"checkpoint action is missing required `{field}` element"
))
};
let action = CheckpointAction {
version: version.ok_or_else(|| missing(CHECKPOINT_METADATA_NAME))?,
content_root: content_root.ok_or_else(|| missing(CONTENT_ROOT_NAME))?,
protocol: protocol.ok_or_else(|| missing(PROTOCOL_NAME))?,
metadata: metadata.ok_or_else(|| missing(METADATA_NAME))?,
transactions,
domain_metadata,
txn_sidecars,
domain_metadata_sidecars,
};
action.validate()?;
Ok(action)
}
fn validate(&self) -> KernelResult<()> {
require!(
self.content_root.version <= self.version,
KernelError::generic(format!(
"checkpoint contentRoot.version {} exceeds checkpointMetadata.version {}",
self.content_root.version, self.version
))
);
Ok(())
}
#[internal_api]
pub(crate) fn path(&self) -> &str {
&self.content_root.path
}
#[internal_api]
pub(crate) fn content_root_version(&self) -> i64 {
self.content_root.version
}
#[internal_api]
pub(crate) fn version(&self) -> i64 {
self.version
}
#[internal_api]
pub(crate) fn root_filemeta(&self, table_root: &Url) -> KernelResult<FileMeta> {
let content_root = &self.content_root;
Ok(FileMeta {
location: resolve_amt_location(&content_root.path, table_root)?,
last_modified: i64::MAX,
size: to_file_size(content_root.size_in_bytes, "checkpoint contentRoot")?,
})
}
#[internal_api]
pub(crate) fn protocol(&self) -> &Protocol {
&self.protocol
}
#[internal_api]
pub(crate) fn metadata(&self) -> &Metadata {
&self.metadata
}
}
#[derive(ToSchema, IntoStructData, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[internal_api]
pub(crate) struct Sidecar {
pub path: String,
pub size_in_bytes: i64,
pub modification_time: i64,
#[allow_null_container_values]
pub tags: Option<HashMap<String, String>>,
}
fn to_file_size(bytes: i64, context: &str) -> KernelResult<FileSize> {
bytes.try_into().map_err(|_| {
KernelError::generic(format!(
"Failed to convert {context} size {bytes} to FileSize"
))
})
}
impl Sidecar {
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn new(
path: String,
size_in_bytes: i64,
modification_time: i64,
tags: Option<HashMap<String, String>>,
) -> Self {
Self {
path,
size_in_bytes,
modification_time,
tags,
}
}
pub(crate) fn to_filemeta(&self, log_root: &Url) -> KernelResult<FileMeta> {
Ok(FileMeta {
location: log_root.join("_sidecars/")?.join(&self.path)?,
last_modified: self.modification_time,
size: to_file_size(self.size_in_bytes, "sidecar")?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[internal_api]
#[derive(Constructor, IntoStructData, ToSchema)]
pub(crate) struct CheckpointMetadata {
pub(crate) version: i64,
#[serde(skip_serializing_if = "Option::is_none")]
#[allow_null_container_values]
pub(crate) tags: Option<HashMap<String, String>>,
}
impl CheckpointMetadata {
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn version(&self) -> i64 {
self.version
}
#[internal_api]
#[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
pub(crate) fn tags(&self) -> Option<&HashMap<String, String>> {
self.tags.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, IntoStructData)]
pub struct DomainMetadata {
domain: String,
configuration: String,
removed: bool,
}
impl DomainMetadata {
#[internal_api]
pub(crate) fn new(domain: String, configuration: String) -> Self {
Self {
domain,
configuration,
removed: false,
}
}
#[internal_api]
pub(crate) fn remove(domain: String, configuration: String) -> Self {
Self {
domain,
configuration,
removed: true,
}
}
#[allow(unused)]
#[internal_api]
pub(crate) fn is_internal(&self) -> bool {
self.domain.starts_with(INTERNAL_DOMAIN_PREFIX)
}
pub fn domain(&self) -> &str {
&self.domain
}
pub fn configuration(&self) -> &str {
&self.configuration
}
pub fn is_removed(&self) -> bool {
self.removed
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use rstest::rstest;
use serde_json::json;
use super::*;
use crate::arrow::array::{
Array, Int32Array, ListBuilder, RecordBatch, StringBuilder, StructArray,
};
use crate::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
use crate::arrow::json::ReaderBuilder;
use crate::engine::arrow_data::EngineDataArrowExt as _;
use crate::engine::arrow_expression::ArrowEvaluationHandler;
#[cfg(feature = "adaptive-metadata-in-dev")]
use crate::engine::to_json_bytes;
#[cfg(feature = "adaptive-metadata-in-dev")]
use crate::engine_data::FilteredEngineData;
use crate::expressions::{Scalar, StructData};
use crate::schema::{schema, schema_ref, DataType, MapType, StructField};
use crate::unit_test_utils::assert_result_error_with_message;
use crate::{
create_row, Engine, EvaluationHandler, JsonHandler, ParquetHandler, StorageHandler,
};
#[rstest]
#[case::add(ADD_NAME, Some("path"))]
#[case::remove(REMOVE_NAME, Some("path"))]
#[case::metadata(METADATA_NAME, Some("id"))]
#[case::protocol(PROTOCOL_NAME, Some("minReaderVersion"))]
#[case::transaction(SET_TRANSACTION_NAME, Some("appId"))]
#[case::cdc(CDC_NAME, Some("path"))]
#[case::domain_metadata(DOMAIN_METADATA_NAME, Some("domain"))]
#[case::checkpoint_metadata(CHECKPOINT_METADATA_NAME, Some("version"))]
#[case::sidecar(SIDECAR_NAME, Some("path"))]
#[case::witnessless_action(COMMIT_INFO_NAME, None)]
#[case::unknown_action("futureAction", None)]
fn test_action_presence_leaf(#[case] action_name: &str, #[case] expected_leaf: Option<&str>) {
assert_eq!(action_presence_leaf(action_name), expected_leaf);
}
struct ExprEngine(Arc<dyn EvaluationHandler>);
impl ExprEngine {
fn new() -> Self {
ExprEngine(Arc::new(ArrowEvaluationHandler))
}
}
impl Engine for ExprEngine {
fn evaluation_handler(&self) -> Arc<dyn EvaluationHandler> {
self.0.clone()
}
fn json_handler(&self) -> Arc<dyn JsonHandler> {
unimplemented!()
}
fn parquet_handler(&self) -> Arc<dyn ParquetHandler> {
unimplemented!()
}
fn storage_handler(&self) -> Arc<dyn StorageHandler> {
unimplemented!()
}
}
#[rstest]
#[case::no_expiration_configured(None, Some(1000), false)]
#[case::null_last_updated_never_expires(Some(5000), None, false)]
#[case::both_none(None, None, false)]
#[case::last_updated_before_expiration(Some(2000), Some(1000), true)]
#[case::last_updated_at_expiration(Some(1000), Some(1000), true)]
#[case::last_updated_after_expiration(Some(2000), Some(3000), false)]
fn test_set_transaction_expiration(
#[case] expiration_timestamp: Option<i64>,
#[case] last_updated: Option<i64>,
#[case] expired: bool,
) {
let txn = SetTransaction::new("app".to_string(), 7, last_updated);
assert_eq!(txn.is_expired(expiration_timestamp), expired);
assert_eq!(
txn.non_expired_version(expiration_timestamp),
(!expired).then_some(7)
);
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_last_manifest_commit_schema() {
let expected = schema! {
not_null "version": LONG,
not_null "contentRootVersion": LONG,
};
assert_eq!(LastManifestCommit::to_schema(), expected);
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[rstest]
#[case::equal(5, 5, true)]
#[case::content_root_older(5, 3, true)]
#[case::content_root_newer(3, 5, false)]
fn test_last_manifest_commit_new_validates(
#[case] version: i64,
#[case] content_root_version: i64,
#[case] ok: bool,
) {
let result = LastManifestCommit::new(version, content_root_version);
assert_eq!(result.is_ok(), ok);
}
#[test]
fn test_metadata_schema() {
let schema = get_commit_schema()
.project(&[METADATA_NAME])
.expect("Couldn't get metaData field");
let expected = schema_ref! {
nullable "metaData": {
not_null "id": STRING,
nullable "name": STRING,
nullable "description": STRING,
not_null "format": {
not_null "provider": STRING,
not_null "options": { STRING => not_null STRING },
},
not_null "schemaString": STRING,
not_null "partitionColumns": [ not_null STRING ],
nullable "createdTime": LONG,
not_null "configuration": { STRING => not_null STRING },
},
};
assert_eq!(schema, expected);
}
#[rstest]
#[case::supported(41, false)]
#[case::exceeded(42, true)]
fn parse_schema_nesting_boundary(#[case] depth: usize, #[case] exceeds_limit: bool) {
let metadata = Metadata {
schema_string: serde_json::to_string(&nested_schema(depth)).unwrap(),
..Default::default()
};
let result = metadata.parse_schema();
if exceeds_limit {
assert_result_error_with_message(
result.as_ref(),
concat!(
"Schema error: Table schema is too deeply nested: decoding ",
"metaData.schemaString exceeded serde_json's ",
"recursion limit: recursion limit exceeded"
),
);
let error = match result.unwrap_err() {
KernelError::Backtraced { source, .. } => *source,
error => error,
};
assert!(matches!(error, KernelError::Schema(_)));
} else {
result.unwrap();
}
}
#[rstest]
#[case::malformed_syntax("{", "MalformedJson")]
#[case::malformed_bad_decimal(
r#"{"type":"struct","fields":[{"name":"t","type":"decimal(nope)","nullable":true,"metadata":{}}]}"#,
"MalformedJson"
)]
#[case::malformed_value_echoes_prefix(
r#"{"type":"struct","fields":[{"name":"t","type":"string","nullable":"Unsupported Delta table type","metadata":{}}]}"#,
"MalformedJson"
)]
#[case::unsupported_time(
r#"{"type":"struct","fields":[{"name":"t","type":"time(6)","nullable":true,"metadata":{}}]}"#,
"Schema"
)]
#[case::unsupported_interval(
r#"{"type":"struct","fields":[{"name":"t","type":"interval week","nullable":true,"metadata":{}}]}"#,
"Schema"
)]
#[case::unsupported_nested(
r#"{"type":"struct","fields":[{"name":"t","type":{"type":"struct","fields":[{"name":"inner","type":"time(6)","nullable":true,"metadata":{}}]},"nullable":true,"metadata":{}}]}"#,
"Schema"
)]
#[case::unsupported_complex(
r#"{"type":"struct","fields":[{"name":"t","type":{"type":"matrix"},"nullable":true,"metadata":{}}]}"#,
"Schema"
)]
fn parse_schema_error_classification(
#[case] schema_string: &str,
#[case] expected_error: &str,
) {
let metadata = Metadata {
schema_string: schema_string.to_string(),
..Default::default()
};
let error = match metadata.parse_schema().unwrap_err() {
KernelError::Backtraced { source, .. } => *source,
error => error,
};
match expected_error {
"MalformedJson" => {
assert!(
matches!(error, KernelError::MalformedJson(_)),
"got: {error:?}"
)
}
"Schema" => {
assert!(matches!(error, KernelError::Schema(_)), "got: {error:?}")
}
other => panic!("unknown expected_error discriminant: {other}"),
}
}
fn nested_schema(depth: usize) -> StructType {
(0..depth).fold(
schema! { nullable "leaf": INTEGER },
|nested, depth| schema! { nullable (format!("level_{depth}")): (nested) },
)
}
#[test]
fn test_add_schema() {
let schema = get_commit_schema()
.project(&[ADD_NAME])
.expect("Couldn't get add field");
#[cfg(feature = "adaptive-metadata-in-dev")]
let expected = schema_ref! {
nullable "add": {
not_null "path": STRING,
not_null "partitionValues": { STRING => nullable STRING },
not_null "size": LONG,
not_null "modificationTime": LONG,
not_null "dataChange": BOOLEAN,
nullable "stats": STRING,
nullable "tags": { STRING => nullable STRING },
(deletion_vector_field()),
nullable "baseRowId": LONG,
nullable "defaultRowCommitVersion": LONG,
nullable "clusteringProvider": STRING,
nullable "backReference": (BackReference::to_schema()),
},
};
#[cfg(not(feature = "adaptive-metadata-in-dev"))]
let expected = schema_ref! {
nullable "add": {
not_null "path": STRING,
not_null "partitionValues": { STRING => nullable STRING },
not_null "size": LONG,
not_null "modificationTime": LONG,
not_null "dataChange": BOOLEAN,
nullable "stats": STRING,
nullable "tags": { STRING => nullable STRING },
(deletion_vector_field()),
nullable "baseRowId": LONG,
nullable "defaultRowCommitVersion": LONG,
nullable "clusteringProvider": STRING,
},
};
assert_eq!(schema, expected);
}
fn tags_field() -> StructField {
StructField::nullable(
"tags",
MapType::new(DataType::STRING, DataType::STRING, true),
)
}
fn partition_values_field() -> StructField {
StructField::nullable(
"partitionValues",
MapType::new(DataType::STRING, DataType::STRING, true),
)
}
fn deletion_vector_field() -> StructField {
StructField::nullable(
"deletionVector",
schema! {
not_null "storageType": STRING,
not_null "pathOrInlineDv": STRING,
nullable "offset": INTEGER,
not_null "sizeInBytes": INTEGER,
not_null "cardinality": LONG,
},
)
}
#[test]
fn test_remove_schema() {
let schema = get_commit_schema()
.project(&[REMOVE_NAME])
.expect("Couldn't get remove field");
#[cfg(feature = "adaptive-metadata-in-dev")]
let expected = schema_ref! {
nullable "remove": {
not_null "path": STRING,
nullable "deletionTimestamp": LONG,
not_null "dataChange": BOOLEAN,
nullable "extendedFileMetadata": BOOLEAN,
(partition_values_field()),
nullable "size": LONG,
nullable "stats": STRING,
(tags_field()),
(deletion_vector_field()),
nullable "baseRowId": LONG,
nullable "defaultRowCommitVersion": LONG,
nullable "backReference": (BackReference::to_schema()),
},
};
#[cfg(not(feature = "adaptive-metadata-in-dev"))]
let expected = schema_ref! {
nullable "remove": {
not_null "path": STRING,
nullable "deletionTimestamp": LONG,
not_null "dataChange": BOOLEAN,
nullable "extendedFileMetadata": BOOLEAN,
(partition_values_field()),
nullable "size": LONG,
nullable "stats": STRING,
(tags_field()),
(deletion_vector_field()),
nullable "baseRowId": LONG,
nullable "defaultRowCommitVersion": LONG,
},
};
assert_eq!(schema, expected);
}
#[test]
fn test_cdc_schema() {
let schema = get_commit_schema()
.project(&[CDC_NAME])
.expect("Couldn't get cdc field");
let expected = schema_ref! {
nullable "cdc": {
not_null "path": STRING,
not_null "partitionValues": { STRING => nullable STRING },
not_null "size": LONG,
not_null "dataChange": BOOLEAN,
(tags_field()),
},
};
assert_eq!(schema, expected);
}
#[test]
fn test_sidecar_schema() {
let schema = Sidecar::to_schema();
let expected = schema! {
not_null "path": STRING,
not_null "sizeInBytes": LONG,
not_null "modificationTime": LONG,
(tags_field()),
};
assert_eq!(schema, expected);
}
#[test]
fn test_checkpoint_metadata_schema() {
let schema = get_all_actions_schema()
.project(&[CHECKPOINT_METADATA_NAME])
.expect("Couldn't get checkpointMetadata field");
let expected = schema_ref! {
nullable "checkpointMetadata": {
not_null "version": LONG,
(tags_field()),
},
};
assert_eq!(schema, expected);
}
#[test]
fn test_transaction_schema() {
let schema = get_commit_schema()
.project(&["txn"])
.expect("Couldn't get transaction field");
let expected = schema_ref! {
nullable "txn": {
not_null "appId": STRING,
not_null "version": LONG,
nullable "lastUpdated": LONG,
},
};
assert_eq!(schema, expected);
}
#[test]
fn test_commit_info_schema() {
let schema = get_commit_schema()
.project(&["commitInfo"])
.expect("Couldn't get commitInfo field");
#[cfg(feature = "adaptive-metadata-in-dev")]
let expected = schema_ref! {
nullable "commitInfo": {
nullable "timestamp": LONG,
nullable "inCommitTimestamp": LONG,
nullable "operation": STRING,
nullable "operationParameters": { STRING => nullable STRING },
nullable "operationMetrics": { STRING => nullable STRING },
nullable "kernelVersion": STRING,
nullable "isBlindAppend": BOOLEAN,
nullable "engineInfo": STRING,
nullable "txnId": STRING,
nullable "tags": { STRING => nullable STRING },
nullable "dataChange": BOOLEAN,
nullable "lastManifestCommit": {
not_null "version": LONG,
not_null "contentRootVersion": LONG,
},
},
};
#[cfg(not(feature = "adaptive-metadata-in-dev"))]
let expected = schema_ref! {
nullable "commitInfo": {
nullable "timestamp": LONG,
nullable "inCommitTimestamp": LONG,
nullable "operation": STRING,
nullable "operationParameters": { STRING => nullable STRING },
nullable "operationMetrics": { STRING => nullable STRING },
nullable "kernelVersion": STRING,
nullable "isBlindAppend": BOOLEAN,
nullable "engineInfo": STRING,
nullable "txnId": STRING,
nullable "tags": { STRING => nullable STRING },
},
};
assert_eq!(schema, expected);
}
#[test]
fn test_domain_metadata_schema() {
let schema = get_commit_schema()
.project(&[DOMAIN_METADATA_NAME])
.expect("Couldn't get domainMetadata field");
let expected = schema_ref! {
nullable "domainMetadata": {
not_null "domain": STRING,
not_null "configuration": STRING,
not_null "removed": BOOLEAN,
},
};
assert_eq!(schema, expected);
}
#[test]
fn test_validate_protocol() {
let invalid_protocols = [
Protocol {
min_reader_version: 3,
min_writer_version: 7,
reader_features: None,
writer_features: Some(vec![]),
},
Protocol {
min_reader_version: 3,
min_writer_version: 7,
reader_features: Some(vec![]),
writer_features: None,
},
Protocol {
min_reader_version: 3,
min_writer_version: 7,
reader_features: None,
writer_features: None,
},
];
for Protocol {
min_reader_version,
min_writer_version,
reader_features,
writer_features,
} in invalid_protocols
{
assert!(matches!(
Protocol::try_new(
min_reader_version,
min_writer_version,
reader_features,
writer_features
),
Err(KernelError::InvalidProtocol(_)),
));
}
}
#[rstest]
#[case(0, 1)]
#[case(1, 0)]
#[case(-1, 2)]
#[case(1, -1)]
fn reject_protocol_version_below_minimum(#[case] rv: i32, #[case] wv: i32) {
let expected = if rv < 1 {
format!("Invalid protocol action in the delta log: min_reader_version must be >= 1, got {rv}")
} else {
format!("Invalid protocol action in the delta log: min_writer_version must be >= 1, got {wv}")
};
assert_result_error_with_message(
Protocol::try_new(rv, wv, TableFeature::NO_LIST, TableFeature::NO_LIST),
&expected,
);
}
#[test]
fn accept_min_versions() {
let p = Protocol::try_new_legacy(1, 1).unwrap();
assert_eq!(p.min_reader_version(), 1);
assert_eq!(p.min_writer_version(), 1);
}
#[test]
fn test_validate_table_features_invalid() {
let invalid_features = [
(
vec![TableFeature::DeletionVectors],
vec![TableFeature::AppendOnly],
"Reader features must contain only ReaderWriter features that are also listed in writer features",
),
(
vec![TableFeature::DeletionVectors],
vec![],
"Reader features must contain only ReaderWriter features that are also listed in writer features",
),
(
vec![],
vec![TableFeature::DeletionVectors],
"Writer features must be Writer-only or also listed in reader features",
),
(
vec![TableFeature::VariantType],
vec![
TableFeature::VariantType,
TableFeature::DeletionVectors,
],
"Writer features must be Writer-only or also listed in reader features",
),
(
vec![TableFeature::AppendOnly],
vec![TableFeature::AppendOnly],
"Reader features must contain only ReaderWriter features that are also listed in writer features",
),
];
for (reader_features, writer_features, error_msg) in invalid_features {
let res = Protocol::try_new_modern(reader_features, writer_features);
assert!(
matches!(
&res,
Err(KernelError::InvalidProtocol(error)) if error.to_string().contains(error_msg)
),
"Expected message containing:\t{error_msg}\nBut got:{res:?}\n"
);
}
}
#[test]
fn test_validate_table_features_unknown() {
let protocol = Protocol::try_new_modern(
vec![TableFeature::Unknown("unknown_reader".to_string())],
vec![TableFeature::Unknown("unknown_reader".to_string())],
);
assert!(protocol.is_ok());
let protocol = Protocol::try_new_modern(
TableFeature::EMPTY_LIST,
vec![TableFeature::Unknown("unknown_writer".to_string())],
);
assert!(protocol.is_ok());
}
#[test]
fn test_validate_table_features_valid() {
let valid_features = [
(
vec![TableFeature::DeletionVectors],
vec![TableFeature::DeletionVectors],
),
(vec![], vec![TableFeature::AppendOnly]),
(
vec![TableFeature::VariantType],
vec![TableFeature::VariantType, TableFeature::AppendOnly],
),
(
vec![TableFeature::Unknown("rw".to_string())],
vec![
TableFeature::Unknown("rw".to_string()),
TableFeature::Unknown("w".to_string()),
],
),
(vec![], vec![]),
];
for (reader_features, writer_features) in valid_features {
assert!(Protocol::try_new_modern(reader_features, writer_features).is_ok());
}
}
#[test]
fn test_validate_legacy_column_mapping_valid() {
let protocol = Protocol::try_new(
2,
7,
TableFeature::NO_LIST,
Some(vec![TableFeature::ColumnMapping]),
);
assert!(protocol.is_ok());
}
#[test]
fn test_validate_legacy_writer_only_features_valid() {
let protocol = Protocol::try_new(
1,
7,
TableFeature::NO_LIST,
Some(vec![TableFeature::AppendOnly]),
);
assert!(protocol.is_ok());
}
#[test]
fn test_validate_legacy_column_mapping_with_writer_features_valid() {
let protocol = Protocol::try_new(
2,
7,
TableFeature::NO_LIST,
Some(vec![TableFeature::AppendOnly, TableFeature::ColumnMapping]),
);
assert!(protocol.is_ok());
}
#[test]
fn test_validate_column_mapping_reader_v1_invalid() {
let protocol = Protocol::try_new(
1,
7,
TableFeature::NO_LIST,
Some(vec![TableFeature::ColumnMapping]),
);
assert!(protocol.is_err());
}
#[test]
fn test_validate_multiple_readerwriter_features_reader_v2_invalid() {
let protocol = Protocol::try_new(
2,
7,
TableFeature::NO_LIST,
Some(vec![
TableFeature::ColumnMapping,
TableFeature::DeletionVectors,
]),
);
assert!(protocol.is_err());
}
#[test]
fn test_parse_table_feature_never_fails() {
let features = Some(["", "absurD_)(+13%^⚙️"]);
let expected = Some(FromIterator::from_iter([
TableFeature::unknown(""),
TableFeature::unknown("absurD_)(+13%^⚙️"),
]));
assert_eq!(parse_features(features), expected);
}
#[test]
fn test_metadata_try_new() {
let schema = schema_ref! { not_null "id": INTEGER };
let config = HashMap::from([("key1".to_string(), "value1".to_string())]);
let metadata = Metadata::try_new(
Some("test_table".to_string()),
Some("description".to_string()),
schema.clone(),
vec!["year".to_string()],
1234567890,
config.clone(),
)
.unwrap();
assert!(!metadata.id.is_empty());
assert_eq!(metadata.name, Some("test_table".to_string()));
assert_eq!(
metadata.schema_string,
serde_json::to_string(&schema).unwrap()
);
assert_eq!(metadata.created_time, Some(1234567890));
assert_eq!(metadata.configuration, config);
}
#[test]
fn test_metadata_try_new_default() {
let schema = schema_ref! { not_null "id": INTEGER };
let metadata = Metadata::try_new(None, None, schema, vec![], 0, HashMap::new()).unwrap();
assert!(!metadata.id.is_empty());
assert_eq!(metadata.name, None);
assert_eq!(metadata.description, None);
}
#[test]
fn test_metadata_unique_ids() {
let schema = schema_ref! { not_null "id": INTEGER };
let m1 = Metadata::try_new(None, None, schema.clone(), vec![], 0, HashMap::new()).unwrap();
let m2 = Metadata::try_new(None, None, schema, vec![], 0, HashMap::new()).unwrap();
assert_ne!(m1.id, m2.id);
}
#[rstest]
#[case::typical(HashMap::from([
("path".to_string(), "/delta/table".to_string()),
("compressionType".to_string(), "snappy".to_string()),
]))]
#[case::empty(HashMap::new())]
#[case::special_characters(HashMap::from([
("path".to_string(), "/path/with spaces".to_string()),
("unicode".to_string(), "测试🎉".to_string()),
("empty".to_string(), String::new()),
]))]
fn test_format_scalar_round_trip(#[case] options: HashMap<String, String>) {
let format = Format {
provider: "parquet".to_string(),
options: options.clone(),
};
let scalar = Scalar::from(format.clone());
let Scalar::Struct(struct_data) = &scalar else {
panic!("Expected struct scalar, got {scalar}");
};
let field_names: Vec<_> = struct_data.fields().iter().map(|f| f.name()).collect();
assert_eq!(field_names, ["provider", "options"]);
assert_eq!(struct_data.values()[0], Scalar::from("parquet"));
let Scalar::Map(map_data) = &struct_data.values()[1] else {
panic!("Expected map options");
};
assert_eq!(map_data.pairs().len(), options.len());
assert_eq!(Format::try_from(scalar).unwrap(), format);
}
#[test]
fn test_format_default() {
let format = Format::default();
let expected = Format {
provider: "parquet".to_string(),
options: HashMap::new(),
};
assert_eq!(format, expected);
}
#[test]
fn test_metadata_with_log_schema() {
let engine = ExprEngine::new();
let schema = schema_ref! { not_null "id": INTEGER };
let metadata = Metadata::try_new(
Some("table".to_string()),
None, schema,
vec![],
456,
HashMap::new(),
)
.unwrap();
let metadata_id = metadata.id.clone();
let commit_schema = LOG_METADATA_SCHEMA.clone();
let actual = create_row(&engine, commit_schema, metadata)
.unwrap()
.try_into_record_batch()
.unwrap();
let expected_json = json!({
"metaData": {
"id": metadata_id,
"name": "table",
"format": {
"provider": "parquet",
"options": {}
},
"schemaString": "{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":false,\"metadata\":{}}]}",
"partitionColumns": [],
"createdTime": 456,
"configuration": {}
}
}).to_string();
let expected = ReaderBuilder::new(actual.schema())
.build(expected_json.as_bytes())
.unwrap()
.next()
.unwrap()
.unwrap();
assert_eq!(actual, expected);
}
#[test]
fn test_protocol_creates_log_row() {
let engine = ExprEngine::new();
let protocol = Protocol::try_new_modern(
[TableFeature::DeletionVectors, TableFeature::ColumnMapping],
[TableFeature::DeletionVectors, TableFeature::ColumnMapping],
)
.unwrap();
let list_field = Arc::new(Field::new("element", ArrowDataType::Utf8, false));
let protocol_fields = vec![
Field::new("minReaderVersion", ArrowDataType::Int32, false),
Field::new("minWriterVersion", ArrowDataType::Int32, false),
Field::new(
"readerFeatures",
ArrowDataType::List(list_field.clone()),
true, ),
Field::new(
"writerFeatures",
ArrowDataType::List(list_field.clone()),
true, ),
];
let string_builder = StringBuilder::new();
let mut list_builder = ListBuilder::new(string_builder).with_field(list_field.clone());
list_builder.values().append_value("deletionVectors");
list_builder.values().append_value("columnMapping");
list_builder.append(true);
let reader_features_array = list_builder.finish();
let string_builder = StringBuilder::new();
let mut list_builder = ListBuilder::new(string_builder).with_field(list_field.clone());
list_builder.values().append_value("deletionVectors");
list_builder.values().append_value("columnMapping");
list_builder.append(true);
let writer_features_array = list_builder.finish();
let commit_schema = LOG_PROTOCOL_SCHEMA.clone();
let engine_data = create_row(&engine, commit_schema, protocol);
let schema = Arc::new(Schema::new(vec![Field::new(
"protocol",
ArrowDataType::Struct(protocol_fields.into()),
true,
)]));
let expected = RecordBatch::try_new(
schema,
vec![Arc::new(StructArray::from(vec![
(
Arc::new(Field::new("minReaderVersion", ArrowDataType::Int32, false)),
Arc::new(Int32Array::from(vec![3])) as Arc<dyn Array>,
),
(
Arc::new(Field::new("minWriterVersion", ArrowDataType::Int32, false)),
Arc::new(Int32Array::from(vec![7])) as Arc<dyn Array>,
),
(
Arc::new(Field::new(
"readerFeatures",
ArrowDataType::List(list_field.clone()),
true,
)),
Arc::new(reader_features_array) as Arc<dyn Array>,
),
(
Arc::new(Field::new(
"writerFeatures",
ArrowDataType::List(list_field),
true,
)),
Arc::new(writer_features_array) as Arc<dyn Array>,
),
]))],
)
.unwrap();
let record_batch = engine_data.try_into_record_batch().unwrap();
assert_eq!(record_batch, expected);
}
#[test]
fn test_schema_contains_file_actions_with_add() {
let schema = get_commit_schema()
.project(&[ADD_NAME, PROTOCOL_NAME])
.unwrap();
assert!(schema_contains_file_actions(&schema));
assert!(schema_contains_file_actions(
&schema.project(&[ADD_NAME]).unwrap()
));
}
#[test]
fn test_schema_contains_file_actions_with_remove() {
let schema = get_commit_schema()
.project(&[REMOVE_NAME, METADATA_NAME])
.unwrap();
assert!(schema_contains_file_actions(&schema));
assert!(schema_contains_file_actions(
&schema.project(&[REMOVE_NAME]).unwrap()
));
}
#[test]
fn test_schema_contains_file_actions_with_both() {
let schema = get_commit_schema()
.project(&[ADD_NAME, REMOVE_NAME])
.unwrap();
assert!(schema_contains_file_actions(&schema));
}
#[test]
fn test_schema_contains_file_actions_with_neither() {
let schema = get_commit_schema()
.project(&[PROTOCOL_NAME, METADATA_NAME])
.unwrap();
assert!(!schema_contains_file_actions(&schema));
}
#[test]
fn test_schema_contains_file_actions_empty_schema() {
let schema = schema_ref! {};
assert!(!schema_contains_file_actions(&schema));
}
#[test]
fn test_add_tags_deserialization_null_case() {
let json1 = r#"{"path":"file1.parquet","partitionValues":{},"size":100,"modificationTime":1234567890,"dataChange":true,"tags":null}"#;
let add1: Add = serde_json::from_str(json1).unwrap();
assert_eq!(add1.tags, None);
}
#[test]
fn test_add_tags_deserialization_nullable_values_case() {
let json2 = r#"{"path":"file2.parquet","partitionValues":{},"size":200,"modificationTime":1234567890,"dataChange":true,"tags":{"INSERTION_TIME":"1677811178336000","NULLABLE_TAG":null}}"#;
let add2: Add = serde_json::from_str(json2).unwrap();
assert!(add2.tags.is_some());
let tags = add2.tags.unwrap();
assert_eq!(tags.len(), 2);
assert_eq!(
tags.get("INSERTION_TIME"),
Some(&Some("1677811178336000".to_string()))
);
assert_eq!(tags.get("NULLABLE_TAG"), Some(&None));
}
#[test]
fn test_add_tags_deserialization_non_null_values_case() {
let json3 = r#"{"path":"file3.parquet","partitionValues":{},"size":300,"modificationTime":1234567890,"dataChange":true,"tags":{"INSERTION_TIME":"1677811178336000","MIN_INSERTION_TIME":"1677811178336000"}}"#;
let add3: Add = serde_json::from_str(json3).unwrap();
assert!(add3.tags.is_some());
let tags = add3.tags.unwrap();
assert_eq!(tags.len(), 2);
assert_eq!(
tags.get("INSERTION_TIME"),
Some(&Some("1677811178336000".to_string()))
);
assert_eq!(
tags.get("MIN_INSERTION_TIME"),
Some(&Some("1677811178336000".to_string()))
);
}
#[test]
fn test_add_deserializes_complete_wire_shape() {
let json = r#"{
"path":"file.parquet",
"partitionValues":{"present":"value","null_partition":null},
"size":300,
"modificationTime":1234567890,
"dataChange":false,
"stats":"{\"numRecords\":1}",
"tags":{"tag":"value","nullable":null},
"deletionVector":{
"storageType":"i",
"pathOrInlineDv":"",
"sizeInBytes":0,
"cardinality":0
},
"baseRowId":10,
"defaultRowCommitVersion":20,
"clusteringProvider":"liquid",
"backReference":{"manifest":"manifest.parquet","pos":3}
}"#;
let add: Add = serde_json::from_str(json).unwrap();
let data: StructData = add.clone().into();
StructData::try_new(data.fields().to_vec(), data.values().to_vec())
.expect("complete add must match its derived schema");
assert_eq!(
add.partition_values,
HashMap::from([("present".to_string(), "value".to_string())])
);
assert_eq!(add.stats.as_deref(), Some(r#"{"numRecords":1}"#));
assert_eq!(
add.tags,
Some(HashMap::from([
("tag".to_string(), Some("value".to_string())),
("nullable".to_string(), None),
]))
);
assert_eq!(
add.deletion_vector.as_ref().unwrap().storage_type,
deletion_vector::DeletionVectorStorageType::Inline
);
assert_eq!(add.base_row_id, Some(10));
assert_eq!(add.default_row_commit_version, Some(20));
assert_eq!(add.clustering_provider.as_deref(), Some("liquid"));
#[cfg(feature = "adaptive-metadata-in-dev")]
assert_eq!(
add.back_reference(),
Some(&BackReference {
manifest: "manifest.parquet".to_string(),
pos: 3,
})
);
}
#[test]
fn test_add_partition_values_duplicate_key_uses_last_value() {
let json = r#"{
"path":"file.parquet",
"partitionValues":{"part":"value","part":null},
"size":1,
"modificationTime":0,
"dataChange":false
}"#;
let add: Add = serde_json::from_str(json).unwrap();
assert!(add.partition_values.is_empty());
#[cfg(feature = "adaptive-metadata-in-dev")]
assert!(add.back_reference().is_none());
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_checkpoint_action_schema() {
let schema = get_commit_schema()
.project(&[CHECKPOINT_ACTION_NAME])
.unwrap();
let checkpoint_field = schema.field(CHECKPOINT_ACTION_NAME).unwrap();
assert!(checkpoint_field.is_nullable());
let array = match checkpoint_field.data_type() {
DataType::Array(array) => array,
other => panic!("Expected array, got {other:?}"),
};
assert!(!array.contains_null());
let element = match array.element_type() {
DataType::Struct(s) => s,
other => panic!("Expected struct element, got {other:?}"),
};
let field_names: Vec<&str> = element.fields().map(|f| f.name.as_str()).collect();
assert_eq!(
field_names,
vec![
CHECKPOINT_METADATA_NAME,
CONTENT_ROOT_NAME,
PROTOCOL_NAME,
METADATA_NAME,
DOMAIN_METADATA_NAME,
SET_TRANSACTION_NAME,
SIDECAR_NAME,
]
);
assert!(!field_names.contains(&COMMIT_INFO_NAME));
for field in element.fields() {
assert!(field.is_nullable(), "{} should be nullable", field.name);
assert!(matches!(field.data_type(), DataType::Struct(_)));
}
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_content_sidecar_is_type_prefixed_sidecar() {
let DataType::Struct(content_sidecar) = CONTENT_SIDECAR_FIELD.data_type() else {
panic!("content sidecar should be a struct");
};
let expected: Vec<StructField> =
std::iter::once(StructField::not_null("type", DataType::STRING))
.chain(Sidecar::to_schema().into_fields())
.collect();
let actual: Vec<StructField> = content_sidecar.fields().cloned().collect();
assert_eq!(actual, expected);
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[rstest]
#[case::relative_path(
"memory:///table/",
"metadata/root.parquet",
2048,
"memory:///table/metadata/root.parquet",
Ok(2048)
)]
#[case::negative_size(
"memory:///table/",
"metadata/root.parquet",
-1,
"memory:///table/metadata/root.parquet",
Err("Failed to convert checkpoint contentRoot size -1")
)]
fn test_checkpoint_action_root_filemeta(
#[case] table_root: &str,
#[case] path: &str,
#[case] size_in_bytes: i64,
#[case] expected_location: &str,
#[case] expected: KernelResult<FileSize, &str>,
) {
let table_root = Url::parse(table_root).unwrap();
let checkpoint_action = CheckpointAction {
version: 1,
content_root: ContentRoot {
path: path.to_string(),
size_in_bytes,
version: 1,
},
protocol: Protocol::new_unchecked(1, 2, None, None),
metadata: Metadata::default(),
transactions: Vec::new(),
domain_metadata: Vec::new(),
txn_sidecars: Vec::new(),
domain_metadata_sidecars: Vec::new(),
};
let result = checkpoint_action.root_filemeta(&table_root);
match expected {
Ok(expected_size) => {
let file_meta = result.unwrap();
assert_eq!(file_meta.location.as_str(), expected_location);
assert_eq!(file_meta.size, expected_size);
assert_eq!(file_meta.last_modified, i64::MAX);
}
Err(expected_message) => assert_result_error_with_message(result, expected_message),
}
}
#[cfg(feature = "adaptive-metadata-in-dev")]
fn sample_checkpoint_action() -> CheckpointAction {
let sidecar = |path: &str| Sidecar {
path: path.to_string(),
size_in_bytes: 100,
modification_time: 1,
tags: Some(HashMap::from([("tag".to_string(), "value".to_string())])),
};
CheckpointAction {
version: 42,
content_root: ContentRoot {
path: "s3://bucket/manifest".to_string(),
size_in_bytes: 1024,
version: 40,
},
protocol: Protocol::new_unchecked(1, 2, None, None),
metadata: Metadata::default(),
transactions: vec![SetTransaction {
app_id: "myApp".to_string(),
version: 3,
last_updated: None,
}],
domain_metadata: vec![DomainMetadata {
domain: "myDomain".to_string(),
configuration: "cfg".to_string(),
removed: false,
}],
txn_sidecars: vec![sidecar("txn-sidecar.parquet")],
domain_metadata_sidecars: vec![sidecar("dm-sidecar.parquet")],
}
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_checkpoint_action_scalar_round_trip() -> KernelResult<()> {
let engine = ExprEngine::new();
let action = sample_checkpoint_action();
let scalar = action.clone().try_into_scalar()?;
let data = create_row(&engine, LOG_CHECKPOINT_SCHEMA.clone(), scalar)?;
let back = CheckpointAction::try_new_from_data(data.as_ref())?
.expect("checkpoint action should round-trip");
assert_eq!(action, back);
Ok(())
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_checkpoint_action_scalar_rejects_invalid_content_root_version() {
let base = sample_checkpoint_action();
let action = CheckpointAction {
content_root: ContentRoot {
version: base.version + 1,
..base.content_root
},
..sample_checkpoint_action()
};
let result = action.try_into_scalar();
assert_result_error_with_message(result, "exceeds checkpointMetadata.version");
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_checkpoint_action_wire_format() -> KernelResult<()> {
let engine = ExprEngine::new();
let scalar = sample_checkpoint_action().try_into_scalar()?;
let data = create_row(&engine, LOG_CHECKPOINT_SCHEMA.clone(), scalar)?;
let filtered = FilteredEngineData::with_all_rows_selected(data);
let bytes = to_json_bytes(std::iter::once(Ok(filtered)))?;
let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(
json,
json!({ "checkpoint": [
{ "checkpointMetadata": { "version": 42 } },
{ "contentRoot": { "path": "s3://bucket/manifest", "sizeInBytes": 1024, "version": 40 } },
{ "protocol": { "minReaderVersion": 1, "minWriterVersion": 2 } },
{ "metaData": {
"id": "",
"format": { "provider": "parquet", "options": {} },
"schemaString": "",
"partitionColumns": [],
"configuration": {},
} },
{ "txn": { "appId": "myApp", "version": 3 } },
{ "domainMetadata": { "domain": "myDomain", "configuration": "cfg", "removed": false } },
{ "sidecar": { "type": "txn", "path": "txn-sidecar.parquet", "sizeInBytes": 100, "modificationTime": 1, "tags": { "tag": "value" } } },
{ "sidecar": { "type": "domainMetadata", "path": "dm-sidecar.parquet", "sizeInBytes": 100, "modificationTime": 1, "tags": { "tag": "value" } } },
] })
);
Ok(())
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_try_new_from_data_returns_none_when_no_checkpoint_action() -> KernelResult<()> {
let data = crate::unit_test_utils::action_batch();
assert!(CheckpointAction::try_new_from_data(data.as_ref())?.is_none());
Ok(())
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_checkpoint_action_round_trip_multiple_and_empty_collections() -> KernelResult<()> {
let sidecar = |path: &str| Sidecar {
path: path.to_string(),
size_in_bytes: 1,
modification_time: 2,
tags: None,
};
let action = CheckpointAction {
version: 10,
content_root: ContentRoot {
path: "s3://bucket/manifest".to_string(),
size_in_bytes: 8,
version: 8,
},
protocol: Protocol::new_unchecked(1, 2, None, None),
metadata: Metadata::default(),
transactions: vec![
SetTransaction {
app_id: "a1".to_string(),
version: 1,
last_updated: None,
},
SetTransaction {
app_id: "a2".to_string(),
version: 2,
last_updated: None,
},
],
domain_metadata: vec![
DomainMetadata {
domain: "d1".to_string(),
configuration: "c1".to_string(),
removed: false,
},
DomainMetadata {
domain: "d2".to_string(),
configuration: "c2".to_string(),
removed: true,
},
],
txn_sidecars: vec![sidecar("t1.parquet"), sidecar("t2.parquet")],
domain_metadata_sidecars: vec![],
};
let engine = ExprEngine::new();
let scalar = action.clone().try_into_scalar()?;
let data = create_row(&engine, LOG_CHECKPOINT_SCHEMA.clone(), scalar)?;
let back = CheckpointAction::try_new_from_data(data.as_ref())?
.expect("checkpoint action should round-trip");
assert_eq!(action, back);
Ok(())
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_checkpoint_action_round_trip_protocol_with_features() -> KernelResult<()> {
let action = CheckpointAction {
protocol: Protocol::new_unchecked(
3,
7,
Some(vec![TableFeature::AdaptiveMetadataPreview]),
Some(vec![TableFeature::AdaptiveMetadataPreview]),
),
..sample_checkpoint_action()
};
let engine = ExprEngine::new();
let scalar = action.clone().try_into_scalar()?;
let data = create_row(&engine, LOG_CHECKPOINT_SCHEMA.clone(), scalar)?;
let back = CheckpointAction::try_new_from_data(data.as_ref())?
.expect("checkpoint action should round-trip");
assert_eq!(action, back);
Ok(())
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_checkpoint_action_serde_round_trip() {
let action = sample_checkpoint_action();
let json = serde_json::to_value(&action).unwrap();
assert!(json.is_array(), "checkpoint action serializes to an array");
let back: CheckpointAction = serde_json::from_value(json).unwrap();
assert_eq!(back, action);
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_checkpoint_action_serde_rejects_invalid_content_root_version() {
let base = sample_checkpoint_action();
let action = CheckpointAction {
content_root: ContentRoot {
version: base.version + 1,
..base.content_root
},
..sample_checkpoint_action()
};
let err = serde_json::to_value(&action).expect_err("invalid action must not serialize");
assert!(
err.to_string()
.contains("exceeds checkpointMetadata.version"),
"expected content-root-version error, got: {err}"
);
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_checkpoint_action_serde_omits_checkpoint_metadata_tags() {
let json = serde_json::to_value(sample_checkpoint_action()).unwrap();
let checkpoint_metadata = json
.as_array()
.and_then(|elements| elements.first())
.and_then(|element| element.get("checkpointMetadata"))
.expect("first element is checkpointMetadata");
assert_eq!(checkpoint_metadata, &json!({ "version": 42 }));
assert!(
checkpoint_metadata.get("tags").is_none(),
"checkpointMetadata must omit the tags key, got: {checkpoint_metadata}"
);
}
#[cfg(feature = "adaptive-metadata-in-dev")]
mod checkpoint_serde_elements {
pub(super) const CHECKPOINT_METADATA: &str = r#"{"checkpointMetadata":{"version":42}}"#;
pub(super) const CONTENT_ROOT: &str =
r#"{"contentRoot":{"path":"p","sizeInBytes":1,"version":40}}"#;
pub(super) const PROTOCOL: &str =
r#"{"protocol":{"minReaderVersion":1,"minWriterVersion":2}}"#;
pub(super) const METADATA: &str = r#"{"metaData":{"id":"id","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[]}","partitionColumns":[],"configuration":{}}}"#;
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[rstest]
#[case::duplicate_metadata(&[
checkpoint_serde_elements::CHECKPOINT_METADATA, checkpoint_serde_elements::CONTENT_ROOT,
checkpoint_serde_elements::PROTOCOL, checkpoint_serde_elements::METADATA,
checkpoint_serde_elements::METADATA,
], "duplicate `metaData` element in checkpoint action")]
#[case::duplicate_checkpoint_metadata(&[
checkpoint_serde_elements::CHECKPOINT_METADATA, checkpoint_serde_elements::CHECKPOINT_METADATA,
checkpoint_serde_elements::CONTENT_ROOT, checkpoint_serde_elements::PROTOCOL,
checkpoint_serde_elements::METADATA,
], "duplicate `checkpointMetadata` element in checkpoint action")]
#[case::missing_protocol(&[
checkpoint_serde_elements::CHECKPOINT_METADATA, checkpoint_serde_elements::CONTENT_ROOT,
checkpoint_serde_elements::METADATA,
], "checkpoint action is missing required `protocol` element")]
#[case::missing_content_root(&[
checkpoint_serde_elements::CHECKPOINT_METADATA, checkpoint_serde_elements::PROTOCOL,
checkpoint_serde_elements::METADATA,
], "checkpoint action is missing required `contentRoot` element")]
#[case::missing_metadata(&[
checkpoint_serde_elements::CHECKPOINT_METADATA, checkpoint_serde_elements::CONTENT_ROOT,
checkpoint_serde_elements::PROTOCOL,
], "checkpoint action is missing required `metaData` element")]
#[case::empty_array(&[], "checkpoint action is missing required `checkpointMetadata` element")]
#[case::bad_sidecar_type(&[
checkpoint_serde_elements::CHECKPOINT_METADATA, checkpoint_serde_elements::CONTENT_ROOT,
checkpoint_serde_elements::PROTOCOL, checkpoint_serde_elements::METADATA,
r#"{"sidecar":{"type":"bogus","path":"s.parquet","sizeInBytes":1,"modificationTime":0}}"#,
], "checkpoint sidecar has unsupported type `bogus`")]
#[case::content_root_version_too_high(&[
checkpoint_serde_elements::CHECKPOINT_METADATA,
r#"{"contentRoot":{"path":"p","sizeInBytes":1,"version":99}}"#,
checkpoint_serde_elements::PROTOCOL, checkpoint_serde_elements::METADATA,
], "checkpoint contentRoot.version 99 exceeds checkpointMetadata.version 42")]
fn test_checkpoint_action_serde_fold_errors(
#[case] elements: &[&str],
#[case] expected_msg: &str,
) {
let array = format!("[{}]", elements.join(","));
let err = serde_json::from_str::<CheckpointAction>(&array)
.expect_err("checkpoint action should fail to fold");
assert!(
err.to_string().contains(expected_msg),
"expected error containing {expected_msg:?}, got: {err}"
);
}
#[cfg(feature = "adaptive-metadata-in-dev")]
#[test]
fn test_checkpoint_action_serde_fails_closed_on_unknown_element() {
let array = format!(
"[{},{},{},{},{}]",
checkpoint_serde_elements::CHECKPOINT_METADATA,
checkpoint_serde_elements::CONTENT_ROOT,
checkpoint_serde_elements::PROTOCOL,
checkpoint_serde_elements::METADATA,
r#"{"someNewAction":{"foo":1}}"#,
);
let err = serde_json::from_str::<CheckpointAction>(&array)
.expect_err("unknown element must fail the parse");
assert!(
err.to_string().contains("unknown variant"),
"expected an unknown-variant error, got: {err}"
);
}
}