1use std::collections::HashMap;
29use std::io::{BufRead, BufReader};
30use std::path::Path;
31
32use crate::error::{IoError, Result};
33
34#[derive(Debug, Clone, PartialEq)]
40pub enum ColumnValue {
41 Null,
43 Integer(i64),
45 Float(f64),
47 Boolean(bool),
49 Text(String),
51}
52
53impl ColumnValue {
54 pub fn as_f64(&self) -> Option<f64> {
56 match self {
57 ColumnValue::Integer(i) => Some(*i as f64),
58 ColumnValue::Float(f) => Some(*f),
59 ColumnValue::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
60 _ => None,
61 }
62 }
63
64 fn sort_key(&self) -> String {
66 match self {
67 ColumnValue::Null => "\x00".to_string(),
68 ColumnValue::Integer(i) => format!("{:020}", i),
69 ColumnValue::Float(f) => format!("{:030.15}", f),
70 ColumnValue::Boolean(b) => if *b { "1" } else { "0" }.to_string(),
71 ColumnValue::Text(s) => s.clone(),
72 }
73 }
74}
75
76impl std::fmt::Display for ColumnValue {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 ColumnValue::Null => write!(f, "NULL"),
80 ColumnValue::Integer(i) => write!(f, "{}", i),
81 ColumnValue::Float(v) => write!(f, "{}", v),
82 ColumnValue::Boolean(b) => write!(f, "{}", b),
83 ColumnValue::Text(s) => write!(f, "{}", s),
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
97pub struct Row {
98 pub columns: Vec<String>,
100 pub values: Vec<ColumnValue>,
102}
103
104impl Row {
105 pub fn from_pairs(pairs: Vec<(String, ColumnValue)>) -> Self {
107 let mut columns = Vec::with_capacity(pairs.len());
108 let mut values = Vec::with_capacity(pairs.len());
109 for (k, v) in pairs {
110 columns.push(k);
111 values.push(v);
112 }
113 Row { columns, values }
114 }
115
116 pub fn get(&self, column: &str) -> Option<&ColumnValue> {
118 self.columns
119 .iter()
120 .position(|c| c == column)
121 .map(|idx| &self.values[idx])
122 }
123
124 pub fn is_empty(&self) -> bool {
126 self.columns.is_empty()
127 }
128
129 pub fn len(&self) -> usize {
131 self.columns.len()
132 }
133
134 pub fn iter(&self) -> impl Iterator<Item = (&str, &ColumnValue)> {
136 self.columns
137 .iter()
138 .map(|s| s.as_str())
139 .zip(self.values.iter())
140 }
141
142 fn project(&self, cols: &[String]) -> Row {
145 let values = cols
146 .iter()
147 .map(|c| self.get(c).cloned().unwrap_or(ColumnValue::Null))
148 .collect();
149 Row {
150 columns: cols.to_vec(),
151 values,
152 }
153 }
154}
155
156pub enum DataSource {
162 CsvFile(String),
164 InMemoryRows(Vec<Vec<(String, ColumnValue)>>),
166 Rows(Vec<Row>),
168}
169
170#[derive(Debug, Clone)]
176pub struct QueryResult {
177 pub rows: Vec<Row>,
179 pub columns: Vec<String>,
181 pub n_rows: usize,
183}
184
185impl QueryResult {
186 pub fn iter(&self) -> impl Iterator<Item = &Row> {
188 self.rows.iter()
189 }
190}
191
192#[derive(Debug, Clone)]
198pub struct GroupedResult {
199 pub groups: HashMap<String, Vec<Row>>,
201 pub group_column: String,
203}
204
205impl GroupedResult {
206 pub fn count(&self) -> HashMap<String, usize> {
208 self.groups
209 .iter()
210 .map(|(k, v)| (k.clone(), v.len()))
211 .collect()
212 }
213
214 pub fn sum(&self, column: &str) -> HashMap<String, f64> {
216 self.groups
217 .iter()
218 .map(|(k, rows)| {
219 let s = rows
220 .iter()
221 .filter_map(|r| r.get(column).and_then(|v| v.as_f64()))
222 .sum();
223 (k.clone(), s)
224 })
225 .collect()
226 }
227
228 pub fn mean(&self, column: &str) -> HashMap<String, f64> {
230 self.groups
231 .iter()
232 .map(|(k, rows)| {
233 let vals: Vec<f64> = rows
234 .iter()
235 .filter_map(|r| r.get(column).and_then(|v| v.as_f64()))
236 .collect();
237 let mean = if vals.is_empty() {
238 0.0
239 } else {
240 vals.iter().sum::<f64>() / vals.len() as f64
241 };
242 (k.clone(), mean)
243 })
244 .collect()
245 }
246
247 pub fn min(&self, column: &str) -> HashMap<String, f64> {
249 self.groups
250 .iter()
251 .map(|(k, rows)| {
252 let min = rows
253 .iter()
254 .filter_map(|r| r.get(column).and_then(|v| v.as_f64()))
255 .fold(f64::INFINITY, f64::min);
256 (k.clone(), min)
257 })
258 .collect()
259 }
260
261 pub fn max(&self, column: &str) -> HashMap<String, f64> {
263 self.groups
264 .iter()
265 .map(|(k, rows)| {
266 let max = rows
267 .iter()
268 .filter_map(|r| r.get(column).and_then(|v| v.as_f64()))
269 .fold(f64::NEG_INFINITY, f64::max);
270 (k.clone(), max)
271 })
272 .collect()
273 }
274}
275
276pub struct Aggregations<'a> {
282 result: &'a QueryResult,
283}
284
285impl<'a> Aggregations<'a> {
286 pub fn count(&self) -> usize {
288 self.result.n_rows
289 }
290
291 pub fn sum(&self, column: &str) -> f64 {
293 self.result
294 .rows
295 .iter()
296 .filter_map(|r| r.get(column).and_then(|v| v.as_f64()))
297 .sum()
298 }
299
300 pub fn mean(&self, column: &str) -> f64 {
302 let vals: Vec<f64> = self
303 .result
304 .rows
305 .iter()
306 .filter_map(|r| r.get(column).and_then(|v| v.as_f64()))
307 .collect();
308 if vals.is_empty() {
309 0.0
310 } else {
311 vals.iter().sum::<f64>() / vals.len() as f64
312 }
313 }
314
315 pub fn min(&self, column: &str) -> f64 {
317 self.result
318 .rows
319 .iter()
320 .filter_map(|r| r.get(column).and_then(|v| v.as_f64()))
321 .fold(f64::INFINITY, f64::min)
322 }
323
324 pub fn max(&self, column: &str) -> f64 {
326 self.result
327 .rows
328 .iter()
329 .filter_map(|r| r.get(column).and_then(|v| v.as_f64()))
330 .fold(f64::NEG_INFINITY, f64::max)
331 }
332}
333
334impl QueryResult {
335 pub fn agg(&self) -> Aggregations<'_> {
337 Aggregations { result: self }
338 }
339
340 pub fn group_by(self, column: &str) -> GroupedResult {
342 let mut groups: HashMap<String, Vec<Row>> = HashMap::new();
343 let col_owned = column.to_string();
344 for row in self.rows {
345 let key = row
346 .get(column)
347 .map(|v| v.to_string())
348 .unwrap_or_else(|| "NULL".to_string());
349 groups.entry(key).or_default().push(row);
350 }
351 GroupedResult {
352 groups,
353 group_column: col_owned,
354 }
355 }
356}
357
358struct OrderSpec {
364 column: String,
365 ascending: bool,
366}
367
368pub struct DataQuery {
375 source: DataSource,
376 select_cols: Option<Vec<String>>,
377 predicates: Vec<Box<dyn Fn(&Row) -> bool + Send + Sync>>,
378 order: Option<OrderSpec>,
379 limit: Option<usize>,
380}
381
382impl DataQuery {
383 pub fn from(source: DataSource) -> Self {
385 DataQuery {
386 source,
387 select_cols: None,
388 predicates: Vec::new(),
389 order: None,
390 limit: None,
391 }
392 }
393
394 pub fn select(mut self, columns: &[&str]) -> Self {
396 if !columns.is_empty() {
397 self.select_cols = Some(columns.iter().map(|s| s.to_string()).collect());
398 }
399 self
400 }
401
402 pub fn filter(mut self, predicate: impl Fn(&Row) -> bool + Send + Sync + 'static) -> Self {
404 self.predicates.push(Box::new(predicate));
405 self
406 }
407
408 pub fn limit(mut self, n: usize) -> Self {
410 self.limit = Some(n);
411 self
412 }
413
414 pub fn order_by(mut self, column: &str, ascending: bool) -> Self {
416 self.order = Some(OrderSpec {
417 column: column.to_string(),
418 ascending,
419 });
420 self
421 }
422
423 pub fn execute(self) -> Result<QueryResult> {
425 let DataQuery {
426 source,
427 select_cols,
428 predicates,
429 order,
430 limit,
431 } = self;
432
433 let mut rows = load_rows(source)?;
435
436 rows.retain(|row| predicates.iter().all(|p| p(row)));
438
439 if let Some(ord) = order {
441 let col = ord.column.clone();
442 let asc = ord.ascending;
443 rows.sort_by(|a, b| {
444 let ka = a.get(&col).map(|v| v.sort_key()).unwrap_or_default();
445 let kb = b.get(&col).map(|v| v.sort_key()).unwrap_or_default();
446 if asc {
447 ka.cmp(&kb)
448 } else {
449 kb.cmp(&ka)
450 }
451 });
452 }
453
454 if let Some(n) = limit {
456 rows.truncate(n);
457 }
458
459 let columns: Vec<String> = match &select_cols {
461 Some(cols) => cols.clone(),
462 None => {
463 let mut seen = std::collections::LinkedList::new();
465 let mut seen_set = std::collections::HashSet::new();
466 for row in &rows {
467 for col in &row.columns {
468 if seen_set.insert(col.clone()) {
469 seen.push_back(col.clone());
470 }
471 }
472 }
473 seen.into_iter().collect()
474 }
475 };
476
477 let projected: Vec<Row> = rows.iter().map(|r| r.project(&columns)).collect();
478 let n_rows = projected.len();
479
480 Ok(QueryResult {
481 rows: projected,
482 columns,
483 n_rows,
484 })
485 }
486}
487
488fn parse_cell(s: &str) -> ColumnValue {
494 let trimmed = s.trim();
495 if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("null") || trimmed == "NA" {
496 return ColumnValue::Null;
497 }
498 if let Ok(i) = trimmed.parse::<i64>() {
499 return ColumnValue::Integer(i);
500 }
501 if let Ok(f) = trimmed.parse::<f64>() {
502 return ColumnValue::Float(f);
503 }
504 if trimmed.eq_ignore_ascii_case("true") {
505 return ColumnValue::Boolean(true);
506 }
507 if trimmed.eq_ignore_ascii_case("false") {
508 return ColumnValue::Boolean(false);
509 }
510 ColumnValue::Text(trimmed.to_string())
511}
512
513fn split_csv_line(line: &str) -> Vec<String> {
515 let mut fields = Vec::new();
516 let mut cur = String::new();
517 let mut in_quotes = false;
518 let mut chars = line.chars().peekable();
519
520 while let Some(ch) = chars.next() {
521 match ch {
522 '"' => {
523 if in_quotes {
524 if chars.peek() == Some(&'"') {
526 chars.next();
527 cur.push('"');
528 } else {
529 in_quotes = false;
530 }
531 } else {
532 in_quotes = true;
533 }
534 }
535 ',' if !in_quotes => {
536 fields.push(cur.clone());
537 cur.clear();
538 }
539 _ => cur.push(ch),
540 }
541 }
542 fields.push(cur);
543 fields
544}
545
546fn load_rows(source: DataSource) -> Result<Vec<Row>> {
548 match source {
549 DataSource::Rows(rows) => Ok(rows),
550
551 DataSource::InMemoryRows(pairs_list) => {
552 Ok(pairs_list.into_iter().map(Row::from_pairs).collect())
553 }
554
555 DataSource::CsvFile(path) => {
556 let file = std::fs::File::open(Path::new(&path))
557 .map_err(|e| IoError::FileError(format!("cannot open '{}': {}", path, e)))?;
558 let reader = BufReader::new(file);
559 let mut lines = reader.lines();
560
561 let header_line = lines
563 .next()
564 .ok_or_else(|| IoError::FormatError("CSV file is empty".to_string()))?
565 .map_err(|e| IoError::Io(e))?;
566 let headers: Vec<String> = split_csv_line(&header_line)
567 .into_iter()
568 .map(|s| s.trim().to_string())
569 .collect();
570
571 let mut rows = Vec::new();
572 for line_result in lines {
573 let line = line_result.map_err(|e| IoError::Io(e))?;
574 if line.trim().is_empty() {
575 continue;
576 }
577 let cells = split_csv_line(&line);
578 let pairs: Vec<(String, ColumnValue)> = headers
579 .iter()
580 .enumerate()
581 .map(|(i, h)| {
582 let val = cells
583 .get(i)
584 .map(|s| parse_cell(s))
585 .unwrap_or(ColumnValue::Null);
586 (h.clone(), val)
587 })
588 .collect();
589 rows.push(Row::from_pairs(pairs));
590 }
591 Ok(rows)
592 }
593 }
594}
595
596pub fn execute(query: DataQuery) -> Result<QueryResult> {
604 query.execute()
605}
606
607#[cfg(test)]
612mod tests {
613 use super::*;
614
615 fn sample_rows() -> Vec<Row> {
616 vec![
617 Row::from_pairs(vec![
618 ("name".to_string(), ColumnValue::Text("Alice".to_string())),
619 ("age".to_string(), ColumnValue::Integer(30)),
620 ("score".to_string(), ColumnValue::Float(95.5)),
621 ("dept".to_string(), ColumnValue::Text("eng".to_string())),
622 ]),
623 Row::from_pairs(vec![
624 ("name".to_string(), ColumnValue::Text("Bob".to_string())),
625 ("age".to_string(), ColumnValue::Integer(25)),
626 ("score".to_string(), ColumnValue::Float(80.0)),
627 ("dept".to_string(), ColumnValue::Text("hr".to_string())),
628 ]),
629 Row::from_pairs(vec![
630 ("name".to_string(), ColumnValue::Text("Carol".to_string())),
631 ("age".to_string(), ColumnValue::Integer(35)),
632 ("score".to_string(), ColumnValue::Float(88.0)),
633 ("dept".to_string(), ColumnValue::Text("eng".to_string())),
634 ]),
635 ]
636 }
637
638 #[test]
639 fn test_select_and_count() {
640 let result = DataQuery::from(DataSource::Rows(sample_rows()))
641 .select(&["name", "age"])
642 .execute()
643 .expect("execute");
644 assert_eq!(result.n_rows, 3);
645 assert_eq!(result.columns, vec!["name", "age"]);
646 assert!(result.rows[0]
648 .get("score")
649 .map(|v| matches!(v, ColumnValue::Null))
650 .unwrap_or(true));
651 }
652
653 #[test]
654 fn test_filter() {
655 let result = DataQuery::from(DataSource::Rows(sample_rows()))
656 .filter(|row| matches!(row.get("age"), Some(ColumnValue::Integer(a)) if *a > 28))
657 .execute()
658 .expect("execute");
659 assert_eq!(result.n_rows, 2); }
661
662 #[test]
663 fn test_order_by_ascending() {
664 let result = DataQuery::from(DataSource::Rows(sample_rows()))
665 .order_by("age", true)
666 .execute()
667 .expect("execute");
668 let ages: Vec<i64> = result
669 .rows
670 .iter()
671 .filter_map(|r| {
672 if let Some(ColumnValue::Integer(a)) = r.get("age") {
673 Some(*a)
674 } else {
675 None
676 }
677 })
678 .collect();
679 assert_eq!(ages, vec![25, 30, 35]);
680 }
681
682 #[test]
683 fn test_order_by_descending() {
684 let result = DataQuery::from(DataSource::Rows(sample_rows()))
685 .order_by("age", false)
686 .execute()
687 .expect("execute");
688 let ages: Vec<i64> = result
689 .rows
690 .iter()
691 .filter_map(|r| {
692 if let Some(ColumnValue::Integer(a)) = r.get("age") {
693 Some(*a)
694 } else {
695 None
696 }
697 })
698 .collect();
699 assert_eq!(ages, vec![35, 30, 25]);
700 }
701
702 #[test]
703 fn test_limit() {
704 let result = DataQuery::from(DataSource::Rows(sample_rows()))
705 .limit(2)
706 .execute()
707 .expect("execute");
708 assert_eq!(result.n_rows, 2);
709 }
710
711 #[test]
712 fn test_aggregations() {
713 let result = DataQuery::from(DataSource::Rows(sample_rows()))
714 .execute()
715 .expect("execute");
716 let agg = result.agg();
717 assert_eq!(agg.count(), 3);
718 assert!((agg.sum("age") - 90.0).abs() < 1e-9);
719 assert!((agg.mean("age") - 30.0).abs() < 1e-9);
720 assert!((agg.min("age") - 25.0).abs() < 1e-9);
721 assert!((agg.max("age") - 35.0).abs() < 1e-9);
722 }
723
724 #[test]
725 fn test_group_by() {
726 let result = DataQuery::from(DataSource::Rows(sample_rows()))
727 .execute()
728 .expect("execute");
729 let grouped = result.group_by("dept");
730 let counts = grouped.count();
731 assert_eq!(*counts.get("eng").unwrap_or(&0), 2);
732 assert_eq!(*counts.get("hr").unwrap_or(&0), 1);
733 }
734
735 #[test]
736 fn test_group_by_sum() {
737 let result = DataQuery::from(DataSource::Rows(sample_rows()))
738 .execute()
739 .expect("execute");
740 let grouped = result.group_by("dept");
741 let sums = grouped.sum("age");
742 assert!((sums.get("eng").copied().unwrap_or(0.0) - 65.0).abs() < 1e-9);
744 }
745
746 #[test]
747 fn test_csv_round_trip() {
748 use std::io::Write;
749
750 let dir = std::env::temp_dir();
751 let path = dir.join("test_query_csv.csv");
752 {
753 let mut f = std::fs::File::create(&path).expect("create csv");
754 writeln!(f, "id,val").expect("write header");
755 writeln!(f, "1,10.5").expect("write row 1");
756 writeln!(f, "2,20.0").expect("write row 2");
757 }
758
759 let result = DataQuery::from(DataSource::CsvFile(
760 path.to_str().expect("path").to_string(),
761 ))
762 .execute()
763 .expect("execute");
764
765 assert_eq!(result.n_rows, 2);
766 assert!((result.agg().sum("val") - 30.5).abs() < 1e-9);
767
768 let _ = std::fs::remove_file(&path);
770 }
771
772 #[test]
773 fn test_in_memory_rows_source() {
774 let pairs = vec![
775 vec![
776 ("x".to_string(), ColumnValue::Integer(1)),
777 ("y".to_string(), ColumnValue::Float(1.1)),
778 ],
779 vec![
780 ("x".to_string(), ColumnValue::Integer(2)),
781 ("y".to_string(), ColumnValue::Float(2.2)),
782 ],
783 ];
784 let result = DataQuery::from(DataSource::InMemoryRows(pairs))
785 .execute()
786 .expect("execute");
787 assert_eq!(result.n_rows, 2);
788 }
789
790 #[test]
791 fn test_combined_filter_order_limit() {
792 let result = DataQuery::from(DataSource::Rows(sample_rows()))
793 .filter(|row| matches!(row.get("dept"), Some(ColumnValue::Text(d)) if d == "eng"))
794 .order_by("age", true)
795 .limit(1)
796 .execute()
797 .expect("execute");
798 assert_eq!(result.n_rows, 1);
800 assert_eq!(
801 result.rows[0].get("name"),
802 Some(&ColumnValue::Text("Alice".to_string()))
803 );
804 }
805}