Limbo extension API
The limbo_ext crate simplifies the creation and registration of libraries meant to extend the functionality of Limbo, that can be loaded
like traditional sqlite3 extensions, but are able to be written in much more ergonomic Rust.
Currently supported features
- [ x ] Scalar Functions: Create scalar functions using the
scalar macro.
- [ x ] Aggregate Functions: Define aggregate functions with
AggregateDerive macro and AggFunc trait.
- [ x ] Virtual tables: Create a module for a virtual table with the
VTabModuleDerive macro and VTabCursor trait.
- [] VFS Modules
Installation
Add the crate to your Cargo.toml:
[features]
static = ["limbo_ext/static"]
[dependencies]
limbo_ext = { path = "path/to/limbo/extensions/core", features = ["static"] }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
mimalloc = { version = "*", default-features = false }
[lib]
crate-type = ["cdylib", "lib"]
cargo build will output a shared library that can be loaded by the following options:
CLI:
`.load target/debug/libyour_crate_name`
SQL:
SELECT load_extension('target/debug/libyour_crate_name')
Extensions can be registered with the register_extension! macro:
register_extension!{
scalars: { double }, aggregates: { Percentile },
vtabs: { CsvVTable },
}
Scalar Example:
use limbo_ext::{register_extension, Value, scalar};
#[scalar(name = "double", alias = "twice")]
fn double(&self, args: &[Value]) -> Value {
if let Some(arg) = args.first() {
match arg.value_type() {
ValueType::Float => {
let val = arg.to_float().unwrap();
Value::from_float(val * 2.0)
}
ValueType::Integer => {
let val = arg.to_integer().unwrap();
Value::from_integer(val * 2)
}
}
} else {
Value::null()
}
}
Aggregates Example:
use limbo_ext::{register_extension, AggregateDerive, AggFunc, Value};
#[derive(AggregateDerive)]
struct Percentile;
impl AggFunc for Percentile {
type State = (Vec<f64>, Option<f64>, Option<String>);
const NAME: &str = "percentile";
const ARGS: i32 = 2;
fn step(state: &mut Self::State, args: &[Value]) {
let (values, p_value, error) = state;
if let (Some(y), Some(p)) = (
args.first().and_then(Value::to_float),
args.get(1).and_then(Value::to_float),
) {
if !(0.0..=100.0).contains(&p) {
*error = Some("Percentile P must be between 0 and 100.".to_string());
return;
}
if let Some(existing_p) = *p_value {
if (existing_p - p).abs() >= 0.001 {
*error = Some("P values must remain consistent.".to_string());
return;
}
} else {
*p_value = Some(p);
}
values.push(y);
}
}
fn finalize(state: Self::State) -> Value {
let (mut values, p_value, error) = state;
if let Some(error) = error {
return Value::custom_error(error);
}
if values.is_empty() {
return Value::null();
}
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = values.len() as f64;
let p = p_value.unwrap();
let index = (p * (n - 1.0) / 100.0).floor() as usize;
Value::from_float(values[index])
}
}
Virtual Table Example:
#[derive(Debug, VTabModuleDerive)]
struct CsvVTable;
impl VTabModule for CsvVTable {
type VCursor = CsvCursor;
const NAME: &'static str = "csv_data";
fn connect(api: &ExtensionApi) -> ResultCode {
let sql = "CREATE TABLE csv_data(
name TEXT,
age TEXT,
city TEXT
)";
api.declare_virtual_table(Self::NAME, sql)
}
fn open() -> Self::VCursor {
let csv_content = fs::read_to_string("data.csv").unwrap_or_default();
let rows: Vec<Vec<String>> = csv_content
.lines()
.skip(1)
.map(|line| {
line.split(',')
.map(|s| s.trim().to_string())
.collect()
})
.collect();
CsvCursor { rows, index: 0 }
}
fn filter(_cursor: &mut Self::VCursor, _arg_count: i32, _args: &[Value]) -> ResultCode {
ResultCode::OK
}
fn column(cursor: &Self::VCursor, idx: u32) -> Value {
cursor.column(idx)
}
fn next(cursor: &mut Self::VCursor) -> ResultCode {
if cursor.index < cursor.rows.len() - 1 {
cursor.index += 1;
ResultCode::OK
} else {
ResultCode::EOF
}
}
fn eof(cursor: &Self::VCursor) -> bool {
cursor.index >= cursor.rows.len()
}
}
#[derive(Debug)]
struct CsvCursor {
rows: Vec<Vec<String>>,
index: usize,
}
impl VTabCursor for CsvCursor {
fn next(&mut self) -> ResultCode {
CsvCursor::next(self)
}
fn eof(&self) -> bool {
self.index >= self.rows.len()
}
fn column(&self, idx: u32) -> Value {
let row = &self.rows[self.index];
if (idx as usize) < row.len() {
Value::from_text(&row[idx as usize])
} else {
Value::null()
}
}
fn rowid(&self) -> i64 {
self.index as i64
}
}