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);
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 {
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
)));
}
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) => {
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(_, _)) => {
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) => {
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) => {
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(_) => {
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())),
_ => {
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)
}
}
}
fn validate_and_parse_date(s: &str) -> Result<chrono::NaiveDate, String> {
match chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
Ok(d) => Ok(d),
Err(_) => {
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
));
}
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))?;
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
));
}
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
));
}
}
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
));
}
chrono::NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| format!("Invalid date '{}' - date does not exist", s))
}
}
}