rullst-orm-macros 13.0.0-alpha.2

Procedural macros and derive attributes for the rullst-orm Active Record pattern.
Documentation
// src/builder/sql_assembly.rs — SQL string building, formatting, and dialect serialization.

use proc_macro2::TokenStream;
use quote::quote;

/// `soft_delete_filters` holds the live and trashed predicates of a
/// soft-delete model, and is `None` for a model without soft deletes.
pub fn generate_sql_assembly_methods(
    table_name: &str,
    soft_delete_filters: Option<(&str, &str)>,
) -> TokenStream {
    let table_lit = table_name;
    let push_soft_deletes = generate_push_soft_deletes(soft_delete_filters);

    quote! {
        fn push_ctes(&self, sql: &mut String) {
            if !self.ctes.is_empty() {
                if self.has_recursive_cte {
                    sql.push_str("WITH RECURSIVE ");
                } else {
                    sql.push_str("WITH ");
                }
                sql.push_str(&self.ctes.join(", "));
                sql.push(' ');
            }
        }

        fn push_select(&self, sql: &mut String) {
            let select_clause = match &self.selects {
                Some(s) => s.as_str(),
                None => "*",
            };
            sql.push_str("SELECT ");
            if self.is_distinct {
                sql.push_str("DISTINCT ");
            }
            sql.push_str(select_clause);
        }

        fn push_from(&self, sql: &mut String) {
            sql.push_str(" FROM ");
            sql.push_str(#table_lit);
        }

        fn push_joins(&self, sql: &mut String) {
            for join in &self.joins {
                sql.push(' ');
                sql.push_str(join);
            }
        }

        fn push_condition_group(sql: &mut String, conditions: &[(String, String)]) {
            if conditions.len() > 1 { sql.push('('); }
            for (index, (operator, condition)) in conditions.iter().enumerate() {
                if index > 0 {
                    sql.push(' ');
                    sql.push_str(operator);
                    sql.push(' ');
                }
                sql.push('(');
                sql.push_str(condition);
                sql.push(')');
            }
            if conditions.len() > 1 { sql.push(')'); }
        }

        // Model-wide and tenant filters must remain independent AND groups;
        // a later user OR filter must never broaden either mandatory scope.
        fn freeze_scope(&mut self) {
            if !self.wheres.is_empty() {
                let mut scope = String::new();
                Self::push_condition_group(&mut scope, &self.wheres);
                self.scope_wheres.push(scope);
                self.wheres.clear();
                self.scope_bindings.append(&mut self.bindings);
            }
        }

        /// A generated predicate (the tenant scope or the keyset cursor) on
        /// one of this model's own columns, qualified with its table so a
        /// join with a table that has the same column cannot make it
        /// ambiguous. The column name was validated when the model was
        /// derived; the column guards still apply.
        fn __rullst_where_own<T: Into<rullst_orm::RullstValue>>(
            mut self,
            column: &'static str,
            operator: &'static str,
            value: T,
        ) -> Self {
            self.reject_skipped_column(column);
            self.wheres.push((
                "AND".to_string(),
                format!("{}.{} {} {}", #table_lit, column, operator, Self::__rullst_bind_marker(column)),
            ));
            self.bindings.push(value.into());
            self
        }

        /// Freezes the filters added so far, such as a relation's ownership
        /// predicate, into their own AND group before a caller's modifier runs.
        #[doc(hidden)]
        pub fn __rullst_freeze_scope(mut self) -> Self {
            self.freeze_scope();
            self
        }

        fn push_wheres(&self, sql: &mut String) -> bool {
            if self.scope_wheres.is_empty() && self.wheres.is_empty() {
                return true;
            }
            sql.push_str(" WHERE ");
            sql.push_str(&self.scope_wheres.join(" AND "));
            if !self.wheres.is_empty() {
                if !self.scope_wheres.is_empty() { sql.push_str(" AND "); }
                Self::push_condition_group(sql, &self.wheres);
            }
            false
        }

        #push_soft_deletes

        fn push_group_by(&self, sql: &mut String) {
            if let Some(group) = &self.group_by {
                sql.push_str(" GROUP BY ");
                sql.push_str(group);
            }
        }

        fn push_havings(&self, sql: &mut String) {
            let mut first_having = true;
            if !self.havings.is_empty() {
                sql.push_str(" HAVING ");
                for (op, cond) in &self.havings {
                    if first_having {
                        sql.push('(');
                        sql.push_str(cond);
                        sql.push(')');
                        first_having = false;
                    } else {
                        sql.push(' ');
                        sql.push_str(op);
                        sql.push_str(" (");
                        sql.push_str(cond);
                        sql.push(')');
                    }
                }
            }
        }

        fn push_order_by(&self, sql: &mut String) {
            if self.__rullst_relevance_omitted() {
                return;
            }
            if let Some(order) = &self.order_by {
                sql.push_str(" ORDER BY ");
                sql.push_str(order);
            }
        }

        fn push_limit_offset(&self, sql: &mut String) {
            if let Some(limit) = self.limit {
                sql.push_str(" LIMIT ");
                sql.push_str(&limit.to_string());
            } else if self.offset.is_some() {
                // SQLite and MySQL/MariaDB accept OFFSET only after LIMIT, so an
                // uncapped offset query names each dialect's "no limit" value.
                match rullst_orm::Orm::driver() {
                    Ok("sqlite") => sql.push_str(" LIMIT -1"),
                    Ok("mysql") => sql.push_str(" LIMIT 18446744073709551615"),
                    Ok(_) | Err(_) => {}
                }
            }
            if let Some(offset) = self.offset {
                sql.push_str(" OFFSET ");
                sql.push_str(&offset.to_string());
            }
        }

        fn format_postgres(&self, sql: &str) -> String {
            match rullst_orm::Orm::driver() {
                Ok("postgres") => rullst_orm::replace_placeholders(sql),
                // SQL assembly is non-executing and historically infallible.
                // Execution still obtains a fallible pool before sending SQL.
                Ok(_) | Err(_) => sql.to_string(),
            }
        }

        pub fn to_sql(&self) -> String {
            let estimated_capacity = 50 + #table_lit.len() + self.joins.iter().map(|j| j.len() + 1).sum::<usize>()
                + self.wheres.iter().map(|(o, c)| o.len() + c.len() + 4).sum::<usize>()
                + self.ctes.iter().map(|c| c.len() + 2).sum::<usize>();
            let mut sql = String::with_capacity(estimated_capacity);

            self.push_ctes(&mut sql);
            self.push_select(&mut sql);
            self.push_from(&mut sql);
            self.push_joins(&mut sql);
            let first_where = self.push_wheres(&mut sql);
            self.push_soft_deletes(&mut sql, first_where);
            self.push_group_by(&mut sql);
            self.push_havings(&mut sql);
            self.push_order_by(&mut sql);
            self.push_limit_offset(&mut sql);

            self.format_postgres(&sql)
        }

        pub fn to_count_sql(&self) -> String {
            let estimated_capacity = 50 + #table_lit.len() + self.joins.iter().map(|j| j.len() + 1).sum::<usize>()
                + self.wheres.iter().map(|(o, c)| o.len() + c.len() + 4).sum::<usize>()
                + self.ctes.iter().map(|c| c.len() + 2).sum::<usize>();
            let mut sql = String::with_capacity(estimated_capacity);

            self.push_ctes(&mut sql);
            // DISTINCT and GROUP BY decide which rows the query returns, so
            // their count wraps that row query instead of counting table rows.
            let wrapped = self.__rullst_count_wraps_rows();
            if !wrapped {
                sql.push_str("SELECT COUNT(*)");
            } else if self.is_distinct {
                sql.push_str("SELECT COUNT(*) FROM (");
                self.push_select(&mut sql);
            } else {
                sql.push_str("SELECT COUNT(*) FROM (SELECT 1");
            }
            self.push_from(&mut sql);
            self.push_joins(&mut sql);
            let first_where = self.push_wheres(&mut sql);
            self.push_soft_deletes(&mut sql, first_where);
            self.push_group_by(&mut sql);
            self.push_havings(&mut sql);
            if wrapped {
                sql.push_str(") AS __rullst_count");
            }

            self.format_postgres(&sql)
        }

        fn __rullst_count_wraps_rows(&self) -> bool {
            self.is_distinct || self.group_by.is_some()
        }

        /// Bindings of `to_count_sql`: a wrapped DISTINCT count keeps the
        /// select list, including a `select_raw_bindings` fragment's values.
        fn __rullst_count_query_bindings(&self) -> Vec<rullst_orm::RullstValue> {
            let select_raw: &[rullst_orm::RullstValue] = if self.is_distinct {
                self.__rullst_select_raw_bindings()
            } else {
                &[]
            };
            self.cte_bindings
                .iter()
                .chain(select_raw.iter())
                .chain(self.join_bindings.iter())
                .chain(self.scope_bindings.iter())
                .chain(self.bindings.iter())
                .cloned()
                .collect()
        }

        pub fn to_pluck_sql(&self, column: &str) -> String {
            let estimated_capacity = 50 + #table_lit.len() + self.joins.iter().map(|j| j.len() + 1).sum::<usize>()
                + self.wheres.iter().map(|(o, c)| o.len() + c.len() + 4).sum::<usize>()
                + self.ctes.iter().map(|c| c.len() + 2).sum::<usize>();
            let mut sql = String::with_capacity(estimated_capacity);

            self.push_ctes(&mut sql);
            sql.push_str("SELECT ");
            if self.is_distinct { sql.push_str("DISTINCT "); }
            sql.push_str(column);

            self.push_from(&mut sql);
            self.push_joins(&mut sql);
            let first_where = self.push_wheres(&mut sql);
            self.push_soft_deletes(&mut sql, first_where);
            self.push_group_by(&mut sql);
            self.push_havings(&mut sql);
            self.push_order_by(&mut sql);
            self.push_limit_offset(&mut sql);

            self.format_postgres(&sql)
        }
    }
}

fn generate_push_soft_deletes(soft_delete_filters: Option<(&str, &str)>) -> TokenStream {
    match soft_delete_filters {
        Some((live, trashed)) => quote! {
            fn push_soft_deletes(&self, sql: &mut String, first_where: bool) {
                if self.with_trashed {
                    return;
                }
                sql.push_str(if first_where { " WHERE " } else { " AND " });
                sql.push_str(if self.only_trashed { #trashed } else { #live });
            }
        },
        // No row of a model without soft deletes is trashed, so a trashed-only
        // statement matches nothing, even when the public `only_trashed` field
        // was set directly instead of through `only_trashed()`.
        None => quote! {
            fn push_soft_deletes(&self, sql: &mut String, first_where: bool) {
                if self.only_trashed && !self.with_trashed {
                    sql.push_str(if first_where { " WHERE " } else { " AND " });
                    sql.push_str("1 = 0");
                }
            }
        },
    }
}