Skip to main content

vantage_csv/
csv.rs

1use std::path::PathBuf;
2
3use indexmap::IndexMap;
4use vantage_core::error;
5use vantage_dataset::traits::Result;
6use vantage_table::column::core::Column;
7use vantage_table::traits::column_like::ColumnLike;
8use vantage_types::Record;
9
10use crate::type_system::{AnyCsvType, CsvTypeVariants, parse_with_type, variant_from_type_name};
11
12/// CSV backend for Vantage — reads data from CSV files.
13///
14/// Each table maps to a CSV file: `{base_dir}/{table_name}.csv`.
15/// CSV is a read-only data source — write operations return errors.
16#[derive(Clone, Debug)]
17pub struct Csv {
18    base_dir: PathBuf,
19    pub(crate) id_column: String,
20}
21
22impl Csv {
23    /// Create a new CSV data source reading files from `base_dir`.
24    pub fn new(base_dir: impl Into<PathBuf>) -> Self {
25        Self {
26            base_dir: base_dir.into(),
27            id_column: "id".to_string(),
28        }
29    }
30
31    /// Set which CSV column to use as the record ID.
32    pub fn with_id_column(mut self, column: impl Into<String>) -> Self {
33        self.id_column = column.into();
34        self
35    }
36
37    fn file_path(&self, table_name: &str) -> PathBuf {
38        self.base_dir.join(format!("{}.csv", table_name))
39    }
40
41    #[cfg(feature = "vista")]
42    pub(crate) fn base_dir(&self) -> &std::path::Path {
43        &self.base_dir
44    }
45
46    /// Parse a CSV file and return all rows as records.
47    ///
48    /// Behavior under aliasing: a column defined with [`Column::with_alias`]
49    /// reads from the CSV header named by its alias and stores under the
50    /// column name. Headers without a corresponding column are passed
51    /// through untyped (string) under their raw header name — so a table
52    /// with zero declared columns still surfaces every field of the file.
53    pub(crate) fn read_csv(
54        &self,
55        table_name: &str,
56        columns: &IndexMap<String, Column<AnyCsvType>>,
57    ) -> Result<IndexMap<String, Record<AnyCsvType>>> {
58        let path = self.file_path(table_name);
59        let mut reader = csv::Reader::from_path(&path)
60            .map_err(|e| error!("Failed to open CSV file", path = path.display(), detail = e))?;
61
62        let headers = reader
63            .headers()
64            .map_err(|e| error!("Failed to read CSV headers", detail = e))?
65            .clone();
66
67        let mut key_for: Vec<String> = headers.iter().map(|h| h.to_string()).collect();
68        let mut variant_for: Vec<Option<CsvTypeVariants>> = vec![None; headers.len()];
69        for (name, col) in columns {
70            let csv_header = col.alias().unwrap_or(name);
71            if let Some(idx) = headers.iter().position(|h| h == csv_header) {
72                key_for[idx] = name.clone();
73                variant_for[idx] = variant_from_type_name(col.get_type());
74            }
75        }
76
77        // id column may itself carry an alias.
78        let id_header = columns
79            .get(&self.id_column)
80            .and_then(|c| c.alias())
81            .unwrap_or(&self.id_column);
82        let id_col_index = headers.iter().position(|h| h == id_header);
83
84        let mut records = IndexMap::new();
85        for (row_idx, result) in reader.records().enumerate() {
86            let csv_record = result.map_err(|e| error!("Failed to read CSV row", detail = e))?;
87
88            let id = if let Some(idx) = id_col_index {
89                csv_record.get(idx).unwrap_or_default().to_string()
90            } else {
91                row_idx.to_string()
92            };
93
94            let mut record = Record::new();
95            for (i, field) in csv_record.iter().enumerate() {
96                if let Some(key) = key_for.get(i) {
97                    let variant = variant_for.get(i).copied().flatten();
98                    record.insert(key.clone(), parse_with_type(field, variant));
99                }
100            }
101
102            records.insert(id, record);
103        }
104
105        Ok(records)
106    }
107}