#![allow(dead_code)]
#![allow(unreachable_pub)]
mod builder;
mod dv_conversion;
mod reader;
pub(crate) mod stats;
use std::collections::HashMap;
use bytes::Bytes;
use delta_kernel_derive::ToSchema;
use url::Url;
use crate::engine_data::EngineData;
use crate::expressions::{null_lit, Expression, Scalar, StructData};
use crate::schema::derive_macro_utils::ToDataType;
use crate::schema::{DataType, StructType};
use crate::{KernelError, KernelResult, Version};
pub(crate) const CONTENT_TYPE: &str = "contentType";
pub(crate) const LOCATION: &str = "location";
pub(crate) const FILE_FORMAT: &str = "fileFormat";
pub(crate) const TRACKING: &str = "tracking";
pub(crate) const DV_INFO: &str = "deletionVector";
pub(crate) const PARTITION_SPEC_ID: &str = "specId";
pub(crate) const PARTITION: &str = "partition";
pub(crate) const SORT_ORDER_ID: &str = "sortOrderId";
pub(crate) const RECORD_COUNT: &str = "recordCount";
pub(crate) const FILE_SIZE_IN_BYTES: &str = "fileSizeInBytes";
pub(crate) const CONTENT_STATS_FIELD_NAME: &str = "content_stats";
pub(crate) const MANIFEST_INFO: &str = "manifestInfo";
pub(crate) const KEY_METADATA: &str = "keyMetadata";
pub(crate) const SPLIT_OFFSETS: &str = "splitOffsets";
pub(crate) const EQUALITY_IDS: &str = "equalityIds";
pub(crate) const FORMAT_VERSION: &str = "formatVersion";
pub(crate) const TAGS: &str = "tags";
pub(crate) const TRACKING_STATUS: &str = "status";
pub(crate) const TRACKING_SNAPSHOT_ID: &str = "snapshotId";
pub(crate) const DV_SNAPSHOT_ID: &str = "dvSnapshotId";
pub(crate) const SEQUENCE_NUMBER: &str = "sequenceNumber";
pub(crate) const FILE_SEQUENCE_NUMBER: &str = "fileSequenceNumber";
pub(crate) const FIRST_ROW_ID: &str = "firstRowId";
pub(crate) const LOWER_BOUND: &str = "lower_bound";
pub(crate) const UPPER_BOUND: &str = "upper_bound";
pub(crate) const TIGHT_BOUNDS: &str = "tight_bounds";
pub(crate) const VALUE_COUNT: &str = "value_count";
pub(crate) const NULL_VALUE_COUNT: &str = "null_value_count";
pub(crate) const NAN_VALUE_COUNT: &str = "nan_value_count";
pub(crate) const TOTAL_BYTES: &str = "total_bytes";
pub(super) struct ContentTreeNode {
data: Vec<Box<dyn EngineData>>,
version: Version,
table_root: Url,
path_in_log: String,
}
#[derive(Debug, Clone, ToSchema)]
#[cfg_attr(test, derive(delta_kernel_derive::IntoStructData))]
pub(crate) struct DeletionVectorInfo {
#[field_id = 155]
pub(crate) location: String,
#[field_id = 144]
pub(crate) offset: i64,
#[field_id = 145]
pub(crate) size_in_bytes: i64,
#[field_id = 156]
pub(crate) cardinality: i64,
}
#[derive(Debug, Clone, ToSchema)]
#[cfg_attr(test, derive(delta_kernel_derive::IntoStructData))]
pub struct TrackingInfo {
#[field_id = 0]
pub(crate) status: TrackingStatus,
#[field_id = 1]
pub snapshot_id: Option<i64>,
#[field_id = 5]
pub(crate) dv_snapshot_id: Option<i64>,
#[field_id = 3]
pub(crate) sequence_number: Option<i64>,
#[field_id = 4]
pub(crate) file_sequence_number: Option<i64>,
#[field_id = 142]
pub(crate) first_row_id: Option<i64>,
#[field_id = 6]
pub(crate) deleted_positions: Option<Bytes>,
#[field_id = 7]
pub(crate) replaced_positions: Option<Bytes>,
}
#[derive(Debug, Clone, ToSchema)]
#[cfg_attr(test, derive(delta_kernel_derive::IntoStructData))]
pub(super) struct ContentTreeNodeEntry {
#[field_id = 134]
pub content_type: DataContentType,
#[field_id = 100]
pub location: String,
#[field_id = 101]
pub(crate) file_format: DataFileFormat,
#[field_id = 147]
pub tracking: TrackingInfo,
#[field_id = 148]
pub(crate) deletion_vector: Option<DeletionVectorInfo>,
#[field_id = 141]
pub(crate) spec_id: Option<i32>,
#[skip_schema]
#[field_id = 102]
pub(crate) partition: Option<StructData>,
#[field_id = 140]
pub(crate) sort_order_id: Option<i32>,
#[field_id = 103]
pub(crate) record_count: i64,
#[field_id = 104]
pub(crate) file_size_in_bytes: i64,
#[skip_schema]
#[field_id = 146]
pub(crate) content_stats: Option<StructData>,
#[field_id = 150]
pub(crate) manifest_info: Option<ManifestInfo>,
#[field_id = 131]
pub(crate) key_metadata: Option<Bytes>,
#[field_id = 132]
#[nested_field_id = 133]
pub(crate) split_offsets: Option<Vec<i64>>,
#[field_id = 135]
#[nested_field_id = 136]
pub(crate) equality_ids: Option<Vec<i32>>,
#[field_id = 157]
pub(crate) format_version: i32,
pub(crate) tags: Option<HashMap<String, Option<String>>>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum DataContentType {
Data = 0,
PositionDeletes = 1,
EqualityDeletes = 2,
DataManifest = 3, DeleteManifest = 4, }
impl DataContentType {
pub(crate) fn try_from_repr(value: i32) -> KernelResult<Self> {
match value {
0 => Ok(Self::Data),
1 => Ok(Self::PositionDeletes),
2 => Ok(Self::EqualityDeletes),
3 => Ok(Self::DataManifest),
4 => Ok(Self::DeleteManifest),
other => Err(KernelError::generic(format!(
"Invalid AMT content type value: {other}"
))),
}
}
}
impl ToDataType for DataContentType {
fn to_data_type() -> DataType {
DataType::INTEGER
}
}
impl From<DataContentType> for Scalar {
fn from(value: DataContentType) -> Self {
Scalar::Integer(value as i32)
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum DataFileFormat {
Parquet,
Puffin,
}
impl ToDataType for DataFileFormat {
fn to_data_type() -> DataType {
DataType::STRING
}
}
impl DataFileFormat {
pub(crate) fn name(self) -> &'static str {
match self {
Self::Parquet => "parquet",
Self::Puffin => "puffin",
}
}
}
impl From<DataFileFormat> for Scalar {
fn from(value: DataFileFormat) -> Self {
Scalar::String(value.name().to_string())
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum TrackingStatus {
Existing = 0,
Added = 1,
Deleted = 2,
Replaced = 3,
Modified = 4,
}
impl TrackingStatus {
pub(crate) fn try_from_repr(value: i32) -> KernelResult<Self> {
match value {
0 => Ok(Self::Existing),
1 => Ok(Self::Added),
2 => Ok(Self::Deleted),
3 => Ok(Self::Replaced),
4 => Ok(Self::Modified),
other => Err(KernelError::generic(format!(
"Invalid AMT tracking status value: {other}"
))),
}
}
pub(crate) fn is_live(self) -> bool {
self == Self::Existing || self == Self::Added || self == Self::Modified
}
}
impl ToDataType for TrackingStatus {
fn to_data_type() -> DataType {
DataType::INTEGER
}
}
impl From<TrackingStatus> for Scalar {
fn from(value: TrackingStatus) -> Self {
Scalar::Integer(value as i32)
}
}
#[derive(Debug, Clone, Default, PartialEq, ToSchema)]
#[cfg_attr(test, derive(delta_kernel_derive::IntoStructData))]
pub(crate) struct ManifestInfo {
#[field_id = 504]
pub(crate) added_files_count: i32,
#[field_id = 505]
pub(crate) existing_files_count: i32,
#[field_id = 506]
pub(crate) deleted_files_count: i32,
#[field_id = 520]
pub(crate) replaced_files_count: i32,
#[field_id = 512]
pub(crate) added_rows_count: i64,
#[field_id = 513]
pub(crate) existing_rows_count: i64,
#[field_id = 514]
pub(crate) deleted_rows_count: i64,
#[field_id = 521]
pub(crate) replaced_rows_count: i64,
#[field_id = 516]
pub(crate) min_sequence_number: i64,
#[field_id = 522]
pub(crate) dv: Option<Bytes>,
#[field_id = 523]
pub(crate) dv_cardinality: Option<i64>,
}
fn struct_expr_from_schema(
schema: &StructType,
project: impl Fn(&str) -> Option<Expression>,
) -> KernelResult<Expression> {
let fields = schema
.fields()
.map(|field| match project(field.name().as_str()) {
Some(expr) => Ok(expr),
None if field.is_nullable() => Ok(null_lit(field.data_type().clone())),
None => Err(KernelError::generic(format!(
"struct_expr_from_schema: no projection for required field '{}'",
field.name()
))),
})
.collect::<KernelResult<Vec<_>>>()?;
Ok(Expression::struct_from(fields))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::schema::{ColumnMetadataKey, MetadataValue, StructField, ToSchema};
use crate::unit_test_utils::assert_result_error_with_message;
#[test]
fn struct_expr_from_schema_errors_on_missing_required_field() {
let schema = StructType::new_unchecked([StructField::not_null("req", DataType::INTEGER)]);
assert_result_error_with_message(struct_expr_from_schema(&schema, |_| None), "req");
}
#[test]
fn struct_expr_from_schema_fills_missing_nullable_field_with_typed_null() {
let schema = StructType::new_unchecked([StructField::nullable("opt", DataType::INTEGER)]);
let expr = struct_expr_from_schema(&schema, |_| None).unwrap();
assert_eq!(expr, Expression::struct_from([null_lit(DataType::INTEGER)]));
}
#[rstest]
#[case(0, TrackingStatus::Existing)]
#[case(1, TrackingStatus::Added)]
#[case(2, TrackingStatus::Deleted)]
#[case(3, TrackingStatus::Replaced)]
#[case(4, TrackingStatus::Modified)]
fn tracking_status_try_from_repr_roundtrips(
#[case] repr: i32,
#[case] expected: TrackingStatus,
) {
assert_eq!(TrackingStatus::try_from_repr(repr).unwrap(), expected);
assert_eq!(expected as i32, repr);
}
#[rstest]
#[case(5)]
#[case(-1)]
#[case(i32::MAX)]
fn tracking_status_try_from_repr_rejects_unknown(#[case] repr: i32) {
assert!(TrackingStatus::try_from_repr(repr).is_err());
}
#[rstest]
#[case(0, DataContentType::Data)]
#[case(1, DataContentType::PositionDeletes)]
#[case(2, DataContentType::EqualityDeletes)]
#[case(3, DataContentType::DataManifest)]
#[case(4, DataContentType::DeleteManifest)]
fn data_content_type_try_from_repr_roundtrips(
#[case] repr: i32,
#[case] expected: DataContentType,
) {
assert_eq!(DataContentType::try_from_repr(repr).unwrap(), expected);
assert_eq!(expected as i32, repr);
}
#[rstest]
#[case(5)]
#[case(-1)]
#[case(i32::MAX)]
fn data_content_type_try_from_repr_rejects_unknown(#[case] repr: i32) {
assert!(DataContentType::try_from_repr(repr).is_err());
}
#[test]
fn content_tree_node_entry_schema_field_contract() {
let schema = ContentTreeNodeEntry::to_schema();
let expected: &[(&str, Option<i64>, bool)] = &[
(CONTENT_TYPE, Some(134), false),
(LOCATION, Some(100), false),
(FILE_FORMAT, Some(101), false),
(TRACKING, Some(147), false),
(DV_INFO, Some(148), true),
(PARTITION_SPEC_ID, Some(141), true),
(SORT_ORDER_ID, Some(140), true),
(RECORD_COUNT, Some(103), false),
(FILE_SIZE_IN_BYTES, Some(104), false),
(MANIFEST_INFO, Some(150), true),
(KEY_METADATA, Some(131), true),
(SPLIT_OFFSETS, Some(132), true),
(EQUALITY_IDS, Some(135), true),
(FORMAT_VERSION, Some(157), false),
(TAGS, None, true),
];
let actual_names: Vec<&str> = schema.fields().map(|f| f.name().as_str()).collect();
let expected_names: Vec<&str> = expected.iter().map(|(name, _, _)| *name).collect();
assert_eq!(actual_names, expected_names, "schema fields drifted");
for &(name, field_id, nullable) in expected {
let field = schema.field(name).expect("field present in schema");
assert_eq!(
field.is_nullable(),
nullable,
"nullability mismatch for {name}"
);
assert_eq!(
field
.get_config_value(&ColumnMetadataKey::ParquetFieldId)
.cloned(),
field_id.map(MetadataValue::Number),
"field id mismatch for {name}",
);
}
}
}