1use crate::{DataFrameError, Result};
2
3#[derive(Debug, Clone, PartialEq)]
5pub enum Expr {
6 Column(String),
8 Literal(Scalar),
10 BinaryOp {
12 left: Box<Expr>,
13 op: Operator,
14 right: Box<Expr>,
15 },
16 UnaryOp { op: UnaryOperator, expr: Box<Expr> },
18 Agg { func: AggFunc, expr: Box<Expr> },
20 Function {
22 input: Box<Expr>,
23 function: ExprFunction,
24 },
25 ConcatStr {
27 inputs: Vec<Expr>,
29 separator: String,
31 null_behavior: ConcatStrNullBehavior,
33 },
34 Alias { expr: Box<Expr>, name: String },
36 Wildcard,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum ExprFunction {
43 String(StringFunction),
45 Datetime(DatetimeFunction),
47 List(ListFunction),
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum StringFunction {
54 ToLowercase,
56 ToUppercase,
58 Contains { pattern: String },
60 Replace {
62 pattern: String,
63 replacement: String,
64 },
65 StripChars { chars: Option<String> },
67 Split { separator: String },
69 LenChars,
71 Extract {
73 pattern: String,
74 capture_group: usize,
75 },
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum DatetimeFunction {
81 Year,
83 Month,
85 Day,
87 Weekday,
89 ToString,
91 ConvertTimeZone {
93 from_offset: String,
94 to_offset: String,
95 },
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum ListFunction {
101 Join {
103 separator: String,
104 null_value: Option<String>,
105 },
106 Len,
108 Contains { value: String },
110}
111
112#[derive(Debug, Copy, Clone, PartialEq, Eq)]
114pub enum Operator {
115 Add,
117 Sub,
119 Mul,
121 Div,
123 Eq,
125 Neq,
127 Gt,
129 Lt,
131 Ge,
133 Le,
135 And,
137 Or,
139}
140
141#[derive(Debug, Copy, Clone, PartialEq, Eq)]
143pub enum UnaryOperator {
144 Not,
146}
147
148#[derive(Debug, Copy, Clone, PartialEq, Eq)]
150pub enum AggFunc {
151 Sum,
153 Mean,
155 Count,
157 Min,
159 Max,
161}
162
163#[derive(Debug, Clone, PartialEq)]
165pub enum Scalar {
166 Null,
168 Boolean(bool),
170 Int64(i64),
172 Float64(f64),
174 Utf8(String),
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum ConcatStrNullBehavior {
181 Propagate,
183 Ignore,
185 Replace(String),
187}
188
189impl From<()> for Scalar {
190 fn from(_: ()) -> Self {
191 Scalar::Null
192 }
193}
194
195impl From<bool> for Scalar {
196 fn from(v: bool) -> Self {
197 Scalar::Boolean(v)
198 }
199}
200
201impl From<i64> for Scalar {
202 fn from(v: i64) -> Self {
203 Scalar::Int64(v)
204 }
205}
206
207impl From<f64> for Scalar {
208 fn from(v: f64) -> Self {
209 Scalar::Float64(v)
210 }
211}
212
213impl From<String> for Scalar {
214 fn from(v: String) -> Self {
215 Scalar::Utf8(v)
216 }
217}
218
219impl From<&str> for Scalar {
220 fn from(v: &str) -> Self {
221 Scalar::Utf8(v.to_string())
222 }
223}
224
225impl Expr {
226 pub fn concat_str(
231 inputs: Vec<Expr>,
232 separator: impl Into<String>,
233 null_behavior: ConcatStrNullBehavior,
234 ) -> Result<Expr> {
235 if inputs.len() < 2 {
236 return Err(DataFrameError::invalid_operation(
237 "concat_str requires at least two input expressions",
238 ));
239 }
240 Ok(Expr::ConcatStr {
241 inputs,
242 separator: separator.into(),
243 null_behavior,
244 })
245 }
246
247 pub fn alias(self, name: impl Into<String>) -> Expr {
249 Expr::Alias {
250 expr: Box::new(self),
251 name: name.into(),
252 }
253 }
254
255 #[allow(clippy::should_implement_trait)]
257 pub fn add(self, rhs: Expr) -> Expr {
258 Expr::BinaryOp {
259 left: Box::new(self),
260 op: Operator::Add,
261 right: Box::new(rhs),
262 }
263 }
264
265 #[allow(clippy::should_implement_trait)]
267 pub fn sub(self, rhs: Expr) -> Expr {
268 Expr::BinaryOp {
269 left: Box::new(self),
270 op: Operator::Sub,
271 right: Box::new(rhs),
272 }
273 }
274
275 #[allow(clippy::should_implement_trait)]
277 pub fn mul(self, rhs: Expr) -> Expr {
278 Expr::BinaryOp {
279 left: Box::new(self),
280 op: Operator::Mul,
281 right: Box::new(rhs),
282 }
283 }
284
285 #[allow(clippy::should_implement_trait)]
287 pub fn div(self, rhs: Expr) -> Expr {
288 Expr::BinaryOp {
289 left: Box::new(self),
290 op: Operator::Div,
291 right: Box::new(rhs),
292 }
293 }
294
295 pub fn eq(self, rhs: Expr) -> Expr {
297 Expr::BinaryOp {
298 left: Box::new(self),
299 op: Operator::Eq,
300 right: Box::new(rhs),
301 }
302 }
303
304 pub fn neq(self, rhs: Expr) -> Expr {
306 Expr::BinaryOp {
307 left: Box::new(self),
308 op: Operator::Neq,
309 right: Box::new(rhs),
310 }
311 }
312
313 pub fn gt(self, rhs: Expr) -> Expr {
315 Expr::BinaryOp {
316 left: Box::new(self),
317 op: Operator::Gt,
318 right: Box::new(rhs),
319 }
320 }
321
322 pub fn lt(self, rhs: Expr) -> Expr {
324 Expr::BinaryOp {
325 left: Box::new(self),
326 op: Operator::Lt,
327 right: Box::new(rhs),
328 }
329 }
330
331 pub fn ge(self, rhs: Expr) -> Expr {
333 Expr::BinaryOp {
334 left: Box::new(self),
335 op: Operator::Ge,
336 right: Box::new(rhs),
337 }
338 }
339
340 pub fn le(self, rhs: Expr) -> Expr {
342 Expr::BinaryOp {
343 left: Box::new(self),
344 op: Operator::Le,
345 right: Box::new(rhs),
346 }
347 }
348
349 pub fn and_(self, rhs: Expr) -> Expr {
351 Expr::BinaryOp {
352 left: Box::new(self),
353 op: Operator::And,
354 right: Box::new(rhs),
355 }
356 }
357
358 pub fn or_(self, rhs: Expr) -> Expr {
360 Expr::BinaryOp {
361 left: Box::new(self),
362 op: Operator::Or,
363 right: Box::new(rhs),
364 }
365 }
366
367 pub fn not_(self) -> Expr {
369 Expr::UnaryOp {
370 op: UnaryOperator::Not,
371 expr: Box::new(self),
372 }
373 }
374
375 pub fn sum(self) -> Expr {
377 Expr::Agg {
378 func: AggFunc::Sum,
379 expr: Box::new(self),
380 }
381 }
382
383 pub fn mean(self) -> Expr {
385 Expr::Agg {
386 func: AggFunc::Mean,
387 expr: Box::new(self),
388 }
389 }
390
391 pub fn count(self) -> Expr {
393 Expr::Agg {
394 func: AggFunc::Count,
395 expr: Box::new(self),
396 }
397 }
398
399 pub fn min(self) -> Expr {
401 Expr::Agg {
402 func: AggFunc::Min,
403 expr: Box::new(self),
404 }
405 }
406
407 pub fn max(self) -> Expr {
409 Expr::Agg {
410 func: AggFunc::Max,
411 expr: Box::new(self),
412 }
413 }
414
415 pub fn str(self) -> StringExpr {
417 StringExpr { input: self }
418 }
419
420 pub fn dt(self) -> DatetimeExpr {
422 DatetimeExpr { input: self }
423 }
424
425 pub fn list(self) -> ListExpr {
427 ListExpr { input: self }
428 }
429}
430
431pub fn concat_str(
433 inputs: Vec<Expr>,
434 separator: impl Into<String>,
435 null_behavior: ConcatStrNullBehavior,
436) -> Result<Expr> {
437 Expr::concat_str(inputs, separator, null_behavior)
438}
439
440#[derive(Debug, Clone, PartialEq)]
442pub struct StringExpr {
443 input: Expr,
444}
445
446impl StringExpr {
447 fn function(self, function: StringFunction) -> Expr {
448 Expr::Function {
449 input: Box::new(self.input),
450 function: ExprFunction::String(function),
451 }
452 }
453
454 pub fn to_lowercase(self) -> Expr {
456 self.function(StringFunction::ToLowercase)
457 }
458
459 pub fn to_uppercase(self) -> Expr {
461 self.function(StringFunction::ToUppercase)
462 }
463
464 pub fn contains(self, pattern: impl Into<String>) -> Expr {
466 self.function(StringFunction::Contains {
467 pattern: pattern.into(),
468 })
469 }
470
471 pub fn replace(self, pattern: impl Into<String>, replacement: impl Into<String>) -> Expr {
473 self.function(StringFunction::Replace {
474 pattern: pattern.into(),
475 replacement: replacement.into(),
476 })
477 }
478
479 pub fn strip_chars(self, chars: Option<impl Into<String>>) -> Expr {
481 self.function(StringFunction::StripChars {
482 chars: chars.map(Into::into),
483 })
484 }
485
486 pub fn split(self, separator: impl Into<String>) -> Expr {
488 self.function(StringFunction::Split {
489 separator: separator.into(),
490 })
491 }
492
493 pub fn len_chars(self) -> Expr {
495 self.function(StringFunction::LenChars)
496 }
497
498 pub fn extract(self, pattern: impl Into<String>, capture_group: usize) -> Expr {
500 self.function(StringFunction::Extract {
501 pattern: pattern.into(),
502 capture_group,
503 })
504 }
505}
506
507#[derive(Debug, Clone, PartialEq)]
509pub struct DatetimeExpr {
510 input: Expr,
511}
512
513impl DatetimeExpr {
514 fn function(self, function: DatetimeFunction) -> Expr {
515 Expr::Function {
516 input: Box::new(self.input),
517 function: ExprFunction::Datetime(function),
518 }
519 }
520
521 pub fn year(self) -> Expr {
523 self.function(DatetimeFunction::Year)
524 }
525
526 pub fn month(self) -> Expr {
528 self.function(DatetimeFunction::Month)
529 }
530
531 pub fn day(self) -> Expr {
533 self.function(DatetimeFunction::Day)
534 }
535
536 pub fn weekday(self) -> Expr {
538 self.function(DatetimeFunction::Weekday)
539 }
540
541 #[allow(clippy::inherent_to_string)]
543 pub fn to_string(self) -> Expr {
544 self.function(DatetimeFunction::ToString)
545 }
546
547 pub fn convert_time_zone(
549 self,
550 from_offset: impl Into<String>,
551 to_offset: impl Into<String>,
552 ) -> Expr {
553 self.function(DatetimeFunction::ConvertTimeZone {
554 from_offset: from_offset.into(),
555 to_offset: to_offset.into(),
556 })
557 }
558}
559
560#[derive(Debug, Clone, PartialEq)]
562pub struct ListExpr {
563 input: Expr,
564}
565
566impl ListExpr {
567 fn function(self, function: ListFunction) -> Expr {
568 Expr::Function {
569 input: Box::new(self.input),
570 function: ExprFunction::List(function),
571 }
572 }
573
574 pub fn join(self, separator: impl Into<String>, null_value: Option<impl Into<String>>) -> Expr {
576 self.function(ListFunction::Join {
577 separator: separator.into(),
578 null_value: null_value.map(Into::into),
579 })
580 }
581
582 pub fn len(self) -> Expr {
584 self.function(ListFunction::Len)
585 }
586
587 pub fn contains(self, value: impl Into<String>) -> Expr {
589 self.function(ListFunction::Contains {
590 value: value.into(),
591 })
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::{
598 concat_str, AggFunc, ConcatStrNullBehavior, Expr, ExprFunction, Operator, Scalar,
599 StringFunction, UnaryOperator,
600 };
601 use crate::expr::{col, lit};
602
603 #[test]
604 fn builder_and_chaining_works() {
605 let expr = col("a").add(lit(1_i64)).alias("b");
606 assert_eq!(
607 expr,
608 Expr::Alias {
609 expr: Box::new(Expr::BinaryOp {
610 left: Box::new(Expr::Column("a".to_string())),
611 op: Operator::Add,
612 right: Box::new(Expr::Literal(Scalar::Int64(1))),
613 }),
614 name: "b".to_string(),
615 }
616 );
617 }
618
619 #[test]
620 fn logical_and_agg_works() {
621 let expr = col("x")
622 .gt(lit(1_i64))
623 .and_(col("y").lt(lit(10_i64)).not_())
624 .alias("p");
625
626 assert!(matches!(
627 expr,
628 Expr::Alias {
629 expr: _,
630 name
631 } if name == "p"
632 ));
633
634 let agg = col("v").sum();
635 assert_eq!(
636 agg,
637 Expr::Agg {
638 func: AggFunc::Sum,
639 expr: Box::new(Expr::Column("v".to_string()))
640 }
641 );
642
643 let u = Expr::Column("a".to_string()).not_();
644 assert_eq!(
645 u,
646 Expr::UnaryOp {
647 op: UnaryOperator::Not,
648 expr: Box::new(Expr::Column("a".to_string()))
649 }
650 );
651 }
652
653 #[test]
654 fn namespace_builders_create_function_exprs() {
655 let expr = col("name").str().to_lowercase().alias("name_lower");
656 assert_eq!(
657 expr,
658 Expr::Alias {
659 expr: Box::new(Expr::Function {
660 input: Box::new(Expr::Column("name".to_string())),
661 function: ExprFunction::String(StringFunction::ToLowercase),
662 }),
663 name: "name_lower".to_string(),
664 }
665 );
666 }
667
668 #[test]
669 fn concat_str_requires_two_or_more_inputs() {
670 let err =
671 concat_str(vec![col("first")], "-", ConcatStrNullBehavior::Propagate).unwrap_err();
672 assert!(err.to_string().contains("at least two"));
673 }
674
675 #[test]
676 fn concat_str_builder_keeps_separator_and_null_policy() {
677 let expr = concat_str(
678 vec![col("first"), col("second")],
679 "-",
680 ConcatStrNullBehavior::Replace("?".into()),
681 )
682 .unwrap();
683 assert!(matches!(
684 expr,
685 Expr::ConcatStr { separator, null_behavior: ConcatStrNullBehavior::Replace(value), .. }
686 if separator == "-" && value == "?"
687 ));
688 }
689}