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 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}