Skip to main content

vantage_sql/primitives/
logical.rs

1//! Logical operators for SQL conditions: `or_()` and `and_()`, and the
2//! [`ConditionGroup`] that they make.
3//!
4//! A group writes one set of brackets around the full chain. It writes
5//! no brackets around each operand: `(a OR b OR c)`. A chain stays flat
6//! while the operator does not change, and thus `a.or_(b).or_(c)` gives
7//! `(a OR b OR c)`. To make a different group, nest the calls.
8//! `a.or_(b.or_(c))` gives `(a OR (b OR c))`, because the inner group is
9//! an operand and it writes its own brackets.
10//!
11//! The group writes the brackets. The statement renderer does not.
12//! `WHERE` joins its conditions with a plain `AND`, and each group
13//! arrives complete. This keeps the conditions of a table when a search
14//! runs on top of them. The query says `role = 'admin' AND (a OR b)`. It
15//! does not say `role = 'admin' AND a OR b`, which means
16//! `(role = 'admin' AND a) OR b`, because `AND` binds more tightly than
17//! `OR`.
18
19use vantage_expressions::{Expression, Expressive, ExpressiveEnum};
20
21/// Conditions that one logical operator joins, written as a single
22/// group in brackets. To make a group, call [`or_`] or [`and_`], or use
23/// the `or_` and `and_` methods on a column or an identifier.
24#[derive(Clone)]
25pub struct ConditionGroup<T> {
26    operator: &'static str,
27    operands: Vec<Expression<T>>,
28}
29
30impl<T> std::fmt::Debug for ConditionGroup<T> {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.debug_struct("ConditionGroup")
33            .field("operator", &self.operator)
34            .field("operands", &self.operands.len())
35            .finish()
36    }
37}
38
39impl<T: Clone> ConditionGroup<T> {
40    pub(crate) fn new(operator: &'static str, operands: Vec<Expression<T>>) -> Self {
41        Self { operator, operands }
42    }
43
44    /// Adds a condition with `OR`. If this group is an `OR` chain, the
45    /// condition goes into the same group.
46    ///
47    /// This method is inherent. Rust selects it before the `or_` of the
48    /// operation trait, and this keeps a chain flat.
49    pub fn or_(self, other: impl Expressive<T>) -> Self {
50        self.extend("OR", other.expr())
51    }
52
53    /// Adds a condition with `AND`. If this group is an `AND` chain, the
54    /// condition goes into the same group.
55    pub fn and_(self, other: impl Expressive<T>) -> Self {
56        self.extend("AND", other.expr())
57    }
58
59    fn extend(mut self, operator: &'static str, other: Expression<T>) -> Self {
60        if self.operator == operator {
61            self.operands.push(other);
62            self
63        } else {
64            // The operator changes. The chain to this point becomes one
65            // operand of the new group. It keeps its brackets and its
66            // meaning.
67            Self::new(operator, vec![self.expr(), other])
68        }
69    }
70}
71
72impl<T: Clone> Expressive<T> for ConditionGroup<T> {
73    fn expr(&self) -> Expression<T> {
74        let separator = format!(" {} ", self.operator);
75        let template = std::iter::repeat_n("{}", self.operands.len())
76            .collect::<Vec<_>>()
77            .join(&separator);
78        Expression::new(
79            format!("({template})"),
80            self.operands
81                .iter()
82                .cloned()
83                .map(ExpressiveEnum::Nested)
84                .collect(),
85        )
86    }
87}
88
89/// Joins two conditions with `OR`: `(lhs OR rhs)`.
90///
91/// ```ignore
92/// use vantage_sql::primitives::*;
93///
94/// or_(ident("role").eq("admin"), ident("role").eq("superuser"))
95/// // => ("role" = 'admin' OR "role" = 'superuser')
96/// ```
97pub fn or_<T: Clone>(lhs: impl Expressive<T>, rhs: impl Expressive<T>) -> ConditionGroup<T> {
98    ConditionGroup::new("OR", vec![lhs.expr(), rhs.expr()])
99}
100
101/// Joins two conditions with `AND`: `(lhs AND rhs)`.
102///
103/// `with_condition()` joins its conditions with `AND` already. Use
104/// `and_()` when you must make a group inside an `or_()`:
105///
106/// ```ignore
107/// use vantage_sql::primitives::*;
108///
109/// // ((price > 100 AND in_stock = 1) OR featured = 1)
110/// or_(
111///     and_(ident("price").gt(100), ident("in_stock").eq(true)),
112///     ident("featured").eq(true),
113/// )
114/// ```
115pub fn and_<T: Clone>(lhs: impl Expressive<T>, rhs: impl Expressive<T>) -> ConditionGroup<T> {
116    ConditionGroup::new("AND", vec![lhs.expr(), rhs.expr()])
117}