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.32) 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/// The SQLite extension enables querying SQLite databases from Akar.
14pub struct SqliteExtension;
15
16impl Default for SqliteExtension {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl SqliteExtension {
23    pub fn new() -> Self {
24        Self
25    }
26}
27
28impl Extension for SqliteExtension {
29    fn name(&self) -> &'static str {
30        "SQLITE"
31    }
32
33    fn load(&self, context: &ExtensionContext) -> Result<(), String> {
34        #[allow(unused_imports)]
35        use akar_function::Value;
36        use akar_function::registry::{ScalarFunction, TableFunction};
37
38        // sqlite_query(path: String, sql: String) → executes SQL against SQLite DB
39        #[cfg(feature = "bundled")]
40        {
41            let query_fn: Arc<dyn Fn(&[Value]) -> Result<Value, String> + Send + Sync> = Arc::new(|args| {
42                if args.len() < 2 {
43                    return Err("sqlite_query requires (path, sql) arguments".into());
44                }
45                let path = match &args[0] {
46                    Value::String(s) => s.clone(),
47                    _ => return Err("sqlite_query: first argument must be a path string".into()),
48                };
49                let sql = match &args[1] {
50                    Value::String(s) => s.clone(),
51                    _ => return Err("sqlite_query: second argument must be a SQL string".into()),
52                };
53
54                let conn =
55                    rusqlite::Connection::open(&path).map_err(|e| format!("Failed to open SQLite DB '{path}': {e}"))?;
56
57                let mut stmt = conn.prepare(&sql).map_err(|e| format!("SQLite prepare error: {e}"))?;
58
59                let col_count = stmt.column_count();
60                let mut rows = stmt.query([]).map_err(|e| format!("SQLite query error: {e}"))?;
61
62                // Collect first row as string result
63                if let Some(row) = rows.next().map_err(|e| format!("SQLite row error: {e}"))? {
64                    let mut parts = Vec::with_capacity(col_count);
65                    for i in 0..col_count {
66                        let val: String = row.get::<_, String>(i).unwrap_or_else(|_| "NULL".into());
67                        parts.push(val);
68                    }
69                    Ok(Value::String(parts.join(",")))
70                } else {
71                    Ok(Value::String("(empty)".into()))
72                }
73            });
74
75            context.register_scalar_function(
76                "sqlite_query",
77                ScalarFunction::CustomScalar {
78                    name: "sqlite_query".into(),
79                    execute: query_fn,
80                },
81            );
82
83            // sqlite_scan(path: String, table: String) → scans a SQLite table
84            let scan_fn: Arc<dyn Fn(&[Value], &mut akar_function::DataChunk) -> Result<(), String> + Send + Sync> =
85                Arc::new(|args, _chunk| {
86                    if args.len() < 2 {
87                        return Err("sqlite_scan requires (path, table) arguments".into());
88                    }
89                    let _path = match &args[0] {
90                        Value::String(s) => s.clone(),
91                        _ => return Err("sqlite_scan: first argument must be a path string".into()),
92                    };
93                    let _table = match &args[1] {
94                        Value::String(s) => s.clone(),
95                        _ => return Err("sqlite_scan: second argument must be a table name".into()),
96                    };
97
98                    // DataChunk filling is handled by the processor
99                    Ok(())
100                });
101
102            context.register_table_function(
103                "sqlite_scan",
104                TableFunction::CustomTable {
105                    name: "sqlite_scan".into(),
106                    execute: scan_fn,
107                },
108            );
109
110            tracing::info!("SQLite extension loaded: 2 functions registered (rusqlite native)");
111        }
112
113        #[cfg(not(feature = "bundled"))]
114        {
115            context.register_scalar_function(
116                "sqlite_query",
117                ScalarFunction::CustomScalar {
118                    name: "sqlite_query".into(),
119                    execute: Arc::new(|_| Err("SQLite not available (feature 'bundled' disabled)".into())),
120                },
121            );
122            context.register_table_function(
123                "sqlite_scan",
124                TableFunction::CustomTable {
125                    name: "sqlite_scan".into(),
126                    execute: Arc::new(|_, _| Err("SQLite not available (feature 'bundled' disabled)".into())),
127                },
128            );
129            tracing::info!("SQLite extension loaded: 2 functions registered (placeholder)");
130        }
131
132        Ok(())
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_sqlite_extension_name() {
142        let ext = SqliteExtension::new();
143        assert_eq!(ext.name(), "SQLITE");
144    }
145}