yamlbase 0.7.2

A lightweight SQL server that serves YAML-defined tables over standard SQL protocols
Documentation
use indexmap::IndexMap;
use std::path::Path;
use tracing::{debug, info};

use crate::database::{Column, Database, Table, Value as DbValue};
use crate::yaml::schema::{AuthConfig, SqlType, YamlColumn, YamlDatabase};

pub async fn parse_yaml_database(path: &Path) -> crate::Result<(Database, Option<AuthConfig>)> {
    info!("Parsing YAML database from: {}", path.display());

    let content = tokio::fs::read_to_string(path).await?;
    let yaml_db: YamlDatabase = serde_yaml::from_str(&content)?;

    let auth_config = yaml_db.database.auth.clone();
    let mut database = Database::new(yaml_db.database.name.clone());

    for (table_name, yaml_table) in yaml_db.tables {
        debug!("Parsing table: {}", table_name);

        let mut columns = Vec::new();
        let mut column_map = IndexMap::new();

        for (col_name, type_def) in &yaml_table.columns {
            let yaml_column = YamlColumn::parse(col_name.clone(), type_def)?;
            let sql_type = yaml_column.get_base_type()?;

            let column = Column {
                name: yaml_column.name.clone(),
                sql_type,
                primary_key: yaml_column.is_primary_key,
                nullable: yaml_column.is_nullable,
                unique: yaml_column.is_unique,
                default: yaml_column.default_value.clone(),
                references: yaml_column.references.as_ref().map(|r| (r.table.clone(), r.column.clone())),
            };
            
            if yaml_column.references.is_some() {
                info!("Column '{}' in table '{}' has foreign key reference: {:?}", col_name, table_name, column.references);
            }

            column_map.insert(yaml_column.name.clone(), columns.len());
            columns.push(column);
        }

        let mut table = Table::new(table_name.clone(), columns);

        // Parse and insert data
        for row_data in yaml_table.data {
            let mut row = Vec::new();

            for column in &table.columns {
                let value = if let Some(yaml_value) = row_data.get(&column.name) {
                    parse_value(yaml_value, &column.sql_type)?
                } else if column.nullable {
                    DbValue::Null
                } else if let Some(default) = &column.default {
                    parse_default_value(default, &column.sql_type)?
                } else {
                    return Err(crate::YamlBaseError::Database {
                        message: format!(
                            "Non-nullable column '{}' has no value and no default",
                            column.name
                        ),
                    });
                };
                row.push(value);
            }

            table.insert_row(row)?;
        }

        database.add_table(table)?;
    }

    info!(
        "Successfully parsed database with {} tables",
        database.tables.len()
    );
    Ok((database, auth_config))
}

fn parse_value(yaml_value: &serde_yaml::Value, sql_type: &SqlType) -> crate::Result<DbValue> {
    use serde_yaml::Value;

    match (yaml_value, sql_type) {
        (Value::Null, _) => Ok(DbValue::Null),

        (Value::Bool(b), SqlType::Boolean) => Ok(DbValue::Boolean(*b)),

        (Value::Number(n), SqlType::Integer) => {
            if let Some(i) = n.as_i64() {
                Ok(DbValue::Integer(i))
            } else {
                // Try to parse as float first to check for overflow/scientific notation
                if let Some(f) = n.as_f64() {
                    if f.is_infinite() || f.is_nan() {
                        return Err(crate::YamlBaseError::TypeConversion(format!(
                            "Numeric value {:?} is not a valid finite number",
                            n
                        )));
                    }
                    if f > i64::MAX as f64 || f < i64::MIN as f64 {
                        return Err(crate::YamlBaseError::TypeConversion(format!(
                            "Numeric value {:?} is too large to fit in an integer (range: {} to {})",
                            n,
                            i64::MIN,
                            i64::MAX
                        )));
                    }
                    if f.fract() != 0.0 {
                        return Err(crate::YamlBaseError::TypeConversion(format!(
                            "Cannot convert decimal value {:?} to integer",
                            n
                        )));
                    }
                    Ok(DbValue::Integer(f as i64))
                } else {
                    Err(crate::YamlBaseError::TypeConversion(format!(
                        "Cannot convert {:?} to integer - invalid numeric format",
                        n
                    )))
                }
            }
        }

        (Value::Number(n), SqlType::Float) => {
            if let Some(f) = n.as_f64() {
                if f.is_infinite() || f.is_nan() {
                    return Err(crate::YamlBaseError::TypeConversion(format!(
                        "Numeric value {:?} is not a valid finite number",
                        n
                    )));
                }
                // Check if the value is too large/small for f32
                if f.is_finite() && (f > f32::MAX as f64 || f < f32::MIN as f64) {
                    return Err(crate::YamlBaseError::TypeConversion(format!(
                        "Numeric value {:?} is out of range for float (32-bit)",
                        n
                    )));
                }
                Ok(DbValue::Float(f as f32))
            } else {
                Err(crate::YamlBaseError::TypeConversion(format!(
                    "Cannot convert {:?} to float - invalid numeric format",
                    n
                )))
            }
        }

        (Value::Number(n), SqlType::Double) => {
            if let Some(f) = n.as_f64() {
                if f.is_infinite() || f.is_nan() {
                    return Err(crate::YamlBaseError::TypeConversion(format!(
                        "Numeric value {:?} is not a valid finite number",
                        n
                    )));
                }
                Ok(DbValue::Double(f))
            } else {
                Err(crate::YamlBaseError::TypeConversion(format!(
                    "Cannot convert {:?} to double - invalid numeric format",
                    n
                )))
            }
        }

        (Value::Number(n), SqlType::Decimal(_, _)) => {
            let s = n.to_string();
            match s.parse::<rust_decimal::Decimal>() {
                Ok(d) => Ok(DbValue::Decimal(d)),
                Err(_) => Err(crate::YamlBaseError::TypeConversion(format!(
                    "Cannot convert {:?} to decimal",
                    n
                ))),
            }
        }

        (Value::String(s), SqlType::Integer) => {
            // Support string-to-integer conversion for type coercion
            match s.parse::<i64>() {
                Ok(i) => Ok(DbValue::Integer(i)),
                Err(_) => Err(crate::YamlBaseError::TypeConversion(format!(
                    "Cannot convert string '{}' to integer",
                    s
                ))),
            }
        }
        (Value::String(s), SqlType::Decimal(_, _)) => {
            // Support string-to-decimal conversion
            match s.parse::<rust_decimal::Decimal>() {
                Ok(d) => Ok(DbValue::Decimal(d)),
                Err(_) => Err(crate::YamlBaseError::TypeConversion(format!(
                    "Cannot convert string '{}' to decimal",
                    s
                ))),
            }
        }
        (Value::String(s), SqlType::Float) => {
            // Support string-to-float conversion
            match s.parse::<f32>() {
                Ok(f) if f.is_finite() => Ok(DbValue::Float(f)),
                _ => Err(crate::YamlBaseError::TypeConversion(format!(
                    "Cannot convert string '{}' to float",
                    s
                ))),
            }
        }
        (Value::String(s), SqlType::Double) => {
            // Support string-to-double conversion
            match s.parse::<f64>() {
                Ok(f) if f.is_finite() => Ok(DbValue::Double(f)),
                _ => Err(crate::YamlBaseError::TypeConversion(format!(
                    "Cannot convert string '{}' to double",
                    s
                ))),
            }
        }
        (Value::String(s), SqlType::Char(_) | SqlType::Varchar(_) | SqlType::Text) => {
            Ok(DbValue::Text(s.clone()))
        }

        (Value::String(s), SqlType::Timestamp) => {
            match chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
                Ok(dt) => Ok(DbValue::Timestamp(dt)),
                Err(_) => {
                    // Try ISO format
                    match chrono::DateTime::parse_from_rfc3339(s) {
                        Ok(dt) => Ok(DbValue::Timestamp(dt.naive_local())),
                        Err(_) => Err(crate::YamlBaseError::TypeConversion(format!(
                            "Cannot parse timestamp: {}",
                            s
                        ))),
                    }
                }
            }
        }

        (Value::String(s), SqlType::Date) => match validate_and_parse_date(s) {
            Ok(d) => Ok(DbValue::Date(d)),
            Err(e) => Err(crate::YamlBaseError::TypeConversion(e)),
        },

        (Value::String(s), SqlType::Time) => {
            match chrono::NaiveTime::parse_from_str(s, "%H:%M:%S") {
                Ok(t) => Ok(DbValue::Time(t)),
                Err(_) => Err(crate::YamlBaseError::TypeConversion(format!(
                    "Cannot parse time: {}",
                    s
                ))),
            }
        }

        (Value::String(s), SqlType::Uuid) => match uuid::Uuid::parse_str(s) {
            Ok(u) => Ok(DbValue::Uuid(u)),
            Err(_) => Err(crate::YamlBaseError::TypeConversion(format!(
                "Cannot parse UUID: {}",
                s
            ))),
        },

        (Value::Mapping(_) | Value::Sequence(_), SqlType::Json) => {
            let json_str = serde_json::to_string(yaml_value).map_err(|e| {
                crate::YamlBaseError::TypeConversion(format!("Cannot convert to JSON: {}", e))
            })?;
            Ok(DbValue::Json(serde_json::from_str(&json_str).map_err(
                |e| crate::YamlBaseError::TypeConversion(format!("Invalid JSON structure: {}", e)),
            )?))
        }

        _ => Err(crate::YamlBaseError::TypeConversion(format!(
            "Cannot convert {:?} to {:?}",
            yaml_value, sql_type
        ))),
    }
}

fn parse_default_value(default: &str, sql_type: &SqlType) -> crate::Result<DbValue> {
    match default.to_uppercase().as_str() {
        "NULL" => Ok(DbValue::Null),
        "TRUE" => Ok(DbValue::Boolean(true)),
        "FALSE" => Ok(DbValue::Boolean(false)),
        "CURRENT_TIMESTAMP" => Ok(DbValue::Timestamp(chrono::Local::now().naive_local())),
        _ => {
            // Try to parse as the specific type
            let yaml_value: serde_yaml::Value = match sql_type {
                SqlType::Boolean => serde_yaml::Value::Bool(default.parse().map_err(|_| {
                    crate::YamlBaseError::TypeConversion(format!("Invalid boolean: {}", default))
                })?),
                SqlType::Integer => serde_yaml::Value::Number(serde_yaml::Number::from(
                    default.parse::<i64>().map_err(|_| {
                        crate::YamlBaseError::TypeConversion(format!(
                            "Invalid integer: {}",
                            default
                        ))
                    })?,
                )),
                _ => serde_yaml::Value::String(default.to_string()),
            };
            parse_value(&yaml_value, sql_type)
        }
    }
}

/// Validates and parses a date string with comprehensive error messages
fn validate_and_parse_date(s: &str) -> Result<chrono::NaiveDate, String> {
    // First try standard parsing
    match chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
        Ok(d) => Ok(d),
        Err(_) => {
            // Provide more specific error messages
            if s.len() != 10 {
                return Err(format!(
                    "Invalid date format '{}'. Expected format: YYYY-MM-DD",
                    s
                ));
            }

            let parts: Vec<&str> = s.split('-').collect();
            if parts.len() != 3 {
                return Err(format!(
                    "Invalid date format '{}'. Expected format: YYYY-MM-DD",
                    s
                ));
            }

            // Validate individual components
            let year = parts[0]
                .parse::<i32>()
                .map_err(|_| format!("Invalid year '{}' in date '{}'", parts[0], s))?;

            let month = parts[1]
                .parse::<u32>()
                .map_err(|_| format!("Invalid month '{}' in date '{}'", parts[1], s))?;

            let day = parts[2]
                .parse::<u32>()
                .map_err(|_| format!("Invalid day '{}' in date '{}'", parts[2], s))?;

            // Check valid ranges
            if !(1..=9999).contains(&year) {
                return Err(format!(
                    "Year {} is out of valid range (1-9999) in date '{}'",
                    year, s
                ));
            }

            if !(1..=12).contains(&month) {
                return Err(format!(
                    "Month {} is out of valid range (1-12) in date '{}'",
                    month, s
                ));
            }

            if !(1..=31).contains(&day) {
                return Err(format!(
                    "Day {} is out of valid range (1-31) in date '{}'",
                    day, s
                ));
            }

            // Check for specific invalid dates like February 30th
            if month == 2 {
                let is_leap_year = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
                let max_feb_days = if is_leap_year { 29 } else { 28 };
                if day > max_feb_days {
                    return Err(format!(
                        "February {} does not exist in year {} (maximum is {})",
                        day, year, max_feb_days
                    ));
                }
            }

            // Check for months with only 30 days
            if [4, 6, 9, 11].contains(&month) && day > 30 {
                return Err(format!(
                    "Month {} only has 30 days, but day {} was specified in date '{}'",
                    month, day, s
                ));
            }

            // If we get here, try one more time with chrono
            chrono::NaiveDate::from_ymd_opt(year, month, day)
                .ok_or_else(|| format!("Invalid date '{}' - date does not exist", s))
        }
    }
}