# Tauri libsql 插件
一个用于 [libsql](https://github.com/tursodatabase/libsql) 的 Tauri 插件,内置 AES-256-CBC 加密、Drizzle ORM 支持,以及浏览器兼容的迁移运行器。
## 为什么选择这个插件?
### 1. Rust ORM 在应用开发中很痛苦
在 Rust 中使用原始 SQL 很冗长,而 Rust ORM(Diesel、SeaORM)需要在 Rust 中定义模式,与 TypeScript 前端配合不佳,并且增加了显著的构建复杂性。对于真正的业务逻辑在 TypeScript 中的 Tauri 应用,你也希望在 TypeScript 中编写数据库代码。
### 2. 无需 Node.js 运行时的 Drizzle ORM
Drizzle ORM 非常出色 —— 类型安全的查询、简洁的迁移系统、出色的开发体验。但它通常需要 Node.js 或 Bun 运行时来直接打开数据库文件。Tauri 的 WebView 没有这样的运行时。
这个插件通过 Drizzle 的 [sqlite-proxy](https://orm.drizzle.team/docs/get-started-sqlite#http-proxy) 模式解决了这个问题:Drizzle 生成 SQL,代理通过 Tauri 的 `invoke()` 将其发送到 Rust 插件,Rust 插件使用 libsql 执行它。你的 TypeScript 代码使用完整的 Drizzle ORM,零 Node.js 依赖。
### 3. 在 WebView 中工作的迁移
Drizzle 内置的迁移器使用 Node 的 `fs` 模块在运行时从磁盘读取 `.sql` 文件 —— 这在浏览器/WebView 环境中不存在。有两种解决方法:
- **Tauri 资源文件夹** —— 将文件打包为应用资源,通过 Tauri 的 asset 协议读取。可以工作,但需要额外的 Tauri 配置。
- **Vite `import.meta.glob`** *(这个插件的方法)* —— Vite 在构建时将 SQL 文件内容直接打包到 JavaScript 中。无需运行时文件系统访问,无需额外配置。
```typescript
// Vite 在构建时解析这些 —— SQL 文本被内联到 JS 包中
const migrations = import.meta.glob<string>("./drizzle/*.sql", {
eager: true,
query: "?raw",
import: "default",
});
await migrate("sqlite:myapp.db", migrations);
```
这个插件中的 `migrate()` 函数接收预加载的 SQL 字符串,在 `__drizzle_migrations` 表中跟踪已应用的迁移,并按顺序运行待处理的迁移。
### 4. 内置加密
`@tauri-apps/plugin-sql`(使用 sqlx)不支持加密。这个插件使用 libsql 的原生 AES-256-CBC 加密,无需额外的原生库或 FFI 包装器。
---
## 功能特性
- **完整的 SQLite 兼容性** 通过 libsql
- **原生加密** —— AES-256-CBC,可在插件级别或每个数据库配置
- **Drizzle ORM 集成** —— sqlite-proxy 模式与 `createDrizzleProxy`
- **迁移运行器** —— 浏览器安全的 `migrate()`,通过 Vite 在构建时打包 SQL 文件
- **API 兼容** 适用于 `@tauri-apps/plugin-sql`(在适用的地方)
- **跨平台**:macOS、Windows、Linux、iOS、Android
**已测试**
- [x] MacOS
- [ ] Windows
- [ ] Linux
- [ ] iOS
- [ ] Android
---
## 安装
### Rust
```toml
[dependencies]
tauri-plugin-libsql = "0.1.0"
```
### JavaScript / TypeScript
```bash
npm install tauri-plugin-libsql-api
# 或
pnpm add tauri-plugin-libsql-api
```
---
## 快速开始
### 1. 注册插件(Rust)
```rust
// src-tauri/src/lib.rs
// 默认:数据库相对于当前工作目录解析
tauri::Builder::default()
.plugin(tauri_plugin_libsql::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
```
要在固定位置存储数据库:
```rust
use std::path::PathBuf;
let config = tauri_plugin_libsql::Config {
base_path: Some(PathBuf::from("/path/to/data")),
encryption: None,
};
tauri::Builder::default()
.plugin(tauri_plugin_libsql::init_with_config(config))
.run(tauri::generate_context!())
.expect("error while running tauri application");
```
### 2. 使用 Database 类(TypeScript)
```typescript
import { Database } from 'tauri-plugin-libsql-api';
const db = await Database.load('sqlite:myapp.db');
await db.execute(
'INSERT INTO users (name) VALUES ($1)',
['Alice']
);
const users = await db.select<{ id: number; name: string }[]>(
'SELECT * FROM users'
);
await db.close();
```
---
## 数据库位置
相对路径(例如 `sqlite:myapp.db`)相对于 `base_path` 解析:
- **默认**:`std::env::current_dir()` —— 你启动 Tauri 进程的目录
- **自定义**:在插件配置中设置 `base_path`(见上文)
- **绝对路径** 按原样使用
- **内存中**:`sqlite::memory:`
相对路径被规范化(`..` 组件被折叠)并且必须保持在 `base_path` 内。会逸出的路径(例如 `sqlite:../../secret`)将被拒绝并返回错误。
---
## Drizzle ORM 集成
### 设置
```typescript
import { drizzle } from 'drizzle-orm/sqlite-proxy';
import { createDrizzleProxy } from 'tauri-plugin-libsql-api';
import * as schema from './schema';
const db = drizzle(createDrizzleProxy('sqlite:myapp.db'), { schema });
const users = await db.select().from(schema.users);
```
`createDrizzleProxy` 在首次使用时延迟加载数据库连接,因此使用它时无需单独调用 `Database.load()`。
### 使用加密
```typescript
import { createDrizzleProxyWithEncryption } from 'tauri-plugin-libsql-api';
const db = drizzle(
createDrizzleProxyWithEncryption({
path: 'sqlite:encrypted.db',
encryption: {
cipher: 'aes256cbc',
key: myKey32Bytes, // number[] | Uint8Array, 32 字节
},
}),
{ schema }
);
```
---
## 迁移
标准的 `drizzle-orm/sqlite-proxy/migrator` 在运行时从文件系统读取,这在 Tauri WebView 中无法工作。这个插件提供了一个 `migrate()` 函数,它接受由 Vite 的 `import.meta.glob` 预打包的 SQL 内容。
### 工作流程
**1. 定义你的模式** (`src/lib/schema.ts`):
```typescript
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
});
```
**2. 配置 drizzle-kit** (`drizzle.config.ts`):
```typescript
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'sqlite',
schema: './src/lib/schema.ts',
out: './drizzle',
});
```
**3. 生成迁移文件**:
```bash
npx drizzle-kit generate
# 创建 drizzle/0000_init.sql, drizzle/0001_add_column.sql, 等等
```
**4. 在启动时运行迁移**:
```typescript
import { Database, migrate } from 'tauri-plugin-libsql-api';
// Vite 在构建时将这些 SQL 文件打包到应用中
const migrations = import.meta.glob<string>('./drizzle/*.sql', {
eager: true,
query: '?raw',
import: 'default',
});
// 启动顺序:加载 → 迁移 → 查询
await Database.load('sqlite:myapp.db');
await migrate('sqlite:myapp.db', migrations);
// 现在可以安全地查询
const db = drizzle(createDrizzleProxy('sqlite:myapp.db'), { schema });
```
### `migrate()` 如何工作
- 如果不存在,创建 `__drizzle_migrations` 跟踪表
- 通过数字前缀解析迁移文件名(`0000_`、`0001_` 等)
- 仅按顺序应用待处理的迁移
- 通过文件名记录每个已应用的迁移
### 添加模式更改
```bash
# 1. 编辑 src/lib/schema.ts
# 2. 生成新迁移
npx drizzle-kit generate
# 3. 新迁移在下次应用启动时自动运行
```
### 选项
```typescript
await migrate('sqlite:myapp.db', migrations, {
migrationsTable: '__my_migrations', // 默认:'__drizzle_migrations'
});
```
---
## 加密
### 插件级别加密(应用于所有数据库)
在 Rust 中配置一次 —— 前端从不处理密钥:
```rust
let config = tauri_plugin_libsql::Config {
base_path: None,
encryption: Some(tauri_plugin_libsql::EncryptionConfig {
cipher: tauri_plugin_libsql::Cipher::Aes256Cbc,
key: my_32_byte_key, // Vec<u8>, 正好 32 字节
}),
};
```
### 每个数据库加密(从前端)
```typescript
const key = new Uint8Array(32);
crypto.getRandomValues(key);
const db = await Database.load({
path: 'sqlite:secrets.db',
encryption: {
cipher: 'aes256cbc',
key: Array.from(key), // number[] 或 Uint8Array
},
});
```
**安全注意事项:**
- AES-256-CBC 需要正好 32 字节
- 将密钥存储在操作系统钥匙串或安全存储中 —— 丢失密钥 = 丢失数据
- 首选插件级别加密;它将密钥排除在 JavaScript 之外
---
## API 参考
### `Database.load(pathOrOptions)`
```typescript
// 简单用法
const db = await Database.load('sqlite:myapp.db');
// 使用加密
const db = await Database.load({
path: 'sqlite:myapp.db',
encryption: { cipher: 'aes256cbc', key: myKey },
});
```
### `db.execute(query, values?)`
```typescript
const result = await db.execute(
'INSERT INTO todos (title) VALUES ($1)',
['Buy milk']
);
// result.rowsAffected, result.lastInsertId
```
### `db.select<T>(query, values?)`
```typescript
const rows = await db.select<{ id: number; title: string }[]>(
'SELECT * FROM todos WHERE completed = $1',
[0]
);
```
### `db.batch(queries)`
在单个事务中原子执行多个 SQL 语句。用于 DDL 或批量 DML。语句不能使用绑定参数(`$1` 占位符)—— 对参数化查询使用 `execute()`。
```typescript
await db.batch([
'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)',
'CREATE INDEX idx_users_name ON users(name)',
]);
```
### `db.sync()`
从 Turso 远程拉取最新更改到本地副本。对纯本地数据库无操作(无错误返回)。需要 `replication` 功能。
```typescript
await db.sync();
```
### `db.close()`
```typescript
await db.close();
```
### `migrate(dbPath, migrationFiles, options?)`
```typescript
import { migrate } from 'tauri-plugin-libsql-api';
const migrations = import.meta.glob<string>('./drizzle/*.sql', {
eager: true,
query: '?raw',
import: 'default',
});
await migrate('sqlite:myapp.db', migrations);
```
### `createDrizzleProxy(dbPath)`
返回一个用于 `drizzle()` 的 sqlite-proxy 回调。延迟加载连接。
### `createDrizzleProxyWithEncryption(options)`
同上,但带加密配置。
### `getConfig()`
```typescript
import { getConfig } from 'tauri-plugin-libsql-api';
const { encrypted } = await getConfig();
```
---
## 权限
添加到你的 `tauri.conf.json`:
```json
{
"plugins": {
"libsql": {}
}
}
```
或配置细粒度能力:
```json
{
"identifier": "libsql:default",
"permissions": [
"libsql:allow-load",
"libsql:allow-batch",
"libsql:allow-execute",
"libsql:allow-select",
"libsql:allow-close"
]
}
```
---
## 与 @tauri-apps/plugin-sql 的比较
| SQLite | ✅ libsql | ✅ sqlx |
| 加密 | ✅ 内置 AES-256-CBC | ❌ |
| Drizzle ORM | ✅ | ✅ |
| 迁移运行器 | ✅ 浏览器安全 | ❌ |
| MySQL / PostgreSQL | ❌ | ✅ |
| API 兼容性 | 部分 | 完整 |
---
## Turso / 远程数据库
该插件支持两种由 libsql 提供支持的远程连接模式。
### 嵌入式副本(推荐用于 Tauri)
本地 SQLite 文件与 Turso 云数据库保持同步。查询从本地文件读取(快速、离线可用),写入同步到远程。
**1. 在你的应用 `Cargo.toml` 中启用 `replication` 功能**:
```toml
tauri-plugin-libsql = { version = "0.1.0", features = ["replication"] }
```
**2. 使用 `syncUrl` 和 `authToken` 加载:**
```typescript
import { Database, migrate } from 'tauri-plugin-libsql-api';
const db = await Database.load({
path: 'sqlite:local.db', // 本地副本文件
syncUrl: 'libsql://mydb-org.turso.io',
authToken: 'your-turso-auth-token',
});
// 按需同步(例如在应用恢复 / 网络重连时)
await db.sync();
```
在 `Database.load()` 时,初始同步将最新数据从 Turso 拉取到本地文件。后续的 `sync()` 调用拉取增量更改。
**使用 Drizzle ORM:**
```typescript
const migrations = import.meta.glob<string>('./drizzle/*.sql', {
eager: true, query: '?raw', import: 'default',
});
const db = await Database.load({
path: 'sqlite:local.db',
syncUrl: 'libsql://mydb-org.turso.io',
authToken: import.meta.env.VITE_TURSO_AUTH_TOKEN,
});
await migrate(db.path, migrations);
const drizzleDb = drizzle(createDrizzleProxy(db.path), { schema });
```
---
### 纯远程
所有查询直接在 Turso 上执行 —— 没有本地文件。每个查询都需要网络。
**启用 `remote` 功能:**
```toml
tauri-plugin-libsql = { version = "0.1.0", features = ["remote"] }
```
```typescript
const db = await Database.load({
path: 'libsql://mydb-org.turso.io',
authToken: 'your-turso-auth-token',
});
```
对于大多数 Tauri 应用,**嵌入式副本是更好的选择** —— 它离线工作,读取速度明显更快。
> **关于 `batch()` 与嵌入式副本的注意事项**:在某些版本中,libsql 的 `execute_batch()` 不能正确地通过嵌入式副本层路由写入。该插件使用显式 `BEGIN`/`COMMIT` 事务内的单个 `execute()` 调用来避免这个问题。
> **关于 URL 验证的注意事项**:libsql 的构建器在内部对同步 URL 调用 `unwrap()`,格式错误的值(例如前导/尾随空格、错误的协议)可能导致 panic。该插件将其包装在 `catch_unwind` 中,因此错误的 URL 会作为适当的错误显示,而不是无限期挂起 IPC。
---
## 包大小
基于包含的 Todo List 演示应用(macOS、aarch64、release 构建):
| `.app` 包 | 15 MB | 15 MB |
| `.dmg` 安装程序 | 6.0 MB | 5.9 MB |
禁用加密基本上节省不了什么 —— 与始终存在的 SQLite 原生库相比,AES 密码代码可以忽略不计。`encryption` 功能标志仍然存在,以避免编译加密相关代码,如果你想在编译时强制没有数据库可以被加密。
### 禁用加密
加密是默认功能。要选择退出,请禁用默认功能并仅选择你需要的:
**`Cargo.toml`**(在你的 Tauri 应用中):
```toml
tauri-plugin-libsql = { version = "0.1.0", default-features = false, features = ["core"] }
```
**可用功能:**
| `core` | ✅ | 本地 SQLite 数据库(始终需要)|
| `encryption` | ✅ | 通过 libsql 的 AES-256-CBC 加密 |
| `replication` | ❌ | libsql 复制支持(添加 TLS)|
| `remote` | ❌ | 远程数据库支持(计划中,见下文)|
当 `encryption` 被禁用时,向 `Database.load()` 传递 `EncryptionConfig` 将在运行时返回错误。TypeScript API 表面保持不变 —— 无需重新构建你的 JS 代码。
---
## 使用 AI 集成此插件
仓库根目录包含一个 `SKILL.md` 文件。它包含有关插件架构、启动顺序、迁移工作流、加密模式和常见错误的结构化上下文 —— 专为 AI 编码助手(Claude Code、Cursor、Copilot 等)编写。
### 使用 Claude Code
将 `SKILL.md` 复制到你项目的 `.claude/skills/tauri-plugin-libsql/` 目录:
```bash
mkdir -p .claude/skills/tauri-plugin-libsql
cp /path/to/tauri-plugin-libsql/SKILL.md .claude/skills/tauri-plugin-libsql/
```
Claude Code 自动发现技能。复制后,你可以自然地提示:
> "使用 tauri-plugin-libsql 为我的 Tauri 应用添加一个 `notes` 表。包括模式、迁移和启动顺序。"
Claude 将应用正确的启动顺序,对迁移使用 `import.meta.glob`,并处理 drizzle 代理模式,无需额外指导。
### 使用其他 AI 工具
直接将 `SKILL.md` 的内容粘贴到你的系统提示或上下文窗口中,然后描述你想构建什么。该技能涵盖足够的上下文,让 AI 能在第一次尝试就生成正确、可工作的代码。
---
## 项目结构
```
tauri-plugin-libsql/
├── src/ # Rust 插件
│ ├── lib.rs # 插件初始化、命令注册
│ ├── commands.rs # load、execute、select、close、ping
│ ├── wrapper.rs # DbConnection 包装 libsql
│ ├── decode.rs # libsql::Value → serde_json::Value
│ ├── models.rs # Cipher、EncryptionConfig、QueryResult
│ ├── error.rs # 错误类型
│ ├── desktop.rs # 桌面配置 & base_path
│ └── mobile.rs # 移动端存根
├── guest-js/ # TypeScript 源代码
│ ├── index.ts # Database 类、getConfig、重新导出
│ ├── drizzle.ts # createDrizzleProxy、createDrizzleProxyWithEncryption
│ └── migrate.ts # migrate() —— 浏览器安全的迁移运行器
├── permissions/ # Tauri 权限文件
├── examples/todo-list/ # 演示:带 Drizzle + 迁移的 Todo 应用(15 MB .app / 6 MB .dmg)
├── SKILL.md # 适用于 Claude Code 和其他助手的 AI 技能上下文
├── build.rs
├── Cargo.toml
└── package.json
```