1use crate::config::{Database, Table};
3use crate::error::{DdbError, Result};
4use crate::lexer::Tokenizer;
5use crate::parser::{Parser, Statement};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Config {
14 pub default_database: String,
16
17 pub schema_dir: PathBuf,
19
20 #[serde(default = "default_delimiter")]
22 pub default_delimiter: char,
23
24 #[serde(default = "default_true")]
26 pub trim_whitespace: bool,
27
28 #[serde(default = "default_true")]
30 pub ignore_comments: bool,
31
32 #[serde(default = "default_comment_char")]
34 pub comment_char: Option<char>,
35
36 #[serde(default)]
38 pub data_starts_on: usize,
39
40 #[serde(default = "default_true")]
42 pub has_header: bool,
43
44 #[serde(default = "default_quote_char")]
46 pub quote_char: Option<char>,
47
48 #[serde(default = "default_true")]
50 pub skip_empty_lines: bool,
51
52 #[serde(default = "default_false")]
54 pub quoted_fields: bool,
55
56 #[serde(default = "default_true")]
58 pub strip_quotes: bool,
59
60 #[serde(default = "default_output_format")]
62 pub default_output_format: String,
63
64 #[serde(default = "default_false")]
66 pub ignore_errors: bool,
67
68 #[serde(default)]
70 pub databases: HashMap<String, DatabaseConfig>,
71}
72
73fn default_delimiter() -> char {
74 ','
75}
76
77fn default_true() -> bool {
78 true
79}
80
81fn default_false() -> bool {
82 false
83}
84
85fn default_comment_char() -> Option<char> {
86 Some('#')
87}
88
89fn default_quote_char() -> Option<char> {
90 Some('"')
91}
92
93fn default_output_format() -> String {
94 "table".to_string()
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct DatabaseConfig {
99 pub name: String,
100 pub schema_dir: Option<PathBuf>,
101
102 pub delimiter: Option<char>,
104 pub trim_whitespace: Option<bool>,
105 pub ignore_comments: Option<bool>,
106 pub comment_char: Option<char>,
107 pub data_starts_on: Option<usize>,
108 pub has_header: Option<bool>,
109}
110
111impl Config {
112 pub fn load() -> Result<Self> {
119 if let Ok(config_path) = std::env::var("DDB_CONFIG") {
121 return Self::from_file(&config_path);
122 }
123
124 let local_config = PathBuf::from(".ddb/config.yaml");
126 if local_config.exists() {
127 return Self::from_file(&local_config);
128 }
129
130 if let Some(home) = std::env::var_os("HOME") {
132 let home_config = PathBuf::from(home).join(".ddb/config.yaml");
133 if home_config.exists() {
134 return Self::from_file(&home_config);
135 }
136 }
137
138 Ok(Self::default())
140 }
141
142 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
144 let contents = fs::read_to_string(path)
145 .map_err(|e| DdbError::ConfigError(format!("Failed to read config file: {}", e)))?;
146
147 serde_yaml::from_str(&contents)
148 .map_err(|e| DdbError::ConfigError(format!("Failed to parse config: {}", e)))
149 }
150
151 pub fn default() -> Self {
154 let schema_dir = std::env::var("DDB_SCHEMA_DIR")
155 .map(PathBuf::from)
156 .unwrap_or_else(|_| PathBuf::from(".ddb/schemas"));
157
158 Config {
159 default_database: "default".to_string(),
160 schema_dir,
161 default_delimiter: ',',
162 trim_whitespace: true,
163 ignore_comments: true,
164 comment_char: Some('#'),
165 data_starts_on: 0,
166 has_header: true,
167 quote_char: Some('"'),
168 skip_empty_lines: true,
169 quoted_fields: false,
170 strip_quotes: true,
171 default_output_format: "table".to_string(),
172 ignore_errors: false,
173 databases: HashMap::new(),
174 }
175 }
176
177 pub fn get_schema_dir(&self, database: &str) -> PathBuf {
179 if let Some(db_config) = self.databases.get(database) {
180 if let Some(ref schema_dir) = db_config.schema_dir {
181 return schema_dir.clone();
182 }
183 }
184 self.schema_dir.clone()
185 }
186}
187
188#[derive(Debug, Clone)]
190pub struct TableCatalog {
191 databases: HashMap<String, Database>,
192 current_database: String,
193}
194
195impl TableCatalog {
196 pub fn new() -> Self {
197 TableCatalog {
198 databases: HashMap::new(),
199 current_database: "default".to_string(),
200 }
201 }
202
203 pub fn load_from_config(config: &Config) -> Result<Self> {
205 let mut catalog = TableCatalog::new();
206 catalog.current_database = config.default_database.clone();
207
208 let mut default_db = Database::new(config.default_database.clone());
210
211 let schema_dir = &config.schema_dir;
213 if schema_dir.exists() {
214 catalog.load_schema_directory(
215 &config.default_database,
216 schema_dir,
217 config.default_delimiter,
218 &mut default_db,
219 )?;
220 }
221
222 for (db_name, db_config) in &config.databases {
224 let mut db = Database::new(db_name.clone());
225 if let Some(ref schema_dir) = db_config.schema_dir {
226 if schema_dir.exists() {
227 catalog.load_schema_directory(
228 db_name,
229 schema_dir,
230 config.default_delimiter,
231 &mut db,
232 )?;
233 }
234 }
235 catalog.databases.insert(db_name.clone(), db);
236 }
237
238 catalog.databases.insert(config.default_database.clone(), default_db);
239
240 Ok(catalog)
241 }
242
243 fn load_schema_directory(
245 &self,
246 database_name: &str,
247 dir: &Path,
248 default_delimiter: char,
249 db: &mut Database,
250 ) -> Result<()> {
251 let entries = fs::read_dir(dir)
252 .map_err(|e| DdbError::ConfigError(format!("Failed to read schema directory: {}", e)))?;
253
254 for entry in entries {
255 let entry = entry.map_err(|e| {
256 DdbError::ConfigError(format!("Failed to read directory entry: {}", e))
257 })?;
258
259 let path = entry.path();
260 if path.extension().and_then(|s| s.to_str()) == Some("sql") {
261 self.load_sql_file(database_name, &path, default_delimiter, db)?;
262 }
263 }
264
265 Ok(())
266 }
267
268 fn load_sql_file(
270 &self,
271 database_name: &str,
272 path: &Path,
273 default_delimiter: char,
274 db: &mut Database,
275 ) -> Result<()> {
276 let contents = fs::read_to_string(path).map_err(|e| {
277 DdbError::ConfigError(format!(
278 "Failed to read SQL file {}: {}",
279 path.display(),
280 e
281 ))
282 })?;
283
284 for statement_str in contents.split(';') {
286 let statement_str: String = statement_str
288 .lines()
289 .filter(|line| !line.trim().starts_with("--"))
290 .collect::<Vec<_>>()
291 .join("\n");
292
293 let statement_str = statement_str.trim();
294 if statement_str.is_empty() {
295 continue;
296 }
297
298 let mut tokenizer = Tokenizer::new();
300 let tokens = tokenizer.tokenize(statement_str).map_err(|e| {
301 DdbError::ConfigError(format!(
302 "Failed to tokenize SQL in {}: {}",
303 path.display(),
304 e
305 ))
306 })?;
307
308 let mut parser = Parser::new(tokens);
309 match parser.parse() {
310 Ok(Statement::CreateTable(create_stmt)) => {
311 let table = Table {
313 name: create_stmt.name.clone(),
314 database: database_name.to_string(),
315 data_file: if create_stmt.file_path.is_empty() {
316 format!("data/{}.csv", create_stmt.name)
317 } else {
318 create_stmt.file_path.clone()
319 },
320 columns: vec![], field_delimiter: create_stmt.delimiter.unwrap_or(default_delimiter),
322 data_starts_on: 0,
323 comment_char: None,
324 };
325
326 db.add_table(table);
327 }
328 Ok(_) => {
329 }
331 Err(e) => {
332 eprintln!(
333 "Warning: Failed to parse statement in {}: {}",
334 path.display(),
335 e
336 );
337 }
338 }
339 }
340
341 Ok(())
342 }
343
344 pub fn get_table(&self, name: &str) -> Option<&Table> {
346 self.databases
347 .get(&self.current_database)
348 .and_then(|db| db.get_table(name))
349 }
350
351 pub fn get_table_from_db(&self, database: &str, table: &str) -> Option<&Table> {
353 self.databases.get(database).and_then(|db| db.get_table(table))
354 }
355
356 pub fn use_database(&mut self, name: &str) -> Result<()> {
358 if self.databases.contains_key(name) {
359 self.current_database = name.to_string();
360 Ok(())
361 } else {
362 Err(DdbError::ConfigError(format!("Database '{}' not found", name)))
363 }
364 }
365
366 pub fn current_database(&self) -> &str {
368 &self.current_database
369 }
370
371 pub fn list_databases(&self) -> Vec<String> {
373 self.databases.keys().cloned().collect()
374 }
375
376 pub fn list_tables(&self) -> Vec<String> {
378 self.databases
379 .get(&self.current_database)
380 .map(|db| db.tables.keys().cloned().collect())
381 .unwrap_or_default()
382 }
383
384 pub fn add_table(&mut self, table: Table) -> Result<()> {
386 let db = self
387 .databases
388 .get_mut(&self.current_database)
389 .ok_or_else(|| DdbError::ConfigError("Current database not found".to_string()))?;
390
391 db.add_table(table);
392 Ok(())
393 }
394}
395
396impl Default for TableCatalog {
397 fn default() -> Self {
398 Self::new()
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use std::io::Write;
406 use tempfile::TempDir;
407
408 #[test]
409 fn test_load_sql_file() {
410 let temp_dir = TempDir::new().unwrap();
411 let sql_file = temp_dir.path().join("test.sql");
412
413 let mut file = fs::File::create(&sql_file).unwrap();
414 writeln!(
415 file,
416 "CREATE TABLE users (id INTEGER, name STRING, email STRING) FILE 'data/users.csv' DELIMITER ',';"
417 )
418 .unwrap();
419 file.flush().unwrap();
420
421 let mut config = Config::default();
422 config.default_database = "test".to_string();
423 config.schema_dir = temp_dir.path().to_path_buf();
424
425 let catalog = TableCatalog::load_from_config(&config).unwrap();
426 let table = catalog.get_table("users");
427
428 assert!(table.is_some());
429 let table = table.unwrap();
430 assert_eq!(table.name, "users");
431 assert_eq!(table.data_file, "data/users.csv");
432 assert_eq!(table.field_delimiter, ',');
433 }
434
435 #[test]
436 fn test_default_config() {
437 let config = Config::default();
438 assert_eq!(config.default_database, "default");
439 assert_eq!(config.default_delimiter, ',');
440 }
441}