Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 45 additions & 4 deletions cot-codegen/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,47 @@ impl Parse for PathAccessParser {
}
}

/// An argument passed to a function or method in a query expression.
#[derive(Debug, PartialEq, Eq)]
pub enum ExprArgument {
/// A reference to a model field.
FieldRef {
field_name: syn::Ident,
field_token: Token![$],
},
/// A regular Rust expression.
Value(syn::Expr),
}

impl Parse for ExprArgument {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
if input.peek(Token![$]) {
let field = input.parse::<FieldParser>()?;
Ok(Self::FieldRef {
field_name: field.name,
field_token: field.field_token,
})
} else {
input.parse().map(Self::Value)
}
}
}

impl quote::ToTokens for ExprArgument {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::FieldRef {
field_name,
field_token,
} => tokens.extend(quote! { #field_token #field_name }),
Self::Value(value) => value.to_tokens(tokens),
}
}
}

#[derive(Debug)]
struct FunctionCallParser {
args: syn::punctuated::Punctuated<syn::Expr, Token![,]>,
args: syn::punctuated::Punctuated<ExprArgument, Token![,]>,
}

impl FunctionCallParser {
Expand All @@ -164,7 +202,7 @@ impl Parse for FunctionCallParser {
let args_content;
syn::parenthesized!(args_content in input);
Ok(Self {
args: args_content.parse_terminated(syn::Expr::parse, Token![,])?,
args: args_content.parse_terminated(ExprArgument::parse, Token![,])?,
})
}
}
Expand Down Expand Up @@ -323,7 +361,7 @@ pub enum Expr {
},
FunctionCall {
function: Box<Expr>,
args: Vec<syn::Expr>,
args: Vec<ExprArgument>,
},
And(Box<Expr>, Box<Expr>),
Or(Box<Expr>, Box<Expr>),
Expand Down Expand Up @@ -689,7 +727,10 @@ mod tests {
Box::new(field("a")),
Box::new(Expr::FunctionCall {
function: Box::new(value("bar")),
args: vec![parse_quote!(42), parse_quote!("baz")],
args: vec![
ExprArgument::Value(parse_quote!(42)),
ExprArgument::Value(parse_quote!("baz")),
],
}),
);

Expand Down
60 changes: 57 additions & 3 deletions cot-macros/src/query.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cot_codegen::expr::Expr;
use cot_codegen::expr::{Expr, ExprArgument};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::Token;
Expand Down Expand Up @@ -62,6 +62,10 @@ const FIELD_REF_METHODS: &[FieldRefMethod] = &[
name: "iraw_like",
arity: 1,
},
FieldRefMethod {
name: "concat",
arity: 1,
},
];

impl FieldRefMethod {
Expand Down Expand Up @@ -201,7 +205,7 @@ fn handle_binary_comparison(
fn handle_field_ref_method(
model_name: &syn::Type,
receiver: Expr,
args: &[syn::Expr],
args: &[ExprArgument],
method: FieldRefMethod,
) -> TokenStream {
let crate_name = cot_ident();
Expand All @@ -222,6 +226,10 @@ fn handle_field_ref_method(
.to_compile_error();
}

if method.name == "concat" {
return handle_concat(model_name, receiver, &args[0]);
}

if let Expr::FieldRef { ref field_name, .. } = receiver {
return quote! {
#crate_name::db::query::expr::ExprLike::#method_ident(
Expand All @@ -244,14 +252,52 @@ fn handle_field_ref_method(
}
}

fn handle_concat(model_name: &syn::Type, receiver: Expr, argument: &ExprArgument) -> TokenStream {
let crate_name = cot_ident();

if let Expr::FieldRef { field_name, .. } = receiver {
return match argument {
ExprArgument::FieldRef {
field_name: argument_field,
..
} => quote! {
#crate_name::db::query::expr::Expr::concat(
<#model_name as #crate_name::db::Model>::Fields::#field_name.as_expr(),
<#model_name as #crate_name::db::Model>::Fields::#argument_field.as_expr(),
)
},
ExprArgument::Value(value) => quote! {
#crate_name::db::query::expr::ExprConcat::concat(
<#model_name as #crate_name::db::Model>::Fields::#field_name,
#value,
)
},
};
}

let receiver_tokens = expr_to_tokens(model_name, receiver);
let argument_tokens = match argument {
ExprArgument::FieldRef { field_name, .. } => quote! {
<#model_name as #crate_name::db::Model>::Fields::#field_name.as_expr()
},
ExprArgument::Value(value) => {
quote!(#crate_name::db::query::expr::Expr::value(#value))
}
};

quote! {
#crate_name::db::query::expr::Expr::concat(#receiver_tokens, #argument_tokens)
}
}

#[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.len(), 9);
assert_eq!(
all_names,
[
Expand All @@ -263,6 +309,7 @@ mod tests {
"iends_with",
"raw_like",
"iraw_like",
"concat",
]
);
}
Expand Down Expand Up @@ -326,6 +373,13 @@ mod tests {
arity: 1,
}),
),
(
"concat",
Some(FieldRefMethod {
name: "concat",
arity: 1,
}),
),
("__non_existent__", None),
];

Expand Down
25 changes: 25 additions & 0 deletions cot-macros/tests/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,31 @@ fn test_query_string_method_string_concat_field_refs() {
);
}

#[test]
fn test_query_string_concat_fields_and_literals() {
assert_eq!(
Query::<MyModel>::new().filter(Expr::eq(
Expr::concat(
<MyModel as cot::db::Model>::Fields::name.as_expr(),
<MyModel as cot::db::Model>::Fields::title.as_expr()
),
Expr::value("firstlast")
)),
query!(MyModel, $name.concat($title) == "firstlast")
);

assert_eq!(
Query::<MyModel>::new().filter(Expr::eq(
Expr::concat(
<MyModel as cot::db::Model>::Fields::name.as_expr(),
Expr::value("-")
),
Expr::value("first-")
)),
query!(MyModel, $name.concat("-") == "first-")
);
}

#[test]
fn test_query_string_method_non_field_receiver_call() {
let allowed_names = &["foo", "bar"];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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)
error: calling functions that reference database fields is unsupported (only `contains`, `icontains`, `starts_with`, `istarts_with`, `ends_with`, `iends_with`, `raw_like`, `iraw_like`, `concat` 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());
Expand Down
30 changes: 28 additions & 2 deletions cot/src/db/query/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ pub mod like;
use std::marker::PhantomData;

use cot::db::query::{IntoField, QueryBuildingError};
use cot::db::{DbFieldValue, DbValue, FromDbValue, Identifier, ToDbFieldValue};
use cot::db::{DbFieldValue, DbValue, FromDbValue, Identifier, TextField, ToDbFieldValue};
pub use like::ExprLike;
use like::{CaseSensitivity, LikeExprBuilder, LikeMode};
use sea_query::{ExprTrait, IntoColumnRef, SimpleExpr};
use sea_query::{ExprTrait, Func, IntoColumnRef, SimpleExpr};

/// An expression that can be used to filter, update, or delete rows.
///
Expand Down Expand Up @@ -296,6 +296,8 @@ pub enum Expr {
/// );
/// ```
Add(Box<Expr>, Box<Expr>),
/// A string concatenation expression.
Concat(Box<Expr>, Box<Expr>),
/// A `-` expression.
///
/// # Example
Expand Down Expand Up @@ -785,6 +787,14 @@ impl Expr {
Self::Add(Box::new(lhs), Box::new(rhs))
}

/// Creates a string concatenation expression.
///
/// This is translated to the portable SQL `CONCAT(lhs, rhs)` function.
#[must_use]
pub fn concat(lhs: Self, rhs: Self) -> Self {
Self::Concat(Box::new(lhs), Box::new(rhs))
}

/// Create a new `-` expression.
///
/// # Example
Expand Down Expand Up @@ -1259,6 +1269,10 @@ impl Expr {
Self::Add(lhs, rhs) => Ok(lhs
.as_sea_query_expr(sql_builder)?
.add(rhs.as_sea_query_expr(sql_builder)?)),
Self::Concat(lhs, rhs) => Ok(Func::cust(Identifier::new("CONCAT"))
.arg(lhs.as_sea_query_expr(sql_builder)?)
.arg(rhs.as_sea_query_expr(sql_builder)?)
.into()),
Self::Sub(lhs, rhs) => Ok(lhs
.as_sea_query_expr(sql_builder)?
.sub(rhs.as_sea_query_expr(sql_builder)?)),
Expand Down Expand Up @@ -1381,6 +1395,18 @@ impl<T: ToDbFieldValue + 'static> ExprEq<T> for FieldRef<T> {
}
}

/// Concatenates a text database field with another text value.
pub trait ExprConcat {
/// Creates a string concatenation expression.
fn concat<V: ToDbFieldValue>(self, other: V) -> Expr;
}

impl<T: TextField> ExprConcat for FieldRef<T> {
fn concat<V: ToDbFieldValue>(self, other: V) -> Expr {
Expr::concat(self.as_expr(), Expr::value(other))
}
}

/// A trait for database types that can be added to each other.
pub trait ExprAdd<T> {
/// Creates an expression that adds the field to the given value.
Expand Down
51 changes: 51 additions & 0 deletions cot/tests/db_testing/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ struct TestModel {
name: String,
}

#[derive(Debug, Clone, PartialEq)]
#[model]
struct ConcatModel {
#[model(primary_key)]
id: Auto<i32>,
first_name: String,
last_name: String,
}

// Check different types for the primary key
#[derive(Debug, PartialEq)]
#[model]
Expand Down Expand Up @@ -59,6 +68,23 @@ const CREATE_TEST_MODEL: Operation = Operation::create_model()
])
.build();

const CREATE_CONCAT_MODEL: Operation = Operation::create_model()
.table_name(Identifier::new("cot__concat_model"))
.fields(&[
Field::new(Identifier::new("id"), <Auto<i32> as DatabaseField>::TYPE)
.primary_key()
.auto(),
Field::new(
Identifier::new("first_name"),
<String as DatabaseField>::TYPE,
),
Field::new(
Identifier::new("last_name"),
<String as DatabaseField>::TYPE,
),
])
.build();

#[cot_macros::dbtest]
async fn model_crud(test_db: &mut TestDatabase) {
migrate_test_model(&*test_db).await;
Expand Down Expand Up @@ -94,6 +120,31 @@ async fn model_crud(test_db: &mut TestDatabase) {
assert_eq!(TestModel::objects().all(&**test_db).await.unwrap(), vec![]);
}

#[cot_macros::dbtest]
async fn string_concat_filter(test_db: &mut TestDatabase) {
CREATE_CONCAT_MODEL.forwards(&*test_db).await.unwrap();

let mut model = ConcatModel {
id: Auto::auto(),
first_name: "Ada".to_owned(),
last_name: "Lovelace".to_owned(),
};
model.insert(&**test_db).await.unwrap();

let with_separator =
query!(ConcatModel, $first_name.concat(" ").concat($last_name) == "Ada Lovelace")
.all(&**test_db)
.await
.unwrap();
assert_eq!(with_separator, vec![model.clone()]);

let fields_only = query!(ConcatModel, $first_name.concat($last_name) == "AdaLovelace")
.all(&**test_db)
.await
.unwrap();
assert_eq!(fields_only, vec![model]);
}

#[cot_macros::dbtest]
async fn model_insert(test_db: &mut TestDatabase) {
migrate_test_model(&*test_db).await;
Expand Down
Loading