1use chryso_core::ChrysoResult;
2use chryso_core::ast::Literal;
3use chryso_core::ast::{Expr, JoinType, OrderByExpr, SelectStatement, Statement};
4use chryso_metadata::type_inference::TypeInferencer;
5
6pub mod cost;
7pub mod explain;
8pub mod plan_diff;
9pub mod serde;
10pub mod validate;
11
12pub use cost::{Cost, CostModel};
13pub use explain::{
14 ExplainConfig, ExplainFormatter, format_simple_logical_plan, format_simple_physical_plan,
15};
16
17#[derive(Debug, Clone)]
18pub enum LogicalPlan {
19 Scan {
20 table: String,
21 },
22 IndexScan {
23 table: String,
24 index: String,
25 predicate: Expr,
26 },
27 Dml {
28 sql: String,
29 },
30 Derived {
31 input: Box<LogicalPlan>,
32 alias: String,
33 column_aliases: Vec<String>,
34 },
35 Filter {
36 predicate: Expr,
37 input: Box<LogicalPlan>,
38 },
39 Projection {
40 exprs: Vec<Expr>,
41 input: Box<LogicalPlan>,
42 },
43 Join {
44 join_type: JoinType,
45 left: Box<LogicalPlan>,
46 right: Box<LogicalPlan>,
47 on: Expr,
48 },
49 Aggregate {
50 group_exprs: Vec<Expr>,
51 aggr_exprs: Vec<Expr>,
52 input: Box<LogicalPlan>,
53 },
54 Distinct {
55 input: Box<LogicalPlan>,
56 },
57 TopN {
58 order_by: Vec<OrderByExpr>,
59 limit: u64,
60 input: Box<LogicalPlan>,
61 },
62 Sort {
63 order_by: Vec<OrderByExpr>,
64 input: Box<LogicalPlan>,
65 },
66 Limit {
67 limit: Option<u64>,
68 offset: Option<u64>,
69 input: Box<LogicalPlan>,
70 },
71}
72
73#[derive(Debug, Clone)]
74pub enum PhysicalPlan {
75 TableScan {
76 table: String,
77 },
78 IndexScan {
79 table: String,
80 index: String,
81 predicate: Expr,
82 },
83 Dml {
84 sql: String,
85 },
86 Derived {
87 input: Box<PhysicalPlan>,
88 alias: String,
89 column_aliases: Vec<String>,
90 },
91 Filter {
92 predicate: Expr,
93 input: Box<PhysicalPlan>,
94 },
95 Projection {
96 exprs: Vec<Expr>,
97 input: Box<PhysicalPlan>,
98 },
99 Join {
100 join_type: JoinType,
101 algorithm: JoinAlgorithm,
102 left: Box<PhysicalPlan>,
103 right: Box<PhysicalPlan>,
104 on: Expr,
105 },
106 Aggregate {
107 group_exprs: Vec<Expr>,
108 aggr_exprs: Vec<Expr>,
109 input: Box<PhysicalPlan>,
110 },
111 Distinct {
112 input: Box<PhysicalPlan>,
113 },
114 TopN {
115 order_by: Vec<OrderByExpr>,
116 limit: u64,
117 input: Box<PhysicalPlan>,
118 },
119 Sort {
120 order_by: Vec<OrderByExpr>,
121 input: Box<PhysicalPlan>,
122 },
123 Limit {
124 limit: Option<u64>,
125 offset: Option<u64>,
126 input: Box<PhysicalPlan>,
127 },
128}
129
130pub struct PlanBuilder;
131
132impl PlanBuilder {
133 pub fn build(statement: Statement) -> ChrysoResult<LogicalPlan> {
134 let statement = chryso_core::ast::normalize_statement(&statement);
135 let formatted_sql = chryso_core::sql_format::format_statement(&statement);
136 let plan = match statement {
137 Statement::With(_) => Ok(LogicalPlan::Dml {
138 sql: formatted_sql.clone(),
139 }),
140 Statement::Select(select) => build_select(select),
141 Statement::SetOp { .. } => Ok(LogicalPlan::Dml {
142 sql: formatted_sql.clone(),
143 }),
144 Statement::Explain(_) => Ok(LogicalPlan::Dml {
145 sql: formatted_sql.clone(),
146 }),
147 Statement::CreateTable(_)
148 | Statement::DropTable(_)
149 | Statement::Truncate(_)
150 | Statement::Analyze(_) => Ok(LogicalPlan::Dml {
151 sql: formatted_sql.clone(),
152 }),
153 Statement::Insert(_) | Statement::Update(_) | Statement::Delete(_) => {
154 Ok(LogicalPlan::Dml {
155 sql: formatted_sql.clone(),
156 })
157 }
158 }?;
159 Ok(simplify_plan(plan))
160 }
161}
162
163fn build_select(select: SelectStatement) -> ChrysoResult<LogicalPlan> {
164 if select.qualify.is_some() || !select.distinct_on.is_empty() {
165 return Ok(LogicalPlan::Dml {
166 sql: chryso_core::sql_format::format_statement(&Statement::Select(select)),
167 });
168 }
169 let mut plan = if let Some(from) = select.from {
170 build_from(from)?
171 } else {
172 return Ok(LogicalPlan::Dml {
173 sql: chryso_core::sql_format::format_statement(&Statement::Select(select)),
174 });
175 };
176 if let Some(predicate) = select.selection {
177 plan = LogicalPlan::Filter {
178 predicate,
179 input: Box::new(plan),
180 };
181 }
182 let projection_exprs = select
183 .projection
184 .into_iter()
185 .map(|item| item.expr)
186 .collect::<Vec<_>>();
187 let has_aggregate = projection_exprs.iter().any(expr_is_aggregate);
188 if has_aggregate || !select.group_by.is_empty() {
189 let aggr_exprs = projection_exprs.clone();
190 plan = LogicalPlan::Aggregate {
191 group_exprs: select.group_by,
192 aggr_exprs,
193 input: Box::new(plan),
194 };
195 if let Some(having) = select.having {
196 plan = LogicalPlan::Filter {
197 predicate: having,
198 input: Box::new(plan),
199 };
200 }
201 }
202 plan = LogicalPlan::Projection {
203 exprs: projection_exprs,
204 input: Box::new(plan),
205 };
206 if select.distinct {
207 plan = LogicalPlan::Distinct {
208 input: Box::new(plan),
209 };
210 }
211 if !select.order_by.is_empty() {
212 if let (Some(limit), None) = (select.limit, select.offset) {
213 plan = LogicalPlan::TopN {
214 order_by: select.order_by,
215 limit,
216 input: Box::new(plan),
217 };
218 } else {
219 plan = LogicalPlan::Sort {
220 order_by: select.order_by,
221 input: Box::new(plan),
222 };
223 }
224 }
225 if select.limit.is_some() || select.offset.is_some() {
226 if matches!(plan, LogicalPlan::TopN { .. }) {
227 return Ok(plan);
228 }
229 plan = LogicalPlan::Limit {
230 limit: select.limit,
231 offset: select.offset,
232 input: Box::new(plan),
233 };
234 }
235 Ok(plan)
236}
237
238fn build_from(table: chryso_core::ast::TableRef) -> ChrysoResult<LogicalPlan> {
239 let mut plan = match table.factor {
240 chryso_core::ast::TableFactor::Table { name } => LogicalPlan::Scan { table: name },
241 chryso_core::ast::TableFactor::Derived { query } => {
242 let alias = table.alias.clone().unwrap_or_else(|| "derived".to_string());
243 LogicalPlan::Derived {
244 input: Box::new(build_query_plan(*query)?),
245 alias,
246 column_aliases: table.column_aliases.clone(),
247 }
248 }
249 };
250 for join in table.joins {
251 let right = build_from(join.right)?;
252 plan = LogicalPlan::Join {
253 join_type: join.join_type,
254 left: Box::new(plan),
255 right: Box::new(right),
256 on: join.on,
257 };
258 }
259 Ok(plan)
260}
261
262fn build_query_plan(statement: Statement) -> ChrysoResult<LogicalPlan> {
263 match statement {
264 Statement::Select(select) => build_select(select),
265 Statement::SetOp { .. } | Statement::With(_) | Statement::Explain(_) => {
266 Ok(LogicalPlan::Dml {
267 sql: chryso_core::sql_format::format_statement(&statement),
268 })
269 }
270 Statement::CreateTable(_)
271 | Statement::DropTable(_)
272 | Statement::Truncate(_)
273 | Statement::Analyze(_)
274 | Statement::Insert(_)
275 | Statement::Update(_)
276 | Statement::Delete(_) => Err(chryso_core::ChrysoError::new(
277 "subquery in FROM must be a query",
278 )),
279 }
280}
281
282fn simplify_plan(plan: LogicalPlan) -> LogicalPlan {
283 match plan {
284 LogicalPlan::Scan { .. } | LogicalPlan::IndexScan { .. } | LogicalPlan::Dml { .. } => plan,
285 LogicalPlan::Derived {
286 input,
287 alias,
288 column_aliases,
289 } => LogicalPlan::Derived {
290 input: Box::new(simplify_plan(*input)),
291 alias,
292 column_aliases,
293 },
294 LogicalPlan::Filter { predicate, input } => {
295 let predicate = predicate.normalize();
296 let input = simplify_plan(*input);
297 if matches!(predicate, Expr::Literal(Literal::Bool(true))) {
298 return input;
299 }
300 LogicalPlan::Filter {
301 predicate,
302 input: Box::new(input),
303 }
304 }
305 LogicalPlan::Projection { exprs, input } => {
306 let exprs = exprs
307 .into_iter()
308 .map(|expr| expr.normalize())
309 .collect::<Vec<_>>();
310 let input = simplify_plan(*input);
311 if exprs.len() == 1 && matches!(exprs[0], Expr::Wildcard) {
312 return input;
313 }
314 LogicalPlan::Projection {
315 exprs,
316 input: Box::new(input),
317 }
318 }
319 LogicalPlan::Join {
320 join_type,
321 left,
322 right,
323 on,
324 } => LogicalPlan::Join {
325 join_type,
326 left: Box::new(simplify_plan(*left)),
327 right: Box::new(simplify_plan(*right)),
328 on: on.normalize(),
329 },
330 LogicalPlan::Aggregate {
331 group_exprs,
332 aggr_exprs,
333 input,
334 } => LogicalPlan::Aggregate {
335 group_exprs: group_exprs
336 .into_iter()
337 .map(|expr| expr.normalize())
338 .collect(),
339 aggr_exprs: aggr_exprs
340 .into_iter()
341 .map(|expr| expr.normalize())
342 .collect(),
343 input: Box::new(simplify_plan(*input)),
344 },
345 LogicalPlan::Distinct { input } => LogicalPlan::Distinct {
346 input: Box::new(simplify_plan(*input)),
347 },
348 LogicalPlan::TopN {
349 order_by,
350 limit,
351 input,
352 } => LogicalPlan::TopN {
353 order_by: order_by
354 .into_iter()
355 .map(|item| OrderByExpr {
356 expr: item.expr.normalize(),
357 asc: item.asc,
358 nulls_first: item.nulls_first,
359 })
360 .collect(),
361 limit,
362 input: Box::new(simplify_plan(*input)),
363 },
364 LogicalPlan::Sort { order_by, input } => LogicalPlan::Sort {
365 order_by: order_by
366 .into_iter()
367 .map(|item| OrderByExpr {
368 expr: item.expr.normalize(),
369 asc: item.asc,
370 nulls_first: item.nulls_first,
371 })
372 .collect(),
373 input: Box::new(simplify_plan(*input)),
374 },
375 LogicalPlan::Limit {
376 limit,
377 offset,
378 input,
379 } => LogicalPlan::Limit {
380 limit,
381 offset,
382 input: Box::new(simplify_plan(*input)),
383 },
384 }
385}
386
387impl LogicalPlan {
388 pub fn explain(&self, indent: usize) -> String {
389 let padding = " ".repeat(indent);
390 match self {
391 LogicalPlan::Scan { table } => format!("{padding}LogicalScan table={table}"),
392 LogicalPlan::IndexScan {
393 table,
394 index,
395 predicate,
396 } => format!(
397 "{padding}LogicalIndexScan table={table} index={index} predicate={}",
398 fmt_expr(predicate)
399 ),
400 LogicalPlan::Dml { sql } => format!("{padding}LogicalDml sql={sql}"),
401 LogicalPlan::Derived {
402 alias,
403 column_aliases,
404 input,
405 } => format!(
406 "{padding}LogicalDerived alias={alias} cols={}\n{}",
407 column_aliases.join(","),
408 input.explain(indent + 2)
409 ),
410 LogicalPlan::Filter { predicate, input } => format!(
411 "{padding}LogicalFilter predicate={}\n{}",
412 fmt_expr(predicate),
413 input.explain(indent + 2)
414 ),
415 LogicalPlan::Projection { exprs, input } => format!(
416 "{padding}LogicalProject exprs={}\n{}",
417 fmt_expr_list(exprs),
418 input.explain(indent + 2)
419 ),
420 LogicalPlan::Join {
421 join_type,
422 left,
423 right,
424 on,
425 } => format!(
426 "{padding}LogicalJoin type={join_type:?} on={}\n{}\n{}",
427 fmt_expr(on),
428 left.explain(indent + 2),
429 right.explain(indent + 2)
430 ),
431 LogicalPlan::Aggregate {
432 group_exprs,
433 aggr_exprs,
434 input,
435 } => format!(
436 "{padding}LogicalAggregate group={} aggr={}\n{}",
437 fmt_expr_list(group_exprs),
438 fmt_expr_list(aggr_exprs),
439 input.explain(indent + 2)
440 ),
441 LogicalPlan::Distinct { input } => {
442 format!("{padding}LogicalDistinct\n{}", input.explain(indent + 2))
443 }
444 LogicalPlan::TopN {
445 order_by,
446 limit,
447 input,
448 } => format!(
449 "{padding}LogicalTopN order_by={} limit={limit}\n{}",
450 fmt_order_by_list(order_by),
451 input.explain(indent + 2)
452 ),
453 LogicalPlan::Sort { order_by, input } => format!(
454 "{padding}LogicalSort order_by={}\n{}",
455 fmt_order_by_list(order_by),
456 input.explain(indent + 2)
457 ),
458 LogicalPlan::Limit {
459 limit,
460 offset,
461 input,
462 } => format!(
463 "{padding}LogicalLimit limit={limit:?} offset={offset:?}\n{}",
464 input.explain(indent + 2)
465 ),
466 }
467 }
468
469 pub fn explain_formatted(
470 &self,
471 config: &ExplainConfig,
472 inferencer: &dyn TypeInferencer,
473 ) -> String {
474 let formatter = ExplainFormatter::new(config.clone());
475 formatter.format_logical_plan(self, inferencer)
476 }
477
478 pub fn explain_typed(
479 &self,
480 indent: usize,
481 inferencer: &dyn chryso_metadata::type_inference::TypeInferencer,
482 ) -> String {
483 let padding = " ".repeat(indent);
484 match self {
485 LogicalPlan::Scan { table } => format!("{padding}LogicalScan table={table}"),
486 LogicalPlan::IndexScan {
487 table,
488 index,
489 predicate,
490 } => format!(
491 "{padding}LogicalIndexScan table={table} index={index} predicate={}",
492 fmt_expr(predicate)
493 ),
494 LogicalPlan::Dml { sql } => format!("{padding}LogicalDml sql={sql}"),
495 LogicalPlan::Derived {
496 alias,
497 column_aliases,
498 input,
499 } => format!(
500 "{padding}LogicalDerived alias={alias} cols={}\n{}",
501 column_aliases.join(","),
502 input.explain_typed(indent + 2, inferencer)
503 ),
504 LogicalPlan::Filter { predicate, input } => format!(
505 "{padding}LogicalFilter predicate={} type={:?}\n{}",
506 fmt_expr(predicate),
507 inferencer.infer_expr(predicate),
508 input.explain_typed(indent + 2, inferencer)
509 ),
510 LogicalPlan::Projection { exprs, input } => {
511 let types = chryso_metadata::type_inference::expr_types(exprs, inferencer);
512 format!(
513 "{padding}LogicalProject exprs={} types={types:?}\n{}",
514 fmt_expr_list(exprs),
515 input.explain_typed(indent + 2, inferencer)
516 )
517 }
518 LogicalPlan::Join {
519 join_type,
520 left,
521 right,
522 on,
523 } => format!(
524 "{padding}LogicalJoin type={join_type:?} on={}\n{}\n{}",
525 fmt_expr(on),
526 left.explain_typed(indent + 2, inferencer),
527 right.explain_typed(indent + 2, inferencer)
528 ),
529 LogicalPlan::Aggregate {
530 group_exprs,
531 aggr_exprs,
532 input,
533 } => format!(
534 "{padding}LogicalAggregate group={} aggr={}\n{}",
535 fmt_expr_list(group_exprs),
536 fmt_expr_list(aggr_exprs),
537 input.explain_typed(indent + 2, inferencer)
538 ),
539 LogicalPlan::Distinct { input } => format!(
540 "{padding}LogicalDistinct\n{}",
541 input.explain_typed(indent + 2, inferencer)
542 ),
543 LogicalPlan::TopN {
544 order_by,
545 limit,
546 input,
547 } => format!(
548 "{padding}LogicalTopN order_by={} limit={limit}\n{}",
549 fmt_order_by_list(order_by),
550 input.explain_typed(indent + 2, inferencer)
551 ),
552 LogicalPlan::Sort { order_by, input } => format!(
553 "{padding}LogicalSort order_by={}\n{}",
554 fmt_order_by_list(order_by),
555 input.explain_typed(indent + 2, inferencer)
556 ),
557 LogicalPlan::Limit {
558 limit,
559 offset,
560 input,
561 } => format!(
562 "{padding}LogicalLimit limit={limit:?} offset={offset:?}\n{}",
563 input.explain_typed(indent + 2, inferencer)
564 ),
565 }
566 }
567}
568
569impl PhysicalPlan {
570 pub fn explain(&self, indent: usize) -> String {
571 let padding = " ".repeat(indent);
572 match self {
573 PhysicalPlan::TableScan { table } => format!("{padding}TableScan table={table}"),
574 PhysicalPlan::IndexScan {
575 table,
576 index,
577 predicate,
578 } => format!(
579 "{padding}IndexScan table={table} index={index} predicate={}",
580 fmt_expr(predicate)
581 ),
582 PhysicalPlan::Dml { sql } => format!("{padding}Dml sql={sql}"),
583 PhysicalPlan::Derived {
584 alias,
585 column_aliases,
586 input,
587 } => format!(
588 "{padding}Derived alias={alias} cols={}\n{}",
589 column_aliases.join(","),
590 input.explain(indent + 2)
591 ),
592 PhysicalPlan::Filter { predicate, input } => format!(
593 "{padding}Filter predicate={}\n{}",
594 fmt_expr(predicate),
595 input.explain(indent + 2)
596 ),
597 PhysicalPlan::Projection { exprs, input } => format!(
598 "{padding}Project exprs={}\n{}",
599 fmt_expr_list(exprs),
600 input.explain(indent + 2)
601 ),
602 PhysicalPlan::Join {
603 join_type,
604 algorithm,
605 left,
606 right,
607 on,
608 } => format!(
609 "{padding}Join type={join_type:?} algorithm={algorithm:?} on={}\n{}\n{}",
610 fmt_expr(on),
611 left.explain(indent + 2),
612 right.explain(indent + 2)
613 ),
614 PhysicalPlan::Aggregate {
615 group_exprs,
616 aggr_exprs,
617 input,
618 } => format!(
619 "{padding}Aggregate group={} aggr={}\n{}",
620 fmt_expr_list(group_exprs),
621 fmt_expr_list(aggr_exprs),
622 input.explain(indent + 2)
623 ),
624 PhysicalPlan::Distinct { input } => {
625 format!("{padding}Distinct\n{}", input.explain(indent + 2))
626 }
627 PhysicalPlan::TopN {
628 order_by,
629 limit,
630 input,
631 } => format!(
632 "{padding}TopN order_by={} limit={limit}\n{}",
633 fmt_order_by_list(order_by),
634 input.explain(indent + 2)
635 ),
636 PhysicalPlan::Sort { order_by, input } => format!(
637 "{padding}Sort order_by={}\n{}",
638 fmt_order_by_list(order_by),
639 input.explain(indent + 2)
640 ),
641 PhysicalPlan::Limit {
642 limit,
643 offset,
644 input,
645 } => format!(
646 "{padding}Limit limit={limit:?} offset={offset:?}\n{}",
647 input.explain(indent + 2)
648 ),
649 }
650 }
651
652 pub fn explain_costed(&self, indent: usize, cost_model: &dyn crate::cost::CostModel) -> String {
653 let padding = " ".repeat(indent);
654 let cost = cost_model.cost(self).0;
655 match self {
656 PhysicalPlan::TableScan { table } => {
657 format!("{padding}TableScan table={table} cost={cost}")
658 }
659 PhysicalPlan::IndexScan {
660 table,
661 index,
662 predicate,
663 } => format!(
664 "{padding}IndexScan table={table} index={index} predicate={} cost={cost}",
665 fmt_expr(predicate)
666 ),
667 PhysicalPlan::Dml { sql } => format!("{padding}Dml sql={sql} cost={cost}"),
668 PhysicalPlan::Derived {
669 alias,
670 column_aliases,
671 input,
672 } => format!(
673 "{padding}Derived alias={alias} cols={} cost={cost}\n{}",
674 column_aliases.join(","),
675 input.explain_costed(indent + 2, cost_model)
676 ),
677 PhysicalPlan::Filter { predicate, input } => format!(
678 "{padding}Filter predicate={} cost={cost}\n{}",
679 fmt_expr(predicate),
680 input.explain_costed(indent + 2, cost_model)
681 ),
682 PhysicalPlan::Projection { exprs, input } => format!(
683 "{padding}Project exprs={} cost={cost}\n{}",
684 fmt_expr_list(exprs),
685 input.explain_costed(indent + 2, cost_model)
686 ),
687 PhysicalPlan::Join {
688 join_type,
689 algorithm,
690 left,
691 right,
692 on,
693 } => format!(
694 "{padding}Join type={join_type:?} algorithm={algorithm:?} on={} cost={cost}\n{}\n{}",
695 fmt_expr(on),
696 left.explain_costed(indent + 2, cost_model),
697 right.explain_costed(indent + 2, cost_model)
698 ),
699 PhysicalPlan::Aggregate {
700 group_exprs,
701 aggr_exprs,
702 input,
703 } => format!(
704 "{padding}Aggregate group={} aggr={} cost={cost}\n{}",
705 fmt_expr_list(group_exprs),
706 fmt_expr_list(aggr_exprs),
707 input.explain_costed(indent + 2, cost_model)
708 ),
709 PhysicalPlan::Distinct { input } => format!(
710 "{padding}Distinct cost={cost}\n{}",
711 input.explain_costed(indent + 2, cost_model)
712 ),
713 PhysicalPlan::TopN {
714 order_by,
715 limit,
716 input,
717 } => format!(
718 "{padding}TopN order_by={} limit={limit} cost={cost}\n{}",
719 fmt_order_by_list(order_by),
720 input.explain_costed(indent + 2, cost_model)
721 ),
722 PhysicalPlan::Sort { order_by, input } => format!(
723 "{padding}Sort order_by={} cost={cost}\n{}",
724 fmt_order_by_list(order_by),
725 input.explain_costed(indent + 2, cost_model)
726 ),
727 PhysicalPlan::Limit {
728 limit,
729 offset,
730 input,
731 } => format!(
732 "{padding}Limit limit={limit:?} offset={offset:?} cost={cost}\n{}",
733 input.explain_costed(indent + 2, cost_model)
734 ),
735 }
736 }
737}
738
739#[derive(Debug, Clone, Copy)]
740pub enum JoinAlgorithm {
741 Hash,
742 NestedLoop,
743}
744
745#[cfg(test)]
746mod tests {
747 use super::{LogicalPlan, PlanBuilder};
748 use chryso_parser::{Dialect, ParserConfig, SimpleParser, SqlParser};
749
750 #[test]
751 fn planner_simplifies_select_star() {
752 let sql = "select * from t";
753 let parser = SimpleParser::new(ParserConfig {
754 dialect: Dialect::Postgres,
755 });
756 let stmt = parser.parse(sql).expect("parse");
757 let logical = PlanBuilder::build(stmt).expect("plan");
758 assert!(matches!(logical, LogicalPlan::Scan { .. }));
759 }
760
761 #[test]
762 fn planner_removes_true_filter() {
763 let sql = "select * from t where not false";
764 let parser = SimpleParser::new(ParserConfig {
765 dialect: Dialect::Postgres,
766 });
767 let stmt = parser.parse(sql).expect("parse");
768 let logical = PlanBuilder::build(stmt).expect("plan");
769 assert!(matches!(logical, LogicalPlan::Scan { .. }));
770 }
771
772 #[test]
773 fn planner_builds_topn_for_ordered_limit() {
774 let sql = "select * from t order by id limit 5";
775 let parser = SimpleParser::new(ParserConfig {
776 dialect: Dialect::Postgres,
777 });
778 let stmt = parser.parse(sql).expect("parse");
779 let logical = PlanBuilder::build(stmt).expect("plan");
780 assert!(matches!(logical, LogicalPlan::TopN { .. }));
781 }
782}
783
784fn expr_is_aggregate(expr: &Expr) -> bool {
785 match expr {
786 Expr::FunctionCall { name, .. } => {
787 matches!(
788 name.to_ascii_lowercase().as_str(),
789 "sum" | "count" | "avg" | "min" | "max"
790 )
791 }
792 _ => false,
793 }
794}
795
796fn fmt_expr(expr: &Expr) -> String {
797 expr.to_sql()
798}
799
800fn fmt_expr_list(exprs: &[Expr]) -> String {
801 exprs.iter().map(fmt_expr).collect::<Vec<_>>().join(", ")
802}
803
804fn fmt_order_by_list(order_by: &[OrderByExpr]) -> String {
805 order_by
806 .iter()
807 .map(|item| {
808 let dir = if item.asc { "asc" } else { "desc" };
809 let mut rendered = format!("{} {dir}", fmt_expr(&item.expr));
810 if let Some(nulls_first) = item.nulls_first {
811 if nulls_first {
812 rendered.push_str(" nulls first");
813 } else {
814 rendered.push_str(" nulls last");
815 }
816 }
817 rendered
818 })
819 .collect::<Vec<_>>()
820 .join(", ")
821}