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}