sift-query-derive 0.1.0

Derive macros for SiftQuery entities
Documentation
use std::collections::{BTreeMap, BTreeSet};

use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use syn::{Data, DeriveInput, Fields, LitStr};

use crate::attrs::{
    DEFAULT_CONTROL_NAMES, FieldConfig, Kind, has_attribute, has_sqlx_skip_or_default,
    option_string_tokens, order_tokens, parse_entity_config, parse_field, parse_order_list,
    reject_sqlx_rename_all,
};
use crate::codegen::fingerprint::schema_fingerprint;
use crate::validation::validate_sql_fragment;

pub(crate) fn expand(input: DeriveInput) -> syn::Result<TokenStream2> {
    if !input.generics.params.is_empty() {
        return Err(syn::Error::new(
            input.generics.span(),
            "SearchEntity does not support generic structs in M1",
        ));
    }
    reject_sqlx_rename_all(&input.attrs)?;
    let ident = input.ident;
    let visibility = input.vis;
    let fields = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields.named,
            _ => {
                return Err(syn::Error::new(
                    data.struct_token.span,
                    "SearchEntity requires a struct with named fields",
                ));
            }
        },
        _ => {
            return Err(syn::Error::new(
                ident.span(),
                "SearchEntity can only be derived for structs",
            ));
        }
    };

    let entity = parse_entity_config(&input.attrs, &ident)?;
    if entity.max_page_size < 15 {
        return Err(syn::Error::new(
            ident.span(),
            "max_page_size must be at least the default page size of 15",
        ));
    }
    validate_sql_fragment(&entity.from, "from", ident.span())?;
    for join in &entity.joins {
        validate_sql_fragment(join, "joins", ident.span())?;
    }
    if let Some(value) = &entity.base_where {
        validate_sql_fragment(value, "base_where", ident.span())?;
    }
    if let Some(value) = &entity.fixed_having {
        validate_sql_fragment(value, "fixed_having", ident.span())?;
    }

    let mut parsed_fields = Vec::new();
    let mut public_names = BTreeSet::new();
    let mut aliases = BTreeSet::new();
    for field in fields {
        if has_attribute(&field.attrs, "search_ignore") {
            if !has_sqlx_skip_or_default(&field.attrs)? {
                return Err(syn::Error::new(
                    field.span(),
                    "search_ignore requires #[sqlx(skip)] or #[sqlx(default)]",
                ));
            }
            continue;
        }
        let index = u16::try_from(parsed_fields.len()).map_err(|_| {
            syn::Error::new(field.span(), "SearchEntity supports at most 65536 fields")
        })?;
        let parsed = parse_field(&field, index, &entity)?;
        if !public_names.insert(parsed.public_name.clone()) {
            return Err(syn::Error::new(
                parsed.ident.span(),
                format!("duplicate public field name {}", parsed.public_name),
            ));
        }
        if DEFAULT_CONTROL_NAMES.contains(&parsed.public_name.as_str()) {
            return Err(syn::Error::new(
                parsed.ident.span(),
                format!(
                    "field name {} conflicts with a default control parameter",
                    parsed.public_name
                ),
            ));
        }
        if let Some(alias) = &parsed.result_alias {
            if alias.len() > 63 {
                return Err(syn::Error::new(
                    parsed.ident.span(),
                    "PostgreSQL result aliases must be at most 63 bytes",
                ));
            }
            if !aliases.insert(alias.clone()) {
                return Err(syn::Error::new(
                    parsed.ident.span(),
                    format!("duplicate result alias {alias}"),
                ));
            }
        }
        parsed_fields.push(parsed);
    }
    if parsed_fields.is_empty() {
        return Err(syn::Error::new(
            ident.span(),
            "SearchEntity requires at least one query field",
        ));
    }

    let grouped = parsed_fields
        .iter()
        .any(|field| matches!(field.kind, Kind::GroupKey | Kind::Aggregate));
    if grouped
        && parsed_fields
            .iter()
            .any(|field| field.result_alias.is_some() && matches!(field.kind, Kind::Scalar))
    {
        return Err(syn::Error::new(
            ident.span(),
            "grouped entities cannot return Scalar fields; use GroupKey, Aggregate, or FilterOnly",
        ));
    }

    let field_lookup = parsed_fields
        .iter()
        .map(|field| (field.public_name.clone(), field))
        .collect::<BTreeMap<_, _>>();
    let default_order = parse_order_list(
        entity.default_order.as_deref(),
        "default_order",
        &field_lookup,
        ident.span(),
    )?;
    let stable_order = parse_order_list(
        entity.stable_order.as_deref(),
        "stable_order",
        &field_lookup,
        ident.span(),
    )?;
    if stable_order.is_empty() {
        return Err(syn::Error::new(
            ident.span(),
            "stable_order is required for M1 entities",
        ));
    }

    let fields_ident = format_ident!("{}Fields", ident);
    let condition_order_arms=parsed_fields.iter().filter_map(|field|field.sort_from.as_ref().map(|source|(field,source))).map(|(field,source)| {
        let source=field_lookup.get(source).ok_or_else(||syn::Error::new(field.ident.span(),"sort_from references an unknown public field"))?;
        if matches!(source.kind,Kind::Aggregate) { return Err(syn::Error::new(field.ident.span(),"sort_from requires a row phase field")); }
        let index=field.index; let source_index=source.index;
        let operator=field.sort_operator.as_ref().expect("validated computed order");
        if !source.custom_operators.contains(operator) { return Err(syn::Error::new(field.ident.span(),"sort_operator must be in the source field custom operator whitelist")); }
        let template=field.sort_template.as_ref().expect("validated computed order");
        Ok(quote! { ::sift_query::__private::FieldId(#index)=>Some(::sift_query::__private::ConditionOrder { source: ::sift_query::__private::FieldId(#source_index),operator:#operator,expression: ::sift_query::__private::CustomOperator::new("condition_order",#template,1).expect("validated order template") }) })
    }).collect::<syn::Result<Vec<_>>>()?;
    let custom_operator_arms = parsed_fields
        .iter()
        .filter(|field| !field.custom_operators.is_empty())
        .map(|field| {
            let index = field.index;
            let names = &field.custom_operators;
            quote! { ::sift_query::__private::FieldId(#index) => &[#(#names),*] }
        });
    let field_meta = parsed_fields.iter().map(FieldConfig::meta_tokens);
    let non_null_arms = parsed_fields
        .iter()
        .filter(|field| field.cursor_non_null)
        .map(|field| {
            let index = field.index;
            quote! { ::sift_query::__private::FieldId(#index) => true }
        });
    let codec_arms = parsed_fields.iter().filter_map(|field| {
        field.codec.as_ref().map(|codec| {
            let index = field.index;
            quote! { ::sift_query::__private::FieldId(#index) => Some(#codec) }
        })
    });
    let label_bindings = parsed_fields.iter().filter_map(|field| {
        field.label.as_ref().map(|key| {
            let source = LitStr::new(&field.public_name, field.ident.span());
            let key = LitStr::new(key, field.ident.span());
            quote! { (#source, #key) }
        })
    });
    let field_constants = parsed_fields.iter().map(|field| {
        let const_ident = &field.const_ident;
        let value_type = &field.rust_value_type;
        let kind = field.kind.marker_tokens();
        let index = field.index;
        quote! {
            pub const #const_ident: ::sift_query::__private::FieldRef<#ident, #value_type, #kind> =
                ::sift_query::__private::FieldRef::new(::sift_query::__private::FieldId(#index));
        }
    });
    let cursor_arms = parsed_fields
        .iter()
        .filter(|field| field.sortable && field.result_alias.is_some())
        .map(|field| {
            let rust_field = &field.ident;
            let index = field.index;
            quote! {
                ::sift_query::__private::FieldId(#index) => Some(
                    ::sift_query::IntoCursorValue::to_cursor_value(&self.#rust_field)
                )
            }
        });
    let default_order_tokens = order_tokens(&default_order);
    let stable_order_tokens = order_tokens(&stable_order);
    let from = LitStr::new(&entity.from, ident.span());
    let entity_name = LitStr::new(&ident.to_string(), ident.span());
    let base_where = option_string_tokens(entity.base_where.as_deref(), ident.span());
    let fixed_having = option_string_tokens(entity.fixed_having.as_deref(), ident.span());
    let distinct = entity.distinct;
    let max_page_size = entity.max_page_size;
    let max_offset = entity.max_offset;
    let timeout_ms = entity.timeout_ms;
    let fingerprint = schema_fingerprint(&ident.to_string(), &entity, &parsed_fields);

    Ok(quote! {
        #visibility struct #fields_ident;

        impl #fields_ident {
            #(#field_constants)*
        }

        impl ::sift_query::__private::SearchEntity for #ident {
            fn cursor_key_non_null(field: ::sift_query::__private::FieldId) -> bool {
                match field { #(#non_null_arms,)* _ => false }
            }
            fn condition_order(field: ::sift_query::__private::FieldId)->Option<::sift_query::__private::ConditionOrder> { match field { #(#condition_order_arms,)* _=>None } }
            fn custom_operator_names(field: ::sift_query::__private::FieldId) -> &'static [&'static str] {
                match field { #(#custom_operator_arms,)* _ => &[] }
            }
            fn codec_name(field: ::sift_query::__private::FieldId) -> Option<&'static str> {
                match field {
                    #(#codec_arms,)*
                    _ => None,
                }
            }
            fn label_bindings() -> &'static [(&'static str, &'static str)] {
                &[#(#label_bindings),*]
            }
            const META: &'static ::sift_query::__private::EntityMeta =
                &::sift_query::__private::EntityMeta {
                    entity_name: #entity_name,
                    from: #from,
                    base_where: #base_where,
                    fixed_having: #fixed_having,
                    distinct: #distinct,
                    fields: &[#(#field_meta),*],
                    default_order: &[#(#default_order_tokens),*],
                    stable_order: &[#(#stable_order_tokens),*],
                    limits: ::sift_query::__private::EntityLimits {
                        max_page_size: #max_page_size,
                        max_offset: #max_offset,
                        timeout_ms: #timeout_ms,
                    },
                    schema_fingerprint: #fingerprint,
                };

            fn cursor_value(
                &self,
                field: ::sift_query::__private::FieldId,
            ) -> Option<::sift_query::CursorValue> {
                match field {
                    #(#cursor_arms,)*
                    _ => None,
                }
            }
        }
    })
}