use std::borrow::Cow;
use keelson_core::clause::{GroupingSet, GroupingSetKind};
use keelson_core::expr::{Expr, IntoExprList};
use keelson_core::{Expression, Query, SqlWriter};
#[derive(Debug, Clone, Default)]
pub struct Distinct {
pub on: Vec<Expr>,
}
impl Expression for Distinct {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
w.push_str("DISTINCT");
w.write_slice(&self.on, " ON (", ", ", ")");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Overriding {
System,
User,
}
impl Overriding {
pub fn as_str(self) -> &'static str {
match self {
Overriding::System => "SYSTEM",
Overriding::User => "USER",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Sample {
pub(crate) method: Cow<'static, str>,
pub(crate) args: Vec<Expr>,
pub(crate) repeatable: Option<Expr>,
}
#[derive(Debug)]
pub(crate) struct SampledTable {
pub(crate) table: Expr,
pub(crate) alias: Option<Cow<'static, str>>,
pub(crate) columns: Vec<Cow<'static, str>>,
pub(crate) sample: Sample,
}
impl Expression for SampledTable {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
w.write_expr(&self.table);
if let Some(alias) = &self.alias {
w.push_str(" AS ");
w.push_quoted(&[alias]);
}
if !self.columns.is_empty() {
w.push_str(" (");
for (i, column) in self.columns.iter().enumerate() {
if i > 0 {
w.push_str(", ");
}
w.push_quoted(&[column]);
}
w.push_str(")");
}
w.push_str(" TABLESAMPLE ");
w.push_str(&self.sample.method);
w.push_str(" (");
w.write_slice(&self.sample.args, "", ", ", "");
w.push_str(")");
if let Some(seed) = &self.sample.repeatable {
w.push_str(" REPEATABLE (");
w.write_expr(seed);
w.push_str(")");
}
}
}
#[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())))
}
#[derive(Debug)]
pub(crate) struct Incomplete(pub(crate) &'static str);
impl Expression for Incomplete {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
w.record_error(keelson_core::Error::Incomplete(self.0));
}
}
#[derive(Debug)]
pub(crate) struct LateralBareName(pub(crate) Expr);
impl Expression for LateralBareName {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
w.record_error(keelson_core::Error::other(
"LATERAL is set on a bare table or CTE name, but LATERAL can precede only a sub-query or a function item",
));
w.write_expr(&self.0);
}
}
fn grouping_element(kind: GroupingSetKind, groups: impl IntoExprList) -> Expr {
let set = GroupingSet::new(kind, groups);
if set.is_empty() {
return Expr::custom(Incomplete("the columns of a grouping element"));
}
Expr::custom(set)
}
pub fn rollup(groups: impl IntoExprList) -> Expr {
grouping_element(GroupingSetKind::Rollup, groups)
}
pub fn cube(groups: impl IntoExprList) -> Expr {
grouping_element(GroupingSetKind::Cube, groups)
}
pub fn grouping_sets(sets: impl IntoExprList) -> Expr {
grouping_element(GroupingSetKind::GroupingSets, sets)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Psql, group, quote, raw};
use keelson_core::build;
fn sql(e: impl Expression) -> String {
build(&Psql, &e).expect("render").0
}
#[test]
fn distinct_renders_with_and_without_an_on_list() {
assert_eq!(sql(Distinct::default()), "DISTINCT");
assert_eq!(
sql(Distinct {
on: vec![quote("a"), quote("b")]
}),
r#"DISTINCT ON ("a", "b")"#
);
}
#[test]
fn excluded_qualifies_the_column_with_the_pseudo_table() {
assert_eq!(sql(excluded("email")), r#"EXCLUDED."email""#);
}
#[test]
fn the_grouping_elements_use_their_own_keywords() {
assert_eq!(
sql(rollup((quote("a"), quote("b")))),
r#"ROLLUP ("a", "b")"#
);
assert_eq!(sql(cube((quote("a"), quote("b")))), r#"CUBE ("a", "b")"#);
assert_eq!(
sql(grouping_sets((group(quote("a")), raw("()")))),
r#"GROUPING SETS (("a"), ())"#
);
}
}