use super::types::{ColumnMetadata, DataCloudRecord, SqlQueryRequest};
use crate::error::Result;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize)]
pub struct SqlQueryResponse {
#[serde(default)]
pub data: Vec<DataCloudRecord>,
#[serde(default)]
pub metadata: Vec<ColumnMetadata>,
#[serde(default, alias = "nextBatchId")]
pub next_batch_id: Option<String>,
#[serde(default, alias = "rowCount")]
pub row_count: Option<usize>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl<A: crate::auth::Authenticator> super::DataCloudHandler<A> {
pub async fn query_sql(&self, sql: &str) -> Result<SqlQueryResponse> {
let request = SqlQueryRequest::new(sql);
self.post("query", &request, "Data Cloud SQL query failed")
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::Must;
#[test]
fn test_sql_query_response_deserialization() {
let json = r#"{
"data": [
{"Id": "abc123", "Name__c": "Test Profile"},
{"Id": "def456", "Name__c": "Another Profile"}
],
"metadata": [
{"columnName": "Id", "type": "VARCHAR"},
{"columnName": "Name__c", "type": "VARCHAR"}
],
"rowCount": 2
}"#;
let response: SqlQueryResponse = serde_json::from_str(json).must();
assert_eq!(response.data.len(), 2);
assert_eq!(response.metadata.len(), 2);
assert_eq!(response.row_count, Some(2));
assert!(response.next_batch_id.is_none());
}
#[test]
fn test_sql_query_response_with_pagination() {
let json = r#"{
"data": [{"Id": "abc"}],
"metadata": [],
"nextBatchId": "cursor_abc123",
"rowCount": 1
}"#;
let response: SqlQueryResponse = serde_json::from_str(json).must();
assert_eq!(response.next_batch_id.as_deref(), Some("cursor_abc123"));
}
#[test]
fn test_sql_query_response_empty() {
let json = r#"{
"data": [],
"metadata": [],
"rowCount": 0
}"#;
let response: SqlQueryResponse = serde_json::from_str(json).must();
assert!(response.data.is_empty());
assert_eq!(response.row_count, Some(0));
}
#[test]
fn test_sql_query_response_with_extra_fields() {
let json = r#"{
"data": [],
"metadata": [],
"rowCount": 0,
"queryId": "q-12345",
"done": true
}"#;
let response: SqlQueryResponse = serde_json::from_str(json).must();
assert!(response.extra.contains_key("queryId"));
assert!(response.extra.contains_key("done"));
}
#[cfg(feature = "mock")]
mod integration {
use crate::auth::DataCloudConfig;
use crate::client::builder;
use crate::test_support::{MockAuthenticator, Must};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn dc_client_for(
mock_server: &MockServer,
) -> crate::client::ForceClient<MockAuthenticator> {
let dc_token_response = serde_json::json!({
"access_token": "dc_token_for_query",
"instance_url": mock_server.uri(),
"token_type": "Bearer",
"expires_in": 7200
});
Mock::given(method("POST"))
.and(path("/services/a360/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(dc_token_response))
.mount(mock_server)
.await;
let auth = MockAuthenticator::new("platform_token", &mock_server.uri());
builder()
.authenticate(auth)
.with_data_cloud(DataCloudConfig {
token_exchange_url: Some(format!("{}/services/a360/token", mock_server.uri())),
..Default::default()
})
.build()
.await
.must()
}
#[tokio::test]
async fn test_query_sql_success() {
let mock_server = MockServer::start().await;
let query_response = serde_json::json!({
"data": [
{"Id": "rec1", "Name__c": "Alice"},
{"Id": "rec2", "Name__c": "Bob"}
],
"metadata": [
{"columnName": "Id", "type": "VARCHAR"},
{"columnName": "Name__c", "type": "VARCHAR"}
],
"rowCount": 2
});
Mock::given(method("POST"))
.and(path("/services/data/v60.0/ssot/query"))
.respond_with(ResponseTemplate::new(200).set_body_json(query_response))
.expect(1)
.mount(&mock_server)
.await;
let client = dc_client_for(&mock_server).await;
let dc = client.data_cloud().must();
let result = dc
.query_sql("SELECT Id, Name__c FROM UnifiedProfile__dlm")
.await
.must();
assert_eq!(result.data.len(), 2);
assert_eq!(result.row_count, Some(2));
let first = &result.data[0];
assert_eq!(first.get("Id").and_then(|v| v.as_str()), Some("rec1"));
assert_eq!(first.get("Name__c").and_then(|v| v.as_str()), Some("Alice"));
}
#[tokio::test]
async fn test_query_sql_empty_result() {
let mock_server = MockServer::start().await;
let query_response = serde_json::json!({
"data": [],
"metadata": [],
"rowCount": 0
});
Mock::given(method("POST"))
.and(path("/services/data/v60.0/ssot/query"))
.respond_with(ResponseTemplate::new(200).set_body_json(query_response))
.mount(&mock_server)
.await;
let client = dc_client_for(&mock_server).await;
let dc = client.data_cloud().must();
let result = dc.query_sql("SELECT Id FROM Empty__dlm").await.must();
assert!(result.data.is_empty());
assert_eq!(result.row_count, Some(0));
}
#[tokio::test]
async fn test_query_sql_error_response() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/services/data/v60.0/ssot/query"))
.respond_with(
ResponseTemplate::new(400).set_body_string(
r#"[{"message":"Invalid SQL","errorCode":"INVALID_QUERY"}]"#,
),
)
.mount(&mock_server)
.await;
let client = dc_client_for(&mock_server).await;
let dc = client.data_cloud().must();
let result = dc.query_sql("INVALID SQL").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_query_sql_with_pagination() {
let mock_server = MockServer::start().await;
let query_response = serde_json::json!({
"data": [{"Id": "rec1"}],
"metadata": [{"columnName": "Id", "type": "VARCHAR"}],
"nextBatchId": "batch_cursor_xyz",
"rowCount": 1
});
Mock::given(method("POST"))
.and(path("/services/data/v60.0/ssot/query"))
.respond_with(ResponseTemplate::new(200).set_body_json(query_response))
.mount(&mock_server)
.await;
let client = dc_client_for(&mock_server).await;
let dc = client.data_cloud().must();
let result = dc.query_sql("SELECT Id FROM Big__dlm").await.must();
assert_eq!(result.next_batch_id.as_deref(), Some("batch_cursor_xyz"));
}
}
}