1use arrow::datatypes::DataType;
6use serde::{Deserialize, Serialize};
7
8use super::measure::AggFunc;
9use crate::error::{Error, Result};
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct CalculatedMeasure {
38 name: String,
40
41 expression: String,
44
45 data_type: DataType,
47
48 default_agg: AggFunc,
50
51 nullable: bool,
53
54 description: Option<String>,
56
57 format: Option<String>,
59}
60
61impl CalculatedMeasure {
62 pub fn new(
73 name: impl Into<String>,
74 expression: impl Into<String>,
75 data_type: DataType,
76 default_agg: AggFunc,
77 ) -> Result<Self> {
78 let name = name.into();
79 let expression = expression.into();
80
81 if name.is_empty() {
83 return Err(Error::Schema("Calculated measure name cannot be empty".into()));
84 }
85 if expression.is_empty() {
86 return Err(Error::Schema("Expression cannot be empty".into()));
87 }
88
89 if !default_agg.is_compatible_with(&data_type) {
91 return Err(Error::Schema(format!(
92 "Aggregation function {} is not compatible with data type {:?}",
93 default_agg, data_type
94 )));
95 }
96
97 Ok(Self {
98 name,
99 expression,
100 data_type,
101 default_agg,
102 nullable: true,
103 description: None,
104 format: None,
105 })
106 }
107
108 pub fn name(&self) -> &str {
110 &self.name
111 }
112
113 pub fn expression(&self) -> &str {
115 &self.expression
116 }
117
118 pub fn data_type(&self) -> &DataType {
120 &self.data_type
121 }
122
123 pub fn default_agg(&self) -> AggFunc {
125 self.default_agg
126 }
127
128 pub fn is_nullable(&self) -> bool {
130 self.nullable
131 }
132
133 pub fn description(&self) -> Option<&str> {
135 self.description.as_deref()
136 }
137
138 pub fn format(&self) -> Option<&str> {
140 self.format.as_deref()
141 }
142
143 pub fn with_nullable(mut self, nullable: bool) -> Self {
145 self.nullable = nullable;
146 self
147 }
148
149 pub fn with_description(mut self, description: impl Into<String>) -> Self {
151 self.description = Some(description.into());
152 self
153 }
154
155 pub fn with_format(mut self, format: impl Into<String>) -> Self {
157 self.format = Some(format.into());
158 self
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186pub struct VirtualDimension {
187 name: String,
189
190 expression: String,
192
193 data_type: DataType,
195
196 nullable: bool,
198
199 cardinality: Option<usize>,
201
202 description: Option<String>,
204}
205
206impl VirtualDimension {
207 pub fn new(
217 name: impl Into<String>,
218 expression: impl Into<String>,
219 data_type: DataType,
220 ) -> Result<Self> {
221 let name = name.into();
222 let expression = expression.into();
223
224 if name.is_empty() {
226 return Err(Error::Schema("Virtual dimension name cannot be empty".into()));
227 }
228 if expression.is_empty() {
229 return Err(Error::Schema("Expression cannot be empty".into()));
230 }
231
232 Ok(Self {
233 name,
234 expression,
235 data_type,
236 nullable: true,
237 cardinality: None,
238 description: None,
239 })
240 }
241
242 pub fn name(&self) -> &str {
244 &self.name
245 }
246
247 pub fn expression(&self) -> &str {
249 &self.expression
250 }
251
252 pub fn data_type(&self) -> &DataType {
254 &self.data_type
255 }
256
257 pub fn is_nullable(&self) -> bool {
259 self.nullable
260 }
261
262 pub fn cardinality(&self) -> Option<usize> {
264 self.cardinality
265 }
266
267 pub fn description(&self) -> Option<&str> {
269 self.description.as_deref()
270 }
271
272 pub fn with_nullable(mut self, nullable: bool) -> Self {
274 self.nullable = nullable;
275 self
276 }
277
278 pub fn with_cardinality(mut self, cardinality: usize) -> Self {
280 self.cardinality = Some(cardinality);
281 self
282 }
283
284 pub fn with_description(mut self, description: impl Into<String>) -> Self {
286 self.description = Some(description.into());
287 self
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_calculated_measure_creation() {
297 let measure = CalculatedMeasure::new(
298 "profit",
299 "revenue - cost",
300 DataType::Float64,
301 AggFunc::Sum,
302 )
303 .unwrap();
304
305 assert_eq!(measure.name(), "profit");
306 assert_eq!(measure.expression(), "revenue - cost");
307 assert_eq!(measure.data_type(), &DataType::Float64);
308 assert_eq!(measure.default_agg(), AggFunc::Sum);
309 assert!(measure.is_nullable());
310 }
311
312 #[test]
313 fn test_calculated_measure_validation() {
314 let result = CalculatedMeasure::new("", "a + b", DataType::Float64, AggFunc::Sum);
316 assert!(result.is_err());
317
318 let result = CalculatedMeasure::new("test", "", DataType::Float64, AggFunc::Sum);
320 assert!(result.is_err());
321
322 let result = CalculatedMeasure::new("test", "a || b", DataType::Utf8, AggFunc::Sum);
324 assert!(result.is_err());
325 }
326
327 #[test]
328 fn test_calculated_measure_builder() {
329 let measure = CalculatedMeasure::new(
330 "margin",
331 "profit / revenue * 100",
332 DataType::Float64,
333 AggFunc::Avg,
334 )
335 .unwrap()
336 .with_nullable(false)
337 .with_description("Profit margin percentage")
338 .with_format(",.2f%");
339
340 assert_eq!(measure.name(), "margin");
341 assert!(!measure.is_nullable());
342 assert_eq!(measure.description(), Some("Profit margin percentage"));
343 assert_eq!(measure.format(), Some(",.2f%"));
344 }
345
346 #[test]
347 fn test_virtual_dimension_creation() {
348 let dim = VirtualDimension::new(
349 "year",
350 "EXTRACT(YEAR FROM sale_date)",
351 DataType::Int32,
352 )
353 .unwrap();
354
355 assert_eq!(dim.name(), "year");
356 assert_eq!(dim.expression(), "EXTRACT(YEAR FROM sale_date)");
357 assert_eq!(dim.data_type(), &DataType::Int32);
358 assert!(dim.is_nullable());
359 }
360
361 #[test]
362 fn test_virtual_dimension_validation() {
363 let result = VirtualDimension::new("", "EXTRACT(YEAR FROM date)", DataType::Int32);
365 assert!(result.is_err());
366
367 let result = VirtualDimension::new("year", "", DataType::Int32);
369 assert!(result.is_err());
370 }
371
372 #[test]
373 fn test_virtual_dimension_builder() {
374 let dim = VirtualDimension::new(
375 "age_group",
376 "CASE WHEN age < 18 THEN 'Minor' ELSE 'Adult' END",
377 DataType::Utf8,
378 )
379 .unwrap()
380 .with_nullable(false)
381 .with_cardinality(2)
382 .with_description("Age category");
383
384 assert_eq!(dim.name(), "age_group");
385 assert!(!dim.is_nullable());
386 assert_eq!(dim.cardinality(), Some(2));
387 assert_eq!(dim.description(), Some("Age category"));
388 }
389}