use crate::iden::{ColumnRef, Ident, IntoIden};
use crate::query::Select;
use crate::value::Value;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Order {
Asc,
Desc,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum BinOp {
Eq,
Ne,
Lt,
Lte,
Gt,
Gte,
And,
Or,
Is,
IsNot,
Add,
Sub,
Mul,
Div,
Mod,
Concat,
JsonArrow,
JsonArrowText,
Match,
}
impl BinOp {
pub(crate) fn sql(self) -> &'static str {
match self {
BinOp::Eq => "=",
BinOp::Ne => "<>",
BinOp::Lt => "<",
BinOp::Lte => "<=",
BinOp::Gt => ">",
BinOp::Gte => ">=",
BinOp::And => "AND",
BinOp::Or => "OR",
BinOp::Is => "IS",
BinOp::IsNot => "IS NOT",
BinOp::Add => "+",
BinOp::Sub => "-",
BinOp::Mul => "*",
BinOp::Div => "/",
BinOp::Mod => "%",
BinOp::Concat => "||",
BinOp::JsonArrow => "->",
BinOp::JsonArrowText => "->>",
BinOp::Match => "MATCH",
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
#[must_use = "an expression does nothing until used in a statement"]
pub enum Expr {
Column(ColumnRef),
Value(Value),
Tuple(Vec<Expr>),
Binary(Box<Expr>, BinOp, Box<Expr>),
Not(Box<Expr>),
Neg(Box<Expr>),
IsNull(Box<Expr>),
IsNotNull(Box<Expr>),
In(Box<Expr>, Box<Expr>),
NotIn(Box<Expr>, Box<Expr>),
Between(Box<Expr>, Box<Expr>, Box<Expr>),
NotBetween(Box<Expr>, Box<Expr>, Box<Expr>),
Like {
expr: Box<Expr>,
pattern: Box<Expr>,
negated: bool,
escape: Option<char>,
},
Func(Func),
Subquery(Box<Select>),
Exists(Box<Select>),
Case(Vec<(Expr, Expr)>, Option<Box<Expr>>),
Cast(Box<Expr>, &'static str),
Alias(Box<Expr>, Ident),
Raw(String, Vec<Value>),
Paren(Box<Expr>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Func {
pub name: &'static str,
pub args: Vec<Expr>,
pub distinct: bool,
}
impl Func {
pub fn call(name: &'static str, args: impl IntoIterator<Item = Expr>) -> Expr {
Expr::Func(Func {
name,
args: args.into_iter().collect(),
distinct: false,
})
}
pub fn count(expr: Expr) -> Expr {
Self::call("COUNT", [expr])
}
pub fn count_star() -> Expr {
Self::call("COUNT", [Expr::Column(ColumnRef::Asterisk)])
}
pub fn count_distinct(expr: Expr) -> Expr {
Expr::Func(Func {
name: "COUNT",
args: vec![expr],
distinct: true,
})
}
pub fn max(expr: Expr) -> Expr {
Self::call("MAX", [expr])
}
pub fn min(expr: Expr) -> Expr {
Self::call("MIN", [expr])
}
pub fn sum(expr: Expr) -> Expr {
Self::call("SUM", [expr])
}
pub fn avg(expr: Expr) -> Expr {
Self::call("AVG", [expr])
}
pub fn coalesce(args: impl IntoIterator<Item = Expr>) -> Expr {
Self::call("COALESCE", args)
}
pub fn lower(expr: Expr) -> Expr {
Self::call("LOWER", [expr])
}
pub fn upper(expr: Expr) -> Expr {
Self::call("UPPER", [expr])
}
pub fn length(expr: Expr) -> Expr {
Self::call("LENGTH", [expr])
}
pub fn abs(expr: Expr) -> Expr {
Self::call("ABS", [expr])
}
pub fn if_null(a: Expr, b: Expr) -> Expr {
Self::call("IFNULL", [a, b])
}
pub fn json_extract(json: Expr, path: impl Into<Value>) -> Expr {
Self::call("json_extract", [json, Expr::Value(path.into())])
}
pub fn vector_distance_cos(a: Expr, b: Expr) -> Expr {
Self::call("vector_distance_cos", [a, b])
}
pub fn vector_distance_l2(a: Expr, b: Expr) -> Expr {
Self::call("vector_distance_l2", [a, b])
}
pub fn vector32(expr: Expr) -> Expr {
Self::call("vector32", [expr])
}
pub fn fts_match(column: Expr, query: impl Into<Value>) -> Expr {
Self::call("fts_match", [column, Expr::Value(query.into())])
}
pub fn fts_score(column: Expr) -> Expr {
Self::call("fts_score", [column])
}
}
fn bin(lhs: Expr, op: BinOp, rhs: Expr) -> Expr {
Expr::Binary(Box::new(lhs), op, Box::new(rhs))
}
impl Expr {
pub fn col(column: impl Into<ColumnRef>) -> Self {
Expr::Column(column.into())
}
pub fn val(value: impl Into<Value>) -> Self {
Expr::Value(value.into())
}
pub fn tuple<V: Into<Value>>(values: impl IntoIterator<Item = V>) -> Self {
Expr::Tuple(values.into_iter().map(|v| Expr::Value(v.into())).collect())
}
pub fn subquery(select: Select) -> Self {
Expr::Subquery(Box::new(select))
}
pub fn exists(select: Select) -> Self {
Expr::Exists(Box::new(select))
}
pub fn cust(sql: impl Into<String>) -> Self {
Expr::Raw(sql.into(), Vec::new())
}
pub fn cust_with_values<V: Into<Value>>(
sql: impl Into<String>,
values: impl IntoIterator<Item = V>,
) -> Self {
Expr::Raw(sql.into(), values.into_iter().map(Into::into).collect())
}
pub fn eq(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Eq, rhs.into())
}
pub fn ne(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Ne, rhs.into())
}
pub fn lt(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Lt, rhs.into())
}
pub fn lte(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Lte, rhs.into())
}
pub fn gt(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Gt, rhs.into())
}
pub fn gte(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Gte, rhs.into())
}
pub fn and(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::And, rhs.into())
}
pub fn or(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Or, rhs.into())
}
#[allow(
clippy::should_implement_trait,
reason = "the SQL-flavoured name reads as the operator it builds"
)]
pub fn not(self) -> Self {
Expr::Not(Box::new(self))
}
#[allow(
clippy::should_implement_trait,
reason = "the SQL-flavoured name reads as the operator it builds"
)]
pub fn neg(self) -> Self {
Expr::Neg(Box::new(self))
}
pub fn is_null(self) -> Self {
Expr::IsNull(Box::new(self))
}
pub fn is_not_null(self) -> Self {
Expr::IsNotNull(Box::new(self))
}
pub fn is(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Is, rhs.into())
}
pub fn is_not(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::IsNot, rhs.into())
}
pub fn like(self, pattern: impl Into<Value>) -> Self {
Expr::Like {
expr: Box::new(self),
pattern: Box::new(Expr::Value(pattern.into())),
negated: false,
escape: None,
}
}
pub fn not_like(self, pattern: impl Into<Value>) -> Self {
Expr::Like {
expr: Box::new(self),
pattern: Box::new(Expr::Value(pattern.into())),
negated: true,
escape: None,
}
}
pub fn like_escaped(self, pattern: impl Into<Value>, escape: char) -> Self {
Expr::Like {
expr: Box::new(self),
pattern: Box::new(Expr::Value(pattern.into())),
negated: false,
escape: Some(escape),
}
}
pub fn matches(self, query: impl Into<Value>) -> Self {
bin(self, BinOp::Match, Expr::Value(query.into()))
}
pub fn is_in<V: Into<Value>>(self, values: impl IntoIterator<Item = V>) -> Self {
Expr::In(Box::new(self), Box::new(Expr::tuple(values)))
}
pub fn is_not_in<V: Into<Value>>(self, values: impl IntoIterator<Item = V>) -> Self {
Expr::NotIn(Box::new(self), Box::new(Expr::tuple(values)))
}
pub fn in_subquery(self, select: Select) -> Self {
Expr::In(Box::new(self), Box::new(Expr::subquery(select)))
}
pub fn not_in_subquery(self, select: Select) -> Self {
Expr::NotIn(Box::new(self), Box::new(Expr::subquery(select)))
}
pub fn between(self, a: impl Into<Expr>, b: impl Into<Expr>) -> Self {
Expr::Between(Box::new(self), Box::new(a.into()), Box::new(b.into()))
}
pub fn not_between(self, a: impl Into<Expr>, b: impl Into<Expr>) -> Self {
Expr::NotBetween(Box::new(self), Box::new(a.into()), Box::new(b.into()))
}
#[allow(
clippy::should_implement_trait,
reason = "the SQL-flavoured name reads as the operator it builds"
)]
pub fn add(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Add, rhs.into())
}
#[allow(
clippy::should_implement_trait,
reason = "the SQL-flavoured name reads as the operator it builds"
)]
pub fn sub(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Sub, rhs.into())
}
#[allow(
clippy::should_implement_trait,
reason = "the SQL-flavoured name reads as the operator it builds"
)]
pub fn mul(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Mul, rhs.into())
}
#[allow(
clippy::should_implement_trait,
reason = "the SQL-flavoured name reads as the operator it builds"
)]
pub fn div(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Div, rhs.into())
}
#[allow(
clippy::should_implement_trait,
reason = "the SQL-flavoured name reads as the operator it builds"
)]
pub fn rem(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Mod, rhs.into())
}
pub fn concat(self, rhs: impl Into<Expr>) -> Self {
bin(self, BinOp::Concat, rhs.into())
}
pub fn json_get(self, path: impl Into<Value>) -> Self {
bin(self, BinOp::JsonArrow, Expr::Value(path.into()))
}
pub fn json_get_text(self, path: impl Into<Value>) -> Self {
bin(self, BinOp::JsonArrowText, Expr::Value(path.into()))
}
pub fn cast_as(self, ty: &'static str) -> Self {
Expr::Cast(Box::new(self), ty)
}
pub fn alias(self, alias: impl IntoIden) -> Self {
Expr::Alias(Box::new(self), alias.into_iden())
}
pub fn paren(self) -> Self {
Expr::Paren(Box::new(self))
}
pub fn case(whens: Vec<(Expr, Expr)>, otherwise: Option<Expr>) -> Self {
Expr::Case(whens, otherwise.map(Box::new))
}
pub fn contains(self, s: &str) -> Self {
self.like_escaped(format!("%{}%", escape_like(s)), LIKE_ESCAPE)
}
pub fn starts_with(self, s: &str) -> Self {
self.like_escaped(format!("{}%", escape_like(s)), LIKE_ESCAPE)
}
pub fn ends_with(self, s: &str) -> Self {
self.like_escaped(format!("%{}", escape_like(s)), LIKE_ESCAPE)
}
}
const LIKE_ESCAPE: char = '\\';
fn escape_like(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c == '%' || c == '_' || c == LIKE_ESCAPE {
out.push(LIKE_ESCAPE);
}
out.push(c);
}
out
}
impl<T: Into<Value>> From<T> for Expr {
fn from(value: T) -> Self {
Expr::Value(value.into())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Condition {
all: bool,
negate: bool,
parts: Vec<Expr>,
}
impl Condition {
pub fn all() -> Self {
Self {
all: true,
negate: false,
parts: Vec::new(),
}
}
pub fn any() -> Self {
Self {
all: false,
negate: false,
parts: Vec::new(),
}
}
#[must_use]
#[allow(
clippy::should_implement_trait,
reason = "the name reads as the SQL it builds"
)]
pub fn add(mut self, part: impl IntoCondition) -> Self {
if let Some(expr) = part.into_condition().into_expr() {
self.parts.push(expr);
}
self
}
#[must_use]
pub fn add_option(self, part: Option<impl IntoCondition>) -> Self {
match part {
Some(p) => self.add(p),
None => self,
}
}
#[must_use]
#[allow(
clippy::should_implement_trait,
reason = "the name reads as the SQL it builds"
)]
pub fn not(mut self) -> Self {
self.negate = !self.negate;
self
}
pub fn is_empty(&self) -> bool {
self.parts.is_empty()
}
pub fn len(&self) -> usize {
self.parts.len()
}
pub fn into_expr(self) -> Option<Expr> {
let op = if self.all { BinOp::And } else { BinOp::Or };
let needs_paren = !self.all || self.negate;
let negate = self.negate;
let mut iter = self.parts.into_iter();
let first = iter.next()?;
let joined = iter.fold(first, |acc, e| bin(acc, op, e));
let joined = if needs_paren {
Expr::Paren(Box::new(joined))
} else {
joined
};
Some(if negate { joined.not() } else { joined })
}
}
impl Default for Condition {
fn default() -> Self {
Condition::all()
}
}
pub trait IntoCondition {
fn into_condition(self) -> Condition;
}
impl IntoCondition for Condition {
fn into_condition(self) -> Condition {
self
}
}
impl IntoCondition for Expr {
fn into_condition(self) -> Condition {
Condition::all().add_expr(self)
}
}
impl Condition {
fn add_expr(mut self, expr: Expr) -> Self {
self.parts.push(expr);
self
}
}