use arrow::datatypes::DataType;
use datafusion::common::ScalarValue;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PartitionTransform {
Identity,
Year,
Month,
Day,
Hour,
Bucket(u32),
Unknown(String),
}
impl PartitionTransform {
pub fn parse(transform: &str) -> Self {
let trimmed = transform.trim();
match trimmed.to_ascii_lowercase().as_str() {
"identity" => return PartitionTransform::Identity,
"year" => return PartitionTransform::Year,
"month" => return PartitionTransform::Month,
"day" => return PartitionTransform::Day,
"hour" => return PartitionTransform::Hour,
_ => {},
}
if let Some(rest) = trimmed
.strip_prefix("bucket(")
.or_else(|| trimmed.strip_prefix("BUCKET("))
&& let Some(inner) = rest.strip_suffix(')')
&& let Ok(n) = inner.trim().parse::<u32>()
{
return PartitionTransform::Bucket(n);
}
PartitionTransform::Unknown(trimmed.to_string())
}
pub fn to_catalog_string(&self) -> String {
match self {
PartitionTransform::Identity => "identity".to_string(),
PartitionTransform::Year => "year".to_string(),
PartitionTransform::Month => "month".to_string(),
PartitionTransform::Day => "day".to_string(),
PartitionTransform::Hour => "hour".to_string(),
PartitionTransform::Bucket(n) => format!("bucket({n})"),
PartitionTransform::Unknown(s) => s.clone(),
}
}
pub fn is_producible(&self) -> bool {
matches!(
self,
PartitionTransform::Identity
| PartitionTransform::Year
| PartitionTransform::Month
| PartitionTransform::Day
| PartitionTransform::Hour
)
}
pub fn source_bounds(
&self,
value: &str,
data_type: &DataType,
) -> Option<(ScalarValue, ScalarValue)> {
match self {
PartitionTransform::Identity => {
let scalar = ScalarValue::try_from_string(value.to_string(), data_type).ok()?;
Some((scalar.clone(), scalar))
},
PartitionTransform::Year => {
let year: i64 = value.trim().parse().ok()?;
year_bounds(year, data_type)
},
PartitionTransform::Month
| PartitionTransform::Day
| PartitionTransform::Hour
| PartitionTransform::Bucket(_)
| PartitionTransform::Unknown(_) => None,
}
}
}
fn year_bounds(year: i64, data_type: &DataType) -> Option<(ScalarValue, ScalarValue)> {
let (min_str, max_str) = match data_type {
DataType::Date32 | DataType::Date64 => {
(format!("{year}-01-01"), format!("{}-01-01", year + 1))
},
DataType::Timestamp(_, None) => (
format!("{year}-01-01 00:00:00"),
format!("{}-01-01 00:00:00", year + 1),
),
DataType::Int8
| DataType::Int16
| DataType::Int32
| DataType::Int64
| DataType::UInt8
| DataType::UInt16
| DataType::UInt32
| DataType::UInt64 => {
let scalar = ScalarValue::try_from_string(year.to_string(), data_type).ok()?;
return Some((scalar.clone(), scalar));
},
_ => return None,
};
let min = ScalarValue::try_from_string(min_str, data_type).ok()?;
let max = ScalarValue::try_from_string(max_str, data_type).ok()?;
Some((min, max))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartitionSpecColumn {
pub partition_key_index: i32,
pub column_id: i64,
pub transform: PartitionTransform,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartitionSpec {
pub partition_id: i64,
pub columns: Vec<PartitionSpecColumn>,
pub prune_safe: bool,
}
impl PartitionSpec {
pub fn transform_for_column(&self, column_id: i64) -> Option<&PartitionTransform> {
self.columns
.iter()
.find(|c| c.column_id == column_id)
.map(|c| &c.transform)
}
pub fn from_rows(
rows: Vec<(i64, i32, i64, String)>,
prune_safe: bool,
) -> Option<PartitionSpec> {
let partition_id = rows.first()?.0;
let columns = rows
.into_iter()
.map(
|(_, partition_key_index, column_id, transform)| PartitionSpecColumn {
partition_key_index,
column_id,
transform: PartitionTransform::parse(&transform),
},
)
.collect();
Some(PartitionSpec {
partition_id,
columns,
prune_safe,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_and_roundtrip() {
for (s, expected) in [
("identity", PartitionTransform::Identity),
("year", PartitionTransform::Year),
("MONTH", PartitionTransform::Month),
("day", PartitionTransform::Day),
("hour", PartitionTransform::Hour),
("bucket(8)", PartitionTransform::Bucket(8)),
] {
assert_eq!(PartitionTransform::parse(s), expected);
}
for t in [
PartitionTransform::Identity,
PartitionTransform::Year,
PartitionTransform::Month,
PartitionTransform::Day,
PartitionTransform::Hour,
PartitionTransform::Bucket(4),
] {
assert_eq!(PartitionTransform::parse(&t.to_catalog_string()), t);
}
}
#[test]
fn unknown_transform_preserved() {
let t = PartitionTransform::parse("truncate(10)");
assert_eq!(t, PartitionTransform::Unknown("truncate(10)".to_string()));
assert_eq!(t.to_catalog_string(), "truncate(10)");
assert!(!t.is_producible());
assert_eq!(t.source_bounds("x", &DataType::Utf8), None);
}
#[test]
fn identity_bounds_are_exact() {
let (min, max) = PartitionTransform::Identity
.source_bounds("42", &DataType::Int32)
.unwrap();
assert_eq!(min, ScalarValue::Int32(Some(42)));
assert_eq!(max, ScalarValue::Int32(Some(42)));
let (min, max) = PartitionTransform::Identity
.source_bounds("us", &DataType::Utf8)
.unwrap();
assert_eq!(min, ScalarValue::Utf8(Some("us".to_string())));
assert_eq!(max, ScalarValue::Utf8(Some("us".to_string())));
}
#[test]
fn year_bounds_span_the_year_for_dates() {
let (min, max) = PartitionTransform::Year
.source_bounds("2023", &DataType::Date32)
.unwrap();
let expected_min =
ScalarValue::try_from_string("2023-01-01".to_string(), &DataType::Date32).unwrap();
let expected_max =
ScalarValue::try_from_string("2024-01-01".to_string(), &DataType::Date32).unwrap();
assert_eq!(min, expected_min);
assert_eq!(max, expected_max);
}
#[test]
fn year_on_tz_aware_timestamp_has_no_bounds() {
let tz = DataType::Timestamp(arrow::datatypes::TimeUnit::Microsecond, Some("UTC".into()));
assert_eq!(PartitionTransform::Year.source_bounds("2023", &tz), None);
let naive = DataType::Timestamp(arrow::datatypes::TimeUnit::Microsecond, None);
assert!(
PartitionTransform::Year
.source_bounds("2023", &naive)
.is_some()
);
}
#[test]
fn non_order_preserving_transforms_have_no_bounds() {
for t in [
PartitionTransform::Month,
PartitionTransform::Day,
PartitionTransform::Hour,
PartitionTransform::Bucket(4),
] {
assert_eq!(t.source_bounds("6", &DataType::Date32), None);
}
}
}