Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
eabc5b1
.contains, .startwith, .endswith proof of concept
ElijahAhianyo Jul 2, 2026
e4b54d3
chore(pre-commit.ci): auto fixes from pre-commit hooks
pre-commit-ci[bot] Jul 2, 2026
3c25675
- add .raw_like escape hatch
ElijahAhianyo Jul 3, 2026
c829176
add unit tests. Rename to LikeExprBuilder and SqlQueryBuilder
ElijahAhianyo Jul 4, 2026
b6540bd
Merge branch 'master' into elijah/like-query
ElijahAhianyo Jul 4, 2026
b89d219
attempt to fix case sensitive arm on mysql
ElijahAhianyo Jul 5, 2026
1d09972
Merge remote-tracking branch 'origin/elijah/like-query' into elijah/l…
ElijahAhianyo Jul 5, 2026
9b21b01
see if this fixes the query
ElijahAhianyo Jul 5, 2026
6c724ce
fix mysql syntax error for case sensitive arm
ElijahAhianyo Jul 5, 2026
b7b48fe
fix postgres error and some more improvements!
ElijahAhianyo Jul 5, 2026
86d311d
update query macro docs
ElijahAhianyo Jul 5, 2026
4588ab9
change mysql like arm for case sensitivity to use utf8 collation
ElijahAhianyo Jul 7, 2026
f0dde66
fix the mysql issue for real
ElijahAhianyo Jul 7, 2026
2815914
move to_sqlite_glob out of DatabaseSqlite
ElijahAhianyo Jul 13, 2026
d2856f2
Merge branch 'master' into elijah/like-query
ElijahAhianyo Jul 13, 2026
62a1ec0
Merge branch 'master' into elijah/like-query
ElijahAhianyo Jul 14, 2026
7a7f055
add some more tests
ElijahAhianyo Jul 15, 2026
4dbd04b
chore(pre-commit.ci): auto fixes from pre-commit hooks
pre-commit-ci[bot] Jul 15, 2026
1141ce2
small refactor and allow fieldref methods with variable arity
ElijahAhianyo Jul 21, 2026
c875703
Merge branch 'master' into elijah/like-query
ElijahAhianyo Jul 21, 2026
8f1861e
add unit tests
ElijahAhianyo Jul 24, 2026
1bc09e8
update docs
ElijahAhianyo Jul 24, 2026
a6f09cf
fix minor doc-test
ElijahAhianyo Jul 24, 2026
d0126e6
fixed ui tests
ElijahAhianyo Jul 24, 2026
5176ad6
restrict ExprLike bounds for FieldRef to String column types
ElijahAhianyo Jul 24, 2026
53c4f93
Merge branch 'master' into elijah/like-query
ElijahAhianyo Jul 27, 2026
9b9dcc3
fix stderr ui tests
ElijahAhianyo Jul 27, 2026
ef68cf6
Gate TextField behind db feature to fix cargo hack test
ElijahAhianyo Jul 27, 2026
d017d69
fix wrong arity stderr ui test
ElijahAhianyo Jul 28, 2026
649ecfb
fix miri tests
ElijahAhianyo Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cot-macros/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
));
}

Expand Down
242 changes: 226 additions & 16 deletions cot-macros/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = &'static str> {
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;
Expand All @@ -40,15 +96,15 @@ 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,
member_name,
..
} => 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(),
Expand All @@ -62,33 +118,47 @@ 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(),
"accessing paths of values that reference database fields is unsupported",
)
.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().collect::<Vec<_>>().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"),
Expand Down Expand Up @@ -117,10 +187,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::<Vec<_>>();
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
);
}
}
}
2 changes: 2 additions & 0 deletions cot-macros/tests/compile_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading