Skip to main content

qraft_core/expression/
json.rs

1//! JSON path and predicate expressions.
2
3use std::fmt::Write;
4
5use crate::{
6    BigInt, Nullable, Text,
7    expression::{Column, Expression, In, InList, LowerIn},
8    lower::{Data, Instructions, JsonContainsLower, LowerCtx, Params},
9    param::Param,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13enum JsonPathSegment<'a> {
14    Key(&'a str),
15    Index(&'a str),
16}
17
18fn parse_json_path(path: &str) -> Vec<JsonPathSegment<'_>> {
19    assert!(!path.is_empty(), "json path cannot be empty");
20
21    path.split("->")
22        .map(|segment| {
23            assert!(!segment.is_empty(), "json path contains an empty segment");
24            if segment.bytes().all(|byte| byte.is_ascii_digit()) {
25                JsonPathSegment::Index(segment)
26            } else {
27                assert!(
28                    segment
29                        .bytes()
30                        .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_'),
31                    "json path segment `{segment}` contains unsupported characters",
32                );
33                JsonPathSegment::Key(segment)
34            }
35        })
36        .collect()
37}
38
39fn write_json_string(out: &mut String, value: &str) {
40    out.push('"');
41    for ch in value.chars() {
42        match ch {
43            '"' => out.push_str("\\\""),
44            '\\' => out.push_str("\\\\"),
45            '\u{08}' => out.push_str("\\b"),
46            '\u{0C}' => out.push_str("\\f"),
47            '\n' => out.push_str("\\n"),
48            '\r' => out.push_str("\\r"),
49            '\t' => out.push_str("\\t"),
50            ch if ch <= '\u{1F}' => {
51                let _ = write!(out, "\\u{:04x}", ch as u32);
52            }
53            ch => out.push(ch),
54        }
55    }
56    out.push('"');
57}
58
59#[doc(hidden)]
60pub trait JsonLiteral {
61    fn write_json(&self, out: &mut String);
62    fn is_compound(&self) -> bool {
63        false
64    }
65}
66
67impl JsonLiteral for str {
68    fn write_json(&self, out: &mut String) {
69        write_json_string(out, self);
70    }
71}
72
73impl JsonLiteral for String {
74    fn write_json(&self, out: &mut String) {
75        self.as_str().write_json(out);
76    }
77}
78
79impl JsonLiteral for &str {
80    fn write_json(&self, out: &mut String) {
81        (*self).write_json(out);
82    }
83}
84
85impl JsonLiteral for bool {
86    fn write_json(&self, out: &mut String) {
87        out.push_str(if *self { "true" } else { "false" });
88    }
89}
90
91macro_rules! impl_json_number {
92    ($($ty:ty),+ $(,)?) => {
93        $(
94            impl JsonLiteral for $ty {
95                fn write_json(&self, out: &mut String) {
96                    let _ = write!(out, "{}", self);
97                }
98            }
99        )+
100    };
101}
102
103impl_json_number!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64);
104
105impl<T> JsonLiteral for Vec<T>
106where
107    T: JsonLiteral,
108{
109    fn write_json(&self, out: &mut String) {
110        out.push('[');
111        for (index, item) in self.iter().enumerate() {
112            if index > 0 {
113                out.push(',');
114            }
115            item.write_json(out);
116        }
117        out.push(']');
118    }
119
120    fn is_compound(&self) -> bool {
121        true
122    }
123}
124
125impl<T, const N: usize> JsonLiteral for [T; N]
126where
127    T: JsonLiteral,
128{
129    fn write_json(&self, out: &mut String) {
130        out.push('[');
131        for (index, item) in self.iter().enumerate() {
132            if index > 0 {
133                out.push(',');
134            }
135            item.write_json(out);
136        }
137        out.push(']');
138    }
139
140    fn is_compound(&self) -> bool {
141        true
142    }
143}
144
145#[cfg(feature = "serde_json")]
146impl JsonLiteral for serde_json::Value {
147    fn write_json(&self, out: &mut String) {
148        out.push_str(&serde_json::to_string(self).expect("json serialization should succeed"));
149    }
150
151    fn is_compound(&self) -> bool {
152        matches!(
153            self,
154            serde_json::Value::Array(_) | serde_json::Value::Object(_)
155        )
156    }
157}
158
159pub struct JsonPath<E> {
160    inner: E,
161    path: &'static str,
162}
163
164pub struct JsonContains<E, V> {
165    inner: E,
166    path: &'static str,
167    value: V,
168    negated: bool,
169}
170
171pub struct JsonContainsKey<E> {
172    inner: E,
173    path: &'static str,
174    negated: bool,
175}
176
177pub struct JsonLength<E> {
178    inner: E,
179    path: &'static str,
180}
181
182pub trait JsonPathExt: Sized {
183    fn json_path(self, path: &'static str) -> JsonPath<Self>;
184}
185
186impl<M> JsonPathExt for Column<M, Text> {
187    fn json_path(self, path: &'static str) -> JsonPath<Self> {
188        let _ = parse_json_path(path);
189        JsonPath { inner: self, path }
190    }
191}
192
193impl<M> JsonPathExt for Column<M, Nullable<Text>> {
194    fn json_path(self, path: &'static str) -> JsonPath<Self> {
195        let _ = parse_json_path(path);
196        JsonPath { inner: self, path }
197    }
198}
199
200impl<E> JsonPath<E> {
201    pub fn one_of<R>(self, list: R) -> InList<Self, R>
202    where
203        Self: Expression<Type = Nullable<Text>>,
204        R: LowerIn<Nullable<Text>>,
205    {
206        In::one_of(self, list)
207    }
208
209    pub fn json_contains<V>(self, value: V) -> JsonContains<E, V>
210    where
211        V: JsonLiteral,
212    {
213        JsonContains {
214            inner: self.inner,
215            path: self.path,
216            value,
217            negated: false,
218        }
219    }
220
221    pub fn json_doesnt_contain<V>(self, value: V) -> JsonContains<E, V>
222    where
223        V: JsonLiteral,
224    {
225        JsonContains {
226            inner: self.inner,
227            path: self.path,
228            value,
229            negated: true,
230        }
231    }
232
233    pub fn json_contains_key(self) -> JsonContainsKey<E> {
234        JsonContainsKey {
235            inner: self.inner,
236            path: self.path,
237            negated: false,
238        }
239    }
240
241    pub fn json_doesnt_contain_key(self) -> JsonContainsKey<E> {
242        JsonContainsKey {
243            inner: self.inner,
244            path: self.path,
245            negated: true,
246        }
247    }
248
249    pub fn json_length(self) -> JsonLength<E> {
250        JsonLength {
251            inner: self.inner,
252            path: self.path,
253        }
254    }
255}
256
257#[qraft_expression_macro::as_expression]
258impl<E> Expression for JsonPath<E>
259where
260    E: Expression,
261{
262    type Type = Nullable<Text>;
263
264    fn lower(&self, ctx: &mut LowerCtx) -> usize {
265        let inner = self.inner.lower(ctx);
266        ctx.lower_json_extract_text(inner, self.path)
267    }
268}
269
270#[qraft_expression_macro::as_expression]
271impl<E, V> Expression for JsonContains<E, V>
272where
273    E: Expression,
274    V: JsonLiteral,
275{
276    type Type = crate::Bool;
277
278    fn lower(&self, ctx: &mut LowerCtx) -> usize {
279        let inner = self.inner.lower(ctx);
280        let mut json = String::new();
281        self.value.write_json(&mut json);
282        let span = ctx.data.intern_text(&json);
283        let id = ctx.params.push_param(Param::Text(Some(span)));
284        ctx.instrs.push_param(id);
285        ctx.lower_json_contains(
286            inner,
287            JsonContainsLower {
288                rhs: 1,
289                path: self.path,
290                negated: self.negated,
291                compound: self.value.is_compound(),
292            },
293        )
294    }
295}
296
297#[qraft_expression_macro::as_expression]
298impl<E> Expression for JsonContainsKey<E>
299where
300    E: Expression,
301{
302    type Type = crate::Bool;
303
304    fn lower(&self, ctx: &mut LowerCtx) -> usize {
305        let inner = self.inner.lower(ctx);
306        ctx.lower_json_contains_key(inner, self.path, self.negated)
307    }
308}
309
310#[qraft_expression_macro::as_expression]
311impl<E> Expression for JsonLength<E>
312where
313    E: Expression,
314{
315    type Type = BigInt;
316
317    fn lower(&self, ctx: &mut LowerCtx) -> usize {
318        let inner = self.inner.lower(ctx);
319        ctx.lower_json_length(inner, self.path)
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::JsonPathExt;
326    use crate::{
327        MySql, Postgres, Sqlite, Text,
328        expression::{EqExt, OrderExt, col},
329        query::select_all,
330    };
331
332    #[test]
333    fn json_path_eq_sqlite_sql() {
334        let preferences = col::<Text>("preferences");
335        let sql = select_all()
336            .from("users")
337            .filter(preferences.json_path("dining->meal").eq("salad"))
338            .to_debug_sql::<Sqlite>();
339
340        assert_eq!(
341            sql,
342            r#"select * from "users" where json_extract("preferences", '$.dining.meal') = ?; params=["salad"]"#
343        );
344    }
345
346    #[test]
347    fn json_path_eq_postgres_sql() {
348        let preferences = col::<Text>("preferences");
349        let sql = select_all()
350            .from("users")
351            .filter(preferences.json_path("dining->meal").eq("salad"))
352            .to_debug_sql::<Postgres>();
353
354        assert_eq!(
355            sql,
356            r#"select * from "users" where jsonb_extract_path_text(("preferences")::jsonb, 'dining', 'meal') = $1; params=["salad"]"#
357        );
358    }
359
360    #[test]
361    fn json_path_eq_mariadb_sql() {
362        let preferences = col::<Text>("preferences");
363        let sql = select_all()
364            .from("users")
365            .filter(preferences.json_path("dining->meal").eq("salad"))
366            .to_debug_sql::<MySql>();
367
368        assert_eq!(
369            sql,
370            "select * from `users` where json_unquote(json_extract(`preferences`, '$.dining.meal')) = ?; params=[\"salad\"]"
371        );
372    }
373
374    #[test]
375    fn json_path_one_of_sqlite_sql() {
376        let preferences = col::<Text>("preferences");
377        let sql = select_all()
378            .from("users")
379            .filter(
380                preferences
381                    .json_path("dining->meal")
382                    .one_of(["pasta", "salad"]),
383            )
384            .to_debug_sql::<Sqlite>();
385
386        assert_eq!(
387            sql,
388            r#"select * from "users" where json_extract("preferences", '$.dining.meal') in (?, ?); params=["pasta", "salad"]"#
389        );
390    }
391
392    #[test]
393    fn json_contains_key_and_length_sqlite_sql() {
394        let options = col::<Text>("options");
395        let contains = select_all()
396            .from("users")
397            .filter(options.json_path("languages").json_contains_key())
398            .to_debug_sql::<Sqlite>();
399        let length = select_all()
400            .from("users")
401            .filter(options.json_path("languages").json_length().gt(1_i64))
402            .to_debug_sql::<Sqlite>();
403
404        assert_eq!(
405            contains,
406            r#"select * from "users" where json_type("options", '$.languages') is not null; params=[]"#
407        );
408        assert_eq!(
409            length,
410            r#"select * from "users" where json_array_length(json_extract("options", '$.languages')) > ?; params=[1]"#
411        );
412    }
413
414    #[test]
415    fn json_contains_sqlite_sql() {
416        let options = col::<Text>("options");
417        let sql = select_all()
418            .from("users")
419            .filter(options.json_path("languages").json_contains("en"))
420            .to_debug_sql::<Sqlite>();
421
422        assert_eq!(
423            sql,
424            r#"select * from "users" where exists (select 1 from json_each(json_extract("options", '$.languages')) where json_each.value = json_extract(?, '$')); params=[""en""]"#
425        );
426    }
427}