Skip to main content

trailbase_qs/
filter.rs

1use itertools::Itertools;
2/// Custom deserializers for urlencoded filters.
3///
4/// Supported examples:
5///
6/// filters[column]=value  <-- might be nice otherwise below.
7/// filter[column]=value
8/// filters[column][eq]=value
9/// filters[and][0][column0][eq]=value0&filters[and][1][column1][eq]=value1
10/// filters[and][0][or][0][column0]=value0&[and][0][or][1][column1]=value1
11use std::collections::BTreeMap;
12
13use crate::column_rel_value::{ColumnOpValue, CompareOp, serde_value_to_single_column_rel_value};
14use crate::value::Value;
15
16#[derive(Clone, Debug, PartialEq)]
17pub enum Combiner {
18  And,
19  Or,
20}
21
22#[derive(Clone, Debug, PartialEq)]
23pub enum ValueOrComposite {
24  Value(ColumnOpValue),
25  Composite(Combiner, Vec<ValueOrComposite>),
26}
27
28impl ValueOrComposite {
29  /// Returns SQL query, and a list of (param_name, param_value).
30  pub fn into_sql<V, E>(
31    self,
32    column_prefix: Option<&str>,
33    convert: &dyn Fn(&str, Value) -> Result<V, E>,
34  ) -> Result<(String, Vec<(String, V)>), E> {
35    fn render_value<V, E>(
36      column_op_value: ColumnOpValue,
37      column_prefix: Option<&str>,
38      convert: &dyn Fn(&str, Value) -> Result<V, E>,
39      index: &mut usize,
40    ) -> Result<(String, Option<(String, V)>), E> {
41      let v = column_op_value.value;
42      let c = column_op_value.column;
43
44      return match column_op_value.op {
45        CompareOp::Is => {
46          debug_assert!(matches!(v, Value::String(_)), "{v:?}");
47
48          Ok(match column_prefix {
49            Some(p) => (format!(r#"{p}."{c}" IS {v}"#), None),
50            None => (format!(r#""{c}" IS {v}"#), None),
51          })
52        }
53        op => {
54          let param = param_name(*index);
55          *index += 1;
56
57          Ok(match column_prefix {
58            Some(p) => (
59              format!(r#"{p}."{c}" {o} {param}"#, o = op.as_sql()),
60              Some((param, convert(&c, v)?)),
61            ),
62            None => (
63              format!(r#""{c}" {o} {param}"#, o = op.as_sql()),
64              Some((param, convert(&c, v)?)),
65            ),
66          })
67        }
68      };
69    }
70
71    fn recurse<V, E>(
72      v: ValueOrComposite,
73      column_prefix: Option<&str>,
74      convert: &dyn Fn(&str, Value) -> Result<V, E>,
75      index: &mut usize,
76    ) -> Result<(String, Vec<(String, V)>), E> {
77      match v {
78        ValueOrComposite::Value(v) => {
79          return Ok(match render_value(v, column_prefix, convert, index)? {
80            (sql, Some(param)) => (sql, vec![param]),
81            (sql, None) => (sql, vec![]),
82          });
83        }
84        ValueOrComposite::Composite(combiner, vec) => {
85          let mut fragments = Vec::<String>::with_capacity(vec.len());
86          let mut params = Vec::<(String, V)>::with_capacity(vec.len());
87
88          for value_or_composite in vec {
89            let (f, p) = recurse(value_or_composite, column_prefix, convert, index)?;
90            fragments.push(f);
91            params.extend(p);
92          }
93
94          let fragment = format!(
95            "({})",
96            fragments.join(match combiner {
97              Combiner::And => " AND ",
98              Combiner::Or => " OR ",
99            }),
100          );
101          return Ok((fragment, params));
102        }
103      };
104    }
105
106    let mut index: usize = 0;
107    return recurse(self, column_prefix, convert, &mut index);
108  }
109
110  /// Return a query-string fragment for this filter (no leading '&').
111  pub fn to_query(&self) -> String {
112    /// Return a (key, value) pair suitable for query-string serialization (not percent-encoded).
113    fn render_value(prefix: &str, v: &ColumnOpValue) -> String {
114      let value: std::borrow::Cow<str> = match (&v.op, &v.value) {
115        (CompareOp::Is, Value::String(s)) if s == "NOT NULL" => "!NULL".into(),
116        (CompareOp::Is, Value::String(s)) if s == "NULL" => "NULL".into(),
117        (_, Value::String(s)) => s.into(),
118        (_, Value::Integer(i)) => i.to_string().into(),
119        (_, Value::Double(d)) => d.to_string().into(),
120      };
121
122      let column = &v.column;
123      return if matches!(v.op, CompareOp::Equal) {
124        format!("{prefix}[{column}]={value}")
125      } else {
126        format!("{prefix}[{column}][{}]={value}", v.op.as_query())
127      };
128    }
129
130    fn recurse(v: &ValueOrComposite, prefix: &str) -> Vec<String> {
131      return match v {
132        ValueOrComposite::Value(v) => vec![render_value(prefix, v)],
133        ValueOrComposite::Composite(combiner, vec) => {
134          let comb = match combiner {
135            Combiner::And => "$and",
136            Combiner::Or => "$or",
137          };
138
139          vec
140            .iter()
141            .enumerate()
142            .flat_map(|(i, el)| recurse(el, &format!("{}[{}][{}]", prefix, comb, i)))
143            .collect()
144        }
145      };
146    }
147
148    return recurse(self, "filter").into_iter().join("&");
149  }
150}
151
152fn serde_value_to_value_or_composite<'de, D>(
153  value: serde_value::Value,
154  depth: usize,
155) -> Result<ValueOrComposite, D::Error>
156where
157  D: serde::de::Deserializer<'de>,
158{
159  use serde::de::Error;
160  use serde_value::Value;
161
162  // Limit recursion depth
163  if depth >= 5 {
164    return Err(Error::custom("Recursion limit exceeded"));
165  }
166
167  // We always expect [key] = value, i.e. a Map[key] = value.
168  let Value::Map(mut m) = value else {
169    return Err(Error::invalid_type(
170      crate::util::unexpected(&value),
171      &"[($and|$or)][index][<nested>] or [column_name][$op]=value",
172    ));
173  };
174
175  return match m.len() {
176    0 => Ok(ValueOrComposite::Composite(Combiner::And, vec![])),
177    1 => {
178      let first = m.pop_first().expect("len == 1");
179      let (Value::String(key), v) = first else {
180        return Err(Error::invalid_type(
181          crate::util::unexpected(&first.0),
182          &"String",
183        ));
184      };
185
186      match (key.as_str(), v) {
187        // Recursive cases.
188        ("$and", Value::Seq(values)) => combine::<D>(Combiner::And, values, depth),
189        ("$and", v) => Err(Error::invalid_type(
190          crate::util::unexpected(&v),
191          &"sequence",
192        )),
193        ("$or", Value::Seq(values)) => combine::<D>(Combiner::Or, values, depth),
194        ("$or", v) => Err(Error::invalid_type(
195          crate::util::unexpected(&v),
196          &"sequence",
197        )),
198        // Single column_name but multiple values, i.e. multiple filters on the same col.
199        (col_name, Value::Map(m)) if m.len() > 1 => Ok(ValueOrComposite::Composite(
200          Combiner::And,
201          m.into_iter()
202            .map(|(key, value)| {
203              return Ok(ValueOrComposite::Value(
204                serde_value_to_single_column_rel_value::<D>(
205                  col_name.to_string(),
206                  Value::Map(BTreeMap::from([(key, value)])),
207                )?,
208              ));
209            })
210            .collect::<Result<Vec<_>, _>>()?,
211        )),
212        // For any other string-type key, turn into a single value.
213        (_key, v) => Ok(ValueOrComposite::Value(
214          serde_value_to_single_column_rel_value::<D>(key, v)?,
215        )),
216      }
217    }
218    // Multiple different keys on the same same level, i.e. no explicit grouping by a single key
219    // like "$and" or "$or" => Implicit AND composite.
220    _n => combine::<D>(
221      Combiner::And,
222      m.into_iter()
223        .map(|(k, v)| Value::Map(BTreeMap::from([(k, v)]))),
224      depth,
225    ),
226  };
227}
228
229/// Recursively combine nested filter expressions.
230fn combine<'de, D>(
231  combiner: Combiner,
232  values: impl IntoIterator<Item = serde_value::Value>,
233  depth: usize,
234) -> Result<ValueOrComposite, D::Error>
235where
236  D: serde::de::Deserializer<'de>,
237{
238  return Ok(ValueOrComposite::Composite(
239    combiner,
240    values
241      .into_iter()
242      .map(|v| serde_value_to_value_or_composite::<D>(v, depth + 1))
243      .collect::<Result<Vec<_>, _>>()?,
244  ));
245}
246
247impl<'de> serde::de::Deserialize<'de> for ValueOrComposite {
248  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
249  where
250    D: serde::de::Deserializer<'de>,
251  {
252    return serde_value_to_value_or_composite::<D>(
253      serde_value::Value::deserialize(deserializer)?,
254      0,
255    );
256  }
257}
258
259#[inline]
260fn param_name(index: usize) -> String {
261  let mut s = String::with_capacity(10);
262  s.push_str(":__p");
263  s.push_str(&index.to_string());
264  return s;
265}
266
267#[cfg(test)]
268mod tests {
269  use super::*;
270
271  use rusqlite::types::Value as SqlValue;
272  use serde_qs::Config;
273
274  use crate::column_rel_value::{ColumnOpValue, CompareOp};
275  use crate::query::FilterQuery as Query;
276  use crate::value::Value;
277
278  #[test]
279  fn test_filter_parsing() {
280    let qs = Config::new(5, false);
281
282    let m_empty: Query = qs.deserialize_str("").unwrap();
283    assert_eq!(m_empty.filter, None);
284
285    let q0 = "filter[$and][0][col0]=val0";
286    let f0 = qs.deserialize_str::<Query>(q0).unwrap().filter.unwrap();
287    assert_eq!(
288      f0,
289      ValueOrComposite::Composite(
290        Combiner::And,
291        vec![ValueOrComposite::Value(ColumnOpValue {
292          column: "col0".to_string(),
293          op: CompareOp::Equal,
294          value: Value::String("val0".to_string()),
295        })]
296      ),
297    );
298    assert_eq!(q0, f0.to_query());
299
300    let q1 = "filter[$and][0][col0]=val0&filter[$and][1][col1]=val1";
301    let f1 = qs.deserialize_str::<Query>(q1).unwrap().filter.unwrap();
302    assert_eq!(
303      f1,
304      ValueOrComposite::Composite(
305        Combiner::And,
306        vec![
307          ValueOrComposite::Value(ColumnOpValue {
308            column: "col0".to_string(),
309            op: CompareOp::Equal,
310            value: Value::String("val0".to_string()),
311          }),
312          ValueOrComposite::Value(ColumnOpValue {
313            column: "col1".to_string(),
314            op: CompareOp::Equal,
315            value: Value::String("val1".to_string()),
316          }),
317        ]
318      )
319    );
320    assert_eq!(q1, f1.to_query());
321
322    let q3 = "filter[col0][$is]=!NULL";
323    let f3 = qs.deserialize_str::<Query>(q3).unwrap().filter.unwrap();
324    assert_eq!(
325      f3,
326      ValueOrComposite::Value(ColumnOpValue {
327        column: "col0".to_string(),
328        op: CompareOp::Is,
329        value: Value::String("NOT NULL".to_string()),
330      })
331    );
332    assert_eq!(q3, f3.to_query());
333
334    // Combiners with only one element each.
335    let m = qs.deserialize_str::<Query>("filter[$and][0][col0]=val0&filter[$or][1][col1]=val1");
336    assert!(m.is_ok(), "M: {m:?}");
337
338    // test implicit $and.
339    let q2 = "filter[col0]=val0&filter[col1]=val1";
340    let f2 = qs.deserialize_str::<Query>(q2).unwrap().filter.unwrap();
341
342    assert_eq!(
343      f2,
344      ValueOrComposite::Composite(
345        Combiner::And,
346        vec![
347          ValueOrComposite::Value(ColumnOpValue {
348            column: "col0".to_string(),
349            op: CompareOp::Equal,
350            value: Value::String("val0".to_string()),
351          }),
352          ValueOrComposite::Value(ColumnOpValue {
353            column: "col1".to_string(),
354            op: CompareOp::Equal,
355            value: Value::String("val1".to_string()),
356          }),
357        ]
358      )
359    );
360
361    let q2_explicit = "filter[$and][0][col0]=val0&filter[$and][1][col1]=val1";
362    let f2_explicit = qs
363      .deserialize_str::<Query>(q2_explicit)
364      .unwrap()
365      .filter
366      .unwrap();
367
368    assert_eq!(f2_explicit, f2);
369    assert_eq!(q2_explicit, f2.to_query());
370  }
371
372  #[test]
373  fn test_filter_to_sql() {
374    let v0 = ValueOrComposite::Value(ColumnOpValue {
375      column: "col0".to_string(),
376      op: CompareOp::Equal,
377      value: Value::String("val0".to_string()),
378    });
379
380    let convert = |_: &str, value: Value| -> Result<SqlValue, String> {
381      return Ok(match value {
382        Value::String(s) => SqlValue::Text(s),
383        Value::Integer(i) => SqlValue::Integer(i),
384        Value::Double(d) => SqlValue::Real(d),
385      });
386    };
387
388    let sql0 = v0
389      .clone()
390      .into_sql(/* column_prefix= */ None, &convert)
391      .unwrap();
392    assert_eq!(sql0.0, r#""col0" = :__p0"#);
393    let sql0 = v0
394      .into_sql(/* column_prefix= */ Some("p"), &convert)
395      .unwrap();
396    assert_eq!(sql0.0, r#"p."col0" = :__p0"#);
397
398    let v1 = ValueOrComposite::Value(ColumnOpValue {
399      column: "col0".to_string(),
400      op: CompareOp::Is,
401      value: Value::String("NULL".to_string()),
402    });
403    let sql1 = v1.into_sql(None, &convert).unwrap();
404    assert_eq!(sql1.0, r#""col0" IS NULL"#, "{sql1:?}",);
405  }
406}