9个版本 (3个稳定版)

新增 3.1.0 2024年8月7日
3.0.0 2024年1月15日
1.0.0 2023年9月28日
0.2.2 2023年9月27日
0.1.1 2022年6月9日

过程宏 中排名第 715

Download history 849/week @ 2024-04-17 670/week @ 2024-04-24 353/week @ 2024-05-01 423/week @ 2024-05-08 704/week @ 2024-05-15 734/week @ 2024-05-22 1311/week @ 2024-05-29 980/week @ 2024-06-05 614/week @ 2024-06-12 555/week @ 2024-06-19 801/week @ 2024-06-26 753/week @ 2024-07-03 786/week @ 2024-07-10 779/week @ 2024-07-17 1177/week @ 2024-07-24 611/week @ 2024-07-31

每月下载量 3,510
9 个crate中使用 (通过 tarantool)

BSD-2-Clause

57KB
1K SLoC

Tarantool Rust SDK

Latest Version Docs badge

Tarantool Rust SDK为从Rust应用程序交互提供库。本文档描述了Tarantool的Rust API绑定,包括以下API

  • Box:空间、索引、序列
  • Fibers:纤维属性、条件变量、闩锁、异步运行时
  • CoIO
  • 事务
  • 模式管理
  • 协议实现 (net.box):CRUD、存储过程调用、触发器
  • 元组工具
  • 日志(参见 https://docs.rs/log/
  • 错误处理

链接

另请参阅

入门指南

以下说明将帮助您在本地机器上获取项目的副本并运行。有关部署,请参阅文件末尾的部署说明。

先决条件

  • Tarantool 2.10+

macOS中的链接问题

在macOS上,您可能会遇到如下链接错误:ld: symbol(s) not found for architecture x86_64。要解决此问题,请将以下行添加到您的 $CARGO_HOME/config.toml (默认为 ~/.cargo/config.toml)中。

[target.x86_64-apple-darwin]
rustflags = [
    "-C", "link-arg=-undefined",  "-C", "link-arg=dynamic_lookup"
]

用法

将以下行添加到您项目的 Cargo.toml 中

[dependencies]
tarantool = "4.0"

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

有关示例用法,请参阅 https://github.com/picodata/brod

功能

  • net_box - 启用协议实现(默认启用)
  • schema - 启用模式操作工具(目前处于开发中)

存储过程

有几个方法可以将 Rust 代码集成到 Tarantool 中。可以使用插件、Lua 到 Rust FFI 代码生成器或存储过程。在此文件中,我们只介绍第三种方法,即 Rust 存储过程。尽管 Tarantool 总是将 Rust 程序视为 "C 函数",但我们仍然使用 "存储过程" 这个术语,作为约定和历史的理由。

本教程包含以下简单步骤

  1. examples/easy - 打印 "hello world";
  2. examples/harder - 解码传入的参数值;
  3. examples/hardest - 使用此库执行 DBMS 插入;
  4. examples/read - 使用此库执行 DBMS 查询;
  5. examples/write - 使用此库执行 DBMS 替换。

我们的示例为希望自信地开始编写自己的存储过程的用户提供了良好的起点。

创建 Cargo 项目

安装必备条件后,按照以下步骤操作

  1. 创建 Cargo 项目
$ cargo init --lib
  1. 将以下行添加到 Cargo.toml
[package]
name = "easy"
version = "0.1.0"
edition = "2018"
# author, license, etc

[dependencies]
tarantool = "4.0"
serde = "1.0"

[lib]
crate-type = ["cdylib"]
  1. 使用以下脚本创建名为 init.lua 的服务器入口点
box.cfg({listen = 3301})
box.schema.func.create('easy', {language = 'C', if_not_exists = true})
box.schema.func.create('easy.easy2', {language = 'C', if_not_exists = true})
box.schema.user.grant('guest', 'execute', 'function', 'easy', {if_not_exists = true})
box.schema.user.grant('guest', 'execute', 'function', 'easy.easy2', {if_not_exists = true})

要了解更多关于上述命令的信息,请查阅 Tarantool 文档中的语法和用法细节

  1. 编辑 lib.rs 文件并添加以下行
use tarantool::proc;

#[proc]
fn easy() {
    println!("hello world");
}

#[proc]
fn easy2() {
    println!("hello world -- easy2");
}

我们现在准备好提供一些用法示例。我们将展示三个难度级别的函数调用,从基本用法示例(easy)到更复杂的共享库(harderhardest)示例。此外,还将有单独的读取和写入数据示例。

基本用法示例

编译应用程序并启动服务器

$ cargo build
$ LUA_CPATH=target/debug/lib?.so tarantool init.lua

检查生成的 .so 文件是否位于之前指定的 LUA_CPATH 上,以便下一行正常工作。

尽管 Rust 和 Lua 布局约定不同,但我们可以利用 Lua 的灵活性,并通过显式设置 LUA_CPATH 环境变量来固定它,如上所示。

现在您可以发出一些请求了。打开单独的控制台窗口,并以客户端方式运行 Tarantool。将以下内容粘贴到控制台中

conn = require('net.box').connect(3301)
conn:call('easy')

如有必要,请查看 net.box 模块文档。

上面的代码建立了一个服务器连接并调用了 'easy' 函数。由于 easy() 函数在 lib.rs 中以 println!("hello world") 开始,因此在服务器控制台输出中会出现 "hello world" 字符串。

该代码还检查了调用是否成功。由于 easy() 函数在 lib.rs 中以 return 0 结束,因此没有错误消息显示,请求结束。

现在让我们在lib.rs中调用另一个函数,即 easy2()。与 easy() 函数的序列几乎相同,但有一个区别:如果文件名与函数名不匹配,我们必须明确指定 {文件名}{函数名}

conn:call('easy.easy2')

...这次的结果将是 hello world -- easy2

正如你所见,调用Rust函数非常简单。

检索调用参数

创建一个名为"harder"的新crate。将这些行放入 lib.rs

#[tarantool::proc]
fn harder(fields: Vec<i32>) {
    println!("field_count = {}", fields.len());

    for val in fields {
        println!("val={}", val);
    }
}

上述代码定义了一个存储过程,它接受一系列整数。

使用 cargo build 将程序编译成 harder.so 库。

现在回到客户端并执行这些请求

box.schema.func.create('harder', {language = 'C'})
box.schema.user.grant('guest', 'execute', 'function', 'harder')
passable_table = {}
table.insert(passable_table, 1)
table.insert(passable_table, 2)
table.insert(passable_table, 3)
capi_connection:call('harder', {passable_table})

这次调用传递一个Lua表格(passable_table)到 harder() 函数。该 harder() 函数将检测它,正如我们在上面的例子中用 args 部分编码的那样。

控制台输出现在应该如下所示

tarantool> capi_connection:call('harder', {passable_table})
field_count = 3
val=1
val=2
val=3
---
- []
...

正如你所见,解码传递给Rust函数的参数值可能很棘手,因为它需要编写额外的例程。

访问Tarantool空间

创建一个名为"hardest"的新crate。将这些行放入 lib.rs

use serde::{Deserialize, Serialize};

use tarantool::{
    proc,
    space::Space,
    tuple::{Encode, Tuple},
};

#[derive(Serialize, Deserialize)]
struct Row {
    pub int_field: i32,
    pub str_field: String,
}

impl Encode for Row {}

#[proc]
fn hardest() -> Tuple {
    let mut space = Space::find("capi_test").unwrap();
    let result = space.insert(&Row {
        int_field: 10000,
        str_field: "String 2".to_string(),
    });
    result.unwrap()
}

这次Rust函数执行以下操作

  1. 通过调用 Space::find() 方法查找 capi_test 空间;
  2. 将行结构序列化到自动模式的元组中;
  3. 使用 .insert() 插入元组。

使用 cargo build 将程序编译成 hardest.so 库。

现在回到客户端并执行这些请求

box.schema.func.create('hardest', {language = "C"})
box.schema.user.grant('guest', 'execute', 'function', 'hardest')
box.schema.user.grant('guest', 'read,write', 'space', 'capi_test')
capi_connection:call('hardest')

此外,在客户端执行另一个请求

box.space.capi_test:select()

结果应该如下所示

tarantool> box.space.capi_test:select()
---
- - [10000, 'String 2']
...

上述证明了 hardest() 函数已经成功。

读取示例

创建一个新的crate "read"。将这些行放入 lib.rs

use serde::{Deserialize, Serialize};

use tarantool::{
    proc,
    space::Space,
    tuple::Encode,
};

#[derive(Serialize, Deserialize, Debug)]
struct Row {
    pub int_field: i32,
    pub str_field: String,
}

impl Encode for Row {}

#[proc]
fn read() {
    let space = Space::find("capi_test").unwrap();

    let key = 10000;
    let result = space.get(&(key,)).unwrap();
    assert!(result.is_some());

    let result = result.unwrap().decode::<Row>().unwrap();
    println!("value={:?}", result);
}

上述代码执行以下操作

  1. 通过调用 Space::find() 查找 capi_test 空间;
  2. 使用Rust元组字面量(序列化结构的替代方案)格式化搜索键 = 10000;
  3. 使用 .get() 获取元组;
  4. 反序列化结果。

使用 cargo build 将程序编译成 read.so 库。

现在回到客户端并执行这些请求

box.schema.func.create('read', {language = "C"})
box.schema.user.grant('guest', 'execute', 'function', 'read')
box.schema.user.grant('guest', 'read,write', 'space', 'capi_test')
capi_connection:call('read')

capi_connection:call('read') 的结果应该如下所示

tarantool> capi_connection:call('read')
uint value=10000.
string value=String 2.
---
- []
...

上述证明,read() 函数已成功。

编写示例

创建一个名为 "write" 的新crate。将这些行放入 lib.rs

use tarantool::{
    proc,
    error::Error,
    fiber::sleep,
    space::Space,
    transaction::transaction,
};

#[proc]
fn write() -> Result<(i32, String), String> {
    let mut space = Space::find("capi_test")
        .ok_or_else(|| "Can't find space capi_test".to_string())?;

    let row = (1, "22".to_string());

    transaction(|| -> Result<(), Error> {
        space.replace(&row)?;
        Ok(())
    })
    .unwrap();

    sleep(std::time::Duration::from_millis(1));
    Ok(row)
}

上述代码执行以下操作

  1. 通过调用 Space::find() 查找 capi_test 空间;
  2. 准备行值;
  3. 启动事务;
  4. box.space.capi_test 中替换元组
  5. 完成事务
    • 在闭包上接收到 Ok() 时执行提交
    • 在接收到 Error() 时执行回滚;
  6. 将整个元组返回给调用者,并让调用者显示它。

使用 cargo build 将程序编译到 write.so 库中。

现在回到客户端并执行这些请求

box.schema.func.create('write', {language = "C"})
box.schema.user.grant('guest', 'execute', 'function', 'write')
box.schema.user.grant('guest', 'read,write', 'space', 'capi_test')
capi_connection:call('write')

capi_connection:call('write') 的结果应如下所示

tarantool> capi_connection:call('write')
---
- [[1, '22']]
...

上述证明,write() 函数已成功。

如您所见,Rust "存储过程" 可以完全访问数据库。

清理

  • 使用 box.schema.func.drop 删除每个函数元组。
  • 使用 box.schema.capi_test:drop() 删除 capi_test 空间。
  • 删除为本次教程创建的 *.so 文件。

运行测试

要调用自动测试,请运行

make
make test

有关测试结构和添加测试的更多信息,请参阅 测试自述文件

贡献

欢迎提交请求。对于重大更改,请先打开一个问题来讨论您想要更改的内容。

请确保根据需要更新测试。

版本

我们使用 SemVer 进行版本控制。有关可用的版本,请参阅 此存储库的标签

作者

  • Anton Melnikov
  • Dmitriy Koltsov
  • Georgy Moshkin
  • Egor Ivkov

© 2020-2022 Picodata.io https://git.picodata.io/picodata

许可证

本项目受 BSD 许可证的许可 - 详细内容请参阅 LICENSE 文件

依赖关系

~2MB
~43K SLoC