Skip to main content

veloq_query/sql/
fragment.rs

1use duckdb::types::Value;
2
3/// SQL text plus the positional parameters introduced by that text.
4#[derive(Debug, Clone, Default, PartialEq)]
5pub struct SqlFragment {
6    pub sql: String,
7    pub params: Vec<Value>,
8}
9
10impl SqlFragment {
11    pub fn new(sql: impl Into<String>, params: Vec<Value>) -> Self {
12        Self {
13            sql: sql.into(),
14            params,
15        }
16    }
17}
18
19/// WHERE-fragment accumulator that keeps predicates and bind values in
20/// the same order they are introduced.
21#[derive(Debug, Clone, Default, PartialEq)]
22pub struct SqlFilter {
23    parts: Vec<String>,
24    params: Vec<Value>,
25}
26
27impl SqlFilter {
28    pub fn push_predicate(&mut self, sql: impl Into<String>) {
29        self.parts.push(sql.into());
30    }
31
32    pub fn push_param(&mut self, value: Value) {
33        self.params.push(value);
34    }
35
36    pub fn push_fragment(&mut self, fragment: SqlFragment) {
37        if !fragment.sql.is_empty() {
38            self.parts.push(fragment.sql);
39        }
40        self.params.extend(fragment.params);
41    }
42
43    pub fn where_clause(&self) -> String {
44        where_clause(&self.parts)
45    }
46
47    pub fn into_params(self) -> Vec<Value> {
48        self.params
49    }
50}
51
52pub fn where_clause(predicates: &[String]) -> String {
53    if predicates.is_empty() {
54        String::new()
55    } else {
56        format!("WHERE {}", predicates.join(" AND "))
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn filter_keeps_predicate_and_param_order() {
66        let mut filter = SqlFilter::default();
67        filter.push_fragment(SqlFragment::new("start < ?", vec![Value::BigInt(20)]));
68        filter.push_predicate("device_id = ?");
69        filter.push_param(Value::Int(3));
70
71        assert_eq!(filter.where_clause(), "WHERE start < ? AND device_id = ?");
72        assert_eq!(
73            filter.clone().into_params(),
74            vec![Value::BigInt(20), Value::Int(3)]
75        );
76    }
77
78    #[test]
79    fn empty_filter_has_empty_where_clause() {
80        let filter = SqlFilter::default();
81        assert_eq!(filter.where_clause(), "");
82        assert!(filter.into_params().is_empty());
83    }
84
85    #[test]
86    fn where_clause_joins_predicates_or_empty() {
87        assert_eq!(where_clause(&[]), "");
88        assert_eq!(
89            where_clause(&["a = ?".to_string(), "b > ?".to_string()]),
90            "WHERE a = ? AND b > ?"
91        );
92    }
93}