use std::borrow::Cow;
use keelson_core::clause::Set;
use keelson_core::expr::{Expr, IntoExpr};
use keelson_core::{Expression, Query, SqlWriter};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Modifier {
Distinct,
DistinctRow,
LowPriority,
HighPriority,
Delayed,
Quick,
Ignore,
StraightJoin,
SmallResult,
BigResult,
BufferResult,
NoCache,
CalcFoundRows,
}
impl Modifier {
pub fn as_str(self) -> &'static str {
match self {
Modifier::Distinct => "DISTINCT",
Modifier::DistinctRow => "DISTINCTROW",
Modifier::LowPriority => "LOW_PRIORITY",
Modifier::HighPriority => "HIGH_PRIORITY",
Modifier::Delayed => "DELAYED",
Modifier::Quick => "QUICK",
Modifier::Ignore => "IGNORE",
Modifier::StraightJoin => "STRAIGHT_JOIN",
Modifier::SmallResult => "SQL_SMALL_RESULT",
Modifier::BigResult => "SQL_BIG_RESULT",
Modifier::BufferResult => "SQL_BUFFER_RESULT",
Modifier::NoCache => "SQL_NO_CACHE",
Modifier::CalcFoundRows => "SQL_CALC_FOUND_ROWS",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Modifiers {
pub modifiers: Vec<Modifier>,
}
impl Modifiers {
pub fn append_modifier(&mut self, modifier: Modifier) {
if let Err(at) = self.modifiers.binary_search(&modifier) {
self.modifiers.insert(at, modifier);
}
}
pub fn is_empty(&self) -> bool {
self.modifiers.is_empty()
}
}
impl Expression for Modifiers {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
for (i, modifier) in self.modifiers.iter().enumerate() {
if i > 0 {
w.push_str(" ");
}
w.push_str(modifier.as_str());
}
}
}
pub trait HasModifiers {
fn modifiers_mut(&mut self) -> &mut Modifiers;
}
impl HasModifiers for Modifiers {
fn modifiers_mut(&mut self) -> &mut Modifiers {
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Hints {
pub hints: Vec<Cow<'static, str>>,
}
impl Hints {
pub fn append_hint(&mut self, hint: impl Into<Cow<'static, str>>) {
self.hints.push(hint.into());
}
pub fn is_empty(&self) -> bool {
self.hints.is_empty()
}
}
impl Expression for Hints {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
if self.hints.is_empty() {
return;
}
w.push_str("/*+ ");
for (i, hint) in self.hints.iter().enumerate() {
if i > 0 {
w.push_str(" ");
}
w.push_str(hint);
}
w.push_str(" */");
}
}
pub trait HasHints {
fn hints_mut(&mut self) -> &mut Hints;
}
impl HasHints for Hints {
fn hints_mut(&mut self) -> &mut Hints {
self
}
}
#[derive(Debug, Clone, Default)]
pub struct RowAlias {
pub name: Option<Cow<'static, str>>,
pub columns: Vec<Cow<'static, str>>,
}
impl RowAlias {
pub fn new(name: impl Into<Cow<'static, str>>) -> RowAlias {
RowAlias {
name: Some(name.into()),
columns: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.name.is_none()
}
}
impl Expression for RowAlias {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
let Some(name) = &self.name else {
return;
};
w.push_str("AS ");
w.push_quoted(&[name]);
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(")");
}
}
}
pub trait HasRowAlias {
fn row_alias_mut(&mut self) -> &mut RowAlias;
}
impl HasRowAlias for RowAlias {
fn row_alias_mut(&mut self) -> &mut RowAlias {
self
}
}
pub trait HasDuplicateKeyUpdate {
fn duplicate_key_update_mut(&mut self) -> &mut Set;
}
#[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 values_of(column: impl Into<Cow<'static, str>>) -> Expr {
Expr::func("VALUES", Expr::ident(column.into()))
}
pub fn row_value(
alias: impl Into<Cow<'static, str>>,
column: impl Into<Cow<'static, str>>,
) -> Expr {
Expr::ident([alias.into(), column.into()])
}
#[derive(Debug)]
pub(crate) struct Match {
pub(crate) columns: Vec<Expr>,
pub(crate) against: Expr,
pub(crate) modifier: Option<Cow<'static, str>>,
}
impl Expression for Match {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
w.push_str("MATCH (");
w.write_slice(&self.columns, "", ", ", "");
w.push_str(") AGAINST (");
w.write_expr(&self.against);
if let Some(modifier) = &self.modifier {
w.push_str(" ");
w.push_str(modifier);
}
w.push_str(")");
}
}
pub fn match_against(
columns: impl keelson_core::expr::IntoExprList,
search: impl IntoExpr,
) -> Expr {
Expr::custom(Match {
columns: columns.into_expr_list(),
against: search.into_expr(),
modifier: None,
})
}
pub fn match_against_mode(
columns: impl keelson_core::expr::IntoExprList,
search: impl IntoExpr,
modifier: impl Into<Cow<'static, str>>,
) -> Expr {
Expr::custom(Match {
columns: columns.into_expr_list(),
against: search.into_expr(),
modifier: Some(modifier.into()),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Mysql, quote, s};
use keelson_core::build;
fn sql(e: impl Expression) -> String {
build(&Mysql, &e).expect("render").0
}
#[test]
fn modifiers_render_in_grammar_order_whatever_order_they_were_added_in() {
let mut m = Modifiers::default();
m.append_modifier(Modifier::CalcFoundRows);
m.append_modifier(Modifier::HighPriority);
m.append_modifier(Modifier::Distinct);
m.append_modifier(Modifier::StraightJoin);
assert_eq!(
sql(m),
"DISTINCT HIGH_PRIORITY STRAIGHT_JOIN SQL_CALC_FOUND_ROWS"
);
}
#[test]
fn a_repeated_modifier_is_written_once() {
let mut m = Modifiers::default();
m.append_modifier(Modifier::Ignore);
m.append_modifier(Modifier::Ignore);
assert_eq!(sql(m), "IGNORE");
}
#[test]
fn an_empty_modifier_list_writes_nothing() {
assert_eq!(sql(Modifiers::default()), "");
assert!(Modifiers::default().is_empty());
}
#[test]
fn hints_are_wrapped_in_one_comment_and_space_separated() {
let mut h = Hints::default();
assert_eq!(sql(h.clone()), "");
h.append_hint("MAX_EXECUTION_TIME(1000)");
assert_eq!(sql(h.clone()), "/*+ MAX_EXECUTION_TIME(1000) */");
h.append_hint("QB_NAME(outer)");
assert_eq!(sql(h), "/*+ MAX_EXECUTION_TIME(1000) QB_NAME(outer) */");
}
#[test]
fn a_row_alias_quotes_its_name_and_its_columns() {
assert_eq!(sql(RowAlias::default()), "");
assert_eq!(sql(RowAlias::new("new")), "AS `new`");
assert_eq!(
sql(RowAlias {
name: Some("new".into()),
columns: vec!["a".into(), "b".into()],
}),
"AS `new` (`a`, `b`)"
);
assert_eq!(
sql(RowAlias {
name: None,
columns: vec!["a".into()],
}),
""
);
}
#[test]
fn the_two_upsert_value_sources_render_as_the_manual_writes_them() {
assert_eq!(sql(values_of("name")), "VALUES(`name`)");
assert_eq!(sql(row_value("new", "name")), "`new`.`name`");
}
#[test]
fn match_against_puts_the_modifier_inside_the_against_parentheses() {
assert_eq!(
sql(match_against(quote("title"), s("rust"))),
"MATCH (`title`) AGAINST ('rust')"
);
assert_eq!(
sql(match_against_mode(
(quote("title"), quote("status")),
s("+rust -go"),
"IN BOOLEAN MODE"
)),
"MATCH (`title`, `status`) AGAINST ('+rust -go' IN BOOLEAN MODE)"
);
}
}