Skip to main content

ddb_core/config/
loader.rs

1// Configuration loader for DDB
2use 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/// DDB configuration structure
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Config {
14    /// Name of the default database
15    pub default_database: String,
16
17    /// Directory containing SQL schema files
18    pub schema_dir: PathBuf,
19
20    /// Default field delimiter
21    #[serde(default = "default_delimiter")]
22    pub default_delimiter: char,
23
24    /// Trim whitespace from fields
25    #[serde(default = "default_true")]
26    pub trim_whitespace: bool,
27
28    /// Ignore comment lines
29    #[serde(default = "default_true")]
30    pub ignore_comments: bool,
31
32    /// Comment character (lines starting with this are ignored)
33    #[serde(default = "default_comment_char")]
34    pub comment_char: Option<char>,
35
36    /// Line number where data starts (0 = first line)
37    #[serde(default)]
38    pub data_starts_on: usize,
39
40    /// Whether the first data line is a header
41    #[serde(default = "default_true")]
42    pub has_header: bool,
43
44    /// Quote character for escaping delimiters
45    #[serde(default = "default_quote_char")]
46    pub quote_char: Option<char>,
47
48    /// Skip empty lines
49    #[serde(default = "default_true")]
50    pub skip_empty_lines: bool,
51
52    /// Whether fields are quoted
53    #[serde(default = "default_false")]
54    pub quoted_fields: bool,
55
56    /// Remove quotes from quoted fields
57    #[serde(default = "default_true")]
58    pub strip_quotes: bool,
59
60    /// Default output format (table, json, yaml, csv)
61    #[serde(default = "default_output_format")]
62    pub default_output_format: String,
63
64    /// Ignore errors (continue processing on errors)
65    #[serde(default = "default_false")]
66    pub ignore_errors: bool,
67
68    /// Databases configuration
69    #[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    /// Override default settings for this database
103    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    /// Load configuration from environment or default locations
113    /// Checks in order:
114    /// 1. DDB_CONFIG environment variable
115    /// 2. .ddb/config.yaml in current directory
116    /// 3. ~/.ddb/config.yaml
117    /// 4. Returns default config if none found
118    pub fn load() -> Result<Self> {
119        // Check DDB_CONFIG environment variable
120        if let Ok(config_path) = std::env::var("DDB_CONFIG") {
121            return Self::from_file(&config_path);
122        }
123
124        // Check local .ddb/config.yaml
125        let local_config = PathBuf::from(".ddb/config.yaml");
126        if local_config.exists() {
127            return Self::from_file(&local_config);
128        }
129
130        // Check home directory ~/.ddb/config.yaml
131        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        // Return default configuration
139        Ok(Self::default())
140    }
141
142    /// Load configuration from a YAML file
143    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    /// Create a default configuration
152    /// Also checks DDB_SCHEMA_DIR environment variable
153    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    /// Get the schema directory for a database
178    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/// Table catalog that holds all loaded table definitions
189#[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    /// Load tables from a configuration
204    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        // Create default database
209        let mut default_db = Database::new(config.default_database.clone());
210
211        // Load SQL files from schema directory
212        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        // Load additional databases
223        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    /// Load all SQL files from a directory
244    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    /// Load and parse a SQL file
269    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        // Split by semicolons to handle multiple statements
285        for statement_str in contents.split(';') {
286            // Strip comment lines (lines starting with --)
287            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            // Tokenize and parse
299            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                    // Convert to Table structure
312                    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![], // Column definitions from CREATE TABLE
321                        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                    // Ignore other statement types
330                }
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    /// Get a table by name from the current database
345    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    /// Get a table from a specific database
352    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    /// Set the current database
357    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    /// Get the current database name
367    pub fn current_database(&self) -> &str {
368        &self.current_database
369    }
370
371    /// List all databases
372    pub fn list_databases(&self) -> Vec<String> {
373        self.databases.keys().cloned().collect()
374    }
375
376    /// List all tables in the current database
377    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    /// Add a table to the current database
385    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}