• Stars
    star
    246
  • Rank 164,726 (Top 4 %)
  • Language
    Rust
  • License
    Apache License 2.0
  • Created almost 2 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A framework for writing fast and performant SQLite extensions in Rust

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.

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!

More Repositories

1

sqlite-vss

A SQLite extension for efficient vector search, based on Faiss!
C++
482
star
2

sqlite-lines

A SQLite extension for reading large files line-by-line (NDJSON, logs, txt, etc.)
C
371
star
3

dataflow

An experimental self-hosted Observable notebook editor, with support for FileAttachments, Secrets, custom standard libraries, and more!
JavaScript
346
star
4

sqlite-html

A SQLite extension for querying, manipulating, and creating HTML elements.
Go
341
star
5

sqlite-http

A SQLite extension for making HTTP requests purely in SQL
Go
173
star
6

sqlite-regex

A fast regular expression SQLite extension, written in Rust
Rust
120
star
7

unofficial-observablehq-compiler

An unofficial compiler for Observable notebook syntax
JavaScript
99
star
8

sqlite-xsv

the fastest CSV SQLite extension, written in Rust
Rust
88
star
9

sqlite-ulid

A SQLite extension for generating and working with ULIDs
Python
79
star
10

streamlit-observable

Embed Observable notebooks into Streamlit apps!
TypeScript
79
star
11

observable-prerender

Pre-render Observable notebooks for automation
JavaScript
55
star
12

sqlite-ecosystem

An overview of all my SQLite extensions, and a roadmap for future extensions and tooling!
TypeScript
53
star
13

sqlite-jsonschema

A SQLite extension for validating JSON objects with JSON Schema
TypeScript
25
star
14

sqlite-geo

A work-in-progress SQLite extension for geospatial data
Rust
24
star
15

sqlite-url

A SQLite extension for parsing, generating, and querying URLs and query strings
C
22
star
16

sqlite-path

A SQLite extension for parsing, generating, and querying paths
C
19
star
17

sqlite-fastrand

A SQLite extension for quickly generating random numbers, booleans, characters, and blobs
Rust
16
star
18

sqlite-vector

A SQLite extension for working with float and binary vectors. Work in progress!
C++
14
star
19

atom-observable

Render Observable notebooks in Atom!
JavaScript
11
star
20

oak

A CLI tool for reproducible, customizable data workflows.
TypeScript
11
star
21

sqlite-base64

Python
8
star
22

sqlite-python

Rust
7
star
23

sqlite-md

A SQLite extension for parsing, querying, and generating HTML from Markdown documents.
Rust
6
star
24

sqlite-parquet

Rust
6
star
25

churro

Access your terminal in an Observable notebook!
JavaScript
5
star
26

sqlite-image

Rust
4
star
27

sqlite-hello

Elixir
4
star
28

TritonFind

A Facebook Messenger Bot that assists UC San Diego students get their lost items back.
JavaScript
4
star
29

sqlite.org-extensions

C
4
star
30

NeverAgainColleges

HTML
3
star
31

sqlite-xml

Rust
3
star
32

ondemand-whisper-fly

Python
3
star
33

streamlit-observable-showcase

An example Streamlit app showcasing the features of the streamlit-observable package.
Python
3
star
34

os-club-constitution

Constitution for a (future) open source club at UCSD
3
star
35

UCSD-Professional-Website-Maker

Make a very professional website for your acsweb.ucsd.edu/~ account
HTML
3
star
36

sqlite-pdf

Rust
2
star
37

unofficial-observable-notebook-react

JavaScript
2
star
38

disney-plus-analysis

Python
2
star
39

har-to-sqlite

TypeScript
2
star
40

shl

A tagged template literal for shell commands! (Work in Progress)
JavaScript
2
star
41

upload-spm

TypeScript
2
star
42

sqlite-jiff

Rust
2
star
43

SDHacks-2016---Trivents-Messenger-Bot

Messenger Bot our team made at SDHacks 2016, Winner of the DocuSign Sponsor Prize
JavaScript
1
star
44

sqlite-versions

TypeScript
1
star
45

sqlite-fake

Rust
1
star
46

sqlite-semver

Rust
1
star
47

sqlite-rembed

Rust
1
star
48

native-lands-colleges

JavaScript
1
star
49

isthegovernmentshutdown.info

HTML
1
star
50

TritonPal-Chrome-Extension

A Chrome Extension that will help all UC San Diego students.
JavaScript
1
star