use crate::error::Error;
use crate::expr::{Expr, IntoExpr};
use crate::writer::{Expression, SqlWriter};
use super::fetch::Fetch;
use super::limit::Limit;
use super::offset::Offset;
use super::order_by::OrderBy;
use super::{MaybeAbsent, write_present};
#[derive(Debug, Clone, Default)]
pub struct Combines {
pub queries: Vec<Combine>,
pub order_by: OrderBy,
pub limit: Limit,
pub offset: Offset,
pub fetch: Fetch,
}
impl Combines {
pub fn append_combine(&mut self, combine: Combine) {
self.queries.push(combine);
}
pub fn is_empty(&self) -> bool {
self.queries.is_empty()
&& self.order_by.is_empty()
&& self.limit.is_empty()
&& self.offset.is_empty()
&& self.fetch.is_empty()
}
pub fn parenthesises_leading_query(&self, leading_has_tail_clauses: bool) -> bool {
!self.queries.is_empty() && leading_has_tail_clauses
}
}
impl Expression for Combines {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
if self.queries.is_empty() {
if self.is_empty() {
return;
}
let missing = if !self.order_by.is_empty() {
"the set operation its combined ORDER BY applies to"
} else if !self.limit.is_empty() {
"the set operation its combined LIMIT applies to"
} else if !self.offset.is_empty() {
"the set operation its combined OFFSET applies to"
} else {
"the set operation its combined FETCH applies to"
};
w.record_error(Error::Incomplete(missing));
return;
}
if !self.limit.is_empty() && !self.fetch.is_empty() {
w.record_error(Error::conflicting_clauses("LIMIT", "FETCH"));
return;
}
let mut written = !self.queries.is_empty();
write_present(w, &self.queries, "", " ", "");
for (present, clause) in [
(!self.order_by.is_empty(), &self.order_by as &dyn Expression),
(!self.limit.is_empty(), &self.limit),
(!self.offset.is_empty(), &self.offset),
(!self.fetch.is_empty(), &self.fetch),
] {
if !present {
continue;
}
if written {
w.push_str(" ");
}
w.write_expr(clause);
written = true;
}
}
}
pub trait HasCombines {
fn combines_mut(&mut self) -> &mut Combines;
}
impl HasCombines for Combines {
fn combines_mut(&mut self) -> &mut Combines {
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Combine {
pub op: Option<SetOp>,
pub query: Option<Expr>,
pub all: bool,
}
impl Combine {
pub fn new(op: SetOp, query: impl IntoExpr) -> Self {
Combine {
op: Some(op),
query: Some(query.into_expr()),
all: false,
}
}
pub fn is_empty(&self) -> bool {
self.op.is_none() && self.query.is_none()
}
}
impl Expression for Combine {
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 set operation"));
return;
};
let Some(query) = &self.query else {
w.record_error(Error::Incomplete("the query of a set operation"));
return;
};
w.push_str(op.as_str());
w.push_str(if self.all { " ALL (" } else { " (" });
w.write_expr(query);
w.push_str(")");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetOp {
Union,
Intersect,
Except,
}
impl SetOp {
pub fn as_str(self) -> &'static str {
match self {
SetOp::Union => "UNION",
SetOp::Intersect => "INTERSECT",
SetOp::Except => "EXCEPT",
}
}
}
impl MaybeAbsent for Combine {
fn is_absent(&self) -> bool {
self.is_empty()
}
}
#[cfg(test)]
mod tests {
use keelson_sqlcheck::testing::assert_frag_sql;
use super::*;
use crate::dialect::testing::Numbered;
use crate::expr::arg;
use crate::value::Value;
use crate::writer::build;
const FRAME: &str = r#"SELECT "id" FROM users {}"#;
fn sub(v: i32) -> Expr {
Expr::join((Expr::raw(r#"SELECT "id" FROM posts WHERE "id" ="#), arg(v)))
}
fn sub_sql(n: usize) -> String {
format!(r#"SELECT "id" FROM posts WHERE "id" = ${n}"#)
}
fn sql(e: &impl Expression) -> String {
build(&Numbered, e).expect("render").0
}
#[test]
fn an_empty_combines_writes_nothing() {
assert_frag_sql(FRAME, &sql(&Combines::default()), "");
assert_frag_sql(FRAME, &sql(&Combine::default()), "");
assert!(Combines::default().is_empty());
assert!(Combine::default().is_empty());
}
#[test]
fn all_goes_between_the_operator_and_the_operand() {
let mut c = Combine::new(SetOp::Union, sub(1));
assert_frag_sql(FRAME, &sql(&c), &format!("UNION ({})", sub_sql(1)));
c.all = true;
assert_frag_sql(FRAME, &sql(&c), &format!("UNION ALL ({})", sub_sql(1)));
}
#[test]
fn every_operator_has_its_spelling() {
for (op, keyword) in [
(SetOp::Union, "UNION"),
(SetOp::Intersect, "INTERSECT"),
(SetOp::Except, "EXCEPT"),
] {
assert_frag_sql(
FRAME,
&sql(&Combine::new(op, Expr::raw("SELECT 1"))),
&format!("{keyword} (SELECT 1)"),
);
}
}
#[test]
fn a_half_filled_combine_is_a_recorded_failure_not_a_broken_fragment() {
let no_op = Combine {
query: Some(sub(1)),
..Combine::default()
};
let err = build(&Numbered, &no_op).unwrap_err();
assert!(
matches!(&err, Error::Incomplete(what) if what.contains("operator")),
"got: {err}"
);
let no_query = Combine {
op: Some(SetOp::Except),
..Combine::default()
};
let err = build(&Numbered, &no_query).unwrap_err();
assert!(
matches!(&err, Error::Incomplete(what) if what.contains("query")),
"got: {err}"
);
}
#[test]
fn chained_operations_keep_one_placeholder_run() {
let mut cs = Combines::default();
cs.append_combine(Combine::new(SetOp::Union, sub(1)));
cs.append_combine(Combine::new(SetOp::Intersect, sub(2)));
let (rendered, args) = build(&Numbered, &cs).unwrap();
assert_frag_sql(
FRAME,
&rendered,
&format!("UNION ({}) INTERSECT ({})", sub_sql(1), sub_sql(2)),
);
assert_eq!(args, vec![Value::I32(1), Value::I32(2)]);
}
#[test]
fn the_combinations_own_tail_clauses_follow_the_last_operand() {
let mut cs = Combines::default();
cs.append_combine(Combine::new(SetOp::Union, sub(1)));
cs.order_by.append_order("1");
cs.limit.set_limit(10i64);
cs.offset.set_offset(5i64);
assert_frag_sql(
FRAME,
&sql(&cs),
&format!("UNION ({}) ORDER BY 1 LIMIT 10 OFFSET 5", sub_sql(1)),
);
}
#[test]
fn a_tail_clause_without_a_set_operation_is_a_recorded_failure() {
let mut cs = Combines::default();
cs.fetch.set_fetch(2i64);
assert!(!cs.is_empty());
let err = build(&Numbered, &cs).unwrap_err();
assert!(
matches!(&err, Error::Incomplete(what)
if what.contains("set operation") && what.contains("FETCH")),
"got: {err}"
);
let mut cs = Combines::default();
cs.order_by.append_order("1");
let err = build(&Numbered, &cs).unwrap_err();
assert!(
matches!(&err, Error::Incomplete(what)
if what.contains("set operation") && what.contains("ORDER BY")),
"got: {err}"
);
}
#[test]
fn a_combined_limit_and_fetch_together_are_a_recorded_failure() {
let mut cs = Combines::default();
cs.append_combine(Combine::new(SetOp::Union, sub(1)));
cs.limit.set_limit(10i64);
cs.fetch.set_fetch(2i64);
let err = build(&Numbered, &cs).unwrap_err();
assert!(
matches!(
&err,
Error::ConflictingClauses {
first: "LIMIT",
second: "FETCH"
}
),
"got: {err}"
);
}
#[test]
fn the_leading_query_is_wrapped_only_when_both_conditions_hold() {
let mut cs = Combines::default();
assert!(
!cs.parenthesises_leading_query(true),
"nothing combined: the parentheses would say nothing"
);
cs.append_combine(Combine::new(SetOp::Union, sub(1)));
assert!(
!cs.parenthesises_leading_query(false),
"no tail clause on the leading query: nothing to protect"
);
assert!(cs.parenthesises_leading_query(true));
}
}