Skip to main content

keelson_core/expr/
chain.rs

1use super::convert::{IntoExpr, IntoExprList, IntoIdent};
2use super::node::Expr;
3
4/// The operator chain: `quote("age").gte(arg(21))`.
5///
6/// Every method builds a node around `self` and then applies the
7/// parenthesisation rule ([`Expr::grouped`]), which is what bob's `expr.X` does
8/// at the end of each of its chain methods. Because the rule is idempotent and
9/// treats [`Expr::Group`] as atomic, a chain value is always "already
10/// parenthesised or not needing it", and successive steps never pile up
11/// redundant parentheses.
12///
13/// # How a dialect adds an operator
14///
15/// bob parameterises `Chain[T, B]` over the dialect's own expression type so that
16/// `keelson-psql` can add `@>` without touching core. In Rust the same
17/// extensibility comes from a trait with default methods, and there are two ways
18/// to take it — neither needs a change to core.
19///
20/// **An extension trait**, which is the normal case and needs no new type:
21///
22/// ```
23/// use keelson_core::expr::{Chain, Expr, IntoExpr, arg, quote};
24///
25/// // PostgreSQL-only operators, reachable only where this trait is imported.
26/// trait PsqlOps: Chain {
27///     /// `@>` — contains. Nothing but a symbol, so `op` is the whole story.
28///     fn contains(self, rhs: impl IntoExpr) -> Self {
29///         self.op("@>", rhs)
30///     }
31///
32///     /// A shape `op` cannot express, built through `step` so that the
33///     /// parenthesisation rule is still applied for us.
34///     fn between_symmetric(self, a: impl IntoExpr, b: impl IntoExpr) -> Self {
35///         self.step(move |lhs| {
36///             Expr::join((lhs, Expr::raw("BETWEEN SYMMETRIC"), a, Expr::raw("AND"), b))
37///         })
38///     }
39/// }
40///
41/// impl<T: Chain> PsqlOps for T {}
42///
43/// # #[derive(Debug)] struct Psql;
44/// # impl keelson_core::Dialect for Psql {
45/// #     fn write_arg(&self, w: &mut keelson_core::SqlWriter<'_>, position: usize) {
46/// #         w.push_str("$"); w.push_str(&position.to_string());
47/// #     }
48/// #     fn write_quoted(&self, w: &mut keelson_core::SqlWriter<'_>, s: &str) {
49/// #         w.push_str("\""); w.push_str(s); w.push_str("\"");
50/// #     }
51/// # }
52/// let (sql, _) = keelson_core::build(&Psql, &quote("tags").contains(arg("x")))?;
53/// assert_eq!(sql, r#"("tags" @> $1)"#);
54/// # Ok::<_, keelson_core::Error>(())
55/// ```
56///
57/// **A newtype**, when the dialect wants its operators to be unreachable from
58/// another dialect's expressions rather than merely un-imported:
59///
60/// ```
61/// use keelson_core::expr::{Chain, Expr, IntoExpr, IntoExprList};
62///
63/// #[derive(Debug, Clone)]
64/// struct PsqlExpr(Expr);
65///
66/// impl IntoExpr for PsqlExpr {
67///     fn into_expr(self) -> Expr { self.0 }
68/// }
69///
70/// impl IntoExprList for PsqlExpr {
71///     fn into_expr_list(self) -> Vec<Expr> { vec![self.0] }
72/// }
73///
74/// impl Chain for PsqlExpr {
75///     fn from_expr(e: Expr) -> Self { PsqlExpr(e) }
76/// }
77///
78/// impl PsqlExpr {
79///     fn contains(self, rhs: impl IntoExpr) -> Self { self.op("@>", rhs) }
80/// }
81///
82/// // Core operators return `PsqlExpr`, so a dialect operator still applies after
83/// // one: the chain never escapes into a type that has lost `@>`.
84/// let e = PsqlExpr::from_expr(Expr::ident("tags")).contains("'{a}'").is_not_null();
85/// ```
86///
87/// Either way [`step`](Self::step) is the only thing an added operator needs, and
88/// the parenthesisation rule is applied for it.
89///
90/// # Why the supertraits
91///
92/// A chain has to be able to hand its expression back — to nest it in another
93/// operator, or to store it in a clause. That is exactly what [`IntoExpr`] means,
94/// so `Chain` requires it instead of declaring a second method with the same job,
95/// and any chain value can therefore be passed to any slot in the library.
96/// [`IntoExprList`] is required for the same reason one step out: it is what lets
97/// `a.and(b)` take another chain value directly instead of the one-element tuple
98/// `(b,)`. Both are one line for a dialect newtype, and requiring them here is how
99/// that requirement gets stated rather than discovered.
100// `is_null`, `is_not_null` and `is_distinct_from` take `self` by value like every
101// other operator here. They are SQL keywords being spelled out, not Rust
102// predicates returning `bool`, and renaming them to satisfy the convention would
103// make the chain read less like the SQL it produces.
104#[allow(clippy::wrong_self_convention)]
105pub trait Chain: IntoExpr + IntoExprList + Sized {
106    /// Wrap a finished expression back into the chain type.
107    ///
108    /// The inverse of [`IntoExpr::into_expr`]; the two together are all a dialect
109    /// has to supply.
110    fn from_expr(e: Expr) -> Self;
111
112    /// One chain step: build a node from this expression, apply the
113    /// parenthesisation rule, and return a chain again.
114    ///
115    /// Every operator below is a one-line call to this, and so is every operator
116    /// a dialect adds.
117    #[must_use]
118    fn step(self, f: impl FnOnce(Expr) -> Expr) -> Self {
119        Self::from_expr(f(self.into_expr()).grouped())
120    }
121
122    /// An arbitrary infix operator: `self op rhs`.
123    ///
124    /// The generic escape hatch — a dialect-specific operator that needs nothing
125    /// but a symbol is this call and no new code in core.
126    #[must_use]
127    fn op(self, op: &'static str, rhs: impl IntoExpr) -> Self {
128        self.step(move |lhs| Expr::binary(lhs, op, rhs))
129    }
130
131    /// `self = rhs`.
132    #[must_use]
133    fn eq(self, rhs: impl IntoExpr) -> Self {
134        self.op("=", rhs)
135    }
136
137    /// `self <> rhs`. The standard spelling, which every dialect accepts.
138    #[must_use]
139    fn ne(self, rhs: impl IntoExpr) -> Self {
140        self.op("<>", rhs)
141    }
142
143    /// `self < rhs`.
144    #[must_use]
145    fn lt(self, rhs: impl IntoExpr) -> Self {
146        self.op("<", rhs)
147    }
148
149    /// `self <= rhs`.
150    #[must_use]
151    fn lte(self, rhs: impl IntoExpr) -> Self {
152        self.op("<=", rhs)
153    }
154
155    /// `self > rhs`.
156    #[must_use]
157    fn gt(self, rhs: impl IntoExpr) -> Self {
158        self.op(">", rhs)
159    }
160
161    /// `self >= rhs`.
162    #[must_use]
163    fn gte(self, rhs: impl IntoExpr) -> Self {
164        self.op(">=", rhs)
165    }
166
167    /// `self IN (a, b, c)`.
168    ///
169    /// The operands are always parenthesised, so a single sub-select operand
170    /// comes out as `IN (SELECT ..)` and a row list as `IN ((..), (..))`.
171    #[must_use]
172    fn in_(self, vals: impl IntoExprList) -> Self {
173        self.step(move |lhs| Expr::binary(lhs, "IN", Expr::group(vals)))
174    }
175
176    /// `self NOT IN (a, b, c)`.
177    #[must_use]
178    fn not_in(self, vals: impl IntoExprList) -> Self {
179        self.step(move |lhs| Expr::binary(lhs, "NOT IN", Expr::group(vals)))
180    }
181
182    /// `self IS NULL`.
183    #[must_use]
184    fn is_null(self) -> Self {
185        self.step(|lhs| Expr::postfix(lhs, "IS NULL"))
186    }
187
188    /// `self IS NOT NULL`.
189    #[must_use]
190    fn is_not_null(self) -> Self {
191        self.step(|lhs| Expr::postfix(lhs, "IS NOT NULL"))
192    }
193
194    /// `self IS DISTINCT FROM rhs`.
195    #[must_use]
196    fn is_distinct_from(self, rhs: impl IntoExpr) -> Self {
197        self.op("IS DISTINCT FROM", rhs)
198    }
199
200    /// `self IS NOT DISTINCT FROM rhs`.
201    #[must_use]
202    fn is_not_distinct_from(self, rhs: impl IntoExpr) -> Self {
203        self.op("IS NOT DISTINCT FROM", rhs)
204    }
205
206    /// `self BETWEEN a AND b`.
207    #[must_use]
208    fn between(self, a: impl IntoExpr, b: impl IntoExpr) -> Self {
209        self.step(move |lhs| Expr::join((lhs, Expr::raw("BETWEEN"), a, Expr::raw("AND"), b)))
210    }
211
212    /// `self NOT BETWEEN a AND b`.
213    #[must_use]
214    fn not_between(self, a: impl IntoExpr, b: impl IntoExpr) -> Self {
215        self.step(move |lhs| Expr::join((lhs, Expr::raw("NOT BETWEEN"), a, Expr::raw("AND"), b)))
216    }
217
218    /// `self LIKE rhs`.
219    #[must_use]
220    fn like(self, rhs: impl IntoExpr) -> Self {
221        self.op("LIKE", rhs)
222    }
223
224    /// `self || a || b` — string concatenation.
225    #[must_use]
226    fn concat(self, others: impl IntoExprList) -> Self {
227        self.step(move |lhs| Expr::join_with(" || ", prepend(lhs, others)))
228    }
229
230    /// `self AND a AND b`.
231    #[must_use]
232    fn and(self, others: impl IntoExprList) -> Self {
233        self.step(move |lhs| Expr::join_with(" AND ", prepend(lhs, others)))
234    }
235
236    /// `self OR a OR b`.
237    #[must_use]
238    fn or(self, others: impl IntoExprList) -> Self {
239        self.step(move |lhs| Expr::join_with(" OR ", prepend(lhs, others)))
240    }
241
242    /// `self + rhs`.
243    #[must_use]
244    fn plus(self, rhs: impl IntoExpr) -> Self {
245        self.op("+", rhs)
246    }
247
248    /// `self - rhs`.
249    #[must_use]
250    fn minus(self, rhs: impl IntoExpr) -> Self {
251        self.op("-", rhs)
252    }
253
254    /// `self AS "alias"`.
255    ///
256    /// This one ends the chain — it returns an [`Expr`], not `Self`. An alias is
257    /// not an operand: nothing may be applied to `x AS "y"`, and unlike every
258    /// other method here the result is deliberately *not* parenthesised, because
259    /// `(x AS "y")` is a syntax error in a select list.
260    fn as_(self, alias: impl IntoIdent) -> Expr {
261        Expr::Binary {
262            lhs: Box::new(self.into_expr()),
263            op: "AS",
264            rhs: Box::new(Expr::ident(alias)),
265        }
266    }
267}
268
269/// `self` first, then the rest — the shape every variadic chain method needs.
270fn prepend(first: Expr, rest: impl IntoExprList) -> Vec<Expr> {
271    let mut exprs = rest.into_expr_list();
272    exprs.insert(0, first);
273    exprs
274}
275
276/// An [`Expr`] is its own chain, so operators are available without any wrapper
277/// type. A dialect that wants its own type implements [`Chain`] for that instead.
278impl Chain for Expr {
279    fn from_expr(e: Expr) -> Expr {
280        e
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use keelson_sqlcheck::testing::assert_frag_sql;
287
288    use super::super::{arg, arg_group, literal, quote, raw};
289    use super::*;
290    use crate::dialect::testing::{Numbered, TestDialect};
291    use crate::value::Value;
292    use crate::writer::build;
293
294    /// An operator expression is a fragment. A boolean one is judged where a
295    /// condition goes and a scalar one where a value goes — putting either in the
296    /// other's place is a mistake the grammar or the engine will name.
297    const COND: &str = r#"SELECT "id" FROM users WHERE {}"#;
298    const VALUE: &str = r#"SELECT {} FROM users"#;
299    const POST_COND: &str = r#"SELECT "id" FROM posts WHERE {}"#;
300
301    fn sql(e: Expr) -> String {
302        build(&Numbered, &e).expect("render").0
303    }
304
305    #[test]
306    fn a_comparison_is_parenthesised_exactly_once() {
307        assert_frag_sql(COND, &sql(quote("age").gte(arg(21i32))), r#"("age" >= $1)"#);
308    }
309
310    #[test]
311    fn every_comparison_operator_uses_its_standard_spelling() {
312        // Chapter 9 of the PostgreSQL manual for each spelling; `<>` rather than
313        // `!=` because that is the standard one.
314        let conditions = [
315            (quote("age").eq(raw("id")), r#"("age" = id)"#),
316            (quote("age").ne(raw("id")), r#"("age" <> id)"#),
317            (quote("age").lt(raw("id")), r#"("age" < id)"#),
318            (quote("age").lte(raw("id")), r#"("age" <= id)"#),
319            (quote("age").gt(raw("id")), r#"("age" > id)"#),
320            (quote("age").gte(raw("id")), r#"("age" >= id)"#),
321            (quote("name").like(literal("b%")), r#"("name" LIKE 'b%')"#),
322        ];
323        for (e, expected) in conditions {
324            assert_frag_sql(COND, &sql(e), expected);
325        }
326
327        // Arithmetic is a value, not a condition, so it goes in the select list.
328        let values = [
329            (quote("age").plus(1i32), r#"("age" + 1)"#),
330            (quote("age").minus(quote("id")), r#"("age" - "id")"#),
331        ];
332        for (e, expected) in values {
333            assert_frag_sql(VALUE, &sql(e), expected);
334        }
335
336        // A dialect operator core has never heard of, reached through `op`.
337        assert_frag_sql(
338            COND,
339            &sql(raw("ARRAY[1, 2]").op("@>", raw("ARRAY[1]"))),
340            "(ARRAY[1, 2] @> ARRAY[1])",
341        );
342    }
343
344    #[test]
345    fn null_tests_are_postfix() {
346        assert_frag_sql(COND, &sql(quote("age").is_null()), r#"("age" IS NULL)"#);
347        assert_frag_sql(
348            COND,
349            &sql(quote("age").is_not_null()),
350            r#"("age" IS NOT NULL)"#,
351        );
352    }
353
354    #[test]
355    fn distinct_from_is_an_infix_keyword_operator() {
356        assert_frag_sql(
357            COND,
358            &sql(quote("age").is_distinct_from(quote("id"))),
359            r#"("age" IS DISTINCT FROM "id")"#,
360        );
361        assert_frag_sql(
362            COND,
363            &sql(quote("age").is_not_distinct_from(quote("id"))),
364            r#"("age" IS NOT DISTINCT FROM "id")"#,
365        );
366    }
367
368    #[test]
369    fn between_keeps_its_three_part_shape() {
370        assert_frag_sql(
371            COND,
372            &sql(quote("age").between(arg(1i32), arg(2i32))),
373            r#"("age" BETWEEN $1 AND $2)"#,
374        );
375        assert_frag_sql(
376            COND,
377            &sql(quote("age").not_between(arg(1i32), arg(2i32))),
378            r#"("age" NOT BETWEEN $1 AND $2)"#,
379        );
380    }
381
382    #[test]
383    fn in_always_parenthesises_its_operands() {
384        assert_frag_sql(
385            POST_COND,
386            &sql(quote("status").in_((literal("A"), literal("B")))),
387            r#"("status" IN ('A', 'B'))"#,
388        );
389        assert_frag_sql(
390            COND,
391            &sql(quote("id").not_in(arg(1i32))),
392            r#"("id" NOT IN ($1))"#,
393        );
394    }
395
396    /// The shape bob's `select with grouped IN` fixture pins: a row constructor
397    /// on the left, a list of row constructors on the right.
398    #[test]
399    fn a_row_constructor_in_a_list_of_row_constructors() {
400        let e = Expr::group((quote("id"), quote("user_id")))
401            .in_((arg_group([1i32, 2]), arg_group([3i32, 4])));
402        let (rendered, args) = build(&Numbered, &e).unwrap();
403        assert_frag_sql(
404            POST_COND,
405            &rendered,
406            r#"(("id", "user_id") IN (($1, $2), ($3, $4)))"#,
407        );
408        assert_eq!(args.len(), 4);
409    }
410
411    #[test]
412    fn boolean_chains_take_one_operand_or_several() {
413        assert_frag_sql(
414            COND,
415            &sql(quote("is_active").and(raw("age > 1"))),
416            r#"("is_active" AND age > 1)"#,
417        );
418        assert_frag_sql(
419            COND,
420            &sql(quote("is_active").or((raw("age > 1"), raw("age < 9")))),
421            r#"("is_active" OR age > 1 OR age < 9)"#,
422        );
423    }
424
425    /// The `psql upsert` fixture's `SET` value, which is where concatenation is
426    /// pinned. `EXCLUDED` only exists inside `ON CONFLICT DO UPDATE`, so that is
427    /// the frame.
428    #[test]
429    fn concat_joins_with_the_pipe_operator() {
430        let e = raw(r#"EXCLUDED."name""#).concat((
431            literal(" (formerly "),
432            quote(("tags", "name")),
433            literal(")"),
434        ));
435        assert_frag_sql(
436            r#"INSERT INTO tags ("id", "name") VALUES (1, 'rust') ON CONFLICT ("id") DO UPDATE SET "name" = {}"#,
437            &sql(e),
438            r#"(EXCLUDED."name" || ' (formerly ' || "tags"."name" || ')')"#,
439        );
440    }
441
442    #[test]
443    fn nesting_a_chain_in_a_chain_adds_no_extra_parentheses() {
444        // Each step's result is already atomic, so re-applying the rule is a
445        // no-op. This is the invariant that replaces bob's "already a chain
446        // value" arm.
447        let e = quote("age").eq(arg(1i32)).and(quote("id").eq(arg(2i32)));
448        assert_frag_sql(COND, &sql(e), r#"(("age" = $1) AND ("id" = $2))"#);
449    }
450
451    #[test]
452    fn an_alias_ends_the_chain_and_is_not_parenthesised() {
453        let e = quote("age").minus(quote("id")).as_("difference");
454        assert_frag_sql(VALUE, &sql(e), r#"("age" - "id") AS "difference""#);
455    }
456
457    #[test]
458    fn arguments_are_numbered_left_to_right_across_a_whole_chain() {
459        let e = quote("age")
460            .between(arg(1i32), arg(2i32))
461            .and(quote("id").in_((arg(3i32), arg(4i32))));
462        let (rendered, args) = build(&Numbered, &e).unwrap();
463        assert_frag_sql(
464            COND,
465            &rendered,
466            r#"(("age" BETWEEN $1 AND $2) AND ("id" IN ($3, $4)))"#,
467        );
468        assert_eq!(
469            args,
470            vec![Value::I32(1), Value::I32(2), Value::I32(3), Value::I32(4)]
471        );
472    }
473
474    /// The extension shape the three dialect crates depend on: a trait of default
475    /// methods over `Chain`, blanket-implemented, adding an operator core has
476    /// never heard of.
477    #[test]
478    fn a_dialect_can_add_an_operator_without_touching_core() {
479        trait PsqlOps: Chain {
480            fn contains(self, rhs: impl IntoExpr) -> Self {
481                self.op("@>", rhs)
482            }
483
484            fn between_symmetric(self, a: impl IntoExpr, b: impl IntoExpr) -> Self {
485                self.step(move |lhs| {
486                    Expr::join((lhs, Expr::raw("BETWEEN SYMMETRIC"), a, Expr::raw("AND"), b))
487                })
488            }
489        }
490        impl<T: Chain> PsqlOps for T {}
491
492        assert_frag_sql(
493            COND,
494            &sql(raw("ARRAY[1, 2]").contains(raw("ARRAY[1]"))),
495            "(ARRAY[1, 2] @> ARRAY[1])",
496        );
497        assert_frag_sql(
498            COND,
499            &sql(quote("age").between_symmetric(arg(1i32), arg(2i32))),
500            r#"("age" BETWEEN SYMMETRIC $1 AND $2)"#,
501        );
502    }
503
504    /// The other extension shape: a dialect newtype, so its operators cannot be
505    /// reached from another dialect's expressions at all.
506    ///
507    /// Not judged: `GLOB` is SQLite's, and the dialect this renders under is
508    /// SQLite-shaped (`?N`, `:name`). PostgreSQL — the judge reachable from
509    /// `keelson-core` — has neither, and `keelson-sqlite` is where the operator
510    /// itself is checked. What is asserted here is that the chain stays in the
511    /// newtype.
512    #[test]
513    fn a_dialect_newtype_keeps_the_whole_chain_in_its_own_type() {
514        #[derive(Debug, Clone)]
515        struct SqliteExpr(Expr);
516
517        impl IntoExpr for SqliteExpr {
518            fn into_expr(self) -> Expr {
519                self.0
520            }
521        }
522
523        impl IntoExprList for SqliteExpr {
524            fn into_expr_list(self) -> Vec<Expr> {
525                vec![self.0]
526            }
527        }
528
529        impl Chain for SqliteExpr {
530            fn from_expr(e: Expr) -> Self {
531                SqliteExpr(e)
532            }
533        }
534
535        impl SqliteExpr {
536            fn glob(self, rhs: impl IntoExpr) -> Self {
537                self.op("GLOB", rhs)
538            }
539        }
540
541        // Chaining a core operator returns the dialect's type, so a
542        // dialect-specific one still applies afterwards.
543        let e = SqliteExpr::from_expr(Expr::ident("name"))
544            .glob(literal("a*"))
545            .and(SqliteExpr::from_expr(Expr::ident("b")).is_null());
546        let (s, _) = build(&TestDialect, &e.into_expr()).unwrap();
547        assert_eq!(s, r#"(("name" GLOB 'a*') AND ("b" IS NULL))"#);
548    }
549}