Skip to main content

cratestack_sql/filter/
json.rs

1use crate::{IntoSqlValue, values::FilterValue};
2
3use super::expr::FilterExpr;
4use super::op::FilterOp;
5
6/// JSON / JSONB filter predicates. Two flavors:
7///
8/// * `HasKey` — `col ? 'key'` on PG (key-exists operator). On SQLite
9///   this lowers to `json_extract(col, '$.key') IS NOT NULL`, which
10///   has the same matches-some-non-null-value semantics for the most
11///   common case (records where the schema sometimes carries a key,
12///   sometimes doesn't); JSON values explicitly stored as `null`
13///   diverge between backends, mirroring the operators themselves.
14/// * `GetText` — `col ->> 'key' <op> $1` on PG (extract-as-text +
15///   compare). On SQLite the same `json_extract` path with a column
16///   accessor handles it. Supported comparison ops are the standard
17///   `Eq/Ne/Lt/Lte/Gt/Gte` plus `IsNull` / `IsNotNull`.
18///
19/// Keys are owned `String` so callers can pass runtime-supplied
20/// metric / setting names (e.g. user-driven `model_run_timeseries`
21/// queries that pivot on `args.metric`). The column slot stays
22/// `&'static str` because columns are always schema-rooted.
23#[derive(Debug, Clone, PartialEq)]
24pub enum JsonFilter {
25    HasKey {
26        column: &'static str,
27        key: String,
28    },
29    GetText {
30        column: &'static str,
31        key: String,
32        op: FilterOp,
33        value: FilterValue,
34    },
35}
36
37/// Left-hand operand of a `json_get_text` filter — chain a comparison
38/// method (`.eq`, `.lt`, `.is_null`, ...) to produce a [`FilterExpr`].
39#[derive(Debug, Clone)]
40pub struct JsonTextPath {
41    pub(super) column: &'static str,
42    pub(super) key: String,
43}
44
45impl JsonTextPath {
46    pub(super) fn new(column: &'static str, key: String) -> Self {
47        Self { column, key }
48    }
49
50    fn binary<V: IntoSqlValue>(self, op: FilterOp, value: V) -> FilterExpr {
51        FilterExpr::Json(JsonFilter::GetText {
52            column: self.column,
53            key: self.key,
54            op,
55            value: FilterValue::Single(value.into_sql_value()),
56        })
57    }
58
59    pub fn eq<V: IntoSqlValue>(self, value: V) -> FilterExpr {
60        self.binary(FilterOp::Eq, value)
61    }
62    pub fn ne<V: IntoSqlValue>(self, value: V) -> FilterExpr {
63        self.binary(FilterOp::Ne, value)
64    }
65    pub fn lt<V: IntoSqlValue>(self, value: V) -> FilterExpr {
66        self.binary(FilterOp::Lt, value)
67    }
68    pub fn lte<V: IntoSqlValue>(self, value: V) -> FilterExpr {
69        self.binary(FilterOp::Lte, value)
70    }
71    pub fn gt<V: IntoSqlValue>(self, value: V) -> FilterExpr {
72        self.binary(FilterOp::Gt, value)
73    }
74    pub fn gte<V: IntoSqlValue>(self, value: V) -> FilterExpr {
75        self.binary(FilterOp::Gte, value)
76    }
77
78    /// `col ->> 'key' IS NULL` — the JSON document either lacks the
79    /// key, or stores it as JSON null. (PG and SQLite agree here.)
80    pub fn is_null(self) -> FilterExpr {
81        FilterExpr::Json(JsonFilter::GetText {
82            column: self.column,
83            key: self.key,
84            op: FilterOp::IsNull,
85            value: FilterValue::None,
86        })
87    }
88
89    /// `col ->> 'key' IS NOT NULL` — the JSON document has the key
90    /// with a non-null primitive value. Note: a PG `?` test (use
91    /// [`super::field_ref::FieldRef::json_has_key`]) treats JSON null
92    /// as a present key where this method does not.
93    pub fn is_not_null(self) -> FilterExpr {
94        FilterExpr::Json(JsonFilter::GetText {
95            column: self.column,
96            key: self.key,
97            op: FilterOp::IsNotNull,
98            value: FilterValue::None,
99        })
100    }
101}