Skip to main content

mdql_core/
database.rs

1//! Load and validate database-level _mdql.md files (type: database).
2
3use std::path::Path;
4
5use crate::errors::MdqlError;
6use crate::parser::parse_file;
7use crate::schema::MDQL_FILENAME;
8
9#[derive(Debug, Clone)]
10pub struct ForeignKey {
11    pub from_table: String,
12    pub from_column: String,
13    pub to_table: String,
14    pub to_column: String,
15}
16
17#[derive(Debug, Clone)]
18pub struct DatabaseConfig {
19    pub name: String,
20    pub foreign_keys: Vec<ForeignKey>,
21}
22
23pub fn load_database_config(db_dir: &Path) -> crate::errors::Result<DatabaseConfig> {
24    let db_path = db_dir.join(MDQL_FILENAME);
25    if !db_path.exists() {
26        return Err(MdqlError::DatabaseConfig(format!(
27            "No {} in {}",
28            MDQL_FILENAME,
29            db_dir.display()
30        )));
31    }
32
33    let parsed = parse_file(&db_path, Some(db_dir), false)?;
34
35    if !parsed.parse_errors.is_empty() {
36        return Err(MdqlError::DatabaseConfig(format!(
37            "Cannot parse {}: {}",
38            MDQL_FILENAME,
39            parsed.parse_errors.join("; ")
40        )));
41    }
42
43    let fm = &parsed.raw_frontmatter;
44    let fm_map = fm.as_mapping().ok_or_else(|| {
45        MdqlError::DatabaseConfig(format!(
46            "{}: frontmatter must be a mapping",
47            MDQL_FILENAME
48        ))
49    })?;
50
51    let type_val = fm_map.get(&serde_yaml::Value::String("type".into()));
52    if type_val.and_then(|v| v.as_str()) != Some("database") {
53        return Err(MdqlError::DatabaseConfig(format!(
54            "{}: frontmatter must have 'type: database'",
55            MDQL_FILENAME
56        )));
57    }
58
59    let name = fm_map
60        .get(&serde_yaml::Value::String("name".into()))
61        .and_then(|v| v.as_str())
62        .ok_or_else(|| {
63            MdqlError::DatabaseConfig(format!(
64                "{}: frontmatter must have 'name' as a string",
65                MDQL_FILENAME
66            ))
67        })?
68        .to_string();
69
70    let mut fks = Vec::new();
71    if let Some(fk_list) = fm_map.get(&serde_yaml::Value::String("foreign_keys".into())) {
72        if let Some(seq) = fk_list.as_sequence() {
73            for fk_def in seq {
74                let fk_map = fk_def.as_mapping().ok_or_else(|| {
75                    MdqlError::DatabaseConfig(format!(
76                        "{}: each foreign_key must be a mapping",
77                        MDQL_FILENAME
78                    ))
79                })?;
80
81                let from_spec = fk_map
82                    .get(&serde_yaml::Value::String("from".into()))
83                    .and_then(|v| v.as_str())
84                    .unwrap_or("");
85                let to_spec = fk_map
86                    .get(&serde_yaml::Value::String("to".into()))
87                    .and_then(|v| v.as_str())
88                    .unwrap_or("");
89
90                if !from_spec.contains('.') || !to_spec.contains('.') {
91                    return Err(MdqlError::DatabaseConfig(format!(
92                        "{}: foreign_key 'from' and 'to' must be 'table.column' format",
93                        MDQL_FILENAME
94                    )));
95                }
96
97                let (from_table, from_col) = from_spec.split_once('.').unwrap();
98                let (to_table, to_col) = to_spec.split_once('.').unwrap();
99
100                fks.push(ForeignKey {
101                    from_table: from_table.to_string(),
102                    from_column: from_col.to_string(),
103                    to_table: to_table.to_string(),
104                    to_column: to_col.to_string(),
105                });
106            }
107        }
108    }
109
110    Ok(DatabaseConfig {
111        name,
112        foreign_keys: fks,
113    })
114}