use std::borrow::Cow;
use keelson_core::expr::{Chain, Expr, IntoExpr, IntoExprList};
#[allow(clippy::wrong_self_convention)]
pub trait PsqlOps: Chain {
#[must_use]
fn ilike(self, rhs: impl IntoExpr) -> Self {
self.op("ILIKE", rhs)
}
#[must_use]
fn not_ilike(self, rhs: impl IntoExpr) -> Self {
self.op("NOT ILIKE", rhs)
}
#[must_use]
fn not_like(self, rhs: impl IntoExpr) -> Self {
self.op("NOT LIKE", rhs)
}
#[must_use]
fn similar_to(self, rhs: impl IntoExpr) -> Self {
self.op("SIMILAR TO", rhs)
}
#[must_use]
fn not_similar_to(self, rhs: impl IntoExpr) -> Self {
self.op("NOT SIMILAR TO", rhs)
}
#[must_use]
fn matches(self, rhs: impl IntoExpr) -> Self {
self.op("~", rhs)
}
#[must_use]
fn imatches(self, rhs: impl IntoExpr) -> Self {
self.op("~*", rhs)
}
#[must_use]
fn not_matches(self, rhs: impl IntoExpr) -> Self {
self.op("!~", rhs)
}
#[must_use]
fn not_imatches(self, rhs: impl IntoExpr) -> Self {
self.op("!~*", rhs)
}
#[must_use]
fn between_symmetric(self, a: impl IntoExpr, b: impl IntoExpr) -> Self {
self.step(move |lhs| {
Expr::join((lhs, Expr::raw("BETWEEN SYMMETRIC"), a, Expr::raw("AND"), b))
})
}
#[must_use]
fn not_between_symmetric(self, a: impl IntoExpr, b: impl IntoExpr) -> Self {
self.step(move |lhs| {
Expr::join((
lhs,
Expr::raw("NOT BETWEEN SYMMETRIC"),
a,
Expr::raw("AND"),
b,
))
})
}
#[must_use]
fn contains(self, rhs: impl IntoExpr) -> Self {
self.op("@>", rhs)
}
#[must_use]
fn contained_by(self, rhs: impl IntoExpr) -> Self {
self.op("<@", rhs)
}
#[must_use]
fn overlaps(self, rhs: impl IntoExpr) -> Self {
self.op("&&", rhs)
}
#[must_use]
fn text_search(self, rhs: impl IntoExpr) -> Self {
self.op("@@", rhs)
}
#[must_use]
fn json_get(self, rhs: impl IntoExpr) -> Self {
self.op("->", rhs)
}
#[must_use]
fn json_get_text(self, rhs: impl IntoExpr) -> Self {
self.op("->>", rhs)
}
#[must_use]
fn json_get_path(self, rhs: impl IntoExpr) -> Self {
self.op("#>", rhs)
}
#[must_use]
fn json_get_path_text(self, rhs: impl IntoExpr) -> Self {
self.op("#>>", rhs)
}
#[must_use]
fn json_has_key(self, rhs: impl IntoExpr) -> Self {
self.op("?", rhs)
}
#[must_use]
fn json_has_any_key(self, rhs: impl IntoExpr) -> Self {
self.op("?|", rhs)
}
#[must_use]
fn json_has_all_keys(self, rhs: impl IntoExpr) -> Self {
self.op("?&", rhs)
}
#[must_use]
fn eq_any(self, vals: impl IntoExprList) -> Self {
self.step(move |lhs| Expr::join((lhs, Expr::raw("= ANY"), Expr::group(vals))))
}
#[must_use]
fn ne_all(self, vals: impl IntoExprList) -> Self {
self.step(move |lhs| Expr::join((lhs, Expr::raw("<> ALL"), Expr::group(vals))))
}
#[must_use]
fn any(self, op: &'static str, vals: impl IntoExprList) -> Self {
self.step(move |lhs| Expr::join((lhs, Expr::raw(op), Expr::raw("ANY"), Expr::group(vals))))
}
#[must_use]
fn all(self, op: &'static str, vals: impl IntoExprList) -> Self {
self.step(move |lhs| Expr::join((lhs, Expr::raw(op), Expr::raw("ALL"), Expr::group(vals))))
}
#[must_use]
fn is_true(self) -> Self {
self.step(|lhs| Expr::postfix(lhs, "IS TRUE"))
}
#[must_use]
fn is_not_true(self) -> Self {
self.step(|lhs| Expr::postfix(lhs, "IS NOT TRUE"))
}
#[must_use]
fn is_false(self) -> Self {
self.step(|lhs| Expr::postfix(lhs, "IS FALSE"))
}
#[must_use]
fn is_not_false(self) -> Self {
self.step(|lhs| Expr::postfix(lhs, "IS NOT FALSE"))
}
#[must_use]
fn is_unknown(self) -> Self {
self.step(|lhs| Expr::postfix(lhs, "IS UNKNOWN"))
}
#[must_use]
fn is_not_unknown(self) -> Self {
self.step(|lhs| Expr::postfix(lhs, "IS NOT UNKNOWN"))
}
#[must_use]
fn cast_to(self, type_name: impl Into<Cow<'static, str>>) -> Self {
let type_name = type_name.into();
self.step(move |lhs| Expr::join_with("", (lhs, Expr::raw("::"), Expr::raw(type_name))))
}
#[must_use]
fn collate(self, name: impl Into<Cow<'static, str>>) -> Self {
let name = name.into();
self.step(move |lhs| Expr::join((lhs, Expr::raw("COLLATE"), Expr::ident(name))))
}
#[must_use]
fn at_time_zone(self, zone: impl IntoExpr) -> Self {
self.step(move |lhs| Expr::join((lhs, Expr::raw("AT TIME ZONE"), zone)))
}
}
impl<T: Chain> PsqlOps for T {}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Psql, arg, quote};
use keelson_core::build;
fn sql(e: Expr) -> String {
build(&Psql, &e).expect("render").0
}
#[test]
fn every_operator_renders_with_one_set_of_parentheses() {
let cases = [
(quote("a").ilike(arg("x")), r#"("a" ILIKE $1)"#),
(quote("a").not_ilike(arg("x")), r#"("a" NOT ILIKE $1)"#),
(quote("a").not_like(arg("x")), r#"("a" NOT LIKE $1)"#),
(quote("a").similar_to(arg("x")), r#"("a" SIMILAR TO $1)"#),
(
quote("a").not_similar_to(arg("x")),
r#"("a" NOT SIMILAR TO $1)"#,
),
(quote("a").matches(arg("x")), r#"("a" ~ $1)"#),
(quote("a").imatches(arg("x")), r#"("a" ~* $1)"#),
(quote("a").not_matches(arg("x")), r#"("a" !~ $1)"#),
(quote("a").not_imatches(arg("x")), r#"("a" !~* $1)"#),
(quote("a").contains(arg("x")), r#"("a" @> $1)"#),
(quote("a").contained_by(arg("x")), r#"("a" <@ $1)"#),
(quote("a").overlaps(arg("x")), r#"("a" && $1)"#),
(quote("a").text_search(arg("x")), r#"("a" @@ $1)"#),
(quote("a").json_get(arg("x")), r#"("a" -> $1)"#),
(quote("a").json_get_text(arg("x")), r#"("a" ->> $1)"#),
(quote("a").json_get_path(arg("x")), r#"("a" #> $1)"#),
(quote("a").json_get_path_text(arg("x")), r#"("a" #>> $1)"#),
(quote("a").json_has_key(arg("x")), r#"("a" ? $1)"#),
(quote("a").json_has_any_key(arg("x")), r#"("a" ?| $1)"#),
(quote("a").json_has_all_keys(arg("x")), r#"("a" ?& $1)"#),
(quote("a").is_true(), r#"("a" IS TRUE)"#),
(quote("a").is_not_true(), r#"("a" IS NOT TRUE)"#),
(quote("a").is_false(), r#"("a" IS FALSE)"#),
(quote("a").is_not_false(), r#"("a" IS NOT FALSE)"#),
(quote("a").is_unknown(), r#"("a" IS UNKNOWN)"#),
(quote("a").is_not_unknown(), r#"("a" IS NOT UNKNOWN)"#),
];
for (e, expected) in cases {
assert_eq!(sql(e), expected);
}
}
#[test]
fn the_multi_token_operators_keep_their_shape() {
assert_eq!(
sql(quote("a").between_symmetric(arg(1i32), arg(2i32))),
r#"("a" BETWEEN SYMMETRIC $1 AND $2)"#
);
assert_eq!(
sql(quote("a").not_between_symmetric(arg(1i32), arg(2i32))),
r#"("a" NOT BETWEEN SYMMETRIC $1 AND $2)"#
);
assert_eq!(sql(quote("a").eq_any(arg(1i32))), r#"("a" = ANY ($1))"#);
assert_eq!(sql(quote("a").ne_all(arg(1i32))), r#"("a" <> ALL ($1))"#);
assert_eq!(sql(quote("a").any(">", arg(1i32))), r#"("a" > ANY ($1))"#);
assert_eq!(sql(quote("a").all("<", arg(1i32))), r#"("a" < ALL ($1))"#);
}
#[test]
fn cast_shorthand_has_no_spaces_and_collate_quotes_its_name() {
assert_eq!(sql(quote("a").cast_to("int")), r#"("a"::int)"#);
assert_eq!(sql(quote("a").collate("C")), r#"("a" COLLATE "C")"#);
assert_eq!(
sql(quote("a").at_time_zone(arg("UTC"))),
r#"("a" AT TIME ZONE $1)"#
);
}
}