use std::fmt;
use crate::error::{Error, Result};
use crate::types::{Field, LogicalType};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Width {
#[default]
Exact,
Month,
Quarter,
Year,
}
impl Width {
#[must_use]
pub fn tag(self) -> u8 {
match self {
Self::Exact => 0,
Self::Month => 1,
Self::Quarter => 2,
Self::Year => 3,
}
}
#[must_use]
pub fn from_tag(tag: u8) -> Option<Self> {
match tag {
0 => Some(Self::Exact),
1 => Some(Self::Month),
2 => Some(Self::Quarter),
3 => Some(Self::Year),
_ => None,
}
}
}
impl fmt::Display for Width {
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
out.write_str(match self {
Self::Exact => "EXACT",
Self::Month => "MONTH",
Self::Quarter => "QUARTER",
Self::Year => "YEAR",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Clustering {
columns: Vec<u32>,
width: Width,
}
impl Clustering {
pub fn new(columns: Vec<u32>, width: Width, fields: &[Field]) -> Result<Self> {
if columns.is_empty() {
return Err(Error::invalid_input("a clustering declaration names no column"));
}
for (at, &column) in columns.iter().enumerate() {
if column as usize >= fields.len() {
return Err(Error::invalid_input(
"a clustering declaration names a column the table does not have",
));
}
if columns[..at].contains(&column) {
return Err(Error::invalid_input(
"a clustering declaration names the same column twice",
));
}
}
let leading = &fields[columns[0] as usize];
if width != Width::Exact
&& !matches!(leading.ty, LogicalType::Date | LogicalType::Timestamp)
{
return Err(Error::invalid_input(format!(
"a clustering declaration buckets {} by {width}, which only a date or a timestamp \
has",
leading.name
)));
}
Ok(Self { columns, width })
}
#[must_use]
pub fn columns(&self) -> &[u32] {
&self.columns
}
#[must_use]
pub fn width(&self) -> Width {
self.width
}
#[must_use]
pub fn partition(&self) -> u32 {
self.columns[0]
}
#[must_use]
pub fn describe(&self, names: &[String]) -> String {
let named =
|at: u32| names.get(at as usize).cloned().unwrap_or_else(|| format!("column {at}"));
let leading = match self.width {
Width::Exact => named(self.partition()),
other => format!("{}({})", other.to_string().to_lowercase(), named(self.partition())),
};
let rest = self.columns[1..].iter().map(|&at| named(at)).collect::<Vec<_>>();
std::iter::once(leading).chain(rest).collect::<Vec<_>>().join(", ")
}
}
#[cfg(test)]
mod tests {
use super::{Clustering, Width};
use crate::types::{Field, LogicalType};
fn lineitem() -> Vec<Field> {
vec![
Field::new("l_orderkey", LogicalType::BigInt),
Field::new("l_linenumber", LogicalType::Integer),
Field::new("l_shipdate", LogicalType::Date),
]
}
#[test]
fn a_declaration_that_could_never_be_satisfied_is_refused() {
let fields = lineitem();
assert!(Clustering::new(Vec::new(), Width::Exact, &fields).is_err(), "no column at all");
assert!(Clustering::new(vec![3], Width::Exact, &fields).is_err(), "past the end");
assert!(Clustering::new(vec![0, 1, 0], Width::Exact, &fields).is_err(), "twice");
assert!(Clustering::new(vec![2, 0], Width::Month, &fields).is_ok());
}
#[test]
fn a_calendar_bucket_on_a_column_with_no_calendar_in_it_is_refused() {
let fields = lineitem();
let complaint = Clustering::new(vec![0, 2], Width::Month, &fields)
.expect_err("a bigint has no months")
.to_string();
assert!(complaint.contains("l_orderkey"), "{complaint}");
assert!(complaint.contains("MONTH"), "{complaint}");
assert!(
Clustering::new(vec![0, 2], Width::Exact, &fields).is_ok(),
"no bucket, no problem"
);
}
#[test]
fn every_width_survives_its_byte() {
for width in [Width::Exact, Width::Month, Width::Quarter, Width::Year] {
assert_eq!(Width::from_tag(width.tag()), Some(width), "{width}");
}
assert_eq!(Width::from_tag(4), None, "a tag from a build that knows more than this one");
}
#[test]
fn a_declaration_reads_back_the_way_it_was_written() {
let fields = lineitem();
let names = fields.iter().map(|field| field.name.clone()).collect::<Vec<_>>();
let stage_zero = Clustering::new(vec![2, 0, 1], Width::Month, &fields).expect("valid");
assert_eq!(stage_zero.describe(&names), "month(l_shipdate), l_orderkey, l_linenumber");
let plain = Clustering::new(vec![0], Width::Exact, &fields).expect("valid");
assert_eq!(plain.describe(&names), "l_orderkey");
}
}