use arrow::datatypes::DataType;
use serde::{Deserialize, Serialize};
use super::measure::AggFunc;
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CalculatedMeasure {
name: String,
expression: String,
data_type: DataType,
default_agg: AggFunc,
nullable: bool,
description: Option<String>,
format: Option<String>,
}
impl CalculatedMeasure {
pub fn new(
name: impl Into<String>,
expression: impl Into<String>,
data_type: DataType,
default_agg: AggFunc,
) -> Result<Self> {
let name = name.into();
let expression = expression.into();
if name.is_empty() {
return Err(Error::Schema("Calculated measure name cannot be empty".into()));
}
if expression.is_empty() {
return Err(Error::Schema("Expression cannot be empty".into()));
}
if !default_agg.is_compatible_with(&data_type) {
return Err(Error::Schema(format!(
"Aggregation function {} is not compatible with data type {:?}",
default_agg, data_type
)));
}
Ok(Self {
name,
expression,
data_type,
default_agg,
nullable: true,
description: None,
format: None,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn expression(&self) -> &str {
&self.expression
}
pub fn data_type(&self) -> &DataType {
&self.data_type
}
pub fn default_agg(&self) -> AggFunc {
self.default_agg
}
pub fn is_nullable(&self) -> bool {
self.nullable
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn format(&self) -> Option<&str> {
self.format.as_deref()
}
pub fn with_nullable(mut self, nullable: bool) -> Self {
self.nullable = nullable;
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VirtualDimension {
name: String,
expression: String,
data_type: DataType,
nullable: bool,
cardinality: Option<usize>,
description: Option<String>,
}
impl VirtualDimension {
pub fn new(
name: impl Into<String>,
expression: impl Into<String>,
data_type: DataType,
) -> Result<Self> {
let name = name.into();
let expression = expression.into();
if name.is_empty() {
return Err(Error::Schema("Virtual dimension name cannot be empty".into()));
}
if expression.is_empty() {
return Err(Error::Schema("Expression cannot be empty".into()));
}
Ok(Self {
name,
expression,
data_type,
nullable: true,
cardinality: None,
description: None,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn expression(&self) -> &str {
&self.expression
}
pub fn data_type(&self) -> &DataType {
&self.data_type
}
pub fn is_nullable(&self) -> bool {
self.nullable
}
pub fn cardinality(&self) -> Option<usize> {
self.cardinality
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn with_nullable(mut self, nullable: bool) -> Self {
self.nullable = nullable;
self
}
pub fn with_cardinality(mut self, cardinality: usize) -> Self {
self.cardinality = Some(cardinality);
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculated_measure_creation() {
let measure = CalculatedMeasure::new(
"profit",
"revenue - cost",
DataType::Float64,
AggFunc::Sum,
)
.unwrap();
assert_eq!(measure.name(), "profit");
assert_eq!(measure.expression(), "revenue - cost");
assert_eq!(measure.data_type(), &DataType::Float64);
assert_eq!(measure.default_agg(), AggFunc::Sum);
assert!(measure.is_nullable());
}
#[test]
fn test_calculated_measure_validation() {
let result = CalculatedMeasure::new("", "a + b", DataType::Float64, AggFunc::Sum);
assert!(result.is_err());
let result = CalculatedMeasure::new("test", "", DataType::Float64, AggFunc::Sum);
assert!(result.is_err());
let result = CalculatedMeasure::new("test", "a || b", DataType::Utf8, AggFunc::Sum);
assert!(result.is_err());
}
#[test]
fn test_calculated_measure_builder() {
let measure = CalculatedMeasure::new(
"margin",
"profit / revenue * 100",
DataType::Float64,
AggFunc::Avg,
)
.unwrap()
.with_nullable(false)
.with_description("Profit margin percentage")
.with_format(",.2f%");
assert_eq!(measure.name(), "margin");
assert!(!measure.is_nullable());
assert_eq!(measure.description(), Some("Profit margin percentage"));
assert_eq!(measure.format(), Some(",.2f%"));
}
#[test]
fn test_virtual_dimension_creation() {
let dim = VirtualDimension::new(
"year",
"EXTRACT(YEAR FROM sale_date)",
DataType::Int32,
)
.unwrap();
assert_eq!(dim.name(), "year");
assert_eq!(dim.expression(), "EXTRACT(YEAR FROM sale_date)");
assert_eq!(dim.data_type(), &DataType::Int32);
assert!(dim.is_nullable());
}
#[test]
fn test_virtual_dimension_validation() {
let result = VirtualDimension::new("", "EXTRACT(YEAR FROM date)", DataType::Int32);
assert!(result.is_err());
let result = VirtualDimension::new("year", "", DataType::Int32);
assert!(result.is_err());
}
#[test]
fn test_virtual_dimension_builder() {
let dim = VirtualDimension::new(
"age_group",
"CASE WHEN age < 18 THEN 'Minor' ELSE 'Adult' END",
DataType::Utf8,
)
.unwrap()
.with_nullable(false)
.with_cardinality(2)
.with_description("Age category");
assert_eq!(dim.name(), "age_group");
assert!(!dim.is_nullable());
assert_eq!(dim.cardinality(), Some(2));
assert_eq!(dim.description(), Some("Age category"));
}
}