Skip to main content

radixdb_executor/
query_classification.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Query Classification Cache
16//!
17//! This module caches pre-computed characteristics of SELECT statements to avoid
18//! repeated AST traversals. For example, determining if a query has aggregation
19//! requires walking the entire SELECT column list - we cache this result.
20//!
21//! # Performance Impact
22//!
23//! Before: has_aggregation() called 3-5 times per query, each traversing all columns
24//! After: Single traversal on first access, O(1) lookup thereafter
25
26use 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
38/// Maximum number of cached query classifications (LRU eviction)
39const CLASSIFICATION_CACHE_SIZE: usize = 512;
40
41/// Global cache for query classifications
42type ClassificationBucket = Vec<(SelectStatement, Arc<QueryClassification>)>;
43static CLASSIFICATION_CACHE: Mutex<Option<LruCache<u64, ClassificationBucket>>> = Mutex::new(None);
44
45/// Clear the classification cache. Call on database drop to release memory.
46pub fn clear_classification_cache() {
47    let mut guard = CLASSIFICATION_CACHE.lock();
48    *guard = None;
49}
50
51/// Pre-computed characteristics of a SELECT statement.
52#[derive(Debug, Clone, PartialEq)]
53pub struct QueryClassification {
54    // === Basic query structure ===
55    /// Whether the query has aggregate functions (COUNT, SUM, etc.)
56    pub has_aggregation: bool,
57    /// Whether the query has window functions (ROW_NUMBER, etc.)
58    pub has_window_functions: bool,
59    /// Whether the query has GROUP BY clause
60    pub has_group_by: bool,
61    /// Whether the query has ORDER BY clause
62    pub has_order_by: bool,
63    /// Whether the query has LIMIT clause
64    pub has_limit: bool,
65    /// Whether the query has OFFSET clause
66    pub has_offset: bool,
67    /// Whether the query has DISTINCT
68    pub has_distinct: bool,
69    /// Whether the query has DISTINCT ON (expr, ...)
70    pub has_distinct_on: bool,
71    /// Whether the query has set operations (UNION, etc.)
72    pub has_set_operations: bool,
73    /// Whether the query has HAVING clause
74    pub has_having: bool,
75
76    // === SELECT clause analysis ===
77    /// Whether the SELECT is `*` (all columns)
78    pub is_select_star: bool,
79    /// Whether SELECT has scalar subqueries
80    pub select_has_scalar_subqueries: bool,
81    /// Current-scope column references required after the JOIN tree. `None`
82    /// means that a star or nested query makes dependency projection unsafe.
83    pub join_projection_dependencies: Option<Arc<Vec<Expression>>>,
84
85    // === JOIN analysis ===
86    /// Whether the query has any joins
87    pub has_joins: bool,
88    /// Number of JOIN edges in the complete table-expression tree.
89    pub join_count: usize,
90    /// Whether at least one JOIN edge is an outer-join reorder barrier.
91    pub has_outer_joins: bool,
92    /// Whether the table expression contains a derived/subquery boundary.
93    pub has_derived_tables: bool,
94    /// INNER equality edges that may participate in a reorder component.
95    pub reorderable_join_count: usize,
96    /// Stable relation identities, complete edge relation sets and explicit
97    /// reorder barriers for the current table-expression scope.
98    #[doc(hidden)]
99    pub logical_join_graph: Option<Arc<LogicalJoinGraph>>,
100
101    // === WHERE clause analysis ===
102    /// Whether query has a WHERE clause
103    pub has_where: bool,
104    /// Whether WHERE clause has parameters ($1, $2, etc.)
105    pub where_has_parameters: bool,
106    /// Whether WHERE clause has any subqueries
107    pub where_has_subqueries: bool,
108    /// Whether WHERE has correlated subqueries (references outer columns)
109    pub where_has_correlated_subqueries: bool,
110    /// Whether WHERE or SELECT has non-deterministic functions (NOW, RANDOM, UUID, etc.)
111    /// that return different values per execution and must not be semantically cached
112    pub has_nondeterministic_functions: bool,
113
114    // === SELECT clause correlated subqueries ===
115    /// Whether any SELECT column has correlated subqueries
116    pub select_has_correlated_subqueries: bool,
117
118    // === ORDER BY analysis ===
119    /// Whether ORDER BY has correlated subqueries
120    pub order_by_has_correlated_subqueries: bool,
121}
122
123impl QueryClassification {
124    /// Classify a SELECT statement, computing all characteristics in a single pass
125    pub fn classify(stmt: &SelectStatement) -> Self {
126        // Basic query structure
127        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        // SELECT clause analysis
139        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        // JOIN analysis
148        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        // WHERE clause analysis
160        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        // Non-deterministic function detection (NOW, RANDOM, UUID, etc.)
176        // Check both WHERE and SELECT columns — semantic cache must not serve stale
177        // results when any part of the query uses non-deterministic functions.
178        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        // SELECT column correlated subquery analysis
188        let select_has_correlated_subqueries = stmt
189            .columns
190            .iter()
191            .any(Self::expression_has_correlated_subqueries);
192
193        // ORDER BY correlated subquery analysis
194        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    /// Analyze table source for joins and derived tables
283    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    /// Recursively analyze table expression for joins and derived tables
303    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                // Check for outer join types
316                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                // Recurse into left and right
325                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    /// Analyze WHERE clause for various subquery types
357    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        // Check for correlated subqueries (expensive AST traversal - cached here)
375        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    /// Recursively analyze WHERE expression for subquery types
389    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    /// Check if expression contains scalar subqueries
413    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    /// Check if any column expression contains aggregate functions
422    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    /// Check if an expression contains aggregate functions
440    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, // Subquery aggregates are handled separately
500            _ => false,
501        }
502    }
503
504    /// Check if any column expression contains window functions
505    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    /// Check if an expression contains window functions
521    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    /// Check if an expression contains parameter placeholders ($1, $2, etc.).
586    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    /// Check if an expression contains non-deterministic functions whose return
594    /// value changes between executions (NOW, CURRENT_DATE, RANDOM, UUID, etc.).
595    /// Queries with these functions must not be served from the semantic cache.
596    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    /// Check if an expression contains correlated subqueries (references outer columns)
680    /// This is an expensive check as it must examine each subquery's WHERE clause
681    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    /// Check if a subquery is correlated (references outer table columns)
700    fn is_subquery_correlated(subquery: &SelectStatement) -> bool {
701        // Collect table names/aliases from the subquery's FROM clause
702        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    /// Collect table names and aliases from a subquery's FROM clause
722    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    /// Recursively collect table names and aliases
731    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                // Add alias (use value_lower for case-insensitive matching)
738                tables.push(aliased.alias.value_lower.to_string());
739                // Also collect from inner expression
740                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                // Subquery has an alias, collect it
748                if let Some(ref alias) = subquery.alias {
749                    tables.push(alias.value_lower.to_string());
750                }
751            }
752            Expression::TableSource(table) => {
753                // Add table name and alias if present
754                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    /// Check if an expression references columns from tables NOT in the given list
771    #[allow(dead_code)]
772    fn has_outer_column_reference(expr: &Expression, inner_tables: &[String]) -> bool {
773        match expr {
774            Expression::QualifiedIdentifier(qi) => {
775                // Has table qualifier - check if it's NOT in inner tables
776                let table_ref = &qi.qualifier.value_lower;
777                // If table reference is NOT in inner tables, it's an outer reference
778                !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
825/// Check if a function name is an aggregate function
826fn 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
859/// Compute a cache key for a SELECT statement
860/// Only hashes structural elements that affect classification (not literal values)
861/// Uses FxHasher which is 2-5x faster than SipHash for small keys.
862fn compute_classification_key(stmt: &SelectStatement) -> u64 {
863    let mut hasher = FxHasher::default();
864
865    // Hash structural properties
866    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    // Hash DISTINCT ON expression structures
878    for expr in &stmt.distinct_on {
879        hash_expression_structure(expr, &mut hasher);
880    }
881
882    // Hash column expression types (not values)
883    for col in &stmt.columns {
884        hash_expression_structure(col, &mut hasher);
885    }
886
887    // Hash WHERE clause structure if present
888    if let Some(ref where_clause) = stmt.where_clause {
889        hash_expression_structure(where_clause, &mut hasher);
890    }
891
892    // The classification cache is not a physical-plan cache, but its key must
893    // still keep distinct FROM/JOIN shapes apart. In particular an eligible
894    // `COUNT(*) ... ON child.fk = parent.pk` and a residual-ON fallback must
895    // never reuse one another's cached structural classification.
896    if let Some(table_expr) = &stmt.table_expr {
897        hash_expression_structure(table_expr, &mut hasher);
898    }
899
900    // Hash ORDER BY expressions (critical for correlated subquery detection)
901    // Without this, queries with same ORDER BY count but different expressions
902    // would incorrectly share classification (e.g., "ORDER BY 1" vs "ORDER BY (SELECT ...)")
903    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    // Hash GROUP BY expressions
910    for gb in &stmt.group_by.columns {
911        hash_expression_structure(gb, &mut hasher);
912    }
913
914    // Hash HAVING clause if present
915    if let Some(ref having) = stmt.having {
916        hash_expression_structure(having, &mut hasher);
917    }
918
919    hasher.finish()
920}
921
922/// Hash the structural elements of an expression (discriminants, not values)
923fn hash_expression_structure(expr: &Expression, hasher: &mut FxHasher) {
924    std::mem::discriminant(expr).hash(hasher);
925
926    match expr {
927        Expression::FunctionCall(func) => {
928            // Hash function name case-insensitively without allocating
929            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            // Hash window function name case-insensitively without allocating
939            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            // CRITICAL: Hash the qualifier (table name/alias) to distinguish correlated references
994            // e.g., "c.id" vs "o.id" - the qualifier determines if it's an outer reference
995            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            // Names do not affect classification, while alias/temporal shape
1004            // does affect join routing and correlated-reference analysis.
1005            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            // Include the nested SELECT's complete structural key; a derived
1032            // table must not collide with a same-shaped base table.
1033            compute_classification_key(&subquery.subquery).hash(hasher);
1034            subquery.alias.is_some().hash(hasher);
1035        }
1036        _ => {
1037            // For literals and unqualified identifiers, just use discriminant
1038        }
1039    }
1040}
1041
1042/// Get or compute the classification for a SELECT statement
1043pub 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    // Classification is pure. Traverse the AST without holding the
1058    // process-wide LRU lock, then reconcile a possible concurrent winner.
1059    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/// Clear the classification cache (for testing)
1078#[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        // Basic flags
1148        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        // The cache is process-wide and may be cleared concurrently when another
1171        // database/test is dropped. Pointer identity is therefore not part of the
1172        // contract; recomputation after eviction must remain semantically exact.
1173        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}