Giter Club home page Giter Club logo

sqlite-loadable-rs's Introduction

sqlite-loadable-rs

Latest Version Documentation

A framework for building loadable SQLite extensions in Rust. Inspired by rusqlite, pgx, and Riyaz Ali's similar SQLite Go library. See Introducing sqlite-loadable-rs: A framework for building SQLite Extensions in Rust (Dec 2022) for more details!

If your company or organization finds this library useful, consider supporting my work!


Warning Still in beta, very unstable and unsafe code! Watch the repo for new releases, or follow my newsletter/RSS feed for future updates.


Background

SQLite's runtime loadable extensions allows one to add new scalar functions, table functions, virtual tables, virtual filesystems, and more to a SQLite database connection. These compiled dynamically-linked libraries can be loaded in any SQLite context, including the SQLite CLI, Python, Node.js, Rust, Go, and many other languages.

Note Notice the word loadable. Loadable extensions are these compiled dynamically-linked libraries, with a suffix of .dylib or .so or .dll (depending on your operating system). These are different than application-defined functions that many language clients support (such as Python's .create_function() or Node.js's .function()).

Historically, the main way one could create these loadable SQLite extensions were with C/C++, such as spatilite, the wonderful sqlean project, or SQLite's official miscellaneous extensions.

But C is difficult to use safely, and integrating 3rd party libraries can be a nightmare. Riyaz Ali wrote a Go library that allows one to easily write loadable extensions in Go, but it comes with a large performance cost and binary size. For Rust, rusqlite has had a few different PRs that attempted to add loadable extension support in that library, but none have been merged. UPDATE December 2023: as of rusqlite 0.30.0, they now support loadable extensions with the loadable_extension feature!

So, sqlite-loadable-rs is the first and most involved framework for writing loadable SQLite extensions in Rust!

Features

Scalar functions

Scalar functions are the simplest functions one can add to SQLite - take in values as inputs, and return a value as output. To implement one in sqlite-loadable-rs, you just need to call define_scalar_function on a "callback" Rust function decorated with #[sqlite_entrypoint], and you'll be able to call it from SQL!

// add(a, b)
fn add(context: *mut sqlite3_context, values: &[*mut sqlite3_value]) -> Result<()> {
    let a = api::value_int(values.get(0).expect("1st argument"));
    let b = api::value_int(values.get(1).expect("2nd argument"));
    api::result_int(context, a + b);
    Ok(())
}

// connect(seperator, string1, string2, ...)
fn connect(context: *mut sqlite3_context, values: &[*mut sqlite3_value]) -> Result<()> {
    let seperator = api::value_text(values.get(0).expect("1st argument"))?;
    let strings:Vec<&str> = values
        .get(1..)
        .expect("more than 1 argument to be given")
        .iter()
        .filter_map(|v| api::value_text(v).ok())
        .collect();
    api::result_text(context, &strings.join(seperator))?;
    Ok(())
}
#[sqlite_entrypoint]
pub fn sqlite3_extension_init(db: *mut sqlite3) -> Result<()> {
    define_scalar_function(db, "add", 2, add, FunctionFlags::DETERMINISTIC)?;
    define_scalar_function(db, "connect", -1, connect, FunctionFlags::DETERMINISTIC)?;
    Ok(())
}
sqlite> select add(1, 2);
3
sqlite> select connect('-', 'alex', 'brian', 'craig');
alex-brian-craig

See define_scalar_function for more info.

Table functions

Table functions, (aka "Eponymous-only virtual tables"), can be added to your extension with define_table_function.

define_table_function::<CharactersTable>(db, "characters", None)?;

Defining a table function is complicated and requires a lot of code - see the characters.rs example for a full solution.

Once compiled, you can invoke a table function like querying any other table, with any arguments that the table function supports.

sqlite> .load target/debug/examples/libcharacters
sqlite> select rowid, * from characters('alex garcia');
┌───────┬───────┐
│ rowid │ value │
├───────┼───────┤
│ 0     │ a     │
│ 1     │ l     │
│ 2     │ e     │
│ 3     │ x     │
│ 4     │       │
│ 5     │ g     │
│ 6     │ a     │
│ 7     │ r     │
│ 8     │ c     │
│ 9     │ i     │
│ 10    │ a     │
└───────┴───────┘

Some real-world non-Rust examples of table functions in SQLite:

Virtual tables

sqlite-loadable-rs also supports more traditional virtual tables, for tables that have a dynamic schema or need insert/update support.

define_virtual_table() can define a new read-only virtual table module for the given SQLite connection. define_virtual_table_writeable() is also available for tables that support INSERT/UPDATE/DELETE, but this API will probably change.

define_virtual_table::<CustomVtab>(db, "custom_vtab", None)?

These virtual tables can be created in SQL with the CREATE VIRTUAL TABLE syntax.

create virtual table xxx using custom_vtab(arg1=...);

select * from xxx;

Some real-world non-Rust examples of traditional virtual tables in SQLite include the CSV virtual table, the full-text search fts5 extension, and the R-Tree extension.

Examples

The examples/ directory has a few bare-bones examples of extensions, which you can build with:

$ cargo build --example hello
$ sqlite3 :memory: '.load target/debug/examples/hello' 'select hello("world");'
hello, world!

# Build all the examples in release mode, with output at target/debug/release/examples/*.dylib
$ cargo build --example --release

Some real-world projects that use sqlite-loadable-rs:

  • sqlite-xsv - An extremely fast CSV/TSV parser in SQLite
  • sqlite-regex - An extremely fast and safe regular expression library for SQLite
  • sqlite-base64 - Fast base64 encoding and decoding in SQLite

I plan to release many more extensions in the near future!

Usage

cargo init --lib a new project, and add sqlite-loadable to your dependencies in Cargo.toml.

[package]
name = "xyz"
version = "0.1.0"
edition = "2021"

[dependencies]
sqlite-loadable = "0.0.3"

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

Then, fill in your src/lib.rs with a "hello world" extension:

use sqlite_loadable::prelude::*;
use sqlite_loadable::{
  api,
  define_scalar_function, Result,
};

pub fn hello(context: *mut sqlite3_context, values: &[*mut sqlite3_value]) -> Result<()> {
    let name = api::value_text_notnull(values.get(0).expect("1st argument as name"))?;
    api::result_text(context, format!("hello, {}!", name))?;
    Ok(())
}

#[sqlite_entrypoint]
pub fn sqlite3_hello_init(db: *mut sqlite3) -> Result<()> {
    define_scalar_function(db, "hello", 1, hello, FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC)?;
    Ok(())
}

Build it cargo build, spin up the SQLite CLI, and try out your new extension!

$ sqlite3
sqlite> .load target/debug/libhello
sqlite> select hello('world');
hello, world!

(MacOS workaround)

Benchmarks

See more details at benchmarks/, but in general, a "hello world" extension built with sqlite-loadable-rs is about 10-15% slower than one built in C, and several orders of magnitude faster than extensions written in Go with riyaz-ali/sqlite (20-30x faster).

However, it depends on what your extension actually does - very rarely do you need a "hello world" type extension in real life. For example, sqlite-xsv is 1.5-1.7x faster than the "offical" CSV SQLite extension written in C, and sqlite-regex is 2x faster than the regexp extension.

Caveats

Heavy use of unsafe Rust

sqlite-loadable-rs uses the SQLite C API heavily, which means unsafe code. I try my best to make it as safe as possible, and it's good that SQLite itself is one of the most well-tested C codebases in the world, but you can never be sure!

Maybe doesn't work in multi-threaded environments

Just because I haven't tested it. If you use SQLite in "serialized mode" or with -DSQLITE_THREADSAFE=1, then I'm not sure if sqlite-loadable-rs will work as expected. If you try this and find problems, please file an issue!

Doesn't work with rusqlite

If you already have Rust code that uses rusqlite to make scalar functions or virtual tables, you won't be able to re-use it in sqlite-loadable-rs. Sorry!

Though if you want to use an extension built with sqlite-loadable-rs in an app that uses rusqlite, consider Connection.load_extension() for dynamic loading, or Connection.handle() + sqlite3_auto_extension() for static compilation.

Probably can't be compiled into WASM

SQLite by itself can be compiled into WASM, and you can also include extensions written in C if you compile those extensions statically before compiling with emscripten (see sqlite-lines or sqlite-path for examples).

However, the same can't be done with sqlite-loadable-rs. As far as I can tell, you can't easily compile a Rust project to WASM if there's a C dependency. There are projects like the wasm32-unknown-emscripten target that could maybe solve this, but I haven't gotten it to work yet. But I'm not an expert in emscripten or Rust/WASM, so if you think it's possible, please file an issue!

Larger binary size

A hello world extension in C is 17KB, while one in Rust is 469k. It's still much smaller than one in Go, which is around 2.2M using riyaz-ali/sqlite, but something to consider. It's still small enough where you won't notice most of the time, however.

Roadmap

Supporting

I (Alex 👋🏼) spent a lot of time and energy on this project and many other open source projects. If your company or organization uses this library (or you're feeling generous), then please consider supporting my work, or share this project with a friend!

sqlite-loadable-rs's People

Contributors

asg017 avatar nebkor avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sqlite-loadable-rs's Issues

wasm build?

Just a question -- have you been able to compile a Rust loadable extension into the WASM build of SQLite?

Considerations for your API

Hello, I saw your original publication on Hacker News and commented there. It seems like you continue working on this project, which is great, but I have spent a fair amount of effort also thinking about this problem and I can tell you about some of the problems that I encountered in my experience. I am offering you some unsolicited code review for your project. If this isn't welcome, feel free to stop reading here and close this issue 🙂

I'll be referencing sqlite3_ext a few times, for comparison, which I released after seeing your crate but I don't consider it ready yet because many APIs aren't supported. The license of my library is compatible with yours, so feel free to adopt anything you like.

General

  • What versions of SQLite are supported? If you load the extension and use features that aren't supported, does the library author have any way of detecting and handling this? I notice your ErrorKind doesn't have anything about versions. Given that library consumers are creating runtime-loadable extensions, I think this will be an important consideration for them.
  • You haven't done any abstraction of sqlite3_value yet, looks like.
    • I offer you sqlite3_ext::ValueRef, a safe Rust wrapper around sqlite3_value that supports all of the latest SQLite features for them. Please take special note of PassedRef, which is an extremely useful construct called the pointer passing interface. There's also UnsafePtr which is supported more widely on older versions of SQLite.
  • It's possible for library consumers to create statically linked extensions for both Rust and C programs, as well as loadable ones. Statically loaded extensions are conceptually easier since they target a fixed version of SQLite and you can make some guarantees about compatibility (enforced at link time).
    • Take a look at the link configurations I outline in my README, to see some configurations I've thought about and how I've enabled them.
  • You will need to create a querying interface; for example you can't implement the FTS5 interface without using SQLite for storing the index data. You can probably lift mine wholesale if you like, it's basically rusqlite's with some of the rough edges polished down (since I wasn't constrained by a preexisting API).

Virtual tables

  • Your collection of VTab configurations appears arbitrarily limited. There's no reason that an eponymous-only module ("table function") can't support transactions. You have no way to add support for find_function except through your define_virtual_table_writeablex function which seems to indicate that you're already unhappy with the way the API is going.
    • Take a look at how I handled this problem; it may be worthwhile to switch to an API closer to this one. It supports all configurations that SQLite supports and I think it's pretty ergonomic.
  • The aux data for VTabs doesn't need to be Option, if I want an optional field I'll make VTab::Aux optional directly.

Application-defined functions

  • You haven't wrapped sqlite3_context yet. Take a look at my wrapper for something you may be able to lift.
  • You haven't added support for aggregate functions yet. Take a look at my create_aggregate_function for something you may be able to lift. In particular, note that the AggregateFunction trait works as both a "legacy aggregate function" and a window-capable one, and this distinction can be supported with automatic fallback depending on the runtime version of SQLite.

Conclusion

Overall I'm happy to see that Rust support for SQLite loadable extensions is a wanted concept. The primary goal of my API was to be the most ergonomic way of creating SQLite extensions (completely eliminate the use of unsafe code in library consumers, configurable SQLite version targeting, etc.). I don't think there necessarily need to exist multiple versions of a Rust crate for creating SQLite loadable extensions, but I definitely respect if you have different goals for your project.

Happy to answer any questions about why I made some of the API considerations that I did, and again, feel free to lift anything you like from my library and modify it as you wish. Cheers!

Using from a single binary

From a deployment/usage perspective, it would be ideal if the resulting executable could contain both the statically-linked sqlite, as well as any extensions. Do you see any way to achieve this?

Prebuilt bindings

Right now the bindings are built at compile time for sqlite3ext-sys, usually it is preferred that libraries pre-built them and offer a feature flag to build them at runtime. This is what libsqlite3-sys does for example. It removes the need to support bindgen in your stack (which is a bit of a pain when doing cross-compilation).

Support Queries (prepared, execute all, etc.)

In order to properly support shadow tables or more advanced SQLite extensions, sqlite-loadable will need to expose some SQLite C APIs like sqlite3_prepare_v2() + sqlite3_step() + sqlite3_column_*(). That way, extensions can call CREATE TABLE xyz_foo() or INSERT INTO xyz_foo or SELECT * FROM xyz_foo for shadow tables or other more advanced usecases.

This will unblock a few features/extensions:

Planned API features:

  • Statement struct that prepares a sqlite3_stmt
    • create (sqlite3_prepare_v2)
    • bind parameters (sqlite3_bind_*())
    • iterate through results (sqlite3_step) (sqlite3_column_*())
    • Wrap sqlite3_mprintf() in rust for easier/safer string escaping, for shadow table creations

Compile issues on ARM architecture

I've been trying to build a SQLite extension on top of library. I am getting multiple compile issues on all ARM builds. Here is my workflow logs. Is there a plan to support ARM and architecture at-least? I believe it's important that we support ARM because most of mobile devices (and potential future macs/desktops are ARM).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.