Skip to main content

dbiewlite_core/
lib.rs

1use rusqlite::Connection;
2use serde::Serialize;
3use std::io::Write;
4use std::path::Path;
5
6/// Wraps an open SQLite database connection.
7pub struct Database {
8    conn: Connection,
9    path: String,
10}
11
12#[derive(Debug, Clone, Serialize)]
13pub struct DbInfo {
14    pub path: String,
15    pub file_size: u64,
16    pub sqlite_version: String,
17    pub page_count: u64,
18    pub page_size: u64,
19    pub table_count: usize,
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct TableInfo {
24    pub name: String,
25    pub row_count: u64,
26    pub column_count: usize,
27}
28
29#[derive(Debug, Clone, Serialize)]
30pub struct ColumnInfo {
31    pub name: String,
32    pub col_type: String,
33    pub nullable: bool,
34    pub primary_key: bool,
35    pub default_value: Option<String>,
36}
37
38#[derive(Debug, Clone, Serialize)]
39pub struct IndexInfo {
40    pub name: String,
41    pub table_name: String,
42    pub unique: bool,
43    pub columns: Vec<String>,
44}
45
46#[derive(Debug, Clone, Serialize)]
47pub struct QueryResult {
48    pub columns: Vec<String>,
49    pub rows: Vec<Vec<CellValue>>,
50    pub total_rows: Option<u64>,
51}
52
53#[derive(Debug, Clone, Serialize)]
54#[serde(untagged)]
55pub enum CellValue {
56    Null,
57    Integer(i64),
58    Real(f64),
59    Text(String),
60    Blob(Vec<u8>),
61}
62
63impl std::fmt::Display for CellValue {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        match self {
66            CellValue::Null => write!(f, "NULL"),
67            CellValue::Integer(i) => write!(f, "{}", i),
68            CellValue::Real(r) => write!(f, "{}", r),
69            CellValue::Text(s) => write!(f, "{}", s),
70            CellValue::Blob(b) => write!(f, "<blob {} bytes>", b.len()),
71        }
72    }
73}
74
75#[derive(Debug, Clone, Serialize)]
76pub struct Sort {
77    pub column: String,
78    pub ascending: bool,
79}
80
81impl Database {
82    pub fn open(path: &str) -> Result<Self, String> {
83        let conn = Connection::open_with_flags(
84            path,
85            rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
86        )
87        .map_err(|e| format!("Failed to open database: {}", e))?;
88        Ok(Database {
89            conn,
90            path: path.to_string(),
91        })
92    }
93
94    pub fn path(&self) -> &str {
95        &self.path
96    }
97
98    pub fn get_info(&self) -> Result<DbInfo, String> {
99        let sqlite_version: String = self
100            .conn
101            .query_row("SELECT sqlite_version()", [], |row| row.get(0))
102            .map_err(|e| e.to_string())?;
103
104        let page_count: u64 = self
105            .conn
106            .pragma_query_value(None, "page_count", |row| row.get(0))
107            .map_err(|e| e.to_string())?;
108
109        let page_size: u64 = self
110            .conn
111            .pragma_query_value(None, "page_size", |row| row.get(0))
112            .map_err(|e| e.to_string())?;
113
114        let file_size = Path::new(&self.path)
115            .metadata()
116            .map(|m| m.len())
117            .unwrap_or(0);
118
119        let tables = self.list_tables().map_err(|e| e.to_string())?;
120
121        Ok(DbInfo {
122            path: self.path.clone(),
123            file_size,
124            sqlite_version,
125            page_count,
126            page_size,
127            table_count: tables.len(),
128        })
129    }
130
131    pub fn list_tables(&self) -> Result<Vec<TableInfo>, String> {
132        let mut stmt = self
133            .conn
134            .prepare(
135                "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
136            )
137            .map_err(|e| e.to_string())?;
138
139        let names: Vec<String> = stmt
140            .query_map([], |row| row.get(0))
141            .map_err(|e| e.to_string())?
142            .filter_map(|r| r.ok())
143            .collect();
144
145        let mut tables = Vec::new();
146        for name in names {
147            let row_count = self.get_row_count(&name).unwrap_or(0);
148            let columns = self.get_schema(&name).unwrap_or_default();
149            tables.push(TableInfo {
150                name,
151                row_count,
152                column_count: columns.len(),
153            });
154        }
155        Ok(tables)
156    }
157
158    pub fn list_views(&self) -> Result<Vec<String>, String> {
159        let mut stmt = self
160            .conn
161            .prepare("SELECT name FROM sqlite_master WHERE type='view' ORDER BY name")
162            .map_err(|e| e.to_string())?;
163
164        let views = stmt
165            .query_map([], |row| row.get(0))
166            .map_err(|e| e.to_string())?
167            .filter_map(|r| r.ok())
168            .collect();
169
170        Ok(views)
171    }
172
173    pub fn list_indexes(&self) -> Result<Vec<IndexInfo>, String> {
174        let mut stmt = self
175            .conn
176            .prepare(
177                "SELECT name, tbl_name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' ORDER BY name",
178            )
179            .map_err(|e| e.to_string())?;
180
181        let raw: Vec<(String, String)> = stmt
182            .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
183            .map_err(|e| e.to_string())?
184            .filter_map(|r| r.ok())
185            .collect();
186
187        let mut indexes = Vec::new();
188        for (name, table_name) in raw {
189            let mut info_stmt = self
190                .conn
191                .prepare(&format!("PRAGMA index_info(\"{}\")", name))
192                .map_err(|e| e.to_string())?;
193
194            let columns: Vec<String> = info_stmt
195                .query_map([], |row| row.get(2))
196                .map_err(|e| e.to_string())?
197                .filter_map(|r| r.ok())
198                .collect();
199
200            let unique = self
201                .conn
202                .prepare(&format!("PRAGMA index_list(\"{}\")", table_name))
203                .and_then(|mut s| {
204                    let mut found = false;
205                    let rows = s.query_map([], |row| {
206                        let idx_name: String = row.get(1)?;
207                        let is_unique: bool = row.get(2)?;
208                        Ok((idx_name, is_unique))
209                    })?;
210                    for r in rows.flatten() {
211                        if r.0 == name {
212                            found = r.1;
213                            break;
214                        }
215                    }
216                    Ok(found)
217                })
218                .unwrap_or(false);
219
220            indexes.push(IndexInfo {
221                name,
222                table_name,
223                unique,
224                columns,
225            });
226        }
227        Ok(indexes)
228    }
229
230    pub fn get_schema(&self, table: &str) -> Result<Vec<ColumnInfo>, String> {
231        let mut stmt = self
232            .conn
233            .prepare(&format!("PRAGMA table_info(\"{}\")", table))
234            .map_err(|e| e.to_string())?;
235
236        let columns = stmt
237            .query_map([], |row| {
238                Ok(ColumnInfo {
239                    name: row.get(1)?,
240                    col_type: row.get::<_, String>(2).unwrap_or_default(),
241                    nullable: !row.get::<_, bool>(3).unwrap_or(false),
242                    primary_key: row.get::<_, bool>(5).unwrap_or(false),
243                    default_value: row.get(4).ok(),
244                })
245            })
246            .map_err(|e| e.to_string())?
247            .filter_map(|r| r.ok())
248            .collect();
249
250        Ok(columns)
251    }
252
253    pub fn query_table(
254        &self,
255        table: &str,
256        limit: usize,
257        offset: usize,
258        sort: Option<Sort>,
259    ) -> Result<QueryResult, String> {
260        let order_clause = match &sort {
261            Some(s) => format!(
262                " ORDER BY \"{}\" {}",
263                s.column,
264                if s.ascending { "ASC" } else { "DESC" }
265            ),
266            None => String::new(),
267        };
268
269        let sql = format!(
270            "SELECT * FROM \"{}\"{} LIMIT {} OFFSET {}",
271            table, order_clause, limit, offset
272        );
273
274        let total = self.get_row_count(table).ok();
275        let mut result = self.run_query(&sql)?;
276        result.total_rows = total;
277        Ok(result)
278    }
279
280    pub fn run_query(&self, sql: &str) -> Result<QueryResult, String> {
281        let mut stmt = self.conn.prepare(sql).map_err(|e| e.to_string())?;
282
283        let columns: Vec<String> = stmt
284            .column_names()
285            .iter()
286            .map(|s| s.to_string())
287            .collect();
288
289        let rows: Vec<Vec<CellValue>> = stmt
290            .query_map([], |row| {
291                let mut cells = Vec::new();
292                for i in 0..columns.len() {
293                    let val = match row.get_ref(i) {
294                        Ok(rusqlite::types::ValueRef::Null) => CellValue::Null,
295                        Ok(rusqlite::types::ValueRef::Integer(n)) => CellValue::Integer(n),
296                        Ok(rusqlite::types::ValueRef::Real(f)) => CellValue::Real(f),
297                        Ok(rusqlite::types::ValueRef::Text(s)) => {
298                            CellValue::Text(String::from_utf8_lossy(s).to_string())
299                        }
300                        Ok(rusqlite::types::ValueRef::Blob(b)) => CellValue::Blob(b.to_vec()),
301                        Err(_) => CellValue::Null,
302                    };
303                    cells.push(val);
304                }
305                Ok(cells)
306            })
307            .map_err(|e| e.to_string())?
308            .filter_map(|r| r.ok())
309            .collect();
310
311        Ok(QueryResult {
312            columns,
313            rows,
314            total_rows: None,
315        })
316    }
317
318    pub fn get_row_count(&self, table: &str) -> Result<u64, String> {
319        self.conn
320            .query_row(
321                &format!("SELECT COUNT(*) FROM \"{}\"", table),
322                [],
323                |row| row.get(0),
324            )
325            .map_err(|e| e.to_string())
326    }
327
328    pub fn export_csv<W: Write>(&self, table: &str, writer: &mut W) -> Result<(), String> {
329        let result = self.run_query(&format!("SELECT * FROM \"{}\"", table))?;
330
331        // Write header
332        let header = result
333            .columns
334            .iter()
335            .map(|c| escape_csv(c))
336            .collect::<Vec<_>>()
337            .join(",");
338        writeln!(writer, "{}", header).map_err(|e| e.to_string())?;
339
340        // Write rows
341        for row in &result.rows {
342            let line = row
343                .iter()
344                .map(|v| escape_csv(&v.to_string()))
345                .collect::<Vec<_>>()
346                .join(",");
347            writeln!(writer, "{}", line).map_err(|e| e.to_string())?;
348        }
349
350        Ok(())
351    }
352}
353
354fn escape_csv(s: &str) -> String {
355    if s.contains(',') || s.contains('"') || s.contains('\n') {
356        format!("\"{}\"", s.replace('"', "\"\""))
357    } else {
358        s.to_string()
359    }
360}
361
362pub fn format_size(bytes: u64) -> String {
363    if bytes < 1024 {
364        format!("{} B", bytes)
365    } else if bytes < 1024 * 1024 {
366        format!("{:.1} KB", bytes as f64 / 1024.0)
367    } else if bytes < 1024 * 1024 * 1024 {
368        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
369    } else {
370        format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
371    }
372}