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