Skip to main content

akar_sqlite/
lib.rs

1//! SQLite extension for Akar.
2//!
3//! Provides integration with SQLite databases using the native `rusqlite` crate.
4//! Supports attaching SQLite databases and executing queries against them.
5//!
6//! ## Native Rust approach
7//! Uses `rusqlite` (v0.40) with bundled feature for a self-contained build.
8//! No DuckDB dependency needed.
9
10use akar_extension::{Extension, ExtensionContext};
11use std::sync::Arc;
12
13/// Convert a single SQLite value to its string representation.
14///
15/// Unlike the previous `row.get::<_, String>` which failed on any non-text
16/// column (yielding a misleading "NULL"), this handles every SQLite type.
17#[cfg(feature = "bundled")]
18fn sqlite_value_to_string(val: &rusqlite::types::Value) -> String {
19    match val {
20        rusqlite::types::Value::Null => "NULL".into(),
21        rusqlite::types::Value::Integer(i) => i.to_string(),
22        rusqlite::types::Value::Real(f) => f.to_string(),
23        rusqlite::types::Value::Text(s) => s.clone(),
24        rusqlite::types::Value::Blob(b) => format!("<blob:{} bytes>", b.len()),
25    }
26}
27
28/// The SQLite extension enables querying SQLite databases from Akar.
29pub struct SqliteExtension;
30
31impl Default for SqliteExtension {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl SqliteExtension {
38    pub fn new() -> Self {
39        Self
40    }
41}
42
43impl Extension for SqliteExtension {
44    fn name(&self) -> &'static str {
45        "SQLITE"
46    }
47
48    fn load(&self, context: &ExtensionContext) -> Result<(), String> {
49        #[allow(unused_imports)]
50        use akar_function::Value;
51        use akar_function::registry::{ScalarFunction, TableFunction};
52
53        // sqlite_query(path: String, sql: String) → executes SQL against SQLite DB
54        #[cfg(feature = "bundled")]
55        {
56            let query_fn: Arc<dyn Fn(&[Value]) -> Result<Value, String> + Send + Sync> = Arc::new(|args| {
57                if args.len() < 2 {
58                    return Err("sqlite_query requires (path, sql) arguments".into());
59                }
60                let path = match &args[0] {
61                    Value::String(s) => s.clone(),
62                    _ => return Err("sqlite_query: first argument must be a path string".into()),
63                };
64                let sql = match &args[1] {
65                    Value::String(s) => s.clone(),
66                    _ => return Err("sqlite_query: second argument must be a SQL string".into()),
67                };
68
69                let conn =
70                    rusqlite::Connection::open(&path).map_err(|e| format!("Failed to open SQLite DB '{path}': {e}"))?;
71
72                let mut stmt = conn.prepare(&sql).map_err(|e| format!("SQLite prepare error: {e}"))?;
73
74                let col_count = stmt.column_count();
75                let mut rows = stmt.query([]).map_err(|e| format!("SQLite query error: {e}"))?;
76
77                // Collect every row (all columns) as strings, not just the first.
78                let mut parts = Vec::new();
79                while let Some(row) = rows.next().map_err(|e| format!("SQLite row error: {e}"))? {
80                    for i in 0..col_count {
81                        let val: rusqlite::types::Value = row
82                            .get::<_, rusqlite::types::Value>(i)
83                            .unwrap_or(rusqlite::types::Value::Null);
84                        parts.push(sqlite_value_to_string(&val));
85                    }
86                }
87                if parts.is_empty() {
88                    Ok(Value::String("(empty)".into()))
89                } else {
90                    Ok(Value::String(parts.join(",")))
91                }
92            });
93
94            context.register_scalar_function(
95                "sqlite_query",
96                ScalarFunction::CustomScalar {
97                    name: "sqlite_query".into(),
98                    execute: query_fn,
99                },
100            );
101
102            // sqlite_scan(path: String, table: String) → scans a SQLite table
103            let scan_fn: Arc<dyn Fn(&[Value], &mut akar_function::DataChunk) -> Result<(), String> + Send + Sync> =
104                Arc::new(|args, chunk| {
105                    if args.len() < 2 {
106                        return Err("sqlite_scan requires (path, table) arguments".into());
107                    }
108                    let path = match &args[0] {
109                        Value::String(s) => s.clone(),
110                        _ => return Err("sqlite_scan: first argument must be a path string".into()),
111                    };
112                    let table = match &args[1] {
113                        Value::String(s) => s.clone(),
114                        _ => return Err("sqlite_scan: second argument must be a table name".into()),
115                    };
116
117                    if chunk.size > 0 {
118                        return Ok(());
119                    }
120
121                    let conn = rusqlite::Connection::open(&path)
122                        .map_err(|e| format!("Failed to open SQLite DB '{path}': {e}"))?;
123
124                    let sql = format!("SELECT * FROM {}", akar_common::extension_utils::quote_sql_table_name(&table));
125                    let mut stmt = conn.prepare(&sql).map_err(|e| format!("SQLite prepare error: {e}"))?;
126                    let col_count = stmt.column_count();
127                    let names: Vec<String> = (0..col_count)
128                        .map(|i| stmt.column_name(i).map(str::to_string).unwrap_or_default())
129                        .collect();
130                    let mut rows = stmt.query([]).map_err(|e| format!("SQLite query error: {e}"))?;
131
132                    let mut columns: Vec<Vec<Option<String>>> = vec![Vec::new(); col_count];
133                    while let Some(row) = rows.next().map_err(|e| format!("SQLite row error: {e}"))? {
134                        for i in 0..col_count {
135                            let val: rusqlite::types::Value = row
136                                .get::<_, rusqlite::types::Value>(i)
137                                .unwrap_or(rusqlite::types::Value::Null);
138                            columns[i].push(Some(sqlite_value_to_string(&val)));
139                        }
140                    }
141
142                    chunk.fields.clear();
143                    chunk.field_types.clear();
144                    chunk.field_names.clear();
145                    for (col, name) in columns.into_iter().zip(names) {
146                        chunk.fields.push(
147                            std::sync::Arc::new(arrow::array::StringArray::from_iter(
148                                col.iter().map(|o| o.as_deref()),
149                            )) as arrow::array::ArrayRef,
150                        );
151                        chunk.field_types.push(akar_common::types::PhysicalTypeID::String);
152                        chunk.field_names.push(name);
153                    }
154                    chunk.size = chunk.fields.first().map(|f| f.len()).unwrap_or(0);
155                    Ok(())
156                });
157
158            context.register_table_function(
159                "sqlite_scan",
160                TableFunction::CustomTable {
161                    name: "sqlite_scan".into(),
162                    execute: scan_fn,
163                },
164            );
165
166            tracing::info!("SQLite extension loaded: 2 functions registered (rusqlite native)");
167        }
168
169        #[cfg(not(feature = "bundled"))]
170        {
171            context.register_scalar_function(
172                "sqlite_query",
173                ScalarFunction::CustomScalar {
174                    name: "sqlite_query".into(),
175                    execute: Arc::new(|_| Err("SQLite not available (feature 'bundled' disabled)".into())),
176                },
177            );
178            context.register_table_function(
179                "sqlite_scan",
180                TableFunction::CustomTable {
181                    name: "sqlite_scan".into(),
182                    execute: Arc::new(|_, _| Err("SQLite not available (feature 'bundled' disabled)".into())),
183                },
184            );
185            tracing::info!("SQLite extension loaded: 2 functions registered (placeholder)");
186        }
187
188        Ok(())
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_sqlite_extension_name() {
198        let ext = SqliteExtension::new();
199        assert_eq!(ext.name(), "SQLITE");
200    }
201}