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}