use super::field_value::proto;
use super::query::QueryState;
use crate::error::FirebaseError;
use crate::firestore::firestore::FirestoreInterceptor;
use proto::google::firestore::v1::firestore_client::FirestoreClient as GrpcClient;
use std::collections::HashMap;
use proto::google::firestore::v1 as firestore_proto;
use firestore_proto::structured_aggregation_query::aggregation::{Count, Sum, Avg, Operator as AggOp};
use firestore_proto::structured_aggregation_query::Aggregation;
use firestore_proto::structured_query::{CollectionSelector, FieldReference, Order};
use firestore_proto::{RunAggregationQueryRequest, StructuredAggregationQuery, StructuredQuery};
#[derive(Debug, Clone)]
pub enum AggregationType {
Count,
Sum(String),
Average(String),
}
#[derive(Debug, Clone)]
pub struct AggregateField {
pub alias: Option<String>,
pub aggregation_type: AggregationType,
}
impl AggregateField {
pub fn count() -> Self {
Self {
alias: None,
aggregation_type: AggregationType::Count,
}
}
pub fn count_with_alias(alias: impl Into<String>) -> Self {
Self {
alias: Some(alias.into()),
aggregation_type: AggregationType::Count,
}
}
pub fn sum(field: impl Into<String>) -> Self {
Self {
alias: None,
aggregation_type: AggregationType::Sum(field.into()),
}
}
pub fn sum_with_alias(field: impl Into<String>, alias: impl Into<String>) -> Self {
Self {
alias: Some(alias.into()),
aggregation_type: AggregationType::Sum(field.into()),
}
}
pub fn average(field: impl Into<String>) -> Self {
Self {
alias: None,
aggregation_type: AggregationType::Average(field.into()),
}
}
pub fn average_with_alias(field: impl Into<String>, alias: impl Into<String>) -> Self {
Self {
alias: Some(alias.into()),
aggregation_type: AggregationType::Average(field.into()),
}
}
pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
self.alias = Some(alias.into());
self
}
}
#[derive(Clone)]
pub struct AggregateQuery {
pub(crate) query_state: QueryState,
pub(crate) aggregations: Vec<AggregateField>,
}
impl AggregateQuery {
pub(crate) fn new(query_state: QueryState, aggregations: Vec<AggregateField>) -> Self {
Self {
query_state,
aggregations,
}
}
pub async fn get(&self) -> Result<AggregateQuerySnapshot, FirebaseError> {
let database_path = format!(
"projects/{}/databases/{}",
self.query_state.firestore.project_id, self.query_state.firestore.database_id
);
let parent = format!("{}/documents", database_path);
let structured_query = query_state_to_structured_query(&self.query_state);
let mut aggregations_proto = Vec::new();
for agg_field in &self.aggregations {
let aggregation = match &agg_field.aggregation_type {
AggregationType::Count => {
let count = Count {
up_to: None, };
Aggregation {
alias: agg_field.alias.clone().unwrap_or_else(|| "count".to_string()),
operator: Some(AggOp::Count(count)),
}
}
AggregationType::Sum(field) => {
let sum = Sum {
field: Some(FieldReference {
field_path: field.clone(),
}),
};
Aggregation {
alias: agg_field.alias.clone().unwrap_or_else(|| format!("sum_{}", field)),
operator: Some(AggOp::Sum(sum)),
}
}
AggregationType::Average(field) => {
let avg = Avg {
field: Some(FieldReference {
field_path: field.clone(),
}),
};
Aggregation {
alias: agg_field
.alias
.clone()
.unwrap_or_else(|| format!("average_{}", field)),
operator: Some(AggOp::Avg(avg)),
}
}
};
aggregations_proto.push(aggregation);
}
let structured_aggregation_query = StructuredAggregationQuery {
query_type: Some(
firestore_proto::structured_aggregation_query::QueryType::StructuredQuery(
structured_query,
),
),
aggregations: aggregations_proto,
};
let request = RunAggregationQueryRequest {
parent,
consistency_selector: None,
explain_options: None,
query_type: Some(
firestore_proto::run_aggregation_query_request::QueryType::StructuredAggregationQuery(
structured_aggregation_query,
),
),
};
let interceptor = FirestoreInterceptor {
auth_data: self.query_state.firestore.auth_data.clone(),
};
let mut client = GrpcClient::with_interceptor(
self.query_state.firestore.channel.clone(),
interceptor,
);
let mut response = client.run_aggregation_query(request).await.map_err(|e| {
crate::error::FirestoreError::Connection(format!("Aggregation query failed: {}", e))
})?;
let stream = response.get_mut();
use futures::stream::StreamExt;
let result = stream.next().await;
match result {
Some(Ok(response)) => {
let mut results = HashMap::new();
if let Some(result) = response.result {
for (alias, value) in result.aggregate_fields {
results.insert(alias, value);
}
}
Ok(AggregateQuerySnapshot { results })
}
Some(Err(e)) => Err(crate::error::FirestoreError::Connection(format!(
"Aggregation query stream error: {}",
e
))
.into()),
None => Ok(AggregateQuerySnapshot {
results: HashMap::new(),
}),
}
}
}
#[derive(Debug, Clone)]
pub struct AggregateQuerySnapshot {
results: HashMap<String, proto::google::firestore::v1::Value>,
}
impl AggregateQuerySnapshot {
pub fn count(&self) -> Option<i64> {
self.get("count")
.and_then(|v| v.value_type.as_ref())
.and_then(|vt| match vt {
proto::google::firestore::v1::value::ValueType::IntegerValue(i) => Some(*i),
_ => None,
})
}
pub fn get(&self, alias: &str) -> Option<&proto::google::firestore::v1::Value> {
self.results.get(alias)
}
pub fn get_int(&self, alias: &str) -> Option<i64> {
self.get(alias)
.and_then(|v| v.value_type.as_ref())
.and_then(|vt| match vt {
proto::google::firestore::v1::value::ValueType::IntegerValue(i) => Some(*i),
_ => None,
})
}
pub fn get_double(&self, alias: &str) -> Option<f64> {
self.get(alias)
.and_then(|v| v.value_type.as_ref())
.and_then(|vt| match vt {
proto::google::firestore::v1::value::ValueType::DoubleValue(d) => Some(*d),
proto::google::firestore::v1::value::ValueType::IntegerValue(i) => {
Some(*i as f64)
}
_ => None,
})
}
pub fn results(&self) -> &HashMap<String, proto::google::firestore::v1::Value> {
&self.results
}
}
fn query_state_to_structured_query(
state: &QueryState,
) -> proto::google::firestore::v1::StructuredQuery {
let collection_id = state
.collection_path
.split('/')
.last()
.unwrap_or("documents")
.to_string();
let mut query = StructuredQuery {
select: None,
from: vec![CollectionSelector {
collection_id,
all_descendants: false,
}],
r#where: None,
order_by: Vec::new(),
start_at: None,
end_at: None,
offset: 0,
limit: None,
find_nearest: None,
};
if !state.filters.is_empty() {
}
for (field, direction) in &state.orders {
query.order_by.push(Order {
field: Some(FieldReference {
field_path: field.clone(),
}),
direction: *direction as i32,
});
}
if let Some(limit) = state.limit_value {
query.limit = Some(limit);
}
query
}