11 个版本
0.0.6-alpha.6 | 2023年10月6日 |
---|---|
0.0.6-alpha.2 | 2023年8月19日 |
0.0.5 | 2023年1月10日 |
0.0.3 | 2022年12月7日 |
0.0.2 | 2022年11月30日 |
#1 in #sqlite-extension
228 每月下载量
被 8 crate 使用
8.5MB
163K SLoC
sqlite-loadable-rs
一个用于在 Rust 中构建可加载 SQLite 扩展的框架。受 rusqlite、pgx 和 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!
基准测试
更多详细信息请参阅 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-regex
比 regexp 扩展快 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-lines或sqlite-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