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,
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::NotLike => "NOT LIKE",
Self::In => "IN",
Self::NotIn => "NOT IN",
Self::IsNull => "IS NULL",
Self::IsNotNull => "IS NOT NULL",
Self::Between => "BETWEEN",
}
}
}
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::Like,
value: serde_json::json!(format!("%{}%", substr)),
}
}
fn starts_with(self, prefix: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Like,
value: serde_json::json!(format!("{}%", prefix)),
}
}
fn ends_with(self, suffix: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Like,
value: serde_json::json!(format!("%{}", 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::Like,
value: serde_json::json!(format!("%{}%", substr)),
}
}
fn starts_with(self, prefix: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Like,
value: serde_json::json!(format!("{}%", prefix)),
}
}
fn ends_with(self, suffix: &str) -> ColumnCondition {
ColumnCondition {
column: self.name.to_string(),
operator: ColumnOperator::Like,
value: serde_json::json!(format!("%{}", 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)]
mod tests {
use super::*;
#[test]
fn test_typed_column_creation() {
let col: Column<i64> = Column::new("id");
assert_eq!(col.name(), "id");
}
#[test]
fn test_typed_column_eq() {
let col: Column<i64> = Column::new("id");
let cond = col.eq(42);
assert_eq!(cond.column, "id");
assert_eq!(cond.operator, ColumnOperator::Eq);
assert_eq!(cond.value, serde_json::json!(42));
}
#[test]
fn test_typed_column_string_like() {
let col: Column<String> = Column::new("name");
let cond = col.contains("test");
assert_eq!(cond.column, "name");
assert_eq!(cond.operator, ColumnOperator::Like);
assert_eq!(cond.value, serde_json::json!("%test%"));
}
#[test]
fn test_typed_column_nullable() {
let col: Column<Option<i32>> = Column::new("age");
let cond = col.is_null();
assert_eq!(cond.column, "age");
assert_eq!(cond.operator, ColumnOperator::IsNull);
}
#[test]
fn test_typed_column_between() {
let col: Column<i32> = Column::new("score");
let cond = col.between(10, 100);
assert_eq!(cond.column, "score");
assert_eq!(cond.operator, ColumnOperator::Between);
assert_eq!(cond.value, serde_json::json!([10, 100]));
}
#[test]
fn test_typed_column_in() {
let col: Column<String> = Column::new("status");
let cond = col.is_in(vec!["active", "pending"]);
assert_eq!(cond.column, "status");
assert_eq!(cond.operator, ColumnOperator::In);
}
}