1use std::hash::{Hash, Hasher};
27use std::num::NonZeroUsize;
28use std::sync::Arc;
29
30use lru::LruCache;
31use parking_lot::Mutex;
32use rustc_hash::FxHasher;
33
34use radixdb_sql::ast::{Expression, GroupByModifier, SelectStatement};
35
36use super::join_graph::LogicalJoinGraph;
37
38const CLASSIFICATION_CACHE_SIZE: usize = 512;
40
41type ClassificationBucket = Vec<(SelectStatement, Arc<QueryClassification>)>;
43static CLASSIFICATION_CACHE: Mutex<Option<LruCache<u64, ClassificationBucket>>> = Mutex::new(None);
44
45pub fn clear_classification_cache() {
47 let mut guard = CLASSIFICATION_CACHE.lock();
48 *guard = None;
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub struct QueryClassification {
54 pub has_aggregation: bool,
57 pub has_window_functions: bool,
59 pub has_group_by: bool,
61 pub has_order_by: bool,
63 pub has_limit: bool,
65 pub has_offset: bool,
67 pub has_distinct: bool,
69 pub has_distinct_on: bool,
71 pub has_set_operations: bool,
73 pub has_having: bool,
75
76 pub is_select_star: bool,
79 pub select_has_scalar_subqueries: bool,
81 pub join_projection_dependencies: Option<Arc<Vec<Expression>>>,
84
85 pub has_joins: bool,
88 pub join_count: usize,
90 pub has_outer_joins: bool,
92 pub has_derived_tables: bool,
94 pub reorderable_join_count: usize,
96 #[doc(hidden)]
99 pub logical_join_graph: Option<Arc<LogicalJoinGraph>>,
100
101 pub has_where: bool,
104 pub where_has_parameters: bool,
106 pub where_has_subqueries: bool,
108 pub where_has_correlated_subqueries: bool,
110 pub has_nondeterministic_functions: bool,
113
114 pub select_has_correlated_subqueries: bool,
117
118 pub order_by_has_correlated_subqueries: bool,
121}
122
123impl QueryClassification {
124 pub fn classify(stmt: &SelectStatement) -> Self {
126 let has_aggregation = Self::check_has_aggregation(stmt);
128 let has_window_functions = Self::check_has_window_functions(stmt);
129 let has_group_by = !stmt.group_by.columns.is_empty();
130 let has_order_by = !stmt.order_by.is_empty();
131 let has_limit = stmt.limit.is_some();
132 let has_offset = stmt.offset.is_some();
133 let has_distinct = stmt.distinct;
134 let has_distinct_on = !stmt.distinct_on.is_empty();
135 let has_set_operations = !stmt.set_operations.is_empty();
136 let has_having = stmt.having.is_some();
137
138 let is_select_star =
140 stmt.columns.len() == 1 && matches!(stmt.columns.first(), Some(Expression::Star(_)));
141 let select_has_scalar_subqueries = stmt
142 .columns
143 .iter()
144 .any(Self::expression_has_scalar_subquery);
145 let join_projection_dependencies = Self::collect_join_projection_dependencies(stmt);
146
147 let (has_joins, has_outer_joins, join_count, has_derived_tables) =
149 Self::analyze_table_source(&stmt.table_expr);
150 let logical_join_graph = stmt
151 .table_expr
152 .as_deref()
153 .and_then(LogicalJoinGraph::bind)
154 .map(Arc::new);
155 let reorderable_join_count = logical_join_graph
156 .as_deref()
157 .map_or(0, LogicalJoinGraph::reorderable_edge_count);
158
159 let has_where = stmt.where_clause.is_some();
161 let (
162 where_has_parameters,
163 where_has_subqueries,
164 _,
165 _,
166 _,
167 _,
168 where_has_correlated_subqueries,
169 ) = if let Some(ref where_clause) = stmt.where_clause {
170 Self::analyze_where_clause(where_clause)
171 } else {
172 (false, false, false, false, false, false, false)
173 };
174
175 let has_nondeterministic_functions = stmt
179 .where_clause
180 .as_ref()
181 .is_some_and(|wc| Self::expression_has_nondeterministic_functions(wc))
182 || stmt
183 .columns
184 .iter()
185 .any(Self::expression_has_nondeterministic_functions);
186
187 let select_has_correlated_subqueries = stmt
189 .columns
190 .iter()
191 .any(Self::expression_has_correlated_subqueries);
192
193 let order_by_has_correlated_subqueries = stmt
195 .order_by
196 .iter()
197 .any(|ob| Self::expression_has_correlated_subqueries(&ob.expression));
198
199 QueryClassification {
200 has_aggregation,
201 has_window_functions,
202 has_group_by,
203 has_order_by,
204 has_limit,
205 has_offset,
206 has_distinct,
207 has_distinct_on,
208 has_set_operations,
209 has_having,
210 is_select_star,
211 select_has_scalar_subqueries,
212 join_projection_dependencies,
213 has_joins,
214 join_count,
215 has_outer_joins,
216 has_derived_tables,
217 reorderable_join_count,
218 logical_join_graph,
219 has_where,
220 where_has_parameters,
221 where_has_subqueries,
222 where_has_correlated_subqueries,
223 has_nondeterministic_functions,
224 select_has_correlated_subqueries,
225 order_by_has_correlated_subqueries,
226 }
227 }
228
229 fn collect_join_projection_dependencies(
230 stmt: &SelectStatement,
231 ) -> Option<Arc<Vec<Expression>>> {
232 let mut dependencies = Vec::new();
233 let mut blocked = false;
234 let mut collect = |expression: &Expression| {
235 radixdb_sql::ast::walk_expression_tree(expression, &mut |node| match node {
236 Expression::Identifier(_) | Expression::QualifiedIdentifier(_) => {
237 dependencies.push(node.clone());
238 }
239 Expression::Star(_)
240 | Expression::QualifiedStar(_)
241 | Expression::Exists(_)
242 | Expression::AllAny(_)
243 | Expression::ScalarSubquery(_) => blocked = true,
244 _ => {}
245 });
246 };
247
248 for expression in stmt.distinct_on.iter().chain(&stmt.columns) {
249 collect(expression);
250 }
251 if let Some(expression) = stmt.where_clause.as_deref() {
252 collect(expression);
253 }
254 for expression in &stmt.group_by.columns {
255 collect(expression);
256 }
257 if let GroupByModifier::GroupingSets(sets) = &stmt.group_by.modifier {
258 for set in sets {
259 for expression in set {
260 collect(expression);
261 }
262 }
263 }
264 if let Some(expression) = stmt.having.as_deref() {
265 collect(expression);
266 }
267 for window in &stmt.window_defs {
268 for expression in &window.partition_by {
269 collect(expression);
270 }
271 for order in &window.order_by {
272 collect(&order.expression);
273 }
274 }
275 for order in &stmt.order_by {
276 collect(&order.expression);
277 }
278
279 (!blocked).then(|| Arc::new(dependencies))
280 }
281
282 fn analyze_table_source(table_expr: &Option<Box<Expression>>) -> (bool, bool, usize, bool) {
284 let mut has_joins = false;
285 let mut has_outer_joins = false;
286 let mut join_count = 0;
287 let mut has_derived_tables = false;
288
289 if let Some(ref expr) = table_expr {
290 Self::analyze_table_expr_recursive(
291 expr,
292 &mut has_joins,
293 &mut has_outer_joins,
294 &mut join_count,
295 &mut has_derived_tables,
296 );
297 }
298
299 (has_joins, has_outer_joins, join_count, has_derived_tables)
300 }
301
302 fn analyze_table_expr_recursive(
304 expr: &Expression,
305 has_joins: &mut bool,
306 has_outer_joins: &mut bool,
307 join_count: &mut usize,
308 has_derived_tables: &mut bool,
309 ) {
310 match expr {
311 Expression::JoinSource(join) => {
312 *has_joins = true;
313 *join_count += 1;
314
315 let join_type = join.join_type.to_uppercase();
317 if join_type.contains("LEFT")
318 || join_type.contains("RIGHT")
319 || join_type.contains("FULL")
320 {
321 *has_outer_joins = true;
322 }
323
324 Self::analyze_table_expr_recursive(
326 &join.left,
327 has_joins,
328 has_outer_joins,
329 join_count,
330 has_derived_tables,
331 );
332 Self::analyze_table_expr_recursive(
333 &join.right,
334 has_joins,
335 has_outer_joins,
336 join_count,
337 has_derived_tables,
338 );
339 }
340 Expression::SubquerySource(_) | Expression::ScalarSubquery(_) => {
341 *has_derived_tables = true;
342 }
343 Expression::Aliased(aliased) => {
344 Self::analyze_table_expr_recursive(
345 &aliased.expression,
346 has_joins,
347 has_outer_joins,
348 join_count,
349 has_derived_tables,
350 );
351 }
352 _ => {}
353 }
354 }
355
356 fn analyze_where_clause(expr: &Expression) -> (bool, bool, bool, bool, bool, bool, bool) {
358 let has_parameters = Self::expression_has_parameters(expr);
359 let mut has_exists = false;
360 let mut has_in_subquery = false;
361 let mut has_scalar_subquery = false;
362 let mut has_all_any = false;
363
364 Self::analyze_where_expr_recursive(
365 expr,
366 &mut has_exists,
367 &mut has_in_subquery,
368 &mut has_scalar_subquery,
369 &mut has_all_any,
370 );
371
372 let has_subqueries = has_exists || has_in_subquery || has_scalar_subquery || has_all_any;
373
374 let has_correlated = Self::expression_has_correlated_subqueries(expr);
376
377 (
378 has_parameters,
379 has_subqueries,
380 has_exists,
381 has_in_subquery,
382 has_scalar_subquery,
383 has_all_any,
384 has_correlated,
385 )
386 }
387
388 fn analyze_where_expr_recursive(
390 expr: &Expression,
391 has_exists: &mut bool,
392 has_in_subquery: &mut bool,
393 has_scalar_subquery: &mut bool,
394 has_all_any: &mut bool,
395 ) {
396 radixdb_sql::ast::walk_expression_tree(expr, &mut |expression| match expression {
397 Expression::Exists(_) => *has_exists = true,
398 Expression::AllAny(_) => *has_all_any = true,
399 Expression::ScalarSubquery(_) => *has_scalar_subquery = true,
400 Expression::In(in_expr)
401 if matches!(
402 in_expr.right.as_ref(),
403 Expression::ScalarSubquery(_) | Expression::SubquerySource(_)
404 ) =>
405 {
406 *has_in_subquery = true;
407 }
408 _ => {}
409 });
410 }
411
412 fn expression_has_scalar_subquery(expr: &Expression) -> bool {
414 let mut found = false;
415 radixdb_sql::ast::walk_expression_tree(expr, &mut |expression| {
416 found |= matches!(expression, Expression::ScalarSubquery(_));
417 });
418 found
419 }
420
421 fn check_has_aggregation(stmt: &SelectStatement) -> bool {
423 !stmt.group_by.columns.is_empty()
424 || stmt
425 .columns
426 .iter()
427 .chain(&stmt.distinct_on)
428 .any(Self::expression_has_aggregation)
429 || stmt
430 .having
431 .as_deref()
432 .is_some_and(Self::expression_has_aggregation)
433 || stmt
434 .order_by
435 .iter()
436 .any(|order| Self::expression_has_aggregation(&order.expression))
437 }
438
439 fn expression_has_aggregation(expr: &Expression) -> bool {
441 match expr {
442 Expression::FunctionCall(func) => {
443 if is_aggregate_function(&func.function) {
444 return true;
445 }
446 func.arguments.iter().any(Self::expression_has_aggregation)
447 || func
448 .order_by
449 .iter()
450 .any(|order| Self::expression_has_aggregation(&order.expression))
451 || func
452 .filter
453 .as_deref()
454 .is_some_and(Self::expression_has_aggregation)
455 }
456 Expression::Aliased(aliased) => Self::expression_has_aggregation(&aliased.expression),
457 Expression::Infix(infix) => {
458 Self::expression_has_aggregation(&infix.left)
459 || Self::expression_has_aggregation(&infix.right)
460 }
461 Expression::Prefix(prefix) => Self::expression_has_aggregation(&prefix.right),
462 Expression::Cast(cast) => Self::expression_has_aggregation(&cast.expr),
463 Expression::Case(case) => {
464 case.value
465 .as_deref()
466 .is_some_and(Self::expression_has_aggregation)
467 || case.when_clauses.iter().any(|w| {
468 Self::expression_has_aggregation(&w.condition)
469 || Self::expression_has_aggregation(&w.then_result)
470 })
471 || case
472 .else_value
473 .as_deref()
474 .is_some_and(Self::expression_has_aggregation)
475 }
476 Expression::Distinct(distinct) => Self::expression_has_aggregation(&distinct.expr),
477 Expression::Between(between) => {
478 Self::expression_has_aggregation(&between.expr)
479 || Self::expression_has_aggregation(&between.lower)
480 || Self::expression_has_aggregation(&between.upper)
481 }
482 Expression::In(in_expr) => {
483 Self::expression_has_aggregation(&in_expr.left)
484 || Self::expression_has_aggregation(&in_expr.right)
485 }
486 Expression::Like(like) => {
487 Self::expression_has_aggregation(&like.left)
488 || Self::expression_has_aggregation(&like.pattern)
489 || like
490 .escape
491 .as_deref()
492 .is_some_and(Self::expression_has_aggregation)
493 }
494 Expression::List(list) => list.elements.iter().any(Self::expression_has_aggregation),
495 Expression::ExpressionList(list) => list
496 .expressions
497 .iter()
498 .any(Self::expression_has_aggregation),
499 Expression::ScalarSubquery(_) | Expression::SubquerySource(_) => false, _ => false,
501 }
502 }
503
504 fn check_has_window_functions(stmt: &SelectStatement) -> bool {
506 stmt.columns
507 .iter()
508 .chain(&stmt.distinct_on)
509 .any(Self::expression_has_window_function)
510 || stmt
511 .having
512 .as_deref()
513 .is_some_and(Self::expression_has_window_function)
514 || stmt
515 .order_by
516 .iter()
517 .any(|order| Self::expression_has_window_function(&order.expression))
518 }
519
520 fn expression_has_window_function(expr: &Expression) -> bool {
522 match expr {
523 Expression::Window(_) => true,
524 Expression::Aliased(aliased) => {
525 Self::expression_has_window_function(&aliased.expression)
526 }
527 Expression::Infix(infix) => {
528 Self::expression_has_window_function(&infix.left)
529 || Self::expression_has_window_function(&infix.right)
530 }
531 Expression::Prefix(prefix) => Self::expression_has_window_function(&prefix.right),
532 Expression::Cast(cast) => Self::expression_has_window_function(&cast.expr),
533 Expression::Distinct(d) => Self::expression_has_window_function(&d.expr),
534 Expression::FunctionCall(func) => {
535 func.arguments
536 .iter()
537 .any(Self::expression_has_window_function)
538 || func
539 .order_by
540 .iter()
541 .any(|order| Self::expression_has_window_function(&order.expression))
542 || func
543 .filter
544 .as_deref()
545 .is_some_and(Self::expression_has_window_function)
546 }
547 Expression::Case(case) => {
548 case.value
549 .as_ref()
550 .is_some_and(|v| Self::expression_has_window_function(v))
551 || case.when_clauses.iter().any(|w| {
552 Self::expression_has_window_function(&w.condition)
553 || Self::expression_has_window_function(&w.then_result)
554 })
555 || case
556 .else_value
557 .as_ref()
558 .is_some_and(|e| Self::expression_has_window_function(e))
559 }
560 Expression::Between(b) => {
561 Self::expression_has_window_function(&b.expr)
562 || Self::expression_has_window_function(&b.lower)
563 || Self::expression_has_window_function(&b.upper)
564 }
565 Expression::In(i) => {
566 Self::expression_has_window_function(&i.left)
567 || Self::expression_has_window_function(&i.right)
568 }
569 Expression::Like(l) => {
570 Self::expression_has_window_function(&l.left)
571 || Self::expression_has_window_function(&l.pattern)
572 || l.escape
573 .as_ref()
574 .is_some_and(|e| Self::expression_has_window_function(e))
575 }
576 Expression::List(l) => l.elements.iter().any(Self::expression_has_window_function),
577 Expression::ExpressionList(l) => l
578 .expressions
579 .iter()
580 .any(Self::expression_has_window_function),
581 _ => false,
582 }
583 }
584
585 fn expression_has_parameters(expr: &Expression) -> bool {
587 let mut found = false;
588 radixdb_sql::ast::walk_expression_tree(expr, &mut |expression| {
589 found |= matches!(expression, Expression::Parameter(_));
590 });
591 found
592 }
593 fn expression_has_nondeterministic_functions(expr: &Expression) -> bool {
597 match expr {
598 Expression::FunctionCall(func) => {
599 super::expression::is_non_foldable_function(&func.function)
600 || func
601 .arguments
602 .iter()
603 .any(Self::expression_has_nondeterministic_functions)
604 }
605 Expression::Prefix(prefix) => {
606 Self::expression_has_nondeterministic_functions(&prefix.right)
607 }
608 Expression::Infix(infix) => {
609 Self::expression_has_nondeterministic_functions(&infix.left)
610 || Self::expression_has_nondeterministic_functions(&infix.right)
611 }
612 Expression::In(in_expr) => {
613 Self::expression_has_nondeterministic_functions(&in_expr.left)
614 || Self::expression_has_nondeterministic_functions(&in_expr.right)
615 }
616 Expression::List(list) => list
617 .elements
618 .iter()
619 .any(Self::expression_has_nondeterministic_functions),
620 Expression::ExpressionList(list) => list
621 .expressions
622 .iter()
623 .any(Self::expression_has_nondeterministic_functions),
624 Expression::Between(between) => {
625 Self::expression_has_nondeterministic_functions(&between.expr)
626 || Self::expression_has_nondeterministic_functions(&between.lower)
627 || Self::expression_has_nondeterministic_functions(&between.upper)
628 }
629 Expression::Like(like) => {
630 Self::expression_has_nondeterministic_functions(&like.left)
631 || Self::expression_has_nondeterministic_functions(&like.pattern)
632 || like
633 .escape
634 .as_ref()
635 .is_some_and(|e| Self::expression_has_nondeterministic_functions(e))
636 }
637 Expression::Case(case) => {
638 case.value
639 .as_ref()
640 .is_some_and(|e| Self::expression_has_nondeterministic_functions(e))
641 || case.when_clauses.iter().any(|w| {
642 Self::expression_has_nondeterministic_functions(&w.condition)
643 || Self::expression_has_nondeterministic_functions(&w.then_result)
644 })
645 || case
646 .else_value
647 .as_ref()
648 .is_some_and(|e| Self::expression_has_nondeterministic_functions(e))
649 }
650 Expression::Aliased(aliased) => {
651 Self::expression_has_nondeterministic_functions(&aliased.expression)
652 }
653 Expression::Cast(cast) => Self::expression_has_nondeterministic_functions(&cast.expr),
654 Expression::Distinct(distinct) => {
655 Self::expression_has_nondeterministic_functions(&distinct.expr)
656 }
657 Expression::ScalarSubquery(subquery) => {
658 subquery
659 .subquery
660 .columns
661 .iter()
662 .any(Self::expression_has_nondeterministic_functions)
663 || subquery
664 .subquery
665 .where_clause
666 .as_ref()
667 .is_some_and(|w| Self::expression_has_nondeterministic_functions(w))
668 }
669 Expression::AllAny(all_any) => {
670 Self::expression_has_nondeterministic_functions(&all_any.left)
671 }
672 Expression::InHashSet(in_hash) => {
673 Self::expression_has_nondeterministic_functions(&in_hash.column)
674 }
675 _ => false,
676 }
677 }
678
679 fn expression_has_correlated_subqueries(expr: &Expression) -> bool {
682 let mut correlated = false;
683 radixdb_sql::ast::walk_expression_tree(expr, &mut |expression| {
684 if correlated {
685 return;
686 }
687 correlated = match expression {
688 Expression::Exists(exists) => Self::is_subquery_correlated(&exists.subquery),
689 Expression::ScalarSubquery(subquery) => {
690 Self::is_subquery_correlated(&subquery.subquery)
691 }
692 Expression::AllAny(all_any) => Self::is_subquery_correlated(&all_any.subquery),
693 _ => false,
694 };
695 });
696 correlated
697 }
698
699 fn is_subquery_correlated(subquery: &SelectStatement) -> bool {
701 let subquery_tables = Self::collect_subquery_tables(&subquery.table_expr);
703 let mut correlated = false;
704 radixdb_sql::ast::walk_select_tree(subquery, &mut |expression| {
705 if correlated {
706 return;
707 }
708 match expression {
709 Expression::QualifiedIdentifier(qid) => {
710 correlated = !subquery_tables
711 .iter()
712 .any(|table| table.eq_ignore_ascii_case(&qid.qualifier.value_lower));
713 }
714 Expression::Identifier(_) => correlated = true,
715 _ => {}
716 }
717 });
718 correlated
719 }
720
721 fn collect_subquery_tables(table_expr: &Option<Box<Expression>>) -> Vec<String> {
723 let mut tables = Vec::new();
724 if let Some(ref expr) = table_expr {
725 Self::collect_tables_recursive(expr, &mut tables);
726 }
727 tables
728 }
729
730 fn collect_tables_recursive(expr: &Expression, tables: &mut Vec<String>) {
732 match expr {
733 Expression::Identifier(ident) => {
734 tables.push(ident.value_lower.to_string());
735 }
736 Expression::Aliased(aliased) => {
737 tables.push(aliased.alias.value_lower.to_string());
739 Self::collect_tables_recursive(&aliased.expression, tables);
741 }
742 Expression::JoinSource(join) => {
743 Self::collect_tables_recursive(&join.left, tables);
744 Self::collect_tables_recursive(&join.right, tables);
745 }
746 Expression::SubquerySource(subquery) => {
747 if let Some(ref alias) = subquery.alias {
749 tables.push(alias.value_lower.to_string());
750 }
751 }
752 Expression::TableSource(table) => {
753 tables.push(table.name.value_lower.to_string());
755 if let Some(ref alias) = table.alias {
756 tables.push(alias.value_lower.to_string());
757 }
758 }
759 Expression::FunctionTableSource(fs) => {
760 if let Some(ref alias) = fs.alias {
761 tables.push(alias.value_lower.to_string());
762 } else {
763 tables.push(fs.function.value_lower.to_string());
764 }
765 }
766 _ => {}
767 }
768 }
769
770 #[allow(dead_code)]
772 fn has_outer_column_reference(expr: &Expression, inner_tables: &[String]) -> bool {
773 match expr {
774 Expression::QualifiedIdentifier(qi) => {
775 let table_ref = &qi.qualifier.value_lower;
777 !inner_tables.iter().any(|t| t == table_ref)
779 }
780 Expression::Infix(infix) => {
781 Self::has_outer_column_reference(&infix.left, inner_tables)
782 || Self::has_outer_column_reference(&infix.right, inner_tables)
783 }
784 Expression::Prefix(prefix) => {
785 Self::has_outer_column_reference(&prefix.right, inner_tables)
786 }
787 Expression::FunctionCall(func) => func
788 .arguments
789 .iter()
790 .any(|a| Self::has_outer_column_reference(a, inner_tables)),
791 Expression::Case(case) => {
792 case.when_clauses.iter().any(|w| {
793 Self::has_outer_column_reference(&w.condition, inner_tables)
794 || Self::has_outer_column_reference(&w.then_result, inner_tables)
795 }) || case
796 .else_value
797 .as_ref()
798 .is_some_and(|e| Self::has_outer_column_reference(e, inner_tables))
799 }
800 Expression::In(in_expr) => {
801 Self::has_outer_column_reference(&in_expr.left, inner_tables)
802 || Self::has_outer_column_reference(&in_expr.right, inner_tables)
803 }
804 Expression::Between(between) => {
805 Self::has_outer_column_reference(&between.expr, inner_tables)
806 || Self::has_outer_column_reference(&between.lower, inner_tables)
807 || Self::has_outer_column_reference(&between.upper, inner_tables)
808 }
809 Expression::Aliased(aliased) => {
810 Self::has_outer_column_reference(&aliased.expression, inner_tables)
811 }
812 Expression::List(list) => list
813 .elements
814 .iter()
815 .any(|e| Self::has_outer_column_reference(e, inner_tables)),
816 Expression::ExpressionList(list) => list
817 .expressions
818 .iter()
819 .any(|e| Self::has_outer_column_reference(e, inner_tables)),
820 _ => false,
821 }
822 }
823}
824
825fn is_aggregate_function(name: &str) -> bool {
827 matches!(
828 name.to_uppercase().as_str(),
829 "COUNT"
830 | "SUM"
831 | "AVG"
832 | "MIN"
833 | "MAX"
834 | "GROUP_CONCAT"
835 | "STRING_AGG"
836 | "ARRAY_AGG"
837 | "STDDEV"
838 | "STDDEV_POP"
839 | "STDDEV_SAMP"
840 | "VARIANCE"
841 | "VAR_POP"
842 | "VAR_SAMP"
843 | "PERCENTILE"
844 | "PERCENTILE_CONT"
845 | "PERCENTILE_DISC"
846 | "MEDIAN"
847 | "MODE"
848 | "BOOL_AND"
849 | "BOOL_OR"
850 | "BIT_AND"
851 | "BIT_OR"
852 | "BIT_XOR"
853 | "FIRST"
854 | "LAST"
855 | "ANY_VALUE"
856 )
857}
858
859fn compute_classification_key(stmt: &SelectStatement) -> u64 {
863 let mut hasher = FxHasher::default();
864
865 stmt.distinct.hash(&mut hasher);
867 stmt.distinct_on.len().hash(&mut hasher);
868 stmt.columns.len().hash(&mut hasher);
869 stmt.group_by.columns.len().hash(&mut hasher);
870 stmt.order_by.len().hash(&mut hasher);
871 stmt.limit.is_some().hash(&mut hasher);
872 stmt.offset.is_some().hash(&mut hasher);
873 stmt.having.is_some().hash(&mut hasher);
874 stmt.with.is_some().hash(&mut hasher);
875 stmt.set_operations.len().hash(&mut hasher);
876
877 for expr in &stmt.distinct_on {
879 hash_expression_structure(expr, &mut hasher);
880 }
881
882 for col in &stmt.columns {
884 hash_expression_structure(col, &mut hasher);
885 }
886
887 if let Some(ref where_clause) = stmt.where_clause {
889 hash_expression_structure(where_clause, &mut hasher);
890 }
891
892 if let Some(table_expr) = &stmt.table_expr {
897 hash_expression_structure(table_expr, &mut hasher);
898 }
899
900 for ob in &stmt.order_by {
904 hash_expression_structure(&ob.expression, &mut hasher);
905 ob.ascending.hash(&mut hasher);
906 ob.nulls_first.hash(&mut hasher);
907 }
908
909 for gb in &stmt.group_by.columns {
911 hash_expression_structure(gb, &mut hasher);
912 }
913
914 if let Some(ref having) = stmt.having {
916 hash_expression_structure(having, &mut hasher);
917 }
918
919 hasher.finish()
920}
921
922fn hash_expression_structure(expr: &Expression, hasher: &mut FxHasher) {
924 std::mem::discriminant(expr).hash(hasher);
925
926 match expr {
927 Expression::FunctionCall(func) => {
928 for c in func.function.bytes() {
930 c.to_ascii_uppercase().hash(hasher);
931 }
932 func.arguments.len().hash(hasher);
933 for arg in &func.arguments {
934 hash_expression_structure(arg, hasher);
935 }
936 }
937 Expression::Window(wf) => {
938 for c in wf.function.function.bytes() {
940 c.to_ascii_uppercase().hash(hasher);
941 }
942 }
943 Expression::Aliased(aliased) => {
944 hash_expression_structure(&aliased.expression, hasher);
945 }
946 Expression::Infix(infix) => {
947 infix.operator.hash(hasher);
948 hash_expression_structure(&infix.left, hasher);
949 hash_expression_structure(&infix.right, hasher);
950 }
951 Expression::Prefix(prefix) => {
952 hash_expression_structure(&prefix.right, hasher);
953 }
954 Expression::Cast(cast) => {
955 cast.type_name.hash(hasher);
956 hash_expression_structure(&cast.expr, hasher);
957 }
958 Expression::Case(case) => {
959 case.when_clauses.len().hash(hasher);
960 for wc in &case.when_clauses {
961 hash_expression_structure(&wc.condition, hasher);
962 hash_expression_structure(&wc.then_result, hasher);
963 }
964 if let Some(ref else_val) = case.else_value {
965 hash_expression_structure(else_val, hasher);
966 }
967 }
968 Expression::In(in_expr) => {
969 in_expr.not.hash(hasher);
970 hash_expression_structure(&in_expr.left, hasher);
971 hash_expression_structure(&in_expr.right, hasher);
972 }
973 Expression::Between(between) => {
974 between.not.hash(hasher);
975 hash_expression_structure(&between.expr, hasher);
976 hash_expression_structure(&between.lower, hasher);
977 hash_expression_structure(&between.upper, hasher);
978 }
979 Expression::List(list) => {
980 list.elements.len().hash(hasher);
981 }
982 Expression::ScalarSubquery(subquery) => {
983 compute_classification_key(&subquery.subquery).hash(hasher);
984 }
985 Expression::Exists(exists) => {
986 compute_classification_key(&exists.subquery).hash(hasher);
987 }
988 Expression::AllAny(all_any) => {
989 hash_expression_structure(&all_any.left, hasher);
990 compute_classification_key(&all_any.subquery).hash(hasher);
991 }
992 Expression::QualifiedIdentifier(qi) => {
993 for c in qi.qualifier.value_lower.bytes() {
996 c.hash(hasher);
997 }
998 }
999 Expression::Parameter(param) => {
1000 param.index.hash(hasher);
1001 }
1002 Expression::TableSource(table) => {
1003 table.alias.is_some().hash(hasher);
1006 table.as_of.is_some().hash(hasher);
1007 if let Some(as_of) = &table.as_of {
1008 for byte in as_of.as_of_type.bytes() {
1009 byte.to_ascii_uppercase().hash(hasher);
1010 }
1011 hash_expression_structure(&as_of.value, hasher);
1012 }
1013 }
1014 Expression::JoinSource(join) => {
1015 for byte in join.join_type.bytes() {
1016 byte.to_ascii_uppercase().hash(hasher);
1017 }
1018 hash_expression_structure(&join.left, hasher);
1019 hash_expression_structure(&join.right, hasher);
1020 join.using_columns.len().hash(hasher);
1021 for column in &join.using_columns {
1022 for byte in column.value_lower.bytes() {
1023 byte.hash(hasher);
1024 }
1025 }
1026 if let Some(condition) = &join.condition {
1027 hash_expression_structure(condition, hasher);
1028 }
1029 }
1030 Expression::SubquerySource(subquery) => {
1031 compute_classification_key(&subquery.subquery).hash(hasher);
1034 subquery.alias.is_some().hash(hasher);
1035 }
1036 _ => {
1037 }
1039 }
1040}
1041
1042pub fn get_classification(stmt: &SelectStatement) -> Arc<QueryClassification> {
1044 let cache_key = compute_classification_key(stmt);
1045 {
1046 let mut guard = CLASSIFICATION_CACHE.lock();
1047 let cache = guard.get_or_insert_with(|| {
1048 LruCache::new(NonZeroUsize::new(CLASSIFICATION_CACHE_SIZE).unwrap())
1049 });
1050 if let Some(bucket) = cache.get(&cache_key) {
1051 if let Some((_, classification)) = bucket.iter().find(|(cached, _)| cached == stmt) {
1052 return classification.clone();
1053 }
1054 }
1055 }
1056
1057 let classification = Arc::new(QueryClassification::classify(stmt));
1060 let mut guard = CLASSIFICATION_CACHE.lock();
1061 let cache = guard.get_or_insert_with(|| {
1062 LruCache::new(NonZeroUsize::new(CLASSIFICATION_CACHE_SIZE).unwrap())
1063 });
1064 if let Some(bucket) = cache.get(&cache_key) {
1065 if let Some((_, winner)) = bucket.iter().find(|(cached, _)| cached == stmt) {
1066 return winner.clone();
1067 }
1068 }
1069 if let Some(bucket) = cache.get_mut(&cache_key) {
1070 bucket.push((stmt.clone(), classification.clone()));
1071 } else {
1072 cache.put(cache_key, vec![(stmt.clone(), classification.clone())]);
1073 }
1074 classification
1075}
1076
1077#[cfg(test)]
1079pub fn clear_cache() {
1080 let mut guard = CLASSIFICATION_CACHE.lock();
1081 if let Some(cache) = guard.as_mut() {
1082 cache.clear();
1083 }
1084}
1085
1086#[cfg(test)]
1087mod tests {
1088 use super::*;
1089 use radixdb_sql::ast::{GroupByClause, StarExpression};
1090 use radixdb_sql::token::{Position, Token, TokenType};
1091
1092 fn parsed_select(sql: &str) -> SelectStatement {
1093 let statements = radixdb_sql::parse_sql(sql).unwrap();
1094 let radixdb_sql::Statement::Select(select) = &statements[0] else {
1095 panic!("expected SELECT")
1096 };
1097 select.clone()
1098 }
1099
1100 #[test]
1101 fn r5_l03_semantic_and_classification_identity_preserve_complete_ast_classification() {
1102 clear_cache();
1103 let uncorrelated = parsed_select("SELECT (SELECT inner_t.id FROM inner_t) FROM outer_t");
1104 let correlated = parsed_select("SELECT (SELECT outer_t.id FROM inner_t) FROM outer_t");
1105 let first = get_classification(&uncorrelated);
1106 let second = get_classification(&correlated);
1107 assert!(!first.select_has_correlated_subqueries);
1108 assert!(second.select_has_correlated_subqueries);
1109
1110 let aggregate_in_order = parsed_select("SELECT x FROM t ORDER BY COUNT(*)");
1111 assert!(get_classification(&aggregate_in_order).has_aggregation);
1112
1113 let window_in_order = parsed_select("SELECT x FROM t ORDER BY ROW_NUMBER() OVER ()");
1114 assert!(get_classification(&window_in_order).has_window_functions);
1115 }
1116
1117 fn dummy_token() -> Token {
1118 Token::new(TokenType::Keyword, "SELECT", Position::new(0, 1, 1))
1119 }
1120
1121 fn create_select_star() -> SelectStatement {
1122 SelectStatement {
1123 token: dummy_token(),
1124 with: None,
1125 distinct: false,
1126 distinct_on: vec![],
1127 columns: vec![Expression::Star(StarExpression {
1128 token: dummy_token(),
1129 })],
1130 table_expr: None,
1131 where_clause: None,
1132 group_by: GroupByClause::default(),
1133 having: None,
1134 window_defs: vec![],
1135 order_by: vec![],
1136 limit: None,
1137 offset: None,
1138 set_operations: vec![],
1139 }
1140 }
1141
1142 #[test]
1143 fn test_select_star_classification() {
1144 let stmt = create_select_star();
1145 let classification = QueryClassification::classify(&stmt);
1146
1147 assert!(classification.is_select_star);
1149 assert!(!classification.has_aggregation);
1150 assert!(!classification.has_window_functions);
1151 assert!(!classification.has_group_by);
1152 assert!(!classification.has_order_by);
1153 assert!(!classification.has_limit);
1154 assert!(!classification.has_distinct);
1155
1156 assert!(!classification.has_joins);
1157 assert_eq!(classification.join_count, 0);
1158 assert!(!classification.has_outer_joins);
1159 assert!(!classification.has_derived_tables);
1160 assert!(!classification.has_where);
1161 assert!(!classification.where_has_subqueries);
1162 }
1163
1164 #[test]
1165 fn test_classification_cache_lifecycle_preserves_result() {
1166 clear_cache();
1167
1168 let stmt = create_select_star();
1169
1170 let class1 = get_classification(&stmt);
1174 assert!(class1.is_select_star);
1175
1176 clear_cache();
1177 let class2 = get_classification(&stmt);
1178 assert_eq!(*class1, *class2);
1179 }
1180
1181 #[test]
1182 fn classification_cache_distinguishes_eligible_and_residual_join_shapes() {
1183 clear_cache();
1184 let eligible = radixdb_sql::parse_sql(
1185 "SELECT COUNT(*) FROM child c INNER JOIN parent p ON c.parent_id = p.id",
1186 )
1187 .unwrap();
1188 let residual = radixdb_sql::parse_sql(
1189 "SELECT COUNT(*) FROM child c INNER JOIN parent p ON c.parent_id = p.id AND c.tag = p.tag",
1190 )
1191 .unwrap();
1192 let eligible = match &eligible[0] {
1193 radixdb_sql::Statement::Select(statement) => statement,
1194 _ => panic!("expected SELECT"),
1195 };
1196 let residual = match &residual[0] {
1197 radixdb_sql::Statement::Select(statement) => statement,
1198 _ => panic!("expected SELECT"),
1199 };
1200
1201 let eligible_classification = get_classification(eligible);
1202 let residual_classification = get_classification(residual);
1203 assert!(eligible_classification.has_joins);
1204 assert!(residual_classification.has_joins);
1205 assert!(!Arc::ptr_eq(
1206 &eligible_classification,
1207 &residual_classification
1208 ));
1209 }
1210
1211 #[test]
1212 fn classification_preserves_join_depth_outer_barrier_and_derived_boundary() {
1213 let statement = parsed_select(
1214 "SELECT * FROM a \
1215 JOIN b ON b.a_id = a.id \
1216 LEFT JOIN (SELECT id FROM c) c1 ON c1.id = b.c_id \
1217 JOIN d ON d.id = a.d_id",
1218 );
1219 let classification = QueryClassification::classify(&statement);
1220 assert!(classification.has_joins);
1221 assert_eq!(classification.join_count, 3);
1222 assert!(classification.has_outer_joins);
1223 assert!(classification.has_derived_tables);
1224 assert_eq!(classification.reorderable_join_count, 2);
1225 let graph = classification.logical_join_graph.as_ref().unwrap();
1226 assert_eq!(graph.edges.len(), classification.join_count);
1227 }
1228}