1use 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#[derive(Clone, Debug, Eq, PartialEq, Hash)]
25pub struct QuerySignature {
26 hash: u64,
27}
28
29impl QuerySignature {
30 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 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 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 pub fn hash(&self) -> u64 {
59 self.hash
60 }
61
62 fn normalize(sql: &str) -> String {
64 sql.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase()
65 }
66
67 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 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 _ => {
137 std::mem::discriminant(stmt).hash(hasher);
138 }
139 }
140 }
141
142 fn hash_select(select: &vibesql_ast::SelectStmt, hasher: &mut DefaultHasher) {
144 select.distinct.hash(hasher);
146
147 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 if let Some(ref from) = select.from {
164 Self::hash_from_clause(from, hasher);
165 }
166
167 if let Some(ref where_clause) = select.where_clause {
169 Self::hash_expression(where_clause, hasher);
170 }
171
172 if let Some(ref group_by) = select.group_by {
174 Self::hash_group_by(group_by, hasher);
175 }
176
177 if let Some(ref having) = select.having {
179 Self::hash_expression(having, hasher);
180 }
181
182 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 select.limit.is_some().hash(hasher);
194 select.offset.is_some().hash(hasher);
195 }
196
197 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 fn hash_expression(expr: &Expression, hasher: &mut DefaultHasher) {
313 match expr {
314 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 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 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 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 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 fn hash_arena_select(select: &ArenaSelectStmt<'_>, hasher: &mut DefaultHasher) {
636 select.distinct.hash(hasher);
638
639 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 if let Some(ref from) = select.from {
656 Self::hash_arena_from_clause(from, hasher);
657 }
658
659 if let Some(ref where_clause) = select.where_clause {
661 Self::hash_arena_expression(where_clause, hasher);
662 }
663
664 if let Some(ref group_by) = select.group_by {
666 Self::hash_arena_group_by(group_by, hasher);
667 }
668
669 if let Some(ref having) = select.having {
671 Self::hash_arena_expression(having, hasher);
672 }
673
674 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 select.limit.is_some().hash(hasher);
686 select.offset.is_some().hash(hasher);
687 }
688
689 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 fn hash_arena_expression(expr: &ArenaExpression<'_>, hasher: &mut DefaultHasher) {
794 match expr {
795 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 ArenaExpression::Extended(ext) => Self::hash_arena_extended_expr(ext, hasher),
865 }
866 }
867
868 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 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 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 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 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 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 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 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, 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 assert_ne!(sig1, sig2);
1304 }
1305}