1use anyhow::{anyhow, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::fmt;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum AggregateFunction {
18 Count,
20 Sum,
22 Avg,
24 Min,
26 Max,
28 GroupConcat,
30 Sample,
32}
33
34impl fmt::Display for AggregateFunction {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 AggregateFunction::Count => write!(f, "COUNT"),
38 AggregateFunction::Sum => write!(f, "SUM"),
39 AggregateFunction::Avg => write!(f, "AVG"),
40 AggregateFunction::Min => write!(f, "MIN"),
41 AggregateFunction::Max => write!(f, "MAX"),
42 AggregateFunction::GroupConcat => write!(f, "GROUP_CONCAT"),
43 AggregateFunction::Sample => write!(f, "SAMPLE"),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct Aggregation {
51 pub function: AggregateFunction,
53 pub field: String,
55 pub alias: String,
57 pub distinct: bool,
59 pub separator: Option<String>,
61}
62
63impl Aggregation {
64 pub fn new(function: AggregateFunction, field: String, alias: String) -> Self {
65 Self {
66 function,
67 field,
68 alias,
69 distinct: false,
70 separator: None,
71 }
72 }
73
74 pub fn with_distinct(mut self) -> Self {
75 self.distinct = true;
76 self
77 }
78
79 pub fn with_separator(mut self, separator: String) -> Self {
80 self.separator = Some(separator);
81 self
82 }
83
84 pub fn to_sparql_expression(&self) -> String {
86 let distinct_prefix = if self.distinct { "DISTINCT " } else { "" };
87 let var = format!("?{}", self.field);
88
89 match self.function {
90 AggregateFunction::Count => {
91 if self.field == "*" {
92 format!("(COUNT({}*) AS ?{})", distinct_prefix, self.alias)
93 } else {
94 format!("(COUNT({}{}) AS ?{})", distinct_prefix, var, self.alias)
95 }
96 }
97 AggregateFunction::Sum => {
98 format!("(SUM({}{}) AS ?{})", distinct_prefix, var, self.alias)
99 }
100 AggregateFunction::Avg => {
101 format!("(AVG({}{}) AS ?{})", distinct_prefix, var, self.alias)
102 }
103 AggregateFunction::Min => {
104 format!("(MIN({}) AS ?{})", var, self.alias)
105 }
106 AggregateFunction::Max => {
107 format!("(MAX({}) AS ?{})", var, self.alias)
108 }
109 AggregateFunction::GroupConcat => {
110 let separator = self.separator.as_deref().unwrap_or(" ");
111 format!(
112 "(GROUP_CONCAT({}{}; SEPARATOR=\"{}\") AS ?{})",
113 distinct_prefix, var, separator, self.alias
114 )
115 }
116 AggregateFunction::Sample => {
117 format!("(SAMPLE({}) AS ?{})", var, self.alias)
118 }
119 }
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct GroupBy {
126 pub fields: Vec<String>,
128}
129
130impl GroupBy {
131 pub fn new(fields: Vec<String>) -> Self {
132 Self { fields }
133 }
134
135 pub fn to_sparql_clause(&self) -> String {
137 let vars: Vec<String> = self.fields.iter().map(|f| format!("?{}", f)).collect();
138 format!("GROUP BY {}", vars.join(" "))
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct HavingCondition {
145 pub aggregation: Aggregation,
147 pub operator: String,
149 pub value: serde_json::Value,
151}
152
153impl HavingCondition {
154 pub fn new(aggregation: Aggregation, operator: String, value: serde_json::Value) -> Self {
155 Self {
156 aggregation,
157 operator,
158 value,
159 }
160 }
161
162 pub fn to_sparql_clause(&self) -> Result<String> {
164 let agg_expr = self.aggregation.to_sparql_expression();
165 let expr_part = if let Some(start) = agg_expr.find('(') {
167 if let Some(end) = agg_expr.find(" AS ") {
168 &agg_expr[start..end]
169 } else {
170 return Err(anyhow!("Invalid aggregation expression"));
171 }
172 } else {
173 return Err(anyhow!("Invalid aggregation expression"));
174 };
175
176 let value_str = match &self.value {
177 serde_json::Value::Number(n) => n.to_string(),
178 serde_json::Value::String(s) => format!("\"{}\"", s),
179 serde_json::Value::Bool(b) => b.to_string(),
180 _ => return Err(anyhow!("Unsupported value type for HAVING")),
181 };
182
183 Ok(format!("{} {} {}", expr_part, self.operator, value_str))
184 }
185}
186
187#[derive(Debug, Clone)]
189pub struct AggregationQuery {
190 pub aggregations: Vec<Aggregation>,
192 pub group_by: Option<GroupBy>,
194 pub having: Vec<HavingCondition>,
196 pub select_fields: Vec<String>,
198}
199
200impl AggregationQuery {
201 pub fn new() -> Self {
202 Self {
203 aggregations: Vec::new(),
204 group_by: None,
205 having: Vec::new(),
206 select_fields: Vec::new(),
207 }
208 }
209
210 pub fn with_aggregation(mut self, aggregation: Aggregation) -> Self {
211 self.aggregations.push(aggregation);
212 self
213 }
214
215 pub fn with_group_by(mut self, group_by: GroupBy) -> Self {
216 self.group_by = Some(group_by);
217 self
218 }
219
220 pub fn with_having(mut self, having: HavingCondition) -> Self {
221 self.having.push(having);
222 self
223 }
224
225 pub fn with_select_field(mut self, field: String) -> Self {
226 self.select_fields.push(field);
227 self
228 }
229
230 pub fn to_sparql_select(&self) -> String {
232 let mut select_parts = Vec::new();
233
234 for field in &self.select_fields {
236 select_parts.push(format!("?{}", field));
237 }
238
239 for agg in &self.aggregations {
241 select_parts.push(agg.to_sparql_expression());
242 }
243
244 format!("SELECT {}", select_parts.join(" "))
245 }
246
247 pub fn to_sparql_query(&self, where_clause: &str) -> Result<String> {
249 let mut query = String::new();
250
251 query.push_str(&self.to_sparql_select());
253 query.push('\n');
254
255 query.push_str("WHERE {\n");
257 query.push_str(where_clause);
258 query.push_str("\n}\n");
259
260 if let Some(group_by) = &self.group_by {
262 query.push_str(&group_by.to_sparql_clause());
263 query.push('\n');
264 }
265
266 if !self.having.is_empty() {
268 let having_conditions: Result<Vec<String>> =
269 self.having.iter().map(|h| h.to_sparql_clause()).collect();
270 let having_conditions = having_conditions?;
271 query.push_str(&format!("HAVING ({})\n", having_conditions.join(" && ")));
272 }
273
274 Ok(query)
275 }
276}
277
278impl Default for AggregationQuery {
279 fn default() -> Self {
280 Self::new()
281 }
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct AggregationResult {
287 pub group_values: HashMap<String, serde_json::Value>,
289 pub aggregates: HashMap<String, serde_json::Value>,
291}
292
293impl AggregationResult {
294 pub fn new() -> Self {
295 Self {
296 group_values: HashMap::new(),
297 aggregates: HashMap::new(),
298 }
299 }
300
301 pub fn add_group_value(&mut self, field: String, value: serde_json::Value) {
302 self.group_values.insert(field, value);
303 }
304
305 pub fn add_aggregate(&mut self, alias: String, value: serde_json::Value) {
306 self.aggregates.insert(alias, value);
307 }
308}
309
310impl Default for AggregationResult {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316pub struct AggregationQueryBuilder {
318 query: AggregationQuery,
319}
320
321impl AggregationQueryBuilder {
322 pub fn new() -> Self {
323 Self {
324 query: AggregationQuery::new(),
325 }
326 }
327
328 pub fn count(mut self, field: String, alias: String) -> Self {
330 self.query =
331 self.query
332 .with_aggregation(Aggregation::new(AggregateFunction::Count, field, alias));
333 self
334 }
335
336 pub fn sum(mut self, field: String, alias: String) -> Self {
338 self.query =
339 self.query
340 .with_aggregation(Aggregation::new(AggregateFunction::Sum, field, alias));
341 self
342 }
343
344 pub fn avg(mut self, field: String, alias: String) -> Self {
346 self.query =
347 self.query
348 .with_aggregation(Aggregation::new(AggregateFunction::Avg, field, alias));
349 self
350 }
351
352 pub fn min(mut self, field: String, alias: String) -> Self {
354 self.query =
355 self.query
356 .with_aggregation(Aggregation::new(AggregateFunction::Min, field, alias));
357 self
358 }
359
360 pub fn max(mut self, field: String, alias: String) -> Self {
362 self.query =
363 self.query
364 .with_aggregation(Aggregation::new(AggregateFunction::Max, field, alias));
365 self
366 }
367
368 pub fn group_by(mut self, fields: Vec<String>) -> Self {
370 self.query = self.query.with_group_by(GroupBy::new(fields));
371 self
372 }
373
374 pub fn having(
376 mut self,
377 aggregation: Aggregation,
378 operator: String,
379 value: serde_json::Value,
380 ) -> Self {
381 self.query = self
382 .query
383 .with_having(HavingCondition::new(aggregation, operator, value));
384 self
385 }
386
387 pub fn select(mut self, field: String) -> Self {
389 self.query = self.query.with_select_field(field);
390 self
391 }
392
393 pub fn build(self) -> AggregationQuery {
395 self.query
396 }
397}
398
399impl Default for AggregationQueryBuilder {
400 fn default() -> Self {
401 Self::new()
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_aggregate_function_display() {
411 assert_eq!(AggregateFunction::Count.to_string(), "COUNT");
412 assert_eq!(AggregateFunction::Sum.to_string(), "SUM");
413 assert_eq!(AggregateFunction::Avg.to_string(), "AVG");
414 assert_eq!(AggregateFunction::Min.to_string(), "MIN");
415 assert_eq!(AggregateFunction::Max.to_string(), "MAX");
416 }
417
418 #[test]
419 fn test_aggregation_count() {
420 let agg = Aggregation::new(
421 AggregateFunction::Count,
422 "name".to_string(),
423 "total".to_string(),
424 );
425
426 let sparql = agg.to_sparql_expression();
427 assert!(sparql.contains("COUNT"));
428 assert!(sparql.contains("?name"));
429 assert!(sparql.contains("?total"));
430 }
431
432 #[test]
433 fn test_aggregation_sum() {
434 let agg = Aggregation::new(
435 AggregateFunction::Sum,
436 "price".to_string(),
437 "total_price".to_string(),
438 );
439
440 let sparql = agg.to_sparql_expression();
441 assert!(sparql.contains("SUM"));
442 assert!(sparql.contains("?price"));
443 assert!(sparql.contains("?total_price"));
444 }
445
446 #[test]
447 fn test_aggregation_with_distinct() {
448 let agg = Aggregation::new(
449 AggregateFunction::Count,
450 "category".to_string(),
451 "unique_categories".to_string(),
452 )
453 .with_distinct();
454
455 let sparql = agg.to_sparql_expression();
456 assert!(sparql.contains("DISTINCT"));
457 }
458
459 #[test]
460 fn test_group_by() {
461 let group_by = GroupBy::new(vec!["category".to_string(), "brand".to_string()]);
462
463 let sparql = group_by.to_sparql_clause();
464 assert_eq!(sparql, "GROUP BY ?category ?brand");
465 }
466
467 #[test]
468 fn test_aggregation_query_select() {
469 let query = AggregationQuery::new()
470 .with_select_field("category".to_string())
471 .with_aggregation(Aggregation::new(
472 AggregateFunction::Count,
473 "id".to_string(),
474 "count".to_string(),
475 ))
476 .with_aggregation(Aggregation::new(
477 AggregateFunction::Sum,
478 "price".to_string(),
479 "total".to_string(),
480 ));
481
482 let select = query.to_sparql_select();
483 assert!(select.contains("SELECT"));
484 assert!(select.contains("?category"));
485 assert!(select.contains("COUNT"));
486 assert!(select.contains("SUM"));
487 }
488
489 #[test]
490 fn test_aggregation_query_full() {
491 let query = AggregationQuery::new()
492 .with_select_field("category".to_string())
493 .with_aggregation(Aggregation::new(
494 AggregateFunction::Count,
495 "id".to_string(),
496 "count".to_string(),
497 ))
498 .with_group_by(GroupBy::new(vec!["category".to_string()]));
499
500 let where_clause = "?s rdf:type ?category . ?s ex:id ?id .";
501 let sparql = query.to_sparql_query(where_clause).expect("should succeed");
502
503 assert!(sparql.contains("SELECT"));
504 assert!(sparql.contains("WHERE"));
505 assert!(sparql.contains("GROUP BY"));
506 assert!(sparql.contains("?category"));
507 }
508
509 #[test]
510 fn test_aggregation_query_builder() {
511 let query = AggregationQueryBuilder::new()
512 .select("category".to_string())
513 .count("id".to_string(), "total".to_string())
514 .sum("price".to_string(), "total_price".to_string())
515 .group_by(vec!["category".to_string()])
516 .build();
517
518 assert_eq!(query.select_fields.len(), 1);
519 assert_eq!(query.aggregations.len(), 2);
520 assert!(query.group_by.is_some());
521 }
522
523 #[test]
524 fn test_aggregation_result() {
525 let mut result = AggregationResult::new();
526 result.add_group_value("category".to_string(), serde_json::json!("Books"));
527 result.add_aggregate("count".to_string(), serde_json::json!(42));
528 result.add_aggregate("total_price".to_string(), serde_json::json!(199.99));
529
530 assert_eq!(result.group_values.len(), 1);
531 assert_eq!(result.aggregates.len(), 2);
532 }
533
534 #[test]
535 fn test_having_condition() {
536 let agg = Aggregation::new(
537 AggregateFunction::Count,
538 "id".to_string(),
539 "count".to_string(),
540 );
541 let having = HavingCondition::new(agg, ">".to_string(), serde_json::json!(10));
542
543 let clause = having.to_sparql_clause().expect("should succeed");
544 assert!(clause.contains(">"));
545 assert!(clause.contains("10"));
546 }
547
548 #[test]
549 fn test_group_concat_with_separator() {
550 let agg = Aggregation::new(
551 AggregateFunction::GroupConcat,
552 "name".to_string(),
553 "names".to_string(),
554 )
555 .with_separator(", ".to_string());
556
557 let sparql = agg.to_sparql_expression();
558 assert!(sparql.contains("GROUP_CONCAT"));
559 assert!(sparql.contains("SEPARATOR"));
560 assert!(sparql.contains(", "));
561 }
562
563 #[test]
564 fn test_sample_aggregation() {
565 let agg = Aggregation::new(
566 AggregateFunction::Sample,
567 "value".to_string(),
568 "sample_value".to_string(),
569 );
570
571 let sparql = agg.to_sparql_expression();
572 assert!(sparql.contains("SAMPLE"));
573 assert!(sparql.contains("?value"));
574 }
575}