1use std::collections::HashMap;
40
41use serde_json::{Value, json};
42
43#[allow(unused_imports)] use crate::error::FraiseQLError;
45use crate::{compiler::aggregation::AggregationPlan, error::Result};
46
47pub struct AggregationProjector;
49
50impl AggregationProjector {
51 pub fn project(rows: Vec<HashMap<String, Value>>, _plan: &AggregationPlan) -> Result<Value> {
89 let projected_rows: Vec<Value> = rows
97 .into_iter()
98 .map(|row| {
99 let mut obj = serde_json::Map::new();
101 for (key, value) in row {
102 obj.insert(key, value);
103 }
104 Value::Object(obj)
105 })
106 .collect();
107
108 Ok(Value::Array(projected_rows))
109 }
110
111 #[allow(clippy::needless_pass_by_value)] pub fn wrap_in_data_envelope(projected: Value, query_name: &str) -> Value {
134 json!({
135 "data": {
136 query_name: projected
137 }
138 })
139 }
140
141 pub fn project_single(row: HashMap<String, Value>, _plan: &AggregationPlan) -> Result<Value> {
165 let mut obj = serde_json::Map::new();
167 for (key, value) in row {
168 obj.insert(key, value);
169 }
170 Ok(Value::Object(obj))
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 #![allow(clippy::unwrap_used)] use super::*;
179 use crate::compiler::{
180 aggregate_types::AggregateFunction,
181 aggregation::{
182 AggregateExpression, AggregateSelection, AggregationRequest, GroupByExpression,
183 GroupBySelection,
184 },
185 fact_table::{DimensionColumn, FactTableMetadata, FilterColumn, MeasureColumn, SqlType},
186 };
187
188 fn create_test_plan() -> AggregationPlan {
189 use crate::compiler::fact_table::DimensionPath;
190
191 let metadata = FactTableMetadata {
192 table_name: "tf_sales".to_string(),
193 measures: vec![MeasureColumn {
194 name: "revenue".to_string(),
195 sql_type: SqlType::Decimal,
196 nullable: false,
197 }],
198 dimensions: DimensionColumn {
199 name: "dimensions".to_string(),
200 paths: vec![DimensionPath {
201 name: "category".to_string(),
202 json_path: "data->>'category'".to_string(),
203 data_type: "text".to_string(),
204 }],
205 },
206 denormalized_filters: vec![FilterColumn {
207 name: "occurred_at".to_string(),
208 sql_type: SqlType::Timestamp,
209 indexed: true,
210 }],
211 calendar_dimensions: vec![],
212 };
213
214 let request = AggregationRequest {
215 table_name: "tf_sales".to_string(),
216 where_clause: None,
217 group_by: vec![GroupBySelection::Dimension {
218 path: "category".to_string(),
219 alias: "category".to_string(),
220 }],
221 aggregates: vec![
222 AggregateSelection::Count {
223 alias: "count".to_string(),
224 },
225 AggregateSelection::MeasureAggregate {
226 measure: "revenue".to_string(),
227 function: AggregateFunction::Sum,
228 alias: "revenue_sum".to_string(),
229 },
230 ],
231 having: vec![],
232 order_by: vec![],
233 limit: None,
234 offset: None,
235 };
236
237 AggregationPlan {
238 metadata,
239 request,
240 group_by_expressions: vec![GroupByExpression::JsonbPath {
241 jsonb_column: "data".to_string(),
242 path: "category".to_string(),
243 alias: "category".to_string(),
244 }],
245 aggregate_expressions: vec![
246 AggregateExpression::Count {
247 alias: "count".to_string(),
248 },
249 AggregateExpression::MeasureAggregate {
250 column: "revenue".to_string(),
251 function: AggregateFunction::Sum,
252 alias: "revenue_sum".to_string(),
253 },
254 ],
255 having_conditions: vec![],
256 }
257 }
258
259 #[test]
260 fn test_project_simple_result() {
261 let plan = create_test_plan();
262 let rows = vec![
263 {
264 let mut row = HashMap::new();
265 row.insert("category".to_string(), json!("Electronics"));
266 row.insert("count".to_string(), json!(42));
267 row.insert("revenue_sum".to_string(), json!(5280.50));
268 row
269 },
270 {
271 let mut row = HashMap::new();
272 row.insert("category".to_string(), json!("Books"));
273 row.insert("count".to_string(), json!(15));
274 row.insert("revenue_sum".to_string(), json!(450.25));
275 row
276 },
277 ];
278
279 let result = AggregationProjector::project(rows, &plan).unwrap();
280
281 assert!(result.is_array());
282 let arr = result.as_array().unwrap();
283 assert_eq!(arr.len(), 2);
284
285 assert_eq!(arr[0]["category"], "Electronics");
286 assert_eq!(arr[0]["count"], 42);
287 assert_eq!(arr[0]["revenue_sum"], 5280.50);
288
289 assert_eq!(arr[1]["category"], "Books");
290 assert_eq!(arr[1]["count"], 15);
291 assert_eq!(arr[1]["revenue_sum"], 450.25);
292 }
293
294 #[test]
295 fn test_project_empty_result() {
296 let plan = create_test_plan();
297 let rows = vec![];
298
299 let result = AggregationProjector::project(rows, &plan).unwrap();
300
301 assert!(result.is_array());
302 let arr = result.as_array().unwrap();
303 assert_eq!(arr.len(), 0);
304 }
305
306 #[test]
307 fn test_wrap_in_data_envelope() {
308 let projected = json!([
309 {"category": "Electronics", "count": 42}
310 ]);
311
312 let response = AggregationProjector::wrap_in_data_envelope(projected, "sales_aggregate");
313
314 assert!(response.get("data").is_some());
315 assert!(response["data"].get("sales_aggregate").is_some());
316 assert!(response["data"]["sales_aggregate"].is_array());
317 assert_eq!(response["data"]["sales_aggregate"][0]["category"], "Electronics");
318 }
319
320 #[test]
321 fn test_project_single() {
322 let plan = create_test_plan();
323 let mut row = HashMap::new();
324 row.insert("count".to_string(), json!(100));
325 row.insert("revenue_sum".to_string(), json!(10000.0));
326
327 let result = AggregationProjector::project_single(row, &plan).unwrap();
328
329 assert!(result.is_object());
330 assert_eq!(result["count"], 100);
331 assert_eq!(result["revenue_sum"], 10000.0);
332 }
333
334 #[test]
335 fn test_project_with_temporal_bucket() {
336 let plan = create_test_plan();
337 let rows = vec![{
338 let mut row = HashMap::new();
339 row.insert("category".to_string(), json!("Electronics"));
340 row.insert("occurred_at_day".to_string(), json!("2025-01-01"));
341 row.insert("count".to_string(), json!(25));
342 row.insert("revenue_sum".to_string(), json!(3000.0));
343 row
344 }];
345
346 let result = AggregationProjector::project(rows, &plan).unwrap();
347
348 assert!(result.is_array());
349 let arr = result.as_array().unwrap();
350 assert_eq!(arr[0]["occurred_at_day"], "2025-01-01");
351 }
352
353 #[test]
354 fn test_project_with_null_values() {
355 let plan = create_test_plan();
356 let rows = vec![{
357 let mut row = HashMap::new();
358 row.insert("category".to_string(), Value::Null);
359 row.insert("count".to_string(), json!(10));
360 row.insert("revenue_sum".to_string(), json!(500.0));
361 row
362 }];
363
364 let result = AggregationProjector::project(rows, &plan).unwrap();
365
366 assert!(result.is_array());
367 let arr = result.as_array().unwrap();
368 assert_eq!(arr[0]["category"], Value::Null);
369 assert_eq!(arr[0]["count"], 10);
370 }
371
372 #[test]
377 fn test_project_array_agg_result() {
378 let plan = create_test_plan();
379 let rows = vec![{
380 let mut row = HashMap::new();
381 row.insert("category".to_string(), json!("Electronics"));
382 row.insert("count".to_string(), json!(10));
383 row.insert("products".to_string(), json!(["prod_1", "prod_2", "prod_3"]));
385 row
386 }];
387
388 let result = AggregationProjector::project(rows, &plan).unwrap();
389
390 assert!(result.is_array());
391 let arr = result.as_array().unwrap();
392 assert_eq!(arr[0]["category"], "Electronics");
393 assert_eq!(arr[0]["products"], json!(["prod_1", "prod_2", "prod_3"]));
394 }
395
396 #[test]
397 fn test_project_json_agg_result() {
398 let plan = create_test_plan();
399 let rows = vec![{
400 let mut row = HashMap::new();
401 row.insert("category".to_string(), json!("Electronics"));
402 row.insert("count".to_string(), json!(10));
403 row.insert(
405 "items".to_string(),
406 json!([
407 {"product": "prod_1", "revenue": 1500},
408 {"product": "prod_2", "revenue": 1200}
409 ]),
410 );
411 row
412 }];
413
414 let result = AggregationProjector::project(rows, &plan).unwrap();
415
416 assert!(result.is_array());
417 let arr = result.as_array().unwrap();
418 assert_eq!(arr[0]["category"], "Electronics");
419 assert!(arr[0]["items"].is_array());
420 let items = arr[0]["items"].as_array().unwrap();
421 assert_eq!(items.len(), 2);
422 assert_eq!(items[0]["product"], "prod_1");
423 assert_eq!(items[0]["revenue"], 1500);
424 }
425
426 #[test]
427 fn test_project_string_agg_result() {
428 let plan = create_test_plan();
429 let rows = vec![{
430 let mut row = HashMap::new();
431 row.insert("category".to_string(), json!("Electronics"));
432 row.insert("count".to_string(), json!(10));
433 row.insert("product_names".to_string(), json!("Laptop, Phone, Tablet"));
435 row
436 }];
437
438 let result = AggregationProjector::project(rows, &plan).unwrap();
439
440 assert!(result.is_array());
441 let arr = result.as_array().unwrap();
442 assert_eq!(arr[0]["category"], "Electronics");
443 assert_eq!(arr[0]["product_names"], "Laptop, Phone, Tablet");
444 }
445
446 #[test]
447 fn test_project_bool_agg_result() {
448 let plan = create_test_plan();
449 let rows = vec![{
450 let mut row = HashMap::new();
451 row.insert("category".to_string(), json!("Electronics"));
452 row.insert("count".to_string(), json!(10));
453 row.insert("all_active".to_string(), json!(true));
455 row.insert("any_discounted".to_string(), json!(false));
457 row
458 }];
459
460 let result = AggregationProjector::project(rows, &plan).unwrap();
461
462 assert!(result.is_array());
463 let arr = result.as_array().unwrap();
464 assert_eq!(arr[0]["category"], "Electronics");
465 assert_eq!(arr[0]["all_active"], true);
466 assert_eq!(arr[0]["any_discounted"], false);
467 }
468
469 #[test]
470 fn test_project_mixed_aggregates() {
471 let plan = create_test_plan();
472 let rows = vec![{
473 let mut row = HashMap::new();
474 row.insert("category".to_string(), json!("Electronics"));
475 row.insert("count".to_string(), json!(42));
477 row.insert("revenue_sum".to_string(), json!(5280.50));
478 row.insert("revenue_avg".to_string(), json!(125.73));
479 row.insert("products".to_string(), json!(["prod_1", "prod_2"]));
481 row.insert("product_names".to_string(), json!("Laptop, Phone"));
482 row.insert("all_active".to_string(), json!(true));
483 row
484 }];
485
486 let result = AggregationProjector::project(rows, &plan).unwrap();
487
488 assert!(result.is_array());
489 let arr = result.as_array().unwrap();
490 assert_eq!(arr[0]["count"], 42);
492 assert_eq!(arr[0]["revenue_sum"], 5280.50);
493 assert_eq!(arr[0]["products"], json!(["prod_1", "prod_2"]));
495 assert_eq!(arr[0]["product_names"], "Laptop, Phone");
496 assert_eq!(arr[0]["all_active"], true);
497 }
498
499 #[test]
500 fn test_project_empty_array_agg() {
501 let plan = create_test_plan();
502 let rows = vec![{
503 let mut row = HashMap::new();
504 row.insert("category".to_string(), json!("Empty"));
505 row.insert("count".to_string(), json!(0));
506 row.insert("products".to_string(), Value::Null);
508 row
509 }];
510
511 let result = AggregationProjector::project(rows, &plan).unwrap();
512
513 assert!(result.is_array());
514 let arr = result.as_array().unwrap();
515 assert_eq!(arr[0]["category"], "Empty");
516 assert!(arr[0]["products"].is_null());
517 }
518}