use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct ExplainResponse {
pub plans: Vec<QueryPlan>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryPlan {
pub cardinality: u64,
#[serde(default)]
pub fields: Vec<String>,
pub leading_operation_type: String,
#[serde(default)]
pub notes: Vec<PlanNote>,
pub relative_cost: f64,
pub sobject_cardinality: u64,
pub sobject_type: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanNote {
pub description: String,
#[serde(default)]
pub fields: Vec<String>,
pub table_enum_or_id: String,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::Must;
#[test]
fn test_deserialize_explain_response() {
let json = serde_json::json!({
"plans": [
{
"cardinality": 50,
"fields": ["Name"],
"leadingOperationType": "TableScan",
"notes": [
{
"description": "Not indexed",
"fields": ["Name"],
"tableEnumOrId": "Account"
}
],
"relativeCost": 1.5,
"sobjectCardinality": 1000,
"sobjectType": "Account"
}
]
});
let response: ExplainResponse = serde_json::from_value(json).must();
assert_eq!(response.plans.len(), 1);
let plan = &response.plans[0];
assert_eq!(plan.cardinality, 50);
assert_eq!(plan.leading_operation_type, "TableScan");
assert!((plan.relative_cost - 1.5).abs() < f64::EPSILON);
assert_eq!(plan.sobject_cardinality, 1000);
assert_eq!(plan.sobject_type, "Account");
assert_eq!(plan.fields, vec!["Name"]);
assert_eq!(plan.notes.len(), 1);
let note = &plan.notes[0];
assert_eq!(note.description, "Not indexed");
assert_eq!(note.table_enum_or_id, "Account");
}
#[test]
fn test_deserialize_explain_response_minimal() {
let json = serde_json::json!({
"plans": [
{
"cardinality": 10,
"leadingOperationType": "IndexScan",
"relativeCost": 0.1,
"sobjectCardinality": 100,
"sobjectType": "Contact"
}
]
});
let response: ExplainResponse = serde_json::from_value(json).must();
let plan = &response.plans[0];
assert!(plan.fields.is_empty());
assert!(plan.notes.is_empty());
assert_eq!(plan.leading_operation_type, "IndexScan");
}
#[tokio::test]
async fn test_explain_api_call() {
use crate::client::builder;
use crate::test_support::{MockAuthenticator, Must};
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
let mock_server = MockServer::start().await;
let auth = MockAuthenticator::new("token", &mock_server.uri());
let client = builder().authenticate(auth).build().await.must();
let soql = "SELECT Id FROM Account";
let json_response = serde_json::json!({
"plans": [
{
"cardinality": 50,
"fields": ["Name"],
"leadingOperationType": "TableScan",
"notes": [],
"relativeCost": 1.5,
"sobjectCardinality": 1000,
"sobjectType": "Account"
}
]
});
Mock::given(method("GET"))
.and(path("/services/data/v60.0/query"))
.and(query_param("explain", soql))
.respond_with(ResponseTemplate::new(200).set_body_json(json_response))
.mount(&mock_server)
.await;
let response: ExplainResponse = client.rest().explain(soql).await.must();
assert_eq!(response.plans.len(), 1);
assert_eq!(response.plans[0].leading_operation_type, "TableScan");
}
}