use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
pub struct ExpressionHasher;
impl ExpressionHasher {
pub fn hash(expr: &vibesql_ast::Expression) -> u64 {
let mut hasher = DefaultHasher::new();
Self::hash_expression(expr, &mut hasher);
hasher.finish()
}
pub fn is_deterministic_for_index(expr: &vibesql_ast::Expression) -> bool {
Self::is_deterministic_impl(expr, true)
}
pub fn is_deterministic(expr: &vibesql_ast::Expression) -> bool {
Self::is_deterministic_impl(expr, false)
}
fn is_deterministic_impl(expr: &vibesql_ast::Expression, allow_column_refs: bool) -> bool {
match expr {
vibesql_ast::Expression::CurrentDate
| vibesql_ast::Expression::CurrentTime { .. }
| vibesql_ast::Expression::CurrentTimestamp { .. } => false,
vibesql_ast::Expression::Function { name, args, .. } => {
let name_upper = name.to_uppercase();
if matches!(
name_upper.as_str(),
"RAND"
| "RANDOM"
| "NOW"
| "CURRENT_DATE"
| "CURRENT_TIME"
| "CURRENT_TIMESTAMP"
| "COUNT"
| "SUM"
| "AVG"
| "MIN"
| "MAX"
) {
return false;
}
args.iter().all(|arg| Self::is_deterministic_impl(arg, allow_column_refs))
}
vibesql_ast::Expression::NextValue { .. } => false,
vibesql_ast::Expression::ScalarSubquery(_)
| vibesql_ast::Expression::In { .. }
| vibesql_ast::Expression::Exists { .. }
| vibesql_ast::Expression::QuantifiedComparison { .. } => false,
vibesql_ast::Expression::BinaryOp { left, right, .. } => {
Self::is_deterministic_impl(left, allow_column_refs)
&& Self::is_deterministic_impl(right, allow_column_refs)
}
vibesql_ast::Expression::UnaryOp { expr, .. } => {
Self::is_deterministic_impl(expr, allow_column_refs)
}
vibesql_ast::Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
if !Self::is_deterministic_impl(op, allow_column_refs) {
return false;
}
}
for clause in when_clauses {
for condition in &clause.conditions {
if !Self::is_deterministic_impl(condition, allow_column_refs) {
return false;
}
}
if !Self::is_deterministic_impl(&clause.result, allow_column_refs) {
return false;
}
}
if let Some(else_expr) = else_result {
if !Self::is_deterministic_impl(else_expr, allow_column_refs) {
return false;
}
}
true
}
vibesql_ast::Expression::Between { expr, low, high, .. } => {
Self::is_deterministic_impl(expr, allow_column_refs)
&& Self::is_deterministic_impl(low, allow_column_refs)
&& Self::is_deterministic_impl(high, allow_column_refs)
}
vibesql_ast::Expression::Cast { expr, .. } => {
Self::is_deterministic_impl(expr, allow_column_refs)
}
vibesql_ast::Expression::Interval { value, .. } => {
Self::is_deterministic_impl(value, allow_column_refs)
}
vibesql_ast::Expression::InList { expr, values, .. } => {
Self::is_deterministic_impl(expr, allow_column_refs)
&& values.iter().all(|v| Self::is_deterministic_impl(v, allow_column_refs))
}
vibesql_ast::Expression::Like { expr, pattern, .. }
| vibesql_ast::Expression::Glob { expr, pattern, .. } => {
Self::is_deterministic_impl(expr, allow_column_refs)
&& Self::is_deterministic_impl(pattern, allow_column_refs)
}
vibesql_ast::Expression::IsNull { expr, .. } => {
Self::is_deterministic_impl(expr, allow_column_refs)
}
vibesql_ast::Expression::IsDistinctFrom { left, right, .. } => {
Self::is_deterministic_impl(left, allow_column_refs)
&& Self::is_deterministic_impl(right, allow_column_refs)
}
vibesql_ast::Expression::IsTruthValue { expr, .. } => {
Self::is_deterministic_impl(expr, allow_column_refs)
}
vibesql_ast::Expression::Position { substring, string, .. } => {
Self::is_deterministic_impl(substring, allow_column_refs)
&& Self::is_deterministic_impl(string, allow_column_refs)
}
vibesql_ast::Expression::Trim { string, removal_char, .. } => {
Self::is_deterministic_impl(string, allow_column_refs)
&& removal_char
.as_ref()
.is_none_or(|c| Self::is_deterministic_impl(c, allow_column_refs))
}
vibesql_ast::Expression::Extract { expr, .. } => {
Self::is_deterministic_impl(expr, allow_column_refs)
}
vibesql_ast::Expression::Literal(_)
| vibesql_ast::Expression::Placeholder(_)
| vibesql_ast::Expression::NumberedPlaceholder(_)
| vibesql_ast::Expression::NamedPlaceholder(_) => true,
vibesql_ast::Expression::ColumnRef(_) => allow_column_refs,
vibesql_ast::Expression::PseudoVariable { .. }
| vibesql_ast::Expression::SessionVariable { .. } => false,
vibesql_ast::Expression::WindowFunction { .. }
| vibesql_ast::Expression::AggregateFunction { .. } => false,
vibesql_ast::Expression::Wildcard
| vibesql_ast::Expression::Default
| vibesql_ast::Expression::DuplicateKeyValue { .. } => true,
vibesql_ast::Expression::Conjunction(children)
| vibesql_ast::Expression::Disjunction(children)
| vibesql_ast::Expression::RowValueConstructor(children) => {
children.iter().all(|c| Self::is_deterministic_impl(c, allow_column_refs))
}
vibesql_ast::Expression::Collate { expr, .. } => {
Self::is_deterministic_impl(expr, allow_column_refs)
}
vibesql_ast::Expression::Raise { .. } => false,
vibesql_ast::Expression::MatchAgainst { .. } => {
false
}
}
}
fn hash_expression(expr: &vibesql_ast::Expression, hasher: &mut DefaultHasher) {
std::mem::discriminant(expr).hash(hasher);
match expr {
vibesql_ast::Expression::Literal(val) => {
Self::hash_sql_value(val, hasher);
}
vibesql_ast::Expression::ColumnRef(col_id) => {
col_id.table_canonical().hash(hasher);
col_id.column_canonical().hash(hasher);
}
vibesql_ast::Expression::PseudoVariable { pseudo_table, column } => {
std::mem::discriminant(pseudo_table).hash(hasher);
column.hash(hasher);
}
vibesql_ast::Expression::BinaryOp { left, op, right } => {
Self::hash_expression(left, hasher);
std::mem::discriminant(op).hash(hasher);
Self::hash_expression(right, hasher);
}
vibesql_ast::Expression::UnaryOp { op, expr } => {
std::mem::discriminant(op).hash(hasher);
Self::hash_expression(expr, hasher);
}
vibesql_ast::Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
Self::hash_expression(op, hasher);
}
when_clauses.len().hash(hasher);
for clause in when_clauses {
clause.conditions.len().hash(hasher);
for condition in &clause.conditions {
Self::hash_expression(condition, hasher);
}
Self::hash_expression(&clause.result, hasher);
}
if let Some(else_expr) = else_result {
Self::hash_expression(else_expr, hasher);
}
}
vibesql_ast::Expression::Between { expr, low, high, negated, symmetric } => {
Self::hash_expression(expr, hasher);
Self::hash_expression(low, hasher);
Self::hash_expression(high, hasher);
negated.hash(hasher);
symmetric.hash(hasher);
}
vibesql_ast::Expression::Cast { expr, data_type } => {
Self::hash_expression(expr, hasher);
Self::hash_data_type(data_type, hasher);
}
vibesql_ast::Expression::InList { expr, values, negated } => {
Self::hash_expression(expr, hasher);
values.len().hash(hasher);
for val in values {
Self::hash_expression(val, hasher);
}
negated.hash(hasher);
}
vibesql_ast::Expression::Like { expr, pattern, negated, .. }
| vibesql_ast::Expression::Glob { expr, pattern, negated, .. } => {
Self::hash_expression(expr, hasher);
Self::hash_expression(pattern, hasher);
negated.hash(hasher);
}
vibesql_ast::Expression::IsNull { expr, negated } => {
Self::hash_expression(expr, hasher);
negated.hash(hasher);
}
vibesql_ast::Expression::IsDistinctFrom { left, right, negated } => {
Self::hash_expression(left, hasher);
Self::hash_expression(right, hasher);
negated.hash(hasher);
}
vibesql_ast::Expression::IsTruthValue { expr, truth_value, negated } => {
Self::hash_expression(expr, hasher);
std::mem::discriminant(truth_value).hash(hasher);
negated.hash(hasher);
}
vibesql_ast::Expression::Function { name, args, character_unit } => {
name.hash(hasher);
args.len().hash(hasher);
for arg in args {
Self::hash_expression(arg, hasher);
}
if let Some(unit) = character_unit {
std::mem::discriminant(unit).hash(hasher);
}
}
vibesql_ast::Expression::Position { substring, string, character_unit } => {
Self::hash_expression(substring, hasher);
Self::hash_expression(string, hasher);
if let Some(unit) = character_unit {
std::mem::discriminant(unit).hash(hasher);
}
}
vibesql_ast::Expression::Trim { position, removal_char, string } => {
if let Some(pos) = position {
std::mem::discriminant(pos).hash(hasher);
}
if let Some(ch) = removal_char {
Self::hash_expression(ch, hasher);
}
Self::hash_expression(string, hasher);
}
vibesql_ast::Expression::Extract { field, expr } => {
"EXTRACT".hash(hasher);
std::mem::discriminant(field).hash(hasher);
Self::hash_expression(expr, hasher);
}
vibesql_ast::Expression::CurrentDate => {
"CURRENT_DATE".hash(hasher);
}
vibesql_ast::Expression::CurrentTime { precision } => {
"CURRENT_TIME".hash(hasher);
precision.hash(hasher);
}
vibesql_ast::Expression::CurrentTimestamp { precision } => {
"CURRENT_TIMESTAMP".hash(hasher);
precision.hash(hasher);
}
vibesql_ast::Expression::Interval {
value,
unit,
leading_precision,
fractional_precision,
} => {
"INTERVAL".hash(hasher);
Self::hash_expression(value, hasher);
format!("{:?}", unit).hash(hasher);
leading_precision.hash(hasher);
fractional_precision.hash(hasher);
}
vibesql_ast::Expression::In { expr, subquery, negated } => {
Self::hash_expression(expr, hasher);
format!("{:?}", subquery).hash(hasher); negated.hash(hasher);
}
vibesql_ast::Expression::ScalarSubquery(subquery) => {
format!("{:?}", subquery).hash(hasher);
}
vibesql_ast::Expression::Exists { subquery, negated } => {
format!("{:?}", subquery).hash(hasher);
negated.hash(hasher);
}
vibesql_ast::Expression::QuantifiedComparison { expr, op, quantifier, subquery } => {
Self::hash_expression(expr, hasher);
std::mem::discriminant(op).hash(hasher);
std::mem::discriminant(quantifier).hash(hasher);
format!("{:?}", subquery).hash(hasher);
}
vibesql_ast::Expression::WindowFunction { .. } => {
format!("{:?}", expr).hash(hasher);
}
vibesql_ast::Expression::AggregateFunction { .. } => {
format!("{:?}", expr).hash(hasher);
}
vibesql_ast::Expression::Wildcard => {
"*".hash(hasher);
}
vibesql_ast::Expression::Default => {
"DEFAULT".hash(hasher);
}
vibesql_ast::Expression::DuplicateKeyValue { column } => {
"DUPLICATE_KEY_VALUE".hash(hasher);
column.hash(hasher);
}
vibesql_ast::Expression::NextValue { sequence_name } => {
sequence_name.hash(hasher);
}
vibesql_ast::Expression::MatchAgainst { columns, search_modifier, mode } => {
columns.len().hash(hasher);
for col in columns {
col.hash(hasher);
}
Self::hash_expression(search_modifier, hasher);
std::mem::discriminant(mode).hash(hasher);
}
vibesql_ast::Expression::SessionVariable { name } => {
name.hash(hasher);
}
vibesql_ast::Expression::Placeholder(idx)
| vibesql_ast::Expression::NumberedPlaceholder(idx) => {
idx.hash(hasher);
}
vibesql_ast::Expression::NamedPlaceholder(name) => {
name.hash(hasher);
}
vibesql_ast::Expression::Conjunction(children)
| vibesql_ast::Expression::Disjunction(children)
| vibesql_ast::Expression::RowValueConstructor(children) => {
children.len().hash(hasher);
for child in children {
Self::hash_expression(child, hasher);
}
}
vibesql_ast::Expression::Collate { expr, collation } => {
"COLLATE".hash(hasher);
Self::hash_expression(expr, hasher);
collation.hash(hasher);
}
vibesql_ast::Expression::Raise { action, error_message } => {
"RAISE".hash(hasher);
std::mem::discriminant(action).hash(hasher);
if let Some(msg) = error_message {
Self::hash_expression(msg, hasher);
}
}
}
}
fn hash_sql_value(val: &vibesql_types::SqlValue, hasher: &mut DefaultHasher) {
std::mem::discriminant(val).hash(hasher);
match val {
vibesql_types::SqlValue::Null => {}
vibesql_types::SqlValue::Integer(i) => i.hash(hasher),
vibesql_types::SqlValue::Smallint(s) => s.hash(hasher),
vibesql_types::SqlValue::Bigint(b) => b.hash(hasher),
vibesql_types::SqlValue::Unsigned(u) => u.hash(hasher),
vibesql_types::SqlValue::Numeric(n) => n.to_bits().hash(hasher), vibesql_types::SqlValue::Float(f) => f.to_bits().hash(hasher),
vibesql_types::SqlValue::Real(r) => r.to_bits().hash(hasher),
vibesql_types::SqlValue::Double(d) => d.to_bits().hash(hasher),
vibesql_types::SqlValue::Varchar(s) | vibesql_types::SqlValue::Character(s) => {
s.hash(hasher)
}
vibesql_types::SqlValue::Boolean(b) => b.hash(hasher),
vibesql_types::SqlValue::Date(d) => d.hash(hasher),
vibesql_types::SqlValue::Time(t) => t.hash(hasher),
vibesql_types::SqlValue::Timestamp(ts) => ts.hash(hasher),
vibesql_types::SqlValue::Interval(i) => i.hash(hasher),
vibesql_types::SqlValue::Vector(v) => {
for f in v.iter() {
f.to_bits().hash(hasher);
}
}
vibesql_types::SqlValue::Blob(b) => b.hash(hasher),
}
}
fn hash_data_type(data_type: &vibesql_types::DataType, hasher: &mut DefaultHasher) {
std::mem::discriminant(data_type).hash(hasher);
match data_type {
vibesql_types::DataType::Integer => {}
vibesql_types::DataType::Smallint => {}
vibesql_types::DataType::Bigint => {}
vibesql_types::DataType::Unsigned => {}
vibesql_types::DataType::Numeric { precision, scale }
| vibesql_types::DataType::Decimal { precision, scale } => {
precision.hash(hasher);
scale.hash(hasher);
}
vibesql_types::DataType::Float { precision } => precision.hash(hasher),
vibesql_types::DataType::Real => {}
vibesql_types::DataType::DoublePrecision => {}
vibesql_types::DataType::Varchar { max_length } => max_length.hash(hasher),
vibesql_types::DataType::Character { length } => length.hash(hasher),
vibesql_types::DataType::CharacterLargeObject => {}
vibesql_types::DataType::Name => {}
vibesql_types::DataType::Boolean => {}
vibesql_types::DataType::Date => {}
vibesql_types::DataType::Time { with_timezone } => with_timezone.hash(hasher),
vibesql_types::DataType::Timestamp { with_timezone } => with_timezone.hash(hasher),
vibesql_types::DataType::Interval { start_field, end_field } => {
std::mem::discriminant(start_field).hash(hasher);
if let Some(ef) = end_field {
std::mem::discriminant(ef).hash(hasher);
}
}
vibesql_types::DataType::BinaryLargeObject => {}
vibesql_types::DataType::Bit { length } => length.hash(hasher),
vibesql_types::DataType::UserDefined { type_name } => type_name.hash(hasher),
vibesql_types::DataType::Vector { dimensions } => dimensions.hash(hasher),
vibesql_types::DataType::Null => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identical_literals_hash_equal() {
let expr1 = vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(42));
let expr2 = vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(42));
assert_eq!(ExpressionHasher::hash(&expr1), ExpressionHasher::hash(&expr2));
}
#[test]
fn test_different_literals_hash_different() {
let expr1 = vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(42));
let expr2 = vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(43));
assert_ne!(ExpressionHasher::hash(&expr1), ExpressionHasher::hash(&expr2));
}
#[test]
fn test_identical_binary_ops_hash_equal() {
let left = Box::new(vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(1)));
let right = Box::new(vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(2)));
let expr1 = vibesql_ast::Expression::BinaryOp {
left: left.clone(),
op: vibesql_ast::BinaryOperator::Plus,
right: right.clone(),
};
let expr2 = vibesql_ast::Expression::BinaryOp {
left,
op: vibesql_ast::BinaryOperator::Plus,
right,
};
assert_eq!(ExpressionHasher::hash(&expr1), ExpressionHasher::hash(&expr2));
}
#[test]
fn test_literals_are_deterministic() {
let expr = vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(42));
assert!(ExpressionHasher::is_deterministic(&expr));
}
#[test]
fn test_column_refs_are_not_deterministic() {
let expr = vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"col1", false,
));
assert!(!ExpressionHasher::is_deterministic(&expr));
}
#[test]
fn test_current_timestamp_is_not_deterministic() {
let expr = vibesql_ast::Expression::CurrentTimestamp { precision: None };
assert!(!ExpressionHasher::is_deterministic(&expr));
}
#[test]
fn test_rand_function_is_not_deterministic() {
let expr = vibesql_ast::Expression::Function {
name: vibesql_ast::FunctionIdentifier::new("RAND"),
args: vec![],
character_unit: None,
};
assert!(!ExpressionHasher::is_deterministic(&expr));
}
#[test]
fn test_deterministic_function_is_deterministic() {
let arg = vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(42));
let expr = vibesql_ast::Expression::Function {
name: vibesql_ast::FunctionIdentifier::new("ABS"),
args: vec![arg],
character_unit: None,
};
assert!(ExpressionHasher::is_deterministic(&expr));
}
}