Skip to main content

vibesql_executor/cache/
query_signature.rs

1//! Query signature generation for cache keys
2//!
3//! Generates deterministic cache keys from SQL queries by normalizing the AST
4//! and creating a hash. Queries with identical structure (different literals)
5//! will have the same signature.
6
7use std::{
8    collections::hash_map::DefaultHasher,
9    hash::{Hash, Hasher},
10};
11
12use vibesql_ast::{
13    arena::{
14        Expression as ArenaExpression, ExtendedExpr as ArenaExtendedExpr,
15        FromClause as ArenaFromClause, GroupByClause as ArenaGroupByClause,
16        GroupingElement as ArenaGroupingElement, GroupingSet as ArenaGroupingSet,
17        MixedGroupingItem as ArenaMixedGroupingItem, SelectItem as ArenaSelectItem,
18        SelectStmt as ArenaSelectStmt, WindowFunctionSpec as ArenaWindowFunctionSpec,
19    },
20    Expression, Statement,
21};
22
23/// Unique identifier for a query based on its structure
24#[derive(Clone, Debug, Eq, PartialEq, Hash)]
25pub struct QuerySignature {
26    hash: u64,
27}
28
29impl QuerySignature {
30    /// Create a signature from SQL text (legacy string-based approach)
31    pub fn from_sql(sql: &str) -> Self {
32        let normalized = Self::normalize(sql);
33        let mut hasher = DefaultHasher::new();
34        normalized.hash(&mut hasher);
35        let hash = hasher.finish();
36        Self { hash }
37    }
38
39    /// Create a signature from parsed AST, ignoring literal values
40    /// This allows queries with different literals but identical structure to share cached plans
41    pub fn from_ast(stmt: &Statement) -> Self {
42        let mut hasher = DefaultHasher::new();
43        Self::hash_statement(stmt, &mut hasher);
44        let hash = hasher.finish();
45        Self { hash }
46    }
47
48    /// Create a signature from arena-allocated SelectStmt, ignoring literal values
49    pub fn from_arena_select(select: &ArenaSelectStmt<'_>) -> Self {
50        let mut hasher = DefaultHasher::new();
51        "SELECT".hash(&mut hasher);
52        Self::hash_arena_select(select, &mut hasher);
53        let hash = hasher.finish();
54        Self { hash }
55    }
56
57    /// Get the underlying hash
58    pub fn hash(&self) -> u64 {
59        self.hash
60    }
61
62    /// Normalize SQL: trim and collapse whitespace
63    fn normalize(sql: &str) -> String {
64        sql.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase()
65    }
66
67    /// Hash a statement, replacing literals with a placeholder marker
68    fn hash_statement(stmt: &Statement, hasher: &mut DefaultHasher) {
69        match stmt {
70            Statement::Select(select) => {
71                "SELECT".hash(hasher);
72                Self::hash_select(select, hasher);
73            }
74            Statement::Insert(insert) => {
75                "INSERT".hash(hasher);
76                insert.table_name.hash(hasher);
77                for col in &insert.columns {
78                    col.hash(hasher);
79                }
80                // Hash the insert source structure without literals
81                match &insert.source {
82                    vibesql_ast::InsertSource::Values(rows) => {
83                        "VALUES".hash(hasher);
84                        rows.len().hash(hasher);
85                        for row in rows {
86                            row.len().hash(hasher);
87                            for expr in row {
88                                Self::hash_expression(expr, hasher);
89                            }
90                        }
91                    }
92                    vibesql_ast::InsertSource::Select(select) => {
93                        "SELECT".hash(hasher);
94                        Self::hash_select(select, hasher);
95                    }
96                    vibesql_ast::InsertSource::DefaultValues => {
97                        "DEFAULT_VALUES".hash(hasher);
98                    }
99                }
100            }
101            Statement::Update(update) => {
102                "UPDATE".hash(hasher);
103                update.table_name.hash(hasher);
104                for assignment in &update.assignments {
105                    assignment.column.hash(hasher);
106                    Self::hash_expression(&assignment.value, hasher);
107                }
108                if let Some(ref where_clause) = update.where_clause {
109                    match where_clause {
110                        vibesql_ast::WhereClause::Condition(expr) => {
111                            Self::hash_expression(expr, hasher);
112                        }
113                        vibesql_ast::WhereClause::CurrentOf(cursor) => {
114                            "CURRENT_OF".hash(hasher);
115                            cursor.hash(hasher);
116                        }
117                    }
118                }
119            }
120            Statement::Delete(delete) => {
121                "DELETE".hash(hasher);
122                delete.table_name.hash(hasher);
123                if let Some(ref where_clause) = delete.where_clause {
124                    match where_clause {
125                        vibesql_ast::WhereClause::Condition(expr) => {
126                            Self::hash_expression(expr, hasher);
127                        }
128                        vibesql_ast::WhereClause::CurrentOf(cursor) => {
129                            "CURRENT_OF".hash(hasher);
130                            cursor.hash(hasher);
131                        }
132                    }
133                }
134            }
135            // For other statement types, fall back to discriminant hashing
136            _ => {
137                std::mem::discriminant(stmt).hash(hasher);
138            }
139        }
140    }
141
142    /// Hash a SELECT statement structure
143    fn hash_select(select: &vibesql_ast::SelectStmt, hasher: &mut DefaultHasher) {
144        // Hash DISTINCT
145        select.distinct.hash(hasher);
146
147        // Hash select items
148        for item in &select.select_list {
149            match item {
150                vibesql_ast::SelectItem::Wildcard { .. } => "WILDCARD".hash(hasher),
151                vibesql_ast::SelectItem::QualifiedWildcard { qualifier, .. } => {
152                    "QUALIFIED_WILDCARD".hash(hasher);
153                    qualifier.hash(hasher);
154                }
155                vibesql_ast::SelectItem::Expression { expr, alias, .. } => {
156                    Self::hash_expression(expr, hasher);
157                    alias.hash(hasher);
158                }
159            }
160        }
161
162        // Hash FROM clause
163        if let Some(ref from) = select.from {
164            Self::hash_from_clause(from, hasher);
165        }
166
167        // Hash WHERE clause
168        if let Some(ref where_clause) = select.where_clause {
169            Self::hash_expression(where_clause, hasher);
170        }
171
172        // Hash GROUP BY
173        if let Some(ref group_by) = select.group_by {
174            Self::hash_group_by(group_by, hasher);
175        }
176
177        // Hash HAVING
178        if let Some(ref having) = select.having {
179            Self::hash_expression(having, hasher);
180        }
181
182        // Hash ORDER BY
183        if let Some(ref order_by) = select.order_by {
184            for item in order_by {
185                Self::hash_expression(&item.expr, hasher);
186                std::mem::discriminant(&item.direction).hash(hasher);
187            }
188        }
189
190        // Hash LIMIT/OFFSET expressions
191        // Note: We hash the presence of limit/offset but not the expression itself
192        // since Expression doesn't implement Hash
193        select.limit.is_some().hash(hasher);
194        select.offset.is_some().hash(hasher);
195    }
196
197    /// Hash a FROM clause structure
198    fn hash_from_clause(from: &vibesql_ast::FromClause, hasher: &mut DefaultHasher) {
199        match from {
200            vibesql_ast::FromClause::Table { name, alias, .. } => {
201                "TABLE".hash(hasher);
202                name.hash(hasher);
203                alias.hash(hasher);
204            }
205            vibesql_ast::FromClause::Join { left, join_type, right, condition, .. } => {
206                "JOIN".hash(hasher);
207                Self::hash_from_clause(left, hasher);
208                std::mem::discriminant(join_type).hash(hasher);
209                Self::hash_from_clause(right, hasher);
210                if let Some(expr) = condition {
211                    Self::hash_expression(expr, hasher);
212                }
213            }
214            vibesql_ast::FromClause::Subquery { query, alias, .. } => {
215                "SUBQUERY".hash(hasher);
216                Self::hash_select(query, hasher);
217                alias.hash(hasher);
218            }
219            vibesql_ast::FromClause::Values { rows, alias, column_aliases } => {
220                "VALUES".hash(hasher);
221                rows.len().hash(hasher);
222                if let Some(first_row) = rows.first() {
223                    first_row.len().hash(hasher);
224                }
225                for row in rows {
226                    for expr in row {
227                        Self::hash_expression(expr, hasher);
228                    }
229                }
230                alias.hash(hasher);
231                column_aliases.hash(hasher);
232            }
233        }
234    }
235
236    fn hash_group_by(group_by: &vibesql_ast::GroupByClause, hasher: &mut DefaultHasher) {
237        match group_by {
238            vibesql_ast::GroupByClause::Simple(exprs) => {
239                "SIMPLE".hash(hasher);
240                for expr in exprs {
241                    Self::hash_expression(expr, hasher);
242                }
243            }
244            vibesql_ast::GroupByClause::Rollup(elements) => {
245                "ROLLUP".hash(hasher);
246                Self::hash_grouping_elements(elements, hasher);
247            }
248            vibesql_ast::GroupByClause::Cube(elements) => {
249                "CUBE".hash(hasher);
250                Self::hash_grouping_elements(elements, hasher);
251            }
252            vibesql_ast::GroupByClause::GroupingSets(sets) => {
253                "GROUPING_SETS".hash(hasher);
254                Self::hash_grouping_sets(sets, hasher);
255            }
256            vibesql_ast::GroupByClause::Mixed(items) => {
257                "MIXED".hash(hasher);
258                for item in items {
259                    match item {
260                        vibesql_ast::MixedGroupingItem::Simple(expr) => {
261                            "SIMPLE".hash(hasher);
262                            Self::hash_expression(expr, hasher);
263                        }
264                        vibesql_ast::MixedGroupingItem::Rollup(elements) => {
265                            "ROLLUP".hash(hasher);
266                            Self::hash_grouping_elements(elements, hasher);
267                        }
268                        vibesql_ast::MixedGroupingItem::Cube(elements) => {
269                            "CUBE".hash(hasher);
270                            Self::hash_grouping_elements(elements, hasher);
271                        }
272                        vibesql_ast::MixedGroupingItem::GroupingSets(sets) => {
273                            "GROUPING_SETS".hash(hasher);
274                            Self::hash_grouping_sets(sets, hasher);
275                        }
276                    }
277                }
278            }
279        }
280    }
281
282    fn hash_grouping_sets(sets: &[vibesql_ast::GroupingSet], hasher: &mut DefaultHasher) {
283        for set in sets {
284            "SET".hash(hasher);
285            for expr in &set.columns {
286                Self::hash_expression(expr, hasher);
287            }
288        }
289    }
290
291    fn hash_grouping_elements(
292        elements: &[vibesql_ast::GroupingElement],
293        hasher: &mut DefaultHasher,
294    ) {
295        for element in elements {
296            match element {
297                vibesql_ast::GroupingElement::Single(expr) => {
298                    "SINGLE".hash(hasher);
299                    Self::hash_expression(expr, hasher);
300                }
301                vibesql_ast::GroupingElement::Composite(exprs) => {
302                    "COMPOSITE".hash(hasher);
303                    for expr in exprs {
304                        Self::hash_expression(expr, hasher);
305                    }
306                }
307            }
308        }
309    }
310
311    /// Hash an expression, replacing literals with a placeholder marker
312    fn hash_expression(expr: &Expression, hasher: &mut DefaultHasher) {
313        match expr {
314            // Key difference: All literals and placeholders hash to the same value
315            // This allows parameterized queries to match with literal values
316            Expression::Literal(_)
317            | Expression::Placeholder(_)
318            | Expression::NumberedPlaceholder(_)
319            | Expression::NamedPlaceholder(_) => "LITERAL_PLACEHOLDER".hash(hasher),
320
321            Expression::ColumnRef(col_id) => {
322                "COLUMN".hash(hasher);
323                col_id.table_canonical().hash(hasher);
324                col_id.column_canonical().hash(hasher);
325            }
326
327            Expression::PseudoVariable { pseudo_table, column } => {
328                "PSEUDO_VARIABLE".hash(hasher);
329                std::mem::discriminant(pseudo_table).hash(hasher);
330                column.hash(hasher);
331            }
332
333            Expression::BinaryOp { op, left, right } => {
334                "BINARY_OP".hash(hasher);
335                std::mem::discriminant(op).hash(hasher);
336                Self::hash_expression(left, hasher);
337                Self::hash_expression(right, hasher);
338            }
339
340            Expression::UnaryOp { op, expr } => {
341                "UNARY_OP".hash(hasher);
342                std::mem::discriminant(op).hash(hasher);
343                Self::hash_expression(expr, hasher);
344            }
345
346            Expression::Function { name, args, character_unit } => {
347                "FUNCTION".hash(hasher);
348                name.canonical().hash(hasher);
349                for arg in args {
350                    Self::hash_expression(arg, hasher);
351                }
352                if let Some(ref unit) = character_unit {
353                    std::mem::discriminant(unit).hash(hasher);
354                }
355            }
356
357            Expression::AggregateFunction { name, distinct, args, order_by, filter } => {
358                "AGGREGATE".hash(hasher);
359                name.canonical().hash(hasher);
360                distinct.hash(hasher);
361                for arg in args {
362                    Self::hash_expression(arg, hasher);
363                }
364                // Hash order_by clause if present
365                if let Some(items) = order_by {
366                    "ORDER_BY".hash(hasher);
367                    for item in items {
368                        Self::hash_expression(&item.expr, hasher);
369                        std::mem::discriminant(&item.direction).hash(hasher);
370                    }
371                }
372                // Hash filter clause if present
373                if let Some(f) = filter {
374                    "FILTER".hash(hasher);
375                    Self::hash_expression(f, hasher);
376                }
377            }
378
379            Expression::IsNull { expr, negated } => {
380                "IS_NULL".hash(hasher);
381                Self::hash_expression(expr, hasher);
382                negated.hash(hasher);
383            }
384
385            Expression::IsDistinctFrom { left, right, negated } => {
386                "IS_DISTINCT_FROM".hash(hasher);
387                Self::hash_expression(left, hasher);
388                Self::hash_expression(right, hasher);
389                negated.hash(hasher);
390            }
391
392            Expression::IsTruthValue { expr, truth_value, negated } => {
393                "IS_TRUTH_VALUE".hash(hasher);
394                Self::hash_expression(expr, hasher);
395                std::mem::discriminant(truth_value).hash(hasher);
396                negated.hash(hasher);
397            }
398
399            Expression::Wildcard => "WILDCARD".hash(hasher),
400
401            Expression::Case { operand, when_clauses, else_result } => {
402                "CASE".hash(hasher);
403                if let Some(ref op) = operand {
404                    Self::hash_expression(op, hasher);
405                }
406                for when in when_clauses {
407                    for cond in &when.conditions {
408                        Self::hash_expression(cond, hasher);
409                    }
410                    Self::hash_expression(&when.result, hasher);
411                }
412                if let Some(ref else_expr) = else_result {
413                    Self::hash_expression(else_expr, hasher);
414                }
415            }
416
417            Expression::ScalarSubquery(subquery) => {
418                "SCALAR_SUBQUERY".hash(hasher);
419                Self::hash_select(subquery, hasher);
420            }
421
422            Expression::In { expr, subquery, negated } => {
423                "IN_SUBQUERY".hash(hasher);
424                Self::hash_expression(expr, hasher);
425                Self::hash_select(subquery, hasher);
426                negated.hash(hasher);
427            }
428
429            Expression::InList { expr, values, negated } => {
430                "IN_LIST".hash(hasher);
431                Self::hash_expression(expr, hasher);
432                values.len().hash(hasher);
433                for val in values {
434                    Self::hash_expression(val, hasher);
435                }
436                negated.hash(hasher);
437            }
438
439            Expression::Between { expr, low, high, negated, symmetric } => {
440                "BETWEEN".hash(hasher);
441                Self::hash_expression(expr, hasher);
442                Self::hash_expression(low, hasher);
443                Self::hash_expression(high, hasher);
444                negated.hash(hasher);
445                symmetric.hash(hasher);
446            }
447
448            Expression::Cast { expr, data_type } => {
449                "CAST".hash(hasher);
450                Self::hash_expression(expr, hasher);
451                std::mem::discriminant(data_type).hash(hasher);
452            }
453
454            Expression::Position { substring, string, character_unit } => {
455                "POSITION".hash(hasher);
456                Self::hash_expression(substring, hasher);
457                Self::hash_expression(string, hasher);
458                if let Some(ref unit) = character_unit {
459                    std::mem::discriminant(unit).hash(hasher);
460                }
461            }
462
463            Expression::Trim { position, removal_char, string } => {
464                "TRIM".hash(hasher);
465                if let Some(ref pos) = position {
466                    std::mem::discriminant(pos).hash(hasher);
467                }
468                if let Some(ref ch) = removal_char {
469                    Self::hash_expression(ch, hasher);
470                }
471                Self::hash_expression(string, hasher);
472            }
473
474            Expression::Extract { field, expr } => {
475                "EXTRACT".hash(hasher);
476                std::mem::discriminant(field).hash(hasher);
477                Self::hash_expression(expr, hasher);
478            }
479
480            Expression::Like { expr, pattern, negated, .. } => {
481                "LIKE".hash(hasher);
482                Self::hash_expression(expr, hasher);
483                Self::hash_expression(pattern, hasher);
484                negated.hash(hasher);
485            }
486
487            Expression::Glob { expr, pattern, negated, .. } => {
488                "GLOB".hash(hasher);
489                Self::hash_expression(expr, hasher);
490                Self::hash_expression(pattern, hasher);
491                negated.hash(hasher);
492            }
493
494            Expression::Exists { subquery, negated } => {
495                "EXISTS".hash(hasher);
496                Self::hash_select(subquery, hasher);
497                negated.hash(hasher);
498            }
499
500            Expression::QuantifiedComparison { expr, op, quantifier, subquery } => {
501                "QUANTIFIED".hash(hasher);
502                Self::hash_expression(expr, hasher);
503                std::mem::discriminant(op).hash(hasher);
504                std::mem::discriminant(quantifier).hash(hasher);
505                Self::hash_select(subquery, hasher);
506            }
507
508            Expression::CurrentDate => "CURRENT_DATE".hash(hasher),
509
510            Expression::CurrentTime { precision } => {
511                "CURRENT_TIME".hash(hasher);
512                precision.hash(hasher);
513            }
514
515            Expression::CurrentTimestamp { precision } => {
516                "CURRENT_TIMESTAMP".hash(hasher);
517                precision.hash(hasher);
518            }
519
520            Expression::Interval { value, unit, leading_precision, fractional_precision } => {
521                "INTERVAL".hash(hasher);
522                Self::hash_expression(value, hasher);
523                format!("{:?}", unit).hash(hasher);
524                leading_precision.hash(hasher);
525                fractional_precision.hash(hasher);
526            }
527
528            Expression::Default => "DEFAULT".hash(hasher),
529
530            Expression::DuplicateKeyValue { column } => {
531                "DUPLICATE_KEY_VALUE".hash(hasher);
532                column.hash(hasher);
533            }
534
535            Expression::WindowFunction { function, over } => {
536                "WINDOW_FUNCTION".hash(hasher);
537                // Hash function type and arguments
538                match function {
539                    vibesql_ast::WindowFunctionSpec::Aggregate { name, args, filter } => {
540                        "AGGREGATE".hash(hasher);
541                        name.canonical().hash(hasher);
542                        for arg in args {
543                            Self::hash_expression(arg, hasher);
544                        }
545                        if let Some(f) = filter {
546                            "FILTER".hash(hasher);
547                            Self::hash_expression(f, hasher);
548                        }
549                    }
550                    vibesql_ast::WindowFunctionSpec::Ranking { name, args } => {
551                        "RANKING".hash(hasher);
552                        name.canonical().hash(hasher);
553                        for arg in args {
554                            Self::hash_expression(arg, hasher);
555                        }
556                    }
557                    vibesql_ast::WindowFunctionSpec::Value { name, args } => {
558                        "VALUE".hash(hasher);
559                        name.canonical().hash(hasher);
560                        for arg in args {
561                            Self::hash_expression(arg, hasher);
562                        }
563                    }
564                }
565
566                // Hash OVER clause components
567                if let Some(ref partition_by) = over.partition_by {
568                    for expr in partition_by {
569                        Self::hash_expression(expr, hasher);
570                    }
571                }
572                if let Some(ref order_by) = over.order_by {
573                    for item in order_by {
574                        Self::hash_expression(&item.expr, hasher);
575                        std::mem::discriminant(&item.direction).hash(hasher);
576                    }
577                }
578                if let Some(ref frame) = over.frame {
579                    std::mem::discriminant(&frame.unit).hash(hasher);
580                    std::mem::discriminant(&frame.start).hash(hasher);
581                    if let Some(ref end) = frame.end {
582                        std::mem::discriminant(end).hash(hasher);
583                    }
584                }
585            }
586
587            Expression::NextValue { sequence_name } => {
588                "NEXT_VALUE".hash(hasher);
589                sequence_name.hash(hasher);
590            }
591
592            Expression::MatchAgainst { columns, search_modifier, mode } => {
593                "MATCH_AGAINST".hash(hasher);
594                for col in columns {
595                    col.hash(hasher);
596                }
597                Self::hash_expression(search_modifier, hasher);
598                std::mem::discriminant(mode).hash(hasher);
599            }
600
601            Expression::SessionVariable { name } => {
602                "SESSION_VARIABLE".hash(hasher);
603                name.hash(hasher);
604            }
605
606            Expression::Conjunction(children)
607            | Expression::Disjunction(children)
608            | Expression::RowValueConstructor(children) => {
609                for child in children {
610                    Self::hash_expression(child, hasher);
611                }
612            }
613
614            Expression::Collate { expr, collation } => {
615                "COLLATE".hash(hasher);
616                Self::hash_expression(expr, hasher);
617                collation.hash(hasher);
618            }
619
620            Expression::Raise { action, error_message } => {
621                "RAISE".hash(hasher);
622                std::mem::discriminant(action).hash(hasher);
623                if let Some(msg) = error_message {
624                    Self::hash_expression(msg, hasher);
625                }
626            }
627        }
628    }
629
630    // ========================================================================
631    // Arena-allocated type hashing
632    // ========================================================================
633
634    /// Hash an arena-allocated SELECT statement structure
635    fn hash_arena_select(select: &ArenaSelectStmt<'_>, hasher: &mut DefaultHasher) {
636        // Hash DISTINCT
637        select.distinct.hash(hasher);
638
639        // Hash select items
640        for item in &select.select_list {
641            match item {
642                ArenaSelectItem::Wildcard { .. } => "WILDCARD".hash(hasher),
643                ArenaSelectItem::QualifiedWildcard { qualifier, .. } => {
644                    "QUALIFIED_WILDCARD".hash(hasher);
645                    qualifier.hash(hasher);
646                }
647                ArenaSelectItem::Expression { expr, alias, .. } => {
648                    Self::hash_arena_expression(expr, hasher);
649                    alias.hash(hasher);
650                }
651            }
652        }
653
654        // Hash FROM clause
655        if let Some(ref from) = select.from {
656            Self::hash_arena_from_clause(from, hasher);
657        }
658
659        // Hash WHERE clause
660        if let Some(ref where_clause) = select.where_clause {
661            Self::hash_arena_expression(where_clause, hasher);
662        }
663
664        // Hash GROUP BY
665        if let Some(ref group_by) = select.group_by {
666            Self::hash_arena_group_by(group_by, hasher);
667        }
668
669        // Hash HAVING
670        if let Some(ref having) = select.having {
671            Self::hash_arena_expression(having, hasher);
672        }
673
674        // Hash ORDER BY
675        if let Some(ref order_by) = select.order_by {
676            for item in order_by {
677                Self::hash_arena_expression(&item.expr, hasher);
678                std::mem::discriminant(&item.direction).hash(hasher);
679            }
680        }
681
682        // Hash LIMIT/OFFSET expressions
683        // Note: We hash the presence of limit/offset but not the expression itself
684        // since arena Expression doesn't implement Hash
685        select.limit.is_some().hash(hasher);
686        select.offset.is_some().hash(hasher);
687    }
688
689    /// Hash an arena-allocated FROM clause structure
690    fn hash_arena_from_clause(from: &ArenaFromClause<'_>, hasher: &mut DefaultHasher) {
691        match from {
692            ArenaFromClause::Table { name, alias, .. } => {
693                "TABLE".hash(hasher);
694                name.hash(hasher);
695                alias.hash(hasher);
696            }
697            ArenaFromClause::Join { left, join_type, right, condition, .. } => {
698                "JOIN".hash(hasher);
699                Self::hash_arena_from_clause(left, hasher);
700                std::mem::discriminant(join_type).hash(hasher);
701                Self::hash_arena_from_clause(right, hasher);
702                if let Some(expr) = condition {
703                    Self::hash_arena_expression(expr, hasher);
704                }
705            }
706            ArenaFromClause::Subquery { query, alias, .. } => {
707                "SUBQUERY".hash(hasher);
708                Self::hash_arena_select(query, hasher);
709                alias.hash(hasher);
710            }
711        }
712    }
713
714    fn hash_arena_group_by(group_by: &ArenaGroupByClause<'_>, hasher: &mut DefaultHasher) {
715        match group_by {
716            ArenaGroupByClause::Simple(exprs) => {
717                "SIMPLE".hash(hasher);
718                for expr in exprs {
719                    Self::hash_arena_expression(expr, hasher);
720                }
721            }
722            ArenaGroupByClause::Rollup(elements) => {
723                "ROLLUP".hash(hasher);
724                Self::hash_arena_grouping_elements(elements, hasher);
725            }
726            ArenaGroupByClause::Cube(elements) => {
727                "CUBE".hash(hasher);
728                Self::hash_arena_grouping_elements(elements, hasher);
729            }
730            ArenaGroupByClause::GroupingSets(sets) => {
731                "GROUPING_SETS".hash(hasher);
732                Self::hash_arena_grouping_sets(sets, hasher);
733            }
734            ArenaGroupByClause::Mixed(items) => {
735                "MIXED".hash(hasher);
736                for item in items {
737                    match item {
738                        ArenaMixedGroupingItem::Simple(expr) => {
739                            "SIMPLE".hash(hasher);
740                            Self::hash_arena_expression(expr, hasher);
741                        }
742                        ArenaMixedGroupingItem::Rollup(elements) => {
743                            "ROLLUP".hash(hasher);
744                            Self::hash_arena_grouping_elements(elements, hasher);
745                        }
746                        ArenaMixedGroupingItem::Cube(elements) => {
747                            "CUBE".hash(hasher);
748                            Self::hash_arena_grouping_elements(elements, hasher);
749                        }
750                        ArenaMixedGroupingItem::GroupingSets(sets) => {
751                            "GROUPING_SETS".hash(hasher);
752                            Self::hash_arena_grouping_sets(sets, hasher);
753                        }
754                    }
755                }
756            }
757        }
758    }
759
760    fn hash_arena_grouping_sets(
761        sets: &bumpalo::collections::Vec<'_, ArenaGroupingSet<'_>>,
762        hasher: &mut DefaultHasher,
763    ) {
764        for set in sets {
765            "SET".hash(hasher);
766            for expr in &set.columns {
767                Self::hash_arena_expression(expr, hasher);
768            }
769        }
770    }
771
772    fn hash_arena_grouping_elements(
773        elements: &bumpalo::collections::Vec<'_, ArenaGroupingElement<'_>>,
774        hasher: &mut DefaultHasher,
775    ) {
776        for element in elements {
777            match element {
778                ArenaGroupingElement::Single(expr) => {
779                    "SINGLE".hash(hasher);
780                    Self::hash_arena_expression(expr, hasher);
781                }
782                ArenaGroupingElement::Composite(exprs) => {
783                    "COMPOSITE".hash(hasher);
784                    for expr in exprs {
785                        Self::hash_arena_expression(expr, hasher);
786                    }
787                }
788            }
789        }
790    }
791
792    /// Hash an arena-allocated expression, replacing literals with a placeholder marker
793    fn hash_arena_expression(expr: &ArenaExpression<'_>, hasher: &mut DefaultHasher) {
794        match expr {
795            // Hot-path inline variants
796            // All literals and placeholders hash to the same value
797            ArenaExpression::Literal(_)
798            | ArenaExpression::Placeholder(_)
799            | ArenaExpression::NumberedPlaceholder(_)
800            | ArenaExpression::NamedPlaceholder(_) => "LITERAL_PLACEHOLDER".hash(hasher),
801
802            ArenaExpression::ColumnRef { table, column, .. } => {
803                "COLUMN".hash(hasher);
804                table.hash(hasher);
805                column.hash(hasher);
806            }
807
808            ArenaExpression::BinaryOp { op, left, right } => {
809                "BINARY_OP".hash(hasher);
810                std::mem::discriminant(op).hash(hasher);
811                Self::hash_arena_expression(left, hasher);
812                Self::hash_arena_expression(right, hasher);
813            }
814
815            ArenaExpression::UnaryOp { op, expr } => {
816                "UNARY_OP".hash(hasher);
817                std::mem::discriminant(op).hash(hasher);
818                Self::hash_arena_expression(expr, hasher);
819            }
820
821            ArenaExpression::IsNull { expr, negated } => {
822                "IS_NULL".hash(hasher);
823                Self::hash_arena_expression(expr, hasher);
824                negated.hash(hasher);
825            }
826
827            ArenaExpression::IsDistinctFrom { left, right, negated } => {
828                "IS_DISTINCT_FROM".hash(hasher);
829                Self::hash_arena_expression(left, hasher);
830                Self::hash_arena_expression(right, hasher);
831                negated.hash(hasher);
832            }
833
834            ArenaExpression::IsTruthValue { expr, truth_value, negated } => {
835                "IS_TRUTH_VALUE".hash(hasher);
836                Self::hash_arena_expression(expr, hasher);
837                std::mem::discriminant(truth_value).hash(hasher);
838                negated.hash(hasher);
839            }
840
841            ArenaExpression::Wildcard => "WILDCARD".hash(hasher),
842
843            ArenaExpression::CurrentDate => "CURRENT_DATE".hash(hasher),
844
845            ArenaExpression::CurrentTime { precision } => {
846                "CURRENT_TIME".hash(hasher);
847                precision.hash(hasher);
848            }
849
850            ArenaExpression::CurrentTimestamp { precision } => {
851                "CURRENT_TIMESTAMP".hash(hasher);
852                precision.hash(hasher);
853            }
854
855            ArenaExpression::Default => "DEFAULT".hash(hasher),
856
857            ArenaExpression::Conjunction(children) | ArenaExpression::Disjunction(children) => {
858                for child in children.iter() {
859                    Self::hash_arena_expression(child, hasher);
860                }
861            }
862
863            // Cold-path extended variants
864            ArenaExpression::Extended(ext) => Self::hash_arena_extended_expr(ext, hasher),
865        }
866    }
867
868    /// Hash an arena-allocated extended expression
869    fn hash_arena_extended_expr(ext: &ArenaExtendedExpr<'_>, hasher: &mut DefaultHasher) {
870        match ext {
871            ArenaExtendedExpr::Function { name, args, character_unit } => {
872                "FUNCTION".hash(hasher);
873                name.hash(hasher);
874                for arg in args {
875                    Self::hash_arena_expression(arg, hasher);
876                }
877                if let Some(ref unit) = character_unit {
878                    std::mem::discriminant(unit).hash(hasher);
879                }
880            }
881
882            ArenaExtendedExpr::AggregateFunction { name, distinct, args } => {
883                "AGGREGATE".hash(hasher);
884                name.hash(hasher);
885                distinct.hash(hasher);
886                for arg in args {
887                    Self::hash_arena_expression(arg, hasher);
888                }
889            }
890
891            ArenaExtendedExpr::Case { operand, when_clauses, else_result } => {
892                "CASE".hash(hasher);
893                if let Some(op) = operand {
894                    Self::hash_arena_expression(op, hasher);
895                }
896                for when in when_clauses {
897                    for cond in &when.conditions {
898                        Self::hash_arena_expression(cond, hasher);
899                    }
900                    Self::hash_arena_expression(&when.result, hasher);
901                }
902                if let Some(else_expr) = else_result {
903                    Self::hash_arena_expression(else_expr, hasher);
904                }
905            }
906
907            ArenaExtendedExpr::ScalarSubquery(subquery) => {
908                "SCALAR_SUBQUERY".hash(hasher);
909                Self::hash_arena_select(subquery, hasher);
910            }
911
912            ArenaExtendedExpr::In { expr, subquery, negated } => {
913                "IN_SUBQUERY".hash(hasher);
914                Self::hash_arena_expression(expr, hasher);
915                Self::hash_arena_select(subquery, hasher);
916                negated.hash(hasher);
917            }
918
919            ArenaExtendedExpr::InList { expr, values, negated } => {
920                "IN_LIST".hash(hasher);
921                Self::hash_arena_expression(expr, hasher);
922                values.len().hash(hasher);
923                for val in values {
924                    Self::hash_arena_expression(val, hasher);
925                }
926                negated.hash(hasher);
927            }
928
929            ArenaExtendedExpr::Between { expr, low, high, negated, symmetric } => {
930                "BETWEEN".hash(hasher);
931                Self::hash_arena_expression(expr, hasher);
932                Self::hash_arena_expression(low, hasher);
933                Self::hash_arena_expression(high, hasher);
934                negated.hash(hasher);
935                symmetric.hash(hasher);
936            }
937
938            ArenaExtendedExpr::Cast { expr, data_type } => {
939                "CAST".hash(hasher);
940                Self::hash_arena_expression(expr, hasher);
941                std::mem::discriminant(data_type).hash(hasher);
942            }
943
944            ArenaExtendedExpr::Position { substring, string, character_unit } => {
945                "POSITION".hash(hasher);
946                Self::hash_arena_expression(substring, hasher);
947                Self::hash_arena_expression(string, hasher);
948                if let Some(unit) = character_unit {
949                    std::mem::discriminant(unit).hash(hasher);
950                }
951            }
952
953            ArenaExtendedExpr::Trim { position, removal_char, string } => {
954                "TRIM".hash(hasher);
955                if let Some(pos) = position {
956                    std::mem::discriminant(pos).hash(hasher);
957                }
958                if let Some(ch) = removal_char {
959                    Self::hash_arena_expression(ch, hasher);
960                }
961                Self::hash_arena_expression(string, hasher);
962            }
963
964            ArenaExtendedExpr::Extract { field, expr } => {
965                "EXTRACT".hash(hasher);
966                std::mem::discriminant(field).hash(hasher);
967                Self::hash_arena_expression(expr, hasher);
968            }
969
970            ArenaExtendedExpr::Like { expr, pattern, negated, .. } => {
971                "LIKE".hash(hasher);
972                Self::hash_arena_expression(expr, hasher);
973                Self::hash_arena_expression(pattern, hasher);
974                negated.hash(hasher);
975            }
976
977            ArenaExtendedExpr::Glob { expr, pattern, negated, .. } => {
978                "GLOB".hash(hasher);
979                Self::hash_arena_expression(expr, hasher);
980                Self::hash_arena_expression(pattern, hasher);
981                negated.hash(hasher);
982            }
983
984            ArenaExtendedExpr::Exists { subquery, negated } => {
985                "EXISTS".hash(hasher);
986                Self::hash_arena_select(subquery, hasher);
987                negated.hash(hasher);
988            }
989
990            ArenaExtendedExpr::QuantifiedComparison { expr, op, quantifier, subquery } => {
991                "QUANTIFIED".hash(hasher);
992                Self::hash_arena_expression(expr, hasher);
993                std::mem::discriminant(op).hash(hasher);
994                std::mem::discriminant(quantifier).hash(hasher);
995                Self::hash_arena_select(subquery, hasher);
996            }
997
998            ArenaExtendedExpr::Interval {
999                value,
1000                unit,
1001                leading_precision,
1002                fractional_precision,
1003            } => {
1004                "INTERVAL".hash(hasher);
1005                Self::hash_arena_expression(value, hasher);
1006                format!("{:?}", unit).hash(hasher);
1007                leading_precision.hash(hasher);
1008                fractional_precision.hash(hasher);
1009            }
1010
1011            ArenaExtendedExpr::DuplicateKeyValue { column } => {
1012                "DUPLICATE_KEY_VALUE".hash(hasher);
1013                column.hash(hasher);
1014            }
1015
1016            ArenaExtendedExpr::WindowFunction { function, over } => {
1017                "WINDOW_FUNCTION".hash(hasher);
1018                match function {
1019                    ArenaWindowFunctionSpec::Aggregate { name, args } => {
1020                        "AGGREGATE".hash(hasher);
1021                        name.hash(hasher);
1022                        for arg in args {
1023                            Self::hash_arena_expression(arg, hasher);
1024                        }
1025                    }
1026                    ArenaWindowFunctionSpec::Ranking { name, args } => {
1027                        "RANKING".hash(hasher);
1028                        name.hash(hasher);
1029                        for arg in args {
1030                            Self::hash_arena_expression(arg, hasher);
1031                        }
1032                    }
1033                    ArenaWindowFunctionSpec::Value { name, args } => {
1034                        "VALUE".hash(hasher);
1035                        name.hash(hasher);
1036                        for arg in args {
1037                            Self::hash_arena_expression(arg, hasher);
1038                        }
1039                    }
1040                }
1041
1042                if let Some(ref partition_by) = over.partition_by {
1043                    for expr in partition_by {
1044                        Self::hash_arena_expression(expr, hasher);
1045                    }
1046                }
1047                if let Some(ref order_by) = over.order_by {
1048                    for item in order_by {
1049                        Self::hash_arena_expression(&item.expr, hasher);
1050                        std::mem::discriminant(&item.direction).hash(hasher);
1051                    }
1052                }
1053                if let Some(ref frame) = over.frame {
1054                    std::mem::discriminant(&frame.unit).hash(hasher);
1055                    std::mem::discriminant(&frame.start).hash(hasher);
1056                    if let Some(ref end) = frame.end {
1057                        std::mem::discriminant(end).hash(hasher);
1058                    }
1059                }
1060            }
1061
1062            ArenaExtendedExpr::NextValue { sequence_name } => {
1063                "NEXT_VALUE".hash(hasher);
1064                sequence_name.hash(hasher);
1065            }
1066
1067            ArenaExtendedExpr::MatchAgainst { columns, search_modifier, mode } => {
1068                "MATCH_AGAINST".hash(hasher);
1069                for col in columns {
1070                    col.hash(hasher);
1071                }
1072                Self::hash_arena_expression(search_modifier, hasher);
1073                std::mem::discriminant(mode).hash(hasher);
1074            }
1075
1076            ArenaExtendedExpr::PseudoVariable { pseudo_table, column } => {
1077                "PSEUDO_VARIABLE".hash(hasher);
1078                std::mem::discriminant(pseudo_table).hash(hasher);
1079                column.hash(hasher);
1080            }
1081
1082            ArenaExtendedExpr::SessionVariable { name } => {
1083                "SESSION_VARIABLE".hash(hasher);
1084                name.hash(hasher);
1085            }
1086
1087            ArenaExtendedExpr::RowValueConstructor(children) => {
1088                "ROW_VALUE_CONSTRUCTOR".hash(hasher);
1089                for child in children {
1090                    Self::hash_arena_expression(child, hasher);
1091                }
1092            }
1093        }
1094    }
1095}
1096
1097#[cfg(test)]
1098mod tests {
1099    use super::*;
1100
1101    #[test]
1102    fn test_same_query_same_signature() {
1103        let sig1 = QuerySignature::from_sql("SELECT * FROM users");
1104        let sig2 = QuerySignature::from_sql("SELECT * FROM users");
1105        assert_eq!(sig1, sig2);
1106    }
1107
1108    #[test]
1109    fn test_whitespace_normalization() {
1110        let sig1 = QuerySignature::from_sql("SELECT * FROM users");
1111        let sig2 = QuerySignature::from_sql("SELECT  *  FROM  users");
1112        assert_eq!(sig1, sig2);
1113    }
1114
1115    #[test]
1116    fn test_case_insensitive() {
1117        let sig1 = QuerySignature::from_sql("SELECT * FROM users");
1118        let sig2 = QuerySignature::from_sql("select * from users");
1119        assert_eq!(sig1, sig2);
1120    }
1121
1122    #[test]
1123    fn test_different_queries_different_signature() {
1124        let sig1 = QuerySignature::from_sql("SELECT * FROM users");
1125        let sig2 = QuerySignature::from_sql("SELECT * FROM orders");
1126        assert_ne!(sig1, sig2);
1127    }
1128
1129    #[test]
1130    fn test_different_literals_different_signature_string_based() {
1131        // Different literals create different signatures with string-based hashing
1132        let sig1 = QuerySignature::from_sql("SELECT col0 FROM tab WHERE col1 > 5");
1133        let sig2 = QuerySignature::from_sql("SELECT col0 FROM tab WHERE col1 > 10");
1134        // String-based hashing includes literals in the signature
1135        assert_ne!(sig1, sig2);
1136    }
1137
1138    #[test]
1139    fn test_ast_based_same_structure_different_literals() {
1140        use vibesql_ast::{
1141            BinaryOperator, Expression, FromClause, SelectItem, SelectStmt, Statement,
1142        };
1143        use vibesql_types::SqlValue;
1144
1145        // SELECT col0 FROM tab WHERE col1 > 5
1146        let stmt1 = Statement::Select(Box::new(SelectStmt {
1147            with_clause: None,
1148            distinct: false,
1149            select_list: vec![SelectItem::Expression {
1150                expr: Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple("col0", false)),
1151                alias: None,
1152                source_text: None,
1153            }],
1154            into_table: None,
1155            into_variables: None,
1156            from: Some(FromClause::Table {
1157                index_hint: None,
1158                name: "tab".to_string(),
1159                alias: None,
1160                column_aliases: None,
1161                quoted: false,
1162            }),
1163            where_clause: Some(Expression::BinaryOp {
1164                op: BinaryOperator::GreaterThan,
1165                left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
1166                    "col1", false,
1167                ))),
1168                right: Box::new(Expression::Literal(SqlValue::Integer(5))),
1169            }),
1170            group_by: None,
1171            having: None,
1172            window_definitions: None,
1173            order_by: None,
1174            limit: None,
1175            offset: None,
1176            set_operation: None,
1177            values: None,
1178        }));
1179
1180        // SELECT col0 FROM tab WHERE col1 > 10 (different literal)
1181        let stmt2 = Statement::Select(Box::new(SelectStmt {
1182            with_clause: None,
1183            distinct: false,
1184            select_list: vec![SelectItem::Expression {
1185                expr: Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple("col0", false)),
1186                alias: None,
1187                source_text: None,
1188            }],
1189            into_table: None,
1190            into_variables: None,
1191            from: Some(FromClause::Table {
1192                index_hint: None,
1193                name: "tab".to_string(),
1194                alias: None,
1195                column_aliases: None,
1196                quoted: false,
1197            }),
1198            where_clause: Some(Expression::BinaryOp {
1199                op: BinaryOperator::GreaterThan,
1200                left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
1201                    "col1", false,
1202                ))),
1203                right: Box::new(Expression::Literal(SqlValue::Integer(10))),
1204            }),
1205            group_by: None,
1206            having: None,
1207            window_definitions: None,
1208            order_by: None,
1209            limit: None,
1210            offset: None,
1211            set_operation: None,
1212            values: None,
1213        }));
1214
1215        let sig1 = QuerySignature::from_ast(&stmt1);
1216        let sig2 = QuerySignature::from_ast(&stmt2);
1217
1218        // AST-based signatures should be the same despite different literals
1219        assert_eq!(sig1, sig2);
1220    }
1221
1222    #[test]
1223    fn test_ast_based_different_structure() {
1224        use vibesql_ast::{
1225            BinaryOperator, Expression, FromClause, SelectItem, SelectStmt, Statement,
1226        };
1227        use vibesql_types::SqlValue;
1228
1229        // SELECT col0 FROM tab WHERE col1 > 5
1230        let stmt1 = Statement::Select(Box::new(SelectStmt {
1231            with_clause: None,
1232            distinct: false,
1233            select_list: vec![SelectItem::Expression {
1234                expr: Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple("col0", false)),
1235                alias: None,
1236                source_text: None,
1237            }],
1238            into_table: None,
1239            into_variables: None,
1240            from: Some(FromClause::Table {
1241                index_hint: None,
1242                name: "tab".to_string(),
1243                alias: None,
1244                column_aliases: None,
1245                quoted: false,
1246            }),
1247            where_clause: Some(Expression::BinaryOp {
1248                op: BinaryOperator::GreaterThan,
1249                left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
1250                    "col1", false,
1251                ))),
1252                right: Box::new(Expression::Literal(SqlValue::Integer(5))),
1253            }),
1254            group_by: None,
1255            having: None,
1256            window_definitions: None,
1257            order_by: None,
1258            limit: None,
1259            offset: None,
1260            set_operation: None,
1261            values: None,
1262        }));
1263
1264        // SELECT col0 FROM tab WHERE col1 < 5 (different operator)
1265        let stmt2 = Statement::Select(Box::new(SelectStmt {
1266            with_clause: None,
1267            distinct: false,
1268            select_list: vec![SelectItem::Expression {
1269                expr: Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple("col0", false)),
1270                alias: None,
1271                source_text: None,
1272            }],
1273            into_table: None,
1274            into_variables: None,
1275            from: Some(FromClause::Table {
1276                index_hint: None,
1277                name: "tab".to_string(),
1278                alias: None,
1279                column_aliases: None,
1280                quoted: false,
1281            }),
1282            where_clause: Some(Expression::BinaryOp {
1283                op: BinaryOperator::LessThan, // Different operator!
1284                left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
1285                    "col1", false,
1286                ))),
1287                right: Box::new(Expression::Literal(SqlValue::Integer(5))),
1288            }),
1289            group_by: None,
1290            having: None,
1291            window_definitions: None,
1292            order_by: None,
1293            limit: None,
1294            offset: None,
1295            set_operation: None,
1296            values: None,
1297        }));
1298
1299        let sig1 = QuerySignature::from_ast(&stmt1);
1300        let sig2 = QuerySignature::from_ast(&stmt2);
1301
1302        // Different structure should produce different signatures
1303        assert_ne!(sig1, sig2);
1304    }
1305}