Skip to main content

drizzle_sqlite/
expr.rs

1//! SQLite-specific SQL expressions and JSON helpers.
2//!
3//! This module provides `SQLite` dialect functions and JSON expressions.
4//! For standard SQL expressions, use `drizzle_core::expr`.
5
6#[cfg(not(feature = "std"))]
7use crate::prelude::*;
8use crate::values::SQLiteValue;
9use drizzle_core::{SQL, ToSQL};
10
11/// Wraps a value with the `SQLite` `json()` function, validating and returning JSON text.
12pub fn json<'a>(value: impl ToSQL<'a, SQLiteValue<'a>>) -> SQL<'a, SQLiteValue<'a>> {
13    SQL::func("json", value.to_sql())
14}
15
16/// Wraps a value with the `SQLite` `jsonb()` function, validating and returning JSON in binary format.
17pub fn jsonb<'a>(value: impl ToSQL<'a, SQLiteValue<'a>>) -> SQL<'a, SQLiteValue<'a>> {
18    SQL::func("jsonb", value.to_sql())
19}
20
21/// Create a JSON field equality condition using `SQLite` ->> operator
22///
23/// # Example
24/// ```
25/// # use drizzle_sqlite::expr::json_eq;
26/// # use drizzle_core::SQL;
27/// # use drizzle_sqlite::values::SQLiteValue;
28/// # fn main() {
29/// let column = SQL::<SQLiteValue>::raw("metadata");
30/// let condition = json_eq(column, "theme", "dark");
31/// assert_eq!(condition.sql(), "metadata ->> ? = ?");
32/// # }
33/// ```
34pub fn json_eq<'a, L, R>(left: L, field: &'a str, value: R) -> SQL<'a, SQLiteValue<'a>>
35where
36    L: ToSQL<'a, SQLiteValue<'a>>,
37    R: Into<SQLiteValue<'a>>,
38{
39    left.to_sql()
40        .append(SQL::raw(" ->> "))
41        .append(SQL::param(SQLiteValue::from(field)))
42        .append(SQL::raw(" = "))
43        .append(SQL::param(value.into()))
44}
45
46/// Create a JSON field inequality condition
47///
48/// # Example
49/// ```
50/// # use drizzle_sqlite::expr::json_ne;
51/// # use drizzle_core::SQL;
52/// # use drizzle_sqlite::values::SQLiteValue;
53/// # fn main() {
54/// let column = SQL::<SQLiteValue>::raw("metadata");
55/// let condition = json_ne(column, "theme", "light");
56/// assert_eq!(condition.sql(), "metadata ->> ? != ?");
57/// # }
58/// ```
59pub fn json_ne<'a, L, R>(left: L, field: &'a str, value: R) -> SQL<'a, SQLiteValue<'a>>
60where
61    L: ToSQL<'a, SQLiteValue<'a>>,
62    R: Into<SQLiteValue<'a>>,
63{
64    left.to_sql()
65        .append(SQL::raw(" ->> "))
66        .append(SQL::param(SQLiteValue::from(field)))
67        .append(SQL::raw(" != "))
68        .append(SQL::param(value.into()))
69}
70
71/// Create a JSON field contains condition using `json_extract`
72///
73/// # Example
74/// ```
75/// # use drizzle_sqlite::expr::json_contains;
76/// # use drizzle_core::SQL;
77/// # use drizzle_sqlite::values::SQLiteValue;
78/// # fn main() {
79/// let column = SQL::<SQLiteValue>::raw("metadata");
80/// let condition = json_contains(column, "$.preferences[0]", "dark_theme");
81/// assert_eq!(condition.sql(), "json_extract( metadata , ? ) = ?");
82/// # }
83/// ```
84pub fn json_contains<'a, L, R>(left: L, path: &'a str, value: R) -> SQL<'a, SQLiteValue<'a>>
85where
86    L: ToSQL<'a, SQLiteValue<'a>>,
87    R: Into<SQLiteValue<'a>>,
88{
89    SQL::raw("json_extract(")
90        .append(left.to_sql())
91        .append(SQL::raw(", "))
92        .append(SQL::param(SQLiteValue::from(path)))
93        .append(SQL::raw(") = "))
94        .append(SQL::param(value.into()))
95}
96
97/// Create a JSON field exists condition using `json_type`
98///
99/// # Example
100/// ```
101/// # use drizzle_sqlite::expr::json_exists;
102/// # use drizzle_core::SQL;
103/// # use drizzle_sqlite::values::SQLiteValue;
104/// # fn main() {
105/// let column = SQL::<SQLiteValue>::raw("metadata");
106/// let condition = json_exists(column, "$.theme");
107/// assert_eq!(condition.sql(), "json_type( metadata , ? ) IS NOT NULL");
108/// # }
109/// ```
110pub fn json_exists<'a, L>(left: L, path: &'a str) -> SQL<'a, SQLiteValue<'a>>
111where
112    L: ToSQL<'a, SQLiteValue<'a>>,
113{
114    SQL::raw("json_type(")
115        .append(left.to_sql())
116        .append(SQL::raw(", "))
117        .append(SQL::param(SQLiteValue::from(path)))
118        .append(SQL::raw(") IS NOT NULL"))
119}
120
121/// Create a JSON field does not exist condition
122///
123/// # Example
124/// ```
125/// # use drizzle_sqlite::expr::json_not_exists;
126/// # use drizzle_core::SQL;
127/// # use drizzle_sqlite::values::SQLiteValue;
128/// # fn main() {
129/// let column = SQL::<SQLiteValue>::raw("metadata");
130/// let condition = json_not_exists(column, "$.theme");
131/// assert_eq!(condition.sql(), "json_type( metadata , ? ) IS NULL");
132/// # }
133/// ```
134pub fn json_not_exists<'a, L>(left: L, path: &'a str) -> SQL<'a, SQLiteValue<'a>>
135where
136    L: ToSQL<'a, SQLiteValue<'a>>,
137{
138    SQL::raw("json_type(")
139        .append(left.to_sql())
140        .append(SQL::raw(", "))
141        .append(SQL::param(SQLiteValue::from(path)))
142        .append(SQL::raw(") IS NULL"))
143}
144
145/// Create a JSON array contains value condition
146///
147/// # Example
148/// ```
149/// # use drizzle_sqlite::expr::json_array_contains;
150/// # use drizzle_core::SQL;
151/// # use drizzle_sqlite::values::SQLiteValue;
152/// # fn main() {
153/// let column = SQL::<SQLiteValue>::raw("metadata");
154/// let condition = json_array_contains(column, "$.preferences", "dark_theme");
155/// assert_eq!(
156///     condition.sql(),
157///     "EXISTS(SELECT 1 FROM json_each( metadata , ? ) WHERE value = ? )"
158/// );
159/// # }
160/// ```
161pub fn json_array_contains<'a, L, R>(left: L, path: &'a str, value: R) -> SQL<'a, SQLiteValue<'a>>
162where
163    L: ToSQL<'a, SQLiteValue<'a>>,
164    R: Into<SQLiteValue<'a>>,
165{
166    SQL::raw("EXISTS(SELECT 1 FROM json_each(")
167        .append(left.to_sql())
168        .append(SQL::raw(", "))
169        .append(SQL::param(SQLiteValue::from(path)))
170        .append(SQL::raw(") WHERE value = "))
171        .append(SQL::param(value.into()))
172        .append(SQL::raw(")"))
173}
174
175/// Create a JSON object contains key condition
176///
177/// # Example
178/// ```
179/// # use drizzle_sqlite::expr::json_object_contains_key;
180/// # use drizzle_core::SQL;
181/// # use drizzle_sqlite::values::SQLiteValue;
182/// # fn main() {
183/// let column = SQL::<SQLiteValue>::raw("metadata");
184/// let condition = json_object_contains_key(column, "$", "theme");
185/// assert_eq!(condition.sql(), "json_type( metadata , ? ) IS NOT NULL");
186/// # }
187/// ```
188pub fn json_object_contains_key<'a, L>(
189    left: L,
190    path: &'a str,
191    key: &'a str,
192) -> SQL<'a, SQLiteValue<'a>>
193where
194    L: ToSQL<'a, SQLiteValue<'a>>,
195{
196    let full_path = if path.ends_with('$') || path.is_empty() {
197        format!("$.{key}")
198    } else {
199        format!("{path}.{key}")
200    };
201
202    SQL::raw("json_type(")
203        .append(left.to_sql())
204        .append(SQL::raw(", "))
205        .append(SQL::param(SQLiteValue::from(full_path)))
206        .append(SQL::raw(") IS NOT NULL"))
207}
208
209/// Create a JSON text search condition using case-insensitive matching
210///
211/// # Example
212/// ```
213/// # use drizzle_sqlite::expr::json_text_contains;
214/// # use drizzle_core::SQL;
215/// # use drizzle_sqlite::values::SQLiteValue;
216/// # fn main() {
217/// let column = SQL::<SQLiteValue>::raw("metadata");
218/// let condition = json_text_contains(column, "$.description", "user");
219/// assert_eq!(
220///     condition.sql(),
221///     "instr(lower(json_extract( metadata , ? )), lower( ? )) > 0"
222/// );
223/// # }
224/// ```
225pub fn json_text_contains<'a, L, R>(left: L, path: &'a str, value: R) -> SQL<'a, SQLiteValue<'a>>
226where
227    L: ToSQL<'a, SQLiteValue<'a>>,
228    R: Into<SQLiteValue<'a>>,
229{
230    SQL::raw("instr(lower(json_extract(")
231        .append(left.to_sql())
232        .append(SQL::raw(", "))
233        .append(SQL::param(SQLiteValue::from(path)))
234        .append(SQL::raw(")), lower("))
235        .append(SQL::param(value.into()))
236        .append(SQL::raw(")) > 0"))
237}
238
239/// Create a JSON numeric greater-than condition
240///
241/// # Example
242/// ```
243/// # use drizzle_sqlite::expr::json_gt;
244/// # use drizzle_core::SQL;
245/// # use drizzle_sqlite::values::SQLiteValue;
246/// let column = SQL::<SQLiteValue>::raw("metadata");
247/// let condition = json_gt(column, "$.score", 85.0);
248/// assert_eq!(condition.sql(), "CAST(json_extract( metadata , ? ) AS NUMERIC) > ?");
249/// ```
250pub fn json_gt<'a, L, R>(left: L, path: &'a str, value: R) -> SQL<'a, SQLiteValue<'a>>
251where
252    L: ToSQL<'a, SQLiteValue<'a>>,
253    R: Into<SQLiteValue<'a>>,
254{
255    SQL::raw("CAST(json_extract(")
256        .append(left.to_sql())
257        .append(SQL::raw(", "))
258        .append(SQL::param(SQLiteValue::from(path)))
259        .append(SQL::raw(") AS NUMERIC) > "))
260        .append(SQL::param(value.into()))
261}
262
263/// Helper function for JSON extraction using ->> operator
264///
265/// # Example
266/// ```
267/// # use drizzle_sqlite::expr::json_extract;
268/// # use drizzle_core::SQL;
269/// # use drizzle_sqlite::values::SQLiteValue;
270/// # fn main() {
271/// let column = SQL::<SQLiteValue>::raw("metadata");
272/// let extract_expr = json_extract(column, "theme");
273/// assert_eq!(extract_expr.sql(), "metadata ->> ?");
274/// # }
275/// ```
276pub fn json_extract<'a, L>(left: L, path: impl AsRef<str>) -> SQL<'a, SQLiteValue<'a>>
277where
278    L: ToSQL<'a, SQLiteValue<'a>>,
279{
280    left.to_sql()
281        .append(SQL::raw(" ->> "))
282        .append(SQL::param(SQLiteValue::from(path.as_ref().to_owned())))
283}
284
285/// Helper function for JSON extraction as JSON text using -> operator
286///
287/// # Example
288/// ```
289/// # use drizzle_sqlite::expr::json_extract_text;
290/// # use drizzle_core::SQL;
291/// # use drizzle_sqlite::values::SQLiteValue;
292/// # fn main() {
293/// let column = SQL::<SQLiteValue>::raw("metadata");
294/// let extract_expr = json_extract_text(column, "preferences");
295/// assert_eq!(extract_expr.sql(), "metadata -> ?");
296/// # }
297/// ```
298pub fn json_extract_text<'a, L>(left: L, path: &'a str) -> SQL<'a, SQLiteValue<'a>>
299where
300    L: ToSQL<'a, SQLiteValue<'a>>,
301{
302    left.to_sql()
303        .append(SQL::raw(" -> "))
304        .append(SQL::param(SQLiteValue::from(path)))
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn json_paths_are_parameters_in_stable_order() {
313        let expression = json_contains(
314            SQL::param(SQLiteValue::from("document")),
315            "$.preferences['quoted']",
316            "dark",
317        );
318
319        assert_eq!(expression.sql(), "json_extract( ? , ? ) = ?");
320        let document = SQLiteValue::from("document");
321        let path = SQLiteValue::from("$.preferences['quoted']");
322        let value = SQLiteValue::from("dark");
323        assert_eq!(
324            expression.params().collect::<Vec<_>>(),
325            vec![&document, &path, &value]
326        );
327    }
328
329    #[test]
330    fn json_object_key_is_bound_as_data() {
331        let expression =
332            json_object_contains_key(SQL::<SQLiteValue>::raw("metadata"), "$", "quote'key");
333
334        assert_eq!(expression.sql(), "json_type( metadata , ? ) IS NOT NULL");
335        let path = SQLiteValue::from("$.quote'key");
336        assert_eq!(expression.params().collect::<Vec<_>>(), vec![&path]);
337    }
338}