1use 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 ViewDef {
19 pub name: String,
20 pub query: String,
21}
22
23#[derive(Debug, Clone)]
24pub struct DatabaseConfig {
25 pub name: String,
26 pub foreign_keys: Vec<ForeignKey>,
27 pub views: Vec<ViewDef>,
28}
29
30pub fn is_database_dir(folder: &Path) -> bool {
31 let mdql_file = folder.join(MDQL_FILENAME);
32 if !mdql_file.exists() {
33 return false;
34 }
35 if let Ok(text) = std::fs::read_to_string(&mdql_file) {
36 if let Some(fm_text) = text
37 .strip_prefix("---\n")
38 .and_then(|rest| rest.split_once("\n---").map(|(fm, _)| fm))
39 {
40 if let Ok(val) = serde_yaml::from_str::<serde_yaml::Value>(fm_text) {
41 if let Some(m) = val.as_mapping() {
42 return m
43 .get(&serde_yaml::Value::String("type".into()))
44 .and_then(|v| v.as_str())
45 == Some("database");
46 }
47 }
48 }
49 }
50 false
51}
52
53pub fn load_database_config(db_dir: &Path) -> crate::errors::Result<DatabaseConfig> {
54 let db_path = db_dir.join(MDQL_FILENAME);
55 if !db_path.exists() {
56 return Err(MdqlError::DatabaseConfig(format!(
57 "No {} in {}",
58 MDQL_FILENAME,
59 db_dir.display()
60 )));
61 }
62
63 let parsed = parse_file(&db_path, Some(db_dir), false)?;
64
65 if !parsed.parse_errors.is_empty() {
66 return Err(MdqlError::DatabaseConfig(format!(
67 "Cannot parse {}: {}",
68 MDQL_FILENAME,
69 parsed.parse_errors.join("; ")
70 )));
71 }
72
73 let fm = &parsed.raw_frontmatter;
74 let fm_map = fm.as_mapping().ok_or_else(|| {
75 MdqlError::DatabaseConfig(format!(
76 "{}: frontmatter must be a mapping",
77 MDQL_FILENAME
78 ))
79 })?;
80
81 let type_val = fm_map.get(&serde_yaml::Value::String("type".into()));
82 if type_val.and_then(|v| v.as_str()) != Some("database") {
83 return Err(MdqlError::DatabaseConfig(format!(
84 "{}: frontmatter must have 'type: database'",
85 MDQL_FILENAME
86 )));
87 }
88
89 let name = fm_map
90 .get(&serde_yaml::Value::String("name".into()))
91 .and_then(|v| v.as_str())
92 .ok_or_else(|| {
93 MdqlError::DatabaseConfig(format!(
94 "{}: frontmatter must have 'name' as a string",
95 MDQL_FILENAME
96 ))
97 })?
98 .to_string();
99
100 let mut fks = Vec::new();
101 if let Some(fk_list) = fm_map.get(&serde_yaml::Value::String("foreign_keys".into())) {
102 if let Some(seq) = fk_list.as_sequence() {
103 for fk_def in seq {
104 let fk_map = fk_def.as_mapping().ok_or_else(|| {
105 MdqlError::DatabaseConfig(format!(
106 "{}: each foreign_key must be a mapping",
107 MDQL_FILENAME
108 ))
109 })?;
110
111 let from_spec = fk_map
112 .get(&serde_yaml::Value::String("from".into()))
113 .and_then(|v| v.as_str())
114 .unwrap_or("");
115 let to_spec = fk_map
116 .get(&serde_yaml::Value::String("to".into()))
117 .and_then(|v| v.as_str())
118 .unwrap_or("");
119
120 if !from_spec.contains('.') || !to_spec.contains('.') {
121 return Err(MdqlError::DatabaseConfig(format!(
122 "{}: foreign_key 'from' and 'to' must be 'table.column' format",
123 MDQL_FILENAME
124 )));
125 }
126
127 let (from_table, from_col) = from_spec.split_once('.').unwrap();
128 let (to_table, to_col) = to_spec.split_once('.').unwrap();
129
130 fks.push(ForeignKey {
131 from_table: from_table.to_string(),
132 from_column: from_col.to_string(),
133 to_table: to_table.to_string(),
134 to_column: to_col.to_string(),
135 });
136 }
137 }
138 }
139
140 let mut views = Vec::new();
141 if let Some(view_list) = fm_map.get(&serde_yaml::Value::String("views".into())) {
142 if let Some(seq) = view_list.as_sequence() {
143 for view_def in seq {
144 let view_map = view_def.as_mapping().ok_or_else(|| {
145 MdqlError::DatabaseConfig(format!(
146 "{}: each view must be a mapping with 'name' and 'query'",
147 MDQL_FILENAME
148 ))
149 })?;
150
151 let view_name = view_map
152 .get(&serde_yaml::Value::String("name".into()))
153 .and_then(|v| v.as_str())
154 .ok_or_else(|| {
155 MdqlError::DatabaseConfig(format!(
156 "{}: each view must have a 'name' string",
157 MDQL_FILENAME
158 ))
159 })?
160 .to_string();
161
162 let view_query = view_map
163 .get(&serde_yaml::Value::String("query".into()))
164 .and_then(|v| v.as_str())
165 .ok_or_else(|| {
166 MdqlError::DatabaseConfig(format!(
167 "{}: view '{}' must have a 'query' string",
168 MDQL_FILENAME, view_name
169 ))
170 })?
171 .to_string();
172
173 views.push(ViewDef {
174 name: view_name,
175 query: view_query,
176 });
177 }
178 }
179 }
180
181 Ok(DatabaseConfig {
182 name,
183 foreign_keys: fks,
184 views,
185 })
186}
187
188pub fn save_database_config(db_dir: &Path, config: &DatabaseConfig) -> crate::errors::Result<()> {
189 let db_path = db_dir.join(MDQL_FILENAME);
190 let text = std::fs::read_to_string(&db_path)?;
191
192 let (_before_fm, fm_text, after_fm) = if let Some(rest) = text.strip_prefix("---\n") {
193 if let Some((fm, after)) = rest.split_once("\n---") {
194 ("---\n".to_string(), fm.to_string(), format!("\n---{}", after))
195 } else {
196 return Err(MdqlError::DatabaseConfig("Malformed frontmatter".into()));
197 }
198 } else {
199 return Err(MdqlError::DatabaseConfig("No frontmatter found".into()));
200 };
201
202 let mut fm: serde_yaml::Value = serde_yaml::from_str(&fm_text)
203 .map_err(|e| MdqlError::DatabaseConfig(format!("YAML parse error: {}", e)))?;
204
205 let fm_map = fm.as_mapping_mut().ok_or_else(|| {
206 MdqlError::DatabaseConfig("Frontmatter is not a mapping".into())
207 })?;
208
209 let views_key = serde_yaml::Value::String("views".into());
210 if config.views.is_empty() {
211 fm_map.remove(&views_key);
212 } else {
213 let views_seq: Vec<serde_yaml::Value> = config
214 .views
215 .iter()
216 .map(|v| {
217 let mut m = serde_yaml::Mapping::new();
218 m.insert(
219 serde_yaml::Value::String("name".into()),
220 serde_yaml::Value::String(v.name.clone()),
221 );
222 m.insert(
223 serde_yaml::Value::String("query".into()),
224 serde_yaml::Value::String(v.query.clone()),
225 );
226 serde_yaml::Value::Mapping(m)
227 })
228 .collect();
229 fm_map.insert(views_key, serde_yaml::Value::Sequence(views_seq));
230 }
231
232 let new_fm_text = serde_yaml::to_string(&fm)
233 .map_err(|e| MdqlError::DatabaseConfig(format!("YAML serialize error: {}", e)))?;
234
235 let new_content = format!("---\n{}---{}", new_fm_text, &after_fm[4..]);
236 crate::txn::atomic_write(&db_path, &new_content)?;
237 Ok(())
238}