limbo/extensions/core
2025-01-17 20:43:10 -05:00
..
src Move limbo_extension crate to extensions/core 2025-01-16 14:39:12 +02:00
Cargo.toml Rename limbo_extension crate to limbo_ext 2025-01-16 14:40:52 +02:00
README.md Add documentation for extensions/core 2025-01-17 20:43:10 -05:00

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 ScalarDerive derive macro and Scalar trait.
  • [ x ] Aggregate Functions: Define aggregate functions with AggregateDerive macro and AggFunc trait.
  • [] Virtual tables: TODO

Installation

Add the crate to your Cargo.toml:

[dependencies]
limbo_ext = { path = "path/to/limbo/extensions/core" } # temporary until crate is published

NOTE Crate must be of type cdylib

[lib]
crate-type = ["cdylib"]

cargo build will output a shared library that can be loaded with .load target/debug/libyour_crate_name

Extensions can be registered with the register_extension! macro:


register_extension!{
    scalars: { Double },
    aggregates: { Percentile },
}

Scalar Example:

use limbo_ext::{register_extension, Value, ScalarDerive, Scalar};

/// Annotate each with the ScalarDerive macro, and implement the Scalar trait on your struct
#[derive(ScalarDerive)]
struct Double;

impl Scalar for Double {
    fn name(&self) -> &'static str { "double" }
    fn call(&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()
        }
    }
    /// OPTIONAL: 'alias' if you would like to provide an additional name
   fn alias(&self) -> &'static str { "twice" }
}


### Aggregates Example:

```rust

use limbo_ext::{register_extension, AggregateDerive, AggFunc, Value};
/// annotate your struct with the AggregateDerive macro, and it must implement the below AggFunc trait
#[derive(AggregateDerive)]
struct Percentile;

impl AggFunc for Percentile {
    /// The state to track during the steps
    type State = (Vec<f64>, Option<f64>, Option<String>); // Tracks the values, Percentile, and errors

    /// Define the name you wish to call your function by. 
    /// e.g. SELECT percentile(value, 40);
    fn name(&self) -> &'static str {
        "percentile"
    }

    /// Define the number of arguments your function takes
    fn args(&self) -> i32 {
        2
    }
    /// Define a function called on each row/value in a relevant group/column
    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);
        }
    }
    /// A function to finalize the state into a value to be returned as a result
    /// or an error (if you chose to track an error state as well)
    fn finalize(state: Self::State) -> Value {
        let (mut values, p_value, error) = state;

        if let Some(error) = error {
            return Value::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])
    }
}