Skip to main content

ferro_cli/commands/
db_query.rs

1//! db:query command - Execute raw SQL queries against the database
2
3use console::style;
4use sea_orm::{ConnectionTrait, Database, DbBackend, Statement};
5use std::env;
6
7pub fn run(query: String) {
8    // Load DATABASE_URL from .env
9    dotenvy::dotenv().ok();
10
11    let database_url = match env::var("DATABASE_URL") {
12        Ok(url) => url,
13        Err(_) => {
14            eprintln!(
15                "{} DATABASE_URL not set in .env",
16                style("Error:").red().bold()
17            );
18            std::process::exit(1);
19        }
20    };
21
22    // Use tokio runtime for async database operations
23    let rt = tokio::runtime::Runtime::new().unwrap();
24    rt.block_on(async {
25        execute_query(&database_url, &query).await;
26    });
27}
28
29async fn execute_query(database_url: &str, query: &str) {
30    let is_sqlite = database_url.starts_with("sqlite");
31    let backend = if is_sqlite {
32        DbBackend::Sqlite
33    } else {
34        DbBackend::Postgres
35    };
36
37    // Connect to database
38    let db = match Database::connect(database_url).await {
39        Ok(db) => db,
40        Err(e) => {
41            eprintln!(
42                "{} Failed to connect to database: {}",
43                style("Error:").red().bold(),
44                e
45            );
46            std::process::exit(1);
47        }
48    };
49
50    // Execute the query
51    let result = db
52        .query_all(Statement::from_string(backend, query.to_string()))
53        .await;
54
55    match result {
56        Ok(rows) => {
57            if rows.is_empty() {
58                println!("{}", style("Empty result set").dim());
59                return;
60            }
61
62            // Try to get column names from the first row
63            // SeaORM doesn't expose column metadata directly, so we'll display raw values
64            print_results(&rows);
65        }
66        Err(e) => {
67            eprintln!("{} Query failed: {}", style("Error:").red().bold(), e);
68            std::process::exit(1);
69        }
70    }
71}
72
73fn print_results(rows: &[sea_orm::QueryResult]) {
74    if rows.is_empty() {
75        return;
76    }
77
78    // For SELECT queries, we need to try to extract values
79    // Since we don't know the schema, we'll try common column indices
80    let mut table_data: Vec<Vec<String>> = Vec::new();
81
82    for row in rows {
83        let mut row_data: Vec<String> = Vec::new();
84
85        // Try to extract values from indices 0-19 (reasonable column limit)
86        for i in 0..20 {
87            // Try different types in order of likelihood
88            if let Ok(val) = row.try_get_by_index::<i32>(i) {
89                row_data.push(val.to_string());
90            } else if let Ok(val) = row.try_get_by_index::<i64>(i) {
91                row_data.push(val.to_string());
92            } else if let Ok(val) = row.try_get_by_index::<String>(i) {
93                row_data.push(val);
94            } else if let Ok(val) = row.try_get_by_index::<bool>(i) {
95                row_data.push(val.to_string());
96            } else if let Ok(val) = row.try_get_by_index::<f64>(i) {
97                row_data.push(val.to_string());
98            } else if let Ok(val) = row.try_get_by_index::<Option<String>>(i) {
99                row_data.push(val.unwrap_or_else(|| "NULL".to_string()));
100            } else if let Ok(val) = row.try_get_by_index::<Option<i32>>(i) {
101                row_data.push(
102                    val.map(|v| v.to_string())
103                        .unwrap_or_else(|| "NULL".to_string()),
104                );
105            } else {
106                // No more columns at this index
107                break;
108            }
109        }
110
111        if !row_data.is_empty() {
112            table_data.push(row_data);
113        }
114    }
115
116    if table_data.is_empty() {
117        println!("{}", style("No data to display").dim());
118        return;
119    }
120
121    // Calculate column widths
122    let num_cols = table_data.iter().map(|r| r.len()).max().unwrap_or(0);
123    let mut col_widths: Vec<usize> = vec![0; num_cols];
124
125    for row in &table_data {
126        for (i, cell) in row.iter().enumerate() {
127            if i < col_widths.len() {
128                col_widths[i] = col_widths[i].max(cell.len()).max(3);
129            }
130        }
131    }
132
133    // Print separator
134    let separator: String = col_widths
135        .iter()
136        .map(|w| "-".repeat(*w + 2))
137        .collect::<Vec<_>>()
138        .join("+");
139    println!("+{separator}+");
140
141    // Print rows
142    for row in &table_data {
143        let formatted: Vec<String> = row
144            .iter()
145            .enumerate()
146            .map(|(i, cell)| {
147                let width = col_widths.get(i).copied().unwrap_or(10);
148                format!(" {cell:width$} ")
149            })
150            .collect();
151        println!("|{}|", formatted.join("|"));
152    }
153
154    println!("+{separator}+");
155    println!("\n{} {} row(s)", style("→").cyan(), table_data.len());
156}
157
158#[cfg(test)]
159mod tests {
160    #[test]
161    fn test_detect_sqlite_backend() {
162        let url = "sqlite://test.db";
163        assert!(url.starts_with("sqlite"));
164    }
165
166    #[test]
167    fn test_detect_postgres_backend() {
168        let url = "postgres://localhost/test";
169        assert!(!url.starts_with("sqlite"));
170    }
171
172    #[test]
173    fn test_column_width_calculation() {
174        let table_data = vec![
175            vec!["id".to_string(), "name".to_string()],
176            vec!["1".to_string(), "Alice".to_string()],
177            vec!["2".to_string(), "Bob".to_string()],
178        ];
179
180        let num_cols = table_data.iter().map(|r| r.len()).max().unwrap_or(0);
181        let mut col_widths: Vec<usize> = vec![0; num_cols];
182
183        for row in &table_data {
184            for (i, cell) in row.iter().enumerate() {
185                if i < col_widths.len() {
186                    col_widths[i] = col_widths[i].max(cell.len()).max(3);
187                }
188            }
189        }
190
191        assert_eq!(col_widths[0], 3); // "id" -> min 3
192        assert_eq!(col_widths[1], 5); // "Alice" -> 5
193    }
194
195    #[test]
196    fn test_separator_generation() {
197        let col_widths = [3, 5, 10];
198        let separator: String = col_widths
199            .iter()
200            .map(|w| "-".repeat(*w + 2))
201            .collect::<Vec<_>>()
202            .join("+");
203
204        assert_eq!(separator, "-----+-------+------------");
205    }
206
207    #[test]
208    fn test_row_formatting() {
209        let row = ["1".to_string(), "Alice".to_string()];
210        let col_widths = [3, 10];
211
212        let formatted: Vec<String> = row
213            .iter()
214            .enumerate()
215            .map(|(i, cell)| {
216                let width = col_widths.get(i).copied().unwrap_or(10);
217                format!(" {cell:width$} ")
218            })
219            .collect();
220
221        assert_eq!(formatted[0], " 1   ");
222        assert_eq!(formatted[1], " Alice      ");
223    }
224}