Skip to main content

helios_persistence/backends/sqlite/search/parameter_handlers/
number.rs

1//! Number parameter SQL handler.
2
3use crate::types::{SearchPrefix, SearchValue};
4
5use super::super::query_builder::{SqlFragment, SqlParam};
6
7/// Handles number parameter SQL generation.
8pub struct NumberHandler;
9
10impl NumberHandler {
11    /// Builds SQL for a number parameter value.
12    ///
13    /// Supports all comparison prefixes: eq, ne, gt, lt, ge, le, ap.
14    pub fn build_sql(value: &SearchValue, param_offset: usize) -> SqlFragment {
15        let param_num = param_offset + 1;
16
17        // Parse the number value
18        let num_value: f64 = match value.value.parse() {
19            Ok(v) => v,
20            Err(_) => {
21                // Invalid number - return impossible condition
22                return SqlFragment::new("1 = 0");
23            }
24        };
25
26        match value.prefix {
27            SearchPrefix::Eq => Self::build_equals(num_value, param_num),
28            SearchPrefix::Ne => Self::build_not_equals(num_value, param_num),
29            SearchPrefix::Gt => Self::build_greater_than(num_value, param_num),
30            SearchPrefix::Lt => Self::build_less_than(num_value, param_num),
31            SearchPrefix::Ge => Self::build_greater_equal(num_value, param_num),
32            SearchPrefix::Le => Self::build_less_equal(num_value, param_num),
33            SearchPrefix::Sa => Self::build_greater_than(num_value, param_num), // Same as gt for numbers
34            SearchPrefix::Eb => Self::build_less_than(num_value, param_num), // Same as lt for numbers
35            SearchPrefix::Ap => Self::build_approximately(num_value, param_num),
36        }
37    }
38
39    /// Equality - exact match with implicit precision.
40    ///
41    /// For numbers like "100", this matches values in range [99.5, 100.5).
42    fn build_equals(value: f64, param_num: usize) -> SqlFragment {
43        // Determine precision from the value
44        let precision = Self::get_implicit_precision(value);
45        let half_precision = precision / 2.0;
46
47        SqlFragment::with_params(
48            format!(
49                "value_number >= ?{} AND value_number < ?{}",
50                param_num,
51                param_num + 1
52            ),
53            vec![
54                SqlParam::float(value - half_precision),
55                SqlParam::float(value + half_precision),
56            ],
57        )
58    }
59
60    /// Not equals.
61    fn build_not_equals(value: f64, param_num: usize) -> SqlFragment {
62        let precision = Self::get_implicit_precision(value);
63        let half_precision = precision / 2.0;
64
65        SqlFragment::with_params(
66            format!(
67                "(value_number < ?{} OR value_number >= ?{})",
68                param_num,
69                param_num + 1
70            ),
71            vec![
72                SqlParam::float(value - half_precision),
73                SqlParam::float(value + half_precision),
74            ],
75        )
76    }
77
78    /// Greater than.
79    fn build_greater_than(value: f64, param_num: usize) -> SqlFragment {
80        SqlFragment::with_params(
81            format!("value_number > ?{}", param_num),
82            vec![SqlParam::float(value)],
83        )
84    }
85
86    /// Less than.
87    fn build_less_than(value: f64, param_num: usize) -> SqlFragment {
88        SqlFragment::with_params(
89            format!("value_number < ?{}", param_num),
90            vec![SqlParam::float(value)],
91        )
92    }
93
94    /// Greater than or equal.
95    fn build_greater_equal(value: f64, param_num: usize) -> SqlFragment {
96        SqlFragment::with_params(
97            format!("value_number >= ?{}", param_num),
98            vec![SqlParam::float(value)],
99        )
100    }
101
102    /// Less than or equal.
103    fn build_less_equal(value: f64, param_num: usize) -> SqlFragment {
104        SqlFragment::with_params(
105            format!("value_number <= ?{}", param_num),
106            vec![SqlParam::float(value)],
107        )
108    }
109
110    /// Approximately equals - +/- 10%.
111    fn build_approximately(value: f64, param_num: usize) -> SqlFragment {
112        let margin = (value.abs() * 0.1).max(0.0001); // At least 0.0001 for very small numbers
113
114        SqlFragment::with_params(
115            format!("value_number BETWEEN ?{} AND ?{}", param_num, param_num + 1),
116            vec![
117                SqlParam::float(value - margin),
118                SqlParam::float(value + margin),
119            ],
120        )
121    }
122
123    /// Gets the implicit precision of a number.
124    ///
125    /// For "100", precision is 1. For "100.0", precision is 0.1. For "100.00", precision is 0.01.
126    fn get_implicit_precision(value: f64) -> f64 {
127        // Determine precision from string representation
128        let s = value.to_string();
129        if let Some(dot_pos) = s.find('.') {
130            let decimal_places = s.len() - dot_pos - 1;
131            10_f64.powi(-(decimal_places as i32))
132        } else {
133            1.0
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_number_eq() {
144        let value = SearchValue::new(SearchPrefix::Eq, "100");
145        let frag = NumberHandler::build_sql(&value, 0);
146
147        assert!(frag.sql.contains(">="));
148        assert!(frag.sql.contains("<"));
149        assert_eq!(frag.params.len(), 2);
150    }
151
152    #[test]
153    fn test_number_gt() {
154        let value = SearchValue::new(SearchPrefix::Gt, "100");
155        let frag = NumberHandler::build_sql(&value, 0);
156
157        assert!(frag.sql.contains("> ?1"));
158        assert_eq!(frag.params.len(), 1);
159    }
160
161    #[test]
162    fn test_number_le() {
163        let value = SearchValue::new(SearchPrefix::Le, "100");
164        let frag = NumberHandler::build_sql(&value, 0);
165
166        assert!(frag.sql.contains("<= ?1"));
167    }
168
169    #[test]
170    fn test_number_ap() {
171        let value = SearchValue::new(SearchPrefix::Ap, "100");
172        let frag = NumberHandler::build_sql(&value, 0);
173
174        assert!(frag.sql.contains("BETWEEN"));
175        assert_eq!(frag.params.len(), 2);
176    }
177
178    #[test]
179    fn test_invalid_number() {
180        let value = SearchValue::new(SearchPrefix::Eq, "not-a-number");
181        let frag = NumberHandler::build_sql(&value, 0);
182
183        assert!(frag.sql.contains("1 = 0"));
184    }
185}