1use std::path::PathBuf;
2
3use crate::ops::{FillNull, JoinKeys, JoinType, SortOptions};
4use crate::{DataFrame, Expr};
5
6#[derive(Debug, Clone)]
8pub enum ProjectionKind {
9 Select,
11 WithColumns,
13}
14
15#[derive(Debug, Clone)]
17pub enum LogicalPlan {
18 DataFrameScan { df: DataFrame },
20 CsvScan {
22 path: PathBuf,
23 predicate: Option<Expr>,
24 projection: Option<Vec<String>>,
25 },
26 ParquetScan {
28 path: PathBuf,
29 predicate: Option<Expr>,
30 projection: Option<Vec<String>>,
31 },
32 Projection {
34 input: Box<LogicalPlan>,
35 exprs: Vec<Expr>,
36 kind: ProjectionKind,
37 },
38 Filter {
40 input: Box<LogicalPlan>,
41 predicate: Expr,
42 },
43 Aggregate {
45 input: Box<LogicalPlan>,
46 group_by: Vec<Expr>,
47 aggs: Vec<Expr>,
48 },
49 Join {
51 left: Box<LogicalPlan>,
52 right: Box<LogicalPlan>,
53 keys: JoinKeys,
54 how: JoinType,
55 },
56 Sort {
58 input: Box<LogicalPlan>,
59 options: SortOptions,
60 },
61 Slice {
63 input: Box<LogicalPlan>,
64 offset: usize,
65 len: usize,
66 from_end: bool,
67 },
68 Unique {
70 input: Box<LogicalPlan>,
71 subset: Option<Vec<String>>,
72 },
73 FillNull {
75 input: Box<LogicalPlan>,
76 fill: FillNull,
77 },
78 DropNulls {
80 input: Box<LogicalPlan>,
81 subset: Option<Vec<String>>,
82 },
83 NullCount { input: Box<LogicalPlan> },
85 Explode {
87 input: Box<LogicalPlan>,
88 column: String,
89 },
90 Implode { input: Box<LogicalPlan> },
92}
93
94impl LogicalPlan {
95 pub fn display(&self) -> String {
97 let mut out = String::new();
98 self.fmt_into(&mut out, 0);
99 out
100 }
101
102 fn fmt_into(&self, out: &mut String, indent: usize) {
103 let pad = " ".repeat(indent);
104 match self {
105 LogicalPlan::DataFrameScan { .. } => {
106 out.push_str(&format!("{pad}scan[dataframe]\n"));
107 }
108 LogicalPlan::CsvScan {
109 path,
110 predicate,
111 projection,
112 } => {
113 out.push_str(&format!("{pad}scan[csv path='{}']", path.display()));
114 if let Some(projection) = projection {
115 out.push_str(&format!(" projection={:?}", projection));
116 }
117 if let Some(predicate) = predicate {
118 out.push_str(&format!(" filters=[{}]", fmt_expr(predicate)));
119 }
120 out.push('\n');
121 }
122 LogicalPlan::ParquetScan {
123 path,
124 predicate,
125 projection,
126 } => {
127 out.push_str(&format!("{pad}scan[parquet path='{}']", path.display()));
128 if let Some(projection) = projection {
129 out.push_str(&format!(" projection={:?}", projection));
130 }
131 if let Some(predicate) = predicate {
132 out.push_str(&format!(" filters=[{}]", fmt_expr(predicate)));
133 }
134 out.push('\n');
135 }
136 LogicalPlan::Projection { input, exprs, kind } => {
137 let label = match kind {
138 ProjectionKind::Select => "project",
139 ProjectionKind::WithColumns => "with_columns",
140 };
141 out.push_str(&format!(
142 "{pad}{label} [{}]\n",
143 exprs.iter().map(fmt_expr).collect::<Vec<_>>().join(", ")
144 ));
145 input.fmt_into(out, indent + 1);
146 }
147 LogicalPlan::Filter { input, predicate } => {
148 out.push_str(&format!("{pad}filter [{}]\n", fmt_expr(predicate)));
149 input.fmt_into(out, indent + 1);
150 }
151 LogicalPlan::Aggregate {
152 input,
153 group_by,
154 aggs,
155 } => {
156 out.push_str(&format!(
157 "{pad}aggregate by=[{}] aggs=[{}]\n",
158 group_by.iter().map(fmt_expr).collect::<Vec<_>>().join(", "),
159 aggs.iter().map(fmt_expr).collect::<Vec<_>>().join(", ")
160 ));
161 input.fmt_into(out, indent + 1);
162 }
163 LogicalPlan::Join {
164 left,
165 right,
166 keys,
167 how,
168 } => {
169 out.push_str(&format!(
170 "{pad}join how={how:?} keys={}\n",
171 fmt_join_keys(keys)
172 ));
173 left.fmt_into(out, indent + 1);
174 right.fmt_into(out, indent + 1);
175 }
176 LogicalPlan::Sort { input, options } => {
177 out.push_str(&format!(
178 "{pad}sort by={:?} desc={:?} nulls_last={} stable={}\n",
179 options.by, options.descending, options.nulls_last, options.stable
180 ));
181 input.fmt_into(out, indent + 1);
182 }
183 LogicalPlan::Slice {
184 input,
185 offset,
186 len,
187 from_end,
188 } => {
189 out.push_str(&format!(
190 "{pad}slice offset={offset} len={len} from_end={from_end}\n"
191 ));
192 input.fmt_into(out, indent + 1);
193 }
194 LogicalPlan::Unique { input, subset } => {
195 out.push_str(&format!("{pad}unique subset={subset:?}\n"));
196 input.fmt_into(out, indent + 1);
197 }
198 LogicalPlan::FillNull { input, fill } => {
199 out.push_str(&format!("{pad}fill_null {}\n", fmt_fill_null(fill)));
200 input.fmt_into(out, indent + 1);
201 }
202 LogicalPlan::DropNulls { input, subset } => {
203 out.push_str(&format!("{pad}drop_nulls subset={subset:?}\n"));
204 input.fmt_into(out, indent + 1);
205 }
206 LogicalPlan::NullCount { input } => {
207 out.push_str(&format!("{pad}null_count\n"));
208 input.fmt_into(out, indent + 1);
209 }
210 LogicalPlan::Explode { input, column } => {
211 out.push_str(&format!("{pad}explode column={column}\n"));
212 input.fmt_into(out, indent + 1);
213 }
214 LogicalPlan::Implode { input } => {
215 out.push_str(&format!("{pad}implode\n"));
216 input.fmt_into(out, indent + 1);
217 }
218 }
219 }
220}
221
222fn fmt_join_keys(keys: &JoinKeys) -> String {
223 match keys {
224 JoinKeys::On(cols) => format!("on={cols:?}"),
225 JoinKeys::LeftRight { left_on, right_on } => {
226 format!("left_on={left_on:?} right_on={right_on:?}")
227 }
228 }
229}
230
231fn fmt_fill_null(fill: &FillNull) -> String {
232 match fill {
233 FillNull::Value(value) => format!("value={value:?}"),
234 FillNull::Strategy(strategy) => format!("strategy={strategy:?}"),
235 }
236}
237
238fn fmt_expr(expr: &Expr) -> String {
239 use crate::expr::{
240 AggFunc, DatetimeFunction, Expr as E, ExprFunction, ListFunction, Operator, Scalar,
241 StringFunction, UnaryOperator,
242 };
243
244 match expr {
245 E::Column(name) => format!("col({name})"),
246 E::Literal(Scalar::Null) => "lit(null)".to_string(),
247 E::Literal(Scalar::Boolean(v)) => format!("lit({v})"),
248 E::Literal(Scalar::Int64(v)) => format!("lit({v})"),
249 E::Literal(Scalar::Float64(v)) => format!("lit({v})"),
250 E::Literal(Scalar::Utf8(v)) => format!("lit({v:?})"),
251 E::Wildcard => "*".to_string(),
252 E::Alias { expr, name } => format!("{} as {name}", fmt_expr(expr)),
253 E::UnaryOp {
254 op: UnaryOperator::Not,
255 expr,
256 } => format!("not({})", fmt_expr(expr)),
257 E::BinaryOp { left, op, right } => {
258 let op_s = match op {
259 Operator::Add => "+",
260 Operator::Sub => "-",
261 Operator::Mul => "*",
262 Operator::Div => "/",
263 Operator::Eq => "==",
264 Operator::Neq => "!=",
265 Operator::Gt => ">",
266 Operator::Lt => "<",
267 Operator::Ge => ">=",
268 Operator::Le => "<=",
269 Operator::And => "and",
270 Operator::Or => "or",
271 };
272 format!("({} {op_s} {})", fmt_expr(left), fmt_expr(right))
273 }
274 E::Agg { func, expr } => {
275 let f = match func {
276 AggFunc::Sum => "sum",
277 AggFunc::Mean => "mean",
278 AggFunc::Count => "count",
279 AggFunc::Min => "min",
280 AggFunc::Max => "max",
281 };
282 format!("{f}({})", fmt_expr(expr))
283 }
284 E::Function { input, function } => {
285 let f = match function {
286 ExprFunction::String(function) => match function {
287 StringFunction::ToLowercase => "str.to_lowercase".to_string(),
288 StringFunction::ToUppercase => "str.to_uppercase".to_string(),
289 StringFunction::Contains { pattern } => {
290 format!("str.contains({pattern:?})")
291 }
292 StringFunction::Replace {
293 pattern,
294 replacement,
295 } => format!("str.replace({pattern:?}, {replacement:?})"),
296 StringFunction::StripChars { chars } => {
297 format!("str.strip_chars({chars:?})")
298 }
299 StringFunction::Split { separator } => {
300 format!("str.split({separator:?})")
301 }
302 StringFunction::LenChars => "str.len_chars".to_string(),
303 StringFunction::Extract {
304 pattern,
305 capture_group,
306 } => format!("str.extract({pattern:?}, {capture_group})"),
307 },
308 ExprFunction::Datetime(function) => match function {
309 DatetimeFunction::Year => "dt.year".to_string(),
310 DatetimeFunction::Month => "dt.month".to_string(),
311 DatetimeFunction::Day => "dt.day".to_string(),
312 DatetimeFunction::Weekday => "dt.weekday".to_string(),
313 DatetimeFunction::ToString => "dt.to_string".to_string(),
314 DatetimeFunction::ConvertTimeZone {
315 from_offset,
316 to_offset,
317 } => format!("dt.convert_time_zone({from_offset:?}, {to_offset:?})"),
318 },
319 ExprFunction::List(function) => match function {
320 ListFunction::Join {
321 separator,
322 null_value,
323 } => format!("list.join({separator:?}, {null_value:?})"),
324 ListFunction::Len => "list.len".to_string(),
325 ListFunction::Contains { value } => {
326 format!("list.contains({value:?})")
327 }
328 },
329 };
330 format!("{f}({})", fmt_expr(input))
331 }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::{LogicalPlan, ProjectionKind};
338 use crate::expr::{col, lit};
339
340 #[test]
341 fn display_is_readable_and_stable() {
342 let plan = LogicalPlan::Filter {
343 input: Box::new(LogicalPlan::Projection {
344 input: Box::new(LogicalPlan::CsvScan {
345 path: "data.csv".into(),
346 predicate: None,
347 projection: Some(vec!["a".to_string(), "b".to_string()]),
348 }),
349 exprs: vec![col("a"), col("b").alias("bb")],
350 kind: ProjectionKind::Select,
351 }),
352 predicate: col("a").gt(lit(1_i64)),
353 };
354
355 let s = plan.display();
356 assert!(s.contains("scan[csv"));
357 assert!(s.contains("project"));
358 assert!(s.contains("filter"));
359 assert!(s.contains("col(a)"));
360 }
361}