#sqlite #database-schema #sql #编译时 #验证

turbosql

一个简单的本地数据持久层,由SQLite支持

18 个版本 (11 个重大更新)

0.11.0 2024年7月28日
0.10.0 2024年2月26日
0.9.0 2023年11月15日
0.8.0 2023年4月6日
0.0.1 2020年12月23日

#192数据库接口

Download history 38/week @ 2024-05-02 60/week @ 2024-05-09 41/week @ 2024-05-16 46/week @ 2024-05-23 140/week @ 2024-05-30 104/week @ 2024-06-06 85/week @ 2024-06-13 90/week @ 2024-06-20 81/week @ 2024-06-27 78/week @ 2024-07-04 59/week @ 2024-07-11 56/week @ 2024-07-18 211/week @ 2024-07-25 96/week @ 2024-08-01 60/week @ 2024-08-08 41/week @ 2024-08-15

每月416 次下载
用于 6 个Crates (直接使用5个)

MIT OR Apache-2.0 OR CC0-1.0

30KB
330

Turbosql

github crates.io docs.rs

一个简单的本地数据持久层,由SQLite支持。

  • 由您的Rust struct 自动定义模式
  • 自动模式迁移
  • 超级简单的基本 INSERT/SELECT/UPDATE/DELETE 操作
  • 如果需要,可以使用复杂的SQL
  • 在编译时验证所有SQL(包括用户提供的SQL)

使用方法

use turbosql::{Turbosql, select, execute};

#[derive(Turbosql, Default)]
struct Person {
    rowid: Option<i64>, // rowid member required & enforced at compile time
    name: Option<String>,
    age: Option<i64>,
    image_jpg: Option<Vec<u8>>
}

fn main() -> Result<(), Box<dyn std::error::Error>> {

    let name = "Joe";

    // INSERT a row
    let rowid = Person {
        name: Some(name.to_string()),
        age: Some(42),
        ..Default::default()
    }.insert()?;

    // SELECT all rows
    let people = select!(Vec<Person>)?;

    // SELECT multiple rows with a predicate
    let people = select!(Vec<Person> "WHERE age > " 21)?;

    // SELECT a single row with a predicate
    let mut person = select!(Person "WHERE name = " name)?;

    // UPDATE based on rowid, rewrites all fields in database row
    person.age = Some(43);
    person.update()?;

    // UPDATE with manual SQL
    execute!("UPDATE person SET age = " 44 " WHERE name = " name)?;

    // DELETE
    execute!("DELETE FROM person WHERE rowid = " 1)?;

    Ok(())
}

请参阅integration_test.rstrevyn/turbo 以获取更多使用示例!

内部结构

Turbosql为每个结构体生成SQLite模式和预编译查询

use turbosql::Turbosql;

#[derive(Turbosql, Default)]
struct Person {
    rowid: Option<i64>, // rowid member required & enforced
    name: Option<String>,
    age: Option<i64>,
    image_jpg: Option<Vec<u8>>
}

        ↓      自动生成并验证模式

CREATE TABLE person (
    rowid INTEGER PRIMARY KEY,
    name TEXT,
    age INTEGER,
    image_jpg BLOB,
) STRICT

INSERT INTO person (rowid, name, age, image_jpg) VALUES (?, ?, ?, ?)

SELECT rowid, name, age, image_jpg FROM person

带有SQL谓词的查询也在编译时组装和验证。请注意,参数绑定时SQL类型和Rust类型目前不在编译时进行检查。

let people = select!(Vec<Person> "WHERE age > ?", 21);

        ↓

SELECT rowid, name, age, image_jpg FROM person WHERE age > ?

自动模式迁移

在编译时,#[derive(Turbosql)] 宏运行并在您的项目根目录中创建一个 migrations.toml 文件,该文件描述了数据库模式。

每次您更改 struct 声明并重新运行宏(例如,通过 cargorust-analyzer),都会生成更新数据库模式的迁移SQL语句。这些新语句记录在 migrations.toml 中,并自动嵌入到您的二进制文件中。

#[derive(turbosql::Turbosql, Default)]
struct Person {
    rowid: Option<i64>,
    name: Option<String>
}

        ↓      自动生成 migrations.toml

migrations_append_only = [
  'CREATE TABLE person(rowid INTEGER PRIMARY KEY) STRICT',
  'ALTER TABLE person ADD COLUMN name TEXT',
]
output_generated_schema_for_your_information_do_not_edit = '''
  CREATE TABLE person (
    rowid INTEGER PRIMARY KEY,
    name TEXT
  ) STRICT
'''

当您的模式发生变化时,任何新的二进制版本都会自动将旧的数据库文件迁移到当前模式,通过按顺序应用适当的迁移。

此迁移过程是一个单向的齿轮:旧版本的二进制运行在具有较新模式的数据库文件上时,将检测到模式不匹配,并阻止操作在未来模式的数据库文件上。

在开发过程中创建的未使用或回滚的迁移可以在发布前从 migrations.toml 中手动删除,但已应用这些已删除迁移的任何数据库文件将出错,必须重新构建。请谨慎操作。如有疑问,请勿手动编辑 migrations.toml,一切应该都能正常工作。

  • 只需声明并自由地向您的 struct 中添加字段。
  • 查看项目根目录中生成的 migrations.toml 文件以了解情况。
  • 如果您遇到任何奇怪的编译器错误,请先尝试重新编译;根据 proc 宏的运行顺序,有时在模式更改后只需要一点推动即可同步。
  • 模式迁移是单向的,只可附加。这与 leafac/sqlite-migration 对 Node.js 生态系统采用的方法类似;请参阅该项目以了解优点的讨论!
  • 启动时,使用较新模式构建的二进制版本将自动将适当的迁移应用到较旧的数据库。
  • 如果您想尝试冒险,可以将您自己的模式迁移条目添加到列表底部。(例如创建索引等。)
  • 您还可以手动编写复杂的迁移,请参阅 turbo/migrations.toml 以获取一些示例。
  • 如有任何问题或建议,请打开 GitHub 问题!

我的数据在哪里?

SQLite 数据库文件位于由 directories_next::ProjectDirs::data_dir() 返回的目录中 + 您的可执行文件的名称,解析为类似

Linux

$XDG_DATA_HOME/{exe_name}$HOME/.local/share/{exe_name} /home/alice/.local/share/fooapp/fooapp.sqlite

macOS

$HOME/Library/Application Support/{exe_name} /Users/Alice/Library/Application Support/org.fooapp.fooapp/fooapp.sqlite

Windows

{FOLDERID_LocalAppData}\{exe_name}\data C:\Users\Alice\AppData\Local\fooapp\fooapp\data\fooapp.sqlite

事务和 async

SQLite以及许多通用文件系统仅提供阻塞(同步)API。在使用Rust的async生态系统中的阻塞API时,正确的方法是使用您的执行器在预期阻塞的线程池上运行闭包的功能。例如:

#[derive(turbosql::Turbosql, Default)]
struct Person {
    rowid: Option<i64>,
    name: Option<String>
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let person = tokio::task::spawn_blocking(|| {
        turbosql::select!(Option<Person> "WHERE name = ?", "Joe")
    }).await??;
    Ok(())
}

(请注意,spawn_blocking返回一个JoinHandle,它本身必须被解包,因此在这些示例的末尾需要??。)

在底层,Turbosql使用持久的thread_local数据库连接,因此来自同一线程的数据库调用连续序列将保证使用相同的专用数据库连接。因此,async事务可以按此方式执行

use turbosql::{Turbosql, select, execute};

#[derive(Turbosql, Default)]
struct Person {
    rowid: Option<i64>,
    age: Option<i64>
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tokio::task::spawn_blocking(|| -> Result<(), turbosql::Error> {
        Person { rowid: None, age: Some(21) }.insert()?;
        execute!("BEGIN IMMEDIATE TRANSACTION")?;
        let p = select!(Person "WHERE rowid = ?", 1)?;
        // [ ...do any other blocking things... ]
        execute!(
            "UPDATE person SET age = ? WHERE rowid = ?",
            p.age.unwrap_or_default() + 1,
            1
        )?;
        execute!("COMMIT")?;
        Ok(())
    }).await??;
    Ok(())
}

Turbosql将SQLite的busy_timeout设置为3秒,因此任何表锁竞争都会在该时间段内自动重试,之后无法获取锁的命令将返回错误。

有关Turbosql对async和事务的方法的进一步讨论,请参阅https://github.com/trevyn/turbosql/issues/4。非常欢迎对解决方案的易用性进行改进的想法。

-wal-shm文件

SQLite是一个非常可靠的数据库引擎,但了解它与文件系统的接口总是有帮助。主要的.sqlite文件包含数据库的大部分内容。在数据库写入期间,SQLite还创建了.sqlite-wal.sqlite-shm文件。如果主进程在没有刷新写入的情况下终止,您可能会得到这三个文件,而您期望只有一个文件。这总是没有问题的;在下次启动时,SQLite知道如何解决任何中断的写入并理解世界。然而,如果存在-wal和/或-shm文件,它们被认为是数据库完整性的关键。删除它们可能导致数据库损坏。请参阅https://sqlite.ac.cn/tempfiles.html

查询示例形式

查看integration_test.rs以获取CI中工作并已测试的更多示例。

 原始类型
let result = select!(String "SELECT name FROM person")?;

返回一个转换为指定类型的值,如果没有可用行,则返回Error

let result = select!(String "name FROM person WHERE rowid = ?", rowid)?;

SELECT关键字在使用select!时是始终可选的;如果需要,它会自动添加。
参数绑定很简单。

 Vec<_>
let result = select!(Vec<String> "name FROM person")?;

返回包含另一种类型的Vec。如果没有行,则返回空的Vec

 Option<_>
let result = select!(Option<String> "name FROM person")?;

如果没有行,则返回Ok(None),如果出错,则返回Error(_)

 您的结构体
let result = select!(Person "WHERE name = ?", name)?;

如果类型是#[derive(Turbosql)]结构体,则列列表和表名是可选的。

let result = select!(Vec<NameAndAdult> "name, age >= 18 AS adult FROM person")?;

您还可以使用其他结构体类型;列名必须与结构体匹配,并且您必须在SQL中指定源表。
实现Default以避免指定未使用的列名。
(当然,您也可以将其全部放入一个 VecOption 中。)

let result = select!(Vec<Person>)?;

有时一切都是可选的;此示例将检索所有 Person 行。


"turbosql" 还是 "Turbosql"?

由您选择,但您绝对不要在名称中的其他字母大写! ;)

许可协议:MIT OR Apache-2.0 OR CC0-1.0(公有领域)

依赖项

~27MB
~508K SLoC