csvsql 0.1.4

SQL like engine that works on CSV file.
Documentation
use std::rc::Rc;

use csv::ReaderBuilder;
use sqlparser::ast::ObjectName;

use crate::engine::Engine;
use crate::error::CvsSqlError;
use crate::result_set_metadata::SimpleResultSetMetadata;
use crate::results_data::{DataRow, ResultsData};
use crate::{results::ResultSet, value::Value};

fn get_default_header(index: usize) -> String {
    let mut index = index;
    let mut title = String::from("$");
    let first = 'A' as usize;
    let size = 'Z' as usize - first + 1;
    loop {
        let chr = index % (size);
        index -= chr;
        title.insert(0, char::from((chr + first) as u8));
        if index == 0 {
            break;
        }
        index = index / size - 1;
    }
    title
}

pub fn read_file(engine: &Engine, name: &ObjectName) -> Result<ResultSet, CvsSqlError> {
    let file = engine.file_name(name)?;
    if !file.exists {
        return Err(CvsSqlError::TableNotExists(file.result_name.full_name()));
    }

    let mut reader = ReaderBuilder::new()
        .flexible(true)
        .has_headers(engine.first_line_as_name)
        .from_path(file.path)?;

    let mut metadata = SimpleResultSetMetadata::new(Some(file.result_name));

    if engine.first_line_as_name {
        let header = reader.headers()?;
        for h in header {
            metadata.add_column(h);
        }
    }
    let mut data = Vec::new();
    for records in reader.records() {
        let mut values = Vec::new();
        let records = records?;
        for (index, record) in records.iter().enumerate() {
            let value = Value::from(record);
            values.push(value);
            if index >= metadata.len() {
                metadata.add_column(&get_default_header(index));
            }
        }
        let values = DataRow::new(values);
        data.push(values);
    }
    let metadata = Rc::new(metadata.build());
    let data = ResultsData::new(data);
    let results = ResultSet { metadata, data };

    Ok(results)
}
#[cfg(test)]
mod tests {
    use std::fs::{self, File};
    use std::io::Write;

    use tempfile::tempdir;

    use crate::{args::Args, engine::Engine, error::CvsSqlError, results::Column};

    #[test]
    fn read_file_with_missing_headers() -> Result<(), CvsSqlError> {
        let working_dir = tempdir()?;
        fs::create_dir_all(&working_dir)?;
        let table = working_dir.path().join("tab.csv");
        fs::write(table, "col1\n1,2\n2\n")?;

        let args = Args {
            first_line_as_data: false,
            home: Some(working_dir.path().to_path_buf()),
            ..Args::default()
        };
        let engine = Engine::try_from(&args)?;

        let results = engine.execute_commands("SELECT * FROM tab")?;
        assert_eq!(results.len(), 1);
        let results = &results.first().unwrap().results;
        assert_eq!(results.metadata.number_of_columns(), 2);

        assert_eq!(
            results.metadata.column_title(&Column::from_index(0)),
            "col1"
        );
        assert_eq!(results.metadata.column_title(&Column::from_index(1)), "B$");
        assert_eq!(results.data.iter().count(), 2);

        let results = engine.execute_commands("SELECT col1, B$ FROM tab")?;
        assert_eq!(results.len(), 1);
        let results = &results.first().unwrap().results;
        assert_eq!(results.data.iter().count(), 2);

        Ok(())
    }

    #[test]
    fn read_file_no_headers() -> Result<(), CvsSqlError> {
        let working_dir = tempdir()?;
        fs::create_dir_all(&working_dir)?;
        let table = working_dir.path().join("tab.csv");
        fs::write(table, "col1\n1,2\n2\n")?;

        let args = Args {
            first_line_as_data: true,
            home: Some(working_dir.path().to_path_buf()),
            ..Args::default()
        };
        let engine = Engine::try_from(&args)?;

        let results = engine.execute_commands("SELECT * FROM tab")?;
        assert_eq!(results.len(), 1);
        let results = &results.first().unwrap().results;
        assert_eq!(results.metadata.number_of_columns(), 2);

        assert_eq!(results.metadata.column_title(&Column::from_index(0)), "A$");
        assert_eq!(results.metadata.column_title(&Column::from_index(1)), "B$");
        assert_eq!(results.data.iter().count(), 3);

        let results = engine.execute_commands("SELECT A$, B$ FROM tab")?;
        assert_eq!(results.len(), 1);
        let results = &results.first().unwrap().results;
        assert_eq!(results.data.iter().count(), 3);

        Ok(())
    }

    #[test]
    fn read_no_headers_large_file() -> Result<(), CvsSqlError> {
        let working_dir = tempdir()?;
        fs::create_dir_all(&working_dir)?;
        let table = working_dir.path().join("tab.csv");
        let mut table = File::create(table)?;
        for i in 0..60 {
            write!(&mut table, "col{},", i + 1)?;
        }
        writeln!(&mut table, "last")?;

        let args = Args {
            first_line_as_data: true,
            home: Some(working_dir.path().to_path_buf()),
            ..Args::default()
        };
        let engine = Engine::try_from(&args)?;

        let results = engine.execute_commands("SELECT * FROM tab")?;
        assert_eq!(results.len(), 1);
        let results = &results.first().unwrap().results;
        assert_eq!(results.metadata.number_of_columns(), 61);

        assert_eq!(results.metadata.column_title(&Column::from_index(0)), "A$");
        assert_eq!(results.metadata.column_title(&Column::from_index(1)), "B$");
        assert_eq!(results.metadata.column_title(&Column::from_index(25)), "Z$");
        assert_eq!(
            results.metadata.column_title(&Column::from_index(26)),
            "AA$"
        );
        assert_eq!(
            results.metadata.column_title(&Column::from_index(27)),
            "AB$"
        );
        assert_eq!(
            results.metadata.column_title(&Column::from_index(28)),
            "AC$"
        );
        assert_eq!(
            results.metadata.column_title(&Column::from_index(51)),
            "AZ$"
        );
        assert_eq!(
            results.metadata.column_title(&Column::from_index(54)),
            "BC$"
        );

        Ok(())
    }
}