force 0.2.0

Production-ready Salesforce Platform API client with REST and Bulk API 2.0 support
Documentation
//! Query Plan Analyzer.
//!
//! Evaluates Query Plan API responses (`ExplainResponse`) to produce insights
//! and actionable warnings (e.g., table scans, high cost, or missing indexes).

use crate::types::explain::ExplainResponse;

/// The severity of a query plan insight.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InsightSeverity {
    /// Action is likely required to prevent performance degradation or limits issues.
    Warning,
    /// An informational note about the query's execution.
    Info,
}

/// A specific insight or recommendation derived from analyzing a query plan.
///
/// ⚡ Bolt: Uses borrowed string references (`&'a str`) tied to the underlying `ExplainResponse`
/// payload instead of owned `String`s, preventing unnecessary heap allocations and `.clone()`
/// calls during schema analysis hot paths.
#[derive(Debug, Clone, PartialEq)]
pub struct QueryInsight<'a> {
    /// The severity of the finding.
    pub severity: InsightSeverity,
    /// A description of the finding.
    pub message: String,
    /// The specific plan that triggered this finding (by index or operation type).
    pub operation_type: &'a str,
}

/// Analysis results for a given Query Plan.
///
/// ⚡ Bolt: Uses borrowed string references (`&'a str`) instead of owned `String`s to
/// significantly reduce heap allocations and memory churn when processing the plan.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct QueryInsights<'a> {
    /// The total number of plans evaluated.
    pub evaluated_plans: usize,
    /// A list of findings generated by the analyzer.
    pub insights: Vec<QueryInsight<'a>>,
    /// The lowest relative cost found among all plans.
    pub lowest_cost: f64,
    /// The operation type of the plan with the lowest cost.
    pub best_operation_type: Option<&'a str>,
}

/// Analyzes an `ExplainResponse` to extract performance insights and warnings.
///
/// # Arguments
///
/// * `response` - The response from the Query Plan API.
///
/// # Returns
///
/// A `QueryInsights` struct containing warnings and the best execution path.
#[must_use]
pub fn analyze_query_plan(response: &ExplainResponse) -> QueryInsights<'_> {
    let mut insights = QueryInsights {
        insights: Vec::with_capacity(response.plans.len() * 2), // Bolt: Pre-allocate capacity to avoid multiple reallocations during analysis.
        evaluated_plans: response.plans.len(),
        lowest_cost: f64::MAX,
        // ⚡ Bolt: Tying the initial vector capacity to the context-aware input size (`response.plans.len()`) avoids the overhead of multiple dynamic heap reallocations.
        best_operation_type: None,
    };

    if response.plans.is_empty() {
        return insights;
    }

    for plan in &response.plans {
        // Track the best plan
        if plan.relative_cost < insights.lowest_cost {
            insights.lowest_cost = plan.relative_cost;
            insights.best_operation_type = Some(&plan.leading_operation_type);
        }

        // Rule: Warn on TableScan
        if plan
            .leading_operation_type
            .eq_ignore_ascii_case("TableScan")
        {
            insights.insights.push(QueryInsight {
                severity: InsightSeverity::Warning,
                message: "Full table scan detected. This query will not scale well. Consider adding filters on indexed fields.".to_string(),
                operation_type: &plan.leading_operation_type,
            });
        }

        // Rule: Warn on high relative cost (> 1.0 is generally considered poor)
        if plan.relative_cost > 1.0 {
            insights.insights.push(QueryInsight {
                severity: InsightSeverity::Warning,
                message: format!("High relative cost detected ({:.2}). The query optimizer is unlikely to use this path efficiently.", plan.relative_cost),
                operation_type: &plan.leading_operation_type,
            });
        }

        // Rule: Surface notes provided by the SFDC optimizer
        for note in &plan.notes {
            insights.insights.push(QueryInsight {
                severity: InsightSeverity::Info,
                message: format!(
                    "Optimizer Note on {}: {}",
                    note.table_enum_or_id, note.description
                ),
                operation_type: &plan.leading_operation_type,
            });
        }

        // Rule: High cardinality relative to table size (Selectivity)
        if plan.sobject_cardinality > 0 {
            let selectivity = (plan.cardinality as f64) / (plan.sobject_cardinality as f64);
            if selectivity > 0.3
                && !plan
                    .leading_operation_type
                    .eq_ignore_ascii_case("TableScan")
            {
                // If a non-tablescan operation is pulling more than 30% of the table, SFDC often falls back to TableScan anyway
                insights.insights.push(QueryInsight {
                    severity: InsightSeverity::Warning,
                    message: format!("Query lacks selectivity (pulling {:.0}% of table). Indexes may be ignored by the optimizer in favor of a TableScan.", selectivity * 100.0),
                    operation_type: &plan.leading_operation_type,
                });
            }
        }
    }

    insights
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::explain::{PlanNote, QueryPlan};

    fn create_plan(
        op_type: &str,
        cost: f64,
        cardinality: u64,
        table_size: u64,
        notes: Vec<PlanNote>,
    ) -> QueryPlan {
        QueryPlan {
            cardinality,
            fields: vec![],
            leading_operation_type: op_type.to_string(),
            notes,
            relative_cost: cost,
            sobject_cardinality: table_size,
            sobject_type: "Account".to_string(),
        }
    }

    #[test]
    fn test_analyze_query_plan_empty() {
        let response = ExplainResponse { plans: vec![] };
        let insights = analyze_query_plan(&response);

        assert_eq!(insights.evaluated_plans, 0);
        assert!(insights.insights.is_empty());
        assert_eq!(insights.best_operation_type, None);
    }

    #[test]
    fn test_analyze_query_plan_table_scan() {
        let response = ExplainResponse {
            plans: vec![create_plan("TableScan", 1.5, 500, 1000, vec![])],
        };

        let insights = analyze_query_plan(&response);

        assert_eq!(insights.evaluated_plans, 1);
        assert_eq!(insights.best_operation_type, Some("TableScan"));
        assert!((insights.lowest_cost - 1.5).abs() < f64::EPSILON);

        // Should trigger TableScan warning and High Cost (> 1.0) warning (selectivity warning is bypassed for TableScan)
        assert_eq!(insights.insights.len(), 2);

        let warnings: Vec<_> = insights.insights.iter().map(|i| &i.message).collect();
        assert!(warnings.iter().any(|msg| msg.contains("Full table scan")));
        assert!(
            warnings
                .iter()
                .any(|msg| msg.contains("High relative cost"))
        );
    }

    #[test]
    fn test_analyze_query_plan_index_scan_optimal() {
        let response = ExplainResponse {
            plans: vec![create_plan("IndexScan", 0.1, 10, 10000, vec![])],
        };

        let insights = analyze_query_plan(&response);

        assert_eq!(insights.evaluated_plans, 1);
        assert_eq!(insights.best_operation_type, Some("IndexScan"));
        assert!((insights.lowest_cost - 0.1).abs() < f64::EPSILON);
        assert!(insights.insights.is_empty()); // Clean, highly selective index scan
    }

    #[test]
    fn test_analyze_query_plan_poor_selectivity() {
        // IndexScan, but pulling 40% of the table
        let response = ExplainResponse {
            plans: vec![create_plan("IndexScan", 0.8, 4000, 10000, vec![])],
        };

        let insights = analyze_query_plan(&response);

        assert_eq!(insights.insights.len(), 1);
        assert_eq!(insights.insights[0].severity, InsightSeverity::Warning);
        assert!(insights.insights[0].message.contains("lacks selectivity"));
    }

    #[test]
    fn test_analyze_query_plan_with_notes() {
        let note = PlanNote {
            description: "Not considered for indexing".to_string(),
            fields: vec!["Name".to_string()],
            table_enum_or_id: "Account".to_string(),
        };

        let response = ExplainResponse {
            plans: vec![create_plan("TableScan", 2.0, 100, 1000, vec![note])],
        };

        let insights = analyze_query_plan(&response);

        let infos: Vec<_> = insights
            .insights
            .iter()
            .filter(|i| i.severity == InsightSeverity::Info)
            .collect();

        assert_eq!(infos.len(), 1);
        assert!(infos[0].message.contains("Not considered for indexing"));
    }
}