Skip to main content

prax_query/inputs/
scalar.rs

1//! Reusable scalar filter wrappers shared by every generated `*WhereInput`.
2//!
3//! Each wrapper is a struct of `Option`-fields, one per operator. Empty
4//! wrappers (all fields `None`) lower to `Filter::None`. Multiple set
5//! fields AND-combine. `From<scalar>` impls support the macro shorthand
6//! `email: "alice@x.com"` => `StringFilter { equals: Some("..."), .. }`.
7//!
8//! Every wrapper implements [`ScalarFilter`], whose `into_filter`
9//! method takes the column name (which the parent `WhereInput` knows)
10//! and produces a runtime [`Filter`].
11
12use crate::filter::{Filter, FilterValue};
13use serde::{Deserialize, Serialize};
14
15/// Helper trait implemented by every scalar filter wrapper.
16///
17/// The wrapper itself doesn't know its column name — the parent
18/// `WhereInput::into_ir` impl threads the column in when lowering.
19pub trait ScalarFilter {
20    /// Lower this scalar filter to a runtime [`Filter`] keyed by
21    /// the given column name.
22    fn into_filter(self, column: &str) -> Filter;
23}
24
25/// Collapse a list of operator filters into a single [`Filter`].
26///
27/// Every `ScalarFilter::into_filter` impl accumulates one entry per
28/// active operator and then needs to reduce that list to a `Filter`.
29/// The reduction is identical across all of them:
30/// - empty → `Filter::None`
31/// - single → that filter unwrapped
32/// - multiple → `Filter::and(parts)`.
33pub(crate) fn combine_filters(parts: Vec<Filter>) -> Filter {
34    match parts.len() {
35        0 => Filter::None,
36        1 => parts.into_iter().next().unwrap(),
37        _ => Filter::and(parts),
38    }
39}
40
41/// Comparison mode for string filters.
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
43pub enum QueryMode {
44    /// Default (case-sensitive) comparison.
45    #[default]
46    Default,
47    /// Case-insensitive comparison. Requires `SupportsCaseInsensitiveMode`
48    /// for engines that don't fall back to `LOWER(...)`.
49    Insensitive,
50}
51
52/// Filter operators for a non-nullable `String` column.
53#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub struct StringFilter {
56    /// `column = value`
57    pub equals: Option<String>,
58    /// Negation of the inner filter.
59    pub not: Option<Box<StringFilter>>,
60    /// `column IN (...)`
61    pub in_list: Option<Vec<String>>,
62    /// `column NOT IN (...)`
63    pub not_in: Option<Vec<String>>,
64    /// `column < value`
65    pub lt: Option<String>,
66    /// `column <= value`
67    pub lte: Option<String>,
68    /// `column > value`
69    pub gt: Option<String>,
70    /// `column >= value`
71    pub gte: Option<String>,
72    /// `column LIKE %value%`
73    pub contains: Option<String>,
74    /// `column LIKE value%`
75    pub starts_with: Option<String>,
76    /// `column LIKE %value`
77    pub ends_with: Option<String>,
78    /// Comparison mode (case sensitivity).
79    pub mode: Option<QueryMode>,
80}
81
82impl StringFilter {
83    /// `equals: Some(value)`.
84    pub fn equals(v: impl Into<String>) -> Self {
85        Self {
86            equals: Some(v.into()),
87            ..Default::default()
88        }
89    }
90    /// `contains: Some(value)`.
91    pub fn contains(v: impl Into<String>) -> Self {
92        Self {
93            contains: Some(v.into()),
94            ..Default::default()
95        }
96    }
97    /// `starts_with: Some(value)`.
98    pub fn starts_with(v: impl Into<String>) -> Self {
99        Self {
100            starts_with: Some(v.into()),
101            ..Default::default()
102        }
103    }
104    /// `ends_with: Some(value)`.
105    pub fn ends_with(v: impl Into<String>) -> Self {
106        Self {
107            ends_with: Some(v.into()),
108            ..Default::default()
109        }
110    }
111}
112
113impl From<&str> for StringFilter {
114    fn from(v: &str) -> Self {
115        Self::equals(v)
116    }
117}
118impl From<String> for StringFilter {
119    fn from(v: String) -> Self {
120        Self::equals(v)
121    }
122}
123
124impl ScalarFilter for StringFilter {
125    fn into_filter(self, column: &str) -> Filter {
126        let mut parts: Vec<Filter> = Vec::new();
127        let col = column.to_string();
128        if let Some(v) = self.equals {
129            parts.push(Filter::Equals(col.clone().into(), FilterValue::String(v)));
130        }
131        if let Some(boxed) = self.not {
132            let inner = boxed.into_filter(column);
133            parts.push(Filter::Not(Box::new(inner)));
134        }
135        if let Some(values) = self.in_list {
136            let vs: Vec<FilterValue> = values.into_iter().map(FilterValue::String).collect();
137            parts.push(Filter::In(col.clone().into(), vs));
138        }
139        if let Some(values) = self.not_in {
140            let vs: Vec<FilterValue> = values.into_iter().map(FilterValue::String).collect();
141            parts.push(Filter::NotIn(col.clone().into(), vs));
142        }
143        if let Some(v) = self.lt {
144            parts.push(Filter::Lt(col.clone().into(), FilterValue::String(v)));
145        }
146        if let Some(v) = self.lte {
147            parts.push(Filter::Lte(col.clone().into(), FilterValue::String(v)));
148        }
149        if let Some(v) = self.gt {
150            parts.push(Filter::Gt(col.clone().into(), FilterValue::String(v)));
151        }
152        if let Some(v) = self.gte {
153            parts.push(Filter::Gte(col.clone().into(), FilterValue::String(v)));
154        }
155        if let Some(v) = self.contains {
156            parts.push(Filter::Contains(col.clone().into(), FilterValue::String(v)));
157        }
158        if let Some(v) = self.starts_with {
159            parts.push(Filter::StartsWith(
160                col.clone().into(),
161                FilterValue::String(v),
162            ));
163        }
164        if let Some(v) = self.ends_with {
165            parts.push(Filter::EndsWith(col.clone().into(), FilterValue::String(v)));
166        }
167        // `mode` is honored by the dialect layer in phase 2+; phase 1 ignores
168        // it here. The field is kept so downstream phases don't need a
169        // breaking-shape change.
170        let _ = self.mode;
171        combine_filters(parts)
172    }
173}
174
175/// Filter operators for a nullable `String` column.
176#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
177#[serde(rename_all = "snake_case")]
178pub struct StringNullableFilter {
179    /// `column = value`
180    pub equals: Option<String>,
181    /// Negation of the inner filter.
182    pub not: Option<Box<StringNullableFilter>>,
183    /// `column IN (...)`
184    pub in_list: Option<Vec<String>>,
185    /// `column NOT IN (...)`
186    pub not_in: Option<Vec<String>>,
187    /// `column < value`
188    pub lt: Option<String>,
189    /// `column <= value`
190    pub lte: Option<String>,
191    /// `column > value`
192    pub gt: Option<String>,
193    /// `column >= value`
194    pub gte: Option<String>,
195    /// `column LIKE %value%`
196    pub contains: Option<String>,
197    /// `column LIKE value%`
198    pub starts_with: Option<String>,
199    /// `column LIKE %value`
200    pub ends_with: Option<String>,
201    /// Comparison mode.
202    pub mode: Option<QueryMode>,
203    /// `is_null: Some(true)` => `IS NULL`; `Some(false)` => `IS NOT NULL`.
204    pub is_null: Option<bool>,
205}
206
207impl From<&str> for StringNullableFilter {
208    fn from(v: &str) -> Self {
209        Self {
210            equals: Some(v.into()),
211            ..Default::default()
212        }
213    }
214}
215impl From<String> for StringNullableFilter {
216    fn from(v: String) -> Self {
217        Self {
218            equals: Some(v),
219            ..Default::default()
220        }
221    }
222}
223
224impl ScalarFilter for StringNullableFilter {
225    fn into_filter(self, column: &str) -> Filter {
226        let mut parts: Vec<Filter> = Vec::new();
227        let col = column.to_string();
228        if let Some(b) = self.is_null {
229            parts.push(if b {
230                Filter::IsNull(col.clone().into())
231            } else {
232                Filter::IsNotNull(col.clone().into())
233            });
234        }
235        // Reuse StringFilter's lowering for the remaining ops.
236        let inner = StringFilter {
237            equals: self.equals,
238            not: self.not.map(|b| {
239                Box::new(StringFilter {
240                    equals: b.equals,
241                    in_list: b.in_list,
242                    not_in: b.not_in,
243                    lt: b.lt,
244                    lte: b.lte,
245                    gt: b.gt,
246                    gte: b.gte,
247                    contains: b.contains,
248                    starts_with: b.starts_with,
249                    ends_with: b.ends_with,
250                    mode: b.mode,
251                    not: None,
252                })
253            }),
254            in_list: self.in_list,
255            not_in: self.not_in,
256            lt: self.lt,
257            lte: self.lte,
258            gt: self.gt,
259            gte: self.gte,
260            contains: self.contains,
261            starts_with: self.starts_with,
262            ends_with: self.ends_with,
263            mode: self.mode,
264        };
265        let inner_filter = inner.into_filter(column);
266        if !matches!(inner_filter, Filter::None) {
267            parts.push(inner_filter);
268        }
269        combine_filters(parts)
270    }
271}
272
273/// Macro to emit a scalar filter wrapper + nullable counterpart that
274/// lowers to a `FilterValue::$variant`. Keeps the table of integer /
275/// floating / temporal / blob types DRY without sacrificing rustdoc
276/// per-type.
277macro_rules! scalar_filter {
278    (
279        $(#[$nn_meta:meta])*
280        $name:ident<$rust:ty> => |$conv_v:ident| $conv:block,
281        $(#[$null_meta:meta])*
282        nullable $null:ident
283    ) => {
284        $(#[$nn_meta])*
285        #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
286        #[serde(rename_all = "snake_case")]
287        pub struct $name {
288            /// `column = value`
289            pub equals: Option<$rust>,
290            /// Negation.
291            pub not: Option<Box<$name>>,
292            /// `column IN (...)`
293            pub in_list: Option<Vec<$rust>>,
294            /// `column NOT IN (...)`
295            pub not_in: Option<Vec<$rust>>,
296            /// `column < value`
297            pub lt: Option<$rust>,
298            /// `column <= value`
299            pub lte: Option<$rust>,
300            /// `column > value`
301            pub gt: Option<$rust>,
302            /// `column >= value`
303            pub gte: Option<$rust>,
304        }
305
306        impl $name {
307            /// `equals: Some(value)`.
308            pub fn equals(v: impl Into<$rust>) -> Self {
309                Self { equals: Some(v.into()), ..Default::default() }
310            }
311            /// `lt: Some(value)`.
312            pub fn lt(v: impl Into<$rust>) -> Self {
313                Self { lt: Some(v.into()), ..Default::default() }
314            }
315            /// `lte: Some(value)`.
316            pub fn lte(v: impl Into<$rust>) -> Self {
317                Self { lte: Some(v.into()), ..Default::default() }
318            }
319            /// `gt: Some(value)`.
320            pub fn gt(v: impl Into<$rust>) -> Self {
321                Self { gt: Some(v.into()), ..Default::default() }
322            }
323            /// `gte: Some(value)`.
324            pub fn gte(v: impl Into<$rust>) -> Self {
325                Self { gte: Some(v.into()), ..Default::default() }
326            }
327        }
328
329        impl ScalarFilter for $name {
330            fn into_filter(self, column: &str) -> Filter {
331                fn to_fv($conv_v: $rust) -> FilterValue $conv
332                let col: crate::filter::FieldName = column.to_string().into();
333                let mut parts: Vec<Filter> = Vec::new();
334                if let Some(v) = self.equals {
335                    parts.push(Filter::Equals(col.clone(), to_fv(v)));
336                }
337                if let Some(boxed) = self.not {
338                    let inner = boxed.into_filter(column);
339                    parts.push(Filter::Not(Box::new(inner)));
340                }
341                if let Some(values) = self.in_list {
342                    let vs: Vec<FilterValue> = values.into_iter().map(to_fv).collect();
343                    parts.push(Filter::In(col.clone(), vs));
344                }
345                if let Some(values) = self.not_in {
346                    let vs: Vec<FilterValue> = values.into_iter().map(to_fv).collect();
347                    parts.push(Filter::NotIn(col.clone(), vs));
348                }
349                if let Some(v) = self.lt { parts.push(Filter::Lt(col.clone(), to_fv(v))); }
350                if let Some(v) = self.lte { parts.push(Filter::Lte(col.clone(), to_fv(v))); }
351                if let Some(v) = self.gt { parts.push(Filter::Gt(col.clone(), to_fv(v))); }
352                if let Some(v) = self.gte { parts.push(Filter::Gte(col, to_fv(v))); }
353                combine_filters(parts)
354            }
355        }
356
357        $(#[$null_meta])*
358        #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
359        #[serde(rename_all = "snake_case")]
360        pub struct $null {
361            /// `column = value`
362            pub equals: Option<$rust>,
363            /// Negation.
364            pub not: Option<Box<$null>>,
365            /// `column IN (...)`
366            pub in_list: Option<Vec<$rust>>,
367            /// `column NOT IN (...)`
368            pub not_in: Option<Vec<$rust>>,
369            /// `column < value`
370            pub lt: Option<$rust>,
371            /// `column <= value`
372            pub lte: Option<$rust>,
373            /// `column > value`
374            pub gt: Option<$rust>,
375            /// `column >= value`
376            pub gte: Option<$rust>,
377            /// IS NULL / IS NOT NULL.
378            pub is_null: Option<bool>,
379        }
380
381        impl ScalarFilter for $null {
382            fn into_filter(self, column: &str) -> Filter {
383                let mut parts: Vec<Filter> = Vec::new();
384                if let Some(b) = self.is_null {
385                    parts.push(if b {
386                        Filter::IsNull(column.to_string().into())
387                    } else {
388                        Filter::IsNotNull(column.to_string().into())
389                    });
390                }
391                let inner = $name {
392                    equals: self.equals,
393                    not: self.not.map(|b| Box::new($name {
394                        equals: b.equals,
395                        in_list: b.in_list,
396                        not_in: b.not_in,
397                        lt: b.lt, lte: b.lte, gt: b.gt, gte: b.gte,
398                        not: None,
399                    })),
400                    in_list: self.in_list,
401                    not_in: self.not_in,
402                    lt: self.lt, lte: self.lte, gt: self.gt, gte: self.gte,
403                };
404                let f = inner.into_filter(column);
405                if !matches!(f, Filter::None) { parts.push(f); }
406                combine_filters(parts)
407            }
408        }
409    };
410}
411
412scalar_filter!(
413    /// Filter for non-nullable `Int` (`i32`) columns.
414    IntFilter<i32> => |v| { FilterValue::Int(v as i64) },
415    /// Filter for nullable `Int` columns.
416    nullable IntNullableFilter
417);
418
419scalar_filter!(
420    /// Filter for non-nullable `BigInt` (`i64`) columns.
421    BigIntFilter<i64> => |v| { FilterValue::Int(v) },
422    /// Filter for nullable `BigInt` columns.
423    nullable BigIntNullableFilter
424);
425
426scalar_filter!(
427    /// Filter for non-nullable `Float` (`f64`) columns.
428    FloatFilter<f64> => |v| { FilterValue::Float(v) },
429    /// Filter for nullable `Float` columns.
430    nullable FloatNullableFilter
431);
432
433scalar_filter!(
434    /// Filter for non-nullable `Decimal` (`rust_decimal::Decimal`) columns.
435    ///
436    /// Lowered as `FilterValue::String` because the runtime IR does not
437    /// have a dedicated `Decimal` variant; the driver layer parses it on
438    /// the wire.
439    DecimalFilter<rust_decimal::Decimal> => |v| { FilterValue::String(v.to_string()) },
440    /// Filter for nullable `Decimal` columns.
441    nullable DecimalNullableFilter
442);
443
444scalar_filter!(
445    /// Filter for non-nullable `Uuid` columns.
446    UuidFilter<uuid::Uuid> => |v| { FilterValue::String(v.to_string()) },
447    /// Filter for nullable `Uuid` columns.
448    nullable UuidNullableFilter
449);
450
451scalar_filter!(
452    /// Filter for non-nullable `Bytes` (`Vec<u8>`) columns.
453    ///
454    /// Encoded as a base64-of-bytes string in FilterValue::String. The
455    /// driver layer decodes back to bytes on the wire.
456    BytesFilter<Vec<u8>> => |v| {
457        use base64::Engine as _;
458        FilterValue::String(base64::engine::general_purpose::STANDARD.encode(&v))
459    },
460    /// Filter for nullable `Bytes` columns.
461    nullable BytesNullableFilter
462);
463
464/// Filter operators for a non-nullable `Boolean` column.
465#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
466#[serde(rename_all = "snake_case")]
467pub struct BoolFilter {
468    /// `column = value`
469    pub equals: Option<bool>,
470    /// Negation of the inner filter.
471    pub not: Option<Box<BoolFilter>>,
472}
473
474impl BoolFilter {
475    /// `equals: Some(value)`.
476    pub fn equals(v: bool) -> Self {
477        Self {
478            equals: Some(v),
479            ..Default::default()
480        }
481    }
482}
483
484impl From<bool> for BoolFilter {
485    fn from(v: bool) -> Self {
486        Self::equals(v)
487    }
488}
489
490impl ScalarFilter for BoolFilter {
491    fn into_filter(self, column: &str) -> Filter {
492        let col: crate::filter::FieldName = column.to_string().into();
493        let mut parts: Vec<Filter> = Vec::new();
494        if let Some(v) = self.equals {
495            parts.push(Filter::Equals(col.clone(), FilterValue::Bool(v)));
496        }
497        if let Some(boxed) = self.not {
498            parts.push(Filter::Not(Box::new(boxed.into_filter(column))));
499        }
500        combine_filters(parts)
501    }
502}
503
504/// Filter operators for a nullable `Boolean` column.
505#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
506#[serde(rename_all = "snake_case")]
507pub struct BoolNullableFilter {
508    /// `column = value`
509    pub equals: Option<bool>,
510    /// Negation.
511    pub not: Option<Box<BoolNullableFilter>>,
512    /// IS NULL / IS NOT NULL.
513    pub is_null: Option<bool>,
514}
515
516impl ScalarFilter for BoolNullableFilter {
517    fn into_filter(self, column: &str) -> Filter {
518        let mut parts: Vec<Filter> = Vec::new();
519        if let Some(b) = self.is_null {
520            parts.push(if b {
521                Filter::IsNull(column.to_string().into())
522            } else {
523                Filter::IsNotNull(column.to_string().into())
524            });
525        }
526        let inner = BoolFilter {
527            equals: self.equals,
528            not: self.not.map(|b| {
529                Box::new(BoolFilter {
530                    equals: b.equals,
531                    not: None,
532                })
533            }),
534        };
535        let f = inner.into_filter(column);
536        if !matches!(f, Filter::None) {
537            parts.push(f);
538        }
539        combine_filters(parts)
540    }
541}
542
543scalar_filter!(
544    /// Filter for non-nullable `DateTime` columns (encoded RFC3339).
545    DateTimeFilter<chrono::DateTime<chrono::Utc>> => |v| {
546        FilterValue::String(v.to_rfc3339())
547    },
548    /// Filter for nullable `DateTime` columns.
549    nullable DateTimeNullableFilter
550);
551
552scalar_filter!(
553    /// Filter for non-nullable `Date` columns (encoded YYYY-MM-DD).
554    DateFilter<chrono::NaiveDate> => |v| {
555        FilterValue::String(v.to_string())
556    },
557    /// Filter for nullable `Date` columns.
558    nullable DateNullableFilter
559);
560
561scalar_filter!(
562    /// Filter for non-nullable `Time` columns (encoded HH:MM:SS).
563    TimeFilter<chrono::NaiveTime> => |v| {
564        FilterValue::String(v.format("%H:%M:%S").to_string())
565    },
566    /// Filter for nullable `Time` columns.
567    nullable TimeNullableFilter
568);
569
570/// Filter operators for an enum-typed column.
571///
572/// `E` is the user-defined enum. Codegen emits `impl ToString for Role` so
573/// the macro's bare-ident shorthand (`role: Admin`) flows through.
574#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
575#[serde(
576    rename_all = "snake_case",
577    bound = "E: Serialize + for<'de2> Deserialize<'de2>"
578)]
579pub struct EnumFilter<E> {
580    /// `column = value`
581    pub equals: Option<E>,
582    /// Negation.
583    pub not: Option<Box<EnumFilter<E>>>,
584    /// `column IN (...)`
585    pub in_list: Option<Vec<E>>,
586    /// `column NOT IN (...)`
587    pub not_in: Option<Vec<E>>,
588}
589
590impl<E> EnumFilter<E> {
591    /// `equals: Some(value)`.
592    pub fn equals(v: E) -> Self {
593        Self {
594            equals: Some(v),
595            not: None,
596            in_list: None,
597            not_in: None,
598        }
599    }
600}
601
602impl<E: ToString> ScalarFilter for EnumFilter<E> {
603    fn into_filter(self, column: &str) -> Filter {
604        let col: crate::filter::FieldName = column.to_string().into();
605        let mut parts: Vec<Filter> = Vec::new();
606        if let Some(v) = self.equals {
607            parts.push(Filter::Equals(
608                col.clone(),
609                FilterValue::String(v.to_string()),
610            ));
611        }
612        if let Some(boxed) = self.not {
613            parts.push(Filter::Not(Box::new(boxed.into_filter(column))));
614        }
615        if let Some(values) = self.in_list {
616            let vs: Vec<FilterValue> = values
617                .into_iter()
618                .map(|v| FilterValue::String(v.to_string()))
619                .collect();
620            parts.push(Filter::In(col.clone(), vs));
621        }
622        if let Some(values) = self.not_in {
623            let vs: Vec<FilterValue> = values
624                .into_iter()
625                .map(|v| FilterValue::String(v.to_string()))
626                .collect();
627            parts.push(Filter::NotIn(col, vs));
628        }
629        combine_filters(parts)
630    }
631}
632
633/// Filter operators for a nullable enum-typed column.
634#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
635#[serde(
636    rename_all = "snake_case",
637    bound = "E: Serialize + for<'de2> Deserialize<'de2>"
638)]
639pub struct EnumNullableFilter<E> {
640    /// `column = value`
641    pub equals: Option<E>,
642    /// Negation.
643    pub not: Option<Box<EnumNullableFilter<E>>>,
644    /// `column IN (...)`
645    pub in_list: Option<Vec<E>>,
646    /// `column NOT IN (...)`
647    pub not_in: Option<Vec<E>>,
648    /// IS NULL / IS NOT NULL.
649    pub is_null: Option<bool>,
650}
651
652impl<E: ToString> ScalarFilter for EnumNullableFilter<E> {
653    fn into_filter(self, column: &str) -> Filter {
654        let mut parts: Vec<Filter> = Vec::new();
655        if let Some(b) = self.is_null {
656            parts.push(if b {
657                Filter::IsNull(column.to_string().into())
658            } else {
659                Filter::IsNotNull(column.to_string().into())
660            });
661        }
662        let inner = EnumFilter::<E> {
663            equals: self.equals,
664            not: self.not.map(|b| {
665                Box::new(EnumFilter {
666                    equals: b.equals,
667                    in_list: b.in_list,
668                    not_in: b.not_in,
669                    not: None,
670                })
671            }),
672            in_list: self.in_list,
673            not_in: self.not_in,
674        };
675        let f = inner.into_filter(column);
676        if !matches!(f, Filter::None) {
677            parts.push(f);
678        }
679        combine_filters(parts)
680    }
681}
682
683/// Filter operators for a non-nullable `Json` column.
684///
685/// Phase 1 supports `equals`/`not`. JSON-path operators land behind
686/// `SupportsJsonPath` in a follow-up.
687#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
688#[serde(rename_all = "snake_case")]
689pub struct JsonFilter {
690    /// `column = value`
691    pub equals: Option<serde_json::Value>,
692    /// Negation.
693    pub not: Option<Box<JsonFilter>>,
694}
695
696impl ScalarFilter for JsonFilter {
697    fn into_filter(self, column: &str) -> Filter {
698        let col: crate::filter::FieldName = column.to_string().into();
699        let mut parts: Vec<Filter> = Vec::new();
700        if let Some(v) = self.equals {
701            parts.push(Filter::Equals(col.clone(), FilterValue::Json(v)));
702        }
703        if let Some(boxed) = self.not {
704            parts.push(Filter::Not(Box::new(boxed.into_filter(column))));
705        }
706        combine_filters(parts)
707    }
708}
709
710/// Filter operators for a nullable `Json` column.
711#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
712#[serde(rename_all = "snake_case")]
713pub struct JsonNullableFilter {
714    /// `column = value`
715    pub equals: Option<serde_json::Value>,
716    /// Negation.
717    pub not: Option<Box<JsonNullableFilter>>,
718    /// IS NULL / IS NOT NULL.
719    pub is_null: Option<bool>,
720}
721
722impl ScalarFilter for JsonNullableFilter {
723    fn into_filter(self, column: &str) -> Filter {
724        let mut parts: Vec<Filter> = Vec::new();
725        if let Some(b) = self.is_null {
726            parts.push(if b {
727                Filter::IsNull(column.to_string().into())
728            } else {
729                Filter::IsNotNull(column.to_string().into())
730            });
731        }
732        let inner = JsonFilter {
733            equals: self.equals,
734            not: self.not.map(|b| {
735                Box::new(JsonFilter {
736                    equals: b.equals,
737                    not: None,
738                })
739            }),
740        };
741        let f = inner.into_filter(column);
742        if !matches!(f, Filter::None) {
743            parts.push(f);
744        }
745        combine_filters(parts)
746    }
747}