Skip to main content

dbiewlite_core/
duckdb_backend.rs

1use duckdb::types::ValueRef;
2use duckdb::Connection;
3use std::io::Write;
4use std::path::Path;
5
6use crate::types::*;
7
8pub struct DuckdbBackend {
9    conn: Connection,
10    path: String,
11    /// When opened from a Parquet file, this is the virtual table name.
12    parquet_table: Option<String>,
13}
14
15impl DuckdbBackend {
16    pub fn open(path: &str) -> Result<Self, String> {
17        let config = duckdb::Config::default()
18            .access_mode(duckdb::AccessMode::ReadOnly)
19            .map_err(|e| format!("Failed to configure DuckDB: {}", e))?;
20        let conn = Connection::open_with_flags(path, config)
21            .map_err(|e| format!("Failed to open DuckDB database: {}", e))?;
22        Ok(DuckdbBackend {
23            conn,
24            path: path.to_string(),
25            parquet_table: None,
26        })
27    }
28
29    pub fn open_parquet(path: &str) -> Result<Self, String> {
30        let conn = Connection::open_in_memory()
31            .map_err(|e| format!("Failed to open in-memory DuckDB: {}", e))?;
32
33        let table_name = Path::new(path)
34            .file_stem()
35            .and_then(|s| s.to_str())
36            .unwrap_or("data")
37            .to_string();
38
39        conn.execute_batch(&format!(
40            "CREATE VIEW \"{}\" AS SELECT * FROM read_parquet('{}')",
41            table_name,
42            path.replace('\'', "''")
43        ))
44        .map_err(|e| format!("Failed to read Parquet file: {}", e))?;
45
46        Ok(DuckdbBackend {
47            conn,
48            path: path.to_string(),
49            parquet_table: Some(table_name),
50        })
51    }
52
53    fn is_parquet(&self) -> bool {
54        self.parquet_table.is_some()
55    }
56
57    pub fn path(&self) -> &str {
58        &self.path
59    }
60
61    pub fn get_info(&self) -> Result<DbInfo, String> {
62        let version: String = self
63            .conn
64            .query_row("SELECT library_version FROM pragma_version()", [], |row| {
65                row.get(0)
66            })
67            .map_err(|e| e.to_string())?;
68
69        let file_size = Path::new(&self.path)
70            .metadata()
71            .map(|m| m.len())
72            .unwrap_or(0);
73
74        let tables = self.list_tables()?;
75
76        let engine = if self.is_parquet() {
77            "Parquet"
78        } else {
79            "DuckDB"
80        };
81
82        Ok(DbInfo {
83            path: self.path.clone(),
84            file_size,
85            engine: engine.to_string(),
86            engine_version: version,
87            page_count: None,
88            page_size: None,
89            table_count: tables.len(),
90        })
91    }
92
93    pub fn list_tables(&self) -> Result<Vec<TableInfo>, String> {
94        if let Some(table_name) = &self.parquet_table {
95            let row_count = self.get_row_count(table_name).unwrap_or(0);
96            let columns = self.get_schema(table_name).unwrap_or_default();
97            return Ok(vec![TableInfo {
98                name: table_name.clone(),
99                row_count,
100                column_count: columns.len(),
101            }]);
102        }
103
104        let mut stmt = self
105            .conn
106            .prepare(
107                "SELECT table_name FROM information_schema.tables \
108                 WHERE table_schema = 'main' AND table_type = 'BASE TABLE' \
109                 ORDER BY table_name",
110            )
111            .map_err(|e| e.to_string())?;
112
113        let names: Vec<String> = stmt
114            .query_map([], |row| row.get(0))
115            .map_err(|e| e.to_string())?
116            .filter_map(|r| r.ok())
117            .collect();
118
119        let mut tables = Vec::new();
120        for name in names {
121            let row_count = self.get_row_count(&name).unwrap_or(0);
122            let columns = self.get_schema(&name).unwrap_or_default();
123            tables.push(TableInfo {
124                name,
125                row_count,
126                column_count: columns.len(),
127            });
128        }
129        Ok(tables)
130    }
131
132    pub fn list_views(&self) -> Result<Vec<String>, String> {
133        if self.is_parquet() {
134            return Ok(Vec::new());
135        }
136
137        let mut stmt = self
138            .conn
139            .prepare(
140                "SELECT table_name FROM information_schema.tables \
141                 WHERE table_schema = 'main' AND table_type = 'VIEW' \
142                 ORDER BY table_name",
143            )
144            .map_err(|e| e.to_string())?;
145
146        let views = stmt
147            .query_map([], |row| row.get(0))
148            .map_err(|e| e.to_string())?
149            .filter_map(|r| r.ok())
150            .collect();
151
152        Ok(views)
153    }
154
155    pub fn list_indexes(&self) -> Result<Vec<IndexInfo>, String> {
156        if self.is_parquet() {
157            return Ok(Vec::new());
158        }
159
160        let mut stmt = self
161            .conn
162            .prepare(
163                "SELECT index_name, table_name, is_unique \
164                 FROM duckdb_indexes() \
165                 WHERE schema_name = 'main' \
166                 ORDER BY index_name",
167            )
168            .map_err(|e| e.to_string())?;
169
170        let indexes: Vec<IndexInfo> = stmt
171            .query_map([], |row| {
172                Ok(IndexInfo {
173                    name: row.get(0)?,
174                    table_name: row.get(1)?,
175                    unique: row.get(2)?,
176                    columns: Vec::new(),
177                })
178            })
179            .map_err(|e| e.to_string())?
180            .filter_map(|r| r.ok())
181            .collect();
182
183        Ok(indexes)
184    }
185
186    pub fn get_schema(&self, table: &str) -> Result<Vec<ColumnInfo>, String> {
187        let mut stmt = self
188            .conn
189            .prepare(
190                "SELECT column_name, data_type, is_nullable, column_default \
191                 FROM information_schema.columns \
192                 WHERE table_schema = 'main' AND table_name = ? \
193                 ORDER BY ordinal_position",
194            )
195            .map_err(|e| e.to_string())?;
196
197        let columns = stmt
198            .query_map([table], |row| {
199                let nullable_str: String = row.get(2)?;
200                Ok(ColumnInfo {
201                    name: row.get(0)?,
202                    col_type: row.get(1)?,
203                    nullable: nullable_str == "YES",
204                    primary_key: false,
205                    default_value: row.get(3).ok(),
206                })
207            })
208            .map_err(|e| e.to_string())?
209            .filter_map(|r| r.ok())
210            .collect();
211
212        Ok(columns)
213    }
214
215    pub fn query_table(
216        &self,
217        table: &str,
218        limit: usize,
219        offset: usize,
220        sort: Option<Sort>,
221    ) -> Result<QueryResult, String> {
222        let order_clause = match &sort {
223            Some(s) => format!(
224                " ORDER BY \"{}\" {}",
225                s.column,
226                if s.ascending { "ASC" } else { "DESC" }
227            ),
228            None => String::new(),
229        };
230
231        let sql = format!(
232            "SELECT * FROM \"{}\"{} LIMIT {} OFFSET {}",
233            table, order_clause, limit, offset
234        );
235
236        let total = self.get_row_count(table).ok();
237        let mut result = self.run_query(&sql)?;
238        result.total_rows = total;
239        Ok(result)
240    }
241
242    pub fn run_query(&self, sql: &str) -> Result<QueryResult, String> {
243        let mut stmt = self.conn.prepare(sql).map_err(|e| e.to_string())?;
244        let mut result_rows = stmt.query([]).map_err(|e| e.to_string())?;
245
246        let columns: Vec<String> = result_rows
247            .as_ref()
248            .expect("query should return rows")
249            .column_names();
250
251        let col_count = columns.len();
252        let mut rows = Vec::new();
253
254        while let Some(row) = result_rows.next().map_err(|e| e.to_string())? {
255            let mut cells = Vec::new();
256            for i in 0..col_count {
257                let val = match row.get_ref(i) {
258                    Ok(ValueRef::Null) => CellValue::Null,
259                    Ok(ValueRef::Int(n)) => CellValue::Integer(n as i64),
260                    Ok(ValueRef::BigInt(n)) => CellValue::Integer(n),
261                    Ok(ValueRef::TinyInt(n)) => CellValue::Integer(n as i64),
262                    Ok(ValueRef::SmallInt(n)) => CellValue::Integer(n as i64),
263                    Ok(ValueRef::HugeInt(n)) => CellValue::Text(n.to_string()),
264                    Ok(ValueRef::Float(f)) => CellValue::Real(f as f64),
265                    Ok(ValueRef::Double(f)) => CellValue::Real(f),
266                    Ok(ValueRef::Text(s)) => {
267                        CellValue::Text(String::from_utf8_lossy(s).to_string())
268                    }
269                    Ok(ValueRef::Blob(b)) => CellValue::Blob(b.to_vec()),
270                    Ok(other) => CellValue::Text(format!("{:?}", other)),
271                    Err(_) => CellValue::Null,
272                };
273                cells.push(val);
274            }
275            rows.push(cells);
276        }
277
278        Ok(QueryResult {
279            columns,
280            rows,
281            total_rows: None,
282        })
283    }
284
285    pub fn get_row_count(&self, table: &str) -> Result<u64, String> {
286        self.conn
287            .query_row(
288                &format!("SELECT COUNT(*) FROM \"{}\"", table),
289                [],
290                |row| row.get::<_, i64>(0),
291            )
292            .map(|n| n as u64)
293            .map_err(|e| e.to_string())
294    }
295
296    pub fn export_csv<W: Write>(&self, table: &str, writer: &mut W) -> Result<(), String> {
297        let result = self.run_query(&format!("SELECT * FROM \"{}\"", table))?;
298        write_csv(&result, writer)
299    }
300}