diff --git a/cot-macros/src/model.rs b/cot-macros/src/model.rs index 91006c40..e885cf99 100644 --- a/cot-macros/src/model.rs +++ b/cot-macros/src/model.rs @@ -153,8 +153,8 @@ impl ModelBuilder { self.fields_as_field_refs.push(quote!( #[doc = concat!("Field reference to [`", stringify!(#name), "::", stringify!(#column_name), "`].")] - pub const #name: #orm_ident::query::FieldRef<#ty> = - #orm_ident::query::FieldRef::<#ty>::new(#orm_ident::Identifier::new(#column_name)); + pub const #name: #orm_ident::query::expr::FieldRef<#ty> = + #orm_ident::query::expr::FieldRef::<#ty>::new(#orm_ident::Identifier::new(#column_name)); )); } diff --git a/cot-macros/src/query.rs b/cot-macros/src/query.rs index 70d1c884..a4f059b8 100644 --- a/cot-macros/src/query.rs +++ b/cot-macros/src/query.rs @@ -23,6 +23,62 @@ impl Parse for Query { } } +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub(crate) struct FieldRefMethod { + name: &'static str, + arity: u32, +} + +const FIELD_REF_METHODS: &[FieldRefMethod] = &[ + FieldRefMethod { + name: "contains", + arity: 1, + }, + FieldRefMethod { + name: "icontains", + arity: 1, + }, + FieldRefMethod { + name: "starts_with", + arity: 1, + }, + FieldRefMethod { + name: "istarts_with", + arity: 1, + }, + FieldRefMethod { + name: "ends_with", + arity: 1, + }, + FieldRefMethod { + name: "iends_with", + arity: 1, + }, + FieldRefMethod { + name: "raw_like", + arity: 1, + }, + FieldRefMethod { + name: "iraw_like", + arity: 1, + }, +]; + +impl FieldRefMethod { + pub(crate) fn lookup(ident: &syn::Ident) -> Option<&'static Self> { + let name = ident.to_string(); + FIELD_REF_METHODS.iter().find(|m| m.name == name) + } + + pub(crate) fn as_ident(self) -> syn::Ident { + format_ident!("{}", self.name) + } + + pub(crate) fn all_names() -> impl Iterator { + FIELD_REF_METHODS.iter().map(|m| m.name) + } +} + pub(super) fn query_to_tokens(query: Query) -> TokenStream { let crate_name = cot_ident(); let model_name = query.model_name; @@ -40,7 +96,7 @@ pub(super) fn expr_to_tokens(model_name: &syn::Type, expr: Expr) -> TokenStream quote!(<#model_name as #crate_name::db::Model>::Fields::#field_name.as_expr()) } Expr::Value(value) => { - quote!(#crate_name::db::query::Expr::value(#value)) + quote!(#crate_name::db::query::expr::Expr::value(#value)) } Expr::MemberAccess { parent, @@ -48,7 +104,7 @@ pub(super) fn expr_to_tokens(model_name: &syn::Type, expr: Expr) -> TokenStream .. } => match parent.as_tokens() { Some(tokens) => { - quote!(#crate_name::db::query::Expr::value(#tokens.#member_name)) + quote!(#crate_name::db::query::expr::Expr::value(#tokens.#member_name)) } None => syn::Error::new_spanned( parent.as_tokens_full(), @@ -62,7 +118,7 @@ pub(super) fn expr_to_tokens(model_name: &syn::Type, expr: Expr) -> TokenStream .. } => match parent.as_tokens() { Some(tokens) => { - quote!(#crate_name::db::query::Expr::value(#tokens::#path_segment)) + quote!(#crate_name::db::query::expr::Expr::value(#tokens::#path_segment)) } None => syn::Error::new_spanned( parent.as_tokens_full(), @@ -70,25 +126,42 @@ pub(super) fn expr_to_tokens(model_name: &syn::Type, expr: Expr) -> TokenStream ) .to_compile_error(), }, - Expr::FunctionCall { function, args } => match function.as_tokens() { - Some(tokens) => { - quote!(#crate_name::db::query::Expr::value(#tokens(#(#args),*))) + Expr::FunctionCall { function, args } => { + let non_field_tokens = function.as_tokens(); + + if non_field_tokens.is_none() + && let Expr::MemberAccess { member_name, .. } = &*function + && let Some(method) = FieldRefMethod::lookup(member_name) + { + let Expr::MemberAccess { parent, .. } = *function else { + unreachable!("function call must have a parent"); + }; + return handle_field_ref_method(model_name, *parent, &args, *method); } - None => syn::Error::new_spanned( - function.as_tokens_full(), - "calling functions that reference database fields is unsupported", - ) - .to_compile_error(), - }, + + if let Some(tokens) = non_field_tokens { + quote!(#crate_name::db::query::expr::Expr::value(#tokens(#(#args),*))) + } else { + let all_function_names = FieldRefMethod::all_names() + .map(|name| format!("`{name}`")) + .collect::>() + .join(", "); + let msg = format!( + "calling functions that reference database fields is unsupported \ + (only {all_function_names} are supported directly on database fields)" + ); + syn::Error::new_spanned(function.as_tokens_full(), msg).to_compile_error() + } + } Expr::And(lhs, rhs) => { let lhs = expr_to_tokens(model_name, *lhs); let rhs = expr_to_tokens(model_name, *rhs); - quote!(#crate_name::db::query::Expr::and(#lhs, #rhs)) + quote!(#crate_name::db::query::expr::Expr::and(#lhs, #rhs)) } Expr::Or(lhs, rhs) => { let lhs = expr_to_tokens(model_name, *lhs); let rhs = expr_to_tokens(model_name, *rhs); - quote!(#crate_name::db::query::Expr::or(#lhs, #rhs)) + quote!(#crate_name::db::query::expr::Expr::or(#lhs, #rhs)) } Expr::Eq(lhs, rhs) => handle_binary_comparison(model_name, *lhs, *rhs, "eq", "ExprEq"), Expr::Ne(lhs, rhs) => handle_binary_comparison(model_name, *lhs, *rhs, "ne", "ExprEq"), @@ -117,10 +190,150 @@ fn handle_binary_comparison( if let Expr::FieldRef { ref field_name, .. } = lhs && let Some(rhs_tokens) = rhs.as_tokens() { - return quote!(#crate_name::db::query::#bin_trait::#bin_fn(<#model_name as #crate_name::db::Model>::Fields::#field_name, #rhs_tokens)); + return quote!(#crate_name::db::query::expr::#bin_trait::#bin_fn(<#model_name as #crate_name::db::Model>::Fields::#field_name, #rhs_tokens)); } let lhs = expr_to_tokens(model_name, lhs); let rhs = expr_to_tokens(model_name, rhs); - quote!(#crate_name::db::query::Expr::#bin_fn(#lhs, #rhs)) + quote!(#crate_name::db::query::expr::Expr::#bin_fn(#lhs, #rhs)) +} + +fn handle_field_ref_method( + model_name: &syn::Type, + receiver: Expr, + args: &[syn::Expr], + method: FieldRefMethod, +) -> TokenStream { + let crate_name = cot_ident(); + let method_ident = method.as_ident(); + + if method.arity as usize != args.len() { + let arity = method.arity; + let span = args + .first() + .map_or_else(proc_macro2::Span::call_site, syn::spanned::Spanned::span); + return syn::Error::new( + span, + format!( + "`{method_ident}` expects exactly {arity} argument(s), found {}", + args.len() + ), + ) + .to_compile_error(); + } + + if let Expr::FieldRef { ref field_name, .. } = receiver { + return quote! { + #crate_name::db::query::expr::ExprLike::#method_ident( + <#model_name as #crate_name::db::Model>::Fields::#field_name, + #(#args),* + ) + }; + } + + let receiver_tokens = expr_to_tokens(model_name, receiver); + let wrapped_args = args + .iter() + .map(|arg| quote!(#crate_name::db::query::expr::Expr::value(#arg))); + + quote! { + #crate_name::db::query::expr::Expr::#method_ident( + #receiver_tokens, + #(#wrapped_args),* + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_field_ref_method_all_names() { + let all_names = FieldRefMethod::all_names().collect::>(); + assert_eq!(all_names.len(), 8); + assert_eq!( + all_names, + [ + "contains", + "icontains", + "starts_with", + "istarts_with", + "ends_with", + "iends_with", + "raw_like", + "iraw_like", + ] + ); + } + + #[test] + fn test_field_ref_method_lookup() { + let idents = [ + ( + "contains", + Some(FieldRefMethod { + name: "contains", + arity: 1, + }), + ), + ( + "icontains", + Some(FieldRefMethod { + name: "icontains", + arity: 1, + }), + ), + ( + "starts_with", + Some(FieldRefMethod { + name: "starts_with", + arity: 1, + }), + ), + ( + "istarts_with", + Some(FieldRefMethod { + name: "istarts_with", + arity: 1, + }), + ), + ( + "ends_with", + Some(FieldRefMethod { + name: "ends_with", + arity: 1, + }), + ), + ( + "iends_with", + Some(FieldRefMethod { + name: "iends_with", + arity: 1, + }), + ), + ( + "raw_like", + Some(FieldRefMethod { + name: "raw_like", + arity: 1, + }), + ), + ( + "iraw_like", + Some(FieldRefMethod { + name: "iraw_like", + arity: 1, + }), + ), + ("__non_existent__", None), + ]; + + for (ident, expected) in idents { + assert_eq!( + FieldRefMethod::lookup(&format_ident!("{}", ident)).copied(), + expected + ); + } + } } diff --git a/cot-macros/tests/compile_tests.rs b/cot-macros/tests/compile_tests.rs index 0ddab5d6..96fb3ebd 100644 --- a/cot-macros/tests/compile_tests.rs +++ b/cot-macros/tests/compile_tests.rs @@ -64,6 +64,8 @@ fn func_query() { t.compile_fail("tests/ui/func_query_double_field.rs"); t.compile_fail("tests/ui/func_query_invalid_field.rs"); t.compile_fail("tests/ui/func_query_method_call_on_db_field.rs"); + t.compile_fail("tests/ui/func_query_field_ref_method_wrong_arity.rs"); + t.compile_fail("tests/ui/func_query_field_ref_non_existing_method.rs"); } #[rustversion::attr( diff --git a/cot-macros/tests/query.rs b/cot-macros/tests/query.rs index b9024057..4b78671f 100644 --- a/cot-macros/tests/query.rs +++ b/cot-macros/tests/query.rs @@ -1,4 +1,5 @@ -use cot::db::query::{Expr, ExprAdd, ExprDiv, ExprEq, ExprMul, ExprOrd, ExprSub, Query}; +use cot::db::query::Query; +use cot::db::query::expr::{Expr, ExprAdd, ExprDiv, ExprEq, ExprLike, ExprMul, ExprOrd, ExprSub}; use cot::db::{model, query}; #[model] @@ -7,8 +8,10 @@ struct MyModel { #[model(primary_key)] id: i32, name: String, + title: String, price: i64, quantity: i64, + valid: bool, } #[test] @@ -189,7 +192,7 @@ fn test_query_mul_fields() { ); assert_eq!( - ::objects().filter(::cot::db::query::ExprMul::mul( + ::objects().filter(::cot::db::query::expr::ExprMul::mul( ::Fields::quantity, 5i64 )), @@ -289,3 +292,135 @@ fn test_query_path_access() { query!(MyModel, $id == constants::ID) ); } + +#[test] +fn test_query_string_methods_on_bare_field() { + assert_eq!( + Query::::new().filter(ExprLike::contains( + ::Fields::name, + "foo" + )), + query!(MyModel, $name.contains("foo")) + ); + + assert_eq!( + Query::::new().filter(ExprLike::icontains( + ::Fields::name, + "FOO" + )), + query!(MyModel, $name.icontains("FOO")) + ); + + assert_eq!( + Query::::new().filter(ExprLike::starts_with( + ::Fields::name, + "foo" + )), + query!(MyModel, $name.starts_with("foo")) + ); + + assert_eq!( + Query::::new().filter(ExprLike::istarts_with( + ::Fields::name, + "FOO" + )), + query!(MyModel, $name.istarts_with("FOO")) + ); + + assert_eq!( + Query::::new().filter(ExprLike::ends_with( + ::Fields::name, + "bar" + )), + query!(MyModel, $name.ends_with("bar")) + ); + + assert_eq!( + Query::::new().filter(ExprLike::iends_with( + ::Fields::name, + "BAR" + )), + query!(MyModel, $name.iends_with("BAR")) + ); + + assert_eq!( + Query::::new().filter(ExprLike::raw_like( + ::Fields::name, + "f??o" + )), + query!(MyModel, $name.raw_like("f??o")) + ); + + assert_eq!( + Query::::new().filter(ExprLike::iraw_like( + ::Fields::name, + "F??O" + )), + query!(MyModel, $name.iraw_like("F??O")) + ); +} + +#[test] +fn test_query_string_method_composite_receiver_falls_back_to_expr() { + assert_eq!( + Query::::new().filter(Expr::contains( + Expr::add( + ::Fields::quantity.as_expr(), + ::Fields::id.as_expr() + ), + Expr::value("50") + )), + query!(MyModel, ($quantity + $id).contains("50")) + ); + + assert_eq!( + Query::::new().filter(Expr::ends_with( + Expr::add( + ::Fields::quantity.as_expr(), + ::Fields::id.as_expr() + ), + Expr::value("0") + )), + query!(MyModel, ($quantity + $id).ends_with("0")) + ); +} + +#[test] +fn test_query_string_method_combined_with_boolean_ops() { + assert_eq!( + Query::::new().filter(Expr::and( + ExprLike::contains(::Fields::name, "foo"), + Expr::gt( + ::Fields::id.as_expr(), + Expr::value(0) + ) + )), + query!(MyModel, $name.contains("foo") && $id > 0) + ); +} + +#[test] +fn test_query_string_method_string_concat_field_refs() { + assert_eq!( + Query::::new().filter(Expr::contains( + Expr::add( + ::Fields::name.as_expr(), + ::Fields::title.as_expr() + ), + Expr::value("foo") + )), + query!(MyModel, ($name + $title).contains("foo")) + ); +} + +#[test] +fn test_query_string_method_non_field_receiver_call() { + let allowed_names = &["foo", "bar"]; + assert_eq!( + Query::::new().filter(Expr::eq( + ::Fields::valid.as_expr(), + Expr::value(true) + )), + query!(MyModel, $valid == allowed_names.contains(&"foo")) + ); +} diff --git a/cot-macros/tests/ui/func_query_field_ref_method_wrong_arity.rs b/cot-macros/tests/ui/func_query_field_ref_method_wrong_arity.rs new file mode 100644 index 00000000..1b7fdc8a --- /dev/null +++ b/cot-macros/tests/ui/func_query_field_ref_method_wrong_arity.rs @@ -0,0 +1,13 @@ +use cot::db::{model, query}; + +#[model] +struct MyModel { + #[model(primary_key)] + id: i32, + name: String, +} + +fn main() { + query!(MyModel, $name.contains()); + query!(MyModel, $name.contains("a", "b")); +} diff --git a/cot-macros/tests/ui/func_query_field_ref_method_wrong_arity.stderr b/cot-macros/tests/ui/func_query_field_ref_method_wrong_arity.stderr new file mode 100644 index 00000000..6ae55b89 --- /dev/null +++ b/cot-macros/tests/ui/func_query_field_ref_method_wrong_arity.stderr @@ -0,0 +1,13 @@ +error: `contains` expects exactly 1 argument(s), found 0 + --> tests/ui/func_query_field_ref_method_wrong_arity.rs:11:5 + | +11 | query!(MyModel, $name.contains()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `query` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `contains` expects exactly 1 argument(s), found 2 + --> tests/ui/func_query_field_ref_method_wrong_arity.rs:12:36 + | +12 | query!(MyModel, $name.contains("a", "b")); + | ^^^ diff --git a/cot-macros/tests/ui/func_query_field_ref_non_existing_method.rs b/cot-macros/tests/ui/func_query_field_ref_non_existing_method.rs new file mode 100644 index 00000000..32734be5 --- /dev/null +++ b/cot-macros/tests/ui/func_query_field_ref_non_existing_method.rs @@ -0,0 +1,12 @@ +use cot::db::{model, query}; + +#[model] +struct MyModel { + #[model(primary_key)] + id: i32, + name: String, +} + +fn main() { + query!(MyModel, $name.non_existing_method()); +} diff --git a/cot-macros/tests/ui/func_query_field_ref_non_existing_method.stderr b/cot-macros/tests/ui/func_query_field_ref_non_existing_method.stderr new file mode 100644 index 00000000..acaf69a0 --- /dev/null +++ b/cot-macros/tests/ui/func_query_field_ref_non_existing_method.stderr @@ -0,0 +1,5 @@ +error: calling functions that reference database fields is unsupported (only `contains`, `icontains`, `starts_with`, `istarts_with`, `ends_with`, `iends_with`, `raw_like`, `iraw_like` are supported directly on database fields) + --> tests/ui/func_query_field_ref_non_existing_method.rs:11:21 + | +11 | query!(MyModel, $name.non_existing_method()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/cot/src/auth.rs b/cot/src/auth.rs index d69d3c33..63c73a9a 100644 --- a/cot/src/auth.rs +++ b/cot/src/auth.rs @@ -27,7 +27,9 @@ use thiserror::Error; use crate::config::SecretKey; #[cfg(feature = "db")] -use crate::db::{ColumnType, DatabaseField, DbValue, FromDbValue, SqlxValueRef, ToDbValue}; +use crate::db::{ + ColumnType, DatabaseField, DbValue, FromDbValue, SqlxValueRef, TextField, ToDbValue, +}; use crate::request::{Request, RequestExt}; use crate::session::Session; @@ -730,6 +732,9 @@ impl ToDbValue for Option { } } +#[cfg(feature = "db")] +impl TextField for PasswordHash {} + /// Authentication helper structure. /// /// This is an object that provides methods to sign users in and out, by using diff --git a/cot/src/common_types.rs b/cot/src/common_types.rs index b0fe2ad2..0f7980e6 100644 --- a/cot/src/common_types.rs +++ b/cot/src/common_types.rs @@ -21,7 +21,9 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; #[cfg(feature = "db")] -use crate::db::{ColumnType, DatabaseField, DbValue, FromDbValue, SqlxValueRef, ToDbValue}; +use crate::db::{ + ColumnType, DatabaseField, DbValue, FromDbValue, SqlxValueRef, TextField, ToDbValue, +}; // Maximum email length as specified in the RFC 5321 const MAX_EMAIL_LENGTH: u32 = 254; @@ -459,6 +461,9 @@ impl DatabaseField for Url { const TYPE: ColumnType = ColumnType::Text; } +#[cfg(feature = "db")] +impl TextField for Url {} + /// A validated email address. /// /// This is a newtype wrapper around [`EmailAddress`] that provides validation @@ -805,6 +810,9 @@ impl Display for Email { } } +#[cfg(feature = "db")] +impl TextField for Email {} + #[cfg(test)] mod tests { use std::convert::TryFrom; diff --git a/cot/src/db.rs b/cot/src/db.rs index 6e0ec415..79caa073 100644 --- a/cot/src/db.rs +++ b/cot/src/db.rs @@ -419,11 +419,81 @@ pub use cot_macros::model; /// let _ = query!(Customer, $status == constants::ACTIVE_STATUS); /// let _ = query!(Customer, $id == next_customer_id()); /// ``` +/// +/// ## String pattern matching +/// +/// Query expressions support substring, prefix, suffix, and raw +/// glob-pattern matching on `String` fields, via +/// [`contains`](cot::db::query::expr::Expr::contains), +/// [`starts_with`](cot::db::query::expr::Expr::starts_with), +/// [`ends_with`](cot::db::query::expr::Expr::ends_with), and +/// [`raw_like`](cot::db::query::expr::Expr::raw_like), along with an +/// `i`-prefixed case-insensitive counterpart of each +/// ([`icontains`](cot::db::query::expr::Expr::icontains), +/// [`istarts_with`](cot::db::query::expr::Expr::istarts_with), +/// [`iends_with`](cot::db::query::expr::Expr::iends_with), +/// [`iraw_like`](cot::db::query::expr::Expr::iraw_like)). +/// +/// ``` +/// use cot::db::{model, query}; +/// +/// # #[model] +/// # struct Customer { +/// # #[model(primary_key)] +/// # id: i32, +/// # full_name: String, +/// # } +/// let _ = query!(Customer, $full_name.contains("Doe")); +/// let _ = query!(Customer, $full_name.icontains("doe")); +/// let _ = query!(Customer, $full_name.starts_with("Jon")); +/// let _ = query!(Customer, $full_name.istarts_with("jon")); +/// let _ = query!(Customer, $full_name.ends_with("Doe")); +/// let _ = query!(Customer, $full_name.iends_with("doe")); +/// ``` +/// +/// `raw_like`/`iraw_like` accept a raw glob pattern (`*` for zero-or-more +/// characters, `?` for exactly one, `\` to escape a wildcard) rather than +/// a plain literal, for shapes the other four methods can't express, such +/// as a fixed-width or middle-of-string match: +/// +/// ``` +/// use cot::db::{model, query}; +/// +/// # #[model] +/// # struct Customer { +/// # #[model(primary_key)] +/// # id: i32, +/// # full_name: String, +/// # } +/// let _ = query!(Customer, $full_name.raw_like("J?n Do*")); +/// let _ = query!(Customer, $full_name.iraw_like("j*n ??e")); +/// ``` +/// +/// These methods can be combined with boolean and comparison operators +/// like any other expression: +/// +/// ``` +/// use cot::db::{model, query}; +/// +/// # #[model] +/// # struct Customer { +/// # #[model(primary_key)] +/// # id: i32, +/// # full_name: String, +/// # is_active: bool, +/// # } +/// let _ = query!(Customer, $full_name.contains("Doe") && $is_active == true); +/// ``` +/// +/// See [`Expr::contains`](cot::db::query::expr::Expr::contains) and +/// [`Expr::raw_like`](cot::db::query::expr::Expr::raw_like) for the full +/// semantics, escaping rules, and glob pattern syntax. pub use cot_macros::query; use derive_more::{Debug, Deref, Display}; #[cfg(test)] use mockall::automock; use query::Query; +use query::expr::like::{CaseSensitivity, LikeExprBuilder}; pub use relations::{ForeignKey, ForeignKeyOnDeletePolicy, ForeignKeyOnUpdatePolicy}; use sea_query::{ ColumnRef, ExprTrait, Iden, IntoColumnRef, OnConflict, ReturningClause, SchemaStatementBuilder, @@ -442,6 +512,7 @@ use crate::db::impl_postgres::{DatabasePostgres, PostgresRow, PostgresValueRef}; #[cfg(feature = "sqlite")] use crate::db::impl_sqlite::{DatabaseSqlite, SqliteRow, SqliteValueRef}; use crate::db::migrations::ColumnTypeMapper; +use crate::db::query::QueryBuildingError; const ERROR_PREFIX: &str = "database error:"; /// An error that can occur when interacting with the database. @@ -453,7 +524,7 @@ pub enum DatabaseError { DatabaseEngineError(#[from] sqlx::Error), /// Error when building query. #[error("{ERROR_PREFIX} error when building query: {0}")] - QueryBuildingError(#[from] sea_query::error::Error), + QueryBuildingError(#[from] QueryBuildingError), /// Type mismatch in database value. #[error( "{ERROR_PREFIX} type mismatch in database value: expected `{expected}`, found `{found}`. \ @@ -1184,6 +1255,9 @@ pub trait SqlxValueRef<'r>: Sized { } } +/// Marker trait for DB field types that behave like texts. +pub trait TextField: ToDbFieldValue {} + /// A database connection structure that holds the connection to the database. /// /// It is used to execute queries and interact with the database. The connection @@ -1383,7 +1457,8 @@ impl Database { .into_iter() .map(SimpleExpr::Value) .collect::>(), - )? + ) + .map_err(QueryBuildingError::SeaQuery)? .or_default_values() .to_owned(); if update && !value_identifiers.is_empty() { @@ -1647,7 +1722,9 @@ impl Database { .collect(); debug_assert!(!db_values.is_empty(), "expected at least 1 value field"); - insert_statement.values(db_values)?; + insert_statement + .values(db_values) + .map_err(QueryBuildingError::SeaQuery)?; } if update { @@ -1744,7 +1821,7 @@ impl Database { let columns_to_get: Vec<_> = T::COLUMNS.iter().map(|column| column.name).collect(); let mut select = sea_query::Query::select(); select.columns(columns_to_get).from(T::TABLE_NAME); - query.add_filter_to_statement(&mut select); + query.add_filter_to_statement(&mut select, self)?; query.add_limit_to_statement(&mut select); query.add_offset_to_statement(&mut select); @@ -1770,7 +1847,7 @@ impl Database { let columns_to_get: Vec<_> = T::COLUMNS.iter().map(|column| column.name).collect(); let mut select = sea_query::Query::select(); select.columns(columns_to_get).from(T::TABLE_NAME); - query.add_filter_to_statement(&mut select); + query.add_filter_to_statement(&mut select, self)?; select.limit(1); let row = self.fetch_option(&select).await?; @@ -1796,7 +1873,7 @@ impl Database { pub async fn exists(&self, query: &Query) -> Result { let mut select = sea_query::Query::select(); select.expr(sea_query::Expr::value(1)).from(T::TABLE_NAME); - query.add_filter_to_statement(&mut select); + query.add_filter_to_statement(&mut select, self)?; select.limit(1); let rows = self.fetch_option(&select).await?; @@ -1818,7 +1895,7 @@ impl Database { pub async fn delete(&self, query: &Query) -> Result { let mut delete = sea_query::Query::delete(); delete.from_table(T::TABLE_NAME); - query.add_filter_to_statement(&mut delete); + query.add_filter_to_statement(&mut delete, self)?; self.execute_statement(&delete).await } @@ -2104,6 +2181,24 @@ impl ColumnTypeMapper for Database { } } +impl LikeExprBuilder for Database { + fn like_expr( + &self, + lhs: SimpleExpr, + glob_pattern: &str, + case_sensitivity: CaseSensitivity, + ) -> std::result::Result { + match &*self.inner { + #[cfg(feature = "sqlite")] + DatabaseImpl::Sqlite(inner) => inner.like_expr(lhs, glob_pattern, case_sensitivity), + #[cfg(feature = "postgres")] + DatabaseImpl::Postgres(inner) => inner.like_expr(lhs, glob_pattern, case_sensitivity), + #[cfg(feature = "mysql")] + DatabaseImpl::MySql(inner) => inner.like_expr(lhs, glob_pattern, case_sensitivity), + } + } +} + /// A trait that provides a backend for the database. /// /// This trait is used to provide a backend for the database. diff --git a/cot/src/db/fields.rs b/cot/src/db/fields.rs index ce820e9f..e420143b 100644 --- a/cot/src/db/fields.rs +++ b/cot/src/db/fields.rs @@ -10,7 +10,7 @@ use crate::db::impl_postgres::PostgresValueRef; use crate::db::impl_sqlite::SqliteValueRef; use crate::db::{ Auto, ColumnType, DatabaseError, DatabaseField, DbFieldValue, DbValue, ForeignKey, FromDbValue, - LimitedString, Model, PrimaryKey, Result, SqlxValueRef, ToDbFieldValue, ToDbValue, + LimitedString, Model, PrimaryKey, Result, SqlxValueRef, TextField, ToDbFieldValue, ToDbValue, }; mod chrono_fields; @@ -248,6 +248,8 @@ impl_db_field!(Vec, Blob); impl_db_field!(Bytes, Blob, with Vec); +impl TextField for String {} + impl ToDbValue for &str { fn to_db_value(&self) -> DbValue { (*self).to_string().into() @@ -333,6 +335,8 @@ impl FromDbValue for Option> { } } +impl TextField for LimitedString {} + impl DatabaseField for ForeignKey { const NULLABLE: bool = T::PrimaryKey::NULLABLE; const TYPE: ColumnType = T::PrimaryKey::TYPE; diff --git a/cot/src/db/impl_mysql.rs b/cot/src/db/impl_mysql.rs index aff01777..eaf2c314 100644 --- a/cot/src/db/impl_mysql.rs +++ b/cot/src/db/impl_mysql.rs @@ -1,6 +1,11 @@ //! Database interface implementation – MySQL backend. +use cot::db::query::expr::like::LIKE_ESCAPE_CHAR; +use sea_query::{ExprTrait, LikeExpr, SimpleExpr}; + use crate::db::ColumnType; +use crate::db::query::QueryBuildingError; +use crate::db::query::expr::like::{CaseSensitivity, LikeExprBuilder, to_sql_like}; use crate::db::sea_query_db::impl_sea_query_db_backend; impl_sea_query_db_backend!(DatabaseMySql: sqlx::mysql::MySql, sqlx::mysql::MySqlPool, MySqlRow, MySqlValueRef, sea_query::MysqlQueryBuilder); @@ -35,3 +40,115 @@ impl DatabaseMySql { sea_query::ColumnType::from(column_type) } } + +impl LikeExprBuilder for DatabaseMySql { + fn like_expr( + &self, + lhs: SimpleExpr, + glob_pattern: &str, + case_sensitivity: CaseSensitivity, + ) -> Result { + let sql_pattern = to_sql_like(glob_pattern); + + match case_sensitivity { + CaseSensitivity::Sensitive => { + // We assume that the database is using utf8mb4 character set, which is the + // default in MySQL 8.0. See https://dev.mysql.com/doc/refman/8.0/en/charset.html + // TODO: Allow users to change collation in the future if needed. + let collated_lhs = sea_query::Expr::cust_with_exprs("? COLLATE utf8mb4_bin", [lhs]); + let like = LikeExpr::new(sql_pattern).escape(LIKE_ESCAPE_CHAR); + Ok(collated_lhs.like(like)) + } + CaseSensitivity::Insensitive => { + let like = LikeExpr::new(sql_pattern.to_lowercase()).escape(LIKE_ESCAPE_CHAR); + Ok(sea_query::Func::lower(lhs).like(like)) + } + } + } +} + +#[cfg(test)] +mod tests { + use sea_query::{Alias, Asterisk, MysqlQueryBuilder, Query}; + + use super::*; + use crate::test::DEFAULT_MYSQL_TEST_URL; + + #[expect(clippy::unused_async)] + async fn test_db() -> DatabaseMySql { + let db_url = + std::env::var("MYSQL_URL").unwrap_or_else(|_| DEFAULT_MYSQL_TEST_URL.to_string()); + let db_connection = sqlx::mysql::MySqlPoolOptions::new() + .connect_lazy(&format!("{db_url}/mysql")) + .expect("lazy pool creation should not fail"); + DatabaseMySql { db_connection } + } + + fn col_expr() -> SimpleExpr { + sea_query::Expr::col(Alias::new("name")) + } + + fn render(expr: SimpleExpr) -> String { + Query::select() + .column(Asterisk) + .from(Alias::new("t")) + .and_where(expr) + .to_string(MysqlQueryBuilder) + } + + fn assert_where(expr: SimpleExpr, expected_where: &str) { + let sql = render(expr); + let expected = format!("SELECT * FROM `t` WHERE {expected_where}"); + assert_eq!(sql, expected); + } + + #[ignore = "Tests that use MySQL are ignored by default"] + #[cot::test] + async fn case_sensitive_applies_binary_collation() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "foo%bar", CaseSensitivity::Sensitive) + .unwrap(); + assert_where( + expr, + "(`name` COLLATE utf8mb4_bin) LIKE 'foo\\\\%bar' ESCAPE '\\\\'", + ); + } + + #[ignore = "Tests that use MySQL are ignored by default"] + #[cot::test] + async fn case_sensitive_translates_positional_wildcards() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "f??o", CaseSensitivity::Sensitive) + .unwrap(); + assert_where( + expr, + "(`name` COLLATE utf8mb4_bin) LIKE 'f__o' ESCAPE '\\\\'", + ); + } + + #[ignore = "Tests that use MySQL are ignored by default"] + #[cot::test] + async fn case_insensitive_lowercases_column_and_pattern() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "FOO*", CaseSensitivity::Insensitive) + .unwrap(); + assert_where(expr, "LOWER(`name`) LIKE 'foo%' ESCAPE '\\\\'"); + } + + #[ignore = "Tests that use MySQL are ignored by default"] + #[cot::test] + async fn case_insensitive_does_not_force_binary_collation() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "foo*", CaseSensitivity::Insensitive) + .unwrap(); + let sql = render(expr); + assert!( + !sql.contains("COLLATE"), + "insensitive path relies on LOWER, not collation: {sql}" + ); + } +} diff --git a/cot/src/db/impl_postgres.rs b/cot/src/db/impl_postgres.rs index b54deadb..46d3f9b0 100644 --- a/cot/src/db/impl_postgres.rs +++ b/cot/src/db/impl_postgres.rs @@ -1,5 +1,10 @@ //! Database interface implementation – PostgreSQL backend. +use cot::db::query::QueryBuildingError; +use sea_query::extension::postgres::PgExpr; +use sea_query::{ExprTrait, LikeExpr, SimpleExpr}; + +use crate::db::query::expr::like::{CaseSensitivity, LikeExprBuilder, to_sql_like}; use crate::db::sea_query_db::impl_sea_query_db_backend; impl_sea_query_db_backend!(DatabasePostgres: sqlx::postgres::Postgres, sqlx::postgres::PgPool, PostgresRow, PostgresValueRef, sea_query::PostgresQueryBuilder); @@ -42,3 +47,95 @@ impl DatabasePostgres { sea_query::ColumnType::from(column_type) } } + +impl LikeExprBuilder for DatabasePostgres { + fn like_expr( + &self, + lhs: SimpleExpr, + glob_pattern: &str, + case_sensitivity: CaseSensitivity, + ) -> Result { + let glob = LikeExpr::new(to_sql_like(glob_pattern)); + + match case_sensitivity { + CaseSensitivity::Sensitive => Ok(lhs.like(glob)), + CaseSensitivity::Insensitive => Ok(lhs.ilike(glob)), + } + } +} + +#[cfg(test)] +mod tests { + use sea_query::{Alias, Asterisk, PostgresQueryBuilder, Query}; + + use super::*; + use crate::test::DEFAULT_POSTGRES_TEST_URL; + + #[expect(clippy::unused_async)] + async fn test_db() -> DatabasePostgres { + let db_url = + std::env::var("POSTGRES_URL").unwrap_or_else(|_| DEFAULT_POSTGRES_TEST_URL.to_string()); + let db_connection = sqlx::postgres::PgPoolOptions::new() + .connect_lazy(&format!("{db_url}/postgres")) + .expect("lazy pool creation should not fail"); + DatabasePostgres { db_connection } + } + + fn col_expr() -> SimpleExpr { + sea_query::Expr::col(Alias::new("name")) + } + + fn render(expr: SimpleExpr) -> String { + Query::select() + .column(Asterisk) + .from(Alias::new("t")) + .and_where(expr) + .to_string(PostgresQueryBuilder) + } + + fn assert_where(expr: SimpleExpr, expected_where: &str) { + let sql = render(expr); + let expected = format!("SELECT * FROM \"t\" WHERE {expected_where}"); + assert_eq!(sql, expected); + } + + #[ignore = "Tests that use PostgreSQL are ignored by default"] + #[cot::test] + async fn case_sensitive_uses_like_not_ilike() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "foo*", CaseSensitivity::Sensitive) + .unwrap(); + assert_where(expr, "\"name\" LIKE 'foo%'"); + } + + #[ignore = "Tests that use PostgreSQL are ignored by default"] + #[cot::test] + async fn case_insensitive_uses_ilike_and_preserves_pattern_case() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "Foo*", CaseSensitivity::Insensitive) + .unwrap(); + assert_where(expr, "\"name\" ILIKE 'Foo%'"); + } + + #[ignore = "Tests that use PostgreSQL are ignored by default"] + #[cot::test] + async fn glob_wildcards_translate_to_sql_wildcards() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "f??o", CaseSensitivity::Sensitive) + .unwrap(); + assert_where(expr, "\"name\" LIKE 'f__o'"); + } + + #[ignore = "Tests that use PostgreSQL are ignored by default"] + #[cot::test] + async fn literal_percent_and_underscore_are_escaped() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "100%off_sale", CaseSensitivity::Sensitive) + .unwrap(); + assert_where(expr, "\"name\" LIKE E'100\\\\%off\\\\_sale'"); + } +} diff --git a/cot/src/db/impl_sqlite.rs b/cot/src/db/impl_sqlite.rs index 576ab9db..abaf2e12 100644 --- a/cot/src/db/impl_sqlite.rs +++ b/cot/src/db/impl_sqlite.rs @@ -1,7 +1,13 @@ //! Database interface implementation – SQLite backend. +use sea_query::extension::sqlite::SqliteExpr; +use sea_query::{ExprTrait, LikeExpr, SimpleExpr}; use sea_query_sqlx::SqlxValues; +use crate::db::query::QueryBuildingError; +use crate::db::query::expr::like::{ + CaseSensitivity, LIKE_ESCAPE_CHAR, LikeExprBuilder, to_sql_like, +}; use crate::db::sea_query_db::impl_sea_query_db_backend; impl_sea_query_db_backend!(DatabaseSqlite: sqlx::sqlite::Sqlite, sqlx::sqlite::SqlitePool, SqliteRow, SqliteValueRef, sea_query::SqliteQueryBuilder); @@ -35,3 +41,180 @@ impl DatabaseSqlite { sea_query::ColumnType::from(column_type) } } + +impl LikeExprBuilder for DatabaseSqlite { + fn like_expr( + &self, + lhs: SimpleExpr, + glob_pattern: &str, + case_sensitivity: CaseSensitivity, + ) -> Result { + match case_sensitivity { + CaseSensitivity::Sensitive => { + let glob = to_sqlite_glob(glob_pattern); + Ok(lhs.glob(glob)) + } + CaseSensitivity::Insensitive => { + let like = LikeExpr::new(to_sql_like(&glob_pattern.to_lowercase())) + .escape(LIKE_ESCAPE_CHAR); + Ok(sea_query::Func::lower(lhs).like(like)) + } + } + } +} + +fn to_sqlite_glob(glob: &str) -> String { + let mut escaped = String::with_capacity(glob.len()); + let mut chars = glob.chars(); + while let Some(ch) = chars.next() { + match ch { + LIKE_ESCAPE_CHAR => { + if let Some(ch) = chars.next() { + push_glob_literal(&mut escaped, ch); + } + } + '*' => escaped.push('*'), + '?' => escaped.push('?'), + other => push_glob_literal(&mut escaped, other), + } + } + escaped +} + +fn push_glob_literal(out: &mut String, ch: char) { + if matches!(ch, '*' | '?' | '[') { + out.push('['); + out.push(ch); + out.push(']'); + } else { + out.push(ch); + } +} + +#[cfg(test)] +mod tests { + use sea_query::{Alias, Asterisk, Query, SqliteQueryBuilder}; + + use super::*; + use crate::test::TestDatabase; + + #[test] + fn test_to_sqlite_glob() { + assert_eq!(to_sqlite_glob("*foo*"), "*foo*"); + assert_eq!(to_sqlite_glob("f?o"), "f?o"); + assert_eq!(to_sqlite_glob("a\\*b"), "a[*]b"); + assert_eq!(to_sqlite_glob("a\\?b"), "a[?]b"); + assert_eq!(to_sqlite_glob("100[percent]"), "100[[]percent]"); + assert_eq!(to_sqlite_glob("[abc]"), "[[]abc]"); + assert_eq!(to_sqlite_glob("hello world"), "hello world"); + assert_eq!(to_sqlite_glob(""), ""); + assert_eq!(to_sqlite_glob("foo\\"), "foo"); + assert_eq!(to_sqlite_glob("C:\\\\Users"), "C:\\Users"); + assert_eq!(to_sqlite_glob("*foo\\*bar?baz*"), "*foo[*]bar?baz*"); + assert_eq!(to_sqlite_glob("\\*\\?\\["), "[*][?][[]"); + assert_eq!(to_sqlite_glob("café*"), "café*"); + assert_eq!(to_sqlite_glob("日本語?"), "日本語?"); + assert_eq!(to_sqlite_glob("***"), "***"); + assert_eq!(to_sqlite_glob("???"), "???"); + } + + fn assert_where(expr: SimpleExpr, expected_where: &str) { + let sql = render(expr); + let expected = format!("SELECT * FROM \"t\" WHERE {expected_where}"); + assert_eq!(sql, expected); + } + + async fn test_db() -> TestDatabase { + TestDatabase::new_sqlite().await.unwrap() + } + + fn col_expr() -> SimpleExpr { + sea_query::Expr::col(Alias::new("name")) + } + + fn render(expr: SimpleExpr) -> String { + Query::select() + .column(Asterisk) + .from(Alias::new("t")) + .and_where(expr) + .to_string(SqliteQueryBuilder) + } + + #[cfg_attr( + miri, + ignore = "unsupported operation: can't call foreign function `sqlite3_open_v2` on OS `linux`" + )] + #[cot::test] + async fn case_sensitive_uses_glob() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "foo*", CaseSensitivity::Sensitive) + .unwrap(); + assert_where(expr, "\"name\" GLOB 'foo*'"); + } + + #[cfg_attr( + miri, + ignore = "unsupported operation: can't call foreign function `sqlite3_open_v2` on OS `linux`" + )] + #[cot::test] + async fn case_sensitive_positional_pattern_translates_question_marks() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "f??o", CaseSensitivity::Sensitive) + .unwrap(); + assert_where(expr, "\"name\" GLOB 'f??o'"); + } + + #[cfg_attr( + miri, + ignore = "unsupported operation: can't call foreign function `sqlite3_open_v2` on OS `linux`" + )] + #[cot::test] + async fn case_sensitive_escapes_literal_wildcard_chars_for_glob() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "50\\*off", CaseSensitivity::Sensitive) + .unwrap(); + assert_where(expr, "\"name\" GLOB '50[*]off'"); + } + + #[cfg_attr( + miri, + ignore = "unsupported operation: can't call foreign function `sqlite3_open_v2` on OS `linux`" + )] + #[cot::test] + async fn case_insensitive_uses_lower_and_like() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "Foo*", CaseSensitivity::Insensitive) + .unwrap(); + assert_where(expr, "LOWER(\"name\") LIKE 'foo%' ESCAPE '\\'"); + } + + #[cfg_attr( + miri, + ignore = "unsupported operation: can't call foreign function `sqlite3_open_v2` on OS `linux`" + )] + #[cot::test] + async fn case_insensitive_pattern_is_lowercased_before_conversion() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "README", CaseSensitivity::Insensitive) + .unwrap(); + assert_where(expr, "LOWER(\"name\") LIKE 'readme' ESCAPE '\\'"); + } + + #[cfg_attr( + miri, + ignore = "unsupported operation: can't call foreign function `sqlite3_open_v2` on OS `linux`" + )] + #[cot::test] + async fn case_insensitive_includes_escape_clause() { + let db = test_db().await; + let expr = db + .like_expr(col_expr(), "100\\%off", CaseSensitivity::Insensitive) + .unwrap(); + assert_where(expr, "LOWER(\"name\") LIKE '100\\%off' ESCAPE '\\'"); + } +} diff --git a/cot/src/db/query.rs b/cot/src/db/query.rs index b3283d95..253ffb4c 100644 --- a/cot/src/db/query.rs +++ b/cot/src/db/query.rs @@ -1,15 +1,32 @@ //! Database query builder. +pub mod expr; + use std::marker::PhantomData; use derive_more::with_trait::Debug; -use sea_query::{ExprTrait, IntoColumnRef}; +use sea_query::ExprTrait; +use thiserror::Error; use crate::db; +use crate::db::query::expr::SqlQueryBuilder; +pub use crate::db::query::expr::{Expr, ExprAdd, ExprDiv, ExprMul, ExprOrd, ExprSub}; use crate::db::{ - Auto, Database, DatabaseBackend, DbFieldValue, DbValue, ForeignKey, FromDbValue, Identifier, - Model, StatementResult, ToDbFieldValue, + Auto, Database, DatabaseBackend, ForeignKey, Model, StatementResult, ToDbFieldValue, }; +const ERROR_PREFIX: &str = "expression error:"; + +/// An error that can occur when building a query. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum QueryBuildingError { + /// Error when building an expression. + #[error("{ERROR_PREFIX} unsupported expression: {0}")] + UnsupportedExpr(String), + /// Error when building a query. + #[error(transparent)] + SeaQuery(#[from] sea_query::error::Error), +} /// A query that can be executed on a database. Can be used to filter, update, /// or delete rows. @@ -201,7 +218,7 @@ impl Query { select .from(T::TABLE_NAME) .expr(sea_query::Expr::col(sea_query::Asterisk).count()); - self.add_filter_to_statement(&mut select); + self.add_filter_to_statement(&mut select, db)?; let row = db.fetch_option(&select).await?; let count = match row { #[expect(clippy::cast_sign_loss)] @@ -232,10 +249,12 @@ impl Query { pub(super) fn add_filter_to_statement( &self, statement: &mut S, - ) { + sql_builder: &dyn SqlQueryBuilder, + ) -> Result<(), QueryBuildingError> { if let Some(filter) = &self.filter { - statement.and_where(filter.as_sea_query_expr()); + statement.and_where(filter.as_sea_query_expr(sql_builder)?); } + Ok(()) } pub(super) fn add_limit_to_statement(&self, statement: &mut sea_query::SelectStatement) { @@ -251,1169 +270,56 @@ impl Query { } } -/// An expression that can be used to filter, update, or delete rows. +/// A trait for database types that can be converted to the field type. /// -/// This is used to create complex queries with multiple conditions. Typically, -/// it is only internally used by the [`cot::db::query!`] macro to create a -/// [`Query`]. +/// This trait is mostly a helper trait to make comparisons like `$id == 5` +/// where `id` is of type [`Auto`] or [`ForeignKey`] easier to write and more +/// readable. /// /// # Example /// /// ``` -/// use cot::db::{model, query}; -/// use cot::db::query::{Expr, Query}; +/// use cot::db::query::Query; +/// use cot::db::query::expr::{Expr, ExprEq}; +/// use cot::db::{Auto, model, query}; /// /// #[model] /// struct MyModel { /// #[model(primary_key)] -/// id: i32, +/// id: Auto, /// }; /// -/// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); -/// -/// assert_eq!( -/// >::new().filter(expr), -/// query!(MyModel, $id == 5) -/// ); +/// // uses the `IntoField` trait to convert the `5` to `Auto` +/// let expr = ::Fields::id.eq(5); /// ``` -#[derive(Debug, Clone, PartialEq)] -#[non_exhaustive] -pub enum Expr { - /// An expression containing a reference to a column. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == 5) - /// ); - /// ``` - Field(Identifier), - /// An expression containing a literal value. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::ne(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id != 5) - /// ); - /// ``` - Value(DbValue), - /// An `AND` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::and( - /// Expr::gt(Expr::field("id"), Expr::value(10)), - /// Expr::lt(Expr::field("id"), Expr::value(20)) - /// ); - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id > 10 && $id < 20) - /// ); - /// ``` - And(Box, Box), - /// An `OR` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::or( - /// Expr::gt(Expr::field("id"), Expr::value(10)), - /// Expr::lt(Expr::field("id"), Expr::value(20)) - /// ); - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id > 10 || $id < 20) - /// ); - /// ``` - Or(Box, Box), - /// An `=` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == 5) - /// ); - /// ``` - Eq(Box, Box), - /// A `!=` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::ne(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id != 5) - /// ); - /// ``` - Ne(Box, Box), - /// A `<` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::lt(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id < 5) - /// ); - /// ``` - Lt(Box, Box), - /// A `<=` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::lte(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id <= 5) - /// ); - /// ``` - Lte(Box, Box), - /// A `>` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::gt(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id > 5) - /// ); - /// ``` - Gt(Box, Box), - /// A `>=` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::gte(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id >= 5) - /// ); - /// ``` - Gte(Box, Box), - /// A `+` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// id_2: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::add(Expr::field("id_2"), Expr::value(5))); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == $id_2 + 5) - /// ); - /// ``` - Add(Box, Box), - /// A `-` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// id_2: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::sub(Expr::field("id_2"), Expr::value(5))); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == $id_2 - 5) - /// ); - /// ``` - Sub(Box, Box), - /// A `*` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// id_2: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::mul(Expr::field("id_2"), Expr::value(2))); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == $id_2 * 2) - /// ); - /// ``` - Mul(Box, Box), - /// A `/` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// id_2: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::div(Expr::field("id_2"), Expr::value(2))); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == $id_2 / 2) - /// ); - /// ``` - Div(Box, Box), +pub trait IntoField { + /// Converts the type to the field type. + fn into_field(self) -> T; } -impl Expr { - /// Create a new field expression. This represents a reference to a column - /// in the database. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == 5) - /// ); - /// ``` - #[must_use] - pub fn field>(identifier: T) -> Self { - Self::Field(identifier.into()) +impl IntoField for T { + fn into_field(self) -> T { + self } +} - /// Create a new value expression. This represents a literal value that gets - /// passed into the SQL query. - /// - /// # Panics - /// - /// If the value provided is a [`DbFieldValue::Auto`]. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::ne(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id != 5) - /// ); - /// ``` - #[must_use] - #[expect(clippy::needless_pass_by_value)] - pub fn value(value: T) -> Self { - match value.to_db_field_value() { - DbFieldValue::Value(value) => Self::Value(value), - DbFieldValue::Auto => panic!("Cannot create query with a non-value field"), - } +impl IntoField> for T { + fn into_field(self) -> Auto { + Auto::fixed(self) } +} - /// Create a new `AND` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::and( - /// Expr::gt(Expr::field("id"), Expr::value(10)), - /// Expr::lt(Expr::field("id"), Expr::value(20)) - /// ); - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id > 10 && $id < 20) - /// ); - /// ``` - #[must_use] - pub fn and(lhs: Self, rhs: Self) -> Self { - Self::And(Box::new(lhs), Box::new(rhs)) +impl IntoField for &str { + fn into_field(self) -> String { + self.to_string() } +} - /// Create a new `OR` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::or( - /// Expr::gt(Expr::field("id"), Expr::value(10)), - /// Expr::lt(Expr::field("id"), Expr::value(20)) - /// ); - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id > 10 || $id < 20) - /// ); - /// ``` - #[must_use] - pub fn or(lhs: Self, rhs: Self) -> Self { - Self::Or(Box::new(lhs), Box::new(rhs)) +impl IntoField> for T { + fn into_field(self) -> ForeignKey { + ForeignKey::from(self) } - - /// Create a new `=` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == 5) - /// ); - /// ``` - #[must_use] - pub fn eq(lhs: Self, rhs: Self) -> Self { - Self::Eq(Box::new(lhs), Box::new(rhs)) - } - - /// Create a new `!=` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::ne(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id != 5) - /// ); - /// ``` - #[must_use] - pub fn ne(lhs: Self, rhs: Self) -> Self { - Self::Ne(Box::new(lhs), Box::new(rhs)) - } - - /// Create a new `<` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::lt(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id < 5) - /// ); - /// ``` - #[must_use] - pub fn lt(lhs: Self, rhs: Self) -> Self { - Self::Lt(Box::new(lhs), Box::new(rhs)) - } - - /// Create a new `<=` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::lte(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id <= 5) - /// ); - /// ``` - #[must_use] - pub fn lte(lhs: Self, rhs: Self) -> Self { - Self::Lte(Box::new(lhs), Box::new(rhs)) - } - - /// Create a new `>` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::gt(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id > 5) - /// ); - /// ``` - #[must_use] - pub fn gt(lhs: Self, rhs: Self) -> Self { - Self::Gt(Box::new(lhs), Box::new(rhs)) - } - - /// Create a new `>=` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = Expr::gte(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id >= 5) - /// ); - /// ``` - #[must_use] - pub fn gte(lhs: Self, rhs: Self) -> Self { - Self::Gte(Box::new(lhs), Box::new(rhs)) - } - - /// Create a new `+` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// id_2: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::add(Expr::field("id_2"), Expr::value(5))); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == $id_2 + 5) - /// ); - /// ``` - #[expect(clippy::should_implement_trait)] - #[must_use] - pub fn add(lhs: Self, rhs: Self) -> Self { - Self::Add(Box::new(lhs), Box::new(rhs)) - } - - /// Create a new `-` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// id_2: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::sub(Expr::field("id_2"), Expr::value(5))); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == $id_2 - 5) - /// ); - /// ``` - #[expect(clippy::should_implement_trait)] - #[must_use] - pub fn sub(lhs: Self, rhs: Self) -> Self { - Self::Sub(Box::new(lhs), Box::new(rhs)) - } - - /// Create a new `*` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// id_2: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::mul(Expr::field("id_2"), Expr::value(2))); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == $id_2 * 2) - /// ); - /// ``` - #[expect(clippy::should_implement_trait)] - #[must_use] - pub fn mul(lhs: Self, rhs: Self) -> Self { - Self::Mul(Box::new(lhs), Box::new(rhs)) - } - - /// Create a new `/` expression. - /// - /// # Example - /// - /// ``` - /// use cot::db::{model, query}; - /// use cot::db::query::{Expr, Query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// id_2: i32, - /// }; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::div(Expr::field("id_2"), Expr::value(2))); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == $id_2 / 2) - /// ); - /// ``` - #[expect(clippy::should_implement_trait)] - #[must_use] - pub fn div(lhs: Self, rhs: Self) -> Self { - Self::Div(Box::new(lhs), Box::new(rhs)) - } - - /// Returns the expression as a [`sea_query::SimpleExpr`]. - /// - /// # Example - /// - /// ``` - /// use cot::db::Identifier; - /// use cot::db::query::Expr; - /// use sea_query::{ExprTrait, IntoColumnRef}; - /// - /// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); - /// - /// assert_eq!( - /// expr.as_sea_query_expr(), - /// ExprTrait::eq( - /// sea_query::SimpleExpr::Column(Identifier::new("id").into_column_ref()), - /// sea_query::SimpleExpr::Value(sea_query::Value::Int(Some(5))) - /// ) - /// ); - /// ``` - #[must_use] - pub fn as_sea_query_expr(&self) -> sea_query::SimpleExpr { - match self { - Self::Field(identifier) => (*identifier).into_column_ref().into(), - Self::Value(value) => (*value).clone().into(), - Self::And(lhs, rhs) => lhs.as_sea_query_expr().and(rhs.as_sea_query_expr()), - Self::Or(lhs, rhs) => lhs.as_sea_query_expr().or(rhs.as_sea_query_expr()), - Self::Eq(lhs, rhs) => lhs.as_sea_query_expr().eq(rhs.as_sea_query_expr()), - Self::Ne(lhs, rhs) => lhs.as_sea_query_expr().ne(rhs.as_sea_query_expr()), - Self::Lt(lhs, rhs) => lhs.as_sea_query_expr().lt(rhs.as_sea_query_expr()), - Self::Lte(lhs, rhs) => lhs.as_sea_query_expr().lte(rhs.as_sea_query_expr()), - Self::Gt(lhs, rhs) => lhs.as_sea_query_expr().gt(rhs.as_sea_query_expr()), - Self::Gte(lhs, rhs) => lhs.as_sea_query_expr().gte(rhs.as_sea_query_expr()), - Self::Add(lhs, rhs) => lhs.as_sea_query_expr().add(rhs.as_sea_query_expr()), - Self::Sub(lhs, rhs) => lhs.as_sea_query_expr().sub(rhs.as_sea_query_expr()), - Self::Mul(lhs, rhs) => lhs.as_sea_query_expr().mul(rhs.as_sea_query_expr()), - Self::Div(lhs, rhs) => lhs.as_sea_query_expr().div(rhs.as_sea_query_expr()), - } - } -} - -/// A reference to a field in a database table. -/// -/// This is used to create expressions that reference a specific column in a -/// table with a specific type. This allows for type-safe creation of queries -/// with some common operators like `=`, `!=`, `+`, `-`, `*`, and `/`. -#[derive(Debug)] -pub struct FieldRef { - identifier: Identifier, - phantom_data: PhantomData, -} - -impl FieldRef { - /// Create a new field reference. - #[must_use] - pub const fn new(identifier: Identifier) -> Self { - Self { - identifier, - phantom_data: PhantomData, - } - } -} - -impl FieldRef { - /// Returns the field reference as an [`Expr`]. - #[must_use] - pub fn as_expr(&self) -> Expr { - Expr::Field(self.identifier) - } -} - -/// A trait for types that can be compared in database expressions. -pub trait ExprEq { - /// Creates an expression that checks if the field is equal to the given - /// value. - /// - /// # Examples - /// - /// ``` - /// use cot::db::query::{Expr, ExprEq, Query}; - /// use cot::db::{model, query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = ::Fields::id.eq(5); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id == 5) - /// ); - /// ``` - fn eq>(self, other: V) -> Expr; - - /// Creates an expression that checks if the field is not equal to the given - /// value. - /// - /// # Examples - /// - /// ``` - /// use cot::db::query::{Expr, ExprEq, Query}; - /// use cot::db::{model, query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = ::Fields::id.ne(5); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id != 5) - /// ); - /// ``` - fn ne>(self, other: V) -> Expr; -} - -impl ExprEq for FieldRef { - fn eq>(self, other: V) -> Expr { - Expr::eq(self.as_expr(), Expr::value(other.into_field())) - } - - fn ne>(self, other: V) -> Expr { - Expr::ne(self.as_expr(), Expr::value(other.into_field())) - } -} - -/// A trait for database types that can be added to each other. -pub trait ExprAdd { - /// Creates an expression that adds the field to the given value. - /// - /// # Examples - /// - /// ``` - /// use cot::db::query::{Expr, ExprAdd, Query}; - /// use cot::db::{model, query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = ::Fields::id.add(5); - /// - /// assert_eq!( - /// >::new().filter(Expr::eq(Expr::field("id"), expr)), - /// query!(MyModel, $id == $id + 5) - /// ); - /// ``` - fn add>(self, other: V) -> Expr; -} - -/// A trait for database types that can be subtracted from each other. -pub trait ExprSub { - /// Creates an expression that subtracts the field from the given value. - /// - /// # Examples - /// - /// ``` - /// use cot::db::query::{Expr, ExprSub, Query}; - /// use cot::db::{model, query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = ::Fields::id.sub(5); - /// - /// assert_eq!( - /// >::new().filter(Expr::eq(Expr::field("id"), expr)), - /// query!(MyModel, $id == $id - 5) - /// ); - /// ``` - fn sub>(self, other: V) -> Expr; -} - -/// A trait for database types that can be multiplied by each other. -pub trait ExprMul { - /// Creates an expression that multiplies the field by the given value. - /// - /// # Examples - /// - /// ``` - /// use cot::db::query::{Expr, ExprMul, Query}; - /// use cot::db::{model, query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = ::Fields::id.mul(2); - /// - /// assert_eq!( - /// >::new().filter(Expr::eq(Expr::field("id"), expr)), - /// query!(MyModel, $id == $id * 2) - /// ); - /// ``` - fn mul>(self, other: V) -> Expr; -} - -/// A trait for database types that can be divided by each other. -pub trait ExprDiv { - /// Creates an expression that divides the field by the given value. - /// - /// # Examples - /// - /// ``` - /// use cot::db::query::{Expr, ExprDiv, Query}; - /// use cot::db::{model, query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = ::Fields::id.div(2); - /// - /// assert_eq!( - /// >::new().filter(Expr::eq(Expr::field("id"), expr)), - /// query!(MyModel, $id == $id / 2) - /// ); - /// ``` - fn div>(self, other: V) -> Expr; -} - -/// A trait for database types that can be ordered. -pub trait ExprOrd { - /// Creates an expression that checks if the field is less than the given - /// value. - /// - /// # Examples - /// - /// ``` - /// use cot::db::query::{Expr, ExprOrd, Query}; - /// use cot::db::{model, query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = ::Fields::id.lt(5); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id < 5) - /// ); - /// ``` - fn lt>(self, other: V) -> Expr; - /// Creates an expression that checks if the field is less than or equal to - /// the given value. - /// - /// # Examples - /// - /// ``` - /// use cot::db::query::{Expr, ExprOrd, Query}; - /// use cot::db::{model, query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = ::Fields::id.lte(5); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id <= 5) - /// ); - /// ``` - fn lte>(self, other: V) -> Expr; - - /// Creates an expression that checks if the field is greater than the given - /// value. - /// - /// # Examples - /// - /// ``` - /// use cot::db::query::{Expr, ExprOrd, Query}; - /// use cot::db::{model, query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = ::Fields::id.gt(5); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id > 5) - /// ); - /// ``` - fn gt>(self, other: V) -> Expr; - - /// Creates an expression that checks if the field is greater than or equal - /// to the given value. - /// - /// # Examples - /// - /// ``` - /// use cot::db::query::{Expr, ExprOrd, Query}; - /// use cot::db::{model, query}; - /// - /// #[model] - /// struct MyModel { - /// #[model(primary_key)] - /// id: i32, - /// }; - /// - /// let expr = ::Fields::id.gte(5); - /// - /// assert_eq!( - /// >::new().filter(expr), - /// query!(MyModel, $id >= 5) - /// ); - /// ``` - fn gte>(self, other: V) -> Expr; -} - -impl ExprOrd for FieldRef { - fn lt>(self, other: V) -> Expr { - Expr::lt(self.as_expr(), Expr::value(other.into_field())) - } - - fn lte>(self, other: V) -> Expr { - Expr::lte(self.as_expr(), Expr::value(other.into_field())) - } - - fn gt>(self, other: V) -> Expr { - Expr::gt(self.as_expr(), Expr::value(other.into_field())) - } - - fn gte>(self, other: V) -> Expr { - Expr::gte(self.as_expr(), Expr::value(other.into_field())) - } -} - -macro_rules! impl_expr { - ($ty:ty, $trait:ident, $method:ident) => { - impl $trait<$ty> for FieldRef<$ty> { - fn $method>(self, other: V) -> Expr { - Expr::$method(self.as_expr(), Expr::value(other.into())) - } - } - }; -} - -macro_rules! impl_num_expr { - ($ty:ty) => { - impl_expr!($ty, ExprAdd, add); - impl_expr!($ty, ExprSub, sub); - impl_expr!($ty, ExprMul, mul); - impl_expr!($ty, ExprDiv, div); - }; -} - -impl_num_expr!(i8); -impl_num_expr!(i16); -impl_num_expr!(i32); -impl_num_expr!(i64); -impl_num_expr!(u8); -impl_num_expr!(u16); -impl_num_expr!(u32); -impl_num_expr!(u64); -impl_num_expr!(f32); -impl_num_expr!(f64); - -/// A trait for database types that can be converted to the field type. -/// -/// This trait is mostly a helper trait to make comparisons like `$id == 5` -/// where `id` is of type [`Auto`] or [`ForeignKey`] easier to write and more -/// readable. -/// -/// # Example -/// -/// ``` -/// use cot::db::query::{Expr, ExprEq, Query}; -/// use cot::db::{Auto, model, query}; -/// -/// #[model] -/// struct MyModel { -/// #[model(primary_key)] -/// id: Auto, -/// }; -/// -/// // uses the `IntoField` trait to convert the `5` to `Auto` -/// let expr = ::Fields::id.eq(5); -/// ``` -pub trait IntoField { - /// Converts the type to the field type. - fn into_field(self) -> T; -} - -impl IntoField for T { - fn into_field(self) -> T { - self - } -} - -impl IntoField> for T { - fn into_field(self) -> Auto { - Auto::fixed(self) - } -} - -impl IntoField for &str { - fn into_field(self) -> String { - self.to_string() - } -} - -impl IntoField> for T { - fn into_field(self) -> ForeignKey { - ForeignKey::from(self) - } -} +} impl IntoField> for &T { fn into_field(self) -> ForeignKey { @@ -1523,52 +429,4 @@ mod tests { assert!(result.is_ok()); } - - #[test] - fn expr_field() { - let expr = Expr::field("name"); - if let Expr::Field(identifier) = expr { - assert_eq!(identifier.to_string(), "name"); - } else { - panic!("Expected Expr::Field"); - } - } - - #[test] - fn expr_value() { - let expr = Expr::value(30); - if let Expr::Value(value) = expr { - assert_eq!(value.to_string(), "30"); - } else { - panic!("Expected Expr::Value"); - } - } - - macro_rules! test_expr_constructor { - ($test_name:ident, $match:ident, $constructor:ident) => { - #[test] - fn $test_name() { - let expr = Expr::$constructor(Expr::field("name"), Expr::value("John")); - if let Expr::$match(lhs, rhs) = expr { - assert!(matches!(*lhs, Expr::Field(_))); - assert!(matches!(*rhs, Expr::Value(_))); - } else { - panic!(concat!("Expected Expr::", stringify!($match))); - } - } - }; - } - - test_expr_constructor!(expr_and, And, and); - test_expr_constructor!(expr_or, Or, or); - test_expr_constructor!(expr_eq, Eq, eq); - test_expr_constructor!(expr_ne, Ne, ne); - test_expr_constructor!(expr_lt, Lt, lt); - test_expr_constructor!(expr_lte, Lte, lte); - test_expr_constructor!(expr_gt, Gt, gt); - test_expr_constructor!(expr_gte, Gte, gte); - test_expr_constructor!(expr_add, Add, add); - test_expr_constructor!(expr_sub, Sub, sub); - test_expr_constructor!(expr_mul, Mul, mul); - test_expr_constructor!(expr_div, Div, div); } diff --git a/cot/src/db/query/expr.rs b/cot/src/db/query/expr.rs new file mode 100644 index 00000000..8946ccd9 --- /dev/null +++ b/cot/src/db/query/expr.rs @@ -0,0 +1,1699 @@ +//! Database expressions. +pub mod like; + +use std::marker::PhantomData; + +use cot::db::query::{IntoField, QueryBuildingError}; +use cot::db::{DbFieldValue, DbValue, FromDbValue, Identifier, ToDbFieldValue}; +pub use like::ExprLike; +use like::{CaseSensitivity, LikeExprBuilder, LikeMode}; +use sea_query::{ExprTrait, IntoColumnRef, SimpleExpr}; + +/// An expression that can be used to filter, update, or delete rows. +/// +/// This is used to create complex queries with multiple conditions. Typically, +/// it is only internally used by the [`cot::db::query!`] macro to create a +/// [`Query`]. +/// +/// # Example +/// +/// ``` +/// use cot::db::{model, query}; +/// use cot::db::query::Query; +/// use cot::db::query::expr::Expr; +/// +/// #[model] +/// struct MyModel { +/// #[model(primary_key)] +/// id: i32, +/// }; +/// +/// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); +/// +/// assert_eq!( +/// >::new().filter(expr), +/// query!(MyModel, $id == 5) +/// ); +/// ``` +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum Expr { + /// An expression containing a reference to a column. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == 5) + /// ); + /// ``` + Field(Identifier), + /// An expression containing a literal value. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::ne(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id != 5) + /// ); + /// ``` + Value(DbValue), + /// An `AND` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::and( + /// Expr::gt(Expr::field("id"), Expr::value(10)), + /// Expr::lt(Expr::field("id"), Expr::value(20)) + /// ); + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id > 10 && $id < 20) + /// ); + /// ``` + And(Box, Box), + /// An `OR` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::or( + /// Expr::gt(Expr::field("id"), Expr::value(10)), + /// Expr::lt(Expr::field("id"), Expr::value(20)) + /// ); + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id > 10 || $id < 20) + /// ); + /// ``` + Or(Box, Box), + /// An `=` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == 5) + /// ); + /// ``` + Eq(Box, Box), + /// A `!=` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::ne(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id != 5) + /// ); + /// ``` + Ne(Box, Box), + /// A `<` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::lt(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id < 5) + /// ); + /// ``` + Lt(Box, Box), + /// A `<=` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::lte(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id <= 5) + /// ); + /// ``` + Lte(Box, Box), + /// A `>` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::gt(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id > 5) + /// ); + /// ``` + Gt(Box, Box), + /// A `>=` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::gte(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id >= 5) + /// ); + /// ``` + Gte(Box, Box), + /// A `+` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// id_2: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::add(Expr::field("id_2"), Expr::value(5))); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == $id_2 + 5) + /// ); + /// ``` + Add(Box, Box), + /// A `-` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// id_2: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::sub(Expr::field("id_2"), Expr::value(5))); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == $id_2 - 5) + /// ); + /// ``` + Sub(Box, Box), + /// A `*` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// id_2: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::mul(Expr::field("id_2"), Expr::value(2))); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == $id_2 * 2) + /// ); + /// ``` + Mul(Box, Box), + /// A `/` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// id_2: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::div(Expr::field("id_2"), Expr::value(2))); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == $id_2 / 2) + /// ); + /// ``` + Div(Box, Box), + /// A case-sensitive substring match, checking whether the left-hand + /// expression contains the right-hand expression as a literal + /// substring. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// name: String, + /// }; + /// + /// let expr = Expr::contains(Expr::field("name"), Expr::value("test")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $name.contains("test")) + /// ); + /// ``` + Contains(Box, Box, CaseSensitivity), + /// A prefix match, checking whether the left-hand expression starts + /// with the right-hand expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// name: String, + /// }; + /// + /// let expr = Expr::starts_with(Expr::field("name"), Expr::value("Mr.")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $name.starts_with("Mr.")) + /// ); + /// ``` + StartsWith(Box, Box, CaseSensitivity), + /// A suffix match, checking whether the left-hand expression ends + /// with the right-hand expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// email: String, + /// }; + /// + /// let expr = Expr::ends_with(Expr::field("email"), Expr::value("@example.com")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $email.ends_with("@example.com")) + /// ); + /// ``` + EndsWith(Box, Box, CaseSensitivity), + /// A match against a raw pattern expressed in Cot's glob syntax. + /// See [`Expr::raw_like`] for the full syntax reference. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// code: String, + /// }; + /// + /// let expr = Expr::raw_like(Expr::field("code"), Expr::value("f??o")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $code.raw_like("f??o")) + /// ); + /// ``` + RawLike(Box, Box, CaseSensitivity), +} + +impl Expr { + /// Create a new field expression. This represents a reference to a column + /// in the database. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == 5) + /// ); + /// ``` + #[must_use] + pub fn field>(identifier: T) -> Self { + Self::Field(identifier.into()) + } + + /// Create a new value expression. This represents a literal value that gets + /// passed into the SQL query. + /// + /// # Panics + /// + /// If the value provided is a [`DbFieldValue::Auto`]. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::ne(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id != 5) + /// ); + /// ``` + #[must_use] + #[expect(clippy::needless_pass_by_value)] + pub fn value(value: T) -> Self { + match value.to_db_field_value() { + DbFieldValue::Value(value) => Self::Value(value), + DbFieldValue::Auto => panic!("Cannot create query with a non-value field"), + } + } + + /// Create a new `AND` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::and( + /// Expr::gt(Expr::field("id"), Expr::value(10)), + /// Expr::lt(Expr::field("id"), Expr::value(20)) + /// ); + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id > 10 && $id < 20) + /// ); + /// ``` + #[must_use] + pub fn and(lhs: Self, rhs: Self) -> Self { + Self::And(Box::new(lhs), Box::new(rhs)) + } + + /// Create a new `OR` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::or( + /// Expr::gt(Expr::field("id"), Expr::value(10)), + /// Expr::lt(Expr::field("id"), Expr::value(20)) + /// ); + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id > 10 || $id < 20) + /// ); + /// ``` + #[must_use] + pub fn or(lhs: Self, rhs: Self) -> Self { + Self::Or(Box::new(lhs), Box::new(rhs)) + } + + /// Create a new `=` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == 5) + /// ); + /// ``` + #[must_use] + pub fn eq(lhs: Self, rhs: Self) -> Self { + Self::Eq(Box::new(lhs), Box::new(rhs)) + } + + /// Create a new `!=` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::ne(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id != 5) + /// ); + /// ``` + #[must_use] + pub fn ne(lhs: Self, rhs: Self) -> Self { + Self::Ne(Box::new(lhs), Box::new(rhs)) + } + + /// Create a new `<` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::lt(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id < 5) + /// ); + /// ``` + #[must_use] + pub fn lt(lhs: Self, rhs: Self) -> Self { + Self::Lt(Box::new(lhs), Box::new(rhs)) + } + + /// Create a new `<=` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::lte(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id <= 5) + /// ); + /// ``` + #[must_use] + pub fn lte(lhs: Self, rhs: Self) -> Self { + Self::Lte(Box::new(lhs), Box::new(rhs)) + } + + /// Create a new `>` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::gt(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id > 5) + /// ); + /// ``` + #[must_use] + pub fn gt(lhs: Self, rhs: Self) -> Self { + Self::Gt(Box::new(lhs), Box::new(rhs)) + } + + /// Create a new `>=` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = Expr::gte(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id >= 5) + /// ); + /// ``` + #[must_use] + pub fn gte(lhs: Self, rhs: Self) -> Self { + Self::Gte(Box::new(lhs), Box::new(rhs)) + } + + /// Create a new `+` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// id_2: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::add(Expr::field("id_2"), Expr::value(5))); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == $id_2 + 5) + /// ); + /// ``` + #[expect(clippy::should_implement_trait)] + #[must_use] + pub fn add(lhs: Self, rhs: Self) -> Self { + Self::Add(Box::new(lhs), Box::new(rhs)) + } + + /// Create a new `-` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// id_2: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::sub(Expr::field("id_2"), Expr::value(5))); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == $id_2 - 5) + /// ); + /// ``` + #[expect(clippy::should_implement_trait)] + #[must_use] + pub fn sub(lhs: Self, rhs: Self) -> Self { + Self::Sub(Box::new(lhs), Box::new(rhs)) + } + + /// Create a new `*` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// id_2: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::mul(Expr::field("id_2"), Expr::value(2))); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == $id_2 * 2) + /// ); + /// ``` + #[expect(clippy::should_implement_trait)] + #[must_use] + pub fn mul(lhs: Self, rhs: Self) -> Self { + Self::Mul(Box::new(lhs), Box::new(rhs)) + } + + /// Create a new `/` expression. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// id_2: i32, + /// }; + /// + /// let expr = Expr::eq(Expr::field("id"), Expr::div(Expr::field("id_2"), Expr::value(2))); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == $id_2 / 2) + /// ); + /// ``` + #[expect(clippy::should_implement_trait)] + #[must_use] + pub fn div(lhs: Self, rhs: Self) -> Self { + Self::Div(Box::new(lhs), Box::new(rhs)) + } + + /// Creates an expression that checks whether `lhs` contains `rhs` as a + /// substring, comparing case-sensitively. + /// + /// The search string in `rhs` is treated as a literal: any characters in + /// it that would otherwise be treated as wildcards by [`raw_like`] are + /// escaped automatically, so `"50% off"` matches the literal text + /// `50% off`, not a wildcard pattern. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// name: String, + /// } + /// + /// let expr = Expr::contains(Expr::field("name"), Expr::value("test")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $name.contains("test")) + /// ); + /// ``` + #[must_use] + pub fn contains(lhs: Self, rhs: Self) -> Self { + Self::Contains(Box::new(lhs), Box::new(rhs), CaseSensitivity::Sensitive) + } + + /// Creates an expression that checks whether `lhs` contains `rhs` as a + /// substring, comparing case-insensitively. + /// + /// The case-insensitive counterpart to [`contains`](Self::contains). As + /// with `contains`, the search string in `rhs` is treated as a literal + /// and any wildcard-like characters in it are escaped automatically. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// name: String, + /// } + /// + /// let expr = Expr::icontains(Expr::field("name"), Expr::value("TEST")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $name.icontains("TEST")) + /// ); + /// ``` + #[must_use] + pub fn icontains(lhs: Self, rhs: Self) -> Self { + Self::Contains(Box::new(lhs), Box::new(rhs), CaseSensitivity::Insensitive) + } + + /// Creates an expression that checks whether `lhs` starts with `rhs`, + /// comparing case-sensitively. + /// + /// Behaves like [`contains`](Self::contains) but anchored to the start of + /// the value: `rhs` is treated as a literal prefix, with any + /// wildcard-like characters in it escaped automatically. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// name: String, + /// } + /// + /// let expr = Expr::starts_with(Expr::field("name"), Expr::value("Mr.")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $name.starts_with("Mr.")) + /// ); + /// ``` + #[must_use] + pub fn starts_with(lhs: Self, rhs: Self) -> Self { + Self::StartsWith(Box::new(lhs), Box::new(rhs), CaseSensitivity::Sensitive) + } + + /// Creates an expression that checks whether `lhs` starts with `rhs`, + /// comparing case-insensitively. + /// + /// The case-insensitive counterpart to [`starts_with`](Self::starts_with). + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// name: String, + /// } + /// + /// let expr = Expr::istarts_with(Expr::field("name"), Expr::value("mr.")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $name.istarts_with("mr.")) + /// ); + /// ``` + #[must_use] + pub fn istarts_with(lhs: Self, rhs: Self) -> Self { + Self::StartsWith(Box::new(lhs), Box::new(rhs), CaseSensitivity::Insensitive) + } + + /// Creates an expression that checks whether `lhs` ends with `rhs`, + /// comparing case-sensitively. + /// + /// Behaves like [`contains`](Self::contains) but anchored to the end of + /// the value: `rhs` is treated as a literal suffix, with any + /// wildcard-like characters in it escaped automatically. + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// name: String, + /// } + /// + /// let expr = Expr::ends_with(Expr::field("name"), Expr::value("foo")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $name.ends_with("foo")) + /// ); + /// ``` + #[must_use] + pub fn ends_with(lhs: Self, rhs: Self) -> Self { + Self::EndsWith(Box::new(lhs), Box::new(rhs), CaseSensitivity::Sensitive) + } + + /// Creates an expression that checks whether `lhs` ends with `rhs`, + /// comparing case-insensitively. + /// + /// The case-insensitive counterpart to [`ends_with`](Self::ends_with). + /// + /// # Example + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// name: String, + /// } + /// + /// let expr = Expr::iends_with(Expr::field("name"), Expr::value("FOO")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $name.iends_with("FOO")) + /// ); + /// ``` + #[must_use] + pub fn iends_with(lhs: Self, rhs: Self) -> Self { + Self::EndsWith(Box::new(lhs), Box::new(rhs), CaseSensitivity::Insensitive) + } + + /// Creates an expression that matches `lhs` against a raw pattern in + /// `rhs`, comparing case-sensitively. + /// + /// Unlike [`contains`](Self::contains)/[`starts_with`](Self::starts_with)/ + /// [`ends_with`](Self::ends_with), which escape the search string and + /// wrap it for you, `raw_like` passes `rhs` through as a **pattern you + /// write yourself** — use it when you need a shape those three methods + /// can't express, such as a match with wildcards in the middle, or a + /// fixed-width positional match. + /// + /// # Pattern syntax + /// + /// The pattern in `rhs` uses glob syntax: + /// + /// | Character | Meaning | + /// |-----------|---------| + /// | `*` | matches zero or more of any character | + /// | `?` | matches exactly one character | + /// | `\` | escapes the following character, making it literal | + /// + /// Any other character matches itself literally. + /// + /// # Escaping literal `*`, `?`, or `\` in the pattern + /// + /// If the text you're matching against can itself contain a literal `*` + /// or `?`, escape it with a backslash so it isn't treated as a wildcard. + /// For example, to match values that literally contain the substring + /// `"a * here"`, write the pattern as `"*a \\* here*"`. `\\*` in the Rust + /// string literal produces the two glob characters `\*`, i.e. an escaped, + /// literal asterisk, while the surrounding bare `*` characters remain + /// wildcards. + /// + /// If you don't need this level of control, prefer + /// [`contains`](Self::contains)/[`starts_with`](Self::starts_with)/ + /// [`ends_with`](Self::ends_with), which handle escaping for you + /// automatically based on a plain search string. + /// + /// # Examples + /// + /// Substring match (equivalent to `contains("foo")`, spelled out + /// explicitly): + /// + /// ``` + /// use cot::db::query::expr::Expr; + /// + /// let expr = Expr::raw_like(Expr::field("name"), Expr::value("*foo*")); + /// ``` + /// + /// Prefix match (equivalent to `starts_with("foo")`): + /// + /// ``` + /// use cot::db::query::expr::Expr; + /// + /// let expr = Expr::raw_like(Expr::field("name"), Expr::value("foo*")); + /// ``` + /// + /// **Positional match**: something `contains`/`starts_with`/`ends_with` + /// cannot express at all: match values that are exactly `f`, then any two + /// characters, then `o` (e.g. matches `faXo`, `fooo`, but not `fo` or + /// `faXYo`): + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// code: String, + /// } + /// + /// let expr = Expr::raw_like(Expr::field("code"), Expr::value("f??o")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $code.raw_like("f??o")) + /// ); + /// ``` + /// + /// Wildcards in the middle of a pattern; matching values that contain + /// `foo`, later followed by `bar`, later followed by `baz`, in that + /// order, with anything in between: + /// + /// ``` + /// use cot::db::query::expr::Expr; + /// + /// let expr = Expr::raw_like(Expr::field("path"), Expr::value("*foo*bar*baz*")); + /// ``` + #[must_use] + pub fn raw_like(lhs: Self, rhs: Self) -> Self { + Self::RawLike(Box::new(lhs), Box::new(rhs), CaseSensitivity::Sensitive) + } + + /// Creates an expression that matches `lhs` against a raw pattern in + /// `rhs`, comparing case-insensitively. + /// + /// The case-insensitive counterpart to [`raw_like`](Self::raw_like). See + /// its documentation for the pattern syntax (`*`/`?`/`\`). + /// + /// # Example + /// + /// Case-insensitive positional match — matches `README`, `ReadMe`, + /// `readme`, etc., but not `READMEE` or `REDME`: + /// + /// ``` + /// use cot::db::{model, query}; + /// use cot::db::query::Query; + /// use cot::db::query::expr::Expr; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// filename: String, + /// } + /// + /// let expr = Expr::iraw_like(Expr::field("filename"), Expr::value("re?dme")); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $filename.iraw_like("re?dme")) + /// ); + /// ``` + #[must_use] + pub fn iraw_like(lhs: Self, rhs: Self) -> Self { + Self::RawLike(Box::new(lhs), Box::new(rhs), CaseSensitivity::Insensitive) + } + + /// Returns the expression as a [`sea_query::SimpleExpr`]. + /// + /// # Example + /// + /// ``` + /// use cot::db::query::expr::Expr; + /// use cot::db::{Database, Identifier}; + /// use sea_query::{ExprTrait, IntoColumnRef}; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let db = Database::new("sqlite::memory:").await.unwrap(); + /// let expr = Expr::eq(Expr::field("id"), Expr::value(5)); + /// + /// assert_eq!( + /// expr.as_sea_query_expr(&db).unwrap(), + /// ExprTrait::eq( + /// sea_query::SimpleExpr::Column(Identifier::new("id").into_column_ref()), + /// sea_query::SimpleExpr::Value(sea_query::Value::Int(Some(5))) + /// ) + /// ); + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns [`QueryBuildingError`] if the backend cannot express a given + /// expression. + pub fn as_sea_query_expr( + &self, + sql_builder: &dyn SqlQueryBuilder, + ) -> Result { + match self { + Self::Field(identifier) => Ok((*identifier).into_column_ref().into()), + Self::Value(value) => Ok((*value).clone().into()), + Self::And(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .and(rhs.as_sea_query_expr(sql_builder)?)), + Self::Or(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .or(rhs.as_sea_query_expr(sql_builder)?)), + Self::Eq(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .eq(rhs.as_sea_query_expr(sql_builder)?)), + Self::Ne(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .ne(rhs.as_sea_query_expr(sql_builder)?)), + Self::Lt(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .lt(rhs.as_sea_query_expr(sql_builder)?)), + Self::Lte(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .lte(rhs.as_sea_query_expr(sql_builder)?)), + Self::Gt(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .gt(rhs.as_sea_query_expr(sql_builder)?)), + Self::Gte(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .gte(rhs.as_sea_query_expr(sql_builder)?)), + Self::Add(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .add(rhs.as_sea_query_expr(sql_builder)?)), + Self::Sub(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .sub(rhs.as_sea_query_expr(sql_builder)?)), + Self::Mul(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .mul(rhs.as_sea_query_expr(sql_builder)?)), + Self::Div(lhs, rhs) => Ok(lhs + .as_sea_query_expr(sql_builder)? + .div(rhs.as_sea_query_expr(sql_builder)?)), + Self::Contains(lhs, rhs, case_sensitivity) => { + like::like_expr(sql_builder, lhs, rhs, LikeMode::Contains, *case_sensitivity) + } + Self::StartsWith(lhs, rhs, case_sensitivity) => like::like_expr( + sql_builder, + lhs, + rhs, + LikeMode::StartsWith, + *case_sensitivity, + ), + Self::EndsWith(lhs, rhs, case_sensitivity) => { + like::like_expr(sql_builder, lhs, rhs, LikeMode::EndsWith, *case_sensitivity) + } + Self::RawLike(lhs, rhs, case_sensitivity) => { + like::like_expr(sql_builder, lhs, rhs, LikeMode::Raw, *case_sensitivity) + } + } + } +} + +/// A reference to a field in a database table. +/// +/// This is used to create expressions that reference a specific column in a +/// table with a specific type. This allows for type-safe creation of queries +/// with some common operators like `=`, `!=`, `+`, `-`, `*`, and `/`. +#[derive(Debug)] +pub struct FieldRef { + identifier: Identifier, + phantom_data: PhantomData, +} + +impl FieldRef { + /// Create a new field reference. + #[must_use] + pub const fn new(identifier: Identifier) -> Self { + Self { + identifier, + phantom_data: PhantomData, + } + } +} + +impl FieldRef { + /// Returns the field reference as an [`Expr`]. + #[must_use] + pub fn as_expr(&self) -> Expr { + Expr::Field(self.identifier) + } +} + +/// A trait for types that can be compared in database expressions. +pub trait ExprEq { + /// Creates an expression that checks if the field is equal to the given + /// value. + /// + /// # Examples + /// + /// ``` + /// use cot::db::query::Query; + /// use cot::db::query::expr::{Expr, ExprEq}; + /// use cot::db::{model, query}; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = ::Fields::id.eq(5); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id == 5) + /// ); + /// ``` + fn eq>(self, other: V) -> Expr; + + /// Creates an expression that checks if the field is not equal to the given + /// value. + /// + /// # Examples + /// + /// ``` + /// use cot::db::query::Query; + /// use cot::db::query::expr::{Expr, ExprEq}; + /// use cot::db::{model, query}; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = ::Fields::id.ne(5); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id != 5) + /// ); + /// ``` + fn ne>(self, other: V) -> Expr; +} + +impl ExprEq for FieldRef { + fn eq>(self, other: V) -> Expr { + Expr::eq(self.as_expr(), Expr::value(other.into_field())) + } + + fn ne>(self, other: V) -> Expr { + Expr::ne(self.as_expr(), Expr::value(other.into_field())) + } +} + +/// A trait for database types that can be added to each other. +pub trait ExprAdd { + /// Creates an expression that adds the field to the given value. + /// + /// # Examples + /// + /// ``` + /// use cot::db::query::Query; + /// use cot::db::query::{Expr, ExprAdd}; + /// use cot::db::{model, query}; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = ::Fields::id.add(5); + /// + /// assert_eq!( + /// >::new().filter(Expr::eq(Expr::field("id"), expr)), + /// query!(MyModel, $id == $id + 5) + /// ); + /// ``` + fn add>(self, other: V) -> Expr; +} + +/// A trait for database types that can be subtracted from each other. +pub trait ExprSub { + /// Creates an expression that subtracts the field from the given value. + /// + /// # Examples + /// + /// ``` + /// use cot::db::query::Query; + /// use cot::db::query::expr::{Expr, ExprSub}; + /// use cot::db::{model, query}; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = ::Fields::id.sub(5); + /// + /// assert_eq!( + /// >::new().filter(Expr::eq(Expr::field("id"), expr)), + /// query!(MyModel, $id == $id - 5) + /// ); + /// ``` + fn sub>(self, other: V) -> Expr; +} + +/// A trait for database types that can be multiplied by each other. +pub trait ExprMul { + /// Creates an expression that multiplies the field by the given value. + /// + /// # Examples + /// + /// ``` + /// use cot::db::query::Query; + /// use cot::db::query::expr::{Expr, ExprMul}; + /// use cot::db::{model, query}; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = ::Fields::id.mul(2); + /// + /// assert_eq!( + /// >::new().filter(Expr::eq(Expr::field("id"), expr)), + /// query!(MyModel, $id == $id * 2) + /// ); + /// ``` + fn mul>(self, other: V) -> Expr; +} + +/// A trait for database types that can be divided by each other. +pub trait ExprDiv { + /// Creates an expression that divides the field by the given value. + /// + /// # Examples + /// + /// ``` + /// use cot::db::query::Query; + /// use cot::db::query::expr::{Expr, ExprDiv}; + /// use cot::db::{model, query}; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = ::Fields::id.div(2); + /// + /// assert_eq!( + /// >::new().filter(Expr::eq(Expr::field("id"), expr)), + /// query!(MyModel, $id == $id / 2) + /// ); + /// ``` + fn div>(self, other: V) -> Expr; +} + +/// A trait for database types that can be ordered. +pub trait ExprOrd { + /// Creates an expression that checks if the field is less than the given + /// value. + /// + /// # Examples + /// + /// ``` + /// use cot::db::query::Query; + /// use cot::db::query::expr::{Expr, ExprOrd}; + /// use cot::db::{model, query}; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = ::Fields::id.lt(5); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id < 5) + /// ); + /// ``` + fn lt>(self, other: V) -> Expr; + /// Creates an expression that checks if the field is less than or equal to + /// the given value. + /// + /// # Examples + /// + /// ``` + /// use cot::db::query::Query; + /// use cot::db::query::expr::{Expr, ExprOrd}; + /// use cot::db::{model, query}; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = ::Fields::id.lte(5); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id <= 5) + /// ); + /// ``` + fn lte>(self, other: V) -> Expr; + + /// Creates an expression that checks if the field is greater than the given + /// value. + /// + /// # Examples + /// + /// ``` + /// use cot::db::query::Query; + /// use cot::db::query::{Expr, ExprOrd}; + /// use cot::db::{model, query}; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = ::Fields::id.gt(5); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id > 5) + /// ); + /// ``` + fn gt>(self, other: V) -> Expr; + + /// Creates an expression that checks if the field is greater than or equal + /// to the given value. + /// + /// # Examples + /// + /// ``` + /// use cot::db::query::Query; + /// use cot::db::query::{Expr, ExprOrd}; + /// use cot::db::{model, query}; + /// + /// #[model] + /// struct MyModel { + /// #[model(primary_key)] + /// id: i32, + /// }; + /// + /// let expr = ::Fields::id.gte(5); + /// + /// assert_eq!( + /// >::new().filter(expr), + /// query!(MyModel, $id >= 5) + /// ); + /// ``` + fn gte>(self, other: V) -> Expr; +} + +impl ExprOrd for FieldRef { + fn lt>(self, other: V) -> Expr { + Expr::lt(self.as_expr(), Expr::value(other.into_field())) + } + + fn lte>(self, other: V) -> Expr { + Expr::lte(self.as_expr(), Expr::value(other.into_field())) + } + + fn gt>(self, other: V) -> Expr { + Expr::gt(self.as_expr(), Expr::value(other.into_field())) + } + + fn gte>(self, other: V) -> Expr { + Expr::gte(self.as_expr(), Expr::value(other.into_field())) + } +} + +/// A marker trait that represents the full set of query-translation +/// capabilities a database backend may support. +pub trait SqlQueryBuilder: LikeExprBuilder {} + +impl SqlQueryBuilder for T where T: LikeExprBuilder {} + +macro_rules! impl_expr { + ($ty:ty, $trait:ident, $method:ident) => { + impl $trait<$ty> for FieldRef<$ty> { + fn $method>(self, other: V) -> Expr { + Expr::$method(self.as_expr(), Expr::value(other.into())) + } + } + }; +} + +macro_rules! impl_num_expr { + ($ty:ty) => { + impl_expr!($ty, ExprAdd, add); + impl_expr!($ty, ExprSub, sub); + impl_expr!($ty, ExprMul, mul); + impl_expr!($ty, ExprDiv, div); + }; +} + +impl_num_expr!(i8); +impl_num_expr!(i16); +impl_num_expr!(i32); +impl_num_expr!(i64); +impl_num_expr!(u8); +impl_num_expr!(u16); +impl_num_expr!(u32); +impl_num_expr!(u64); +impl_num_expr!(f32); +impl_num_expr!(f64); + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn expr_field() { + let expr = Expr::field("name"); + if let Expr::Field(identifier) = expr { + assert_eq!(identifier.to_string(), "name"); + } else { + panic!("Expected Expr::Field"); + } + } + + #[test] + fn expr_value() { + let expr = Expr::value(30); + if let Expr::Value(value) = expr { + assert_eq!(value.to_string(), "30"); + } else { + panic!("Expected Expr::Value"); + } + } + + macro_rules! test_expr_constructor { + ($test_name:ident, $match:ident, $constructor:ident) => { + #[test] + fn $test_name() { + let expr = Expr::$constructor(Expr::field("name"), Expr::value("John")); + if let Expr::$match(lhs, rhs) = expr { + assert!(matches!(*lhs, Expr::Field(_))); + assert!(matches!(*rhs, Expr::Value(_))); + } else { + panic!(concat!("Expected Expr::", stringify!($match))); + } + } + }; + } + + test_expr_constructor!(expr_and, And, and); + test_expr_constructor!(expr_or, Or, or); + test_expr_constructor!(expr_eq, Eq, eq); + test_expr_constructor!(expr_ne, Ne, ne); + test_expr_constructor!(expr_lt, Lt, lt); + test_expr_constructor!(expr_lte, Lte, lte); + test_expr_constructor!(expr_gt, Gt, gt); + test_expr_constructor!(expr_gte, Gte, gte); + test_expr_constructor!(expr_add, Add, add); + test_expr_constructor!(expr_sub, Sub, sub); + test_expr_constructor!(expr_mul, Mul, mul); + test_expr_constructor!(expr_div, Div, div); +} diff --git a/cot/src/db/query/expr/like.rs b/cot/src/db/query/expr/like.rs new file mode 100644 index 00000000..365683f9 --- /dev/null +++ b/cot/src/db/query/expr/like.rs @@ -0,0 +1,228 @@ +//! Database expressions for pattern-matching. + +use cot::db::query::expr::{FieldRef, SqlQueryBuilder}; +use cot::db::query::{Expr, QueryBuildingError}; +use sea_query::{ExprTrait, SimpleExpr}; + +use crate::db::TextField; + +pub(crate) const LIKE_ESCAPE_CHAR: char = '\\'; + +/// A trait for database types that support literal-substring pattern +/// matching. +pub trait ExprLike { + /// Checks if the field contains `other` as a literal substring, + /// comparing case-sensitively. + /// + /// See [`Expr::contains`] for the underlying semantics. + fn contains>(self, other: V) -> Expr; + + /// Checks if the field contains `other` as a literal substring. + /// This is the case-insensitive counterpart of [`Self::contains`]. + /// + /// See [`Expr::contains`] for the underlying semantics. + fn icontains>(self, other: V) -> Expr; + + /// Checks if the field starts with `other`, comparing + /// case-sensitively. + /// + /// See [`Expr::starts_with`] for the underlying semantics. + fn starts_with>(self, other: V) -> Expr; + + /// Checks if the field starts with `other`. + /// This is the case-insensitive counterpart of [`Self::starts_with`]. + /// + /// See [`Expr::starts_with`] for the underlying semantics. + fn istarts_with>(self, other: V) -> Expr; + + /// Checks if the field ends with `other`, comparing case-sensitively. + /// + /// See [`Expr::ends_with`] for the underlying semantics. + fn ends_with>(self, other: V) -> Expr; + + /// Checks if the field ends with `other`. + /// This is the case-insensitive counterpart of [`Self::ends_with`]. + /// + /// See [`Expr::ends_with`] for the underlying semantics. + fn iends_with>(self, other: V) -> Expr; + + /// Matches an expression against the raw pattern provided in `other`, + /// matching case-sensitively. + /// + /// See [`Expr::raw_like`] for the underlying semantics. + fn raw_like>(self, other: V) -> Expr; + + /// Matches an expression against the raw pattern provided in `other`. + /// This is the case-insensitive counterpart of [`Self::raw_like`]. + /// + /// See [`Expr::iraw_like`] for the underlying semantics. + fn iraw_like>(self, other: V) -> Expr; +} + +impl ExprLike for FieldRef { + fn contains>(self, other: V) -> Expr { + Expr::contains(self.as_expr(), Expr::value(other.into())) + } + + fn icontains>(self, other: V) -> Expr { + Expr::icontains(self.as_expr(), Expr::value(other.into())) + } + + fn starts_with>(self, other: V) -> Expr { + Expr::starts_with(self.as_expr(), Expr::value(other.into())) + } + + fn istarts_with>(self, other: V) -> Expr { + Expr::istarts_with(self.as_expr(), Expr::value(other.into())) + } + + fn ends_with>(self, other: V) -> Expr { + Expr::ends_with(self.as_expr(), Expr::value(other.into())) + } + + fn iends_with>(self, other: V) -> Expr { + Expr::iends_with(self.as_expr(), Expr::value(other.into())) + } + + fn raw_like>(self, other: V) -> Expr { + Expr::raw_like(self.as_expr(), Expr::value(other.into())) + } + + fn iraw_like>(self, other: V) -> Expr { + Expr::iraw_like(self.as_expr(), Expr::value(other.into())) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum LikeMode { + Contains, + StartsWith, + EndsWith, + Raw, +} + +fn escape_literal(value: &str) -> String { + let mut escaped = String::with_capacity(value.len()); + for c in value.chars() { + if matches!(c, '*' | '?' | LIKE_ESCAPE_CHAR) { + escaped.push(LIKE_ESCAPE_CHAR); + } + escaped.push(c); + } + escaped +} + +pub(crate) fn to_sql_like(glob: &str) -> String { + let mut escaped = String::with_capacity(glob.len()); + let mut chars = glob.chars(); + while let Some(ch) = chars.next() { + match ch { + LIKE_ESCAPE_CHAR => { + if let Some(ch) = chars.next() { + push_like_literal(&mut escaped, ch); + } + } + '*' => escaped.push('%'), + '?' => escaped.push('_'), + other => push_like_literal(&mut escaped, other), + } + } + escaped +} + +fn push_like_literal(out: &mut String, ch: char) { + if matches!(ch, '%' | '_' | LIKE_ESCAPE_CHAR) { + out.push(LIKE_ESCAPE_CHAR); + } + out.push(ch); +} + +pub(crate) fn like_expr( + sql_builder: &dyn SqlQueryBuilder, + lhs: &Expr, + rhs: &Expr, + mode: LikeMode, + case_sensitivity: CaseSensitivity, +) -> Result { + let lhs_expr = lhs.as_sea_query_expr(sql_builder)?; + + let Expr::Value(value) = rhs else { + return Ok(sea_query::Expr::val(1).eq(0)); + }; + + let sea_value: sea_query::Value = value.clone(); + let sea_query::Value::String(Some(text)) = sea_value else { + return Ok(sea_query::Expr::val(1).eq(0)); + }; + let glob_pattern = match mode { + LikeMode::Contains => format!("*{}*", escape_literal(&text)), + LikeMode::EndsWith => format!("*{}", escape_literal(&text)), + LikeMode::StartsWith => format!("{}*", escape_literal(&text)), + LikeMode::Raw => text.clone(), + }; + + sql_builder.like_expr(lhs_expr, &glob_pattern, case_sensitivity) +} + +/// Whether a pattern-matching expression should consider letter case when +/// comparing text. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CaseSensitivity { + /// Matching considers letter case (`"Foo"` does not match `"foo"`). + Sensitive, + /// Matching ignores letter case (`"Foo"` matches `"foo"`). + Insensitive, +} + +/// Translates Cot's pattern-matching query expressions (`contains`, +/// `starts_with`, `ends_with`, `raw_like`, and their case-insensitive and +/// `i`-prefixed counterparts) into a backend-specific `sea_query` +/// expression. +/// +/// Each of Cot's built-in database backends implements this trait; a +/// custom `sea_query`-based backend can implement it too to get correct, +/// dialect-appropriate translation of pattern-matching expressions without +/// needing to know anything about the individual `Expr` variants that +/// produce them. An implementor only ever sees a single, already-built +/// left-hand-side expression, a pattern already expressed in Cot's +/// canonical glob syntax (`*`/`?`/`\` — see [`Expr::raw_like`]), and the +/// requested [`CaseSensitivity`]. +pub trait LikeExprBuilder { + /// Builds the `sea_query` expression that checks whether `lhs` + /// matches `glob_pattern`, honoring `case_sensitivity`. + /// + /// # Errors + /// + /// Returns [`QueryBuildingError`] if the backend cannot + /// express pattern matching, or cannot express the requested + /// [`CaseSensitivity`] variant. + fn like_expr( + &self, + lhs: SimpleExpr, + glob_pattern: &str, + case_sensitivity: CaseSensitivity, + ) -> Result; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn converts_glob_wildcards_to_sql_wildcards() { + assert_eq!(to_sql_like("*foo*"), "%foo%"); + assert_eq!(to_sql_like("foo*"), "foo%"); + assert_eq!(to_sql_like("*foo"), "%foo"); + assert_eq!(to_sql_like("f?o"), "f_o"); + assert_eq!(to_sql_like("f??o"), "f__o"); + assert_eq!(to_sql_like("100% off"), "100\\% off"); + assert_eq!(to_sql_like("under_score"), "under\\_score"); + assert_eq!(to_sql_like("a\\*b"), "a*b"); + assert_eq!(to_sql_like("a\\?b"), "a?b"); + assert_eq!(to_sql_like("C:\\\\Users"), "C:\\\\Users"); + assert_eq!(to_sql_like(""), ""); + assert_eq!(to_sql_like("foo\\"), "foo"); + assert_eq!(to_sql_like("café*"), "café%"); + assert_eq!(to_sql_like("日本語"), "日本語"); + } +} diff --git a/cot/src/test.rs b/cot/src/test.rs index b123cc19..24c2d61f 100644 --- a/cot/src/test.rs +++ b/cot/src/test.rs @@ -51,6 +51,9 @@ use crate::session::Session; use crate::static_files::{StaticFile, StaticFiles}; use crate::{Body, Bootstrapper, Project, ProjectContext, Result}; +pub(crate) const DEFAULT_POSTGRES_TEST_URL: &str = "postgresql://cot:cot@localhost"; +pub(crate) const DEFAULT_MYSQL_TEST_URL: &str = "mysql://root:@localhost"; + /// A test client for making requests to a Cot project. /// /// This client is useful for end-to-end testing of Cot projects. @@ -987,8 +990,8 @@ impl TestDatabase { /// # } /// ``` pub async fn new_postgres(test_name: &str) -> Result { - let db_url = std::env::var("POSTGRES_URL") - .unwrap_or_else(|_| "postgresql://cot:cot@localhost".to_string()); + let db_url = + std::env::var("POSTGRES_URL").unwrap_or_else(|_| DEFAULT_POSTGRES_TEST_URL.to_string()); let database = Database::new(format!("{db_url}/postgres")).await?; let test_database_name = format!("test_cot__{test_name}"); @@ -1056,7 +1059,7 @@ impl TestDatabase { /// ``` pub async fn new_mysql(test_name: &str) -> Result { let db_url = - std::env::var("MYSQL_URL").unwrap_or_else(|_| "mysql://root:@localhost".to_string()); + std::env::var("MYSQL_URL").unwrap_or_else(|_| DEFAULT_MYSQL_TEST_URL.to_string()); let database = Database::new(format!("{db_url}/mysql")).await?; let test_database_name = format!("test_cot__{test_name}"); diff --git a/cot/tests/db.rs b/cot/tests/db.rs index ea4887cc..b03b6c79 100644 --- a/cot/tests/db.rs +++ b/cot/tests/db.rs @@ -5,7 +5,7 @@ use bytes::Bytes; use cot::auth::PasswordHash; use cot::common_types::{Email, Password, Url}; use cot::db::migrations::{Field, Operation}; -use cot::db::query::ExprEq; +use cot::db::query::expr::ExprEq; use cot::db::{ Auto, Database, DatabaseError, DatabaseField, ForeignKey, ForeignKeyOnDeletePolicy, ForeignKeyOnUpdatePolicy, Identifier, LimitedString, Model, model, query, @@ -1199,3 +1199,298 @@ async fn bulk_insert_with_fixed_pk(test_db: &mut TestDatabase) { .unwrap(); assert_eq!(model300.name, "test300"); } + +async fn seed(test_db: &TestDatabase, names: &[&str]) { + let mut models: Vec = names + .iter() + .map(|n| TestModel { + id: Auto::auto(), + name: (*n).to_owned(), + }) + .collect(); + TestModel::bulk_insert(&**test_db, &mut models) + .await + .unwrap(); +} + +fn names_of(objects: &[TestModel]) -> Vec<&str> { + objects.iter().map(|o| o.name.as_str()).collect() +} + +#[cot_macros::dbtest] +async fn model_query_contains_case_sensitive(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed(test_db, &["foo", "Foo", "fOO", "FOO", "bar"]).await; + + let objects = query!(TestModel, $name.contains("oo")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["foo", "Foo"]); + + let objects = query!(TestModel, $name.contains("fo")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["foo"]); + + let objects = query!(TestModel, $name.contains("bar")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["bar"]); + + let objects = query!(TestModel, $name.contains("xyz")) + .all(&**test_db) + .await + .unwrap(); + assert!(objects.is_empty()); + + let objects = query!(TestModel, $name.contains("")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(objects.len(), 5); +} + +#[cot_macros::dbtest] +async fn model_query_icontains(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed(test_db, &["foo", "Foo", "fOO", "FOO", "bar"]).await; + + let mut objects = query!(TestModel, $name.icontains("OO")) + .all(&**test_db) + .await + .unwrap(); + objects.sort_by_key(|a| a.id.unwrap()); + assert_eq!(names_of(&objects), vec!["foo", "Foo", "fOO", "FOO"]); + + let objects = query!(TestModel, $name.icontains("xyz")) + .all(&**test_db) + .await + .unwrap(); + assert!(objects.is_empty()); +} + +#[cot_macros::dbtest] +async fn model_query_starts_with(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed(test_db, &["foobar", "Foobar", "barfoo", "foo"]).await; + + let objects = query!(TestModel, $name.starts_with("foo")) + .all(&**test_db) + .await + .unwrap(); + let mut got = names_of(&objects); + got.sort_unstable(); + assert_eq!(got, vec!["foo", "foobar"]); + + let objects = query!(TestModel, $name.starts_with("bar")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["barfoo"]); + + let objects = query!(TestModel, $name.starts_with("foobarbaz")) + .all(&**test_db) + .await + .unwrap(); + assert!(objects.is_empty()); +} + +#[cot_macros::dbtest] +async fn model_query_istarts_with(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed(test_db, &["foobar", "Foobar", "barfoo"]).await; + + let mut objects = query!(TestModel, $name.istarts_with("FOO")) + .all(&**test_db) + .await + .unwrap(); + objects.sort_by_key(|a| a.id.unwrap()); + assert_eq!(names_of(&objects), vec!["foobar", "Foobar"]); +} + +#[cot_macros::dbtest] +async fn model_query_ends_with(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed(test_db, &["report.pdf", "report.PDF", "archive.zip", "pdf"]).await; + + let objects = query!(TestModel, $name.ends_with(".pdf")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["report.pdf"]); + + let objects = query!(TestModel, $name.ends_with("report.pdf.pdf")) + .all(&**test_db) + .await + .unwrap(); + assert!(objects.is_empty()); +} + +#[cot_macros::dbtest] +async fn model_query_iends_with(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed(test_db, &["report.pdf", "report.PDF", "archive.zip"]).await; + + let mut objects = query!(TestModel, $name.iends_with(".PDF")) + .all(&**test_db) + .await + .unwrap(); + objects.sort_by_key(|a| a.id.unwrap()); + assert_eq!(names_of(&objects), vec!["report.pdf", "report.PDF"]); +} + +#[cot_macros::dbtest] +async fn model_query_raw_positional(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed(test_db, &["faXo", "fooo", "fo", "faXYo", "f_o"]).await; + + let mut objects = query!(TestModel, $name.raw_like("f??o")) + .all(&**test_db) + .await + .unwrap(); + objects.sort_by_key(|a| a.id.unwrap()); + let mut got = names_of(&objects); + got.sort_unstable(); + assert_eq!(got, vec!["faXo", "fooo"]); +} + +#[cot_macros::dbtest] +async fn model_query_raw_middle_wildcards(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed( + test_db, + &[ + "foo_bar_baz", + "foo bar baz extra", + "foobarbaz", + "bar_foo_baz", // wrong order, must not match + ], + ) + .await; + + let objects = query!(TestModel, $name.raw_like("*foo*bar*baz*")) + .all(&**test_db) + .await + .unwrap(); + let mut got = names_of(&objects); + got.sort_unstable(); + assert_eq!(got, vec!["foo bar baz extra", "foo_bar_baz", "foobarbaz"]); +} + +#[cot_macros::dbtest] +async fn model_query_raw_escaped_wildcard(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed(test_db, &["a*b", "aXb", "a?b"]).await; + + let objects = query!(TestModel, $name.raw_like("a\\*b")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["a*b"]); + + let mut objects = query!(TestModel, $name.raw_like("a?b")) + .all(&**test_db) + .await + .unwrap(); + objects.sort_by_key(|a| a.id.unwrap()); + let mut got = names_of(&objects); + got.sort_unstable(); + assert_eq!(got, vec!["a*b", "a?b", "aXb"]); +} + +#[cot_macros::dbtest] +async fn model_query_iraw(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed(test_db, &["README", "ReadMe", "readme", "READMEE", "REDME"]).await; + + let mut objects = query!(TestModel, $name.iraw_like("re?dme")) + .all(&**test_db) + .await + .unwrap(); + objects.sort_by_key(|a| a.id.unwrap()); + let mut got = names_of(&objects); + got.sort_unstable(); + assert_eq!(got, vec!["README", "ReadMe", "readme",]); +} + +#[cot_macros::dbtest] +async fn model_query_literal_wildcard_characters_in_data(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed(test_db, &["100% off", "under_score", "a*b", "aXb"]).await; + + let objects = query!(TestModel, $name.contains("100% off")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["100% off"]); + + let objects = query!(TestModel, $name.contains("_score")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["under_score"]); + + let objects = query!(TestModel, $name.contains("a*b")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["a*b"]); +} + +#[cot_macros::dbtest] +async fn model_query_unicode_case_sensitive(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed( + test_db, + &["café", "CAFÉ", "日本語のテスト", "🎉 party time", "naïve"], + ) + .await; + + let objects = query!(TestModel, $name.contains("café")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["café"]); + + let objects = query!(TestModel, $name.starts_with("日本")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["日本語のテスト"]); + + let objects = query!(TestModel, $name.ends_with("time")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["🎉 party time"]); + + let objects = query!(TestModel, $name.raw_like("na?ve")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["naïve"]); +} + +#[cot_macros::dbtest] +async fn model_query_contains_combined_with_boolean_ops(test_db: &mut TestDatabase) { + migrate_test_model(&*test_db).await; + seed(test_db, &["apple pie", "apple tart", "banana split"]).await; + + let objects = query!(TestModel, $name.contains("apple") && $name.contains("pie")) + .all(&**test_db) + .await + .unwrap(); + assert_eq!(names_of(&objects), vec!["apple pie"]); + let mut objects = query!(TestModel, $name.starts_with("apple") || $name.ends_with("split")) + .all(&**test_db) + .await + .unwrap(); + objects.sort_by_key(|a| a.id.unwrap()); + let mut got = names_of(&objects); + got.sort_unstable(); + + assert_eq!(got, vec!["apple pie", "apple tart", "banana split"]); +} diff --git a/docs/databases/queries.md b/docs/databases/queries.md index 7c2d92ad..c541eb3d 100644 --- a/docs/databases/queries.md +++ b/docs/databases/queries.md @@ -257,12 +257,12 @@ async fn get_customer(db: Database) -> cot::Result<()> { The [`query!`](macro@cot::db::query) macro returns a [`Query`](struct@cot::db::query::Query) object, on which you can call terminal methods (such as [`get`](struct@cot::db::query::Query#method.get) which returns the first matching result, and [`all`](struct@cot::db::query::Query#method.all) which returns all matching results) to retrieve the final results. ### Using the Query struct -The [`query!`](macro@cot::db::query) macro is syntactic sugar for manually constructing a [`Query`](struct@cot::db::query::Query) with [`Expr`](enum@cot::db::query::Expr) expressions. The [`Query`](struct@cot::db::query::Query) object can be accessed directly by calling the [`objects`](trait@cot::db::Model#method.objects) method on the model, and filtered using the [`filter`](struct@cot::db::query::Query#method.filter) method. +The [`query!`](macro@cot::db::query) macro is syntactic sugar for manually constructing a [`Query`](struct@cot::db::query::Query) with [`Expr`](enum@cot::db::query::expr::Expr) expressions. The [`Query`](struct@cot::db::query::Query) object can be accessed directly by calling the [`objects`](trait@cot::db::Model#method.objects) method on the model, and filtered using the [`filter`](struct@cot::db::query::Query#method.filter) method. You may prefer this approach when you need more control over how expressions are constructed. The example below is equivalent to the one above: ```rust use cot::db::Database; -use cot::db::query::Expr; +use cot::db::query::expr::Expr; # #[model] #[derive(Debug)] struct Customer { #[model(primary_key)] id: Auto, #[model(unique)] email: cot::common_types::Email, full_name: LimitedString<128>, is_verified: bool } async fn get_customer(db: Database) -> cot::Result<()> { @@ -272,14 +272,14 @@ async fn get_customer(db: Database) -> cot::Result<()> { } ``` -The [`filter`](struct@cot::db::query::Query#method.filter) method takes a [`filter expression`](enum@cot::db::query::Expr). In the example above, the expression `Expr::eq(Expr::field("id"), Expr::value("5"))` is evaluated as `id = 5`. +The [`filter`](struct@cot::db::query::Query#method.filter) method takes a [`filter expression`](enum@cot::db::query::expr::Expr). In the example above, the expression `Expr::eq(Expr::field("id"), Expr::value("5"))` is evaluated as `id = 5`. ### Retrieving all objects One way to retrieve all objects of a model is to call the [`all`](struct@cot::db::query::Query#method.all) method after filtering the query results. ```rust use cot::db::Database; -use cot::db::query::Expr; +use cot::db::query::expr::Expr; # #[model] #[derive(Debug)] struct Customer { #[model(primary_key)] id: Auto, #[model(unique)] email: cot::common_types::Email, full_name: LimitedString<128>, is_verified: bool } async fn get_all_customers(db: Database) -> cot::Result<()> { @@ -296,7 +296,7 @@ The [`filter`](struct@cot::db::query::Query#method.filter) method returns a new ```rust use cot::db::Database; -use cot::db::query::Expr; +use cot::db::query::expr::Expr; # #[model] #[derive(Debug)] struct Customer { #[model(primary_key)] id: Auto, #[model(unique)] email: cot::common_types::Email, full_name: LimitedString<128>, is_verified: bool } async fn get_customers(db: Database) -> cot::Result<()> { @@ -314,6 +314,112 @@ The example above shows how to retrieve all customers with a primary key greater Similarly, the [`query`](macro@cot::db::query) macro returns a new [`Query`](struct@cot::db::query::Query) instance which can be used to chain multiple filters. +### Searching within a field (pattern matching) + +Sometimes an exact match isn't what you want. You might want to find all customers whose name contains a certain word, or all products whose name starts with a certain prefix. Cot supports this kind of substring, prefix, and suffix search directly in the `query!` macro (and, if you're building expressions by hand, on [`Expr`](enum@cot::db::query::expr::Expr) as well). + +```rust +use cot::db::Database; + +# #[model] #[derive(Debug)] struct Customer { #[model(primary_key)] id: Auto, #[model(unique)] email: cot::common_types::Email, full_name: LimitedString<128>, is_verified: bool } +async fn search_customers(db: Database) -> cot::Result<()> { + let customers = query!(Customer, $full_name.contains("Doe")).all(&db).await?; + println!("Customers: {:?}", customers); +# Ok(()) +} +``` + +Alongside `contains`, there's `starts_with` and `ends_with`, which anchor the match to the beginning or the end of the field's value instead of allowing it anywhere: + +```rust +use cot::db::Database; + +# #[model] #[derive(Debug)] struct Customer { #[model(primary_key)] id: Auto, #[model(unique)] email: cot::common_types::Email, full_name: LimitedString<128>, is_verified: bool } +async fn search_customers_further(db: Database) -> cot::Result<()> { + // Matches "Jon Doe", "Jonathan Smith", etc. + let jons = query!(Customer, $full_name.starts_with("Jon")).all(&db).await?; + + // Matches anyone whose name ends with "Doe" + let does = query!(Customer, $full_name.ends_with("Doe")).all(&db).await?; + + println!("{:?} {:?}", jons, does); +# Ok(()) +} +``` + +By default, all three of these are case-sensitive, so `contains("Doe")` won't match a customer named "jane doe". If you'd rather the match ignore case, each method has an `i`-prefixed counterpart: `icontains`, `istarts_with`, and `iends_with`. + +```rust +use cot::db::Database; + +# #[model] #[derive(Debug)] struct Customer { #[model(primary_key)] id: Auto, #[model(unique)] email: cot::common_types::Email, full_name: LimitedString<128>, is_verified: bool } +async fn case_insensitive_search(db: Database) -> cot::Result<()> { + let customers = query!(Customer, $full_name.icontains("doe")).all(&db).await?; + println!("Customers: {:?}", customers); +# Ok(()) +} +``` + +These combine with the rest of the filter naturally too. Use them alongside boolean and comparison operators just like any other condition: + +```rust +use cot::db::Database; + +# #[model] #[derive(Debug)] struct Customer { #[model(primary_key)] id: Auto, #[model(unique)] email: cot::common_types::Email, full_name: LimitedString<128>, is_verified: bool } +async fn verified_does(db: Database) -> cot::Result<()> { + let customers = query!(Customer, $full_name.icontains("doe") && $is_verified == true) + .all(&db) + .await?; + println!("Customers: {:?}", customers); +# Ok(()) +} +``` + +#### Matching a custom pattern + +`contains`, `starts_with`, and `ends_with` cover most everyday searches, but they can't express every shape of match. What if you need to check both the start and the end of a value at once, or match a value that follows a specific structure? For cases like this, Cot provides `raw_like` (and its case-insensitive counterpart, `iraw_like`), which let you write the match pattern yourself, using a small glob-style syntax: + +- `*` matches zero or more of any character +- `?` matches exactly one character +- `\` escapes the character after it, so it's matched literally instead of as a wildcard + +Say you're searching for shipment tracking codes that start with `"PKG"` and end with `"US"`, with anything in between: + +```rust +use cot::db::Database; + +# #[model] #[derive(Debug)] struct Shipment { #[model(primary_key)] id: Auto, tracking_code: LimitedString<64>, is_delivered: bool } +async fn find_matching_tracking_codes(db: Database) -> cot::Result<()> { + // Matches "PKG-US", "PKG123-US", "PKGXXUS", and so on + let shipments = query!(Shipment, $tracking_code.raw_like("PKG*US")).all(&db).await?; + println!("Shipments: {:?}", shipments); +# Ok(()) +} +``` + +`raw_like` is more flexible than `contains`, `starts_with`, and `ends_with`, but it also means you're responsible for escaping any wildcard characters you want to match literally. See the [`Expr::raw_like`](enum@cot::db::query::expr::Expr#method.raw_like) docs for the full pattern syntax and escaping rules. + +#### Using the Expr struct directly + +As with the other operators in this guide, all of these have an equivalent on [`Expr`](enum@cot::db::query::expr::Expr) for when you're building queries by hand rather than through the `query!` macro: + +```rust +use cot::db::Database; +use cot::db::query::expr::Expr; + +# #[model] #[derive(Debug)] struct Customer { #[model(primary_key)] id: Auto, #[model(unique)] email: cot::common_types::Email, full_name: LimitedString<128>, is_verified: bool } +async fn search_customers_with_expr(db: Database) -> cot::Result<()> { + let customers = Customer::objects() + .filter(Expr::contains(Expr::field("full_name"), Expr::value("Doe"))) + .all(&db) + .await?; + println!("Customers: {:?}", customers); +# Ok(()) +} +``` + +For the complete list of pattern-matching methods, their case-insensitive counterparts, and the glob pattern syntax used by `raw_like`, see the [`Expr`](enum@cot::db::query::expr::Expr) and [`ExprLike`](trait@cot::db::query::expr::ExprLike) docs. + ## Removing an object The [`delete`](struct@cot::db::query::Query#method.delete) method can be used to remove an object from the database. The example below shows how to remove a `Customer` instance with the primary key of `5`.