use std::borrow::Cow;
use keelson_core::clause::ConflictClause;
use keelson_core::expr::{Expr, IntoExpr};
use keelson_core::{Error, Expression, Query, SqlWriter};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Or {
Rollback,
Abort,
Replace,
Fail,
Ignore,
}
impl Or {
pub fn as_str(self) -> &'static str {
match self {
Or::Rollback => "ROLLBACK",
Or::Abort => "ABORT",
Or::Replace => "REPLACE",
Or::Fail => "FAIL",
Or::Ignore => "IGNORE",
}
}
}
pub trait HasOr {
fn or_mut(&mut self) -> &mut Option<Or>;
}
impl HasOr for Option<Or> {
fn or_mut(&mut self) -> &mut Option<Or> {
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompoundOp {
Union,
UnionAll,
Intersect,
Except,
}
impl CompoundOp {
pub fn as_str(self) -> &'static str {
match self {
CompoundOp::Union => "UNION",
CompoundOp::UnionAll => "UNION ALL",
CompoundOp::Intersect => "INTERSECT",
CompoundOp::Except => "EXCEPT",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Compound {
pub op: Option<CompoundOp>,
pub query: Option<Expr>,
}
impl Compound {
pub fn new(op: CompoundOp, query: impl IntoExpr) -> Compound {
Compound {
op: Some(op),
query: Some(query.into_expr()),
}
}
pub fn is_empty(&self) -> bool {
self.op.is_none() && self.query.is_none()
}
}
impl Expression for Compound {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
if self.is_empty() {
return;
}
let Some(op) = self.op else {
w.record_error(Error::Incomplete("the operator of a compound SELECT"));
return;
};
let Some(query) = &self.query else {
w.record_error(Error::Incomplete("the operand of a compound SELECT"));
return;
};
w.push_str(op.as_str());
w.push_str(" ");
w.write_expr(query);
}
}
#[derive(Debug, Clone, Default)]
pub struct Compounds {
pub operands: Vec<Compound>,
}
impl Compounds {
pub fn append_compound(&mut self, compound: Compound) {
self.operands.push(compound);
}
pub fn is_empty(&self) -> bool {
self.operands.iter().all(Compound::is_empty)
}
}
impl Expression for Compounds {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
write_spaced(w, self.operands.iter().filter(|c| !c.is_empty()));
}
}
pub trait HasCompounds {
fn compounds_mut(&mut self) -> &mut Compounds;
}
impl HasCompounds for Compounds {
fn compounds_mut(&mut self) -> &mut Compounds {
self
}
}
pub trait HasUpserts {
fn upserts_mut(&mut self) -> &mut Vec<ConflictClause>;
}
impl HasUpserts for Vec<ConflictClause> {
fn upserts_mut(&mut self) -> &mut Vec<ConflictClause> {
self
}
}
pub(crate) fn write_spaced<'a, E: Expression + 'a>(
w: &mut SqlWriter<'_>,
items: impl IntoIterator<Item = &'a E>,
) {
let mut written = false;
for item in items {
if written {
w.push_str(" ");
}
w.write_expr(item);
written = true;
}
}
#[derive(Debug)]
struct QueryExpr<Q>(Q);
impl<Q: Query> Expression for QueryExpr<Q> {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
w.write_with_dialect(self.0.dialect(), &self.0);
}
}
pub fn query(q: impl Query + 'static) -> Expr {
Expr::custom(QueryExpr(q))
}
pub fn subquery(q: impl Query + 'static) -> Expr {
Expr::group(query(q))
}
pub fn excluded(column: impl Into<Cow<'static, str>>) -> Expr {
Expr::join_with("", (Expr::raw("excluded."), Expr::ident(column.into())))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Sqlite;
use keelson_core::build;
fn sql(e: impl Expression) -> String {
build(&Sqlite, &e).expect("render").0
}
#[test]
fn a_compound_operand_carries_its_operator_and_no_parentheses() {
assert_eq!(
sql(Compound::new(CompoundOp::UnionAll, Expr::raw("SELECT 1"))),
"UNION ALL SELECT 1"
);
assert_eq!(
sql(Compound::new(CompoundOp::Intersect, Expr::raw("SELECT 1"))),
"INTERSECT SELECT 1"
);
assert_eq!(
sql(Compound::new(CompoundOp::Except, Expr::raw("SELECT 1"))),
"EXCEPT SELECT 1"
);
assert_eq!(
sql(Compound::new(CompoundOp::Union, Expr::raw("SELECT 1"))),
"UNION SELECT 1"
);
}
#[test]
fn an_absent_operand_takes_its_separator_with_it() {
let mut cs = Compounds::default();
assert!(cs.is_empty());
assert_eq!(sql(Compounds::default()), "");
cs.append_compound(Compound::default());
assert!(
cs.is_empty(),
"a list of nothing but absent operands is an absent clause, or the \
statement writes the separator in front of nothing"
);
cs.append_compound(Compound::new(CompoundOp::Union, Expr::raw("SELECT 1")));
cs.append_compound(Compound::default());
assert!(!cs.is_empty());
assert_eq!(sql(cs), "UNION SELECT 1");
}
#[test]
fn a_half_filled_operand_is_a_recorded_failure() {
let no_op = Compound {
query: Some(Expr::raw("SELECT 1")),
..Compound::default()
};
let err = build(&Sqlite, &no_op).unwrap_err();
assert!(
matches!(&err, Error::Incomplete(what) if what.contains("operator")),
"got: {err}"
);
let no_query = Compound {
op: Some(CompoundOp::Union),
..Compound::default()
};
let err = build(&Sqlite, &no_query).unwrap_err();
assert!(
matches!(&err, Error::Incomplete(what) if what.contains("operand")),
"got: {err}"
);
}
#[test]
fn excluded_qualifies_the_column_with_the_pseudo_table() {
assert_eq!(sql(excluded("email")), r#"excluded."email""#);
}
#[test]
fn every_conflict_algorithm_has_its_keyword() {
assert_eq!(Or::Rollback.as_str(), "ROLLBACK");
assert_eq!(Or::Abort.as_str(), "ABORT");
assert_eq!(Or::Replace.as_str(), "REPLACE");
assert_eq!(Or::Fail.as_str(), "FAIL");
assert_eq!(Or::Ignore.as_str(), "IGNORE");
}
}