Skip to main content

keelson_sqlite/
ops.rs

1use std::borrow::Cow;
2
3use keelson_core::expr::{Chain, Expr, IntoExpr};
4
5/// The operators SQLite has and the other two dialects do not.
6///
7/// An extension trait over [`Chain`] with a blanket impl, so nothing in core
8/// changes and these are reachable only where this trait is imported.
9///
10/// ```
11/// use keelson_sqlite::{SqliteOps, quote, s};
12///
13/// let e = quote("name").glob(s("Ada*"));
14/// ```
15///
16/// The list is short on purpose. SQLite's `LIKE` is already case-insensitive for
17/// ASCII, so there is no `ILIKE`; it has no `SIMILAR TO`, no `~` operators, no
18/// array or range containment, no `ANY`/`ALL` quantifiers, no `IS TRUE`/`IS FALSE`
19/// (it has no boolean type to test), no `::` cast shorthand and no
20/// `AT TIME ZONE`. Every one of those is a PostgreSQL operator, and none of them is
21/// reachable from a SQLite expression.
22///
23/// Every method finishes through [`Chain::step`] or [`Chain::op`], so the
24/// parenthesisation rule is applied for it.
25// The `is_*` methods take `self` by value like every other operator: they are SQL
26// keywords being spelled out, not Rust predicates returning `bool`.
27#[allow(clippy::wrong_self_convention)]
28pub trait SqliteOps: Chain {
29    // -- pattern matching ----------------------------------------------------
30
31    /// `self GLOB rhs` — Unix file-glob matching, and case-sensitive, which is
32    /// what distinguishes it from `LIKE`.
33    #[must_use]
34    fn glob(self, rhs: impl IntoExpr) -> Self {
35        self.op("GLOB", rhs)
36    }
37
38    /// `self NOT GLOB rhs`.
39    #[must_use]
40    fn not_glob(self, rhs: impl IntoExpr) -> Self {
41        self.op("NOT GLOB", rhs)
42    }
43
44    /// `self REGEXP rhs`.
45    ///
46    /// The operator is in the grammar, but SQLite ships **no** `regexp()`
47    /// implementation: a statement using it prepares only against a connection that
48    /// has registered one, and fails with *no such function: REGEXP* otherwise.
49    /// That is a property of the build, not of the SQL, so the operator is offered
50    /// and the connection is left to answer for it.
51    #[must_use]
52    fn regexp(self, rhs: impl IntoExpr) -> Self {
53        self.op("REGEXP", rhs)
54    }
55
56    /// `self NOT REGEXP rhs`. See [`regexp`](Self::regexp) about availability.
57    #[must_use]
58    fn not_regexp(self, rhs: impl IntoExpr) -> Self {
59        self.op("NOT REGEXP", rhs)
60    }
61
62    /// `self MATCH rhs` — the full-text and R-tree extension operator.
63    ///
64    /// Named with a trailing underscore because `match` is a Rust keyword.
65    #[must_use]
66    fn match_(self, rhs: impl IntoExpr) -> Self {
67        self.op("MATCH", rhs)
68    }
69
70    /// `self NOT MATCH rhs`.
71    #[must_use]
72    fn not_match(self, rhs: impl IntoExpr) -> Self {
73        self.op("NOT MATCH", rhs)
74    }
75
76    /// `self NOT LIKE rhs`.
77    #[must_use]
78    fn not_like(self, rhs: impl IntoExpr) -> Self {
79        self.op("NOT LIKE", rhs)
80    }
81
82    /// `self LIKE pattern ESCAPE escape`.
83    ///
84    /// `ESCAPE` is a third operand of `LIKE` in SQLite's grammar rather than a
85    /// separate operator, which is why it cannot be a chain step of its own.
86    #[must_use]
87    fn like_escape(self, pattern: impl IntoExpr, escape: impl IntoExpr) -> Self {
88        self.step(move |lhs| {
89            Expr::join((lhs, Expr::raw("LIKE"), pattern, Expr::raw("ESCAPE"), escape))
90        })
91    }
92
93    /// `self NOT LIKE pattern ESCAPE escape`.
94    #[must_use]
95    fn not_like_escape(self, pattern: impl IntoExpr, escape: impl IntoExpr) -> Self {
96        self.step(move |lhs| {
97            Expr::join((
98                lhs,
99                Expr::raw("NOT LIKE"),
100                pattern,
101                Expr::raw("ESCAPE"),
102                escape,
103            ))
104        })
105    }
106
107    // -- json ----------------------------------------------------------------
108
109    /// `self -> rhs` — the field or element, as JSON text. SQLite 3.38 and later.
110    ///
111    /// Spelled the same as PostgreSQL's, and means something slightly different:
112    /// SQLite always yields a JSON representation, where PostgreSQL's `->` yields
113    /// `json`/`jsonb` and preserves the input type.
114    #[must_use]
115    fn json_get(self, rhs: impl IntoExpr) -> Self {
116        self.op("->", rhs)
117    }
118
119    /// `self ->> rhs` — the field or element as a SQL text, integer or real.
120    #[must_use]
121    fn json_get_text(self, rhs: impl IntoExpr) -> Self {
122        self.op("->>", rhs)
123    }
124
125    // -- null-safe comparison ------------------------------------------------
126
127    /// `self IS rhs` — like `=`, except that two nulls compare equal.
128    ///
129    /// SQLite's own spelling, and much older than the standard
130    /// `IS NOT DISTINCT FROM` that [`Chain::is_not_distinct_from`] writes; the two
131    /// mean the same thing here.
132    #[must_use]
133    fn is_(self, rhs: impl IntoExpr) -> Self {
134        self.op("IS", rhs)
135    }
136
137    /// `self IS NOT rhs` — like `<>`, except that two nulls compare equal.
138    #[must_use]
139    fn is_not(self, rhs: impl IntoExpr) -> Self {
140        self.op("IS NOT", rhs)
141    }
142
143    // -- misc ----------------------------------------------------------------
144
145    /// `self COLLATE "name"` — compare with a named collating sequence.
146    ///
147    /// SQLite's built-in sequences are `BINARY`, `NOCASE` and `RTRIM`.
148    #[must_use]
149    fn collate(self, name: impl Into<Cow<'static, str>>) -> Self {
150        let name = name.into();
151        self.step(move |lhs| Expr::join((lhs, Expr::raw("COLLATE"), Expr::ident(name))))
152    }
153}
154
155impl<T: Chain> SqliteOps for T {}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::{Sqlite, arg, quote, s};
161    use keelson_core::build;
162
163    fn sql(e: Expr) -> String {
164        build(&Sqlite, &e).expect("render").0
165    }
166
167    /// Spellings from <https://www.sqlite.org/lang_expr.html>. `regexp` is checked
168    /// here rather than in a statement test because the engine tier has no
169    /// `regexp()` function to resolve it against.
170    #[test]
171    fn every_operator_renders_with_one_set_of_parentheses() {
172        let cases = [
173            (quote("a").glob(arg("x")), r#"("a" GLOB ?1)"#),
174            (quote("a").not_glob(arg("x")), r#"("a" NOT GLOB ?1)"#),
175            (quote("a").regexp(arg("x")), r#"("a" REGEXP ?1)"#),
176            (quote("a").not_regexp(arg("x")), r#"("a" NOT REGEXP ?1)"#),
177            (quote("a").match_(arg("x")), r#"("a" MATCH ?1)"#),
178            (quote("a").not_match(arg("x")), r#"("a" NOT MATCH ?1)"#),
179            (quote("a").not_like(arg("x")), r#"("a" NOT LIKE ?1)"#),
180            (quote("a").json_get(s("$.b")), r#"("a" -> '$.b')"#),
181            (quote("a").json_get_text(s("$.b")), r#"("a" ->> '$.b')"#),
182            (quote("a").is_(quote("b")), r#"("a" IS "b")"#),
183            (quote("a").is_not(quote("b")), r#"("a" IS NOT "b")"#),
184        ];
185        for (e, expected) in cases {
186            assert_eq!(sql(e), expected);
187        }
188    }
189
190    /// `expr LIKE pattern ESCAPE expr` — one production with three operands.
191    #[test]
192    fn escape_is_a_third_operand_of_like() {
193        assert_eq!(
194            sql(quote("a").like_escape(s("100\\%"), s("\\"))),
195            r#"("a" LIKE '100\%' ESCAPE '\')"#
196        );
197        assert_eq!(
198            sql(quote("a").not_like_escape(s("100\\%"), s("\\"))),
199            r#"("a" NOT LIKE '100\%' ESCAPE '\')"#
200        );
201    }
202
203    #[test]
204    fn collate_quotes_its_sequence_name() {
205        assert_eq!(
206            sql(quote("a").collate("NOCASE")),
207            r#"("a" COLLATE "NOCASE")"#
208        );
209    }
210
211    #[test]
212    fn a_sqlite_operator_still_applies_after_a_core_one() {
213        assert_eq!(
214            sql(quote("a").is_null().or(quote("b").glob(s("x*")))),
215            r#"(("a" IS NULL) OR ("b" GLOB 'x*'))"#
216        );
217    }
218}