11 个版本

0.0.6-alpha.62023年10月6日
0.0.6-alpha.22023年8月19日
0.0.5 2023年1月10日
0.0.3 2022年12月7日
0.0.2 2022年11月30日

#1 in #sqlite-extension

Download history 42/week @ 2024-04-22 23/week @ 2024-04-29 15/week @ 2024-05-06 39/week @ 2024-05-13 42/week @ 2024-05-20 106/week @ 2024-05-27 97/week @ 2024-06-03 30/week @ 2024-06-10 63/week @ 2024-06-17 66/week @ 2024-06-24 41/week @ 2024-07-01 31/week @ 2024-07-08 39/week @ 2024-07-15 35/week @ 2024-07-22 111/week @ 2024-07-29 41/week @ 2024-08-05

228 每月下载量
8 crate 使用

MIT/Apache 协议

8.5MB
163K SLoC

C 160K SLoC // 0.3% comments Rust 2.5K SLoC // 0.0% comments Go 194 SLoC // 0.1% comments Shell 44 SLoC // 0.3% comments SQL 8 SLoC

sqlite-loadable-rs

Latest Version Documentation

一个用于在 Rust 中构建可加载 SQLite 扩展的框架。受 rusqlitepgx 和 Riyaz Ali 的类似 SQLite Go 库 的启发。有关更多详情,请参阅 介绍 sqlite-loadable-rs:构建 SQLite 扩展的 Rust 框架 (2022年12月)。

如果您所在的公司或组织觉得这个库很有用,请考虑 支持我的工作


注意 目前仍处于测试版,代码非常不稳定且不安全!请关注仓库以获取新版本发布,或 订阅我的通讯/RSS 源 以获取未来更新。


背景

SQLite 的 运行时可加载扩展 允许您向 SQLite 数据库连接添加新的标量函数、表函数、虚拟表、虚拟文件系统等。这些编译的 动态链接库 可在包括 SQLite CLI、Python、Node.js、Rust、Go 等多种语言的任何 SQLite 环境中加载。

注意 注意单词 可加载。可加载扩展是指这些动态链接库,后缀为 .dylib.so.dll(取决于您的操作系统)。这些与许多语言客户端支持的应用定义函数不同(例如Python的.create_function()或Node.js的.function())。

从历史上看,创建这些可加载SQLite扩展的主要方式是使用C/C++,例如spatilite,令人惊叹的sqlean项目或SQLite的官方杂项扩展

但是C语言难以安全使用,并且集成第三方库可能是一个噩梦。Riyaz Ali编写了一个Go库,允许用户轻松地用Go编写可加载扩展,但它带来了很大的性能成本和二进制文件大小。对于Rust,rusqlite在该库中尝试添加可加载扩展支持的几个不同的PR,但都未被合并。

因此,sqlite-loadable-rs是第一个也是最复杂的Rust编写可加载SQLite扩展的框架!

特性

标量函数

标量函数是您可以添加到SQLite的最简单函数 - 以值作为输入,并返回一个值作为输出。要在sqlite-loadable-rs中实现它,只需在装饰有#[sqlite_entrypoint]的“回调”Rust函数上调用define_scalar_function即可,然后您就可以从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

有关更多信息,请参阅define_scalar_function

表函数

可以通过define_table_function将表函数添加到您的扩展中。

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

定义表函数很复杂,需要大量的代码 - 请参阅characters.rs示例以获取完整解决方案。

一旦编译,您就可以像查询任何其他表一样调用表函数,使用表函数支持的任何参数。

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     │
└───────┴───────┘

SQLite中一些真实的非Rust示例表函数

虚拟表

sqlite-loadable-rs还支持更传统的虚拟表,用于具有动态模式或需要插入/更新支持的表。

define_virtual_table() 可以为指定的 SQLite 连接定义一个新的只读虚拟表模块。 define_virtual_table_writeable() 也适用于支持 INSERT/UPDATE/DELETE 的表,但这个 API 可能会更改。

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

这些虚拟表可以使用 SQL 中的 CREATE VIRTUAL TABLE 语法创建。


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

select * from xxx;

SQLite 中传统虚拟表的几个真实世界非 Rust 示例包括 CSV 虚拟表、全文搜索 fts5 扩展R-Tree 扩展

示例

examples/ 目录中有一些建立扩展的简单示例,您可以使用以下命令构建

$ 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

使用 sqlite-loadable-rs 的真实世界项目

  • sqlite-xsv - SQLite 中一个非常快速的 CSV/TSV 解析器
  • sqlite-regex - SQLite 中一个非常快速且安全的正则表达式库
  • sqlite-base64 - SQLite 中的快速 base64 编码和解码

我计划在不久的将来发布更多扩展!

使用方法

cargo init --lib 创建一个新项目,并将 sqlite-loadable 添加到 Cargo.toml 中的依赖项。

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

[dependencies]
sqlite-loadable = "0.0.3"

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

然后,用 "hello world" 扩展填充您的 src/lib.rs

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(())
}

构建它 cargo build,启动 SQLite CLI,并测试您的新扩展!

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

(MacOS 解决方案)

基准测试

更多详细信息请参阅 benchmarks/,但一般来说,使用 sqlite-loadable-rs 构建的 "hello world" 扩展比用 C 构建的要慢 10-15%,但比用 Go 编写的 riyaz-ali/sqlite 扩展快几个数量级(20-30 倍)。

然而,这取决于您的扩展实际上 做了什么 - 在现实生活中很少需要 "hello world" 类型的扩展。例如,sqlite-xsv 比 "官方" 的用 C 编写的 CSV SQLite 扩展 快 1.5-1.7 倍,而 sqlite-regexregexp 扩展快 2 倍。

注意事项

重用 unsafe Rust

sqlite-loadable-rs 大量使用 SQLite C API,这意味着需要使用 unsafe 代码。我会尽力让它尽可能安全,而且 SQLite 本身是 世界上最经过充分测试的 C 代码库之一,但您永远不能确定!

可能在多线程环境中不起作用

仅仅因为我还没有测试过。如果你使用SQLite的"序列化模式"或者设置了-DSQLITE_THREADSAFE=1,那么我不确定sqlite-loadable-rs是否能够按预期工作。如果你尝试了并且发现问题,请提交一个issue!

与rusqlite不兼容

如果你已经有了使用rusqlite编写标量函数或虚拟表的Rust代码,你将无法在sqlite-loadable-rs中重用它。抱歉!

不过,如果你想在使用rusqlite的应用程序中使用用sqlite-loadable-rs构建的扩展,可以考虑使用Connection.load_extension()进行动态加载,或者使用Connection.handle() + sqlite3_auto_extension()进行静态编译。

可能无法编译成WASM

SQLite本身可以编译成WASM,你还可以在用emscripten编译之前将用C编写的扩展静态编译进去(见sqlite-linessqlite-path的示例)。

但是,对于sqlite-loadable-rs来说,不能这样做。据我所知,如果你有C依赖项,那么你无法轻松地将Rust项目编译成WASM。有像wasm32-unknown-emscripten这样的目标可能可以解决这个问题,但我还没有成功。但我不是emscripten或Rust/WASM的专家,所以如果你认为这有可能,请提交一个issue!

二进制文件大小更大

C语言编写的hello world扩展为17KB,而Rust编写的为469k。它仍然比使用riyaz-ali/sqlite的Go编写的扩展小得多,后者约为2.2M,但它仍然值得考虑。不过,它仍然小到你在大多数时候都不会注意到。

路线图

支持中

我(Alex 👋🏼)在这个项目上投入了大量的时间和精力,以及许多其他开源项目。如果你的公司或组织使用这个库(或者你很慷慨),那么请考虑支持我的工作,或者与朋友分享这个项目!

依赖项

~0.8–8.5MB
~169K SLoC