use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Expr, ExprClosure, ExprMethodCall, Ident, Pat, Type};
use super::super::context::{extract_field, extract_field_ref, field_const, FieldKind, LinqCtx};
use super::bool::compile_bool_expr;
#[derive(Clone, Copy)]
pub(crate) enum SubqueryKind {
Any,
None,
All,
}
impl SubqueryKind {
fn negated(self) -> bool {
matches!(self, SubqueryKind::None | SubqueryKind::All)
}
fn negate_predicate(self) -> bool {
matches!(self, SubqueryKind::All)
}
}
pub(crate) fn extract_subquery_closure(closure: &ExprClosure) -> syn::Result<(Ident, Type)> {
let input = closure.inputs.first().ok_or_else(|| {
syn::Error::new_spanned(closure, "subquery closure requires one parameter")
})?;
let pat_type = match input {
Pat::Type(pt) => pt,
_ => {
return Err(syn::Error::new_spanned(
input,
"subquery closure parameter requires a type annotation (e.g. `|p: Post| ...`)",
));
}
};
let param = match &*pat_type.pat {
Pat::Ident(pi) => pi.ident.clone(),
_ => {
return Err(syn::Error::new_spanned(
&pat_type.pat,
"subquery closure parameter must be a simple identifier",
));
}
};
Ok((param, (*pat_type.ty).clone()))
}
fn compile_subquery_parts(
ctx: &LinqCtx<'_>,
call: &ExprMethodCall,
kind: SubqueryKind,
) -> syn::Result<(TokenStream2, TokenStream2, TokenStream2)> {
let nav_ref = extract_field_ref(ctx, &call.receiver)?;
let nav_field_const = field_const(&nav_ref.entity, &nav_ref.field_name, FieldKind::Navigation);
let nav_related_ident = Ident::new(
&format!("NAV_RELATED_{}", nav_ref.field_name.to_uppercase()),
proc_macro2::Span::call_site(),
);
let nav_entity = &nav_ref.entity;
let nav_related_const = quote! { #nav_entity::#nav_related_ident };
let closure_arg = call.args.first().ok_or_else(|| {
syn::Error::new_spanned(
call,
"subquery method requires a closure argument like `|p: Post| p.published`",
)
})?;
let closure = match closure_arg {
Expr::Closure(c) => c,
_ => {
return Err(syn::Error::new_spanned(
closure_arg,
"subquery method requires a closure argument like `|p: Post| p.published`",
));
}
};
let (param, related_entity) = extract_subquery_closure(closure)?;
let sub_ctx = LinqCtx::single(&related_entity, Some(¶m));
let predicate_expr = compile_bool_expr(&sub_ctx, &closure.body)?;
let predicate_bool_expr = if kind.negate_predicate() {
quote! {
rust_ef::query::BoolExpr::Not(Box::new(#predicate_expr))
}
} else {
predicate_expr
};
Ok((nav_field_const, nav_related_const, predicate_bool_expr))
}
pub(super) fn compile_subquery_method(
ctx: &LinqCtx<'_>,
call: &ExprMethodCall,
kind: SubqueryKind,
) -> syn::Result<TokenStream2> {
let (nav_field, nav_related, predicate) = compile_subquery_parts(ctx, call, kind)?;
let negated = kind.negated();
Ok(quote! {
.where_exists_internal(#nav_field, #nav_related, Some(#predicate), #negated)
})
}
pub(crate) fn compile_subquery_bool(
ctx: &LinqCtx<'_>,
call: &ExprMethodCall,
kind: SubqueryKind,
) -> syn::Result<TokenStream2> {
let (nav_field, nav_related, predicate) = compile_subquery_parts(ctx, call, kind)?;
let negated = kind.negated();
let ctor = if negated {
quote! { rust_ef::query::BoolExpr::NotExists }
} else {
quote! { rust_ef::query::BoolExpr::Exists }
};
Ok(quote! {
#ctor(Box::new({
let mut __spec = rust_ef::query::SubquerySpec::new(#nav_field, #nav_related);
__spec.predicate = Some(Box::new(#predicate));
__spec
}))
})
}
pub(super) fn compile_not_subquery(
ctx: &LinqCtx<'_>,
call: &ExprMethodCall,
) -> syn::Result<TokenStream2> {
let method_str = call.method.to_string();
let (kind, flip_outer) = match method_str.as_str() {
"any" => (SubqueryKind::None, false),
"none" => (SubqueryKind::Any, false),
"all" => (SubqueryKind::All, true),
_ => unreachable!("compile_not_subquery called on non-subquery method"),
};
let (nav_field, nav_related, predicate) = compile_subquery_parts(ctx, call, kind)?;
let negated = kind.negated() ^ flip_outer;
Ok(quote! {
.where_exists_internal(#nav_field, #nav_related, Some(#predicate), #negated)
})
}
fn compile_in_subquery_parts(
ctx: &LinqCtx<'_>,
call: &ExprMethodCall,
) -> syn::Result<(TokenStream2, TokenStream2, TokenStream2)> {
let outer_column = extract_field(ctx, &call.receiver)?;
let closure_arg = call.args.first().ok_or_else(|| {
syn::Error::new_spanned(
call,
"in_subquery requires a closure like `|p: Post| p.blog_id`",
)
})?;
let closure = match closure_arg {
Expr::Closure(c) => c,
_ => {
return Err(syn::Error::new_spanned(
closure_arg,
"in_subquery requires a closure like `|p: Post| p.blog_id`",
));
}
};
let (param, related_entity) = extract_subquery_closure(closure)?;
let sub_ctx = LinqCtx::single(&related_entity, Some(¶m));
let projection_column = extract_field(&sub_ctx, &closure.body)?;
Ok((
outer_column,
quote! { #related_entity::TABLE },
projection_column,
))
}
pub(crate) fn compile_in_subquery_method(
ctx: &LinqCtx<'_>,
call: &ExprMethodCall,
negated: bool,
) -> syn::Result<TokenStream2> {
let (outer_col, source_tbl, proj_col) = compile_in_subquery_parts(ctx, call)?;
Ok(quote! {
.where_in_subquery_internal(#outer_col, #source_tbl, #proj_col, ::core::option::Option::None, #negated)
})
}
pub(crate) fn compile_in_subquery_bool(
ctx: &LinqCtx<'_>,
call: &ExprMethodCall,
negated: bool,
) -> syn::Result<TokenStream2> {
let (outer_col, source_tbl, proj_col) = compile_in_subquery_parts(ctx, call)?;
let ctor = if negated {
quote! { rust_ef::query::BoolExpr::NotInSubquery }
} else {
quote! { rust_ef::query::BoolExpr::InSubquery }
};
Ok(quote! {
#ctor(Box::new(
rust_ef::query::InSubquerySpec::new(#outer_col, #source_tbl, #proj_col)
))
})
}