use std::{collections::BTreeMap, fmt::Display};
use crate::{
error::Error,
prelude::Tuple,
types::{scalar::ScalarType, AttributeName},
};
#[macro_export]
macro_rules! heading {
($($key:ident = $value:expr),* $(,)?) => {
$crate::HeadingBuilder::new()
$(
.with_attribute(stringify!($key), $value)
)*
.build()
};
}
#[derive(Debug, Default)]
pub struct HeadingBuilder {
attributes: Vec<(AttributeName, ScalarType)>,
}
impl HeadingBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_attribute<A>(mut self, name: A, ty: ScalarType) -> Self
where
A: Into<AttributeName>,
{
self.attributes.push((name.into(), ty));
self
}
pub fn build(self) -> Result<Heading, Error> {
Heading::try_from(self.attributes)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Heading {
pub(crate) attributes: BTreeMap<AttributeName, ScalarType>,
}
impl Heading {
#[must_use]
pub fn new(attributes: BTreeMap<AttributeName, ScalarType>) -> Self {
Self { attributes }
}
#[must_use]
pub fn degree(&self) -> usize {
self.attributes.len()
}
pub fn validate_tuple(&self, tuple: &Tuple) -> Result<(), Error> {
if self.degree() != tuple.arity() {
return Err(Error::InvalidWidth {
expected: self.degree(),
actual: tuple.arity(),
});
}
for (name, ty) in &self.attributes {
let Some(value) = tuple.get(name) else {
return Err(Error::AttributeNotFound { name: name.clone() });
};
if value.ty() != *ty {
return Err(Error::ScalarTypeMismatch {
lhs: value.ty(),
rhs: *ty,
});
}
}
Ok(())
}
#[must_use]
pub fn get(&self, name: &AttributeName) -> Option<&ScalarType> {
self.attributes.get(name)
}
#[must_use]
pub fn contains(&self, name: &AttributeName) -> bool {
self.attributes.contains_key(name)
}
pub fn iter(&self) -> impl Iterator<Item = (&AttributeName, &ScalarType)> {
self.attributes.iter()
}
pub fn common(&self, other: &Heading) -> Result<Vec<AttributeName>, Error> {
let mut common = Vec::with_capacity(self.degree());
for (attr, ty) in self.iter() {
let Some(other_ty) = other.get(attr) else {
continue;
};
if ty != other_ty {
return Err(Error::ScalarTypeMismatch {
lhs: *ty,
rhs: *other_ty,
});
}
common.push(attr.clone());
}
Ok(common)
}
#[must_use]
pub fn is_subset_of(&self, other: &Heading) -> bool {
self.iter().all(|(attr, ty)| other.get(attr) == Some(ty))
}
}
impl<K, V> TryFrom<Vec<(K, V)>> for Heading
where
K: Into<AttributeName>,
V: Into<ScalarType>,
{
type Error = Error;
fn try_from(value: Vec<(K, V)>) -> Result<Self, Self::Error> {
let mut attributes = BTreeMap::new();
for (name, ty) in value {
let key = name.into();
if attributes.contains_key(&key) {
return Err(Error::AttributeAlreadyExists { name: key });
}
attributes.insert(key, ty.into());
}
Ok(Self { attributes })
}
}
impl Display for Heading {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{{ ")?;
for (name, ty) in &self.attributes {
write!(f, "{name} {ty}, ")?;
}
write!(f, "}}")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::prelude::Scalar;
use super::*;
#[test]
fn test_new() {
let heading = Heading::try_from(vec![
("foo".to_string(), ScalarType::Boolean),
("bar".to_string(), ScalarType::Integer),
])
.unwrap();
assert_eq!(heading.attributes.len(), 2);
assert_eq!(
heading.attributes[&AttributeName::from("foo")],
ScalarType::Boolean
);
assert_eq!(
heading.attributes[&AttributeName::from("bar")],
ScalarType::Integer
);
}
#[test]
fn test_degree() {
let heading = Heading::try_from(vec![
("foo".to_string(), ScalarType::Boolean),
("bar".to_string(), ScalarType::Integer),
])
.unwrap();
assert_eq!(heading.degree(), 2);
}
#[test]
fn test_validate_tuple() {
let heading = Heading::try_from(vec![
("foo".to_string(), ScalarType::Boolean),
("bar".to_string(), ScalarType::Integer),
])
.unwrap();
let tuple = Tuple::try_from(vec![
(AttributeName::from("foo"), Scalar::Boolean(true)),
(AttributeName::from("bar"), Scalar::Integer(42)),
])
.unwrap();
assert!(heading.validate_tuple(&tuple).is_ok());
}
#[test]
fn test_validate_tuple_mismatch() {
let heading = Heading::try_from(vec![
("foo".to_string(), ScalarType::Boolean),
("bar".to_string(), ScalarType::Integer),
])
.unwrap();
let tuple = Tuple::try_from(vec![
(AttributeName::from("foo"), Scalar::Boolean(true)),
(AttributeName::from("bar"), Scalar::Boolean(false)),
])
.unwrap();
assert!(heading.validate_tuple(&tuple).is_err());
}
#[test]
fn test_validate_tuple_rejects_invalid_width() {
let heading = Heading::try_from(vec![
(AttributeName::from("foo"), ScalarType::Boolean),
(AttributeName::from("bar"), ScalarType::Integer),
])
.unwrap();
let tuple =
Tuple::try_from(vec![(AttributeName::from("foo"), Scalar::Boolean(true))]).unwrap();
assert_eq!(
heading.validate_tuple(&tuple),
Err(Error::InvalidWidth {
expected: 2,
actual: 1,
})
);
}
#[test]
fn test_common_returns_shared_attributes() -> Result<(), Error> {
let lhs = Heading::try_from(vec![
(AttributeName::from("foo"), ScalarType::Boolean),
(AttributeName::from("bar"), ScalarType::Integer),
])
.unwrap();
let rhs = Heading::try_from(vec![
(AttributeName::from("bar"), ScalarType::Integer),
(AttributeName::from("baz"), ScalarType::Boolean),
])
.unwrap();
assert_eq!(lhs.common(&rhs)?, vec![AttributeName::from("bar")]);
Ok(())
}
#[test]
fn test_common_rejects_type_mismatches() {
let lhs =
Heading::try_from(vec![(AttributeName::from("foo"), ScalarType::Integer)]).unwrap();
let rhs =
Heading::try_from(vec![(AttributeName::from("foo"), ScalarType::Boolean)]).unwrap();
assert_eq!(
lhs.common(&rhs),
Err(Error::ScalarTypeMismatch {
lhs: ScalarType::Integer,
rhs: ScalarType::Boolean,
})
);
}
#[test]
fn test_is_subset_of_returns_true_for_matching_subset() {
let subset = Heading::try_from(vec![
(AttributeName::from("foo"), ScalarType::Integer),
(AttributeName::from("bar"), ScalarType::Boolean),
])
.unwrap();
let superset = Heading::try_from(vec![
(AttributeName::from("foo"), ScalarType::Integer),
(AttributeName::from("bar"), ScalarType::Boolean),
(AttributeName::from("baz"), ScalarType::String),
])
.unwrap();
assert!(subset.is_subset_of(&superset));
}
#[test]
fn test_is_subset_of_returns_false_when_attribute_is_missing() {
let subset = Heading::try_from(vec![
(AttributeName::from("foo"), ScalarType::Integer),
(AttributeName::from("bar"), ScalarType::Boolean),
])
.unwrap();
let other =
Heading::try_from(vec![(AttributeName::from("foo"), ScalarType::Integer)]).unwrap();
assert!(!subset.is_subset_of(&other));
}
#[test]
fn test_is_subset_of_returns_false_when_attribute_type_differs() {
let subset =
Heading::try_from(vec![(AttributeName::from("foo"), ScalarType::Integer)]).unwrap();
let other =
Heading::try_from(vec![(AttributeName::from("foo"), ScalarType::Boolean)]).unwrap();
assert!(!subset.is_subset_of(&other));
}
#[test]
#[should_panic]
fn test_heading_from_rejects_duplicate_attribute_names() {
let _ = Heading::try_from(vec![
(AttributeName::from("foo"), ScalarType::Integer),
(AttributeName::from("foo"), ScalarType::Boolean),
])
.unwrap();
}
}