1use 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}