use std::marker::PhantomData;
pub trait IntoColumnName {
fn column_name(&self) -> &str;
}
impl IntoColumnName for &str {
fn column_name(&self) -> &str {
self
}
}
impl IntoColumnName for String {
fn column_name(&self) -> &str {
self.as_str()
}
}
impl IntoColumnName for &String {
fn column_name(&self) -> &str {
self.as_str()
}
}
impl<T> IntoColumnName for Column<T> {
fn column_name(&self) -> &str {
self.name
}
}
#[derive(Debug, Clone, Copy)]
pub struct Column<T> {
name: &'static str,
_phantom: PhantomData<T>,
}
impl<T> Column<T> {
pub const fn new(name: &'static str) -> Self {
Self {
name,
_phantom: PhantomData,
}
}
pub const fn name(&self) -> &'static str {
self.name
}
}
#[derive(Debug, Clone)]
pub struct ColumnCondition {
pub column: String,
pub operator: ColumnOperator,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnOperator {
Eq,
NotEq,
Gt,
Gte,
Lt,
Lte,
Like,
LikeEscaped,
NotLike,
In,
NotIn,
IsNull,
IsNotNull,
Between,
}
impl ColumnOperator {
pub fn to_sql(&self) -> &'static str {
match self {
Self::Eq => "=",
Self::NotEq => "<>",
Self::Gt => ">",
Self::Gte => ">=",
Self::Lt => "<",
Self::Lte => "<=",
Self::Like => "LIKE",
Self::LikeEscaped => "LIKE",
Self::NotLike => "NOT LIKE",
Self::In => "IN",
Self::NotIn => "NOT IN",
Self::IsNull => "IS NULL",
Self::IsNotNull => "IS NOT NULL",
Self::Between => "BETWEEN",
}
}
}
pub(crate) fn escape_like_literal(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'\\' | '%' | '_' => {
escaped.push('\\');
escaped.push(ch);
}
_ => escaped.push(ch),
}
}
escaped
}
pub trait ColumnEq<T> {
fn eq(self, value: T) -> ColumnCondition;
fn ne(self, value: T) -> ColumnCondition;
}
pub trait ColumnOrd<T>: ColumnEq<T> {
fn gt(self, value: T) -> ColumnCondition;
fn gte(self, value: T) -> ColumnCondition;
fn lt(self, value: T) -> ColumnCondition;
fn lte(self, value: T) -> ColumnCondition;
fn between(self, low: T, high: T) -> ColumnCondition;
}
pub trait ColumnLike {
fn like(self, pattern: &str) -> ColumnCondition;
fn not_like(self, pattern: &str) -> ColumnCondition;
fn contains(self, substr: &str) -> ColumnCondition;
fn starts_with(self, prefix: &str) -> ColumnCondition;
fn ends_with(self, suffix: &str) -> ColumnCondition;
}
#[allow(clippy::wrong_self_convention)]
pub trait ColumnNullable {
fn is_null(self) -> ColumnCondition;
fn is_not_null(self) -> ColumnCondition;
}
#[allow(clippy::wrong_self_convention)]
pub trait ColumnIn<T> {
fn is_in(self, values: Vec<T>) -> ColumnCondition;
fn not_in(self, values: Vec<T>) -> ColumnCondition;
}
macro_rules! impl_column_numeric {
($($t:ty),*) => {
$(
impl ColumnEq<$t> for Column<$t> {
fn eq(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Eq,
value: serde_json::json!(value),
}
}
fn ne(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotEq,
value: serde_json::json!(value),
}
}
}
impl ColumnOrd<$t> for Column<$t> {
fn gt(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Gt,
value: serde_json::json!(value),
}
}
fn gte(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Gte,
value: serde_json::json!(value),
}
}
fn lt(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Lt,
value: serde_json::json!(value),
}
}
fn lte(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Lte,
value: serde_json::json!(value),
}
}
fn between(self, low: $t, high: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Between,
value: serde_json::json!([low, high]),
}
}
}
impl ColumnIn<$t> for Column<$t> {
fn is_in(self, values: Vec<$t>) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::In,
value: serde_json::json!(values),
}
}
fn not_in(self, values: Vec<$t>) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotIn,
value: serde_json::json!(values),
}
}
}
impl ColumnEq<$t> for Column<Option<$t>> {
fn eq(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Eq,
value: serde_json::json!(value),
}
}
fn ne(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotEq,
value: serde_json::json!(value),
}
}
}
impl ColumnOrd<$t> for Column<Option<$t>> {
fn gt(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Gt,
value: serde_json::json!(value),
}
}
fn gte(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Gte,
value: serde_json::json!(value),
}
}
fn lt(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Lt,
value: serde_json::json!(value),
}
}
fn lte(self, value: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Lte,
value: serde_json::json!(value),
}
}
fn between(self, low: $t, high: $t) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Between,
value: serde_json::json!([low, high]),
}
}
}
impl ColumnNullable for Column<Option<$t>> {
fn is_null(self) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::IsNull,
value: serde_json::Value::Null,
}
}
fn is_not_null(self) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::IsNotNull,
value: serde_json::Value::Null,
}
}
}
)*
};
}
impl_column_numeric!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
impl ColumnEq<&str> for Column<String> {
fn eq(self, value: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Eq,
value: serde_json::json!(value),
}
}
fn ne(self, value: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotEq,
value: serde_json::json!(value),
}
}
}
impl ColumnEq<String> for Column<String> {
fn eq(self, value: String) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Eq,
value: serde_json::json!(value),
}
}
fn ne(self, value: String) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotEq,
value: serde_json::json!(value),
}
}
}
impl ColumnLike for Column<String> {
fn like(self, pattern: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Like,
value: serde_json::json!(pattern),
}
}
fn not_like(self, pattern: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotLike,
value: serde_json::json!(pattern),
}
}
fn contains(self, substr: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::LikeEscaped,
value: serde_json::json!(format!("%{}%", escape_like_literal(substr))),
}
}
fn starts_with(self, prefix: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::LikeEscaped,
value: serde_json::json!(format!("{}%", escape_like_literal(prefix))),
}
}
fn ends_with(self, suffix: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::LikeEscaped,
value: serde_json::json!(format!("%{}", escape_like_literal(suffix))),
}
}
}
impl ColumnIn<&str> for Column<String> {
fn is_in(self, values: Vec<&str>) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::In,
value: serde_json::json!(values),
}
}
fn not_in(self, values: Vec<&str>) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotIn,
value: serde_json::json!(values),
}
}
}
impl ColumnIn<String> for Column<String> {
fn is_in(self, values: Vec<String>) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::In,
value: serde_json::json!(values),
}
}
fn not_in(self, values: Vec<String>) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotIn,
value: serde_json::json!(values),
}
}
}
impl ColumnEq<&str> for Column<Option<String>> {
fn eq(self, value: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Eq,
value: serde_json::json!(value),
}
}
fn ne(self, value: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotEq,
value: serde_json::json!(value),
}
}
}
impl ColumnLike for Column<Option<String>> {
fn like(self, pattern: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Like,
value: serde_json::json!(pattern),
}
}
fn not_like(self, pattern: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotLike,
value: serde_json::json!(pattern),
}
}
fn contains(self, substr: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::LikeEscaped,
value: serde_json::json!(format!("%{}%", escape_like_literal(substr))),
}
}
fn starts_with(self, prefix: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::LikeEscaped,
value: serde_json::json!(format!("{}%", escape_like_literal(prefix))),
}
}
fn ends_with(self, suffix: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::LikeEscaped,
value: serde_json::json!(format!("%{}", escape_like_literal(suffix))),
}
}
}
impl ColumnNullable for Column<Option<String>> {
fn is_null(self) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::IsNull,
value: serde_json::Value::Null,
}
}
fn is_not_null(self) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::IsNotNull,
value: serde_json::Value::Null,
}
}
}
impl ColumnEq<bool> for Column<bool> {
fn eq(self, value: bool) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Eq,
value: serde_json::json!(value),
}
}
fn ne(self, value: bool) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotEq,
value: serde_json::json!(value),
}
}
}
impl ColumnEq<bool> for Column<Option<bool>> {
fn eq(self, value: bool) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Eq,
value: serde_json::json!(value),
}
}
fn ne(self, value: bool) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::NotEq,
value: serde_json::json!(value),
}
}
}
impl ColumnNullable for Column<Option<bool>> {
fn is_null(self) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::IsNull,
value: serde_json::Value::Null,
}
}
fn is_not_null(self) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::IsNotNull,
value: serde_json::Value::Null,
}
}
}
#[cfg(test)]
#[path = "testing/columns_tests.rs"]
mod tests;