sql_cli/data/
value_comparisons.rs

1/// Centralized value comparison logic for consistent behavior across all query paths
2use crate::data::datatable::DataValue;
3use crate::data::value_parsing::parse_bool;
4use crate::sql::functions::date_time::parse_datetime;
5use std::cmp::Ordering;
6
7/// Compare two DataValues for equality
8pub fn values_equal(left: &DataValue, right: &DataValue, case_insensitive: bool) -> bool {
9    compare_values(left, right, case_insensitive) == Some(Ordering::Equal)
10}
11
12/// Compare two DataValues for inequality
13pub fn values_not_equal(left: &DataValue, right: &DataValue, case_insensitive: bool) -> bool {
14    compare_values(left, right, case_insensitive) != Some(Ordering::Equal)
15}
16
17/// Compare two DataValues with a comparison operator
18pub fn compare_with_op(
19    left: &DataValue,
20    right: &DataValue,
21    op: &str,
22    case_insensitive: bool,
23) -> bool {
24    let ordering = compare_values(left, right, case_insensitive);
25
26    match (ordering, op) {
27        (Some(Ordering::Equal), "=" | "==") => true,
28        (Some(ord), "!=" | "<>") if ord != Ordering::Equal => true,
29        (Some(Ordering::Less), "<") => true,
30        (Some(ord), "<=") if ord != Ordering::Greater => true,
31        (Some(Ordering::Greater), ">") => true,
32        (Some(ord), ">=") if ord != Ordering::Less => true,
33        _ => false,
34    }
35}
36
37/// Core comparison logic that returns an Ordering
38/// This is the single source of truth for all value comparisons
39pub fn compare_values(
40    left: &DataValue,
41    right: &DataValue,
42    case_insensitive: bool,
43) -> Option<Ordering> {
44    match (left, right) {
45        // Null comparisons
46        (DataValue::Null, DataValue::Null) => Some(Ordering::Equal),
47        (DataValue::Null, _) | (_, DataValue::Null) => None, // NULL comparisons are undefined
48
49        // Boolean comparisons
50        (DataValue::Boolean(a), DataValue::Boolean(b)) => Some(a.cmp(b)),
51
52        // Integer comparisons
53        (DataValue::Integer(a), DataValue::Integer(b)) => Some(a.cmp(b)),
54
55        // Float comparisons
56        (DataValue::Float(a), DataValue::Float(b)) => {
57            // Handle NaN properly
58            if a.is_nan() && b.is_nan() {
59                Some(Ordering::Equal)
60            } else if a.is_nan() {
61                Some(Ordering::Greater) // NaN sorts last
62            } else if b.is_nan() {
63                Some(Ordering::Less)
64            } else {
65                a.partial_cmp(b)
66            }
67        }
68
69        // Mixed numeric comparisons
70        (DataValue::Integer(a), DataValue::Float(b)) => {
71            let a_float = *a as f64;
72            if b.is_nan() {
73                Some(Ordering::Less)
74            } else {
75                a_float.partial_cmp(b)
76            }
77        }
78        (DataValue::Float(a), DataValue::Integer(b)) => {
79            let b_float = *b as f64;
80            if a.is_nan() {
81                Some(Ordering::Greater)
82            } else {
83                a.partial_cmp(&b_float)
84            }
85        }
86
87        // DateTime comparisons
88        (DataValue::DateTime(a), DataValue::DateTime(b)) => {
89            // Parse both as dates for comparison
90            match (parse_datetime(a), parse_datetime(b)) {
91                (Ok(a_dt), Ok(b_dt)) => Some(a_dt.cmp(&b_dt)),
92                _ => Some(a.cmp(b)), // Fall back to string comparison
93            }
94        }
95
96        // String comparisons
97        (DataValue::String(a), DataValue::String(b)) => {
98            // Try date comparison first
99            if let (Ok(a_dt), Ok(b_dt)) = (parse_datetime(a), parse_datetime(b)) {
100                return Some(a_dt.cmp(&b_dt));
101            }
102
103            // String comparison
104            if case_insensitive {
105                Some(a.to_lowercase().cmp(&b.to_lowercase()))
106            } else {
107                Some(a.cmp(b))
108            }
109        }
110
111        // InternedString comparisons
112        (DataValue::InternedString(a), DataValue::InternedString(b)) => {
113            // Try date comparison first
114            if let (Ok(a_dt), Ok(b_dt)) = (parse_datetime(a.as_str()), parse_datetime(b.as_str())) {
115                return Some(a_dt.cmp(&b_dt));
116            }
117
118            // String comparison
119            if case_insensitive {
120                Some(a.to_lowercase().cmp(&b.to_lowercase()))
121            } else {
122                Some(a.as_str().cmp(b.as_str()))
123            }
124        }
125
126        // Mixed String/InternedString comparisons
127        (DataValue::String(a), DataValue::InternedString(b)) => {
128            // Try date comparison first
129            if let (Ok(a_dt), Ok(b_dt)) = (parse_datetime(a), parse_datetime(b.as_str())) {
130                return Some(a_dt.cmp(&b_dt));
131            }
132
133            // String comparison
134            if case_insensitive {
135                Some(a.to_lowercase().cmp(&b.to_lowercase()))
136            } else {
137                Some(a.as_str().cmp(b))
138            }
139        }
140        (DataValue::InternedString(a), DataValue::String(b)) => {
141            // Try date comparison first
142            if let (Ok(a_dt), Ok(b_dt)) = (parse_datetime(a.as_str()), parse_datetime(b)) {
143                return Some(a_dt.cmp(&b_dt));
144            }
145
146            // String comparison
147            if case_insensitive {
148                Some(a.to_lowercase().cmp(&b.to_lowercase()))
149            } else {
150                Some(a.as_str().cmp(b))
151            }
152        }
153
154        // DateTime vs String comparisons
155        (DataValue::DateTime(dt), DataValue::String(s)) => {
156            match (parse_datetime(dt), parse_datetime(s)) {
157                (Ok(dt_val), Ok(s_val)) => Some(dt_val.cmp(&s_val)),
158                _ => None,
159            }
160        }
161        (DataValue::String(s), DataValue::DateTime(dt)) => {
162            match (parse_datetime(s), parse_datetime(dt)) {
163                (Ok(s_val), Ok(dt_val)) => Some(s_val.cmp(&dt_val)),
164                _ => None,
165            }
166        }
167        (DataValue::DateTime(dt), DataValue::InternedString(s)) => {
168            match (parse_datetime(dt), parse_datetime(s.as_str())) {
169                (Ok(dt_val), Ok(s_val)) => Some(dt_val.cmp(&s_val)),
170                _ => None,
171            }
172        }
173        (DataValue::InternedString(s), DataValue::DateTime(dt)) => {
174            match (parse_datetime(s.as_str()), parse_datetime(dt)) {
175                (Ok(s_val), Ok(dt_val)) => Some(s_val.cmp(&dt_val)),
176                _ => None,
177            }
178        }
179
180        // Cross-type comparisons with coercion
181
182        // Boolean vs String
183        (DataValue::Boolean(a), DataValue::String(b))
184        | (DataValue::String(b), DataValue::Boolean(a)) => {
185            // Try to parse string as boolean
186            if let Some(b_bool) = parse_bool(b) {
187                Some(a.cmp(&b_bool))
188            } else {
189                // Fall back to string comparison
190                let a_str = a.to_string();
191                if case_insensitive {
192                    Some(a_str.to_lowercase().cmp(&b.to_lowercase()))
193                } else {
194                    Some(a_str.cmp(b))
195                }
196            }
197        }
198
199        // Boolean vs Integer (0/1 as false/true)
200        (DataValue::Boolean(a), DataValue::Integer(b))
201        | (DataValue::Integer(b), DataValue::Boolean(a)) => {
202            match *b {
203                0 => Some(a.cmp(&false)),
204                1 => Some(a.cmp(&true)),
205                _ => None, // Other integers don't compare to booleans
206            }
207        }
208
209        // String vs Numeric
210        (DataValue::String(a), DataValue::Integer(b))
211        | (DataValue::Integer(b), DataValue::String(a)) => {
212            if let Ok(a_int) = a.parse::<i64>() {
213                Some(a_int.cmp(b))
214            } else {
215                None
216            }
217        }
218        (DataValue::String(a), DataValue::Float(b))
219        | (DataValue::Float(b), DataValue::String(a)) => {
220            if let Ok(a_float) = a.parse::<f64>() {
221                if b.is_nan() {
222                    Some(Ordering::Less)
223                } else {
224                    a_float.partial_cmp(b)
225                }
226            } else {
227                None
228            }
229        }
230
231        // InternedString vs Numeric
232        (DataValue::InternedString(a), DataValue::Integer(b))
233        | (DataValue::Integer(b), DataValue::InternedString(a)) => {
234            if let Ok(a_int) = a.parse::<i64>() {
235                Some(a_int.cmp(b))
236            } else {
237                None
238            }
239        }
240        (DataValue::InternedString(a), DataValue::Float(b))
241        | (DataValue::Float(b), DataValue::InternedString(a)) => {
242            if let Ok(a_float) = a.parse::<f64>() {
243                if b.is_nan() {
244                    Some(Ordering::Less)
245                } else {
246                    a_float.partial_cmp(b)
247                }
248            } else {
249                None
250            }
251        }
252
253        // InternedString vs Boolean
254        (DataValue::Boolean(a), DataValue::InternedString(b)) => {
255            if let Some(b_bool) = parse_bool(b.as_str()) {
256                Some(a.cmp(&b_bool))
257            } else {
258                let a_str = a.to_string();
259                if case_insensitive {
260                    Some(a_str.to_lowercase().cmp(&b.to_lowercase()))
261                } else {
262                    Some(a_str.as_str().cmp(b.as_str()))
263                }
264            }
265        }
266        (DataValue::InternedString(a), DataValue::Boolean(b)) => {
267            if let Some(a_bool) = parse_bool(a.as_str()) {
268                Some(a_bool.cmp(b))
269            } else {
270                let b_str = b.to_string();
271                if case_insensitive {
272                    Some(a.to_lowercase().cmp(&b_str.to_lowercase()))
273                } else {
274                    Some(a.as_str().cmp(&b_str))
275                }
276            }
277        }
278
279        // Default: types don't compare
280        _ => None,
281    }
282}
283
284/// Helper to evaluate a comparison for WHERE clauses
285/// Handles the special case of comparing a DataValue from the table with a literal value
286pub fn compare_value_with_literal(
287    table_value: &DataValue,
288    op: &str,
289    literal: &str,
290    case_insensitive: bool,
291) -> bool {
292    // Parse the literal into the most appropriate DataValue type
293    let literal_value = parse_literal(literal);
294    compare_with_op(table_value, &literal_value, op, case_insensitive)
295}
296
297/// Parse a literal string from SQL into a DataValue
298fn parse_literal(literal: &str) -> DataValue {
299    // Try boolean literals
300    if let Some(b) = parse_bool(literal) {
301        return DataValue::Boolean(b);
302    }
303
304    // Try integer
305    if let Ok(i) = literal.parse::<i64>() {
306        return DataValue::Integer(i);
307    }
308
309    // Try float
310    if let Ok(f) = literal.parse::<f64>() {
311        return DataValue::Float(f);
312    }
313
314    // Try date/time
315    if let Ok(dt) = parse_datetime(literal) {
316        return DataValue::DateTime(dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string());
317    }
318
319    // Default to string
320    DataValue::String(literal.to_string())
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_boolean_comparisons() {
329        let true_val = DataValue::Boolean(true);
330        let false_val = DataValue::Boolean(false);
331
332        assert!(values_equal(&true_val, &true_val, false));
333        assert!(!values_equal(&true_val, &false_val, false));
334        assert!(compare_with_op(&true_val, &false_val, ">", false));
335    }
336
337    #[test]
338    fn test_boolean_string_coercion() {
339        let bool_val = DataValue::Boolean(true);
340        let str_val = DataValue::String("true".to_string());
341        let num_str = DataValue::String("1".to_string());
342
343        assert!(values_equal(&bool_val, &str_val, false));
344        assert!(values_equal(&bool_val, &num_str, false));
345    }
346
347    #[test]
348    fn test_boolean_integer_coercion() {
349        let bool_true = DataValue::Boolean(true);
350        let bool_false = DataValue::Boolean(false);
351        let one = DataValue::Integer(1);
352        let zero = DataValue::Integer(0);
353
354        assert!(values_equal(&bool_true, &one, false));
355        assert!(values_equal(&bool_false, &zero, false));
356    }
357
358    #[test]
359    fn test_case_insensitive_strings() {
360        let upper = DataValue::String("HELLO".to_string());
361        let lower = DataValue::String("hello".to_string());
362
363        assert!(!values_equal(&upper, &lower, false));
364        assert!(values_equal(&upper, &lower, true));
365    }
366}